summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/shared
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/shared')
-rw-r--r--devtools/client/shared/AppCacheUtils.jsm631
-rw-r--r--devtools/client/shared/DOMHelpers.jsm166
-rw-r--r--devtools/client/shared/Jsbeautify.jsm16
-rw-r--r--devtools/client/shared/SplitView.jsm312
-rw-r--r--devtools/client/shared/autocomplete-popup.js599
-rw-r--r--devtools/client/shared/browser-loader.js235
-rw-r--r--devtools/client/shared/components/.eslintrc.js7
-rw-r--r--devtools/client/shared/components/frame.js239
-rw-r--r--devtools/client/shared/components/h-split-box.js154
-rw-r--r--devtools/client/shared/components/moz.build27
-rw-r--r--devtools/client/shared/components/notification-box.css95
-rw-r--r--devtools/client/shared/components/notification-box.js263
-rw-r--r--devtools/client/shared/components/reps/array.js186
-rw-r--r--devtools/client/shared/components/reps/attribute.js70
-rw-r--r--devtools/client/shared/components/reps/caption.js31
-rw-r--r--devtools/client/shared/components/reps/comment-node.js60
-rw-r--r--devtools/client/shared/components/reps/date-time.js70
-rw-r--r--devtools/client/shared/components/reps/document.js78
-rw-r--r--devtools/client/shared/components/reps/element-node.js114
-rw-r--r--devtools/client/shared/components/reps/event.js81
-rw-r--r--devtools/client/shared/components/reps/function.js73
-rw-r--r--devtools/client/shared/components/reps/grip-array.js198
-rw-r--r--devtools/client/shared/components/reps/grip-map.js193
-rw-r--r--devtools/client/shared/components/reps/grip.js247
-rw-r--r--devtools/client/shared/components/reps/infinity.js41
-rw-r--r--devtools/client/shared/components/reps/long-string.js71
-rw-r--r--devtools/client/shared/components/reps/moz.build40
-rw-r--r--devtools/client/shared/components/reps/nan.js41
-rw-r--r--devtools/client/shared/components/reps/null.js46
-rw-r--r--devtools/client/shared/components/reps/number.js51
-rw-r--r--devtools/client/shared/components/reps/object-with-text.js76
-rw-r--r--devtools/client/shared/components/reps/object-with-url.js76
-rw-r--r--devtools/client/shared/components/reps/object.js171
-rw-r--r--devtools/client/shared/components/reps/promise.js111
-rw-r--r--devtools/client/shared/components/reps/prop-rep.js70
-rw-r--r--devtools/client/shared/components/reps/regexp.js63
-rw-r--r--devtools/client/shared/components/reps/rep-utils.js160
-rw-r--r--devtools/client/shared/components/reps/rep.js144
-rw-r--r--devtools/client/shared/components/reps/reps.css174
-rw-r--r--devtools/client/shared/components/reps/string.js69
-rw-r--r--devtools/client/shared/components/reps/stylesheet.js77
-rw-r--r--devtools/client/shared/components/reps/symbol.js48
-rw-r--r--devtools/client/shared/components/reps/text-node.js94
-rw-r--r--devtools/client/shared/components/reps/undefined.js46
-rw-r--r--devtools/client/shared/components/reps/window.js73
-rw-r--r--devtools/client/shared/components/search-box.js110
-rw-r--r--devtools/client/shared/components/sidebar-toggle.css32
-rw-r--r--devtools/client/shared/components/sidebar-toggle.js66
-rw-r--r--devtools/client/shared/components/splitter/draggable.js54
-rw-r--r--devtools/client/shared/components/splitter/moz.build11
-rw-r--r--devtools/client/shared/components/splitter/split-box.css88
-rw-r--r--devtools/client/shared/components/splitter/split-box.js205
-rw-r--r--devtools/client/shared/components/stack-trace.js68
-rw-r--r--devtools/client/shared/components/tabs/moz.build12
-rw-r--r--devtools/client/shared/components/tabs/tabbar.css53
-rw-r--r--devtools/client/shared/components/tabs/tabbar.js204
-rw-r--r--devtools/client/shared/components/tabs/tabs.css183
-rw-r--r--devtools/client/shared/components/tabs/tabs.js369
-rw-r--r--devtools/client/shared/components/test/browser/.eslintrc.js6
-rw-r--r--devtools/client/shared/components/test/browser/browser.ini7
-rw-r--r--devtools/client/shared/components/test/browser/browser_notification_box_basic.js36
-rw-r--r--devtools/client/shared/components/test/mochitest/.eslintrc.js6
-rw-r--r--devtools/client/shared/components/test/mochitest/chrome.ini51
-rw-r--r--devtools/client/shared/components/test/mochitest/head.js217
-rw-r--r--devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html126
-rw-r--r--devtools/client/shared/components/test/mochitest/test_frame_01.html309
-rw-r--r--devtools/client/shared/components/test/mochitest/test_notification_box_01.html108
-rw-r--r--devtools/client/shared/components/test/mochitest/test_notification_box_02.html70
-rw-r--r--devtools/client/shared/components/test/mochitest/test_notification_box_03.html84
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_array.html259
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_attribute.html56
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_comment-node.html80
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_date-time.html79
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_document.html56
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_element-node.html341
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_event.html300
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_function.html206
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_grip-array.html707
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_grip-map.html405
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_grip.html887
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_infinity.html73
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_long-string.html125
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_nan.html48
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_null.html44
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_number.html97
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html54
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_object-with-url.html60
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_object.html225
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_promise.html333
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_regexp.html51
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_string.html79
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_stylesheet.html54
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_symbol.html77
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_text-node.html115
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_undefined.html47
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_window.html58
-rw-r--r--devtools/client/shared/components/test/mochitest/test_sidebar_toggle.html56
-rw-r--r--devtools/client/shared/components/test/mochitest/test_stack-trace.html102
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html79
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tabs_menu.html81
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_01.html64
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_02.html45
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_03.html46
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_04.html128
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_05.html83
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_06.html320
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_07.html64
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_08.html51
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_09.html77
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_10.html52
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_11.html92
-rw-r--r--devtools/client/shared/components/tree.js773
-rw-r--r--devtools/client/shared/components/tree/label-cell.js66
-rw-r--r--devtools/client/shared/components/tree/moz.build14
-rw-r--r--devtools/client/shared/components/tree/object-provider.js90
-rw-r--r--devtools/client/shared/components/tree/tree-cell.js101
-rw-r--r--devtools/client/shared/components/tree/tree-header.js100
-rw-r--r--devtools/client/shared/components/tree/tree-row.js184
-rw-r--r--devtools/client/shared/components/tree/tree-view.css157
-rw-r--r--devtools/client/shared/components/tree/tree-view.js352
-rw-r--r--devtools/client/shared/css-angle.js345
-rw-r--r--devtools/client/shared/css-reload.js142
-rw-r--r--devtools/client/shared/curl.js401
-rw-r--r--devtools/client/shared/demangle.js64
-rw-r--r--devtools/client/shared/developer-toolbar.js1397
-rw-r--r--devtools/client/shared/devices.js88
-rw-r--r--devtools/client/shared/devtools-file-watcher.js78
-rw-r--r--devtools/client/shared/doorhanger.js164
-rw-r--r--devtools/client/shared/file-watcher-worker.js81
-rw-r--r--devtools/client/shared/file-watcher.js28
-rw-r--r--devtools/client/shared/frame-script-utils.js206
-rw-r--r--devtools/client/shared/getjson.js76
-rw-r--r--devtools/client/shared/inplace-editor.js1566
-rw-r--r--devtools/client/shared/key-shortcuts.js251
-rw-r--r--devtools/client/shared/keycodes.js146
-rw-r--r--devtools/client/shared/moz.build54
-rw-r--r--devtools/client/shared/network-throttling-profiles.js68
-rw-r--r--devtools/client/shared/node-attribute-parser.js294
-rw-r--r--devtools/client/shared/options-view.js186
-rw-r--r--devtools/client/shared/output-parser.js695
-rw-r--r--devtools/client/shared/poller.js114
-rw-r--r--devtools/client/shared/prefs.js178
-rw-r--r--devtools/client/shared/redux/create-store.js51
-rw-r--r--devtools/client/shared/redux/middleware/history.js23
-rw-r--r--devtools/client/shared/redux/middleware/log.js17
-rw-r--r--devtools/client/shared/redux/middleware/moz.build16
-rw-r--r--devtools/client/shared/redux/middleware/promise.js54
-rw-r--r--devtools/client/shared/redux/middleware/task.js42
-rw-r--r--devtools/client/shared/redux/middleware/test/.eslintrc.js17
-rw-r--r--devtools/client/shared/redux/middleware/test/head.js27
-rw-r--r--devtools/client/shared/redux/middleware/test/test_middleware-task-01.js56
-rw-r--r--devtools/client/shared/redux/middleware/test/test_middleware-task-02.js67
-rw-r--r--devtools/client/shared/redux/middleware/test/test_middleware-task-03.js42
-rw-r--r--devtools/client/shared/redux/middleware/test/xpcshell.ini10
-rw-r--r--devtools/client/shared/redux/middleware/thunk.js19
-rw-r--r--devtools/client/shared/redux/middleware/wait-service.js64
-rw-r--r--devtools/client/shared/redux/moz.build14
-rw-r--r--devtools/client/shared/redux/non-react-subscriber.js153
-rw-r--r--devtools/client/shared/scroll.js52
-rw-r--r--devtools/client/shared/shim/Services.js620
-rw-r--r--devtools/client/shared/shim/moz.build13
-rw-r--r--devtools/client/shared/shim/test/.eslintrc.js6
-rw-r--r--devtools/client/shared/shim/test/file_service_wm.html20
-rw-r--r--devtools/client/shared/shim/test/mochitest.ini10
-rw-r--r--devtools/client/shared/shim/test/prefs-wrapper.js80
-rw-r--r--devtools/client/shared/shim/test/test_service_appinfo.html29
-rw-r--r--devtools/client/shared/shim/test/test_service_focus.html78
-rw-r--r--devtools/client/shared/shim/test/test_service_prefs.html244
-rw-r--r--devtools/client/shared/shim/test/test_service_prefs_defaults.html71
-rw-r--r--devtools/client/shared/shim/test/test_service_wm.html36
-rw-r--r--devtools/client/shared/source-utils.js328
-rw-r--r--devtools/client/shared/splitview.css83
-rw-r--r--devtools/client/shared/suggestion-picker.js176
-rw-r--r--devtools/client/shared/telemetry.js341
-rw-r--r--devtools/client/shared/test/.eslintrc.js9
-rw-r--r--devtools/client/shared/test/browser.ini188
-rw-r--r--devtools/client/shared/test/browser_css_angle.js176
-rw-r--r--devtools/client/shared/test/browser_css_color.js137
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-01.js38
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-02.js200
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-03.js68
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-04.js50
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-05.js48
-rw-r--r--devtools/client/shared/test/browser_cubic-bezier-06.js79
-rw-r--r--devtools/client/shared/test/browser_devices.js57
-rw-r--r--devtools/client/shared/test/browser_devices.json23
-rw-r--r--devtools/client/shared/test/browser_filter-editor-01.js114
-rw-r--r--devtools/client/shared/test/browser_filter-editor-02.js107
-rw-r--r--devtools/client/shared/test/browser_filter-editor-03.js65
-rw-r--r--devtools/client/shared/test/browser_filter-editor-04.js87
-rw-r--r--devtools/client/shared/test/browser_filter-editor-05.js148
-rw-r--r--devtools/client/shared/test/browser_filter-editor-06.js71
-rw-r--r--devtools/client/shared/test/browser_filter-editor-07.js27
-rw-r--r--devtools/client/shared/test/browser_filter-editor-08.js84
-rw-r--r--devtools/client/shared/test/browser_filter-editor-09.js125
-rw-r--r--devtools/client/shared/test/browser_filter-editor-10.js87
-rw-r--r--devtools/client/shared/test/browser_filter-presets-01.js99
-rw-r--r--devtools/client/shared/test/browser_filter-presets-02.js45
-rw-r--r--devtools/client/shared/test/browser_filter-presets-03.js40
-rw-r--r--devtools/client/shared/test/browser_flame-graph-01.js61
-rw-r--r--devtools/client/shared/test/browser_flame-graph-02.js44
-rw-r--r--devtools/client/shared/test/browser_flame-graph-03a.js138
-rw-r--r--devtools/client/shared/test/browser_flame-graph-03b.js92
-rw-r--r--devtools/client/shared/test/browser_flame-graph-03c.js155
-rw-r--r--devtools/client/shared/test/browser_flame-graph-04.js90
-rw-r--r--devtools/client/shared/test/browser_flame-graph-05.js113
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-01.js256
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-02.js130
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-03.js136
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-04.js188
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-05.js48
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-06.js117
-rw-r--r--devtools/client/shared/test/browser_flame-graph-utils-hash.js24
-rw-r--r--devtools/client/shared/test/browser_graphs-01.js70
-rw-r--r--devtools/client/shared/test/browser_graphs-02.js107
-rw-r--r--devtools/client/shared/test/browser_graphs-03.js111
-rw-r--r--devtools/client/shared/test/browser_graphs-04.js69
-rw-r--r--devtools/client/shared/test/browser_graphs-05.js154
-rw-r--r--devtools/client/shared/test/browser_graphs-06.js112
-rw-r--r--devtools/client/shared/test/browser_graphs-07a.js232
-rw-r--r--devtools/client/shared/test/browser_graphs-07b.js88
-rw-r--r--devtools/client/shared/test/browser_graphs-07c.js139
-rw-r--r--devtools/client/shared/test/browser_graphs-07d.js71
-rw-r--r--devtools/client/shared/test/browser_graphs-07e.js127
-rw-r--r--devtools/client/shared/test/browser_graphs-08.js88
-rw-r--r--devtools/client/shared/test/browser_graphs-09a.js104
-rw-r--r--devtools/client/shared/test/browser_graphs-09b.js82
-rw-r--r--devtools/client/shared/test/browser_graphs-09c.js38
-rw-r--r--devtools/client/shared/test/browser_graphs-09d.js39
-rw-r--r--devtools/client/shared/test/browser_graphs-09e.js84
-rw-r--r--devtools/client/shared/test/browser_graphs-09f.js53
-rw-r--r--devtools/client/shared/test/browser_graphs-10a.js162
-rw-r--r--devtools/client/shared/test/browser_graphs-10b.js71
-rw-r--r--devtools/client/shared/test/browser_graphs-10c.js109
-rw-r--r--devtools/client/shared/test/browser_graphs-11a.js60
-rw-r--r--devtools/client/shared/test/browser_graphs-11b.js133
-rw-r--r--devtools/client/shared/test/browser_graphs-12.js157
-rw-r--r--devtools/client/shared/test/browser_graphs-13.js44
-rw-r--r--devtools/client/shared/test/browser_graphs-14.js111
-rw-r--r--devtools/client/shared/test/browser_graphs-15.js49
-rw-r--r--devtools/client/shared/test/browser_graphs-16.js45
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-01.js88
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-02.js174
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-03.js155
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-04.js110
-rw-r--r--devtools/client/shared/test/browser_html_tooltip-05.js109
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_arrow-01.js108
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_arrow-02.js100
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_consecutive-show.js71
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_hover.js65
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_offset.js99
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_rtl.js140
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_variable-height.js75
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_width-auto.js61
-rw-r--r--devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js80
-rw-r--r--devtools/client/shared/test/browser_inplace-editor-01.js150
-rw-r--r--devtools/client/shared/test/browser_inplace-editor-02.js71
-rw-r--r--devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js75
-rw-r--r--devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js80
-rw-r--r--devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js119
-rw-r--r--devtools/client/shared/test/browser_inplace-editor_maxwidth.js114
-rw-r--r--devtools/client/shared/test/browser_key_shortcuts.js425
-rw-r--r--devtools/client/shared/test/browser_keycodes.js12
-rw-r--r--devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html65
-rw-r--r--devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js219
-rw-r--r--devtools/client/shared/test/browser_layoutHelpers.html24
-rw-r--r--devtools/client/shared/test/browser_layoutHelpers.js93
-rw-r--r--devtools/client/shared/test/browser_mdn-docs-01.js168
-rw-r--r--devtools/client/shared/test/browser_mdn-docs-02.js128
-rw-r--r--devtools/client/shared/test/browser_mdn-docs-03.js277
-rw-r--r--devtools/client/shared/test/browser_num-l10n.js27
-rw-r--r--devtools/client/shared/test/browser_options-view-01.js110
-rw-r--r--devtools/client/shared/test/browser_outputparser.js292
-rw-r--r--devtools/client/shared/test/browser_poller.js136
-rw-r--r--devtools/client/shared/test/browser_prefs-01.js44
-rw-r--r--devtools/client/shared/test/browser_prefs-02.js45
-rw-r--r--devtools/client/shared/test/browser_require_raw.js20
-rw-r--r--devtools/client/shared/test/browser_spectrum.js114
-rw-r--r--devtools/client/shared/test/browser_tableWidget_basic.js390
-rw-r--r--devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js194
-rw-r--r--devtools/client/shared/test/browser_tableWidget_mouse_interaction.js317
-rw-r--r--devtools/client/shared/test/browser_telemetry_button_eyedropper.js52
-rw-r--r--devtools/client/shared/test/browser_telemetry_button_paintflashing.js89
-rw-r--r--devtools/client/shared/test/browser_telemetry_button_responsive.js95
-rw-r--r--devtools/client/shared/test/browser_telemetry_button_scratchpad.js127
-rw-r--r--devtools/client/shared/test/browser_telemetry_sidebar.js84
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolbox.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js29
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js23
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js22
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js37
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js28
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js23
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js29
-rw-r--r--devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js22
-rw-r--r--devtools/client/shared/test/browser_templater_basic.html13
-rw-r--r--devtools/client/shared/test/browser_templater_basic.js286
-rw-r--r--devtools/client/shared/test/browser_theme.js98
-rw-r--r--devtools/client/shared/test/browser_theme_switching.js53
-rw-r--r--devtools/client/shared/test/browser_toolbar_basic.html40
-rw-r--r--devtools/client/shared/test/browser_toolbar_basic.js60
-rw-r--r--devtools/client/shared/test/browser_toolbar_tooltip.js111
-rw-r--r--devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html33
-rw-r--r--devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js256
-rw-r--r--devtools/client/shared/test/browser_treeWidget_basic.js267
-rw-r--r--devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js228
-rw-r--r--devtools/client/shared/test/browser_treeWidget_mouse_interaction.js135
-rw-r--r--devtools/client/shared/test/doc_options-view.xul26
-rw-r--r--devtools/client/shared/test/head.js346
-rw-r--r--devtools/client/shared/test/helper_color_data.js175
-rw-r--r--devtools/client/shared/test/helper_html_tooltip.js96
-rw-r--r--devtools/client/shared/test/helper_inplace_editor.js115
-rw-r--r--devtools/client/shared/test/html-mdn-css-basic-testing.html21
-rw-r--r--devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html12
-rw-r--r--devtools/client/shared/test/html-mdn-css-no-summary.html21
-rw-r--r--devtools/client/shared/test/html-mdn-css-no-syntax.html17
-rw-r--r--devtools/client/shared/test/html-mdn-css-syntax-old-style.html23
-rw-r--r--devtools/client/shared/test/leakhunt.js165
-rw-r--r--devtools/client/shared/test/test-actor-registry.js97
-rw-r--r--devtools/client/shared/test/test-actor.js1138
-rw-r--r--devtools/client/shared/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js36
-rw-r--r--devtools/client/shared/test/unit/test_VariablesView_getString_promise.js76
-rw-r--r--devtools/client/shared/test/unit/test_advanceValidate.js31
-rw-r--r--devtools/client/shared/test/unit/test_attribute-parsing-01.js73
-rw-r--r--devtools/client/shared/test/unit/test_attribute-parsing-02.js134
-rw-r--r--devtools/client/shared/test/unit/test_bezierCanvas.js117
-rw-r--r--devtools/client/shared/test/unit/test_cssAngle.js33
-rw-r--r--devtools/client/shared/test/unit/test_cssColor-01.js76
-rw-r--r--devtools/client/shared/test/unit/test_cssColor-02.js45
-rw-r--r--devtools/client/shared/test/unit/test_cssColor-03.js61
-rw-r--r--devtools/client/shared/test/unit/test_cssColorDatabase.js63
-rw-r--r--devtools/client/shared/test/unit/test_cubicBezier.js146
-rw-r--r--devtools/client/shared/test/unit/test_escapeCSSComment.js40
-rw-r--r--devtools/client/shared/test/unit/test_parseDeclarations.js439
-rw-r--r--devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js213
-rw-r--r--devtools/client/shared/test/unit/test_parseSingleValue.js93
-rw-r--r--devtools/client/shared/test/unit/test_rewriteDeclarations.js529
-rw-r--r--devtools/client/shared/test/unit/test_source-utils.js181
-rw-r--r--devtools/client/shared/test/unit/test_suggestion-picker.js149
-rw-r--r--devtools/client/shared/test/unit/test_undoStack.js98
-rw-r--r--devtools/client/shared/test/unit/xpcshell.ini30
-rw-r--r--devtools/client/shared/theme-switching.js185
-rw-r--r--devtools/client/shared/theme.js84
-rw-r--r--devtools/client/shared/undo.js192
-rw-r--r--devtools/client/shared/vendor/D3_LICENSE26
-rw-r--r--devtools/client/shared/vendor/DAGRE_D3_LICENSE19
-rw-r--r--devtools/client/shared/vendor/REACT_REDUX_LICENSE21
-rw-r--r--devtools/client/shared/vendor/REACT_REDUX_UPGRADING9
-rw-r--r--devtools/client/shared/vendor/REACT_UPGRADING54
-rw-r--r--devtools/client/shared/vendor/REACT_VIRTUALIZED_UPGRADING14
-rw-r--r--devtools/client/shared/vendor/REDUX_LICENSE21
-rw-r--r--devtools/client/shared/vendor/REDUX_UPGRADING10
-rw-r--r--devtools/client/shared/vendor/RESELECT_LICENSE21
-rw-r--r--devtools/client/shared/vendor/RESELECT_UPGRADING7
-rw-r--r--devtools/client/shared/vendor/d3.js9275
-rw-r--r--devtools/client/shared/vendor/dagre-d3.js4560
-rw-r--r--devtools/client/shared/vendor/immutable.js4997
-rwxr-xr-xdevtools/client/shared/vendor/jsol.js97
-rw-r--r--devtools/client/shared/vendor/moz.build29
-rw-r--r--devtools/client/shared/vendor/react-addons-shallow-compare.js9
-rw-r--r--devtools/client/shared/vendor/react-dev.js20763
-rw-r--r--devtools/client/shared/vendor/react-dom.js42
-rw-r--r--devtools/client/shared/vendor/react-proxy.js1909
-rw-r--r--devtools/client/shared/vendor/react-redux.js724
-rw-r--r--devtools/client/shared/vendor/react-virtualized.js4296
-rw-r--r--devtools/client/shared/vendor/react.js20763
-rw-r--r--devtools/client/shared/vendor/redux.js775
-rw-r--r--devtools/client/shared/vendor/reselect.js136
-rw-r--r--devtools/client/shared/vendor/seamless-immutable.js392
-rw-r--r--devtools/client/shared/view-source.js185
-rw-r--r--devtools/client/shared/webgl-utils.js55
-rw-r--r--devtools/client/shared/widgets/AbstractTreeItem.jsm661
-rw-r--r--devtools/client/shared/widgets/BarGraphWidget.js498
-rw-r--r--devtools/client/shared/widgets/BreadcrumbsWidget.jsm250
-rw-r--r--devtools/client/shared/widgets/Chart.jsm449
-rw-r--r--devtools/client/shared/widgets/CubicBezierPresets.js64
-rw-r--r--devtools/client/shared/widgets/CubicBezierWidget.js897
-rw-r--r--devtools/client/shared/widgets/FastListWidget.js249
-rw-r--r--devtools/client/shared/widgets/FilterWidget.js1073
-rw-r--r--devtools/client/shared/widgets/FlameGraph.js1462
-rw-r--r--devtools/client/shared/widgets/Graphs.js1424
-rw-r--r--devtools/client/shared/widgets/GraphsWorker.js103
-rw-r--r--devtools/client/shared/widgets/LineGraphWidget.js402
-rw-r--r--devtools/client/shared/widgets/MdnDocsWidget.js510
-rw-r--r--devtools/client/shared/widgets/MountainGraphWidget.js195
-rw-r--r--devtools/client/shared/widgets/SideMenuWidget.jsm725
-rw-r--r--devtools/client/shared/widgets/SimpleListWidget.jsm255
-rw-r--r--devtools/client/shared/widgets/Spectrum.js336
-rw-r--r--devtools/client/shared/widgets/TableWidget.js1817
-rw-r--r--devtools/client/shared/widgets/TreeWidget.js605
-rw-r--r--devtools/client/shared/widgets/VariablesView.jsm4182
-rw-r--r--devtools/client/shared/widgets/VariablesView.xul18
-rw-r--r--devtools/client/shared/widgets/VariablesViewController.jsm858
-rw-r--r--devtools/client/shared/widgets/cubic-bezier.css216
-rw-r--r--devtools/client/shared/widgets/filter-widget.css238
-rw-r--r--devtools/client/shared/widgets/graphs-frame.xhtml26
-rw-r--r--devtools/client/shared/widgets/mdn-docs.css39
-rw-r--r--devtools/client/shared/widgets/moz.build34
-rw-r--r--devtools/client/shared/widgets/spectrum.css155
-rw-r--r--devtools/client/shared/widgets/tooltip/CssDocsTooltip.js93
-rw-r--r--devtools/client/shared/widgets/tooltip/EventTooltipHelper.js313
-rw-r--r--devtools/client/shared/widgets/tooltip/HTMLTooltip.js638
-rw-r--r--devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js131
-rw-r--r--devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js209
-rw-r--r--devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js182
-rw-r--r--devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip.js102
-rw-r--r--devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js116
-rw-r--r--devtools/client/shared/widgets/tooltip/Tooltip.js410
-rw-r--r--devtools/client/shared/widgets/tooltip/TooltipToggle.js182
-rw-r--r--devtools/client/shared/widgets/tooltip/VariableContentHelper.js89
-rw-r--r--devtools/client/shared/widgets/tooltip/moz.build19
-rw-r--r--devtools/client/shared/widgets/view-helpers.js1625
-rw-r--r--devtools/client/shared/widgets/widgets.css109
-rw-r--r--devtools/client/shared/zoom-keys.js85
418 files changed, 136516 insertions, 0 deletions
diff --git a/devtools/client/shared/AppCacheUtils.jsm b/devtools/client/shared/AppCacheUtils.jsm
new file mode 100644
index 000000000..a2beca993
--- /dev/null
+++ b/devtools/client/shared/AppCacheUtils.jsm
@@ -0,0 +1,631 @@
+/* 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/. */
+
+/**
+ * validateManifest() warns of the following errors:
+ * - No manifest specified in page
+ * - Manifest is not utf-8
+ * - Manifest mimetype not text/cache-manifest
+ * - Manifest does not begin with "CACHE MANIFEST"
+ * - Page modified since appcache last changed
+ * - Duplicate entries
+ * - Conflicting entries e.g. in both CACHE and NETWORK sections or in cache
+ * but blocked by FALLBACK namespace
+ * - Detect referenced files that are not available
+ * - Detect referenced files that have cache-control set to no-store
+ * - Wildcards used in a section other than NETWORK
+ * - Spaces in URI not replaced with %20
+ * - Completely invalid URIs
+ * - Too many dot dot slash operators
+ * - SETTINGS section is valid
+ * - Invalid section name
+ * - etc.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+var { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+var { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+var { LoadContextInfo } = Cu.import("resource://gre/modules/LoadContextInfo.jsm", {});
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+var { gDevTools } = require("devtools/client/framework/devtools");
+var Services = require("Services");
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+
+this.EXPORTED_SYMBOLS = ["AppCacheUtils"];
+
+function AppCacheUtils(documentOrUri) {
+ this._parseManifest = this._parseManifest.bind(this);
+
+ if (documentOrUri) {
+ if (typeof documentOrUri == "string") {
+ this.uri = documentOrUri;
+ }
+ if (/HTMLDocument/.test(documentOrUri.toString())) {
+ this.doc = documentOrUri;
+ }
+ }
+}
+
+AppCacheUtils.prototype = {
+ get cachePath() {
+ return "";
+ },
+
+ validateManifest: function ACU_validateManifest() {
+ let deferred = defer();
+ this.errors = [];
+ // Check for missing manifest.
+ this._getManifestURI().then(manifestURI => {
+ this.manifestURI = manifestURI;
+
+ if (!this.manifestURI) {
+ this._addError(0, "noManifest");
+ deferred.resolve(this.errors);
+ }
+
+ this._getURIInfo(this.manifestURI).then(uriInfo => {
+ this._parseManifest(uriInfo).then(() => {
+ // Sort errors by line number.
+ this.errors.sort(function (a, b) {
+ return a.line - b.line;
+ });
+ deferred.resolve(this.errors);
+ });
+ });
+ });
+
+ return deferred.promise;
+ },
+
+ _parseManifest: function ACU__parseManifest(uriInfo) {
+ let deferred = defer();
+ let manifestName = uriInfo.name;
+ let manifestLastModified = new Date(uriInfo.responseHeaders["Last-Modified"]);
+
+ if (uriInfo.charset.toLowerCase() != "utf-8") {
+ this._addError(0, "notUTF8", uriInfo.charset);
+ }
+
+ if (uriInfo.mimeType != "text/cache-manifest") {
+ this._addError(0, "badMimeType", uriInfo.mimeType);
+ }
+
+ let parser = new ManifestParser(uriInfo.text, this.manifestURI);
+ let parsed = parser.parse();
+
+ if (parsed.errors.length > 0) {
+ this.errors.push.apply(this.errors, parsed.errors);
+ }
+
+ // Check for duplicate entries.
+ let dupes = {};
+ for (let parsedUri of parsed.uris) {
+ dupes[parsedUri.uri] = dupes[parsedUri.uri] || [];
+ dupes[parsedUri.uri].push({
+ line: parsedUri.line,
+ section: parsedUri.section,
+ original: parsedUri.original
+ });
+ }
+ for (let [uri, value] of Object.entries(dupes)) {
+ if (value.length > 1) {
+ this._addError(0, "duplicateURI", uri, JSON.stringify(value));
+ }
+ }
+
+ // Loop through network entries making sure that fallback and cache don't
+ // contain uris starting with the network uri.
+ for (let neturi of parsed.uris) {
+ if (neturi.section == "NETWORK") {
+ for (let parsedUri of parsed.uris) {
+ if (parsedUri.section !== "NETWORK" &&
+ parsedUri.uri.startsWith(neturi.uri)) {
+ this._addError(neturi.line, "networkBlocksURI", neturi.line,
+ neturi.original, parsedUri.line, parsedUri.original,
+ parsedUri.section);
+ }
+ }
+ }
+ }
+
+ // Loop through fallback entries making sure that fallback and cache don't
+ // contain uris starting with the network uri.
+ for (let fb of parsed.fallbacks) {
+ for (let parsedUri of parsed.uris) {
+ if (parsedUri.uri.startsWith(fb.namespace)) {
+ this._addError(fb.line, "fallbackBlocksURI", fb.line,
+ fb.original, parsedUri.line, parsedUri.original,
+ parsedUri.section);
+ }
+ }
+ }
+
+ // Check that all resources exist and that their cach-control headers are
+ // not set to no-store.
+ let current = -1;
+ for (let i = 0, len = parsed.uris.length; i < len; i++) {
+ let parsedUri = parsed.uris[i];
+ this._getURIInfo(parsedUri.uri).then(uriInfo => {
+ current++;
+
+ if (uriInfo.success) {
+ // Check that the resource was not modified after the manifest was last
+ // modified. If it was then the manifest file should be refreshed.
+ let resourceLastModified =
+ new Date(uriInfo.responseHeaders["Last-Modified"]);
+
+ if (manifestLastModified < resourceLastModified) {
+ this._addError(parsedUri.line, "fileChangedButNotManifest",
+ uriInfo.name, manifestName, parsedUri.line);
+ }
+
+ // If cache-control: no-store the file will not be added to the
+ // appCache.
+ if (uriInfo.nocache) {
+ this._addError(parsedUri.line, "cacheControlNoStore",
+ parsedUri.original, parsedUri.line);
+ }
+ } else if (parsedUri.original !== "*") {
+ this._addError(parsedUri.line, "notAvailable",
+ parsedUri.original, parsedUri.line);
+ }
+
+ if (current == len - 1) {
+ deferred.resolve();
+ }
+ });
+ }
+
+ return deferred.promise;
+ },
+
+ _getURIInfo: function ACU__getURIInfo(uri) {
+ let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
+ .createInstance(Ci.nsIScriptableInputStream);
+ let deferred = defer();
+ let buffer = "";
+ var channel = NetUtil.newChannel({
+ uri: uri,
+ loadUsingSystemPrincipal: true,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL
+ });
+
+ // Avoid the cache:
+ channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+
+ channel.asyncOpen2({
+ onStartRequest: function (request, context) {
+ // This empty method is needed in order for onDataAvailable to be
+ // called.
+ },
+
+ onDataAvailable: function (request, context, stream, offset, count) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ inputStream.init(stream);
+ buffer = buffer.concat(inputStream.read(count));
+ },
+
+ onStopRequest: function onStartRequest(request, context, statusCode) {
+ if (statusCode === 0) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+
+ let result = {
+ name: request.name,
+ success: request.requestSucceeded,
+ status: request.responseStatus + " - " + request.responseStatusText,
+ charset: request.contentCharset || "utf-8",
+ mimeType: request.contentType,
+ contentLength: request.contentLength,
+ nocache: request.isNoCacheResponse() || request.isNoStoreResponse(),
+ prePath: request.URI.prePath + "/",
+ text: buffer
+ };
+
+ result.requestHeaders = {};
+ request.visitRequestHeaders(function (header, value) {
+ result.requestHeaders[header] = value;
+ });
+
+ result.responseHeaders = {};
+ request.visitResponseHeaders(function (header, value) {
+ result.responseHeaders[header] = value;
+ });
+
+ deferred.resolve(result);
+ } else {
+ deferred.resolve({
+ name: request.name,
+ success: false
+ });
+ }
+ }
+ });
+ return deferred.promise;
+ },
+
+ listEntries: function ACU_show(searchTerm) {
+ if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
+ throw new Error(l10n.GetStringFromName("cacheDisabled"));
+ }
+
+ let entries = [];
+
+ let appCacheStorage = Services.cache2.appCacheStorage(LoadContextInfo.default, null);
+ appCacheStorage.asyncVisitStorage({
+ onCacheStorageInfo: function () {},
+
+ onCacheEntryInfo: function (aURI, aIdEnhance, aDataSize, aFetchCount, aLastModifiedTime, aExpirationTime) {
+ let lowerKey = aURI.asciiSpec.toLowerCase();
+
+ if (searchTerm && lowerKey.indexOf(searchTerm.toLowerCase()) == -1) {
+ return;
+ }
+
+ if (aIdEnhance) {
+ aIdEnhance += ":";
+ }
+
+ let entry = {
+ "deviceID": "offline",
+ "key": aIdEnhance + aURI.asciiSpec,
+ "fetchCount": aFetchCount,
+ "lastFetched": null,
+ "lastModified": new Date(aLastModifiedTime * 1000),
+ "expirationTime": new Date(aExpirationTime * 1000),
+ "dataSize": aDataSize
+ };
+
+ entries.push(entry);
+ return true;
+ }
+ }, true);
+
+ if (entries.length === 0) {
+ throw new Error(l10n.GetStringFromName("noResults"));
+ }
+ return entries;
+ },
+
+ viewEntry: function ACU_viewEntry(key) {
+ let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Ci.nsIWindowMediator);
+ let win = wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ let url = "about:cache-entry?storage=appcache&context=&eid=&uri=" + key;
+ win.openUILinkIn(url, "tab");
+ },
+
+ clearAll: function ACU_clearAll() {
+ if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
+ throw new Error(l10n.GetStringFromName("cacheDisabled"));
+ }
+
+ let appCacheStorage = Services.cache2.appCacheStorage(LoadContextInfo.default, null);
+ appCacheStorage.asyncEvictStorage({
+ onCacheEntryDoomed: function (result) {}
+ });
+ },
+
+ _getManifestURI: function ACU__getManifestURI() {
+ let deferred = defer();
+
+ let getURI = () => {
+ let htmlNode = this.doc.querySelector("html[manifest]");
+ if (htmlNode) {
+ let pageUri = this.doc.location ? this.doc.location.href : this.uri;
+ let origin = pageUri.substr(0, pageUri.lastIndexOf("/") + 1);
+ let manifestURI = htmlNode.getAttribute("manifest");
+
+ if (manifestURI.startsWith("/")) {
+ manifestURI = manifestURI.substr(1);
+ }
+
+ return origin + manifestURI;
+ }
+ };
+
+ if (this.doc) {
+ let uri = getURI();
+ return promise.resolve(uri);
+ } else {
+ this._getURIInfo(this.uri).then(uriInfo => {
+ if (uriInfo.success) {
+ let html = uriInfo.text;
+ let parser = _DOMParser;
+ this.doc = parser.parseFromString(html, "text/html");
+ let uri = getURI();
+ deferred.resolve(uri);
+ } else {
+ this.errors.push({
+ line: 0,
+ msg: l10n.GetStringFromName("invalidURI")
+ });
+ }
+ });
+ }
+ return deferred.promise;
+ },
+
+ _addError: function ACU__addError(line, l10nString, ...params) {
+ let msg;
+
+ if (params) {
+ msg = l10n.formatStringFromName(l10nString, params, params.length);
+ } else {
+ msg = l10n.GetStringFromName(l10nString);
+ }
+
+ this.errors.push({
+ line: line,
+ msg: msg
+ });
+ },
+};
+
+/**
+ * We use our own custom parser because we need far more detailed information
+ * than the system manifest parser provides.
+ *
+ * @param {String} manifestText
+ * The text content of the manifest file.
+ * @param {String} manifestURI
+ * The URI of the manifest file. This is used in calculating the path of
+ * relative URIs.
+ */
+function ManifestParser(manifestText, manifestURI) {
+ this.manifestText = manifestText;
+ this.origin = manifestURI.substr(0, manifestURI.lastIndexOf("/") + 1)
+ .replace(" ", "%20");
+}
+
+ManifestParser.prototype = {
+ parse: function OCIMP_parse() {
+ let lines = this.manifestText.split(/\r?\n/);
+ let fallbacks = this.fallbacks = [];
+ let settings = this.settings = [];
+ let errors = this.errors = [];
+ let uris = this.uris = [];
+
+ this.currSection = "CACHE";
+
+ for (let i = 0; i < lines.length; i++) {
+ let text = this.text = lines[i].trim();
+ this.currentLine = i + 1;
+
+ if (i === 0 && text !== "CACHE MANIFEST") {
+ this._addError(1, "firstLineMustBeCacheManifest", 1);
+ }
+
+ // Ignore comments
+ if (/^#/.test(text) || !text.length) {
+ continue;
+ }
+
+ if (text == "CACHE MANIFEST") {
+ if (this.currentLine != 1) {
+ this._addError(this.currentLine, "cacheManifestOnlyFirstLine2",
+ this.currentLine);
+ }
+ continue;
+ }
+
+ if (this._maybeUpdateSectionName()) {
+ continue;
+ }
+
+ switch (this.currSection) {
+ case "CACHE":
+ case "NETWORK":
+ this.parseLine();
+ break;
+ case "FALLBACK":
+ this.parseFallbackLine();
+ break;
+ case "SETTINGS":
+ this.parseSettingsLine();
+ break;
+ }
+ }
+
+ return {
+ uris: uris,
+ fallbacks: fallbacks,
+ settings: settings,
+ errors: errors
+ };
+ },
+
+ parseLine: function OCIMP_parseLine() {
+ let text = this.text;
+
+ if (text.indexOf("*") != -1) {
+ if (this.currSection != "NETWORK" || text.length != 1) {
+ this._addError(this.currentLine, "asteriskInWrongSection2",
+ this.currSection, this.currentLine);
+ return;
+ }
+ }
+
+ if (/\s/.test(text)) {
+ this._addError(this.currentLine, "escapeSpaces", this.currentLine);
+ text = text.replace(/\s/g, "%20");
+ }
+
+ if (text[0] == "/") {
+ if (text.substr(0, 4) == "/../") {
+ this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine);
+ } else {
+ this.uris.push(this._wrapURI(this.origin + text.substring(1)));
+ }
+ } else if (text.substr(0, 2) == "./") {
+ this.uris.push(this._wrapURI(this.origin + text.substring(2)));
+ } else if (text.substr(0, 4) == "http") {
+ this.uris.push(this._wrapURI(text));
+ } else {
+ let origin = this.origin;
+ let path = text;
+
+ while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) {
+ let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1;
+ origin = origin.substr(0, trimIdx);
+ path = path.substr(3);
+ }
+
+ if (path.substr(0, 3) == "../") {
+ this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine);
+ return;
+ }
+
+ if (/^https?:\/\//.test(path)) {
+ this.uris.push(this._wrapURI(path));
+ return;
+ }
+ this.uris.push(this._wrapURI(origin + path));
+ }
+ },
+
+ parseFallbackLine: function OCIMP_parseFallbackLine() {
+ let split = this.text.split(/\s+/);
+ let origURI = this.text;
+
+ if (split.length != 2) {
+ this._addError(this.currentLine, "fallbackUseSpaces", this.currentLine);
+ return;
+ }
+
+ let [ namespace, fallback ] = split;
+
+ if (namespace.indexOf("*") != -1) {
+ this._addError(this.currentLine, "fallbackAsterisk2", this.currentLine);
+ }
+
+ if (/\s/.test(namespace)) {
+ this._addError(this.currentLine, "escapeSpaces", this.currentLine);
+ namespace = namespace.replace(/\s/g, "%20");
+ }
+
+ if (namespace.substr(0, 4) == "/../") {
+ this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine);
+ }
+
+ if (namespace.substr(0, 2) == "./") {
+ namespace = this.origin + namespace.substring(2);
+ }
+
+ if (namespace.substr(0, 4) != "http") {
+ let origin = this.origin;
+ let path = namespace;
+
+ while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) {
+ let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1;
+ origin = origin.substr(0, trimIdx);
+ path = path.substr(3);
+ }
+
+ if (path.substr(0, 3) == "../") {
+ this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine);
+ }
+
+ if (/^https?:\/\//.test(path)) {
+ namespace = path;
+ } else {
+ if (path[0] == "/") {
+ path = path.substring(1);
+ }
+ namespace = origin + path;
+ }
+ }
+
+ this.text = fallback;
+ this.parseLine();
+
+ this.fallbacks.push({
+ line: this.currentLine,
+ original: origURI,
+ namespace: namespace,
+ fallback: fallback
+ });
+ },
+
+ parseSettingsLine: function OCIMP_parseSettingsLine() {
+ let text = this.text;
+
+ if (this.settings.length == 1 || !/prefer-online|fast/.test(text)) {
+ this._addError(this.currentLine, "settingsBadValue", this.currentLine);
+ return;
+ }
+
+ switch (text) {
+ case "prefer-online":
+ this.settings.push(this._wrapURI(text));
+ break;
+ case "fast":
+ this.settings.push(this._wrapURI(text));
+ break;
+ }
+ },
+
+ _wrapURI: function OCIMP__wrapURI(uri) {
+ return {
+ section: this.currSection,
+ line: this.currentLine,
+ uri: uri,
+ original: this.text
+ };
+ },
+
+ _addError: function OCIMP__addError(line, l10nString, ...params) {
+ let msg;
+
+ if (params) {
+ msg = l10n.formatStringFromName(l10nString, params, params.length);
+ } else {
+ msg = l10n.GetStringFromName(l10nString);
+ }
+
+ this.errors.push({
+ line: line,
+ msg: msg
+ });
+ },
+
+ _maybeUpdateSectionName: function OCIMP__maybeUpdateSectionName() {
+ let text = this.text;
+
+ if (text == text.toUpperCase() && text.charAt(text.length - 1) == ":") {
+ text = text.substr(0, text.length - 1);
+
+ switch (text) {
+ case "CACHE":
+ case "NETWORK":
+ case "FALLBACK":
+ case "SETTINGS":
+ this.currSection = text;
+ return true;
+ default:
+ this._addError(this.currentLine,
+ "invalidSectionName", text, this.currentLine);
+ return false;
+ }
+ }
+ },
+};
+
+XPCOMUtils.defineLazyGetter(this, "l10n", () => Services.strings
+ .createBundle("chrome://devtools/locale/appcacheutils.properties"));
+
+XPCOMUtils.defineLazyGetter(this, "appcacheservice", function () {
+ return Cc["@mozilla.org/network/application-cache-service;1"]
+ .getService(Ci.nsIApplicationCacheService);
+
+});
+
+XPCOMUtils.defineLazyGetter(this, "_DOMParser", function () {
+ return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
+});
diff --git a/devtools/client/shared/DOMHelpers.jsm b/devtools/client/shared/DOMHelpers.jsm
new file mode 100644
index 000000000..9c861006e
--- /dev/null
+++ b/devtools/client/shared/DOMHelpers.jsm
@@ -0,0 +1,166 @@
+/* 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 Ci = Components.interfaces;
+const Cu = Components.utils;
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
+
+this.EXPORTED_SYMBOLS = ["DOMHelpers"];
+
+/**
+ * DOMHelpers
+ * Makes DOM traversal easier. Goes through iframes.
+ *
+ * @constructor
+ * @param nsIDOMWindow aWindow
+ * The content window, owning the document to traverse.
+ */
+this.DOMHelpers = function DOMHelpers(aWindow) {
+ if (!aWindow) {
+ throw new Error("window can't be null or undefined");
+ }
+ this.window = aWindow;
+};
+
+DOMHelpers.prototype = {
+ getParentObject: function Helpers_getParentObject(node)
+ {
+ let parentNode = node ? node.parentNode : null;
+
+ if (!parentNode) {
+ // Documents have no parentNode; Attr, Document, DocumentFragment, Entity,
+ // and Notation. top level windows have no parentNode
+ if (node && node == this.window.Node.DOCUMENT_NODE) {
+ // document type
+ if (node.defaultView) {
+ let embeddingFrame = node.defaultView.frameElement;
+ if (embeddingFrame)
+ return embeddingFrame.parentNode;
+ }
+ }
+ // a Document object without a parentNode or window
+ return null; // top level has no parent
+ }
+
+ if (parentNode.nodeType == this.window.Node.DOCUMENT_NODE) {
+ if (parentNode.defaultView) {
+ return parentNode.defaultView.frameElement;
+ }
+ // parent is document element, but no window at defaultView.
+ return null;
+ }
+
+ if (!parentNode.localName)
+ return null;
+
+ return parentNode;
+ },
+
+ getChildObject: function Helpers_getChildObject(node, index, previousSibling,
+ showTextNodesWithWhitespace)
+ {
+ if (!node)
+ return null;
+
+ if (node.contentDocument) {
+ // then the node is a frame
+ if (index == 0) {
+ return node.contentDocument.documentElement; // the node's HTMLElement
+ }
+ return null;
+ }
+
+ if (node.getSVGDocument) {
+ let svgDocument = node.getSVGDocument();
+ if (svgDocument) {
+ // then the node is a frame
+ if (index == 0) {
+ return svgDocument.documentElement; // the node's SVGElement
+ }
+ return null;
+ }
+ }
+
+ let child = null;
+ if (previousSibling) // then we are walking
+ child = this.getNextSibling(previousSibling);
+ else
+ child = this.getFirstChild(node);
+
+ if (showTextNodesWithWhitespace)
+ return child;
+
+ for (; child; child = this.getNextSibling(child)) {
+ if (!this.isWhitespaceText(child))
+ return child;
+ }
+
+ return null; // we have no children worth showing.
+ },
+
+ getFirstChild: function Helpers_getFirstChild(node)
+ {
+ let SHOW_ALL = nodeFilterConstants.SHOW_ALL;
+ this.treeWalker = node.ownerDocument.createTreeWalker(node,
+ SHOW_ALL, null);
+ return this.treeWalker.firstChild();
+ },
+
+ getNextSibling: function Helpers_getNextSibling(node)
+ {
+ let next = this.treeWalker.nextSibling();
+
+ if (!next)
+ delete this.treeWalker;
+
+ return next;
+ },
+
+ isWhitespaceText: function Helpers_isWhitespaceText(node)
+ {
+ return node.nodeType == this.window.Node.TEXT_NODE &&
+ !/[^\s]/.exec(node.nodeValue);
+ },
+
+ destroy: function Helpers_destroy()
+ {
+ delete this.window;
+ delete this.treeWalker;
+ },
+
+ /**
+ * A simple way to be notified (once) when a window becomes
+ * interactive (DOMContentLoaded).
+ *
+ * It is based on the chromeEventHandler. This is useful when
+ * chrome iframes are loaded in content docshells (in Firefox
+ * tabs for example).
+ */
+ onceDOMReady: function Helpers_onLocationChange(callback, targetURL) {
+ let window = this.window;
+ let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ let onReady = function (event) {
+ if (event.target == window.document) {
+ docShell.chromeEventHandler.removeEventListener("DOMContentLoaded", onReady, false);
+ // If in `callback` the URL of the window is changed and a listener to DOMContentLoaded
+ // is attached, the event we just received will be also be caught by the new listener.
+ // We want to avoid that so we execute the callback in the next queue.
+ Services.tm.mainThread.dispatch(callback, 0);
+ }
+ };
+ if ((window.document.readyState == "complete" ||
+ window.document.readyState == "interactive") &&
+ window.location.href == targetURL) {
+ Services.tm.mainThread.dispatch(callback, 0);
+ } else {
+ docShell.chromeEventHandler.addEventListener("DOMContentLoaded", onReady, false);
+ }
+ }
+};
diff --git a/devtools/client/shared/Jsbeautify.jsm b/devtools/client/shared/Jsbeautify.jsm
new file mode 100644
index 000000000..2293afc63
--- /dev/null
+++ b/devtools/client/shared/Jsbeautify.jsm
@@ -0,0 +1,16 @@
+/* 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";
+
+/*
+ * JS Beautifier. Please use require("devtools/shared/jsbeautify/beautify") instead of
+ * this JSM.
+ */
+
+this.EXPORTED_SYMBOLS = [ "jsBeautify" ];
+
+const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const { beautify } = require("devtools/shared/jsbeautify/beautify");
+const jsBeautify = beautify.js;
diff --git a/devtools/client/shared/SplitView.jsm b/devtools/client/shared/SplitView.jsm
new file mode 100644
index 000000000..f72aad2ac
--- /dev/null
+++ b/devtools/client/shared/SplitView.jsm
@@ -0,0 +1,312 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+this.EXPORTED_SYMBOLS = ["SplitView"];
+
+/* this must be kept in sync with CSS (ie. splitview.css) */
+const LANDSCAPE_MEDIA_QUERY = "(min-width: 701px)";
+
+var bindings = new WeakMap();
+
+/**
+ * SplitView constructor
+ *
+ * Initialize the split view UI on an existing DOM element.
+ *
+ * A split view contains items, each of those having one summary and one details
+ * elements.
+ * It is adaptive as it behaves similarly to a richlistbox when there the aspect
+ * ratio is narrow or as a pair listbox-box otherwise.
+ *
+ * @param DOMElement aRoot
+ * @see appendItem
+ */
+this.SplitView = function SplitView(aRoot)
+{
+ this._root = aRoot;
+ this._controller = aRoot.querySelector(".splitview-controller");
+ this._nav = aRoot.querySelector(".splitview-nav");
+ this._side = aRoot.querySelector(".splitview-side-details");
+ this._activeSummary = null;
+
+ this._mql = aRoot.ownerDocument.defaultView.matchMedia(LANDSCAPE_MEDIA_QUERY);
+
+ // items list focus and search-on-type handling
+ this._nav.addEventListener("keydown", (aEvent) => {
+ function getFocusedItemWithin(nav) {
+ let node = nav.ownerDocument.activeElement;
+ while (node && node.parentNode != nav) {
+ node = node.parentNode;
+ }
+ return node;
+ }
+
+ // do not steal focus from inside iframes or textboxes
+ if (aEvent.target.ownerDocument != this._nav.ownerDocument ||
+ aEvent.target.tagName == "input" ||
+ aEvent.target.tagName == "textbox" ||
+ aEvent.target.tagName == "textarea" ||
+ aEvent.target.classList.contains("textbox")) {
+ return false;
+ }
+
+ // handle keyboard navigation within the items list
+ let newFocusOrdinal;
+ if (aEvent.keyCode == KeyCodes.DOM_VK_PAGE_UP ||
+ aEvent.keyCode == KeyCodes.DOM_VK_HOME) {
+ newFocusOrdinal = 0;
+ } else if (aEvent.keyCode == KeyCodes.DOM_VK_PAGE_DOWN ||
+ aEvent.keyCode == KeyCodes.DOM_VK_END) {
+ newFocusOrdinal = this._nav.childNodes.length - 1;
+ } else if (aEvent.keyCode == KeyCodes.DOM_VK_UP) {
+ newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
+ newFocusOrdinal--;
+ } else if (aEvent.keyCode == KeyCodes.DOM_VK_DOWN) {
+ newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
+ newFocusOrdinal++;
+ }
+ if (newFocusOrdinal !== undefined) {
+ aEvent.stopPropagation();
+ let el = this.getSummaryElementByOrdinal(newFocusOrdinal);
+ if (el) {
+ el.focus();
+ }
+ return false;
+ }
+ }, false);
+};
+
+SplitView.prototype = {
+ /**
+ * Retrieve whether the UI currently has a landscape orientation.
+ *
+ * @return boolean
+ */
+ get isLandscape()
+ {
+ return this._mql.matches;
+ },
+
+ /**
+ * Retrieve the root element.
+ *
+ * @return DOMElement
+ */
+ get rootElement()
+ {
+ return this._root;
+ },
+
+ /**
+ * Retrieve the active item's summary element or null if there is none.
+ *
+ * @return DOMElement
+ */
+ get activeSummary()
+ {
+ return this._activeSummary;
+ },
+
+ /**
+ * Set the active item's summary element.
+ *
+ * @param DOMElement aSummary
+ */
+ set activeSummary(aSummary)
+ {
+ if (aSummary == this._activeSummary) {
+ return;
+ }
+
+ if (this._activeSummary) {
+ let binding = bindings.get(this._activeSummary);
+
+ if (binding.onHide) {
+ binding.onHide(this._activeSummary, binding._details, binding.data);
+ }
+
+ this._activeSummary.classList.remove("splitview-active");
+ binding._details.classList.remove("splitview-active");
+ }
+
+ if (!aSummary) {
+ return;
+ }
+
+ let binding = bindings.get(aSummary);
+ aSummary.classList.add("splitview-active");
+ binding._details.classList.add("splitview-active");
+
+ this._activeSummary = aSummary;
+
+ if (binding.onShow) {
+ binding.onShow(aSummary, binding._details, binding.data);
+ }
+ },
+
+ /**
+ * Retrieve the active item's details element or null if there is none.
+ * @return DOMElement
+ */
+ get activeDetails()
+ {
+ let summary = this.activeSummary;
+ return summary ? bindings.get(summary)._details : null;
+ },
+
+ /**
+ * Retrieve the summary element for a given ordinal.
+ *
+ * @param number aOrdinal
+ * @return DOMElement
+ * Summary element with given ordinal or null if not found.
+ * @see appendItem
+ */
+ getSummaryElementByOrdinal: function SEC_getSummaryElementByOrdinal(aOrdinal)
+ {
+ return this._nav.querySelector("* > li[data-ordinal='" + aOrdinal + "']");
+ },
+
+ /**
+ * Append an item to the split view.
+ *
+ * @param DOMElement aSummary
+ * The summary element for the item.
+ * @param DOMElement aDetails
+ * The details element for the item.
+ * @param object aOptions
+ * Optional object that defines custom behavior and data for the item.
+ * All properties are optional :
+ * - function(DOMElement summary, DOMElement details, object data) onCreate
+ * Called when the item has been added.
+ * - function(summary, details, data) onShow
+ * Called when the item is shown/active.
+ * - function(summary, details, data) onHide
+ * Called when the item is hidden/inactive.
+ * - function(summary, details, data) onDestroy
+ * Called when the item has been removed.
+ * - object data
+ * Object to pass to the callbacks above.
+ * - number ordinal
+ * Items with a lower ordinal are displayed before those with a
+ * higher ordinal.
+ */
+ appendItem: function ASV_appendItem(aSummary, aDetails, aOptions)
+ {
+ let binding = aOptions || {};
+
+ binding._summary = aSummary;
+ binding._details = aDetails;
+ bindings.set(aSummary, binding);
+
+ this._nav.appendChild(aSummary);
+
+ aSummary.addEventListener("click", (aEvent) => {
+ aEvent.stopPropagation();
+ this.activeSummary = aSummary;
+ }, false);
+
+ this._side.appendChild(aDetails);
+
+ if (binding.onCreate) {
+ binding.onCreate(aSummary, aDetails, binding.data);
+ }
+ },
+
+ /**
+ * Append an item to the split view according to two template elements
+ * (one for the item's summary and the other for the item's details).
+ *
+ * @param string aName
+ * Name of the template elements to instantiate.
+ * Requires two (hidden) DOM elements with id "splitview-tpl-summary-"
+ * and "splitview-tpl-details-" suffixed with aName.
+ * @param object aOptions
+ * Optional object that defines custom behavior and data for the item.
+ * See appendItem for full description.
+ * @return object{summary:,details:}
+ * Object with the new DOM elements created for summary and details.
+ * @see appendItem
+ */
+ appendTemplatedItem: function ASV_appendTemplatedItem(aName, aOptions)
+ {
+ aOptions = aOptions || {};
+ let summary = this._root.querySelector("#splitview-tpl-summary-" + aName);
+ let details = this._root.querySelector("#splitview-tpl-details-" + aName);
+
+ summary = summary.cloneNode(true);
+ summary.id = "";
+ if (aOptions.ordinal !== undefined) { // can be zero
+ summary.style.MozBoxOrdinalGroup = aOptions.ordinal;
+ summary.setAttribute("data-ordinal", aOptions.ordinal);
+ }
+ details = details.cloneNode(true);
+ details.id = "";
+
+ this.appendItem(summary, details, aOptions);
+ return {summary: summary, details: details};
+ },
+
+ /**
+ * Remove an item from the split view.
+ *
+ * @param DOMElement aSummary
+ * Summary element of the item to remove.
+ */
+ removeItem: function ASV_removeItem(aSummary)
+ {
+ if (aSummary == this._activeSummary) {
+ this.activeSummary = null;
+ }
+
+ let binding = bindings.get(aSummary);
+ aSummary.parentNode.removeChild(aSummary);
+ binding._details.parentNode.removeChild(binding._details);
+
+ if (binding.onDestroy) {
+ binding.onDestroy(aSummary, binding._details, binding.data);
+ }
+ },
+
+ /**
+ * Remove all items from the split view.
+ */
+ removeAll: function ASV_removeAll()
+ {
+ while (this._nav.hasChildNodes()) {
+ this.removeItem(this._nav.firstChild);
+ }
+ },
+
+ /**
+ * Set the item's CSS class name.
+ * This sets the class on both the summary and details elements, retaining
+ * any SplitView-specific classes.
+ *
+ * @param DOMElement aSummary
+ * Summary element of the item to set.
+ * @param string aClassName
+ * One or more space-separated CSS classes.
+ */
+ setItemClassName: function ASV_setItemClassName(aSummary, aClassName)
+ {
+ let binding = bindings.get(aSummary);
+ let viewSpecific;
+
+ viewSpecific = aSummary.className.match(/(splitview\-[\w-]+)/g);
+ viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
+ aSummary.className = viewSpecific + " " + aClassName;
+
+ viewSpecific = binding._details.className.match(/(splitview\-[\w-]+)/g);
+ viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
+ binding._details.className = viewSpecific + " " + aClassName;
+ },
+};
diff --git a/devtools/client/shared/autocomplete-popup.js b/devtools/client/shared/autocomplete-popup.js
new file mode 100644
index 000000000..1d24c948e
--- /dev/null
+++ b/devtools/client/shared/autocomplete-popup.js
@@ -0,0 +1,599 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const Services = require("Services");
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+let itemIdCounter = 0;
+/**
+ * Autocomplete popup UI implementation.
+ *
+ * @constructor
+ * @param {Document} toolboxDoc
+ * The toolbox document to attach the autocomplete popup panel.
+ * @param {Object} options
+ * An object consiting any of the following options:
+ * - listId {String} The id for the list <LI> element.
+ * - position {String} The position for the tooltip ("top" or "bottom").
+ * - theme {String} String related to the theme of the popup
+ * - autoSelect {Boolean} Boolean to allow the first entry of the popup
+ * panel to be automatically selected when the popup shows.
+ * - onSelect {String} Callback called when the selected index is updated.
+ * - onClick {String} Callback called when the autocomplete popup receives a click
+ * event. The selectedIndex will already be updated if need be.
+ */
+function AutocompletePopup(toolboxDoc, options = {}) {
+ EventEmitter.decorate(this);
+
+ this._document = toolboxDoc;
+
+ this.autoSelect = options.autoSelect || false;
+ this.position = options.position || "bottom";
+ let theme = options.theme || "dark";
+
+ this.onSelectCallback = options.onSelect;
+ this.onClickCallback = options.onClick;
+
+ // If theme is auto, use the devtools.theme pref
+ if (theme === "auto") {
+ theme = Services.prefs.getCharPref("devtools.theme");
+ this.autoThemeEnabled = true;
+ // Setup theme change listener.
+ this._handleThemeChange = this._handleThemeChange.bind(this);
+ gDevTools.on("pref-changed", this._handleThemeChange);
+ }
+
+ // Create HTMLTooltip instance
+ this._tooltip = new HTMLTooltip(this._document);
+ this._tooltip.panel.classList.add(
+ "devtools-autocomplete-popup",
+ "devtools-monospace",
+ theme + "-theme");
+ // Stop this appearing as an alert to accessibility.
+ this._tooltip.panel.setAttribute("role", "presentation");
+
+ this._list = this._document.createElementNS(HTML_NS, "ul");
+ this._list.setAttribute("flex", "1");
+
+ // The list clone will be inserted in the same document as the anchor, and will receive
+ // a copy of the main list innerHTML to allow screen readers to access the list.
+ this._listClone = this._document.createElementNS(HTML_NS, "ul");
+ this._listClone.className = "devtools-autocomplete-list-aria-clone";
+
+ if (options.listId) {
+ this._list.setAttribute("id", options.listId);
+ }
+ this._list.className = "devtools-autocomplete-listbox " + theme + "-theme";
+
+ this._tooltip.setContent(this._list);
+
+ this.onClick = this.onClick.bind(this);
+ this._list.addEventListener("click", this.onClick, false);
+
+ // Array of raw autocomplete items
+ this.items = [];
+ // Map of autocompleteItem to HTMLElement
+ this.elements = new WeakMap();
+
+ this.selectedIndex = -1;
+}
+exports.AutocompletePopup = AutocompletePopup;
+
+AutocompletePopup.prototype = {
+ _document: null,
+ _tooltip: null,
+ _list: null,
+
+ onSelect: function (e) {
+ if (this.onSelectCallback) {
+ this.onSelectCallback(e);
+ }
+ },
+
+ onClick: function (e) {
+ let item = e.target.closest(".autocomplete-item");
+ if (item && typeof item.dataset.index !== "undefined") {
+ this.selectedIndex = parseInt(item.dataset.index, 10);
+ }
+
+ this.emit("popup-click");
+ if (this.onClickCallback) {
+ this.onClickCallback(e);
+ }
+ },
+
+ /**
+ * Open the autocomplete popup panel.
+ *
+ * @param {nsIDOMNode} anchor
+ * Optional node to anchor the panel to.
+ * @param {Number} xOffset
+ * Horizontal offset in pixels from the left of the node to the left
+ * of the popup.
+ * @param {Number} yOffset
+ * Vertical offset in pixels from the top of the node to the starting
+ * of the popup.
+ * @param {Number} index
+ * The position of item to select.
+ */
+ openPopup: function (anchor, xOffset = 0, yOffset = 0, index) {
+ this.__maxLabelLength = -1;
+ this._updateSize();
+
+ // Retrieve the anchor's document active element to add accessibility metadata.
+ this._activeElement = anchor.ownerDocument.activeElement;
+
+ this._tooltip.show(anchor, {
+ x: xOffset,
+ y: yOffset,
+ position: this.position,
+ });
+
+ this._tooltip.once("shown", () => {
+ if (this.autoSelect) {
+ this.selectItemAtIndex(index);
+ }
+
+ this.emit("popup-opened");
+ });
+ },
+
+ /**
+ * Select item at the provided index.
+ *
+ * @param {Number} index
+ * The position of the item to select.
+ */
+ selectItemAtIndex: function (index) {
+ if (typeof index !== "number") {
+ // If no index was provided, select the item closest to the input.
+ let isAboveInput = this.position === "top";
+ index = isAboveInput ? this.itemCount - 1 : 0;
+ }
+ this.selectedIndex = index;
+ },
+
+ /**
+ * Hide the autocomplete popup panel.
+ */
+ hidePopup: function () {
+ this._tooltip.once("hidden", () => {
+ this.emit("popup-closed");
+ });
+
+ this._clearActiveDescendant();
+ this._activeElement = null;
+ this._tooltip.hide();
+ },
+
+ /**
+ * Check if the autocomplete popup is open.
+ */
+ get isOpen() {
+ return this._tooltip && this._tooltip.isVisible();
+ },
+
+ /**
+ * Destroy the object instance. Please note that the panel DOM elements remain
+ * in the DOM, because they might still be in use by other instances of the
+ * same code. It is the responsability of the client code to perform DOM
+ * cleanup.
+ */
+ destroy: function () {
+ if (this.isOpen) {
+ this.hidePopup();
+ }
+
+ this._list.removeEventListener("click", this.onClick, false);
+
+ if (this.autoThemeEnabled) {
+ gDevTools.off("pref-changed", this._handleThemeChange);
+ }
+
+ this._list.remove();
+ this._listClone.remove();
+ this._tooltip.destroy();
+ this._document = null;
+ this._list = null;
+ this._tooltip = null;
+ },
+
+ /**
+ * Get the autocomplete items array.
+ *
+ * @param {Number} index
+ * The index of the item what is wanted.
+ *
+ * @return {Object} The autocomplete item at index index.
+ */
+ getItemAtIndex: function (index) {
+ return this.items[index];
+ },
+
+ /**
+ * Get the autocomplete items array.
+ *
+ * @return {Array} The array of autocomplete items.
+ */
+ getItems: function () {
+ // Return a copy of the array to avoid side effects from the caller code.
+ return this.items.slice(0);
+ },
+
+ /**
+ * Set the autocomplete items list, in one go.
+ *
+ * @param {Array} items
+ * The list of items you want displayed in the popup list.
+ * @param {Number} index
+ * The position of the item to select.
+ */
+ setItems: function (items, index) {
+ this.clearItems();
+ items.forEach(this.appendItem, this);
+
+ if (this.isOpen && this.autoSelect) {
+ this.selectItemAtIndex(index);
+ }
+ },
+
+ __maxLabelLength: -1,
+
+ get _maxLabelLength() {
+ if (this.__maxLabelLength !== -1) {
+ return this.__maxLabelLength;
+ }
+
+ let max = 0;
+ for (let {label, count} of this.items) {
+ if (count) {
+ label += count + "";
+ }
+ max = Math.max(label.length, max);
+ }
+
+ this.__maxLabelLength = max;
+ return this.__maxLabelLength;
+ },
+
+ /**
+ * Update the panel size to fit the content.
+ */
+ _updateSize: function () {
+ if (!this._tooltip) {
+ return;
+ }
+
+ this._list.style.width = (this._maxLabelLength + 3) + "ch";
+ let selectedItem = this.selectedItem;
+ if (selectedItem) {
+ this._scrollElementIntoViewIfNeeded(this.elements.get(selectedItem));
+ }
+ },
+
+ _scrollElementIntoViewIfNeeded: function (element) {
+ let quads = element.getBoxQuads({relativeTo: this._tooltip.panel});
+ if (!quads || !quads[0]) {
+ return;
+ }
+
+ let {top, height} = quads[0].bounds;
+ let containerHeight = this._tooltip.panel.getBoundingClientRect().height;
+ if (top < 0) {
+ // Element is above container.
+ element.scrollIntoView(true);
+ } else if ((top + height) > containerHeight) {
+ // Element is beloew container.
+ element.scrollIntoView(false);
+ }
+ },
+
+ /**
+ * Clear all the items from the autocomplete list.
+ */
+ clearItems: function () {
+ // Reset the selectedIndex to -1 before clearing the list
+ this.selectedIndex = -1;
+ this._list.innerHTML = "";
+ this.__maxLabelLength = -1;
+ this.items = [];
+ this.elements = new WeakMap();
+ },
+
+ /**
+ * Getter for the index of the selected item.
+ *
+ * @type {Number}
+ */
+ get selectedIndex() {
+ return this._selectedIndex;
+ },
+
+ /**
+ * Setter for the selected index.
+ *
+ * @param {Number} index
+ * The number (index) of the item you want to select in the list.
+ */
+ set selectedIndex(index) {
+ let previousSelected = this._list.querySelector(".autocomplete-selected");
+ if (previousSelected) {
+ previousSelected.classList.remove("autocomplete-selected");
+ }
+
+ let item = this.items[index];
+ if (this.isOpen && item) {
+ let element = this.elements.get(item);
+
+ element.classList.add("autocomplete-selected");
+ this._scrollElementIntoViewIfNeeded(element);
+ this._setActiveDescendant(element.id);
+ } else {
+ this._clearActiveDescendant();
+ }
+ this._selectedIndex = index;
+
+ if (this.isOpen && item && this.onSelectCallback) {
+ // Call the user-defined select callback if defined.
+ this.onSelectCallback();
+ }
+ },
+
+ /**
+ * Getter for the selected item.
+ * @type Object
+ */
+ get selectedItem() {
+ return this.items[this._selectedIndex];
+ },
+
+ /**
+ * Setter for the selected item.
+ *
+ * @param {Object} item
+ * The object you want selected in the list.
+ */
+ set selectedItem(item) {
+ let index = this.items.indexOf(item);
+ if (index !== -1 && this.isOpen) {
+ this.selectedIndex = index;
+ }
+ },
+
+ /**
+ * Update the aria-activedescendant attribute on the current active element for
+ * accessibility.
+ *
+ * @param {String} id
+ * The id (as in DOM id) of the currently selected autocomplete suggestion
+ */
+ _setActiveDescendant: function (id) {
+ if (!this._activeElement) {
+ return;
+ }
+
+ // Make sure the list clone is in the same document as the anchor.
+ let anchorDoc = this._activeElement.ownerDocument;
+ if (!this._listClone.parentNode || this._listClone.ownerDocument !== anchorDoc) {
+ anchorDoc.documentElement.appendChild(this._listClone);
+ }
+
+ // Update the clone content to match the current list content.
+ this._listClone.innerHTML = this._list.innerHTML;
+
+ this._activeElement.setAttribute("aria-activedescendant", id);
+ },
+
+ /**
+ * Clear the aria-activedescendant attribute on the current active element.
+ */
+ _clearActiveDescendant: function () {
+ if (!this._activeElement) {
+ return;
+ }
+
+ this._activeElement.removeAttribute("aria-activedescendant");
+ },
+
+ /**
+ * Append an item into the autocomplete list.
+ *
+ * @param {Object} item
+ * The item you want appended to the list.
+ * The item object can have the following properties:
+ * - label {String} Property which is used as the displayed value.
+ * - preLabel {String} [Optional] The String that will be displayed
+ * before the label indicating that this is the already
+ * present text in the input box, and label is the text
+ * that will be auto completed. When this property is
+ * present, |preLabel.length| starting characters will be
+ * removed from label.
+ * - count {Number} [Optional] The number to represent the count of
+ * autocompleted label.
+ */
+ appendItem: function (item) {
+ let listItem = this._document.createElementNS(HTML_NS, "li");
+ // Items must have an id for accessibility.
+ listItem.setAttribute("id", "autocomplete-item-" + itemIdCounter++);
+ listItem.className = "autocomplete-item";
+ listItem.setAttribute("data-index", this.items.length);
+ if (this.direction) {
+ listItem.setAttribute("dir", this.direction);
+ }
+ let label = this._document.createElementNS(HTML_NS, "span");
+ label.textContent = item.label;
+ label.className = "autocomplete-value";
+ if (item.preLabel) {
+ let preDesc = this._document.createElementNS(HTML_NS, "span");
+ preDesc.textContent = item.preLabel;
+ preDesc.className = "initial-value";
+ listItem.appendChild(preDesc);
+ label.textContent = item.label.slice(item.preLabel.length);
+ }
+ listItem.appendChild(label);
+ if (item.count && item.count > 1) {
+ let countDesc = this._document.createElementNS(HTML_NS, "span");
+ countDesc.textContent = item.count;
+ countDesc.setAttribute("flex", "1");
+ countDesc.className = "autocomplete-count";
+ listItem.appendChild(countDesc);
+ }
+
+ this._list.appendChild(listItem);
+ this.items.push(item);
+ this.elements.set(item, listItem);
+ },
+
+ /**
+ * Remove an item from the popup list.
+ *
+ * @param {Object} item
+ * The item you want removed.
+ */
+ removeItem: function (item) {
+ if (!this.items.includes(item)) {
+ return;
+ }
+
+ let itemIndex = this.items.indexOf(item);
+ let selectedIndex = this.selectedIndex;
+
+ // Remove autocomplete item.
+ this.items.splice(itemIndex, 1);
+
+ // Remove corresponding DOM element from the elements WeakMap and from the DOM.
+ let elementToRemove = this.elements.get(item);
+ this.elements.delete(elementToRemove);
+ elementToRemove.remove();
+
+ if (itemIndex <= selectedIndex) {
+ // If the removed item index was before or equal to the selected index, shift the
+ // selected index by 1.
+ this.selectedIndex = Math.max(0, selectedIndex - 1);
+ }
+ },
+
+ /**
+ * Getter for the number of items in the popup.
+ * @type {Number}
+ */
+ get itemCount() {
+ return this.items.length;
+ },
+
+ /**
+ * Getter for the height of each item in the list.
+ *
+ * @type {Number}
+ */
+ get _itemsPerPane() {
+ if (this.items.length) {
+ let listHeight = this._tooltip.panel.clientHeight;
+ let element = this.elements.get(this.items[0]);
+ let elementHeight = element.getBoundingClientRect().height;
+ return Math.floor(listHeight / elementHeight);
+ }
+ return 0;
+ },
+
+ /**
+ * Select the next item in the list.
+ *
+ * @return {Object}
+ * The newly selected item object.
+ */
+ selectNextItem: function () {
+ if (this.selectedIndex < (this.items.length - 1)) {
+ this.selectedIndex++;
+ } else {
+ this.selectedIndex = 0;
+ }
+ return this.selectedItem;
+ },
+
+ /**
+ * Select the previous item in the list.
+ *
+ * @return {Object}
+ * The newly-selected item object.
+ */
+ selectPreviousItem: function () {
+ if (this.selectedIndex > 0) {
+ this.selectedIndex--;
+ } else {
+ this.selectedIndex = this.items.length - 1;
+ }
+
+ return this.selectedItem;
+ },
+
+ /**
+ * Select the top-most item in the next page of items or
+ * the last item in the list.
+ *
+ * @return {Object}
+ * The newly-selected item object.
+ */
+ selectNextPageItem: function () {
+ let nextPageIndex = this.selectedIndex + this._itemsPerPane + 1;
+ this.selectedIndex = Math.min(nextPageIndex, this.itemCount - 1);
+ return this.selectedItem;
+ },
+
+ /**
+ * Select the bottom-most item in the previous page of items,
+ * or the first item in the list.
+ *
+ * @return {Object}
+ * The newly-selected item object.
+ */
+ selectPreviousPageItem: function () {
+ let prevPageIndex = this.selectedIndex - this._itemsPerPane - 1;
+ this.selectedIndex = Math.max(prevPageIndex, 0);
+ return this.selectedItem;
+ },
+
+ /**
+ * Manages theme switching for the popup based on the devtools.theme pref.
+ *
+ * @private
+ *
+ * @param {String} event
+ * The name of the event. In this case, "pref-changed".
+ * @param {Object} data
+ * An object passed by the emitter of the event. In this case, the
+ * object consists of three properties:
+ * - pref {String} The name of the preference that was modified.
+ * - newValue {Object} The new value of the preference.
+ * - oldValue {Object} The old value of the preference.
+ */
+ _handleThemeChange: function (event, data) {
+ if (data.pref === "devtools.theme") {
+ this._tooltip.panel.classList.toggle(data.oldValue + "-theme", false);
+ this._tooltip.panel.classList.toggle(data.newValue + "-theme", true);
+ this._list.classList.toggle(data.oldValue + "-theme", false);
+ this._list.classList.toggle(data.newValue + "-theme", true);
+ }
+ },
+
+ /**
+ * Used by tests.
+ */
+ get _panel() {
+ return this._tooltip.panel;
+ },
+
+ /**
+ * Used by tests.
+ */
+ get _window() {
+ return this._document.defaultView;
+ },
+};
diff --git a/devtools/client/shared/browser-loader.js b/devtools/client/shared/browser-loader.js
new file mode 100644
index 000000000..f5cac31e7
--- /dev/null
+++ b/devtools/client/shared/browser-loader.js
@@ -0,0 +1,235 @@
+/* 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";
+
+var Cu = Components.utils;
+const loaders = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+const { devtools } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { joinURI } = devtools.require("devtools/shared/path");
+const { assert } = devtools.require("devtools/shared/DevToolsUtils");
+const Services = devtools.require("Services");
+const { AppConstants } = devtools.require("resource://gre/modules/AppConstants.jsm");
+
+const BROWSER_BASED_DIRS = [
+ "resource://devtools/client/inspector/layout",
+ "resource://devtools/client/jsonview",
+ "resource://devtools/client/shared/vendor",
+ "resource://devtools/client/shared/redux",
+];
+
+// Any directory that matches the following regular expression
+// is also considered as browser based module directory.
+// ('resource://devtools/client/.*/components/')
+//
+// An example:
+// * `resource://devtools/client/inspector/components`
+// * `resource://devtools/client/inspector/shared/components`
+const browserBasedDirsRegExp =
+ /^resource\:\/\/devtools\/client\/\S*\/components\//;
+
+function clearCache() {
+ Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+}
+
+/*
+ * Create a loader to be used in a browser environment. This evaluates
+ * modules in their own environment, but sets window (the normal
+ * global object) as the sandbox prototype, so when a variable is not
+ * defined it checks `window` before throwing an error. This makes all
+ * browser APIs available to modules by default, like a normal browser
+ * environment, but modules are still evaluated in their own scope.
+ *
+ * Another very important feature of this loader is that it *only*
+ * deals with modules loaded from under `baseURI`. Anything loaded
+ * outside of that path will still be loaded from the devtools loader,
+ * so all system modules are still shared and cached across instances.
+ * An exception to this is anything under
+ * `devtools/client/shared/{vendor/components}`, which is where shared libraries
+ * and React components live that should be evaluated in a browser environment.
+ *
+ * @param string baseURI
+ * Base path to load modules from. If null or undefined, only
+ * the shared vendor/components modules are loaded with the browser
+ * loader.
+ * @param Object window
+ * The window instance to evaluate modules within
+ * @param Boolean useOnlyShared
+ * If true, ignores `baseURI` and only loads the shared
+ * BROWSER_BASED_DIRS via BrowserLoader.
+ * @return Object
+ * An object with two properties:
+ * - loader: the Loader instance
+ * - require: a function to require modules with
+ */
+function BrowserLoader(options) {
+ const browserLoaderBuilder = new BrowserLoaderBuilder(options);
+ return {
+ loader: browserLoaderBuilder.loader,
+ require: browserLoaderBuilder.require
+ };
+}
+
+/**
+ * Private class used to build the Loader instance and require method returned
+ * by BrowserLoader(baseURI, window).
+ *
+ * @param string baseURI
+ * Base path to load modules from.
+ * @param Object window
+ * The window instance to evaluate modules within
+ * @param Boolean useOnlyShared
+ * If true, ignores `baseURI` and only loads the shared
+ * BROWSER_BASED_DIRS via BrowserLoader.
+ */
+function BrowserLoaderBuilder({ baseURI, window, useOnlyShared }) {
+ assert(!!baseURI !== !!useOnlyShared,
+ "Cannot use both `baseURI` and `useOnlyShared`.");
+
+ const loaderOptions = devtools.require("@loader/options");
+ const dynamicPaths = {};
+ const componentProxies = new Map();
+
+ if (AppConstants.DEBUG || AppConstants.DEBUG_JS_MODULES) {
+ dynamicPaths["devtools/client/shared/vendor/react"] =
+ "resource://devtools/client/shared/vendor/react-dev";
+ }
+
+ const opts = {
+ id: "browser-loader",
+ sharedGlobal: true,
+ sandboxPrototype: window,
+ paths: Object.assign({}, dynamicPaths, loaderOptions.paths),
+ invisibleToDebugger: loaderOptions.invisibleToDebugger,
+ requireHook: (id, require) => {
+ // If |id| requires special handling, simply defer to devtools
+ // immediately.
+ if (devtools.isLoaderPluginId(id)) {
+ return devtools.require(id);
+ }
+
+ const uri = require.resolve(id);
+ let isBrowserDir = BROWSER_BASED_DIRS.filter(dir => {
+ return uri.startsWith(dir);
+ }).length > 0;
+
+ // If the URI doesn't match hardcoded paths try the regexp.
+ if (!isBrowserDir) {
+ isBrowserDir = uri.match(browserBasedDirsRegExp) != null;
+ }
+
+ if ((useOnlyShared || !uri.startsWith(baseURI)) && !isBrowserDir) {
+ return devtools.require(uri);
+ }
+
+ return require(uri);
+ },
+ globals: {
+ // Allow modules to use the window's console to ensure logs appear in a
+ // tab toolbox, if one exists, instead of just the browser console.
+ console: window.console,
+ // Make sure `define` function exists. This allows defining some modules
+ // in AMD format while retaining CommonJS compatibility through this hook.
+ // JSON Viewer needs modules in AMD format, as it currently uses RequireJS
+ // from a content document and can't access our usual loaders. So, any
+ // modules shared with the JSON Viewer should include a define wrapper:
+ //
+ // // Make this available to both AMD and CJS environments
+ // define(function(require, exports, module) {
+ // ... code ...
+ // });
+ //
+ // Bug 1248830 will work out a better plan here for our content module
+ // loading needs, especially as we head towards devtools.html.
+ define(factory) {
+ factory(this.require, this.exports, this.module);
+ },
+ // Allow modules to use the DevToolsLoader lazy loading helpers.
+ loader: {
+ lazyGetter: devtools.lazyGetter,
+ lazyImporter: devtools.lazyImporter,
+ lazyServiceGetter: devtools.lazyServiceGetter,
+ lazyRequireGetter: this.lazyRequireGetter.bind(this),
+ },
+ }
+ };
+
+ if (Services.prefs.getBoolPref("devtools.loader.hotreload")) {
+ opts.loadModuleHook = (module, require) => {
+ const { uri, exports } = module;
+
+ if (exports.prototype &&
+ exports.prototype.isReactComponent) {
+ const { createProxy, getForceUpdate } =
+ require("devtools/client/shared/vendor/react-proxy");
+ const React = require("devtools/client/shared/vendor/react");
+
+ if (!componentProxies.get(uri)) {
+ const proxy = createProxy(exports);
+ componentProxies.set(uri, proxy);
+ module.exports = proxy.get();
+ } else {
+ const proxy = componentProxies.get(uri);
+ const instances = proxy.update(exports);
+ instances.forEach(getForceUpdate(React));
+ module.exports = proxy.get();
+ }
+ }
+ return exports;
+ };
+ const watcher = devtools.require("devtools/client/shared/devtools-file-watcher");
+ let onFileChanged = (_, relativePath, path) => {
+ this.hotReloadFile(componentProxies, "resource://devtools/" + relativePath);
+ };
+ watcher.on("file-changed", onFileChanged);
+ window.addEventListener("unload", () => {
+ watcher.off("file-changed", onFileChanged);
+ });
+ }
+
+ const mainModule = loaders.Module(baseURI, joinURI(baseURI, "main.js"));
+ this.loader = loaders.Loader(opts);
+ this.require = loaders.Require(this.loader, mainModule);
+}
+
+BrowserLoaderBuilder.prototype = {
+ /**
+ * Define a getter property on the given object that requires the given
+ * module. This enables delaying importing modules until the module is
+ * actually used.
+ *
+ * @param Object obj
+ * The object to define the property on.
+ * @param String property
+ * The property name.
+ * @param String module
+ * The module path.
+ * @param Boolean destructure
+ * Pass true if the property name is a member of the module's exports.
+ */
+ lazyRequireGetter: function (obj, property, module, destructure) {
+ devtools.lazyGetter(obj, property, () => {
+ return destructure
+ ? this.require(module)[property]
+ : this.require(module || property);
+ });
+ },
+
+ hotReloadFile: function (componentProxies, fileURI) {
+ if (fileURI.match(/\.js$/)) {
+ // Test for React proxy components
+ const proxy = componentProxies.get(fileURI);
+ if (proxy) {
+ // Remove the old module and re-require the new one; the require
+ // hook in the loader will take care of the rest
+ delete this.loader.modules[fileURI];
+ clearCache();
+ this.require(fileURI);
+ }
+ }
+ }
+};
+
+this.BrowserLoader = BrowserLoader;
+
+this.EXPORTED_SYMBOLS = ["BrowserLoader"];
diff --git a/devtools/client/shared/components/.eslintrc.js b/devtools/client/shared/components/.eslintrc.js
new file mode 100644
index 000000000..3112895e9
--- /dev/null
+++ b/devtools/client/shared/components/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "globals": {
+ "define": true,
+ }
+};
diff --git a/devtools/client/shared/components/frame.js b/devtools/client/shared/components/frame.js
new file mode 100644
index 000000000..5abe5f057
--- /dev/null
+++ b/devtools/client/shared/components/frame.js
@@ -0,0 +1,239 @@
+/* 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 { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const { getSourceNames, parseURL,
+ isScratchpadScheme, getSourceMappedFile } = require("devtools/client/shared/source-utils");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
+const webl10n = new LocalizationHelper("devtools/client/locales/webconsole.properties");
+
+module.exports = createClass({
+ displayName: "Frame",
+
+ propTypes: {
+ // SavedFrame, or an object containing all the required properties.
+ frame: PropTypes.shape({
+ functionDisplayName: PropTypes.string,
+ source: PropTypes.string.isRequired,
+ line: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ column: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ }).isRequired,
+ // Clicking on the frame link -- probably should link to the debugger.
+ onClick: PropTypes.func.isRequired,
+ // Option to display a function name before the source link.
+ showFunctionName: PropTypes.bool,
+ // Option to display a function name even if it's anonymous.
+ showAnonymousFunctionName: PropTypes.bool,
+ // Option to display a host name after the source link.
+ showHost: PropTypes.bool,
+ // Option to display a host name if the filename is empty or just '/'
+ showEmptyPathAsHost: PropTypes.bool,
+ // Option to display a full source instead of just the filename.
+ showFullSourceUrl: PropTypes.bool,
+ // Service to enable the source map feature for console.
+ sourceMapService: PropTypes.object,
+ },
+
+ getDefaultProps() {
+ return {
+ showFunctionName: false,
+ showAnonymousFunctionName: false,
+ showHost: false,
+ showEmptyPathAsHost: false,
+ showFullSourceUrl: false,
+ };
+ },
+
+ componentWillMount() {
+ const sourceMapService = this.props.sourceMapService;
+ if (sourceMapService) {
+ const source = this.getSource();
+ sourceMapService.subscribe(source, this.onSourceUpdated);
+ }
+ },
+
+ componentWillUnmount() {
+ const sourceMapService = this.props.sourceMapService;
+ if (sourceMapService) {
+ const source = this.getSource();
+ sourceMapService.unsubscribe(source, this.onSourceUpdated);
+ }
+ },
+
+ /**
+ * Component method to update the FrameView when a resolved location is available
+ * @param event
+ * @param location
+ */
+ onSourceUpdated(event, location, resolvedLocation) {
+ const frame = this.getFrame(resolvedLocation);
+ this.setState({
+ frame,
+ isSourceMapped: true,
+ });
+ },
+
+ /**
+ * Utility method to convert the Frame object to the
+ * Source Object model required by SourceMapService
+ * @param frame
+ * @returns {{url: *, line: *, column: *}}
+ */
+ getSource(frame) {
+ frame = frame || this.props.frame;
+ const { source, line, column } = frame;
+ return {
+ url: source,
+ line,
+ column,
+ };
+ },
+
+ /**
+ * Utility method to convert the Source object model to the
+ * Frame object model required by FrameView class.
+ * @param source
+ * @returns {{source: *, line: *, column: *, functionDisplayName: *}}
+ */
+ getFrame(source) {
+ const { url, line, column } = source;
+ return {
+ source: url,
+ line,
+ column,
+ functionDisplayName: this.props.frame.functionDisplayName,
+ };
+ },
+
+ render() {
+ let frame, isSourceMapped;
+ let {
+ onClick,
+ showFunctionName,
+ showAnonymousFunctionName,
+ showHost,
+ showEmptyPathAsHost,
+ showFullSourceUrl
+ } = this.props;
+
+ if (this.state && this.state.isSourceMapped) {
+ frame = this.state.frame;
+ isSourceMapped = this.state.isSourceMapped;
+ } else {
+ frame = this.props.frame;
+ }
+
+ let source = frame.source ? String(frame.source) : "";
+ let line = frame.line != void 0 ? Number(frame.line) : null;
+ let column = frame.column != void 0 ? Number(frame.column) : null;
+
+ const { short, long, host } = getSourceNames(source);
+ // Reparse the URL to determine if we should link this; `getSourceNames`
+ // has already cached this indirectly. We don't want to attempt to
+ // link to "self-hosted" and "(unknown)". However, we do want to link
+ // to Scratchpad URIs.
+ // Source mapped sources might not necessary linkable, but they
+ // are still valid in the debugger.
+ const isLinkable = !!(isScratchpadScheme(source) || parseURL(source))
+ || isSourceMapped;
+ const elements = [];
+ const sourceElements = [];
+ let sourceEl;
+
+ let tooltip = long;
+
+ // Exclude all falsy values, including `0`, as line numbers start with 1.
+ if (line) {
+ tooltip += `:${line}`;
+ // Intentionally exclude 0
+ if (column) {
+ tooltip += `:${column}`;
+ }
+ }
+
+ let attributes = {
+ "data-url": long,
+ className: "frame-link",
+ };
+
+ if (showFunctionName) {
+ let functionDisplayName = frame.functionDisplayName;
+ if (!functionDisplayName && showAnonymousFunctionName) {
+ functionDisplayName = webl10n.getStr("stacktrace.anonymousFunction");
+ }
+
+ if (functionDisplayName) {
+ elements.push(
+ dom.span({ className: "frame-link-function-display-name" },
+ functionDisplayName),
+ " "
+ );
+ }
+ }
+
+ let displaySource = showFullSourceUrl ? long : short;
+ if (isSourceMapped) {
+ displaySource = getSourceMappedFile(displaySource);
+ } else if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) {
+ displaySource = host;
+ }
+
+ sourceElements.push(dom.span({
+ className: "frame-link-filename",
+ }, displaySource));
+
+ // If we have a line number > 0.
+ if (line) {
+ let lineInfo = `:${line}`;
+ // Add `data-line` attribute for testing
+ attributes["data-line"] = line;
+
+ // Intentionally exclude 0
+ if (column) {
+ lineInfo += `:${column}`;
+ // Add `data-column` attribute for testing
+ attributes["data-column"] = column;
+ }
+
+ sourceElements.push(dom.span({ className: "frame-link-line" }, lineInfo));
+ }
+
+ // Inner el is useful for achieving ellipsis on the left and correct LTR/RTL
+ // ordering. See CSS styles for frame-link-source-[inner] and bug 1290056.
+ let sourceInnerEl = dom.span({
+ className: "frame-link-source-inner",
+ title: isLinkable ?
+ l10n.getFormatStr("frame.viewsourceindebugger", tooltip) : tooltip,
+ }, sourceElements);
+
+ // If source is not a URL (self-hosted, eval, etc.), don't make
+ // it an anchor link, as we can't link to it.
+ if (isLinkable) {
+ sourceEl = dom.a({
+ onClick: e => {
+ e.preventDefault();
+ onClick(this.getSource(frame));
+ },
+ href: source,
+ className: "frame-link-source",
+ draggable: false,
+ }, sourceInnerEl);
+ } else {
+ sourceEl = dom.span({
+ className: "frame-link-source",
+ }, sourceInnerEl);
+ }
+ elements.push(sourceEl);
+
+ if (showHost && host) {
+ elements.push(" ", dom.span({ className: "frame-link-host" }, host));
+ }
+
+ return dom.span(attributes, ...elements);
+ }
+});
diff --git a/devtools/client/shared/components/h-split-box.js b/devtools/client/shared/components/h-split-box.js
new file mode 100644
index 000000000..d804a6ba1
--- /dev/null
+++ b/devtools/client/shared/components/h-split-box.js
@@ -0,0 +1,154 @@
+/* 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/. */
+
+/* eslint-env browser */
+"use strict";
+
+// A box with a start and a end pane, separated by a dragable splitter that
+// allows the user to resize the relative widths of the panes.
+//
+// +-----------------------+---------------------+
+// | | |
+// | | |
+// | S |
+// | Start Pane p End Pane |
+// | l |
+// | i |
+// | t |
+// | t |
+// | e |
+// | r |
+// | | |
+// | | |
+// +-----------------------+---------------------+
+
+const {
+ DOM: dom,
+ createClass,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { assert } = require("devtools/shared/DevToolsUtils");
+
+module.exports = createClass({
+ displayName: "HSplitBox",
+
+ propTypes: {
+ // The contents of the start pane.
+ start: PropTypes.any.isRequired,
+
+ // The contents of the end pane.
+ end: PropTypes.any.isRequired,
+
+ // The relative width of the start pane, expressed as a number between 0 and
+ // 1. The relative width of the end pane is 1 - startWidth. For example,
+ // with startWidth = .5, both panes are of equal width; with startWidth =
+ // .25, the start panel will take up 1/4 width and the end panel will take
+ // up 3/4 width.
+ startWidth: PropTypes.number,
+
+ // A minimum css width value for the start and end panes.
+ minStartWidth: PropTypes.any,
+ minEndWidth: PropTypes.any,
+
+ // A callback fired when the user drags the splitter to resize the relative
+ // pane widths. The function is passed the startWidth value that would put
+ // the splitter underneath the users mouse.
+ onResize: PropTypes.func.isRequired,
+ },
+
+ getDefaultProps() {
+ return {
+ startWidth: 0.5,
+ minStartWidth: "20px",
+ minEndWidth: "20px",
+ };
+ },
+
+ getInitialState() {
+ return {
+ mouseDown: false
+ };
+ },
+
+ componentDidMount() {
+ document.defaultView.top.addEventListener("mouseup", this._onMouseUp,
+ false);
+ document.defaultView.top.addEventListener("mousemove", this._onMouseMove,
+ false);
+ },
+
+ componentWillUnmount() {
+ document.defaultView.top.removeEventListener("mouseup", this._onMouseUp,
+ false);
+ document.defaultView.top.removeEventListener("mousemove", this._onMouseMove,
+ false);
+ },
+
+ _onMouseDown(event) {
+ if (event.button !== 0) {
+ return;
+ }
+
+ this.setState({ mouseDown: true });
+ event.preventDefault();
+ },
+
+ _onMouseUp(event) {
+ if (event.button !== 0 || !this.state.mouseDown) {
+ return;
+ }
+
+ this.setState({ mouseDown: false });
+ event.preventDefault();
+ },
+
+ _onMouseMove(event) {
+ if (!this.state.mouseDown) {
+ return;
+ }
+
+ const rect = this.refs.box.getBoundingClientRect();
+ const { left, right } = rect;
+ const width = right - left;
+ const relative = event.clientX - left;
+ this.props.onResize(relative / width);
+
+ event.preventDefault();
+ },
+
+ render() {
+ /* eslint-disable no-shadow */
+ const { start, end, startWidth, minStartWidth, minEndWidth } = this.props;
+ assert(startWidth => 0 && startWidth <= 1,
+ "0 <= this.props.startWidth <= 1");
+ /* eslint-enable */
+ return dom.div(
+ {
+ className: "h-split-box",
+ ref: "box",
+ },
+
+ dom.div(
+ {
+ className: "h-split-box-pane",
+ style: { flex: startWidth, minWidth: minStartWidth },
+ },
+ start
+ ),
+
+ dom.div({
+ className: "devtools-side-splitter",
+ onMouseDown: this._onMouseDown,
+ }),
+
+ dom.div(
+ {
+ className: "h-split-box-pane",
+ style: { flex: 1 - startWidth, minWidth: minEndWidth },
+ },
+ end
+ )
+ );
+ }
+});
diff --git a/devtools/client/shared/components/moz.build b/devtools/client/shared/components/moz.build
new file mode 100644
index 000000000..0d67e90b5
--- /dev/null
+++ b/devtools/client/shared/components/moz.build
@@ -0,0 +1,27 @@
+# -*- 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 += [
+ 'reps',
+ 'splitter',
+ 'tabs',
+ 'tree'
+]
+
+DevToolsModules(
+ 'frame.js',
+ 'h-split-box.js',
+ 'notification-box.css',
+ 'notification-box.js',
+ 'search-box.js',
+ 'sidebar-toggle.css',
+ 'sidebar-toggle.js',
+ 'stack-trace.js',
+ 'tree.js',
+)
+
+MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
diff --git a/devtools/client/shared/components/notification-box.css b/devtools/client/shared/components/notification-box.css
new file mode 100644
index 000000000..83c29b616
--- /dev/null
+++ b/devtools/client/shared/components/notification-box.css
@@ -0,0 +1,95 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* Layout */
+
+.notificationbox .notificationInner {
+ display: flex;
+ flex-direction: row;
+}
+
+.notificationbox .details {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.notificationbox .notification-button {
+ text-align: right;
+}
+
+.notificationbox .messageText {
+ flex-grow: 1;
+}
+
+.notificationbox .details:-moz-dir(rtl)
+.notificationbox .notificationInner:-moz-dir(rtl) {
+ flex-direction: row-reverse;
+}
+
+/* Style */
+
+.notificationbox .notification {
+ background-color: InfoBackground;
+ text-shadow: none;
+ border-top: 1px solid ThreeDShadow;
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+.notificationbox .notification[data-type="info"] {
+ color: -moz-DialogText;
+ background-color: -moz-Dialog;
+}
+
+.notificationbox .notification[data-type="critical"] {
+ color: white;
+ background-image: linear-gradient(rgb(212,0,0), rgb(152,0,0));
+}
+
+.notificationbox .messageImage {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ margin: 6px;
+}
+
+/* Default icons for notifications */
+
+.notificationbox .messageImage[data-type="info"] {
+ background-image: url("chrome://global/skin/icons/information-16.png");
+}
+
+.notificationbox .messageImage[data-type="warning"] {
+ background-image: url("chrome://global/skin/icons/warning-16.png");
+}
+
+.notificationbox .messageImage[data-type="critical"] {
+ background-image: url("chrome://global/skin/icons/error-16.png");
+}
+
+/* Close button */
+
+.notificationbox .messageCloseButton {
+ width: 20px;
+ height: 20px;
+ margin: 4px;
+ margin-inline-end: 8px;
+ background-image: url("chrome://devtools/skin/images/close.svg");
+ background-position: center;
+ background-color: transparent;
+ background-repeat: no-repeat;
+ border-radius: 11px;
+ filter: invert(0);
+}
+
+.notificationbox .messageCloseButton:hover {
+ background-color: gray;
+ filter: invert(1);
+}
+
+.notificationbox .messageCloseButton:active {
+ background-color: rgba(170, 170, 170, .4); /* --toolbar-tab-hover-active */
+}
diff --git a/devtools/client/shared/components/notification-box.js b/devtools/client/shared/components/notification-box.js
new file mode 100644
index 000000000..87fc76cd6
--- /dev/null
+++ b/devtools/client/shared/components/notification-box.js
@@ -0,0 +1,263 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const Immutable = require("devtools/client/shared/vendor/immutable");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
+
+// Shortcuts
+const { PropTypes, createClass, DOM } = React;
+const { div, span, button } = DOM;
+
+// Priority Levels
+const PriorityLevels = {
+ PRIORITY_INFO_LOW: 1,
+ PRIORITY_INFO_MEDIUM: 2,
+ PRIORITY_INFO_HIGH: 3,
+ PRIORITY_WARNING_LOW: 4,
+ PRIORITY_WARNING_MEDIUM: 5,
+ PRIORITY_WARNING_HIGH: 6,
+ PRIORITY_CRITICAL_LOW: 7,
+ PRIORITY_CRITICAL_MEDIUM: 8,
+ PRIORITY_CRITICAL_HIGH: 9,
+ PRIORITY_CRITICAL_BLOCK: 10,
+};
+
+/**
+ * This component represents Notification Box - HTML alternative for
+ * <xul:notifictionbox> binding.
+ *
+ * See also MDN for more info about <xul:notificationbox>:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/notificationbox
+ */
+var NotificationBox = createClass({
+ displayName: "NotificationBox",
+
+ propTypes: {
+ // List of notifications appended into the box.
+ notifications: PropTypes.arrayOf(PropTypes.shape({
+ // label to appear on the notification.
+ label: PropTypes.string.isRequired,
+
+ // Value used to identify the notification
+ value: PropTypes.string.isRequired,
+
+ // URL of image to appear on the notification. If "" then an icon
+ // appropriate for the priority level is used.
+ image: PropTypes.string.isRequired,
+
+ // Notification priority; see Priority Levels.
+ priority: PropTypes.number.isRequired,
+
+ // Array of button descriptions to appear on the notification.
+ buttons: PropTypes.arrayOf(PropTypes.shape({
+ // Function to be called when the button is activated.
+ // This function is passed three arguments:
+ // 1) the NotificationBox component the button is associated with
+ // 2) the button description as passed to appendNotification.
+ // 3) the element which was the target of the button press event.
+ // If the return value from this function is not True, then the
+ // notification is closed. The notification is also not closed
+ // if an error is thrown.
+ callback: PropTypes.func.isRequired,
+
+ // The label to appear on the button.
+ label: PropTypes.string.isRequired,
+
+ // The accesskey attribute set on the <button> element.
+ accesskey: PropTypes.string,
+ })),
+
+ // A function to call to notify you of interesting things that happen
+ // with the notification box.
+ eventCallback: PropTypes.func,
+ })),
+
+ // Message that should be shown when hovering over the close button
+ closeButtonTooltip: PropTypes.string
+ },
+
+ getDefaultProps() {
+ return {
+ closeButtonTooltip: l10n.getStr("notificationBox.closeTooltip")
+ };
+ },
+
+ getInitialState() {
+ return {
+ notifications: new Immutable.OrderedMap()
+ };
+ },
+
+ /**
+ * Create a new notification and display it. If another notification is
+ * already present with a higher priority, the new notification will be
+ * added behind it. See `propTypes` for arguments description.
+ */
+ appendNotification(label, value, image, priority, buttons = [],
+ eventCallback) {
+ // Priority level must be within expected interval
+ // (see priority levels at the top of this file).
+ if (priority < PriorityLevels.PRIORITY_INFO_LOW ||
+ priority > PriorityLevels.PRIORITY_CRITICAL_BLOCK) {
+ throw new Error("Invalid notification priority " + priority);
+ }
+
+ // Custom image URL is not supported yet.
+ if (image) {
+ throw new Error("Custom image URL is not supported yet");
+ }
+
+ let type = "warning";
+ if (priority >= PriorityLevels.PRIORITY_CRITICAL_LOW) {
+ type = "critical";
+ } else if (priority <= PriorityLevels.PRIORITY_INFO_HIGH) {
+ type = "info";
+ }
+
+ let notifications = this.state.notifications.set(value, {
+ label: label,
+ value: value,
+ image: image,
+ priority: priority,
+ type: type,
+ buttons: buttons,
+ eventCallback: eventCallback,
+ });
+
+ // High priorities must be on top.
+ notifications = notifications.sortBy((val, key) => {
+ return -val.priority;
+ });
+
+ this.setState({
+ notifications: notifications
+ });
+ },
+
+ /**
+ * Remove specific notification from the list.
+ */
+ removeNotification(notification) {
+ this.close(this.state.notifications.get(notification.value));
+ },
+
+ /**
+ * Returns an object that represents a notification. It can be
+ * used to close it.
+ */
+ getNotificationWithValue(value) {
+ let notification = this.state.notifications.get(value);
+ if (!notification) {
+ return null;
+ }
+
+ // Return an object that can be used to remove the notification
+ // later (using `removeNotification` method) or directly close it.
+ return Object.assign({}, notification, {
+ close: () => {
+ this.close(notification);
+ }
+ });
+ },
+
+ getCurrentNotification() {
+ return this.state.notifications.first();
+ },
+
+ /**
+ * Close specified notification.
+ */
+ close(notification) {
+ if (!notification) {
+ return;
+ }
+
+ if (notification.eventCallback) {
+ notification.eventCallback("removed");
+ }
+
+ this.setState({
+ notifications: this.state.notifications.remove(notification.value)
+ });
+ },
+
+ /**
+ * Render a button. A notification can have a set of custom buttons.
+ * These are used to execute custom callback.
+ */
+ renderButton(props, notification) {
+ let onClick = event => {
+ if (props.callback) {
+ let result = props.callback(this, props, event.target);
+ if (!result) {
+ this.close(notification);
+ }
+ event.stopPropagation();
+ }
+ };
+
+ return (
+ button({
+ key: props.label,
+ className: "notification-button",
+ accesskey: props.accesskey,
+ onClick: onClick},
+ props.label
+ )
+ );
+ },
+
+ /**
+ * Render a notification.
+ */
+ renderNotification(notification) {
+ return (
+ div({
+ key: notification.value,
+ className: "notification",
+ "data-type": notification.type},
+ div({className: "notificationInner"},
+ div({className: "details"},
+ div({
+ className: "messageImage",
+ "data-type": notification.type}),
+ span({className: "messageText"},
+ notification.label
+ ),
+ notification.buttons.map(props =>
+ this.renderButton(props, notification)
+ )
+ ),
+ div({
+ className: "messageCloseButton",
+ title: this.props.closeButtonTooltip,
+ onClick: this.close.bind(this, notification)}
+ )
+ )
+ )
+ );
+ },
+
+ /**
+ * Render the top (highest priority) notification. Only one
+ * notification is rendered at a time.
+ */
+ render() {
+ let notification = this.state.notifications.first();
+ let content = notification ?
+ this.renderNotification(notification) :
+ null;
+
+ return div({className: "notificationbox"},
+ content
+ );
+ },
+});
+
+module.exports.NotificationBox = NotificationBox;
+module.exports.PriorityLevels = PriorityLevels;
diff --git a/devtools/client/shared/components/reps/array.js b/devtools/client/shared/components/reps/array.js
new file mode 100644
index 000000000..8ec1443e1
--- /dev/null
+++ b/devtools/client/shared/components/reps/array.js
@@ -0,0 +1,186 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+
+ // Shortcuts
+ const DOM = React.DOM;
+
+ /**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+ let ArrayRep = React.createClass({
+ displayName: "ArrayRep",
+
+ getTitle: function (object, context) {
+ return "[" + object.length + "]";
+ },
+
+ arrayIterator: function (array, max) {
+ let items = [];
+ let delim;
+
+ for (let i = 0; i < array.length && i < max; i++) {
+ try {
+ let value = array[i];
+
+ delim = (i == array.length - 1 ? "" : ", ");
+
+ items.push(ItemRep({
+ object: value,
+ // Hardcode tiny mode to avoid recursive handling.
+ mode: "tiny",
+ delim: delim
+ }));
+ } catch (exc) {
+ items.push(ItemRep({
+ object: exc,
+ mode: "tiny",
+ delim: delim
+ }));
+ }
+ }
+
+ if (array.length > max) {
+ let objectLink = this.props.objectLink || DOM.span;
+ items.push(Caption({
+ object: objectLink({
+ object: this.props.object
+ }, (array.length - max) + " more…")
+ }));
+ }
+
+ return items;
+ },
+
+ /**
+ * Returns true if the passed object is an array with additional (custom)
+ * properties, otherwise returns false. Custom properties should be
+ * displayed in extra expandable section.
+ *
+ * Example array with a custom property.
+ * let arr = [0, 1];
+ * arr.myProp = "Hello";
+ *
+ * @param {Array} array The array object.
+ */
+ hasSpecialProperties: function (array) {
+ function isInteger(x) {
+ let y = parseInt(x, 10);
+ if (isNaN(y)) {
+ return false;
+ }
+ return x === y.toString();
+ }
+
+ let props = Object.getOwnPropertyNames(array);
+ for (let i = 0; i < props.length; i++) {
+ let p = props[i];
+
+ // Valid indexes are skipped
+ if (isInteger(p)) {
+ continue;
+ }
+
+ // Ignore standard 'length' property, anything else is custom.
+ if (p != "length") {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ // Event Handlers
+
+ onToggleProperties: function (event) {
+ },
+
+ onClickBracket: function (event) {
+ },
+
+ render: function () {
+ let mode = this.props.mode || "short";
+ let object = this.props.object;
+ let items;
+ let brackets;
+ let needSpace = function (space) {
+ return space ? { left: "[ ", right: " ]"} : { left: "[", right: "]"};
+ };
+
+ if (mode == "tiny") {
+ let isEmpty = object.length === 0;
+ items = [DOM.span({className: "length"}, isEmpty ? "" : object.length)];
+ brackets = needSpace(false);
+ } else {
+ let max = (mode == "short") ? 3 : 300;
+ items = this.arrayIterator(object, max);
+ brackets = needSpace(items.length > 0);
+ }
+
+ let objectLink = this.props.objectLink || DOM.span;
+
+ return (
+ DOM.span({
+ className: "objectBox objectBox-array"},
+ objectLink({
+ className: "arrayLeftBracket",
+ object: object
+ }, brackets.left),
+ ...items,
+ objectLink({
+ className: "arrayRightBracket",
+ object: object
+ }, brackets.right),
+ DOM.span({
+ className: "arrayProperties",
+ role: "group"}
+ )
+ )
+ );
+ },
+ });
+
+ /**
+ * Renders array item. Individual values are separated by a comma.
+ */
+ let ItemRep = React.createFactory(React.createClass({
+ displayName: "ItemRep",
+
+ render: function () {
+ const { Rep } = createFactories(require("./rep"));
+
+ let object = this.props.object;
+ let delim = this.props.delim;
+ let mode = this.props.mode;
+ return (
+ DOM.span({},
+ Rep({object: object, mode: mode}),
+ delim
+ )
+ );
+ }
+ }));
+
+ function supportsObject(object, type) {
+ return Array.isArray(object) ||
+ Object.prototype.toString.call(object) === "[object Arguments]";
+ }
+
+ // Exports from this module
+ exports.ArrayRep = {
+ rep: ArrayRep,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/attribute.js b/devtools/client/shared/components/reps/attribute.js
new file mode 100644
index 000000000..f57ed0380
--- /dev/null
+++ b/devtools/client/shared/components/reps/attribute.js
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { StringRep } = require("./string");
+
+ // Shortcuts
+ const { span } = React.DOM;
+ const { rep: StringRepFactory } = createFactories(StringRep);
+
+ /**
+ * Renders DOM attribute
+ */
+ let Attribute = React.createClass({
+ displayName: "Attr",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ return grip.preview.nodeName;
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ let value = grip.preview.value;
+ let objectLink = this.props.objectLink || span;
+
+ return (
+ objectLink({className: "objectLink-Attr"},
+ span({},
+ span({className: "attrTitle"},
+ this.getTitle(grip)
+ ),
+ span({className: "attrEqual"},
+ "="
+ ),
+ StringRepFactory({object: value})
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (type == "Attr" && grip.preview);
+ }
+
+ exports.Attribute = {
+ rep: Attribute,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/caption.js b/devtools/client/shared/components/reps/caption.js
new file mode 100644
index 000000000..7f00b01e8
--- /dev/null
+++ b/devtools/client/shared/components/reps/caption.js
@@ -0,0 +1,31 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const DOM = React.DOM;
+
+ /**
+ * Renders a caption. This template is used by other components
+ * that needs to distinguish between a simple text/value and a label.
+ */
+ const Caption = React.createClass({
+ displayName: "Caption",
+
+ render: function () {
+ return (
+ DOM.span({"className": "caption"}, this.props.object)
+ );
+ },
+ });
+
+ // Exports from this module
+ exports.Caption = Caption;
+});
diff --git a/devtools/client/shared/components/reps/comment-node.js b/devtools/client/shared/components/reps/comment-node.js
new file mode 100644
index 000000000..2c69c1414
--- /dev/null
+++ b/devtools/client/shared/components/reps/comment-node.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ const { isGrip, cropString, cropMultipleLines } = require("./rep-utils");
+
+ // Utils
+ const nodeConstants = require("devtools/shared/dom-node-constants");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders DOM comment node.
+ */
+ const CommentNode = React.createClass({
+ displayName: "CommentNode",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ },
+
+ render: function () {
+ let {object} = this.props;
+
+ let mode = this.props.mode || "short";
+
+ let {textContent} = object.preview;
+ if (mode === "tiny") {
+ textContent = cropMultipleLines(textContent, 30);
+ } else if (mode === "short") {
+ textContent = cropString(textContent, 50);
+ }
+
+ return span({className: "objectBox theme-comment"}, `<!-- ${textContent} -->`);
+ },
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return object.preview && object.preview.nodeType === nodeConstants.COMMENT_NODE;
+ }
+
+ // Exports from this module
+ exports.CommentNode = {
+ rep: CommentNode,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/date-time.js b/devtools/client/shared/components/reps/date-time.js
new file mode 100644
index 000000000..55dfb7d2d
--- /dev/null
+++ b/devtools/client/shared/components/reps/date-time.js
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Used to render JS built-in Date() object.
+ */
+ let DateTime = React.createClass({
+ displayName: "Date",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: grip
+ }, grip.class + " ");
+ }
+ return "";
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ let date;
+ try {
+ date = span({className: "objectBox"},
+ this.getTitle(grip),
+ span({className: "Date"},
+ new Date(grip.preview.timestamp).toISOString()
+ )
+ );
+ } catch (e) {
+ date = span({className: "objectBox"}, "Invalid Date");
+ }
+ return date;
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (type == "Date" && grip.preview);
+ }
+
+ // Exports from this module
+ exports.DateTime = {
+ rep: DateTime,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/document.js b/devtools/client/shared/components/reps/document.js
new file mode 100644
index 000000000..25e42609f
--- /dev/null
+++ b/devtools/client/shared/components/reps/document.js
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, getURLDisplayString } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders DOM document object.
+ */
+ let Document = React.createClass({
+ displayName: "Document",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getLocation: function (grip) {
+ let location = grip.preview.location;
+ return location ? getURLDisplayString(location) : "";
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, grip.class + " ")
+ );
+ }
+ return "";
+ },
+
+ getTooltip: function (doc) {
+ return doc.location.href;
+ },
+
+ render: function () {
+ let grip = this.props.object;
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(grip),
+ span({className: "objectPropValue"},
+ this.getLocation(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return (object.preview && type == "HTMLDocument");
+ }
+
+ // Exports from this module
+ exports.Document = {
+ rep: Document,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/element-node.js b/devtools/client/shared/components/reps/element-node.js
new file mode 100644
index 000000000..6315fb5b1
--- /dev/null
+++ b/devtools/client/shared/components/reps/element-node.js
@@ -0,0 +1,114 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ const { isGrip } = require("./rep-utils");
+
+ // Utils
+ const nodeConstants = require("devtools/shared/dom-node-constants");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders DOM element node.
+ */
+ const ElementNode = React.createClass({
+ displayName: "ElementNode",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ },
+
+ getElements: function (grip, mode) {
+ let {attributes, nodeName} = grip.preview;
+ const nodeNameElement = span({
+ className: "tag-name theme-fg-color3"
+ }, nodeName);
+
+ if (mode === "tiny") {
+ let elements = [nodeNameElement];
+ if (attributes.id) {
+ elements.push(
+ span({className: "attr-name theme-fg-color2"}, `#${attributes.id}`));
+ }
+ if (attributes.class) {
+ elements.push(
+ span({className: "attr-name theme-fg-color2"},
+ attributes.class
+ .replace(/(^\s+)|(\s+$)/g, "")
+ .split(" ")
+ .map(cls => `.${cls}`)
+ .join("")
+ )
+ );
+ }
+ return elements;
+ }
+ let attributeElements = Object.keys(attributes)
+ .sort(function getIdAndClassFirst(a1, a2) {
+ if ([a1, a2].includes("id")) {
+ return 3 * (a1 === "id" ? -1 : 1);
+ }
+ if ([a1, a2].includes("class")) {
+ return 2 * (a1 === "class" ? -1 : 1);
+ }
+
+ // `id` and `class` excepted,
+ // we want to keep the same order that in `attributes`.
+ return 0;
+ })
+ .reduce((arr, name, i, keys) => {
+ let value = attributes[name];
+ let attribute = span({},
+ span({className: "attr-name theme-fg-color2"}, `${name}`),
+ `="`,
+ span({className: "attr-value theme-fg-color6"}, `${value}`),
+ `"`
+ );
+
+ return arr.concat([" ", attribute]);
+ }, []);
+
+ return [
+ "<",
+ nodeNameElement,
+ ...attributeElements,
+ ">",
+ ];
+ },
+
+ render: function () {
+ let {object, mode} = this.props;
+ let elements = this.getElements(object, mode);
+ const baseElement = span({className: "objectBox"}, ...elements);
+
+ if (this.props.objectLink) {
+ return this.props.objectLink({object}, baseElement);
+ }
+ return baseElement;
+ },
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return object.preview && object.preview.nodeType === nodeConstants.ELEMENT_NODE;
+ }
+
+ // Exports from this module
+ exports.ElementNode = {
+ rep: ElementNode,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/event.js b/devtools/client/shared/components/reps/event.js
new file mode 100644
index 000000000..1d37e0150
--- /dev/null
+++ b/devtools/client/shared/components/reps/event.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { rep } = createFactories(require("./grip").Grip);
+
+ /**
+ * Renders DOM event objects.
+ */
+ let Event = React.createClass({
+ displayName: "event",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ render: function () {
+ // Use `Object.assign` to keep `this.props` without changes because:
+ // 1. JSON.stringify/JSON.parse is slow.
+ // 2. Immutable.js is planned for the future.
+ let props = Object.assign({}, this.props);
+ props.object = Object.assign({}, this.props.object);
+ props.object.preview = Object.assign({}, this.props.object.preview);
+ props.object.preview.ownProperties = props.object.preview.properties;
+ delete props.object.preview.properties;
+ props.object.ownPropertyLength =
+ Object.keys(props.object.preview.ownProperties).length;
+
+ switch (props.object.class) {
+ case "MouseEvent":
+ props.isInterestingProp = (type, value, name) => {
+ return (name == "clientX" ||
+ name == "clientY" ||
+ name == "layerX" ||
+ name == "layerY");
+ };
+ break;
+ case "KeyboardEvent":
+ props.isInterestingProp = (type, value, name) => {
+ return (name == "key" ||
+ name == "charCode" ||
+ name == "keyCode");
+ };
+ break;
+ case "MessageEvent":
+ props.isInterestingProp = (type, value, name) => {
+ return (name == "isTrusted" ||
+ name == "data");
+ };
+ break;
+ }
+ return rep(props);
+ }
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && grip.preview.kind == "DOMEvent");
+ }
+
+ // Exports from this module
+ exports.Event = {
+ rep: Event,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/function.js b/devtools/client/shared/components/reps/function.js
new file mode 100644
index 000000000..fd20dc318
--- /dev/null
+++ b/devtools/client/shared/components/reps/function.js
@@ -0,0 +1,73 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, cropString } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * This component represents a template for Function objects.
+ */
+ let Func = React.createClass({
+ displayName: "Func",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: grip
+ }, "function ");
+ }
+ return "";
+ },
+
+ summarizeFunction: function (grip) {
+ let name = grip.userDisplayName || grip.displayName || grip.name || "function";
+ return cropString(name + "()", 100);
+ },
+
+ render: function () {
+ let grip = this.props.object;
+
+ return (
+ // Set dir="ltr" to prevent function parentheses from
+ // appearing in the wrong direction
+ span({dir: "ltr", className: "objectBox objectBox-function"},
+ this.getTitle(grip),
+ this.summarizeFunction(grip)
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return (type == "function");
+ }
+
+ return (type == "Function");
+ }
+
+ // Exports from this module
+
+ exports.Func = {
+ rep: Func,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/grip-array.js b/devtools/client/shared/components/reps/grip-array.js
new file mode 100644
index 000000000..04a5603bb
--- /dev/null
+++ b/devtools/client/shared/components/reps/grip-array.js
@@ -0,0 +1,198 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+ let GripArray = React.createClass({
+ displayName: "GripArray",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ provider: React.PropTypes.object,
+ },
+
+ getLength: function (grip) {
+ if (!grip.preview) {
+ return 0;
+ }
+
+ return grip.preview.length || grip.preview.childNodesLength || 0;
+ },
+
+ getTitle: function (object, context) {
+ let objectLink = this.props.objectLink || span;
+ if (this.props.mode != "tiny") {
+ return objectLink({
+ object: object
+ }, object.class + " ");
+ }
+ return "";
+ },
+
+ getPreviewItems: function (grip) {
+ if (!grip.preview) {
+ return null;
+ }
+
+ return grip.preview.items || grip.preview.childNodes || null;
+ },
+
+ arrayIterator: function (grip, max) {
+ let items = [];
+ const gripLength = this.getLength(grip);
+
+ if (!gripLength) {
+ return items;
+ }
+
+ const previewItems = this.getPreviewItems(grip);
+ if (!previewItems) {
+ return items;
+ }
+
+ let delim;
+ // number of grip preview items is limited to 10, but we may have more
+ // items in grip-array.
+ let delimMax = gripLength > previewItems.length ?
+ previewItems.length : previewItems.length - 1;
+ let provider = this.props.provider;
+
+ for (let i = 0; i < previewItems.length && i < max; i++) {
+ try {
+ let itemGrip = previewItems[i];
+ let value = provider ? provider.getValue(itemGrip) : itemGrip;
+
+ delim = (i == delimMax ? "" : ", ");
+
+ items.push(GripArrayItem(Object.assign({}, this.props, {
+ object: value,
+ delim: delim
+ })));
+ } catch (exc) {
+ items.push(GripArrayItem(Object.assign({}, this.props, {
+ object: exc,
+ delim: delim
+ })));
+ }
+ }
+ if (previewItems.length > max || gripLength > previewItems.length) {
+ let objectLink = this.props.objectLink || span;
+ let leftItemNum = gripLength - max > 0 ?
+ gripLength - max : gripLength - previewItems.length;
+ items.push(Caption({
+ object: objectLink({
+ object: this.props.object
+ }, leftItemNum + " more…")
+ }));
+ }
+
+ return items;
+ },
+
+ render: function () {
+ let mode = this.props.mode || "short";
+ let object = this.props.object;
+
+ let items;
+ let brackets;
+ let needSpace = function (space) {
+ return space ? { left: "[ ", right: " ]"} : { left: "[", right: "]"};
+ };
+
+ if (mode == "tiny") {
+ let objectLength = this.getLength(object);
+ let isEmpty = objectLength === 0;
+ items = [span({className: "length"}, isEmpty ? "" : objectLength)];
+ brackets = needSpace(false);
+ } else {
+ let max = (mode == "short") ? 3 : 300;
+ items = this.arrayIterator(object, max);
+ brackets = needSpace(items.length > 0);
+ }
+
+ let objectLink = this.props.objectLink || span;
+ let title = this.getTitle(object);
+
+ return (
+ span({
+ className: "objectBox objectBox-array"},
+ title,
+ objectLink({
+ className: "arrayLeftBracket",
+ object: object
+ }, brackets.left),
+ ...items,
+ objectLink({
+ className: "arrayRightBracket",
+ object: object
+ }, brackets.right),
+ span({
+ className: "arrayProperties",
+ role: "group"}
+ )
+ )
+ );
+ },
+ });
+
+ /**
+ * Renders array item. Individual values are separated by
+ * a delimiter (a comma by default).
+ */
+ let GripArrayItem = React.createFactory(React.createClass({
+ displayName: "GripArrayItem",
+
+ propTypes: {
+ delim: React.PropTypes.string,
+ },
+
+ render: function () {
+ let { Rep } = createFactories(require("./rep"));
+
+ return (
+ span({},
+ Rep(Object.assign({}, this.props, {
+ mode: "tiny"
+ })),
+ this.props.delim
+ )
+ );
+ }
+ }));
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && (
+ grip.preview.kind == "ArrayLike" ||
+ type === "DocumentFragment"
+ )
+ );
+ }
+
+ // Exports from this module
+ exports.GripArray = {
+ rep: GripArray,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/grip-map.js b/devtools/client/shared/components/reps/grip-map.js
new file mode 100644
index 000000000..df673d005
--- /dev/null
+++ b/devtools/client/shared/components/reps/grip-map.js
@@ -0,0 +1,193 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+ const { PropRep } = createFactories(require("./prop-rep"));
+
+ // Shortcuts
+ const { span } = React.DOM;
+ /**
+ * Renders an map. A map is represented by a list of its
+ * entries enclosed in curly brackets.
+ */
+ const GripMap = React.createClass({
+ displayName: "GripMap",
+
+ propTypes: {
+ object: React.PropTypes.object,
+ mode: React.PropTypes.string,
+ },
+
+ getTitle: function (object) {
+ let title = object && object.class ? object.class : "Map";
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, title);
+ }
+ return title;
+ },
+
+ safeEntriesIterator: function (object, max) {
+ max = (typeof max === "undefined") ? 3 : max;
+ try {
+ return this.entriesIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ entriesIterator: function (object, max) {
+ // Entry filter. Show only interesting entries to the user.
+ let isInterestingEntry = this.props.isInterestingEntry || ((type, value) => {
+ return (
+ type == "boolean" ||
+ type == "number" ||
+ (type == "string" && value.length != 0)
+ );
+ });
+
+ let mapEntries = object.preview && object.preview.entries
+ ? object.preview.entries : [];
+
+ let indexes = this.getEntriesIndexes(mapEntries, max, isInterestingEntry);
+ if (indexes.length < max && indexes.length < mapEntries.length) {
+ // There are not enough entries yet, so we add uninteresting entries.
+ indexes = indexes.concat(
+ this.getEntriesIndexes(mapEntries, max - indexes.length, (t, value, name) => {
+ return !isInterestingEntry(t, value, name);
+ })
+ );
+ }
+
+ let entries = this.getEntries(mapEntries, indexes);
+ if (entries.length < mapEntries.length) {
+ // There are some undisplayed entries. Then display "more…".
+ let objectLink = this.props.objectLink || span;
+
+ entries.push(Caption({
+ key: "more",
+ object: objectLink({
+ object: object
+ }, `${mapEntries.length - max} more…`)
+ }));
+ }
+
+ return entries;
+ },
+
+ /**
+ * Get entries ordered by index.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Array} indexes Indexes of entries.
+ * @return {Array} Array of PropRep.
+ */
+ getEntries: function (entries, indexes) {
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ return indexes.map((index, i) => {
+ let [key, entryValue] = entries[index];
+ let value = entryValue.value !== undefined ? entryValue.value : entryValue;
+
+ return PropRep({
+ // key,
+ name: key,
+ equal: ": ",
+ object: value,
+ // Do not add a trailing comma on the last entry
+ // if there won't be a "more..." item.
+ delim: (i < indexes.length - 1 || indexes.length < entries.length) ? ", " : "",
+ mode: "tiny",
+ objectLink: this.props.objectLink,
+ });
+ });
+ },
+
+ /**
+ * Get the indexes of entries in the map.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the entry you want.
+ * @return {Array} Indexes of filtered entries in the map.
+ */
+ getEntriesIndexes: function (entries, max, filter) {
+ return entries
+ .reduce((indexes, [key, entry], i) => {
+ if (indexes.length < max) {
+ let value = (entry && entry.value !== undefined) ? entry.value : entry;
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ let type = (value && value.class ? value.class : typeof value).toLowerCase();
+
+ if (filter(type, value, key)) {
+ indexes.push(i);
+ }
+ }
+
+ return indexes;
+ }, []);
+ },
+
+ render: function () {
+ let object = this.props.object;
+ let props = this.safeEntriesIterator(object,
+ (this.props.mode == "long") ? 100 : 3);
+
+ let objectLink = this.props.objectLink || span;
+ if (this.props.mode == "tiny") {
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, "")
+ )
+ );
+ }
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+ return (grip.preview && grip.preview.kind == "MapLike");
+ }
+
+ // Exports from this module
+ exports.GripMap = {
+ rep: GripMap,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/grip.js b/devtools/client/shared/components/reps/grip.js
new file mode 100644
index 000000000..c63ee19f3
--- /dev/null
+++ b/devtools/client/shared/components/reps/grip.js
@@ -0,0 +1,247 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ // Dependencies
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+ const { PropRep } = createFactories(require("./prop-rep"));
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders generic grip. Grip is client representation
+ * of remote JS object and is used as an input object
+ * for this rep component.
+ */
+ const GripRep = React.createClass({
+ displayName: "Grip",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ isInterestingProp: React.PropTypes.func
+ },
+
+ getTitle: function (object) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, object.class);
+ }
+ return object.class || "Object";
+ },
+
+ safePropIterator: function (object, max) {
+ max = (typeof max === "undefined") ? 3 : max;
+ try {
+ return this.propIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ propIterator: function (object, max) {
+ if (object.preview && Object.keys(object.preview).includes("wrappedValue")) {
+ const { Rep } = createFactories(require("./rep"));
+
+ return [Rep({
+ object: object.preview.wrappedValue,
+ mode: this.props.mode || "tiny",
+ defaultRep: Grip,
+ })];
+ }
+
+ // Property filter. Show only interesting properties to the user.
+ let isInterestingProp = this.props.isInterestingProp || ((type, value) => {
+ return (
+ type == "boolean" ||
+ type == "number" ||
+ (type == "string" && value.length != 0)
+ );
+ });
+
+ let properties = object.preview
+ ? object.preview.ownProperties
+ : {};
+ let propertiesLength = object.preview && object.preview.ownPropertiesLength
+ ? object.preview.ownPropertiesLength
+ : object.ownPropertyLength;
+
+ if (object.preview && object.preview.safeGetterValues) {
+ properties = Object.assign({}, properties, object.preview.safeGetterValues);
+ propertiesLength += Object.keys(object.preview.safeGetterValues).length;
+ }
+
+ let indexes = this.getPropIndexes(properties, max, isInterestingProp);
+ if (indexes.length < max && indexes.length < propertiesLength) {
+ // There are not enough props yet. Then add uninteresting props to display them.
+ indexes = indexes.concat(
+ this.getPropIndexes(properties, max - indexes.length, (t, value, name) => {
+ return !isInterestingProp(t, value, name);
+ })
+ );
+ }
+
+ const truncate = Object.keys(properties).length > max;
+ let props = this.getProps(properties, indexes, truncate);
+ if (truncate) {
+ // There are some undisplayed props. Then display "more...".
+ let objectLink = this.props.objectLink || span;
+
+ props.push(Caption({
+ object: objectLink({
+ object: object
+ }, `${object.ownPropertyLength - max} more…`)
+ }));
+ }
+
+ return props;
+ },
+
+ /**
+ * Get props ordered by index.
+ *
+ * @param {Object} properties Props object.
+ * @param {Array} indexes Indexes of props.
+ * @param {Boolean} truncate true if the grip will be truncated.
+ * @return {Array} Props.
+ */
+ getProps: function (properties, indexes, truncate) {
+ let props = [];
+
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ indexes.forEach((i) => {
+ let name = Object.keys(properties)[i];
+ let value = this.getPropValue(properties[name]);
+
+ props.push(PropRep(Object.assign({}, this.props, {
+ mode: "tiny",
+ name: name,
+ object: value,
+ equal: ": ",
+ delim: i !== indexes.length - 1 || truncate ? ", " : "",
+ defaultRep: Grip
+ })));
+ });
+
+ return props;
+ },
+
+ /**
+ * Get the indexes of props in the object.
+ *
+ * @param {Object} properties Props object.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the props you want.
+ * @return {Array} Indexes of interesting props in the object.
+ */
+ getPropIndexes: function (properties, max, filter) {
+ let indexes = [];
+
+ try {
+ let i = 0;
+ for (let name in properties) {
+ if (indexes.length >= max) {
+ return indexes;
+ }
+
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ let value = this.getPropValue(properties[name]);
+ let type = (value.class || typeof value);
+ type = type.toLowerCase();
+
+ if (filter(type, value, name)) {
+ indexes.push(i);
+ }
+ i++;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ return indexes;
+ },
+
+ /**
+ * Get the actual value of a property.
+ *
+ * @param {Object} property
+ * @return {Object} Value of the property.
+ */
+ getPropValue: function (property) {
+ let value = property;
+ if (typeof property === "object") {
+ let keys = Object.keys(property);
+ if (keys.includes("value")) {
+ value = property.value;
+ } else if (keys.includes("getterValue")) {
+ value = property.getterValue;
+ }
+ }
+ return value;
+ },
+
+ render: function () {
+ let object = this.props.object;
+ let props = this.safePropIterator(object,
+ (this.props.mode == "long") ? 100 : 3);
+
+ let objectLink = this.props.objectLink || span;
+ if (this.props.mode == "tiny") {
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, "")
+ )
+ );
+ }
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ ...props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return (object.preview && object.preview.ownProperties);
+ }
+
+ let Grip = {
+ rep: GripRep,
+ supportsObject: supportsObject
+ };
+
+ // Exports from this module
+ exports.Grip = Grip;
+});
diff --git a/devtools/client/shared/components/reps/infinity.js b/devtools/client/shared/components/reps/infinity.js
new file mode 100644
index 000000000..604e31f06
--- /dev/null
+++ b/devtools/client/shared/components/reps/infinity.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a Infinity object
+ */
+ const InfinityRep = React.createClass({
+ displayName: "Infinity",
+
+ render: function () {
+ return (
+ span({className: "objectBox objectBox-number"},
+ this.props.object.type
+ )
+ );
+ }
+ });
+
+ function supportsObject(object, type) {
+ return type == "Infinity" || type == "-Infinity";
+ }
+
+ // Exports from this module
+ exports.InfinityRep = {
+ rep: InfinityRep,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/long-string.js b/devtools/client/shared/components/reps/long-string.js
new file mode 100644
index 000000000..f19f020dc
--- /dev/null
+++ b/devtools/client/shared/components/reps/long-string.js
@@ -0,0 +1,71 @@
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { sanitizeString, isGrip } = require("./rep-utils");
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a long string grip.
+ */
+ const LongStringRep = React.createClass({
+ displayName: "LongStringRep",
+
+ propTypes: {
+ useQuotes: React.PropTypes.bool,
+ style: React.PropTypes.object,
+ },
+
+ getDefaultProps: function () {
+ return {
+ useQuotes: true,
+ };
+ },
+
+ render: function () {
+ let {
+ cropLimit,
+ member,
+ object,
+ style,
+ useQuotes
+ } = this.props;
+ let {fullText, initial, length} = object;
+
+ let config = {className: "objectBox objectBox-string"};
+ if (style) {
+ config.style = style;
+ }
+
+ let string = member && member.open
+ ? fullText || initial
+ : initial.substring(0, cropLimit);
+
+ if (string.length < length) {
+ string += "\u2026";
+ }
+ let formattedString = useQuotes ? `"${string}"` : string;
+ return span(config, sanitizeString(formattedString));
+ },
+ });
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return object.type === "longString";
+ }
+
+ // Exports from this module
+ exports.LongStringRep = {
+ rep: LongStringRep,
+ supportsObject: supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/moz.build b/devtools/client/shared/components/reps/moz.build
new file mode 100644
index 000000000..f5df589f7
--- /dev/null
+++ b/devtools/client/shared/components/reps/moz.build
@@ -0,0 +1,40 @@
+# -*- 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/.
+
+DevToolsModules(
+ 'array.js',
+ 'attribute.js',
+ 'caption.js',
+ 'comment-node.js',
+ 'date-time.js',
+ 'document.js',
+ 'element-node.js',
+ 'event.js',
+ 'function.js',
+ 'grip-array.js',
+ 'grip-map.js',
+ 'grip.js',
+ 'infinity.js',
+ 'long-string.js',
+ 'nan.js',
+ 'null.js',
+ 'number.js',
+ 'object-with-text.js',
+ 'object-with-url.js',
+ 'object.js',
+ 'promise.js',
+ 'prop-rep.js',
+ 'regexp.js',
+ 'rep-utils.js',
+ 'rep.js',
+ 'reps.css',
+ 'string.js',
+ 'stylesheet.js',
+ 'symbol.js',
+ 'text-node.js',
+ 'undefined.js',
+ 'window.js',
+)
diff --git a/devtools/client/shared/components/reps/nan.js b/devtools/client/shared/components/reps/nan.js
new file mode 100644
index 000000000..b76a5cfd3
--- /dev/null
+++ b/devtools/client/shared/components/reps/nan.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a NaN object
+ */
+ const NaNRep = React.createClass({
+ displayName: "NaN",
+
+ render: function () {
+ return (
+ span({className: "objectBox objectBox-nan"},
+ "NaN"
+ )
+ );
+ }
+ });
+
+ function supportsObject(object, type) {
+ return type == "NaN";
+ }
+
+ // Exports from this module
+ exports.NaNRep = {
+ rep: NaNRep,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/null.js b/devtools/client/shared/components/reps/null.js
new file mode 100644
index 000000000..5de00f026
--- /dev/null
+++ b/devtools/client/shared/components/reps/null.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders null value
+ */
+ const Null = React.createClass({
+ displayName: "NullRep",
+
+ render: function () {
+ return (
+ span({className: "objectBox objectBox-null"},
+ "null"
+ )
+ );
+ },
+ });
+
+ function supportsObject(object, type) {
+ if (object && object.type && object.type == "null") {
+ return true;
+ }
+
+ return (object == null);
+ }
+
+ // Exports from this module
+
+ exports.Null = {
+ rep: Null,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/number.js b/devtools/client/shared/components/reps/number.js
new file mode 100644
index 000000000..31be3009b
--- /dev/null
+++ b/devtools/client/shared/components/reps/number.js
@@ -0,0 +1,51 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a number
+ */
+ const Number = React.createClass({
+ displayName: "Number",
+
+ stringify: function (object) {
+ let isNegativeZero = Object.is(object, -0) ||
+ (object.type && object.type == "-0");
+
+ return (isNegativeZero ? "-0" : String(object));
+ },
+
+ render: function () {
+ let value = this.props.object;
+
+ return (
+ span({className: "objectBox objectBox-number"},
+ this.stringify(value)
+ )
+ );
+ }
+ });
+
+ function supportsObject(object, type) {
+ return ["boolean", "number", "-0"].includes(type);
+ }
+
+ // Exports from this module
+
+ exports.Number = {
+ rep: Number,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/object-with-text.js b/devtools/client/shared/components/reps/object-with-text.js
new file mode 100644
index 000000000..85168ce78
--- /dev/null
+++ b/devtools/client/shared/components/reps/object-with-text.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a grip object with textual data.
+ */
+ let ObjectWithText = React.createClass({
+ displayName: "ObjectWithText",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, this.getType(grip) + " ")
+ );
+ }
+ return "";
+ },
+
+ getType: function (grip) {
+ return grip.class;
+ },
+
+ getDescription: function (grip) {
+ return "\"" + grip.preview.text + "\"";
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ return (
+ span({className: "objectBox objectBox-" + this.getType(grip)},
+ this.getTitle(grip),
+ span({className: "objectPropValue"},
+ this.getDescription(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && grip.preview.kind == "ObjectWithText");
+ }
+
+ // Exports from this module
+ exports.ObjectWithText = {
+ rep: ObjectWithText,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/object-with-url.js b/devtools/client/shared/components/reps/object-with-url.js
new file mode 100644
index 000000000..9c4b9a229
--- /dev/null
+++ b/devtools/client/shared/components/reps/object-with-url.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, getURLDisplayString } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a grip object with URL data.
+ */
+ let ObjectWithURL = React.createClass({
+ displayName: "ObjectWithURL",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, this.getType(grip) + " ")
+ );
+ }
+ return "";
+ },
+
+ getType: function (grip) {
+ return grip.class;
+ },
+
+ getDescription: function (grip) {
+ return getURLDisplayString(grip.preview.url);
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ return (
+ span({className: "objectBox objectBox-" + this.getType(grip)},
+ this.getTitle(grip),
+ span({className: "objectPropValue"},
+ this.getDescription(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && grip.preview.kind == "ObjectWithURL");
+ }
+
+ // Exports from this module
+ exports.ObjectWithURL = {
+ rep: ObjectWithURL,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/object.js b/devtools/client/shared/components/reps/object.js
new file mode 100644
index 000000000..ffb1d1525
--- /dev/null
+++ b/devtools/client/shared/components/reps/object.js
@@ -0,0 +1,171 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+ const { PropRep } = createFactories(require("./prop-rep"));
+ // Shortcuts
+ const { span } = React.DOM;
+ /**
+ * Renders an object. An object is represented by a list of its
+ * properties enclosed in curly brackets.
+ */
+ const Obj = React.createClass({
+ displayName: "Obj",
+
+ propTypes: {
+ object: React.PropTypes.object,
+ mode: React.PropTypes.string,
+ },
+
+ getTitle: function (object) {
+ let className = object && object.class ? object.class : "Object";
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, className);
+ }
+ return className;
+ },
+
+ safePropIterator: function (object, max) {
+ max = (typeof max === "undefined") ? 3 : max;
+ try {
+ return this.propIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ propIterator: function (object, max) {
+ let isInterestingProp = (t, value) => {
+ // Do not pick objects, it could cause recursion.
+ return (t == "boolean" || t == "number" || (t == "string" && value));
+ };
+
+ // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=945377
+ if (Object.prototype.toString.call(object) === "[object Generator]") {
+ object = Object.getPrototypeOf(object);
+ }
+
+ // Object members with non-empty values are preferred since it gives the
+ // user a better overview of the object.
+ let props = this.getProps(object, max, isInterestingProp);
+
+ if (props.length <= max) {
+ // There are not enough props yet (or at least, not enough props to
+ // be able to know whether we should print "more…" or not).
+ // Let's display also empty members and functions.
+ props = props.concat(this.getProps(object, max, (t, value) => {
+ return !isInterestingProp(t, value);
+ }));
+ }
+
+ if (props.length > max) {
+ props.pop();
+ let objectLink = this.props.objectLink || span;
+
+ props.push(Caption({
+ object: objectLink({
+ object: object
+ }, (Object.keys(object).length - max) + " more…")
+ }));
+ } else if (props.length > 0) {
+ // Remove the last comma.
+ props[props.length - 1] = React.cloneElement(
+ props[props.length - 1], { delim: "" });
+ }
+
+ return props;
+ },
+
+ getProps: function (object, max, filter) {
+ let props = [];
+
+ max = max || 3;
+ if (!object) {
+ return props;
+ }
+
+ // Hardcode tiny mode to avoid recursive handling.
+ let mode = "tiny";
+
+ try {
+ for (let name in object) {
+ if (props.length > max) {
+ return props;
+ }
+
+ let value;
+ try {
+ value = object[name];
+ } catch (exc) {
+ continue;
+ }
+
+ let t = typeof value;
+ if (filter(t, value)) {
+ props.push(PropRep({
+ mode: mode,
+ name: name,
+ object: value,
+ equal: ": ",
+ delim: ", ",
+ }));
+ }
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ return props;
+ },
+
+ render: function () {
+ let object = this.props.object;
+ let props = this.safePropIterator(object);
+ let objectLink = this.props.objectLink || span;
+
+ if (this.props.mode == "tiny" || !props.length) {
+ return (
+ span({className: "objectBox objectBox-object"},
+ objectLink({className: "objectTitle"}, this.getTitle(object))
+ )
+ );
+ }
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ ...props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+ function supportsObject(object, type) {
+ return true;
+ }
+
+ // Exports from this module
+ exports.Obj = {
+ rep: Obj,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/promise.js b/devtools/client/shared/components/reps/promise.js
new file mode 100644
index 000000000..0a903d366
--- /dev/null
+++ b/devtools/client/shared/components/reps/promise.js
@@ -0,0 +1,111 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ // Dependencies
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { PropRep } = createFactories(require("./prop-rep"));
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a DOM Promise object.
+ */
+ const PromiseRep = React.createClass({
+ displayName: "Promise",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ },
+
+ getTitle: function (object) {
+ const title = object.class;
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, title);
+ }
+ return title;
+ },
+
+ getProps: function (promiseState) {
+ const keys = ["state"];
+ if (Object.keys(promiseState).includes("value")) {
+ keys.push("value");
+ }
+
+ return keys.map((key, i) => {
+ return PropRep(Object.assign({}, this.props, {
+ mode: "tiny",
+ name: `<${key}>`,
+ object: promiseState[key],
+ equal: ": ",
+ delim: i < keys.length - 1 ? ", " : ""
+ }));
+ });
+ },
+
+ render: function () {
+ const object = this.props.object;
+ const {promiseState} = object;
+ let objectLink = this.props.objectLink || span;
+
+ if (this.props.mode == "tiny") {
+ let { Rep } = createFactories(require("./rep"));
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ Rep({object: promiseState.state}),
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ }
+
+ const props = this.getProps(promiseState);
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ ...props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return type === "Promise";
+ }
+
+ // Exports from this module
+ exports.PromiseRep = {
+ rep: PromiseRep,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/prop-rep.js b/devtools/client/shared/components/reps/prop-rep.js
new file mode 100644
index 000000000..775dfea2b
--- /dev/null
+++ b/devtools/client/shared/components/reps/prop-rep.js
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("./rep-utils");
+ const { span } = React.DOM;
+
+ /**
+ * Property for Obj (local JS objects), Grip (remote JS objects)
+ * and GripMap (remote JS maps and weakmaps) reps.
+ * It's used to render object properties.
+ */
+ let PropRep = React.createFactory(React.createClass({
+ displayName: "PropRep",
+
+ propTypes: {
+ // Property name.
+ name: React.PropTypes.oneOfType([
+ React.PropTypes.string,
+ React.PropTypes.object,
+ ]).isRequired,
+ // Equal character rendered between property name and value.
+ equal: React.PropTypes.string,
+ // Delimiter character used to separate individual properties.
+ delim: React.PropTypes.string,
+ mode: React.PropTypes.string,
+ },
+
+ render: function () {
+ const { Grip } = require("./grip");
+ let { Rep } = createFactories(require("./rep"));
+
+ let key;
+ // The key can be a simple string, for plain objects,
+ // or another object for maps and weakmaps.
+ if (typeof this.props.name === "string") {
+ key = span({"className": "nodeName"}, this.props.name);
+ } else {
+ key = Rep({
+ object: this.props.name,
+ mode: this.props.mode || "tiny",
+ defaultRep: Grip,
+ objectLink: this.props.objectLink,
+ });
+ }
+
+ return (
+ span({},
+ key,
+ span({
+ "className": "objectEqual"
+ }, this.props.equal),
+ Rep(this.props),
+ span({
+ "className": "objectComma"
+ }, this.props.delim)
+ )
+ );
+ }
+ }));
+
+ // Exports from this module
+ exports.PropRep = PropRep;
+});
diff --git a/devtools/client/shared/components/reps/regexp.js b/devtools/client/shared/components/reps/regexp.js
new file mode 100644
index 000000000..2f9212658
--- /dev/null
+++ b/devtools/client/shared/components/reps/regexp.js
@@ -0,0 +1,63 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a grip object with regular expression.
+ */
+ let RegExp = React.createClass({
+ displayName: "regexp",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getSource: function (grip) {
+ return grip.displayString;
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ let objectLink = this.props.objectLink || span;
+
+ return (
+ span({className: "objectBox objectBox-regexp"},
+ objectLink({
+ object: grip,
+ className: "regexpSource"
+ }, this.getSource(grip))
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return (type == "RegExp");
+ }
+
+ // Exports from this module
+ exports.RegExp = {
+ rep: RegExp,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/rep-utils.js b/devtools/client/shared/components/reps/rep-utils.js
new file mode 100644
index 000000000..d9580ac8d
--- /dev/null
+++ b/devtools/client/shared/components/reps/rep-utils.js
@@ -0,0 +1,160 @@
+/* globals URLSearchParams */
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ /**
+ * Create React factories for given arguments.
+ * Example:
+ * const { Rep } = createFactories(require("./rep"));
+ */
+ function createFactories(args) {
+ let result = {};
+ for (let p in args) {
+ result[p] = React.createFactory(args[p]);
+ }
+ return result;
+ }
+
+ /**
+ * Returns true if the given object is a grip (see RDP protocol)
+ */
+ function isGrip(object) {
+ return object && object.actor;
+ }
+
+ function escapeNewLines(value) {
+ return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n");
+ }
+
+ function cropMultipleLines(text, limit) {
+ return escapeNewLines(cropString(text, limit));
+ }
+
+ function cropString(text, limit, alternativeText) {
+ if (!alternativeText) {
+ alternativeText = "\u2026";
+ }
+
+ // Make sure it's a string and sanitize it.
+ text = sanitizeString(text + "");
+
+ // Crop the string only if a limit is actually specified.
+ if (!limit || limit <= 0) {
+ return text;
+ }
+
+ // Set the limit at least to the length of the alternative text
+ // plus one character of the original text.
+ if (limit <= alternativeText.length) {
+ limit = alternativeText.length + 1;
+ }
+
+ let halfLimit = (limit - alternativeText.length) / 2;
+
+ if (text.length > limit) {
+ return text.substr(0, Math.ceil(halfLimit)) + alternativeText +
+ text.substr(text.length - Math.floor(halfLimit));
+ }
+
+ return text;
+ }
+
+ function sanitizeString(text) {
+ // Replace all non-printable characters, except of
+ // (horizontal) tab (HT: \x09) and newline (LF: \x0A, CR: \x0D),
+ // with unicode replacement character (u+fffd).
+ // eslint-disable-next-line no-control-regex
+ let re = new RegExp("[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]", "g");
+ return text.replace(re, "\ufffd");
+ }
+
+ function parseURLParams(url) {
+ url = new URL(url);
+ return parseURLEncodedText(url.searchParams);
+ }
+
+ function parseURLEncodedText(text) {
+ let params = [];
+
+ // In case the text is empty just return the empty parameters
+ if (text == "") {
+ return params;
+ }
+
+ let searchParams = new URLSearchParams(text);
+ let entries = [...searchParams.entries()];
+ return entries.map(entry => {
+ return {
+ name: entry[0],
+ value: entry[1]
+ };
+ });
+ }
+
+ function getFileName(url) {
+ let split = splitURLBase(url);
+ return split.name;
+ }
+
+ function splitURLBase(url) {
+ if (!isDataURL(url)) {
+ return splitURLTrue(url);
+ }
+ return {};
+ }
+
+ function getURLDisplayString(url) {
+ return cropString(url);
+ }
+
+ function isDataURL(url) {
+ return (url && url.substr(0, 5) == "data:");
+ }
+
+ function splitURLTrue(url) {
+ const reSplitFile = /(.*?):\/{2,3}([^\/]*)(.*?)([^\/]*?)($|\?.*)/;
+ let m = reSplitFile.exec(url);
+
+ if (!m) {
+ return {
+ name: url,
+ path: url
+ };
+ } else if (m[4] == "" && m[5] == "") {
+ return {
+ protocol: m[1],
+ domain: m[2],
+ path: m[3],
+ name: m[3] != "/" ? m[3] : m[2]
+ };
+ }
+
+ return {
+ protocol: m[1],
+ domain: m[2],
+ path: m[2] + m[3],
+ name: m[4] + m[5]
+ };
+ }
+
+ // Exports from this module
+ exports.createFactories = createFactories;
+ exports.isGrip = isGrip;
+ exports.cropString = cropString;
+ exports.cropMultipleLines = cropMultipleLines;
+ exports.parseURLParams = parseURLParams;
+ exports.parseURLEncodedText = parseURLEncodedText;
+ exports.getFileName = getFileName;
+ exports.getURLDisplayString = getURLDisplayString;
+ exports.sanitizeString = sanitizeString;
+});
diff --git a/devtools/client/shared/components/reps/rep.js b/devtools/client/shared/components/reps/rep.js
new file mode 100644
index 000000000..0891fe0ce
--- /dev/null
+++ b/devtools/client/shared/components/reps/rep.js
@@ -0,0 +1,144 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ const { isGrip } = require("./rep-utils");
+
+ // Load all existing rep templates
+ const { Undefined } = require("./undefined");
+ const { Null } = require("./null");
+ const { StringRep } = require("./string");
+ const { LongStringRep } = require("./long-string");
+ const { Number } = require("./number");
+ const { ArrayRep } = require("./array");
+ const { Obj } = require("./object");
+ const { SymbolRep } = require("./symbol");
+ const { InfinityRep } = require("./infinity");
+ const { NaNRep } = require("./nan");
+
+ // DOM types (grips)
+ const { Attribute } = require("./attribute");
+ const { DateTime } = require("./date-time");
+ const { Document } = require("./document");
+ const { Event } = require("./event");
+ const { Func } = require("./function");
+ const { PromiseRep } = require("./promise");
+ const { RegExp } = require("./regexp");
+ const { StyleSheet } = require("./stylesheet");
+ const { CommentNode } = require("./comment-node");
+ const { ElementNode } = require("./element-node");
+ const { TextNode } = require("./text-node");
+ const { Window } = require("./window");
+ const { ObjectWithText } = require("./object-with-text");
+ const { ObjectWithURL } = require("./object-with-url");
+ const { GripArray } = require("./grip-array");
+ const { GripMap } = require("./grip-map");
+ const { Grip } = require("./grip");
+
+ // List of all registered template.
+ // XXX there should be a way for extensions to register a new
+ // or modify an existing rep.
+ let reps = [
+ RegExp,
+ StyleSheet,
+ Event,
+ DateTime,
+ CommentNode,
+ ElementNode,
+ TextNode,
+ Attribute,
+ LongStringRep,
+ Func,
+ PromiseRep,
+ ArrayRep,
+ Document,
+ Window,
+ ObjectWithText,
+ ObjectWithURL,
+ GripArray,
+ GripMap,
+ Grip,
+ Undefined,
+ Null,
+ StringRep,
+ Number,
+ SymbolRep,
+ InfinityRep,
+ NaNRep,
+ ];
+
+ /**
+ * Generic rep that is using for rendering native JS types or an object.
+ * The right template used for rendering is picked automatically according
+ * to the current value type. The value must be passed is as 'object'
+ * property.
+ */
+ const Rep = React.createClass({
+ displayName: "Rep",
+
+ propTypes: {
+ object: React.PropTypes.any,
+ defaultRep: React.PropTypes.object,
+ mode: React.PropTypes.string
+ },
+
+ render: function () {
+ let rep = getRep(this.props.object, this.props.defaultRep);
+ return rep(this.props);
+ },
+ });
+
+ // Helpers
+
+ /**
+ * Return a rep object that is responsible for rendering given
+ * object.
+ *
+ * @param object {Object} Object to be rendered in the UI. This
+ * can be generic JS object as well as a grip (handle to a remote
+ * debuggee object).
+ *
+ * @param defaultObject {React.Component} The default template
+ * that should be used to render given object if none is found.
+ */
+ function getRep(object, defaultRep = Obj) {
+ let type = typeof object;
+ if (type == "object" && object instanceof String) {
+ type = "string";
+ } else if (object && type == "object" && object.type) {
+ type = object.type;
+ }
+
+ if (isGrip(object)) {
+ type = object.class;
+ }
+
+ for (let i = 0; i < reps.length; i++) {
+ let rep = reps[i];
+ try {
+ // supportsObject could return weight (not only true/false
+ // but a number), which would allow to priorities templates and
+ // support better extensibility.
+ if (rep.supportsObject(object, type)) {
+ return React.createFactory(rep.rep);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return React.createFactory(defaultRep.rep);
+ }
+
+ // Exports from this module
+ exports.Rep = Rep;
+});
diff --git a/devtools/client/shared/components/reps/reps.css b/devtools/client/shared/components/reps/reps.css
new file mode 100644
index 000000000..61e5e3dac
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps.css
@@ -0,0 +1,174 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.theme-dark,
+.theme-light {
+ --number-color: var(--theme-highlight-green);
+ --string-color: var(--theme-highlight-orange);
+ --null-color: var(--theme-comment);
+ --object-color: var(--theme-body-color);
+ --caption-color: var(--theme-highlight-blue);
+ --location-color: var(--theme-content-color1);
+ --source-link-color: var(--theme-highlight-blue);
+ --node-color: var(--theme-highlight-bluegrey);
+ --reference-color: var(--theme-highlight-purple);
+}
+
+.theme-firebug {
+ --number-color: #000088;
+ --string-color: #FF0000;
+ --null-color: #787878;
+ --object-color: DarkGreen;
+ --caption-color: #444444;
+ --location-color: #555555;
+ --source-link-color: blue;
+ --node-color: rgb(0, 0, 136);
+ --reference-color: rgb(102, 102, 255);
+}
+
+/******************************************************************************/
+
+.objectLink:hover {
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+.inline {
+ display: inline;
+ white-space: normal;
+}
+
+.objectBox-object {
+ font-weight: bold;
+ color: var(--object-color);
+ white-space: pre-wrap;
+}
+
+.objectBox-string,
+.objectBox-symbol,
+.objectBox-text,
+.objectLink-textNode,
+.objectBox-table {
+ white-space: pre-wrap;
+}
+
+.objectBox-number,
+.objectLink-styleRule,
+.objectLink-element,
+.objectLink-textNode,
+.objectBox-array > .length {
+ color: var(--number-color);
+}
+
+.objectBox-textNode,
+.objectBox-string,
+.objectBox-symbol {
+ color: var(--string-color);
+}
+
+.objectLink-function,
+.objectBox-stackTrace,
+.objectLink-profile {
+ color: var(--object-color);
+}
+
+.objectLink-Location {
+ font-style: italic;
+ color: var(--location-color);
+}
+
+.objectBox-null,
+.objectBox-undefined,
+.objectBox-hint,
+.logRowHint {
+ font-style: italic;
+ color: var(--null-color);
+}
+
+.objectLink-sourceLink {
+ position: absolute;
+ right: 4px;
+ top: 2px;
+ padding-left: 8px;
+ font-weight: bold;
+ color: var(--source-link-color);
+}
+
+/******************************************************************************/
+
+.objectLink-event,
+.objectLink-eventLog,
+.objectLink-regexp,
+.objectLink-object,
+.objectLink-Date {
+ font-weight: bold;
+ color: var(--object-color);
+ white-space: pre-wrap;
+}
+
+/******************************************************************************/
+
+.objectLink-object .nodeName,
+.objectLink-NamedNodeMap .nodeName,
+.objectLink-NamedNodeMap .objectEqual,
+.objectLink-NamedNodeMap .arrayLeftBracket,
+.objectLink-NamedNodeMap .arrayRightBracket,
+.objectLink-Attr .attrEqual,
+.objectLink-Attr .attrTitle {
+ color: var(--node-color);
+}
+
+.objectLink-object .nodeName {
+ font-weight: normal;
+}
+
+/******************************************************************************/
+
+.objectLeftBrace,
+.objectRightBrace,
+.arrayLeftBracket,
+.arrayRightBracket {
+ cursor: pointer;
+ font-weight: bold;
+}
+
+/******************************************************************************/
+/* Cycle reference*/
+
+.objectLink-Reference {
+ font-weight: bold;
+ color: var(--reference-color);
+}
+
+.objectBox-array > .objectTitle {
+ font-weight: bold;
+ color: var(--object-color);
+}
+
+.caption {
+ font-weight: bold;
+ color: var(--caption-color);
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-dark .objectBox-null,
+.theme-dark .objectBox-undefined,
+.theme-light .objectBox-null,
+.theme-light .objectBox-undefined {
+ font-style: normal;
+}
+
+.theme-dark .objectBox-object,
+.theme-light .objectBox-object {
+ font-weight: normal;
+ white-space: pre-wrap;
+}
+
+.theme-dark .caption,
+.theme-light .caption {
+ font-weight: normal;
+}
diff --git a/devtools/client/shared/components/reps/string.js b/devtools/client/shared/components/reps/string.js
new file mode 100644
index 000000000..f8b4b1986
--- /dev/null
+++ b/devtools/client/shared/components/reps/string.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { cropString } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a string. String value is enclosed within quotes.
+ */
+ const StringRep = React.createClass({
+ displayName: "StringRep",
+
+ propTypes: {
+ useQuotes: React.PropTypes.bool,
+ style: React.PropTypes.object,
+ },
+
+ getDefaultProps: function () {
+ return {
+ useQuotes: true,
+ };
+ },
+
+ render: function () {
+ let text = this.props.object;
+ let member = this.props.member;
+ let style = this.props.style;
+
+ let config = {className: "objectBox objectBox-string"};
+ if (style) {
+ config.style = style;
+ }
+
+ if (member && member.open) {
+ return span(config, "\"" + text + "\"");
+ }
+
+ let croppedString = this.props.cropLimit ?
+ cropString(text, this.props.cropLimit) : cropString(text);
+
+ let formattedString = this.props.useQuotes ?
+ "\"" + croppedString + "\"" : croppedString;
+
+ return span(config, formattedString);
+ },
+ });
+
+ function supportsObject(object, type) {
+ return (type == "string");
+ }
+
+ // Exports from this module
+
+ exports.StringRep = {
+ rep: StringRep,
+ supportsObject: supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/stylesheet.js b/devtools/client/shared/components/reps/stylesheet.js
new file mode 100644
index 000000000..c1fc7f1be
--- /dev/null
+++ b/devtools/client/shared/components/reps/stylesheet.js
@@ -0,0 +1,77 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, getURLDisplayString } = require("./rep-utils");
+
+ // Shortcuts
+ const DOM = React.DOM;
+
+ /**
+ * Renders a grip representing CSSStyleSheet
+ */
+ let StyleSheet = React.createClass({
+ displayName: "object",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getTitle: function (grip) {
+ let title = "StyleSheet ";
+ if (this.props.objectLink) {
+ return DOM.span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, title)
+ );
+ }
+ return title;
+ },
+
+ getLocation: function (grip) {
+ // Embedded stylesheets don't have URL and so, no preview.
+ let url = grip.preview ? grip.preview.url : "";
+ return url ? getURLDisplayString(url) : "";
+ },
+
+ render: function () {
+ let grip = this.props.object;
+
+ return (
+ DOM.span({className: "objectBox objectBox-object"},
+ this.getTitle(grip),
+ DOM.span({className: "objectPropValue"},
+ this.getLocation(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return (type == "CSSStyleSheet");
+ }
+
+ // Exports from this module
+
+ exports.StyleSheet = {
+ rep: StyleSheet,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/symbol.js b/devtools/client/shared/components/reps/symbol.js
new file mode 100644
index 000000000..111794008
--- /dev/null
+++ b/devtools/client/shared/components/reps/symbol.js
@@ -0,0 +1,48 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a symbol.
+ */
+ const SymbolRep = React.createClass({
+ displayName: "SymbolRep",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ render: function () {
+ let {object} = this.props;
+ let {name} = object;
+
+ return (
+ span({className: "objectBox objectBox-symbol"},
+ `Symbol(${name || ""})`
+ )
+ );
+ },
+ });
+
+ function supportsObject(object, type) {
+ return (type == "symbol");
+ }
+
+ // Exports from this module
+ exports.SymbolRep = {
+ rep: SymbolRep,
+ supportsObject: supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/text-node.js b/devtools/client/shared/components/reps/text-node.js
new file mode 100644
index 000000000..d80545cea
--- /dev/null
+++ b/devtools/client/shared/components/reps/text-node.js
@@ -0,0 +1,94 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, cropString } = require("./rep-utils");
+
+ // Shortcuts
+ const DOM = React.DOM;
+
+ /**
+ * Renders DOM #text node.
+ */
+ let TextNode = React.createClass({
+ displayName: "TextNode",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ },
+
+ getTextContent: function (grip) {
+ return cropString(grip.preview.textContent);
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: grip
+ }, "#text ");
+ }
+ return "";
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ let mode = this.props.mode || "short";
+
+ if (mode == "short" || mode == "tiny") {
+ return (
+ DOM.span({className: "objectBox objectBox-textNode"},
+ this.getTitle(grip),
+ DOM.span({className: "nodeValue"},
+ "\"" + this.getTextContent(grip) + "\""
+ )
+ )
+ );
+ }
+
+ let objectLink = this.props.objectLink || DOM.span;
+ return (
+ DOM.span({className: "objectBox objectBox-textNode"},
+ this.getTitle(grip),
+ objectLink({
+ object: grip
+ }, "<"),
+ DOM.span({className: "nodeTag"}, "TextNode"),
+ " textContent=\"",
+ DOM.span({className: "nodeValue"},
+ this.getTextContent(grip)
+ ),
+ "\"",
+ objectLink({
+ object: grip
+ }, ">;")
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && grip.class == "Text");
+ }
+
+ // Exports from this module
+ exports.TextNode = {
+ rep: TextNode,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/undefined.js b/devtools/client/shared/components/reps/undefined.js
new file mode 100644
index 000000000..c4e64a12c
--- /dev/null
+++ b/devtools/client/shared/components/reps/undefined.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders undefined value
+ */
+ const Undefined = React.createClass({
+ displayName: "UndefinedRep",
+
+ render: function () {
+ return (
+ span({className: "objectBox objectBox-undefined"},
+ "undefined"
+ )
+ );
+ },
+ });
+
+ function supportsObject(object, type) {
+ if (object && object.type && object.type == "undefined") {
+ return true;
+ }
+
+ return (type == "undefined");
+ }
+
+ // Exports from this module
+
+ exports.Undefined = {
+ rep: Undefined,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/window.js b/devtools/client/shared/components/reps/window.js
new file mode 100644
index 000000000..628d69562
--- /dev/null
+++ b/devtools/client/shared/components/reps/window.js
@@ -0,0 +1,73 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, getURLDisplayString } = require("./rep-utils");
+
+ // Shortcuts
+ const DOM = React.DOM;
+
+ /**
+ * Renders a grip representing a window.
+ */
+ let Window = React.createClass({
+ displayName: "Window",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return DOM.span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, grip.class + " ")
+ );
+ }
+ return "";
+ },
+
+ getLocation: function (grip) {
+ return getURLDisplayString(grip.preview.url);
+ },
+
+ render: function () {
+ let grip = this.props.object;
+
+ return (
+ DOM.span({className: "objectBox objectBox-Window"},
+ this.getTitle(grip),
+ DOM.span({className: "objectPropValue"},
+ this.getLocation(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return (object.preview && type == "Window");
+ }
+
+ // Exports from this module
+ exports.Window = {
+ rep: Window,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/search-box.js b/devtools/client/shared/components/search-box.js
new file mode 100644
index 000000000..bd572f8b2
--- /dev/null
+++ b/devtools/client/shared/components/search-box.js
@@ -0,0 +1,110 @@
+/* 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/. */
+
+/* global window */
+
+"use strict";
+
+const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+
+/**
+ * A generic search box component for use across devtools
+ */
+module.exports = createClass({
+ displayName: "SearchBox",
+
+ propTypes: {
+ delay: PropTypes.number,
+ keyShortcut: PropTypes.string,
+ onChange: PropTypes.func,
+ placeholder: PropTypes.string,
+ type: PropTypes.string
+ },
+
+ getInitialState() {
+ return {
+ value: ""
+ };
+ },
+
+ componentDidMount() {
+ if (!this.props.keyShortcut) {
+ return;
+ }
+
+ this.shortcuts = new KeyShortcuts({
+ window
+ });
+ this.shortcuts.on(this.props.keyShortcut, (name, event) => {
+ event.preventDefault();
+ this.refs.input.focus();
+ });
+ },
+
+ componentWillUnmount() {
+ if (this.shortcuts) {
+ this.shortcuts.destroy();
+ }
+
+ // Clean up an existing timeout.
+ if (this.searchTimeout) {
+ clearTimeout(this.searchTimeout);
+ }
+ },
+
+ onChange() {
+ if (this.state.value !== this.refs.input.value) {
+ this.setState({ value: this.refs.input.value });
+ }
+
+ if (!this.props.delay) {
+ this.props.onChange(this.state.value);
+ return;
+ }
+
+ // Clean up an existing timeout before creating a new one.
+ if (this.searchTimeout) {
+ clearTimeout(this.searchTimeout);
+ }
+
+ // Execute the search after a timeout. It makes the UX
+ // smoother if the user is typing quickly.
+ this.searchTimeout = setTimeout(() => {
+ this.searchTimeout = null;
+ this.props.onChange(this.state.value);
+ }, this.props.delay);
+ },
+
+ onClearButtonClick() {
+ this.refs.input.value = "";
+ this.onChange();
+ },
+
+ render() {
+ let { type = "search", placeholder } = this.props;
+ let { value } = this.state;
+ let divClassList = ["devtools-searchbox", "has-clear-btn"];
+ let inputClassList = [`devtools-${type}input`];
+
+ if (value !== "") {
+ inputClassList.push("filled");
+ }
+ return dom.div(
+ { className: divClassList.join(" ") },
+ dom.input({
+ className: inputClassList.join(" "),
+ onChange: this.onChange,
+ placeholder,
+ ref: "input",
+ value
+ }),
+ dom.button({
+ className: "devtools-searchinput-clear",
+ hidden: value == "",
+ onClick: this.onClearButtonClick
+ })
+ );
+ }
+});
diff --git a/devtools/client/shared/components/sidebar-toggle.css b/devtools/client/shared/components/sidebar-toggle.css
new file mode 100644
index 000000000..659a3d23f
--- /dev/null
+++ b/devtools/client/shared/components/sidebar-toggle.css
@@ -0,0 +1,32 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.sidebar-toggle {
+ display: block;
+}
+
+.sidebar-toggle::before,
+.sidebar-toggle.pane-collapsed:dir(rtl)::before {
+ background-image: var(--theme-pane-collapse-image);
+}
+
+.sidebar-toggle.pane-collapsed::before,
+.sidebar-toggle:dir(rtl)::before {
+ background-image: var(--theme-pane-expand-image);
+}
+
+/* Rotate button icon 90deg if the toolbox container is
+ in vertical mode (sidebar displayed under the main panel) */
+@media (max-width: 700px) {
+ .sidebar-toggle::before {
+ transform: rotate(90deg);
+ }
+
+ /* Since RTL swaps the used images, we need to flip them
+ the other way round */
+ .sidebar-toggle:dir(rtl)::before {
+ transform: rotate(-90deg);
+ }
+}
diff --git a/devtools/client/shared/components/sidebar-toggle.js b/devtools/client/shared/components/sidebar-toggle.js
new file mode 100644
index 000000000..013e95f3d
--- /dev/null
+++ b/devtools/client/shared/components/sidebar-toggle.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const { button } = DOM;
+
+/**
+ * Sidebar toggle button. This button is used to exapand
+ * and collapse Sidebar.
+ */
+var SidebarToggle = createClass({
+ displayName: "SidebarToggle",
+
+ propTypes: {
+ // Set to true if collapsed.
+ collapsed: PropTypes.bool.isRequired,
+ // Tooltip text used when the button indicates expanded state.
+ collapsePaneTitle: PropTypes.string.isRequired,
+ // Tooltip text used when the button indicates collapsed state.
+ expandPaneTitle: PropTypes.string.isRequired,
+ // Click callback
+ onClick: PropTypes.func.isRequired,
+ },
+
+ getInitialState: function () {
+ return {
+ collapsed: this.props.collapsed,
+ };
+ },
+
+ // Events
+
+ onClick: function (event) {
+ this.props.onClick(event);
+ },
+
+ // Rendering
+
+ render: function () {
+ let title = this.state.collapsed ?
+ this.props.expandPaneTitle :
+ this.props.collapsePaneTitle;
+
+ let classNames = ["devtools-button", "sidebar-toggle"];
+ if (this.state.collapsed) {
+ classNames.push("pane-collapsed");
+ }
+
+ return (
+ button({
+ className: classNames.join(" "),
+ title: title,
+ onClick: this.onClick
+ })
+ );
+ }
+});
+
+module.exports = SidebarToggle;
diff --git a/devtools/client/shared/components/splitter/draggable.js b/devtools/client/shared/components/splitter/draggable.js
new file mode 100644
index 000000000..9caf93089
--- /dev/null
+++ b/devtools/client/shared/components/splitter/draggable.js
@@ -0,0 +1,54 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { DOM: dom, PropTypes } = React;
+
+const Draggable = React.createClass({
+ displayName: "Draggable",
+
+ propTypes: {
+ onMove: PropTypes.func.isRequired,
+ onStart: PropTypes.func,
+ onStop: PropTypes.func,
+ style: PropTypes.object,
+ className: PropTypes.string
+ },
+
+ startDragging(ev) {
+ ev.preventDefault();
+ const doc = ReactDOM.findDOMNode(this).ownerDocument;
+ doc.addEventListener("mousemove", this.onMove);
+ doc.addEventListener("mouseup", this.onUp);
+ this.props.onStart && this.props.onStart();
+ },
+
+ onMove(ev) {
+ ev.preventDefault();
+ // Use viewport coordinates so, moving mouse over iframes
+ // doesn't mangle (relative) coordinates.
+ this.props.onMove(ev.clientX, ev.clientY);
+ },
+
+ onUp(ev) {
+ ev.preventDefault();
+ const doc = ReactDOM.findDOMNode(this).ownerDocument;
+ doc.removeEventListener("mousemove", this.onMove);
+ doc.removeEventListener("mouseup", this.onUp);
+ this.props.onStop && this.props.onStop();
+ },
+
+ render() {
+ return dom.div({
+ style: this.props.style,
+ className: this.props.className,
+ onMouseDown: this.startDragging
+ });
+ }
+});
+
+module.exports = Draggable;
diff --git a/devtools/client/shared/components/splitter/moz.build b/devtools/client/shared/components/splitter/moz.build
new file mode 100644
index 000000000..924732aea
--- /dev/null
+++ b/devtools/client/shared/components/splitter/moz.build
@@ -0,0 +1,11 @@
+# -*- 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/.
+
+DevToolsModules(
+ 'draggable.js',
+ 'split-box.css',
+ 'split-box.js',
+)
diff --git a/devtools/client/shared/components/splitter/split-box.css b/devtools/client/shared/components/splitter/split-box.css
new file mode 100644
index 000000000..ea8fdaa6f
--- /dev/null
+++ b/devtools/client/shared/components/splitter/split-box.css
@@ -0,0 +1,88 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.split-box {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ height: 100%;
+ width: 100%;
+}
+
+.split-box.vert {
+ flex-direction: row;
+}
+
+.split-box.horz {
+ flex-direction: column;
+}
+
+.split-box > .uncontrolled {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ overflow: auto;
+}
+
+.split-box > .controlled {
+ display: flex;
+ overflow: auto;
+}
+
+.split-box > .splitter {
+ background-image: none;
+ border: 0;
+ border-style: solid;
+ border-color: transparent;
+ background-color: var(--theme-splitter-color);
+ background-clip: content-box;
+ position: relative;
+
+ box-sizing: border-box;
+
+ /* Positive z-index positions the splitter on top of its siblings and makes
+ it clickable on both sides. */
+ z-index: 1;
+}
+
+.split-box.vert > .splitter {
+ min-width: calc(var(--devtools-splitter-inline-start-width) +
+ var(--devtools-splitter-inline-end-width) + 1px);
+
+ border-inline-start-width: var(--devtools-splitter-inline-start-width);
+ border-inline-end-width: var(--devtools-splitter-inline-end-width);
+
+ margin-inline-start: calc(-1 * var(--devtools-splitter-inline-start-width) - 1px);
+ margin-inline-end: calc(-1 * var(--devtools-splitter-inline-end-width));
+
+ cursor: ew-resize;
+}
+
+.split-box.horz > .splitter {
+ min-height: calc(var(--devtools-splitter-top-width) +
+ var(--devtools-splitter-bottom-width) + 1px);
+
+ border-top-width: var(--devtools-splitter-top-width);
+ border-bottom-width: var(--devtools-splitter-bottom-width);
+
+ margin-top: calc(-1 * var(--devtools-splitter-top-width) - 1px);
+ margin-bottom: calc(-1 * var(--devtools-splitter-bottom-width));
+
+ cursor: ns-resize;
+}
+
+.split-box.disabled {
+ pointer-events: none;
+}
+
+/**
+ * Make sure splitter panels are not processing any mouse
+ * events. This is good for performance during splitter
+ * bar dragging.
+ */
+.split-box.dragging > .controlled,
+.split-box.dragging > .uncontrolled {
+ pointer-events: none;
+}
diff --git a/devtools/client/shared/components/splitter/split-box.js b/devtools/client/shared/components/splitter/split-box.js
new file mode 100644
index 000000000..85835f3e1
--- /dev/null
+++ b/devtools/client/shared/components/splitter/split-box.js
@@ -0,0 +1,205 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const Draggable = React.createFactory(require("devtools/client/shared/components/splitter/draggable"));
+const { DOM: dom, PropTypes } = React;
+
+/**
+ * This component represents a Splitter. The splitter supports vertical
+ * as well as horizontal mode.
+ */
+const SplitBox = React.createClass({
+ displayName: "SplitBox",
+
+ propTypes: {
+ // Custom class name. You can use more names separated by a space.
+ className: PropTypes.string,
+ // Initial size of controlled panel.
+ initialSize: PropTypes.number,
+ // Left/top panel
+ startPanel: PropTypes.any,
+ // Min panel size.
+ minSize: PropTypes.number,
+ // Max panel size.
+ maxSize: PropTypes.number,
+ // Right/bottom panel
+ endPanel: PropTypes.any,
+ // True if the right/bottom panel should be controlled.
+ endPanelControl: PropTypes.bool,
+ // Size of the splitter handle bar.
+ splitterSize: PropTypes.number,
+ // True if the splitter bar is vertical (default is vertical).
+ vert: PropTypes.bool
+ },
+
+ getDefaultProps() {
+ return {
+ splitterSize: 5,
+ vert: true,
+ endPanelControl: false
+ };
+ },
+
+ /**
+ * The state stores the current orientation (vertical or horizontal)
+ * and the current size (width/height). All these values can change
+ * during the component's life time.
+ */
+ getInitialState() {
+ return {
+ vert: this.props.vert,
+ width: this.props.initialWidth || this.props.initialSize,
+ height: this.props.initialHeight || this.props.initialSize
+ };
+ },
+
+ // Dragging Events
+
+ /**
+ * Set 'resizing' cursor on entire document during splitter dragging.
+ * This avoids cursor-flickering that happens when the mouse leaves
+ * the splitter bar area (happens frequently).
+ */
+ onStartMove() {
+ const splitBox = ReactDOM.findDOMNode(this);
+ const doc = splitBox.ownerDocument;
+ let defaultCursor = doc.documentElement.style.cursor;
+ doc.documentElement.style.cursor = (this.state.vert ? "ew-resize" : "ns-resize");
+
+ splitBox.classList.add("dragging");
+
+ this.setState({
+ defaultCursor: defaultCursor
+ });
+ },
+
+ onStopMove() {
+ const splitBox = ReactDOM.findDOMNode(this);
+ const doc = splitBox.ownerDocument;
+ doc.documentElement.style.cursor = this.state.defaultCursor;
+
+ splitBox.classList.remove("dragging");
+ },
+
+ /**
+ * Adjust size of the controlled panel. Depending on the current
+ * orientation we either remember the width or height of
+ * the splitter box.
+ */
+ onMove(x, y) {
+ const node = ReactDOM.findDOMNode(this);
+ const doc = node.ownerDocument;
+ const win = doc.defaultView;
+
+ let size;
+ let { endPanelControl } = this.props;
+
+ if (this.state.vert) {
+ // Switch the control flag in case of RTL. Note that RTL
+ // has impact on vertical splitter only.
+ let dir = win.getComputedStyle(doc.documentElement).direction;
+ if (dir == "rtl") {
+ endPanelControl = !endPanelControl;
+ }
+
+ size = endPanelControl ?
+ (node.offsetLeft + node.offsetWidth) - x :
+ x - node.offsetLeft;
+
+ this.setState({
+ width: size
+ });
+ } else {
+ size = endPanelControl ?
+ (node.offsetTop + node.offsetHeight) - y :
+ y - node.offsetTop;
+
+ this.setState({
+ height: size
+ });
+ }
+ },
+
+ // Rendering
+
+ render() {
+ const vert = this.state.vert;
+ const { startPanel, endPanel, endPanelControl, minSize,
+ maxSize, splitterSize } = this.props;
+
+ let style = Object.assign({}, this.props.style);
+
+ // Calculate class names list.
+ let classNames = ["split-box"];
+ classNames.push(vert ? "vert" : "horz");
+ if (this.props.className) {
+ classNames = classNames.concat(this.props.className.split(" "));
+ }
+
+ let leftPanelStyle;
+ let rightPanelStyle;
+
+ // Set proper size for panels depending on the current state.
+ if (vert) {
+ leftPanelStyle = {
+ maxWidth: endPanelControl ? null : maxSize,
+ minWidth: endPanelControl ? null : minSize,
+ width: endPanelControl ? null : this.state.width
+ };
+ rightPanelStyle = {
+ maxWidth: endPanelControl ? maxSize : null,
+ minWidth: endPanelControl ? minSize : null,
+ width: endPanelControl ? this.state.width : null
+ };
+ } else {
+ leftPanelStyle = {
+ maxHeight: endPanelControl ? null : maxSize,
+ minHeight: endPanelControl ? null : minSize,
+ height: endPanelControl ? null : this.state.height
+ };
+ rightPanelStyle = {
+ maxHeight: endPanelControl ? maxSize : null,
+ minHeight: endPanelControl ? minSize : null,
+ height: endPanelControl ? this.state.height : null
+ };
+ }
+
+ // Calculate splitter size
+ let splitterStyle = {
+ flex: "0 0 " + splitterSize + "px"
+ };
+
+ return (
+ dom.div({
+ className: classNames.join(" "),
+ style: style },
+ startPanel ?
+ dom.div({
+ className: endPanelControl ? "uncontrolled" : "controlled",
+ style: leftPanelStyle},
+ startPanel
+ ) : null,
+ Draggable({
+ className: "splitter",
+ style: splitterStyle,
+ onStart: this.onStartMove,
+ onStop: this.onStopMove,
+ onMove: this.onMove
+ }),
+ endPanel ?
+ dom.div({
+ className: endPanelControl ? "controlled" : "uncontrolled",
+ style: rightPanelStyle},
+ endPanel
+ ) : null
+ )
+ );
+ }
+});
+
+module.exports = SplitBox;
diff --git a/devtools/client/shared/components/stack-trace.js b/devtools/client/shared/components/stack-trace.js
new file mode 100644
index 000000000..43d0b8716
--- /dev/null
+++ b/devtools/client/shared/components/stack-trace.js
@@ -0,0 +1,68 @@
+/* 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 React = require("devtools/client/shared/vendor/react");
+const { DOM: dom, createClass, createFactory, PropTypes } = React;
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const Frame = createFactory(require("./frame"));
+
+const l10n = new LocalizationHelper("devtools/client/locales/webconsole.properties");
+
+const AsyncFrame = createFactory(createClass({
+ displayName: "AsyncFrame",
+
+ PropTypes: {
+ asyncCause: PropTypes.string.isRequired
+ },
+
+ render() {
+ let { asyncCause } = this.props;
+
+ return dom.span(
+ { className: "frame-link-async-cause" },
+ l10n.getFormatStr("stacktrace.asyncStack", asyncCause)
+ );
+ }
+}));
+
+const StackTrace = createClass({
+ displayName: "StackTrace",
+
+ PropTypes: {
+ stacktrace: PropTypes.array.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired
+ },
+
+ render() {
+ let { stacktrace, onViewSourceInDebugger } = this.props;
+
+ let frames = [];
+ stacktrace.forEach(s => {
+ if (s.asyncCause) {
+ frames.push("\t", AsyncFrame({
+ asyncCause: s.asyncCause
+ }), "\n");
+ }
+
+ frames.push("\t", Frame({
+ frame: {
+ functionDisplayName: s.functionName,
+ source: s.filename.split(" -> ").pop(),
+ line: s.lineNumber,
+ column: s.columnNumber,
+ },
+ showFunctionName: true,
+ showAnonymousFunctionName: true,
+ showFullSourceUrl: true,
+ onClick: onViewSourceInDebugger
+ }), "\n");
+ });
+
+ return dom.div({ className: "stack-trace" }, frames);
+ }
+});
+
+module.exports = StackTrace;
diff --git a/devtools/client/shared/components/tabs/moz.build b/devtools/client/shared/components/tabs/moz.build
new file mode 100644
index 000000000..d4d5dc35d
--- /dev/null
+++ b/devtools/client/shared/components/tabs/moz.build
@@ -0,0 +1,12 @@
+# -*- 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/.
+
+DevToolsModules(
+ 'tabbar.css',
+ 'tabbar.js',
+ 'tabs.css',
+ 'tabs.js',
+)
diff --git a/devtools/client/shared/components/tabs/tabbar.css b/devtools/client/shared/components/tabs/tabbar.css
new file mode 100644
index 000000000..72445e43e
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabbar.css
@@ -0,0 +1,53 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.tabs .tabs-navigation {
+ line-height: 15px;
+}
+
+.tabs .tabs-navigation {
+ height: 24px;
+}
+
+.tabs .tabs-menu-item:first-child {
+ border-inline-start-width: 0;
+}
+
+.tabs .tabs-navigation .tabs-menu-item:focus {
+ outline: var(--theme-focus-outline);
+ outline-offset: -2px;
+}
+
+.tabs .tabs-menu-item.is-active {
+ height: 23px;
+}
+
+/* Firebug theme is using slightly different height. */
+.theme-firebug .tabs .tabs-navigation {
+ height: 24px;
+}
+
+/* The tab takes entire horizontal space and individual tabs
+ should stretch accordingly. Use flexbox for the behavior.
+ Use also `overflow: hidden` so, 'overflow' and 'underflow'
+ events are fired (it's utilized by the all-tabs-menu). */
+.tabs .tabs-navigation .tabs-menu {
+ overflow: hidden;
+ display: flex;
+}
+
+.tabs .tabs-navigation .tabs-menu-item {
+ flex-grow: 1;
+}
+
+.tabs .tabs-navigation .tabs-menu-item a {
+ text-align: center;
+}
+
+/* Firebug theme doesn't stretch the tabs. */
+.theme-firebug .tabs .tabs-navigation .tabs-menu-item {
+ flex-grow: 0;
+}
+
diff --git a/devtools/client/shared/components/tabs/tabbar.js b/devtools/client/shared/components/tabs/tabbar.js
new file mode 100644
index 000000000..1e3aa4617
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabbar.js
@@ -0,0 +1,204 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { DOM, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const Tabs = createFactory(require("devtools/client/shared/components/tabs/tabs").Tabs);
+
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
+// Shortcuts
+const { div } = DOM;
+
+/**
+ * Renders Tabbar component.
+ */
+let Tabbar = createClass({
+ displayName: "Tabbar",
+
+ propTypes: {
+ onSelect: PropTypes.func,
+ showAllTabsMenu: PropTypes.bool,
+ toolbox: PropTypes.object,
+ },
+
+ getDefaultProps: function () {
+ return {
+ showAllTabsMenu: false,
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ tabs: [],
+ activeTab: 0
+ };
+ },
+
+ // Public API
+
+ addTab: function (id, title, selected = false, panel, url) {
+ let tabs = this.state.tabs.slice();
+ tabs.push({id, title, panel, url});
+
+ let newState = Object.assign({}, this.state, {
+ tabs: tabs,
+ });
+
+ if (selected) {
+ newState.activeTab = tabs.length - 1;
+ }
+
+ this.setState(newState, () => {
+ if (this.props.onSelect && selected) {
+ this.props.onSelect(id);
+ }
+ });
+ },
+
+ toggleTab: function (tabId, isVisible) {
+ let index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ let tabs = this.state.tabs.slice();
+ tabs[index] = Object.assign({}, tabs[index], {
+ isVisible: isVisible
+ });
+
+ this.setState(Object.assign({}, this.state, {
+ tabs: tabs,
+ }));
+ },
+
+ removeTab: function (tabId) {
+ let index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ let tabs = this.state.tabs.slice();
+ tabs.splice(index, 1);
+
+ this.setState(Object.assign({}, this.state, {
+ tabs: tabs,
+ }));
+ },
+
+ select: function (tabId) {
+ let index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ let newState = Object.assign({}, this.state, {
+ activeTab: index,
+ });
+
+ this.setState(newState, () => {
+ if (this.props.onSelect) {
+ this.props.onSelect(tabId);
+ }
+ });
+ },
+
+ // Helpers
+
+ getTabIndex: function (tabId) {
+ let tabIndex = -1;
+ this.state.tabs.forEach((tab, index) => {
+ if (tab.id == tabId) {
+ tabIndex = index;
+ }
+ });
+ return tabIndex;
+ },
+
+ getTabId: function (index) {
+ return this.state.tabs[index].id;
+ },
+
+ getCurrentTabId: function () {
+ return this.state.tabs[this.state.activeTab].id;
+ },
+
+ // Event Handlers
+
+ onTabChanged: function (index) {
+ this.setState({
+ activeTab: index
+ });
+
+ if (this.props.onSelect) {
+ this.props.onSelect(this.state.tabs[index].id);
+ }
+ },
+
+ onAllTabsMenuClick: function (event) {
+ let menu = new Menu();
+ let target = event.target;
+
+ // Generate list of menu items from the list of tabs.
+ this.state.tabs.forEach(tab => {
+ menu.append(new MenuItem({
+ label: tab.title,
+ type: "checkbox",
+ checked: this.getCurrentTabId() == tab.id,
+ click: () => this.select(tab.id),
+ }));
+ });
+
+ // Show a drop down menu with frames.
+ // XXX Missing menu API for specifying target (anchor)
+ // and relative position to it. See also:
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
+ let rect = target.getBoundingClientRect();
+ let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
+ let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
+ menu.popup(rect.left + screenX, rect.bottom + screenY, this.props.toolbox);
+
+ return menu;
+ },
+
+ // Rendering
+
+ renderTab: function (tab) {
+ if (typeof tab.panel === "function") {
+ return tab.panel({
+ key: tab.id,
+ title: tab.title,
+ id: tab.id,
+ url: tab.url,
+ });
+ }
+
+ return tab.panel;
+ },
+
+ render: function () {
+ let tabs = this.state.tabs.map(tab => {
+ return this.renderTab(tab);
+ });
+
+ return (
+ div({className: "devtools-sidebar-tabs"},
+ Tabs({
+ onAllTabsMenuClick: this.onAllTabsMenuClick,
+ showAllTabsMenu: this.props.showAllTabsMenu,
+ tabActive: this.state.activeTab,
+ onAfterChange: this.onTabChanged},
+ tabs
+ )
+ )
+ );
+ },
+});
+
+module.exports = Tabbar;
diff --git a/devtools/client/shared/components/tabs/tabs.css b/devtools/client/shared/components/tabs/tabs.css
new file mode 100644
index 000000000..0e70549c5
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabs.css
@@ -0,0 +1,183 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* Tabs General Styles */
+
+.tabs {
+ height: 100%;
+}
+
+.tabs .tabs-menu {
+ display: table;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.tabs .tabs-menu-item {
+ display: inline-block;
+}
+
+.tabs .tabs-menu-item a {
+ display: block;
+ color: #A9A9A9;
+ padding: 4px 8px;
+ border: 1px solid transparent;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.tabs .tabs-menu-item a {
+ cursor: default;
+}
+
+/* Make sure panel content takes entire vertical space.
+ (minus the height of the tab bar) */
+.tabs .panels {
+ height: calc(100% - 24px);
+}
+
+.tabs .tab-panel {
+ height: 100%;
+}
+
+.tabs .all-tabs-menu {
+ position: absolute;
+ top: 0;
+ offset-inline-end: 0;
+ width: 15px;
+ height: 100%;
+ border-inline-start: 1px solid var(--theme-splitter-color);
+ background: url("chrome://devtools/skin/images/dropmarker.svg");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+/* Light Theme */
+
+.theme-dark .tabs,
+.theme-light .tabs {
+ background: var(--theme-body-background);
+}
+
+.theme-dark .tabs .tabs-navigation,
+.theme-light .tabs .tabs-navigation {
+ position: relative;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ background: var(--theme-tab-toolbar-background);
+}
+
+.theme-dark .tabs .tabs-menu-item,
+.theme-light .tabs .tabs-menu-item {
+ margin: 0;
+ padding: 0;
+ border-style: solid;
+ border-width: 0;
+ border-inline-start-width: 1px;
+ border-color: var(--theme-splitter-color);
+}
+
+.theme-dark .tabs .tabs-menu-item:last-child,
+.theme-light:not(.theme-firebug) .tabs .tabs-menu-item:last-child {
+ border-inline-end-width: 1px;
+}
+
+.theme-dark .tabs .tabs-menu-item a,
+.theme-light .tabs .tabs-menu-item a {
+ color: var(--theme-content-color1);
+ padding: 3px 15px;
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:not(.is-active),
+.theme-light .tabs .tabs-menu-item:hover:not(.is-active) {
+ background-color: var(--toolbar-tab-hover);
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:active:not(.is-active),
+.theme-light .tabs .tabs-menu-item:hover:active:not(.is-active) {
+ background-color: var(--toolbar-tab-hover-active);
+}
+
+.theme-dark .tabs .tabs-menu-item.is-active,
+.theme-light .tabs .tabs-menu-item.is-active {
+ background-color: var(--theme-selection-background);
+}
+
+.theme-dark .tabs .tabs-menu-item.is-active a,
+.theme-light .tabs .tabs-menu-item.is-active a {
+ color: var(--theme-selection-color);
+}
+
+/* Dark Theme */
+
+.theme-dark .tabs .tabs-menu-item a {
+ color: var(--theme-body-color-alt);
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:not(.is-active) a {
+ color: #CED3D9;
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:active a {
+ color: var(--theme-selection-color);
+}
+
+/* Firebug Theme */
+
+.theme-firebug .tabs .tabs-navigation {
+ background-image: linear-gradient(rgba(253, 253, 253, 0.2), rgba(253, 253, 253, 0));
+ padding-top: 3px;
+ padding-left: 3px;
+ border-bottom: 1px solid rgb(170, 188, 207);
+}
+
+.theme-firebug .tabs .tabs-menu {
+ margin-bottom: -1px;
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active,
+.theme-firebug .tabs .tabs-menu-item.is-active:hover {
+ background-color: transparent;
+}
+
+.theme-firebug .tabs .tabs-menu-item {
+ position: relative;
+ border-inline-start-width: 0;
+}
+
+.theme-firebug .tabs .tabs-menu-item a {
+ font-family: var(--proportional-font-family);
+ font-weight: bold;
+ color: var(--theme-body-color);
+ border-radius: 4px 4px 0 0;
+}
+
+.theme-firebug .tabs .tabs-menu-item:hover:not(.is-active) a {
+ border: 1px solid #C8C8C8;
+ border-bottom: 1px solid transparent;
+ background-color: transparent;
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active a {
+ background-color: rgb(247, 251, 254);
+ border: 1px solid rgb(170, 188, 207);
+ border-bottom-color: transparent;
+ color: var(--theme-body-color);
+}
+
+.theme-firebug .tabs .tabs-menu-item:hover:active a {
+ background-color: var(--toolbar-tab-hover-active);
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active:hover:active a {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.theme-firebug .tabs .tabs-menu-item a {
+ border: 1px solid transparent;
+ padding: 4px 8px;
+}
diff --git a/devtools/client/shared/components/tabs/tabs.js b/devtools/client/shared/components/tabs/tabs.js
new file mode 100644
index 000000000..eaa0738b3
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabs.js
@@ -0,0 +1,369 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+define(function (require, exports, module) {
+ const React = require("devtools/client/shared/vendor/react");
+ const { DOM } = React;
+ const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
+
+ /**
+ * Renders simple 'tab' widget.
+ *
+ * Based on ReactSimpleTabs component
+ * https://github.com/pedronauck/react-simpletabs
+ *
+ * Component markup (+CSS) example:
+ *
+ * <div class='tabs'>
+ * <nav class='tabs-navigation'>
+ * <ul class='tabs-menu'>
+ * <li class='tabs-menu-item is-active'>Tab #1</li>
+ * <li class='tabs-menu-item'>Tab #2</li>
+ * </ul>
+ * </nav>
+ * <div class='panels'>
+ * The content of active panel here
+ * </div>
+ * <div>
+ */
+ let Tabs = React.createClass({
+ displayName: "Tabs",
+
+ propTypes: {
+ className: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.string,
+ React.PropTypes.object
+ ]),
+ tabActive: React.PropTypes.number,
+ onMount: React.PropTypes.func,
+ onBeforeChange: React.PropTypes.func,
+ onAfterChange: React.PropTypes.func,
+ children: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.element
+ ]).isRequired,
+ showAllTabsMenu: React.PropTypes.bool,
+ onAllTabsMenuClick: React.PropTypes.func,
+ },
+
+ getDefaultProps: function () {
+ return {
+ tabActive: 0,
+ showAllTabsMenu: false,
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ tabActive: this.props.tabActive,
+
+ // This array is used to store an information whether a tab
+ // at specific index has already been created (e.g. selected
+ // at least once).
+ // If yes, it's rendered even if not currently selected.
+ // This is because in some cases we don't want to re-create
+ // tab content when it's being unselected/selected.
+ // E.g. in case of an iframe being used as a tab-content
+ // we want the iframe to stay in the DOM.
+ created: [],
+
+ // True if tabs can't fit into available horizontal space.
+ overflow: false,
+ };
+ },
+
+ componentDidMount: function () {
+ let node = findDOMNode(this);
+ node.addEventListener("keydown", this.onKeyDown, false);
+
+ // Register overflow listeners to manage visibility
+ // of all-tabs-menu. This menu is displayed when there
+ // is not enough h-space to render all tabs.
+ // It allows the user to select a tab even if it's hidden.
+ if (this.props.showAllTabsMenu) {
+ node.addEventListener("overflow", this.onOverflow, false);
+ node.addEventListener("underflow", this.onUnderflow, false);
+ }
+
+ let index = this.state.tabActive;
+ if (this.props.onMount) {
+ this.props.onMount(index);
+ }
+ },
+
+ componentWillReceiveProps: function (newProps) {
+ // Check type of 'tabActive' props to see if it's valid
+ // (it's 0-based index).
+ if (typeof newProps.tabActive == "number") {
+ let created = [...this.state.created];
+ created[newProps.tabActive] = true;
+
+ this.setState(Object.assign({}, this.state, {
+ tabActive: newProps.tabActive,
+ created: created,
+ }));
+ }
+ },
+
+ componentWillUnmount: function () {
+ let node = findDOMNode(this);
+ node.removeEventListener("keydown", this.onKeyDown, false);
+
+ if (this.props.showAllTabsMenu) {
+ node.removeEventListener("overflow", this.onOverflow, false);
+ node.removeEventListener("underflow", this.onUnderflow, false);
+ }
+ },
+
+ // DOM Events
+
+ onOverflow: function (event) {
+ if (event.target.classList.contains("tabs-menu")) {
+ this.setState({
+ overflow: true
+ });
+ }
+ },
+
+ onUnderflow: function (event) {
+ if (event.target.classList.contains("tabs-menu")) {
+ this.setState({
+ overflow: false
+ });
+ }
+ },
+
+ onKeyDown: function (event) {
+ // Bail out if the focus isn't on a tab.
+ if (!event.target.closest(".tabs-menu-item")) {
+ return;
+ }
+
+ let tabActive = this.state.tabActive;
+ let tabCount = this.props.children.length;
+
+ switch (event.code) {
+ case "ArrowRight":
+ tabActive = Math.min(tabCount - 1, tabActive + 1);
+ break;
+ case "ArrowLeft":
+ tabActive = Math.max(0, tabActive - 1);
+ break;
+ }
+
+ if (this.state.tabActive != tabActive) {
+ this.setActive(tabActive);
+ }
+ },
+
+ onClickTab: function (index, event) {
+ this.setActive(index);
+ event.preventDefault();
+ },
+
+ onAllTabsMenuClick: function (event) {
+ if (this.props.onAllTabsMenuClick) {
+ this.props.onAllTabsMenuClick(event);
+ }
+ },
+
+ // API
+
+ setActive: function (index) {
+ let onAfterChange = this.props.onAfterChange;
+ let onBeforeChange = this.props.onBeforeChange;
+
+ if (onBeforeChange) {
+ let cancel = onBeforeChange(index);
+ if (cancel) {
+ return;
+ }
+ }
+
+ let created = [...this.state.created];
+ created[index] = true;
+
+ let newState = Object.assign({}, this.state, {
+ tabActive: index,
+ created: created
+ });
+
+ this.setState(newState, () => {
+ // Properly set focus on selected tab.
+ let node = findDOMNode(this);
+ let selectedTab = node.querySelector(".is-active > a");
+ if (selectedTab) {
+ selectedTab.focus();
+ }
+
+ if (onAfterChange) {
+ onAfterChange(index);
+ }
+ });
+ },
+
+ // Rendering
+
+ renderMenuItems: function () {
+ if (!this.props.children) {
+ throw new Error("There must be at least one Tab");
+ }
+
+ if (!Array.isArray(this.props.children)) {
+ this.props.children = [this.props.children];
+ }
+
+ let tabs = this.props.children
+ .map(tab => {
+ return typeof tab === "function" ? tab() : tab;
+ }).filter(tab => {
+ return tab;
+ }).map((tab, index) => {
+ let ref = ("tab-menu-" + index);
+ let title = tab.props.title;
+ let tabClassName = tab.props.className;
+ let isTabSelected = this.state.tabActive === index;
+
+ let classes = [
+ "tabs-menu-item",
+ tabClassName,
+ isTabSelected ? "is-active" : ""
+ ].join(" ");
+
+ // Set tabindex to -1 (except the selected tab) so, it's focusable,
+ // but not reachable via sequential tab-key navigation.
+ // Changing selected tab (and so, moving focus) is done through
+ // left and right arrow keys.
+ // See also `onKeyDown()` event handler.
+ return (
+ DOM.li({
+ ref: ref,
+ key: index,
+ id: "tab-" + index,
+ className: classes,
+ role: "presentation",
+ },
+ DOM.a({
+ tabIndex: this.state.tabActive === index ? 0 : -1,
+ "aria-controls": "panel-" + index,
+ "aria-selected": isTabSelected,
+ role: "tab",
+ onClick: this.onClickTab.bind(this, index),
+ },
+ title
+ )
+ )
+ );
+ });
+
+ // Display the menu only if there is not enough horizontal
+ // space for all tabs (and overflow happened).
+ let allTabsMenu = this.state.overflow ? (
+ DOM.div({
+ className: "all-tabs-menu",
+ onClick: this.props.onAllTabsMenuClick
+ })
+ ) : null;
+
+ return (
+ DOM.nav({className: "tabs-navigation"},
+ DOM.ul({className: "tabs-menu", role: "tablist"},
+ tabs
+ ),
+ allTabsMenu
+ )
+ );
+ },
+
+ renderPanels: function () {
+ if (!this.props.children) {
+ throw new Error("There must be at least one Tab");
+ }
+
+ if (!Array.isArray(this.props.children)) {
+ this.props.children = [this.props.children];
+ }
+
+ let selectedIndex = this.state.tabActive;
+
+ let panels = this.props.children
+ .map(tab => {
+ return typeof tab === "function" ? tab() : tab;
+ }).filter(tab => {
+ return tab;
+ }).map((tab, index) => {
+ let selected = selectedIndex == index;
+
+ // Use 'visibility:hidden' + 'width/height:0' for hiding
+ // content of non-selected tab. It's faster (not sure why)
+ // than display:none and visibility:collapse.
+ let style = {
+ visibility: selected ? "visible" : "hidden",
+ height: selected ? "100%" : "0",
+ width: selected ? "100%" : "0",
+ };
+
+ return (
+ DOM.div({
+ key: index,
+ id: "panel-" + index,
+ style: style,
+ className: "tab-panel-box",
+ role: "tabpanel",
+ "aria-labelledby": "tab-" + index,
+ },
+ (selected || this.state.created[index]) ? tab : null
+ )
+ );
+ });
+
+ return (
+ DOM.div({className: "panels"},
+ panels
+ )
+ );
+ },
+
+ render: function () {
+ let classNames = ["tabs", this.props.className].join(" ");
+
+ return (
+ DOM.div({className: classNames},
+ this.renderMenuItems(),
+ this.renderPanels()
+ )
+ );
+ },
+ });
+
+ /**
+ * Renders simple tab 'panel'.
+ */
+ let Panel = React.createClass({
+ displayName: "Panel",
+
+ propTypes: {
+ title: React.PropTypes.string.isRequired,
+ children: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.element
+ ]).isRequired
+ },
+
+ render: function () {
+ return DOM.div({className: "tab-panel"},
+ this.props.children
+ );
+ }
+ });
+
+ // Exports from this module
+ exports.TabPanel = Panel;
+ exports.Tabs = Tabs;
+});
diff --git a/devtools/client/shared/components/test/browser/.eslintrc.js b/devtools/client/shared/components/test/browser/.eslintrc.js
new file mode 100644
index 000000000..76904829d
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/.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/shared/components/test/browser/browser.ini b/devtools/client/shared/components/test/browser/browser.ini
new file mode 100644
index 000000000..9db9eca66
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_notification_box_basic.js]
diff --git a/devtools/client/shared/components/test/browser/browser_notification_box_basic.js b/devtools/client/shared/components/test/browser/browser_notification_box_basic.js
new file mode 100644
index 000000000..b7c6a669b
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/browser_notification_box_basic.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+/* import-globals-from ../../../../framework/test/shared-head.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+const TEST_URI = "data:text/html;charset=utf-8,Test page";
+
+/**
+ * Basic test that checks existence of the Notification box.
+ */
+add_task(function* () {
+ info("Test Notification box basic started");
+
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+
+ // Append a notification
+ let notificationBox = toolbox.getNotificationBox();
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ notificationBox.PRIORITY_INFO_HIGH
+ );
+
+ // Verify existence of one notification.
+ let parentNode = toolbox.doc.getElementById("toolbox-notificationbox");
+ let nodes = parentNode.querySelectorAll(".notification");
+ is(nodes.length, 1, "There must be one notification");
+});
diff --git a/devtools/client/shared/components/test/mochitest/.eslintrc.js b/devtools/client/shared/components/test/mochitest/.eslintrc.js
new file mode 100644
index 000000000..677cbb424
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/.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/shared/components/test/mochitest/chrome.ini b/devtools/client/shared/components/test/mochitest/chrome.ini
new file mode 100644
index 000000000..27a4be137
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -0,0 +1,51 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[test_frame_01.html]
+[test_HSplitBox_01.html]
+[test_notification_box_01.html]
+[test_notification_box_02.html]
+[test_notification_box_03.html]
+[test_reps_array.html]
+[test_reps_attribute.html]
+[test_reps_comment-node.html]
+[test_reps_date-time.html]
+[test_reps_document.html]
+[test_reps_element-node.html]
+[test_reps_event.html]
+[test_reps_function.html]
+[test_reps_grip.html]
+[test_reps_grip-array.html]
+[test_reps_grip-map.html]
+[test_reps_infinity.html]
+[test_reps_long-string.html]
+[test_reps_nan.html]
+[test_reps_null.html]
+[test_reps_number.html]
+[test_reps_object.html]
+[test_reps_object-with-text.html]
+[test_reps_object-with-url.html]
+[test_reps_promise.html]
+[test_reps_regexp.html]
+[test_reps_string.html]
+[test_reps_stylesheet.html]
+[test_reps_symbol.html]
+[test_reps_text-node.html]
+[test_reps_undefined.html]
+[test_reps_window.html]
+[test_sidebar_toggle.html]
+[test_stack-trace.html]
+[test_tabs_accessibility.html]
+[test_tabs_menu.html]
+[test_tree_01.html]
+[test_tree_02.html]
+[test_tree_03.html]
+[test_tree_04.html]
+[test_tree_05.html]
+[test_tree_06.html]
+[test_tree_07.html]
+[test_tree_08.html]
+[test_tree_09.html]
+[test_tree_10.html]
+[test_tree_11.html]
diff --git a/devtools/client/shared/components/test/mochitest/head.js b/devtools/client/shared/components/test/mochitest/head.js
new file mode 100644
index 000000000..b66b72814
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/head.js
@@ -0,0 +1,217 @@
+/* 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var { Assert } = require("resource://testing-common/Assert.jsm");
+var { gDevTools } = require("devtools/client/framework/devtools");
+var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+var Services = require("Services");
+var { DebuggerServer } = require("devtools/server/main");
+var { DebuggerClient } = require("devtools/shared/client/main");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var flags = require("devtools/shared/flags");
+var { Task } = require("devtools/shared/task");
+var { TargetFactory } = require("devtools/client/framework/target");
+var { Toolbox } = require("devtools/client/framework/toolbox");
+
+flags.testing = true;
+var { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/shared/",
+ window
+});
+
+let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+let React = browserRequire("devtools/client/shared/vendor/react");
+var TestUtils = React.addons.TestUtils;
+
+var EXAMPLE_URL = "http://example.com/browser/browser/devtools/shared/test/";
+
+function forceRender(comp) {
+ return setState(comp, {})
+ .then(() => setState(comp, {}));
+}
+
+// All tests are asynchronous.
+SimpleTest.waitForExplicitFinish();
+
+function onNextAnimationFrame(fn) {
+ return () =>
+ requestAnimationFrame(() =>
+ requestAnimationFrame(fn));
+}
+
+function setState(component, newState) {
+ return new Promise(resolve => {
+ component.setState(newState, onNextAnimationFrame(resolve));
+ });
+}
+
+function setProps(component, newProps) {
+ return new Promise(resolve => {
+ component.setProps(newProps, onNextAnimationFrame(resolve));
+ });
+}
+
+function dumpn(msg) {
+ dump(`SHARED-COMPONENTS-TEST: ${msg}\n`);
+}
+
+/**
+ * Tree
+ */
+
+var TEST_TREE_INTERFACE = {
+ getParent: x => TEST_TREE.parent[x],
+ getChildren: x => TEST_TREE.children[x],
+ renderItem: (x, depth, focused) => "-".repeat(depth) + x + ":" + focused + "\n",
+ getRoots: () => ["A", "M"],
+ getKey: x => "key-" + x,
+ itemHeight: 1,
+ onExpand: x => TEST_TREE.expanded.add(x),
+ onCollapse: x => TEST_TREE.expanded.delete(x),
+ isExpanded: x => TEST_TREE.expanded.has(x),
+};
+
+function isRenderedTree(actual, expectedDescription, msg) {
+ const expected = expectedDescription.map(x => x + "\n").join("");
+ dumpn(`Expected tree:\n${expected}`);
+ dumpn(`Actual tree:\n${actual}`);
+ is(actual, expected, msg);
+}
+
+// Encoding of the following tree/forest:
+//
+// A
+// |-- B
+// | |-- E
+// | | |-- K
+// | | `-- L
+// | |-- F
+// | `-- G
+// |-- C
+// | |-- H
+// | `-- I
+// `-- D
+// `-- J
+// M
+// `-- N
+// `-- O
+var TEST_TREE = {
+ children: {
+ A: ["B", "C", "D"],
+ B: ["E", "F", "G"],
+ C: ["H", "I"],
+ D: ["J"],
+ E: ["K", "L"],
+ F: [],
+ G: [],
+ H: [],
+ I: [],
+ J: [],
+ K: [],
+ L: [],
+ M: ["N"],
+ N: ["O"],
+ O: []
+ },
+ parent: {
+ A: null,
+ B: "A",
+ C: "A",
+ D: "A",
+ E: "B",
+ F: "B",
+ G: "B",
+ H: "C",
+ I: "C",
+ J: "D",
+ K: "E",
+ L: "E",
+ M: null,
+ N: "M",
+ O: "N"
+ },
+ expanded: new Set(),
+};
+
+/**
+ * Frame
+ */
+function checkFrameString({
+ el, file, line, column, source, functionName, shouldLink, tooltip
+}) {
+ let $ = selector => el.querySelector(selector);
+
+ let $func = $(".frame-link-function-display-name");
+ let $source = $(".frame-link-source");
+ let $sourceInner = $(".frame-link-source-inner");
+ let $filename = $(".frame-link-filename");
+ let $line = $(".frame-link-line");
+
+ is($filename.textContent, file, "Correct filename");
+ is(el.getAttribute("data-line"), line ? `${line}` : null, "Expected `data-line` found");
+ is(el.getAttribute("data-column"),
+ column ? `${column}` : null, "Expected `data-column` found");
+ is($sourceInner.getAttribute("title"), tooltip, "Correct tooltip");
+ is($source.tagName, shouldLink ? "A" : "SPAN", "Correct linkable status");
+ if (shouldLink) {
+ is($source.getAttribute("href"), source, "Correct source");
+ }
+
+ if (line != null) {
+ let lineText = `:${line}`;
+ if (column != null) {
+ lineText += `:${column}`;
+ }
+
+ is($line.textContent, lineText, "Correct line number");
+ } else {
+ ok(!$line, "Should not have an element for `line`");
+ }
+
+ if (functionName != null) {
+ is($func.textContent, functionName, "Correct function name");
+ } else {
+ ok(!$func, "Should not have an element for `functionName`");
+ }
+}
+
+function renderComponent(component, props) {
+ const el = React.createElement(component, props, {});
+ // By default, renderIntoDocument() won't work for stateless components, but
+ // it will work if the stateless component is wrapped in a stateful one.
+ // See https://github.com/facebook/react/issues/4839
+ const wrappedEl = React.DOM.span({}, [el]);
+ const renderedComponent = TestUtils.renderIntoDocument(wrappedEl);
+ return ReactDOM.findDOMNode(renderedComponent).children[0];
+}
+
+function shallowRenderComponent(component, props) {
+ const el = React.createElement(component, props);
+ const renderer = TestUtils.createRenderer();
+ renderer.render(el, {});
+ return renderer.getRenderOutput();
+}
+
+/**
+ * Test that a rep renders correctly across different modes.
+ */
+function testRepRenderModes(modeTests, testName, componentUnderTest, gripStub) {
+ modeTests.forEach(({mode, expectedOutput, message}) => {
+ const modeString = typeof mode === "undefined" ? "no mode" : mode;
+ if (!message) {
+ message = `${testName}: ${modeString} renders correctly.`;
+ }
+
+ const rendered = renderComponent(componentUnderTest.rep, { object: gripStub, mode });
+ is(rendered.textContent, expectedOutput, message);
+ });
+}
diff --git a/devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html b/devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html
new file mode 100644
index 000000000..7a7187de6
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html
@@ -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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Basic tests for the HSplitBox component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript "src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <link rel="stylesheet" href="resource://devtools/client/themes/splitters.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
+ <style>
+ html {
+ --theme-splitter-color: black;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+const FUDGE_FACTOR = .1;
+function aboutEq(a, b) {
+ dumpn(`Checking ${a} ~= ${b}`);
+ return Math.abs(a - b) < FUDGE_FACTOR;
+}
+
+window.onload = Task.async(function* () {
+ try {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+
+ let HSplitBox = React.createFactory(browserRequire("devtools/client/shared/components/h-split-box"));
+ ok(HSplitBox, "Should get HSplitBox");
+
+ const newSizes = [];
+ const box = ReactDOM.render(HSplitBox({
+ start: "hello!",
+ end: "world!",
+ startWidth: .5,
+ onResize(newSize) {
+ newSizes.push(newSize);
+ },
+ }), window.document.body);
+
+ // Test that we properly rendered our two panes.
+
+ let panes = document.querySelectorAll(".h-split-box-pane");
+ is(panes.length, 2, "Should get two panes");
+ is(panes[0].style.flexGrow, "0.5", "Each pane should have .5 width");
+ is(panes[1].style.flexGrow, "0.5", "Each pane should have .5 width");
+ is(panes[0].textContent.trim(), "hello!", "First pane should be hello");
+ is(panes[1].textContent.trim(), "world!", "Second pane should be world");
+
+ // Now change the left width and assert that the changes are reflected.
+
+ yield setProps(box, { startWidth: .25 });
+ panes = document.querySelectorAll(".h-split-box-pane");
+ is(panes.length, 2, "Should still have two panes");
+ is(panes[0].style.flexGrow, "0.25", "First pane's width should be .25");
+ is(panes[1].style.flexGrow, "0.75", "Second pane's width should be .75");
+
+ // Mouse moves without having grabbed the splitter should have no effect.
+
+ let container = document.querySelector(".h-split-box");
+ ok(container, "Should get our container .h-split-box");
+
+ const { left, top, width } = container.getBoundingClientRect();
+ const middle = left + width / 2;
+ const oneQuarter = left + width / 4;
+ const threeQuarters = left + 3 * width / 4;
+
+ synthesizeMouse(container, middle, top, { type: "mousemove" }, window);
+ is(newSizes.length, 0, "Mouse moves without dragging the splitter should have no effect");
+
+ // Send a mouse down on the splitter, and then move the mouse a couple
+ // times. Now we should get resizes.
+
+ const splitter = document.querySelector(".devtools-side-splitter");
+ ok(splitter, "Should get our splitter");
+
+ synthesizeMouseAtCenter(splitter, { button: 0, type: "mousedown" }, window);
+
+ function mouseMove(clientX) {
+ const event = new MouseEvent("mousemove", { clientX });
+ document.defaultView.top.dispatchEvent(event);
+ }
+
+ mouseMove(middle);
+ is(newSizes.length, 1, "Should get 1 resize");
+ ok(aboutEq(newSizes[0], .5), "New size should be ~.5");
+
+ mouseMove(left);
+ is(newSizes.length, 2, "Should get 2 resizes");
+ ok(aboutEq(newSizes[1], 0), "New size should be ~0");
+
+ mouseMove(oneQuarter);
+ is(newSizes.length, 3, "Sould get 3 resizes");
+ ok(aboutEq(newSizes[2], .25), "New size should be ~.25");
+
+ mouseMove(threeQuarters);
+ is(newSizes.length, 4, "Should get 4 resizes");
+ ok(aboutEq(newSizes[3], .75), "New size should be ~.75");
+
+ synthesizeMouseAtCenter(splitter, { button: 0, type: "mouseup" }, window);
+
+ // Now that we have let go of the splitter, mouse moves should not result in resizes.
+
+ synthesizeMouse(container, middle, top, { type: "mousemove" }, window);
+ is(newSizes.length, 4, "Should still have 4 resizes");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_frame_01.html b/devtools/client/shared/components/test/mochitest/test_frame_01.html
new file mode 100644
index 000000000..ed3bc90c2
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_frame_01.html
@@ -0,0 +1,309 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the formatting of the file name, line and columns are correct in frame components,
+with optional columns, unknown and non-URL sources.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Frame component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let Frame = React.createFactory(browserRequire("devtools/client/shared/components/frame"));
+ ok(Frame, "Should get Frame");
+
+ // Check when there's a column
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 55,
+ column: 10,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:55:10",
+ });
+
+ // Check when there's no column
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:55",
+ });
+
+ // Check when column === 0
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 55,
+ column: 0,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:55",
+ });
+
+ // Check when there's no parseable URL source;
+ // should not link but should render line/columns
+ yield checkFrameComponent({
+ frame: {
+ source: "self-hosted",
+ line: 1,
+ }
+ }, {
+ file: "self-hosted",
+ line: "1",
+ shouldLink: false,
+ tooltip: "self-hosted:1",
+ });
+ yield checkFrameComponent({
+ frame: {
+ source: "self-hosted",
+ line: 1,
+ column: 10,
+ }
+ }, {
+ file: "self-hosted",
+ line: "1",
+ column: "10",
+ shouldLink: false,
+ tooltip: "self-hosted:1:10",
+ });
+
+ // Check when there's no source;
+ // should not link but should render line/columns
+ yield checkFrameComponent({
+ frame: {
+ line: 1,
+ }
+ }, {
+ file: "(unknown)",
+ line: "1",
+ shouldLink: false,
+ tooltip: "(unknown):1",
+ });
+ yield checkFrameComponent({
+ frame: {
+ line: 1,
+ column: 10,
+ }
+ }, {
+ file: "(unknown)",
+ line: "1",
+ column: "10",
+ shouldLink: false,
+ tooltip: "(unknown):1:10",
+ });
+
+ // Check when there's a column, but no line;
+ // no line/column info should render
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ column: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check when line is 0; this should be an invalid
+ // line option, so don't render line/column
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ column: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check when source is via Scratchpad; we should render out the
+ // lines and columns as this is linkable.
+ yield checkFrameComponent({
+ frame: {
+ source: "Scratchpad/1",
+ line: 10,
+ column: 50,
+ }
+ }, {
+ file: "Scratchpad/1",
+ line: 10,
+ column: 50,
+ shouldLink: true,
+ tooltip: "View source in Debugger → Scratchpad/1:10:50",
+ });
+
+ // Check that line and column can be strings
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: "10",
+ column: "55",
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 10,
+ column: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:10:55",
+ });
+
+ // Check that line and column can be strings,
+ // and that the `0` rendering rules apply when they are strings as well
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: "0",
+ column: "55",
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check that the showFullSourceUrl option works correctly
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFullSourceUrl: true
+ }, {
+ file: "http://myfile.com/mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check that the showFunctionName option works correctly
+ yield checkFrameComponent({
+ frame: {
+ functionDisplayName: "myfun",
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ }
+ }, {
+ functionName: null,
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ yield checkFrameComponent({
+ frame: {
+ functionDisplayName: "myfun",
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true
+ }, {
+ functionName: "myfun",
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check that anonymous function name is not displayed unless explicitly enabled
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true
+ }, {
+ functionName: null,
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true,
+ showAnonymousFunctionName: true
+ }, {
+ functionName: "<anonymous>",
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check if file is rendered with "/" for root documents when showEmptyPathAsHost is false
+ yield checkFrameComponent({
+ frame: {
+ source: "http://www.cnn.com/",
+ line: "1",
+ },
+ showEmptyPathAsHost: false,
+ }, {
+ file: "/",
+ line: "1",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://www.cnn.com/:1",
+ });
+
+ // Check if file is rendered with hostname for root documents when showEmptyPathAsHost is true
+ yield checkFrameComponent({
+ frame: {
+ source: "http://www.cnn.com/",
+ line: "1",
+ },
+ showEmptyPathAsHost: true,
+ }, {
+ file: "www.cnn.com",
+ line: "1",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://www.cnn.com/:1",
+ });
+
+ function* checkFrameComponent(input, expected) {
+ let props = Object.assign({ onClick: () => {} }, input);
+ let frame = ReactDOM.render(Frame(props), window.document.body);
+ yield forceRender(frame);
+
+ let el = frame.getDOMNode();
+ let { source } = input.frame;
+ checkFrameString(Object.assign({ el, source }, expected));
+ }
+
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_notification_box_01.html b/devtools/client/shared/components/test/mochitest/test_notification_box_01.html
new file mode 100644
index 000000000..947fb9803
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_notification_box_01.html
@@ -0,0 +1,108 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Basic rendering
+* Appending a notification
+* Notification priority
+* Closing notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/notification-box");
+
+ const renderedBox = shallowRenderComponent(NotificationBox, {});
+ is(renderedBox.type, "div", "NotificationBox is rendered as <div>");
+
+ // Test rendering
+ let boxElement = React.createElement(NotificationBox);
+ let notificationBox = TestUtils.renderIntoDocument(boxElement);
+ let notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ is(notificationNode.className, "notificationbox",
+ "NotificationBox has expected classname");
+ is(notificationNode.textContent, "",
+ "Empty NotificationBox has no text content");
+
+ checkNumberOfNotifications(notificationBox, 0);
+
+ // Append a notification
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_HIGH
+ );
+
+ is (notificationNode.textContent, "Info message",
+ "The box must display notification message");
+ checkNumberOfNotifications(notificationBox, 1);
+
+ // Append more important notification
+ notificationBox.appendNotification(
+ "Critical message",
+ "id2",
+ null,
+ PriorityLevels.PRIORITY_CRITICAL_BLOCK
+ );
+
+ checkNumberOfNotifications(notificationBox, 1);
+
+ is (notificationNode.textContent, "Critical message",
+ "The box must display more important notification message");
+
+ // Append less important notification
+ notificationBox.appendNotification(
+ "Warning message",
+ "id3",
+ null,
+ PriorityLevels.PRIORITY_WARNING_HIGH
+ );
+
+ checkNumberOfNotifications(notificationBox, 1);
+
+ is (notificationNode.textContent, "Critical message",
+ "The box must still display the more important notification");
+
+ ok(notificationBox.getCurrentNotification(),
+ "There must be current notification");
+
+ notificationBox.getNotificationWithValue("id1").close();
+ checkNumberOfNotifications(notificationBox, 1);
+
+ notificationBox.getNotificationWithValue("id2").close();
+ checkNumberOfNotifications(notificationBox, 1);
+
+ notificationBox.getNotificationWithValue("id3").close();
+ checkNumberOfNotifications(notificationBox, 0);
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+
+function checkNumberOfNotifications(notificationBox, expected) {
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, expected,
+ "The notification box must have expected number of notifications");
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_notification_box_02.html b/devtools/client/shared/components/test/mochitest/test_notification_box_02.html
new file mode 100644
index 000000000..ebeb0400d
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_notification_box_02.html
@@ -0,0 +1,70 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Using custom callback in a notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/notification-box");
+
+ // Test rendering
+ let boxElement = React.createElement(NotificationBox);
+ let notificationBox = TestUtils.renderIntoDocument(boxElement);
+ let notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ let callbackExecuted = false;
+
+ // Append a notification.
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_LOW,
+ undefined,
+ (reason) => {
+ callbackExecuted = true;
+ is(reason, "removed", "The reason must be expected string");
+ }
+ );
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 1,
+ "There must be one notification");
+
+ let closeButton = notificationNode.querySelector(
+ ".messageCloseButton");
+
+ // Click the close button to close the notification.
+ TestUtils.Simulate.click(closeButton);
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 0,
+ "The notification box must be empty now");
+
+ ok(callbackExecuted, "Event callback must be executed.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_notification_box_03.html b/devtools/client/shared/components/test/mochitest/test_notification_box_03.html
new file mode 100644
index 000000000..d7fc146fe
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_notification_box_03.html
@@ -0,0 +1,84 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Using custom buttons in a notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/notification-box");
+
+ // Test rendering
+ let boxElement = React.createElement(NotificationBox);
+ let notificationBox = TestUtils.renderIntoDocument(boxElement);
+ let notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ let buttonCallbackExecuted = false;
+ var buttons = [{
+ label: "Button1",
+ callback: () => {
+ buttonCallbackExecuted = true;
+
+ // Do not close the notification
+ return true;
+ },
+ }, {
+ label: "Button2",
+ callback: () => {
+ // Close the notification (return value undefined)
+ },
+ }];
+
+ // Append a notification with buttons.
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_LOW,
+ buttons
+ );
+
+ let buttonNodes = notificationNode.querySelectorAll(
+ ".notification-button");
+
+ is(buttonNodes.length, 2, "There must be two buttons");
+
+ // Click the first button
+ TestUtils.Simulate.click(buttonNodes[0]);
+ ok(buttonCallbackExecuted, "Button callback must be executed.");
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 1,
+ "There must be one notification");
+
+ // Click the second button (closing the notification)
+ TestUtils.Simulate.click(buttonNodes[1]);
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 0,
+ "The notification box must be empty now");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_array.html b/devtools/client/shared/components/test/mochitest/test_reps_array.html
new file mode 100644
index 000000000..6ad7f2c43
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_array.html
@@ -0,0 +1,259 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test ArrayRep rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - ArrayRep</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+/* import-globals-from head.js */
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { ArrayRep } = browserRequire("devtools/client/shared/components/reps/array");
+
+ let componentUnderTest = ArrayRep;
+ const maxLength = {
+ short: 3,
+ long: 300
+ };
+
+ try {
+ yield testBasic();
+
+ // Test property iterator
+ yield testMaxProps();
+ yield testMoreThanShortMaxProps();
+ yield testMoreThanLongMaxProps();
+ yield testRecursiveArray();
+
+ // Test that properties are rendered as expected by ItemRep
+ yield testNested();
+
+ yield testArray();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBasic() {
+ // Test that correct rep is chosen
+ const stub = [];
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ArrayRep.rep,
+ `Rep correctly selects ${ArrayRep.rep.displayName}`);
+
+
+ // Test rendering
+ const defaultOutput = `[]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testBasic", componentUnderTest, stub);
+ }
+
+ function testMaxProps() {
+ const stub = [1, "foo", {}];
+ const defaultOutput = `[ 1, "foo", Object ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[3]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMaxProps", componentUnderTest, stub);
+ }
+
+ function testMoreThanShortMaxProps() {
+ const stub = Array(maxLength.short + 1).fill("foo");
+ const defaultShortOutput = `[ ${Array(maxLength.short).fill("\"foo\"").join(", ")}, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[${maxLength.short + 1}]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: `[ ${Array(maxLength.short + 1).fill("\"foo\"").join(", ")} ]`,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMoreThanMaxProps", componentUnderTest, stub);
+ }
+
+ function testMoreThanLongMaxProps() {
+ const stub = Array(maxLength.long + 1).fill("foo");
+ const defaultShortOutput = `[ ${Array(maxLength.short).fill("\"foo\"").join(", ")}, ${maxLength.long + 1 - maxLength.short} more… ]`;
+ const defaultLongOutput = `[ ${Array(maxLength.long).fill("\"foo\"").join(", ")}, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[${maxLength.long + 1}]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultLongOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMoreThanMaxProps", componentUnderTest, stub);
+ }
+
+ function testRecursiveArray() {
+ let stub = [1];
+ stub.push(stub);
+ const defaultOutput = `[ 1, [2] ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[2]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testRecursiveArray", componentUnderTest, stub);
+ }
+
+ function testNested() {
+ let stub = [
+ {
+ p1: "s1",
+ p2: ["a1", "a2", "a3"],
+ p3: "s3",
+ p4: "s4"
+ }
+ ];
+ const defaultOutput = `[ Object ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[1]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testNested", componentUnderTest, stub);
+ }
+
+ function testArray() {
+ let stub = [
+ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+ "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
+ ];
+
+ const defaultOutput = `[ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",` +
+ ` "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",` +
+ ` "u", "v", "w", "x", "y", "z" ]`;
+ const shortOutput = `[ "a", "b", "c", 23 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: shortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[26]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: shortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testNested", componentUnderTest, stub);
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_attribute.html b/devtools/client/shared/components/test/mochitest/test_reps_attribute.html
new file mode 100644
index 000000000..aa8a5dfad
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_attribute.html
@@ -0,0 +1,56 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Attribute rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Attribute</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Attribute } = browserRequire("devtools/client/shared/components/reps/attribute");
+
+ let gripStub = {
+ "type": "object",
+ "class": "Attr",
+ "actor": "server1.conn19.obj65",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "class",
+ "value": "autocomplete-suggestions"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Attribute.rep, `Rep correctly selects ${Attribute.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Attribute.rep, { object: gripStub });
+ is(renderedComponent.textContent, "class=\"autocomplete-suggestions\"", "Attribute rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_comment-node.html b/devtools/client/shared/components/test/mochitest/test_reps_comment-node.html
new file mode 100644
index 000000000..4e03d8b30
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_comment-node.html
@@ -0,0 +1,80 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test comment-node rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - comment-node</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { CommentNode } = browserRequire("devtools/client/shared/components/reps/comment-node");
+
+ let gripStub = {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj47",
+ "class": "Comment",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 8,
+ "nodeName": "#comment",
+ "textContent": "test\nand test\nand test\nand test\nand test\nand test\nand test"
+ }
+ };
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, CommentNode.rep,
+ `Rep correctly selects ${CommentNode.rep.displayName}`);
+
+ // Test rendering.
+ const renderedComponent = renderComponent(CommentNode.rep, { object: gripStub });
+ is(renderedComponent.className, "objectBox theme-comment",
+ "CommentNode rep has expected class names");
+ is(renderedComponent.textContent,
+ `<!-- test\nand test\nand test\nan…d test\nand test\nand test -->`,
+ "CommentNode rep has expected text content");
+
+ // Test tiny rendering.
+ const tinyRenderedComponent = renderComponent(CommentNode.rep, {
+ object: gripStub,
+ mode: "tiny"
+ });
+ is(tinyRenderedComponent.textContent,
+ `<!-- test\\nand test\\na… test\\nand test -->`,
+ "CommentNode rep has expected text content in tiny mode");
+
+ // Test long rendering.
+ const longRenderedComponent = renderComponent(CommentNode.rep, {
+ object: gripStub,
+ mode: "long"
+ });
+ is(longRenderedComponent.textContent, `<!-- ${gripStub.preview.textContent} -->`,
+ "CommentNode rep has expected text content in long mode");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_date-time.html b/devtools/client/shared/components/test/mochitest/test_reps_date-time.html
new file mode 100644
index 000000000..a82783b6b
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_date-time.html
@@ -0,0 +1,79 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test DateTime rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - DateTime</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { DateTime } = browserRequire("devtools/client/shared/components/reps/date-time");
+
+ try {
+ testValid();
+ testInvalid();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testValid() {
+ let gripStub = {
+ "type": "object",
+ "class": "Date",
+ "actor": "server1.conn0.child1/obj32",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "timestamp": 1459372644859
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, DateTime.rep, `Rep correctly selects ${DateTime.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(DateTime.rep, { object: gripStub });
+ is(renderedComponent.textContent, "2016-03-30T21:17:24.859Z", "DateTime rep has expected text content for valid date");
+ }
+
+ function testInvalid() {
+ let gripStub = {
+ "type": "object",
+ "actor": "server1.conn0.child1/obj32",
+ "class": "Date",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "timestamp": {
+ "type": "NaN"
+ }
+ }
+ };
+
+ // Test rendering
+ const renderedComponent = renderComponent(DateTime.rep, { object: gripStub });
+ is(renderedComponent.textContent, "Invalid Date", "DateTime rep has expected text content for invalid date");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_document.html b/devtools/client/shared/components/test/mochitest/test_reps_document.html
new file mode 100644
index 000000000..2afabca44
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_document.html
@@ -0,0 +1,56 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Document rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Document</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Document } = browserRequire("devtools/client/shared/components/reps/document");
+
+ try {
+ let gripStub = {
+ "type": "object",
+ "class": "HTMLDocument",
+ "actor": "server1.conn17.obj115",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 9,
+ "nodeName": "#document",
+ "location": "https://www.mozilla.org/en-US/firefox/new/"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Document.rep, `Rep correctly selects ${Document.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Document.rep, { object: gripStub });
+ is(renderedComponent.textContent, "https://www.mozilla.org/en-US/firefox/new/", "Document rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_element-node.html b/devtools/client/shared/components/test/mochitest/test_reps_element-node.html
new file mode 100644
index 000000000..d4e22c7ab
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_element-node.html
@@ -0,0 +1,341 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Element node rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Element node</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { ElementNode } = browserRequire("devtools/client/shared/components/reps/element-node");
+
+ try {
+ yield testBodyNode();
+ yield testDocumentElement();
+ yield testNode();
+ yield testNodeWithLeadingAndTrailingSpacesClassName();
+ yield testNodeWithoutAttributes();
+ yield testLotsOfAttributes();
+ yield testSvgNode();
+ yield testSvgNodeInXHTML();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBodyNode() {
+ const stub = getGripStub("testBodyNode");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for body node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent, `<body id="body-id" class="body-class">`,
+ "Element node rep has expected text content for body node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `body#body-id.body-class`,
+ "Element node rep has expected text content for body node in tiny mode");
+ }
+
+ function testDocumentElement() {
+ const stub = getGripStub("testDocumentElement");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for document element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent, `<html dir="ltr" lang="en-US">`,
+ "Element node rep has expected text content for document element node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `html`,
+ "Element node rep has expected text content for document element in tiny mode");
+ }
+
+ function testNode() {
+ const stub = getGripStub("testNode");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ `<input id="newtab-customize-button" class="bar baz" dir="ltr" ` +
+ `title="Customize your New Tab page" value="foo" type="button">`,
+ "Element node rep has expected text content for element node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent,
+ `input#newtab-customize-button.bar.baz`,
+ "Element node rep has expected text content for element node in tiny mode");
+ }
+
+ function testNodeWithLeadingAndTrailingSpacesClassName() {
+ const stub = getGripStub("testNodeWithLeadingAndTrailingSpacesClassName");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ `<body id="nightly-whatsnew" class=" html-ltr ">`,
+ "Element node rep output element node with the class trailing spaces");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent,
+ `body#nightly-whatsnew.html-ltr`,
+ "Element node rep does not show leading nor trailing spaces " +
+ "on class attribute in tiny mode");
+ }
+
+ function testNodeWithoutAttributes() {
+ const stub = getGripStub("testNodeWithoutAttributes");
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent, "<p>",
+ "Element node rep has expected text content for element node without attributes");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `p`,
+ "Element node rep has expected text content for element node without attributes");
+ }
+
+ function testLotsOfAttributes() {
+ const stub = getGripStub("testLotsOfAttributes");
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ '<p id="lots-of-attributes" a="" b="" c="" d="" e="" f="" g="" ' +
+ 'h="" i="" j="" k="" l="" m="" n="">',
+ "Element node rep has expected text content for node with lots of attributes");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `p#lots-of-attributes`,
+ "Element node rep has expected text content for node in tiny mode");
+ }
+
+ function testSvgNode() {
+ const stub = getGripStub("testSvgNode");
+
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for SVG element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ '<clipPath id="clip" class="svg-element">',
+ "Element node rep has expected text content for SVG element node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `clipPath#clip.svg-element`,
+ "Element node rep has expected text content for SVG element node in tiny mode");
+ }
+
+ function testSvgNodeInXHTML() {
+ const stub = getGripStub("testSvgNodeInXHTML");
+
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for XHTML SVG element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ '<svg:circle class="svg-element" cx="0" cy="0" r="5">',
+ "Element node rep has expected text content for XHTML SVG element node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `svg:circle.svg-element`,
+ "Element node rep has expected text content for XHTML SVG element in tiny mode");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testBodyNode":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj30",
+ "class": "HTMLBodyElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "body",
+ "attributes": {
+ "class": "body-class",
+ "id": "body-id"
+ },
+ "attributesLength": 2
+ }
+ };
+ case "testDocumentElement":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj40",
+ "class": "HTMLHtmlElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "html",
+ "attributes": {
+ "dir": "ltr",
+ "lang": "en-US"
+ },
+ "attributesLength": 2
+ }
+ };
+ case "testNode":
+ return {
+ "type": "object",
+ "actor": "server1.conn2.child1/obj116",
+ "class": "HTMLInputElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "input",
+ "attributes": {
+ "id": "newtab-customize-button",
+ "dir": "ltr",
+ "title": "Customize your New Tab page",
+ "class": "bar baz",
+ "value": "foo",
+ "type": "button"
+ },
+ "attributesLength": 6
+ }
+ };
+ case "testNodeWithLeadingAndTrailingSpacesClassName":
+ return {
+ "type": "object",
+ "actor": "server1.conn3.child1/obj59",
+ "class": "HTMLBodyElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "body",
+ "attributes": {
+ "id": "nightly-whatsnew",
+ "class": " html-ltr "
+ },
+ "attributesLength": 2
+ }
+ };
+ case "testNodeWithoutAttributes":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj32",
+ "class": "HTMLParagraphElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "p",
+ "attributes": {},
+ "attributesLength": 1
+ }
+ };
+ case "testLotsOfAttributes":
+ return {
+ "type": "object",
+ "actor": "server1.conn2.child1/obj30",
+ "class": "HTMLParagraphElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "p",
+ "attributes": {
+ "id": "lots-of-attributes",
+ "a": "",
+ "b": "",
+ "c": "",
+ "d": "",
+ "e": "",
+ "f": "",
+ "g": "",
+ "h": "",
+ "i": "",
+ "j": "",
+ "k": "",
+ "l": "",
+ "m": "",
+ "n": ""
+ },
+ "attributesLength": 15
+ }
+ };
+ case "testSvgNode":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj42",
+ "class": "SVGClipPathElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "clipPath",
+ "attributes": {
+ "id": "clip",
+ "class": "svg-element"
+ },
+ "attributesLength": 0
+ }
+ };
+ case "testSvgNodeInXHTML":
+ return {
+ "type": "object",
+ "actor": "server1.conn3.child1/obj34",
+ "class": "SVGCircleElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "svg:circle",
+ "attributes": {
+ "class": "svg-element",
+ "cx": "0",
+ "cy": "0",
+ "r": "5"
+ },
+ "attributesLength": 3
+ }
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_event.html b/devtools/client/shared/components/test/mochitest/test_reps_event.html
new file mode 100644
index 000000000..7dfe72d6f
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_event.html
@@ -0,0 +1,300 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Event rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Event</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Event } = browserRequire("devtools/client/shared/components/reps/event");
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testEvent") });
+ is(renderedRep.type, Event.rep, `Rep correctly selects ${Event.rep.displayName}`);
+
+ yield testEvent();
+ yield testMouseEvent();
+ yield testKeyboardEvent();
+ yield testMessageEvent();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testEvent() {
+ const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testEvent") });
+ is(renderedComponent.textContent,
+ "Event { isTrusted: true, eventPhase: 2, bubbles: false, 7 more… }",
+ "Event rep has expected text content for an event");
+ }
+
+ function testMouseEvent() {
+ const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testMouseEvent") });
+ is(renderedComponent.textContent,
+ "MouseEvent { clientX: 62, clientY: 18, layerX: 0, 2 more… }",
+ "Event rep has expected text content for a mouse event");
+ }
+
+ function testKeyboardEvent() {
+ const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testKeyboardEvent") });
+ is(renderedComponent.textContent,
+ "KeyboardEvent { key: \"Control\", charCode: 0, keyCode: 17 }",
+ "Event rep has expected text content for a keyboard event");
+ }
+
+ function testMessageEvent() {
+ const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testMessageEvent") });
+ is(renderedComponent.textContent,
+ "MessageEvent { isTrusted: false, data: \"test data\", origin: \"null\", 7 more… }",
+ "Event rep has expected text content for a message event");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testEvent":
+ return {
+ "type": "object",
+ "class": "Event",
+ "actor": "server1.conn23.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMEvent",
+ "type": "beforeprint",
+ "properties": {
+ "isTrusted": true,
+ "currentTarget": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn23.obj37",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com"
+ }
+ },
+ "eventPhase": 2,
+ "bubbles": false,
+ "cancelable": false,
+ "defaultPrevented": false,
+ "timeStamp": 1466780008434005,
+ "originalTarget": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn23.obj38",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com"
+ }
+ },
+ "explicitOriginalTarget": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn23.obj39",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com"
+ }
+ },
+ "NONE": 0
+ },
+ "target": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn23.obj36",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com"
+ }
+ }
+ }
+ };
+
+ case "testMouseEvent":
+ return {
+ "type": "object",
+ "class": "MouseEvent",
+ "actor": "server1.conn20.obj39",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMEvent",
+ "type": "click",
+ "properties": {
+ "buttons": 0,
+ "clientX": 62,
+ "clientY": 18,
+ "layerX": 0,
+ "layerY": 0
+ },
+ "target": {
+ "type": "object",
+ "class": "HTMLDivElement",
+ "actor": "server1.conn20.obj40",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "div",
+ "attributes": {
+ "id": "test"
+ },
+ "attributesLength": 1
+ }
+ }
+ }
+ };
+
+ case "testKeyboardEvent":
+ return {
+ "type": "object",
+ "class": "KeyboardEvent",
+ "actor": "server1.conn21.obj49",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMEvent",
+ "type": "keyup",
+ "properties": {
+ "key": "Control",
+ "charCode": 0,
+ "keyCode": 17
+ },
+ "target": {
+ "type": "object",
+ "class": "HTMLBodyElement",
+ "actor": "server1.conn21.obj50",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "body",
+ "attributes": {},
+ "attributesLength": 0
+ }
+ },
+ "eventKind": "key",
+ "modifiers": []
+ }
+ };
+
+ case "testMessageEvent":
+ return {
+ "type": "object",
+ "class": "MessageEvent",
+ "actor": "server1.conn3.obj34",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMEvent",
+ "type": "message",
+ "properties": {
+ "isTrusted": false,
+ "data": "test data",
+ "origin": "null",
+ "lastEventId": "",
+ "source": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn3.obj36",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": ""
+ }
+ },
+ "ports": {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn3.obj37",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0
+ },
+ "currentTarget": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn3.obj38",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": ""
+ }
+ },
+ "eventPhase": 2,
+ "bubbles": false,
+ "cancelable": false
+ },
+ "target": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn3.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": ""
+ }
+ }
+ }
+ };
+
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_function.html b/devtools/client/shared/components/test/mochitest/test_reps_function.html
new file mode 100644
index 000000000..ede694329
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_function.html
@@ -0,0 +1,206 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Func rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Func</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Func } = browserRequire("devtools/client/shared/components/reps/function");
+
+ const componentUnderTest = Func;
+
+ try {
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testNamed");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Func.rep, `Rep correctly selects ${Func.rep.displayName}`);
+
+ yield testNamed();
+ yield testVarNamed();
+ yield testAnon();
+ yield testLongName();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testNamed() {
+ // Test declaration: `function testName{ let innerVar = "foo" }`
+ const testName = "testNamed";
+
+ const defaultOutput = `testName()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testUserNamed() {
+ // Test declaration: `function testName{ let innerVar = "foo" }`
+ const testName = "testUserNamed";
+
+ const defaultOutput = `testUserName()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testVarNamed() {
+ // Test declaration: `let testVarName = function() { }`
+ const testName = "testVarNamed";
+
+ const defaultOutput = `testVarName()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testAnon() {
+ // Test declaration: `() => {}`
+ const testName = "testAnon";
+
+ const defaultOutput = `function()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testLongName() {
+ // Test declaration: `let f = function loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong() { }`
+ const testName = "testLongName";
+
+ const defaultOutput = `looooooooooooooooooooooooooooooooooooooooooooooooo\u2026ooooooooooooooooooooooooooooooooooooooooooooong()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testNamed":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn6.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "name": "testName",
+ "displayName": "testName",
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+
+ case "testUserNamed":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn6.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "name": "testName",
+ "userDisplayName": "testUserName",
+ "displayName": "testName",
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+
+ case "testVarNamed":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn7.obj41",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "displayName": "testVarName",
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+
+ case "testAnon":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn7.obj45",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+
+ case "testLongName":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn7.obj67",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "name": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
+ "displayName": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
new file mode 100644
index 000000000..db4f0296e
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
@@ -0,0 +1,707 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test GripArray rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - GripArray</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { GripArray } = browserRequire("devtools/client/shared/components/reps/grip-array");
+
+ let componentUnderTest = GripArray;
+ const maxLength = {
+ short: 3,
+ long: 300
+ };
+
+ try {
+ yield testBasic();
+
+ // Test property iterator
+ yield testMaxProps();
+ yield testMoreThanShortMaxProps();
+ yield testMoreThanLongMaxProps();
+ yield testRecursiveArray();
+ yield testPreviewLimit();
+ yield testNamedNodeMap();
+ yield testNodeList();
+ yield testDocumentFragment();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBasic() {
+ // Test array: `[]`
+ const testName = "testBasic";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testBasic");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, GripArray.rep, `Rep correctly selects ${GripArray.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Array []`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMaxProps() {
+ // Test array: `[1, "foo", {}]`;
+ const testName = "testMaxProps";
+
+ const defaultOutput = `Array [ 1, "foo", Object ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[3]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanShortMaxProps() {
+ // Test array = `["test string"…] //4 items`
+ const testName = "testMoreThanShortMaxProps";
+
+ const defaultOutput = `Array [ ${Array(maxLength.short).fill("\"test string\"").join(", ")}, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[${maxLength.short + 1}]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: `Array [ ${Array(maxLength.short + 1).fill("\"test string\"").join(", ")} ]`,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanLongMaxProps() {
+ // Test array = `["test string"…] //301 items`
+ const testName = "testMoreThanLongMaxProps";
+
+ const defaultShortOutput = `Array [ ${Array(maxLength.short).fill("\"test string\"").join(", ")}, ${maxLength.long + 1 - maxLength.short} more… ]`;
+ const defaultLongOutput = `Array [ ${Array(maxLength.long).fill("\"test string\"").join(", ")}, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[${maxLength.long + 1}]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultLongOutput
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testRecursiveArray() {
+ // Test array = `let a = []; a = [a]`
+ const testName = "testRecursiveArray";
+
+ const defaultOutput = `Array [ [1] ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[1]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testPreviewLimit() {
+ const testName = "testPreviewLimit";
+
+ const shortOutput = `Array [ 0, 1, 2, 8 more… ]`;
+ const defaultOutput = `Array [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: shortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[11]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: shortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNamedNodeMap() {
+ const testName = "testNamedNodeMap";
+
+ const defaultOutput = `NamedNodeMap [ class="myclass", cellpadding="7", border="3" ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[3]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNodeList() {
+ const testName = "testNodeList";
+ const defaultOutput = "NodeList [ button#btn-1.btn.btn-log, " +
+ "button#btn-2.btn.btn-err, button#btn-3.btn.btn-count ]";
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[3]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testDocumentFragment() {
+ const testName = "testDocumentFragment";
+
+ const defaultOutput = "DocumentFragment [ li#li-0.list-element, " +
+ "li#li-1.list-element, li#li-2.list-element, 2 more… ]";
+
+ const longOutput = "DocumentFragment [ " +
+ "li#li-0.list-element, li#li-1.list-element, li#li-2.list-element, " +
+ "li#li-3.list-element, li#li-4.list-element ]";
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[5]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testBasic":
+ return {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn0.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 0,
+ "items": []
+ }
+ };
+
+ case "testMaxProps":
+ return {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn1.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ 1,
+ "foo",
+ {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn1.obj36",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0
+ }
+ ]
+ }
+ };
+
+ case "testMoreThanShortMaxProps":
+ let shortArrayGrip = {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn1.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": maxLength.short + 1,
+ "items": []
+ }
+ };
+
+ // Generate array grip with length 4, which is more that the maximum
+ // limit in case of the 'short' mode.
+ for (let i = 0; i < maxLength.short + 1; i++) {
+ shortArrayGrip.preview.items.push("test string");
+ }
+
+ return shortArrayGrip;
+
+ case "testMoreThanLongMaxProps":
+ let longArrayGrip = {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn1.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": maxLength.long + 1,
+ "items": []
+ }
+ };
+
+ // Generate array grip with length 301, which is more that the maximum
+ // limit in case of the 'long' mode.
+ for (let i = 0; i < maxLength.long + 1; i++) {
+ longArrayGrip.preview.items.push("test string");
+ }
+
+ return longArrayGrip;
+
+ case "testPreviewLimit":
+ return {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn1.obj31",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 12,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 11,
+ "items": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+ }
+ };
+
+ case "testRecursiveArray":
+ return {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn3.obj42",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 2,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 1,
+ "items": [
+ {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn3.obj43",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 2,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 1
+ }
+ }
+ ]
+ }
+ };
+
+ case "testNamedNodeMap":
+ return {
+ "type": "object",
+ "class": "NamedNodeMap",
+ "actor": "server1.conn3.obj42",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 6,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ {
+ "type": "object",
+ "class": "Attr",
+ "actor": "server1.conn3.obj43",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "class",
+ "value": "myclass"
+ }
+ },
+ {
+ "type": "object",
+ "class": "Attr",
+ "actor": "server1.conn3.obj44",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "cellpadding",
+ "value": "7"
+ }
+ },
+ {
+ "type": "object",
+ "class": "Attr",
+ "actor": "server1.conn3.obj44",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "border",
+ "value": "3"
+ }
+ }
+ ]
+ }
+ };
+
+ case "testNodeList":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj51",
+ "class": "NodeList",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 3,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj52",
+ "class": "HTMLButtonElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "button",
+ "attributes": {
+ "id": "btn-1",
+ "class": "btn btn-log",
+ "type": "button"
+ },
+ "attributesLength": 3
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj53",
+ "class": "HTMLButtonElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "button",
+ "attributes": {
+ "id": "btn-2",
+ "class": "btn btn-err",
+ "type": "button"
+ },
+ "attributesLength": 3
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj54",
+ "class": "HTMLButtonElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "button",
+ "attributes": {
+ "id": "btn-3",
+ "class": "btn btn-count",
+ "type": "button"
+ },
+ "attributesLength": 3
+ }
+ }
+ ]
+ }
+ };
+
+ case "testDocumentFragment":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj45",
+ "class": "DocumentFragment",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 11,
+ "nodeName": "#document-fragment",
+ "childNodesLength": 5,
+ "childNodes": [
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj46",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-0",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj47",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-1",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj48",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-2",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj49",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-3",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj50",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-4",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ }
+ ]
+ }
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_grip-map.html b/devtools/client/shared/components/test/mochitest/test_reps_grip-map.html
new file mode 100644
index 000000000..18470367c
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-map.html
@@ -0,0 +1,405 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test GripMap rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - GripMap</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { GripMap } = browserRequire("devtools/client/shared/components/reps/grip-map");
+
+ const componentUnderTest = GripMap;
+
+ try {
+ yield testEmptyMap();
+ yield testSymbolKeyedMap();
+ yield testWeakMap();
+
+ // // Test entries iterator
+ yield testMaxEntries();
+ yield testMoreThanMaxEntries();
+ yield testUninterestingEntries();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testEmptyMap() {
+ // Test object: `new Map()`
+ const testName = "testEmptyMap";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testEmptyMap");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, GripMap.rep, `Rep correctly selects ${GripMap.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Map { }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "Map",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testSymbolKeyedMap() {
+ // Test object:
+ // `new Map([[Symbol("a"), "value-a"], [Symbol("b"), "value-b"]])`
+ const testName = "testSymbolKeyedMap";
+
+ const defaultOutput = `Map { Symbol(a): "value-a", Symbol(b): "value-b" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "Map",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testWeakMap() {
+ // Test object: `new WeakMap([[{a: "key-a"}, "value-a"]])`
+ const testName = "testWeakMap";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testWeakMap");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, GripMap.rep, `Rep correctly selects ${GripMap.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `WeakMap { Object: "value-a" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "WeakMap",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMaxEntries() {
+ // Test object:
+ // `new Map([["key-a","value-a"], ["key-b","value-b"], ["key-c","value-c"]])`
+ const testName = "testMaxEntries";
+
+ const defaultOutput = `Map { key-a: "value-a", key-b: "value-b", key-c: "value-c" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "Map",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanMaxEntries() {
+ // Test object = `new Map(
+ // [["key-0", "value-0"], ["key-1", "value-1"]], …, ["key-100", "value-100"]]}`
+ const testName = "testMoreThanMaxEntries";
+
+ const defaultOutput =
+ `Map { key-0: "value-0", key-1: "value-1", key-2: "value-2", 98 more… }`;
+
+ // Generate string with 101 entries, which is the max limit for 'long' mode.
+ let longString = Array.from({length: 100}).map((_, i) => `key-${i}: "value-${i}"`);
+ const longOutput = `Map { ${longString.join(", ")}, 1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Map`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testUninterestingEntries() {
+ // Test object:
+ // `new Map([["key-a",null], ["key-b",undefined], ["key-c","value-c"], ["key-d",4]])`
+ const testName = "testUninterestingEntries";
+
+ const defaultOutput =
+ `Map { key-a: null, key-c: "value-c", key-d: 4, 1 more… }`;
+ const longOutput =
+ `Map { key-a: null, key-b: undefined, key-c: "value-c", key-d: 4 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Map`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testEmptyMap":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj97",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 0,
+ "entries": []
+ }
+ };
+
+ case "testSymbolKeyedMap":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj118",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 2,
+ "entries": [
+ [
+ {
+ "type": "symbol",
+ "name": "a"
+ },
+ "value-a"
+ ],
+ [
+ {
+ "type": "symbol",
+ "name": "b"
+ },
+ "value-b"
+ ]
+ ]
+ }
+ };
+
+ case "testWeakMap":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj115",
+ "class": "WeakMap",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 1,
+ "entries": [
+ [
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj116",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1
+ },
+ "value-a"
+ ]
+ ]
+ }
+ };
+
+ case "testMaxEntries":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj109",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 3,
+ "entries": [
+ [
+ "key-a",
+ "value-a"
+ ],
+ [
+ "key-b",
+ "value-b"
+ ],
+ [
+ "key-c",
+ "value-c"
+ ]
+ ]
+ }
+ };
+
+ case "testMoreThanMaxEntries": {
+ let entryNb = 101;
+ return {
+ "type": "object",
+ "class": "Map",
+ "actor": "server1.conn0.obj332",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": entryNb,
+ // Generate 101 entries, which is more that the maximum
+ // limit in case of the 'long' mode.
+ "entries": Array.from({length: entryNb}).map((_, i) => {
+ return [`key-${i}`, `value-${i}`];
+ })
+ }
+ };
+ }
+
+ case "testUninterestingEntries":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj111",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 4,
+ "entries": [
+ [
+ "key-a",
+ {
+ "type": "null"
+ }
+ ],
+ [
+ "key-b",
+ {
+ "type": "undefined"
+ }
+ ],
+ [
+ "key-c",
+ "value-c"
+ ],
+ [
+ "key-d",
+ 4
+ ]
+ ]
+ }
+ };
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_grip.html b/devtools/client/shared/components/test/mochitest/test_reps_grip.html
new file mode 100644
index 000000000..15d4e1d25
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip.html
@@ -0,0 +1,887 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test grip rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - grip</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Grip } = browserRequire("devtools/client/shared/components/reps/grip");
+
+ const componentUnderTest = Grip;
+
+ try {
+ yield testBasic();
+ yield testBooleanObject();
+ yield testNumberObject();
+ yield testStringObject();
+ yield testProxy();
+ yield testArrayBuffer();
+ yield testSharedArrayBuffer();
+
+ // Test property iterator
+ yield testMaxProps();
+ yield testMoreThanMaxProps();
+ yield testUninterestingProps();
+ yield testNonEnumerableProps();
+
+ // Test that properties are rendered as expected by PropRep
+ yield testNestedObject();
+ yield testNestedArray();
+
+ // Test that 'more' property doesn't clobber the caption.
+ yield testMoreProp();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBasic() {
+ // Test object: `{}`
+ const testName = "testBasic";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testBasic");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Object { }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testBooleanObject() {
+ // Test object: `new Boolean(true)`
+ const testName = "testBooleanObject";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Boolean { true }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Boolean`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNumberObject() {
+ // Test object: `new Number(42)`
+ const testName = "testNumberObject";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Number { 42 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Number`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testStringObject() {
+ // Test object: `new String("foo")`
+ const testName = "testStringObject";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `String { "foo" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `String`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testProxy() {
+ // Test object: `new Proxy({a:1},[1,2,3])`
+ const testName = "testProxy";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Proxy { <target>: Object, <handler>: [3] }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Proxy`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testArrayBuffer() {
+ // Test object: `new ArrayBuffer(10)`
+ const testName = "testArrayBuffer";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `ArrayBuffer { byteLength: 10 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `ArrayBuffer`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testSharedArrayBuffer() {
+ // Test object: `new SharedArrayBuffer(5)`
+ const testName = "testSharedArrayBuffer";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `SharedArrayBuffer { byteLength: 5 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `SharedArrayBuffer`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMaxProps() {
+ // Test object: `{a: "a", b: "b", c: "c"}`;
+ const testName = "testMaxProps";
+
+ const defaultOutput = `Object { a: "a", b: "b", c: "c" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanMaxProps() {
+ // Test object = `{p0: "0", p1: "1", p2: "2", …, p100: "100"}`
+ const testName = "testMoreThanMaxProps";
+
+ const defaultOutput = `Object { p0: "0", p1: "1", p2: "2", 98 more… }`;
+
+ // Generate string with 100 properties, which is the max limit
+ // for 'long' mode.
+ let props = "";
+ for (let i = 0; i < 100; i++) {
+ props += "p" + i + ": \"" + i + "\", ";
+ }
+
+ const longOutput = `Object { ${props}1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testUninterestingProps() {
+ // Test object: `{a: undefined, b: undefined, c: "c", d: 1}`
+ // @TODO This is not how we actually want the preview to be output.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1276376
+ const expectedOutput = `Object { a: undefined, b: undefined, c: "c", 1 more… }`;
+ }
+
+ function testNonEnumerableProps() {
+ // Test object: `Object.defineProperty({}, "foo", {enumerable : false});`
+ const testName = "testNonEnumerableProps";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testNonEnumerableProps");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Object { }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNestedObject() {
+ // Test object: `{objProp: {id: 1}, strProp: "test string"}`
+ const testName = "testNestedObject";
+
+ const defaultOutput = `Object { objProp: Object, strProp: "test string" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNestedArray() {
+ // Test object: `{arrProp: ["foo", "bar", "baz"]}`
+ const testName = "testNestedArray";
+
+ const defaultOutput = `Object { arrProp: [3] }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreProp() {
+ // Test object: `{a: undefined, b: 1, more: 2, d: 3}`;
+ const testName = "testMoreProp";
+
+ const defaultOutput = `Object { b: 1, more: 2, d: 3, 1 more… }`;
+ const longOutput = `Object { a: undefined, b: 1, more: 2, d: 3 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testBasic":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj304",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+
+ case "testMaxProps":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj337",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 3,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "a": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "a"
+ },
+ "b": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "b"
+ },
+ "c": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "c"
+ }
+ },
+ "ownPropertiesLength": 3,
+ "safeGetterValues": {}
+ }
+ };
+
+ case "testMoreThanMaxProps": {
+ let grip = {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj332",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 101,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 101,
+ "safeGetterValues": {}
+ }
+ };
+
+ // Generate 101 properties, which is more that the maximum
+ // limit in case of the 'long' mode.
+ for (let i = 0; i < 101; i++) {
+ grip.preview.ownProperties["p" + i] = {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": i + ""
+ };
+ }
+
+ return grip;
+ }
+
+ case "testUninterestingProps":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj342",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "a": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "undefined"
+ }
+ },
+ "b": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "undefined"
+ }
+ },
+ "c": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "c"
+ },
+ "d": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 1
+ }
+ },
+ "ownPropertiesLength": 4,
+ "safeGetterValues": {}
+ }
+ };
+ case "testNonEnumerableProps":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj30",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 1,
+ "safeGetterValues": {}
+ }
+ };
+ case "testNestedObject":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj145",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 2,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "objProp": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj146",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1
+ }
+ },
+ "strProp": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "test string"
+ }
+ },
+ "ownPropertiesLength": 2,
+ "safeGetterValues": {}
+ }
+ };
+
+ case "testNestedArray":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj326",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "arrProp": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn0.obj327",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3
+ }
+ }
+ }
+ },
+ "ownPropertiesLength": 1,
+ "safeGetterValues": {}
+ },
+ };
+
+ case "testMoreProp":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj342",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "a": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "undefined"
+ }
+ },
+ "b": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 1
+ },
+ "more": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 2
+ },
+ "d": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 3
+ }
+ },
+ "ownPropertiesLength": 4,
+ "safeGetterValues": {}
+ }
+ };
+ case "testBooleanObject":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj57",
+ "class": "Boolean",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {},
+ "wrappedValue": true
+ }
+ };
+ case "testNumberObject":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj59",
+ "class": "Number",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {},
+ "wrappedValue": 42
+ }
+ };
+ case "testStringObject":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj61",
+ "class": "String",
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 4,
+ "safeGetterValues": {},
+ "wrappedValue": "foo"
+ }
+ };
+ case "testProxy":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj47",
+ "class": "Proxy",
+ "proxyTarget": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj48",
+ "class": "Object",
+ "ownPropertyLength": 1
+ },
+ "proxyHandler": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj49",
+ "class": "Array",
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3
+ }
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "<target>": {
+ "value": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj48",
+ "class": "Object",
+ "ownPropertyLength": 1
+ }
+ },
+ "<handler>": {
+ "value": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj49",
+ "class": "Array",
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3
+ }
+ }
+ }
+ },
+ "ownPropertiesLength": 2
+ }
+ };
+ case "testArrayBuffer":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj170",
+ "class": "ArrayBuffer",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {
+ "byteLength": {
+ "getterValue": 10,
+ "getterPrototypeLevel": 1,
+ "enumerable": false,
+ "writable": true
+ }
+ }
+ }
+ };
+ case "testSharedArrayBuffer":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj171",
+ "class": "SharedArrayBuffer",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {
+ "byteLength": {
+ "getterValue": 5,
+ "getterPrototypeLevel": 1,
+ "enumerable": false,
+ "writable": true
+ }
+ }
+ }
+ };
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_infinity.html b/devtools/client/shared/components/test/mochitest/test_reps_infinity.html
new file mode 100644
index 000000000..e3a7e871f
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_infinity.html
@@ -0,0 +1,73 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Infinity rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Infinity</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { InfinityRep } = browserRequire("devtools/client/shared/components/reps/infinity");
+
+ try {
+ yield testInfinity();
+ yield testNegativeInfinity();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testInfinity() {
+ const stub = getGripStub("testInfinity");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, InfinityRep.rep,
+ `Rep correctly selects ${InfinityRep.rep.displayName} for Infinity value`);
+
+ const renderedComponent = renderComponent(InfinityRep.rep, { object: stub });
+ is(renderedComponent.textContent, "Infinity",
+ "Infinity rep has expected text content for Infinity");
+ }
+
+ function testNegativeInfinity() {
+ const stub = getGripStub("testNegativeInfinity");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, InfinityRep.rep,
+ `Rep correctly selects ${InfinityRep.rep.displayName} for negative Infinity value`);
+
+ const renderedComponent = renderComponent(InfinityRep.rep, { object: stub });
+ is(renderedComponent.textContent, "-Infinity",
+ "Infinity rep has expected text content for negative Infinity");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testInfinity":
+ return {
+ type: "Infinity"
+ };
+ case "testNegativeInfinity":
+ return {
+ type: "-Infinity"
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_long-string.html b/devtools/client/shared/components/test/mochitest/test_reps_long-string.html
new file mode 100644
index 000000000..3caaac913
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_long-string.html
@@ -0,0 +1,125 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test LongString rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - LongString</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { LongStringRep } = browserRequire("devtools/client/shared/components/reps/long-string");
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testMultiline") });
+ is(renderedRep.type, LongStringRep.rep,
+ `Rep correctly selects ${LongStringRep.rep.displayName}`);
+
+ // Test rendering
+ yield testMultiline();
+ yield testMultilineOpen();
+ yield testFullText();
+ yield testMultilineLimit();
+ yield testUseQuotes();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testMultiline() {
+ const stub = getGripStub("testMultiline");
+ const renderedComponent = renderComponent(
+ LongStringRep.rep, { object: stub });
+
+ is(renderedComponent.textContent, `"${stub.initial}…"`,
+ "LongString rep has expected text content for multiline string");
+ }
+
+ function testMultilineLimit() {
+ const renderedComponent = renderComponent(
+ LongStringRep.rep, { object: getGripStub("testMultiline"), cropLimit: 20 });
+
+ is(
+ renderedComponent.textContent,
+ `"a\naaaaaaaaaaaaaaaaaa…"`,
+ "LongString rep has expected text content for multiline string " +
+ "with specified number of characters");
+ }
+
+ function testMultilineOpen() {
+ const stub = getGripStub("testMultiline");
+ const renderedComponent = renderComponent(
+ LongStringRep.rep, { object: stub, member: {open: true}, cropLimit: 20 });
+
+ is(renderedComponent.textContent, `"${stub.initial}…"`,
+ "LongString rep has expected text content for multiline string when open");
+ }
+
+ function testFullText() {
+ const stub = getGripStub("testFullText");
+ const renderedComponentOpen = renderComponent(
+ LongStringRep.rep, { object: stub, member: {open: true}, cropLimit: 20 });
+
+ is(renderedComponentOpen.textContent, `"${stub.fullText}"`,
+ "LongString rep has expected text content when grip has a fullText " +
+ "property and is open");
+
+ const renderedComponentNotOpen = renderComponent(
+ LongStringRep.rep, { object: stub, cropLimit: 20 });
+
+ is(renderedComponentNotOpen.textContent,
+ `"a\naaaaaaaaaaaaaaaaaa…"`,
+ "LongString rep has expected text content when grip has a fullText " +
+ "property and is not open");
+ }
+
+ function testUseQuotes() {
+ const renderedComponent = renderComponent(LongStringRep.rep,
+ { object: getGripStub("testMultiline"), cropLimit: 20, useQuotes: false });
+
+ is(renderedComponent.textContent,
+ "a\naaaaaaaaaaaaaaaaaa…",
+ "LongString rep was expected to omit quotes");
+ }
+
+ function getGripStub(name) {
+ const multilineFullText = "a\n" + Array(20000).fill("a").join("");
+ const fullTextLength = multilineFullText.length;
+ const initialText = multilineFullText.substring(0, 10000);
+
+ switch (name) {
+ case "testMultiline":
+ return {
+ "type": "longString",
+ "initial": initialText,
+ "length": fullTextLength,
+ "actor": "server1.conn1.child1/longString58"
+ };
+ case "testFullText":
+ return {
+ "type": "longString",
+ "fullText": multilineFullText,
+ "initial": initialText,
+ "length": fullTextLength,
+ "actor": "server1.conn1.child1/longString58"
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_nan.html b/devtools/client/shared/components/test/mochitest/test_reps_nan.html
new file mode 100644
index 000000000..35dc5a08f
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_nan.html
@@ -0,0 +1,48 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test NaN rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - NaN</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { NaNRep } = browserRequire("devtools/client/shared/components/reps/nan");
+
+ try {
+ yield testNaN();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testNaN() {
+ const stub = {
+ type: "NaN"
+ };
+ const renderedRep = shallowRenderComponent(Rep, {object: stub});
+ is(renderedRep.type, NaNRep.rep,
+ `Rep correctly selects ${NaNRep.rep.displayName} for NaN value`);
+
+ const renderedComponent = renderComponent(NaNRep.rep, {object: stub});
+ is(renderedComponent.textContent, "NaN", "NaN rep has expected text content");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_null.html b/devtools/client/shared/components/test/mochitest/test_reps_null.html
new file mode 100644
index 000000000..99a06fed1
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_null.html
@@ -0,0 +1,44 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Null rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Null</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Null } = browserRequire("devtools/client/shared/components/reps/null");
+
+ let gripStub = {
+ "type": "null"
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Null.rep, `Rep correctly selects ${Null.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Null.rep, { object: gripStub });
+ is(renderedComponent.textContent, "null", "Null rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_number.html b/devtools/client/shared/components/test/mochitest/test_reps_number.html
new file mode 100644
index 000000000..50f91d8b0
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_number.html
@@ -0,0 +1,97 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Number rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Number</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Number } = browserRequire("devtools/client/shared/components/reps/number");
+
+ try {
+ yield testInt();
+ yield testBoolean();
+ yield testNegativeZero();
+ yield testUnsafeInt();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+
+ function testInt() {
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testInt") });
+ is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for integer value`);
+
+ const renderedComponent = renderComponent(Number.rep, { object: getGripStub("testInt") });
+ is(renderedComponent.textContent, "5", "Number rep has expected text content for integer");
+ }
+
+ function testBoolean() {
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testTrue") });
+ is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for boolean value`);
+
+ let renderedComponent = renderComponent(Number.rep, { object: getGripStub("testTrue") });
+ is(renderedComponent.textContent, "true", "Number rep has expected text content for boolean true");
+
+ renderedComponent = renderComponent(Number.rep, { object: getGripStub("testFalse") });
+ is(renderedComponent.textContent, "false", "Number rep has expected text content for boolean false");
+ }
+
+ function testNegativeZero() {
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testNegZeroGrip") });
+ is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for negative zero value`);
+
+ let renderedComponent = renderComponent(Number.rep, { object: getGripStub("testNegZeroGrip") });
+ is(renderedComponent.textContent, "-0", "Number rep has expected text content for negative zero grip");
+
+ renderedComponent = renderComponent(Number.rep, { object: getGripStub("testNegZeroValue") });
+ is(renderedComponent.textContent, "-0", "Number rep has expected text content for negative zero value");
+ }
+
+ function testUnsafeInt() {
+ const renderedComponent = renderComponent(Number.rep, { object: getGripStub("testUnsafeInt") });
+ is(renderedComponent.textContent, "900719925474099100", "Number rep has expected text content for a long number");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testInt":
+ return 5;
+
+ case "testTrue":
+ return true;
+
+ case "testFalse":
+ return false;
+
+ case "testNegZeroValue":
+ return -0;
+
+ case "testNegZeroGrip":
+ return {
+ "type": "-0"
+ };
+
+ case "testUnsafeInt":
+ return 900719925474099122;
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html b/devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html
new file mode 100644
index 000000000..eeb4aa325
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html
@@ -0,0 +1,54 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test ObjectWithText rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - ObjectWithText</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { ObjectWithText } = browserRequire("devtools/client/shared/components/reps/object-with-text");
+
+ let gripStub = {
+ "type": "object",
+ "class": "CSSStyleRule",
+ "actor": "server1.conn3.obj273",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "ObjectWithText",
+ "text": ".Shadow"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, ObjectWithText.rep, `Rep correctly selects ${ObjectWithText.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(ObjectWithText.rep, { object: gripStub });
+ is(renderedComponent.textContent, "\".Shadow\"", "ObjectWithText rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_object-with-url.html b/devtools/client/shared/components/test/mochitest/test_reps_object-with-url.html
new file mode 100644
index 000000000..488c28dc2
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_object-with-url.html
@@ -0,0 +1,60 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test ObjectWithURL rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - ObjectWithURL</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { ObjectWithURL } = browserRequire("devtools/client/shared/components/reps/object-with-url");
+
+ let gripStub = {
+ "type": "object",
+ "class": "Location",
+ "actor": "server1.conn2.obj272",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 15,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "https://www.mozilla.org/en-US/"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, ObjectWithURL.rep, `Rep correctly selects ${ObjectWithURL.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(ObjectWithURL.rep, { object: gripStub });
+ ok(renderedComponent.className.includes("objectBox-Location"), "ObjectWithURL rep has expected class name");
+ const innerNode = renderedComponent.querySelector(".objectPropValue");
+ is(innerNode.textContent, "https://www.mozilla.org/en-US/", "ObjectWithURL rep has expected inner HTML structure and text content");
+
+ // @TODO test link once Bug 1245303 has been implemented.
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_object.html b/devtools/client/shared/components/test/mochitest/test_reps_object.html
new file mode 100644
index 000000000..c3332361d
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_object.html
@@ -0,0 +1,225 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Obj rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Obj</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Obj } = browserRequire("devtools/client/shared/components/reps/object");
+
+ const componentUnderTest = Obj;
+
+ try {
+ yield testBasic();
+
+ // Test property iterator
+ yield testMaxProps();
+ yield testMoreThanMaxProps();
+ yield testUninterestingProps();
+
+ // Test that properties are rendered as expected by PropRep
+ yield testNested();
+
+ // Test that 'more' property doesn't clobber the caption.
+ yield testMoreProp();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBasic() {
+ const stub = {};
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, Obj.rep, `Rep correctly selects ${Obj.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Object`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testBasic", componentUnderTest, stub);
+ }
+
+ function testMaxProps() {
+ const testName = "testMaxProps";
+
+ const stub = {a: "a", b: "b", c: "c"};
+ const defaultOutput = `Object { a: "a", b: "b", c: "c" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMaxProps", componentUnderTest, stub);
+ }
+
+ function testMoreThanMaxProps() {
+ let stub = {};
+ for (let i = 0; i<100; i++) {
+ stub[`p${i}`] = i
+ }
+ const defaultOutput = `Object { p0: 0, p1: 1, p2: 2, 97 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMoreThanMaxProps", componentUnderTest, stub);
+ }
+
+ function testUninterestingProps() {
+ const stub = {a:undefined, b:undefined, c:"c", d:0};
+ const defaultOutput = `Object { c: "c", d: 0, a: undefined, 1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testUninterestingProps", componentUnderTest, stub);
+ }
+
+ function testNested() {
+ const stub = {
+ objProp: {
+ id: 1,
+ arr: [2]
+ },
+ strProp: "test string",
+ arrProp: [1]
+ };
+ const defaultOutput = `Object { strProp: "test string", objProp: Object, arrProp: [1] }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testNestedObject", componentUnderTest, stub);
+ }
+
+ function testMoreProp() {
+ const stub = {
+ a: undefined,
+ b: 1,
+ 'more': 2,
+ d: 3
+ };
+ const defaultOutput = `Object { b: 1, more: 2, d: 3, 1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMoreProp", componentUnderTest, stub);
+ }});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_promise.html b/devtools/client/shared/components/test/mochitest/test_reps_promise.html
new file mode 100644
index 000000000..31de7136d
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_promise.html
@@ -0,0 +1,333 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Promise rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Promise</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { PromiseRep } = browserRequire("devtools/client/shared/components/reps/promise");
+
+ const componentUnderTest = PromiseRep;
+
+ try {
+ yield testPending();
+ yield testFulfilledWithNumber();
+ yield testFulfilledWithString();
+ yield testFulfilledWithObject();
+ yield testFulfilledWithArray();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testPending() {
+ // Test object = `new Promise((resolve, reject) => true)`
+ const stub = getGripStub("testPending");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${PromiseRep.rep.displayName} for pending Promise`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "pending" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "pending" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testPending", componentUnderTest, stub);
+ }
+ function testFulfilledWithNumber() {
+ // Test object = `Promise.resolve(42)`
+ const stub = getGripStub("testFulfilledWithNumber");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ const {displayName} = PromiseRep.rep;
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${displayName} for Promise fulfilled with a number`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "fulfilled", <value>: 42 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "fulfilled" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testFulfilledWithNumber", componentUnderTest, stub);
+ }
+ function testFulfilledWithString() {
+ // Test object = `Promise.resolve("foo")`
+ const stub = getGripStub("testFulfilledWithString");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ const {displayName} = PromiseRep.rep;
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${displayName} for Promise fulfilled with a string`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "fulfilled", <value>: "foo" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "fulfilled" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testFulfilledWithString", componentUnderTest, stub);
+ }
+
+ function testFulfilledWithObject() {
+ // Test object = `Promise.resolve({foo: "bar", baz: "boo"})`
+ const stub = getGripStub("testFulfilledWithObject");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ const {displayName} = PromiseRep.rep;
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${displayName} for Promise fulfilled with an object`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "fulfilled", <value>: Object }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "fulfilled" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testFulfilledWithObject", componentUnderTest, stub);
+ }
+
+ function testFulfilledWithArray() {
+ // Test object = `Promise.resolve([1,2,3])`
+ const stub = getGripStub("testFulfilledWithArray");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ const {displayName} = PromiseRep.rep;
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${displayName} for Promise fulfilled with an array`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "fulfilled", <value>: [3] }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "fulfilled" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testFulfilledWithArray", componentUnderTest, stub);
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testPending":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj54",
+ "class": "Promise",
+ "promiseState": {
+ "state": "pending",
+ "creationTimestamp": 1477327760242.5752
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ case "testFulfilledWithNumber":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj55",
+ "class": "Promise",
+ "promiseState": {
+ "state": "fulfilled",
+ "value": 42,
+ "creationTimestamp": 1477327760242.721,
+ "timeToSettle": 0.018497000000479602
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ case "testFulfilledWithString":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj56",
+ "class": "Promise",
+ "promiseState": {
+ "state": "fulfilled",
+ "value": "foo",
+ "creationTimestamp": 1477327760243.2483,
+ "timeToSettle": 0.0019969999998465937
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ case "testFulfilledWithObject":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj59",
+ "class": "Promise",
+ "promiseState": {
+ "state": "fulfilled",
+ "value": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj60",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 2
+ },
+ "creationTimestamp": 1477327760243.2214,
+ "timeToSettle": 0.002035999999861815
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ case "testFulfilledWithArray":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj57",
+ "class": "Promise",
+ "promiseState": {
+ "state": "fulfilled",
+ "value": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj58",
+ "class": "Array",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3
+ }
+ },
+ "creationTimestamp": 1477327760242.9597,
+ "timeToSettle": 0.006158000000141328
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_regexp.html b/devtools/client/shared/components/test/mochitest/test_reps_regexp.html
new file mode 100644
index 000000000..074948494
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_regexp.html
@@ -0,0 +1,51 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test RegExp rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - RegExp</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { RegExp } = browserRequire("devtools/client/shared/components/reps/regexp");
+
+ let gripStub = {
+ "type": "object",
+ "class": "RegExp",
+ "actor": "server1.conn22.obj39",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "displayString": "/ab+c/i"
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, RegExp.rep, `Rep correctly selects ${RegExp.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(RegExp.rep, { object: gripStub });
+ is(renderedComponent.textContent, "/ab+c/i", "RegExp rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_string.html b/devtools/client/shared/components/test/mochitest/test_reps_string.html
new file mode 100644
index 000000000..f9fc9e5b0
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_string.html
@@ -0,0 +1,79 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test String rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - String</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { StringRep } = browserRequire("devtools/client/shared/components/reps/string");
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testMultiline") });
+ is(renderedRep.type, StringRep.rep, `Rep correctly selects ${StringRep.rep.displayName}`);
+
+ // Test rendering
+ yield testMultiline();
+ yield testMultilineOpen();
+ yield testMultilineLimit();
+ yield testUseQuotes();
+ yield testNonPritableCharacters();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testMultiline() {
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline") });
+ is(renderedComponent.textContent, "\"aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n\"", "String rep has expected text content for multiline string");
+ }
+
+ function testMultilineLimit() {
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline"), cropLimit: 20 });
+ is(renderedComponent.textContent, "\"aaaaaaaaaa…cccccccc\n\"", "String rep has expected text content for multiline string with specified number of characters");
+ }
+
+ function testMultilineOpen() {
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline"), member: {open: true} });
+ is(renderedComponent.textContent, "\"aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n\"", "String rep has expected text content for multiline string when open");
+ }
+
+ function testUseQuotes(){
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testUseQuotes"), useQuotes: false });
+ is(renderedComponent.textContent, "abc", "String rep was expected to omit quotes");
+ }
+
+ function testNonPritableCharacters(){
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testNonPritableCharacters"), useQuotes: false });
+ is(renderedComponent.textContent, "a\ufffdb", "String rep was expected to omit non printable characters");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testMultiline":
+ return "aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n";
+ case "testUseQuotes":
+ return "abc";
+ case "testNonPritableCharacters":
+ return "a\x01b";
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_stylesheet.html b/devtools/client/shared/components/test/mochitest/test_reps_stylesheet.html
new file mode 100644
index 000000000..6f54dee48
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_stylesheet.html
@@ -0,0 +1,54 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Stylesheet rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Stylesheet</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { StyleSheet } = browserRequire("devtools/client/shared/components/reps/stylesheet");
+
+ let gripStub = {
+ "type": "object",
+ "class": "CSSStyleSheet",
+ "actor": "server1.conn2.obj1067",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "https://example.com/styles.css"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, StyleSheet.rep, `Rep correctly selects ${StyleSheet.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(StyleSheet.rep, { object: gripStub });
+ is(renderedComponent.textContent, "StyleSheet https://example.com/styles.css", "StyleSheet rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_symbol.html b/devtools/client/shared/components/test/mochitest/test_reps_symbol.html
new file mode 100644
index 000000000..0112eac0f
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_symbol.html
@@ -0,0 +1,77 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Symbol rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - String</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+/* import-globals-from head.js */
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { SymbolRep } = browserRequire("devtools/client/shared/components/reps/symbol");
+
+ let gripStubs = new Map();
+ gripStubs.set("testSymbolFoo", {
+ type: "symbol",
+ name: "foo"
+ });
+ gripStubs.set("testSymbolWithoutIdentifier", {
+ type: "symbol"
+ });
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(
+ Rep,
+ { object: gripStubs.get("testSymbolFoo")}
+ );
+
+ is(renderedRep.type, SymbolRep.rep,
+ `Rep correctly selects ${SymbolRep.rep.displayName}`);
+
+ // Test rendering
+ yield testSymbol();
+ yield testSymbolWithoutIdentifier();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testSymbol() {
+ const renderedComponent = renderComponent(
+ SymbolRep.rep,
+ { object: gripStubs.get("testSymbolFoo") }
+ );
+
+ is(renderedComponent.textContent, "Symbol(foo)",
+ "Symbol rep has expected text content");
+ }
+
+ function testSymbolWithoutIdentifier() {
+ const renderedComponent = renderComponent(
+ SymbolRep.rep,
+ { object: gripStubs.get("testSymbolWithoutIdentifier") }
+ );
+
+ is(renderedComponent.textContent, "Symbol()",
+ "Symbol rep without identifier has expected text content");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_text-node.html b/devtools/client/shared/components/test/mochitest/test_reps_text-node.html
new file mode 100644
index 000000000..f64902a63
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_text-node.html
@@ -0,0 +1,115 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test text-node rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - text-node</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { TextNode } = browserRequire("devtools/client/shared/components/reps/text-node");
+
+ let gripStubs = new Map();
+ gripStubs.set("testRendering", {
+ "class": "Text",
+ "actor": "server1.conn1.child1/obj50",
+ "preview": {
+ "textContent": "hello world"
+ }
+ });
+ gripStubs.set("testRenderingWithEOL", {
+ "class": "Text",
+ "actor": "server1.conn1.child1/obj50",
+ "preview": {
+ "textContent": "hello\nworld"
+ }
+ });
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, {
+ object: gripStubs.get("testRendering")
+ });
+
+ is(renderedRep.type, TextNode.rep,
+ `Rep correctly selects ${TextNode.rep.displayName}`);
+
+ yield testRendering();
+ yield testRenderingWithEOL();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testRendering() {
+ const stub = gripStubs.get("testRendering");
+ const defaultShortOutput = `"hello world"`;
+ const defaultLongOutput = `<TextNode textContent="hello world">;`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultLongOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testRendering", TextNode, stub);
+ }
+
+ function testRenderingWithEOL() {
+ const stub = gripStubs.get("testRenderingWithEOL");
+ const defaultShortOutput = `"hello\nworld"`;
+ const defaultLongOutput = `<TextNode textContent="hello\nworld">;`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultLongOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testRenderingWithEOL", TextNode, stub);
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_undefined.html b/devtools/client/shared/components/test/mochitest/test_reps_undefined.html
new file mode 100644
index 000000000..26b3345ac
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_undefined.html
@@ -0,0 +1,47 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test undefined rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - undefined</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Undefined } = browserRequire("devtools/client/shared/components/reps/undefined");
+
+ let gripStub = {
+ "type": "undefined"
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Undefined.rep, `Rep correctly selects ${Undefined.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Undefined.rep, {});
+ is(renderedComponent.className, "objectBox objectBox-undefined", "Undefined rep has expected class names");
+ is(renderedComponent.textContent, "undefined", "Undefined rep has expected text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_window.html b/devtools/client/shared/components/test/mochitest/test_reps_window.html
new file mode 100644
index 000000000..55d60e9f5
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_window.html
@@ -0,0 +1,58 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test window rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep tests - window</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Window } = browserRequire("devtools/client/shared/components/reps/window");
+
+ let gripStub = {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn3.obj198",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 887,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "about:newtab"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Window.rep, `Rep correctly selects ${Window.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Window.rep, { object: gripStub });
+ ok(renderedComponent.className.includes("objectBox-Window"), "Window rep has expected class name");
+ const innerNode = renderedComponent.querySelector(".objectPropValue");
+ is(innerNode.textContent, "about:newtab", "Window rep has expected inner HTML structure and text content");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_sidebar_toggle.html b/devtools/client/shared/components/test/mochitest/test_sidebar_toggle.html
new file mode 100644
index 000000000..252f2fbb1
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_sidebar_toggle.html
@@ -0,0 +1,56 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test sidebar toggle button
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Sidebar toggle button test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let SidebarToggle = browserRequire("devtools/client/shared/components/sidebar-toggle.js");
+
+ try {
+ yield test();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function test() {
+ const output1 = shallowRenderComponent(SidebarToggle, {
+ collapsed: false,
+ collapsePaneTitle: "Expand",
+ expandPaneTitle: "Collapse"
+ });
+
+ is(output1.type, "button", "Output is a button element");
+ is(output1.props.title, "Expand", "Proper title is set");
+ is(output1.props.className.indexOf("pane-collapsed"), -1,
+ "Proper class name is set");
+
+ const output2 = shallowRenderComponent(SidebarToggle, {
+ collapsed: true,
+ collapsePaneTitle: "Expand",
+ expandPaneTitle: "Collapse"
+ });
+
+ is(output2.props.title, "Collapse", "Proper title is set");
+ ok(output2.props.className.indexOf("pane-collapsed") >= 0,
+ "Proper class name is set");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_stack-trace.html b/devtools/client/shared/components/test/mochitest/test_stack-trace.html
new file mode 100644
index 000000000..121316cb4
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_stack-trace.html
@@ -0,0 +1,102 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace
+-->
+<head>
+ <meta charset="utf-8">
+ <title>StackTrace component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+/* import-globals-from head.js */
+"use strict";
+
+window.onload = function () {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let StackTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/stack-trace")
+ );
+ ok(StackTrace, "Got the StackTrace factory");
+
+ add_task(function* () {
+ let stacktrace = [
+ {
+ filename: "http://myfile.com/mahscripts.js",
+ lineNumber: 55,
+ columnNumber: 10
+ },
+ {
+ asyncCause: "because",
+ functionName: "loadFunc",
+ filename: "http://myfile.com/loader.js -> http://myfile.com/loadee.js",
+ lineNumber: 10
+ }
+ ];
+
+ let props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {}
+ };
+
+ let trace = ReactDOM.render(StackTrace(props), window.document.body);
+ yield forceRender(trace);
+
+ let traceEl = trace.getDOMNode();
+ ok(traceEl, "Rendered StackTrace has an element");
+
+ // Get the child nodes and filter out the text-only whitespace ones
+ let frameEls = Array.from(traceEl.childNodes)
+ .filter(n => n.className.includes("frame"));
+ ok(frameEls, "Rendered StackTrace has frames");
+ is(frameEls.length, 3, "StackTrace has 3 frames");
+
+ // Check the top frame, function name should be anonymous
+ checkFrameString({
+ el: frameEls[0],
+ functionName: "<anonymous>",
+ source: "http://myfile.com/mahscripts.js",
+ file: "http://myfile.com/mahscripts.js",
+ line: 55,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:55:10",
+ });
+
+ // Check the async cause node
+ is(frameEls[1].className, "frame-link-async-cause",
+ "Async cause has the right class");
+ is(frameEls[1].textContent, "(Async: because)", "Async cause has the right label");
+
+ // Check the third frame, the source should be parsed into a valid source URL
+ checkFrameString({
+ el: frameEls[2],
+ functionName: "loadFunc",
+ source: "http://myfile.com/loadee.js",
+ file: "http://myfile.com/loadee.js",
+ line: 10,
+ column: null,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/loadee.js:10",
+ });
+
+ // Check the tabs and newlines in the stack trace textContent
+ let traceText = traceEl.textContent;
+ let traceLines = traceText.split("\n");
+ ok(traceLines.length > 0, "There are newlines in the stack trace text");
+ is(traceLines.pop(), "", "There is a newline at the end of the stack trace text");
+ is(traceLines.length, 3, "The stack trace text has 3 lines");
+ ok(traceLines.every(l => l[0] == "\t"), "Every stack trace line starts with tab");
+ });
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html b/devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html
new file mode 100644
index 000000000..a86082187
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html
@@ -0,0 +1,79 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test tabs accessibility.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tabs component accessibility test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const InspectorTabPanel = React.createFactory(browserRequire("devtools/client/inspector/components/inspector-tab-panel"));
+ const Tabbar = React.createFactory(browserRequire("devtools/client/shared/components/tabs/tabbar"));
+ const tabbar = Tabbar();
+ const tabbarReact = ReactDOM.render(tabbar, window.document.body);
+ const tabbarEl = ReactDOM.findDOMNode(tabbarReact);
+
+ // Setup for InspectorTabPanel
+ const tabpanels = document.createElement("div");
+ tabpanels.id = "tabpanels";
+ document.body.appendChild(tabpanels);
+
+ yield addTabWithPanel(0);
+ yield addTabWithPanel(1);
+
+ const tabAnchors = tabbarEl.querySelectorAll("li.tabs-menu-item a");
+
+ is(tabAnchors[0].parentElement.getAttribute("role"), "presentation", "li role is set correctly");
+ is(tabAnchors[0].getAttribute("role"), "tab", "Anchor role is set correctly");
+ is(tabAnchors[0].getAttribute("aria-selected"), "true", "Anchor aria-selected is set correctly by default");
+ is(tabAnchors[0].getAttribute("aria-controls"), "panel-0", "Anchor aria-controls is set correctly");
+ is(tabAnchors[1].parentElement.getAttribute("role"), "presentation", "li role is set correctly");
+ is(tabAnchors[1].getAttribute("role"), "tab", "Anchor role is set correctly");
+ is(tabAnchors[1].getAttribute("aria-selected"), "false", "Anchor aria-selected is set correctly by default");
+ is(tabAnchors[1].getAttribute("aria-controls"), "panel-1", "Anchor aria-controls is set correctly");
+
+ yield setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ activeTab: 1
+ }));
+
+ is(tabAnchors[0].getAttribute("aria-selected"), "false", "Anchor aria-selected is reset correctly");
+ is(tabAnchors[1].getAttribute("aria-selected"), "true", "Anchor aria-selected is reset correctly");
+
+ function addTabWithPanel(tabId) {
+ // Setup for InspectorTabPanel
+ let panel = document.createElement("div");
+ panel.id = `sidebar-panel-${tabId}`;
+ document.body.appendChild(panel);
+
+ return setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ tabs: tabbarReact.state.tabs.concat({
+ id: `sidebar-panel-${tabId}`,
+ title: `tab-${tabId}`,
+ panel: InspectorTabPanel
+ }),
+ }));
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tabs_menu.html b/devtools/client/shared/components/test/mochitest/test_tabs_menu.html
new file mode 100644
index 000000000..ac8e99289
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tabs_menu.html
@@ -0,0 +1,81 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html class="theme-light">
+<!--
+Test all-tabs menu.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tabs component All-tabs menu test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/themes/variables.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/themes/common.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/themes/light-theme.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/shared/components/tabs/tabs.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/shared/components/tabs/tabbar.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/inspector/components/side-panel.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/inspector/components/inspector-tab-panel.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tabbar = React.createFactory(browserRequire("devtools/client/shared/components/tabs/tabbar"));
+
+ // Create container for the TabBar. Set smaller width
+ // to ensure that tabs won't fit and the all-tabs menu
+ // needs to appear.
+ const tabBarBox = document.createElement("div");
+ tabBarBox.style.width = "200px";
+ tabBarBox.style.height = "200px";
+ tabBarBox.style.border = "1px solid lightgray";
+ document.body.appendChild(tabBarBox);
+
+ // Render the tab-bar.
+ const tabbar = Tabbar({
+ showAllTabsMenu: true,
+ });
+
+ const tabbarReact = ReactDOM.render(tabbar, tabBarBox);
+
+ // Test panel.
+ let TabPanel = React.createFactory(React.createClass({
+ render: function () {
+ return React.DOM.div({}, "content");
+ }
+ }));
+
+ // Create a few panels.
+ yield addTabWithPanel(1);
+ yield addTabWithPanel(2);
+ yield addTabWithPanel(3);
+ yield addTabWithPanel(4);
+ yield addTabWithPanel(5);
+
+ // Make sure the all-tabs menu is there.
+ const allTabsMenu = tabBarBox.querySelector(".all-tabs-menu");
+ ok(allTabsMenu, "All-tabs menu must be rendered");
+
+ function addTabWithPanel(tabId) {
+ return setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ tabs: tabbarReact.state.tabs.concat({id: `${tabId}`,
+ title: `tab-${tabId}`, panel: TabPanel}),
+ }));
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_01.html b/devtools/client/shared/components/test/mochitest/test_tree_01.html
new file mode 100644
index 000000000..dfd666348
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_01.html
@@ -0,0 +1,64 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test trees get displayed with the items in correct order and at the correct
+depth.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ ok(React, "Should get React");
+ ok(Tree, "Should get Tree");
+
+ const t = Tree(TEST_TREE_INTERFACE);
+ ok(t, "Should be able to create Tree instances");
+
+ const tree = ReactDOM.render(t, window.document.body);
+ ok(tree, "Should be able to mount Tree instances");
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Should get the items rendered and indented as expected");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_02.html b/devtools/client/shared/components/test/mochitest/test_tree_02.html
new file mode 100644
index 000000000..a1fc33a38
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_02.html
@@ -0,0 +1,45 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that collapsed subtrees aren't rendered.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+
+ TEST_TREE.expanded = new Set("MNO".split(""));
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Collapsed subtrees shouldn't be rendered");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_03.html b/devtools/client/shared/components/test/mochitest/test_tree_03.html
new file mode 100644
index 000000000..feabc7e0a
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_03.html
@@ -0,0 +1,46 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Tree's autoExpandDepth.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ autoExpandDepth: 1
+ })), window.document.body);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "-C:false",
+ "-D:false",
+ "M:false",
+ "-N:false",
+ ], "Tree should be auto expanded one level");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_04.html b/devtools/client/shared/components/test/mochitest/test_tree_04.html
new file mode 100644
index 000000000..24948c003
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_04.html
@@ -0,0 +1,128 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we only render visible tree items.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ function getSpacerHeights() {
+ return {
+ top: document.querySelector(".tree > div:first-of-type").clientHeight,
+ bottom: document.querySelector(".tree > div:last-of-type").clientHeight,
+ };
+ }
+
+ const ITEM_HEIGHT = 3;
+
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ const tree = ReactDOM.render(
+ Tree(Object.assign({}, TEST_TREE_INTERFACE, { itemHeight: ITEM_HEIGHT })),
+ window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ yield setState(tree, {
+ height: 3 * ITEM_HEIGHT,
+ scroll: 1 * ITEM_HEIGHT
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ ], "Tree should show the 2nd, 3rd, and 4th items + buffer of 1 item at each end");
+
+ let spacers = getSpacerHeights();
+ is(spacers.top, 0, "Top spacer has the correct height");
+ is(spacers.bottom, 10 * ITEM_HEIGHT, "Bottom spacer has the correct height");
+
+ yield setState(tree, {
+ height: 2 * ITEM_HEIGHT,
+ scroll: 3 * ITEM_HEIGHT
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ ], "Tree should show the 4th and 5th item + buffer of 1 item at each end");
+
+ spacers = getSpacerHeights();
+ is(spacers.top, 2 * ITEM_HEIGHT, "Top spacer has the correct height");
+ is(spacers.bottom, 9 * ITEM_HEIGHT, "Bottom spacer has the correct height");
+
+ // Set height to 2 items + 1 pixel at each end, scroll so that 4 items are visible
+ // (2 fully, 2 partially with 1 visible pixel)
+ yield setState(tree, {
+ height: 2 * ITEM_HEIGHT + 2,
+ scroll: 3 * ITEM_HEIGHT - 1
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ ], "Tree should show the 4 visible items + buffer of 1 item at each end");
+
+ spacers = getSpacerHeights();
+ is(spacers.top, 1 * ITEM_HEIGHT, "Top spacer has the correct height");
+ is(spacers.bottom, 8 * ITEM_HEIGHT, "Bottom spacer has the correct height");
+
+ yield setState(tree, {
+ height: 20 * ITEM_HEIGHT,
+ scroll: 0
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Tree should show all rows");
+
+ spacers = getSpacerHeights();
+ is(spacers.top, 0, "Top spacer has zero height");
+ is(spacers.bottom, 0, "Bottom spacer has zero height");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_05.html b/devtools/client/shared/components/test/mochitest/test_tree_05.html
new file mode 100644
index 000000000..76116ab51
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_05.html
@@ -0,0 +1,83 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test focusing with the Tree component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ onFocus: x => setProps(tree, { focused: x }),
+ })), window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ yield setProps(tree, {
+ focused: "G",
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:true",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "G should be focused");
+
+ // Click the first tree node
+ Simulate.click(document.querySelector(".tree-node"));
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "A should be focused");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_06.html b/devtools/client/shared/components/test/mochitest/test_tree_06.html
new file mode 100644
index 000000000..1d8f28ec9
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_06.html
@@ -0,0 +1,320 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test keyboard navigation with the Tree component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ onFocus: x => setProps(tree, { focused: x }),
+ })), window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ // UP ----------------------------------------------------------------------
+
+ info("Up to the previous sibling.");
+
+ yield setProps(tree, {
+ focused: "L"
+ });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the UP, K should be focused.");
+
+ info("Up to the parent.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the UP, E should be focused.");
+
+ info("Try and navigate up, past the first item.");
+
+ yield setProps(tree, {
+ focused: "A"
+ });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the UP, A should be focused and we shouldn't have overflowed past it.");
+
+ // DOWN --------------------------------------------------------------------
+
+ yield setProps(tree, {
+ focused: "K"
+ });
+
+ info("Down to next sibling.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:true",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the DOWN, L should be focused.");
+
+ info("Down to parent's next sibling.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:true",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the DOWN, F should be focused.");
+
+ info("Try and go down past the last item.");
+
+ yield setProps(tree, {
+ focused: "O"
+ });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:true",
+ ], "After the DOWN, O should still be focused and we shouldn't have overflowed past it.");
+
+ // LEFT --------------------------------------------------------------------
+
+ info("Left to go to parent.");
+
+ yield setProps(tree, {
+ focused: "L"
+ })
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the LEFT, E should be focused.");
+
+ info("Left to collapse children.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the LEFT, E's children should be collapsed.");
+
+ // RIGHT -------------------------------------------------------------------
+
+ info("Right to expand children.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the RIGHT, E's children should be expanded again.");
+
+ info("Right to go to next item.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the RIGHT, K should be focused.");
+
+ // Check that keys are ignored if any modifier is present.
+ let keysWithModifier = [
+ { key: "ArrowDown", altKey: true },
+ { key: "ArrowDown", ctrlKey: true },
+ { key: "ArrowDown", metaKey: true },
+ { key: "ArrowDown", shiftKey: true },
+ ];
+ for (let key of keysWithModifier) {
+ Simulate.keyDown(document.querySelector(".tree"), key);
+ yield forceRender(tree);
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After DOWN + (alt|ctrl|meta|shift), K should remain focused.");
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_07.html b/devtools/client/shared/components/test/mochitest/test_tree_07.html
new file mode 100644
index 000000000..178ac77e3
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_07.html
@@ -0,0 +1,64 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that arrows get the open attribute when their item's children are expanded.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+ const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+
+ yield setProps(tree, {
+ renderItem: (item, depth, focused, arrow) => {
+ return React.DOM.div(
+ {
+ id: item,
+ style: { marginLeft: depth * 16 + "px" }
+ },
+ arrow,
+ item
+ );
+ }
+ });
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ yield forceRender(tree);
+
+ let arrows = document.querySelectorAll(".arrow");
+ for (let a of arrows) {
+ ok(a.classList.contains("open"), "Every arrow should be open.");
+ }
+
+ TEST_TREE.expanded = new Set();
+ yield forceRender(tree);
+
+ arrows = document.querySelectorAll(".arrow");
+ for (let a of arrows) {
+ ok(!a.classList.contains("open"), "Every arrow should be closed.");
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_08.html b/devtools/client/shared/components/test/mochitest/test_tree_08.html
new file mode 100644
index 000000000..d024f37f8
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_08.html
@@ -0,0 +1,51 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is clicked, it steals focus from
+other inputs.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ onFocus: x => setProps(tree, { focused: x }),
+ })), window.document.body);
+
+ const input = document.createElement("input");
+ document.body.appendChild(input);
+
+ input.focus();
+ is(document.activeElement, input, "The text input should be focused.");
+
+ Simulate.click(document.querySelector(".tree-node"));
+ yield forceRender(tree);
+
+ isnot(document.activeElement, input,
+ "The input should have had it's focus stolen by clicking on a tree item.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_09.html b/devtools/client/shared/components/test/mochitest/test_tree_09.html
new file mode 100644
index 000000000..96650134b
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_09.html
@@ -0,0 +1,77 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is expanded or collapsed the appropriate event handler fires.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ let numberOfExpands = 0;
+ let lastExpandedItem = null;
+
+ let numberOfCollapses = 0;
+ let lastCollapsedItem = null;
+
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ autoExpandDepth: 0,
+ onExpand: item => {
+ lastExpandedItem = item;
+ numberOfExpands++;
+ TEST_TREE.expanded.add(item);
+ },
+ onCollapse: item => {
+ lastCollapsedItem = item;
+ numberOfCollapses++;
+ TEST_TREE.expanded.delete(item);
+ },
+ onFocus: item => setProps(tree, { focused: item }),
+ })), window.document.body);
+
+ yield setProps(tree, {
+ focused: "A"
+ });
+
+ is(lastExpandedItem, null);
+ is(lastCollapsedItem, null);
+
+ // Expand "A" via the keyboard and then let the component re-render.
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ yield forceRender(tree);
+
+ is(lastExpandedItem, "A", "Our onExpand callback should have been fired.");
+ is(numberOfExpands, 1);
+
+ // Now collapse "A" via the keyboard and then let the component re-render.
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ yield forceRender(tree);
+
+ is(lastCollapsedItem, "A", "Our onCollapsed callback should have been fired.");
+ is(numberOfCollapses, 1);
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_10.html b/devtools/client/shared/components/test/mochitest/test_tree_10.html
new file mode 100644
index 000000000..34f8a2633
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_10.html
@@ -0,0 +1,52 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is expanded or collapsed the appropriate event handler fires.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ const tree = ReactDOM.render(Tree(Object.assign({
+ autoExpandDepth: 1
+ }, TEST_TREE_INTERFACE)), window.document.body);
+
+ yield setProps(tree, {
+ focused: "A"
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "-C:false",
+ "-D:false",
+ "M:false",
+ "-N:false",
+ ], "Should have auto-expanded one level.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_tree_11.html b/devtools/client/shared/components/test/mochitest/test_tree_11.html
new file mode 100644
index 000000000..1611f7d26
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_11.html
@@ -0,0 +1,92 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is focused by arrow key, the view is scrolled.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+ <style>
+ * {
+ margin: 0;
+ padding: 0;
+ height: 30px;
+ max-height: 30px;
+ min-height: 30px;
+ font-size: 10px;
+ overflow: auto;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+
+ yield setProps(tree, {
+ itemHeight: 10,
+ onFocus: item => setProps(tree, { focused: item }),
+ focused: "K",
+ });
+ yield setState(tree, {
+ scroll: 10,
+ });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ ], "Should render initial correctly");
+
+ yield new Promise(resolve => {
+ const treeElem = document.querySelector(".tree");
+ treeElem.addEventListener("scroll", function onScroll() {
+ dumpn("Got scroll event");
+ treeElem.removeEventListener("scroll", onScroll);
+ resolve();
+ });
+
+ dumpn("Sending ArrowDown key");
+ Simulate.keyDown(treeElem, { key: "ArrowDown" });
+ });
+
+ dumpn("Forcing re-render");
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:true",
+ "--F:false",
+ ], "Should have scrolled down one");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/tree.js b/devtools/client/shared/components/tree.js
new file mode 100644
index 000000000..49b5d1497
--- /dev/null
+++ b/devtools/client/shared/components/tree.js
@@ -0,0 +1,773 @@
+/* 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/. */
+/* eslint-env browser */
+"use strict";
+
+const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+
+const AUTO_EXPAND_DEPTH = 0;
+const NUMBER_OF_OFFSCREEN_ITEMS = 1;
+
+/**
+ * A fast, generic, expandable and collapsible tree component.
+ *
+ * This tree component is fast: it can handle trees with *many* items. It only
+ * renders the subset of those items which are visible in the viewport. It's
+ * been battle tested on huge trees in the memory panel. We've optimized tree
+ * traversal and rendering, even in the presence of cross-compartment wrappers.
+ *
+ * This tree component doesn't make any assumptions about the structure of your
+ * tree data. Whether children are computed on demand, or stored in an array in
+ * the parent's `_children` property, it doesn't matter. We only require the
+ * implementation of `getChildren`, `getRoots`, `getParent`, and `isExpanded`
+ * functions.
+ *
+ * This tree component is well tested and reliable. See
+ * devtools/client/shared/components/test/mochitest/test_tree_* and its usage in
+ * the performance and memory panels.
+ *
+ * This tree component doesn't make any assumptions about how to render items in
+ * the tree. You provide a `renderItem` function, and this component will ensure
+ * that only those items whose parents are expanded and which are visible in the
+ * viewport are rendered. The `renderItem` function could render the items as a
+ * "traditional" tree or as rows in a table or anything else. It doesn't
+ * restrict you to only one certain kind of tree.
+ *
+ * The only requirement is that every item in the tree render as the same
+ * height. This is required in order to compute which items are visible in the
+ * viewport in constant time.
+ *
+ * ### Example Usage
+ *
+ * Suppose we have some tree data where each item has this form:
+ *
+ * {
+ * id: Number,
+ * label: String,
+ * parent: Item or null,
+ * children: Array of child items,
+ * expanded: bool,
+ * }
+ *
+ * Here is how we could render that data with this component:
+ *
+ * const MyTree = createClass({
+ * displayName: "MyTree",
+ *
+ * propTypes: {
+ * // The root item of the tree, with the form described above.
+ * root: PropTypes.object.isRequired
+ * },
+ *
+ * render() {
+ * return Tree({
+ * itemHeight: 20, // px
+ *
+ * getRoots: () => [this.props.root],
+ *
+ * getParent: item => item.parent,
+ * getChildren: item => item.children,
+ * getKey: item => item.id,
+ * isExpanded: item => item.expanded,
+ *
+ * renderItem: (item, depth, isFocused, arrow, isExpanded) => {
+ * let className = "my-tree-item";
+ * if (isFocused) {
+ * className += " focused";
+ * }
+ * return dom.div(
+ * {
+ * className,
+ * // Apply 10px nesting per expansion depth.
+ * style: { marginLeft: depth * 10 + "px" }
+ * },
+ * // Here is the expando arrow so users can toggle expansion and
+ * // collapse state.
+ * arrow,
+ * // And here is the label for this item.
+ * dom.span({ className: "my-tree-item-label" }, item.label)
+ * );
+ * },
+ *
+ * onExpand: item => dispatchExpandActionToRedux(item),
+ * onCollapse: item => dispatchCollapseActionToRedux(item),
+ * });
+ * }
+ * });
+ */
+module.exports = createClass({
+ displayName: "Tree",
+
+ propTypes: {
+ // Required props
+
+ // A function to get an item's parent, or null if it is a root.
+ //
+ // Type: getParent(item: Item) -> Maybe<Item>
+ //
+ // Example:
+ //
+ // // The parent of this item is stored in its `parent` property.
+ // getParent: item => item.parent
+ getParent: PropTypes.func.isRequired,
+
+ // A function to get an item's children.
+ //
+ // Type: getChildren(item: Item) -> [Item]
+ //
+ // Example:
+ //
+ // // This item's children are stored in its `children` property.
+ // getChildren: item => item.children
+ getChildren: PropTypes.func.isRequired,
+
+ // A function which takes an item and ArrowExpander component instance and
+ // returns a component, or text, or anything else that React considers
+ // renderable.
+ //
+ // Type: renderItem(item: Item,
+ // depth: Number,
+ // isFocused: Boolean,
+ // arrow: ReactComponent,
+ // isExpanded: Boolean) -> ReactRenderable
+ //
+ // Example:
+ //
+ // renderItem: (item, depth, isFocused, arrow, isExpanded) => {
+ // let className = "my-tree-item";
+ // if (isFocused) {
+ // className += " focused";
+ // }
+ // return dom.div(
+ // {
+ // className,
+ // style: { marginLeft: depth * 10 + "px" }
+ // },
+ // arrow,
+ // dom.span({ className: "my-tree-item-label" }, item.label)
+ // );
+ // },
+ renderItem: PropTypes.func.isRequired,
+
+ // A function which returns the roots of the tree (forest).
+ //
+ // Type: getRoots() -> [Item]
+ //
+ // Example:
+ //
+ // // In this case, we only have one top level, root item. You could
+ // // return multiple items if you have many top level items in your
+ // // tree.
+ // getRoots: () => [this.props.rootOfMyTree]
+ getRoots: PropTypes.func.isRequired,
+
+ // A function to get a unique key for the given item. This helps speed up
+ // React's rendering a *TON*.
+ //
+ // Type: getKey(item: Item) -> String
+ //
+ // Example:
+ //
+ // getKey: item => `my-tree-item-${item.uniqueId}`
+ getKey: PropTypes.func.isRequired,
+
+ // A function to get whether an item is expanded or not. If an item is not
+ // expanded, then it must be collapsed.
+ //
+ // Type: isExpanded(item: Item) -> Boolean
+ //
+ // Example:
+ //
+ // isExpanded: item => item.expanded,
+ isExpanded: PropTypes.func.isRequired,
+
+ // The height of an item in the tree including margin and padding, in
+ // pixels.
+ itemHeight: PropTypes.number.isRequired,
+
+ // Optional props
+
+ // The currently focused item, if any such item exists.
+ focused: PropTypes.any,
+
+ // Handle when a new item is focused.
+ onFocus: PropTypes.func,
+
+ // The depth to which we should automatically expand new items.
+ autoExpandDepth: PropTypes.number,
+
+ // Optional event handlers for when items are expanded or collapsed. Useful
+ // for dispatching redux events and updating application state, maybe lazily
+ // loading subtrees from a worker, etc.
+ //
+ // Type:
+ // onExpand(item: Item)
+ // onCollapse(item: Item)
+ //
+ // Example:
+ //
+ // onExpand: item => dispatchExpandActionToRedux(item)
+ onExpand: PropTypes.func,
+ onCollapse: PropTypes.func,
+ },
+
+ getDefaultProps() {
+ return {
+ autoExpandDepth: AUTO_EXPAND_DEPTH,
+ };
+ },
+
+ getInitialState() {
+ return {
+ scroll: 0,
+ height: window.innerHeight,
+ seen: new Set(),
+ };
+ },
+
+ componentDidMount() {
+ window.addEventListener("resize", this._updateHeight);
+ this._autoExpand();
+ this._updateHeight();
+ },
+
+ componentWillReceiveProps(nextProps) {
+ this._autoExpand();
+ this._updateHeight();
+ },
+
+ componentWillUnmount() {
+ window.removeEventListener("resize", this._updateHeight);
+ },
+
+ _autoExpand() {
+ if (!this.props.autoExpandDepth) {
+ return;
+ }
+
+ // Automatically expand the first autoExpandDepth levels for new items. Do
+ // not use the usual DFS infrastructure because we don't want to ignore
+ // collapsed nodes.
+ const autoExpand = (item, currentDepth) => {
+ if (currentDepth >= this.props.autoExpandDepth ||
+ this.state.seen.has(item)) {
+ return;
+ }
+
+ this.props.onExpand(item);
+ this.state.seen.add(item);
+
+ const children = this.props.getChildren(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ autoExpand(children[i], currentDepth + 1);
+ }
+ };
+
+ const roots = this.props.getRoots();
+ const length = roots.length;
+ for (let i = 0; i < length; i++) {
+ autoExpand(roots[i], 0);
+ }
+ },
+
+ _preventArrowKeyScrolling(e) {
+ switch (e.key) {
+ case "ArrowUp":
+ case "ArrowDown":
+ case "ArrowLeft":
+ case "ArrowRight":
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.nativeEvent) {
+ if (e.nativeEvent.preventDefault) {
+ e.nativeEvent.preventDefault();
+ }
+ if (e.nativeEvent.stopPropagation) {
+ e.nativeEvent.stopPropagation();
+ }
+ }
+ }
+ },
+
+ /**
+ * Updates the state's height based on clientHeight.
+ */
+ _updateHeight() {
+ this.setState({
+ height: this.refs.tree.clientHeight
+ });
+ },
+
+ /**
+ * Perform a pre-order depth-first search from item.
+ */
+ _dfs(item, maxDepth = Infinity, traversal = [], _depth = 0) {
+ traversal.push({ item, depth: _depth });
+
+ if (!this.props.isExpanded(item)) {
+ return traversal;
+ }
+
+ const nextDepth = _depth + 1;
+
+ if (nextDepth > maxDepth) {
+ return traversal;
+ }
+
+ const children = this.props.getChildren(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ this._dfs(children[i], maxDepth, traversal, nextDepth);
+ }
+
+ return traversal;
+ },
+
+ /**
+ * Perform a pre-order depth-first search over the whole forest.
+ */
+ _dfsFromRoots(maxDepth = Infinity) {
+ const traversal = [];
+
+ const roots = this.props.getRoots();
+ const length = roots.length;
+ for (let i = 0; i < length; i++) {
+ this._dfs(roots[i], maxDepth, traversal);
+ }
+
+ return traversal;
+ },
+
+ /**
+ * Expands current row.
+ *
+ * @param {Object} item
+ * @param {Boolean} expandAllChildren
+ */
+ _onExpand: oncePerAnimationFrame(function (item, expandAllChildren) {
+ if (this.props.onExpand) {
+ this.props.onExpand(item);
+
+ if (expandAllChildren) {
+ const children = this._dfs(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ this.props.onExpand(children[i].item);
+ }
+ }
+ }
+ }),
+
+ /**
+ * Collapses current row.
+ *
+ * @param {Object} item
+ */
+ _onCollapse: oncePerAnimationFrame(function (item) {
+ if (this.props.onCollapse) {
+ this.props.onCollapse(item);
+ }
+ }),
+
+ /**
+ * Sets the passed in item to be the focused item.
+ *
+ * @param {Number} index
+ * The index of the item in a full DFS traversal (ignoring collapsed
+ * nodes). Ignored if `item` is undefined.
+ *
+ * @param {Object|undefined} item
+ * The item to be focused, or undefined to focus no item.
+ */
+ _focus(index, item) {
+ if (item !== undefined) {
+ const itemStartPosition = index * this.props.itemHeight;
+ const itemEndPosition = (index + 1) * this.props.itemHeight;
+
+ // Note that if the height of the viewport (this.state.height) is less
+ // than `this.props.itemHeight`, we could accidentally try and scroll both
+ // up and down in a futile attempt to make both the item's start and end
+ // positions visible. Instead, give priority to the start of the item by
+ // checking its position first, and then using an "else if", rather than
+ // a separate "if", for the end position.
+ if (this.state.scroll > itemStartPosition) {
+ this.refs.tree.scrollTo(0, itemStartPosition);
+ } else if ((this.state.scroll + this.state.height) < itemEndPosition) {
+ this.refs.tree.scrollTo(0, itemEndPosition - this.state.height);
+ }
+ }
+
+ if (this.props.onFocus) {
+ this.props.onFocus(item);
+ }
+ },
+
+ /**
+ * Sets the state to have no focused item.
+ */
+ _onBlur() {
+ this._focus(0, undefined);
+ },
+
+ /**
+ * Fired on a scroll within the tree's container, updates
+ * the stored position of the view port to handle virtual view rendering.
+ *
+ * @param {Event} e
+ */
+ _onScroll: oncePerAnimationFrame(function (e) {
+ this.setState({
+ scroll: Math.max(this.refs.tree.scrollTop, 0),
+ height: this.refs.tree.clientHeight
+ });
+ }),
+
+ /**
+ * Handles key down events in the tree's container.
+ *
+ * @param {Event} e
+ */
+ _onKeyDown(e) {
+ if (this.props.focused == null) {
+ return;
+ }
+
+ // Allow parent nodes to use navigation arrows with modifiers.
+ if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
+ return;
+ }
+
+ this._preventArrowKeyScrolling(e);
+
+ switch (e.key) {
+ case "ArrowUp":
+ this._focusPrevNode();
+ return;
+
+ case "ArrowDown":
+ this._focusNextNode();
+ return;
+
+ case "ArrowLeft":
+ if (this.props.isExpanded(this.props.focused)
+ && this.props.getChildren(this.props.focused).length) {
+ this._onCollapse(this.props.focused);
+ } else {
+ this._focusParentNode();
+ }
+ return;
+
+ case "ArrowRight":
+ if (!this.props.isExpanded(this.props.focused)) {
+ this._onExpand(this.props.focused);
+ } else {
+ this._focusNextNode();
+ }
+ return;
+ }
+ },
+
+ /**
+ * Sets the previous node relative to the currently focused item, to focused.
+ */
+ _focusPrevNode: oncePerAnimationFrame(function () {
+ // Start a depth first search and keep going until we reach the currently
+ // focused node. Focus the previous node in the DFS, if it exists. If it
+ // doesn't exist, we're at the first node already.
+
+ let prev;
+ let prevIndex;
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ for (let i = 0; i < length; i++) {
+ const item = traversal[i].item;
+ if (item === this.props.focused) {
+ break;
+ }
+ prev = item;
+ prevIndex = i;
+ }
+
+ if (prev === undefined) {
+ return;
+ }
+
+ this._focus(prevIndex, prev);
+ }),
+
+ /**
+ * Handles the down arrow key which will focus either the next child
+ * or sibling row.
+ */
+ _focusNextNode: oncePerAnimationFrame(function () {
+ // Start a depth first search and keep going until we reach the currently
+ // focused node. Focus the next node in the DFS, if it exists. If it
+ // doesn't exist, we're at the last node already.
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ let i = 0;
+
+ while (i < length) {
+ if (traversal[i].item === this.props.focused) {
+ break;
+ }
+ i++;
+ }
+
+ if (i + 1 < traversal.length) {
+ this._focus(i + 1, traversal[i + 1].item);
+ }
+ }),
+
+ /**
+ * Handles the left arrow key, going back up to the current rows'
+ * parent row.
+ */
+ _focusParentNode: oncePerAnimationFrame(function () {
+ const parent = this.props.getParent(this.props.focused);
+ if (!parent) {
+ return;
+ }
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ let parentIndex = 0;
+ for (; parentIndex < length; parentIndex++) {
+ if (traversal[parentIndex].item === parent) {
+ break;
+ }
+ }
+
+ this._focus(parentIndex, parent);
+ }),
+
+ render() {
+ const traversal = this._dfsFromRoots();
+
+ // 'begin' and 'end' are the index of the first (at least partially) visible item
+ // and the index after the last (at least partially) visible item, respectively.
+ // `NUMBER_OF_OFFSCREEN_ITEMS` is removed from `begin` and added to `end` so that
+ // the top and bottom of the page are filled with the `NUMBER_OF_OFFSCREEN_ITEMS`
+ // previous and next items respectively, which helps the user to see fewer empty
+ // gaps when scrolling quickly.
+ const { itemHeight } = this.props;
+ const { scroll, height } = this.state;
+ const begin = Math.max(((scroll / itemHeight) | 0) - NUMBER_OF_OFFSCREEN_ITEMS, 0);
+ const end = Math.ceil((scroll + height) / itemHeight) + NUMBER_OF_OFFSCREEN_ITEMS;
+ const toRender = traversal.slice(begin, end);
+ const topSpacerHeight = begin * itemHeight;
+ const bottomSpacerHeight = Math.max(traversal.length - end, 0) * itemHeight;
+
+ const nodes = [
+ dom.div({
+ key: "top-spacer",
+ style: {
+ padding: 0,
+ margin: 0,
+ height: topSpacerHeight + "px"
+ }
+ })
+ ];
+
+ for (let i = 0; i < toRender.length; i++) {
+ const index = begin + i;
+ const first = index == 0;
+ const last = index == traversal.length - 1;
+ const { item, depth } = toRender[i];
+ nodes.push(TreeNode({
+ key: this.props.getKey(item),
+ index,
+ first,
+ last,
+ item,
+ depth,
+ renderItem: this.props.renderItem,
+ focused: this.props.focused === item,
+ expanded: this.props.isExpanded(item),
+ hasChildren: !!this.props.getChildren(item).length,
+ onExpand: this._onExpand,
+ onCollapse: this._onCollapse,
+ onFocus: () => this._focus(begin + i, item),
+ onFocusedNodeUnmount: () => this.refs.tree && this.refs.tree.focus(),
+ }));
+ }
+
+ nodes.push(dom.div({
+ key: "bottom-spacer",
+ style: {
+ padding: 0,
+ margin: 0,
+ height: bottomSpacerHeight + "px"
+ }
+ }));
+
+ return dom.div(
+ {
+ className: "tree",
+ ref: "tree",
+ onKeyDown: this._onKeyDown,
+ onKeyPress: this._preventArrowKeyScrolling,
+ onKeyUp: this._preventArrowKeyScrolling,
+ onScroll: this._onScroll,
+ style: {
+ padding: 0,
+ margin: 0
+ }
+ },
+ nodes
+ );
+ }
+});
+
+/**
+ * An arrow that displays whether its node is expanded (▼) or collapsed
+ * (▶). When its node has no children, it is hidden.
+ */
+const ArrowExpander = createFactory(createClass({
+ displayName: "ArrowExpander",
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.item !== nextProps.item
+ || this.props.visible !== nextProps.visible
+ || this.props.expanded !== nextProps.expanded;
+ },
+
+ render() {
+ const attrs = {
+ className: "arrow theme-twisty",
+ onClick: this.props.expanded
+ ? () => this.props.onCollapse(this.props.item)
+ : e => this.props.onExpand(this.props.item, e.altKey)
+ };
+
+ if (this.props.expanded) {
+ attrs.className += " open";
+ }
+
+ if (!this.props.visible) {
+ attrs.style = {
+ visibility: "hidden"
+ };
+ }
+
+ return dom.div(attrs);
+ }
+}));
+
+const TreeNode = createFactory(createClass({
+ componentDidMount() {
+ if (this.props.focused) {
+ this.refs.button.focus();
+ }
+ },
+
+ componentDidUpdate() {
+ if (this.props.focused) {
+ this.refs.button.focus();
+ }
+ },
+
+ componentWillUnmount() {
+ // If this node is being destroyed and has focus, transfer the focus manually
+ // to the parent tree component. Otherwise, the focus will get lost and keyboard
+ // navigation in the tree will stop working. This is a workaround for a XUL bug.
+ // See bugs 1259228 and 1152441 for details.
+ // DE-XUL: Remove this hack once all usages are only in HTML documents.
+ if (this.props.focused) {
+ this.refs.button.blur();
+ if (this.props.onFocusedNodeUnmount) {
+ this.props.onFocusedNodeUnmount();
+ }
+ }
+ },
+
+ _buttonAttrs: {
+ ref: "button",
+ style: {
+ opacity: 0,
+ width: "0 !important",
+ height: "0 !important",
+ padding: "0 !important",
+ outline: "none",
+ MozAppearance: "none",
+ // XXX: Despite resetting all of the above properties (and margin), the
+ // button still ends up with ~79px width, so we set a large negative
+ // margin to completely hide it.
+ MozMarginStart: "-1000px !important",
+ }
+ },
+
+ render() {
+ const arrow = ArrowExpander({
+ item: this.props.item,
+ expanded: this.props.expanded,
+ visible: this.props.hasChildren,
+ onExpand: this.props.onExpand,
+ onCollapse: this.props.onCollapse,
+ });
+
+ let classList = [ "tree-node", "div" ];
+ if (this.props.index % 2) {
+ classList.push("tree-node-odd");
+ }
+ if (this.props.first) {
+ classList.push("tree-node-first");
+ }
+ if (this.props.last) {
+ classList.push("tree-node-last");
+ }
+
+ return dom.div(
+ {
+ className: classList.join(" "),
+ onFocus: this.props.onFocus,
+ onClick: this.props.onFocus,
+ onBlur: this.props.onBlur,
+ "data-expanded": this.props.expanded ? "" : undefined,
+ "data-depth": this.props.depth,
+ style: {
+ padding: 0,
+ margin: 0
+ }
+ },
+
+ this.props.renderItem(this.props.item,
+ this.props.depth,
+ this.props.focused,
+ arrow,
+ this.props.expanded),
+
+ // XXX: OSX won't focus/blur regular elements even if you set tabindex
+ // unless there is an input/button child.
+ dom.button(this._buttonAttrs)
+ );
+ }
+}));
+
+/**
+ * Create a function that calls the given function `fn` only once per animation
+ * frame.
+ *
+ * @param {Function} fn
+ * @returns {Function}
+ */
+function oncePerAnimationFrame(fn) {
+ let animationId = null;
+ let argsToPass = null;
+ return function (...args) {
+ argsToPass = args;
+ if (animationId !== null) {
+ return;
+ }
+
+ animationId = requestAnimationFrame(() => {
+ fn.call(this, ...argsToPass);
+ animationId = null;
+ argsToPass = null;
+ });
+ };
+}
diff --git a/devtools/client/shared/components/tree/label-cell.js b/devtools/client/shared/components/tree/label-cell.js
new file mode 100644
index 000000000..e14875b4d
--- /dev/null
+++ b/devtools/client/shared/components/tree/label-cell.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { td, span } = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * Render the default cell used for toggle buttons
+ */
+ let LabelCell = React.createClass({
+ displayName: "LabelCell",
+
+ // See the TreeView component for details related
+ // to the 'member' object.
+ propTypes: {
+ member: PropTypes.object.isRequired
+ },
+
+ render: function () {
+ let member = this.props.member;
+ let level = member.level || 0;
+
+ // Compute indentation dynamically. The deeper the item is
+ // inside the hierarchy, the bigger is the left padding.
+ let rowStyle = {
+ "paddingInlineStart": (level * 16) + "px",
+ };
+
+ let iconClassList = ["treeIcon"];
+ if (member.hasChildren && member.loading) {
+ iconClassList.push("devtools-throbber");
+ } else if (member.hasChildren) {
+ iconClassList.push("theme-twisty");
+ }
+ if (member.open) {
+ iconClassList.push("open");
+ }
+
+ return (
+ td({
+ className: "treeLabelCell",
+ key: "default",
+ style: rowStyle},
+ span({ className: iconClassList.join(" ") }),
+ span({
+ className: "treeLabel " + member.type + "Label",
+ "data-level": level
+ }, member.name)
+ )
+ );
+ }
+ });
+
+ // Exports from this module
+ module.exports = LabelCell;
+});
diff --git a/devtools/client/shared/components/tree/moz.build b/devtools/client/shared/components/tree/moz.build
new file mode 100644
index 000000000..a7413f25a
--- /dev/null
+++ b/devtools/client/shared/components/tree/moz.build
@@ -0,0 +1,14 @@
+# 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/.
+
+DevToolsModules(
+ 'label-cell.js',
+ 'object-provider.js',
+ 'tree-cell.js',
+ 'tree-header.js',
+ 'tree-row.js',
+ 'tree-view.css',
+ 'tree-view.js',
+)
diff --git a/devtools/client/shared/components/tree/object-provider.js b/devtools/client/shared/components/tree/object-provider.js
new file mode 100644
index 000000000..58519f81f
--- /dev/null
+++ b/devtools/client/shared/components/tree/object-provider.js
@@ -0,0 +1,90 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ /**
+ * Implementation of the default data provider. A provider is state less
+ * object responsible for transformation data (usually a state) to
+ * a structure that can be directly consumed by the tree-view component.
+ */
+ let ObjectProvider = {
+ getChildren: function (object) {
+ let children = [];
+
+ if (object instanceof ObjectProperty) {
+ object = object.value;
+ }
+
+ if (!object) {
+ return [];
+ }
+
+ if (typeof (object) == "string") {
+ return [];
+ }
+
+ for (let prop in object) {
+ try {
+ children.push(new ObjectProperty(prop, object[prop]));
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ return children;
+ },
+
+ hasChildren: function (object) {
+ if (object instanceof ObjectProperty) {
+ object = object.value;
+ }
+
+ if (!object) {
+ return false;
+ }
+
+ if (typeof object == "string") {
+ return false;
+ }
+
+ if (typeof object !== "object") {
+ return false;
+ }
+
+ return Object.keys(object).length > 0;
+ },
+
+ getLabel: function (object) {
+ return (object instanceof ObjectProperty) ?
+ object.name : null;
+ },
+
+ getValue: function (object) {
+ return (object instanceof ObjectProperty) ?
+ object.value : null;
+ },
+
+ getKey: function (object) {
+ return (object instanceof ObjectProperty) ?
+ object.name : null;
+ },
+
+ getType: function (object) {
+ return (object instanceof ObjectProperty) ?
+ typeof object.value : typeof object;
+ }
+ };
+
+ function ObjectProperty(name, value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ // Exports from this module
+ exports.ObjectProperty = ObjectProperty;
+ exports.ObjectProvider = ObjectProvider;
+});
diff --git a/devtools/client/shared/components/tree/tree-cell.js b/devtools/client/shared/components/tree/tree-cell.js
new file mode 100644
index 000000000..f3c48510f
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-cell.js
@@ -0,0 +1,101 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { td, span } = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This template represents a cell in TreeView row. It's rendered
+ * using <td> element (the row is <tr> and the entire tree is <table>).
+ */
+ let TreeCell = React.createClass({
+ displayName: "TreeCell",
+
+ // See TreeView component for detailed property explanation.
+ propTypes: {
+ value: PropTypes.any,
+ decorator: PropTypes.object,
+ id: PropTypes.string.isRequired,
+ member: PropTypes.object.isRequired,
+ renderValue: PropTypes.func.isRequired
+ },
+
+ /**
+ * Optimize cell rendering. Rerender cell content only if
+ * the value or expanded state changes.
+ */
+ shouldComponentUpdate: function (nextProps) {
+ return (this.props.value != nextProps.value) ||
+ (this.props.member.open != nextProps.member.open);
+ },
+
+ getCellClass: function (object, id) {
+ let decorator = this.props.decorator;
+ if (!decorator || !decorator.getCellClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getCellClass(object, id);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ },
+
+ render: function () {
+ let member = this.props.member;
+ let type = member.type || "";
+ let id = this.props.id;
+ let value = this.props.value;
+ let decorator = this.props.decorator;
+
+ // Compute class name list for the <td> element.
+ let classNames = this.getCellClass(member.object, id) || [];
+ classNames.push("treeValueCell");
+ classNames.push(type + "Cell");
+
+ // Render value using a default render function or custom
+ // provided function from props or a decorator.
+ let renderValue = this.props.renderValue || defaultRenderValue;
+ if (decorator && decorator.renderValue) {
+ renderValue = decorator.renderValue(member.object, id) || renderValue;
+ }
+
+ let props = Object.assign({}, this.props, {
+ object: value,
+ });
+
+ // Render me!
+ return (
+ td({ className: classNames.join(" ") },
+ span({}, renderValue(props))
+ )
+ );
+ }
+ });
+
+ // Default value rendering.
+ let defaultRenderValue = props => {
+ return (
+ props.object + ""
+ );
+ };
+
+ // Exports from this module
+ module.exports = TreeCell;
+});
diff --git a/devtools/client/shared/components/tree/tree-header.js b/devtools/client/shared/components/tree/tree-header.js
new file mode 100644
index 000000000..eec5363dd
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-header.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { thead, tr, td, div } = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This component is responsible for rendering tree header.
+ * It's based on <thead> element.
+ */
+ let TreeHeader = React.createClass({
+ displayName: "TreeHeader",
+
+ // See also TreeView component for detailed info about properties.
+ propTypes: {
+ // Custom tree decorator
+ decorator: PropTypes.object,
+ // True if the header should be visible
+ header: PropTypes.bool,
+ // Array with column definition
+ columns: PropTypes.array
+ },
+
+ getDefaultProps: function () {
+ return {
+ columns: [{
+ id: "default"
+ }]
+ };
+ },
+
+ getHeaderClass: function (colId) {
+ let decorator = this.props.decorator;
+ if (!decorator || !decorator.getHeaderClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getHeaderClass(colId);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ },
+
+ render: function () {
+ let cells = [];
+ let visible = this.props.header;
+
+ // Render the rest of the columns (if any)
+ this.props.columns.forEach(col => {
+ let cellStyle = {
+ "width": col.width ? col.width : "",
+ };
+
+ let classNames = [];
+
+ if (visible) {
+ classNames = this.getHeaderClass(col.id);
+ classNames.push("treeHeaderCell");
+ }
+
+ cells.push(
+ td({
+ className: classNames.join(" "),
+ style: cellStyle,
+ key: col.id},
+ div({ className: visible ? "treeHeaderCellBox" : "" },
+ visible ? col.title : ""
+ )
+ )
+ );
+ });
+
+ return (
+ thead({}, tr({ className: visible ? "treeHeaderRow" : "" },
+ cells
+ ))
+ );
+ }
+ });
+
+ // Exports from this module
+ module.exports = TreeHeader;
+});
diff --git a/devtools/client/shared/components/tree/tree-row.js b/devtools/client/shared/components/tree/tree-row.js
new file mode 100644
index 000000000..adfb1f3ae
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-row.js
@@ -0,0 +1,184 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+ // Tree
+ const TreeCell = React.createFactory(require("./tree-cell"));
+ const LabelCell = React.createFactory(require("./label-cell"));
+
+ // Shortcuts
+ const { tr } = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This template represents a node in TreeView component. It's rendered
+ * using <tr> element (the entire tree is one big <table>).
+ */
+ let TreeRow = React.createClass({
+ displayName: "TreeRow",
+
+ // See TreeView component for more details about the props and
+ // the 'member' object.
+ propTypes: {
+ member: PropTypes.shape({
+ object: PropTypes.obSject,
+ name: PropTypes.sring,
+ type: PropTypes.string.isRequired,
+ rowClass: PropTypes.string.isRequired,
+ level: PropTypes.number.isRequired,
+ hasChildren: PropTypes.bool,
+ value: PropTypes.any,
+ open: PropTypes.bool.isRequired,
+ path: PropTypes.string.isRequired,
+ hidden: PropTypes.bool,
+ }),
+ decorator: PropTypes.object,
+ renderCell: PropTypes.object,
+ renderLabelCell: PropTypes.object,
+ columns: PropTypes.array.isRequired,
+ provider: PropTypes.object.isRequired,
+ onClick: PropTypes.func.isRequired
+ },
+
+ componentWillReceiveProps(nextProps) {
+ // I don't like accessing the underlying DOM elements directly,
+ // but this optimization makes the filtering so damn fast!
+ // The row doesn't have to be re-rendered, all we really need
+ // to do is toggling a class name.
+ // The important part is that DOM elements don't need to be
+ // re-created when they should appear again.
+ if (nextProps.member.hidden != this.props.member.hidden) {
+ let row = ReactDOM.findDOMNode(this);
+ row.classList.toggle("hidden");
+ }
+ },
+
+ /**
+ * Optimize row rendering. If props are the same do not render.
+ * This makes the rendering a lot faster!
+ */
+ shouldComponentUpdate: function (nextProps) {
+ let props = ["name", "open", "value", "loading"];
+ for (let p in props) {
+ if (nextProps.member[props[p]] != this.props.member[props[p]]) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ getRowClass: function (object) {
+ let decorator = this.props.decorator;
+ if (!decorator || !decorator.getRowClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getRowClass(object);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ },
+
+ render: function () {
+ let member = this.props.member;
+ let decorator = this.props.decorator;
+
+ // Compute class name list for the <tr> element.
+ let classNames = this.getRowClass(member.object) || [];
+ classNames.push("treeRow");
+ classNames.push(member.type + "Row");
+
+ if (member.hasChildren) {
+ classNames.push("hasChildren");
+ }
+
+ if (member.open) {
+ classNames.push("opened");
+ }
+
+ if (member.loading) {
+ classNames.push("loading");
+ }
+
+ if (member.hidden) {
+ classNames.push("hidden");
+ }
+
+ // The label column (with toggle buttons) is usually
+ // the first one, but there might be cases (like in
+ // the Memory panel) where the toggling is done
+ // in the last column.
+ let cells = [];
+
+ // Get components for rendering cells.
+ let renderCell = this.props.renderCell || RenderCell;
+ let renderLabelCell = this.props.renderLabelCell || RenderLabelCell;
+ if (decorator && decorator.renderLabelCell) {
+ renderLabelCell = decorator.renderLabelCell(member.object) ||
+ renderLabelCell;
+ }
+
+ // Render a cell for every column.
+ this.props.columns.forEach(col => {
+ let props = Object.assign({}, this.props, {
+ key: col.id,
+ id: col.id,
+ value: this.props.provider.getValue(member.object, col.id)
+ });
+
+ if (decorator && decorator.renderCell) {
+ renderCell = decorator.renderCell(member.object, col.id);
+ }
+
+ let render = (col.id == "default") ? renderLabelCell : renderCell;
+
+ // Some cells don't have to be rendered. This happens when some
+ // other cells span more columns. Note that the label cells contains
+ // toggle buttons and should be usually there unless we are rendering
+ // a simple non-expandable table.
+ if (render) {
+ cells.push(render(props));
+ }
+ });
+
+ // Render tree row
+ return (
+ tr({
+ className: classNames.join(" "),
+ onClick: this.props.onClick},
+ cells
+ )
+ );
+ }
+ });
+
+ // Helpers
+
+ let RenderCell = props => {
+ return TreeCell(props);
+ };
+
+ let RenderLabelCell = props => {
+ return LabelCell(props);
+ };
+
+ // Exports from this module
+ module.exports = TreeRow;
+});
diff --git a/devtools/client/shared/components/tree/tree-view.css b/devtools/client/shared/components/tree/tree-view.css
new file mode 100644
index 000000000..850533872
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-view.css
@@ -0,0 +1,157 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 url('resource://devtools/client/shared/components/reps/reps.css');
+
+/******************************************************************************/
+/* TreeView Colors */
+
+:root {
+ --tree-link-color: blue;
+ --tree-header-background: #C8D2DC;
+ --tree-header-sorted-background: #AAC3DC;
+}
+
+/******************************************************************************/
+/* TreeView Table*/
+
+.treeTable .treeLabelCell {
+ padding: 2px 0;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+.treeTable .treeLabelCell::after {
+ content: ":";
+ color: var(--object-color);
+}
+
+.treeTable .treeValueCell {
+ padding: 2px 0;
+ padding-inline-start: 5px;
+ overflow: hidden;
+}
+
+.treeTable .treeLabel {
+ cursor: default;
+ overflow: hidden;
+ padding-inline-start: 4px;
+ white-space: nowrap;
+}
+
+/* No paddding if there is actually no label */
+.treeTable .treeLabel:empty {
+ padding-inline-start: 0;
+}
+
+.treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
+ cursor: pointer;
+ color: var(--tree-link-color);
+ text-decoration: underline;
+}
+
+/* Filtering */
+.treeTable .treeRow.hidden {
+ display: none;
+}
+
+/******************************************************************************/
+/* Toggle Icon */
+
+.treeTable .treeRow .treeIcon {
+ height: 14px;
+ width: 14px;
+ font-size: 10px; /* Set the size of loading spinner */
+ display: inline-block;
+ vertical-align: bottom;
+ margin-inline-start: 3px;
+ padding-top: 1px;
+}
+
+/* All expanded/collapsed styles need to apply on immediate children
+ since there might be nested trees within a tree. */
+.treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
+ cursor: pointer;
+ background-repeat: no-repeat;
+}
+
+/******************************************************************************/
+/* Header */
+
+.treeTable .treeHeaderRow {
+ height: 18px;
+}
+
+.treeTable .treeHeaderCell {
+ cursor: pointer;
+ -moz-user-select: none;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ padding: 0 !important;
+ background: linear-gradient(
+ rgba(255, 255, 255, 0.05),
+ rgba(0, 0, 0, 0.05)),
+ radial-gradient(1px 60% at right,
+ rgba(0, 0, 0, 0.8) 0%,
+ transparent 80%) repeat-x var(--tree-header-background);
+ color: var(--theme-body-color);
+ white-space: nowrap;
+}
+
+.treeTable .treeHeaderCellBox {
+ padding: 2px 0;
+ padding-inline-start: 10px;
+ padding-inline-end: 14px;
+}
+
+.treeTable .treeHeaderRow > .treeHeaderCell:first-child > .treeHeaderCellBox {
+ padding: 0;
+}
+
+.treeTable .treeHeaderSorted {
+ background-color: var(--tree-header-sorted-background);
+}
+
+.treeTable .treeHeaderSorted > .treeHeaderCellBox {
+ background: url(chrome://devtools/skin/images/firebug/arrow-down.svg) no-repeat calc(100% - 4px);
+}
+
+.treeTable .treeHeaderSorted.sortedAscending > .treeHeaderCellBox {
+ background-image: url(chrome://devtools/skin/images/firebug/arrow-up.svg);
+}
+
+.treeTable .treeHeaderCell:hover:active {
+ background-image: linear-gradient(
+ rgba(0, 0, 0, 0.1),
+ transparent),
+ radial-gradient(1px 60% at right,
+ rgba(0, 0, 0, 0.8) 0%,
+ transparent 80%);
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-light .treeTable .treeRow:hover,
+.theme-dark .treeTable .treeRow:hover {
+ background-color: var(--theme-selection-background-semitransparent) !important;
+}
+
+.theme-firebug .treeTable .treeRow:hover {
+ background-color: var(--theme-body-background);
+}
+
+.theme-light .treeTable .treeLabel,
+.theme-dark .treeTable .treeLabel {
+ color: var(--theme-highlight-pink);
+}
+
+.theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover,
+.theme-dark .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
+ color: var(--theme-highlight-pink);
+}
+
+.theme-firebug .treeTable .treeLabel {
+ color: var(--theme-body-color);
+}
diff --git a/devtools/client/shared/components/tree/tree-view.js b/devtools/client/shared/components/tree/tree-view.js
new file mode 100644
index 000000000..9fae9addb
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-view.js
@@ -0,0 +1,352 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { ObjectProvider } = require("./object-provider");
+ const TreeRow = React.createFactory(require("./tree-row"));
+ const TreeHeader = React.createFactory(require("./tree-header"));
+
+ // Shortcuts
+ const DOM = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This component represents a tree view with expandable/collapsible nodes.
+ * The tree is rendered using <table> element where every node is represented
+ * by <tr> element. The tree is one big table where nodes (rows) are properly
+ * indented from the left to mimic hierarchical structure of the data.
+ *
+ * The tree can have arbitrary number of columns and so, might be use
+ * as an expandable tree-table UI widget as well. By default, there is
+ * one column for node label and one for node value.
+ *
+ * The tree is maintaining its (presentation) state, which consists
+ * from list of expanded nodes and list of columns.
+ *
+ * Complete data provider interface:
+ * var TreeProvider = {
+ * getChildren: function(object);
+ * hasChildren: function(object);
+ * getLabel: function(object, colId);
+ * getValue: function(object, colId);
+ * getKey: function(object);
+ * getType: function(object);
+ * }
+ *
+ * Complete tree decorator interface:
+ * var TreeDecorator = {
+ * getRowClass: function(object);
+ * getCellClass: function(object, colId);
+ * getHeaderClass: function(colId);
+ * renderValue: function(object, colId);
+ * renderRow: function(object);
+ * renderCelL: function(object, colId);
+ * renderLabelCell: function(object);
+ * }
+ */
+ let TreeView = React.createClass({
+ displayName: "TreeView",
+
+ // The only required property (not set by default) is the input data
+ // object that is used to puputate the tree.
+ propTypes: {
+ // The input data object.
+ object: PropTypes.any,
+ className: PropTypes.string,
+ // Data provider (see also the interface above)
+ provider: PropTypes.shape({
+ getChildren: PropTypes.func,
+ hasChildren: PropTypes.func,
+ getLabel: PropTypes.func,
+ getValue: PropTypes.func,
+ getKey: PropTypes.func,
+ getType: PropTypes.func,
+ }).isRequired,
+ // Tree decorator (see also the interface above)
+ decorator: PropTypes.shape({
+ getRowClass: PropTypes.func,
+ getCellClass: PropTypes.func,
+ getHeaderClass: PropTypes.func,
+ renderValue: PropTypes.func,
+ renderRow: PropTypes.func,
+ renderCelL: PropTypes.func,
+ renderLabelCell: PropTypes.func,
+ }),
+ // Custom tree row (node) renderer
+ renderRow: PropTypes.func,
+ // Custom cell renderer
+ renderCell: PropTypes.func,
+ // Custom value renderef
+ renderValue: PropTypes.func,
+ // Custom tree label (including a toggle button) renderer
+ renderLabelCell: PropTypes.func,
+ // Set of expanded nodes
+ expandedNodes: PropTypes.object,
+ // Custom filtering callback
+ onFilter: PropTypes.func,
+ // Custom sorting callback
+ onSort: PropTypes.func,
+ // A header is displayed if set to true
+ header: PropTypes.bool,
+ // Array of columns
+ columns: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string,
+ width: PropTypes.string
+ }))
+ },
+
+ getDefaultProps: function () {
+ return {
+ object: null,
+ renderRow: null,
+ provider: ObjectProvider,
+ expandedNodes: new Set(),
+ columns: []
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ expandedNodes: this.props.expandedNodes,
+ columns: ensureDefaultColumn(this.props.columns)
+ };
+ },
+
+ // Node expand/collapse
+
+ toggle: function (nodePath) {
+ let nodes = this.state.expandedNodes;
+ if (this.isExpanded(nodePath)) {
+ nodes.delete(nodePath);
+ } else {
+ nodes.add(nodePath);
+ }
+
+ // Compute new state and update the tree.
+ this.setState(Object.assign({}, this.state, {
+ expandedNodes: nodes
+ }));
+ },
+
+ isExpanded: function (nodePath) {
+ return this.state.expandedNodes.has(nodePath);
+ },
+
+ // Event Handlers
+
+ onClickRow: function (nodePath, event) {
+ event.stopPropagation();
+ this.toggle(nodePath);
+ },
+
+ // Filtering & Sorting
+
+ /**
+ * Filter out nodes that don't correspond to the current filter.
+ * @return {Boolean} true if the node should be visible otherwise false.
+ */
+ onFilter: function (object) {
+ let onFilter = this.props.onFilter;
+ return onFilter ? onFilter(object) : true;
+ },
+
+ onSort: function (parent, children) {
+ let onSort = this.props.onSort;
+ return onSort ? onSort(parent, children) : children;
+ },
+
+ // Members
+
+ /**
+ * Return children node objects (so called 'members') for given
+ * parent object.
+ */
+ getMembers: function (parent, level, path) {
+ // Strings don't have children. Note that 'long' strings are using
+ // the expander icon (+/-) to display the entire original value,
+ // but there are no child items.
+ if (typeof parent == "string") {
+ return [];
+ }
+
+ let provider = this.props.provider;
+ let children = provider.getChildren(parent) || [];
+
+ // If the return value is non-array, the children
+ // are being loaded asynchronously.
+ if (!Array.isArray(children)) {
+ return children;
+ }
+
+ children = this.onSort(parent, children) || children;
+
+ return children.map(child => {
+ let key = provider.getKey(child);
+ let nodePath = path + "/" + key;
+ let type = provider.getType(child);
+ let hasChildren = provider.hasChildren(child);
+
+ // Value with no column specified is used for optimization.
+ // The row is re-rendered only if this value changes.
+ // Value for actual column is get when a cell is rendered.
+ let value = provider.getValue(child);
+
+ if (isLongString(value)) {
+ hasChildren = true;
+ }
+
+ // Return value is a 'member' object containing meta-data about
+ // tree node. It describes node label, value, type, etc.
+ return {
+ // An object associated with this node.
+ object: child,
+ // A label for the child node
+ name: provider.getLabel(child),
+ // Data type of the child node (used for CSS customization)
+ type: type,
+ // Class attribute computed from the type.
+ rowClass: "treeRow-" + type,
+ // Level of the child within the hierarchy (top == 0)
+ level: level,
+ // True if this node has children.
+ hasChildren: hasChildren,
+ // Value associated with this node (as provided by the data provider)
+ value: value,
+ // True if the node is expanded.
+ open: this.isExpanded(nodePath),
+ // Node path
+ path: nodePath,
+ // True if the node is hidden (used for filtering)
+ hidden: !this.onFilter(child)
+ };
+ });
+ },
+
+ /**
+ * Render tree rows/nodes.
+ */
+ renderRows: function (parent, level = 0, path = "") {
+ let rows = [];
+ let decorator = this.props.decorator;
+ let renderRow = this.props.renderRow || TreeRow;
+
+ // Get children for given parent node, iterate over them and render
+ // a row for every one. Use row template (a component) from properties.
+ // If the return value is non-array, the children are being loaded
+ // asynchronously.
+ let members = this.getMembers(parent, level, path);
+ if (!Array.isArray(members)) {
+ return members;
+ }
+
+ members.forEach(member => {
+ if (decorator && decorator.renderRow) {
+ renderRow = decorator.renderRow(member.object) || renderRow;
+ }
+
+ let props = Object.assign({}, this.props, {
+ key: member.path,
+ member: member,
+ columns: this.state.columns,
+ onClick: this.onClickRow.bind(this, member.path)
+ });
+
+ // Render single row.
+ rows.push(renderRow(props));
+
+ // If a child node is expanded render its rows too.
+ if (member.hasChildren && member.open) {
+ let childRows = this.renderRows(member.object, level + 1,
+ member.path);
+
+ // If children needs to be asynchronously fetched first,
+ // set 'loading' property to the parent row. Otherwise
+ // just append children rows to the array of all rows.
+ if (!Array.isArray(childRows)) {
+ let lastIndex = rows.length - 1;
+ props.member.loading = true;
+ rows[lastIndex] = React.cloneElement(rows[lastIndex], props);
+ } else {
+ rows = rows.concat(childRows);
+ }
+ }
+ });
+
+ return rows;
+ },
+
+ render: function () {
+ let root = this.props.object;
+ let classNames = ["treeTable"];
+
+ // Use custom class name from props.
+ let className = this.props.className;
+ if (className) {
+ classNames.push(...className.split(" "));
+ }
+
+ // Alright, let's render all tree rows. The tree is one big <table>.
+ let rows = this.renderRows(root, 0, "");
+
+ // This happens when the view needs to do initial asynchronous
+ // fetch for the root object. The tree might provide a hook API
+ // for rendering animated spinner (just like for tree nodes).
+ if (!Array.isArray(rows)) {
+ rows = [];
+ }
+
+ let props = Object.assign({}, this.props, {
+ columns: this.state.columns
+ });
+
+ return (
+ DOM.table({
+ className: classNames.join(" "),
+ cellPadding: 0,
+ cellSpacing: 0},
+ TreeHeader(props),
+ DOM.tbody({},
+ rows
+ )
+ )
+ );
+ }
+ });
+
+ // Helpers
+
+ /**
+ * There should always be at least one column (the one with toggle buttons)
+ * and this function ensures that it's true.
+ */
+ function ensureDefaultColumn(columns) {
+ if (!columns) {
+ columns = [];
+ }
+
+ let defaultColumn = columns.filter(col => col.id == "default");
+ if (defaultColumn.length) {
+ return columns;
+ }
+
+ // The default column is usually the first one.
+ return [{id: "default"}, ...columns];
+ }
+
+ function isLongString(value) {
+ return typeof value == "string" && value.length > 50;
+ }
+
+ // Exports from this module
+ module.exports = TreeView;
+});
diff --git a/devtools/client/shared/css-angle.js b/devtools/client/shared/css-angle.js
new file mode 100644
index 000000000..f3612ed84
--- /dev/null
+++ b/devtools/client/shared/css-angle.js
@@ -0,0 +1,345 @@
+/* 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 {CSS_ANGLEUNIT} = require("devtools/shared/css/properties-db");
+
+const SPECIALVALUES = new Set([
+ "initial",
+ "inherit",
+ "unset"
+]);
+
+const {getCSSLexer} = require("devtools/shared/css/lexer");
+
+/**
+ * This module is used to convert between various angle units.
+ *
+ * Usage:
+ * let {angleUtils} = require("devtools/client/shared/css-angle");
+ * let angle = new angleUtils.CssAngle("180deg");
+ *
+ * angle.authored === "180deg"
+ * angle.valid === true
+ * angle.rad === "3,14rad"
+ * angle.grad === "200grad"
+ * angle.turn === "0.5turn"
+ *
+ * angle.toString() === "180deg"; // Outputs the angle value and its unit
+ * // Angle objects can be reused
+ * angle.newAngle("-1TURN") === "-1TURN"; // true
+ */
+
+function CssAngle(angleValue) {
+ this.newAngle(angleValue);
+}
+
+module.exports.angleUtils = {
+ CssAngle: CssAngle,
+ classifyAngle: classifyAngle
+};
+
+CssAngle.ANGLEUNIT = CSS_ANGLEUNIT;
+
+CssAngle.prototype = {
+ _angleUnit: null,
+ _angleUnitUppercase: false,
+
+ // The value as-authored.
+ authored: null,
+ // A lower-cased copy of |authored|.
+ lowerCased: null,
+
+ get angleUnit() {
+ if (this._angleUnit === null) {
+ this._angleUnit = classifyAngle(this.authored);
+ }
+ return this._angleUnit;
+ },
+
+ set angleUnit(unit) {
+ this._angleUnit = unit;
+ },
+
+ get valid() {
+ let token = getCSSLexer(this.authored).nextToken();
+ if (!token) {
+ return false;
+ }
+ return (token.tokenType === "dimension"
+ && token.text.toLowerCase() in CssAngle.ANGLEUNIT);
+ },
+
+ get specialValue() {
+ return SPECIALVALUES.has(this.lowerCased) ? this.authored : null;
+ },
+
+ get deg() {
+ let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+
+ let angleUnit = classifyAngle(this.authored);
+ if (angleUnit === CssAngle.ANGLEUNIT.deg) {
+ // The angle is valid and is in degree.
+ return this.authored;
+ }
+
+ let degValue;
+ if (angleUnit === CssAngle.ANGLEUNIT.rad) {
+ // The angle is valid and is in radian.
+ degValue = this.authoredAngleValue / (Math.PI / 180);
+ }
+
+ if (angleUnit === CssAngle.ANGLEUNIT.grad) {
+ // The angle is valid and is in gradian.
+ degValue = this.authoredAngleValue * 0.9;
+ }
+
+ if (angleUnit === CssAngle.ANGLEUNIT.turn) {
+ // The angle is valid and is in turn.
+ degValue = this.authoredAngleValue * 360;
+ }
+
+ let unitStr = CssAngle.ANGLEUNIT.deg;
+ if (this._angleUnitUppercase === true) {
+ unitStr = unitStr.toUpperCase();
+ }
+ return `${Math.round(degValue * 100) / 100}${unitStr}`;
+ },
+
+ get rad() {
+ let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+
+ let unit = classifyAngle(this.authored);
+ if (unit === CssAngle.ANGLEUNIT.rad) {
+ // The angle is valid and is in radian.
+ return this.authored;
+ }
+
+ let radValue;
+ if (unit === CssAngle.ANGLEUNIT.deg) {
+ // The angle is valid and is in degree.
+ radValue = this.authoredAngleValue * (Math.PI / 180);
+ }
+
+ if (unit === CssAngle.ANGLEUNIT.grad) {
+ // The angle is valid and is in gradian.
+ radValue = this.authoredAngleValue * 0.9 * (Math.PI / 180);
+ }
+
+ if (unit === CssAngle.ANGLEUNIT.turn) {
+ // The angle is valid and is in turn.
+ radValue = this.authoredAngleValue * 360 * (Math.PI / 180);
+ }
+
+ let unitStr = CssAngle.ANGLEUNIT.rad;
+ if (this._angleUnitUppercase === true) {
+ unitStr = unitStr.toUpperCase();
+ }
+ return `${Math.round(radValue * 10000) / 10000}${unitStr}`;
+ },
+
+ get grad() {
+ let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+
+ let unit = classifyAngle(this.authored);
+ if (unit === CssAngle.ANGLEUNIT.grad) {
+ // The angle is valid and is in gradian
+ return this.authored;
+ }
+
+ let gradValue;
+ if (unit === CssAngle.ANGLEUNIT.deg) {
+ // The angle is valid and is in degree
+ gradValue = this.authoredAngleValue / 0.9;
+ }
+
+ if (unit === CssAngle.ANGLEUNIT.rad) {
+ // The angle is valid and is in radian
+ gradValue = this.authoredAngleValue / 0.9 / (Math.PI / 180);
+ }
+
+ if (unit === CssAngle.ANGLEUNIT.turn) {
+ // The angle is valid and is in turn
+ gradValue = this.authoredAngleValue * 400;
+ }
+
+ let unitStr = CssAngle.ANGLEUNIT.grad;
+ if (this._angleUnitUppercase === true) {
+ unitStr = unitStr.toUpperCase();
+ }
+ return `${Math.round(gradValue * 100) / 100}${unitStr}`;
+ },
+
+ get turn() {
+ let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
+ if (invalidOrSpecialValue !== false) {
+ return invalidOrSpecialValue;
+ }
+
+ let unit = classifyAngle(this.authored);
+ if (unit === CssAngle.ANGLEUNIT.turn) {
+ // The angle is valid and is in turn
+ return this.authored;
+ }
+
+ let turnValue;
+ if (unit === CssAngle.ANGLEUNIT.deg) {
+ // The angle is valid and is in degree
+ turnValue = this.authoredAngleValue / 360;
+ }
+
+ if (unit === CssAngle.ANGLEUNIT.rad) {
+ // The angle is valid and is in radian
+ turnValue = (this.authoredAngleValue / (Math.PI / 180)) / 360;
+ }
+
+ if (unit === CssAngle.ANGLEUNIT.grad) {
+ // The angle is valid and is in gradian
+ turnValue = this.authoredAngleValue / 400;
+ }
+
+ let unitStr = CssAngle.ANGLEUNIT.turn;
+ if (this._angleUnitUppercase === true) {
+ unitStr = unitStr.toUpperCase();
+ }
+ return `${Math.round(turnValue * 100) / 100}${unitStr}`;
+ },
+
+ /**
+ * Check whether the angle value is in the special list e.g.
+ * inherit or invalid.
+ *
+ * @return {String|Boolean}
+ * - If the current angle is a special value e.g. "inherit" then
+ * return the angle.
+ * - If the angle is invalid return an empty string.
+ * - If the angle is a regular angle e.g. 90deg so we return false
+ * to indicate that the angle is neither invalid nor special.
+ */
+ _getInvalidOrSpecialValue: function () {
+ if (this.specialValue) {
+ return this.specialValue;
+ }
+ if (!this.valid) {
+ return "";
+ }
+ return false;
+ },
+
+ /**
+ * Change angle
+ *
+ * @param {String} angle
+ * Any valid angle value + unit string
+ */
+ newAngle: function (angle) {
+ // Store a lower-cased version of the angle to help with format
+ // testing. The original text is kept as well so it can be
+ // returned when needed.
+ this.lowerCased = angle.toLowerCase();
+ this._angleUnitUppercase = (angle === angle.toUpperCase());
+ this.authored = angle;
+
+ let reg = new RegExp(
+ `(${Object.keys(CssAngle.ANGLEUNIT).join("|")})$`, "i");
+ let unitStartIdx = angle.search(reg);
+ this.authoredAngleValue = angle.substring(0, unitStartIdx);
+ this.authoredAngleUnit = angle.substring(unitStartIdx, angle.length);
+
+ return this;
+ },
+
+ nextAngleUnit: function () {
+ // Get a reordered array from the formats object
+ // to have the current format at the front so we can cycle through.
+ let formats = Object.keys(CssAngle.ANGLEUNIT);
+ let putOnEnd = formats.splice(0, formats.indexOf(this.angleUnit));
+ formats = formats.concat(putOnEnd);
+ let currentDisplayedValue = this[formats[0]];
+
+ for (let format of formats) {
+ if (this[format].toLowerCase() !== currentDisplayedValue.toLowerCase()) {
+ this.angleUnit = CssAngle.ANGLEUNIT[format];
+ break;
+ }
+ }
+ return this.toString();
+ },
+
+ /**
+ * Return a string representing a angle
+ */
+ toString: function () {
+ let angle;
+
+ switch (this.angleUnit) {
+ case CssAngle.ANGLEUNIT.deg:
+ angle = this.deg;
+ break;
+ case CssAngle.ANGLEUNIT.rad:
+ angle = this.rad;
+ break;
+ case CssAngle.ANGLEUNIT.grad:
+ angle = this.grad;
+ break;
+ case CssAngle.ANGLEUNIT.turn:
+ angle = this.turn;
+ break;
+ default:
+ angle = this.deg;
+ }
+
+ if (this._angleUnitUppercase &&
+ this.angleUnit != CssAngle.ANGLEUNIT.authored) {
+ angle = angle.toUpperCase();
+ }
+ return angle;
+ },
+
+ /**
+ * This method allows comparison of CssAngle objects using ===.
+ */
+ valueOf: function () {
+ return this.deg;
+ },
+};
+
+/**
+ * Given a color, classify its type as one of the possible angle
+ * units, as known by |CssAngle.angleUnit|.
+ *
+ * @param {String} value
+ * The angle, in any form accepted by CSS.
+ * @return {String}
+ * The angle classification, one of "deg", "rad", "grad", or "turn".
+ */
+function classifyAngle(value) {
+ value = value.toLowerCase();
+ if (value.endsWith("deg")) {
+ return CssAngle.ANGLEUNIT.deg;
+ }
+
+ if (value.endsWith("grad")) {
+ return CssAngle.ANGLEUNIT.grad;
+ }
+
+ if (value.endsWith("rad")) {
+ return CssAngle.ANGLEUNIT.rad;
+ }
+ if (value.endsWith("turn")) {
+ return CssAngle.ANGLEUNIT.turn;
+ }
+
+ return CssAngle.ANGLEUNIT.deg;
+}
diff --git a/devtools/client/shared/css-reload.js b/devtools/client/shared/css-reload.js
new file mode 100644
index 000000000..de82c6c5f
--- /dev/null
+++ b/devtools/client/shared/css-reload.js
@@ -0,0 +1,142 @@
+/* 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 { Services } = require("resource://gre/modules/Services.jsm");
+const { getTheme } = require("devtools/client/shared/theme");
+
+function iterStyleNodes(window, func) {
+ for (let node of window.document.childNodes) {
+ // Look for ProcessingInstruction nodes.
+ if (node.nodeType === 7) {
+ func(node);
+ }
+ }
+
+ const links = window.document.getElementsByTagNameNS(
+ "http://www.w3.org/1999/xhtml", "link"
+ );
+ for (let node of links) {
+ func(node);
+ }
+}
+
+function replaceCSS(window, fileURI) {
+ const document = window.document;
+ const randomKey = Math.random();
+ Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+
+ // Scan every CSS tag and reload ones that match the file we are
+ // looking for.
+ iterStyleNodes(window, node => {
+ if (node.nodeType === 7) {
+ // xml-stylesheet declaration
+ if (node.data.includes(fileURI)) {
+ const newNode = window.document.createProcessingInstruction(
+ "xml-stylesheet",
+ `href="${fileURI}?s=${randomKey}" type="text/css"`
+ );
+ document.insertBefore(newNode, node);
+ document.removeChild(node);
+ }
+ } else if (node.href.includes(fileURI)) {
+ const parentNode = node.parentNode;
+ const newNode = window.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "link"
+ );
+ newNode.rel = "stylesheet";
+ newNode.type = "text/css";
+ newNode.href = fileURI + "?s=" + randomKey;
+
+ parentNode.insertBefore(newNode, node);
+ parentNode.removeChild(node);
+ }
+ });
+}
+
+function _replaceResourceInSheet(sheet, filename, randomKey) {
+ for (let i = 0; i < sheet.cssRules.length; i++) {
+ const rule = sheet.cssRules[i];
+ if (rule.type === rule.IMPORT_RULE) {
+ _replaceResourceInSheet(rule.styleSheet, filename);
+ } else if (rule.cssText.includes(filename)) {
+ // Strip off any existing query strings. This might lose
+ // updates for files if there are multiple resources
+ // referenced in the same rule, but the chances of someone hot
+ // reloading multiple resources in the same rule is very low.
+ const text = rule.cssText.replace(/\?s=0.\d+/g, "");
+ const newRule = (
+ text.replace(filename, filename + "?s=" + randomKey)
+ );
+
+ sheet.deleteRule(i);
+ sheet.insertRule(newRule, i);
+ }
+ }
+}
+
+function replaceCSSResource(window, fileURI) {
+ const document = window.document;
+ const randomKey = Math.random();
+
+ // Only match the filename. False positives are much better than
+ // missing updates, as all that would happen is we reload more
+ // resources than we need. We do this because many resources only
+ // use relative paths.
+ const parts = fileURI.split("/");
+ const file = parts[parts.length - 1];
+
+ // Scan every single rule in the entire page for any reference to
+ // this resource, and re-insert the rule to force it to update.
+ for (let sheet of document.styleSheets) {
+ _replaceResourceInSheet(sheet, file, randomKey);
+ }
+
+ for (let node of document.querySelectorAll("img,image")) {
+ if (node.src.startsWith(fileURI)) {
+ node.src = fileURI + "?s=" + randomKey;
+ }
+ }
+}
+
+function watchCSS(window) {
+ if (Services.prefs.getBoolPref("devtools.loader.hotreload")) {
+ const watcher = require("devtools/client/shared/devtools-file-watcher");
+
+ function onFileChanged(_, relativePath) {
+ if (relativePath.match(/\.css$/)) {
+ if (relativePath.startsWith("client/themes")) {
+ let path = relativePath.replace(/^client\/themes\//, "");
+
+ // Special-case a few files that get imported from other CSS
+ // files. We just manually hot reload the parent CSS file.
+ if (path === "variables.css" || path === "toolbars.css" ||
+ path === "common.css" || path === "splitters.css") {
+ replaceCSS(window, "chrome://devtools/skin/" + getTheme() + "-theme.css");
+ } else {
+ replaceCSS(window, "chrome://devtools/skin/" + path);
+ }
+ return;
+ }
+
+ replaceCSS(
+ window,
+ "chrome://devtools/content/" + relativePath.replace(/^client\//, "")
+ );
+ replaceCSS(window, "resource://devtools/" + relativePath);
+ } else if (relativePath.match(/\.(svg|png)$/)) {
+ relativePath = relativePath.replace(/^client\/themes\//, "");
+ replaceCSSResource(window, "chrome://devtools/skin/" + relativePath);
+ }
+ }
+ watcher.on("file-changed", onFileChanged);
+
+ window.addEventListener("unload", () => {
+ watcher.off("file-changed", onFileChanged);
+ });
+ }
+}
+
+module.exports = { watchCSS };
diff --git a/devtools/client/shared/curl.js b/devtools/client/shared/curl.js
new file mode 100644
index 000000000..978cbad9c
--- /dev/null
+++ b/devtools/client/shared/curl.js
@@ -0,0 +1,401 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ * Copyright (C) 2009 Mozilla Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+"use strict";
+
+const Services = require("Services");
+
+const DEFAULT_HTTP_VERSION = "HTTP/1.1";
+
+const Curl = {
+ /**
+ * Generates a cURL command string which can be used from the command line etc.
+ *
+ * @param object data
+ * Datasource to create the command from.
+ * The object must contain the following properties:
+ * - url:string, the URL of the request.
+ * - method:string, the request method upper cased. HEAD / GET / POST etc.
+ * - headers:array, an array of request headers {name:x, value:x} tuples.
+ * - httpVersion:string, http protocol version rfc2616 formatted. Eg. "HTTP/1.1"
+ * - postDataText:string, optional - the request payload.
+ *
+ * @return string
+ * A cURL command.
+ */
+ generateCommand: function (data) {
+ let utils = CurlUtils;
+
+ let command = ["curl"];
+ let ignoredHeaders = new Set();
+
+ // The cURL command is expected to run on the same platform that Firefox runs
+ // (it may be different from the inspected page platform).
+ let escapeString = Services.appinfo.OS == "WINNT" ?
+ utils.escapeStringWin : utils.escapeStringPosix;
+
+ // Add URL.
+ command.push(escapeString(data.url));
+
+ let postDataText = null;
+ let multipartRequest = utils.isMultipartRequest(data);
+
+ // Create post data.
+ let postData = [];
+ if (utils.isUrlEncodedRequest(data) || data.method == "PUT") {
+ postDataText = data.postDataText;
+ postData.push("--data");
+ postData.push(escapeString(utils.writePostDataTextParams(postDataText)));
+ ignoredHeaders.add("Content-Length");
+ } else if (multipartRequest) {
+ postDataText = data.postDataText;
+ postData.push("--data-binary");
+ let boundary = utils.getMultipartBoundary(data);
+ let text = utils.removeBinaryDataFromMultipartText(postDataText, boundary);
+ postData.push(escapeString(text));
+ ignoredHeaders.add("Content-Length");
+ }
+
+ // Add method.
+ // For GET and POST requests this is not necessary as GET is the
+ // default. If --data or --binary is added POST is the default.
+ if (!(data.method == "GET" || data.method == "POST")) {
+ command.push("-X");
+ command.push(data.method);
+ }
+
+ // Add -I (HEAD)
+ // For servers that supports HEAD.
+ // This will fetch the header of a document only.
+ if (data.method == "HEAD") {
+ command.push("-I");
+ }
+
+ // Add http version.
+ if (data.httpVersion && data.httpVersion != DEFAULT_HTTP_VERSION) {
+ command.push("--" + data.httpVersion.split("/")[1]);
+ }
+
+ // Add request headers.
+ let headers = data.headers;
+ if (multipartRequest) {
+ let multipartHeaders = utils.getHeadersFromMultipartText(postDataText);
+ headers = headers.concat(multipartHeaders);
+ }
+ for (let i = 0; i < headers.length; i++) {
+ let header = headers[i];
+ if (header.name === "Accept-Encoding") {
+ command.push("--compressed");
+ continue;
+ }
+ if (ignoredHeaders.has(header.name)) {
+ continue;
+ }
+ command.push("-H");
+ command.push(escapeString(header.name + ": " + header.value));
+ }
+
+ // Add post data.
+ command = command.concat(postData);
+
+ return command.join(" ");
+ }
+};
+
+exports.Curl = Curl;
+
+/**
+ * Utility functions for the Curl command generator.
+ */
+const CurlUtils = {
+ /**
+ * Check if the request is an URL encoded request.
+ *
+ * @param object data
+ * The data source. See the description in the Curl object.
+ * @return boolean
+ * True if the request is URL encoded, false otherwise.
+ */
+ isUrlEncodedRequest: function (data) {
+ let postDataText = data.postDataText;
+ if (!postDataText) {
+ return false;
+ }
+
+ postDataText = postDataText.toLowerCase();
+ if (postDataText.includes("content-type: application/x-www-form-urlencoded")) {
+ return true;
+ }
+
+ let contentType = this.findHeader(data.headers, "content-type");
+
+ return (contentType &&
+ contentType.toLowerCase().includes("application/x-www-form-urlencoded"));
+ },
+
+ /**
+ * Check if the request is a multipart request.
+ *
+ * @param object data
+ * The data source.
+ * @return boolean
+ * True if the request is multipart reqeust, false otherwise.
+ */
+ isMultipartRequest: function (data) {
+ let postDataText = data.postDataText;
+ if (!postDataText) {
+ return false;
+ }
+
+ postDataText = postDataText.toLowerCase();
+ if (postDataText.includes("content-type: multipart/form-data")) {
+ return true;
+ }
+
+ let contentType = this.findHeader(data.headers, "content-type");
+
+ return (contentType &&
+ contentType.toLowerCase().includes("multipart/form-data;"));
+ },
+
+ /**
+ * Write out paramters from post data text.
+ *
+ * @param object postDataText
+ * Post data text.
+ * @return string
+ * Post data parameters.
+ */
+ writePostDataTextParams: function (postDataText) {
+ let lines = postDataText.split("\r\n");
+ return lines[lines.length - 1];
+ },
+
+ /**
+ * Finds the header with the given name in the headers array.
+ *
+ * @param array headers
+ * Array of headers info {name:x, value:x}.
+ * @param string name
+ * The header name to find.
+ * @return string
+ * The found header value or null if not found.
+ */
+ findHeader: function (headers, name) {
+ if (!headers) {
+ return null;
+ }
+
+ name = name.toLowerCase();
+ for (let header of headers) {
+ if (name == header.name.toLowerCase()) {
+ return header.value;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Returns the boundary string for a multipart request.
+ *
+ * @param string data
+ * The data source. See the description in the Curl object.
+ * @return string
+ * The boundary string for the request.
+ */
+ getMultipartBoundary: function (data) {
+ let boundaryRe = /\bboundary=(-{3,}\w+)/i;
+
+ // Get the boundary string from the Content-Type request header.
+ let contentType = this.findHeader(data.headers, "Content-Type");
+ if (boundaryRe.test(contentType)) {
+ return contentType.match(boundaryRe)[1];
+ }
+ // Temporary workaround. As of 2014-03-11 the requestHeaders array does not
+ // always contain the Content-Type header for mulitpart requests. See bug 978144.
+ // Find the header from the request payload.
+ let boundaryString = data.postDataText.match(boundaryRe)[1];
+ if (boundaryString) {
+ return boundaryString;
+ }
+
+ return null;
+ },
+
+ /**
+ * Removes the binary data from multipart text.
+ *
+ * @param string multipartText
+ * Multipart form data text.
+ * @param string boundary
+ * The boundary string.
+ * @return string
+ * The multipart text without the binary data.
+ */
+ removeBinaryDataFromMultipartText: function (multipartText, boundary) {
+ let result = "";
+ boundary = "--" + boundary;
+ let parts = multipartText.split(boundary);
+ for (let part of parts) {
+ // Each part is expected to have a content disposition line.
+ let contentDispositionLine = part.trimLeft().split("\r\n")[0];
+ if (!contentDispositionLine) {
+ continue;
+ }
+ contentDispositionLine = contentDispositionLine.toLowerCase();
+ if (contentDispositionLine.includes("content-disposition: form-data")) {
+ if (contentDispositionLine.includes("filename=")) {
+ // The header lines and the binary blob is separated by 2 CRLF's.
+ // Add only the headers to the result.
+ let headers = part.split("\r\n\r\n")[0];
+ result += boundary + "\r\n" + headers + "\r\n\r\n";
+ } else {
+ result += boundary + "\r\n" + part;
+ }
+ }
+ }
+ result += boundary + "--\r\n";
+
+ return result;
+ },
+
+ /**
+ * Get the headers from a multipart post data text.
+ *
+ * @param string multipartText
+ * Multipart post text.
+ * @return array
+ * An array of header objects {name:x, value:x}
+ */
+ getHeadersFromMultipartText: function (multipartText) {
+ let headers = [];
+ if (!multipartText || multipartText.startsWith("---")) {
+ return headers;
+ }
+
+ // Get the header section.
+ let index = multipartText.indexOf("\r\n\r\n");
+ if (index == -1) {
+ return headers;
+ }
+
+ // Parse the header lines.
+ let headersText = multipartText.substring(0, index);
+ let headerLines = headersText.split("\r\n");
+ let lastHeaderName = null;
+
+ for (let line of headerLines) {
+ // Create a header for each line in fields that spans across multiple lines.
+ // Subsquent lines always begins with at least one space or tab character.
+ // (rfc2616)
+ if (lastHeaderName && /^\s+/.test(line)) {
+ headers.push({ name: lastHeaderName, value: line.trim() });
+ continue;
+ }
+
+ let indexOfColon = line.indexOf(":");
+ if (indexOfColon == -1) {
+ continue;
+ }
+
+ let header = [line.slice(0, indexOfColon), line.slice(indexOfColon + 1)];
+ if (header.length != 2) {
+ continue;
+ }
+ lastHeaderName = header[0].trim();
+ headers.push({ name: lastHeaderName, value: header[1].trim() });
+ }
+
+ return headers;
+ },
+
+ /**
+ * Escape util function for POSIX oriented operating systems.
+ * Credit: Google DevTools
+ */
+ escapeStringPosix: function (str) {
+ function escapeCharacter(x) {
+ let code = x.charCodeAt(0);
+ if (code < 256) {
+ // Add leading zero when needed to not care about the next character.
+ return code < 16 ? "\\x0" + code.toString(16) : "\\x" + code.toString(16);
+ }
+ code = code.toString(16);
+ return "\\u" + ("0000" + code).substr(code.length, 4);
+ }
+
+ if (/[^\x20-\x7E]|\'/.test(str)) {
+ // Use ANSI-C quoting syntax.
+ return "$\'" + str.replace(/\\/g, "\\\\")
+ .replace(/\'/g, "\\\'")
+ .replace(/\n/g, "\\n")
+ .replace(/\r/g, "\\r")
+ .replace(/[^\x20-\x7E]/g, escapeCharacter) + "'";
+ }
+
+ // Use single quote syntax.
+ return "'" + str + "'";
+ },
+
+ /**
+ * Escape util function for Windows systems.
+ * Credit: Google DevTools
+ */
+ escapeStringWin: function (str) {
+ /* Replace quote by double quote (but not by \") because it is
+ recognized by both cmd.exe and MS Crt arguments parser.
+
+ Replace % by "%" because it could be expanded to an environment
+ variable value. So %% becomes "%""%". Even if an env variable ""
+ (2 doublequotes) is declared, the cmd.exe will not
+ substitute it with its value.
+
+ Replace each backslash with double backslash to make sure
+ MS Crt arguments parser won't collapse them.
+
+ Replace new line outside of quotes since cmd.exe doesn't let
+ to do it inside.
+ */
+ return "\"" + str.replace(/"/g, "\"\"")
+ .replace(/%/g, "\"%\"")
+ .replace(/\\/g, "\\\\")
+ .replace(/[\r\n]+/g, "\"^$&\"") + "\"";
+ }
+};
+
+exports.CurlUtils = CurlUtils;
diff --git a/devtools/client/shared/demangle.js b/devtools/client/shared/demangle.js
new file mode 100644
index 000000000..e6792cceb
--- /dev/null
+++ b/devtools/client/shared/demangle.js
@@ -0,0 +1,64 @@
+/**
+ * Exposes a function that demangles function names.
+ * Can be found at: https://github.com/kripken/cxx_demangle
+ */
+var demangle = (function() {
+ // In Firefox CommonJS environment, the module boilerplate thinks it's node,
+ // but `process` does not exist.
+ if (typeof process !== "object" && typeof window !== "object" && typeof require === "function") {
+ // null out `require` since no filesystem is necessary in this module, and this
+ // way the boilerplate thinks its in a shell.
+ require = null;
+ // The `print` function only exists in scope when in a node environment,
+ // and there doesn't seem to account for when in a shell environment and NOT node.js,
+ // so just stub out the print function.
+ var print = function(){}
+ }
+
+var Module = function(Module) {
+ Module = Module || {};
+
+var Module;if(!Module)Module=(typeof Module!=="undefined"?Module:null)||{};var moduleOverrides={};for(var key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}var ENVIRONMENT_IS_WEB=typeof window==="object";var ENVIRONMENT_IS_WORKER=typeof importScripts==="function";var ENVIRONMENT_IS_NODE=typeof process==="object"&&typeof require==="function"&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER;var ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;if(ENVIRONMENT_IS_NODE){if(!Module["print"])Module["print"]=function print(x){process["stdout"].write(x+"\n")};if(!Module["printErr"])Module["printErr"]=function printErr(x){process["stderr"].write(x+"\n")};var nodeFS=require("fs");var nodePath=require("path");Module["read"]=function read(filename,binary){filename=nodePath["normalize"](filename);var ret=nodeFS["readFileSync"](filename);if(!ret&&filename!=nodePath["resolve"](filename)){filename=path.join(__dirname,"..","src",filename);ret=nodeFS["readFileSync"](filename)}if(ret&&!binary)ret=ret.toString();return ret};Module["readBinary"]=function readBinary(filename){var ret=Module["read"](filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}assert(ret.buffer);return ret};Module["load"]=function load(f){globalEval(read(f))};if(!Module["thisProgram"]){if(process["argv"].length>1){Module["thisProgram"]=process["argv"][1].replace(/\\/g,"/")}else{Module["thisProgram"]="unknown-program"}}Module["arguments"]=process["argv"].slice(2);if(typeof module!=="undefined"){module["exports"]=Module}process["on"]("uncaughtException",(function(ex){if(!(ex instanceof ExitStatus)){throw ex}}));Module["inspect"]=(function(){return"[Emscripten Module object]"})}else if(ENVIRONMENT_IS_SHELL){if(!Module["print"])Module["print"]=print;if(typeof printErr!="undefined")Module["printErr"]=printErr;if(typeof read!="undefined"){Module["read"]=read}else{Module["read"]=function read(){throw"no read() available (jsc?)"}}Module["readBinary"]=function readBinary(f){if(typeof readbuffer==="function"){return new Uint8Array(readbuffer(f))}var data=read(f,"binary");assert(typeof data==="object");return data};if(typeof scriptArgs!="undefined"){Module["arguments"]=scriptArgs}else if(typeof arguments!="undefined"){Module["arguments"]=arguments}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){Module["read"]=function read(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(typeof arguments!="undefined"){Module["arguments"]=arguments}if(typeof console!=="undefined"){if(!Module["print"])Module["print"]=function print(x){console.log(x)};if(!Module["printErr"])Module["printErr"]=function printErr(x){console.log(x)}}else{var TRY_USE_DUMP=false;if(!Module["print"])Module["print"]=TRY_USE_DUMP&&typeof dump!=="undefined"?(function(x){dump(x)}):(function(x){})}if(ENVIRONMENT_IS_WORKER){Module["load"]=importScripts}if(typeof Module["setWindowTitle"]==="undefined"){Module["setWindowTitle"]=(function(title){document.title=title})}}else{throw"Unknown runtime environment. Where are we?"}function globalEval(x){eval.call(null,x)}if(!Module["load"]&&Module["read"]){Module["load"]=function load(f){globalEval(Module["read"](f))}}if(!Module["print"]){Module["print"]=(function(){})}if(!Module["printErr"]){Module["printErr"]=Module["print"]}if(!Module["arguments"]){Module["arguments"]=[]}if(!Module["thisProgram"]){Module["thisProgram"]="./this.program"}Module.print=Module["print"];Module.printErr=Module["printErr"];Module["preRun"]=[];Module["postRun"]=[];for(var key in moduleOverrides){if(moduleOverrides.hasOwnProperty(key)){Module[key]=moduleOverrides[key]}}var Runtime={setTempRet0:(function(value){tempRet0=value}),getTempRet0:(function(){return tempRet0}),stackSave:(function(){return STACKTOP}),stackRestore:(function(stackTop){STACKTOP=stackTop}),getNativeTypeSize:(function(type){switch(type){case"i1":case"i8":return 1;case"i16":return 2;case"i32":return 4;case"i64":return 8;case"float":return 4;case"double":return 8;default:{if(type[type.length-1]==="*"){return Runtime.QUANTUM_SIZE}else if(type[0]==="i"){var bits=parseInt(type.substr(1));assert(bits%8===0);return bits/8}else{return 0}}}}),getNativeFieldSize:(function(type){return Math.max(Runtime.getNativeTypeSize(type),Runtime.QUANTUM_SIZE)}),STACK_ALIGN:16,prepVararg:(function(ptr,type){if(type==="double"||type==="i64"){if(ptr&7){assert((ptr&7)===4);ptr+=4}}else{assert((ptr&3)===0)}return ptr}),getAlignSize:(function(type,size,vararg){if(!vararg&&(type=="i64"||type=="double"))return 8;if(!type)return Math.min(size,8);return Math.min(size||(type?Runtime.getNativeFieldSize(type):0),Runtime.QUANTUM_SIZE)}),dynCall:(function(sig,ptr,args){if(args&&args.length){if(!args.splice)args=Array.prototype.slice.call(args);args.splice(0,0,ptr);return Module["dynCall_"+sig].apply(null,args)}else{return Module["dynCall_"+sig].call(null,ptr)}}),functionPointers:[],addFunction:(function(func){for(var i=0;i<Runtime.functionPointers.length;i++){if(!Runtime.functionPointers[i]){Runtime.functionPointers[i]=func;return 2*(1+i)}}throw"Finished up all reserved function pointers. Use a higher value for RESERVED_FUNCTION_POINTERS."}),removeFunction:(function(index){Runtime.functionPointers[(index-2)/2]=null}),warnOnce:(function(text){if(!Runtime.warnOnce.shown)Runtime.warnOnce.shown={};if(!Runtime.warnOnce.shown[text]){Runtime.warnOnce.shown[text]=1;Module.printErr(text)}}),funcWrappers:{},getFuncWrapper:(function(func,sig){assert(sig);if(!Runtime.funcWrappers[sig]){Runtime.funcWrappers[sig]={}}var sigCache=Runtime.funcWrappers[sig];if(!sigCache[func]){sigCache[func]=function dynCall_wrapper(){return Runtime.dynCall(sig,func,arguments)}}return sigCache[func]}),getCompilerSetting:(function(name){throw"You must build with -s RETAIN_COMPILER_SETTINGS=1 for Runtime.getCompilerSetting or emscripten_get_compiler_setting to work"}),stackAlloc:(function(size){var ret=STACKTOP;STACKTOP=STACKTOP+size|0;STACKTOP=STACKTOP+15&-16;return ret}),staticAlloc:(function(size){var ret=STATICTOP;STATICTOP=STATICTOP+size|0;STATICTOP=STATICTOP+15&-16;return ret}),dynamicAlloc:(function(size){var ret=DYNAMICTOP;DYNAMICTOP=DYNAMICTOP+size|0;DYNAMICTOP=DYNAMICTOP+15&-16;if(DYNAMICTOP>=TOTAL_MEMORY){var success=enlargeMemory();if(!success){DYNAMICTOP=ret;return 0}}return ret}),alignMemory:(function(size,quantum){var ret=size=Math.ceil(size/(quantum?quantum:16))*(quantum?quantum:16);return ret}),makeBigInt:(function(low,high,unsigned){var ret=unsigned?+(low>>>0)+ +(high>>>0)*+4294967296:+(low>>>0)+ +(high|0)*+4294967296;return ret}),GLOBAL_BASE:8,QUANTUM_SIZE:4,__dummy__:0};var __THREW__=0;var ABORT=false;var EXITSTATUS=0;var undef=0;var tempValue,tempInt,tempBigInt,tempInt2,tempBigInt2,tempPair,tempBigIntI,tempBigIntR,tempBigIntS,tempBigIntP,tempBigIntD,tempDouble,tempFloat;var tempI64,tempI64b;var tempRet0,tempRet1,tempRet2,tempRet3,tempRet4,tempRet5,tempRet6,tempRet7,tempRet8,tempRet9;function assert(condition,text){if(!condition){abort("Assertion failed: "+text)}}var globalScope=this;function getCFunc(ident){var func=Module["_"+ident];if(!func){try{func=eval("_"+ident)}catch(e){}}assert(func,"Cannot call unknown function "+ident+" (perhaps LLVM optimizations or closure removed it?)");return func}var cwrap,ccall;((function(){var JSfuncs={"stackSave":(function(){Runtime.stackSave()}),"stackRestore":(function(){Runtime.stackRestore()}),"arrayToC":(function(arr){var ret=Runtime.stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}),"stringToC":(function(str){var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=Runtime.stackAlloc((str.length<<2)+1);writeStringToMemory(str,ret)}return ret})};var toC={"string":JSfuncs["stringToC"],"array":JSfuncs["arrayToC"]};ccall=function ccallFunc(ident,returnType,argTypes,args,opts){var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i<args.length;i++){var converter=toC[argTypes[i]];if(converter){if(stack===0)stack=Runtime.stackSave();cArgs[i]=converter(args[i])}else{cArgs[i]=args[i]}}}var ret=func.apply(null,cArgs);if(returnType==="string")ret=Pointer_stringify(ret);if(stack!==0){if(opts&&opts.async){EmterpreterAsync.asyncFinalizers.push((function(){Runtime.stackRestore(stack)}));return}Runtime.stackRestore(stack)}return ret};var sourceRegex=/^function\s*\(([^)]*)\)\s*{\s*([^*]*?)[\s;]*(?:return\s*(.*?)[;\s]*)?}$/;function parseJSFunc(jsfunc){var parsed=jsfunc.toString().match(sourceRegex).slice(1);return{arguments:parsed[0],body:parsed[1],returnValue:parsed[2]}}var JSsource={};for(var fun in JSfuncs){if(JSfuncs.hasOwnProperty(fun)){JSsource[fun]=parseJSFunc(JSfuncs[fun])}}cwrap=function cwrap(ident,returnType,argTypes){argTypes=argTypes||[];var cfunc=getCFunc(ident);var numericArgs=argTypes.every((function(type){return type==="number"}));var numericRet=returnType!=="string";if(numericRet&&numericArgs){return cfunc}var argNames=argTypes.map((function(x,i){return"$"+i}));var funcstr="(function("+argNames.join(",")+") {";var nargs=argTypes.length;if(!numericArgs){funcstr+="var stack = "+JSsource["stackSave"].body+";";for(var i=0;i<nargs;i++){var arg=argNames[i],type=argTypes[i];if(type==="number")continue;var convertCode=JSsource[type+"ToC"];funcstr+="var "+convertCode.arguments+" = "+arg+";";funcstr+=convertCode.body+";";funcstr+=arg+"=("+convertCode.returnValue+");"}}var cfuncname=parseJSFunc((function(){return cfunc})).returnValue;funcstr+="var ret = "+cfuncname+"("+argNames.join(",")+");";if(!numericRet){var strgfy=parseJSFunc((function(){return Pointer_stringify})).returnValue;funcstr+="ret = "+strgfy+"(ret);"}if(!numericArgs){funcstr+=JSsource["stackRestore"].body.replace("()","(stack)")+";"}funcstr+="return ret})";return eval(funcstr)}}))();function setValue(ptr,value,type,noSafe){type=type||"i8";if(type.charAt(type.length-1)==="*")type="i32";switch(type){case"i1":HEAP8[ptr>>0]=value;break;case"i8":HEAP8[ptr>>0]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":tempI64=[value>>>0,(tempDouble=value,+Math_abs(tempDouble)>=+1?tempDouble>+0?(Math_min(+Math_floor(tempDouble/+4294967296),+4294967295)|0)>>>0:~~+Math_ceil((tempDouble- +(~~tempDouble>>>0))/+4294967296)>>>0:0)],HEAP32[ptr>>2]=tempI64[0],HEAP32[ptr+4>>2]=tempI64[1];break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;default:abort("invalid type for setValue: "+type)}}function getValue(ptr,type,noSafe){type=type||"i8";if(type.charAt(type.length-1)==="*")type="i32";switch(type){case"i1":return HEAP8[ptr>>0];case"i8":return HEAP8[ptr>>0];case"i16":return HEAP16[ptr>>1];case"i32":return HEAP32[ptr>>2];case"i64":return HEAP32[ptr>>2];case"float":return HEAPF32[ptr>>2];case"double":return HEAPF64[ptr>>3];default:abort("invalid type for setValue: "+type)}return null}var ALLOC_NORMAL=0;var ALLOC_STACK=1;var ALLOC_STATIC=2;var ALLOC_DYNAMIC=3;var ALLOC_NONE=4;function allocate(slab,types,allocator,ptr){var zeroinit,size;if(typeof slab==="number"){zeroinit=true;size=slab}else{zeroinit=false;size=slab.length}var singleType=typeof types==="string"?types:null;var ret;if(allocator==ALLOC_NONE){ret=ptr}else{ret=[typeof _malloc==="function"?_malloc:null,Runtime.stackAlloc,Runtime.staticAlloc,Runtime.dynamicAlloc][allocator===undefined?ALLOC_STATIC:allocator](Math.max(size,singleType?1:types.length))}if(zeroinit){var ptr=ret,stop;assert((ret&3)==0);stop=ret+(size&~3);for(;ptr<stop;ptr+=4){HEAP32[ptr>>2]=0}stop=ret+size;while(ptr<stop){HEAP8[ptr++>>0]=0}return ret}if(singleType==="i8"){if(slab.subarray||slab.slice){HEAPU8.set(slab,ret)}else{HEAPU8.set(new Uint8Array(slab),ret)}return ret}var i=0,type,typeSize,previousType;while(i<size){var curr=slab[i];if(typeof curr==="function"){curr=Runtime.getFunctionIndex(curr)}type=singleType||types[i];if(type===0){i++;continue}if(type=="i64")type="i32";setValue(ret+i,curr,type);if(previousType!==type){typeSize=Runtime.getNativeTypeSize(type);previousType=type}i+=typeSize}return ret}function getMemory(size){if(!staticSealed)return Runtime.staticAlloc(size);if(typeof _sbrk!=="undefined"&&!_sbrk.called||!runtimeInitialized)return Runtime.dynamicAlloc(size);return _malloc(size)}function Pointer_stringify(ptr,length){if(length===0||!ptr)return"";var hasUtf=0;var t;var i=0;while(1){t=HEAPU8[ptr+i>>0];hasUtf|=t;if(t==0&&!length)break;i++;if(length&&i==length)break}if(!length)length=i;var ret="";if(hasUtf<128){var MAX_CHUNK=1024;var curr;while(length>0){curr=String.fromCharCode.apply(String,HEAPU8.subarray(ptr,ptr+Math.min(length,MAX_CHUNK)));ret=ret?ret+curr:curr;ptr+=MAX_CHUNK;length-=MAX_CHUNK}return ret}return Module["UTF8ToString"](ptr)}Module["Pointer_stringify"]=Pointer_stringify;function AsciiToString(ptr){var str="";while(1){var ch=HEAP8[ptr++>>0];if(!ch)return str;str+=String.fromCharCode(ch)}}function stringToAscii(str,outPtr){return writeAsciiToMemory(str,outPtr,false)}function UTF8ArrayToString(u8Array,idx){var u0,u1,u2,u3,u4,u5;var str="";while(1){u0=u8Array[idx++];if(!u0)return str;if(!(u0&128)){str+=String.fromCharCode(u0);continue}u1=u8Array[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}u2=u8Array[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u3=u8Array[idx++]&63;if((u0&248)==240){u0=(u0&7)<<18|u1<<12|u2<<6|u3}else{u4=u8Array[idx++]&63;if((u0&252)==248){u0=(u0&3)<<24|u1<<18|u2<<12|u3<<6|u4}else{u5=u8Array[idx++]&63;u0=(u0&1)<<30|u1<<24|u2<<18|u3<<12|u4<<6|u5}}}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}}function UTF8ToString(ptr){return UTF8ArrayToString(HEAPU8,ptr)}function stringToUTF8Array(str,outU8Array,outIdx,maxBytesToWrite){if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.charCodeAt(i);if(u>=55296&&u<=57343)u=65536+((u&1023)<<10)|str.charCodeAt(++i)&1023;if(u<=127){if(outIdx>=endIdx)break;outU8Array[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;outU8Array[outIdx++]=192|u>>6;outU8Array[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;outU8Array[outIdx++]=224|u>>12;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}else if(u<=2097151){if(outIdx+3>=endIdx)break;outU8Array[outIdx++]=240|u>>18;outU8Array[outIdx++]=128|u>>12&63;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}else if(u<=67108863){if(outIdx+4>=endIdx)break;outU8Array[outIdx++]=248|u>>24;outU8Array[outIdx++]=128|u>>18&63;outU8Array[outIdx++]=128|u>>12&63;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}else{if(outIdx+5>=endIdx)break;outU8Array[outIdx++]=252|u>>30;outU8Array[outIdx++]=128|u>>24&63;outU8Array[outIdx++]=128|u>>18&63;outU8Array[outIdx++]=128|u>>12&63;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}}outU8Array[outIdx]=0;return outIdx-startIdx}function stringToUTF8(str,outPtr,maxBytesToWrite){return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)}function lengthBytesUTF8(str){var len=0;for(var i=0;i<str.length;++i){var u=str.charCodeAt(i);if(u>=55296&&u<=57343)u=65536+((u&1023)<<10)|str.charCodeAt(++i)&1023;if(u<=127){++len}else if(u<=2047){len+=2}else if(u<=65535){len+=3}else if(u<=2097151){len+=4}else if(u<=67108863){len+=5}else{len+=6}}return len}function UTF16ToString(ptr){var i=0;var str="";while(1){var codeUnit=HEAP16[ptr+i*2>>1];if(codeUnit==0)return str;++i;str+=String.fromCharCode(codeUnit)}}function stringToUTF16(str,outPtr,maxBytesToWrite){if(maxBytesToWrite===undefined){maxBytesToWrite=2147483647}if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite<str.length*2?maxBytesToWrite/2:str.length;for(var i=0;i<numCharsToWrite;++i){var codeUnit=str.charCodeAt(i);HEAP16[outPtr>>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr}function lengthBytesUTF16(str){return str.length*2}function UTF32ToString(ptr){var i=0;var str="";while(1){var utf32=HEAP32[ptr+i*4>>2];if(utf32==0)return str;++i;if(utf32>=65536){var ch=utf32-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}else{str+=String.fromCharCode(utf32)}}}function stringToUTF32(str,outPtr,maxBytesToWrite){if(maxBytesToWrite===undefined){maxBytesToWrite=2147483647}if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i<str.length;++i){var codeUnit=str.charCodeAt(i);if(codeUnit>=55296&&codeUnit<=57343){var trailSurrogate=str.charCodeAt(++i);codeUnit=65536+((codeUnit&1023)<<10)|trailSurrogate&1023}HEAP32[outPtr>>2]=codeUnit;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr}function lengthBytesUTF32(str){var len=0;for(var i=0;i<str.length;++i){var codeUnit=str.charCodeAt(i);if(codeUnit>=55296&&codeUnit<=57343)++i;len+=4}return len}function demangle(func){var hasLibcxxabi=!!Module["___cxa_demangle"];if(hasLibcxxabi){try{var buf=_malloc(func.length);writeStringToMemory(func.substr(1),buf);var status=_malloc(4);var ret=Module["___cxa_demangle"](buf,0,0,status);if(getValue(status,"i32")===0&&ret){return Pointer_stringify(ret)}}catch(e){}finally{if(buf)_free(buf);if(status)_free(status);if(ret)_free(ret)}}var i=3;var basicTypes={"v":"void","b":"bool","c":"char","s":"short","i":"int","l":"long","f":"float","d":"double","w":"wchar_t","a":"signed char","h":"unsigned char","t":"unsigned short","j":"unsigned int","m":"unsigned long","x":"long long","y":"unsigned long long","z":"..."};var subs=[];var first=true;function dump(x){if(x)Module.print(x);Module.print(func);var pre="";for(var a=0;a<i;a++)pre+=" ";Module.print(pre+"^")}function parseNested(){i++;if(func[i]==="K")i++;var parts=[];while(func[i]!=="E"){if(func[i]==="S"){i++;var next=func.indexOf("_",i);var num=func.substring(i,next)||0;parts.push(subs[num]||"?");i=next+1;continue}if(func[i]==="C"){parts.push(parts[parts.length-1]);i+=2;continue}var size=parseInt(func.substr(i));var pre=size.toString().length;if(!size||!pre){i--;break}var curr=func.substr(i+pre,size);parts.push(curr);subs.push(curr);i+=pre+size}i++;return parts}function parse(rawList,limit,allowVoid){limit=limit||Infinity;var ret="",list=[];function flushList(){return"("+list.join(", ")+")"}var name;if(func[i]==="N"){name=parseNested().join("::");limit--;if(limit===0)return rawList?[name]:name}else{if(func[i]==="K"||first&&func[i]==="L")i++;var size=parseInt(func.substr(i));if(size){var pre=size.toString().length;name=func.substr(i+pre,size);i+=pre+size}}first=false;if(func[i]==="I"){i++;var iList=parse(true);var iRet=parse(true,1,true);ret+=iRet[0]+" "+name+"<"+iList.join(", ")+">"}else{ret=name}paramLoop:while(i<func.length&&limit-->0){var c=func[i++];if(c in basicTypes){list.push(basicTypes[c])}else{switch(c){case"P":list.push(parse(true,1,true)[0]+"*");break;case"R":list.push(parse(true,1,true)[0]+"&");break;case"L":{i++;var end=func.indexOf("E",i);var size=end-i;list.push(func.substr(i,size));i+=size+2;break};case"A":{var size=parseInt(func.substr(i));i+=size.toString().length;if(func[i]!=="_")throw"?";i++;list.push(parse(true,1,true)[0]+" ["+size+"]");break};case"E":break paramLoop;default:ret+="?"+c;break paramLoop}}}if(!allowVoid&&list.length===1&&list[0]==="void")list=[];if(rawList){if(ret){list.push(ret+"?")}return list}else{return ret+flushList()}}var parsed=func;try{if(func=="Object._main"||func=="_main"){return"main()"}if(typeof func==="number")func=Pointer_stringify(func);if(func[0]!=="_")return func;if(func[1]!=="_")return func;if(func[2]!=="Z")return func;switch(func[3]){case"n":return"operator new()";case"d":return"operator delete()"}parsed=parse()}catch(e){parsed+="?"}if(parsed.indexOf("?")>=0&&!hasLibcxxabi){Runtime.warnOnce("warning: a problem occurred in builtin C++ name demangling; build with -s DEMANGLE_SUPPORT=1 to link in libcxxabi demangling")}return parsed}function demangleAll(text){return text.replace(/__Z[\w\d_]+/g,(function(x){var y=demangle(x);return x===y?x:x+" ["+y+"]"}))}function jsStackTrace(){var err=new Error;if(!err.stack){try{throw new Error(0)}catch(e){err=e}if(!err.stack){return"(no stack trace available)"}}return err.stack.toString()}function stackTrace(){return demangleAll(jsStackTrace())}var PAGE_SIZE=4096;function alignMemoryPage(x){if(x%4096>0){x+=4096-x%4096}return x}var HEAP;var buffer;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBuffer(buf){Module["buffer"]=buffer=buf}function updateGlobalBufferViews(){Module["HEAP8"]=HEAP8=new Int8Array(buffer);Module["HEAP16"]=HEAP16=new Int16Array(buffer);Module["HEAP32"]=HEAP32=new Int32Array(buffer);Module["HEAPU8"]=HEAPU8=new Uint8Array(buffer);Module["HEAPU16"]=HEAPU16=new Uint16Array(buffer);Module["HEAPU32"]=HEAPU32=new Uint32Array(buffer);Module["HEAPF32"]=HEAPF32=new Float32Array(buffer);Module["HEAPF64"]=HEAPF64=new Float64Array(buffer)}var STATIC_BASE=0,STATICTOP=0,staticSealed=false;var STACK_BASE=0,STACKTOP=0,STACK_MAX=0;var DYNAMIC_BASE=0,DYNAMICTOP=0;function abortOnCannotGrowMemory(){abort("Cannot enlarge memory arrays. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value "+TOTAL_MEMORY+", (2) compile with -s ALLOW_MEMORY_GROWTH=1 which adjusts the size at runtime but prevents some optimizations, (3) set Module.TOTAL_MEMORY to a higher value before the program runs, or if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ")}function enlargeMemory(){abortOnCannotGrowMemory()}var TOTAL_STACK=Module["TOTAL_STACK"]||65536;var TOTAL_MEMORY=Module["TOTAL_MEMORY"]||1048576;var totalMemory=64*1024;while(totalMemory<TOTAL_MEMORY||totalMemory<2*TOTAL_STACK){if(totalMemory<16*1024*1024){totalMemory*=2}else{totalMemory+=16*1024*1024}}if(totalMemory!==TOTAL_MEMORY){TOTAL_MEMORY=totalMemory}assert(typeof Int32Array!=="undefined"&&typeof Float64Array!=="undefined"&&!!(new Int32Array(1))["subarray"]&&!!(new Int32Array(1))["set"],"JS engine does not provide full typed array support");if(Module["buffer"]){buffer=Module["buffer"];assert(buffer.byteLength===TOTAL_MEMORY,"provided buffer should be "+TOTAL_MEMORY+" bytes, but it is "+buffer.byteLength)}else{buffer=new ArrayBuffer(TOTAL_MEMORY)}updateGlobalBufferViews();HEAP32[0]=255;assert(HEAPU8[0]===255&&HEAPU8[3]===0,"Typed arrays 2 must be run on a little-endian system");Module["HEAP"]=HEAP;Module["buffer"]=buffer;Module["HEAP8"]=HEAP8;Module["HEAP16"]=HEAP16;Module["HEAP32"]=HEAP32;Module["HEAPU8"]=HEAPU8;Module["HEAPU16"]=HEAPU16;Module["HEAPU32"]=HEAPU32;Module["HEAPF32"]=HEAPF32;Module["HEAPF64"]=HEAPF64;function callRuntimeCallbacks(callbacks){while(callbacks.length>0){var callback=callbacks.shift();if(typeof callback=="function"){callback();continue}var func=callback.func;if(typeof func==="number"){if(callback.arg===undefined){Runtime.dynCall("v",func)}else{Runtime.dynCall("vi",func,[callback.arg])}}else{func(callback.arg===undefined?null:callback.arg)}}}var __ATPRERUN__=[];var __ATINIT__=[];var __ATMAIN__=[];var __ATEXIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeExited=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function ensureInitRuntime(){if(runtimeInitialized)return;runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function exitRuntime(){callRuntimeCallbacks(__ATEXIT__);runtimeExited=true}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPreMain(cb){__ATMAIN__.unshift(cb)}function addOnExit(cb){__ATEXIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}function intArrayToString(array){var ret=[];for(var i=0;i<array.length;i++){var chr=array[i];if(chr>255){chr&=255}ret.push(String.fromCharCode(chr))}return ret.join("")}function writeStringToMemory(string,buffer,dontAddNull){var array=intArrayFromString(string,dontAddNull);var i=0;while(i<array.length){var chr=array[i];HEAP8[buffer+i>>0]=chr;i=i+1}}Module["writeStringToMemory"]=writeStringToMemory;function writeArrayToMemory(array,buffer){for(var i=0;i<array.length;i++){HEAP8[buffer++>>0]=array[i]}}function writeAsciiToMemory(str,buffer,dontAddNull){for(var i=0;i<str.length;++i){HEAP8[buffer++>>0]=str.charCodeAt(i)}if(!dontAddNull)HEAP8[buffer>>0]=0}function unSign(value,bits,ignore){if(value>=0){return value}return bits<=32?2*Math.abs(1<<bits-1)+value:Math.pow(2,bits)+value}function reSign(value,bits,ignore){if(value<=0){return value}var half=bits<=32?Math.abs(1<<bits-1):Math.pow(2,bits-1);if(value>=half&&(bits<=32||value>half)){value=-2*half+value}return value}if(!Math["imul"]||Math["imul"](4294967295,5)!==-5)Math["imul"]=function imul(a,b){var ah=a>>>16;var al=a&65535;var bh=b>>>16;var bl=b&65535;return al*bl+(ah*bl+al*bh<<16)|0};Math.imul=Math["imul"];if(!Math["clz32"])Math["clz32"]=(function(x){x=x>>>0;for(var i=0;i<32;i++){if(x&1<<31-i)return i}return 32});Math.clz32=Math["clz32"];var Math_abs=Math.abs;var Math_cos=Math.cos;var Math_sin=Math.sin;var Math_tan=Math.tan;var Math_acos=Math.acos;var Math_asin=Math.asin;var Math_atan=Math.atan;var Math_atan2=Math.atan2;var Math_exp=Math.exp;var Math_log=Math.log;var Math_sqrt=Math.sqrt;var Math_ceil=Math.ceil;var Math_floor=Math.floor;var Math_pow=Math.pow;var Math_imul=Math.imul;var Math_fround=Math.fround;var Math_min=Math.min;var Math_clz32=Math.clz32;var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}Module["preloadedImages"]={};Module["preloadedAudios"]={};var memoryInitializer=null;var ASM_CONSTS=[];STATIC_BASE=8;STATICTOP=STATIC_BASE+5360;__ATINIT__.push();allocate([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,33,34,118,101,99,116,111,114,32,108,101,110,103,116,104,95,101,114,114,111,114,34,0,47,109,101,100,105,97,47,97,108,111,110,47,100,54,57,100,100,57,98,50,45,52,55,57,49,45,52,98,56,101,45,97,101,98,51,45,102,54,51,53,51,98,52,53,100,55,49,48,47,104,111,109,101,47,97,108,111,110,47,68,101,118,47,101,109,115,99,114,105,112,116,101,110,47,115,121,115,116,101,109,47,105,110,99,108,117,100,101,47,108,105,98,99,120,120,47,118,101,99,116,111,114,0,95,95,116,104,114,111,119,95,108,101,110,103,116,104,95,101,114,114,111,114,0,32,99,111,110,115,116,0,33,34,98,97,115,105,99,95,115,116,114,105,110,103,32,111,117,116,95,111,102,95,114,97,110,103,101,34,0,47,109,101,100,105,97,47,97,108,111,110,47,100,54,57,100,100,57,98,50,45,52,55,57,49,45,52,98,56,101,45,97,101,98,51,45,102,54,51,53,51,98,52,53,100,55,49,48,47,104,111,109,101,47,97,108,111,110,47,68,101,118,47,101,109,115,99,114,105,112,116,101,110,47,115,121,115,116,101,109,47,105,110,99,108,117,100,101,47,108,105,98,99,120,120,47,115,116,114,105,110,103,0,95,95,116,104,114,111,119,95,111,117,116,95,111,102,95,114,97,110,103,101,0,33,34,98,97,115,105,99,95,115,116,114,105,110,103,32,108,101,110,103,116,104,95,101,114,114,111,114,34,0,32,118,111,108,97,116,105,108,101,0,32,114,101,115,116,114,105,99,116,0,118,111,105,100,0,119,99,104,97,114,95,116,0,98,111,111,108,0,99,104,97,114,0,115,105,103,110,101,100,32,99,104,97,114,0,117,110,115,105,103,110,101,100,32,99,104,97,114,0,115,104,111,114,116,0,117,110,115,105,103,110,101,100,32,115,104,111,114,116,0,105,110,116,0,117,110,115,105,103,110,101,100,32,105,110,116,0,108,111,110,103,0,117,110,115,105,103,110,101,100,32,108,111,110,103,0,108,111,110,103,32,108,111,110,103,0,117,110,115,105,103,110,101,100,32,108,111,110,103,32,108,111,110,103,0,95,95,105,110,116,49,50,56,0,117,110,115,105,103,110,101,100,32,95,95,105,110,116,49,50,56,0,102,108,111,97,116,0,100,111,117,98,108,101,0,108,111,110,103,32,100,111,117,98,108,101,0,95,95,102,108,111,97,116,49,50,56,0,46,46,46,0,95,71,76,79,66,65,76,95,95,78,0,40,97,110,111,110,121,109,111,117,115,32,110,97,109,101,115,112,97,99,101,41,0,100,101,99,105,109,97,108,54,52,0,100,101,99,105,109,97,108,49,50,56,0,100,101,99,105,109,97,108,51,50,0,100,101,99,105,109,97,108,49,54,0,99,104,97,114,51,50,95,116,0,99,104,97,114,49,54,95,116,0,97,117,116,111,0,115,116,100,58,58,110,117,108,108,112,116,114,95,116,0,32,91,0,32,91,93,0,40,0,41,0,102,97,108,115,101,0,116,114,117,101,0,117,0,108,0,117,108,0,108,108,0,117,108,108,0,37,97,102,0,37,97,0,37,76,97,76,0,102,112,0,38,38,0,62,0,41,32,0,32,40,0,38,0,38,61,0,61,0,97,108,105,103,110,111,102,32,40,0,99,111,110,115,116,95,99,97,115,116,60,0,62,40,0,44,0,126,0,41,40,0,58,58,0,100,101,108,101,116,101,91,93,32,0,100,121,110,97,109,105,99,95,99,97,115,116,60,0,100,101,108,101,116,101,32,0,111,112,101,114,97,116,111,114,38,38,0,111,112,101,114,97,116,111,114,38,0,111,112,101,114,97,116,111,114,38,61,0,111,112,101,114,97,116,111,114,61,0,111,112,101,114,97,116,111,114,40,41,0,111,112,101,114,97,116,111,114,44,0,111,112,101,114,97,116,111,114,126,0,111,112,101,114,97,116,111,114,32,0,111,112,101,114,97,116,111,114,32,100,101,108,101,116,101,91,93,0,111,112,101,114,97,116,111,114,42,0,111,112,101,114,97,116,111,114,32,100,101,108,101,116,101,0,111,112,101,114,97,116,111,114,47,0,111,112,101,114,97,116,111,114,47,61,0,111,112,101,114,97,116,111,114,94,0,111,112,101,114,97,116,111,114,94,61,0,111,112,101,114,97,116,111,114,61,61,0,111,112,101,114,97,116,111,114,62,61,0,111,112,101,114,97,116,111,114,62,0,111,112,101,114,97,116,111,114,91,93,0,111,112,101,114,97,116,111,114,60,61,0,111,112,101,114,97,116,111,114,34,34,32,0,111,112,101,114,97,116,111,114,60,60,0,111,112,101,114,97,116,111,114,60,60,61,0,111,112,101,114,97,116,111,114,60,0,111,112,101,114,97,116,111,114,45,0,111,112,101,114,97,116,111,114,45,61,0,111,112,101,114,97,116,111,114,42,61,0,111,112,101,114,97,116,111,114,45,45,0,111,112,101,114,97,116,111,114,32,110,101,119,91,93,0,111,112,101,114,97,116,111,114,33,61,0,111,112,101,114,97,116,111,114,33,0,111,112,101,114,97,116,111,114,32,110,101,119,0,111,112,101,114,97,116,111,114,124,124,0,111,112,101,114,97,116,111,114,124,0,111,112,101,114,97,116,111,114,124,61,0,111,112,101,114,97,116,111,114,45,62,42,0,111,112,101,114,97,116,111,114,43,0,111,112,101,114,97,116,111,114,43,61,0,111,112,101,114,97,116,111,114,43,43,0,111,112,101,114,97,116,111,114,45,62,0,111,112,101,114,97,116,111,114,63,0,111,112,101,114,97,116,111,114,37,0,111,112,101,114,97,116,111,114,37,61,0,111,112,101,114,97,116,111,114,62,62,0,111,112,101,114,97,116,111,114,62,62,61,0,60,0,44,32,0,32,62,0,100,101,99,108,116,121,112,101,40,0,115,116,100,58,58,97,108,108,111,99,97,116,111,114,0,115,116,100,58,58,98,97,115,105,99,95,115,116,114,105,110,103,0,115,116,100,58,58,115,116,114,105,110,103,0,115,116,100,58,58,105,115,116,114,101,97,109,0,115,116,100,58,58,111,115,116,114,101,97,109,0,115,116,100,58,58,105,111,115,116,114,101,97,109,0,115,116,100,58,58,98,97,115,105,99,95,115,116,114,105,110,103,60,99,104,97,114,44,32,115,116,100,58,58,99,104,97,114,95,116,114,97,105,116,115,60,99,104,97,114,62,44,32,115,116,100,58,58,97,108,108,111,99,97,116,111,114,60,99,104,97,114,62,32,62,0,98,97,115,105,99,95,115,116,114,105,110,103,0,115,116,100,58,58,98,97,115,105,99,95,105,115,116,114,101,97,109,60,99,104,97,114,44,32,115,116,100,58,58,99,104,97,114,95,116,114,97,105,116,115,60,99,104,97,114,62,32,62,0,98,97,115,105,99,95,105,115,116,114,101,97,109,0,115,116,100,58,58,98,97,115,105,99,95,111,115,116,114,101,97,109,60,99,104,97,114,44,32,115,116,100,58,58,99,104,97,114,95,116,114,97,105,116,115,60,99,104,97,114,62,32,62,0,98,97,115,105,99,95,111,115,116,114,101,97,109,0,115,116,100,58,58,98,97,115,105,99,95,105,111,115,116,114,101,97,109,60,99,104,97,114,44,32,115,116,100,58,58,99,104,97,114,95,116,114,97,105,116,115,60,99,104,97,114,62,32,62,0,98,97,115,105,99,95,105,111,115,116,114,101,97,109,0,39,117,110,110,97,109,101,100,0,39,108,97,109,98,100,97,39,40,0,115,116,100,58,58,0,46,42,0,47,61,0,94,0,94,61,0,61,61,0,62,61,0,41,91,0,60,61,0,60,60,0,60,60,61,0,45,0,45,61,0,42,61,0,45,45,0,41,45,45,0,91,93,32,0,32,0,33,61,0,33,0,110,111,101,120,99,101,112,116,32,40,0,124,124,0,124,0,124,61,0,45,62,42,0,43,0,43,61,0,43,43,0,41,43,43,0,45,62,0,41,32,63,32,40,0,41,32,58,32,40,0,114,101,105,110,116,101,114,112,114,101,116,95,99,97,115,116,60,0,37,0,37,61,0,62,62,0,62,62,61,0,115,116,97,116,105,99,95,99,97,115,116,60,0,115,105,122,101,111,102,32,40,0,115,105,122,101,111,102,46,46,46,40,0,116,121,112,101,105,100,40,0,116,104,114,111,119,0,116,104,114,111,119,32,0,32,99,111,109,112,108,101,120,0,32,38,0,32,38,38,0,32,105,109,97,103,105,110,97,114,121,0,58,58,42,0,111,98,106,99,95,111,98,106,101,99,116,60,0,105,100,0,111,98,106,99,112,114,111,116,111,0,115,116,100,0,58,58,115,116,114,105,110,103,32,108,105,116,101,114,97,108,0,32,118,101,99,116,111,114,91,0,112,105,120,101,108,32,118,101,99,116,111,114,91,0,118,116,97,98,108,101,32,102,111,114,32,0,86,84,84,32,102,111,114,32,0,116,121,112,101,105,110,102,111,32,102,111,114,32,0,116,121,112,101,105,110,102,111,32,110,97,109,101,32,102,111,114,32,0,99,111,118,97,114,105,97,110,116,32,114,101,116,117,114,110,32,116,104,117,110,107,32,116,111,32,0,99,111,110,115,116,114,117,99,116,105,111,110,32,118,116,97,98,108,101,32,102,111,114,32,0,45,105,110,45,0,118,105,114,116,117,97,108,32,116,104,117,110,107,32,116,111,32,0,110,111,110,45,118,105,114,116,117,97,108,32,116,104,117,110,107,32,116,111,32,0,103,117,97,114,100,32,118,97,114,105,97,98,108,101,32,102,111,114,32,0,114,101,102,101,114,101,110,99,101,32,116,101,109,112,111,114,97,114,121,32,102,111,114,32,0,95,98,108,111,99,107,95,105,110,118,111,107,101,0,105,110,118,111,99,97,116,105,111,110,32,102,117,110,99,116,105,111,110,32,102,111,114,32,98,108,111,99,107,32,105,110,32,0,47,0,84,33,34,25,13,1,2,3,17,75,28,12,16,4,11,29,18,30,39,104,110,111,112,113,98,32,5,6,15,19,20,21,26,8,22,7,40,36,23,24,9,10,14,27,31,37,35,131,130,125,38,42,43,60,61,62,63,67,71,74,77,88,89,90,91,92,93,94,95,96,97,99,100,101,102,103,105,106,107,108,114,115,116,121,122,123,124,0,73,108,108,101,103,97,108,32,98,121,116,101,32,115,101,113,117,101,110,99,101,0,68,111,109,97,105,110,32,101,114,114,111,114,0,82,101,115,117,108,116,32,110,111,116,32,114,101,112,114,101,115,101,110,116,97,98,108,101,0,78,111,116,32,97,32,116,116,121,0,80,101,114,109,105,115,115,105,111,110,32,100,101,110,105,101,100,0,79,112,101,114,97,116,105,111,110,32,110,111,116,32,112,101,114,109,105,116,116,101,100,0,78,111,32,115,117,99,104,32,102,105,108,101,32,111,114,32,100,105,114,101,99,116,111,114,121,0,78,111,32,115,117,99,104,32,112,114,111,99,101,115,115,0,70,105,108,101,32,101,120,105,115,116,115,0,86,97,108,117,101,32,116,111,111,32,108,97,114,103,101,32,102,111,114,32,100,97,116,97,32,116,121,112,101,0,78,111,32,115,112,97,99,101,32,108,101,102,116,32,111,110,32,100,101,118,105,99,101,0,79,117,116,32,111,102,32,109,101,109,111,114,121,0,82,101,115,111,117,114,99,101,32,98,117,115,121,0,73,110,116,101,114,114,117,112,116,101,100,32,115,121,115,116,101,109,32,99,97,108,108,0,82,101,115,111,117,114,99,101,32,116,101,109,112,111,114,97,114,105,108,121,32,117,110,97,118,97,105,108,97,98,108,101,0,73,110,118,97,108,105,100,32,115,101,101,107,0,67,114,111,115,115,45,100,101,118,105,99,101,32,108,105,110,107,0,82,101,97,100,45,111,110,108,121,32,102,105,108,101,32,115,121,115,116,101,109,0,68,105,114,101,99,116,111,114,121,32,110,111,116,32,101,109,112,116,121,0,67,111,110,110,101,99,116,105,111,110,32,114,101,115,101,116,32,98,121,32,112,101,101,114,0,79,112,101,114,97,116,105,111,110,32,116,105,109,101,100,32,111,117,116,0,67,111,110,110,101,99,116,105,111,110,32,114,101,102,117,115,101,100,0,72,111,115,116,32,105,115,32,100,111,119,110,0,72,111,115,116,32,105,115,32,117,110,114,101,97,99,104,97,98,108,101,0,65,100,100,114,101,115,115,32,105,110,32,117,115,101,0,66,114,111,107,101,110,32,112,105,112,101,0,73,47,79,32,101,114,114,111,114,0,78,111,32,115,117,99,104,32,100,101,118,105,99,101,32,111,114,32,97,100,100,114,101,115,115,0,66,108,111,99,107,32,100,101,118,105,99,101,32,114,101,113,117,105,114,101,100,0,78,111,32,115,117,99,104,32,100,101,118,105,99,101,0,78,111,116,32,97,32,100,105,114,101,99,116,111,114,121,0,73,115,32,97,32,100,105,114,101,99,116,111,114,121,0,84,101,120,116,32,102,105,108,101,32,98,117,115,121,0,69,120,101,99,32,102,111,114,109,97,116,32,101,114,114,111,114,0,73,110,118,97,108,105,100,32,97,114,103,117,109,101,110,116,0,65,114,103,117,109,101,110,116,32,108,105,115,116,32,116,111,111,32,108,111,110,103,0,83,121,109,98,111,108,105,99,32,108,105,110,107,32,108,111,111,112,0,70,105,108,101,110,97,109,101,32,116,111,111,32,108,111,110,103,0,84,111,111,32,109,97,110,121,32,111,112,101,110,32,102,105,108,101,115,32,105,110,32,115,121,115,116,101,109,0,78,111,32,102,105,108,101,32,100,101,115,99,114,105,112,116,111,114,115,32,97,118,97,105,108,97,98,108,101,0,66,97,100,32,102,105,108,101,32,100,101,115,99,114,105,112,116,111,114,0,78,111,32,99,104,105,108,100,32,112,114,111,99,101,115,115,0,66,97,100,32,97,100,100,114,101,115,115,0,70,105,108,101,32,116,111,111,32,108,97,114,103,101,0,84,111,111,32,109,97,110,121,32,108,105,110,107,115,0,78,111,32,108,111,99,107,115,32,97,118,97,105,108,97,98,108,101,0,82,101,115,111,117,114,99,101,32,100,101,97,100,108,111,99,107,32,119,111,117,108,100,32,111,99,99,117,114,0,83,116,97,116,101,32,110,111,116,32,114,101,99,111,118,101,114,97,98,108,101,0,80,114,101,118,105,111,117,115,32,111,119,110,101,114,32,100,105,101,100,0,79,112,101,114,97,116,105,111,110,32,99,97,110,99,101,108,101,100,0,70,117,110,99,116,105,111,110,32,110,111,116,32,105,109,112,108,101,109,101,110,116,101,100,0,78,111,32,109,101,115,115,97,103,101,32,111,102,32,100,101,115,105,114,101,100,32,116,121,112,101,0,73,100,101,110,116,105,102,105,101,114,32,114,101,109,111,118,101,100,0,68,101,118,105,99,101,32,110,111,116,32,97,32,115,116,114,101,97,109,0,78,111,32,100,97,116,97,32,97,118,97,105,108,97,98,108,101,0,68,101,118,105,99,101,32,116,105,109,101,111,117,116,0,79,117,116,32,111,102,32,115,116,114,101,97,109,115,32,114,101,115,111,117,114,99,101,115,0,76,105,110,107,32,104,97,115,32,98,101,101,110,32,115,101,118,101,114,101,100,0,80,114,111,116,111,99,111,108,32,101,114,114,111,114,0,66,97,100,32,109,101,115,115,97,103,101,0,70,105,108,101,32,100,101,115,99,114,105,112,116,111,114,32,105,110,32,98,97,100,32,115,116,97,116,101,0,78,111,116,32,97,32,115,111,99,107,101,116,0,68,101,115,116,105,110,97,116,105,111,110,32,97,100,100,114,101,115,115,32,114,101,113,117,105,114,101,100,0,77,101,115,115,97,103,101,32,116,111,111,32,108,97,114,103,101,0,80,114,111,116,111,99,111,108,32,119,114,111,110,103,32,116,121,112,101,32,102,111,114,32,115,111,99,107,101,116,0,80,114,111,116,111,99,111,108,32,110,111,116,32,97,118,97,105,108,97,98,108,101,0,80,114,111,116,111,99,111,108,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,83,111,99,107,101,116,32,116,121,112,101,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,78,111,116,32,115,117,112,112,111,114,116,101,100,0,80,114,111,116,111,99,111,108,32,102,97,109,105,108,121,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,65,100,100,114,101,115,115,32,102,97,109,105,108,121,32,110,111,116,32,115,117,112,112,111,114,116,101,100,32,98,121,32,112,114,111,116,111,99,111,108,0,65,100,100,114,101,115,115,32,110,111,116,32,97,118,97,105,108,97,98,108,101,0,78,101,116,119,111,114,107,32,105,115,32,100,111,119,110,0,78,101,116,119,111,114,107,32,117,110,114,101,97,99,104,97,98,108,101,0,67,111,110,110,101,99,116,105,111,110,32,114,101,115,101,116,32,98,121,32,110,101,116,119,111,114,107,0,67,111,110,110,101,99,116,105,111,110,32,97,98,111,114,116,101,100,0,78,111,32,98,117,102,102,101,114,32,115,112,97,99,101,32,97,118,97,105,108,97,98,108,101,0,83,111,99,107,101,116,32,105,115,32,99,111,110,110,101,99,116,101,100,0,83,111,99,107,101,116,32,110,111,116,32,99,111,110,110,101,99,116,101,100,0,67,97,110,110,111,116,32,115,101,110,100,32,97,102,116,101,114,32,115,111,99,107,101,116,32,115,104,117,116,100,111,119,110,0,79,112,101,114,97,116,105,111,110,32,97,108,114,101,97,100,121,32,105,110,32,112,114,111,103,114,101,115,115,0,79,112,101,114,97,116,105,111,110,32,105,110,32,112,114,111,103,114,101,115,115,0,83,116,97,108,101,32,102,105,108,101,32,104,97,110,100,108,101,0,82,101,109,111,116,101,32,73,47,79,32,101,114,114,111,114,0,81,117,111,116,97,32,101,120,99,101,101,100,101,100,0,78,111,32,109,101,100,105,117,109,32,102,111,117,110,100,0,87,114,111,110,103,32,109,101,100,105,117,109,32,116,121,112,101,0,78,111,32,101,114,114,111,114,32,105,110,102,111,114,109,97,116,105,111,110,0,0,42,0,93,0,17,0,10,0,17,17,17,0,0,0,0,5,0,0,0,0,0,0,9,0,0,0,0,11,0,0,0,0,0,0,0,0,17,0,15,10,17,17,17,3,10,7,0,1,19,9,11,11,0,0,9,6,11,0,0,11,0,6,17,0,0,0,17,17,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,17,0,10,10,17,17,17,0,10,0,0,2,0,9,11,0,0,0,9,0,11,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,12,0,0,0,0,9,12,0,0,0,0,0,12,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,4,13,0,0,0,0,9,14,0,0,0,0,0,14,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,15,0,0,0,0,15,0,0,0,0,9,16,0,0,0,0,0,16,0,0,16,0,0,18,0,0,0,18,18,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,0,18,18,18,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,10,0,0,0,0,9,11,0,0,0,0,0,11,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,12,0,0,0,0,9,12,0,0,0,0,0,12,0,0,12,0,0,48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70,45,43,32,32,32,48,88,48,120,0,40,110,117,108,108,41,0,45,48,88,43,48,88,32,48,88,45,48,120,43,48,120,32,48,120,0,105,110,102,0,73,78,70,0,110,97,110,0,78,65,78,0,46,0],"i8",ALLOC_NONE,Runtime.GLOBAL_BASE);var tempDoublePtr=Runtime.alignMemory(allocate(12,"i8",ALLOC_STATIC),8);assert(tempDoublePtr%8==0);function copyTempFloat(ptr){HEAP8[tempDoublePtr]=HEAP8[ptr];HEAP8[tempDoublePtr+1]=HEAP8[ptr+1];HEAP8[tempDoublePtr+2]=HEAP8[ptr+2];HEAP8[tempDoublePtr+3]=HEAP8[ptr+3]}function copyTempDouble(ptr){HEAP8[tempDoublePtr]=HEAP8[ptr];HEAP8[tempDoublePtr+1]=HEAP8[ptr+1];HEAP8[tempDoublePtr+2]=HEAP8[ptr+2];HEAP8[tempDoublePtr+3]=HEAP8[ptr+3];HEAP8[tempDoublePtr+4]=HEAP8[ptr+4];HEAP8[tempDoublePtr+5]=HEAP8[ptr+5];HEAP8[tempDoublePtr+6]=HEAP8[ptr+6];HEAP8[tempDoublePtr+7]=HEAP8[ptr+7]}Module["_memset"]=_memset;Module["_i64Subtract"]=_i64Subtract;function ___setErrNo(value){if(Module["___errno_location"])HEAP32[Module["___errno_location"]()>>2]=value;return value}var ERRNO_CODES={EPERM:1,ENOENT:2,ESRCH:3,EINTR:4,EIO:5,ENXIO:6,E2BIG:7,ENOEXEC:8,EBADF:9,ECHILD:10,EAGAIN:11,EWOULDBLOCK:11,ENOMEM:12,EACCES:13,EFAULT:14,ENOTBLK:15,EBUSY:16,EEXIST:17,EXDEV:18,ENODEV:19,ENOTDIR:20,EISDIR:21,EINVAL:22,ENFILE:23,EMFILE:24,ENOTTY:25,ETXTBSY:26,EFBIG:27,ENOSPC:28,ESPIPE:29,EROFS:30,EMLINK:31,EPIPE:32,EDOM:33,ERANGE:34,ENOMSG:42,EIDRM:43,ECHRNG:44,EL2NSYNC:45,EL3HLT:46,EL3RST:47,ELNRNG:48,EUNATCH:49,ENOCSI:50,EL2HLT:51,EDEADLK:35,ENOLCK:37,EBADE:52,EBADR:53,EXFULL:54,ENOANO:55,EBADRQC:56,EBADSLT:57,EDEADLOCK:35,EBFONT:59,ENOSTR:60,ENODATA:61,ETIME:62,ENOSR:63,ENONET:64,ENOPKG:65,EREMOTE:66,ENOLINK:67,EADV:68,ESRMNT:69,ECOMM:70,EPROTO:71,EMULTIHOP:72,EDOTDOT:73,EBADMSG:74,ENOTUNIQ:76,EBADFD:77,EREMCHG:78,ELIBACC:79,ELIBBAD:80,ELIBSCN:81,ELIBMAX:82,ELIBEXEC:83,ENOSYS:38,ENOTEMPTY:39,ENAMETOOLONG:36,ELOOP:40,EOPNOTSUPP:95,EPFNOSUPPORT:96,ECONNRESET:104,ENOBUFS:105,EAFNOSUPPORT:97,EPROTOTYPE:91,ENOTSOCK:88,ENOPROTOOPT:92,ESHUTDOWN:108,ECONNREFUSED:111,EADDRINUSE:98,ECONNABORTED:103,ENETUNREACH:101,ENETDOWN:100,ETIMEDOUT:110,EHOSTDOWN:112,EHOSTUNREACH:113,EINPROGRESS:115,EALREADY:114,EDESTADDRREQ:89,EMSGSIZE:90,EPROTONOSUPPORT:93,ESOCKTNOSUPPORT:94,EADDRNOTAVAIL:99,ENETRESET:102,EISCONN:106,ENOTCONN:107,ETOOMANYREFS:109,EUSERS:87,EDQUOT:122,ESTALE:116,ENOTSUP:95,ENOMEDIUM:123,EILSEQ:84,EOVERFLOW:75,ECANCELED:125,ENOTRECOVERABLE:131,EOWNERDEAD:130,ESTRPIPE:86};function _sysconf(name){switch(name){case 30:return PAGE_SIZE;case 85:return totalMemory/PAGE_SIZE;case 132:case 133:case 12:case 137:case 138:case 15:case 235:case 16:case 17:case 18:case 19:case 20:case 149:case 13:case 10:case 236:case 153:case 9:case 21:case 22:case 159:case 154:case 14:case 77:case 78:case 139:case 80:case 81:case 82:case 68:case 67:case 164:case 11:case 29:case 47:case 48:case 95:case 52:case 51:case 46:return 200809;case 79:return 0;case 27:case 246:case 127:case 128:case 23:case 24:case 160:case 161:case 181:case 182:case 242:case 183:case 184:case 243:case 244:case 245:case 165:case 178:case 179:case 49:case 50:case 168:case 169:case 175:case 170:case 171:case 172:case 97:case 76:case 32:case 173:case 35:return-1;case 176:case 177:case 7:case 155:case 8:case 157:case 125:case 126:case 92:case 93:case 129:case 130:case 131:case 94:case 91:return 1;case 74:case 60:case 69:case 70:case 4:return 1024;case 31:case 42:case 72:return 32;case 87:case 26:case 33:return 2147483647;case 34:case 1:return 47839;case 38:case 36:return 99;case 43:case 37:return 2048;case 0:return 2097152;case 3:return 65536;case 28:return 32768;case 44:return 32767;case 75:return 16384;case 39:return 1e3;case 89:return 700;case 71:return 256;case 40:return 255;case 2:return 100;case 180:return 64;case 25:return 20;case 5:return 16;case 6:return 6;case 73:return 4;case 84:{if(typeof navigator==="object")return navigator["hardwareConcurrency"]||1;return 1}}___setErrNo(ERRNO_CODES.EINVAL);return-1}var _BDtoIHigh=true;var _BDtoILow=true;Module["_bitshift64Lshr"]=_bitshift64Lshr;var _BItoD=true;Module["_bitshift64Shl"]=_bitshift64Shl;function _abort(){Module["abort"]()}function _emscripten_memcpy_big(dest,src,num){HEAPU8.set(HEAPU8.subarray(src,src+num),dest);return dest}Module["_memcpy"]=_memcpy;Module["_i64Add"]=_i64Add;function ___assert_fail(condition,filename,line,func){ABORT=true;throw"Assertion failed: "+Pointer_stringify(condition)+", at: "+[filename?Pointer_stringify(filename):"unknown filename",line,func?Pointer_stringify(func):"unknown function"]+" at "+stackTrace()}function _sbrk(bytes){var self=_sbrk;if(!self.called){DYNAMICTOP=alignMemoryPage(DYNAMICTOP);self.called=true;assert(Runtime.dynamicAlloc);self.alloc=Runtime.dynamicAlloc;Runtime.dynamicAlloc=(function(){abort("cannot dynamically allocate, sbrk now has control")})}var ret=DYNAMICTOP;if(bytes!=0){var success=self.alloc(bytes);if(!success)return-1>>>0}return ret}Module["_memmove"]=_memmove;function __ZSt18uncaught_exceptionv(){return!!__ZSt18uncaught_exceptionv.uncaught_exception}var EXCEPTIONS={last:0,caught:[],infos:{},deAdjust:(function(adjusted){if(!adjusted||EXCEPTIONS.infos[adjusted])return adjusted;for(var ptr in EXCEPTIONS.infos){var info=EXCEPTIONS.infos[ptr];if(info.adjusted===adjusted){return ptr}}return adjusted}),addRef:(function(ptr){if(!ptr)return;var info=EXCEPTIONS.infos[ptr];info.refcount++}),decRef:(function(ptr){if(!ptr)return;var info=EXCEPTIONS.infos[ptr];assert(info.refcount>0);info.refcount--;if(info.refcount===0){if(info.destructor){Runtime.dynCall("vi",info.destructor,[ptr])}delete EXCEPTIONS.infos[ptr];___cxa_free_exception(ptr)}}),clearRef:(function(ptr){if(!ptr)return;var info=EXCEPTIONS.infos[ptr];info.refcount=0})};function ___resumeException(ptr){if(!EXCEPTIONS.last){EXCEPTIONS.last=ptr}EXCEPTIONS.clearRef(EXCEPTIONS.deAdjust(ptr));throw ptr+" - Exception catching is disabled, this exception cannot be caught. Compile with -s DISABLE_EXCEPTION_CATCHING=0 or DISABLE_EXCEPTION_CATCHING=2 to catch."}function ___cxa_find_matching_catch(){var thrown=EXCEPTIONS.last;if(!thrown){return(asm["setTempRet0"](0),0)|0}var info=EXCEPTIONS.infos[thrown];var throwntype=info.type;if(!throwntype){return(asm["setTempRet0"](0),thrown)|0}var typeArray=Array.prototype.slice.call(arguments);var pointer=Module["___cxa_is_pointer_type"](throwntype);if(!___cxa_find_matching_catch.buffer)___cxa_find_matching_catch.buffer=_malloc(4);HEAP32[___cxa_find_matching_catch.buffer>>2]=thrown;thrown=___cxa_find_matching_catch.buffer;for(var i=0;i<typeArray.length;i++){if(typeArray[i]&&Module["___cxa_can_catch"](typeArray[i],throwntype,thrown)){thrown=HEAP32[thrown>>2];info.adjusted=thrown;return(asm["setTempRet0"](typeArray[i]),thrown)|0}}thrown=HEAP32[thrown>>2];return(asm["setTempRet0"](throwntype),thrown)|0}function ___gxx_personality_v0(){}function _time(ptr){var ret=Date.now()/1e3|0;if(ptr){HEAP32[ptr>>2]=ret}return ret}function _pthread_self(){return 0}STACK_BASE=STACKTOP=Runtime.alignMemory(STATICTOP);staticSealed=true;STACK_MAX=STACK_BASE+TOTAL_STACK;DYNAMIC_BASE=DYNAMICTOP=Runtime.alignMemory(STACK_MAX);assert(DYNAMIC_BASE<TOTAL_MEMORY,"TOTAL_MEMORY not big enough for stack");var cttz_i8=allocate([8,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,6,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,7,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,6,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0],"i8",ALLOC_DYNAMIC);function invoke_iiii(index,a1,a2,a3){try{return Module["dynCall_iiii"](index,a1,a2,a3)}catch(e){if(typeof e!=="number"&&e!=="longjmp")throw e;asm["setThrew"](1,0)}}Module.asmGlobalArg={"Math":Math,"Int8Array":Int8Array,"Int16Array":Int16Array,"Int32Array":Int32Array,"Uint8Array":Uint8Array,"Uint16Array":Uint16Array,"Uint32Array":Uint32Array,"Float32Array":Float32Array,"Float64Array":Float64Array,"NaN":NaN,"Infinity":Infinity};Module.asmLibraryArg={"abort":abort,"assert":assert,"invoke_iiii":invoke_iiii,"_sysconf":_sysconf,"_pthread_self":_pthread_self,"_abort":_abort,"___setErrNo":___setErrNo,"_sbrk":_sbrk,"_time":_time,"_emscripten_memcpy_big":_emscripten_memcpy_big,"___gxx_personality_v0":___gxx_personality_v0,"___resumeException":___resumeException,"__ZSt18uncaught_exceptionv":__ZSt18uncaught_exceptionv,"___assert_fail":___assert_fail,"___cxa_find_matching_catch":___cxa_find_matching_catch,"STACKTOP":STACKTOP,"STACK_MAX":STACK_MAX,"tempDoublePtr":tempDoublePtr,"ABORT":ABORT,"cttz_i8":cttz_i8};// EMSCRIPTEN_START_ASM
+var asm=(function(global,env,buffer) {
+"use asm";var a=new global.Int8Array(buffer);var b=new global.Int16Array(buffer);var c=new global.Int32Array(buffer);var d=new global.Uint8Array(buffer);var e=new global.Uint16Array(buffer);var f=new global.Uint32Array(buffer);var g=new global.Float32Array(buffer);var h=new global.Float64Array(buffer);var i=env.STACKTOP|0;var j=env.STACK_MAX|0;var k=env.tempDoublePtr|0;var l=env.ABORT|0;var m=env.cttz_i8|0;var n=0;var o=0;var p=0;var q=0;var r=global.NaN,s=global.Infinity;var t=0,u=0,v=0,w=0,x=0.0,y=0,z=0,A=0,B=0.0;var C=0;var D=0;var E=0;var F=0;var G=0;var H=0;var I=0;var J=0;var K=0;var L=0;var M=global.Math.floor;var N=global.Math.abs;var O=global.Math.sqrt;var P=global.Math.pow;var Q=global.Math.cos;var R=global.Math.sin;var S=global.Math.tan;var T=global.Math.acos;var U=global.Math.asin;var V=global.Math.atan;var W=global.Math.atan2;var X=global.Math.exp;var Y=global.Math.log;var Z=global.Math.ceil;var _=global.Math.imul;var $=global.Math.min;var aa=global.Math.clz32;var ba=env.abort;var ca=env.assert;var da=env.invoke_iiii;var ea=env._sysconf;var fa=env._pthread_self;var ga=env._abort;var ha=env.___setErrNo;var ia=env._sbrk;var ja=env._time;var ka=env._emscripten_memcpy_big;var la=env.___gxx_personality_v0;var ma=env.___resumeException;var na=env.__ZSt18uncaught_exceptionv;var oa=env.___assert_fail;var pa=env.___cxa_find_matching_catch;var qa=0.0;
+// EMSCRIPTEN_START_FUNCS
+function ub(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,_=0,$=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,na=0,oa=0,pa=0,qa=0,ra=0,sa=0,ta=0,ua=0,va=0,wa=0,xa=0,ya=0,za=0,Aa=0,Ba=0,Ca=0,Da=0,Ea=0,Fa=0,Ga=0,Ha=0,Ka=0,La=0,Ma=0,Oa=0,Qa=0,Ra=0,Sa=0,Ua=0,Va=0,Wa=0,Xa=0,_a=0,eb=0,fb=0,gb=0,hb=0,jb=0,kb=0,lb=0,mb=0,nb=0,ob=0,pb=0,qb=0,sb=0,tb=0,wb=0,yb=0,zb=0,Ab=0,Bb=0,Ib=0,Kb=0,Lb=0,Mb=0,Nb=0,Ob=0,Pb=0,Qb=0,Rb=0,Sb=0,Tb=0,Vb=0,Wb=0,Xb=0,Yb=0,Zb=0,_b=0,$b=0,ac=0,bc=0,cc=0,dc=0,ec=0,fc=0;fc=i;i=i+1104|0;dc=fc+1072|0;ec=fc+1048|0;cc=fc+1032|0;bc=fc+1020|0;$b=fc+1008|0;_b=fc+984|0;ac=fc+972|0;Sb=fc+596|0;Tb=fc+572|0;Xb=fc+548|0;Wb=fc+524|0;Yb=fc+488|0;Zb=fc+460|0;f=fc+960|0;k=fc+948|0;n=fc+936|0;r=fc+924|0;u=fc+912|0;w=fc+900|0;x=fc+888|0;Ib=fc+876|0;Kb=fc+864|0;Lb=fc+852|0;Mb=fc+840|0;y=fc+828|0;Nb=fc+816|0;Ob=fc+804|0;Pb=fc+792|0;Qb=fc+780|0;B=fc+768|0;C=fc+756|0;D=fc+744|0;F=fc+732|0;G=fc+720|0;H=fc+708|0;I=fc+696|0;fb=fc+672|0;gb=fc+656|0;hb=fc+644|0;jb=fc+632|0;kb=fc+620|0;J=fc+608|0;K=fc+584|0;L=fc+560|0;M=fc+536|0;N=fc+512|0;O=fc+472|0;P=fc+448|0;Q=fc+436|0;na=fc+424|0;Ga=fc+400|0;Ha=fc+384|0;Ka=fc+372|0;La=fc+360|0;R=fc+348|0;S=fc+336|0;T=fc+324|0;U=fc+312|0;V=fc+300|0;W=fc+288|0;X=fc+276|0;_=fc+264|0;$=fc+252|0;oa=fc+240|0;Ma=fc+216|0;Oa=fc+204|0;Qa=fc+192|0;Ra=fc+180|0;aa=fc+168|0;sb=fc+144|0;tb=fc+132|0;wb=fc+120|0;yb=fc+108|0;zb=fc+96|0;Ab=fc+84|0;Bb=fc+72|0;da=fc+60|0;fa=fc+48|0;ha=fc+36|0;ia=fc+24|0;Sa=fc;lb=d;ja=lb-b|0;a:do if((ja|0)>1){ka=(ja|0)>3;if(ka?(a[b>>0]|0)==103:0){la=(a[b+1>>0]|0)==115;Ua=la;la=la?b+2|0:b}else{Ua=0;la=b}do switch(a[la>>0]|0){case 76:{b=vb(b,d,e)|0;break a}case 84:{b=Eb(b,d,e)|0;break a}case 102:{b=Fb(b,d,e)|0;break a}case 97:switch(a[la+1>>0]|0){case 97:{dc=b+2|0;$a(f,841,2);ec=Gb(dc,d,f,e)|0;Ja(f);b=(ec|0)==(dc|0)?b:ec;break a}case 100:{dc=b+2|0;$a(k,852,1);ec=Hb(dc,d,k,e)|0;Ja(k);b=(ec|0)==(dc|0)?b:ec;break a}case 110:{dc=b+2|0;$a(n,852,1);ec=Gb(dc,d,n,e)|0;Ja(n);b=(ec|0)==(dc|0)?b:ec;break a}case 78:{dc=b+2|0;$a(r,854,2);ec=Gb(dc,d,r,e)|0;Ja(r);b=(ec|0)==(dc|0)?b:ec;break a}case 83:{dc=b+2|0;$a(u,857,1);ec=Gb(dc,d,u,e)|0;Ja(u);b=(ec|0)==(dc|0)?b:ec;break a}case 116:{if(((((ja|0)>2?(a[b>>0]|0)==97:0)?(a[b+1>>0]|0)==116:0)?(bc=b+2|0,nb=Na(bc,d,e)|0,(nb|0)!=(bc|0)):0)?(Da=c[e+4>>2]|0,(c[e>>2]|0)!=(Da|0)):0){o=Da+-24|0;Cb(cc,o);b=Ta(cc,0,859)|0;c[ec>>2]=c[b>>2];c[ec+4>>2]=c[b+4>>2];c[ec+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(ec,799)|0;c[dc>>2]=c[b>>2];c[dc+4>>2]=c[b+4>>2];c[dc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}do if(a[o>>0]&1){n=Da+-16|0;a[c[n>>2]>>0]=0;k=Da+-20|0;c[k>>2]=0;b=a[o>>0]|0;if(!(b&1))j=10;else{j=c[o>>2]|0;b=j&255;j=(j&-2)+-1|0}if(!(b&1)){f=(b&255)>>>1;if((b&255)<22){h=10;m=1;l=f}else{h=(f+16&240)+-1|0;m=1;l=f}}else{h=10;m=0;l=0}if((h|0)!=(j|0)){if((h|0)==10){g=o+1|0;f=c[n>>2]|0;if(m){Fc(g|0,f|0,((b&255)>>>1)+1|0)|0;wc(f)}else{a[g>>0]=a[f>>0]|0;wc(f)}a[o>>0]=l<<1;break}f=h+1|0;g=vc(f)|0;if(!(h>>>0<=j>>>0&(g|0)==0)){if(m)Fc(g|0,o+1|0,((b&255)>>>1)+1|0)|0;else{bc=c[n>>2]|0;a[g>>0]=a[bc>>0]|0;wc(bc)}c[o>>2]=f|1;c[k>>2]=l;c[n>>2]=g}}}else{a[o+1>>0]=0;a[o>>0]=0}while(0);c[o>>2]=c[dc>>2];c[o+4>>2]=c[dc+4>>2];c[o+8>>2]=c[dc+8>>2];b=0;while(1){if((b|0)==3)break;c[dc+(b<<2)>>2]=0;b=b+1|0}Ja(dc);Ja(ec);Ja(cc);b=nb}break a}case 122:{if(((((ja|0)>2?(a[b>>0]|0)==97:0)?(a[b+1>>0]|0)==122:0)?(bc=b+2|0,ob=ub(bc,d,e)|0,(ob|0)!=(bc|0)):0)?(Ea=c[e+4>>2]|0,(c[e>>2]|0)!=(Ea|0)):0){o=Ea+-24|0;Cb(cc,o);b=Ta(cc,0,859)|0;c[ec>>2]=c[b>>2];c[ec+4>>2]=c[b+4>>2];c[ec+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(ec,799)|0;c[dc>>2]=c[b>>2];c[dc+4>>2]=c[b+4>>2];c[dc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}do if(a[o>>0]&1){n=Ea+-16|0;a[c[n>>2]>>0]=0;k=Ea+-20|0;c[k>>2]=0;b=a[o>>0]|0;if(!(b&1))j=10;else{j=c[o>>2]|0;b=j&255;j=(j&-2)+-1|0}if(!(b&1)){f=(b&255)>>>1;if((b&255)<22){m=1;h=10;l=f}else{m=1;h=(f+16&240)+-1|0;l=f}}else{m=0;h=10;l=0}if((h|0)!=(j|0)){if((h|0)==10){g=o+1|0;f=c[n>>2]|0;if(m){Fc(g|0,f|0,((b&255)>>>1)+1|0)|0;wc(f)}else{a[g>>0]=a[f>>0]|0;wc(f)}a[o>>0]=l<<1;break}f=h+1|0;g=vc(f)|0;if(!(h>>>0<=j>>>0&(g|0)==0)){if(m)Fc(g|0,o+1|0,((b&255)>>>1)+1|0)|0;else{bc=c[n>>2]|0;a[g>>0]=a[bc>>0]|0;wc(bc)}c[o>>2]=f|1;c[k>>2]=l;c[n>>2]=g}}}else{a[o+1>>0]=0;a[o>>0]=0}while(0);c[o>>2]=c[dc>>2];c[o+4>>2]=c[dc+4>>2];c[o+8>>2]=c[dc+8>>2];b=0;while(1){if((b|0)==3)break;c[dc+(b<<2)>>2]=0;b=b+1|0}Ja(dc);Ja(ec);Ja(cc);b=ob}break a}default:break a}case 99:switch(a[la+1>>0]|0){case 99:{if((((((ja|0)>2?(a[b>>0]|0)==99:0)?(a[b+1>>0]|0)==99:0)?(Zb=b+2|0,z=Na(Zb,d,e)|0,(z|0)!=(Zb|0)):0)?(Xa=ub(z,d,e)|0,(Xa|0)!=(z|0)):0)?(ua=e+4|0,A=c[ua>>2]|0,((A-(c[e>>2]|0)|0)/24|0)>>>0>=2):0){Cb(dc,A+-24|0);b=c[ua>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;e=g+-24|0;c[ua>>2]=e;Ia(e);g=c[ua>>2]|0}g=b+-48|0;Cb(ac,g);b=Ta(ac,0,869)|0;c[_b>>2]=c[b>>2];c[_b+4>>2]=c[b+4>>2];c[_b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(_b,881)|0;c[$b>>2]=c[b>>2];c[$b+4>>2]=c[b+4>>2];c[$b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[dc>>0]|0;f=(b&1)==0;b=Za($b,f?dc+1|0:c[dc+8>>2]|0,f?(b&255)>>>1:c[dc+4>>2]|0)|0;c[bc>>2]=c[b>>2];c[bc+4>>2]=c[b+4>>2];c[bc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(bc,799)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(ec,cc);Db(g,ec);Ia(ec);Ja(cc);Ja(bc);Ja($b);Ja(_b);Ja(ac);Ja(dc);b=Xa}break a}case 108:{b:do if((((ka?(a[b>>0]|0)==99:0)?(a[b+1>>0]|0)==108:0)?(cc=b+2|0,pb=ub(cc,d,e)|0,!((pb|0)==(cc|0)|(pb|0)==(d|0))):0)?(Rb=e+4|0,E=c[Rb>>2]|0,(c[e>>2]|0)!=(E|0)):0){cc=E+-12|0;g=a[cc>>0]|0;f=(g&1)==0;Za(E+-24|0,f?cc+1|0:c[E+-4>>2]|0,f?(g&255)>>>1:c[E+-8>>2]|0)|0;g=c[Rb>>2]|0;f=0;while(1){if((f|0)==3)break;c[dc+(f<<2)>>2]=0;f=f+1|0}p=g+-12|0;do if(a[p>>0]&1){o=g+-4|0;a[c[o>>2]>>0]=0;l=g+-8|0;c[l>>2]=0;f=a[p>>0]|0;if(!(f&1))k=10;else{k=c[p>>2]|0;f=k&255;k=(k&-2)+-1|0}if(!(f&1)){g=(f&255)>>>1;if((f&255)<22){n=1;j=10;m=g}else{n=1;j=(g+16&240)+-1|0;m=g}}else{n=0;j=10;m=0}if((j|0)!=(k|0)){if((j|0)==10){h=p+1|0;g=c[o>>2]|0;if(n){Fc(h|0,g|0,((f&255)>>>1)+1|0)|0;wc(g)}else{a[h>>0]=a[g>>0]|0;wc(g)}a[p>>0]=m<<1;break}g=j+1|0;h=vc(g)|0;if(!(j>>>0<=k>>>0&(h|0)==0)){if(n)Fc(h|0,p+1|0,((f&255)>>>1)+1|0)|0;else{cc=c[o>>2]|0;a[h>>0]=a[cc>>0]|0;wc(cc)}c[p>>2]=g|1;c[l>>2]=m;c[o>>2]=h}}}else{a[p+1>>0]=0;a[p>>0]=0}while(0);c[p>>2]=c[dc>>2];c[p+4>>2]=c[dc+4>>2];c[p+8>>2]=c[dc+8>>2];f=0;while(1){if((f|0)==3)break;c[dc+(f<<2)>>2]=0;f=f+1|0}Ja(dc);Ya((c[Rb>>2]|0)+-24|0,797)|0;l=ec+4|0;m=ec+8|0;n=ec+1|0;g=pb;while(1){if((a[g>>0]|0)==69)break;k=ub(g,d,e)|0;if((k|0)==(g|0)|(k|0)==(d|0))break b;f=c[Rb>>2]|0;if((c[e>>2]|0)==(f|0))break b;Cb(ec,f+-24|0);h=c[Rb>>2]|0;j=h+-24|0;f=h;while(1){if((f|0)==(j|0))break;dc=f+-24|0;c[Rb>>2]=dc;Ia(dc);f=c[Rb>>2]|0}g=a[ec>>0]|0;f=(g&1)==0;g=f?(g&255)>>>1:c[l>>2]|0;if(g){if((c[e>>2]|0)==(j|0)){Vb=147;break}Za(h+-48|0,f?n:c[m>>2]|0,g)|0}Ja(ec);g=k}if((Vb|0)==147){Ja(ec);break}f=c[Rb>>2]|0;if((c[e>>2]|0)!=(f|0)){Ya(f+-24|0,799)|0;b=g+1|0}}while(0);break a}case 109:{dc=b+2|0;$a(w,884,1);ec=Gb(dc,d,w,e)|0;Ja(w);b=(ec|0)==(dc|0)?b:ec;break a}case 111:{dc=b+2|0;$a(x,886,1);ec=Hb(dc,d,x,e)|0;Ja(x);b=(ec|0)==(dc|0)?b:ec;break a}case 118:{c:do if((((ja|0)>2?(a[b>>0]|0)==99:0)?(a[b+1>>0]|0)==118:0)?(Yb=e+63|0,Xb=a[Yb>>0]|0,a[Yb>>0]=0,Zb=b+2|0,ma=Na(Zb,d,e)|0,a[Yb>>0]=Xb,!((ma|0)==(Zb|0)|(ma|0)==(d|0))):0){if((a[ma>>0]|0)!=95){f=ub(ma,d,e)|0;if((f|0)==(ma|0))break}else{f=ma+1|0;if((f|0)==(d|0))break;g=a[f>>0]|0;d:do if(g<<24>>24==69){j=e+4|0;h=c[j>>2]|0;Zb=c[e+8>>2]|0;k=Zb;if(h>>>0<Zb>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=0;c[h+16>>2]=0;c[h+20>>2]=0;g=0;while(1){if((g|0)==3)break;c[h+(g<<2)>>2]=0;g=g+1|0}g=h+12|0;h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}c[j>>2]=(c[j>>2]|0)+24;break}g=c[e>>2]|0;Zb=h-g|0;j=(Zb|0)/24|0;h=j+1|0;if((Zb|0)<-24)Pa();g=(k-g|0)/24|0;if(g>>>0<1073741823){g=g<<1;g=g>>>0<h>>>0?h:g}else g=2147483647;ab(dc,g,j,e+12|0);j=dc+8|0;k=c[j>>2]|0;c[k>>2]=0;c[k+4>>2]=0;c[k+8>>2]=0;c[k+12>>2]=0;c[k+16>>2]=0;c[k+20>>2]=0;g=0;while(1){if((g|0)==3)break;c[k+(g<<2)>>2]=0;g=g+1|0}g=k+12|0;h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}c[j>>2]=k+24;cb(e,dc);bb(dc)}else while(1){if(g<<24>>24==69)break d;h=ub(f,d,e)|0;if((h|0)==(f|0)|(h|0)==(d|0))break c;g=a[h>>0]|0;f=h}while(0);f=f+1|0}j=e+4|0;g=c[j>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0>=2){Cb(dc,g+-24|0);b=c[j>>2]|0;g=b+-24|0;h=b;while(1){if((h|0)==(g|0))break;e=h+-24|0;c[j>>2]=e;Ia(e);h=c[j>>2]|0}h=b+-48|0;Cb(ac,h);b=Ta(ac,0,797)|0;c[_b>>2]=c[b>>2];c[_b+4>>2]=c[b+4>>2];c[_b+8>>2]=c[b+8>>2];g=0;while(1){if((g|0)==3)break;c[b+(g<<2)>>2]=0;g=g+1|0}b=Ya(_b,888)|0;c[$b>>2]=c[b>>2];c[$b+4>>2]=c[b+4>>2];c[$b+8>>2]=c[b+8>>2];g=0;while(1){if((g|0)==3)break;c[b+(g<<2)>>2]=0;g=g+1|0}b=a[dc>>0]|0;g=(b&1)==0;b=Za($b,g?dc+1|0:c[dc+8>>2]|0,g?(b&255)>>>1:c[dc+4>>2]|0)|0;c[bc>>2]=c[b>>2];c[bc+4>>2]=c[b+4>>2];c[bc+8>>2]=c[b+8>>2];g=0;while(1){if((g|0)==3)break;c[b+(g<<2)>>2]=0;g=g+1|0}b=Ya(bc,799)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];g=0;while(1){if((g|0)==3)break;c[b+(g<<2)>>2]=0;g=g+1|0}rb(ec,cc);Db(h,ec);Ia(ec);Ja(cc);Ja(bc);Ja($b);Ja(_b);Ja(ac);Ja(dc);b=f}}while(0);break a}default:break a}case 100:switch(a[la+1>>0]|0){case 97:{ec=la+2|0;p=ub(ec,d,e)|0;if((p|0)==(ec|0))break a;g=e+4|0;h=c[g>>2]|0;if((c[e>>2]|0)==(h|0))break a;o=h+-24|0;e:do if(Ua)$a(Lb,891,2);else{b=0;while(1){if((b|0)==3)break e;c[Lb+(b<<2)>>2]=0;b=b+1|0}}while(0);b=Ya(Lb,894)|0;c[Kb>>2]=c[b>>2];c[Kb+4>>2]=c[b+4>>2];c[Kb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}Cb(Mb,(c[g>>2]|0)+-24|0);b=a[Mb>>0]|0;f=(b&1)==0;b=Za(Kb,f?Mb+1|0:c[Mb+8>>2]|0,f?(b&255)>>>1:c[Mb+4>>2]|0)|0;c[Ib>>2]=c[b>>2];c[Ib+4>>2]=c[b+4>>2];c[Ib+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}do if(a[o>>0]&1){n=h+-16|0;a[c[n>>2]>>0]=0;k=h+-20|0;c[k>>2]=0;b=a[o>>0]|0;if(!(b&1))j=10;else{j=c[o>>2]|0;b=j&255;j=(j&-2)+-1|0}if(!(b&1)){f=(b&255)>>>1;if((b&255)<22){m=1;h=10;l=f}else{m=1;h=(f+16&240)+-1|0;l=f}}else{m=0;h=10;l=0}if((h|0)!=(j|0)){if((h|0)==10){g=o+1|0;f=c[n>>2]|0;if(m){Fc(g|0,f|0,((b&255)>>>1)+1|0)|0;wc(f)}else{a[g>>0]=a[f>>0]|0;wc(f)}a[o>>0]=l<<1;break}f=h+1|0;g=vc(f)|0;if(!(h>>>0<=j>>>0&(g|0)==0)){if(m)Fc(g|0,o+1|0,((b&255)>>>1)+1|0)|0;else{ec=c[n>>2]|0;a[g>>0]=a[ec>>0]|0;wc(ec)}c[o>>2]=f|1;c[k>>2]=l;c[n>>2]=g}}}else{a[o+1>>0]=0;a[o>>0]=0}while(0);c[o>>2]=c[Ib>>2];c[o+4>>2]=c[Ib+4>>2];c[o+8>>2]=c[Ib+8>>2];b=0;while(1){if((b|0)==3)break;c[Ib+(b<<2)>>2]=0;b=b+1|0}Ja(Ib);Ja(Mb);Ja(Kb);Ja(Lb);b=p;break a}case 99:{if((((((ja|0)>2?(a[b>>0]|0)==100:0)?(a[b+1>>0]|0)==99:0)?(Zb=b+2|0,Y=Na(Zb,d,e)|0,(Y|0)!=(Zb|0)):0)?(_a=ub(Y,d,e)|0,(_a|0)!=(Y|0)):0)?(va=e+4|0,Z=c[va>>2]|0,((Z-(c[e>>2]|0)|0)/24|0)>>>0>=2):0){Cb(dc,Z+-24|0);b=c[va>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;e=g+-24|0;c[va>>2]=e;Ia(e);g=c[va>>2]|0}g=b+-48|0;Cb(ac,g);b=Ta(ac,0,904)|0;c[_b>>2]=c[b>>2];c[_b+4>>2]=c[b+4>>2];c[_b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(_b,881)|0;c[$b>>2]=c[b>>2];c[$b+4>>2]=c[b+4>>2];c[$b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[dc>>0]|0;f=(b&1)==0;b=Za($b,f?dc+1|0:c[dc+8>>2]|0,f?(b&255)>>>1:c[dc+4>>2]|0)|0;c[bc>>2]=c[b>>2];c[bc+4>>2]=c[b+4>>2];c[bc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(bc,799)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(ec,cc);Db(g,ec);Ia(ec);Ja(cc);Ja(bc);Ja($b);Ja(_b);Ja(ac);Ja(dc);b=_a}break a}case 101:{dc=b+2|0;$a(y,4262,1);ec=Hb(dc,d,y,e)|0;Ja(y);b=(ec|0)==(dc|0)?b:ec;break a}case 108:{ec=la+2|0;p=ub(ec,d,e)|0;if((p|0)==(ec|0))break a;g=e+4|0;h=c[g>>2]|0;if((c[e>>2]|0)==(h|0))break a;o=h+-24|0;f:do if(Ua)$a(Pb,891,2);else{b=0;while(1){if((b|0)==3)break f;c[Pb+(b<<2)>>2]=0;b=b+1|0}}while(0);b=Ya(Pb,918)|0;c[Ob>>2]=c[b>>2];c[Ob+4>>2]=c[b+4>>2];c[Ob+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}Cb(Qb,(c[g>>2]|0)+-24|0);b=a[Qb>>0]|0;f=(b&1)==0;b=Za(Ob,f?Qb+1|0:c[Qb+8>>2]|0,f?(b&255)>>>1:c[Qb+4>>2]|0)|0;c[Nb>>2]=c[b>>2];c[Nb+4>>2]=c[b+4>>2];c[Nb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}do if(a[o>>0]&1){n=h+-16|0;a[c[n>>2]>>0]=0;k=h+-20|0;c[k>>2]=0;b=a[o>>0]|0;if(!(b&1))j=10;else{j=c[o>>2]|0;b=j&255;j=(j&-2)+-1|0}if(!(b&1)){f=(b&255)>>>1;if((b&255)<22){m=1;h=10;l=f}else{m=1;h=(f+16&240)+-1|0;l=f}}else{m=0;h=10;l=0}if((h|0)!=(j|0)){if((h|0)==10){g=o+1|0;f=c[n>>2]|0;if(m){Fc(g|0,f|0,((b&255)>>>1)+1|0)|0;wc(f)}else{a[g>>0]=a[f>>0]|0;wc(f)}a[o>>0]=l<<1;break}f=h+1|0;g=vc(f)|0;if(!(h>>>0<=j>>>0&(g|0)==0)){if(m)Fc(g|0,o+1|0,((b&255)>>>1)+1|0)|0;else{ec=c[n>>2]|0;a[g>>0]=a[ec>>0]|0;wc(ec)}c[o>>2]=f|1;c[k>>2]=l;c[n>>2]=g}}}else{a[o+1>>0]=0;a[o>>0]=0}while(0);c[o>>2]=c[Nb>>2];c[o+4>>2]=c[Nb+4>>2];c[o+8>>2]=c[Nb+8>>2];b=0;while(1){if((b|0)==3)break;c[Nb+(b<<2)>>2]=0;b=b+1|0}Ja(Nb);Ja(Qb);Ja(Ob);Ja(Pb);b=p;break a}case 110:{b=Jb(b,d,e)|0;break a}case 115:{if((((((ja|0)>2?(a[b>>0]|0)==100:0)?(a[b+1>>0]|0)==115:0)?(cc=b+2|0,ba=ub(cc,d,e)|0,(ba|0)!=(cc|0)):0)?(wa=ub(ba,d,e)|0,(wa|0)!=(ba|0)):0)?(xa=e+4|0,ca=c[xa>>2]|0,((ca-(c[e>>2]|0)|0)/24|0)>>>0>=2):0){Cb(dc,ca+-24|0);b=c[xa>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;cc=g+-24|0;c[xa>>2]=cc;Ia(cc);g=c[xa>>2]|0}xb(ec,1833,dc);cc=a[ec>>0]|0;bc=(cc&1)==0;Za(b+-48|0,bc?ec+1|0:c[ec+8>>2]|0,bc?(cc&255)>>>1:c[ec+4>>2]|0)|0;Ja(ec);Ja(dc);b=wa}break a}case 116:{if((((((ja|0)>2?(a[b>>0]|0)==100:0)?(a[b+1>>0]|0)==116:0)?(cc=b+2|0,ea=ub(cc,d,e)|0,(ea|0)!=(cc|0)):0)?(ya=Jb(ea,d,e)|0,(ya|0)!=(ea|0)):0)?(za=e+4|0,ga=c[za>>2]|0,((ga-(c[e>>2]|0)|0)/24|0)>>>0>=2):0){Cb(dc,ga+-24|0);b=c[za>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;cc=g+-24|0;c[za>>2]=cc;Ia(cc);g=c[za>>2]|0}xb(ec,4798,dc);cc=a[ec>>0]|0;bc=(cc&1)==0;Za(b+-48|0,bc?ec+1|0:c[ec+8>>2]|0,bc?(cc&255)>>>1:c[ec+4>>2]|0)|0;Ja(ec);Ja(dc);b=ya}break a}case 118:{dc=b+2|0;$a(B,2368,1);ec=Gb(dc,d,B,e)|0;Ja(B);b=(ec|0)==(dc|0)?b:ec;break a}case 86:{dc=b+2|0;$a(C,1836,2);ec=Gb(dc,d,C,e)|0;Ja(C);b=(ec|0)==(dc|0)?b:ec;break a}default:break a}case 101:switch(a[la+1>>0]|0){case 111:{dc=b+2|0;$a(D,1839,1);ec=Gb(dc,d,D,e)|0;Ja(D);b=(ec|0)==(dc|0)?b:ec;break a}case 79:{dc=b+2|0;$a(F,1841,2);ec=Gb(dc,d,F,e)|0;Ja(F);b=(ec|0)==(dc|0)?b:ec;break a}case 113:{dc=b+2|0;$a(G,1844,2);ec=Gb(dc,d,G,e)|0;Ja(G);b=(ec|0)==(dc|0)?b:ec;break a}default:break a}case 103:switch(a[la+1>>0]|0){case 101:{dc=b+2|0;$a(H,1847,2);ec=Gb(dc,d,H,e)|0;Ja(H);b=(ec|0)==(dc|0)?b:ec;break a}case 116:{dc=b+2|0;$a(I,844,1);ec=Gb(dc,d,I,e)|0;Ja(I);b=(ec|0)==(dc|0)?b:ec;break a}default:break a}case 105:{if((a[la+1>>0]|0)!=120)break a;cc=b+2|0;f=ub(cc,d,e)|0;if((f|0)==(cc|0))break a;j=ub(f,d,e)|0;h=e+4|0;if((j|0)==(f|0)){g=c[h>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break a;ec=g+-24|0;c[h>>2]=ec;Ia(ec);g=c[h>>2]|0}}f=c[h>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2)break a;Cb(dc,f+-24|0);b=c[h>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;cc=g+-24|0;c[h>>2]=cc;Ia(cc);g=c[h>>2]|0}Cb(ec,b+-48|0);g=(c[h>>2]|0)+-24|0;xb(kb,797,ec);b=Ya(kb,1850)|0;c[jb>>2]=c[b>>2];c[jb+4>>2]=c[b+4>>2];c[jb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[dc>>0]|0;f=(b&1)==0;b=Za(jb,f?dc+1|0:c[dc+8>>2]|0,f?(b&255)>>>1:c[dc+4>>2]|0)|0;c[hb>>2]=c[b>>2];c[hb+4>>2]=c[b+4>>2];c[hb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(hb,4264)|0;c[gb>>2]=c[b>>2];c[gb+4>>2]=c[b+4>>2];c[gb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(fb,gb);Db(g,fb);Ia(fb);Ja(gb);Ja(hb);Ja(jb);Ja(kb);Ja(ec);Ja(dc);b=j;break a}case 108:switch(a[la+1>>0]|0){case 101:{dc=b+2|0;$a(J,1853,2);ec=Gb(dc,d,J,e)|0;Ja(J);b=(ec|0)==(dc|0)?b:ec;break a}case 115:{dc=b+2|0;$a(K,1856,2);ec=Gb(dc,d,K,e)|0;Ja(K);b=(ec|0)==(dc|0)?b:ec;break a}case 83:{dc=b+2|0;$a(L,1859,3);ec=Gb(dc,d,L,e)|0;Ja(L);b=(ec|0)==(dc|0)?b:ec;break a}case 116:{dc=b+2|0;$a(M,1427,1);ec=Gb(dc,d,M,e)|0;Ja(M);b=(ec|0)==(dc|0)?b:ec;break a}default:break a}case 109:switch(a[la+1>>0]|0){case 105:{dc=b+2|0;$a(N,1863,1);ec=Gb(dc,d,N,e)|0;Ja(N);b=(ec|0)==(dc|0)?b:ec;break a}case 73:{dc=b+2|0;$a(O,1865,2);ec=Gb(dc,d,O,e)|0;Ja(O);b=(ec|0)==(dc|0)?b:ec;break a}case 108:{dc=b+2|0;$a(P,4262,1);ec=Gb(dc,d,P,e)|0;Ja(P);b=(ec|0)==(dc|0)?b:ec;break a}case 76:{dc=b+2|0;$a(Q,1868,2);ec=Gb(dc,d,Q,e)|0;Ja(Q);b=(ec|0)==(dc|0)?b:ec;break a}case 109:{f=b+2|0;if((f|0)!=(d|0)?(a[f>>0]|0)==95:0){dc=b+3|0;$a(na,1871,2);ec=Hb(dc,d,na,e)|0;Ja(na);b=(ec|0)==(dc|0)?b:ec;break a}h=ub(f,d,e)|0;if((h|0)==(f|0))break a;f=c[e+4>>2]|0;if((c[e>>2]|0)==(f|0))break a;g=f+-24|0;Cb(La,g);b=Ta(La,0,797)|0;c[Ka>>2]=c[b>>2];c[Ka+4>>2]=c[b+4>>2];c[Ka+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(Ka,1874)|0;c[Ha>>2]=c[b>>2];c[Ha+4>>2]=c[b+4>>2];c[Ha+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(Ga,Ha);Db(g,Ga);Ia(Ga);Ja(Ha);Ja(Ka);Ja(La);b=h;break a}default:break a}case 110:switch(a[la+1>>0]|0){case 119:case 97:{g:do if(ka){f=a[b>>0]|0;if(f<<24>>24==103){s=(a[b+1>>0]|0)==115;g=s?b+2|0:b;f=a[g>>0]|0}else{s=0;g=b}if(f<<24>>24==110){f=a[g+1>>0]|0;switch(f<<24>>24){case 97:case 119:break;default:break g}q=f<<24>>24==97;f=g+2|0;h:do if((f|0)!=(d|0)){p=0;while(1){if((a[f>>0]|0)==95)break;h=ub(f,d,e)|0;f=(h|0)==(f|0);g=(h|0)==(d|0);if(f|g)break h;else{p=p|(f|g)^1;f=h}}Rb=f+1|0;g=Na(Rb,d,e)|0;if(!((g|0)==(Rb|0)|(g|0)==(d|0))){f=a[g>>0]|0;i:do if(!((lb-g|0)>2&f<<24>>24==112))if(f<<24>>24==69){o=0;r=g}else break h;else{if((a[g+1>>0]|0)!=105)break h;f=g+2|0;while(1){if((a[f>>0]|0)==69){o=1;r=f;break i}Rb=f;f=ub(f,d,e)|0;if((f|0)==(Rb|0)|(f|0)==(d|0))break h}}while(0);f=0;while(1){if((f|0)==3)break;c[ec+(f<<2)>>2]=0;f=f+1|0}j:do if(o){n=e+4|0;f=c[n>>2]|0;if((c[e>>2]|0)==(f|0)){g=b;f=1}else{Cb(cc,f+-24|0);k:do if(!(a[ec>>0]&1)){a[ec+1>>0]=0;a[ec>>0]=0}else{k=ec+8|0;g=c[k>>2]|0;a[g>>0]=0;l=ec+4|0;c[l>>2]=0;f=c[ec>>2]|0;m=(f&-2)+-1|0;h=f&255;do if(!(h&1)){f=f>>>1&127;if((h&255)<22){Fc(ec+1|0,g|0,f+1|0)|0;wc(g);break}g=f+16&240;j=g+-1|0;if((j|0)==(m|0))break k;h=vc(g)|0;if(j>>>0<=m>>>0&(h|0)==0)break k;Fc(h|0,ec+1|0,f+1|0)|0;c[ec>>2]=g|1;c[l>>2]=f;c[k>>2]=h;break k}else{a[ec+1>>0]=0;wc(g);f=0}while(0);a[ec>>0]=f<<1}while(0);c[ec>>2]=c[cc>>2];c[ec+4>>2]=c[cc+4>>2];c[ec+8>>2]=c[cc+8>>2];f=0;while(1){if((f|0)==3)break;c[cc+(f<<2)>>2]=0;f=f+1|0}Ja(cc);f=c[n>>2]|0;g=f+-24|0;while(1){if((f|0)==(g|0)){j=e;f=g;Vb=409;break j}cc=f+-24|0;c[n>>2]=cc;Ia(cc);f=c[n>>2]|0}}}else{f=e+4|0;n=f;j=e;f=c[f>>2]|0;Vb=409}while(0);if((Vb|0)==409)if((c[j>>2]|0)==(f|0)){g=b;f=1}else{Cb(bc,f+-24|0);g=c[n>>2]|0;h=g+-24|0;f=g;while(1){if((f|0)==(h|0))break;cc=f+-24|0;c[n>>2]=cc;Ia(cc);f=c[n>>2]|0}f=0;while(1){if((f|0)==3)break;c[$b+(f<<2)>>2]=0;f=f+1|0}l:do if(p)if((c[j>>2]|0)==(h|0)){g=b;f=1}else{Cb(_b,g+-48|0);m:do if(!(a[$b>>0]&1)){a[$b+1>>0]=0;a[$b>>0]=0}else{k=$b+8|0;g=c[k>>2]|0;a[g>>0]=0;l=$b+4|0;c[l>>2]=0;f=c[$b>>2]|0;m=(f&-2)+-1|0;h=f&255;do if(!(h&1)){f=f>>>1&127;if((h&255)<22){Fc($b+1|0,g|0,f+1|0)|0;wc(g);break}g=f+16&240;j=g+-1|0;if((j|0)==(m|0))break m;h=vc(g)|0;if(j>>>0<=m>>>0&(h|0)==0)break m;Fc(h|0,$b+1|0,f+1|0)|0;c[$b>>2]=g|1;c[l>>2]=f;c[k>>2]=h;break m}else{a[$b+1>>0]=0;wc(g);f=0}while(0);a[$b>>0]=f<<1}while(0);c[$b>>2]=c[_b>>2];c[$b+4>>2]=c[_b+4>>2];c[$b+8>>2]=c[_b+8>>2];f=0;while(1){if((f|0)==3)break;c[_b+(f<<2)>>2]=0;f=f+1|0}Ja(_b);g=c[n>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0)){Vb=434;break l}cc=g+-24|0;c[n>>2]=cc;Ia(cc);g=c[n>>2]|0}}else Vb=434;while(0);if((Vb|0)==434){f=0;while(1){if((f|0)==3)break;c[ac+(f<<2)>>2]=0;f=f+1|0}if(s)Ub(ac,891,2);if(q)Ya(ac,1878)|0;else Ya(ac,1882)|0;if(p){xb(Tb,797,$b);f=Ya(Tb,846)|0;c[Sb>>2]=c[f>>2];c[Sb+4>>2]=c[f+4>>2];c[Sb+8>>2]=c[f+8>>2];g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}cc=a[Sb>>0]|0;_b=(cc&1)==0;Za(ac,_b?Sb+1|0:c[Sb+8>>2]|0,_b?(cc&255)>>>1:c[Sb+4>>2]|0)|0;Ja(Sb);Ja(Tb)}cc=a[bc>>0]|0;_b=(cc&1)==0;Za(ac,_b?bc+1|0:c[bc+8>>2]|0,_b?(cc&255)>>>1:c[bc+4>>2]|0)|0;if(o){xb(Wb,849,ec);f=Ya(Wb,799)|0;c[Xb>>2]=c[f>>2];c[Xb+4>>2]=c[f+4>>2];c[Xb+8>>2]=c[f+8>>2];g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}cc=a[Xb>>0]|0;_b=(cc&1)==0;Za(ac,_b?Xb+1|0:c[Xb+8>>2]|0,_b?(cc&255)>>>1:c[Xb+4>>2]|0)|0;Ja(Xb);Ja(Wb)};c[Zb>>2]=c[ac>>2];c[Zb+4>>2]=c[ac+4>>2];c[Zb+8>>2]=c[ac+8>>2];f=0;while(1){if((f|0)==3)break;c[ac+(f<<2)>>2]=0;f=f+1|0}rb(Yb,Zb);f=c[n>>2]|0;cc=c[e+8>>2]|0;j=cc;if(f>>>0<cc>>>0){db(f,Yb);c[n>>2]=(c[n>>2]|0)+24}else{g=c[e>>2]|0;cc=f-g|0;k=(cc|0)/24|0;h=k+1|0;if((cc|0)<-24)Pa();f=(j-g|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<h>>>0?h:f}else f=2147483647;ab(dc,f,k,e+12|0);cc=dc+8|0;_b=c[cc>>2]|0;db(_b,Yb);c[cc>>2]=_b+24;cb(e,dc);bb(dc)}Ia(Yb);Ja(Zb);Ja(ac);g=r+1|0;f=0}Ja($b);Ja(bc)}Ja(ec);if(!f){b=g;break g}}}while(0)}}while(0);break a}case 101:{dc=b+2|0;$a(R,1884,2);ec=Gb(dc,d,R,e)|0;Ja(R);b=(ec|0)==(dc|0)?b:ec;break a}case 103:{dc=b+2|0;$a(S,1863,1);ec=Hb(dc,d,S,e)|0;Ja(S);b=(ec|0)==(dc|0)?b:ec;break a}case 116:{dc=b+2|0;$a(T,1887,1);ec=Hb(dc,d,T,e)|0;Ja(T);b=(ec|0)==(dc|0)?b:ec;break a}case 120:{r=b+2|0;f=ub(r,d,e)|0;if((f|0)!=(r|0)?(Fa=c[e+4>>2]|0,(c[e>>2]|0)!=(Fa|0)):0){q=Fa+-24|0;Cb(cc,q);g=Ta(cc,0,1889)|0;c[ec>>2]=c[g>>2];c[ec+4>>2]=c[g+4>>2];c[ec+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}g=Ya(ec,799)|0;c[dc>>2]=c[g>>2];c[dc+4>>2]=c[g+4>>2];c[dc+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}do if(a[q>>0]&1){p=Fa+-16|0;a[c[p>>2]>>0]=0;m=Fa+-20|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){k=10;n=h;o=1}else{k=(h+16&240)+-1|0;n=h;o=1}}else{k=10;n=0;o=0}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{bc=c[p>>2]|0;a[j>>0]=a[bc>>0]|0;wc(bc)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[dc>>2];c[q+4>>2]=c[dc+4>>2];c[q+8>>2]=c[dc+8>>2];g=0;while(1){if((g|0)==3)break;c[dc+(g<<2)>>2]=0;g=g+1|0}Ja(dc);Ja(ec);Ja(cc)}else f=r;b=(f|0)==(r|0)?b:f;break a}default:break a}case 111:switch(a[la+1>>0]|0){case 110:{b=Jb(b,d,e)|0;break a}case 111:{dc=b+2|0;$a(U,1900,2);ec=Gb(dc,d,U,e)|0;Ja(U);b=(ec|0)==(dc|0)?b:ec;break a}case 114:{dc=b+2|0;$a(V,1903,1);ec=Gb(dc,d,V,e)|0;Ja(V);b=(ec|0)==(dc|0)?b:ec;break a}case 82:{dc=b+2|0;$a(W,1905,2);ec=Gb(dc,d,W,e)|0;Ja(W);b=(ec|0)==(dc|0)?b:ec;break a}default:break a}case 112:switch(a[la+1>>0]|0){case 109:{dc=b+2|0;$a(X,1908,3);ec=Gb(dc,d,X,e)|0;Ja(X);b=(ec|0)==(dc|0)?b:ec;break a}case 108:{dc=b+2|0;$a(_,1912,1);ec=Gb(dc,d,_,e)|0;Ja(_);b=(ec|0)==(dc|0)?b:ec;break a}case 76:{dc=b+2|0;$a($,1914,2);ec=Gb(dc,d,$,e)|0;Ja($);b=(ec|0)==(dc|0)?b:ec;break a}case 112:{f=b+2|0;if((f|0)!=(d|0)?(a[f>>0]|0)==95:0){dc=b+3|0;$a(oa,1917,2);ec=Hb(dc,d,oa,e)|0;Ja(oa);b=(ec|0)==(dc|0)?b:ec;break a}h=ub(f,d,e)|0;if((h|0)==(f|0))break a;f=c[e+4>>2]|0;if((c[e>>2]|0)==(f|0))break a;g=f+-24|0;Cb(Ra,g);b=Ta(Ra,0,797)|0;c[Qa>>2]=c[b>>2];c[Qa+4>>2]=c[b+4>>2];c[Qa+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(Qa,1920)|0;c[Oa>>2]=c[b>>2];c[Oa+4>>2]=c[b+4>>2];c[Oa+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(Ma,Oa);Db(g,Ma);Ia(Ma);Ja(Oa);Ja(Qa);Ja(Ra);b=h;break a}case 115:{dc=b+2|0;$a(aa,1912,1);ec=Hb(dc,d,aa,e)|0;Ja(aa);b=(ec|0)==(dc|0)?b:ec;break a}case 116:{if((ja|0)<=2)break a;if((a[b>>0]|0)!=112)break a;if((a[b+1>>0]|0)!=116)break a;ec=b+2|0;f=ub(ec,d,e)|0;if((f|0)==(ec|0))break a;j=ub(f,d,e)|0;if((j|0)==(f|0))break a;h=e+4|0;f=c[h>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2)break a;Cb(dc,f+-24|0);b=c[h>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;ec=g+-24|0;c[h>>2]=ec;Ia(ec);g=c[h>>2]|0}Ya(b+-48|0,1924)|0;b=a[dc>>0]|0;ec=(b&1)==0;Za((c[h>>2]|0)+-24|0,ec?dc+1|0:c[dc+8>>2]|0,ec?(b&255)>>>1:c[dc+4>>2]|0)|0;Ja(dc);b=j;break a}default:break a}case 113:{if((a[la+1>>0]|0)!=117)break a;bc=b+2|0;f=ub(bc,d,e)|0;if((f|0)==(bc|0))break a;g=ub(f,d,e)|0;if((g|0)==(f|0)){f=e+4|0;h=c[f>>2]|0;g=h+-24|0;while(1){if((h|0)==(g|0))break a;ec=h+-24|0;c[f>>2]=ec;Ia(ec);h=c[f>>2]|0}}h=ub(g,d,e)|0;j=e+4|0;if((h|0)==(g|0)){g=c[j>>2]|0;f=g+-24|0;h=g;while(1){if((h|0)==(f|0))break;ec=h+-24|0;c[j>>2]=ec;Ia(ec);h=c[j>>2]|0}g=g+-48|0;while(1){if((f|0)==(g|0))break a;ec=f+-24|0;c[j>>2]=ec;Ia(ec);f=c[j>>2]|0}}f=c[j>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<3)break a;Cb(dc,f+-24|0);b=c[j>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;bc=g+-24|0;c[j>>2]=bc;Ia(bc);g=c[j>>2]|0}Cb(ec,b+-48|0);b=c[j>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;bc=g+-24|0;c[j>>2]=bc;Ia(bc);g=c[j>>2]|0}Cb(cc,b+-48|0);g=(c[j>>2]|0)+-24|0;xb(Bb,797,cc);b=Ya(Bb,1927)|0;c[Ab>>2]=c[b>>2];c[Ab+4>>2]=c[b+4>>2];c[Ab+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[ec>>0]|0;f=(b&1)==0;b=Za(Ab,f?ec+1|0:c[ec+8>>2]|0,f?(b&255)>>>1:c[ec+4>>2]|0)|0;c[zb>>2]=c[b>>2];c[zb+4>>2]=c[b+4>>2];c[zb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(zb,1933)|0;c[yb>>2]=c[b>>2];c[yb+4>>2]=c[b+4>>2];c[yb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[dc>>0]|0;f=(b&1)==0;b=Za(yb,f?dc+1|0:c[dc+8>>2]|0,f?(b&255)>>>1:c[dc+4>>2]|0)|0;c[wb>>2]=c[b>>2];c[wb+4>>2]=c[b+4>>2];c[wb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(wb,799)|0;c[tb>>2]=c[b>>2];c[tb+4>>2]=c[b+4>>2];c[tb+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(sb,tb);Db(g,sb);Ia(sb);Ja(tb);Ja(wb);Ja(yb);Ja(zb);Ja(Ab);Ja(Bb);Ja(cc);Ja(ec);Ja(dc);b=h;break a}case 114:switch(a[la+1>>0]|0){case 99:{if((((((ja|0)>2?(a[b>>0]|0)==114:0)?(a[b+1>>0]|0)==99:0)?(Zb=b+2|0,h=Na(Zb,d,e)|0,(h|0)!=(Zb|0)):0)?(Va=ub(h,d,e)|0,(Va|0)!=(h|0)):0)?(pa=e+4|0,j=c[pa>>2]|0,((j-(c[e>>2]|0)|0)/24|0)>>>0>=2):0){Cb(dc,j+-24|0);b=c[pa>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;e=g+-24|0;c[pa>>2]=e;Ia(e);g=c[pa>>2]|0}g=b+-48|0;Cb(ac,g);b=Ta(ac,0,1939)|0;c[_b>>2]=c[b>>2];c[_b+4>>2]=c[b+4>>2];c[_b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(_b,881)|0;c[$b>>2]=c[b>>2];c[$b+4>>2]=c[b+4>>2];c[$b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[dc>>0]|0;f=(b&1)==0;b=Za($b,f?dc+1|0:c[dc+8>>2]|0,f?(b&255)>>>1:c[dc+4>>2]|0)|0;c[bc>>2]=c[b>>2];c[bc+4>>2]=c[b+4>>2];c[bc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(bc,799)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(ec,cc);Db(g,ec);Ia(ec);Ja(cc);Ja(bc);Ja($b);Ja(_b);Ja(ac);Ja(dc);b=Va}break a}case 109:{dc=b+2|0;$a(da,1957,1);ec=Gb(dc,d,da,e)|0;Ja(da);b=(ec|0)==(dc|0)?b:ec;break a}case 77:{dc=b+2|0;$a(fa,1959,2);ec=Gb(dc,d,fa,e)|0;Ja(fa);b=(ec|0)==(dc|0)?b:ec;break a}case 115:{dc=b+2|0;$a(ha,1962,2);ec=Gb(dc,d,ha,e)|0;Ja(ha);b=(ec|0)==(dc|0)?b:ec;break a}case 83:{dc=b+2|0;$a(ia,1965,3);ec=Gb(dc,d,ia,e)|0;Ja(ia);b=(ec|0)==(dc|0)?b:ec;break a}default:break a}case 115:switch(a[la+1>>0]|0){case 99:{if((((((ja|0)>2?(a[b>>0]|0)==115:0)?(a[b+1>>0]|0)==99:0)?(Zb=b+2|0,l=Na(Zb,d,e)|0,(l|0)!=(Zb|0)):0)?(Wa=ub(l,d,e)|0,(Wa|0)!=(l|0)):0)?(qa=e+4|0,m=c[qa>>2]|0,((m-(c[e>>2]|0)|0)/24|0)>>>0>=2):0){Cb(dc,m+-24|0);b=c[qa>>2]|0;f=b+-24|0;g=b;while(1){if((g|0)==(f|0))break;e=g+-24|0;c[qa>>2]=e;Ia(e);g=c[qa>>2]|0}g=b+-48|0;Cb(ac,g);b=Ta(ac,0,1969)|0;c[_b>>2]=c[b>>2];c[_b+4>>2]=c[b+4>>2];c[_b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(_b,881)|0;c[$b>>2]=c[b>>2];c[$b+4>>2]=c[b+4>>2];c[$b+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=a[dc>>0]|0;f=(b&1)==0;b=Za($b,f?dc+1|0:c[dc+8>>2]|0,f?(b&255)>>>1:c[dc+4>>2]|0)|0;c[bc>>2]=c[b>>2];c[bc+4>>2]=c[b+4>>2];c[bc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(bc,799)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(ec,cc);Db(g,ec);Ia(ec);Ja(cc);Ja(bc);Ja($b);Ja(_b);Ja(ac);Ja(dc);b=Wa}break a}case 112:{if((ja|0)<=2)break a;if((a[b>>0]|0)!=115)break a;if((a[b+1>>0]|0)!=112)break a;dc=b+2|0;ec=ub(dc,d,e)|0;b=(ec|0)==(dc|0)?b:ec;break a}case 114:{b=Jb(b,d,e)|0;break a}case 116:{if(((((ja|0)>2?(a[b>>0]|0)==115:0)?(a[b+1>>0]|0)==116:0)?(ac=b+2|0,Aa=Na(ac,d,e)|0,(Aa|0)!=(ac|0)):0)?(o=c[e+4>>2]|0,(c[e>>2]|0)!=(o|0)):0){g=o+-24|0;Cb(bc,g);b=Ta(bc,0,1982)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(cc,799)|0;c[ec>>2]=c[b>>2];c[ec+4>>2]=c[b+4>>2];c[ec+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(dc,ec);Db(g,dc);Ia(dc);Ja(ec);Ja(cc);Ja(bc);b=Aa}break a}case 122:{if(((((ja|0)>2?(a[b>>0]|0)==115:0)?(a[b+1>>0]|0)==122:0)?(ac=b+2|0,Ba=ub(ac,d,e)|0,(Ba|0)!=(ac|0)):0)?(p=c[e+4>>2]|0,(c[e>>2]|0)!=(p|0)):0){g=p+-24|0;Cb(bc,g);b=Ta(bc,0,1982)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(cc,799)|0;c[ec>>2]=c[b>>2];c[ec+4>>2]=c[b+4>>2];c[ec+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(dc,ec);Db(g,dc);Ia(dc);Ja(ec);Ja(cc);Ja(bc);b=Ba}break a}case 90:{if((lb-la|0)<=2)break a;switch(a[la+2>>0]|0){case 84:break;case 102:{if((((((ja|0)>2?(a[b>>0]|0)==115:0)?(a[b+1>>0]|0)==90:0)?(s=b+2|0,(a[s>>0]|0)==102):0)?(Ca=Fb(s,d,e)|0,(Ca|0)!=(s|0)):0)?(t=c[e+4>>2]|0,(c[e>>2]|0)!=(t|0)):0){g=t+-24|0;Cb(bc,g);b=Ta(bc,0,1991)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(cc,799)|0;c[ec>>2]=c[b>>2];c[ec+4>>2]=c[b+4>>2];c[ec+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(dc,ec);Db(g,dc);Ia(dc);Ja(ec);Ja(cc);Ja(bc);b=Ca}break a}default:break a}if(((((ja|0)>2?(a[b>>0]|0)==115:0)?(a[b+1>>0]|0)==90:0)?(q=b+2|0,(a[q>>0]|0)==84):0)?(mb=e+4|0,eb=((c[mb>>2]|0)-(c[e>>2]|0)|0)/24|0,qb=Eb(q,d,e)|0,ra=c[e>>2]|0,g=((c[mb>>2]|0)-ra|0)/24|0,ra,(qb|0)!=(q|0)):0){a[ec>>0]=20;b=ec+1|0;f=1991;h=b+10|0;do{a[b>>0]=a[f>>0]|0;b=b+1|0;f=f+1|0}while((b|0)<(h|0));a[ec+11>>0]=0;n:do if((eb|0)!=(g|0)){Cb(cc,ra+(eb*24|0)|0);j=a[cc>>0]|0;k=(j&1)==0;Za(ec,k?cc+1|0:c[cc+8>>2]|0,k?(j&255)>>>1:c[cc+4>>2]|0)|0;Ja(cc);j=bc+8|0;k=bc+1|0;l=bc+4|0;b=eb;while(1){b=b+1|0;if((b|0)==(g|0))break n;Cb($b,(c[e>>2]|0)+(b*24|0)|0);f=Ta($b,0,1429)|0;c[bc>>2]=c[f>>2];c[bc+4>>2]=c[f+4>>2];c[bc+8>>2]=c[f+8>>2];h=0;while(1){if((h|0)==3)break;c[f+(h<<2)>>2]=0;h=h+1|0}cc=a[bc>>0]|0;Zb=(cc&1)==0;Za(ec,Zb?k:c[j>>2]|0,Zb?(cc&255)>>>1:c[l>>2]|0)|0;Ja(bc);Ja($b)}}while(0);Ya(ec,799)|0;while(1){if((g|0)==(eb|0))break;f=c[mb>>2]|0;b=f+-24|0;while(1){if((f|0)==(b|0))break;cc=f+-24|0;c[mb>>2]=cc;Ia(cc);f=c[mb>>2]|0}g=g+-1|0}c[ac>>2]=c[ec>>2];c[ac+4>>2]=c[ec+4>>2];c[ac+8>>2]=c[ec+8>>2];b=0;while(1){if((b|0)==3)break;c[ec+(b<<2)>>2]=0;b=b+1|0}rb(_b,ac);b=c[mb>>2]|0;cc=c[e+8>>2]|0;h=cc;if(b>>>0<cc>>>0){db(b,_b);c[mb>>2]=(c[mb>>2]|0)+24}else{f=c[e>>2]|0;cc=b-f|0;j=(cc|0)/24|0;g=j+1|0;if((cc|0)<-24)Pa();b=(h-f|0)/24|0;if(b>>>0<1073741823){b=b<<1;b=b>>>0<g>>>0?g:b}else b=2147483647;ab(dc,b,j,e+12|0);cc=dc+8|0;bc=c[cc>>2]|0;db(bc,_b);c[cc>>2]=bc+24;cb(e,dc);bb(dc)}Ia(_b);Ja(ac);Ja(ec);b=qb}break a}default:break a}case 116:switch(a[la+1>>0]|0){case 105:case 101:{o:do if((ja|0)>2?(a[b>>0]|0)==116:0){f=a[b+1>>0]|0;switch(f<<24>>24){case 105:case 101:break;default:break o}g=b+2|0;if(f<<24>>24==101)h=ub(g,d,e)|0;else h=Na(g,d,e)|0;if((h|0)!=(g|0)?(sa=c[e+4>>2]|0,(c[e>>2]|0)!=(sa|0)):0){g=sa+-24|0;Cb(bc,g);b=Ta(bc,0,2002)|0;c[cc>>2]=c[b>>2];c[cc+4>>2]=c[b+4>>2];c[cc+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(cc,799)|0;c[ec>>2]=c[b>>2];c[ec+4>>2]=c[b+4>>2];c[ec+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(dc,ec);Db(g,dc);Ia(dc);Ja(ec);Ja(cc);Ja(bc);b=h}}while(0);break a}case 114:{ib(Sa,2010);f=e+4|0;g=c[f>>2]|0;ec=c[e+8>>2]|0;h=ec;if(g>>>0<ec>>>0){db(g,Sa);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;ec=g-f|0;j=(ec|0)/24|0;g=j+1|0;if((ec|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(dc,f,j,e+12|0);ec=dc+8|0;cc=c[ec>>2]|0;db(cc,Sa);c[ec>>2]=cc+24;cb(e,dc);bb(dc)}Ia(Sa);b=b+2|0;break a}case 119:{if(((((ja|0)>2?(a[b>>0]|0)==116:0)?(a[b+1>>0]|0)==119:0)?(bc=b+2|0,ta=ub(bc,d,e)|0,(ta|0)!=(bc|0)):0)?(v=c[e+4>>2]|0,(c[e>>2]|0)!=(v|0)):0){b=v+-24|0;Cb(cc,b);f=Ta(cc,0,2016)|0;c[ec>>2]=c[f>>2];c[ec+4>>2]=c[f+4>>2];c[ec+8>>2]=c[f+8>>2];g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}rb(dc,ec);Db(b,dc);Ia(dc);Ja(ec);Ja(cc);b=ta}break a}default:break a}case 57:case 56:case 55:case 54:case 53:case 52:case 51:case 50:case 49:{b=Jb(b,d,e)|0;break a}default:break a}while(0)}while(0);i=fc;return b|0}function vb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0;O=i;i=i+480|0;J=O+72|0;I=O+48|0;H=O+24|0;L=O;K=O+432|0;M=O+408|0;N=O+384|0;r=O+396|0;y=O+360|0;z=O+336|0;s=O+320|0;t=O+308|0;u=O+296|0;v=O+284|0;f=O+272|0;j=O+260|0;k=O+248|0;l=O+236|0;m=O+224|0;n=O+212|0;o=O+200|0;p=O+188|0;q=O+176|0;A=O+152|0;B=O+140|0;C=O+128|0;D=O+116|0;E=O+104|0;F=O+92|0;x=d;a:do if((x-b|0)>3?(a[b>>0]|0)==76:0){w=b+1|0;do switch(a[w>>0]|0){case 84:break a;case 119:{N=b+2|0;$a(r,481,7);e=wb(N,d,r,e)|0;Ja(r);b=(e|0)==(N|0)?b:e;break a}case 98:{if((a[b+3>>0]|0)!=69)break a;switch(a[b+2>>0]|0){case 48:{ib(y,801);f=e+4|0;j=c[f>>2]|0;N=c[e+8>>2]|0;k=N;if(j>>>0<N>>>0){db(j,y);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;N=j-f|0;l=(N|0)/24|0;j=l+1|0;if((N|0)<-24)Pa();f=(k-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(L,f,l,e+12|0);N=L+8|0;M=c[N>>2]|0;db(M,y);c[N>>2]=M+24;cb(e,L);bb(L)}Ia(y);b=b+4|0;break a}case 49:{fb(z,807);f=e+4|0;j=c[f>>2]|0;N=c[e+8>>2]|0;k=N;if(j>>>0<N>>>0){db(j,z);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;N=j-f|0;l=(N|0)/24|0;j=l+1|0;if((N|0)<-24)Pa();f=(k-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(L,f,l,e+12|0);N=L+8|0;M=c[N>>2]|0;db(M,z);c[N>>2]=M+24;cb(e,L);bb(L)}Ia(z);b=b+4|0;break a}default:break a}}case 99:{N=b+2|0;$a(s,494,4);e=wb(N,d,s,e)|0;Ja(s);b=(e|0)==(N|0)?b:e;break a}case 97:{N=b+2|0;$a(t,499,11);e=wb(N,d,t,e)|0;Ja(t);b=(e|0)==(N|0)?b:e;break a}case 104:{N=b+2|0;$a(u,511,13);e=wb(N,d,u,e)|0;Ja(u);b=(e|0)==(N|0)?b:e;break a}case 115:{N=b+2|0;$a(v,525,5);e=wb(N,d,v,e)|0;Ja(v);b=(e|0)==(N|0)?b:e;break a}case 116:{N=b+2|0;$a(f,531,14);e=wb(N,d,f,e)|0;Ja(f);b=(e|0)==(N|0)?b:e;break a}case 105:{N=b+2|0;$a(j,5344,0);e=wb(N,d,j,e)|0;Ja(j);b=(e|0)==(N|0)?b:e;break a}case 106:{N=b+2|0;$a(k,812,1);e=wb(N,d,k,e)|0;Ja(k);b=(e|0)==(N|0)?b:e;break a}case 108:{N=b+2|0;$a(l,814,1);e=wb(N,d,l,e)|0;Ja(l);b=(e|0)==(N|0)?b:e;break a}case 109:{N=b+2|0;$a(m,816,2);e=wb(N,d,m,e)|0;Ja(m);b=(e|0)==(N|0)?b:e;break a}case 120:{N=b+2|0;$a(n,819,2);e=wb(N,d,n,e)|0;Ja(n);b=(e|0)==(N|0)?b:e;break a}case 121:{N=b+2|0;$a(o,822,3);e=wb(N,d,o,e)|0;Ja(o);b=(e|0)==(N|0)?b:e;break a}case 110:{N=b+2|0;$a(p,611,8);e=wb(N,d,p,e)|0;Ja(p);b=(e|0)==(N|0)?b:e;break a}case 111:{N=b+2|0;$a(q,620,17);e=wb(N,d,q,e)|0;Ja(q);b=(e|0)==(N|0)?b:e;break a}case 102:{m=b+2|0;b:do if((x-m|0)>>>0>8){k=b+10|0;f=L;l=m;while(1){j=a[l>>0]|0;if((l|0)==(k|0)){G=41;break}j=j<<24>>24;if(!(fc(j)|0))break;J=a[l+1>>0]|0;a[f>>0]=(((J<<24>>24)+-48|0)>>>0<10?208:169)+(J&255)+(((j+-48|0)>>>0<10?0:9)+j<<4);f=f+1|0;l=l+2|0}do if((G|0)==41){if(j<<24>>24==69){c:do if((L|0)!=(f|0)){j=L;while(1){f=f+-1|0;if(j>>>0>=f>>>0)break c;J=a[j>>0]|0;a[j>>0]=a[f>>0]|0;a[f>>0]=J;j=j+1|0}}while(0);f=K;j=f+24|0;do{a[f>>0]=0;f=f+1|0}while((f|0)<(j|0));h[H>>3]=+g[L>>2];f=jc(K,24,826,H)|0;if(f>>>0>23)break;$a(N,K,f);rb(M,N);f=e+4|0;j=c[f>>2]|0;L=c[e+8>>2]|0;k=L;if(j>>>0<L>>>0){db(j,M);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=j-f|0;l=(L|0)/24|0;j=l+1|0;if((L|0)<-24)Pa();f=(k-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(H,f,l,e+12|0);L=H+8|0;K=c[L>>2]|0;db(K,M);c[L>>2]=K+24;cb(e,H);bb(H)}Ia(M);Ja(N);f=b+11|0}else f=m;break b}while(0);f=m}else f=m;while(0);b=(f|0)==(m|0)?b:f;break a}case 100:{m=b+2|0;d:do if((x-m|0)>>>0>16){k=b+18|0;f=L;l=m;while(1){j=a[l>>0]|0;if((l|0)==(k|0)){G=63;break}j=j<<24>>24;if(!(fc(j)|0))break;J=a[l+1>>0]|0;a[f>>0]=(((J<<24>>24)+-48|0)>>>0<10?208:169)+(J&255)+(((j+-48|0)>>>0<10?0:9)+j<<4);f=f+1|0;l=l+2|0}do if((G|0)==63){if(j<<24>>24==69){e:do if((L|0)!=(f|0)){j=L;while(1){f=f+-1|0;if(j>>>0>=f>>>0)break e;J=a[j>>0]|0;a[j>>0]=a[f>>0]|0;a[f>>0]=J;j=j+1|0}}while(0);f=K;j=f+32|0;do{a[f>>0]=0;f=f+1|0}while((f|0)<(j|0));h[I>>3]=+h[L>>3];f=jc(K,32,830,I)|0;if(f>>>0>31)break;$a(N,K,f);rb(M,N);f=e+4|0;j=c[f>>2]|0;L=c[e+8>>2]|0;k=L;if(j>>>0<L>>>0){db(j,M);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=j-f|0;l=(L|0)/24|0;j=l+1|0;if((L|0)<-24)Pa();f=(k-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(I,f,l,e+12|0);L=I+8|0;K=c[L>>2]|0;db(K,M);c[L>>2]=K+24;cb(e,I);bb(I)}Ia(M);Ja(N);f=b+19|0}else f=m;break d}while(0);f=m}else f=m;while(0);b=(f|0)==(m|0)?b:f;break a}case 101:{m=b+2|0;f:do if((x-m|0)>>>0>20){k=b+22|0;f=L;l=m;while(1){j=a[l>>0]|0;if((l|0)==(k|0)){G=85;break}j=j<<24>>24;if(!(fc(j)|0))break;I=a[l+1>>0]|0;a[f>>0]=(((I<<24>>24)+-48|0)>>>0<10?208:169)+(I&255)+(((j+-48|0)>>>0<10?0:9)+j<<4);f=f+1|0;l=l+2|0}do if((G|0)==85){if(j<<24>>24==69){g:do if((L|0)!=(f|0)){j=L;while(1){f=f+-1|0;if(j>>>0>=f>>>0)break g;I=a[j>>0]|0;a[j>>0]=a[f>>0]|0;a[f>>0]=I;j=j+1|0}}while(0);f=K;j=f+40|0;do{a[f>>0]=0;f=f+1|0}while((f|0)<(j|0));h[J>>3]=+h[L>>3];f=jc(K,40,833,J)|0;if(f>>>0>39)break;$a(N,K,f);rb(M,N);f=e+4|0;j=c[f>>2]|0;L=c[e+8>>2]|0;k=L;if(j>>>0<L>>>0){db(j,M);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=j-f|0;l=(L|0)/24|0;j=l+1|0;if((L|0)<-24)Pa();f=(k-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(J,f,l,e+12|0);L=J+8|0;K=c[L>>2]|0;db(K,M);c[L>>2]=K+24;cb(e,J);bb(J)}Ia(M);Ja(N);f=b+23|0}else f=m;break f}while(0);f=m}else f=m;while(0);b=(f|0)==(m|0)?b:f;break a}case 95:{if((a[b+2>>0]|0)!=90)break a;N=b+3|0;f=Ma(N,d,e)|0;if((f|0)==(N|0)|(f|0)==(d|0))break a;b=(a[f>>0]|0)==69?f+1|0:b;break a}default:{m=Na(w,d,e)|0;if((m|0)==(w|0)|(m|0)==(d|0))break a;if((a[m>>0]|0)==69){b=m+1|0;break a}else n=m;while(1){if((n|0)==(d|0))break a;f=a[n>>0]|0;if(((f<<24>>24)+-48|0)>>>0>=10)break;n=n+1|0}if(!((n|0)!=(m|0)&f<<24>>24==69))break a;f=c[e+4>>2]|0;if((c[e>>2]|0)==(f|0))break a;l=f+-24|0;Cb(E,l);b=Ta(E,0,797)|0;c[D>>2]=c[b>>2];c[D+4>>2]=c[b+4>>2];c[D+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(D,799)|0;c[C>>2]=c[b>>2];c[C+4>>2]=c[b+4>>2];c[C+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}j=n-m|0;if(j>>>0>4294967279)Xa();if(j>>>0<11){a[F>>0]=j<<1;k=F+1|0}else{e=j+16&-16;k=vc(e)|0;c[F+8>>2]=k;c[F>>2]=e|1;c[F+4>>2]=j}b=m;f=k;while(1){if((b|0)==(n|0))break;a[f>>0]=a[b>>0]|0;b=b+1|0;f=f+1|0}a[k+j>>0]=0;b=a[F>>0]|0;f=(b&1)==0;b=Za(C,f?F+1|0:c[F+8>>2]|0,f?(b&255)>>>1:c[F+4>>2]|0)|0;c[B>>2]=c[b>>2];c[B+4>>2]=c[b+4>>2];c[B+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(A,B);Db(l,A);Ia(A);Ja(B);Ja(F);Ja(C);Ja(D);Ja(E);b=n+1|0;break a}}while(0)}while(0);i=O;return b|0}function wb(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0;s=i;i=i+80|0;p=s+48|0;m=s+24|0;n=s+12|0;o=s;r=tb(b,d)|0;if(!((r|0)==(b|0)|(r|0)==(d|0))?(a[r>>0]|0)==69:0){l=a[e>>0]|0;q=e+4|0;do if(((l&1)==0?(l&255)>>>1:c[q>>2]|0)>>>0>3){xb(o,797,e);d=Ya(o,799)|0;c[n>>2]=c[d>>2];c[n+4>>2]=c[d+4>>2];c[n+8>>2]=c[d+8>>2];g=0;while(1){if((g|0)==3)break;c[d+(g<<2)>>2]=0;g=g+1|0}rb(m,n);g=f+4|0;d=c[g>>2]|0;l=c[f+8>>2]|0;h=l;if(d>>>0<l>>>0){db(d,m);c[g>>2]=(c[g>>2]|0)+24}else{j=c[f>>2]|0;d=d-j|0;l=(d|0)/24|0;k=l+1|0;if((d|0)<-24)Pa();d=(h-j|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<k>>>0?k:d}else d=2147483647;ab(p,d,l,f+12|0);l=p+8|0;k=c[l>>2]|0;db(k,m);c[l>>2]=k+24;cb(f,p);bb(p)}Ia(m);Ja(n);Ja(o)}else{k=f+4|0;g=c[k>>2]|0;o=c[f+8>>2]|0;d=o;if(g>>>0<o>>>0){c[g>>2]=0;c[g+4>>2]=0;c[g+8>>2]=0;c[g+12>>2]=0;c[g+16>>2]=0;c[g+20>>2]=0;d=0;while(1){if((d|0)==3)break;c[g+(d<<2)>>2]=0;d=d+1|0}d=g+12|0;g=0;while(1){if((g|0)==3)break;c[d+(g<<2)>>2]=0;g=g+1|0}c[k>>2]=(c[k>>2]|0)+24;g=k;break}h=c[f>>2]|0;o=g-h|0;j=(o|0)/24|0;g=j+1|0;if((o|0)<-24)Pa();d=(d-h|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<g>>>0?g:d}else d=2147483647;ab(p,d,j,f+12|0);h=p+8|0;j=c[h>>2]|0;c[j>>2]=0;c[j+4>>2]=0;c[j+8>>2]=0;c[j+12>>2]=0;c[j+16>>2]=0;c[j+20>>2]=0;d=0;while(1){if((d|0)==3)break;c[j+(d<<2)>>2]=0;d=d+1|0}d=j+12|0;g=0;while(1){if((g|0)==3)break;c[d+(g<<2)>>2]=0;g=g+1|0}c[h>>2]=j+24;cb(f,p);bb(p);g=k}while(0);if((a[b>>0]|0)==110){zb((c[g>>2]|0)+-24|0,45);b=b+1|0}Bb((c[g>>2]|0)+-24|0,b,r);b=a[e>>0]|0;d=(b&1)==0;b=d?(b&255)>>>1:c[q>>2]|0;if(b>>>0<4)Za((c[g>>2]|0)+-24|0,d?e+1|0:c[e+8>>2]|0,b)|0;b=r+1|0}i=s;return b|0}function xb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0;f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}g=bc(d)|0;f=a[e>>0]|0;f=(f&1)==0?(f&255)>>>1:c[e+4>>2]|0;yb(b,d,g,f+g|0);Za(b,(a[e>>0]&1)==0?e+1|0:c[e+8>>2]|0,f)|0;return}function yb(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0;if(f>>>0>4294967279)Xa();if(f>>>0<11){a[b>>0]=e<<1;f=b+1|0}else{g=f+16&-16;f=vc(g)|0;c[b+8>>2]=f;c[b>>2]=g|1;c[b+4>>2]=e}Fc(f|0,d|0,e|0)|0;a[f+e>>0]=0;return}function zb(b,d){b=b|0;d=d|0;var e=0,f=0,g=0,h=0;e=a[b>>0]|0;f=(e&1)!=0;if(f){g=(c[b>>2]&-2)+-1|0;h=c[b+4>>2]|0}else{g=10;h=(e&255)>>>1}if((h|0)==(g|0)){Ab(b,g,1,g,g,0);if(!(a[b>>0]&1))f=7;else f=8}else if(f)f=8;else f=7;if((f|0)==7){a[b>>0]=(h<<1)+2;e=b+1|0}else if((f|0)==8){e=c[b+8>>2]|0;c[b+4>>2]=h+1}b=e+h|0;a[b>>0]=d;a[b+1>>0]=0;return}function Ab(b,d,e,f,g,h){b=b|0;d=d|0;e=e|0;f=f|0;g=g|0;h=h|0;var i=0,j=0;if((-17-d|0)>>>0<e>>>0)Xa();if(!(a[b>>0]&1))j=b+1|0;else j=c[b+8>>2]|0;if(d>>>0<2147483623){e=e+d|0;i=d<<1;e=e>>>0<i>>>0?i:e;e=e>>>0<11?11:e+16&-16}else e=-17;i=vc(e)|0;if(g)Fc(i|0,j|0,g|0)|0;if((f|0)!=(g|0))Fc(i+g+h|0,j+g|0,f-g|0)|0;if((d|0)!=10)wc(j);c[b+8>>2]=i;c[b>>2]=e|1;return}function Bb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,i=0,j=0,k=0;h=d;f=a[b>>0]|0;if(!(f&1)){k=(f&255)>>>1;g=10}else{f=c[b>>2]|0;k=c[b+4>>2]|0;g=(f&-2)+-1|0;f=f&255}j=e-h|0;do if((e|0)!=(d|0)){if((g-k|0)>>>0<j>>>0){Ab(b,g,k+j-g|0,k,k,0);f=a[b>>0]|0}if(!(f&1))i=b+1|0;else i=c[b+8>>2]|0;h=e+(k-h)|0;f=d;g=i+k|0;while(1){if((f|0)==(e|0))break;a[g>>0]=a[f>>0]|0;f=f+1|0;g=g+1|0}a[i+h>>0]=0;f=k+j|0;if(!(a[b>>0]&1)){a[b>>0]=f<<1;break}else{c[b+4>>2]=f;break}}while(0);return}function Cb(b,d){b=b|0;d=d|0;var e=0,f=0,g=0;g=d+12|0;e=a[g>>0]|0;f=(e&1)==0;e=Za(d,f?g+1|0:c[d+20>>2]|0,f?(e&255)>>>1:c[d+16>>2]|0)|0;c[b>>2]=c[e>>2];c[b+4>>2]=c[e+4>>2];c[b+8>>2]=c[e+8>>2];d=0;while(1){if((d|0)==3)break;c[e+(d<<2)>>2]=0;d=d+1|0}return}function Db(b,d){b=b|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0;do if(a[b>>0]&1){m=b+8|0;a[c[m>>2]>>0]=0;k=b+4|0;c[k>>2]=0;e=a[b>>0]|0;if(!(e&1))i=10;else{i=c[b>>2]|0;e=i&255;i=(i&-2)+-1|0}if(!(e&1)){f=(e&255)>>>1;if((e&255)<22){h=10;j=f;l=1}else{h=(f+16&240)+-1|0;j=f;l=1}}else{h=10;j=0;l=0}if((h|0)!=(i|0)){if((h|0)==10){g=b+1|0;f=c[m>>2]|0;if(l){Fc(g|0,f|0,((e&255)>>>1)+1|0)|0;wc(f)}else{a[g>>0]=a[f>>0]|0;wc(f)}a[b>>0]=j<<1;break}f=h+1|0;g=vc(f)|0;if(!(h>>>0<=i>>>0&(g|0)==0)){if(l)Fc(g|0,b+1|0,((e&255)>>>1)+1|0)|0;else{n=c[m>>2]|0;a[g>>0]=a[n>>0]|0;wc(n)}c[b>>2]=f|1;c[k>>2]=j;c[m>>2]=g}}}else{a[b+1>>0]=0;a[b>>0]=0}while(0);c[b>>2]=c[d>>2];c[b+4>>2]=c[d+4>>2];c[b+8>>2]=c[d+8>>2];e=0;while(1){if((e|0)==3)break;c[d+(e<<2)>>2]=0;e=e+1|0}n=b+12|0;d=d+12|0;do if(a[n>>0]&1){m=b+20|0;a[c[m>>2]>>0]=0;j=b+16|0;c[j>>2]=0;e=a[n>>0]|0;if(!(e&1))i=10;else{i=c[n>>2]|0;e=i&255;i=(i&-2)+-1|0}if(!(e&1)){f=(e&255)>>>1;if((e&255)<22){h=10;k=f;l=1}else{h=(f+16&240)+-1|0;k=f;l=1}}else{h=10;k=0;l=0}if((h|0)!=(i|0)){if((h|0)==10){g=n+1|0;f=c[m>>2]|0;if(l){Fc(g|0,f|0,((e&255)>>>1)+1|0)|0;wc(f)}else{a[g>>0]=a[f>>0]|0;wc(f)}a[n>>0]=k<<1;break}f=h+1|0;g=vc(f)|0;if(!(h>>>0<=i>>>0&(g|0)==0)){if(l)Fc(g|0,n+1|0,((e&255)>>>1)+1|0)|0;else{b=c[m>>2]|0;a[g>>0]=a[b>>0]|0;wc(b)}c[n>>2]=f|1;c[j>>2]=k;c[m>>2]=g}}}else{a[n+1>>0]=0;a[n>>0]=0}while(0);c[n>>2]=c[d>>2];c[n+4>>2]=c[d+4>>2];c[n+8>>2]=c[d+8>>2];e=0;while(1){if((e|0)==3)break;c[d+(e<<2)>>2]=0;e=e+1|0}return}function Eb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0;x=i;i=i+96|0;w=x+64|0;k=x+40|0;u=x+16|0;v=x;t=b;a:do if((d-t|0)>1?(a[b>>0]|0)==84:0){r=a[b+1>>0]|0;if(r<<24>>24==95){f=c[e+36>>2]|0;if((c[e+32>>2]|0)==(f|0)){f=b;break}g=c[f+-16>>2]|0;if((g|0)!=(c[f+-12>>2]|0)){m=c[g+4>>2]|0;n=e+4|0;o=e+8|0;p=e+12|0;q=w+8|0;l=c[g>>2]|0;while(1){if((l|0)==(m|0)){f=8;break}f=c[n>>2]|0;k=c[o>>2]|0;g=k;if((f|0)==(k|0)){h=c[e>>2]|0;f=f-h|0;k=(f|0)/24|0;j=k+1|0;if((f|0)<-24){f=12;break}f=(g-h|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(w,f,k,p);k=c[q>>2]|0;_a(k,l);_a(k+12|0,l+12|0);c[q>>2]=k+24;cb(e,w);bb(w)}else{_a(f,l);_a(f+12|0,l+12|0);c[n>>2]=(c[n>>2]|0)+24}l=l+24|0}if((f|0)==8){f=b+2|0;break}else if((f|0)==12)Pa()}else{a[k>>0]=4;f=k+1|0;a[f>>0]=84;a[f+1>>0]=95;a[k+3>>0]=0;f=k+12|0;g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}f=e+4|0;g=c[f>>2]|0;v=c[e+8>>2]|0;h=v;if(g>>>0<v>>>0){db(g,k);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;v=g-f|0;j=(v|0)/24|0;g=j+1|0;if((v|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(w,f,j,e+12|0);v=w+8|0;u=c[v>>2]|0;db(u,k);c[v>>2]=u+24;cb(e,w);bb(w)}Ia(k);a[e+62>>0]=1;f=b+2|0;break}}f=(r<<24>>24)+-48|0;if(f>>>0<10){r=b+2|0;while(1){if((r|0)==(d|0)){f=b;break a}g=a[r>>0]|0;h=(g<<24>>24)+-48|0;if(h>>>0>=10)break;f=h+(f*10|0)|0;r=r+1|0}if(g<<24>>24==95?(s=c[e+36>>2]|0,(c[e+32>>2]|0)!=(s|0)):0){f=f+1|0;d=c[s+-16>>2]|0;g=d;if(f>>>0<(c[s+-12>>2]|0)-d>>4>>>0){m=c[g+(f<<4)+4>>2]|0;n=e+4|0;o=e+8|0;p=e+12|0;q=w+8|0;l=c[g+(f<<4)>>2]|0;while(1){if((l|0)==(m|0)){f=38;break}f=c[n>>2]|0;s=c[o>>2]|0;g=s;if((f|0)==(s|0)){h=c[e>>2]|0;s=f-h|0;k=(s|0)/24|0;j=k+1|0;if((s|0)<-24){f=42;break}f=(g-h|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(w,f,k,p);s=c[q>>2]|0;_a(s,l);_a(s+12|0,l+12|0);c[q>>2]=s+24;cb(e,w);bb(w)}else{_a(f,l);_a(f+12|0,l+12|0);c[n>>2]=(c[n>>2]|0)+24}l=l+24|0}if((f|0)==38){f=r+1|0;break}else if((f|0)==42)Pa()}f=r+1|0;j=f-t|0;if(j>>>0>4294967279)Xa();if(j>>>0<11){a[v>>0]=j<<1;k=v+1|0}else{t=j+16&-16;k=vc(t)|0;c[v+8>>2]=k;c[v>>2]=t|1;c[v+4>>2]=j}g=b;h=k;while(1){if((g|0)==(f|0))break;a[h>>0]=a[g>>0]|0;g=g+1|0;h=h+1|0}a[k+j>>0]=0;rb(u,v);g=e+4|0;h=c[g>>2]|0;b=c[e+8>>2]|0;j=b;if(h>>>0<b>>>0){db(h,u);c[g>>2]=(c[g>>2]|0)+24}else{g=c[e>>2]|0;b=h-g|0;k=(b|0)/24|0;h=k+1|0;if((b|0)<-24)Pa();g=(j-g|0)/24|0;if(g>>>0<1073741823){g=g<<1;g=g>>>0<h>>>0?h:g}else g=2147483647;ab(w,g,k,e+12|0);b=w+8|0;t=c[b>>2]|0;db(t,u);c[b>>2]=t+24;cb(e,w);bb(w)}Ia(u);Ja(v);a[e+62>>0]=1}else f=b}else f=b}else f=b;while(0);i=x;return f|0}function Fb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0;r=i;i=i+128|0;q=r+104|0;g=r+72|0;n=r+80|0;o=r+60|0;p=r+48|0;j=r+24|0;k=r+12|0;l=r;a:do if((d-b|0)>2?(a[b>>0]|0)==102:0){switch(a[b+1>>0]|0){case 112:{f=Oa(b+2|0,d,g)|0;h=tb(f,d)|0;if((h|0)!=(d|0)?(a[h>>0]|0)==95:0){g=h-f|0;if(g>>>0>4294967279)Xa();if(g>>>0<11){a[p>>0]=g<<1;d=p+1|0}else{m=g+16&-16;d=vc(m)|0;c[p+8>>2]=d;c[p>>2]=m|1;c[p+4>>2]=g}b=f;f=d;while(1){if((b|0)==(h|0))break;a[f>>0]=a[b>>0]|0;b=b+1|0;f=f+1|0}a[d+g>>0]=0;b=Ta(p,0,838)|0;c[o>>2]=c[b>>2];c[o+4>>2]=c[b+4>>2];c[o+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(n,o);b=e+4|0;f=c[b>>2]|0;m=c[e+8>>2]|0;g=m;if(f>>>0<m>>>0){db(f,n);c[b>>2]=(c[b>>2]|0)+24}else{b=c[e>>2]|0;m=f-b|0;d=(m|0)/24|0;f=d+1|0;if((m|0)<-24)Pa();b=(g-b|0)/24|0;if(b>>>0<1073741823){b=b<<1;b=b>>>0<f>>>0?f:b}else b=2147483647;ab(q,b,d,e+12|0);m=q+8|0;l=c[m>>2]|0;db(l,n);c[m>>2]=l+24;cb(e,q);bb(q)}Ia(n);Ja(o);Ja(p);b=h+1|0}break a}case 76:break;default:break a}f=tb(b+2|0,d)|0;if((((f|0)!=(d|0)?(a[f>>0]|0)==112:0)?(h=Oa(f+1|0,d,g)|0,m=tb(h,d)|0,(m|0)!=(d|0)):0)?(a[m>>0]|0)==95:0){g=m-h|0;if(g>>>0>4294967279)Xa();if(g>>>0<11){a[l>>0]=g<<1;d=l+1|0}else{p=g+16&-16;d=vc(p)|0;c[l+8>>2]=d;c[l>>2]=p|1;c[l+4>>2]=g}b=h;f=d;while(1){if((b|0)==(m|0))break;a[f>>0]=a[b>>0]|0;b=b+1|0;f=f+1|0}a[d+g>>0]=0;b=Ta(l,0,838)|0;c[k>>2]=c[b>>2];c[k+4>>2]=c[b+4>>2];c[k+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(j,k);b=e+4|0;f=c[b>>2]|0;p=c[e+8>>2]|0;g=p;if(f>>>0<p>>>0){db(f,j);c[b>>2]=(c[b>>2]|0)+24}else{b=c[e>>2]|0;p=f-b|0;d=(p|0)/24|0;f=d+1|0;if((p|0)<-24)Pa();b=(g-b|0)/24|0;if(b>>>0<1073741823){b=b<<1;b=b>>>0<f>>>0?f:b}else b=2147483647;ab(q,b,d,e+12|0);p=q+8|0;o=c[p>>2]|0;db(o,j);c[p>>2]=o+24;cb(e,q);bb(q)}Ia(j);Ja(k);Ja(l);b=m+1|0}}while(0);i=r;return b|0}function Gb(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0;t=i;i=i+96|0;s=t+84|0;r=t+72|0;l=t+60|0;m=t+48|0;n=t+36|0;o=t+24|0;p=t+12|0;q=t;g=ub(b,d,f)|0;a:do if((g|0)!=(b|0)){d=ub(g,d,f)|0;h=f+4|0;if((d|0)==(g|0)){g=c[h>>2]|0;d=g+-24|0;while(1){if((g|0)==(d|0)){d=b;break a}s=g+-24|0;c[h>>2]=s;Ia(s);g=c[h>>2]|0}}g=c[h>>2]|0;if(((g-(c[f>>2]|0)|0)/24|0)>>>0>=2){Cb(s,g+-24|0);g=c[h>>2]|0;f=g+-24|0;b=g;while(1){if((b|0)==(f|0))break;k=b+-24|0;c[h>>2]=k;Ia(k);b=c[h>>2]|0}Cb(r,g+-48|0);g=c[h>>2]|0;k=g+-24|0;if(!(a[k>>0]&1)){a[k+1>>0]=0;a[k>>0]=0}else{a[c[g+-16>>2]>>0]=0;c[g+-20>>2]=0}u=a[e>>0]|0;f=(u&1)==0;b=e+4|0;u=f?(u&255)>>>1:c[b>>2]|0;h=e+8|0;j=e+1|0;g=u>>>0>1;f=ac(f?j:c[h>>2]|0,844,g?1:u)|0;if(!(((f|0)==0?((u|0)==0?-1:g&1):f)|0))zb(k,40);xb(q,797,r);g=Ya(q,846)|0;c[p>>2]=c[g>>2];c[p+4>>2]=c[g+4>>2];c[p+8>>2]=c[g+8>>2];f=0;while(1){if((f|0)==3)break;c[g+(f<<2)>>2]=0;f=f+1|0}g=a[e>>0]|0;f=(g&1)==0;g=Za(p,f?j:c[h>>2]|0,f?(g&255)>>>1:c[b>>2]|0)|0;c[o>>2]=c[g>>2];c[o+4>>2]=c[g+4>>2];c[o+8>>2]=c[g+8>>2];f=0;while(1){if((f|0)==3)break;c[g+(f<<2)>>2]=0;f=f+1|0}g=Ya(o,849)|0;c[n>>2]=c[g>>2];c[n+4>>2]=c[g+4>>2];c[n+8>>2]=c[g+8>>2];f=0;while(1){if((f|0)==3)break;c[g+(f<<2)>>2]=0;f=f+1|0}g=a[s>>0]|0;f=(g&1)==0;g=Za(n,f?s+1|0:c[s+8>>2]|0,f?(g&255)>>>1:c[s+4>>2]|0)|0;c[m>>2]=c[g>>2];c[m+4>>2]=c[g+4>>2];c[m+8>>2]=c[g+8>>2];f=0;while(1){if((f|0)==3)break;c[g+(f<<2)>>2]=0;f=f+1|0}g=Ya(m,799)|0;c[l>>2]=c[g>>2];c[l+4>>2]=c[g+4>>2];c[l+8>>2]=c[g+8>>2];f=0;while(1){if((f|0)==3)break;c[g+(f<<2)>>2]=0;f=f+1|0}u=a[l>>0]|0;f=(u&1)==0;Za(k,f?l+1|0:c[l+8>>2]|0,f?(u&255)>>>1:c[l+4>>2]|0)|0;Ja(l);Ja(m);Ja(n);Ja(o);Ja(p);Ja(q);q=a[e>>0]|0;u=(q&1)==0;q=u?(q&255)>>>1:c[b>>2]|0;e=q>>>0>1;u=ac(u?j:c[h>>2]|0,844,e?1:q)|0;if(!(((u|0)==0?((q|0)==0?-1:e&1):u)|0))zb(k,41);Ja(r);Ja(s)}else d=b}else d=b;while(0);i=t;return d|0}function Hb(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0;s=i;i=i+48|0;o=s+36|0;p=s+24|0;q=s+12|0;r=s;d=ub(b,d,f)|0;if((d|0)!=(b|0)?(g=f+4|0,h=c[g>>2]|0,(c[f>>2]|0)!=(h|0)):0){n=h+-24|0;Ib(q,e,797);Cb(r,(c[g>>2]|0)+-24|0);f=a[r>>0]|0;b=(f&1)==0;f=Za(q,b?r+1|0:c[r+8>>2]|0,b?(f&255)>>>1:c[r+4>>2]|0)|0;c[p>>2]=c[f>>2];c[p+4>>2]=c[f+4>>2];c[p+8>>2]=c[f+8>>2];b=0;while(1){if((b|0)==3)break;c[f+(b<<2)>>2]=0;b=b+1|0}f=Ya(p,799)|0;c[o>>2]=c[f>>2];c[o+4>>2]=c[f+4>>2];c[o+8>>2]=c[f+8>>2];b=0;while(1){if((b|0)==3)break;c[f+(b<<2)>>2]=0;b=b+1|0}do if(a[n>>0]&1){m=h+-16|0;a[c[m>>2]>>0]=0;j=h+-20|0;c[j>>2]=0;f=a[n>>0]|0;if(!(f&1))h=10;else{h=c[n>>2]|0;f=h&255;h=(h&-2)+-1|0}if(!(f&1)){b=(f&255)>>>1;if((f&255)<22){e=10;k=b;l=1}else{e=(b+16&240)+-1|0;k=b;l=1}}else{e=10;k=0;l=0}if((e|0)!=(h|0)){if((e|0)==10){g=n+1|0;b=c[m>>2]|0;if(l){Fc(g|0,b|0,((f&255)>>>1)+1|0)|0;wc(b)}else{a[g>>0]=a[b>>0]|0;wc(b)}a[n>>0]=k<<1;break}b=e+1|0;g=vc(b)|0;if(!(e>>>0<=h>>>0&(g|0)==0)){if(l)Fc(g|0,n+1|0,((f&255)>>>1)+1|0)|0;else{l=c[m>>2]|0;a[g>>0]=a[l>>0]|0;wc(l)}c[n>>2]=b|1;c[j>>2]=k;c[m>>2]=g}}}else{a[n+1>>0]=0;a[n>>0]=0}while(0);c[n>>2]=c[o>>2];c[n+4>>2]=c[o+4>>2];c[n+8>>2]=c[o+8>>2];f=0;while(1){if((f|0)==3)break;c[o+(f<<2)>>2]=0;f=f+1|0}Ja(o);Ja(p);Ja(r);Ja(q)}else d=b;i=s;return d|0}function Ib(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0;f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}g=a[d>>0]|0;h=(g&1)==0;g=h?(g&255)>>>1:c[d+4>>2]|0;f=bc(e)|0;yb(b,h?d+1|0:c[d+8>>2]|0,g,g+f|0);Za(b,e,f)|0;return}function Jb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0;t=i;i=i+80|0;s=t+60|0;p=t+48|0;r=t+36|0;l=t+24|0;o=t+12|0;q=t;g=d;a:do if((g-b|0)>2){if((a[b>>0]|0)==103){h=(a[b+1>>0]|0)==115;j=h;h=h?b+2|0:b}else{j=0;h=b}f=Kb(h,d,e)|0;if((f|0)!=(h|0)){if(!j)break;g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break}Ta(g+-24|0,0,891)|0;break}if(((g-h|0)>2?(a[h>>0]|0)==115:0)?(a[h+1>>0]|0)==114:0){f=h+2|0;if((a[f>>0]|0)==78){q=h+3|0;f=Ob(q,d,e)|0;if((f|0)==(q|0)|(f|0)==(d|0)){f=b;break}j=Mb(f,d,e)|0;o=e+4|0;do if((j|0)==(f|0))n=e;else{f=c[o>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break a}Cb(s,f+-24|0);f=c[o>>2]|0;g=f+-24|0;h=f;while(1){if((h|0)==(g|0))break;q=h+-24|0;c[o>>2]=q;Ia(q);h=c[o>>2]|0}q=a[s>>0]|0;n=(q&1)==0;Za(f+-48|0,n?s+1|0:c[s+8>>2]|0,n?(q&255)>>>1:c[s+4>>2]|0)|0;if((j|0)!=(d|0)){Ja(s);n=e;f=j;break}g=c[o>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break;e=g+-24|0;c[o>>2]=e;Ia(e);g=c[o>>2]|0}Ja(s);f=b;break a}while(0);k=p+8|0;l=p+1|0;m=p+4|0;while(1){if((a[f>>0]|0)==69)break;j=Vb(f,d,e)|0;if((j|0)==(f|0)|(j|0)==(d|0)){f=b;break a}f=c[o>>2]|0;if(((f-(c[n>>2]|0)|0)/24|0)>>>0<2){f=b;break a}Cb(s,f+-24|0);h=c[o>>2]|0;f=h+-24|0;g=h;while(1){if((g|0)==(f|0))break;q=g+-24|0;c[o>>2]=q;Ia(q);g=c[o>>2]|0}f=Ta(s,0,891)|0;c[p>>2]=c[f>>2];c[p+4>>2]=c[f+4>>2];c[p+8>>2]=c[f+8>>2];g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}f=a[p>>0]|0;q=(f&1)==0;Za(h+-48|0,q?l:c[k>>2]|0,q?(f&255)>>>1:c[m>>2]|0)|0;Ja(p);Ja(s);f=j}q=f+1|0;f=Kb(q,d,e)|0;if((f|0)==(q|0)){f=c[o>>2]|0;if((c[e>>2]|0)==(f|0)){f=b;break}g=f+-24|0;while(1){if((f|0)==(g|0)){f=b;break a}s=f+-24|0;c[o>>2]=s;Ia(s);f=c[o>>2]|0}}g=c[o>>2]|0;if(((g-(c[n>>2]|0)|0)/24|0)>>>0<2){f=b;break}Cb(s,g+-24|0);j=c[o>>2]|0;g=j+-24|0;h=j;while(1){if((h|0)==(g|0))break;b=h+-24|0;c[o>>2]=b;Ia(b);h=c[o>>2]|0}g=Ta(s,0,891)|0;c[r>>2]=c[g>>2];c[r+4>>2]=c[g+4>>2];c[r+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}b=a[r>>0]|0;e=(b&1)==0;Za(j+-48|0,e?r+1|0:c[r+8>>2]|0,e?(b&255)>>>1:c[r+4>>2]|0)|0;Ja(r);Ja(s);break}g=Ob(f,d,e)|0;if((g|0)!=(f|0)){k=Mb(g,d,e)|0;if((k|0)!=(g|0)){j=e+4|0;f=c[j>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break}Cb(s,f+-24|0);f=c[j>>2]|0;g=f+-24|0;h=f;while(1){if((h|0)==(g|0))break;r=h+-24|0;c[j>>2]=r;Ia(r);h=c[j>>2]|0}g=a[s>>0]|0;r=(g&1)==0;Za(f+-48|0,r?s+1|0:c[s+8>>2]|0,r?(g&255)>>>1:c[s+4>>2]|0)|0;Ja(s);g=k}f=Kb(g,d,e)|0;if((f|0)==(g|0)){h=e+4|0;f=c[h>>2]|0;if((c[e>>2]|0)==(f|0)){f=b;break}g=f+-24|0;while(1){if((f|0)==(g|0)){f=b;break a}s=f+-24|0;c[h>>2]=s;Ia(s);f=c[h>>2]|0}}k=e+4|0;g=c[k>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break}Cb(s,g+-24|0);j=c[k>>2]|0;g=j+-24|0;h=j;while(1){if((h|0)==(g|0))break;b=h+-24|0;c[k>>2]=b;Ia(b);h=c[k>>2]|0}g=Ta(s,0,891)|0;c[l>>2]=c[g>>2];c[l+4>>2]=c[g+4>>2];c[l+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}b=a[l>>0]|0;e=(b&1)==0;Za(j+-48|0,e?l+1|0:c[l+8>>2]|0,e?(b&255)>>>1:c[l+4>>2]|0)|0;Ja(l);Ja(s);break}h=Vb(f,d,e)|0;if(!((h|0)==(f|0)|(h|0)==(d|0))){if(j){f=e+4|0;g=c[f>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break}Ta(g+-24|0,0,891)|0;n=f}else n=e+4|0;k=o+8|0;l=o+1|0;m=o+4|0;f=h;while(1){if((a[f>>0]|0)==69)break;j=Vb(f,d,e)|0;if((j|0)==(f|0)|(j|0)==(d|0)){f=b;break a}f=c[n>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break a}Cb(s,f+-24|0);h=c[n>>2]|0;f=h+-24|0;g=h;while(1){if((g|0)==(f|0))break;r=g+-24|0;c[n>>2]=r;Ia(r);g=c[n>>2]|0}f=Ta(s,0,891)|0;c[o>>2]=c[f>>2];c[o+4>>2]=c[f+4>>2];c[o+8>>2]=c[f+8>>2];g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}f=a[o>>0]|0;r=(f&1)==0;Za(h+-48|0,r?l:c[k>>2]|0,r?(f&255)>>>1:c[m>>2]|0)|0;Ja(o);Ja(s);f=j}r=f+1|0;f=Kb(r,d,e)|0;if((f|0)==(r|0)){f=c[n>>2]|0;if((c[e>>2]|0)==(f|0)){f=b;break}g=f+-24|0;while(1){if((f|0)==(g|0)){f=b;break a}s=f+-24|0;c[n>>2]=s;Ia(s);f=c[n>>2]|0}}g=c[n>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0>=2){Cb(s,g+-24|0);j=c[n>>2]|0;g=j+-24|0;h=j;while(1){if((h|0)==(g|0))break;b=h+-24|0;c[n>>2]=b;Ia(b);h=c[n>>2]|0}g=Ta(s,0,891)|0;c[q>>2]=c[g>>2];c[q+4>>2]=c[g+4>>2];c[q+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}b=a[q>>0]|0;e=(b&1)==0;Za(j+-48|0,e?q+1|0:c[q+8>>2]|0,e?(b&255)>>>1:c[q+4>>2]|0)|0;Ja(q);Ja(s)}else f=b}else f=b}else f=b}else f=b;while(0);i=t;return f|0}function Kb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0;k=i;i=i+16|0;j=k;a:do if((d-b|0)>1){f=a[b>>0]|0;switch(f<<24>>24){case 100:case 111:{if((a[b+1>>0]|0)==110){h=b+2|0;if(f<<24>>24==111){f=Lb(h,d,e)|0;if((f|0)==(h|0)){f=b;break a}b=Mb(f,d,e)|0;if((b|0)==(f|0))break a;d=e+4|0;f=c[d>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break a}Cb(j,f+-24|0);f=c[d>>2]|0;g=f+-24|0;h=f;while(1){if((h|0)==(g|0))break;e=h+-24|0;c[d>>2]=e;Ia(e);h=c[d>>2]|0}e=a[j>>0]|0;d=(e&1)==0;Za(f+-48|0,d?j+1|0:c[j+8>>2]|0,d?(e&255)>>>1:c[j+4>>2]|0)|0;Ja(j);f=b;break a}else{if((h|0)!=(d|0)){f=Ob(h,d,e)|0;if((f|0)==(h|0))f=Vb(h,d,e)|0;if((f|0)!=(h|0)?(g=c[e+4>>2]|0,(c[e>>2]|0)!=(g|0)):0)Ta(g+-24|0,0,886)|0;else f=h}else f=d;f=(f|0)==(h|0)?b:f;break a}}break}default:{}}f=Vb(b,d,e)|0;if((f|0)==(b|0)){f=Lb(b,d,e)|0;if((f|0)!=(b|0)){b=Mb(f,d,e)|0;if((b|0)!=(f|0)){d=e+4|0;f=c[d>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2)f=b;else{Cb(j,f+-24|0);f=c[d>>2]|0;g=f+-24|0;h=f;while(1){if((h|0)==(g|0))break;e=h+-24|0;c[d>>2]=e;Ia(e);h=c[d>>2]|0}e=a[j>>0]|0;d=(e&1)==0;Za(f+-48|0,d?j+1|0:c[j+8>>2]|0,d?(e&255)>>>1:c[j+4>>2]|0)|0;Ja(j);f=b}}}else f=b}}else f=b;while(0);i=k;return f|0}function Lb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,_=0,$=0,aa=0,ba=0,ca=0;ca=i;i=i+1136|0;ba=ca+1104|0;j=ca+1080|0;k=ca+1056|0;v=ca+1032|0;G=ca+1008|0;R=ca+984|0;Y=ca+960|0;Z=ca+936|0;_=ca+912|0;$=ca+888|0;aa=ca+864|0;l=ca+840|0;m=ca+816|0;n=ca+792|0;o=ca+768|0;p=ca+744|0;q=ca+720|0;r=ca+696|0;s=ca+672|0;t=ca+648|0;u=ca+624|0;w=ca+600|0;x=ca+576|0;y=ca+552|0;z=ca+528|0;A=ca+504|0;B=ca+480|0;C=ca+456|0;D=ca+432|0;E=ca+408|0;F=ca+384|0;H=ca+360|0;I=ca+336|0;J=ca+312|0;K=ca+288|0;L=ca+264|0;M=ca+240|0;N=ca+216|0;O=ca+192|0;P=ca+168|0;Q=ca+144|0;S=ca+120|0;T=ca+96|0;U=ca+72|0;V=ca+48|0;W=ca+24|0;X=ca;a:do if((d-b|0)>1)do switch(a[b>>0]|0){case 97:switch(a[b+1>>0]|0){case 97:{pb(j,926);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,j);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,j);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(j);d=b+2|0;break a}case 110:case 100:{mb(k,937);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,k);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,k);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(k);d=b+2|0;break a}case 78:{pb(v,947);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,v);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,v);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(v);d=b+2|0;break a}case 83:{mb(G,958);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,G);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,G);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(G);d=b+2|0;break a}default:{d=b;break a}}case 99:switch(a[b+1>>0]|0){case 108:{pb(R,968);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,R);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,R);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(R);d=b+2|0;break a}case 109:{mb(Y,979);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,Y);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,Y);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(Y);d=b+2|0;break a}case 111:{mb(Z,989);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,Z);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,Z);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(Z);d=b+2|0;break a}case 118:{aa=e+63|0;$=a[aa>>0]|0;a[aa>>0]=0;ba=b+2|0;d=Na(ba,d,e)|0;a[aa>>0]=$;if((d|0)==(ba|0)){d=b;break a}f=c[e+4>>2]|0;if((c[e>>2]|0)==(f|0)){d=b;break a}Ta(f+-24|0,0,999)|0;a[e+60>>0]=1;break a}default:{d=b;break a}}case 100:switch(a[b+1>>0]|0){case 97:{ob(_,1009);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,_);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,_);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(_);d=b+2|0;break a}case 101:{mb($,1027);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,$);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;_=c[aa>>2]|0;db(_,$);c[aa>>2]=_+24;cb(e,ba);bb(ba)}Ia($);d=b+2|0;break a}case 108:{d=vc(16)|0;c[aa+8>>2]=d;c[aa>>2]=17;c[aa+4>>2]=15;f=d;g=1037;h=f+15|0;do{a[f>>0]=a[g>>0]|0;f=f+1|0;g=g+1|0}while((f|0)<(h|0));a[d+15>>0]=0;d=aa+12|0;f=0;while(1){if((f|0)==3)break;c[d+(f<<2)>>2]=0;f=f+1|0}d=e+4|0;f=c[d>>2]|0;$=c[e+8>>2]|0;g=$;if(f>>>0<$>>>0){db(f,aa);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;$=f-d|0;h=($|0)/24|0;f=h+1|0;if(($|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);$=ba+8|0;_=c[$>>2]|0;db(_,aa);c[$>>2]=_+24;cb(e,ba);bb(ba)}Ia(aa);d=b+2|0;break a}case 118:{mb(l,1053);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,l);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,l);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(l);d=b+2|0;break a}case 86:{pb(m,1063);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,m);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,m);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(m);d=b+2|0;break a}default:{d=b;break a}}case 101:switch(a[b+1>>0]|0){case 111:{mb(n,1074);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,n);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,n);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(n);d=b+2|0;break a}case 79:{pb(o,1084);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,o);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,o);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(o);d=b+2|0;break a}case 113:{pb(p,1095);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,p);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,p);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(p);d=b+2|0;break a}default:{d=b;break a}}case 103:switch(a[b+1>>0]|0){case 101:{pb(q,1106);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,q);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,q);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(q);d=b+2|0;break a}case 116:{mb(r,1117);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,r);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,r);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(r);d=b+2|0;break a}default:{d=b;break a}}case 105:{if((a[b+1>>0]|0)!=120){d=b;break a}pb(s,1127);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,s);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,s);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(s);d=b+2|0;break a}case 108:switch(a[b+1>>0]|0){case 101:{pb(t,1138);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,t);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,t);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(t);d=b+2|0;break a}case 105:{ba=b+2|0;d=qb(ba,d,e)|0;if((d|0)==(ba|0)){d=b;break a}f=c[e+4>>2]|0;if((c[e>>2]|0)==(f|0)){d=b;break a}Ta(f+-24|0,0,1149)|0;break a}case 115:{pb(u,1161);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,u);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,u);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(u);d=b+2|0;break a}case 83:{gb(w,1172);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,w);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,w);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(w);d=b+2|0;break a}case 116:{mb(x,1184);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,x);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,x);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(x);d=b+2|0;break a}default:{d=b;break a}}case 109:switch(a[b+1>>0]|0){case 105:{mb(y,1194);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,y);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,y);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(y);d=b+2|0;break a}case 73:{pb(z,1204);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,z);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,z);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(z);d=b+2|0;break a}case 108:{mb(A,1027);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,A);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,A);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(A);d=b+2|0;break a}case 76:{pb(B,1215);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,B);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,B);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(B);d=b+2|0;break a}case 109:{pb(C,1226);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,C);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,C);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(C);d=b+2|0;break a}default:{d=b;break a}}case 110:switch(a[b+1>>0]|0){case 97:{jb(D,1237);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,D);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,D);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(D);d=b+2|0;break a}case 101:{pb(E,1252);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,E);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,E);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(E);d=b+2|0;break a}case 103:{mb(F,1194);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,F);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,F);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(F);d=b+2|0;break a}case 116:{mb(H,1263);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,H);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,H);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(H);d=b+2|0;break a}case 119:{lb(I,1273);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,I);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,I);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(I);d=b+2|0;break a}default:{d=b;break a}}case 111:switch(a[b+1>>0]|0){case 111:{pb(J,1286);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,J);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,J);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(J);d=b+2|0;break a}case 114:{mb(K,1297);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,K);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,K);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(K);d=b+2|0;break a}case 82:{pb(L,1307);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,L);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,L);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(L);d=b+2|0;break a}default:{d=b;break a}}case 112:switch(a[b+1>>0]|0){case 109:{gb(M,1318);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,M);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,M);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(M);d=b+2|0;break a}case 108:{mb(N,1330);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,N);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,N);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(N);d=b+2|0;break a}case 76:{pb(O,1340);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,O);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,O);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(O);d=b+2|0;break a}case 112:{pb(P,1351);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,P);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,P);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(P);d=b+2|0;break a}case 115:{mb(Q,1330);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,Q);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,Q);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(Q);d=b+2|0;break a}case 116:{pb(S,1362);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,S);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,S);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(S);d=b+2|0;break a}default:{d=b;break a}}case 113:{if((a[b+1>>0]|0)!=117){d=b;break a}mb(T,1373);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,T);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,T);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(T);d=b+2|0;break a}case 114:switch(a[b+1>>0]|0){case 109:{mb(U,1383);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,U);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,U);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(U);d=b+2|0;break a}case 77:{pb(V,1393);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,V);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,V);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(V);d=b+2|0;break a}case 115:{pb(W,1404);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,W);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,W);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(W);d=b+2|0;break a}case 83:{gb(X,1415);d=e+4|0;f=c[d>>2]|0;aa=c[e+8>>2]|0;g=aa;if(f>>>0<aa>>>0){db(f,X);c[d>>2]=(c[d>>2]|0)+24}else{d=c[e>>2]|0;aa=f-d|0;h=(aa|0)/24|0;f=h+1|0;if((aa|0)<-24)Pa();d=(g-d|0)/24|0;if(d>>>0<1073741823){d=d<<1;d=d>>>0<f>>>0?f:d}else d=2147483647;ab(ba,d,h,e+12|0);aa=ba+8|0;$=c[aa>>2]|0;db($,X);c[aa>>2]=$+24;cb(e,ba);bb(ba)}Ia(X);d=b+2|0;break a}default:{d=b;break a}}case 118:{if(((a[b+1>>0]|0)+-48|0)>>>0>=10){d=b;break a}ba=b+2|0;d=qb(ba,d,e)|0;if((d|0)==(ba|0)){d=b;break a}f=c[e+4>>2]|0;if((c[e>>2]|0)==(f|0)){d=b;break a}Ta(f+-24|0,0,999)|0;break a}default:{d=b;break a}}while(0);else d=b;while(0);i=ca;return d|0}function Mb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0;M=i;i=i+80|0;K=M+60|0;L=M;F=M+48|0;I=M+24|0;J=M+12|0;do if((d-b|0)>1?(a[b>>0]|0)==73:0){G=e+61|0;E=e+36|0;a:do if(a[G>>0]|0){g=c[E>>2]|0;f=c[g+-16>>2]|0;g=g+-12|0;while(1){h=c[g>>2]|0;if((h|0)==(f|0))break a;H=h+-16|0;c[g>>2]=H;Ha(H)}}while(0);$a(L,1427,1);H=e+4|0;t=e+12|0;u=K+8|0;v=K+8|0;D=L+4|0;w=F+8|0;x=F+1|0;y=F+4|0;z=e+32|0;A=e+40|0;B=e+44|0;C=K+8|0;n=b+1|0;b:while(1){if((a[n>>0]|0)==69){f=48;break}do if(a[G>>0]|0){m=c[t>>2]|0;f=c[E>>2]|0;g=c[A>>2]|0;if(f>>>0<g>>>0){c[f>>2]=0;c[f+4>>2]=0;c[f+8>>2]=0;c[f+12>>2]=m;c[E>>2]=(c[E>>2]|0)+16;break}h=c[z>>2]|0;s=f-h|0;k=s>>4;j=k+1|0;if((s|0)<-16){f=13;break b}f=g-h|0;if(f>>4>>>0<1073741823){f=f>>3;f=f>>>0<j>>>0?j:f}else f=2147483647;Ca(K,f,k,B);s=c[C>>2]|0;c[s>>2]=0;c[s+4>>2]=0;c[s+8>>2]=0;c[s+12>>2]=m;c[C>>2]=s+16;Ea(z,K);Fa(K)}while(0);r=((c[H>>2]|0)-(c[e>>2]|0)|0)/24|0;s=Nb(n,d,e)|0;h=((c[H>>2]|0)-(c[e>>2]|0)|0)/24|0;c:do if(a[G>>0]|0){g=c[E>>2]|0;f=g+-16|0;while(1){if((g|0)==(f|0))break c;q=g+-16|0;c[E>>2]=q;Ga(q);g=c[E>>2]|0}}while(0);if((s|0)==(n|0)|(s|0)==(d|0)){f=62;break}d:do if(!(a[G>>0]|0))f=r;else{m=c[E>>2]|0;n=m+-16|0;o=c[t>>2]|0;f=m+-12|0;g=c[f>>2]|0;q=c[m+-8>>2]|0;k=q;if(g>>>0<q>>>0){c[g>>2]=0;c[g+4>>2]=0;c[g+8>>2]=0;c[g+12>>2]=o;c[f>>2]=(c[f>>2]|0)+16;q=r}else{f=c[n>>2]|0;q=g-f|0;j=q>>4;g=j+1|0;if((q|0)<-16){f=26;break b}f=k-f|0;if(f>>4>>>0<1073741823){f=f>>3;f=f>>>0<g>>>0?g:f}else f=2147483647;Qa(K,f,j,m+-4|0);q=c[u>>2]|0;c[q>>2]=0;c[q+4>>2]=0;c[q+8>>2]=0;c[q+12>>2]=o;c[u>>2]=q+16;Ra(n,K);Sa(K);q=r}while(1){if(q>>>0>=h>>>0){f=r;break d}m=c[(c[E>>2]|0)+-12>>2]|0;n=m+-16|0;o=c[e>>2]|0;p=o+(q*24|0)|0;f=m+-12|0;g=c[f>>2]|0;k=c[m+-8>>2]|0;j=k;if((g|0)==(k|0)){f=c[n>>2]|0;N=g-f|0;k=(N|0)/24|0;g=k+1|0;if((N|0)<-24){f=34;break b}f=(j-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(K,f,k,m+-4|0);N=c[v>>2]|0;_a(N,p);_a(N+12|0,o+(q*24|0)+12|0);c[v>>2]=N+24;cb(n,K);bb(K)}else{_a(g,p);_a(g+12|0,o+(q*24|0)+12|0);c[f>>2]=(c[f>>2]|0)+24}q=q+1|0}}while(0);while(1){if(f>>>0>=h>>>0)break;N=a[L>>0]|0;if(((N&1)==0?(N&255)>>>1:c[D>>2]|0)>>>0>1)Ya(L,1429)|0;Cb(F,(c[e>>2]|0)+(f*24|0)|0);N=a[F>>0]|0;q=(N&1)==0;Za(L,q?x:c[w>>2]|0,q?(N&255)>>>1:c[y>>2]|0)|0;Ja(F);f=f+1|0}while(1){if((h|0)==(r|0)){n=s;continue b}g=c[H>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break;N=g+-24|0;c[H>>2]=N;Ia(N);g=c[H>>2]|0}h=h+-1|0}}if((f|0)==13)Pa();else if((f|0)==26)Pa();else if((f|0)==34)Pa();else if((f|0)==48){l=n+1|0;N=a[L>>0]|0;b=(N&1)==0;if((a[(b?L+1|0:c[L+8>>2]|0)+(b?(N&255)>>>1:c[D>>2]|0)+-1>>0]|0)==62)Ya(L,1432)|0;else Ya(L,844)|0;c[J>>2]=c[L>>2];c[J+4>>2]=c[L+4>>2];c[J+8>>2]=c[L+8>>2];f=0;while(1){if((f|0)==3)break;c[L+(f<<2)>>2]=0;f=f+1|0}rb(I,J);f=c[H>>2]|0;N=c[e+8>>2]|0;j=N;if(f>>>0<N>>>0){db(f,I);c[H>>2]=(c[H>>2]|0)+24}else{g=c[e>>2]|0;N=f-g|0;k=(N|0)/24|0;h=k+1|0;if((N|0)<-24)Pa();f=(j-g|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<h>>>0?h:f}else f=2147483647;ab(K,f,k,e+12|0);N=K+8|0;H=c[N>>2]|0;db(H,I);c[N>>2]=H+24;cb(e,K);bb(K)}Ia(I);Ja(J);Ja(L);break}else if((f|0)==62){Ja(L);l=b;break}}else l=b;while(0);i=M;return l|0}function Nb(b,c,d){b=b|0;c=c|0;d=d|0;var e=0,f=0;a:do if((b|0)!=(c|0))switch(a[b>>0]|0){case 88:{f=b+1|0;e=ub(f,c,d)|0;if((e|0)==(f|0)|(e|0)==(c|0))break a;b=(a[e>>0]|0)==69?e+1|0:b;break a}case 74:{e=b+1|0;if((e|0)==(c|0))break a;while(1){if((a[e>>0]|0)==69)break;f=Nb(e,c,d)|0;if((f|0)==(e|0))break a;else e=f}b=e+1|0;break a}case 76:{f=b+1|0;if((f|0)!=(c|0)?(a[f>>0]|0)==90:0){f=b+2|0;e=Ma(f,c,d)|0;if((e|0)==(f|0)|(e|0)==(c|0))break a;b=(a[e>>0]|0)==69?e+1|0:b;break a}b=vb(b,c,d)|0;break a}default:{b=Na(b,c,d)|0;break a}}while(0);return b|0}function Ob(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0;p=i;i=i+96|0;o=p+72|0;n=p+56|0;k=p+48|0;l=p+32|0;g=p+24|0;m=p+8|0;h=p;a:do if((b|0)==(d|0))f=b;else switch(a[b>>0]|0){case 84:{j=e+4|0;h=((c[j>>2]|0)-(c[e>>2]|0)|0)/24|0;f=Eb(b,d,e)|0;d=c[j>>2]|0;g=(d-(c[e>>2]|0)|0)/24|0;if(!((f|0)!=(b|0)&(g|0)==(h+1|0))){f=d;while(1){if((g|0)==(h|0)){f=b;break a}d=f+-24|0;while(1){if((f|0)==(d|0))break;e=f+-24|0;c[j>>2]=e;Ia(e);f=c[j>>2]|0}f=d;g=g+-1|0}}b=e+16|0;c[k>>2]=c[e+12>>2];Pb(n,d+-24|0,k);d=e+20|0;g=c[d>>2]|0;m=c[e+24>>2]|0;h=m;if(g>>>0<m>>>0){c[g+12>>2]=c[n+12>>2];c[g>>2]=c[n>>2];e=n+4|0;c[g+4>>2]=c[e>>2];o=n+8|0;c[g+8>>2]=c[o>>2];c[o>>2]=0;c[e>>2]=0;c[n>>2]=0;c[d>>2]=(c[d>>2]|0)+16}else{d=c[b>>2]|0;m=g-d|0;j=m>>4;g=j+1|0;if((m|0)<-16)Pa();d=h-d|0;if(d>>4>>>0<1073741823){d=d>>3;d=d>>>0<g>>>0?g:d}else d=2147483647;Qa(o,d,j,e+28|0);e=o+8|0;m=c[e>>2]|0;c[m+12>>2]=c[n+12>>2];c[m>>2]=c[n>>2];l=n+4|0;c[m+4>>2]=c[l>>2];k=n+8|0;c[m+8>>2]=c[k>>2];c[k>>2]=0;c[l>>2]=0;c[n>>2]=0;c[e>>2]=m+16;Ra(b,o);Sa(o)}Ha(n);break a}case 68:{f=Qb(b,d,e)|0;if((f|0)==(b|0)){f=b;break a}d=c[e+4>>2]|0;if((c[e>>2]|0)==(d|0)){f=b;break a}b=e+16|0;c[g>>2]=c[e+12>>2];Pb(l,d+-24|0,g);d=e+20|0;g=c[d>>2]|0;n=c[e+24>>2]|0;j=n;if(g>>>0<n>>>0){c[g+12>>2]=c[l+12>>2];c[g>>2]=c[l>>2];e=l+4|0;c[g+4>>2]=c[e>>2];o=l+8|0;c[g+8>>2]=c[o>>2];c[o>>2]=0;c[e>>2]=0;c[l>>2]=0;c[d>>2]=(c[d>>2]|0)+16}else{d=c[b>>2]|0;n=g-d|0;h=n>>4;g=h+1|0;if((n|0)<-16)Pa();d=j-d|0;if(d>>4>>>0<1073741823){d=d>>3;d=d>>>0<g>>>0?g:d}else d=2147483647;Qa(o,d,h,e+28|0);e=o+8|0;n=c[e>>2]|0;c[n+12>>2]=c[l+12>>2];c[n>>2]=c[l>>2];m=l+4|0;c[n+4>>2]=c[m>>2];k=l+8|0;c[n+8>>2]=c[k>>2];c[k>>2]=0;c[m>>2]=0;c[l>>2]=0;c[e>>2]=n+16;Ra(b,o);Sa(o)}Ha(l);break a}case 83:{f=Rb(b,d,e)|0;if((f|0)!=(b|0))break a;if((d-b|0)<=2){f=b;break a}if((a[b+1>>0]|0)!=116){f=b;break a}n=b+2|0;f=Sb(n,d,e)|0;if((f|0)==(n|0)){f=b;break a}g=e+4|0;d=c[g>>2]|0;if((c[e>>2]|0)==(d|0)){f=b;break a}Ta(d+-24|0,0,1827)|0;b=e+16|0;d=(c[g>>2]|0)+-24|0;c[h>>2]=c[e+12>>2];Pb(m,d,h);d=e+20|0;g=c[d>>2]|0;n=c[e+24>>2]|0;h=n;if(g>>>0<n>>>0){c[g+12>>2]=c[m+12>>2];c[g>>2]=c[m>>2];e=m+4|0;c[g+4>>2]=c[e>>2];o=m+8|0;c[g+8>>2]=c[o>>2];c[o>>2]=0;c[e>>2]=0;c[m>>2]=0;c[d>>2]=(c[d>>2]|0)+16}else{d=c[b>>2]|0;n=g-d|0;j=n>>4;g=j+1|0;if((n|0)<-16)Pa();d=h-d|0;if(d>>4>>>0<1073741823){d=d>>3;d=d>>>0<g>>>0?g:d}else d=2147483647;Qa(o,d,j,e+28|0);e=o+8|0;n=c[e>>2]|0;c[n+12>>2]=c[m+12>>2];c[n>>2]=c[m>>2];l=m+4|0;c[n+4>>2]=c[l>>2];k=m+8|0;c[n+8>>2]=c[k>>2];c[k>>2]=0;c[l>>2]=0;c[m>>2]=0;c[e>>2]=n+16;Ra(b,o);Sa(o)}Ha(m);break a}default:{f=b;break a}}while(0);i=p;return f|0}function Pb(a,b,d){a=a|0;b=b|0;d=d|0;var e=0;c[a>>2]=0;e=a+4|0;c[e>>2]=0;d=c[d>>2]|0;c[a+8>>2]=0;c[a+12>>2]=d;d=Da(d,24)|0;c[e>>2]=d;c[a>>2]=d;c[a+8>>2]=d+24;_a(d,b);_a(d+12|0,b+12|0);c[e>>2]=(c[e>>2]|0)+24;return}function Qb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0;m=i;i=i+64|0;g=m+40|0;h=m+24|0;k=m+12|0;l=m;a:do if((d-b|0)>3?(a[b>>0]|0)==68:0){switch(a[b+1>>0]|0){case 84:case 116:break;default:break a}n=b+2|0;j=ub(n,d,e)|0;if((!((j|0)==(n|0)|(j|0)==(d|0))?(a[j>>0]|0)==69:0)?(f=c[e+4>>2]|0,(c[e>>2]|0)!=(f|0)):0){e=f+-24|0;Cb(l,e);b=Ta(l,0,1435)|0;c[k>>2]=c[b>>2];c[k+4>>2]=c[b+4>>2];c[k+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=Ya(k,799)|0;c[h>>2]=c[b>>2];c[h+4>>2]=c[b+4>>2];c[h+8>>2]=c[b+8>>2];f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}rb(g,h);Db(e,g);Ia(g);Ja(h);Ja(k);Ja(l);b=j+1|0}}while(0);i=m;return b|0}function Rb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0;t=i;i=i+176|0;s=t+144|0;k=t+120|0;l=t+96|0;m=t+72|0;n=t+48|0;o=t+24|0;p=t;a:do if((d-b|0)>1?(a[b>>0]|0)==83:0){h=a[b+1>>0]|0;switch(h|0){case 97:{jb(k,1445);f=e+4|0;g=c[f>>2]|0;r=c[e+8>>2]|0;h=r;if(g>>>0<r>>>0){db(g,k);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;r=g-f|0;j=(r|0)/24|0;g=j+1|0;if((r|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(s,f,j,e+12|0);r=s+8|0;q=c[r>>2]|0;db(q,k);c[r>>2]=q+24;cb(e,s);bb(s)}Ia(k);r=b+2|0;break a}case 98:{ob(l,1460);f=e+4|0;g=c[f>>2]|0;r=c[e+8>>2]|0;h=r;if(g>>>0<r>>>0){db(g,l);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;r=g-f|0;j=(r|0)/24|0;g=j+1|0;if((r|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(s,f,j,e+12|0);r=s+8|0;q=c[r>>2]|0;db(q,l);c[r>>2]=q+24;cb(e,s);bb(s)}Ia(l);r=b+2|0;break a}case 115:{gb(m,1478);f=e+4|0;g=c[f>>2]|0;r=c[e+8>>2]|0;h=r;if(g>>>0<r>>>0){db(g,m);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;r=g-f|0;j=(r|0)/24|0;g=j+1|0;if((r|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(s,f,j,e+12|0);r=s+8|0;q=c[r>>2]|0;db(q,m);c[r>>2]=q+24;cb(e,s);bb(s)}Ia(m);r=b+2|0;break a}case 105:{lb(n,1490);f=e+4|0;g=c[f>>2]|0;r=c[e+8>>2]|0;j=r;if(g>>>0<r>>>0){db(g,n);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;r=g-f|0;h=(r|0)/24|0;g=h+1|0;if((r|0)<-24)Pa();f=(j-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(s,f,h,e+12|0);r=s+8|0;q=c[r>>2]|0;db(q,n);c[r>>2]=q+24;cb(e,s);bb(s)}Ia(n);r=b+2|0;break a}case 111:{lb(o,1503);f=e+4|0;g=c[f>>2]|0;r=c[e+8>>2]|0;h=r;if(g>>>0<r>>>0){db(g,o);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;r=g-f|0;j=(r|0)/24|0;g=j+1|0;if((r|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(s,f,j,e+12|0);r=s+8|0;q=c[r>>2]|0;db(q,o);c[r>>2]=q+24;cb(e,s);bb(s)}Ia(o);r=b+2|0;break a}case 100:{hb(p,1516);f=e+4|0;g=c[f>>2]|0;r=c[e+8>>2]|0;h=r;if(g>>>0<r>>>0){db(g,p);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;r=g-f|0;j=(r|0)/24|0;g=j+1|0;if((r|0)<-24)Pa();f=(h-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<g>>>0?g:f}else f=2147483647;ab(s,f,j,e+12|0);r=s+8|0;q=c[r>>2]|0;db(q,p);c[r>>2]=q+24;cb(e,s);bb(s)}Ia(p);r=b+2|0;break a}case 95:{f=c[e+16>>2]|0;if((f|0)==(c[e+20>>2]|0)){r=b;break a}m=c[f+4>>2]|0;n=e+4|0;o=e+8|0;p=e+12|0;d=s+8|0;l=c[f>>2]|0;while(1){if((l|0)==(m|0)){f=55;break}f=c[n>>2]|0;q=c[o>>2]|0;g=q;if((f|0)==(q|0)){h=c[e>>2]|0;q=f-h|0;k=(q|0)/24|0;j=k+1|0;if((q|0)<-24){f=59;break}f=(g-h|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(s,f,k,p);q=c[d>>2]|0;_a(q,l);_a(q+12|0,l+12|0);c[d>>2]=q+24;cb(e,s);bb(s)}else{_a(f,l);_a(f+12|0,l+12|0);c[n>>2]=(c[n>>2]|0)+24}l=l+24|0}if((f|0)==55){r=b+2|0;break a}else if((f|0)==59)Pa();break}default:{g=h+-48|0;f=g>>>0<10;if(!f?(gc(h)|0)==0:0){r=b;break a}k=f?g:h+-55|0;q=b+2|0;while(1){if((q|0)==(d|0)){r=b;break a}f=a[q>>0]|0;g=f<<24>>24;j=g+-48|0;h=j>>>0<10;if(!h?(gc(g)|0)==0:0)break;k=(h?j:g+-55|0)+(k*36|0)|0;q=q+1|0}if(f<<24>>24!=95){r=b;break a}f=k+1|0;d=c[e+16>>2]|0;g=d;if(f>>>0>=(c[e+20>>2]|0)-d>>4>>>0){r=b;break a}m=c[g+(f<<4)+4>>2]|0;n=e+4|0;o=e+8|0;p=e+12|0;d=s+8|0;l=c[g+(f<<4)>>2]|0;while(1){if((l|0)==(m|0)){f=75;break}f=c[n>>2]|0;b=c[o>>2]|0;g=b;if((f|0)==(b|0)){h=c[e>>2]|0;b=f-h|0;k=(b|0)/24|0;j=k+1|0;if((b|0)<-24){f=79;break}f=(g-h|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(s,f,k,p);b=c[d>>2]|0;_a(b,l);_a(b+12|0,l+12|0);c[d>>2]=b+24;cb(e,s);bb(s)}else{_a(f,l);_a(f+12|0,l+12|0);c[n>>2]=(c[n>>2]|0)+24}l=l+24|0}if((f|0)==75){r=q+1|0;break a}else if((f|0)==79)Pa()}}}else r=b;while(0);i=t;return r|0}
+function sa(a){a=a|0;var b=0;b=i;i=i+a|0;i=i+15&-16;return b|0}function ta(){return i|0}function ua(a){a=a|0;i=a}function va(a,b){a=a|0;b=b|0;i=a;j=b}function wa(a,b){a=a|0;b=b|0;if(!n){n=a;o=b}}function xa(b){b=b|0;a[k>>0]=a[b>>0];a[k+1>>0]=a[b+1>>0];a[k+2>>0]=a[b+2>>0];a[k+3>>0]=a[b+3>>0]}function ya(b){b=b|0;a[k>>0]=a[b>>0];a[k+1>>0]=a[b+1>>0];a[k+2>>0]=a[b+2>>0];a[k+3>>0]=a[b+3>>0];a[k+4>>0]=a[b+4>>0];a[k+5>>0]=a[b+5>>0];a[k+6>>0]=a[b+6>>0];a[k+7>>0]=a[b+7>>0]}function za(a){a=a|0;C=a}function Aa(){return C|0}function Ba(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0;v=i;i=i+4208|0;t=v+4176|0;h=v;u=v+4112|0;if((b|0)!=0?(g=(d|0)!=0,s=(e|0)==0,!(g&s)):0){if(g)q=c[e>>2]|0;else q=0;c[h+4096>>2]=h;g=h;c[u>>2]=0;r=u+4|0;c[r>>2]=0;c[u+8>>2]=0;c[u+12>>2]=g;l=u+16|0;c[l>>2]=0;m=u+20|0;c[m>>2]=0;c[u+24>>2]=0;c[u+28>>2]=g;c[u+32>>2]=0;h=u+36|0;c[h>>2]=0;c[u+40>>2]=0;n=u+44|0;c[n>>2]=g;k=u+48|0;j=u+61|0;c[k>>2]=0;c[k+4>>2]=0;c[k+8>>2]=0;a[k+12>>0]=0;a[j>>0]=1;k=u+32|0;Ca(t,1,0,n);n=t+8|0;o=c[n>>2]|0;c[o>>2]=0;c[o+4>>2]=0;c[o+8>>2]=0;c[o+12>>2]=g;c[n>>2]=o+16;Ea(k,t);Fa(t);n=u+62|0;a[n>>0]=0;a[u+63>>0]=1;c[t>>2]=0;o=b+(bc(b)|0)|0;La(b,o,u,t);g=c[t>>2]|0;do if(!((g|0)!=0|(a[n>>0]|0)==0)){k=c[k>>2]|0;if((k|0)!=(c[h>>2]|0)?(c[k>>2]|0)!=(c[k+4>>2]|0):0){a[n>>0]=0;a[j>>0]=0;g=c[u>>2]|0;while(1){h=c[r>>2]|0;if((h|0)==(g|0))break;k=h+-24|0;c[r>>2]=k;Ia(k)}g=c[l>>2]|0;while(1){h=c[m>>2]|0;if((h|0)==(g|0))break;l=h+-16|0;c[m>>2]=l;Ha(l)}La(b,o,u,t);if(!(a[n>>0]|0)){g=c[t>>2]|0;p=19;break}else{c[t>>2]=-2;d=0;g=-2;break}}else p=20}else p=19;while(0);if((p|0)==19)if(!g)p=20;else d=0;do if((p|0)==20){h=c[r>>2]|0;g=a[h+-24>>0]|0;if(!(g&1))j=(g&255)>>>1;else j=c[h+-20>>2]|0;g=a[h+-12>>0]|0;if(!(g&1))g=(g&255)>>>1;else g=c[h+-8>>2]|0;j=g+j|0;g=j+1|0;if(g>>>0>q>>>0){d=xc(d,g)|0;if(!d){c[t>>2]=-1;d=0;g=-1;break}if(!s)c[e>>2]=g}else if(!d){d=0;g=0;break}g=c[r>>2]|0;t=g+-12|0;h=a[t>>0]|0;e=(h&1)==0;Za(g+-24|0,e?t+1|0:c[g+-4>>2]|0,e?(h&255)>>>1:c[g+-8>>2]|0)|0;g=c[r>>2]|0;h=g+-24|0;if(!(a[h>>0]&1))g=h+1|0;else g=c[g+-16>>2]|0;Fc(d|0,g|0,j|0)|0;a[d+j>>0]=0;g=0}while(0);if(f)c[f>>2]=g;_b(u)}else if(!f)d=0;else{c[f>>2]=-3;d=0}i=v;return d|0}function Ca(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;c[a+12>>2]=0;c[a+16>>2]=e;if(!b)e=0;else e=Da(c[e>>2]|0,b<<4)|0;c[a>>2]=e;d=e+(d<<4)|0;c[a+8>>2]=d;c[a+4>>2]=d;c[a+12>>2]=e+(b<<4);return}function Da(a,b){a=a|0;b=b|0;var d=0,e=0;d=b+15&-16;e=a+4096|0;b=c[e>>2]|0;if((a+4096-b|0)>>>0<d>>>0)b=vc(d)|0;else c[e>>2]=b+d;return b|0}function Ea(a,b){a=a|0;b=b|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;e=c[a>>2]|0;f=a+4|0;g=b+4|0;d=c[f>>2]|0;while(1){if((d|0)==(e|0))break;k=c[g>>2]|0;i=k+-16|0;h=d+-16|0;c[i>>2]=0;j=k+-12|0;c[j>>2]=0;l=c[d+-4>>2]|0;c[k+-8>>2]=0;c[k+-4>>2]=l;c[i>>2]=c[h>>2];i=d+-12|0;c[j>>2]=c[i>>2];j=d+-8|0;c[k+-8>>2]=c[j>>2];c[j>>2]=0;c[i>>2]=0;c[h>>2]=0;c[g>>2]=(c[g>>2]|0)+-16;d=h}j=c[a>>2]|0;c[a>>2]=c[g>>2];c[g>>2]=j;j=b+8|0;l=c[f>>2]|0;c[f>>2]=c[j>>2];c[j>>2]=l;j=a+8|0;l=b+12|0;k=c[j>>2]|0;c[j>>2]=c[l>>2];c[l>>2]=k;c[b>>2]=c[g>>2];return}function Fa(a){a=a|0;var b=0,d=0,e=0;b=c[a+4>>2]|0;d=a+8|0;while(1){e=c[d>>2]|0;if((e|0)==(b|0))break;e=e+-16|0;c[d>>2]=e;Ga(e)}b=c[a>>2]|0;if(b)Ka(c[c[a+16>>2]>>2]|0,b,(c[a+12>>2]|0)-b|0);return}function Ga(a){a=a|0;var b=0,d=0,e=0;b=c[a>>2]|0;if(b){d=a+4|0;while(1){e=c[d>>2]|0;if((e|0)==(b|0))break;e=e+-16|0;c[d>>2]=e;Ha(e)}e=c[a>>2]|0;Ka(c[a+12>>2]|0,e,(c[a+8>>2]|0)-e|0)}return}function Ha(a){a=a|0;var b=0,d=0,e=0;b=c[a>>2]|0;if(b){d=a+4|0;while(1){e=c[d>>2]|0;if((e|0)==(b|0))break;e=e+-24|0;c[d>>2]=e;Ia(e)}e=c[a>>2]|0;Ka(c[a+12>>2]|0,e,(c[a+8>>2]|0)-e|0)}return}function Ia(a){a=a|0;Ja(a+12|0);Ja(a);return}function Ja(b){b=b|0;if(a[b>>0]&1)wc(c[b+8>>2]|0);return}function Ka(a,b,d){a=a|0;b=b|0;d=d|0;if(a>>>0<=b>>>0&(a+4096|0)>>>0>=b>>>0){a=a+4096|0;if((b+(d+15&-16)|0)==(c[a>>2]|0))c[a>>2]=b}else wc(b);return}function La(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0;o=i;i=i+48|0;l=o+24|0;m=o+12|0;n=o;a:do if(b>>>0<d>>>0){b:do if((a[b>>0]|0)!=95){if((Na(b,d,e)|0)!=(d|0)){c[f>>2]=-2;break a}}else{h=d;if((h-b|0)<=3){c[f>>2]=-2;break a}switch(a[b+1>>0]|0){case 90:{k=b+2|0;b=Ma(k,d,e)|0;if(!((b|0)==(k|0)|(b|0)==(d|0))?(a[b>>0]|0)==46:0){g=c[e+4>>2]|0;if((c[e>>2]|0)!=(g|0)){k=g+-24|0;h=h-b|0;if(h>>>0>4294967279)Xa();if(h>>>0<11){a[n>>0]=h<<1;j=n+1|0}else{g=h+16&-16;j=vc(g)|0;c[n+8>>2]=j;c[n>>2]=g|1;c[n+4>>2]=h}g=j;while(1){if((b|0)==(d|0))break;a[g>>0]=a[b>>0]|0;b=b+1|0;g=g+1|0}a[j+h>>0]=0;b=Ta(n,0,849)|0;c[m>>2]=c[b>>2];c[m+4>>2]=c[b+4>>2];c[m+8>>2]=c[b+8>>2];g=0;while(1){if((g|0)==3)break;c[b+(g<<2)>>2]=0;g=g+1|0}b=Ya(m,799)|0;c[l>>2]=c[b>>2];c[l+4>>2]=c[b+4>>2];c[l+8>>2]=c[b+8>>2];g=0;while(1){if((g|0)==3)break;c[b+(g<<2)>>2]=0;g=g+1|0}b=a[l>>0]|0;j=(b&1)==0;Za(k,j?l+1|0:c[l+8>>2]|0,j?(b&255)>>>1:c[l+4>>2]|0)|0;Ja(l);Ja(m);Ja(n);b=d}}if((b|0)==(d|0))break b;c[f>>2]=-2;break a}case 95:{if((a[b+2>>0]|0)==95?(a[b+3>>0]|0)==90:0){n=b+4|0;b=Ma(n,d,e)|0;if((b|0)==(n|0)|(b|0)==(d|0)){c[f>>2]=-2;break a}c:do if((h-b|0)>12){h=0;g=b;while(1){if((h|0)>=13)break;if((a[g>>0]|0)!=(a[2320+h>>0]|0))break c;h=h+1|0;g=g+1|0}d:do if((g|0)==(d|0))g=d;else{if((a[g>>0]|0)==95){h=g+1|0;if((h|0)==(d|0))break c;if(((a[h>>0]|0)+-48|0)>>>0>=10)break c;g=g+2|0}while(1){if((g|0)==(d|0)){g=d;break d}if(((a[g>>0]|0)+-48|0)>>>0>=10)break d;g=g+1|0}}while(0);h=c[e+4>>2]|0;if((c[e>>2]|0)!=(h|0)){Ta(h+-24|0,0,2334)|0;b=g}}while(0);if((b|0)==(d|0))break b;c[f>>2]=-2;break a}break}default:{}}c[f>>2]=-2;break a}while(0);if((c[f>>2]|0)==0?(c[e>>2]|0)==(c[e+4>>2]|0):0)c[f>>2]=-2}else c[f>>2]=-2;while(0);i=o;return}function Ma(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0;E=i;i=i+80|0;z=E+60|0;y=E+48|0;r=E+36|0;s=E+24|0;t=E+12|0;w=E;a:do if((b|0)==(d|0))f=b;else{B=e+56|0;C=c[B>>2]|0;x=C+1|0;c[B>>2]=x;D=e+61|0;A=a[D>>0]|0;if(x>>>0>1)a[D>>0]=1;f=a[b>>0]|0;b:do switch(f|0){case 84:case 71:{c:do if((d-b|0)>2){switch(f|0){case 84:break;case 71:switch(a[b+1>>0]|0){case 86:{z=b+2|0;f=Wb(z,d,e)|0;if((f|0)==(z|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2275)|0;break c}case 82:{z=b+2|0;f=Wb(z,d,e)|0;if((f|0)==(z|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2295)|0;break c}default:{f=b;break c}}default:{f=b;break c}}f=b+1|0;switch(a[f>>0]|0){case 86:{z=b+2|0;f=Na(z,d,e)|0;if((f|0)==(z|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2124)|0;break c}case 84:{z=b+2|0;f=Na(z,d,e)|0;if((f|0)==(z|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2136)|0;break c}case 73:{z=b+2|0;f=Na(z,d,e)|0;if((f|0)==(z|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2145)|0;break c}case 83:{z=b+2|0;f=Na(z,d,e)|0;if((f|0)==(z|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2159)|0;break c}case 99:{z=b+2|0;f=Zb(z,d)|0;if((f|0)==(z|0)){f=b;break c}g=Zb(f,d)|0;if((g|0)==(f|0)){f=b;break c}f=Ma(g,d,e)|0;if((f|0)==(g|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}Ta(g+-24|0,0,2178)|0;break c}case 67:{x=b+2|0;f=Na(x,d,e)|0;if((f|0)==(x|0)){f=b;break c}g=tb(f,d)|0;if((g|0)==(f|0)|(g|0)==(d|0)){f=b;break c}if((a[g>>0]|0)!=95){f=b;break c}x=g+1|0;f=Na(x,d,e)|0;if((f|0)==(x|0)){f=b;break c}j=e+4|0;g=c[j>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break c}Cb(z,g+-24|0);k=c[j>>2]|0;g=k+-24|0;h=k;while(1){if((h|0)==(g|0))break;b=h+-24|0;c[j>>2]=b;Ia(b);h=c[j>>2]|0}q=k+-48|0;g=Ta(z,0,2205)|0;c[s>>2]=c[g>>2];c[s+4>>2]=c[g+4>>2];c[s+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}g=Ya(s,2230)|0;c[r>>2]=c[g>>2];c[r+4>>2]=c[g+4>>2];c[r+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}Cb(t,(c[j>>2]|0)+-24|0);g=a[t>>0]|0;h=(g&1)==0;g=Za(r,h?t+1|0:c[t+8>>2]|0,h?(g&255)>>>1:c[t+4>>2]|0)|0;c[y>>2]=c[g>>2];c[y+4>>2]=c[g+4>>2];c[y+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}do if(a[q>>0]&1){p=k+-40|0;a[c[p>>2]>>0]=0;m=k+-44|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){k=10;n=h;o=1}else{k=(h+16&240)+-1|0;n=h;o=1}}else{k=10;n=0;o=0}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{b=c[p>>2]|0;a[j>>0]=a[b>>0]|0;wc(b)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[y>>2];c[q+4>>2]=c[y+4>>2];c[q+8>>2]=c[y+8>>2];g=0;while(1){if((g|0)==3)break;c[y+(g<<2)>>2]=0;g=g+1|0}Ja(y);Ja(t);Ja(r);Ja(s);Ja(z);break c}default:{g=Zb(f,d)|0;if((g|0)==(f|0)){f=b;break c}f=Ma(g,d,e)|0;if((f|0)==(g|0)){f=b;break c}g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break c}g=g+-24|0;if((a[b+2>>0]|0)==118){Ta(g,0,2235)|0;break c}else{Ta(g,0,2253)|0;break c}}}}else f=b;while(0);break}default:{f=Wb(b,d,e)|0;u=c[e+48>>2]|0;v=c[e+52>>2]|0;if((f|0)!=(b|0))if((f|0)!=(d|0)){switch(a[f>>0]|0){case 46:case 69:break b;default:{}}x=a[D>>0]|0;a[D>>0]=0;g=0;while(1){if((g|0)==3)break;c[z+(g<<2)>>2]=0;g=g+1|0}t=e+4|0;m=c[t>>2]|0;d:do if((c[e>>2]|0)!=(m|0)){l=m+-24|0;j=a[l>>0]|0;k=(j&1)==0;if(k)g=(j&255)>>>1;else g=c[m+-20>>2]|0;if(g){if(!(a[e+60>>0]|0)){if(k){g=l+1|0;h=(j&255)>>>1}else{g=c[m+-16>>2]|0;h=c[m+-20>>2]|0}if((a[g+h+-1>>0]|0)==62){if(k){g=(j&255)>>>1;h=l+1|0}else{g=c[m+-20>>2]|0;h=c[m+-16>>2]|0}if((a[h+(g+-2)>>0]|0)!=45){if(k){h=(j&255)>>>1;g=l+1|0}else{h=c[m+-20>>2]|0;g=c[m+-16>>2]|0}if((a[g+(h+-2)>>0]|0)!=62){o=Na(f,d,e)|0;if((o|0)==(f|0)){f=b;g=0;break}s=c[t>>2]|0;f=s;if(((s-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;g=0;break}g=f+-24|0;c[y>>2]=c[g>>2];c[y+4>>2]=c[g+4>>2];c[y+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}n=f+-12|0;e:do if(!(a[z>>0]&1)){a[z+1>>0]=0;a[z>>0]=0}else{k=z+8|0;g=c[k>>2]|0;a[g>>0]=0;l=z+4|0;c[l>>2]=0;f=c[z>>2]|0;m=(f&-2)+-1|0;h=f&255;do if(!(h&1)){f=f>>>1&127;if((h&255)<22){Fc(z+1|0,g|0,f+1|0)|0;wc(g);break}g=f+16&240;j=g+-1|0;if((j|0)==(m|0))break e;h=vc(g)|0;if(j>>>0<=m>>>0&(h|0)==0)break e;Fc(h|0,z+1|0,f+1|0)|0;c[z>>2]=g|1;c[l>>2]=f;c[k>>2]=h;break e}else{a[z+1>>0]=0;wc(g);f=0}while(0);a[z>>0]=f<<1}while(0);c[z>>2]=c[n>>2];c[z+4>>2]=c[n+4>>2];c[z+8>>2]=c[n+8>>2];f=0;while(1){if((f|0)==3)break;c[n+(f<<2)>>2]=0;f=f+1|0}s=a[z>>0]|0;if(!(((s&1)==0?(s&255)>>>1:c[z+4>>2]|0)|0))zb(y,32);f=c[t>>2]|0;g=f+-24|0;h=f;while(1){if((h|0)==(g|0))break;s=h+-24|0;c[t>>2]=s;Ia(s);h=c[t>>2]|0}g=a[y>>0]|0;s=(g&1)==0;Ua(f+-48|0,0,s?y+1|0:c[y+8>>2]|0,s?(g&255)>>>1:c[y+4>>2]|0)|0;Ja(y);g=c[t>>2]|0;f=o}else g=m}else g=m}else g=m}else g=m;zb(g+-24|0,40);if((f|0)!=(d|0)?(a[f>>0]|0)==118:0){h=c[e>>2]|0;g=c[t>>2]|0;f=f+1|0}else p=128;do if((p|0)==128){n=y+4|0;o=w+8|0;p=w+1|0;q=w+4|0;r=y+8|0;s=y+1|0;l=1;f:while(1){h=c[e>>2]|0;g=c[t>>2]|0;while(1){j=(g-h|0)/24|0;m=Na(f,d,e)|0;g=c[t>>2]|0;h=c[e>>2]|0;k=(g-h|0)/24|0;if((m|0)==(f|0)){p=151;break f}if(k>>>0>j>>>0)break;else f=m}f=0;while(1){if((f|0)==3){f=j;break}c[y+(f<<2)>>2]=0;f=f+1|0}while(1){if(f>>>0>=k>>>0){h=j;break}h=a[y>>0]|0;if(((h&1)==0?(h&255)>>>1:c[n>>2]|0)|0)Ya(y,1429)|0;Cb(w,(c[e>>2]|0)+(f*24|0)|0);h=a[w>>0]|0;g=(h&1)==0;Za(y,g?p:c[o>>2]|0,g?(h&255)>>>1:c[q>>2]|0)|0;Ja(w);f=f+1|0}while(1){if(h>>>0>=k>>>0)break;g=c[t>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break;j=g+-24|0;c[t>>2]=j;Ia(j);g=c[t>>2]|0}h=h+1|0}h=a[y>>0]|0;f=c[n>>2]|0;if(!(((h&1)==0?(h&255)>>>1:f)|0))f=l;else{g=c[t>>2]|0;if((c[e>>2]|0)==(g|0)){p=163;break}if(!l){Ya(g+-24|0,1429)|0;g=c[t>>2]|0;h=a[y>>0]|0;f=c[n>>2]|0}l=(h&1)==0;Za(g+-24|0,l?s:c[r>>2]|0,l?(h&255)>>>1:f)|0;f=0}Ja(y);l=f;f=m}if((p|0)==151)break;else if((p|0)==163){Ja(y);f=b;g=0;break d}}while(0);if((h|0)!=(g|0)){zb(g+-24|0,41);if(u&1)Ya((c[t>>2]|0)+-24|0,267)|0;if(u&2)Ya((c[t>>2]|0)+-24|0,456)|0;if(u&4)Ya((c[t>>2]|0)+-24|0,466)|0;switch(v|0){case 1:{Ya((c[t>>2]|0)+-24|0,2032)|0;break}case 2:{Ya((c[t>>2]|0)+-24|0,2035)|0;break}default:{}}g=a[z>>0]|0;y=(g&1)==0;Za((c[t>>2]|0)+-24|0,y?z+1|0:c[z+8>>2]|0,y?(g&255)>>>1:c[z+4>>2]|0)|0;g=1}else{f=b;g=0}}else{f=b;g=0}}else{f=b;g=0}while(0);Ja(z);a[D>>0]=x;if(!g){a[D>>0]=A;c[B>>2]=C;f=b;break a}}else f=d;else f=b}}while(0);a[D>>0]=A;c[B>>2]=C}while(0);i=E;return f|0}function Na(d,e,f){d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,_=0,$=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,na=0,oa=0,pa=0,qa=0,ra=0,sa=0,ta=0,ua=0,va=0,wa=0,xa=0,ya=0,za=0;za=i;i=i+736|0;ya=za+704|0;xa=za+680|0;la=za+668|0;ca=za+656|0;fa=za+632|0;pa=za+608|0;sa=za+584|0;ia=za+572|0;oa=za+560|0;qa=za+548|0;ra=za+536|0;$=za+384|0;ja=za+520|0;ha=za+512|0;A=za+496|0;o=za+488|0;S=za+472|0;O=za+464|0;B=za+448|0;p=za+440|0;na=za+424|0;ma=za+420|0;T=za+408|0;da=za+396|0;ea=za+372|0;U=za+360|0;X=za+344|0;V=za+340|0;t=za+328|0;v=za+304|0;w=za+288|0;x=za+276|0;y=za+264|0;E=za+240|0;F=za+228|0;G=za+216|0;H=za+204|0;I=za+192|0;L=za+168|0;M=za+156|0;N=za+144|0;W=za+128|0;R=za+120|0;z=za+104|0;n=za+96|0;D=za+80|0;s=za+72|0;C=za+56|0;r=za+48|0;ga=za+32|0;ba=za+24|0;wa=za+8|0;va=za;a:do if((d|0)!=(e|0)){switch(a[d>>0]|0){case 75:case 86:case 114:{c[xa>>2]=0;h=Oa(d,e,xa)|0;b:do if((h|0)!=(d|0)?(j=a[h>>0]|0,Z=f+4|0,q=((c[Z>>2]|0)-(c[f>>2]|0)|0)/24|0,Y=Na(h,e,f)|0,Z=((c[Z>>2]|0)-(c[f>>2]|0)|0)/24|0,(Y|0)!=(h|0)):0){v=j<<24>>24==70;w=f+20|0;h=c[w>>2]|0;c:do if(v){j=h+-16|0;while(1){if((h|0)==(j|0)){h=j;break c}d=h+-16|0;c[w>>2]=d;Ha(d);h=c[w>>2]|0}}while(0);n=f+16|0;o=c[f+12>>2]|0;d=c[f+24>>2]|0;j=d;if(h>>>0<d>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=o;c[w>>2]=(c[w>>2]|0)+16}else{k=c[n>>2]|0;d=h-k|0;m=d>>4;l=m+1|0;if((d|0)<-16)Pa();h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ya,h,m,f+28|0);d=ya+8|0;e=c[d>>2]|0;c[e>>2]=0;c[e+4>>2]=0;c[e+8>>2]=0;c[e+12>>2]=o;c[d>>2]=e+16;Ra(n,ya);Sa(ya)}t=c[xa>>2]|0;r=(t&1|0)==0;s=(t&2|0)==0;t=(t&4|0)==0;u=ya+8|0;while(1){if(q>>>0>=Z>>>0){g=Y;break b}if(v){k=c[f>>2]|0;o=k+(q*24|0)+12|0;l=a[o>>0]|0;h=(l&1)==0;if(h){m=(l&255)>>>1;j=o+1|0}else{m=c[k+(q*24|0)+16>>2]|0;j=c[k+(q*24|0)+20>>2]|0}n=m+-2|0;if((a[j+n>>0]|0)==38)h=m+-3|0;else{if(h){j=o+1|0;h=(l&255)>>>1}else{j=c[k+(q*24|0)+20>>2]|0;h=c[k+(q*24|0)+16>>2]|0}h=(a[j+h+-1>>0]|0)==38?n:m}if(!r){Ta(o,h,267)|0;h=h+6|0}if(!s){Ta((c[f>>2]|0)+(q*24|0)+12|0,h,456)|0;h=h+9|0}if(!t)Ta((c[f>>2]|0)+(q*24|0)+12|0,h,466)|0}else{if(!r)Ya((c[f>>2]|0)+(q*24|0)|0,267)|0;if(!s)Ya((c[f>>2]|0)+(q*24|0)|0,456)|0;if(!t)Ya((c[f>>2]|0)+(q*24|0)|0,466)|0}m=c[w>>2]|0;n=m+-16|0;o=c[f>>2]|0;p=o+(q*24|0)|0;h=m+-12|0;j=c[h>>2]|0;d=c[m+-8>>2]|0;k=d;if((j|0)==(d|0)){h=c[n>>2]|0;d=j-h|0;l=(d|0)/24|0;j=l+1|0;if((d|0)<-24)break;h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,m+-4|0);d=c[u>>2]|0;_a(d,p);_a(d+12|0,o+(q*24|0)+12|0);c[u>>2]=d+24;cb(n,ya);bb(ya)}else{_a(j,p);_a(j+12|0,o+(q*24|0)+12|0);c[h>>2]=(c[h>>2]|0)+24}q=q+1|0}Pa()}else g=d;while(0);break a}default:{}}g=eb(d,e,f)|0;if((g|0)==(d|0)){h=a[d>>0]|0;d:do switch(h<<24>>24|0){case 65:{do if(h<<24>>24==65?(u=d+1|0,(u|0)!=(e|0)):0){g=a[u>>0]|0;if(g<<24>>24==95){xa=d+2|0;g=Na(xa,e,f)|0;if((g|0)==(xa|0)){g=d;break}h=f+4|0;j=c[h>>2]|0;if((c[f>>2]|0)==(j|0)){g=d;break}e=j+-12|0;wa=a[e>>0]|0;xa=(wa&1)==0;wa=xa?(wa&255)>>>1:c[j+-8>>2]|0;$a(ya,xa?e+1|0:c[j+-4>>2]|0,wa>>>0<2?wa:2);wa=a[ya>>0]|0;e=(wa&1)==0;wa=e?(wa&255)>>>1:c[ya+4>>2]|0;xa=wa>>>0>2;e=ac(e?ya+1|0:c[ya+8>>2]|0,790,xa?2:wa)|0;Ja(ya);if(!(((e|0)==0?(wa>>>0<2?-1:xa&1):e)|0))sb((c[h>>2]|0)+-12|0);Ta((c[h>>2]|0)+-12|0,0,793)|0;break}if((g+-49&255)<9){m=tb(u,e)|0;if((m|0)==(e|0)){g=d;break}if((a[m>>0]|0)!=95){g=d;break}wa=m+1|0;g=Na(wa,e,f)|0;if((g|0)==(wa|0)){g=d;break}h=f+4|0;j=c[h>>2]|0;if((c[f>>2]|0)==(j|0)){g=d;break}e=j+-12|0;va=a[e>>0]|0;wa=(va&1)==0;va=wa?(va&255)>>>1:c[j+-8>>2]|0;$a(xa,wa?e+1|0:c[j+-4>>2]|0,va>>>0<2?va:2);va=a[xa>>0]|0;e=(va&1)==0;va=e?(va&255)>>>1:c[xa+4>>2]|0;wa=va>>>0>2;e=ac(e?xa+1|0:c[xa+8>>2]|0,790,wa?2:va)|0;Ja(xa);if(!(((e|0)==0?(va>>>0<2?-1:wa&1):e)|0))sb((c[h>>2]|0)+-12|0);n=(c[h>>2]|0)+-12|0;k=m-u|0;if(k>>>0>4294967279)Xa();if(k>>>0<11){a[fa>>0]=k<<1;l=fa+1|0}else{e=k+16&-16;l=vc(e)|0;c[fa+8>>2]=l;c[fa>>2]=e|1;c[fa+4>>2]=k}h=u;j=l;while(1){if((h|0)==(m|0))break;a[j>>0]=a[h>>0]|0;h=h+1|0;j=j+1|0}a[l+k>>0]=0;h=Ta(fa,0,790)|0;c[ca>>2]=c[h>>2];c[ca+4>>2]=c[h+4>>2];c[ca+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}h=Ya(ca,4264)|0;c[la>>2]=c[h>>2];c[la+4>>2]=c[h+4>>2];c[la+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}e=a[la>>0]|0;xa=(e&1)==0;Ua(n,0,xa?la+1|0:c[la+8>>2]|0,xa?(e&255)>>>1:c[la+4>>2]|0)|0;Ja(la);Ja(ca);Ja(fa);break}g=ub(u,e,f)|0;if(((!((g|0)==(u|0)|(g|0)==(e|0))?(a[g>>0]|0)==95:0)?(xa=g+1|0,aa=Na(xa,e,f)|0,(aa|0)!=(xa|0)):0)?(P=f+4|0,m=c[P>>2]|0,((m-(c[f>>2]|0)|0)/24|0)>>>0>=2):0){db(pa,m+-24|0);g=c[P>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;e=j+-24|0;c[P>>2]=e;Ia(e);j=c[P>>2]|0}db(sa,g+-48|0);g=c[P>>2]|0;q=g+-24|0;do if(a[q>>0]&1){p=g+-16|0;a[c[p>>2]>>0]=0;m=g+-20|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){o=1;k=10;n=h}else{o=1;k=(h+16&240)+-1|0;n=h}}else{o=0;k=10;n=0}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{e=c[p>>2]|0;a[j>>0]=a[e>>0]|0;wc(e)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[pa>>2];c[q+4>>2]=c[pa+4>>2];c[q+8>>2]=c[pa+8>>2];g=0;while(1){if((g|0)==3)break;c[pa+(g<<2)>>2]=0;g=g+1|0}j=pa+12|0;wa=a[j>>0]|0;e=(wa&1)==0;k=pa+16|0;wa=e?(wa&255)>>>1:c[k>>2]|0;l=pa+20|0;m=j+1|0;$a(ia,e?m:c[l>>2]|0,wa>>>0<2?wa:2);wa=a[ia>>0]|0;e=(wa&1)==0;wa=e?(wa&255)>>>1:c[ia+4>>2]|0;xa=wa>>>0>2;e=ac(e?ia+1|0:c[ia+8>>2]|0,790,xa?2:wa)|0;Ja(ia);if(!(((e|0)==0?(wa>>>0<2?-1:xa&1):e)|0))sb(j);n=c[P>>2]|0;q=n+-12|0;Cb($,sa);g=Ta($,0,790)|0;c[ra>>2]=c[g>>2];c[ra+4>>2]=c[g+4>>2];c[ra+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}g=Ya(ra,4264)|0;c[qa>>2]=c[g>>2];c[qa+4>>2]=c[g+4>>2];c[qa+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}g=a[j>>0]|0;h=(g&1)==0;g=Za(qa,h?m:c[l>>2]|0,h?(g&255)>>>1:c[k>>2]|0)|0;c[oa>>2]=c[g>>2];c[oa+4>>2]=c[g+4>>2];c[oa+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}do if(a[q>>0]&1){p=n+-4|0;a[c[p>>2]>>0]=0;m=n+-8|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}do if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){o=1;k=10;n=h;break}o=1;k=(h+16&240)+-1|0;n=h}else{o=0;k=10;n=0}while(0);if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(k>>>0<=l>>>0&(j|0)==0)break;if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{e=c[p>>2]|0;a[j>>0]=a[e>>0]|0;wc(e)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[oa>>2];c[q+4>>2]=c[oa+4>>2];c[q+8>>2]=c[oa+8>>2];g=0;while(1){if((g|0)==3)break;c[oa+(g<<2)>>2]=0;g=g+1|0}Ja(oa);Ja(qa);Ja(ra);Ja($);Ia(sa);Ia(pa);g=aa}else g=d}else g=d;while(0);if((g|0)==(d|0)){g=d;break a}h=c[f+4>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}m=f+16|0;c[ha>>2]=c[f+12>>2];Pb(ja,h+-24|0,ha);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[ja+12>>2];c[j>>2]=c[ja>>2];ya=ja+4|0;c[j+4>>2]=c[ya>>2];f=ja+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[ja>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[ja+12>>2];c[d>>2]=c[ja>>2];e=ja+4|0;c[d+4>>2]=c[e>>2];xa=ja+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[ja>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(ja);break a}case 67:{xa=d+1|0;g=Na(xa,e,f)|0;if((g|0)==(xa|0)){g=d;break a}j=f+4|0;h=c[j>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}Ya(h+-24|0,2023)|0;m=f+16|0;h=(c[j>>2]|0)+-24|0;c[o>>2]=c[f+12>>2];Pb(A,h,o);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[A+12>>2];c[j>>2]=c[A>>2];ya=A+4|0;c[j+4>>2]=c[ya>>2];f=A+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[A>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[A+12>>2];c[d>>2]=c[A>>2];e=A+4|0;c[d+4>>2]=c[e>>2];xa=A+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[A>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(A);break a}case 70:{do if(h<<24>>24==70){g=d+1|0;if((g|0)!=(e|0)){if((a[g>>0]|0)==89){g=d+2|0;if((g|0)==(e|0))break}h=Na(g,e,f)|0;if((h|0)!=(g|0)){$a(ya,797,1);r=f+4|0;q=ya+4|0;m=xa+8|0;n=xa+1|0;o=xa+4|0;p=0;g=h;e:while(1){j=g;f:while(1){if((j|0)==(e|0)){ta=170;break e}switch(a[j>>0]|0){case 69:{ta=174;break e}case 118:{j=j+1|0;continue f}case 82:{g=j+1|0;if((g|0)!=(e|0)?(a[g>>0]|0)==69:0){p=1;continue e}break}case 79:{g=j+1|0;if((g|0)!=(e|0)?(a[g>>0]|0)==69:0){p=2;continue e}break}default:{}}h=((c[r>>2]|0)-(c[f>>2]|0)|0)/24|0;k=Na(j,e,f)|0;l=((c[r>>2]|0)-(c[f>>2]|0)|0)/24|0;if((k|0)==(j|0)|(k|0)==(e|0))break e;else g=h;while(1){if(g>>>0>=l>>>0)break;wa=a[ya>>0]|0;if(((wa&1)==0?(wa&255)>>>1:c[q>>2]|0)>>>0>1)Ya(ya,1429)|0;Cb(xa,(c[f>>2]|0)+(g*24|0)|0);wa=a[xa>>0]|0;va=(wa&1)==0;Za(ya,va?n:c[m>>2]|0,va?(wa&255)>>>1:c[o>>2]|0)|0;Ja(xa);g=g+1|0}while(1){if(h>>>0>=l>>>0){j=k;continue f}j=c[r>>2]|0;g=j+-24|0;while(1){if((j|0)==(g|0))break;wa=j+-24|0;c[r>>2]=wa;Ia(wa);j=c[r>>2]|0}h=h+1|0}}}g:do if((ta|0)==170){h=c[r>>2]|0;g=h+-24|0;while(1){if((h|0)==(g|0))break g;f=h+-24|0;c[r>>2]=f;Ia(f);h=c[r>>2]|0}}else if((ta|0)==174){g=j+1|0;Ya(ya,799)|0;switch(p|0){case 1:{Ya(ya,2032)|0;break}case 2:{Ya(ya,2035)|0;break}default:{}}h=c[r>>2]|0;if((c[f>>2]|0)!=(h|0)){Ya(h+-24|0,1882)|0;e=a[ya>>0]|0;xa=(e&1)==0;Ua((c[r>>2]|0)+-12|0,0,xa?ya+1|0:c[ya+8>>2]|0,xa?(e&255)>>>1:c[q>>2]|0)|0;Ja(ya);if((g|0)==(d|0)){g=d;break a}h=c[r>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}m=f+16|0;c[O>>2]=c[f+12>>2];Pb(S,h+-24|0,O);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[S+12>>2];c[j>>2]=c[S>>2];ya=S+4|0;c[j+4>>2]=c[ya>>2];f=S+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[S>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[S+12>>2];c[d>>2]=c[S>>2];e=S+4|0;c[d+4>>2]=c[e>>2];xa=S+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[S>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(S);break a}}while(0);Ja(ya);break}}g=d;break a}while(0);g=d;break a}case 71:{xa=d+1|0;g=Na(xa,e,f)|0;if((g|0)==(xa|0)){g=d;break a}j=f+4|0;h=c[j>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}Ya(h+-24|0,2039)|0;m=f+16|0;h=(c[j>>2]|0)+-24|0;c[p>>2]=c[f+12>>2];Pb(B,h,p);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[B+12>>2];c[j>>2]=c[B>>2];ya=B+4|0;c[j+4>>2]=c[ya>>2];f=B+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[B>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[B+12>>2];c[d>>2]=c[B>>2];e=B+4|0;c[d+4>>2]=c[e>>2];xa=B+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[B>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(B);break a}case 77:{if(((h<<24>>24==77?(wa=d+1|0,k=Na(wa,e,f)|0,(k|0)!=(wa|0)):0)?(ka=Na(k,e,f)|0,(ka|0)!=(k|0)):0)?(_=f+4|0,l=c[_>>2]|0,((l-(c[f>>2]|0)|0)/24|0)>>>0>=2):0){db(ya,l+-24|0);g=c[_>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;e=j+-24|0;c[_>>2]=e;Ia(e);j=c[_>>2]|0}db(xa,g+-48|0);r=ya+12|0;j=c[_>>2]|0;q=j+-24|0;h:do if((a[((a[r>>0]&1)==0?r+1|0:c[ya+20>>2]|0)>>0]|0)==40){g=Ya(ya,797)|0;c[fa>>2]=c[g>>2];c[fa+4>>2]=c[g+4>>2];c[fa+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}Cb(pa,xa);g=a[pa>>0]|0;h=(g&1)==0;g=Za(fa,h?pa+1|0:c[pa+8>>2]|0,h?(g&255)>>>1:c[pa+4>>2]|0)|0;c[ca>>2]=c[g>>2];c[ca+4>>2]=c[g+4>>2];c[ca+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}g=Ya(ca,2050)|0;c[la>>2]=c[g>>2];c[la+4>>2]=c[g+4>>2];c[la+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}do if(a[q>>0]&1){p=j+-16|0;a[c[p>>2]>>0]=0;m=j+-20|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){o=1;k=10;n=h}else{o=1;k=(h+16&240)+-1|0;n=h}}else{o=0;k=10;n=0}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{e=c[p>>2]|0;a[j>>0]=a[e>>0]|0;wc(e)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[la>>2];c[q+4>>2]=c[la+4>>2];c[q+8>>2]=c[la+8>>2];g=0;while(1){if((g|0)==3)break;c[la+(g<<2)>>2]=0;g=g+1|0}Ja(la);Ja(ca);Ja(pa);Ja(fa);j=c[_>>2]|0;g=Ta(r,0,799)|0;c[sa>>2]=c[g>>2];c[sa+4>>2]=c[g+4>>2];c[sa+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}q=j+-12|0;do if(a[q>>0]&1){p=j+-4|0;a[c[p>>2]>>0]=0;m=j+-8|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){o=1;k=10;n=h}else{o=1;k=(h+16&240)+-1|0;n=h}}else{o=0;k=10;n=0}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{e=c[p>>2]|0;a[j>>0]=a[e>>0]|0;wc(e)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[sa>>2];c[q+4>>2]=c[sa+4>>2];c[q+8>>2]=c[sa+8>>2];g=0;while(1){if((g|0)==3)break;c[sa+(g<<2)>>2]=0;g=g+1|0}Ja(sa)}else{g=Ya(ya,1882)|0;c[qa>>2]=c[g>>2];c[qa+4>>2]=c[g+4>>2];c[qa+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}Cb(ra,xa);g=a[ra>>0]|0;h=(g&1)==0;g=Za(qa,h?ra+1|0:c[ra+8>>2]|0,h?(g&255)>>>1:c[ra+4>>2]|0)|0;c[oa>>2]=c[g>>2];c[oa+4>>2]=c[g+4>>2];c[oa+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}g=Ya(oa,2050)|0;c[ia>>2]=c[g>>2];c[ia+4>>2]=c[g+4>>2];c[ia+8>>2]=c[g+8>>2];h=0;while(1){if((h|0)==3)break;c[g+(h<<2)>>2]=0;h=h+1|0}do if(a[q>>0]&1){p=j+-16|0;a[c[p>>2]>>0]=0;m=j+-20|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{g=c[q>>2]|0;l=(g&-2)+-1|0;g=g&255}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){n=h;o=1;k=10}else{n=h;o=1;k=(h+16&240)+-1|0}}else{n=0;o=0;k=10}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{e=c[p>>2]|0;a[j>>0]=a[e>>0]|0;wc(e)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[ia>>2];c[q+4>>2]=c[ia+4>>2];c[q+8>>2]=c[ia+8>>2];g=0;while(1){if((g|0)==3)break;c[ia+(g<<2)>>2]=0;g=g+1|0}Ja(ia);Ja(oa);Ja(ra);Ja(qa);g=c[_>>2]|0;q=g+-12|0;do if(a[q>>0]&1){p=g+-4|0;a[c[p>>2]>>0]=0;m=g+-8|0;c[m>>2]=0;g=a[q>>0]|0;if(!(g&1))l=10;else{l=c[q>>2]|0;g=l&255;l=(l&-2)+-1|0}if(!(g&1)){h=(g&255)>>>1;if((g&255)<22){k=10;n=h;o=1}else{k=(h+16&240)+-1|0;n=h;o=1}}else{k=10;n=0;o=0}if((k|0)!=(l|0)){if((k|0)==10){j=q+1|0;h=c[p>>2]|0;if(o){Fc(j|0,h|0,((g&255)>>>1)+1|0)|0;wc(h)}else{a[j>>0]=a[h>>0]|0;wc(h)}a[q>>0]=n<<1;break}h=k+1|0;j=vc(h)|0;if(!(k>>>0<=l>>>0&(j|0)==0)){if(o)Fc(j|0,q+1|0,((g&255)>>>1)+1|0)|0;else{e=c[p>>2]|0;a[j>>0]=a[e>>0]|0;wc(e)}c[q>>2]=h|1;c[m>>2]=n;c[p>>2]=j}}}else{a[q+1>>0]=0;a[q>>0]=0}while(0);c[q>>2]=c[r>>2];c[q+4>>2]=c[r+4>>2];c[q+8>>2]=c[r+8>>2];g=0;while(1){if((g|0)==3)break h;c[r+(g<<2)>>2]=0;g=g+1|0}}while(0);Ia(xa);Ia(ya);g=ka}else g=d;if((g|0)==(d|0)){g=d;break a}h=c[f+4>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}m=f+16|0;c[ma>>2]=c[f+12>>2];Pb(na,h+-24|0,ma);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[na+12>>2];c[j>>2]=c[na>>2];ya=na+4|0;c[j+4>>2]=c[ya>>2];f=na+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[na>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[na+12>>2];c[d>>2]=c[na>>2];e=na+4|0;c[d+4>>2]=c[e>>2];xa=na+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[na>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(na);break a}case 79:{v=f+4|0;p=((c[v>>2]|0)-(c[f>>2]|0)|0)/24|0;xa=d+1|0;g=Na(xa,e,f)|0;v=((c[v>>2]|0)-(c[f>>2]|0)|0)/24|0;if((g|0)==(xa|0)){g=d;break a}n=f+16|0;o=c[f+12>>2]|0;w=f+20|0;h=c[w>>2]|0;xa=c[f+24>>2]|0;j=xa;if(h>>>0<xa>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=o;c[w>>2]=(c[w>>2]|0)+16}else{k=c[n>>2]|0;xa=h-k|0;m=xa>>4;l=m+1|0;if((xa|0)<-16)Pa();h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ya,h,m,f+28|0);xa=ya+8|0;ta=c[xa>>2]|0;c[ta>>2]=0;c[ta+4>>2]=0;c[ta+8>>2]=0;c[ta+12>>2]=o;c[xa>>2]=ta+16;Ra(n,ya);Sa(ya)}r=T+4|0;s=T+8|0;t=T+1|0;u=ya+8|0;while(1){if(p>>>0>=v>>>0)break a;xa=c[f>>2]|0;ta=xa+(p*24|0)+12|0;sa=a[ta>>0]|0;j=(sa&1)==0;sa=j?(sa&255)>>>1:c[xa+(p*24|0)+16>>2]|0;$a(T,j?ta+1|0:c[xa+(p*24|0)+20>>2]|0,sa>>>0<2?sa:2);sa=a[T>>0]|0;xa=(sa&1)==0;sa=xa?(sa&255)>>>1:c[r>>2]|0;ta=sa>>>0>2;xa=ac(xa?t:c[s>>2]|0,790,ta?2:sa)|0;Ja(T);j=c[f>>2]|0;if(((xa|0)==0?(sa>>>0<2?-1:ta&1):xa)|0){h=b[j+(p*24|0)+12>>1]|0;if(!(h&1))h=(h&65535)>>>8&255;else h=a[c[j+(p*24|0)+20>>2]>>0]|0;if(h<<24>>24==40){Ya(j+(p*24|0)|0,797)|0;Ta((c[f>>2]|0)+(p*24|0)+12|0,0,799)|0}}else{Ya(j+(p*24|0)|0,849)|0;Ta((c[f>>2]|0)+(p*24|0)+12|0,0,799)|0}Ya((c[f>>2]|0)+(p*24|0)|0,841)|0;m=c[w>>2]|0;n=m+-16|0;o=c[f>>2]|0;q=o+(p*24|0)|0;h=m+-12|0;j=c[h>>2]|0;xa=c[m+-8>>2]|0;k=xa;if((j|0)==(xa|0)){h=c[n>>2]|0;xa=j-h|0;l=(xa|0)/24|0;j=l+1|0;if((xa|0)<-24)break;h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,m+-4|0);xa=c[u>>2]|0;_a(xa,q);_a(xa+12|0,o+(p*24|0)+12|0);c[u>>2]=xa+24;cb(n,ya);bb(ya)}else{_a(j,q);_a(j+12|0,o+(p*24|0)+12|0);c[h>>2]=(c[h>>2]|0)+24}p=p+1|0}Pa();break}case 80:{B=f+4|0;p=((c[B>>2]|0)-(c[f>>2]|0)|0)/24|0;A=d+1|0;g=Na(A,e,f)|0;B=((c[B>>2]|0)-(c[f>>2]|0)|0)/24|0;if((g|0)==(A|0)){g=d;break a}n=f+16|0;o=c[f+12>>2]|0;C=f+20|0;h=c[C>>2]|0;xa=c[f+24>>2]|0;j=xa;if(h>>>0<xa>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=o;c[C>>2]=(c[C>>2]|0)+16}else{k=c[n>>2]|0;xa=h-k|0;m=xa>>4;l=m+1|0;if((xa|0)<-16)Pa();h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ya,h,m,f+28|0);xa=ya+8|0;sa=c[xa>>2]|0;c[sa>>2]=0;c[sa+4>>2]=0;c[sa+8>>2]=0;c[sa+12>>2]=o;c[xa>>2]=sa+16;Ra(n,ya);Sa(ya)}t=da+4|0;u=da+8|0;v=da+1|0;w=ea+4|0;x=ea+8|0;y=ea+1|0;z=ya+8|0;while(1){if(p>>>0>=B>>>0)break a;xa=c[f>>2]|0;sa=xa+(p*24|0)+12|0;ra=a[sa>>0]|0;j=(ra&1)==0;ra=j?(ra&255)>>>1:c[xa+(p*24|0)+16>>2]|0;$a(da,j?sa+1|0:c[xa+(p*24|0)+20>>2]|0,ra>>>0<2?ra:2);ra=a[da>>0]|0;xa=(ra&1)==0;ra=xa?(ra&255)>>>1:c[t>>2]|0;sa=ra>>>0>2;xa=ac(xa?v:c[u>>2]|0,790,sa?2:ra)|0;Ja(da);j=c[f>>2]|0;if(((xa|0)==0?(ra>>>0<2?-1:sa&1):xa)|0){h=b[j+(p*24|0)+12>>1]|0;if(!(h&1))h=(h&65535)>>>8&255;else h=a[c[j+(p*24|0)+20>>2]>>0]|0;if(h<<24>>24==40){Ya(j+(p*24|0)|0,797)|0;Ta((c[f>>2]|0)+(p*24|0)+12|0,0,799)|0}}else{Ya(j+(p*24|0)|0,849)|0;Ta((c[f>>2]|0)+(p*24|0)+12|0,0,799)|0}j=c[f>>2]|0;h=j+(p*24|0)|0;do if((a[A>>0]|0)==85){ra=a[h>>0]|0;xa=(ra&1)==0;ra=xa?(ra&255)>>>1:c[j+(p*24|0)+4>>2]|0;$a(ea,xa?h+1|0:c[j+(p*24|0)+8>>2]|0,ra>>>0<12?ra:12);ra=a[ea>>0]|0;xa=(ra&1)==0;ra=xa?(ra&255)>>>1:c[w>>2]|0;sa=ra>>>0>12;xa=ac(xa?y:c[x>>2]|0,2054,sa?12:ra)|0;Ja(ea);s=c[f>>2]|0;h=s+(p*24|0)|0;if(!(((xa|0)==0?(ra>>>0<12?-1:sa&1):xa)|0)){j=a[h>>0]|0;if(!(j&1)){o=(j&255)>>>1;r=o;o=o>>>0<11?o:11;k=10}else{o=c[s+(p*24|0)+4>>2]|0;j=c[h>>2]|0;r=o;o=o>>>0<11?o:11;k=(j&-2)+-1|0;j=j&255}if((o-r+k|0)>>>0<2){Wa(h,k,2-o+r-k|0,r,0,o,2,2067);break}if(!(j&1))q=h+1|0;else q=c[s+(p*24|0)+8>>2]|0;do if((o|0)!=2){n=r-o|0;if((r|0)==(o|0)){k=o;m=0;l=2067;j=2;ta=402}else{if(o>>>0>2){a[q>>0]=105;a[q+1>>0]=100;Hc(q+2|0,q+o|0,n|0)|0;k=o;j=2;break}do if(q>>>0<2067>>>0&(q+r|0)>>>0>2067>>>0)if((q+o|0)>>>0>2067>>>0){Fc(q|0,2067,o|0)|0;m=o;l=2069;k=0;j=2-o|0;break}else{m=0;l=2067+(2-o)|0;k=o;j=2;break}else{m=0;l=2067;k=o;j=2}while(0);ta=q+m|0;Hc(ta+j|0,ta+k|0,n|0)|0;ta=402}}else{k=2;m=0;l=2067;j=2;ta=402}while(0);if((ta|0)==402){ta=0;Hc(q+m|0,l|0,j|0)|0}j=j-k+r|0;if(!(a[h>>0]&1))a[h>>0]=j<<1;else c[s+(p*24|0)+4>>2]=j;a[q+j>>0]=0}else ta=385}else ta=385;while(0);if((ta|0)==385){ta=0;Ya(h,4262)|0}m=c[C>>2]|0;n=m+-16|0;o=c[f>>2]|0;q=o+(p*24|0)|0;h=m+-12|0;j=c[h>>2]|0;xa=c[m+-8>>2]|0;k=xa;if((j|0)==(xa|0)){h=c[n>>2]|0;xa=j-h|0;l=(xa|0)/24|0;j=l+1|0;if((xa|0)<-24)break;h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,m+-4|0);xa=c[z>>2]|0;_a(xa,q);_a(xa+12|0,o+(p*24|0)+12|0);c[z>>2]=xa+24;cb(n,ya);bb(ya)}else{_a(j,q);_a(j+12|0,o+(p*24|0)+12|0);c[h>>2]=(c[h>>2]|0)+24}p=p+1|0}Pa();break}case 82:{v=f+4|0;p=((c[v>>2]|0)-(c[f>>2]|0)|0)/24|0;xa=d+1|0;g=Na(xa,e,f)|0;v=((c[v>>2]|0)-(c[f>>2]|0)|0)/24|0;if((g|0)==(xa|0)){g=d;break a}n=f+16|0;o=c[f+12>>2]|0;w=f+20|0;h=c[w>>2]|0;xa=c[f+24>>2]|0;j=xa;if(h>>>0<xa>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=o;c[w>>2]=(c[w>>2]|0)+16}else{k=c[n>>2]|0;xa=h-k|0;m=xa>>4;l=m+1|0;if((xa|0)<-16)Pa();h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ya,h,m,f+28|0);xa=ya+8|0;ta=c[xa>>2]|0;c[ta>>2]=0;c[ta+4>>2]=0;c[ta+8>>2]=0;c[ta+12>>2]=o;c[xa>>2]=ta+16;Ra(n,ya);Sa(ya)}r=U+4|0;s=U+8|0;t=U+1|0;u=ya+8|0;while(1){if(p>>>0>=v>>>0)break a;xa=c[f>>2]|0;ta=xa+(p*24|0)+12|0;sa=a[ta>>0]|0;j=(sa&1)==0;sa=j?(sa&255)>>>1:c[xa+(p*24|0)+16>>2]|0;$a(U,j?ta+1|0:c[xa+(p*24|0)+20>>2]|0,sa>>>0<2?sa:2);sa=a[U>>0]|0;xa=(sa&1)==0;sa=xa?(sa&255)>>>1:c[r>>2]|0;ta=sa>>>0>2;xa=ac(xa?t:c[s>>2]|0,790,ta?2:sa)|0;Ja(U);j=c[f>>2]|0;if(((xa|0)==0?(sa>>>0<2?-1:ta&1):xa)|0){h=b[j+(p*24|0)+12>>1]|0;if(!(h&1))h=(h&65535)>>>8&255;else h=a[c[j+(p*24|0)+20>>2]>>0]|0;if(h<<24>>24==40){Ya(j+(p*24|0)|0,797)|0;Ta((c[f>>2]|0)+(p*24|0)+12|0,0,799)|0}}else{Ya(j+(p*24|0)|0,849)|0;Ta((c[f>>2]|0)+(p*24|0)+12|0,0,799)|0}Ya((c[f>>2]|0)+(p*24|0)|0,852)|0;m=c[w>>2]|0;n=m+-16|0;o=c[f>>2]|0;q=o+(p*24|0)|0;h=m+-12|0;j=c[h>>2]|0;xa=c[m+-8>>2]|0;k=xa;if((j|0)==(xa|0)){h=c[n>>2]|0;xa=j-h|0;l=(xa|0)/24|0;j=l+1|0;if((xa|0)<-24)break;h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,m+-4|0);xa=c[u>>2]|0;_a(xa,q);_a(xa+12|0,o+(p*24|0)+12|0);c[u>>2]=xa+24;cb(n,ya);bb(ya)}else{_a(j,q);_a(j+12|0,o+(p*24|0)+12|0);c[h>>2]=(c[h>>2]|0)+24}p=p+1|0}Pa();break}case 84:{v=f+4|0;s=((c[v>>2]|0)-(c[f>>2]|0)|0)/24|0;g=Eb(d,e,f)|0;t=((c[v>>2]|0)-(c[f>>2]|0)|0)/24|0;if((g|0)==(d|0)){g=d;break a}y=f+16|0;u=f+12|0;n=c[u>>2]|0;x=f+20|0;h=c[x>>2]|0;w=f+24|0;d=c[w>>2]|0;j=d;if(h>>>0<d>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=n;c[x>>2]=(c[x>>2]|0)+16}else{k=c[y>>2]|0;d=h-k|0;m=d>>4;l=m+1|0;if((d|0)<-16)Pa();h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ya,h,m,f+28|0);d=ya+8|0;wa=c[d>>2]|0;c[wa>>2]=0;c[wa+4>>2]=0;c[wa+8>>2]=0;c[wa+12>>2]=n;c[d>>2]=wa+16;Ra(y,ya);Sa(ya)}m=ya+8|0;r=s;while(1){if(r>>>0>=t>>>0)break;n=c[x>>2]|0;o=n+-16|0;p=c[f>>2]|0;q=p+(r*24|0)|0;h=n+-12|0;j=c[h>>2]|0;d=c[n+-8>>2]|0;k=d;if((j|0)==(d|0)){h=c[o>>2]|0;d=j-h|0;l=(d|0)/24|0;j=l+1|0;if((d|0)<-24){ta=455;break}h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,n+-4|0);d=c[m>>2]|0;_a(d,q);_a(d+12|0,p+(r*24|0)+12|0);c[m>>2]=d+24;cb(o,ya);bb(ya)}else{_a(j,q);_a(j+12|0,p+(r*24|0)+12|0);c[h>>2]=(c[h>>2]|0)+24}r=r+1|0}if((ta|0)==455)Pa();if(!((t|0)==(s+1|0)&(a[f+63>>0]|0)!=0))break a;m=Mb(g,e,f)|0;if((m|0)==(g|0))break a;Cb(xa,(c[v>>2]|0)+-24|0);g=c[v>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;d=j+-24|0;c[v>>2]=d;Ia(d);j=c[v>>2]|0}d=a[xa>>0]|0;k=(d&1)==0;Za(g+-48|0,k?xa+1|0:c[xa+8>>2]|0,k?(d&255)>>>1:c[xa+4>>2]|0)|0;g=(c[v>>2]|0)+-24|0;c[V>>2]=c[u>>2];Pb(X,g,V);g=c[x>>2]|0;d=c[w>>2]|0;k=d;if(g>>>0<d>>>0){c[g+12>>2]=c[X+12>>2];c[g>>2]=c[X>>2];ya=X+4|0;c[g+4>>2]=c[ya>>2];f=X+8|0;c[g+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[X>>2]=0;c[x>>2]=(c[x>>2]|0)+16}else{h=c[y>>2]|0;d=g-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();g=k-h|0;if(g>>4>>>0<1073741823){g=g>>3;g=g>>>0<j>>>0?j:g}else g=2147483647;Qa(ya,g,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[X+12>>2];c[d>>2]=c[X>>2];e=X+4|0;c[d+4>>2]=c[e>>2];wa=X+8|0;c[d+8>>2]=c[wa>>2];c[wa>>2]=0;c[e>>2]=0;c[X>>2]=0;c[f>>2]=d+16;Ra(y,ya);Sa(ya)}Ha(X);Ja(xa);g=m;break a}case 85:{g=d+1|0;if((g|0)==(e|0)){g=d;break a}h=qb(g,e,f)|0;if((h|0)==(g|0)){g=d;break a}g=Na(h,e,f)|0;if((g|0)==(h|0)){g=d;break a}n=f+4|0;h=c[n>>2]|0;if(((h-(c[f>>2]|0)|0)/24|0)>>>0<2){g=d;break a}Cb(xa,h+-24|0);h=c[n>>2]|0;j=h+-24|0;k=h;while(1){if((k|0)==(j|0))break;d=k+-24|0;c[n>>2]=d;Ia(d);k=c[n>>2]|0}d=h+-48|0;wa=a[d>>0]|0;e=(wa&1)==0;wa=e?(wa&255)>>>1:c[h+-44>>2]|0;$a(t,e?d+1|0:c[h+-40>>2]|0,wa>>>0<9?wa:9);wa=a[t>>0]|0;d=(wa&1)==0;wa=d?(wa&255)>>>1:c[t+4>>2]|0;e=wa>>>0>9;d=ac(d?t+1|0:c[t+8>>2]|0,2070,e?9:wa)|0;Ja(t);if(!(((d|0)==0?(wa>>>0<9?-1:e&1):d)|0)){Cb(la,(c[n>>2]|0)+-24|0);j=c[n>>2]|0;h=j+-24|0;while(1){if((j|0)==(h|0))break;d=j+-24|0;c[n>>2]=d;Ia(d);j=c[n>>2]|0}d=a[la>>0]|0;e=(d&1)==0;h=la+8|0;j=la+1|0;wa=e?j:c[h>>2]|0;k=la+4|0;d=qb(wa+9|0,wa+(e?(d&255)>>>1:c[k>>2]|0)|0,f)|0;if((d|0)==(((a[la>>0]&1)==0?j:c[h>>2]|0)+9|0)){Ib(N,xa,1882);d=a[la>>0]|0;e=(d&1)==0;h=Za(N,e?j:c[h>>2]|0,e?(d&255)>>>1:c[k>>2]|0)|0;c[M>>2]=c[h>>2];c[M+4>>2]=c[h+4>>2];c[M+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}rb(L,M);h=c[n>>2]|0;d=c[f+8>>2]|0;j=d;if(h>>>0<d>>>0){db(h,L);c[n>>2]=(c[n>>2]|0)+24}else{k=c[f>>2]|0;d=h-k|0;m=(d|0)/24|0;l=m+1|0;if((d|0)<-24)Pa();h=(j-k|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<l>>>0?l:h}else h=2147483647;ab(ya,h,m,f+12|0);d=ya+8|0;e=c[d>>2]|0;db(e,L);c[d>>2]=e+24;cb(f,ya);bb(ya)}Ia(L);Ja(M);Ja(N)}else{k=(c[n>>2]|0)+-24|0;Ib(H,xa,1427);Cb(I,(c[n>>2]|0)+-24|0);h=a[I>>0]|0;j=(h&1)==0;h=Za(H,j?I+1|0:c[I+8>>2]|0,j?(h&255)>>>1:c[I+4>>2]|0)|0;c[G>>2]=c[h>>2];c[G+4>>2]=c[h+4>>2];c[G+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}h=Ya(G,844)|0;c[F>>2]=c[h>>2];c[F+4>>2]=c[h+4>>2];c[F+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}rb(E,F);Db(k,E);Ia(E);Ja(F);Ja(G);Ja(I);Ja(H)}Ja(la)}else{h=(c[n>>2]|0)+-24|0;Ib(x,xa,1882);Cb(y,(c[n>>2]|0)+-24|0);j=a[y>>0]|0;k=(j&1)==0;j=Za(x,k?y+1|0:c[y+8>>2]|0,k?(j&255)>>>1:c[y+4>>2]|0)|0;c[w>>2]=c[j>>2];c[w+4>>2]=c[j+4>>2];c[w+8>>2]=c[j+8>>2];k=0;while(1){if((k|0)==3)break;c[j+(k<<2)>>2]=0;k=k+1|0}rb(v,w);Db(h,v);Ia(v);Ja(w);Ja(y);Ja(x)}m=(c[n>>2]|0)+-24|0;c[R>>2]=c[f+12>>2];Pb(W,m,R);m=f+16|0;h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[W+12>>2];c[j>>2]=c[W>>2];ya=W+4|0;c[j+4>>2]=c[ya>>2];f=W+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[W>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[W+12>>2];c[d>>2]=c[W>>2];e=W+4|0;c[d+4>>2]=c[e>>2];wa=W+8|0;c[d+8>>2]=c[wa>>2];c[wa>>2]=0;c[e>>2]=0;c[W>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(W);Ja(xa);break a}case 83:{wa=d+1|0;if((wa|0)!=(e|0)?(a[wa>>0]|0)==116:0){g=Wb(d,e,f)|0;if((g|0)==(d|0)){g=d;break a}h=c[f+4>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}m=f+16|0;c[n>>2]=c[f+12>>2];Pb(z,h+-24|0,n);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[z+12>>2];c[j>>2]=c[z>>2];ya=z+4|0;c[j+4>>2]=c[ya>>2];f=z+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[z>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[z+12>>2];c[d>>2]=c[z>>2];e=z+4|0;c[d+4>>2]=c[e>>2];xa=z+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[z>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(z);break a}g=Rb(d,e,f)|0;if((g|0)==(d|0)){g=d;break a}m=Mb(g,e,f)|0;if((m|0)==(g|0))break a;k=f+4|0;h=c[k>>2]|0;if(((h-(c[f>>2]|0)|0)/24|0)>>>0<2)break a;Cb(xa,h+-24|0);g=c[k>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;d=j+-24|0;c[k>>2]=d;Ia(d);j=c[k>>2]|0}l=a[xa>>0]|0;h=(l&1)==0;Za(g+-48|0,h?xa+1|0:c[xa+8>>2]|0,h?(l&255)>>>1:c[xa+4>>2]|0)|0;l=(c[k>>2]|0)+-24|0;c[s>>2]=c[f+12>>2];Pb(D,l,s);l=f+16|0;g=f+20|0;h=c[g>>2]|0;d=c[f+24>>2]|0;j=d;if(h>>>0<d>>>0){c[h+12>>2]=c[D+12>>2];c[h>>2]=c[D>>2];ya=D+4|0;c[h+4>>2]=c[ya>>2];f=D+8|0;c[h+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[D>>2]=0;c[g>>2]=(c[g>>2]|0)+16}else{g=c[l>>2]|0;d=h-g|0;k=d>>4;h=k+1|0;if((d|0)<-16)Pa();g=j-g|0;if(g>>4>>>0<1073741823){g=g>>3;g=g>>>0<h>>>0?h:g}else g=2147483647;Qa(ya,g,k,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[D+12>>2];c[d>>2]=c[D>>2];e=D+4|0;c[d+4>>2]=c[e>>2];wa=D+8|0;c[d+8>>2]=c[wa>>2];c[wa>>2]=0;c[e>>2]=0;c[D>>2]=0;c[f>>2]=d+16;Ra(l,ya);Sa(ya)}Ha(D);Ja(xa);g=m;break a}case 68:{g=d+1|0;if((g|0)!=(e|0)){g=a[g>>0]|0;switch(g<<24>>24|0){case 112:{s=f+4|0;p=((c[s>>2]|0)-(c[f>>2]|0)|0)/24|0;xa=d+2|0;g=Na(xa,e,f)|0;s=((c[s>>2]|0)-(c[f>>2]|0)|0)/24|0;if((g|0)==(xa|0))break d;n=f+16|0;o=c[f+12>>2]|0;t=f+20|0;h=c[t>>2]|0;xa=c[f+24>>2]|0;j=xa;if(h>>>0<xa>>>0){c[h>>2]=0;c[h+4>>2]=0;c[h+8>>2]=0;c[h+12>>2]=o;c[t>>2]=(c[t>>2]|0)+16}else{k=c[n>>2]|0;xa=h-k|0;m=xa>>4;l=m+1|0;if((xa|0)<-16)Pa();h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ya,h,m,f+28|0);xa=ya+8|0;ta=c[xa>>2]|0;c[ta>>2]=0;c[ta+4>>2]=0;c[ta+8>>2]=0;c[ta+12>>2]=o;c[xa>>2]=ta+16;Ra(n,ya);Sa(ya)}r=ya+8|0;while(1){if(p>>>0>=s>>>0)break a;m=c[t>>2]|0;n=m+-16|0;o=c[f>>2]|0;q=o+(p*24|0)|0;h=m+-12|0;j=c[h>>2]|0;xa=c[m+-8>>2]|0;k=xa;if((j|0)==(xa|0)){h=c[n>>2]|0;xa=j-h|0;l=(xa|0)/24|0;j=l+1|0;if((xa|0)<-24)break;h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,m+-4|0);xa=c[r>>2]|0;_a(xa,q);_a(xa+12|0,o+(p*24|0)+12|0);c[r>>2]=xa+24;cb(n,ya);bb(ya)}else{_a(j,q);_a(j+12|0,o+(p*24|0)+12|0);c[h>>2]=(c[h>>2]|0)+24}p=p+1|0}Pa();break}case 84:case 116:{g=Qb(d,e,f)|0;if((g|0)==(d|0))break d;h=c[f+4>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}m=f+16|0;c[r>>2]=c[f+12>>2];Pb(C,h+-24|0,r);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[C+12>>2];c[j>>2]=c[C>>2];ya=C+4|0;c[j+4>>2]=c[ya>>2];f=C+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[C>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[C+12>>2];c[d>>2]=c[C>>2];e=C+4|0;c[d+4>>2]=c[e>>2];xa=C+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[C>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(C);break a}case 118:{i:do if((e-d|0)>3&h<<24>>24==68&g<<24>>24==118){l=d+2|0;h=a[l>>0]|0;if((h+-49&255)<9){g=tb(l,e)|0;if((g|0)==(e|0)){g=d;break}if((a[g>>0]|0)!=95){g=d;break}j=g-l|0;h=g+1|0;if((h|0)==(e|0)){g=d;break}if((a[h>>0]|0)!=112){g=Na(h,e,f)|0;if((g|0)==(h|0)){g=d;break}h=c[f+4>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break}k=h+-24|0;$a(ca,l,j);h=Ta(ca,0,2101)|0;c[la>>2]=c[h>>2];c[la+4>>2]=c[h+4>>2];c[la+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}h=Ya(la,4264)|0;c[xa>>2]=c[h>>2];c[xa+4>>2]=c[h+4>>2];c[xa+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}ta=a[xa>>0]|0;sa=(ta&1)==0;Za(k,sa?xa+1|0:c[xa+8>>2]|0,sa?(ta&255)>>>1:c[xa+4>>2]|0)|0;Ja(xa);Ja(la);Ja(ca);break}g=g+2|0;$a(ia,l,j);h=Ta(ia,0,2110)|0;c[sa>>2]=c[h>>2];c[sa+4>>2]=c[h+4>>2];c[sa+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}h=Ya(sa,4264)|0;c[pa>>2]=c[h>>2];c[pa+4>>2]=c[h+4>>2];c[pa+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}rb(fa,pa);h=f+4|0;j=c[h>>2]|0;xa=c[f+8>>2]|0;k=xa;if(j>>>0<xa>>>0){db(j,fa);c[h>>2]=(c[h>>2]|0)+24}else{h=c[f>>2]|0;xa=j-h|0;l=(xa|0)/24|0;j=l+1|0;if((xa|0)<-24)Pa();h=(k-h|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ya,h,l,f+12|0);xa=ya+8|0;ta=c[xa>>2]|0;db(ta,fa);c[xa>>2]=ta+24;cb(f,ya);bb(ya)}Ia(fa);Ja(pa);Ja(sa);Ja(ia);break}g=0;while(1){if((g|0)==3)break;c[ya+(g<<2)>>2]=0;g=g+1|0}j:do if(h<<24>>24!=95?(J=ub(l,e,f)|0,(J|0)!=(l|0)):0){o=f+4|0;g=c[o>>2]|0;if((c[f>>2]|0)!=(g|0)){Cb(oa,g+-24|0);k:do if(!(a[ya>>0]&1)){a[ya+1>>0]=0;a[ya>>0]=0}else{l=ya+8|0;h=c[l>>2]|0;a[h>>0]=0;m=ya+4|0;c[m>>2]=0;g=c[ya>>2]|0;n=(g&-2)+-1|0;j=g&255;do if(!(j&1)){g=g>>>1&127;if((j&255)<22){Fc(ya+1|0,h|0,g+1|0)|0;wc(h);break}h=g+16&240;k=h+-1|0;if((k|0)==(n|0))break k;j=vc(h)|0;if(k>>>0<=n>>>0&(j|0)==0)break k;Fc(j|0,ya+1|0,g+1|0)|0;c[ya>>2]=h|1;c[m>>2]=g;c[l>>2]=j;break k}else{a[ya+1>>0]=0;wc(h);g=0}while(0);a[ya>>0]=g<<1}while(0);c[ya>>2]=c[oa>>2];c[ya+4>>2]=c[oa+4>>2];c[ya+8>>2]=c[oa+8>>2];g=0;while(1){if((g|0)==3)break;c[oa+(g<<2)>>2]=0;g=g+1|0}Ja(oa);h=c[o>>2]|0;g=h+-24|0;while(1){if((h|0)==(g|0)){g=J;ta=622;break j}xa=h+-24|0;c[o>>2]=xa;Ia(xa);h=c[o>>2]|0}}}else{g=l;ta=622}while(0);do if((ta|0)==622){if((((g|0)!=(e|0)?(a[g>>0]|0)==95:0)?(K=g+1|0,(K|0)!=(e|0)):0)?(Q=Na(K,e,f)|0,(Q|0)!=(K|0)):0){g=c[f+4>>2]|0;if((c[f>>2]|0)==(g|0))break;g=g+-24|0;xb(ra,2101,ya);h=Ya(ra,4264)|0;c[qa>>2]=c[h>>2];c[qa+4>>2]=c[h+4>>2];c[qa+8>>2]=c[h+8>>2];j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}xa=a[qa>>0]|0;ta=(xa&1)==0;Za(g,ta?qa+1|0:c[qa+8>>2]|0,ta?(xa&255)>>>1:c[qa+4>>2]|0)|0;Ja(qa);Ja(ra);g=Q}else g=d;Ja(ya);break i}while(0);Ja(ya);g=d}else g=d;while(0);if((g|0)==(d|0))break d;h=c[f+4>>2]|0;if((c[f>>2]|0)==(h|0)){g=d;break a}m=f+16|0;c[ba>>2]=c[f+12>>2];Pb(ga,h+-24|0,ba);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[ga+12>>2];c[j>>2]=c[ga>>2];ya=ga+4|0;c[j+4>>2]=c[ya>>2];f=ga+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[ga>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[ga+12>>2];c[d>>2]=c[ga>>2];e=ga+4|0;c[d+4>>2]=c[e>>2];xa=ga+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[ga>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(ga);break a}default:break d}}break}default:{}}while(0);g=eb(d,e,f)|0;if((g|0)==(d|0)){g=Wb(d,e,f)|0;if((g|0)!=(d|0)?(ua=c[f+4>>2]|0,(c[f>>2]|0)!=(ua|0)):0){m=f+16|0;c[va>>2]=c[f+12>>2];Pb(wa,ua+-24|0,va);h=f+20|0;j=c[h>>2]|0;d=c[f+24>>2]|0;k=d;if(j>>>0<d>>>0){c[j+12>>2]=c[wa+12>>2];c[j>>2]=c[wa>>2];ya=wa+4|0;c[j+4>>2]=c[ya>>2];f=wa+8|0;c[j+8>>2]=c[f>>2];c[f>>2]=0;c[ya>>2]=0;c[wa>>2]=0;c[h>>2]=(c[h>>2]|0)+16}else{h=c[m>>2]|0;d=j-h|0;l=d>>4;j=l+1|0;if((d|0)<-16)Pa();h=k-h|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<j>>>0?j:h}else h=2147483647;Qa(ya,h,l,f+28|0);f=ya+8|0;d=c[f>>2]|0;c[d+12>>2]=c[wa+12>>2];c[d>>2]=c[wa>>2];e=wa+4|0;c[d+4>>2]=c[e>>2];xa=wa+8|0;c[d+8>>2]=c[xa>>2];c[xa>>2]=0;c[e>>2]=0;c[wa>>2]=0;c[f>>2]=d+16;Ra(m,ya);Sa(ya)}Ha(wa)}else g=d}}}else g=d;while(0);i=za;return g|0}function Oa(b,d,e){b=b|0;d=d|0;e=e|0;var f=0;c[e>>2]=0;if((b|0)!=(d|0)){d=a[b>>0]|0;if(d<<24>>24==114){c[e>>2]=4;d=b+1|0;b=d;d=a[d>>0]|0;f=4}else f=0;if(d<<24>>24==86){f=f|2;c[e>>2]=f;d=b+1|0;b=d;d=a[d>>0]|0}if(d<<24>>24==75){c[e>>2]=f|1;b=b+1|0}}return b|0}function Pa(){oa(120,143,303,246)}function Qa(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;c[a+12>>2]=0;c[a+16>>2]=e;if(!b)e=0;else e=Da(c[e>>2]|0,b<<4)|0;c[a>>2]=e;d=e+(d<<4)|0;c[a+8>>2]=d;c[a+4>>2]=d;c[a+12>>2]=e+(b<<4);return}function Ra(a,b){a=a|0;b=b|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;e=c[a>>2]|0;f=a+4|0;g=b+4|0;d=c[f>>2]|0;while(1){if((d|0)==(e|0))break;k=c[g>>2]|0;i=k+-16|0;h=d+-16|0;c[i>>2]=0;j=k+-12|0;c[j>>2]=0;l=c[d+-4>>2]|0;c[k+-8>>2]=0;c[k+-4>>2]=l;c[i>>2]=c[h>>2];i=d+-12|0;c[j>>2]=c[i>>2];j=d+-8|0;c[k+-8>>2]=c[j>>2];c[j>>2]=0;c[i>>2]=0;c[h>>2]=0;c[g>>2]=(c[g>>2]|0)+-16;d=h}j=c[a>>2]|0;c[a>>2]=c[g>>2];c[g>>2]=j;j=b+8|0;l=c[f>>2]|0;c[f>>2]=c[j>>2];c[j>>2]=l;j=a+8|0;l=b+12|0;k=c[j>>2]|0;c[j>>2]=c[l>>2];c[l>>2]=k;c[b>>2]=c[g>>2];return}function Sa(a){a=a|0;var b=0,d=0,e=0;b=c[a+4>>2]|0;d=a+8|0;while(1){e=c[d>>2]|0;if((e|0)==(b|0))break;e=e+-16|0;c[d>>2]=e;Ha(e)}b=c[a>>2]|0;if(b)Ka(c[c[a+16>>2]>>2]|0,b,(c[a+12>>2]|0)-b|0);return}function Ta(a,b,c){a=a|0;b=b|0;c=c|0;return Ua(a,b,c,bc(c)|0)|0}function Ua(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0;g=a[b>>0]|0;h=(g&1)==0;if(h)i=(g&255)>>>1;else i=c[b+4>>2]|0;if(i>>>0<d>>>0)Va();if(h)h=10;else{g=c[b>>2]|0;h=(g&-2)+-1|0;g=g&255}if((h-i|0)>>>0>=f>>>0){if(f){if(!(g&1))h=b+1|0;else h=c[b+8>>2]|0;if((i|0)==(d|0))g=h+d|0;else{g=h+d|0;Hc(g+f|0,g|0,i-d|0)|0;e=g>>>0<=e>>>0&(h+i|0)>>>0>e>>>0?e+f|0:e}Hc(g|0,e|0,f|0)|0;g=i+f|0;if(!(a[b>>0]&1))a[b>>0]=g<<1;else c[b+4>>2]=g;a[h+g>>0]=0}}else Wa(b,h,i+f-h|0,i,d,0,f,e);return b|0}function Va(){oa(274,303,1175,406)}function Wa(b,d,e,f,g,h,i,j){b=b|0;d=d|0;e=e|0;f=f|0;g=g|0;h=h|0;i=i|0;j=j|0;var k=0,l=0,m=0;if((-18-d|0)>>>0<e>>>0)Xa();if(!(a[b>>0]&1))m=b+1|0;else m=c[b+8>>2]|0;if(d>>>0<2147483623){k=e+d|0;l=d<<1;k=k>>>0<l>>>0?l:k;k=k>>>0<11?11:k+16&-16}else k=-17;l=vc(k)|0;if(g)Fc(l|0,m|0,g|0)|0;if(i)Fc(l+g|0,j|0,i|0)|0;e=f-h|0;if((e|0)!=(g|0))Fc(l+g+i|0,m+g+h|0,e-g|0)|0;if((d|0)!=10)wc(m);c[b+8>>2]=l;c[b>>2]=k|1;d=e+i|0;c[b+4>>2]=d;a[l+d>>0]=0;return}function Xa(){oa(427,303,1164,246)}function Ya(a,b){a=a|0;b=b|0;return Za(a,b,bc(b)|0)|0}function Za(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0;f=a[b>>0]|0;if(!(f&1))h=10;else{f=c[b>>2]|0;h=(f&-2)+-1|0;f=f&255}g=(f&1)==0;if(g)f=(f&255)>>>1;else f=c[b+4>>2]|0;if((h-f|0)>>>0>=e>>>0){if(e){if(g)g=b+1|0;else g=c[b+8>>2]|0;Fc(g+f|0,d|0,e|0)|0;f=f+e|0;if(!(a[b>>0]&1))a[b>>0]=f<<1;else c[b+4>>2]=f;a[g+f>>0]=0}}else Wa(b,h,e-h+f|0,f,f,0,e,d);return b|0}function _a(b,d){b=b|0;d=d|0;if(!(a[d>>0]&1)){c[b>>2]=c[d>>2];c[b+4>>2]=c[d+4>>2];c[b+8>>2]=c[d+8>>2]}else $a(b,c[d+8>>2]|0,c[d+4>>2]|0);return}function $a(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0;if(e>>>0>4294967279)Xa();if(e>>>0<11){a[b>>0]=e<<1;b=b+1|0}else{g=e+16&-16;f=vc(g)|0;c[b+8>>2]=f;c[b>>2]=g|1;c[b+4>>2]=e;b=f}Fc(b|0,d|0,e|0)|0;a[b+e>>0]=0;return}function ab(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;c[a+12>>2]=0;c[a+16>>2]=e;if(!b)e=0;else e=Da(c[e>>2]|0,b*24|0)|0;c[a>>2]=e;d=e+(d*24|0)|0;c[a+8>>2]=d;c[a+4>>2]=d;c[a+12>>2]=e+(b*24|0);return}function bb(a){a=a|0;var b=0,d=0,e=0;b=c[a+4>>2]|0;d=a+8|0;while(1){e=c[d>>2]|0;if((e|0)==(b|0))break;e=e+-24|0;c[d>>2]=e;Ia(e)}b=c[a>>2]|0;if(b)Ka(c[c[a+16>>2]>>2]|0,b,(c[a+12>>2]|0)-b|0);return}function cb(a,b){a=a|0;b=b|0;var d=0,e=0,f=0,g=0,h=0;e=c[a>>2]|0;f=a+4|0;g=b+4|0;d=c[f>>2]|0;while(1){if((d|0)==(e|0))break;h=d+-24|0;db((c[g>>2]|0)+-24|0,h);c[g>>2]=(c[g>>2]|0)+-24;d=h}h=c[a>>2]|0;c[a>>2]=c[g>>2];c[g>>2]=h;h=b+8|0;e=c[f>>2]|0;c[f>>2]=c[h>>2];c[h>>2]=e;f=a+8|0;h=b+12|0;a=c[f>>2]|0;c[f>>2]=c[h>>2];c[h>>2]=a;c[b>>2]=c[g>>2];return}function db(a,b){a=a|0;b=b|0;var d=0;c[a>>2]=c[b>>2];c[a+4>>2]=c[b+4>>2];c[a+8>>2]=c[b+8>>2];d=0;while(1){if((d|0)==3)break;c[b+(d<<2)>>2]=0;d=d+1|0}d=a+12|0;b=b+12|0;c[d>>2]=c[b>>2];c[d+4>>2]=c[b+4>>2];c[d+8>>2]=c[b+8>>2];d=0;while(1){if((d|0)==3)break;c[b+(d<<2)>>2]=0;d=d+1|0}return}function eb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0;N=i;i=i+720|0;M=N+696|0;j=N+672|0;J=N+648|0;s=N+624|0;u=N+600|0;v=N+576|0;w=N+552|0;x=N+528|0;y=N+504|0;z=N+480|0;A=N+456|0;k=N+432|0;l=N+408|0;m=N+384|0;L=N+360|0;n=N+336|0;o=N+312|0;p=N+288|0;K=N+264|0;q=N+240|0;r=N+216|0;t=N+192|0;B=N+168|0;C=N+144|0;D=N+120|0;E=N+96|0;F=N+72|0;G=N+48|0;H=N+24|0;I=N;a:do if((b|0)!=(d|0))do switch(a[b>>0]|0){case 118:{fb(j,476);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,j);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,j);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(j);b=b+1|0;break a}case 119:{a[J>>0]=14;f=J+1|0;a[f>>0]=a[481]|0;a[f+1>>0]=a[482]|0;a[f+2>>0]=a[483]|0;a[f+3>>0]=a[484]|0;a[f+4>>0]=a[485]|0;a[f+5>>0]=a[486]|0;a[f+6>>0]=a[487]|0;a[J+8>>0]=0;f=J+12|0;d=0;while(1){if((d|0)==3)break;c[f+(d<<2)>>2]=0;d=d+1|0}f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,J);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,J);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(J);b=b+1|0;break a}case 98:{fb(s,489);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,s);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,s);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(s);b=b+1|0;break a}case 99:{fb(u,494);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,u);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,u);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(u);b=b+1|0;break a}case 97:{gb(v,499);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,v);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,v);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(v);b=b+1|0;break a}case 104:{hb(w,511);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,w);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,w);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(w);b=b+1|0;break a}case 115:{ib(x,525);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,x);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,x);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(x);b=b+1|0;break a}case 116:{jb(y,531);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,y);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,y);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(y);b=b+1|0;break a}case 105:{kb(z,546);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,z);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,z);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(z);b=b+1|0;break a}case 106:{lb(A,550);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,A);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,A);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(A);b=b+1|0;break a}case 108:{fb(k,563);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,k);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,k);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(k);b=b+1|0;break a}case 109:{hb(l,568);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,l);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,l);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(l);b=b+1|0;break a}case 120:{mb(m,582);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,m);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,m);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(m);b=b+1|0;break a}case 121:{f=vc(32)|0;c[L+8>>2]=f;c[L>>2]=33;c[L+4>>2]=18;d=f;g=592;h=d+18|0;do{a[d>>0]=a[g>>0]|0;d=d+1|0;g=g+1|0}while((d|0)<(h|0));a[f+18>>0]=0;f=L+12|0;d=0;while(1){if((d|0)==3)break;c[f+(d<<2)>>2]=0;d=d+1|0}f=e+4|0;d=c[f>>2]|0;K=c[e+8>>2]|0;g=K;if(d>>>0<K>>>0){db(d,L);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;K=d-f|0;h=(K|0)/24|0;d=h+1|0;if((K|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);K=M+8|0;J=c[K>>2]|0;db(J,L);c[K>>2]=J+24;cb(e,M);bb(M)}Ia(L);b=b+1|0;break a}case 110:{nb(n,611);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,n);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,n);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(n);b=b+1|0;break a}case 111:{ob(o,620);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,o);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,o);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(o);b=b+1|0;break a}case 102:{ib(p,638);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,p);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,p);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(p);b=b+1|0;break a}case 100:{a[K>>0]=12;f=K+1|0;a[f>>0]=a[644]|0;a[f+1>>0]=a[645]|0;a[f+2>>0]=a[646]|0;a[f+3>>0]=a[647]|0;a[f+4>>0]=a[648]|0;a[f+5>>0]=a[649]|0;a[K+7>>0]=0;f=K+12|0;d=0;while(1){if((d|0)==3)break;c[f+(d<<2)>>2]=0;d=d+1|0}f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,K);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;J=c[L>>2]|0;db(J,K);c[L>>2]=J+24;cb(e,M);bb(M)}Ia(K);b=b+1|0;break a}case 101:{gb(q,651);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,q);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,q);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(q);b=b+1|0;break a}case 103:{pb(r,663);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,r);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,r);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(r);b=b+1|0;break a}case 122:{kb(t,674);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,t);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,t);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(t);b=b+1|0;break a}case 117:{M=b+1|0;e=qb(M,d,e)|0;b=(e|0)==(M|0)?b:e;break a}case 68:{f=b+1|0;if((f|0)==(d|0))break a;switch(a[f>>0]|0){case 100:{mb(B,711);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,B);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,B);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(B);b=b+2|0;break a}case 101:{pb(C,721);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,C);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,C);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(C);b=b+2|0;break a}case 102:{mb(D,732);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,D);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,D);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(D);b=b+2|0;break a}case 104:{mb(E,742);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,E);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,E);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(E);b=b+2|0;break a}case 105:{nb(F,752);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,F);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,F);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(F);b=b+2|0;break a}case 115:{nb(G,761);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,G);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,G);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(G);b=b+2|0;break a}case 97:{fb(H,770);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,H);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,H);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(H);b=b+2|0;break a}case 110:{jb(I,775);f=e+4|0;d=c[f>>2]|0;L=c[e+8>>2]|0;g=L;if(d>>>0<L>>>0){db(d,I);c[f>>2]=(c[f>>2]|0)+24}else{f=c[e>>2]|0;L=d-f|0;h=(L|0)/24|0;d=h+1|0;if((L|0)<-24)Pa();f=(g-f|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<d>>>0?d:f}else f=2147483647;ab(M,f,h,e+12|0);L=M+8|0;K=c[L>>2]|0;db(K,I);c[L>>2]=K+24;cb(e,M);bb(M)}Ia(I);b=b+2|0;break a}default:break a}}default:break a}while(0);while(0);i=N;return b|0}function fb(b,e){b=b|0;e=e|0;var f=0;a[b>>0]=8;f=b+1|0;e=d[e>>0]|d[e+1>>0]<<8|d[e+2>>0]<<16|d[e+3>>0]<<24;a[f>>0]=e;a[f+1>>0]=e>>8;a[f+2>>0]=e>>16;a[f+3>>0]=e>>24;a[b+5>>0]=0;e=b+12|0;b=0;while(1){if((b|0)==3)break;c[e+(b<<2)>>2]=0;b=b+1|0}return}function gb(a,b){a=a|0;b=b|0;$a(a,b,11);b=a+12|0;a=0;while(1){if((a|0)==3)break;c[b+(a<<2)>>2]=0;a=a+1|0}return}function hb(a,b){a=a|0;b=b|0;$a(a,b,13);b=a+12|0;a=0;while(1){if((a|0)==3)break;c[b+(a<<2)>>2]=0;a=a+1|0}return}function ib(b,d){b=b|0;d=d|0;var e=0;a[b>>0]=10;e=b+1|0;a[e>>0]=a[d>>0]|0;a[e+1>>0]=a[d+1>>0]|0;a[e+2>>0]=a[d+2>>0]|0;a[e+3>>0]=a[d+3>>0]|0;a[e+4>>0]=a[d+4>>0]|0;a[b+6>>0]=0;d=b+12|0;b=0;while(1){if((b|0)==3)break;c[d+(b<<2)>>2]=0;b=b+1|0}return}function jb(a,b){a=a|0;b=b|0;$a(a,b,14);b=a+12|0;a=0;while(1){if((a|0)==3)break;c[b+(a<<2)>>2]=0;a=a+1|0}return}function kb(b,d){b=b|0;d=d|0;var e=0;a[b>>0]=6;e=b+1|0;a[e>>0]=a[d>>0]|0;a[e+1>>0]=a[d+1>>0]|0;a[e+2>>0]=a[d+2>>0]|0;a[b+4>>0]=0;d=b+12|0;b=0;while(1){if((b|0)==3)break;c[d+(b<<2)>>2]=0;b=b+1|0}return}function lb(a,b){a=a|0;b=b|0;$a(a,b,12);b=a+12|0;a=0;while(1){if((a|0)==3)break;c[b+(a<<2)>>2]=0;a=a+1|0}return}function mb(b,d){b=b|0;d=d|0;var e=0,f=0;a[b>>0]=18;f=b+1|0;e=f+9|0;do{a[f>>0]=a[d>>0]|0;f=f+1|0;d=d+1|0}while((f|0)<(e|0));a[b+10>>0]=0;d=b+12|0;e=0;while(1){if((e|0)==3)break;c[d+(e<<2)>>2]=0;e=e+1|0}return}function nb(b,e){b=b|0;e=e|0;var f=0,g=0,h=0;a[b>>0]=16;f=e;h=f;h=d[h>>0]|d[h+1>>0]<<8|d[h+2>>0]<<16|d[h+3>>0]<<24;f=f+4|0;f=d[f>>0]|d[f+1>>0]<<8|d[f+2>>0]<<16|d[f+3>>0]<<24;e=b+1|0;g=e;a[g>>0]=h;a[g+1>>0]=h>>8;a[g+2>>0]=h>>16;a[g+3>>0]=h>>24;e=e+4|0;a[e>>0]=f;a[e+1>>0]=f>>8;a[e+2>>0]=f>>16;a[e+3>>0]=f>>24;a[b+9>>0]=0;e=b+12|0;b=0;while(1){if((b|0)==3)break;c[e+(b<<2)>>2]=0;b=b+1|0}return}function ob(a,b){a=a|0;b=b|0;$a(a,b,17);b=a+12|0;a=0;while(1){if((a|0)==3)break;c[b+(a<<2)>>2]=0;a=a+1|0}return}function pb(b,d){b=b|0;d=d|0;var e=0,f=0;a[b>>0]=20;f=b+1|0;e=f+10|0;do{a[f>>0]=a[d>>0]|0;f=f+1|0;d=d+1|0}while((f|0)<(e|0));a[b+11>>0]=0;d=b+12|0;e=0;while(1){if((e|0)==3)break;c[d+(e<<2)>>2]=0;e=e+1|0}return}function qb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0;q=i;i=i+112|0;o=q+88|0;p=q+64|0;h=q+76|0;l=q+40|0;j=q+16|0;k=q;a:do if(((b|0)!=(d|0)?(g=(a[b>>0]|0)+-48|0,g>>>0<10):0)?(f=b+1|0,(f|0)!=(d|0)):0){m=f;n=g;while(1){g=(a[m>>0]|0)+-48|0;if(g>>>0>=10)break;f=m+1|0;if((f|0)==(d|0))break a;m=f;n=g+(n*10|0)|0}if((d-m|0)>>>0>=n>>>0){$a(p,m,n);f=a[p>>0]|0;d=(f&1)==0;f=d?(f&255)>>>1:c[p+4>>2]|0;$a(h,d?p+1|0:c[p+8>>2]|0,f>>>0<10?f:10);f=a[h>>0]|0;d=(f&1)==0;f=d?(f&255)>>>1:c[h+4>>2]|0;g=f>>>0>10;d=ac(d?h+1|0:c[h+8>>2]|0,678,g?10:f)|0;Ja(h);if(!(((d|0)==0?(f>>>0<10?-1:g&1):d)|0)){b=vc(32)|0;c[l+8>>2]=b;c[l>>2]=33;c[l+4>>2]=21;f=b;g=689;h=f+21|0;do{a[f>>0]=a[g>>0]|0;f=f+1|0;g=g+1|0}while((f|0)<(h|0));a[b+21>>0]=0;b=l+12|0;f=0;while(1){if((f|0)==3)break;c[b+(f<<2)>>2]=0;f=f+1|0}b=e+4|0;f=c[b>>2]|0;k=c[e+8>>2]|0;g=k;if(f>>>0<k>>>0){db(f,l);c[b>>2]=(c[b>>2]|0)+24}else{b=c[e>>2]|0;k=f-b|0;h=(k|0)/24|0;f=h+1|0;if((k|0)<-24)Pa();b=(g-b|0)/24|0;if(b>>>0<1073741823){b=b<<1;b=b>>>0<f>>>0?f:b}else b=2147483647;ab(o,b,h,e+12|0);k=o+8|0;j=c[k>>2]|0;db(j,l);c[k>>2]=j+24;cb(e,o);bb(o)}Ia(l)}else{c[k>>2]=c[p>>2];c[k+4>>2]=c[p+4>>2];c[k+8>>2]=c[p+8>>2];b=0;while(1){if((b|0)==3)break;c[p+(b<<2)>>2]=0;b=b+1|0}rb(j,k);b=e+4|0;f=c[b>>2]|0;l=c[e+8>>2]|0;g=l;if(f>>>0<l>>>0){db(f,j);c[b>>2]=(c[b>>2]|0)+24}else{b=c[e>>2]|0;l=f-b|0;h=(l|0)/24|0;f=h+1|0;if((l|0)<-24)Pa();b=(g-b|0)/24|0;if(b>>>0<1073741823){b=b<<1;b=b>>>0<f>>>0?f:b}else b=2147483647;ab(o,b,h,e+12|0);l=o+8|0;d=c[l>>2]|0;db(d,j);c[l>>2]=d+24;cb(e,o);bb(o)}Ia(j);Ja(k)}Ja(p);b=m+n|0}}while(0);i=q;return b|0}function rb(a,b){a=a|0;b=b|0;var d=0;c[a>>2]=c[b>>2];c[a+4>>2]=c[b+4>>2];c[a+8>>2]=c[b+8>>2];d=0;while(1){if((d|0)==3)break;c[b+(d<<2)>>2]=0;d=d+1|0}d=a+12|0;b=0;while(1){if((b|0)==3)break;c[d+(b<<2)>>2]=0;b=b+1|0}return}function sb(b){b=b|0;var d=0,e=0,f=0,g=0,h=0;d=a[b>>0]|0;if(!(d&1)){e=(d&255)>>>1;h=b+1|0}else{e=c[b+4>>2]|0;h=c[b+8>>2]|0}f=(e|0)!=0&1;g=e-f|0;if((e|0)!=(f|0)){Hc(h|0,h+f|0,g|0)|0;d=a[b>>0]|0}if(!(d&1))a[b>>0]=g<<1;else c[b+4>>2]=g;a[h+g>>0]=0;return}function tb(b,c){b=b|0;c=c|0;var d=0,e=0;a:do if((b|0)!=(c|0)?(d=(a[b>>0]|0)==110?b+1|0:b,(d|0)!=(c|0)):0){e=a[d>>0]|0;if(e<<24>>24==48){d=d+1|0;break}if((e+-49&255)<9)do{d=d+1|0;if((d|0)==(c|0)){d=c;break a}}while(((a[d>>0]|0)+-48|0)>>>0<10);else d=b}else d=b;while(0);return d|0}
+function Sb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0;s=i;i=i+112|0;r=s+88|0;m=s+64|0;n=s+48|0;l=s+24|0;o=s+12|0;p=s;a:do if((b|0)!=(d|0)){g=a[b>>0]|0;h=g<<24>>24;switch(h|0){case 68:case 67:{b:do if((d-b|0)>1?(k=e+4|0,f=c[k>>2]|0,(c[e>>2]|0)!=(f|0)):0){switch(h|0){case 67:{switch(a[b+1>>0]|0){case 53:case 51:case 50:case 49:break;default:break b}Tb(n,f+-24|0);rb(m,n);f=c[k>>2]|0;d=c[e+8>>2]|0;j=d;if(f>>>0<d>>>0){db(f,m);c[k>>2]=(c[k>>2]|0)+24}else{g=c[e>>2]|0;d=f-g|0;k=(d|0)/24|0;h=k+1|0;if((d|0)<-24)Pa();f=(j-g|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<h>>>0?h:f}else f=2147483647;ab(r,f,k,e+12|0);d=r+8|0;q=c[d>>2]|0;db(q,m);c[d>>2]=q+24;cb(e,r);bb(r)}Ia(m);Ja(n);a[e+60>>0]=1;b=b+2|0;break b}case 68:break;default:break b}switch(a[b+1>>0]|0){case 53:case 50:case 49:case 48:break;default:break b}Tb(p,f+-24|0);f=Ta(p,0,886)|0;c[o>>2]=c[f>>2];c[o+4>>2]=c[f+4>>2];c[o+8>>2]=c[f+8>>2];g=0;while(1){if((g|0)==3)break;c[f+(g<<2)>>2]=0;g=g+1|0}rb(l,o);f=c[k>>2]|0;d=c[e+8>>2]|0;j=d;if(f>>>0<d>>>0){db(f,l);c[k>>2]=(c[k>>2]|0)+24}else{g=c[e>>2]|0;d=f-g|0;k=(d|0)/24|0;h=k+1|0;if((d|0)<-24)Pa();f=(j-g|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<h>>>0?h:f}else f=2147483647;ab(r,f,k,e+12|0);d=r+8|0;q=c[d>>2]|0;db(q,l);c[d>>2]=q+24;cb(e,r);bb(r)}Ia(l);Ja(o);Ja(p);a[e+60>>0]=1;b=b+2|0}while(0);break a}case 85:{c:do if((d-b|0)>2&g<<24>>24==85){switch(a[b+1>>0]|0){case 116:{$a(n,1808,8);rb(m,n);l=e+4|0;f=c[l>>2]|0;q=c[e+8>>2]|0;g=q;if(f>>>0<q>>>0){db(f,m);c[l>>2]=(c[l>>2]|0)+24}else{h=c[e>>2]|0;q=f-h|0;k=(q|0)/24|0;j=k+1|0;if((q|0)<-24)Pa();f=(g-h|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(r,f,k,e+12|0);q=r+8|0;p=c[q>>2]|0;db(p,m);c[q>>2]=p+24;cb(e,r);bb(r)}Ia(m);Ja(n);f=b+2|0;if((f|0)==(d|0)){g=c[l>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;d=g+-24|0;c[l>>2]=d;Ia(d);g=c[l>>2]|0}}if(((a[f>>0]|0)+-48|0)>>>0<10){g=b+3|0;while(1){if((g|0)==(d|0)){g=d;break}if(((a[g>>0]|0)+-48|0)>>>0>=10)break;g=g+1|0}Bb((c[l>>2]|0)+-24|0,f,g);f=g}zb((c[l>>2]|0)+-24|0,39);if((f|0)!=(d|0)?(a[f>>0]|0)==95:0){b=f+1|0;break c}g=c[l>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;d=g+-24|0;c[l>>2]=d;Ia(d);g=c[l>>2]|0}}case 108:break;default:break c}$a(o,1817,9);rb(l,o);q=e+4|0;f=c[q>>2]|0;n=c[e+8>>2]|0;g=n;if(f>>>0<n>>>0){db(f,l);c[q>>2]=(c[q>>2]|0)+24}else{h=c[e>>2]|0;n=f-h|0;k=(n|0)/24|0;j=k+1|0;if((n|0)<-24)Pa();f=(g-h|0)/24|0;if(f>>>0<1073741823){f=f<<1;f=f>>>0<j>>>0?j:f}else f=2147483647;ab(r,f,k,e+12|0);n=r+8|0;m=c[n>>2]|0;db(m,l);c[n>>2]=m+24;cb(e,r);bb(r)}Ia(l);Ja(o);f=b+2|0;do if((a[f>>0]|0)!=118){g=Na(f,d,e)|0;if((g|0)==(f|0)){g=c[q>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;d=g+-24|0;c[q>>2]=d;Ia(d);g=c[q>>2]|0}}f=c[q>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2)break c;Cb(r,f+-24|0);j=c[q>>2]|0;f=j+-24|0;h=j;while(1){if((h|0)==(f|0))break;o=h+-24|0;c[q>>2]=o;Ia(o);h=c[q>>2]|0}h=a[r>>0]|0;l=(h&1)==0;m=r+8|0;n=r+1|0;o=r+4|0;Za(j+-48|0,l?n:c[m>>2]|0,l?(h&255)>>>1:c[o>>2]|0)|0;while(1){l=Na(g,d,e)|0;if((l|0)==(g|0)){f=91;break}f=c[q>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2){f=129;break}Cb(p,f+-24|0);d:do if(!(h&1)){a[n>>0]=0;a[r>>0]=0}else{g=c[m>>2]|0;a[g>>0]=0;c[o>>2]=0;f=c[r>>2]|0;k=(f&-2)+-1|0;h=f&255;do if(!(h&1)){f=f>>>1&127;if((h&255)<22){Fc(n|0,g|0,f+1|0)|0;wc(g);break}g=f+16&240;j=g+-1|0;if((j|0)==(k|0))break d;h=vc(g)|0;if(j>>>0<=k>>>0&(h|0)==0)break d;Fc(h|0,n|0,f+1|0)|0;c[r>>2]=g|1;c[o>>2]=f;c[m>>2]=h;break d}else{a[n>>0]=0;wc(g);f=0}while(0);a[r>>0]=f<<1}while(0);c[r>>2]=c[p>>2];c[r+4>>2]=c[p+4>>2];c[r+8>>2]=c[p+8>>2];f=0;while(1){if((f|0)==3)break;c[p+(f<<2)>>2]=0;f=f+1|0}Ja(p);j=c[q>>2]|0;f=j+-24|0;g=j;while(1){if((g|0)==(f|0))break;k=g+-24|0;c[q>>2]=k;Ia(k);g=c[q>>2]|0}h=a[r>>0]|0;f=(h&1)==0;g=f?(h&255)>>>1:c[o>>2]|0;if(!g){g=l;continue}Ya(j+-48|0,1429)|0;Za((c[q>>2]|0)+-24|0,f?n:c[m>>2]|0,g)|0;g=l}if((f|0)==91){Ya((c[q>>2]|0)+-24|0,799)|0;Ja(r);break}else if((f|0)==129){Ja(r);break c}}else{zb((c[q>>2]|0)+-24|0,41);g=b+3|0}while(0);if((g|0)!=(d|0)?(a[g>>0]|0)==69:0){f=g+1|0;if((f|0)==(d|0)){g=c[q>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;d=g+-24|0;c[q>>2]=d;Ia(d);g=c[q>>2]|0}}e:do if(((a[f>>0]|0)+-48|0)>>>0<10){g=g+2|0;while(1){if((g|0)==(d|0)){g=d;break}if(((a[g>>0]|0)+-48|0)>>>0>=10)break;g=g+1|0}p=c[q>>2]|0;e=p+-24|0;h=a[e>>0]|0;l=p+-16|0;if(!(h&1)){j=l;k=e+1|0;o=(h&255)>>>1;m=10}else{k=c[l>>2]|0;h=c[e>>2]|0;j=k+7|0;o=c[p+-20>>2]|0;m=(h&-2)+-1|0;h=h&255}n=j-k|0;k=g-f|0;if((g|0)!=(f|0)){if((m-o|0)>>>0>=k>>>0){if(!(h&1))h=e+1|0;else h=c[l>>2]|0;if((o|0)==(n|0))j=h;else{j=h+n|0;Hc(j+k|0,j|0,o-n|0)|0;j=h}}else{Ab(e,m,o+k-m|0,o,n,k);j=c[l>>2]|0}h=o+k|0;if(!(a[e>>0]&1))a[e>>0]=h<<1;else c[p+-20>>2]=h;a[j+h>>0]=0;h=j+n|0;while(1){if((f|0)==(g|0)){f=g;break e}a[h>>0]=a[f>>0]|0;f=f+1|0;h=h+1|0}}}while(0);if((f|0)!=(d|0)?(a[f>>0]|0)==95:0){b=f+1|0;break}g=c[q>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;d=g+-24|0;c[q>>2]=d;Ia(d);g=c[q>>2]|0}}g=c[q>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;d=g+-24|0;c[q>>2]=d;Ia(d);g=c[q>>2]|0}}while(0);break a}case 57:case 56:case 55:case 54:case 53:case 52:case 51:case 50:case 49:{b=qb(b,d,e)|0;break a}default:{d=Lb(b,d,e)|0;i=s;return d|0}}}while(0);i=s;return b|0}function Tb(b,d){b=b|0;d=d|0;var e=0,f=0,g=0,h=0,i=0;h=a[d>>0]|0;e=(h&1)==0;h=e?(h&255)>>>1:c[d+4>>2]|0;a:do if(!h)_a(b,d);else{f=e?d+1|0:c[d+8>>2]|0;e=h>>>0>11;g=ac(f,1478,e?11:h)|0;if(!(((g|0)==0?(h>>>0<11?-1:e&1):g)|0)){Ub(d,1530,70);$a(b,1601,12);break}e=h>>>0>12;g=e?12:h;i=ac(f,1490,g)|0;e=h>>>0<12?-1:e&1;if(!(((i|0)==0?e:i)|0)){Ub(d,1614,49);$a(b,1664,13);break}i=ac(f,1503,g)|0;if(!(((i|0)==0?e:i)|0)){Ub(d,1678,49);$a(b,1728,13);break}g=h>>>0>13;i=ac(f,1516,g?13:h)|0;if(!(((i|0)==0?(h>>>0<13?-1:g&1):i)|0)){Ub(d,1742,50);$a(b,1793,14);break}e=f+h|0;b:do if((a[e+-1>>0]|0)==62){g=1;c:while(1){h=e;d:while(1){e=h+-1|0;if((e|0)==(f|0))break c;h=h+-2|0;switch(a[h>>0]|0){case 60:{d=18;break d}case 62:{d=19;break d}default:h=e}}if((d|0)==18){g=g+-1|0;if(!g){e=h;break b}else continue}else if((d|0)==19){g=g+1|0;continue}}e=0;while(1){if((e|0)==3)break a;c[b+(e<<2)>>2]=0;e=e+1|0}}while(0);h=e;while(1){g=h+-1|0;if((g|0)==(f|0))break;if((a[g>>0]|0)==58){f=h;break}else h=g}d=e-f|0;if(d>>>0>4294967279)Xa();if(d>>>0<11){a[b>>0]=d<<1;h=b+1|0}else{i=d+16&-16;h=vc(i)|0;c[b+8>>2]=h;c[b>>2]=i|1;c[b+4>>2]=d}g=h;while(1){if((f|0)==(e|0))break;a[g>>0]=a[f>>0]|0;f=f+1|0;g=g+1|0}a[h+d>>0]=0}while(0);return}function Ub(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0;f=a[b>>0]|0;if(!(f&1))h=10;else{f=c[b>>2]|0;h=(f&-2)+-1|0;f=f&255}g=(f&1)==0;do if(h>>>0>=e>>>0){if(g)f=b+1|0;else f=c[b+8>>2]|0;Hc(f|0,d|0,e|0)|0;a[f+e>>0]=0;if(!(a[b>>0]&1)){a[b>>0]=e<<1;break}else{c[b+4>>2]=e;break}}else{if(g)f=(f&255)>>>1;else f=c[b+4>>2]|0;Wa(b,h,e-h|0,f,0,f,e,d)}while(0);return}function Vb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0;k=i;i=i+16|0;j=k;if((b|0)!=(d|0)?(f=qb(b,d,e)|0,(f|0)!=(b|0)):0){h=Mb(f,d,e)|0;if((h|0)!=(f|0)){g=e+4|0;f=c[g>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2)f=b;else{Cb(j,f+-24|0);f=c[g>>2]|0;d=f+-24|0;e=f;while(1){if((e|0)==(d|0))break;b=e+-24|0;c[g>>2]=b;Ia(b);e=c[g>>2]|0}g=a[j>>0]|0;b=(g&1)==0;Za(f+-48|0,b?j+1|0:c[j+8>>2]|0,b?(g&255)>>>1:c[j+4>>2]|0)|0;Ja(j);f=h}}}else f=b;i=k;return f|0}function Wb(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,_=0,$=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,na=0,oa=0,pa=0,qa=0,ra=0,sa=0,ta=0,ua=0;ua=i;i=i+208|0;ta=ua+188|0;ra=ua+184|0;pa=ua+172|0;ba=ua+160|0;ca=ua+144|0;ha=ua+140|0;ia=ua+128|0;ja=ua+112|0;ka=ua+108|0;la=ua+96|0;ma=ua+64|0;na=ua+56|0;oa=ua+40|0;da=ua+36|0;ea=ua+24|0;fa=ua+8|0;ga=ua;n=ua+80|0;k=ua+60|0;m=d;a:do if((m-b|0)>1){sa=(a[b>>0]|0)==76?b+1|0:b;f=a[sa>>0]|0;switch(f<<24>>24|0){case 78:{b:do if((sa|0)!=(d|0))if(f<<24>>24==78){f=Oa(sa+1|0,d,ra)|0;c:do if((f|0)!=(d|0)){h=e+52|0;c[h>>2]=0;switch(a[f>>0]|0){case 82:{c[h>>2]=1;f=f+1|0;break}case 79:{c[h>>2]=2;f=f+1|0;break}default:{}}aa=e+4|0;j=c[aa>>2]|0;$=c[e+8>>2]|0;h=$;if(j>>>0<$>>>0){c[j>>2]=0;c[j+4>>2]=0;c[j+8>>2]=0;c[j+12>>2]=0;c[j+16>>2]=0;c[j+20>>2]=0;h=0;while(1){if((h|0)==3)break;c[j+(h<<2)>>2]=0;h=h+1|0}h=j+12|0;j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}c[aa>>2]=(c[aa>>2]|0)+24}else{k=c[e>>2]|0;$=j-k|0;l=($|0)/24|0;j=l+1|0;if(($|0)<-24)Pa();h=(h-k|0)/24|0;if(h>>>0<1073741823){h=h<<1;h=h>>>0<j>>>0?j:h}else h=2147483647;ab(ta,h,l,e+12|0);k=ta+8|0;l=c[k>>2]|0;c[l>>2]=0;c[l+4>>2]=0;c[l+8>>2]=0;c[l+12>>2]=0;c[l+16>>2]=0;c[l+20>>2]=0;h=0;while(1){if((h|0)==3)break;c[l+(h<<2)>>2]=0;h=h+1|0}h=l+12|0;j=0;while(1){if((j|0)==3)break;c[h+(j<<2)>>2]=0;j=j+1|0}c[k>>2]=l+24;cb(e,ta);bb(ta)}if(((m-f|0)>1?(a[f>>0]|0)==83:0)?(a[f+1>>0]|0)==116:0){Ub((c[aa>>2]|0)+-24|0,2080,3);f=f+2|0}if((f|0)==(d|0)){g=c[aa>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0))break c;ta=g+-24|0;c[aa>>2]=ta;Ia(ta);g=c[aa>>2]|0}}I=pa+8|0;J=pa+1|0;K=pa+4|0;L=e+12|0;M=e+16|0;$=e+20|0;N=e+24|0;O=oa+12|0;P=oa+4|0;Q=oa+8|0;R=e+28|0;S=ta+8|0;T=fa+12|0;U=fa+4|0;V=fa+8|0;W=ta+8|0;X=ea+8|0;Y=ea+1|0;Z=ea+4|0;_=ba+8|0;o=ba+1|0;p=ba+4|0;q=ca+12|0;r=ca+4|0;s=ca+8|0;t=ta+8|0;u=ja+12|0;v=ja+4|0;w=ja+8|0;x=ta+8|0;y=ia+8|0;z=ia+1|0;A=ia+4|0;B=ma+12|0;C=ma+4|0;D=ma+8|0;E=ta+8|0;F=la+8|0;G=la+1|0;H=la+4|0;n=0;d:while(1){h=f;e:while(1){f=a[h>>0]|0;if(f<<24>>24==69){qa=129;break d}switch(f<<24>>24|0){case 83:{qa=39;break e}case 84:{qa=59;break e}case 68:{qa=77;break e}case 73:break;case 76:{f=h+1|0;if((f|0)==(d|0))break c;else{h=f;continue e}}default:break e}m=Mb(h,d,e)|0;if((m|0)==(h|0)|(m|0)==(d|0))break c;Cb(pa,(c[aa>>2]|0)+-24|0);f=c[aa>>2]|0;h=f+-24|0;j=f;while(1){if((j|0)==(h|0))break;l=j+-24|0;c[aa>>2]=l;Ia(l);j=c[aa>>2]|0}l=a[pa>>0]|0;h=(l&1)==0;Za(f+-48|0,h?J:c[I>>2]|0,h?(l&255)>>>1:c[K>>2]|0)|0;f=(c[aa>>2]|0)+-24|0;c[da>>2]=c[L>>2];Pb(oa,f,da);f=c[$>>2]|0;l=c[N>>2]|0;h=l;if(f>>>0<l>>>0){c[f+12>>2]=c[O>>2];c[f>>2]=c[oa>>2];c[f+4>>2]=c[P>>2];c[f+8>>2]=c[Q>>2];c[Q>>2]=0;c[P>>2]=0;c[oa>>2]=0;c[$>>2]=(c[$>>2]|0)+16}else{j=c[M>>2]|0;f=f-j|0;l=f>>4;k=l+1|0;if((f|0)<-16){qa=104;break d}f=h-j|0;if(f>>4>>>0<1073741823){f=f>>3;f=f>>>0<k>>>0?k:f}else f=2147483647;Qa(ta,f,l,R);l=c[S>>2]|0;c[l+12>>2]=c[O>>2];c[l>>2]=c[oa>>2];c[l+4>>2]=c[P>>2];c[l+8>>2]=c[Q>>2];c[Q>>2]=0;c[P>>2]=0;c[oa>>2]=0;c[S>>2]=l+16;Ra(M,ta);Sa(ta)}Ha(oa);Ja(pa);h=m}f:do if((qa|0)==39){qa=0;n=h+1|0;if((n|0)!=(d|0)?(a[n>>0]|0)==116:0)break;f=Rb(h,d,e)|0;if((f|0)==(h|0)|(f|0)==(d|0))break c;Cb(pa,(c[aa>>2]|0)+-24|0);k=c[aa>>2]|0;h=k+-24|0;j=k;while(1){if((j|0)==(h|0))break;n=j+-24|0;c[aa>>2]=n;Ia(n);j=c[aa>>2]|0}j=k+-48|0;h=a[j>>0]|0;if(!(h&1))h=(h&255)>>>1;else h=c[k+-44>>2]|0;if(!h)Xb(j,pa);else{xb(ba,891,pa);h=a[ba>>0]|0;n=(h&1)==0;Za(j,n?o:c[_>>2]|0,n?(h&255)>>>1:c[p>>2]|0)|0;Ja(ba);h=(c[aa>>2]|0)+-24|0;c[ha>>2]=c[L>>2];Pb(ca,h,ha);h=c[$>>2]|0;n=c[N>>2]|0;j=n;if(h>>>0<n>>>0){c[h+12>>2]=c[q>>2];c[h>>2]=c[ca>>2];c[h+4>>2]=c[r>>2];c[h+8>>2]=c[s>>2];c[s>>2]=0;c[r>>2]=0;c[ca>>2]=0;c[$>>2]=(c[$>>2]|0)+16}else{k=c[M>>2]|0;n=h-k|0;m=n>>4;l=m+1|0;if((n|0)<-16){qa=52;break d}h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ta,h,m,R);n=c[t>>2]|0;c[n+12>>2]=c[q>>2];c[n>>2]=c[ca>>2];c[n+4>>2]=c[r>>2];c[n+8>>2]=c[s>>2];c[s>>2]=0;c[r>>2]=0;c[ca>>2]=0;c[t>>2]=n+16;Ra(M,ta);Sa(ta)}Ha(ca)}Ja(pa);n=1;continue d}else if((qa|0)==59){qa=0;f=Eb(h,d,e)|0;if((f|0)==(h|0)|(f|0)==(d|0))break c;Cb(pa,(c[aa>>2]|0)+-24|0);k=c[aa>>2]|0;h=k+-24|0;j=k;while(1){if((j|0)==(h|0))break;n=j+-24|0;c[aa>>2]=n;Ia(n);j=c[aa>>2]|0}j=k+-48|0;h=a[j>>0]|0;if(!(h&1))h=(h&255)>>>1;else h=c[k+-44>>2]|0;if(!h)Xb(j,pa);else{xb(ia,891,pa);n=a[ia>>0]|0;m=(n&1)==0;Za(j,m?z:c[y>>2]|0,m?(n&255)>>>1:c[A>>2]|0)|0;Ja(ia)}h=(c[aa>>2]|0)+-24|0;c[ka>>2]=c[L>>2];Pb(ja,h,ka);h=c[$>>2]|0;n=c[N>>2]|0;j=n;if(h>>>0<n>>>0){c[h+12>>2]=c[u>>2];c[h>>2]=c[ja>>2];c[h+4>>2]=c[v>>2];c[h+8>>2]=c[w>>2];c[w>>2]=0;c[v>>2]=0;c[ja>>2]=0;c[$>>2]=(c[$>>2]|0)+16}else{k=c[M>>2]|0;n=h-k|0;m=n>>4;l=m+1|0;if((n|0)<-16){qa=72;break d}h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ta,h,m,R);n=c[x>>2]|0;c[n+12>>2]=c[u>>2];c[n>>2]=c[ja>>2];c[n+4>>2]=c[v>>2];c[n+8>>2]=c[w>>2];c[w>>2]=0;c[v>>2]=0;c[ja>>2]=0;c[x>>2]=n+16;Ra(M,ta);Sa(ta)}Ha(ja);Ja(pa);n=1;continue d}else if((qa|0)==77){qa=0;f=h+1|0;if((f|0)!=(d|0))switch(a[f>>0]|0){case 84:case 116:break;default:break f}f=Qb(h,d,e)|0;if((f|0)==(h|0)|(f|0)==(d|0))break c;Cb(pa,(c[aa>>2]|0)+-24|0);k=c[aa>>2]|0;h=k+-24|0;j=k;while(1){if((j|0)==(h|0))break;n=j+-24|0;c[aa>>2]=n;Ia(n);j=c[aa>>2]|0}j=k+-48|0;h=a[j>>0]|0;if(!(h&1))h=(h&255)>>>1;else h=c[k+-44>>2]|0;if(!h)Xb(j,pa);else{xb(la,891,pa);n=a[la>>0]|0;m=(n&1)==0;Za(j,m?G:c[F>>2]|0,m?(n&255)>>>1:c[H>>2]|0)|0;Ja(la)}h=(c[aa>>2]|0)+-24|0;c[na>>2]=c[L>>2];Pb(ma,h,na);h=c[$>>2]|0;n=c[N>>2]|0;j=n;if(h>>>0<n>>>0){c[h+12>>2]=c[B>>2];c[h>>2]=c[ma>>2];c[h+4>>2]=c[C>>2];c[h+8>>2]=c[D>>2];c[D>>2]=0;c[C>>2]=0;c[ma>>2]=0;c[$>>2]=(c[$>>2]|0)+16}else{k=c[M>>2]|0;n=h-k|0;m=n>>4;l=m+1|0;if((n|0)<-16){qa=92;break d}h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ta,h,m,R);n=c[E>>2]|0;c[n+12>>2]=c[B>>2];c[n>>2]=c[ma>>2];c[n+4>>2]=c[C>>2];c[n+8>>2]=c[D>>2];c[D>>2]=0;c[C>>2]=0;c[ma>>2]=0;c[E>>2]=n+16;Ra(M,ta);Sa(ta)}Ha(ma);Ja(pa);n=1;continue d}while(0);f=Sb(h,d,e)|0;if((f|0)==(h|0)|(f|0)==(d|0))break c;Cb(pa,(c[aa>>2]|0)+-24|0);k=c[aa>>2]|0;h=k+-24|0;j=k;while(1){if((j|0)==(h|0))break;n=j+-24|0;c[aa>>2]=n;Ia(n);j=c[aa>>2]|0}j=k+-48|0;h=a[j>>0]|0;if(!(h&1))h=(h&255)>>>1;else h=c[k+-44>>2]|0;if(!h)Xb(j,pa);else{xb(ea,891,pa);n=a[ea>>0]|0;m=(n&1)==0;Za(j,m?Y:c[X>>2]|0,m?(n&255)>>>1:c[Z>>2]|0)|0;Ja(ea)}h=(c[aa>>2]|0)+-24|0;c[ga>>2]=c[L>>2];Pb(fa,h,ga);h=c[$>>2]|0;n=c[N>>2]|0;j=n;if(h>>>0<n>>>0){c[h+12>>2]=c[T>>2];c[h>>2]=c[fa>>2];c[h+4>>2]=c[U>>2];c[h+8>>2]=c[V>>2];c[V>>2]=0;c[U>>2]=0;c[fa>>2]=0;c[$>>2]=(c[$>>2]|0)+16}else{k=c[M>>2]|0;n=h-k|0;m=n>>4;l=m+1|0;if((n|0)<-16){qa=123;break}h=j-k|0;if(h>>4>>>0<1073741823){h=h>>3;h=h>>>0<l>>>0?l:h}else h=2147483647;Qa(ta,h,m,R);n=c[W>>2]|0;c[n+12>>2]=c[T>>2];c[n>>2]=c[fa>>2];c[n+4>>2]=c[U>>2];c[n+8>>2]=c[V>>2];c[V>>2]=0;c[U>>2]=0;c[fa>>2]=0;c[W>>2]=n+16;Ra(M,ta);Sa(ta)}Ha(fa);Ja(pa);n=1}if((qa|0)==52)Pa();else if((qa|0)==72)Pa();else if((qa|0)==92)Pa();else if((qa|0)==104)Pa();else if((qa|0)==123)Pa();else if((qa|0)==129){f=h+1|0;c[e+48>>2]=c[ra>>2];g:do if(n?(g=c[$>>2]|0,(c[e+16>>2]|0)!=(g|0)):0){h=g+-16|0;while(1){if((g|0)==(h|0))break g;ta=g+-16|0;c[$>>2]=ta;Ha(ta);g=c[$>>2]|0}}while(0);break b}}while(0);f=sa}else f=sa;else f=d;while(0);f=(f|0)==(sa|0)?b:f;break a}case 90:{h:do if(((f<<24>>24==90&(sa|0)!=(d|0)?(ra=sa+1|0,h=Ma(ra,d,e)|0,!((h|0)==(ra|0)|(h|0)==(d|0))):0)?(a[h>>0]|0)==69:0)?(j=h+1|0,(j|0)!=(d|0)):0)switch(a[j>>0]|0){case 115:{f=Yb(h+2|0,d)|0;g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0))break h;Ya(g+-24|0,2084)|0;break h}case 100:{f=h+2|0;if((f|0)==(d|0)){f=sa;break h}f=tb(f,d)|0;if((f|0)==(d|0)){f=sa;break h}if((a[f>>0]|0)!=95){f=sa;break h}ra=f+1|0;f=Wb(ra,d,e)|0;k=e+4|0;if((f|0)==(ra|0)){g=c[k>>2]|0;f=g+-24|0;while(1){if((g|0)==(f|0)){f=sa;break h}ta=g+-24|0;c[k>>2]=ta;Ia(ta);g=c[k>>2]|0}}g=c[k>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0<2){f=sa;break h}Cb(ta,g+-24|0);g=c[k>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;e=j+-24|0;c[k>>2]=e;Ia(e);j=c[k>>2]|0}Ya(g+-48|0,891)|0;e=a[ta>>0]|0;d=(e&1)==0;Za((c[k>>2]|0)+-24|0,d?ta+1|0:c[ta+8>>2]|0,d?(e&255)>>>1:c[ta+4>>2]|0)|0;Ja(ta);break h}default:{f=Wb(j,d,e)|0;if((f|0)==(j|0)){f=e+4|0;h=c[f>>2]|0;g=h+-24|0;while(1){if((h|0)==(g|0)){f=sa;break h}ta=h+-24|0;c[f>>2]=ta;Ia(ta);h=c[f>>2]|0}}f=Yb(f,d)|0;k=e+4|0;g=c[k>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0<2)break h;Cb(ta,g+-24|0);g=c[k>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;e=j+-24|0;c[k>>2]=e;Ia(e);j=c[k>>2]|0}Ya(g+-48|0,891)|0;e=a[ta>>0]|0;d=(e&1)==0;Za((c[k>>2]|0)+-24|0,d?ta+1|0:c[ta+8>>2]|0,d?(e&255)>>>1:c[ta+4>>2]|0)|0;Ja(ta);break h}}else f=sa;while(0);f=(f|0)==(sa|0)?b:f;break a}default:{do if((m-sa|0)>1){if(f<<24>>24==83?(a[sa+1>>0]|0)==116:0){f=sa+2|0;if((f|0)==(d|0)){h=0;g=d}else{h=0;g=(a[f>>0]|0)==76?sa+3|0:f}}else{h=1;g=sa}f=Sb(g,d,e)|0;g=(f|0)==(g|0);if(h|g)f=g?sa:f;else{g=c[e+4>>2]|0;if((c[e>>2]|0)==(g|0))break;Ta(g+-24|0,0,1827)|0}if((f|0)!=(sa|0)){if((f|0)==(d|0)){f=d;break a}if((a[f>>0]|0)!=73)break a;m=e+4|0;g=c[m>>2]|0;if((c[e>>2]|0)==(g|0)){f=b;break a}l=e+16|0;c[k>>2]=c[e+12>>2];Pb(n,g+-24|0,k);g=e+20|0;h=c[g>>2]|0;sa=c[e+24>>2]|0;j=sa;if(h>>>0<sa>>>0){c[h+12>>2]=c[n+12>>2];c[h>>2]=c[n>>2];sa=n+4|0;c[h+4>>2]=c[sa>>2];ra=n+8|0;c[h+8>>2]=c[ra>>2];c[ra>>2]=0;c[sa>>2]=0;c[n>>2]=0;c[g>>2]=(c[g>>2]|0)+16}else{g=c[l>>2]|0;sa=h-g|0;k=sa>>4;h=k+1|0;if((sa|0)<-16)Pa();g=j-g|0;if(g>>4>>>0<1073741823){g=g>>3;g=g>>>0<h>>>0?h:g}else g=2147483647;Qa(ta,g,k,e+28|0);sa=ta+8|0;ra=c[sa>>2]|0;c[ra+12>>2]=c[n+12>>2];c[ra>>2]=c[n>>2];qa=n+4|0;c[ra+4>>2]=c[qa>>2];pa=n+8|0;c[ra+8>>2]=c[pa>>2];c[pa>>2]=0;c[qa>>2]=0;c[n>>2]=0;c[sa>>2]=ra+16;Ra(l,ta);Sa(ta)}Ha(n);j=Mb(f,d,e)|0;if((j|0)==(f|0)){f=b;break a}f=c[m>>2]|0;if(((f-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break a}Cb(ta,f+-24|0);f=c[m>>2]|0;g=f+-24|0;h=f;while(1){if((h|0)==(g|0))break;b=h+-24|0;c[m>>2]=b;Ia(b);h=c[m>>2]|0}b=a[ta>>0]|0;sa=(b&1)==0;Za(f+-48|0,sa?ta+1|0:c[ta+8>>2]|0,sa?(b&255)>>>1:c[ta+4>>2]|0)|0;Ja(ta);f=j;break a}}while(0);g=Rb(sa,d,e)|0;if((g|0)==(sa|0)|(g|0)==(d|0)){f=b;break a}if((a[g>>0]|0)!=73){f=b;break a}f=Mb(g,d,e)|0;if((f|0)==(g|0)){f=b;break a}k=e+4|0;g=c[k>>2]|0;if(((g-(c[e>>2]|0)|0)/24|0)>>>0<2){f=b;break a}Cb(ta,g+-24|0);g=c[k>>2]|0;h=g+-24|0;j=g;while(1){if((j|0)==(h|0))break;b=j+-24|0;c[k>>2]=b;Ia(b);j=c[k>>2]|0}b=a[ta>>0]|0;sa=(b&1)==0;Za(g+-48|0,sa?ta+1|0:c[ta+8>>2]|0,sa?(b&255)>>>1:c[ta+4>>2]|0)|0;Ja(ta);break a}}}else f=b;while(0);i=ua;return f|0}function Xb(b,d){b=b|0;d=d|0;var e=0,f=0;if((b|0)!=(d|0)){e=a[d>>0]|0;f=(e&1)==0;Ub(b,f?d+1|0:c[d+8>>2]|0,f?(e&255)>>>1:c[d+4>>2]|0)}return}function Yb(b,c){b=b|0;c=c|0;var d=0,e=0;a:do if((b|0)!=(c|0)){d=a[b>>0]|0;if(d<<24>>24!=95){if(((d<<24>>24)+-48|0)>>>0>=10)break;while(1){b=b+1|0;if((b|0)==(c|0)){b=c;break a}if(((a[b>>0]|0)+-48|0)>>>0>=10)break a}}d=b+1|0;if((d|0)!=(c|0)){d=a[d>>0]|0;if(((d<<24>>24)+-48|0)>>>0<10){b=b+2|0;break}if(d<<24>>24==95){e=b+2|0;while(1){if((e|0)==(c|0))break a;d=a[e>>0]|0;if(((d<<24>>24)+-48|0)>>>0>=10)break;e=e+1|0}return (d<<24>>24==95?e+1|0:b)|0}}}while(0);return b|0}function Zb(b,c){b=b|0;c=c|0;var d=0,e=0,f=0;a:do if((b|0)!=(c|0)){switch(a[b>>0]|0){case 104:{e=b+1|0;d=tb(e,c)|0;if((d|0)==(e|0)|(d|0)==(c|0))break a;return ((a[d>>0]|0)==95?d+1|0:b)|0}case 118:break;default:break a}f=b+1|0;d=tb(f,c)|0;if((!((d|0)==(f|0)|(d|0)==(c|0))?(a[d>>0]|0)==95:0)?(f=d+1|0,e=tb(f,c)|0,!((e|0)==(f|0)|(e|0)==(c|0))):0)b=(a[e>>0]|0)==95?e+1|0:b}while(0);return b|0}function _b(a){a=a|0;$b(a+32|0);Ga(a+16|0);Ha(a);return}function $b(a){a=a|0;var b=0,d=0,e=0;b=c[a>>2]|0;if(b){d=a+4|0;while(1){e=c[d>>2]|0;if((e|0)==(b|0))break;e=e+-16|0;c[d>>2]=e;Ga(e)}e=c[a>>2]|0;Ka(c[a+12>>2]|0,e,(c[a+8>>2]|0)-e|0)}return}function ac(b,c,d){b=b|0;c=c|0;d=d|0;var e=0,f=0;a:do if(!d)d=0;else{while(1){e=a[b>>0]|0;f=a[c>>0]|0;if(e<<24>>24!=f<<24>>24)break;d=d+-1|0;if(!d){d=0;break a}else{b=b+1|0;c=c+1|0}}d=(e&255)-(f&255)|0}while(0);return d|0}function bc(b){b=b|0;var d=0,e=0,f=0;f=b;a:do if(!(f&3))e=4;else{d=b;b=f;while(1){if(!(a[d>>0]|0))break a;d=d+1|0;b=d;if(!(b&3)){b=d;e=4;break}}}while(0);if((e|0)==4){while(1){d=c[b>>2]|0;if(!((d&-2139062144^-2139062144)&d+-16843009))b=b+4|0;else break}if((d&255)<<24>>24)do b=b+1|0;while((a[b>>0]|0)!=0)}return b-f|0}function cc(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,i=0;h=d&255;f=(e|0)!=0;a:do if(f&(b&3|0)!=0){g=d&255;while(1){if((a[b>>0]|0)==g<<24>>24)break a;b=b+1|0;e=e+-1|0;f=(e|0)!=0;if(!(f&(b&3|0)!=0)){i=5;break}}}else i=5;while(0);b:do if((i|0)==5)if(f){g=d&255;if((a[b>>0]|0)!=g<<24>>24){f=_(h,16843009)|0;c:do if(e>>>0>3)while(1){h=c[b>>2]^f;if((h&-2139062144^-2139062144)&h+-16843009)break;b=b+4|0;e=e+-4|0;if(e>>>0<=3){i=11;break c}}else i=11;while(0);if((i|0)==11)if(!e){e=0;break}while(1){if((a[b>>0]|0)==g<<24>>24)break b;b=b+1|0;e=e+-1|0;if(!e){e=0;break}}}}else e=0;while(0);return ((e|0)!=0?b:0)|0}function dc(b){b=b|0;var c=0,e=0;c=0;while(1){if((d[2370+c>>0]|0)==(b|0)){e=2;break}c=c+1|0;if((c|0)==87){c=87;b=2458;e=5;break}}if((e|0)==2)if(!c)c=2458;else{b=2458;e=5}if((e|0)==5)while(1){do{e=b;b=b+1|0}while((a[e>>0]|0)!=0);c=c+-1|0;if(!c){c=b;break}else e=5}return c|0}function ec(){var a=0;if(!(c[1200]|0))a=4844;else a=c[(fa()|0)+60>>2]|0;return a|0}function fc(a){a=a|0;if((a+-48|0)>>>0<10)a=1;else a=((a|32)+-97|0)>>>0<6;return a&1|0}function gc(a){a=a|0;return (a+-65|0)>>>0<26|0}function hc(b,d,e,f){b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,j=0,k=0,l=0,m=0,n=0;n=i;i=i+128|0;g=n+112|0;m=n;h=m;j=8;k=h+112|0;do{c[h>>2]=c[j>>2];h=h+4|0;j=j+4|0}while((h|0)<(k|0));if((d+-1|0)>>>0>2147483646)if(!d){d=1;l=4}else{c[(ec()|0)>>2]=75;d=-1}else{g=b;l=4}if((l|0)==4){l=-2-g|0;l=d>>>0>l>>>0?l:d;c[m+48>>2]=l;b=m+20|0;c[b>>2]=g;c[m+44>>2]=g;d=g+l|0;g=m+16|0;c[g>>2]=d;c[m+28>>2]=d;d=lc(m,e,f)|0;if(l){e=c[b>>2]|0;a[e+(((e|0)==(c[g>>2]|0))<<31>>31)>>0]=0}}i=n;return d|0}function ic(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,i=0;f=e+16|0;g=c[f>>2]|0;if(!g)if(!(kc(e)|0)){g=c[f>>2]|0;h=5}else f=0;else h=5;a:do if((h|0)==5){i=e+20|0;f=c[i>>2]|0;h=f;if((g-f|0)>>>0<d>>>0){f=ra[c[e+36>>2]&1](e,b,d)|0;break}b:do if((a[e+75>>0]|0)>-1){f=d;while(1){if(!f){g=h;f=0;break b}g=f+-1|0;if((a[b+g>>0]|0)==10)break;else f=g}if((ra[c[e+36>>2]&1](e,b,f)|0)>>>0<f>>>0)break a;d=d-f|0;b=b+f|0;g=c[i>>2]|0}else{g=h;f=0}while(0);Fc(g|0,b|0,d|0)|0;c[i>>2]=(c[i>>2]|0)+d;f=f+d|0}while(0);return f|0}function jc(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;var f=0,g=0;f=i;i=i+16|0;g=f;c[g>>2]=e;e=hc(a,b,d,g)|0;i=f;return e|0}function kc(b){b=b|0;var d=0,e=0;d=b+74|0;e=a[d>>0]|0;a[d>>0]=e+255|e;d=c[b>>2]|0;if(!(d&8)){c[b+8>>2]=0;c[b+4>>2]=0;d=c[b+44>>2]|0;c[b+28>>2]=d;c[b+20>>2]=d;c[b+16>>2]=d+(c[b+48>>2]|0);d=0}else{c[b>>2]=d|32;d=-1}return d|0}function lc(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0;r=i;i=i+224|0;n=r+120|0;q=r+80|0;p=r;o=r+136|0;f=q;g=f+40|0;do{c[f>>2]=0;f=f+4|0}while((f|0)<(g|0));c[n>>2]=c[e>>2];if((rc(0,d,n,p,q)|0)<0)e=-1;else{e=c[b>>2]|0;m=e&32;if((a[b+74>>0]|0)<1)c[b>>2]=e&-33;l=b+48|0;if(!(c[l>>2]|0)){f=b+44|0;g=c[f>>2]|0;c[f>>2]=o;h=b+28|0;c[h>>2]=o;j=b+20|0;c[j>>2]=o;c[l>>2]=80;k=b+16|0;c[k>>2]=o+80;e=rc(b,d,n,p,q)|0;if(g){ra[c[b+36>>2]&1](b,0,0)|0;e=(c[j>>2]|0)==0?-1:e;c[f>>2]=g;c[l>>2]=0;c[k>>2]=0;c[h>>2]=0;c[j>>2]=0}}else e=rc(b,d,n,p,q)|0;q=c[b>>2]|0;c[b>>2]=q|m;e=(q&32|0)==0?e:-1}i=r;return e|0}function mc(b,d){b=b|0;d=d|0;do if(b){if(d>>>0<128){a[b>>0]=d;b=1;break}if(d>>>0<2048){a[b>>0]=d>>>6|192;a[b+1>>0]=d&63|128;b=2;break}if(d>>>0<55296|(d&-8192|0)==57344){a[b>>0]=d>>>12|224;a[b+1>>0]=d>>>6&63|128;a[b+2>>0]=d&63|128;b=3;break}if((d+-65536|0)>>>0<1048576){a[b>>0]=d>>>18|240;a[b+1>>0]=d>>>12&63|128;a[b+2>>0]=d>>>6&63|128;a[b+3>>0]=d&63|128;b=4;break}else{c[(ec()|0)>>2]=84;b=-1;break}}else b=1;while(0);return b|0}function nc(a,b){a=a|0;b=b|0;if(!a)a=0;else a=mc(a,b)|0;return a|0}function oc(a,b){a=+a;b=b|0;return +(+pc(a,b))}function pc(a,b){a=+a;b=b|0;var d=0,e=0,f=0;h[k>>3]=a;d=c[k>>2]|0;e=c[k+4>>2]|0;f=Dc(d|0,e|0,52)|0;f=f&2047;switch(f|0){case 0:{if(a!=0.0){a=+pc(a*18446744073709551616.0,b);d=(c[b>>2]|0)+-64|0}else d=0;c[b>>2]=d;break}case 2047:break;default:{c[b>>2]=f+-1022;c[k>>2]=d;c[k+4>>2]=e&-2146435073|1071644672;a=+h[k>>3]}}return +a}function qc(a,b,d){a=a|0;b=b|0;d=d|0;var e=0,f=0;e=a+20|0;f=c[e>>2]|0;a=(c[a+16>>2]|0)-f|0;a=a>>>0>d>>>0?d:a;Fc(f|0,b|0,a|0)|0;c[e>>2]=(c[e>>2]|0)+a;return d|0}function rc(e,f,g,j,l){e=e|0;f=f|0;g=g|0;j=j|0;l=l|0;var m=0,n=0,o=0,p=0.0,q=0,r=0,s=0,t=0,u=0,v=0,w=0.0,x=0,y=0,z=0,A=0,B=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,$=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0;ia=i;i=i+624|0;da=ia+24|0;fa=ia+16|0;ea=ia+588|0;aa=ia+576|0;ca=ia;W=ia+536|0;ha=ia+8|0;ga=ia+528|0;M=(e|0)!=0;N=W+40|0;V=N;W=W+39|0;X=ha+4|0;Y=ea;Z=0-Y|0;$=aa+12|0;aa=aa+11|0;ba=$;O=ba-Y|0;P=-2-Y|0;Q=ba+2|0;R=da+288|0;S=ea+9|0;T=S;U=ea+8|0;m=0;n=0;r=0;x=f;a:while(1){do if((m|0)>-1)if((n|0)>(2147483647-m|0)){c[(ec()|0)>>2]=75;m=-1;break}else{m=n+m|0;break}while(0);f=a[x>>0]|0;if(!(f<<24>>24)){L=244;break}else n=x;b:while(1){switch(f<<24>>24){case 37:{f=n;L=9;break b}case 0:{f=n;break b}default:{}}K=n+1|0;f=a[K>>0]|0;n=K}c:do if((L|0)==9)while(1){L=0;if((a[f+1>>0]|0)!=37)break c;n=n+1|0;f=f+2|0;if((a[f>>0]|0)==37)L=9;else break}while(0);v=n-x|0;if(M?(c[e>>2]&32|0)==0:0)ic(x,v,e)|0;if((n|0)!=(x|0)){n=v;x=f;continue}q=f+1|0;n=a[q>>0]|0;o=(n<<24>>24)+-48|0;if(o>>>0<10){K=(a[f+2>>0]|0)==36;q=K?f+3|0:q;n=a[q>>0]|0;t=K?o:-1;r=K?1:r}else t=-1;f=n<<24>>24;d:do if((f&-32|0)==32){o=0;do{if(!(1<<f+-32&75913))break d;o=1<<(n<<24>>24)+-32|o;q=q+1|0;n=a[q>>0]|0;f=n<<24>>24}while((f&-32|0)==32)}else o=0;while(0);do if(n<<24>>24==42){n=q+1|0;f=(a[n>>0]|0)+-48|0;if(f>>>0<10?(a[q+2>>0]|0)==36:0){c[l+(f<<2)>>2]=10;f=1;q=q+3|0;n=c[j+((a[n>>0]|0)+-48<<3)>>2]|0}else{if(r){m=-1;break a}if(!M){u=o;K=0;q=n;J=0;break}f=(c[g>>2]|0)+(4-1)&~(4-1);K=c[f>>2]|0;c[g>>2]=f+4;f=0;q=n;n=K}if((n|0)<0){u=o|8192;K=f;J=0-n|0}else{u=o;K=f;J=n}}else{f=(n<<24>>24)+-48|0;if(f>>>0<10){n=0;do{n=(n*10|0)+f|0;q=q+1|0;f=(a[q>>0]|0)+-48|0}while(f>>>0<10);if((n|0)<0){m=-1;break a}else{u=o;K=r;J=n}}else{u=o;K=r;J=0}}while(0);e:do if((a[q>>0]|0)==46){f=q+1|0;n=a[f>>0]|0;if(n<<24>>24!=42){o=(n<<24>>24)+-48|0;if(o>>>0<10)n=0;else{r=0;break}while(1){n=(n*10|0)+o|0;f=f+1|0;o=(a[f>>0]|0)+-48|0;if(o>>>0>=10){r=n;break e}}}f=q+2|0;n=(a[f>>0]|0)+-48|0;if(n>>>0<10?(a[q+3>>0]|0)==36:0){c[l+(n<<2)>>2]=10;r=c[j+((a[f>>0]|0)+-48<<3)>>2]|0;f=q+4|0;break}if(K){m=-1;break a}if(M){I=(c[g>>2]|0)+(4-1)&~(4-1);r=c[I>>2]|0;c[g>>2]=I+4}else r=0}else{r=-1;f=q}while(0);s=0;while(1){n=(a[f>>0]|0)+-65|0;if(n>>>0>57){m=-1;break a}I=f+1|0;n=a[4266+(s*58|0)+n>>0]|0;o=n&255;if((o+-1|0)>>>0<8){f=I;s=o}else break}if(!(n<<24>>24)){m=-1;break}q=(t|0)>-1;do if(n<<24>>24==19)if(q){m=-1;break a}else L=52;else{if(q){c[l+(t<<2)>>2]=o;G=j+(t<<3)|0;H=c[G+4>>2]|0;L=ca;c[L>>2]=c[G>>2];c[L+4>>2]=H;L=52;break}if(!M){m=0;break a}sc(ca,o,g)}while(0);if((L|0)==52?(L=0,!M):0){n=v;r=K;x=I;continue}t=a[f>>0]|0;t=(s|0)!=0&(t&15|0)==3?t&-33:t;o=u&-65537;H=(u&8192|0)==0?u:o;f:do switch(t|0){case 110:switch(s|0){case 0:{c[c[ca>>2]>>2]=m;n=v;r=K;x=I;continue a}case 1:{c[c[ca>>2]>>2]=m;n=v;r=K;x=I;continue a}case 2:{n=c[ca>>2]|0;c[n>>2]=m;c[n+4>>2]=((m|0)<0)<<31>>31;n=v;r=K;x=I;continue a}case 3:{b[c[ca>>2]>>1]=m;n=v;r=K;x=I;continue a}case 4:{a[c[ca>>2]>>0]=m;n=v;r=K;x=I;continue a}case 6:{c[c[ca>>2]>>2]=m;n=v;r=K;x=I;continue a}case 7:{n=c[ca>>2]|0;c[n>>2]=m;c[n+4>>2]=((m|0)<0)<<31>>31;n=v;r=K;x=I;continue a}default:{n=v;r=K;x=I;continue a}}case 112:{s=H|8;r=r>>>0>8?r:8;t=120;L=64;break}case 88:case 120:{s=H;L=64;break}case 111:{o=ca;n=c[o>>2]|0;o=c[o+4>>2]|0;if((n|0)==0&(o|0)==0)f=N;else{f=N;do{f=f+-1|0;a[f>>0]=n&7|48;n=Dc(n|0,o|0,3)|0;o=C}while(!((n|0)==0&(o|0)==0))}if(!(H&8)){n=H;s=0;q=4746;L=77}else{s=V-f|0;n=H;r=(r|0)>(s|0)?r:s+1|0;s=0;q=4746;L=77}break}case 105:case 100:{n=ca;f=c[n>>2]|0;n=c[n+4>>2]|0;if((n|0)<0){f=Cc(0,0,f|0,n|0)|0;n=C;o=ca;c[o>>2]=f;c[o+4>>2]=n;o=1;q=4746;L=76;break f}if(!(H&2048)){q=H&1;o=q;q=(q|0)==0?4746:4748;L=76}else{o=1;q=4747;L=76}break}case 117:{n=ca;f=c[n>>2]|0;n=c[n+4>>2]|0;o=0;q=4746;L=76;break}case 99:{a[W>>0]=c[ca>>2];f=W;t=1;v=0;u=4746;n=N;break}case 109:{n=dc(c[(ec()|0)>>2]|0)|0;L=82;break}case 115:{n=c[ca>>2]|0;n=(n|0)!=0?n:4756;L=82;break}case 67:{c[ha>>2]=c[ca>>2];c[X>>2]=0;c[ca>>2]=ha;f=ha;r=-1;L=86;break}case 83:{f=c[ca>>2]|0;if(!r){uc(e,32,J,0,H);f=0;L=97}else L=86;break}case 65:case 71:case 70:case 69:case 97:case 103:case 102:case 101:{p=+h[ca>>3];c[fa>>2]=0;h[k>>3]=p;if((c[k+4>>2]|0)>=0)if(!(H&2048)){G=H&1;F=G;G=(G|0)==0?4764:4769}else{F=1;G=4766}else{p=-p;F=1;G=4763}h[k>>3]=p;E=c[k+4>>2]&2146435072;do if(E>>>0<2146435072|(E|0)==2146435072&0<0){w=+oc(p,fa)*2.0;n=w!=0.0;if(n)c[fa>>2]=(c[fa>>2]|0)+-1;B=t|32;if((B|0)==97){u=t&32;x=(u|0)==0?G:G+9|0;v=F|2;f=12-r|0;do if(!(r>>>0>11|(f|0)==0)){p=8.0;do{f=f+-1|0;p=p*16.0}while((f|0)!=0);if((a[x>>0]|0)==45){p=-(p+(-w-p));break}else{p=w+p-p;break}}else p=w;while(0);n=c[fa>>2]|0;f=(n|0)<0?0-n|0:n;f=tc(f,((f|0)<0)<<31>>31,$)|0;if((f|0)==($|0)){a[aa>>0]=48;f=aa}a[f+-1>>0]=(n>>31&2)+43;s=f+-2|0;a[s>>0]=t+15;q=(r|0)<1;o=(H&8|0)==0;n=ea;while(1){G=~~p;f=n+1|0;a[n>>0]=d[4730+G>>0]|u;p=(p-+(G|0))*16.0;do if((f-Y|0)==1){if(o&(q&p==0.0))break;a[f>>0]=46;f=n+2|0}while(0);if(!(p!=0.0))break;else n=f}o=s;r=(r|0)!=0&(P+f|0)<(r|0)?Q+r-o|0:O-o+f|0;q=r+v|0;uc(e,32,J,q,H);if(!(c[e>>2]&32))ic(x,v,e)|0;uc(e,48,J,q,H^65536);n=f-Y|0;if(!(c[e>>2]&32))ic(ea,n,e)|0;f=ba-o|0;uc(e,48,r-(n+f)|0,0,0);if(!(c[e>>2]&32))ic(s,f,e)|0;uc(e,32,J,q,H^8192);f=(q|0)<(J|0)?J:q;break}f=(r|0)<0?6:r;if(n){n=(c[fa>>2]|0)+-28|0;c[fa>>2]=n;p=w*268435456.0}else{p=w;n=c[fa>>2]|0}E=(n|0)<0?da:R;D=E;o=E;do{A=~~p>>>0;c[o>>2]=A;o=o+4|0;p=(p-+(A>>>0))*1.0e9}while(p!=0.0);n=c[fa>>2]|0;if((n|0)>0){q=E;r=o;while(1){s=(n|0)>29?29:n;n=r+-4|0;do if(n>>>0>=q>>>0){o=0;do{z=Ec(c[n>>2]|0,0,s|0)|0;z=Gc(z|0,C|0,o|0,0)|0;A=C;y=Pc(z|0,A|0,1e9,0)|0;c[n>>2]=y;o=Oc(z|0,A|0,1e9,0)|0;n=n+-4|0}while(n>>>0>=q>>>0);if(!o)break;q=q+-4|0;c[q>>2]=o}while(0);o=r;while(1){if(o>>>0<=q>>>0)break;n=o+-4|0;if(!(c[n>>2]|0))o=n;else break}n=(c[fa>>2]|0)-s|0;c[fa>>2]=n;if((n|0)>0)r=o;else break}}else q=E;if((n|0)<0){x=((f+25|0)/9|0)+1|0;y=(B|0)==102;do{v=0-n|0;v=(v|0)>9?9:v;do if(q>>>0<o>>>0){n=(1<<v)+-1|0;r=1e9>>>v;u=0;s=q;do{A=c[s>>2]|0;c[s>>2]=(A>>>v)+u;u=_(A&n,r)|0;s=s+4|0}while(s>>>0<o>>>0);n=(c[q>>2]|0)==0?q+4|0:q;if(!u){q=n;n=o;break}c[o>>2]=u;q=n;n=o+4|0}else{q=(c[q>>2]|0)==0?q+4|0:q;n=o}while(0);o=y?E:q;o=(n-o>>2|0)>(x|0)?o+(x<<2)|0:n;n=(c[fa>>2]|0)+v|0;c[fa>>2]=n}while((n|0)<0);x=q;y=o}else{x=q;y=o}do if(x>>>0<y>>>0){n=(D-x>>2)*9|0;q=c[x>>2]|0;if(q>>>0<10)break;else o=10;do{o=o*10|0;n=n+1|0}while(q>>>0>=o>>>0)}else n=0;while(0);z=(B|0)==103;A=(f|0)!=0;o=f-((B|0)!=102?n:0)+((A&z)<<31>>31)|0;if((o|0)<(((y-D>>2)*9|0)+-9|0)){r=o+9216|0;o=E+4+(((r|0)/9|0)+-1024<<2)|0;r=((r|0)%9|0)+1|0;if((r|0)<9){q=10;do{q=q*10|0;r=r+1|0}while((r|0)!=9)}else q=10;u=c[o>>2]|0;v=(u>>>0)%(q>>>0)|0;r=(o+4|0)==(y|0);do if(r&(v|0)==0)q=x;else{w=(((u>>>0)/(q>>>0)|0)&1|0)==0?9007199254740992.0:9007199254740994.0;s=(q|0)/2|0;if(v>>>0<s>>>0)p=.5;else p=r&(v|0)==(s|0)?1.0:1.5;do if(F){if((a[G>>0]|0)!=45)break;w=-w;p=-p}while(0);r=u-v|0;c[o>>2]=r;if(!(w+p!=w)){q=x;break}B=r+q|0;c[o>>2]=B;if(B>>>0>999999999){n=x;while(1){q=o+-4|0;c[o>>2]=0;if(q>>>0<n>>>0){n=n+-4|0;c[n>>2]=0}B=(c[q>>2]|0)+1|0;c[q>>2]=B;if(B>>>0>999999999)o=q;else{s=n;o=q;break}}}else s=x;n=(D-s>>2)*9|0;r=c[s>>2]|0;if(r>>>0<10){q=s;break}else q=10;do{q=q*10|0;n=n+1|0}while(r>>>0>=q>>>0);q=s}while(0);o=o+4|0;x=q;o=y>>>0>o>>>0?o:y}else o=y;v=0-n|0;B=o;while(1){if(B>>>0<=x>>>0){y=0;break}o=B+-4|0;if(!(c[o>>2]|0))B=o;else{y=1;break}}do if(z){f=(A&1^1)+f|0;if((f|0)>(n|0)&(n|0)>-5){t=t+-1|0;f=f+-1-n|0}else{t=t+-2|0;f=f+-1|0}o=H&8;if(o)break;do if(y){o=c[B+-4>>2]|0;if(!o){q=9;break}if(!((o>>>0)%10|0)){r=10;q=0}else{q=0;break}do{r=r*10|0;q=q+1|0}while(((o>>>0)%(r>>>0)|0|0)==0)}else q=9;while(0);o=((B-D>>2)*9|0)+-9|0;if((t|32|0)==102){o=o-q|0;o=(o|0)<0?0:o;f=(f|0)<(o|0)?f:o;o=0;break}else{o=o+n-q|0;o=(o|0)<0?0:o;f=(f|0)<(o|0)?f:o;o=0;break}}else o=H&8;while(0);u=f|o;r=(u|0)!=0&1;s=(t|32|0)==102;if(s){n=(n|0)>0?n:0;t=0}else{q=(n|0)<0?v:n;q=tc(q,((q|0)<0)<<31>>31,$)|0;if((ba-q|0)<2)do{q=q+-1|0;a[q>>0]=48}while((ba-q|0)<2);a[q+-1>>0]=(n>>31&2)+43;D=q+-2|0;a[D>>0]=t;n=ba-D|0;t=D}v=F+1+f+r+n|0;uc(e,32,J,v,H);if(!(c[e>>2]&32))ic(G,F,e)|0;uc(e,48,J,v,H^65536);do if(s){q=x>>>0>E>>>0?E:x;o=q;do{n=tc(c[o>>2]|0,0,S)|0;do if((o|0)==(q|0)){if((n|0)!=(S|0))break;a[U>>0]=48;n=U}else{if(n>>>0<=ea>>>0)break;Bc(ea|0,48,n-Y|0)|0;do n=n+-1|0;while(n>>>0>ea>>>0)}while(0);if(!(c[e>>2]&32))ic(n,T-n|0,e)|0;o=o+4|0}while(o>>>0<=E>>>0);do if(u){if(c[e>>2]&32)break;ic(4798,1,e)|0}while(0);if((f|0)>0&o>>>0<B>>>0)while(1){n=tc(c[o>>2]|0,0,S)|0;if(n>>>0>ea>>>0){Bc(ea|0,48,n-Y|0)|0;do n=n+-1|0;while(n>>>0>ea>>>0)}if(!(c[e>>2]&32))ic(n,(f|0)>9?9:f,e)|0;o=o+4|0;n=f+-9|0;if(!((f|0)>9&o>>>0<B>>>0)){f=n;break}else f=n}uc(e,48,f+9|0,9,0)}else{s=y?B:x+4|0;if((f|0)>-1){r=(o|0)==0;q=x;do{n=tc(c[q>>2]|0,0,S)|0;if((n|0)==(S|0)){a[U>>0]=48;n=U}do if((q|0)==(x|0)){o=n+1|0;if(!(c[e>>2]&32))ic(n,1,e)|0;if(r&(f|0)<1){n=o;break}if(c[e>>2]&32){n=o;break}ic(4798,1,e)|0;n=o}else{if(n>>>0<=ea>>>0)break;Bc(ea|0,48,n+Z|0)|0;do n=n+-1|0;while(n>>>0>ea>>>0)}while(0);o=T-n|0;if(!(c[e>>2]&32))ic(n,(f|0)>(o|0)?o:f,e)|0;f=f-o|0;q=q+4|0}while(q>>>0<s>>>0&(f|0)>-1)}uc(e,48,f+18|0,18,0);if(c[e>>2]&32)break;ic(t,ba-t|0,e)|0}while(0);uc(e,32,J,v,H^8192);f=(v|0)<(J|0)?J:v}else{s=(t&32|0)!=0;r=p!=p|0.0!=0.0;n=r?0:F;q=n+3|0;uc(e,32,J,q,o);f=c[e>>2]|0;if(!(f&32)){ic(G,n,e)|0;f=c[e>>2]|0}if(!(f&32))ic(r?(s?4790:4794):s?4782:4786,3,e)|0;uc(e,32,J,q,H^8192);f=(q|0)<(J|0)?J:q}while(0);n=f;r=K;x=I;continue a}default:{f=x;o=H;t=r;v=0;u=4746;n=N}}while(0);g:do if((L|0)==64){o=ca;n=c[o>>2]|0;o=c[o+4>>2]|0;q=t&32;if(!((n|0)==0&(o|0)==0)){f=N;do{f=f+-1|0;a[f>>0]=d[4730+(n&15)>>0]|q;n=Dc(n|0,o|0,4)|0;o=C}while(!((n|0)==0&(o|0)==0));L=ca;if((s&8|0)==0|(c[L>>2]|0)==0&(c[L+4>>2]|0)==0){n=s;s=0;q=4746;L=77}else{n=s;s=2;q=4746+(t>>4)|0;L=77}}else{f=N;n=s;s=0;q=4746;L=77}}else if((L|0)==76){f=tc(f,n,N)|0;n=H;s=o;L=77}else if((L|0)==82){L=0;H=cc(n,0,r)|0;G=(H|0)==0;f=n;t=G?r:H-n|0;v=0;u=4746;n=G?n+r|0:H}else if((L|0)==86){L=0;o=0;n=0;s=f;while(1){q=c[s>>2]|0;if(!q)break;n=nc(ga,q)|0;if((n|0)<0|n>>>0>(r-o|0)>>>0)break;o=n+o|0;if(r>>>0>o>>>0)s=s+4|0;else break}if((n|0)<0){m=-1;break a}uc(e,32,J,o,H);if(!o){f=0;L=97}else{q=0;while(1){n=c[f>>2]|0;if(!n){f=o;L=97;break g}n=nc(ga,n)|0;q=n+q|0;if((q|0)>(o|0)){f=o;L=97;break g}if(!(c[e>>2]&32))ic(ga,n,e)|0;if(q>>>0>=o>>>0){f=o;L=97;break}else f=f+4|0}}}while(0);if((L|0)==97){L=0;uc(e,32,J,f,H^8192);n=(J|0)>(f|0)?J:f;r=K;x=I;continue}if((L|0)==77){L=0;o=(r|0)>-1?n&-65537:n;n=ca;n=(c[n>>2]|0)!=0|(c[n+4>>2]|0)!=0;if((r|0)!=0|n){t=(n&1^1)+(V-f)|0;t=(r|0)>(t|0)?r:t;v=s;u=q;n=N}else{f=N;t=0;v=s;u=q;n=N}}s=n-f|0;q=(t|0)<(s|0)?s:t;r=v+q|0;n=(J|0)<(r|0)?r:J;uc(e,32,n,r,o);if(!(c[e>>2]&32))ic(u,v,e)|0;uc(e,48,n,r,o^65536);uc(e,48,q,s,0);if(!(c[e>>2]&32))ic(f,s,e)|0;uc(e,32,n,r,o^8192);r=K;x=I}h:do if((L|0)==244)if(!e)if(!r)m=0;else{m=1;while(1){f=c[l+(m<<2)>>2]|0;if(!f){f=0;break}sc(j+(m<<3)|0,f,g);m=m+1|0;if((m|0)>=10){m=1;break h}}while(1){m=m+1|0;if(f){m=-1;break h}if((m|0)>=10){m=1;break h}f=c[l+(m<<2)>>2]|0}}while(0);i=ia;return m|0}function sc(a,b,d){a=a|0;b=b|0;d=d|0;var e=0,f=0,g=0.0;a:do if(b>>>0<=20)do switch(b|0){case 9:{e=(c[d>>2]|0)+(4-1)&~(4-1);b=c[e>>2]|0;c[d>>2]=e+4;c[a>>2]=b;break a}case 10:{e=(c[d>>2]|0)+(4-1)&~(4-1);b=c[e>>2]|0;c[d>>2]=e+4;e=a;c[e>>2]=b;c[e+4>>2]=((b|0)<0)<<31>>31;break a}case 11:{e=(c[d>>2]|0)+(4-1)&~(4-1);b=c[e>>2]|0;c[d>>2]=e+4;e=a;c[e>>2]=b;c[e+4>>2]=0;break a}case 12:{e=(c[d>>2]|0)+(8-1)&~(8-1);b=e;f=c[b>>2]|0;b=c[b+4>>2]|0;c[d>>2]=e+8;e=a;c[e>>2]=f;c[e+4>>2]=b;break a}case 13:{f=(c[d>>2]|0)+(4-1)&~(4-1);e=c[f>>2]|0;c[d>>2]=f+4;e=(e&65535)<<16>>16;f=a;c[f>>2]=e;c[f+4>>2]=((e|0)<0)<<31>>31;break a}case 14:{f=(c[d>>2]|0)+(4-1)&~(4-1);e=c[f>>2]|0;c[d>>2]=f+4;f=a;c[f>>2]=e&65535;c[f+4>>2]=0;break a}case 15:{f=(c[d>>2]|0)+(4-1)&~(4-1);e=c[f>>2]|0;c[d>>2]=f+4;e=(e&255)<<24>>24;f=a;c[f>>2]=e;c[f+4>>2]=((e|0)<0)<<31>>31;break a}case 16:{f=(c[d>>2]|0)+(4-1)&~(4-1);e=c[f>>2]|0;c[d>>2]=f+4;f=a;c[f>>2]=e&255;c[f+4>>2]=0;break a}case 17:{f=(c[d>>2]|0)+(8-1)&~(8-1);g=+h[f>>3];c[d>>2]=f+8;h[a>>3]=g;break a}case 18:{f=(c[d>>2]|0)+(8-1)&~(8-1);g=+h[f>>3];c[d>>2]=f+8;h[a>>3]=g;break a}default:break a}while(0);while(0);return}function tc(b,c,d){b=b|0;c=c|0;d=d|0;var e=0;if(c>>>0>0|(c|0)==0&b>>>0>4294967295)while(1){e=Pc(b|0,c|0,10,0)|0;d=d+-1|0;a[d>>0]=e|48;e=b;b=Oc(b|0,c|0,10,0)|0;if(!(c>>>0>9|(c|0)==9&e>>>0>4294967295))break;else c=C}if(b)while(1){d=d+-1|0;a[d>>0]=(b>>>0)%10|0|48;if(b>>>0<10)break;else b=(b>>>0)/10|0}return d|0}function uc(a,b,d,e,f){a=a|0;b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0;h=i;i=i+256|0;g=h;do if((d|0)>(e|0)&(f&73728|0)==0){f=d-e|0;Bc(g|0,b|0,(f>>>0>256?256:f)|0)|0;e=c[a>>2]|0;d=(e&32|0)==0;if(f>>>0>255){b=f;do{if(d){ic(g,256,a)|0;e=c[a>>2]|0}b=b+-256|0;d=(e&32|0)==0}while(b>>>0>255);if(d)f=f&255;else break}else if(!d)break;ic(g,f,a)|0}while(0);i=h;return}function vc(a){a=a|0;var b=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0;do if(a>>>0<245){o=a>>>0<11?16:a+11&-8;a=o>>>3;j=c[1212]|0;b=j>>>a;if(b&3){b=(b&1^1)+a|0;d=4888+(b<<1<<2)|0;e=d+8|0;f=c[e>>2]|0;g=f+8|0;h=c[g>>2]|0;do if((d|0)!=(h|0)){if(h>>>0<(c[1216]|0)>>>0)ga();a=h+12|0;if((c[a>>2]|0)==(f|0)){c[a>>2]=d;c[e>>2]=h;break}else ga()}else c[1212]=j&~(1<<b);while(0);G=b<<3;c[f+4>>2]=G|3;G=f+G+4|0;c[G>>2]=c[G>>2]|1;G=g;return G|0}h=c[1214]|0;if(o>>>0>h>>>0){if(b){d=2<<a;d=b<<a&(d|0-d);d=(d&0-d)+-1|0;i=d>>>12&16;d=d>>>i;f=d>>>5&8;d=d>>>f;g=d>>>2&4;d=d>>>g;e=d>>>1&2;d=d>>>e;b=d>>>1&1;b=(f|i|g|e|b)+(d>>>b)|0;d=4888+(b<<1<<2)|0;e=d+8|0;g=c[e>>2]|0;i=g+8|0;f=c[i>>2]|0;do if((d|0)!=(f|0)){if(f>>>0<(c[1216]|0)>>>0)ga();a=f+12|0;if((c[a>>2]|0)==(g|0)){c[a>>2]=d;c[e>>2]=f;k=c[1214]|0;break}else ga()}else{c[1212]=j&~(1<<b);k=h}while(0);h=(b<<3)-o|0;c[g+4>>2]=o|3;e=g+o|0;c[e+4>>2]=h|1;c[e+h>>2]=h;if(k){f=c[1217]|0;b=k>>>3;d=4888+(b<<1<<2)|0;a=c[1212]|0;b=1<<b;if(a&b){a=d+8|0;b=c[a>>2]|0;if(b>>>0<(c[1216]|0)>>>0)ga();else{l=a;m=b}}else{c[1212]=a|b;l=d+8|0;m=d}c[l>>2]=f;c[m+12>>2]=f;c[f+8>>2]=m;c[f+12>>2]=d}c[1214]=h;c[1217]=e;G=i;return G|0}a=c[1213]|0;if(a){i=(a&0-a)+-1|0;F=i>>>12&16;i=i>>>F;E=i>>>5&8;i=i>>>E;G=i>>>2&4;i=i>>>G;b=i>>>1&2;i=i>>>b;j=i>>>1&1;j=c[5152+((E|F|G|b|j)+(i>>>j)<<2)>>2]|0;i=(c[j+4>>2]&-8)-o|0;b=j;while(1){a=c[b+16>>2]|0;if(!a){a=c[b+20>>2]|0;if(!a)break}b=(c[a+4>>2]&-8)-o|0;G=b>>>0<i>>>0;i=G?b:i;b=a;j=G?a:j}f=c[1216]|0;if(j>>>0<f>>>0)ga();h=j+o|0;if(j>>>0>=h>>>0)ga();g=c[j+24>>2]|0;d=c[j+12>>2]|0;do if((d|0)==(j|0)){b=j+20|0;a=c[b>>2]|0;if(!a){b=j+16|0;a=c[b>>2]|0;if(!a){n=0;break}}while(1){d=a+20|0;e=c[d>>2]|0;if(e){a=e;b=d;continue}d=a+16|0;e=c[d>>2]|0;if(!e)break;else{a=e;b=d}}if(b>>>0<f>>>0)ga();else{c[b>>2]=0;n=a;break}}else{e=c[j+8>>2]|0;if(e>>>0<f>>>0)ga();a=e+12|0;if((c[a>>2]|0)!=(j|0))ga();b=d+8|0;if((c[b>>2]|0)==(j|0)){c[a>>2]=d;c[b>>2]=e;n=d;break}else ga()}while(0);do if(g){a=c[j+28>>2]|0;b=5152+(a<<2)|0;if((j|0)==(c[b>>2]|0)){c[b>>2]=n;if(!n){c[1213]=c[1213]&~(1<<a);break}}else{if(g>>>0<(c[1216]|0)>>>0)ga();a=g+16|0;if((c[a>>2]|0)==(j|0))c[a>>2]=n;else c[g+20>>2]=n;if(!n)break}b=c[1216]|0;if(n>>>0<b>>>0)ga();c[n+24>>2]=g;a=c[j+16>>2]|0;do if(a)if(a>>>0<b>>>0)ga();else{c[n+16>>2]=a;c[a+24>>2]=n;break}while(0);a=c[j+20>>2]|0;if(a)if(a>>>0<(c[1216]|0)>>>0)ga();else{c[n+20>>2]=a;c[a+24>>2]=n;break}}while(0);if(i>>>0<16){G=i+o|0;c[j+4>>2]=G|3;G=j+G+4|0;c[G>>2]=c[G>>2]|1}else{c[j+4>>2]=o|3;c[h+4>>2]=i|1;c[h+i>>2]=i;a=c[1214]|0;if(a){e=c[1217]|0;b=a>>>3;d=4888+(b<<1<<2)|0;a=c[1212]|0;b=1<<b;if(a&b){a=d+8|0;b=c[a>>2]|0;if(b>>>0<(c[1216]|0)>>>0)ga();else{p=a;q=b}}else{c[1212]=a|b;p=d+8|0;q=d}c[p>>2]=e;c[q+12>>2]=e;c[e+8>>2]=q;c[e+12>>2]=d}c[1214]=i;c[1217]=h}G=j+8|0;return G|0}}}else if(a>>>0<=4294967231){a=a+11|0;o=a&-8;k=c[1213]|0;if(k){d=0-o|0;a=a>>>8;if(a)if(o>>>0>16777215)j=31;else{q=(a+1048320|0)>>>16&8;z=a<<q;p=(z+520192|0)>>>16&4;z=z<<p;j=(z+245760|0)>>>16&2;j=14-(p|q|j)+(z<<j>>>15)|0;j=o>>>(j+7|0)&1|j<<1}else j=0;b=c[5152+(j<<2)>>2]|0;a:do if(!b){a=0;b=0;z=86}else{f=d;a=0;h=o<<((j|0)==31?0:25-(j>>>1)|0);i=b;b=0;while(1){e=c[i+4>>2]&-8;d=e-o|0;if(d>>>0<f>>>0)if((e|0)==(o|0)){a=i;b=i;z=90;break a}else b=i;else d=f;e=c[i+20>>2]|0;i=c[i+16+(h>>>31<<2)>>2]|0;a=(e|0)==0|(e|0)==(i|0)?a:e;e=(i|0)==0;if(e){z=86;break}else{f=d;h=h<<(e&1^1)}}}while(0);if((z|0)==86){if((a|0)==0&(b|0)==0){a=2<<j;a=k&(a|0-a);if(!a)break;q=(a&0-a)+-1|0;m=q>>>12&16;q=q>>>m;l=q>>>5&8;q=q>>>l;n=q>>>2&4;q=q>>>n;p=q>>>1&2;q=q>>>p;a=q>>>1&1;a=c[5152+((l|m|n|p|a)+(q>>>a)<<2)>>2]|0}if(!a){i=d;j=b}else z=90}if((z|0)==90)while(1){z=0;q=(c[a+4>>2]&-8)-o|0;e=q>>>0<d>>>0;d=e?q:d;b=e?a:b;e=c[a+16>>2]|0;if(e){a=e;z=90;continue}a=c[a+20>>2]|0;if(!a){i=d;j=b;break}else z=90}if((j|0)!=0?i>>>0<((c[1214]|0)-o|0)>>>0:0){f=c[1216]|0;if(j>>>0<f>>>0)ga();h=j+o|0;if(j>>>0>=h>>>0)ga();g=c[j+24>>2]|0;d=c[j+12>>2]|0;do if((d|0)==(j|0)){b=j+20|0;a=c[b>>2]|0;if(!a){b=j+16|0;a=c[b>>2]|0;if(!a){s=0;break}}while(1){d=a+20|0;e=c[d>>2]|0;if(e){a=e;b=d;continue}d=a+16|0;e=c[d>>2]|0;if(!e)break;else{a=e;b=d}}if(b>>>0<f>>>0)ga();else{c[b>>2]=0;s=a;break}}else{e=c[j+8>>2]|0;if(e>>>0<f>>>0)ga();a=e+12|0;if((c[a>>2]|0)!=(j|0))ga();b=d+8|0;if((c[b>>2]|0)==(j|0)){c[a>>2]=d;c[b>>2]=e;s=d;break}else ga()}while(0);do if(g){a=c[j+28>>2]|0;b=5152+(a<<2)|0;if((j|0)==(c[b>>2]|0)){c[b>>2]=s;if(!s){c[1213]=c[1213]&~(1<<a);break}}else{if(g>>>0<(c[1216]|0)>>>0)ga();a=g+16|0;if((c[a>>2]|0)==(j|0))c[a>>2]=s;else c[g+20>>2]=s;if(!s)break}b=c[1216]|0;if(s>>>0<b>>>0)ga();c[s+24>>2]=g;a=c[j+16>>2]|0;do if(a)if(a>>>0<b>>>0)ga();else{c[s+16>>2]=a;c[a+24>>2]=s;break}while(0);a=c[j+20>>2]|0;if(a)if(a>>>0<(c[1216]|0)>>>0)ga();else{c[s+20>>2]=a;c[a+24>>2]=s;break}}while(0);do if(i>>>0>=16){c[j+4>>2]=o|3;c[h+4>>2]=i|1;c[h+i>>2]=i;a=i>>>3;if(i>>>0<256){d=4888+(a<<1<<2)|0;b=c[1212]|0;a=1<<a;if(b&a){a=d+8|0;b=c[a>>2]|0;if(b>>>0<(c[1216]|0)>>>0)ga();else{t=a;v=b}}else{c[1212]=b|a;t=d+8|0;v=d}c[t>>2]=h;c[v+12>>2]=h;c[h+8>>2]=v;c[h+12>>2]=d;break}a=i>>>8;if(a)if(i>>>0>16777215)d=31;else{F=(a+1048320|0)>>>16&8;G=a<<F;E=(G+520192|0)>>>16&4;G=G<<E;d=(G+245760|0)>>>16&2;d=14-(E|F|d)+(G<<d>>>15)|0;d=i>>>(d+7|0)&1|d<<1}else d=0;e=5152+(d<<2)|0;c[h+28>>2]=d;a=h+16|0;c[a+4>>2]=0;c[a>>2]=0;a=c[1213]|0;b=1<<d;if(!(a&b)){c[1213]=a|b;c[e>>2]=h;c[h+24>>2]=e;c[h+12>>2]=h;c[h+8>>2]=h;break}d=i<<((d|0)==31?0:25-(d>>>1)|0);e=c[e>>2]|0;while(1){if((c[e+4>>2]&-8|0)==(i|0)){z=148;break}b=e+16+(d>>>31<<2)|0;a=c[b>>2]|0;if(!a){z=145;break}else{d=d<<1;e=a}}if((z|0)==145)if(b>>>0<(c[1216]|0)>>>0)ga();else{c[b>>2]=h;c[h+24>>2]=e;c[h+12>>2]=h;c[h+8>>2]=h;break}else if((z|0)==148){a=e+8|0;b=c[a>>2]|0;G=c[1216]|0;if(b>>>0>=G>>>0&e>>>0>=G>>>0){c[b+12>>2]=h;c[a>>2]=h;c[h+8>>2]=b;c[h+12>>2]=e;c[h+24>>2]=0;break}else ga()}}else{G=i+o|0;c[j+4>>2]=G|3;G=j+G+4|0;c[G>>2]=c[G>>2]|1}while(0);G=j+8|0;return G|0}}}else o=-1;while(0);d=c[1214]|0;if(d>>>0>=o>>>0){a=d-o|0;b=c[1217]|0;if(a>>>0>15){G=b+o|0;c[1217]=G;c[1214]=a;c[G+4>>2]=a|1;c[G+a>>2]=a;c[b+4>>2]=o|3}else{c[1214]=0;c[1217]=0;c[b+4>>2]=d|3;G=b+d+4|0;c[G>>2]=c[G>>2]|1}G=b+8|0;return G|0}a=c[1215]|0;if(a>>>0>o>>>0){E=a-o|0;c[1215]=E;G=c[1218]|0;F=G+o|0;c[1218]=F;c[F+4>>2]=E|1;c[G+4>>2]=o|3;G=G+8|0;return G|0}do if(!(c[1330]|0)){a=ea(30)|0;if(!(a+-1&a)){c[1332]=a;c[1331]=a;c[1333]=-1;c[1334]=-1;c[1335]=0;c[1323]=0;c[1330]=(ja(0)|0)&-16^1431655768;break}else ga()}while(0);h=o+48|0;e=c[1332]|0;i=o+47|0;d=e+i|0;e=0-e|0;j=d&e;if(j>>>0<=o>>>0){G=0;return G|0}a=c[1322]|0;if((a|0)!=0?(t=c[1320]|0,v=t+j|0,v>>>0<=t>>>0|v>>>0>a>>>0):0){G=0;return G|0}b:do if(!(c[1323]&4)){b=c[1218]|0;c:do if(b){f=5296;while(1){a=c[f>>2]|0;if(a>>>0<=b>>>0?(r=f+4|0,(a+(c[r>>2]|0)|0)>>>0>b>>>0):0)break;a=c[f+8>>2]|0;if(!a){z=173;break c}else f=a}a=d-(c[1215]|0)&e;if(a>>>0<2147483647){b=ia(a|0)|0;if((b|0)==((c[f>>2]|0)+(c[r>>2]|0)|0)){if((b|0)!=(-1|0)){h=b;g=a;z=193;break b}}else z=183}}else z=173;while(0);do if((z|0)==173?(u=ia(0)|0,(u|0)!=(-1|0)):0){a=u;b=c[1331]|0;d=b+-1|0;if(!(d&a))a=j;else a=j-a+(d+a&0-b)|0;b=c[1320]|0;d=b+a|0;if(a>>>0>o>>>0&a>>>0<2147483647){v=c[1322]|0;if((v|0)!=0?d>>>0<=b>>>0|d>>>0>v>>>0:0)break;b=ia(a|0)|0;if((b|0)==(u|0)){h=u;g=a;z=193;break b}else z=183}}while(0);d:do if((z|0)==183){d=0-a|0;do if(h>>>0>a>>>0&(a>>>0<2147483647&(b|0)!=(-1|0))?(w=c[1332]|0,w=i-a+w&0-w,w>>>0<2147483647):0)if((ia(w|0)|0)==(-1|0)){ia(d|0)|0;break d}else{a=w+a|0;break}while(0);if((b|0)!=(-1|0)){h=b;g=a;z=193;break b}}while(0);c[1323]=c[1323]|4;z=190}else z=190;while(0);if((((z|0)==190?j>>>0<2147483647:0)?(x=ia(j|0)|0,y=ia(0)|0,x>>>0<y>>>0&((x|0)!=(-1|0)&(y|0)!=(-1|0))):0)?(g=y-x|0,g>>>0>(o+40|0)>>>0):0){h=x;z=193}if((z|0)==193){a=(c[1320]|0)+g|0;c[1320]=a;if(a>>>0>(c[1321]|0)>>>0)c[1321]=a;k=c[1218]|0;do if(k){f=5296;while(1){a=c[f>>2]|0;b=f+4|0;d=c[b>>2]|0;if((h|0)==(a+d|0)){z=203;break}e=c[f+8>>2]|0;if(!e)break;else f=e}if(((z|0)==203?(c[f+12>>2]&8|0)==0:0)?k>>>0<h>>>0&k>>>0>=a>>>0:0){c[b>>2]=d+g;G=k+8|0;G=(G&7|0)==0?0:0-G&7;F=k+G|0;G=g-G+(c[1215]|0)|0;c[1218]=F;c[1215]=G;c[F+4>>2]=G|1;c[F+G+4>>2]=40;c[1219]=c[1334];break}a=c[1216]|0;if(h>>>0<a>>>0){c[1216]=h;i=h}else i=a;b=h+g|0;a=5296;while(1){if((c[a>>2]|0)==(b|0)){z=211;break}a=c[a+8>>2]|0;if(!a){b=5296;break}}if((z|0)==211)if(!(c[a+12>>2]&8)){c[a>>2]=h;m=a+4|0;c[m>>2]=(c[m>>2]|0)+g;m=h+8|0;m=h+((m&7|0)==0?0:0-m&7)|0;a=b+8|0;a=b+((a&7|0)==0?0:0-a&7)|0;l=m+o|0;j=a-m-o|0;c[m+4>>2]=o|3;do if((a|0)!=(k|0)){if((a|0)==(c[1217]|0)){G=(c[1214]|0)+j|0;c[1214]=G;c[1217]=l;c[l+4>>2]=G|1;c[l+G>>2]=G;break}b=c[a+4>>2]|0;if((b&3|0)==1){h=b&-8;f=b>>>3;e:do if(b>>>0>=256){g=c[a+24>>2]|0;e=c[a+12>>2]|0;do if((e|0)==(a|0)){e=a+16|0;d=e+4|0;b=c[d>>2]|0;if(!b){b=c[e>>2]|0;if(!b){E=0;break}else d=e}while(1){e=b+20|0;f=c[e>>2]|0;if(f){b=f;d=e;continue}e=b+16|0;f=c[e>>2]|0;if(!f)break;else{b=f;d=e}}if(d>>>0<i>>>0)ga();else{c[d>>2]=0;E=b;break}}else{f=c[a+8>>2]|0;if(f>>>0<i>>>0)ga();b=f+12|0;if((c[b>>2]|0)!=(a|0))ga();d=e+8|0;if((c[d>>2]|0)==(a|0)){c[b>>2]=e;c[d>>2]=f;E=e;break}else ga()}while(0);if(!g)break;b=c[a+28>>2]|0;d=5152+(b<<2)|0;do if((a|0)!=(c[d>>2]|0)){if(g>>>0<(c[1216]|0)>>>0)ga();b=g+16|0;if((c[b>>2]|0)==(a|0))c[b>>2]=E;else c[g+20>>2]=E;if(!E)break e}else{c[d>>2]=E;if(E)break;c[1213]=c[1213]&~(1<<b);break e}while(0);e=c[1216]|0;if(E>>>0<e>>>0)ga();c[E+24>>2]=g;b=a+16|0;d=c[b>>2]|0;do if(d)if(d>>>0<e>>>0)ga();else{c[E+16>>2]=d;c[d+24>>2]=E;break}while(0);b=c[b+4>>2]|0;if(!b)break;if(b>>>0<(c[1216]|0)>>>0)ga();else{c[E+20>>2]=b;c[b+24>>2]=E;break}}else{d=c[a+8>>2]|0;e=c[a+12>>2]|0;b=4888+(f<<1<<2)|0;do if((d|0)!=(b|0)){if(d>>>0<i>>>0)ga();if((c[d+12>>2]|0)==(a|0))break;ga()}while(0);if((e|0)==(d|0)){c[1212]=c[1212]&~(1<<f);break}do if((e|0)==(b|0))B=e+8|0;else{if(e>>>0<i>>>0)ga();b=e+8|0;if((c[b>>2]|0)==(a|0)){B=b;break}ga()}while(0);c[d+12>>2]=e;c[B>>2]=d}while(0);a=a+h|0;f=h+j|0}else f=j;a=a+4|0;c[a>>2]=c[a>>2]&-2;c[l+4>>2]=f|1;c[l+f>>2]=f;a=f>>>3;if(f>>>0<256){d=4888+(a<<1<<2)|0;b=c[1212]|0;a=1<<a;do if(!(b&a)){c[1212]=b|a;F=d+8|0;G=d}else{a=d+8|0;b=c[a>>2]|0;if(b>>>0>=(c[1216]|0)>>>0){F=a;G=b;break}ga()}while(0);c[F>>2]=l;c[G+12>>2]=l;c[l+8>>2]=G;c[l+12>>2]=d;break}a=f>>>8;do if(!a)d=0;else{if(f>>>0>16777215){d=31;break}F=(a+1048320|0)>>>16&8;G=a<<F;E=(G+520192|0)>>>16&4;G=G<<E;d=(G+245760|0)>>>16&2;d=14-(E|F|d)+(G<<d>>>15)|0;d=f>>>(d+7|0)&1|d<<1}while(0);e=5152+(d<<2)|0;c[l+28>>2]=d;a=l+16|0;c[a+4>>2]=0;c[a>>2]=0;a=c[1213]|0;b=1<<d;if(!(a&b)){c[1213]=a|b;c[e>>2]=l;c[l+24>>2]=e;c[l+12>>2]=l;c[l+8>>2]=l;break}d=f<<((d|0)==31?0:25-(d>>>1)|0);e=c[e>>2]|0;while(1){if((c[e+4>>2]&-8|0)==(f|0)){z=281;break}b=e+16+(d>>>31<<2)|0;a=c[b>>2]|0;if(!a){z=278;break}else{d=d<<1;e=a}}if((z|0)==278)if(b>>>0<(c[1216]|0)>>>0)ga();else{c[b>>2]=l;c[l+24>>2]=e;c[l+12>>2]=l;c[l+8>>2]=l;break}else if((z|0)==281){a=e+8|0;b=c[a>>2]|0;G=c[1216]|0;if(b>>>0>=G>>>0&e>>>0>=G>>>0){c[b+12>>2]=l;c[a>>2]=l;c[l+8>>2]=b;c[l+12>>2]=e;c[l+24>>2]=0;break}else ga()}}else{G=(c[1215]|0)+j|0;c[1215]=G;c[1218]=l;c[l+4>>2]=G|1}while(0);G=m+8|0;return G|0}else b=5296;while(1){a=c[b>>2]|0;if(a>>>0<=k>>>0?(A=a+(c[b+4>>2]|0)|0,A>>>0>k>>>0):0)break;b=c[b+8>>2]|0}f=A+-47|0;b=f+8|0;b=f+((b&7|0)==0?0:0-b&7)|0;f=k+16|0;b=b>>>0<f>>>0?k:b;a=b+8|0;d=h+8|0;d=(d&7|0)==0?0:0-d&7;G=h+d|0;d=g+-40-d|0;c[1218]=G;c[1215]=d;c[G+4>>2]=d|1;c[G+d+4>>2]=40;c[1219]=c[1334];d=b+4|0;c[d>>2]=27;c[a>>2]=c[1324];c[a+4>>2]=c[1325];c[a+8>>2]=c[1326];c[a+12>>2]=c[1327];c[1324]=h;c[1325]=g;c[1327]=0;c[1326]=a;a=b+24|0;do{a=a+4|0;c[a>>2]=7}while((a+4|0)>>>0<A>>>0);if((b|0)!=(k|0)){g=b-k|0;c[d>>2]=c[d>>2]&-2;c[k+4>>2]=g|1;c[b>>2]=g;a=g>>>3;if(g>>>0<256){d=4888+(a<<1<<2)|0;b=c[1212]|0;a=1<<a;if(b&a){a=d+8|0;b=c[a>>2]|0;if(b>>>0<(c[1216]|0)>>>0)ga();else{C=a;D=b}}else{c[1212]=b|a;C=d+8|0;D=d}c[C>>2]=k;c[D+12>>2]=k;c[k+8>>2]=D;c[k+12>>2]=d;break}a=g>>>8;if(a)if(g>>>0>16777215)d=31;else{F=(a+1048320|0)>>>16&8;G=a<<F;E=(G+520192|0)>>>16&4;G=G<<E;d=(G+245760|0)>>>16&2;d=14-(E|F|d)+(G<<d>>>15)|0;d=g>>>(d+7|0)&1|d<<1}else d=0;e=5152+(d<<2)|0;c[k+28>>2]=d;c[k+20>>2]=0;c[f>>2]=0;a=c[1213]|0;b=1<<d;if(!(a&b)){c[1213]=a|b;c[e>>2]=k;c[k+24>>2]=e;c[k+12>>2]=k;c[k+8>>2]=k;break}d=g<<((d|0)==31?0:25-(d>>>1)|0);e=c[e>>2]|0;while(1){if((c[e+4>>2]&-8|0)==(g|0)){z=307;break}b=e+16+(d>>>31<<2)|0;a=c[b>>2]|0;if(!a){z=304;break}else{d=d<<1;e=a}}if((z|0)==304)if(b>>>0<(c[1216]|0)>>>0)ga();else{c[b>>2]=k;c[k+24>>2]=e;c[k+12>>2]=k;c[k+8>>2]=k;break}else if((z|0)==307){a=e+8|0;b=c[a>>2]|0;G=c[1216]|0;if(b>>>0>=G>>>0&e>>>0>=G>>>0){c[b+12>>2]=k;c[a>>2]=k;c[k+8>>2]=b;c[k+12>>2]=e;c[k+24>>2]=0;break}else ga()}}}else{G=c[1216]|0;if((G|0)==0|h>>>0<G>>>0)c[1216]=h;c[1324]=h;c[1325]=g;c[1327]=0;c[1221]=c[1330];c[1220]=-1;a=0;do{G=4888+(a<<1<<2)|0;c[G+12>>2]=G;c[G+8>>2]=G;a=a+1|0}while((a|0)!=32);G=h+8|0;G=(G&7|0)==0?0:0-G&7;F=h+G|0;G=g+-40-G|0;c[1218]=F;c[1215]=G;c[F+4>>2]=G|1;c[F+G+4>>2]=40;c[1219]=c[1334]}while(0);a=c[1215]|0;if(a>>>0>o>>>0){E=a-o|0;c[1215]=E;G=c[1218]|0;F=G+o|0;c[1218]=F;c[F+4>>2]=E|1;c[G+4>>2]=o|3;G=G+8|0;return G|0}}c[(ec()|0)>>2]=12;G=0;return G|0}function wc(a){a=a|0;var b=0,d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0;if(!a)return;d=a+-8|0;h=c[1216]|0;if(d>>>0<h>>>0)ga();a=c[a+-4>>2]|0;b=a&3;if((b|0)==1)ga();e=a&-8;m=d+e|0;do if(!(a&1)){a=c[d>>2]|0;if(!b)return;k=d+(0-a)|0;j=a+e|0;if(k>>>0<h>>>0)ga();if((k|0)==(c[1217]|0)){a=m+4|0;b=c[a>>2]|0;if((b&3|0)!=3){q=k;f=j;break}c[1214]=j;c[a>>2]=b&-2;c[k+4>>2]=j|1;c[k+j>>2]=j;return}e=a>>>3;if(a>>>0<256){b=c[k+8>>2]|0;d=c[k+12>>2]|0;a=4888+(e<<1<<2)|0;if((b|0)!=(a|0)){if(b>>>0<h>>>0)ga();if((c[b+12>>2]|0)!=(k|0))ga()}if((d|0)==(b|0)){c[1212]=c[1212]&~(1<<e);q=k;f=j;break}if((d|0)!=(a|0)){if(d>>>0<h>>>0)ga();a=d+8|0;if((c[a>>2]|0)==(k|0))g=a;else ga()}else g=d+8|0;c[b+12>>2]=d;c[g>>2]=b;q=k;f=j;break}g=c[k+24>>2]|0;d=c[k+12>>2]|0;do if((d|0)==(k|0)){d=k+16|0;b=d+4|0;a=c[b>>2]|0;if(!a){a=c[d>>2]|0;if(!a){i=0;break}else b=d}while(1){d=a+20|0;e=c[d>>2]|0;if(e){a=e;b=d;continue}d=a+16|0;e=c[d>>2]|0;if(!e)break;else{a=e;b=d}}if(b>>>0<h>>>0)ga();else{c[b>>2]=0;i=a;break}}else{e=c[k+8>>2]|0;if(e>>>0<h>>>0)ga();a=e+12|0;if((c[a>>2]|0)!=(k|0))ga();b=d+8|0;if((c[b>>2]|0)==(k|0)){c[a>>2]=d;c[b>>2]=e;i=d;break}else ga()}while(0);if(g){a=c[k+28>>2]|0;b=5152+(a<<2)|0;if((k|0)==(c[b>>2]|0)){c[b>>2]=i;if(!i){c[1213]=c[1213]&~(1<<a);q=k;f=j;break}}else{if(g>>>0<(c[1216]|0)>>>0)ga();a=g+16|0;if((c[a>>2]|0)==(k|0))c[a>>2]=i;else c[g+20>>2]=i;if(!i){q=k;f=j;break}}d=c[1216]|0;if(i>>>0<d>>>0)ga();c[i+24>>2]=g;a=k+16|0;b=c[a>>2]|0;do if(b)if(b>>>0<d>>>0)ga();else{c[i+16>>2]=b;c[b+24>>2]=i;break}while(0);a=c[a+4>>2]|0;if(a)if(a>>>0<(c[1216]|0)>>>0)ga();else{c[i+20>>2]=a;c[a+24>>2]=i;q=k;f=j;break}else{q=k;f=j}}else{q=k;f=j}}else{q=d;f=e}while(0);if(q>>>0>=m>>>0)ga();a=m+4|0;b=c[a>>2]|0;if(!(b&1))ga();if(!(b&2)){if((m|0)==(c[1218]|0)){p=(c[1215]|0)+f|0;c[1215]=p;c[1218]=q;c[q+4>>2]=p|1;if((q|0)!=(c[1217]|0))return;c[1217]=0;c[1214]=0;return}if((m|0)==(c[1217]|0)){p=(c[1214]|0)+f|0;c[1214]=p;c[1217]=q;c[q+4>>2]=p|1;c[q+p>>2]=p;return}f=(b&-8)+f|0;e=b>>>3;do if(b>>>0>=256){g=c[m+24>>2]|0;a=c[m+12>>2]|0;do if((a|0)==(m|0)){d=m+16|0;b=d+4|0;a=c[b>>2]|0;if(!a){a=c[d>>2]|0;if(!a){n=0;break}else b=d}while(1){d=a+20|0;e=c[d>>2]|0;if(e){a=e;b=d;continue}d=a+16|0;e=c[d>>2]|0;if(!e)break;else{a=e;b=d}}if(b>>>0<(c[1216]|0)>>>0)ga();else{c[b>>2]=0;n=a;break}}else{b=c[m+8>>2]|0;if(b>>>0<(c[1216]|0)>>>0)ga();d=b+12|0;if((c[d>>2]|0)!=(m|0))ga();e=a+8|0;if((c[e>>2]|0)==(m|0)){c[d>>2]=a;c[e>>2]=b;n=a;break}else ga()}while(0);if(g){a=c[m+28>>2]|0;b=5152+(a<<2)|0;if((m|0)==(c[b>>2]|0)){c[b>>2]=n;if(!n){c[1213]=c[1213]&~(1<<a);break}}else{if(g>>>0<(c[1216]|0)>>>0)ga();a=g+16|0;if((c[a>>2]|0)==(m|0))c[a>>2]=n;else c[g+20>>2]=n;if(!n)break}d=c[1216]|0;if(n>>>0<d>>>0)ga();c[n+24>>2]=g;a=m+16|0;b=c[a>>2]|0;do if(b)if(b>>>0<d>>>0)ga();else{c[n+16>>2]=b;c[b+24>>2]=n;break}while(0);a=c[a+4>>2]|0;if(a)if(a>>>0<(c[1216]|0)>>>0)ga();else{c[n+20>>2]=a;c[a+24>>2]=n;break}}}else{b=c[m+8>>2]|0;d=c[m+12>>2]|0;a=4888+(e<<1<<2)|0;if((b|0)!=(a|0)){if(b>>>0<(c[1216]|0)>>>0)ga();if((c[b+12>>2]|0)!=(m|0))ga()}if((d|0)==(b|0)){c[1212]=c[1212]&~(1<<e);break}if((d|0)!=(a|0)){if(d>>>0<(c[1216]|0)>>>0)ga();a=d+8|0;if((c[a>>2]|0)==(m|0))l=a;else ga()}else l=d+8|0;c[b+12>>2]=d;c[l>>2]=b}while(0);c[q+4>>2]=f|1;c[q+f>>2]=f;if((q|0)==(c[1217]|0)){c[1214]=f;return}}else{c[a>>2]=b&-2;c[q+4>>2]=f|1;c[q+f>>2]=f}a=f>>>3;if(f>>>0<256){d=4888+(a<<1<<2)|0;b=c[1212]|0;a=1<<a;if(b&a){a=d+8|0;b=c[a>>2]|0;if(b>>>0<(c[1216]|0)>>>0)ga();else{o=a;p=b}}else{c[1212]=b|a;o=d+8|0;p=d}c[o>>2]=q;c[p+12>>2]=q;c[q+8>>2]=p;c[q+12>>2]=d;return}a=f>>>8;if(a)if(f>>>0>16777215)d=31;else{o=(a+1048320|0)>>>16&8;p=a<<o;n=(p+520192|0)>>>16&4;p=p<<n;d=(p+245760|0)>>>16&2;d=14-(n|o|d)+(p<<d>>>15)|0;d=f>>>(d+7|0)&1|d<<1}else d=0;e=5152+(d<<2)|0;c[q+28>>2]=d;c[q+20>>2]=0;c[q+16>>2]=0;a=c[1213]|0;b=1<<d;do if(a&b){d=f<<((d|0)==31?0:25-(d>>>1)|0);e=c[e>>2]|0;while(1){if((c[e+4>>2]&-8|0)==(f|0)){a=130;break}b=e+16+(d>>>31<<2)|0;a=c[b>>2]|0;if(!a){a=127;break}else{d=d<<1;e=a}}if((a|0)==127)if(b>>>0<(c[1216]|0)>>>0)ga();else{c[b>>2]=q;c[q+24>>2]=e;c[q+12>>2]=q;c[q+8>>2]=q;break}else if((a|0)==130){a=e+8|0;b=c[a>>2]|0;p=c[1216]|0;if(b>>>0>=p>>>0&e>>>0>=p>>>0){c[b+12>>2]=q;c[a>>2]=q;c[q+8>>2]=b;c[q+12>>2]=e;c[q+24>>2]=0;break}else ga()}}else{c[1213]=a|b;c[e>>2]=q;c[q+24>>2]=e;c[q+12>>2]=q;c[q+8>>2]=q}while(0);q=(c[1220]|0)+-1|0;c[1220]=q;if(!q)a=5304;else return;while(1){a=c[a>>2]|0;if(!a)break;else a=a+8|0}c[1220]=-1;return}function xc(a,b){a=a|0;b=b|0;var d=0,e=0;if(!a){a=vc(b)|0;return a|0}if(b>>>0>4294967231){c[(ec()|0)>>2]=12;a=0;return a|0}d=yc(a+-8|0,b>>>0<11?16:b+11&-8)|0;if(d){a=d+8|0;return a|0}d=vc(b)|0;if(!d){a=0;return a|0}e=c[a+-4>>2]|0;e=(e&-8)-((e&3|0)==0?8:4)|0;Fc(d|0,a|0,(e>>>0<b>>>0?e:b)|0)|0;wc(a);a=d;return a|0}function yc(a,b){a=a|0;b=b|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0;n=a+4|0;o=c[n>>2]|0;d=o&-8;k=a+d|0;i=c[1216]|0;e=o&3;if(!((e|0)!=1&a>>>0>=i>>>0&a>>>0<k>>>0))ga();f=c[k+4>>2]|0;if(!(f&1))ga();if(!e){if(b>>>0<256){a=0;return a|0}if(d>>>0>=(b+4|0)>>>0?(d-b|0)>>>0<=c[1332]<<1>>>0:0)return a|0;a=0;return a|0}if(d>>>0>=b>>>0){d=d-b|0;if(d>>>0<=15)return a|0;m=a+b|0;c[n>>2]=o&1|b|2;c[m+4>>2]=d|3;b=m+d+4|0;c[b>>2]=c[b>>2]|1;zc(m,d);return a|0}if((k|0)==(c[1218]|0)){d=(c[1215]|0)+d|0;if(d>>>0<=b>>>0){a=0;return a|0}m=d-b|0;l=a+b|0;c[n>>2]=o&1|b|2;c[l+4>>2]=m|1;c[1218]=l;c[1215]=m;return a|0}if((k|0)==(c[1217]|0)){e=(c[1214]|0)+d|0;if(e>>>0<b>>>0){a=0;return a|0}d=e-b|0;if(d>>>0>15){e=a+b|0;m=e+d|0;c[n>>2]=o&1|b|2;c[e+4>>2]=d|1;c[m>>2]=d;b=m+4|0;c[b>>2]=c[b>>2]&-2}else{c[n>>2]=o&1|e|2;e=a+e+4|0;c[e>>2]=c[e>>2]|1;e=0;d=0}c[1214]=d;c[1217]=e;return a|0}if(f&2){a=0;return a|0}l=(f&-8)+d|0;if(l>>>0<b>>>0){a=0;return a|0}m=l-b|0;g=f>>>3;do if(f>>>0>=256){h=c[k+24>>2]|0;f=c[k+12>>2]|0;do if((f|0)==(k|0)){f=k+16|0;e=f+4|0;d=c[e>>2]|0;if(!d){d=c[f>>2]|0;if(!d){j=0;break}else e=f}while(1){f=d+20|0;g=c[f>>2]|0;if(g){d=g;e=f;continue}f=d+16|0;g=c[f>>2]|0;if(!g)break;else{d=g;e=f}}if(e>>>0<i>>>0)ga();else{c[e>>2]=0;j=d;break}}else{g=c[k+8>>2]|0;if(g>>>0<i>>>0)ga();d=g+12|0;if((c[d>>2]|0)!=(k|0))ga();e=f+8|0;if((c[e>>2]|0)==(k|0)){c[d>>2]=f;c[e>>2]=g;j=f;break}else ga()}while(0);if(h){d=c[k+28>>2]|0;e=5152+(d<<2)|0;if((k|0)==(c[e>>2]|0)){c[e>>2]=j;if(!j){c[1213]=c[1213]&~(1<<d);break}}else{if(h>>>0<(c[1216]|0)>>>0)ga();d=h+16|0;if((c[d>>2]|0)==(k|0))c[d>>2]=j;else c[h+20>>2]=j;if(!j)break}f=c[1216]|0;if(j>>>0<f>>>0)ga();c[j+24>>2]=h;d=k+16|0;e=c[d>>2]|0;do if(e)if(e>>>0<f>>>0)ga();else{c[j+16>>2]=e;c[e+24>>2]=j;break}while(0);d=c[d+4>>2]|0;if(d)if(d>>>0<(c[1216]|0)>>>0)ga();else{c[j+20>>2]=d;c[d+24>>2]=j;break}}}else{e=c[k+8>>2]|0;f=c[k+12>>2]|0;d=4888+(g<<1<<2)|0;if((e|0)!=(d|0)){if(e>>>0<i>>>0)ga();if((c[e+12>>2]|0)!=(k|0))ga()}if((f|0)==(e|0)){c[1212]=c[1212]&~(1<<g);break}if((f|0)!=(d|0)){if(f>>>0<i>>>0)ga();d=f+8|0;if((c[d>>2]|0)==(k|0))h=d;else ga()}else h=f+8|0;c[e+12>>2]=f;c[h>>2]=e}while(0);if(m>>>0<16){c[n>>2]=l|o&1|2;b=a+l+4|0;c[b>>2]=c[b>>2]|1;return a|0}else{l=a+b|0;c[n>>2]=o&1|b|2;c[l+4>>2]=m|3;b=l+m+4|0;c[b>>2]=c[b>>2]|1;zc(l,m);return a|0}return 0}function zc(a,b){a=a|0;b=b|0;var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0;o=a+b|0;d=c[a+4>>2]|0;do if(!(d&1)){g=c[a>>2]|0;if(!(d&3))return;l=a+(0-g)|0;k=g+b|0;i=c[1216]|0;if(l>>>0<i>>>0)ga();if((l|0)==(c[1217]|0)){a=o+4|0;d=c[a>>2]|0;if((d&3|0)!=3){r=l;f=k;break}c[1214]=k;c[a>>2]=d&-2;c[l+4>>2]=k|1;c[l+k>>2]=k;return}e=g>>>3;if(g>>>0<256){a=c[l+8>>2]|0;b=c[l+12>>2]|0;d=4888+(e<<1<<2)|0;if((a|0)!=(d|0)){if(a>>>0<i>>>0)ga();if((c[a+12>>2]|0)!=(l|0))ga()}if((b|0)==(a|0)){c[1212]=c[1212]&~(1<<e);r=l;f=k;break}if((b|0)!=(d|0)){if(b>>>0<i>>>0)ga();d=b+8|0;if((c[d>>2]|0)==(l|0))h=d;else ga()}else h=b+8|0;c[a+12>>2]=b;c[h>>2]=a;r=l;f=k;break}g=c[l+24>>2]|0;b=c[l+12>>2]|0;do if((b|0)==(l|0)){b=l+16|0;a=b+4|0;d=c[a>>2]|0;if(!d){d=c[b>>2]|0;if(!d){j=0;break}else a=b}while(1){b=d+20|0;e=c[b>>2]|0;if(e){d=e;a=b;continue}b=d+16|0;e=c[b>>2]|0;if(!e)break;else{d=e;a=b}}if(a>>>0<i>>>0)ga();else{c[a>>2]=0;j=d;break}}else{e=c[l+8>>2]|0;if(e>>>0<i>>>0)ga();d=e+12|0;if((c[d>>2]|0)!=(l|0))ga();a=b+8|0;if((c[a>>2]|0)==(l|0)){c[d>>2]=b;c[a>>2]=e;j=b;break}else ga()}while(0);if(g){d=c[l+28>>2]|0;a=5152+(d<<2)|0;if((l|0)==(c[a>>2]|0)){c[a>>2]=j;if(!j){c[1213]=c[1213]&~(1<<d);r=l;f=k;break}}else{if(g>>>0<(c[1216]|0)>>>0)ga();d=g+16|0;if((c[d>>2]|0)==(l|0))c[d>>2]=j;else c[g+20>>2]=j;if(!j){r=l;f=k;break}}b=c[1216]|0;if(j>>>0<b>>>0)ga();c[j+24>>2]=g;d=l+16|0;a=c[d>>2]|0;do if(a)if(a>>>0<b>>>0)ga();else{c[j+16>>2]=a;c[a+24>>2]=j;break}while(0);d=c[d+4>>2]|0;if(d)if(d>>>0<(c[1216]|0)>>>0)ga();else{c[j+20>>2]=d;c[d+24>>2]=j;r=l;f=k;break}else{r=l;f=k}}else{r=l;f=k}}else{r=a;f=b}while(0);h=c[1216]|0;if(o>>>0<h>>>0)ga();d=o+4|0;a=c[d>>2]|0;if(!(a&2)){if((o|0)==(c[1218]|0)){q=(c[1215]|0)+f|0;c[1215]=q;c[1218]=r;c[r+4>>2]=q|1;if((r|0)!=(c[1217]|0))return;c[1217]=0;c[1214]=0;return}if((o|0)==(c[1217]|0)){q=(c[1214]|0)+f|0;c[1214]=q;c[1217]=r;c[r+4>>2]=q|1;c[r+q>>2]=q;return}f=(a&-8)+f|0;e=a>>>3;do if(a>>>0>=256){g=c[o+24>>2]|0;b=c[o+12>>2]|0;do if((b|0)==(o|0)){b=o+16|0;a=b+4|0;d=c[a>>2]|0;if(!d){d=c[b>>2]|0;if(!d){n=0;break}else a=b}while(1){b=d+20|0;e=c[b>>2]|0;if(e){d=e;a=b;continue}b=d+16|0;e=c[b>>2]|0;if(!e)break;else{d=e;a=b}}if(a>>>0<h>>>0)ga();else{c[a>>2]=0;n=d;break}}else{e=c[o+8>>2]|0;if(e>>>0<h>>>0)ga();d=e+12|0;if((c[d>>2]|0)!=(o|0))ga();a=b+8|0;if((c[a>>2]|0)==(o|0)){c[d>>2]=b;c[a>>2]=e;n=b;break}else ga()}while(0);if(g){d=c[o+28>>2]|0;a=5152+(d<<2)|0;if((o|0)==(c[a>>2]|0)){c[a>>2]=n;if(!n){c[1213]=c[1213]&~(1<<d);break}}else{if(g>>>0<(c[1216]|0)>>>0)ga();d=g+16|0;if((c[d>>2]|0)==(o|0))c[d>>2]=n;else c[g+20>>2]=n;if(!n)break}b=c[1216]|0;if(n>>>0<b>>>0)ga();c[n+24>>2]=g;d=o+16|0;a=c[d>>2]|0;do if(a)if(a>>>0<b>>>0)ga();else{c[n+16>>2]=a;c[a+24>>2]=n;break}while(0);d=c[d+4>>2]|0;if(d)if(d>>>0<(c[1216]|0)>>>0)ga();else{c[n+20>>2]=d;c[d+24>>2]=n;break}}}else{a=c[o+8>>2]|0;b=c[o+12>>2]|0;d=4888+(e<<1<<2)|0;if((a|0)!=(d|0)){if(a>>>0<h>>>0)ga();if((c[a+12>>2]|0)!=(o|0))ga()}if((b|0)==(a|0)){c[1212]=c[1212]&~(1<<e);break}if((b|0)!=(d|0)){if(b>>>0<h>>>0)ga();d=b+8|0;if((c[d>>2]|0)==(o|0))m=d;else ga()}else m=b+8|0;c[a+12>>2]=b;c[m>>2]=a}while(0);c[r+4>>2]=f|1;c[r+f>>2]=f;if((r|0)==(c[1217]|0)){c[1214]=f;return}}else{c[d>>2]=a&-2;c[r+4>>2]=f|1;c[r+f>>2]=f}d=f>>>3;if(f>>>0<256){b=4888+(d<<1<<2)|0;a=c[1212]|0;d=1<<d;if(a&d){d=b+8|0;a=c[d>>2]|0;if(a>>>0<(c[1216]|0)>>>0)ga();else{p=d;q=a}}else{c[1212]=a|d;p=b+8|0;q=b}c[p>>2]=r;c[q+12>>2]=r;c[r+8>>2]=q;c[r+12>>2]=b;return}d=f>>>8;if(d)if(f>>>0>16777215)b=31;else{p=(d+1048320|0)>>>16&8;q=d<<p;o=(q+520192|0)>>>16&4;q=q<<o;b=(q+245760|0)>>>16&2;b=14-(o|p|b)+(q<<b>>>15)|0;b=f>>>(b+7|0)&1|b<<1}else b=0;e=5152+(b<<2)|0;c[r+28>>2]=b;c[r+20>>2]=0;c[r+16>>2]=0;d=c[1213]|0;a=1<<b;if(!(d&a)){c[1213]=d|a;c[e>>2]=r;c[r+24>>2]=e;c[r+12>>2]=r;c[r+8>>2]=r;return}b=f<<((b|0)==31?0:25-(b>>>1)|0);e=c[e>>2]|0;while(1){if((c[e+4>>2]&-8|0)==(f|0)){d=127;break}a=e+16+(b>>>31<<2)|0;d=c[a>>2]|0;if(!d){d=124;break}else{b=b<<1;e=d}}if((d|0)==124){if(a>>>0<(c[1216]|0)>>>0)ga();c[a>>2]=r;c[r+24>>2]=e;c[r+12>>2]=r;c[r+8>>2]=r;return}else if((d|0)==127){d=e+8|0;a=c[d>>2]|0;q=c[1216]|0;if(!(a>>>0>=q>>>0&e>>>0>=q>>>0))ga();c[a+12>>2]=r;c[d>>2]=r;c[r+8>>2]=a;c[r+12>>2]=e;c[r+24>>2]=0;return}}function Ac(){}function Bc(b,d,e){b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,i=0;f=b+e|0;if((e|0)>=20){d=d&255;h=b&3;i=d|d<<8|d<<16|d<<24;g=f&~3;if(h){h=b+4-h|0;while((b|0)<(h|0)){a[b>>0]=d;b=b+1|0}}while((b|0)<(g|0)){c[b>>2]=i;b=b+4|0}}while((b|0)<(f|0)){a[b>>0]=d;b=b+1|0}return b-e|0}function Cc(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;d=b-d-(c>>>0>a>>>0|0)>>>0;return (C=d,a-c>>>0|0)|0}function Dc(a,b,c){a=a|0;b=b|0;c=c|0;if((c|0)<32){C=b>>>c;return a>>>c|(b&(1<<c)-1)<<32-c}C=0;return b>>>c-32|0}function Ec(a,b,c){a=a|0;b=b|0;c=c|0;if((c|0)<32){C=b<<c|(a&(1<<c)-1<<32-c)>>>32-c;return a<<c}C=a<<c-32;return 0}function Fc(b,d,e){b=b|0;d=d|0;e=e|0;var f=0;if((e|0)>=4096)return ka(b|0,d|0,e|0)|0;f=b|0;if((b&3)==(d&3)){while(b&3){if(!e)return f|0;a[b>>0]=a[d>>0]|0;b=b+1|0;d=d+1|0;e=e-1|0}while((e|0)>=4){c[b>>2]=c[d>>2];b=b+4|0;d=d+4|0;e=e-4|0}}while((e|0)>0){a[b>>0]=a[d>>0]|0;b=b+1|0;d=d+1|0;e=e-1|0}return f|0}function Gc(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;c=a+c>>>0;return (C=b+d+(c>>>0<a>>>0|0)>>>0,c|0)|0}function Hc(b,c,d){b=b|0;c=c|0;d=d|0;var e=0;if((c|0)<(b|0)&(b|0)<(c+d|0)){e=b;c=c+d|0;b=b+d|0;while((d|0)>0){b=b-1|0;c=c-1|0;d=d-1|0;a[b>>0]=a[c>>0]|0}b=e}else Fc(b,c,d)|0;return b|0}function Ic(a,b,c){a=a|0;b=b|0;c=c|0;if((c|0)<32){C=b>>c;return a>>>c|(b&(1<<c)-1)<<32-c}C=(b|0)<0?-1:0;return b>>c-32|0}function Jc(b){b=b|0;var c=0;c=a[m+(b&255)>>0]|0;if((c|0)<8)return c|0;c=a[m+(b>>8&255)>>0]|0;if((c|0)<8)return c+8|0;c=a[m+(b>>16&255)>>0]|0;if((c|0)<8)return c+16|0;return (a[m+(b>>>24)>>0]|0)+24|0}function Kc(a,b){a=a|0;b=b|0;var c=0,d=0,e=0,f=0;f=a&65535;e=b&65535;c=_(e,f)|0;d=a>>>16;a=(c>>>16)+(_(e,d)|0)|0;e=b>>>16;b=_(e,f)|0;return (C=(a>>>16)+(_(e,d)|0)+(((a&65535)+b|0)>>>16)|0,a+b<<16|c&65535|0)|0}function Lc(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0,g=0,h=0,i=0,j=0;j=b>>31|((b|0)<0?-1:0)<<1;i=((b|0)<0?-1:0)>>31|((b|0)<0?-1:0)<<1;f=d>>31|((d|0)<0?-1:0)<<1;e=((d|0)<0?-1:0)>>31|((d|0)<0?-1:0)<<1;h=Cc(j^a,i^b,j,i)|0;g=C;a=f^j;b=e^i;return Cc((Qc(h,g,Cc(f^c,e^d,f,e)|0,C,0)|0)^a,C^b,a,b)|0}function Mc(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;var f=0,g=0,h=0,j=0,k=0,l=0;f=i;i=i+16|0;j=f|0;h=b>>31|((b|0)<0?-1:0)<<1;g=((b|0)<0?-1:0)>>31|((b|0)<0?-1:0)<<1;l=e>>31|((e|0)<0?-1:0)<<1;k=((e|0)<0?-1:0)>>31|((e|0)<0?-1:0)<<1;a=Cc(h^a,g^b,h,g)|0;b=C;Qc(a,b,Cc(l^d,k^e,l,k)|0,C,j)|0;e=Cc(c[j>>2]^h,c[j+4>>2]^g,h,g)|0;d=C;i=f;return (C=d,e)|0}function Nc(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;var e=0,f=0;e=a;f=c;c=Kc(e,f)|0;a=C;return (C=(_(b,f)|0)+(_(d,e)|0)+a|a&0,c|0|0)|0}function Oc(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;return Qc(a,b,c,d,0)|0}function Pc(a,b,d,e){a=a|0;b=b|0;d=d|0;e=e|0;var f=0,g=0;g=i;i=i+16|0;f=g|0;Qc(a,b,d,e,f)|0;i=g;return (C=c[f+4>>2]|0,c[f>>2]|0)|0}function Qc(a,b,d,e,f){a=a|0;b=b|0;d=d|0;e=e|0;f=f|0;var g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0;l=a;j=b;k=j;h=d;n=e;i=n;if(!k){g=(f|0)!=0;if(!i){if(g){c[f>>2]=(l>>>0)%(h>>>0);c[f+4>>2]=0}n=0;f=(l>>>0)/(h>>>0)>>>0;return (C=n,f)|0}else{if(!g){n=0;f=0;return (C=n,f)|0}c[f>>2]=a|0;c[f+4>>2]=b&0;n=0;f=0;return (C=n,f)|0}}g=(i|0)==0;do if(h){if(!g){g=(aa(i|0)|0)-(aa(k|0)|0)|0;if(g>>>0<=31){m=g+1|0;i=31-g|0;b=g-31>>31;h=m;a=l>>>(m>>>0)&b|k<<i;b=k>>>(m>>>0)&b;g=0;i=l<<i;break}if(!f){n=0;f=0;return (C=n,f)|0}c[f>>2]=a|0;c[f+4>>2]=j|b&0;n=0;f=0;return (C=n,f)|0}g=h-1|0;if(g&h){i=(aa(h|0)|0)+33-(aa(k|0)|0)|0;p=64-i|0;m=32-i|0;j=m>>31;o=i-32|0;b=o>>31;h=i;a=m-1>>31&k>>>(o>>>0)|(k<<m|l>>>(i>>>0))&b;b=b&k>>>(i>>>0);g=l<<p&j;i=(k<<p|l>>>(o>>>0))&j|l<<m&i-33>>31;break}if(f){c[f>>2]=g&l;c[f+4>>2]=0}if((h|0)==1){o=j|b&0;p=a|0|0;return (C=o,p)|0}else{p=Jc(h|0)|0;o=k>>>(p>>>0)|0;p=k<<32-p|l>>>(p>>>0)|0;return (C=o,p)|0}}else{if(g){if(f){c[f>>2]=(k>>>0)%(h>>>0);c[f+4>>2]=0}o=0;p=(k>>>0)/(h>>>0)>>>0;return (C=o,p)|0}if(!l){if(f){c[f>>2]=0;c[f+4>>2]=(k>>>0)%(i>>>0)}o=0;p=(k>>>0)/(i>>>0)>>>0;return (C=o,p)|0}g=i-1|0;if(!(g&i)){if(f){c[f>>2]=a|0;c[f+4>>2]=g&k|b&0}o=0;p=k>>>((Jc(i|0)|0)>>>0);return (C=o,p)|0}g=(aa(i|0)|0)-(aa(k|0)|0)|0;if(g>>>0<=30){b=g+1|0;i=31-g|0;h=b;a=k<<i|l>>>(b>>>0);b=k>>>(b>>>0);g=0;i=l<<i;break}if(!f){o=0;p=0;return (C=o,p)|0}c[f>>2]=a|0;c[f+4>>2]=j|b&0;o=0;p=0;return (C=o,p)|0}while(0);if(!h){k=i;j=0;i=0}else{m=d|0|0;l=n|e&0;k=Gc(m|0,l|0,-1,-1)|0;d=C;j=i;i=0;do{e=j;j=g>>>31|j<<1;g=i|g<<1;e=a<<1|e>>>31|0;n=a>>>31|b<<1|0;Cc(k,d,e,n)|0;p=C;o=p>>31|((p|0)<0?-1:0)<<1;i=o&1;a=Cc(e,n,o&m,(((p|0)<0?-1:0)>>31|((p|0)<0?-1:0)<<1)&l)|0;b=C;h=h-1|0}while((h|0)!=0);k=j;j=0}h=0;if(f){c[f>>2]=a;c[f+4>>2]=b}o=(g|0)>>>31|(k|h)<<1|(h<<1|g>>>31)&0|j;p=(g<<1|0>>>31)&-2|i;return (C=o,p)|0}function Rc(a,b,c,d){a=a|0;b=b|0;c=c|0;d=d|0;return ra[a&1](b|0,c|0,d|0)|0}function Sc(a,b,c){a=a|0;b=b|0;c=c|0;ba(0);return 0}
+
+// EMSCRIPTEN_END_FUNCS
+var ra=[Sc,qc];return{_malloc:vc,_i64Subtract:Cc,_free:wc,_i64Add:Gc,_memmove:Hc,_memset:Bc,___cxa_demangle:Ba,_memcpy:Fc,_bitshift64Lshr:Dc,_bitshift64Shl:Ec,runPostSets:Ac,stackAlloc:sa,stackSave:ta,stackRestore:ua,establishStackSpace:va,setThrew:wa,setTempRet0:za,getTempRet0:Aa,dynCall_iiii:Rc}})
+
+
+// EMSCRIPTEN_END_ASM
+(Module.asmGlobalArg,Module.asmLibraryArg,buffer);var ___cxa_demangle=Module["___cxa_demangle"]=asm["___cxa_demangle"];var _i64Subtract=Module["_i64Subtract"]=asm["_i64Subtract"];var _free=Module["_free"]=asm["_free"];var runPostSets=Module["runPostSets"]=asm["runPostSets"];var _i64Add=Module["_i64Add"]=asm["_i64Add"];var _memmove=Module["_memmove"]=asm["_memmove"];var _memset=Module["_memset"]=asm["_memset"];var _malloc=Module["_malloc"]=asm["_malloc"];var _memcpy=Module["_memcpy"]=asm["_memcpy"];var _bitshift64Lshr=Module["_bitshift64Lshr"]=asm["_bitshift64Lshr"];var _bitshift64Shl=Module["_bitshift64Shl"]=asm["_bitshift64Shl"];var dynCall_iiii=Module["dynCall_iiii"]=asm["dynCall_iiii"];Runtime.stackAlloc=asm["stackAlloc"];Runtime.stackSave=asm["stackSave"];Runtime.stackRestore=asm["stackRestore"];Runtime.establishStackSpace=asm["establishStackSpace"];Runtime.setTempRet0=asm["setTempRet0"];Runtime.getTempRet0=asm["getTempRet0"];function ExitStatus(status){this.name="ExitStatus";this.message="Program terminated with exit("+status+")";this.status=status}ExitStatus.prototype=new Error;ExitStatus.prototype.constructor=ExitStatus;var initialStackTop;var preloadStartTime=null;var calledMain=false;dependenciesFulfilled=function runCaller(){if(!Module["calledRun"])run();if(!Module["calledRun"])dependenciesFulfilled=runCaller};Module["callMain"]=Module.callMain=function callMain(args){assert(runDependencies==0,"cannot call main when async dependencies remain! (listen on __ATMAIN__)");assert(__ATPRERUN__.length==0,"cannot call main when preRun functions remain to be called");args=args||[];ensureInitRuntime();var argc=args.length+1;function pad(){for(var i=0;i<4-1;i++){argv.push(0)}}var argv=[allocate(intArrayFromString(Module["thisProgram"]),"i8",ALLOC_NORMAL)];pad();for(var i=0;i<argc-1;i=i+1){argv.push(allocate(intArrayFromString(args[i]),"i8",ALLOC_NORMAL));pad()}argv.push(0);argv=allocate(argv,"i32",ALLOC_NORMAL);try{var ret=Module["_main"](argc,argv,0);exit(ret,true)}catch(e){if(e instanceof ExitStatus){return}else if(e=="SimulateInfiniteLoop"){Module["noExitRuntime"]=true;return}else{if(e&&typeof e==="object"&&e.stack)Module.printErr("exception thrown: "+[e,e.stack]);throw e}}finally{calledMain=true}};function run(args){args=args||Module["arguments"];if(preloadStartTime===null)preloadStartTime=Date.now();if(runDependencies>0){return}preRun();if(runDependencies>0)return;if(Module["calledRun"])return;function doRun(){if(Module["calledRun"])return;Module["calledRun"]=true;if(ABORT)return;ensureInitRuntime();preMain();if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();if(Module["_main"]&&shouldRunNow)Module["callMain"](args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout((function(){setTimeout((function(){Module["setStatus"]("")}),1);doRun()}),1)}else{doRun()}}Module["run"]=Module.run=run;function exit(status,implicit){if(implicit&&Module["noExitRuntime"]){return}if(Module["noExitRuntime"]){}else{ABORT=true;EXITSTATUS=status;STACKTOP=initialStackTop;exitRuntime();if(Module["onExit"])Module["onExit"](status)}if(ENVIRONMENT_IS_NODE){process["stdout"]["once"]("drain",(function(){process["exit"](status)}));console.log(" ");setTimeout((function(){process["exit"](status)}),500)}else if(ENVIRONMENT_IS_SHELL&&typeof quit==="function"){quit(status)}throw new ExitStatus(status)}Module["exit"]=Module.exit=exit;var abortDecorators=[];function abort(what){if(what!==undefined){Module.print(what);Module.printErr(what);what=JSON.stringify(what)}else{what=""}ABORT=true;EXITSTATUS=1;var extra="\nIf this abort() is unexpected, build with -s ASSERTIONS=1 which can give more information.";var output="abort("+what+") at "+stackTrace()+extra;if(abortDecorators){abortDecorators.forEach((function(decorator){output=decorator(output,what)}))}throw output}Module["abort"]=Module.abort=abort;if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}var shouldRunNow=true;if(Module["noInitialRun"]){shouldRunNow=false}run()
+
+
+
+
+
+ return Module;
+};
+
+ var m = Module();
+ var status = m._malloc(4);
+ var buf = m._malloc(2048);
+
+ return function(func) {
+ if (func.length >= 2048) return null;
+ m.writeStringToMemory(func.substr(1), buf);
+ var ret = m['___cxa_demangle'](buf, 0, 0, status);
+ var result = null;
+ if (m.HEAP32[status >> 2] === 0 && ret) {
+ result = m.Pointer_stringify(ret);
+ m._free(ret);
+ }
+ return result;
+ };
+})();
+
+// The emscripten compiler exports the Module object; we just want
+// the demangle function
+if (typeof module === "object" && typeof module.exports === "object") {
+ module.exports = demangle;
+}
diff --git a/devtools/client/shared/developer-toolbar.js b/devtools/client/shared/developer-toolbar.js
new file mode 100644
index 000000000..2528591a6
--- /dev/null
+++ b/devtools/client/shared/developer-toolbar.js
@@ -0,0 +1,1397 @@
+/* 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 { Ci } = require("chrome");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const Services = require("Services");
+const { TargetFactory } = require("devtools/client/framework/target");
+const Telemetry = require("devtools/client/shared/telemetry");
+const {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+const {Task} = require("devtools/shared/task");
+
+const NS_XHTML = "http://www.w3.org/1999/xhtml";
+
+const { PluralForm } = require("devtools/shared/plural-form");
+
+loader.lazyGetter(this, "prefBranch", function () {
+ return Services.prefs.getBranch(null)
+ .QueryInterface(Ci.nsIPrefBranch2);
+});
+
+loader.lazyRequireGetter(this, "gcliInit", "devtools/shared/gcli/commands/index");
+loader.lazyRequireGetter(this, "util", "gcli/util/util");
+loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/server/actors/utils/webconsole-utils", true);
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
+loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+
+/**
+ * A collection of utilities to help working with commands
+ */
+var CommandUtils = {
+ /**
+ * Utility to ensure that things are loaded in the correct order
+ */
+ createRequisition: function (target, options) {
+ if (!gcliInit) {
+ return promise.reject("Unable to load gcli");
+ }
+ return gcliInit.getSystem(target).then(system => {
+ let Requisition = require("gcli/cli").Requisition;
+ return new Requisition(system, options);
+ });
+ },
+
+ /**
+ * Destroy the remote side of the requisition as well as the local side
+ */
+ destroyRequisition: function (requisition, target) {
+ requisition.destroy();
+ gcliInit.releaseSystem(target);
+ },
+
+ /**
+ * Read a toolbarSpec from preferences
+ * @param pref The name of the preference to read
+ */
+ getCommandbarSpec: function (pref) {
+ let value = prefBranch.getComplexValue(pref, Ci.nsISupportsString).data;
+ return JSON.parse(value);
+ },
+
+ /**
+ * A toolbarSpec is an array of strings each of which is a GCLI command.
+ *
+ * Warning: this method uses the unload event of the window that owns the
+ * buttons that are of type checkbox. this means that we don't properly
+ * unregister event handlers until the window is destroyed.
+ */
+ createButtons: function (toolbarSpec, target, document, requisition) {
+ return util.promiseEach(toolbarSpec, typed => {
+ // Ask GCLI to parse the typed string (doesn't execute it)
+ return requisition.update(typed).then(() => {
+ let button = document.createElementNS(NS_XHTML, "button");
+
+ // Ignore invalid commands
+ let command = requisition.commandAssignment.value;
+ if (command == null) {
+ throw new Error("No command '" + typed + "'");
+ }
+
+ if (command.buttonId != null) {
+ button.id = command.buttonId;
+ if (command.buttonClass != null) {
+ button.className = command.buttonClass;
+ }
+ } else {
+ button.setAttribute("text-as-image", "true");
+ button.setAttribute("label", command.name);
+ }
+
+ button.classList.add("devtools-button");
+
+ if (command.tooltipText != null) {
+ button.setAttribute("title", command.tooltipText);
+ } else if (command.description != null) {
+ button.setAttribute("title", command.description);
+ }
+
+ button.addEventListener("click",
+ requisition.updateExec.bind(requisition, typed));
+
+ button.addEventListener("keypress", (event) => {
+ if (ViewHelpers.isSpaceOrReturn(event)) {
+ event.preventDefault();
+ requisition.updateExec(typed);
+ }
+ }, false);
+
+ // Allow the command button to be toggleable
+ let onChange = null;
+ if (command.state) {
+ button.setAttribute("autocheck", false);
+
+ /**
+ * The onChange event should be called with an event object that
+ * contains a target property which specifies which target the event
+ * applies to. For legacy reasons the event object can also contain
+ * a tab property.
+ */
+ onChange = (eventName, ev) => {
+ if (ev.target == target || ev.tab == target.tab) {
+ let updateChecked = (checked) => {
+ if (checked) {
+ button.setAttribute("checked", true);
+ } else if (button.hasAttribute("checked")) {
+ button.removeAttribute("checked");
+ }
+ };
+
+ // isChecked would normally be synchronous. An annoying quirk
+ // of the 'csscoverage toggle' command forces us to accept a
+ // promise here, but doing Promise.resolve(reply).then(...) here
+ // makes this async for everyone, which breaks some tests so we
+ // treat non-promise replies separately to keep then synchronous.
+ let reply = command.state.isChecked(target);
+ if (typeof reply.then == "function") {
+ reply.then(updateChecked, console.error);
+ } else {
+ updateChecked(reply);
+ }
+ }
+ };
+
+ command.state.onChange(target, onChange);
+ onChange("", { target: target });
+ }
+ document.defaultView.addEventListener("unload", function (event) {
+ if (onChange && command.state.offChange) {
+ command.state.offChange(target, onChange);
+ }
+ button.remove();
+ button = null;
+ }, { once: true });
+
+ requisition.clear();
+
+ return button;
+ });
+ });
+ },
+
+ /**
+ * A helper function to create the environment object that is passed to
+ * GCLI commands.
+ * @param targetContainer An object containing a 'target' property which
+ * reflects the current debug target
+ */
+ createEnvironment: function (container, targetProperty = "target") {
+ if (!container[targetProperty].toString ||
+ !/TabTarget/.test(container[targetProperty].toString())) {
+ throw new Error("Missing target");
+ }
+
+ return {
+ get target() {
+ if (!container[targetProperty].toString ||
+ !/TabTarget/.test(container[targetProperty].toString())) {
+ throw new Error("Removed target");
+ }
+
+ return container[targetProperty];
+ },
+
+ get chromeWindow() {
+ return this.target.tab.ownerDocument.defaultView;
+ },
+
+ get chromeDocument() {
+ return this.target.tab.ownerDocument.defaultView.document;
+ },
+
+ get window() {
+ // throw new
+ // Error("environment.window is not available in runAt:client commands");
+ return this.chromeWindow.gBrowser.contentWindowAsCPOW;
+ },
+
+ get document() {
+ // throw new
+ // Error("environment.document is not available in runAt:client commands");
+ return this.chromeWindow.gBrowser.contentDocumentAsCPOW;
+ }
+ };
+ },
+};
+
+exports.CommandUtils = CommandUtils;
+
+/**
+ * Due to a number of panel bugs we need a way to check if we are running on
+ * Linux. See the comments for TooltipPanel and OutputPanel for further details.
+ *
+ * When bug 780102 is fixed all isLinux checks can be removed and we can revert
+ * to using panels.
+ */
+loader.lazyGetter(this, "isLinux", function () {
+ return Services.appinfo.OS == "Linux";
+});
+loader.lazyGetter(this, "isMac", function () {
+ return Services.appinfo.OS == "Darwin";
+});
+
+/**
+ * A component to manage the global developer toolbar, which contains a GCLI
+ * and buttons for various developer tools.
+ * @param chromeWindow The browser window to which this toolbar is attached
+ */
+function DeveloperToolbar(chromeWindow) {
+ this._chromeWindow = chromeWindow;
+
+ // Will be setup when show() is called
+ this.target = null;
+
+ this._doc = chromeWindow.document;
+
+ this._telemetry = new Telemetry();
+ this._errorsCount = {};
+ this._warningsCount = {};
+ this._errorListeners = {};
+
+ this._onToolboxReady = this._onToolboxReady.bind(this);
+ this._onToolboxDestroyed = this._onToolboxDestroyed.bind(this);
+
+ EventEmitter.decorate(this);
+}
+exports.DeveloperToolbar = DeveloperToolbar;
+
+/**
+ * Inspector notifications dispatched through the nsIObserverService
+ */
+const NOTIFICATIONS = {
+ /** DeveloperToolbar.show() has been called, and we're working on it */
+ LOAD: "developer-toolbar-load",
+
+ /** DeveloperToolbar.show() has completed */
+ SHOW: "developer-toolbar-show",
+
+ /** DeveloperToolbar.hide() has been called */
+ HIDE: "developer-toolbar-hide"
+};
+
+/**
+ * Attach notification constants to the object prototype so tests etc can
+ * use them without needing to import anything
+ */
+DeveloperToolbar.prototype.NOTIFICATIONS = NOTIFICATIONS;
+
+/**
+ * Is the toolbar open?
+ */
+Object.defineProperty(DeveloperToolbar.prototype, "visible", {
+ get: function () {
+ return this._element && !this._element.hidden;
+ },
+ enumerable: true
+});
+
+var _gSequenceId = 0;
+
+/**
+ * Getter for a unique ID.
+ */
+Object.defineProperty(DeveloperToolbar.prototype, "sequenceId", {
+ get: function () {
+ return _gSequenceId++;
+ },
+ enumerable: true
+});
+
+/**
+ * Create the <toolbar> element to insert within browser UI
+ */
+DeveloperToolbar.prototype.createToolbar = function () {
+ if (this._element) {
+ return;
+ }
+ let toolbar = this._doc.createElement("toolbar");
+ toolbar.setAttribute("id", "developer-toolbar");
+ toolbar.setAttribute("hidden", "true");
+
+ let close = this._doc.createElement("toolbarbutton");
+ close.setAttribute("id", "developer-toolbar-closebutton");
+ close.setAttribute("class", "close-icon");
+ close.setAttribute("oncommand", "DeveloperToolbar.hide();");
+ let closeTooltip = L10N.getStr("toolbar.closeButton.tooltip");
+ close.setAttribute("tooltiptext", closeTooltip);
+
+ let stack = this._doc.createElement("stack");
+ stack.setAttribute("flex", "1");
+
+ let input = this._doc.createElement("textbox");
+ input.setAttribute("class", "gclitoolbar-input-node");
+ input.setAttribute("rows", "1");
+ stack.appendChild(input);
+
+ let hbox = this._doc.createElement("hbox");
+ hbox.setAttribute("class", "gclitoolbar-complete-node");
+ stack.appendChild(hbox);
+
+ let toolboxBtn = this._doc.createElement("toolbarbutton");
+ toolboxBtn.setAttribute("id", "developer-toolbar-toolbox-button");
+ toolboxBtn.setAttribute("class", "developer-toolbar-button");
+ let toolboxTooltip = L10N.getStr("toolbar.toolsButton.tooltip");
+ toolboxBtn.setAttribute("tooltiptext", toolboxTooltip);
+ let toolboxOpen = gDevToolsBrowser.hasToolboxOpened(this._chromeWindow);
+ toolboxBtn.setAttribute("checked", toolboxOpen);
+ toolboxBtn.addEventListener("command", function (event) {
+ let window = event.target.ownerDocument.defaultView;
+ gDevToolsBrowser.toggleToolboxCommand(window.gBrowser);
+ });
+ this._errorCounterButton = toolboxBtn;
+ this._errorCounterButton._defaultTooltipText = toolboxTooltip;
+
+ // On Mac, the close button is on the left,
+ // while it is on the right on every other platforms.
+ if (isMac) {
+ toolbar.appendChild(close);
+ toolbar.appendChild(stack);
+ toolbar.appendChild(toolboxBtn);
+ } else {
+ toolbar.appendChild(stack);
+ toolbar.appendChild(toolboxBtn);
+ toolbar.appendChild(close);
+ }
+
+ this._element = toolbar;
+ let bottomBox = this._doc.getElementById("browser-bottombox");
+ if (bottomBox) {
+ bottomBox.appendChild(this._element);
+ } else {
+ // SeaMonkey does not have a "browser-bottombox".
+ let statusBar = this._doc.getElementById("status-bar");
+ if (statusBar) {
+ statusBar.parentNode.insertBefore(this._element, statusBar);
+ }
+ }
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.toggle = function () {
+ if (this.visible) {
+ return this.hide().catch(console.error);
+ }
+ return this.show(true).catch(console.error);
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.focus = function () {
+ if (this.visible) {
+ this._input.focus();
+ return promise.resolve();
+ }
+ return this.show(true);
+};
+
+/**
+ * Called from browser.xul in response to menu-click or keyboard shortcut to
+ * toggle the toolbar
+ */
+DeveloperToolbar.prototype.focusToggle = function () {
+ if (this.visible) {
+ // If we have focus then the active element is the HTML input contained
+ // inside the xul input element
+ let active = this._chromeWindow.document.activeElement;
+ let position = this._input.compareDocumentPosition(active);
+ if (position & nodeConstants.DOCUMENT_POSITION_CONTAINED_BY) {
+ this.hide();
+ } else {
+ this._input.focus();
+ }
+ } else {
+ this.show(true);
+ }
+};
+
+/**
+ * Even if the user has not clicked on 'Got it' in the intro, we only show it
+ * once per session.
+ * Warning this is slightly messed up because this.DeveloperToolbar is not the
+ * same as this.DeveloperToolbar when in browser.js context.
+ */
+DeveloperToolbar.introShownThisSession = false;
+
+/**
+ * Show the developer toolbar
+ */
+DeveloperToolbar.prototype.show = function (focus) {
+ if (this._showPromise != null) {
+ return this._showPromise;
+ }
+
+ this._showPromise = Task.spawn((function* () {
+ // hide() is async, so ensure we don't need to wait for hide() to
+ // finish. We unconditionally yield here, even if _hidePromise is
+ // null, so that the spawn call returns a promise before starting
+ // to do any real work.
+ yield this._hidePromise;
+
+ this.createToolbar();
+
+ Services.prefs.setBoolPref("devtools.toolbar.visible", true);
+
+ this._telemetry.toolOpened("developertoolbar");
+
+ this._notify(NOTIFICATIONS.LOAD);
+
+ this._input = this._doc.querySelector(".gclitoolbar-input-node");
+
+ // Initializing GCLI can only be done when we've got content windows to
+ // write to, so this needs to be done asynchronously.
+ let panelPromises = [
+ TooltipPanel.create(this),
+ OutputPanel.create(this)
+ ];
+ let panels = yield promise.all(panelPromises);
+
+ [ this.tooltipPanel, this.outputPanel ] = panels;
+
+ this._doc.getElementById("menu_devToolbar").setAttribute("checked", "true");
+
+ this.target = TargetFactory.forTab(this._chromeWindow.gBrowser.selectedTab);
+ const options = {
+ environment: CommandUtils.createEnvironment(this, "target"),
+ document: this.outputPanel.document,
+ };
+ let requisition = yield CommandUtils.createRequisition(this.target, options);
+ this.requisition = requisition;
+
+ // The <textbox> `value` may still be undefined on the XUL binding if
+ // we fetch it early
+ let value = this._input.value || "";
+ yield this.requisition.update(value);
+
+ const Inputter = require("gcli/mozui/inputter").Inputter;
+ const Completer = require("gcli/mozui/completer").Completer;
+ const Tooltip = require("gcli/mozui/tooltip").Tooltip;
+ const FocusManager = require("gcli/ui/focus").FocusManager;
+
+ this.onOutput = this.requisition.commandOutputManager.onOutput;
+
+ this.focusManager = new FocusManager(this._doc, requisition.system.settings);
+
+ this.inputter = new Inputter({
+ requisition: this.requisition,
+ focusManager: this.focusManager,
+ element: this._input,
+ });
+
+ this.completer = new Completer({
+ requisition: this.requisition,
+ inputter: this.inputter,
+ backgroundElement: this._doc.querySelector(".gclitoolbar-stack-node"),
+ element: this._doc.querySelector(".gclitoolbar-complete-node"),
+ });
+
+ this.tooltip = new Tooltip({
+ requisition: this.requisition,
+ focusManager: this.focusManager,
+ inputter: this.inputter,
+ element: this.tooltipPanel.hintElement,
+ });
+
+ this.inputter.tooltip = this.tooltip;
+
+ this.focusManager.addMonitoredElement(this.outputPanel._frame);
+ this.focusManager.addMonitoredElement(this._element);
+
+ this.focusManager.onVisibilityChange.add(this.outputPanel._visibilityChanged,
+ this.outputPanel);
+ this.focusManager.onVisibilityChange.add(this.tooltipPanel._visibilityChanged,
+ this.tooltipPanel);
+ this.onOutput.add(this.outputPanel._outputChanged, this.outputPanel);
+
+ let tabbrowser = this._chromeWindow.gBrowser;
+ tabbrowser.tabContainer.addEventListener("TabSelect", this, false);
+ tabbrowser.tabContainer.addEventListener("TabClose", this, false);
+ tabbrowser.addEventListener("load", this, true);
+ tabbrowser.addEventListener("beforeunload", this, true);
+
+ gDevTools.on("toolbox-ready", this._onToolboxReady);
+ gDevTools.on("toolbox-destroyed", this._onToolboxDestroyed);
+
+ this._initErrorsCount(tabbrowser.selectedTab);
+
+ this._element.hidden = false;
+
+ if (focus) {
+ // If the toolbar was just inserted, the <textbox> may still have
+ // its binding in process of being applied and not be focusable yet
+ let waitForBinding = () => {
+ // Bail out if the toolbar has been destroyed in the meantime
+ if (!this._input) {
+ return;
+ }
+ // mInputField is a xbl field of <xul:textbox>
+ if (typeof this._input.mInputField != "undefined") {
+ this._input.focus();
+ this._notify(NOTIFICATIONS.SHOW);
+ } else {
+ this._input.ownerDocument.defaultView.setTimeout(waitForBinding, 50);
+ }
+ };
+ waitForBinding();
+ } else {
+ this._notify(NOTIFICATIONS.SHOW);
+ }
+
+ if (!DeveloperToolbar.introShownThisSession) {
+ let intro = require("gcli/ui/intro");
+ intro.maybeShowIntro(this.requisition.commandOutputManager,
+ this.requisition.conversionContext,
+ this.outputPanel);
+ DeveloperToolbar.introShownThisSession = true;
+ }
+
+ this._showPromise = null;
+ }).bind(this));
+
+ return this._showPromise;
+};
+
+/**
+ * Hide the developer toolbar.
+ */
+DeveloperToolbar.prototype.hide = function () {
+ // If we're already in the process of hiding, just use the other promise
+ if (this._hidePromise != null) {
+ return this._hidePromise;
+ }
+
+ // show() is async, so ensure we don't need to wait for show() to finish
+ let waitPromise = this._showPromise || promise.resolve();
+
+ this._hidePromise = waitPromise.then(() => {
+ this._element.hidden = true;
+
+ Services.prefs.setBoolPref("devtools.toolbar.visible", false);
+
+ this._doc.getElementById("menu_devToolbar").setAttribute("checked", "false");
+ this.destroy();
+
+ this._telemetry.toolClosed("developertoolbar");
+ this._notify(NOTIFICATIONS.HIDE);
+
+ this._hidePromise = null;
+ });
+
+ return this._hidePromise;
+};
+
+/**
+ * Initialize the listeners needed for tracking the number of errors for a given
+ * tab.
+ *
+ * @private
+ * @param nsIDOMNode tab the xul:tab for which you want to track the number of
+ * errors.
+ */
+DeveloperToolbar.prototype._initErrorsCount = function (tab) {
+ let tabId = tab.linkedPanel;
+ if (tabId in this._errorsCount) {
+ this._updateErrorsCount();
+ return;
+ }
+
+ let window = tab.linkedBrowser.contentWindow;
+ let listener = new ConsoleServiceListener(window, {
+ onConsoleServiceMessage: this._onPageError.bind(this, tabId),
+ });
+ listener.init();
+
+ this._errorListeners[tabId] = listener;
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+
+ let messages = listener.getCachedMessages();
+ messages.forEach(this._onPageError.bind(this, tabId));
+
+ this._updateErrorsCount();
+};
+
+/**
+ * Stop the listeners needed for tracking the number of errors for a given
+ * tab.
+ *
+ * @private
+ * @param nsIDOMNode tab the xul:tab for which you want to stop tracking the
+ * number of errors.
+ */
+DeveloperToolbar.prototype._stopErrorsCount = function (tab) {
+ let tabId = tab.linkedPanel;
+ if (!(tabId in this._errorsCount) || !(tabId in this._warningsCount)) {
+ this._updateErrorsCount();
+ return;
+ }
+
+ this._errorListeners[tabId].destroy();
+ delete this._errorListeners[tabId];
+ delete this._errorsCount[tabId];
+ delete this._warningsCount[tabId];
+
+ this._updateErrorsCount();
+};
+
+/**
+ * Hide the developer toolbar
+ */
+DeveloperToolbar.prototype.destroy = function () {
+ if (this._input == null) {
+ // Already destroyed
+ return;
+ }
+
+ let tabbrowser = this._chromeWindow.gBrowser;
+ tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
+ tabbrowser.tabContainer.removeEventListener("TabClose", this, false);
+ tabbrowser.removeEventListener("load", this, true);
+ tabbrowser.removeEventListener("beforeunload", this, true);
+
+ gDevTools.off("toolbox-ready", this._onToolboxReady);
+ gDevTools.off("toolbox-destroyed", this._onToolboxDestroyed);
+
+ Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
+
+ this.focusManager.removeMonitoredElement(this.outputPanel._frame);
+ this.focusManager.removeMonitoredElement(this._element);
+
+ this.focusManager.onVisibilityChange.remove(this.outputPanel._visibilityChanged,
+ this.outputPanel);
+ this.focusManager.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged,
+ this.tooltipPanel);
+ this.onOutput.remove(this.outputPanel._outputChanged, this.outputPanel);
+
+ this.tooltip.destroy();
+ this.completer.destroy();
+ this.inputter.destroy();
+ this.focusManager.destroy();
+
+ this.outputPanel.destroy();
+ this.tooltipPanel.destroy();
+ delete this._input;
+
+ CommandUtils.destroyRequisition(this.requisition, this.target);
+ this.target = undefined;
+
+ this._element.remove();
+ delete this._element;
+};
+
+/**
+ * Utility for sending notifications
+ * @param topic a NOTIFICATION constant
+ */
+DeveloperToolbar.prototype._notify = function (topic) {
+ let data = { toolbar: this };
+ data.wrappedJSObject = data;
+ Services.obs.notifyObservers(data, topic, null);
+};
+
+/**
+ * Update various parts of the UI when the current tab changes
+ */
+DeveloperToolbar.prototype.handleEvent = function (ev) {
+ if (ev.type == "TabSelect" || ev.type == "load") {
+ if (this.visible) {
+ let tab = this._chromeWindow.gBrowser.selectedTab;
+ this.target = TargetFactory.forTab(tab);
+ gcliInit.getSystem(this.target).then(system => {
+ this.requisition.system = system;
+ }, error => {
+ if (!this._chromeWindow.gBrowser.getBrowserForTab(tab)) {
+ // The tab was closed, suppress the error and print a warning as the
+ // destroyed tab was likely the cause.
+ console.warn("An error occurred as the tab was closed while " +
+ "updating Developer Toolbar state. The error was: ", error);
+ return;
+ }
+
+ // Propagate other errors as they're more likely to cause real issues
+ // and thus should cause tests to fail.
+ throw error;
+ });
+
+ if (ev.type == "TabSelect") {
+ let toolboxOpen = gDevToolsBrowser.hasToolboxOpened(this._chromeWindow);
+ this._errorCounterButton.setAttribute("checked", toolboxOpen);
+ this._initErrorsCount(ev.target);
+ }
+ }
+ } else if (ev.type == "TabClose") {
+ this._stopErrorsCount(ev.target);
+ } else if (ev.type == "beforeunload") {
+ this._onPageBeforeUnload(ev);
+ }
+};
+
+/**
+ * Update toolbox toggle button when toolbox goes on and off
+ */
+DeveloperToolbar.prototype._onToolboxReady = function () {
+ this._errorCounterButton.setAttribute("checked", "true");
+};
+DeveloperToolbar.prototype._onToolboxDestroyed = function () {
+ this._errorCounterButton.setAttribute("checked", "false");
+};
+
+/**
+ * Count a page error received for the currently selected tab. This
+ * method counts the JavaScript exceptions received and CSS errors/warnings.
+ *
+ * @private
+ * @param string tabId the ID of the tab from where the page error comes.
+ * @param object pageError the page error object received from the
+ * PageErrorListener.
+ */
+DeveloperToolbar.prototype._onPageError = function (tabId, pageError) {
+ if (pageError.category == "CSS Parser" ||
+ pageError.category == "CSS Loader") {
+ return;
+ }
+ if ((pageError.flags & pageError.warningFlag) ||
+ (pageError.flags & pageError.strictFlag)) {
+ this._warningsCount[tabId]++;
+ } else {
+ this._errorsCount[tabId]++;
+ }
+ this._updateErrorsCount(tabId);
+};
+
+/**
+ * The |beforeunload| event handler. This function resets the errors count when
+ * a different page starts loading.
+ *
+ * @private
+ * @param nsIDOMEvent ev the beforeunload DOM event.
+ */
+DeveloperToolbar.prototype._onPageBeforeUnload = function (ev) {
+ let window = ev.target.defaultView;
+ if (window.top !== window) {
+ return;
+ }
+
+ let tabs = this._chromeWindow.gBrowser.tabs;
+ Array.prototype.some.call(tabs, function (tab) {
+ if (tab.linkedBrowser.contentWindow === window) {
+ let tabId = tab.linkedPanel;
+ if (tabId in this._errorsCount || tabId in this._warningsCount) {
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+ this._updateErrorsCount(tabId);
+ }
+ return true;
+ }
+ return false;
+ }, this);
+};
+
+/**
+ * Update the page errors count displayed in the Web Console button for the
+ * currently selected tab.
+ *
+ * @private
+ * @param string [changedTabId] Optional. The tab ID that had its page errors
+ * count changed. If this is provided and it doesn't match the currently
+ * selected tab, then the button is not updated.
+ */
+DeveloperToolbar.prototype._updateErrorsCount = function (changedTabId) {
+ let tabId = this._chromeWindow.gBrowser.selectedTab.linkedPanel;
+ if (changedTabId && tabId != changedTabId) {
+ return;
+ }
+
+ let errors = this._errorsCount[tabId];
+ let warnings = this._warningsCount[tabId];
+ let btn = this._errorCounterButton;
+ if (errors) {
+ let errorsText = L10N.getStr("toolboxToggleButton.errors");
+ errorsText = PluralForm.get(errors, errorsText).replace("#1", errors);
+
+ let warningsText = L10N.getStr("toolboxToggleButton.warnings");
+ warningsText = PluralForm.get(warnings, warningsText).replace("#1", warnings);
+
+ let tooltiptext = L10N.getFormatStr("toolboxToggleButton.tooltip",
+ errorsText, warningsText);
+
+ btn.setAttribute("error-count", errors);
+ btn.setAttribute("tooltiptext", tooltiptext);
+ } else {
+ btn.removeAttribute("error-count");
+ btn.setAttribute("tooltiptext", btn._defaultTooltipText);
+ }
+
+ this.emit("errors-counter-updated");
+};
+
+/**
+ * Reset the errors counter for the given tab.
+ *
+ * @param nsIDOMElement tab The xul:tab for which you want to reset the page
+ * errors counters.
+ */
+DeveloperToolbar.prototype.resetErrorsCount = function (tab) {
+ let tabId = tab.linkedPanel;
+ if (tabId in this._errorsCount || tabId in this._warningsCount) {
+ this._errorsCount[tabId] = 0;
+ this._warningsCount[tabId] = 0;
+ this._updateErrorsCount(tabId);
+ }
+};
+
+/**
+ * Creating a OutputPanel is asynchronous
+ */
+function OutputPanel() {
+ throw new Error("Use OutputPanel.create()");
+}
+
+/**
+ * Panel to handle command line output.
+ *
+ * There is a tooltip bug on Windows and OSX that prevents tooltips from being
+ * positioned properly (bug 786975). There is a Gnome panel bug on Linux that
+ * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848).
+ * We now use a tooltip on Linux and a panel on OSX & Windows.
+ *
+ * If a panel has no content and no height it is not shown when openPopup is
+ * called on Windows and OSX (bug 692348) ... this prevents the panel from
+ * appearing the first time it is shown. Setting the panel's height to 1px
+ * before calling openPopup works around this issue as we resize it ourselves
+ * anyway.
+ *
+ * @param devtoolbar The parent DeveloperToolbar object
+ */
+OutputPanel.create = function (devtoolbar) {
+ let outputPanel = Object.create(OutputPanel.prototype);
+ return outputPanel._init(devtoolbar);
+};
+
+/**
+ * @private See OutputPanel.create
+ */
+OutputPanel.prototype._init = function (devtoolbar) {
+ this._devtoolbar = devtoolbar;
+ this._input = this._devtoolbar._input;
+ this._toolbar = this._devtoolbar._doc.getElementById("developer-toolbar");
+
+ /*
+ <tooltip|panel id="gcli-output"
+ noautofocus="true"
+ noautohide="true"
+ class="gcli-panel">
+ <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
+ id="gcli-output-frame"
+ src="chrome://devtools/content/commandline/commandlineoutput.xhtml"
+ sandbox="allow-same-origin"/>
+ </tooltip|panel>
+ */
+
+ // TODO: Switch back from tooltip to panel when metacity focus issue is fixed:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ this._panel = this._devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel");
+
+ this._panel.id = "gcli-output";
+ this._panel.classList.add("gcli-panel");
+
+ if (isLinux) {
+ this.canHide = false;
+ this._onpopuphiding = this._onpopuphiding.bind(this);
+ this._panel.addEventListener("popuphiding", this._onpopuphiding, true);
+ } else {
+ this._panel.setAttribute("noautofocus", "true");
+ this._panel.setAttribute("noautohide", "true");
+
+ // Bug 692348: On Windows and OSX if a panel has no content and no height
+ // openPopup fails to display it. Setting the height to 1px alows the panel
+ // to be displayed before has content or a real height i.e. the first time
+ // it is displayed.
+ this._panel.setAttribute("height", "1px");
+ }
+
+ this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
+
+ this._frame = this._devtoolbar._doc.createElementNS(NS_XHTML, "iframe");
+ this._frame.id = "gcli-output-frame";
+ this._frame.setAttribute("src", "chrome://devtools/content/commandline/commandlineoutput.xhtml");
+ this._frame.setAttribute("sandbox", "allow-same-origin");
+ this._panel.appendChild(this._frame);
+
+ this.displayedOutput = undefined;
+
+ this._update = this._update.bind(this);
+
+ // Wire up the element from the iframe, and resolve the promise
+ let deferred = defer();
+ let onload = () => {
+ this._frame.removeEventListener("load", onload, true);
+
+ this.document = this._frame.contentDocument;
+ this._copyTheme();
+
+ this._div = this.document.getElementById("gcli-output-root");
+ this._div.classList.add("gcli-row-out");
+ this._div.setAttribute("aria-live", "assertive");
+
+ let styles = this._toolbar.ownerDocument.defaultView
+ .getComputedStyle(this._toolbar);
+ this._div.setAttribute("dir", styles.direction);
+
+ deferred.resolve(this);
+ };
+ this._frame.addEventListener("load", onload, true);
+
+ return deferred.promise;
+};
+
+/* Copy the current devtools theme attribute into the iframe,
+ so it can be styled correctly. */
+OutputPanel.prototype._copyTheme = function () {
+ if (this.document) {
+ let theme =
+ this._devtoolbar._doc.documentElement.getAttribute("devtoolstheme");
+ this.document.documentElement.setAttribute("devtoolstheme", theme);
+ }
+};
+
+/**
+ * Prevent the popup from hiding if it is not permitted via this.canHide.
+ */
+OutputPanel.prototype._onpopuphiding = function (ev) {
+ // TODO: When we switch back from tooltip to panel we can remove this hack:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ if (isLinux && !this.canHide) {
+ ev.preventDefault();
+ }
+};
+
+/**
+ * Display the OutputPanel.
+ */
+OutputPanel.prototype.show = function () {
+ if (isLinux) {
+ this.canHide = false;
+ }
+
+ // We need to reset the iframe size in order for future size calculations to
+ // be correct
+ this._frame.style.minHeight = this._frame.style.maxHeight = 0;
+ this._frame.style.minWidth = 0;
+
+ this._copyTheme();
+ this._panel.openPopup(this._input, "before_start", 0, 0, false, false, null);
+ this._resize();
+
+ this._input.focus();
+};
+
+/**
+ * Internal helper to set the height of the output panel to fit the available
+ * content;
+ */
+OutputPanel.prototype._resize = function () {
+ if (this._panel == null || this.document == null || !this._panel.state == "closed") {
+ return;
+ }
+
+ // Set max panel width to match any content with a max of the width of the
+ // browser window.
+ let maxWidth = this._panel.ownerDocument.documentElement.clientWidth;
+
+ // Adjust max width according to OS.
+ // We'd like to put this in CSS but we can't:
+ // body { width: calc(min(-5px, max-content)); }
+ // #_panel { max-width: -5px; }
+ switch (Services.appinfo.OS) {
+ case "Linux":
+ maxWidth -= 5;
+ break;
+ case "Darwin":
+ maxWidth -= 25;
+ break;
+ case "WINNT":
+ maxWidth -= 5;
+ break;
+ }
+
+ this.document.body.style.width = "-moz-max-content";
+ let style = this._frame.contentWindow.getComputedStyle(this.document.body);
+ let frameWidth = parseInt(style.width, 10);
+ let width = Math.min(maxWidth, frameWidth);
+ this.document.body.style.width = width + "px";
+
+ // Set the width of the iframe.
+ this._frame.style.minWidth = width + "px";
+ this._panel.style.maxWidth = maxWidth + "px";
+
+ // browserAdjustment is used to correct the panel height according to the
+ // browsers borders etc.
+ const browserAdjustment = 15;
+
+ // Set max panel height to match any content with a max of the height of the
+ // browser window.
+ let maxHeight =
+ this._panel.ownerDocument.documentElement.clientHeight - browserAdjustment;
+ let height = Math.min(maxHeight, this.document.documentElement.scrollHeight);
+
+ // Set the height of the iframe. Setting iframe.height does not work.
+ this._frame.style.minHeight = this._frame.style.maxHeight = height + "px";
+
+ // Set the height and width of the panel to match the iframe.
+ this._panel.sizeTo(width, height);
+
+ // Move the panel to the correct position in the case that it has been
+ // positioned incorrectly.
+ let screenX = this._input.boxObject.screenX;
+ let screenY = this._toolbar.boxObject.screenY;
+ this._panel.moveTo(screenX, screenY - height);
+};
+
+/**
+ * Called by GCLI when a command is executed.
+ */
+OutputPanel.prototype._outputChanged = function (ev) {
+ if (ev.output.hidden) {
+ return;
+ }
+
+ this.remove();
+
+ this.displayedOutput = ev.output;
+
+ if (this.displayedOutput.completed) {
+ this._update();
+ } else {
+ this.displayedOutput.promise.then(this._update, this._update)
+ .then(null, console.error);
+ }
+};
+
+/**
+ * Called when displayed Output says it's changed or from outputChanged, which
+ * happens when there is a new displayed Output.
+ */
+OutputPanel.prototype._update = function () {
+ // destroy has been called, bail out
+ if (this._div == null) {
+ return;
+ }
+
+ // Empty this._div
+ while (this._div.hasChildNodes()) {
+ this._div.removeChild(this._div.firstChild);
+ }
+
+ if (this.displayedOutput.data != null) {
+ let context = this._devtoolbar.requisition.conversionContext;
+ this.displayedOutput.convert("dom", context).then(node => {
+ if (node == null) {
+ return;
+ }
+
+ while (this._div.hasChildNodes()) {
+ this._div.removeChild(this._div.firstChild);
+ }
+
+ let links = node.querySelectorAll("*[href]");
+ for (let i = 0; i < links.length; i++) {
+ links[i].setAttribute("target", "_blank");
+ }
+
+ this._div.appendChild(node);
+ this.show();
+ });
+ }
+};
+
+/**
+ * Detach listeners from the currently displayed Output.
+ */
+OutputPanel.prototype.remove = function () {
+ if (isLinux) {
+ this.canHide = true;
+ }
+
+ if (this._panel && this._panel.hidePopup) {
+ this._panel.hidePopup();
+ }
+
+ if (this.displayedOutput) {
+ delete this.displayedOutput;
+ }
+};
+
+/**
+ * Detach listeners from the currently displayed Output.
+ */
+OutputPanel.prototype.destroy = function () {
+ this.remove();
+
+ this._panel.removeEventListener("popuphiding", this._onpopuphiding, true);
+
+ this._panel.removeChild(this._frame);
+ this._toolbar.parentElement.removeChild(this._panel);
+
+ delete this._devtoolbar;
+ delete this._input;
+ delete this._toolbar;
+ delete this._onpopuphiding;
+ delete this._panel;
+ delete this._frame;
+ delete this._content;
+ delete this._div;
+ delete this.document;
+};
+
+/**
+ * Called by GCLI to indicate that we should show or hide one either the
+ * tooltip panel or the output panel.
+ */
+OutputPanel.prototype._visibilityChanged = function (ev) {
+ if (ev.outputVisible === true) {
+ // this.show is called by _outputChanged
+ } else {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ this._panel.hidePopup();
+ }
+};
+
+/**
+ * Creating a TooltipPanel is asynchronous
+ */
+function TooltipPanel() {
+ throw new Error("Use TooltipPanel.create()");
+}
+
+/**
+ * Panel to handle tooltips.
+ *
+ * There is a tooltip bug on Windows and OSX that prevents tooltips from being
+ * positioned properly (bug 786975). There is a Gnome panel bug on Linux that
+ * causes ugly focus issues (https://bugzilla.gnome.org/show_bug.cgi?id=621848).
+ * We now use a tooltip on Linux and a panel on OSX & Windows.
+ *
+ * If a panel has no content and no height it is not shown when openPopup is
+ * called on Windows and OSX (bug 692348) ... this prevents the panel from
+ * appearing the first time it is shown. Setting the panel's height to 1px
+ * before calling openPopup works around this issue as we resize it ourselves
+ * anyway.
+ *
+ * @param devtoolbar The parent DeveloperToolbar object
+ */
+TooltipPanel.create = function (devtoolbar) {
+ let tooltipPanel = Object.create(TooltipPanel.prototype);
+ return tooltipPanel._init(devtoolbar);
+};
+
+/**
+ * @private See TooltipPanel.create
+ */
+TooltipPanel.prototype._init = function (devtoolbar) {
+ let deferred = defer();
+
+ this._devtoolbar = devtoolbar;
+ this._input = devtoolbar._doc.querySelector(".gclitoolbar-input-node");
+ this._toolbar = devtoolbar._doc.querySelector("#developer-toolbar");
+ this._dimensions = { start: 0, end: 0 };
+
+ /*
+ <tooltip|panel id="gcli-tooltip"
+ type="arrow"
+ noautofocus="true"
+ noautohide="true"
+ class="gcli-panel">
+ <html:iframe xmlns:html="http://www.w3.org/1999/xhtml"
+ id="gcli-tooltip-frame"
+ src="chrome://devtools/content/commandline/commandlinetooltip.xhtml"
+ flex="1"
+ sandbox="allow-same-origin"/>
+ </tooltip|panel>
+ */
+
+ // TODO: Switch back from tooltip to panel when metacity focus issue is fixed:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ this._panel = devtoolbar._doc.createElement(isLinux ? "tooltip" : "panel");
+
+ this._panel.id = "gcli-tooltip";
+ this._panel.classList.add("gcli-panel");
+
+ if (isLinux) {
+ this.canHide = false;
+ this._onpopuphiding = this._onpopuphiding.bind(this);
+ this._panel.addEventListener("popuphiding", this._onpopuphiding, true);
+ } else {
+ this._panel.setAttribute("noautofocus", "true");
+ this._panel.setAttribute("noautohide", "true");
+
+ // Bug 692348: On Windows and OSX if a panel has no content and no height
+ // openPopup fails to display it. Setting the height to 1px alows the panel
+ // to be displayed before has content or a real height i.e. the first time
+ // it is displayed.
+ this._panel.setAttribute("height", "1px");
+ }
+
+ this._toolbar.parentElement.insertBefore(this._panel, this._toolbar);
+
+ this._frame = devtoolbar._doc.createElementNS(NS_XHTML, "iframe");
+ this._frame.id = "gcli-tooltip-frame";
+ this._frame.setAttribute("src", "chrome://devtools/content/commandline/commandlinetooltip.xhtml");
+ this._frame.setAttribute("flex", "1");
+ this._frame.setAttribute("sandbox", "allow-same-origin");
+ this._panel.appendChild(this._frame);
+
+ /**
+ * Wire up the element from the iframe, and resolve the promise.
+ */
+ let onload = () => {
+ this._frame.removeEventListener("load", onload, true);
+
+ this.document = this._frame.contentDocument;
+ this._copyTheme();
+ this.hintElement = this.document.getElementById("gcli-tooltip-root");
+ this._connector = this.document.getElementById("gcli-tooltip-connector");
+
+ let styles = this._toolbar.ownerDocument.defaultView
+ .getComputedStyle(this._toolbar);
+ this.hintElement.setAttribute("dir", styles.direction);
+
+ deferred.resolve(this);
+ };
+ this._frame.addEventListener("load", onload, true);
+
+ return deferred.promise;
+};
+
+/* Copy the current devtools theme attribute into the iframe,
+ so it can be styled correctly. */
+TooltipPanel.prototype._copyTheme = function () {
+ if (this.document) {
+ let theme =
+ this._devtoolbar._doc.documentElement.getAttribute("devtoolstheme");
+ this.document.documentElement.setAttribute("devtoolstheme", theme);
+ }
+};
+
+/**
+ * Prevent the popup from hiding if it is not permitted via this.canHide.
+ */
+TooltipPanel.prototype._onpopuphiding = function (ev) {
+ // TODO: When we switch back from tooltip to panel we can remove this hack:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
+ if (isLinux && !this.canHide) {
+ ev.preventDefault();
+ }
+};
+
+/**
+ * Display the TooltipPanel.
+ */
+TooltipPanel.prototype.show = function (dimensions) {
+ if (!dimensions) {
+ dimensions = { start: 0, end: 0 };
+ }
+ this._dimensions = dimensions;
+
+ // This is nasty, but displaying the panel causes it to re-flow, which can
+ // change the size it should be, so we need to resize the iframe after the
+ // panel has displayed
+ this._panel.ownerDocument.defaultView.setTimeout(() => {
+ this._resize();
+ }, 0);
+
+ if (isLinux) {
+ this.canHide = false;
+ }
+
+ this._copyTheme();
+ this._resize();
+ this._panel.openPopup(this._input, "before_start", dimensions.start * 10, 0,
+ false, false, null);
+ this._input.focus();
+};
+
+/**
+ * One option is to spend lots of time taking an average width of characters
+ * in the current font, dynamically, and weighting for the frequency of use of
+ * various characters, or even to render the given string off screen, and then
+ * measure the width.
+ * Or we could do this...
+ */
+const AVE_CHAR_WIDTH = 4.5;
+
+/**
+ * Display the TooltipPanel.
+ */
+TooltipPanel.prototype._resize = function () {
+ if (this._panel == null || this.document == null || !this._panel.state == "closed") {
+ return;
+ }
+
+ let offset = 10 + Math.floor(this._dimensions.start * AVE_CHAR_WIDTH);
+ this._panel.style.marginLeft = offset + "px";
+
+ /*
+ // Bug 744906: UX review - Not sure if we want this code to fatten connector
+ // with param width
+ let width = Math.floor(this._dimensions.end * AVE_CHAR_WIDTH);
+ width = Math.min(width, 100);
+ width = Math.max(width, 10);
+ this._connector.style.width = width + "px";
+ */
+
+ this._frame.height = this.document.body.scrollHeight;
+};
+
+/**
+ * Hide the TooltipPanel.
+ */
+TooltipPanel.prototype.remove = function () {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ if (this._panel && this._panel.hidePopup) {
+ this._panel.hidePopup();
+ }
+};
+
+/**
+ * Hide the TooltipPanel.
+ */
+TooltipPanel.prototype.destroy = function () {
+ this.remove();
+
+ this._panel.removeEventListener("popuphiding", this._onpopuphiding, true);
+
+ this._panel.removeChild(this._frame);
+ this._toolbar.parentElement.removeChild(this._panel);
+
+ delete this._connector;
+ delete this._dimensions;
+ delete this._input;
+ delete this._onpopuphiding;
+ delete this._panel;
+ delete this._frame;
+ delete this._toolbar;
+ delete this._content;
+ delete this.document;
+ delete this.hintElement;
+};
+
+/**
+ * Called by GCLI to indicate that we should show or hide one either the
+ * tooltip panel or the output panel.
+ */
+TooltipPanel.prototype._visibilityChanged = function (ev) {
+ if (ev.tooltipVisible === true) {
+ this.show(ev.dimensions);
+ } else {
+ if (isLinux) {
+ this.canHide = true;
+ }
+ this._panel.hidePopup();
+ }
+};
diff --git a/devtools/client/shared/devices.js b/devtools/client/shared/devices.js
new file mode 100644
index 000000000..82bd493c4
--- /dev/null
+++ b/devtools/client/shared/devices.js
@@ -0,0 +1,88 @@
+/* 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 { getJSON } = require("devtools/client/shared/getjson");
+
+const DEVICES_URL = "devtools.devices.url";
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/device.properties");
+
+/* This is a catalog of common web-enabled devices and their properties,
+ * intended for (mobile) device emulation.
+ *
+ * The properties of a device are:
+ * - name: brand and model(s).
+ * - width: viewport width.
+ * - height: viewport height.
+ * - pixelRatio: ratio from viewport to physical screen pixels.
+ * - userAgent: UA string of the device's browser.
+ * - touch: whether it has a touch screen.
+ * - firefoxOS: whether Firefox OS is supported.
+ *
+ * The device types are:
+ * ["phones", "tablets", "laptops", "televisions", "consoles", "watches"].
+ *
+ * You can easily add more devices to this catalog from your own code (e.g. an
+ * addon) like so:
+ *
+ * var myPhone = { name: "My Phone", ... };
+ * require("devtools/client/shared/devices").addDevice(myPhone, "phones");
+ */
+
+// Local devices catalog that addons can add to.
+let localDevices = {};
+
+// Add a device to the local catalog.
+function addDevice(device, type = "phones") {
+ let list = localDevices[type];
+ if (!list) {
+ list = localDevices[type] = [];
+ }
+ list.push(device);
+}
+exports.addDevice = addDevice;
+
+// Remove a device from the local catalog.
+// returns `true` if the device is removed, `false` otherwise.
+function removeDevice(device, type = "phones") {
+ let list = localDevices[type];
+ if (!list) {
+ return false;
+ }
+
+ let index = list.findIndex(item => device);
+
+ if (index === -1) {
+ return false;
+ }
+
+ list.splice(index, 1);
+
+ return true;
+}
+exports.removeDevice = removeDevice;
+
+// Get the complete devices catalog.
+function getDevices() {
+ // Fetch common devices from Mozilla's CDN.
+ return getJSON(DEVICES_URL).then(devices => {
+ for (let type in localDevices) {
+ if (!devices[type]) {
+ devices.TYPES.push(type);
+ devices[type] = [];
+ }
+ devices[type] = localDevices[type].concat(devices[type]);
+ }
+ return devices;
+ });
+}
+exports.getDevices = getDevices;
+
+// Get the localized string for a device type.
+function getDeviceString(deviceType) {
+ return L10N.getStr("device." + deviceType);
+}
+exports.getDeviceString = getDeviceString;
diff --git a/devtools/client/shared/devtools-file-watcher.js b/devtools/client/shared/devtools-file-watcher.js
new file mode 100644
index 000000000..59ec1b136
--- /dev/null
+++ b/devtools/client/shared/devtools-file-watcher.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Ci } = require("chrome");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+const HOTRELOAD_PREF = "devtools.loader.hotreload";
+
+function resolveResourcePath(uri) {
+ const handler = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ const resolved = handler.resolveURI(Services.io.newURI(uri, null, null));
+ return resolved.replace(/file:\/\//, "");
+}
+
+function findSourceDir(path) {
+ if (path === "" || path === "/") {
+ return Promise.resolve(null);
+ }
+
+ return OS.File.exists(
+ OS.Path.join(path, "devtools/client/shared/file-watcher.js")
+ ).then(exists => {
+ if (exists) {
+ return path;
+ }
+ return findSourceDir(OS.Path.dirname(path));
+ });
+}
+
+let worker = null;
+const onPrefChange = function () {
+ // We need to figure out a src dir to watch. These are the actual
+ // files the user is working with, not the files in the obj dir. We
+ // do this by walking up the filesystem and looking for the devtools
+ // directories, and falling back to the raw path. This means none of
+ // this will work for users who store their obj dirs outside of the
+ // src dir.
+ //
+ // We take care not to mess with the `devtoolsPath` if that's what
+ // we end up using, because it might be intentionally mapped to a
+ // specific place on the filesystem for loading devtools externally.
+ //
+ // `devtoolsPath` is currently the devtools directory inside of the
+ // obj dir, and we search for `devtools/client`, so go up 2 levels
+ // to skip that devtools dir and start searching for the src dir.
+ if (Services.prefs.getBoolPref(HOTRELOAD_PREF) && !worker) {
+ const devtoolsPath = resolveResourcePath("resource://devtools")
+ .replace(/\/$/, "");
+ const searchPoint = OS.Path.dirname(OS.Path.dirname(devtoolsPath));
+ findSourceDir(searchPoint)
+ .then(srcPath => {
+ const rootPath = srcPath ? OS.Path.join(srcPath, "devtools")
+ : devtoolsPath;
+ const watchPath = OS.Path.join(rootPath, "client");
+ const { watchFiles } = require("devtools/client/shared/file-watcher");
+ worker = watchFiles(watchPath, path => {
+ let relativePath = path.replace(rootPath + "/", "");
+ module.exports.emit("file-changed", relativePath, path);
+ });
+ });
+ } else if (worker) {
+ worker.terminate();
+ worker = null;
+ }
+};
+
+Services.prefs.addObserver(HOTRELOAD_PREF, {
+ observe: onPrefChange
+}, false);
+onPrefChange();
+
+EventEmitter.decorate(module.exports);
diff --git a/devtools/client/shared/doorhanger.js b/devtools/client/shared/doorhanger.js
new file mode 100644
index 000000000..fc2767966
--- /dev/null
+++ b/devtools/client/shared/doorhanger.js
@@ -0,0 +1,164 @@
+/* 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 { Ci, Cc } = require("chrome");
+const Services = require("Services");
+const { DOMHelpers } = require("resource://devtools/client/shared/DOMHelpers.jsm");
+const { Task } = require("devtools/shared/task");
+const defer = require("devtools/shared/defer");
+const { getMostRecentBrowserWindow } = require("sdk/window/utils");
+
+const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const DEV_EDITION_PROMO_URL = "chrome://devtools/content/framework/dev-edition-promo/dev-edition-promo.xul";
+const DEV_EDITION_PROMO_ENABLED_PREF = "devtools.devedition.promo.enabled";
+const DEV_EDITION_PROMO_SHOWN_PREF = "devtools.devedition.promo.shown";
+const DEV_EDITION_PROMO_URL_PREF = "devtools.devedition.promo.url";
+const LOCALE = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry)
+ .getSelectedLocale("global");
+
+/**
+ * Only show Dev Edition promo if it's enabled (beta channel),
+ * if it has not been shown before, and it's a locale build
+ * for `en-US`
+ */
+function shouldDevEditionPromoShow() {
+ return Services.prefs.getBoolPref(DEV_EDITION_PROMO_ENABLED_PREF) &&
+ !Services.prefs.getBoolPref(DEV_EDITION_PROMO_SHOWN_PREF) &&
+ LOCALE === "en-US";
+}
+
+var TYPES = {
+ // The Developer Edition promo doorhanger, called by
+ // opening the toolbox, browser console, WebIDE, or responsive design mode
+ // in Beta releases. Only displayed once per profile.
+ deveditionpromo: {
+ predicate: shouldDevEditionPromoShow,
+ success: () => {
+ return Services.prefs.setBoolPref(DEV_EDITION_PROMO_SHOWN_PREF, true);
+ },
+ action: () => {
+ let url = Services.prefs.getCharPref(DEV_EDITION_PROMO_URL_PREF);
+ getGBrowser().selectedTab = getGBrowser().addTab(url);
+ },
+ url: DEV_EDITION_PROMO_URL
+ }
+};
+
+var panelAttrs = {
+ orient: "vertical",
+ hidden: "false",
+ consumeoutsideclicks: "true",
+ noautofocus: "true",
+ align: "start",
+ role: "alert"
+};
+
+/**
+ * Helper to call a doorhanger, defined in `TYPES`, with defined conditions,
+ * success handlers and loads its own XUL in a frame. Takes an object with
+ * several properties:
+ *
+ * @param {XULWindow} window
+ * The window that should house the doorhanger.
+ * @param {String} type
+ * The type of doorhanger to be displayed is, using the `TYPES`
+ * definition.
+ * @param {String} selector
+ * The selector that the doorhanger should be appended to within
+ * `window`. Defaults to a XUL Document's `window` element.
+ */
+exports.showDoorhanger = Task.async(function* ({ window, type, anchor }) {
+ let { predicate, success, url, action } = TYPES[type];
+ // Abort if predicate fails
+ if (!predicate()) {
+ return;
+ }
+
+ // Call success function to set preferences/cleanup immediately,
+ // so if triggered multiple times, only happens once (Windows/Linux)
+ success();
+
+ // Wait 200ms to prevent flickering where the popup is displayed
+ // before the underlying window (Windows 7, 64bit)
+ yield wait(200);
+
+ let document = window.document;
+
+ let panel = document.createElementNS(XULNS, "panel");
+ let frame = document.createElementNS(XULNS, "iframe");
+ let parentEl = document.querySelector("window");
+
+ frame.setAttribute("src", url);
+ let close = () => parentEl.removeChild(panel);
+
+ setDoorhangerStyle(panel, frame);
+
+ panel.appendChild(frame);
+ parentEl.appendChild(panel);
+
+ yield onFrameLoad(frame);
+
+ panel.openPopup(anchor);
+
+ let closeBtn = frame.contentDocument.querySelector("#close");
+ if (closeBtn) {
+ closeBtn.addEventListener("click", close);
+ }
+
+ let goBtn = frame.contentDocument.querySelector("#go");
+ if (goBtn) {
+ goBtn.addEventListener("click", () => {
+ if (action) {
+ action();
+ }
+ close();
+ });
+ }
+});
+
+function setDoorhangerStyle(panel, frame) {
+ Object.keys(panelAttrs).forEach(prop => {
+ return panel.setAttribute(prop, panelAttrs[prop]);
+ });
+ panel.style.margin = "20px";
+ panel.style.borderRadius = "5px";
+ panel.style.border = "none";
+ panel.style.MozAppearance = "none";
+ panel.style.backgroundColor = "transparent";
+
+ frame.style.borderRadius = "5px";
+ frame.setAttribute("flex", "1");
+ frame.setAttribute("width", "450");
+ frame.setAttribute("height", "179");
+}
+
+function onFrameLoad(frame) {
+ let { resolve, promise } = defer();
+
+ if (frame.contentWindow) {
+ let domHelper = new DOMHelpers(frame.contentWindow);
+ domHelper.onceDOMReady(resolve);
+ } else {
+ let callback = () => {
+ frame.removeEventListener("DOMContentLoaded", callback);
+ resolve();
+ };
+ frame.addEventListener("DOMContentLoaded", callback);
+ }
+
+ return promise;
+}
+
+function getGBrowser() {
+ return getMostRecentBrowserWindow().gBrowser;
+}
+
+function wait(n) {
+ let { resolve, promise } = defer();
+ setTimeout(resolve, n);
+ return promise;
+}
diff --git a/devtools/client/shared/file-watcher-worker.js b/devtools/client/shared/file-watcher-worker.js
new file mode 100644
index 000000000..c9edd6127
--- /dev/null
+++ b/devtools/client/shared/file-watcher-worker.js
@@ -0,0 +1,81 @@
+/* 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";
+
+/* eslint-env worker */
+/* global OS */
+importScripts("resource://gre/modules/osfile.jsm");
+
+const modifiedTimes = new Map();
+
+function gatherFiles(path, fileRegex) {
+ let files = [];
+ const iterator = new OS.File.DirectoryIterator(path);
+
+ try {
+ for (let child in iterator) {
+ // Don't descend into test directories. Saves us some time and
+ // there's no reason to.
+ if (child.isDir && !child.path.endsWith("/test")) {
+ files = files.concat(gatherFiles(child.path, fileRegex));
+ } else if (child.path.match(fileRegex)) {
+ let info;
+ try {
+ info = OS.File.stat(child.path);
+ } catch (e) {
+ // Just ignore it.
+ continue;
+ }
+
+ files.push(child.path);
+ modifiedTimes.set(child.path, info.lastModificationDate.getTime());
+ }
+ }
+ } finally {
+ iterator.close();
+ }
+
+ return files;
+}
+
+function scanFiles(files, onChangedFile) {
+ files.forEach(file => {
+ let info;
+ try {
+ info = OS.File.stat(file);
+ } catch (e) {
+ // Just ignore it. It was probably deleted.
+ return;
+ }
+
+ const lastTime = modifiedTimes.get(file);
+
+ if (info.lastModificationDate.getTime() > lastTime) {
+ modifiedTimes.set(file, info.lastModificationDate.getTime());
+ onChangedFile(file);
+ }
+ });
+}
+
+onmessage = function (event) {
+ const { path, fileRegex } = event.data;
+
+ const info = OS.File.stat(path);
+ if (!info.isDir) {
+ throw new Error("Watcher expects a directory as root path");
+ }
+
+ // We get a list of all the files upfront, which means we don't
+ // support adding new files. But you need to rebuild Firefox when
+ // adding a new file anyway.
+ const files = gatherFiles(path, fileRegex || /.*/);
+
+ // Every second, scan for file changes by stat-ing each of them and
+ // comparing modification time.
+ setInterval(() => {
+ scanFiles(files, changedFile => {
+ postMessage({ path: changedFile });
+ });
+ }, 1000);
+};
diff --git a/devtools/client/shared/file-watcher.js b/devtools/client/shared/file-watcher.js
new file mode 100644
index 000000000..7799422f1
--- /dev/null
+++ b/devtools/client/shared/file-watcher.js
@@ -0,0 +1,28 @@
+/* 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 { ChromeWorker } = require("chrome");
+
+function watchFiles(path, onFileChanged) {
+ const watchWorker = new ChromeWorker(
+ "resource://devtools/client/shared/file-watcher-worker.js"
+ );
+
+ watchWorker.onmessage = event => {
+ // We need to turn a local path back into a resource URI (or
+ // chrome). This means that this system will only work when built
+ // files are symlinked, so that these URIs actually read from
+ // local sources. There might be a better way to do this.
+ const { path: newPath } = event.data;
+ onFileChanged(newPath);
+ };
+
+ watchWorker.postMessage({
+ path,
+ fileRegex: /\.(js|css|svg|png)$/
+ });
+ return watchWorker;
+}
+exports.watchFiles = watchFiles;
diff --git a/devtools/client/shared/frame-script-utils.js b/devtools/client/shared/frame-script-utils.js
new file mode 100644
index 000000000..3db7ed9ab
--- /dev/null
+++ b/devtools/client/shared/frame-script-utils.js
@@ -0,0 +1,206 @@
+/* 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/. */
+
+/* eslint-env browser */
+/* global addMessageListener, sendAsyncMessage, content */
+"use strict";
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const defer = require("devtools/shared/defer");
+const { Task } = require("devtools/shared/task");
+
+loader.lazyGetter(this, "nsIProfilerModule", () => {
+ return Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
+});
+
+addMessageListener("devtools:test:history", function ({ data }) {
+ content.history[data.direction]();
+});
+
+addMessageListener("devtools:test:navigate", function ({ data }) {
+ content.location = data.location;
+});
+
+addMessageListener("devtools:test:reload", function ({ data }) {
+ data = data || {};
+ content.location.reload(data.forceget);
+});
+
+addMessageListener("devtools:test:console", function ({ data }) {
+ let { method, args, id } = data;
+ content.console[method].apply(content.console, args);
+ sendAsyncMessage("devtools:test:console:response", { id });
+});
+
+/**
+ * Performs a single XMLHttpRequest and returns a promise that resolves once
+ * the request has loaded.
+ *
+ * @param Object data
+ * { method: the request method (default: "GET"),
+ * url: the url to request (default: content.location.href),
+ * body: the request body to send (default: ""),
+ * nocache: append an unique token to the query string (default: true),
+ * requestHeaders: set request headers (default: none)
+ * }
+ *
+ * @return Promise A promise that's resolved with object
+ * { status: XMLHttpRequest.status,
+ * response: XMLHttpRequest.response }
+ *
+ */
+function promiseXHR(data) {
+ let xhr = new content.XMLHttpRequest();
+
+ let method = data.method || "GET";
+ let url = data.url || content.location.href;
+ let body = data.body || "";
+
+ if (data.nocache) {
+ url += "?devtools-cachebust=" + Math.random();
+ }
+
+ let deferred = defer();
+ xhr.addEventListener("loadend", function loadend(event) {
+ xhr.removeEventListener("loadend", loadend);
+ deferred.resolve({ status: xhr.status, response: xhr.response });
+ });
+
+ xhr.open(method, url);
+
+ // Set request headers
+ if (data.requestHeaders) {
+ data.requestHeaders.forEach(header => {
+ xhr.setRequestHeader(header.name, header.value);
+ });
+ }
+
+ xhr.send(body);
+ return deferred.promise;
+}
+
+/**
+ * Performs XMLHttpRequest request(s) in the context of the page. The data
+ * parameter can be either a single object or an array of objects described
+ * below. The requests will be performed one at a time in the order they appear
+ * in the data.
+ *
+ * The objects should have following form (any of them can be omitted; defaults
+ * shown below):
+ * {
+ * method: "GET",
+ * url: content.location.href,
+ * body: "",
+ * nocache: true, // Adds a cache busting random token to the URL,
+ * requestHeaders: [{
+ * name: "Content-Type",
+ * value: "application/json"
+ * }]
+ * }
+ *
+ * The handler will respond with devtools:test:xhr message after all requests
+ * have finished. Following data will be available for each requests
+ * (in the same order as requests):
+ * {
+ * status: XMLHttpRequest.status
+ * response: XMLHttpRequest.response
+ * }
+ */
+addMessageListener("devtools:test:xhr", Task.async(function* ({ data }) {
+ let requests = Array.isArray(data) ? data : [data];
+ let responses = [];
+
+ for (let request of requests) {
+ let response = yield promiseXHR(request);
+ responses.push(response);
+ }
+
+ sendAsyncMessage("devtools:test:xhr", responses);
+}));
+
+addMessageListener("devtools:test:profiler", function ({ data }) {
+ let { method, args, id } = data;
+ let result = nsIProfilerModule[method](...args);
+ sendAsyncMessage("devtools:test:profiler:response", {
+ data: result,
+ id: id
+ });
+});
+
+// To eval in content, look at `evalInDebuggee` in the shared-head.js.
+addMessageListener("devtools:test:eval", function ({ data }) {
+ sendAsyncMessage("devtools:test:eval:response", {
+ value: content.eval(data.script),
+ id: data.id
+ });
+});
+
+addEventListener("load", function () {
+ sendAsyncMessage("devtools:test:load");
+}, true);
+
+/**
+ * Set a given style property value on a node.
+ * @param {Object} data
+ * - {String} selector The CSS selector to get the node (can be a "super"
+ * selector).
+ * - {String} propertyName The name of the property to set.
+ * - {String} propertyValue The value for the property.
+ */
+addMessageListener("devtools:test:setStyle", function (msg) {
+ let {selector, propertyName, propertyValue} = msg.data;
+ let node = superQuerySelector(selector);
+ if (!node) {
+ return;
+ }
+
+ node.style[propertyName] = propertyValue;
+
+ sendAsyncMessage("devtools:test:setStyle");
+});
+
+/**
+ * Set a given attribute value on a node.
+ * @param {Object} data
+ * - {String} selector The CSS selector to get the node (can be a "super"
+ * selector).
+ * - {String} attributeName The name of the attribute to set.
+ * - {String} attributeValue The value for the attribute.
+ */
+addMessageListener("devtools:test:setAttribute", function (msg) {
+ let {selector, attributeName, attributeValue} = msg.data;
+ let node = superQuerySelector(selector);
+ if (!node) {
+ return;
+ }
+
+ node.setAttribute(attributeName, attributeValue);
+
+ sendAsyncMessage("devtools:test:setAttribute");
+});
+
+/**
+ * Like document.querySelector but can go into iframes too.
+ * ".container iframe || .sub-container div" will first try to find the node
+ * matched by ".container iframe" in the root document, then try to get the
+ * content document inside it, and then try to match ".sub-container div" inside
+ * this document.
+ * Any selector coming before the || separator *MUST* match a frame node.
+ * @param {String} superSelector.
+ * @return {DOMNode} The node, or null if not found.
+ */
+function superQuerySelector(superSelector, root = content.document) {
+ let frameIndex = superSelector.indexOf("||");
+ if (frameIndex === -1) {
+ return root.querySelector(superSelector);
+ }
+ let rootSelector = superSelector.substring(0, frameIndex).trim();
+ let childSelector = superSelector.substring(frameIndex + 2).trim();
+ root = root.querySelector(rootSelector);
+ if (!root || !root.contentWindow) {
+ return null;
+ }
+
+ return superQuerySelector(childSelector, root.contentWindow.document);
+}
diff --git a/devtools/client/shared/getjson.js b/devtools/client/shared/getjson.js
new file mode 100644
index 000000000..3c4d48e07
--- /dev/null
+++ b/devtools/client/shared/getjson.js
@@ -0,0 +1,76 @@
+/* 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 {CC} = require("chrome");
+const defer = require("devtools/shared/defer");
+const promise = require("promise");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
+
+const XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
+
+/**
+ * Downloads and caches a JSON file from an URL given by a pref.
+ *
+ * @param {String} prefName
+ * The preference for the target URL
+ *
+ * @return {Promise}
+ * - Resolved with the JSON object in case of successful request
+ * or cache hit
+ * - Rejected with an error message in case of failure
+ */
+exports.getJSON = function (prefName) {
+ let deferred = defer();
+ let xhr = new XMLHttpRequest();
+
+ // We used to store cached data in preferences, but now we use asyncStorage
+ // Migration step: if it still exists, move this now useless preference in its
+ // new location and clear it
+ if (Services.prefs.prefHasUserValue(prefName + "_cache")) {
+ let json = Services.prefs.getCharPref(prefName + "_cache");
+ asyncStorage.setItem(prefName + "_cache", json).catch(function (e) {
+ // Could not move the cache, let's log the error but continue
+ console.error(e);
+ });
+ Services.prefs.clearUserPref(prefName + "_cache");
+ }
+
+ function readFromStorage(networkError) {
+ asyncStorage.getItem(prefName + "_cache").then(function (json) {
+ if (!json) {
+ return promise.reject("Empty cache for " + prefName);
+ }
+ return deferred.resolve(json);
+ }).catch(function (e) {
+ deferred.reject("JSON not available, CDN error: " + networkError +
+ ", storage error: " + e);
+ });
+ }
+
+ xhr.onload = () => {
+ try {
+ let json = JSON.parse(xhr.responseText);
+ asyncStorage.setItem(prefName + "_cache", json).catch(function (e) {
+ // Could not update cache, let's log the error but continue
+ console.error(e);
+ });
+ deferred.resolve(json);
+ } catch (e) {
+ readFromStorage(e);
+ }
+ };
+
+ xhr.onerror = (e) => {
+ readFromStorage(e);
+ };
+
+ xhr.open("get", Services.prefs.getCharPref(prefName));
+ xhr.send();
+
+ return deferred.promise;
+};
diff --git a/devtools/client/shared/inplace-editor.js b/devtools/client/shared/inplace-editor.js
new file mode 100644
index 000000000..652163233
--- /dev/null
+++ b/devtools/client/shared/inplace-editor.js
@@ -0,0 +1,1566 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Basic use:
+ * let spanToEdit = document.getElementById("somespan");
+ *
+ * editableField({
+ * element: spanToEdit,
+ * done: function(value, commit, direction) {
+ * if (commit) {
+ * spanToEdit.textContent = value;
+ * }
+ * },
+ * trigger: "dblclick"
+ * });
+ *
+ * See editableField() for more options.
+ */
+
+"use strict";
+
+const Services = require("Services");
+const focusManager = Services.focus;
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const CONTENT_TYPES = {
+ PLAIN_TEXT: 0,
+ CSS_VALUE: 1,
+ CSS_MIXED: 2,
+ CSS_PROPERTY: 3,
+};
+
+// The limit of 500 autocomplete suggestions should not be reached but is kept
+// for safety.
+const MAX_POPUP_ENTRIES = 500;
+
+const FOCUS_FORWARD = focusManager.MOVEFOCUS_FORWARD;
+const FOCUS_BACKWARD = focusManager.MOVEFOCUS_BACKWARD;
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { findMostRelevantCssPropertyIndex } = require("./suggestion-picker");
+
+/**
+ * Helper to check if the provided key matches one of the expected keys.
+ * Keys will be prefixed with DOM_VK_ and should match a key in KeyCodes.
+ *
+ * @param {String} key
+ * the key to check (can be a keyCode).
+ * @param {...String} keys
+ * list of possible keys allowed.
+ * @return {Boolean} true if the key matches one of the keys.
+ */
+function isKeyIn(key, ...keys) {
+ return keys.some(expectedKey => {
+ return key === KeyCodes["DOM_VK_" + expectedKey];
+ });
+}
+
+/**
+ * Mark a span editable. |editableField| will listen for the span to
+ * be focused and create an InlineEditor to handle text input.
+ * Changes will be committed when the InlineEditor's input is blurred
+ * or dropped when the user presses escape.
+ *
+ * @param {Object} options
+ * Options for the editable field, including:
+ * {Element} element:
+ * (required) The span to be edited on focus.
+ * {Function} canEdit:
+ * Will be called before creating the inplace editor. Editor
+ * won't be created if canEdit returns false.
+ * {Function} start:
+ * Will be called when the inplace editor is initialized.
+ * {Function} change:
+ * Will be called when the text input changes. Will be called
+ * with the current value of the text input.
+ * {Function} done:
+ * Called when input is committed or blurred. Called with
+ * current value, a boolean telling the caller whether to
+ * commit the change, and the direction of the next element to be
+ * selected. Direction may be one of Services.focus.MOVEFOCUS_FORWARD,
+ * Services.focus.MOVEFOCUS_BACKWARD, or null (no movement).
+ * This function is called before the editor has been torn down.
+ * {Function} destroy:
+ * Called when the editor is destroyed and has been torn down.
+ * {Function} contextMenu:
+ * Called when the user triggers a contextmenu event on the input.
+ * {Object} advanceChars:
+ * This can be either a string or a function.
+ * If it is a string, then if any characters in it are typed,
+ * focus will advance to the next element.
+ * Otherwise, if it is a function, then the function will
+ * be called with three arguments: a key code, the current text,
+ * and the insertion point. If the function returns true,
+ * then the focus advance takes place. If it returns false,
+ * then the character is inserted instead.
+ * {Boolean} stopOnReturn:
+ * If true, the return key will not advance the editor to the next
+ * focusable element.
+ * {Boolean} stopOnTab:
+ * If true, the tab key will not advance the editor to the next
+ * focusable element.
+ * {Boolean} stopOnShiftTab:
+ * If true, shift tab will not advance the editor to the previous
+ * focusable element.
+ * {String} trigger: The DOM event that should trigger editing,
+ * defaults to "click"
+ * {Boolean} multiline: Should the editor be a multiline textarea?
+ * defaults to false
+ * {Function or Number} maxWidth:
+ * Should the editor wrap to remain below the provided max width. Only
+ * available if multiline is true. If a function is provided, it will be
+ * called when replacing the element by the inplace input.
+ * {Boolean} trimOutput: Should the returned string be trimmed?
+ * defaults to true
+ * {Boolean} preserveTextStyles: If true, do not copy text-related styles
+ * from `element` to the new input.
+ * defaults to false
+ * {Object} cssProperties: An instance of CSSProperties.
+ */
+function editableField(options) {
+ return editableItem(options, function (element, event) {
+ if (!options.element.inplaceEditor) {
+ new InplaceEditor(options, event);
+ }
+ });
+}
+
+exports.editableField = editableField;
+
+/**
+ * Handle events for an element that should respond to
+ * clicks and sit in the editing tab order, and call
+ * a callback when it is activated.
+ *
+ * @param {Object} options
+ * The options for this editor, including:
+ * {Element} element: The DOM element.
+ * {String} trigger: The DOM event that should trigger editing,
+ * defaults to "click"
+ * @param {Function} callback
+ * Called when the editor is activated.
+ * @return {Function} function which calls callback
+ */
+function editableItem(options, callback) {
+ let trigger = options.trigger || "click";
+ let element = options.element;
+ element.addEventListener(trigger, function (evt) {
+ if (evt.target.nodeName !== "a") {
+ let win = this.ownerDocument.defaultView;
+ let selection = win.getSelection();
+ if (trigger != "click" || selection.isCollapsed) {
+ callback(element, evt);
+ }
+ evt.stopPropagation();
+ }
+ }, false);
+
+ // If focused by means other than a click, start editing by
+ // pressing enter or space.
+ element.addEventListener("keypress", function (evt) {
+ if (isKeyIn(evt.keyCode, "RETURN") || isKeyIn(evt.charCode, "SPACE")) {
+ callback(element);
+ }
+ }, true);
+
+ // Ugly workaround - the element is focused on mousedown but
+ // the editor is activated on click/mouseup. This leads
+ // to an ugly flash of the focus ring before showing the editor.
+ // So hide the focus ring while the mouse is down.
+ element.addEventListener("mousedown", function (evt) {
+ if (evt.target.nodeName !== "a") {
+ let cleanup = function () {
+ element.style.removeProperty("outline-style");
+ element.removeEventListener("mouseup", cleanup, false);
+ element.removeEventListener("mouseout", cleanup, false);
+ };
+ element.style.setProperty("outline-style", "none");
+ element.addEventListener("mouseup", cleanup, false);
+ element.addEventListener("mouseout", cleanup, false);
+ }
+ }, false);
+
+ // Mark the element editable field for tab
+ // navigation while editing.
+ element._editable = true;
+
+ // Save the trigger type so we can dispatch this later
+ element._trigger = trigger;
+
+ // Add button semantics to the element, to indicate that it can be activated.
+ element.setAttribute("role", "button");
+
+ return function turnOnEditMode() {
+ callback(element);
+ };
+}
+
+exports.editableItem = editableItem;
+
+/*
+ * Various API consumers (especially tests) sometimes want to grab the
+ * inplaceEditor expando off span elements. However, when each global has its
+ * own compartment, those expandos live on Xray wrappers that are only visible
+ * within this JSM. So we provide a little workaround here.
+ */
+
+function getInplaceEditorForSpan(span) {
+ return span.inplaceEditor;
+}
+
+exports.getInplaceEditorForSpan = getInplaceEditorForSpan;
+
+function InplaceEditor(options, event) {
+ this.elt = options.element;
+ let doc = this.elt.ownerDocument;
+ this.doc = doc;
+ this.elt.inplaceEditor = this;
+ this.cssProperties = options.cssProperties;
+ this.change = options.change;
+ this.done = options.done;
+ this.contextMenu = options.contextMenu;
+ this.destroy = options.destroy;
+ this.initial = options.initial ? options.initial : this.elt.textContent;
+ this.multiline = options.multiline || false;
+ this.maxWidth = options.maxWidth;
+ if (typeof this.maxWidth == "function") {
+ this.maxWidth = this.maxWidth();
+ }
+
+ this.trimOutput = options.trimOutput === undefined
+ ? true
+ : !!options.trimOutput;
+ this.stopOnShiftTab = !!options.stopOnShiftTab;
+ this.stopOnTab = !!options.stopOnTab;
+ this.stopOnReturn = !!options.stopOnReturn;
+ this.contentType = options.contentType || CONTENT_TYPES.PLAIN_TEXT;
+ this.property = options.property;
+ this.popup = options.popup;
+ this.preserveTextStyles = options.preserveTextStyles === undefined
+ ? false
+ : !!options.preserveTextStyles;
+
+ this._onBlur = this._onBlur.bind(this);
+ this._onWindowBlur = this._onWindowBlur.bind(this);
+ this._onKeyPress = this._onKeyPress.bind(this);
+ this._onInput = this._onInput.bind(this);
+ this._onKeyup = this._onKeyup.bind(this);
+ this._onAutocompletePopupClick = this._onAutocompletePopupClick.bind(this);
+ this._onContextMenu = this._onContextMenu.bind(this);
+
+ this._createInput();
+
+ // Hide the provided element and add our editor.
+ this.originalDisplay = this.elt.style.display;
+ this.elt.style.display = "none";
+ this.elt.parentNode.insertBefore(this.input, this.elt);
+
+ // After inserting the input to have all CSS styles applied, start autosizing.
+ this._autosize();
+
+ this.inputCharDimensions = this._getInputCharDimensions();
+ // Pull out character codes for advanceChars, listing the
+ // characters that should trigger a blur.
+ if (typeof options.advanceChars === "function") {
+ this._advanceChars = options.advanceChars;
+ } else {
+ let advanceCharcodes = {};
+ let advanceChars = options.advanceChars || "";
+ for (let i = 0; i < advanceChars.length; i++) {
+ advanceCharcodes[advanceChars.charCodeAt(i)] = true;
+ }
+ this._advanceChars = charCode => charCode in advanceCharcodes;
+ }
+
+ this.input.focus();
+
+ if (typeof options.selectAll == "undefined" || options.selectAll) {
+ this.input.select();
+ }
+
+ if (this.contentType == CONTENT_TYPES.CSS_VALUE && this.input.value == "") {
+ this._maybeSuggestCompletion(false);
+ }
+
+ this.input.addEventListener("blur", this._onBlur, false);
+ this.input.addEventListener("keypress", this._onKeyPress, false);
+ this.input.addEventListener("input", this._onInput, false);
+ this.input.addEventListener("dblclick", this._stopEventPropagation, false);
+ this.input.addEventListener("click", this._stopEventPropagation, false);
+ this.input.addEventListener("mousedown", this._stopEventPropagation, false);
+ this.input.addEventListener("contextmenu", this._onContextMenu, false);
+ this.doc.defaultView.addEventListener("blur", this._onWindowBlur, false);
+
+ this.validate = options.validate;
+
+ if (this.validate) {
+ this.input.addEventListener("keyup", this._onKeyup, false);
+ }
+
+ this._updateSize();
+
+ EventEmitter.decorate(this);
+
+ if (options.start) {
+ options.start(this, event);
+ }
+}
+
+exports.InplaceEditor = InplaceEditor;
+
+InplaceEditor.CONTENT_TYPES = CONTENT_TYPES;
+
+InplaceEditor.prototype = {
+
+ get currentInputValue() {
+ let val = this.trimOutput ? this.input.value.trim() : this.input.value;
+ return val;
+ },
+
+ _createInput: function () {
+ this.input =
+ this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
+ this.input.inplaceEditor = this;
+
+ if (this.multiline) {
+ // Hide the textarea resize handle.
+ this.input.style.resize = "none";
+ this.input.style.overflow = "hidden";
+ }
+
+ this.input.classList.add("styleinspector-propertyeditor");
+ this.input.value = this.initial;
+ if (!this.preserveTextStyles) {
+ copyTextStyles(this.elt, this.input);
+ }
+ },
+
+ /**
+ * Get rid of the editor.
+ */
+ _clear: function () {
+ if (!this.input) {
+ // Already cleared.
+ return;
+ }
+
+ this.input.removeEventListener("blur", this._onBlur, false);
+ this.input.removeEventListener("keypress", this._onKeyPress, false);
+ this.input.removeEventListener("keyup", this._onKeyup, false);
+ this.input.removeEventListener("input", this._onInput, false);
+ this.input.removeEventListener("dblclick", this._stopEventPropagation, false);
+ this.input.removeEventListener("click", this._stopEventPropagation, false);
+ this.input.removeEventListener("mousedown", this._stopEventPropagation, false);
+ this.input.removeEventListener("contextmenu", this._onContextMenu, false);
+ this.doc.defaultView.removeEventListener("blur", this._onWindowBlur, false);
+
+ this._stopAutosize();
+
+ this.elt.style.display = this.originalDisplay;
+
+ if (this.doc.activeElement == this.input) {
+ this.elt.focus();
+ }
+
+ this.input.remove();
+ this.input = null;
+
+ delete this.elt.inplaceEditor;
+ delete this.elt;
+
+ if (this.destroy) {
+ this.destroy();
+ }
+ },
+
+ /**
+ * Keeps the editor close to the size of its input string. This is pretty
+ * crappy, suggestions for improvement welcome.
+ */
+ _autosize: function () {
+ // Create a hidden, absolutely-positioned span to measure the text
+ // in the input. Boo.
+
+ // We can't just measure the original element because a) we don't
+ // change the underlying element's text ourselves (we leave that
+ // up to the client), and b) without tweaking the style of the
+ // original element, it might wrap differently or something.
+ this._measurement =
+ this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span");
+ this._measurement.className = "autosizer";
+ this.elt.parentNode.appendChild(this._measurement);
+ let style = this._measurement.style;
+ style.visibility = "hidden";
+ style.position = "absolute";
+ style.top = "0";
+ style.left = "0";
+
+ if (this.multiline) {
+ style.whiteSpace = "pre-wrap";
+ style.wordWrap = "break-word";
+ if (this.maxWidth) {
+ style.maxWidth = this.maxWidth + "px";
+ // Use position fixed to measure dimensions without any influence from
+ // the container of the editor.
+ style.position = "fixed";
+ }
+ }
+
+ copyAllStyles(this.input, this._measurement);
+ this._updateSize();
+ },
+
+ /**
+ * Clean up the mess created by _autosize().
+ */
+ _stopAutosize: function () {
+ if (!this._measurement) {
+ return;
+ }
+ this._measurement.remove();
+ delete this._measurement;
+ },
+
+ /**
+ * Size the editor to fit its current contents.
+ */
+ _updateSize: function () {
+ // Replace spaces with non-breaking spaces. Otherwise setting
+ // the span's textContent will collapse spaces and the measurement
+ // will be wrong.
+ let content = this.input.value;
+ let unbreakableSpace = "\u00a0";
+
+ // Make sure the content is not empty.
+ if (content === "") {
+ content = unbreakableSpace;
+ }
+
+ // If content ends with a new line, add a blank space to force the autosize
+ // element to adapt its height.
+ if (content.lastIndexOf("\n") === content.length - 1) {
+ content = content + unbreakableSpace;
+ }
+
+ if (!this.multiline) {
+ content = content.replace(/ /g, unbreakableSpace);
+ }
+
+ this._measurement.textContent = content;
+
+ // Do not use offsetWidth: it will round floating width values.
+ let width = this._measurement.getBoundingClientRect().width + 2;
+ if (this.multiline) {
+ if (this.maxWidth) {
+ width = Math.min(this.maxWidth, width);
+ }
+ let height = this._measurement.getBoundingClientRect().height;
+ this.input.style.height = height + "px";
+ }
+ this.input.style.width = width + "px";
+ },
+
+ /**
+ * Get the width and height of a single character in the input to properly
+ * position the autocompletion popup.
+ */
+ _getInputCharDimensions: function () {
+ // Just make the text content to be 'x' to get the width and height of any
+ // character in a monospace font.
+ this._measurement.textContent = "x";
+ let width = this._measurement.clientWidth;
+ let height = this._measurement.clientHeight;
+ return { width, height };
+ },
+
+ /**
+ * Increment property values in rule view.
+ *
+ * @param {Number} increment
+ * The amount to increase/decrease the property value.
+ * @return {Boolean} true if value has been incremented.
+ */
+ _incrementValue: function (increment) {
+ let value = this.input.value;
+ let selectionStart = this.input.selectionStart;
+ let selectionEnd = this.input.selectionEnd;
+
+ let newValue = this._incrementCSSValue(value, increment, selectionStart,
+ selectionEnd);
+
+ if (!newValue) {
+ return false;
+ }
+
+ this.input.value = newValue.value;
+ this.input.setSelectionRange(newValue.start, newValue.end);
+ this._doValidation();
+
+ // Call the user's change handler if available.
+ if (this.change) {
+ this.change(this.currentInputValue);
+ }
+
+ return true;
+ },
+
+ /**
+ * Increment the property value based on the property type.
+ *
+ * @param {String} value
+ * Property value.
+ * @param {Number} increment
+ * Amount to increase/decrease the property value.
+ * @param {Number} selStart
+ * Starting index of the value.
+ * @param {Number} selEnd
+ * Ending index of the value.
+ * @return {Object} object with properties 'value', 'start', and 'end'.
+ */
+ _incrementCSSValue: function (value, increment, selStart, selEnd) {
+ let range = this._parseCSSValue(value, selStart);
+ let type = (range && range.type) || "";
+ let rawValue = range ? value.substring(range.start, range.end) : "";
+ let preRawValue = range ? value.substr(0, range.start) : "";
+ let postRawValue = range ? value.substr(range.end) : "";
+ let info;
+
+ let incrementedValue = null, selection;
+ if (type === "num") {
+ if (rawValue == "0") {
+ info = {};
+ info.units = this._findCompatibleUnit(preRawValue, postRawValue);
+ }
+
+ let newValue = this._incrementRawValue(rawValue, increment, info);
+ if (newValue !== null) {
+ incrementedValue = newValue;
+ selection = [0, incrementedValue.length];
+ }
+ } else if (type === "hex") {
+ let exprOffset = selStart - range.start;
+ let exprOffsetEnd = selEnd - range.start;
+ let newValue = this._incHexColor(rawValue, increment, exprOffset,
+ exprOffsetEnd);
+ if (newValue) {
+ incrementedValue = newValue.value;
+ selection = newValue.selection;
+ }
+ } else {
+ if (type === "rgb" || type === "hsl") {
+ info = {};
+ let part = value.substring(range.start, selStart).split(",").length - 1;
+ if (part === 3) {
+ // alpha
+ info.minValue = 0;
+ info.maxValue = 1;
+ } else if (type === "rgb") {
+ info.minValue = 0;
+ info.maxValue = 255;
+ } else if (part !== 0) {
+ // hsl percentage
+ info.minValue = 0;
+ info.maxValue = 100;
+
+ // select the previous number if the selection is at the end of a
+ // percentage sign.
+ if (value.charAt(selStart - 1) === "%") {
+ --selStart;
+ }
+ }
+ }
+ return this._incrementGenericValue(value, increment, selStart, selEnd,
+ info);
+ }
+
+ if (incrementedValue === null) {
+ return null;
+ }
+
+ return {
+ value: preRawValue + incrementedValue + postRawValue,
+ start: range.start + selection[0],
+ end: range.start + selection[1]
+ };
+ },
+
+ /**
+ * Find a compatible unit to use for a CSS number value inserted between the
+ * provided beforeValue and afterValue. The compatible unit will be picked
+ * from a selection of default units corresponding to supported CSS value
+ * dimensions (distance, angle, duration).
+ *
+ * @param {String} beforeValue
+ * The string preceeding the number value in the current property
+ * value.
+ * @param {String} afterValue
+ * The string following the number value in the current property value.
+ * @return {String} a valid unit that can be used for this number value or
+ * empty string if no match could be found.
+ */
+ _findCompatibleUnit: function (beforeValue, afterValue) {
+ if (!this.property || !this.property.name) {
+ return "";
+ }
+
+ // A DOM element is used to test the validity of various units. This is to
+ // avoid having to do an async call to the server to get this information.
+ let el = this.doc.createElement("div");
+ let units = ["px", "deg", "s"];
+ for (let unit of units) {
+ let value = beforeValue + "1" + unit + afterValue;
+ el.style.setProperty(this.property.name, "");
+ el.style.setProperty(this.property.name, value);
+ if (el.style.getPropertyValue(this.property.name) !== "") {
+ return unit;
+ }
+ }
+ return "";
+ },
+
+ /**
+ * Parses the property value and type.
+ *
+ * @param {String} value
+ * Property value.
+ * @param {Number} offset
+ * Starting index of value.
+ * @return {Object} object with properties 'value', 'start', 'end', and
+ * 'type'.
+ */
+ _parseCSSValue: function (value, offset) {
+ /* eslint-disable max-len */
+ const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d*\.?\d+(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
+ /* eslint-enable */
+ let start = 0;
+ let m;
+
+ // retreive values from left to right until we find the one at our offset
+ while ((m = reSplitCSS.exec(value)) &&
+ (m.index + m[0].length < offset)) {
+ value = value.substr(m.index + m[0].length);
+ start += m.index + m[0].length;
+ offset -= m.index + m[0].length;
+ }
+
+ if (!m) {
+ return null;
+ }
+
+ let type;
+ if (m[1]) {
+ type = "url";
+ } else if (m[2]) {
+ type = "rgb";
+ } else if (m[3]) {
+ type = "hsl";
+ } else if (m[4]) {
+ type = "hex";
+ } else if (m[5]) {
+ type = "num";
+ }
+
+ return {
+ value: m[0],
+ start: start + m.index,
+ end: start + m.index + m[0].length,
+ type: type
+ };
+ },
+
+ /**
+ * Increment the property value for types other than
+ * number or hex, such as rgb, hsl, and file names.
+ *
+ * @param {String} value
+ * Property value.
+ * @param {Number} increment
+ * Amount to increment/decrement.
+ * @param {Number} offset
+ * Starting index of the property value.
+ * @param {Number} offsetEnd
+ * Ending index of the property value.
+ * @param {Object} info
+ * Object with details about the property value.
+ * @return {Object} object with properties 'value', 'start', and 'end'.
+ */
+ _incrementGenericValue: function (value, increment, offset, offsetEnd, info) {
+ // Try to find a number around the cursor to increment.
+ let start, end;
+ // Check if we are incrementing in a non-number context (such as a URL)
+ if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) &&
+ !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) {
+ // We have a number selected, possibly with a suffix, and we are not in
+ // the disallowed case of just part of a known number being selected.
+ // Use that number.
+ start = offset;
+ end = offsetEnd;
+ } else {
+ // Parse periods as belonging to the number only if we are in a known
+ // number context. (This makes incrementing the 1 in 'image1.gif' work.)
+ let pattern = "[" + (info ? "0-9." : "0-9") + "]*";
+ let before = new RegExp(pattern + "$")
+ .exec(value.substr(0, offset))[0].length;
+ let after = new RegExp("^" + pattern)
+ .exec(value.substr(offset))[0].length;
+
+ start = offset - before;
+ end = offset + after;
+
+ // Expand the number to contain an initial minus sign if it seems
+ // free-standing.
+ if (value.charAt(start - 1) === "-" &&
+ (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) {
+ --start;
+ }
+ }
+
+ if (start !== end) {
+ // Include percentages as part of the incremented number (they are
+ // common enough).
+ if (value.charAt(end) === "%") {
+ ++end;
+ }
+
+ let first = value.substr(0, start);
+ let mid = value.substring(start, end);
+ let last = value.substr(end);
+
+ mid = this._incrementRawValue(mid, increment, info);
+
+ if (mid !== null) {
+ return {
+ value: first + mid + last,
+ start: start,
+ end: start + mid.length
+ };
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Increment the property value for numbers.
+ *
+ * @param {String} rawValue
+ * Raw value to increment.
+ * @param {Number} increment
+ * Amount to increase/decrease the raw value.
+ * @param {Object} info
+ * Object with info about the property value.
+ * @return {String} the incremented value.
+ */
+ _incrementRawValue: function (rawValue, increment, info) {
+ let num = parseFloat(rawValue);
+
+ if (isNaN(num)) {
+ return null;
+ }
+
+ let number = /\d+(\.\d+)?/.exec(rawValue);
+
+ let units = rawValue.substr(number.index + number[0].length);
+ if (info && "units" in info) {
+ units = info.units;
+ }
+
+ // avoid rounding errors
+ let newValue = Math.round((num + increment) * 1000) / 1000;
+
+ if (info && "minValue" in info) {
+ newValue = Math.max(newValue, info.minValue);
+ }
+ if (info && "maxValue" in info) {
+ newValue = Math.min(newValue, info.maxValue);
+ }
+
+ newValue = newValue.toString();
+
+ return newValue + units;
+ },
+
+ /**
+ * Increment the property value for hex.
+ *
+ * @param {String} value
+ * Property value.
+ * @param {Number} increment
+ * Amount to increase/decrease the property value.
+ * @param {Number} offset
+ * Starting index of the property value.
+ * @param {Number} offsetEnd
+ * Ending index of the property value.
+ * @return {Object} object with properties 'value' and 'selection'.
+ */
+ _incHexColor: function (rawValue, increment, offset, offsetEnd) {
+ // Return early if no part of the rawValue is selected.
+ if (offsetEnd > rawValue.length && offset >= rawValue.length) {
+ return null;
+ }
+ if (offset < 1 && offsetEnd <= 1) {
+ return null;
+ }
+ // Ignore the leading #.
+ rawValue = rawValue.substr(1);
+ --offset;
+ --offsetEnd;
+
+ // Clamp the selection to within the actual value.
+ offset = Math.max(offset, 0);
+ offsetEnd = Math.min(offsetEnd, rawValue.length);
+ offsetEnd = Math.max(offsetEnd, offset);
+
+ // Normalize #ABC -> #AABBCC.
+ if (rawValue.length === 3) {
+ rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
+ rawValue.charAt(1) + rawValue.charAt(1) +
+ rawValue.charAt(2) + rawValue.charAt(2);
+ offset *= 2;
+ offsetEnd *= 2;
+ }
+
+ // Normalize #ABCD -> #AABBCCDD.
+ if (rawValue.length === 4) {
+ rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
+ rawValue.charAt(1) + rawValue.charAt(1) +
+ rawValue.charAt(2) + rawValue.charAt(2) +
+ rawValue.charAt(3) + rawValue.charAt(3);
+ offset *= 2;
+ offsetEnd *= 2;
+ }
+
+ if (rawValue.length !== 6 && rawValue.length !== 8) {
+ return null;
+ }
+
+ // If no selection, increment an adjacent color, preferably one to the left.
+ if (offset === offsetEnd) {
+ if (offset === 0) {
+ offsetEnd = 1;
+ } else {
+ offset = offsetEnd - 1;
+ }
+ }
+
+ // Make the selection cover entire parts.
+ offset -= offset % 2;
+ offsetEnd += offsetEnd % 2;
+
+ // Remap the increments from [0.1, 1, 10] to [1, 1, 16].
+ if (increment > -1 && increment < 1) {
+ increment = (increment < 0 ? -1 : 1);
+ }
+ if (Math.abs(increment) === 10) {
+ increment = (increment < 0 ? -16 : 16);
+ }
+
+ let isUpper = (rawValue.toUpperCase() === rawValue);
+
+ for (let pos = offset; pos < offsetEnd; pos += 2) {
+ // Increment the part in [pos, pos+2).
+ let mid = rawValue.substr(pos, 2);
+ let value = parseInt(mid, 16);
+
+ if (isNaN(value)) {
+ return null;
+ }
+
+ mid = Math.min(Math.max(value + increment, 0), 255).toString(16);
+
+ while (mid.length < 2) {
+ mid = "0" + mid;
+ }
+ if (isUpper) {
+ mid = mid.toUpperCase();
+ }
+
+ rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2);
+ }
+
+ return {
+ value: "#" + rawValue,
+ selection: [offset + 1, offsetEnd + 1]
+ };
+ },
+
+ /**
+ * Cycle through the autocompletion suggestions in the popup.
+ *
+ * @param {Boolean} reverse
+ * true to select previous item from the popup.
+ * @param {Boolean} noSelect
+ * true to not select the text after selecting the newly selectedItem
+ * from the popup.
+ */
+ _cycleCSSSuggestion: function (reverse, noSelect) {
+ // selectedItem can be null when nothing is selected in an empty editor.
+ let {label, preLabel} = this.popup.selectedItem ||
+ {label: "", preLabel: ""};
+ if (reverse) {
+ this.popup.selectPreviousItem();
+ } else {
+ this.popup.selectNextItem();
+ }
+
+ this._selectedIndex = this.popup.selectedIndex;
+ let input = this.input;
+ let pre = "";
+
+ if (input.selectionStart < input.selectionEnd) {
+ pre = input.value.slice(0, input.selectionStart);
+ } else {
+ pre = input.value.slice(0, input.selectionStart - label.length +
+ preLabel.length);
+ }
+
+ let post = input.value.slice(input.selectionEnd, input.value.length);
+ let item = this.popup.selectedItem;
+ let toComplete = item.label.slice(item.preLabel.length);
+ input.value = pre + toComplete + post;
+
+ if (!noSelect) {
+ input.setSelectionRange(pre.length, pre.length + toComplete.length);
+ } else {
+ input.setSelectionRange(pre.length + toComplete.length,
+ pre.length + toComplete.length);
+ }
+
+ this._updateSize();
+ // This emit is mainly for the purpose of making the test flow simpler.
+ this.emit("after-suggest");
+ },
+
+ /**
+ * Call the client's done handler and clear out.
+ */
+ _apply: function (event, direction) {
+ if (this._applied) {
+ return null;
+ }
+
+ this._applied = true;
+
+ if (this.done) {
+ let val = this.cancelled ? this.initial : this.currentInputValue;
+ return this.done(val, !this.cancelled, direction);
+ }
+
+ return null;
+ },
+
+ /**
+ * Hide the popup and cancel any pending popup opening.
+ */
+ _onWindowBlur: function () {
+ if (this.popup && this.popup.isOpen) {
+ this.popup.hidePopup();
+ }
+
+ if (this._openPopupTimeout) {
+ this.doc.defaultView.clearTimeout(this._openPopupTimeout);
+ }
+ },
+
+ /**
+ * Event handler called when the inplace-editor's input loses focus.
+ */
+ _onBlur: function (event) {
+ if (event && this.popup && this.popup.isOpen &&
+ this.popup.selectedIndex >= 0) {
+ this._acceptPopupSuggestion();
+ } else {
+ this._apply();
+ this._clear();
+ }
+ },
+
+ /**
+ * Event handler called by the autocomplete popup when receiving a click
+ * event.
+ */
+ _onAutocompletePopupClick: function () {
+ this._acceptPopupSuggestion();
+ },
+
+ _acceptPopupSuggestion: function () {
+ let label, preLabel;
+
+ if (this._selectedIndex === undefined) {
+ ({label, preLabel} = this.popup.getItemAtIndex(this.popup.selectedIndex));
+ } else {
+ ({label, preLabel} = this.popup.getItemAtIndex(this._selectedIndex));
+ }
+
+ let input = this.input;
+
+ let pre = "";
+
+ // CSS_MIXED needs special treatment here to make it so that
+ // multiple presses of tab will cycle through completions, but
+ // without selecting the completed text. However, this same
+ // special treatment will do the wrong thing for other editing
+ // styles.
+ if (input.selectionStart < input.selectionEnd ||
+ this.contentType !== CONTENT_TYPES.CSS_MIXED) {
+ pre = input.value.slice(0, input.selectionStart);
+ } else {
+ pre = input.value.slice(0, input.selectionStart - label.length +
+ preLabel.length);
+ }
+ let post = input.value.slice(input.selectionEnd, input.value.length);
+ let item = this.popup.selectedItem;
+ this._selectedIndex = this.popup.selectedIndex;
+ let toComplete = item.label.slice(item.preLabel.length);
+ input.value = pre + toComplete + post;
+ input.setSelectionRange(pre.length + toComplete.length,
+ pre.length + toComplete.length);
+ this._updateSize();
+ // Wait for the popup to hide and then focus input async otherwise it does
+ // not work.
+ let onPopupHidden = () => {
+ this.popup.off("popup-closed", onPopupHidden);
+ this.doc.defaultView.setTimeout(()=> {
+ input.focus();
+ this.emit("after-suggest");
+ }, 0);
+ };
+ this.popup.on("popup-closed", onPopupHidden);
+ this._hideAutocompletePopup();
+ },
+
+ /**
+ * Handle the input field's keypress event.
+ */
+ _onKeyPress: function (event) {
+ let prevent = false;
+
+ let key = event.keyCode;
+ let input = this.input;
+
+ let multilineNavigation = !this._isSingleLine() &&
+ isKeyIn(key, "UP", "DOWN", "LEFT", "RIGHT");
+ let isPlainText = this.contentType == CONTENT_TYPES.PLAIN_TEXT;
+ let isPopupOpen = this.popup && this.popup.isOpen;
+
+ let increment = 0;
+ if (!isPlainText && !multilineNavigation) {
+ increment = this._getIncrement(event);
+ }
+
+ if (isKeyIn(key, "PAGE_UP", "PAGE_DOWN")) {
+ this._preventSuggestions = true;
+ }
+
+ let cycling = false;
+ if (increment && this._incrementValue(increment)) {
+ this._updateSize();
+ prevent = true;
+ cycling = true;
+ }
+
+ if (isPopupOpen && isKeyIn(key, "UP", "DOWN", "PAGE_UP", "PAGE_DOWN")) {
+ prevent = true;
+ cycling = true;
+ this._cycleCSSSuggestion(isKeyIn(key, "UP", "PAGE_UP"));
+ this._doValidation();
+ }
+
+ if (isKeyIn(key, "BACK_SPACE", "DELETE", "LEFT", "RIGHT", "HOME", "END")) {
+ if (isPopupOpen) {
+ this._hideAutocompletePopup();
+ }
+ } else if (!cycling && !multilineNavigation &&
+ !event.metaKey && !event.altKey && !event.ctrlKey) {
+ this._maybeSuggestCompletion(true);
+ }
+
+ if (this.multiline && event.shiftKey && isKeyIn(key, "RETURN")) {
+ prevent = false;
+ } else if (
+ this._advanceChars(event.charCode, input.value, input.selectionStart) ||
+ isKeyIn(key, "RETURN", "TAB")) {
+ prevent = true;
+
+ let direction;
+ if ((this.stopOnReturn && isKeyIn(key, "RETURN")) ||
+ (this.stopOnTab && !event.shiftKey && isKeyIn(key, "TAB")) ||
+ (this.stopOnShiftTab && event.shiftKey && isKeyIn(key, "TAB"))) {
+ direction = null;
+ } else if (event.shiftKey && isKeyIn(key, "TAB")) {
+ direction = FOCUS_BACKWARD;
+ } else {
+ direction = FOCUS_FORWARD;
+ }
+
+ // Now we don't want to suggest anything as we are moving out.
+ this._preventSuggestions = true;
+ // But we still want to show suggestions for css values. i.e. moving out
+ // of css property input box in forward direction
+ if (this.contentType == CONTENT_TYPES.CSS_PROPERTY &&
+ direction == FOCUS_FORWARD) {
+ this._preventSuggestions = false;
+ }
+
+ if (isKeyIn(key, "TAB") && this.contentType == CONTENT_TYPES.CSS_MIXED) {
+ if (this.popup && input.selectionStart < input.selectionEnd) {
+ event.preventDefault();
+ input.setSelectionRange(input.selectionEnd, input.selectionEnd);
+ this.emit("after-suggest");
+ return;
+ } else if (this.popup && this.popup.isOpen) {
+ event.preventDefault();
+ this._cycleCSSSuggestion(event.shiftKey, true);
+ return;
+ }
+ }
+
+ this._apply(event, direction);
+
+ // Close the popup if open
+ if (this.popup && this.popup.isOpen) {
+ this._hideAutocompletePopup();
+ }
+
+ if (direction !== null && focusManager.focusedElement === input) {
+ // If the focused element wasn't changed by the done callback,
+ // move the focus as requested.
+ let next = moveFocus(this.doc.defaultView, direction);
+
+ // If the next node to be focused has been tagged as an editable
+ // node, trigger editing using the configured event
+ if (next && next.ownerDocument === this.doc && next._editable) {
+ let e = this.doc.createEvent("Event");
+ e.initEvent(next._trigger, true, true);
+ next.dispatchEvent(e);
+ }
+ }
+
+ this._clear();
+ } else if (isKeyIn(key, "ESCAPE")) {
+ // Cancel and blur ourselves.
+ // Now we don't want to suggest anything as we are moving out.
+ this._preventSuggestions = true;
+ // Close the popup if open
+ if (this.popup && this.popup.isOpen) {
+ this._hideAutocompletePopup();
+ }
+ prevent = true;
+ this.cancelled = true;
+ this._apply();
+ this._clear();
+ event.stopPropagation();
+ } else if (isKeyIn(key, "SPACE")) {
+ // No need for leading spaces here. This is particularly
+ // noticable when adding a property: it's very natural to type
+ // <name>: (which advances to the next property) then spacebar.
+ prevent = !input.value;
+ }
+
+ if (prevent) {
+ event.preventDefault();
+ }
+ },
+
+ _onContextMenu: function (event) {
+ if (this.contextMenu) {
+ this.contextMenu(event);
+ }
+ },
+
+ /**
+ * Open the autocomplete popup, adding a custom click handler and classname.
+ *
+ * @param {Number} offset
+ * X-offset relative to the input starting edge.
+ * @param {Number} selectedIndex
+ * The index of the item that should be selected. Use -1 to have no
+ * item selected.
+ */
+ _openAutocompletePopup: function (offset, selectedIndex) {
+ this.popup.on("popup-click", this._onAutocompletePopupClick);
+ this.popup.openPopup(this.input, offset, 0, selectedIndex);
+ },
+
+ /**
+ * Remove the custom classname and click handler and close the autocomplete
+ * popup.
+ */
+ _hideAutocompletePopup: function () {
+ this.popup.off("popup-click", this._onAutocompletePopupClick);
+ this.popup.hidePopup();
+ },
+
+ /**
+ * Get the increment/decrement step to use for the provided key event.
+ */
+ _getIncrement: function (event) {
+ const largeIncrement = 100;
+ const mediumIncrement = 10;
+ const smallIncrement = 0.1;
+
+ let increment = 0;
+ let key = event.keyCode;
+
+ if (isKeyIn(key, "UP", "PAGE_UP")) {
+ increment = 1;
+ } else if (isKeyIn(key, "DOWN", "PAGE_DOWN")) {
+ increment = -1;
+ }
+
+ if (event.shiftKey && !event.altKey) {
+ if (isKeyIn(key, "PAGE_UP", "PAGE_DOWN")) {
+ increment *= largeIncrement;
+ } else {
+ increment *= mediumIncrement;
+ }
+ } else if (event.altKey && !event.shiftKey) {
+ increment *= smallIncrement;
+ }
+
+ return increment;
+ },
+
+ /**
+ * Handle the input field's keyup event.
+ */
+ _onKeyup: function () {
+ this._applied = false;
+ },
+
+ /**
+ * Handle changes to the input text.
+ */
+ _onInput: function () {
+ // Validate the entered value.
+ this._doValidation();
+
+ // Update size if we're autosizing.
+ if (this._measurement) {
+ this._updateSize();
+ }
+
+ // Call the user's change handler if available.
+ if (this.change) {
+ this.change(this.currentInputValue);
+ }
+ },
+
+ /**
+ * Stop propagation on the provided event
+ */
+ _stopEventPropagation: function (e) {
+ e.stopPropagation();
+ },
+
+ /**
+ * Fire validation callback with current input
+ */
+ _doValidation: function () {
+ if (this.validate && this.input) {
+ this.validate(this.input.value);
+ }
+ },
+
+ /**
+ * Handles displaying suggestions based on the current input.
+ *
+ * @param {Boolean} autoInsert
+ * Pass true to automatically insert the most relevant suggestion.
+ */
+ _maybeSuggestCompletion: function (autoInsert) {
+ // Input can be null in cases when you intantaneously switch out of it.
+ if (!this.input) {
+ return;
+ }
+ let preTimeoutQuery = this.input.value;
+
+ // Since we are calling this method from a keypress event handler, the
+ // |input.value| does not include currently typed character. Thus we perform
+ // this method async.
+ this._openPopupTimeout = this.doc.defaultView.setTimeout(() => {
+ if (this._preventSuggestions) {
+ this._preventSuggestions = false;
+ return;
+ }
+ if (this.contentType == CONTENT_TYPES.PLAIN_TEXT) {
+ return;
+ }
+ if (!this.input) {
+ return;
+ }
+ let input = this.input;
+ // The length of input.value should be increased by 1
+ if (input.value.length - preTimeoutQuery.length > 1) {
+ return;
+ }
+ let query = input.value.slice(0, input.selectionStart);
+ let startCheckQuery = query;
+ if (query == null) {
+ return;
+ }
+ // If nothing is selected and there is a word (\w) character after the cursor, do
+ // not autocomplete.
+ if (input.selectionStart == input.selectionEnd &&
+ input.selectionStart < input.value.length) {
+ let nextChar = input.value.slice(input.selectionStart)[0];
+ // Check if the next character is a valid word character, no suggestion should be
+ // provided when preceeding a word.
+ if (/[\w-]/.test(nextChar)) {
+ // This emit is mainly to make the test flow simpler.
+ this.emit("after-suggest", "nothing to autocomplete");
+ return;
+ }
+ }
+ let list = [];
+ if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
+ list = this._getCSSPropertyList();
+ } else if (this.contentType == CONTENT_TYPES.CSS_VALUE) {
+ // Get the last query to be completed before the caret.
+ let match = /([^\s,.\/]+$)/.exec(query);
+ if (match) {
+ startCheckQuery = match[0];
+ } else {
+ startCheckQuery = "";
+ }
+
+ list =
+ ["!important",
+ ...this._getCSSValuesForPropertyName(this.property.name)];
+
+ if (query == "") {
+ // Do not suggest '!important' without any manually typed character.
+ list.splice(0, 1);
+ }
+ } else if (this.contentType == CONTENT_TYPES.CSS_MIXED &&
+ /^\s*style\s*=/.test(query)) {
+ // Check if the style attribute is closed before the selection.
+ let styleValue = query.replace(/^\s*style\s*=\s*/, "");
+ // Look for a quote matching the opening quote (single or double).
+ if (/^("[^"]*"|'[^']*')/.test(styleValue)) {
+ // This emit is mainly to make the test flow simpler.
+ this.emit("after-suggest", "nothing to autocomplete");
+ return;
+ }
+
+ // Detecting if cursor is at property or value;
+ let match = query.match(/([:;"'=]?)\s*([^"';:=]+)?$/);
+ if (match && match.length >= 2) {
+ if (match[1] == ":") {
+ // We are in CSS value completion
+ let propertyName =
+ query.match(/[;"'=]\s*([^"';:= ]+)\s*:\s*[^"';:=]*$/)[1];
+ list =
+ ["!important;",
+ ...this._getCSSValuesForPropertyName(propertyName)];
+ let matchLastQuery = /([^\s,.\/]+$)/.exec(match[2] || "");
+ if (matchLastQuery) {
+ startCheckQuery = matchLastQuery[0];
+ } else {
+ startCheckQuery = "";
+ }
+ if (!match[2]) {
+ // Don't suggest '!important' without any manually typed character
+ list.splice(0, 1);
+ }
+ } else if (match[1]) {
+ // We are in CSS property name completion
+ list = this._getCSSPropertyList();
+ startCheckQuery = match[2];
+ }
+ if (startCheckQuery == null) {
+ // This emit is mainly to make the test flow simpler.
+ this.emit("after-suggest", "nothing to autocomplete");
+ return;
+ }
+ }
+ }
+
+ if (!this.popup) {
+ // This emit is mainly to make the test flow simpler.
+ this.emit("after-suggest", "no popup");
+ return;
+ }
+
+ let finalList = [];
+ let length = list.length;
+ for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) {
+ if (startCheckQuery != null && list[i].startsWith(startCheckQuery)) {
+ count++;
+ finalList.push({
+ preLabel: startCheckQuery,
+ label: list[i]
+ });
+ } else if (count > 0) {
+ // Since count was incremented, we had already crossed the entries
+ // which would have started with query, assuming that list is sorted.
+ break;
+ } else if (startCheckQuery != null && list[i][0] > startCheckQuery[0]) {
+ // We have crossed all possible matches alphabetically.
+ break;
+ }
+ }
+
+ // Sort items starting with [a-z0-9] first, to make sure vendor-prefixed
+ // values and "!important" are suggested only after standard values.
+ finalList.sort((item1, item2) => {
+ // Get the expected alphabetical comparison between the items.
+ let comparison = item1.label.localeCompare(item2.label);
+ if (/^\w/.test(item1.label) != /^\w/.test(item2.label)) {
+ // One starts with [a-z0-9], one does not: flip the comparison.
+ comparison = -1 * comparison;
+ }
+ return comparison;
+ });
+
+ let index = 0;
+ if (startCheckQuery) {
+ // Only select a "best" suggestion when the user started a query.
+ let cssValues = finalList.map(item => item.label);
+ index = findMostRelevantCssPropertyIndex(cssValues);
+ }
+
+ // Insert the most relevant item from the final list as the input value.
+ if (autoInsert && finalList[index]) {
+ let item = finalList[index].label;
+ input.value = query + item.slice(startCheckQuery.length) +
+ input.value.slice(query.length);
+ input.setSelectionRange(query.length, query.length + item.length -
+ startCheckQuery.length);
+ this._updateSize();
+ }
+
+ // Display the list of suggestions if there are more than one.
+ if (finalList.length > 1) {
+ // Calculate the popup horizontal offset.
+ let indent = this.input.selectionStart - startCheckQuery.length;
+ let offset = indent * this.inputCharDimensions.width;
+ offset = this._isSingleLine() ? offset : 0;
+
+ // Select the most relevantItem if autoInsert is allowed
+ let selectedIndex = autoInsert ? index : -1;
+
+ // Open the suggestions popup.
+ this.popup.setItems(finalList);
+ this._openAutocompletePopup(offset, selectedIndex);
+ } else {
+ this._hideAutocompletePopup();
+ }
+ // This emit is mainly for the purpose of making the test flow simpler.
+ this.emit("after-suggest");
+ this._doValidation();
+ }, 0);
+ },
+
+ /**
+ * Check if the current input is displaying more than one line of text.
+ *
+ * @return {Boolean} true if the input has a single line of text
+ */
+ _isSingleLine: function () {
+ let inputRect = this.input.getBoundingClientRect();
+ return inputRect.height < 2 * this.inputCharDimensions.height;
+ },
+
+ /**
+ * Returns the list of CSS properties to use for the autocompletion. This
+ * method is overridden by tests in order to use mocked suggestion lists.
+ *
+ * @return {Array} array of CSS property names (Strings)
+ */
+ _getCSSPropertyList: function () {
+ return this.cssProperties.getNames().sort();
+ },
+
+ /**
+ * Returns a list of CSS values valid for a provided property name to use for
+ * the autocompletion. This method is overridden by tests in order to use
+ * mocked suggestion lists.
+ *
+ * @param {String} propertyName
+ * @return {Array} array of CSS property values (Strings)
+ */
+ _getCSSValuesForPropertyName: function (propertyName) {
+ return this.cssProperties.getValues(propertyName);
+ },
+};
+
+/**
+ * Copy text-related styles from one element to another.
+ */
+function copyTextStyles(from, to) {
+ let win = from.ownerDocument.defaultView;
+ let style = win.getComputedStyle(from);
+ let getCssText = name => style.getPropertyCSSValue(name).cssText;
+
+ to.style.fontFamily = getCssText("font-family");
+ to.style.fontSize = getCssText("font-size");
+ to.style.fontWeight = getCssText("font-weight");
+ to.style.fontStyle = getCssText("font-style");
+}
+
+/**
+ * Copy all styles which could have an impact on the element size.
+ */
+function copyAllStyles(from, to) {
+ let win = from.ownerDocument.defaultView;
+ let style = win.getComputedStyle(from);
+ let getCssText = name => style.getPropertyCSSValue(name).cssText;
+
+ copyTextStyles(from, to);
+ to.style.lineHeight = getCssText("line-height");
+
+ // If box-sizing is set to border-box, box model styles also need to be
+ // copied.
+ let boxSizing = getCssText("box-sizing");
+ if (boxSizing === "border-box") {
+ to.style.boxSizing = boxSizing;
+ copyBoxModelStyles(from, to);
+ }
+}
+
+/**
+ * Copy box model styles that can impact width and height measurements when box-
+ * sizing is set to "border-box" instead of "content-box".
+ *
+ * @param {DOMNode} from
+ * the element from which styles are copied
+ * @param {DOMNode} to
+ * the element on which copied styles are applied
+ */
+function copyBoxModelStyles(from, to) {
+ let win = from.ownerDocument.defaultView;
+ let style = win.getComputedStyle(from);
+ let getCssText = name => style.getPropertyCSSValue(name).cssText;
+
+ // Copy all paddings.
+ to.style.paddingTop = getCssText("padding-top");
+ to.style.paddingRight = getCssText("padding-right");
+ to.style.paddingBottom = getCssText("padding-bottom");
+ to.style.paddingLeft = getCssText("padding-left");
+
+ // Copy border styles.
+ to.style.borderTopStyle = getCssText("border-top-style");
+ to.style.borderRightStyle = getCssText("border-right-style");
+ to.style.borderBottomStyle = getCssText("border-bottom-style");
+ to.style.borderLeftStyle = getCssText("border-left-style");
+
+ // Copy border widths.
+ to.style.borderTopWidth = getCssText("border-top-width");
+ to.style.borderRightWidth = getCssText("border-right-width");
+ to.style.borderBottomWidth = getCssText("border-bottom-width");
+ to.style.borderLeftWidth = getCssText("border-left-width");
+}
+
+/**
+ * Trigger a focus change similar to pressing tab/shift-tab.
+ */
+function moveFocus(win, direction) {
+ return focusManager.moveFocus(win, null, direction, 0);
+}
diff --git a/devtools/client/shared/key-shortcuts.js b/devtools/client/shared/key-shortcuts.js
new file mode 100644
index 000000000..ec7d30bcb
--- /dev/null
+++ b/devtools/client/shared/key-shortcuts.js
@@ -0,0 +1,251 @@
+/* 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 Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const isOSX = Services.appinfo.OS === "Darwin";
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+// List of electron keys mapped to DOM API (DOM_VK_*) key code
+const ElectronKeysMapping = {
+ "F1": "DOM_VK_F1",
+ "F2": "DOM_VK_F2",
+ "F3": "DOM_VK_F3",
+ "F4": "DOM_VK_F4",
+ "F5": "DOM_VK_F5",
+ "F6": "DOM_VK_F6",
+ "F7": "DOM_VK_F7",
+ "F8": "DOM_VK_F8",
+ "F9": "DOM_VK_F9",
+ "F10": "DOM_VK_F10",
+ "F11": "DOM_VK_F11",
+ "F12": "DOM_VK_F12",
+ "F13": "DOM_VK_F13",
+ "F14": "DOM_VK_F14",
+ "F15": "DOM_VK_F15",
+ "F16": "DOM_VK_F16",
+ "F17": "DOM_VK_F17",
+ "F18": "DOM_VK_F18",
+ "F19": "DOM_VK_F19",
+ "F20": "DOM_VK_F20",
+ "F21": "DOM_VK_F21",
+ "F22": "DOM_VK_F22",
+ "F23": "DOM_VK_F23",
+ "F24": "DOM_VK_F24",
+ "Space": "DOM_VK_SPACE",
+ "Backspace": "DOM_VK_BACK_SPACE",
+ "Delete": "DOM_VK_DELETE",
+ "Insert": "DOM_VK_INSERT",
+ "Return": "DOM_VK_RETURN",
+ "Enter": "DOM_VK_RETURN",
+ "Up": "DOM_VK_UP",
+ "Down": "DOM_VK_DOWN",
+ "Left": "DOM_VK_LEFT",
+ "Right": "DOM_VK_RIGHT",
+ "Home": "DOM_VK_HOME",
+ "End": "DOM_VK_END",
+ "PageUp": "DOM_VK_PAGE_UP",
+ "PageDown": "DOM_VK_PAGE_DOWN",
+ "Escape": "DOM_VK_ESCAPE",
+ "Esc": "DOM_VK_ESCAPE",
+ "Tab": "DOM_VK_TAB",
+ "VolumeUp": "DOM_VK_VOLUME_UP",
+ "VolumeDown": "DOM_VK_VOLUME_DOWN",
+ "VolumeMute": "DOM_VK_VOLUME_MUTE",
+ "PrintScreen": "DOM_VK_PRINTSCREEN",
+};
+
+/**
+ * Helper to listen for keyboard events decribed in .properties file.
+ *
+ * let shortcuts = new KeyShortcuts({
+ * window
+ * });
+ * shortcuts.on("Ctrl+F", event => {
+ * // `event` is the KeyboardEvent which relates to the key shortcuts
+ * });
+ *
+ * @param DOMWindow window
+ * The window object of the document to listen events from.
+ * @param DOMElement target
+ * Optional DOM Element on which we should listen events from.
+ * If omitted, we listen for all events fired on `window`.
+ */
+function KeyShortcuts({ window, target }) {
+ this.window = window;
+ this.target = target || window;
+ this.keys = new Map();
+ this.eventEmitter = new EventEmitter();
+ this.target.addEventListener("keydown", this);
+}
+
+/*
+ * Parse an electron-like key string and return a normalized object which
+ * allow efficient match on DOM key event. The normalized object matches DOM
+ * API.
+ *
+ * @param DOMWindow window
+ * Any DOM Window object, just to fetch its `KeyboardEvent` object
+ * @param String str
+ * The shortcut string to parse, following this document:
+ * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
+ */
+KeyShortcuts.parseElectronKey = function (window, str) {
+ let modifiers = str.split("+");
+ let key = modifiers.pop();
+
+ let shortcut = {
+ ctrl: false,
+ meta: false,
+ alt: false,
+ shift: false,
+ // Set for character keys
+ key: undefined,
+ // Set for non-character keys
+ keyCode: undefined,
+ };
+ for (let mod of modifiers) {
+ if (mod === "Alt") {
+ shortcut.alt = true;
+ } else if (["Command", "Cmd"].includes(mod)) {
+ shortcut.meta = true;
+ } else if (["CommandOrControl", "CmdOrCtrl"].includes(mod)) {
+ if (isOSX) {
+ shortcut.meta = true;
+ } else {
+ shortcut.ctrl = true;
+ }
+ } else if (["Control", "Ctrl"].includes(mod)) {
+ shortcut.ctrl = true;
+ } else if (mod === "Shift") {
+ shortcut.shift = true;
+ } else {
+ console.error("Unsupported modifier:", mod, "from key:", str);
+ return null;
+ }
+ }
+
+ // Plus is a special case. It's a character key and shouldn't be matched
+ // against a keycode as it is only accessible via Shift/Capslock
+ if (key === "Plus") {
+ key = "+";
+ }
+
+ if (typeof key === "string" && key.length === 1) {
+ // Match any single character
+ shortcut.key = key.toLowerCase();
+ } else if (key in ElectronKeysMapping) {
+ // Maps the others manually to DOM API DOM_VK_*
+ key = ElectronKeysMapping[key];
+ shortcut.keyCode = KeyCodes[key];
+ // Used only to stringify the shortcut
+ shortcut.keyCodeString = key;
+ shortcut.key = key;
+ } else {
+ console.error("Unsupported key:", key);
+ return null;
+ }
+
+ return shortcut;
+};
+
+KeyShortcuts.stringify = function (shortcut) {
+ let list = [];
+ if (shortcut.alt) {
+ list.push("Alt");
+ }
+ if (shortcut.ctrl) {
+ list.push("Ctrl");
+ }
+ if (shortcut.meta) {
+ list.push("Cmd");
+ }
+ if (shortcut.shift) {
+ list.push("Shift");
+ }
+ let key;
+ if (shortcut.key) {
+ key = shortcut.key.toUpperCase();
+ } else {
+ key = shortcut.keyCodeString;
+ }
+ list.push(key);
+ return list.join("+");
+};
+
+KeyShortcuts.prototype = {
+ destroy() {
+ this.target.removeEventListener("keydown", this);
+ this.keys.clear();
+ },
+
+ doesEventMatchShortcut(event, shortcut) {
+ if (shortcut.meta != event.metaKey) {
+ return false;
+ }
+ if (shortcut.ctrl != event.ctrlKey) {
+ return false;
+ }
+ if (shortcut.alt != event.altKey) {
+ return false;
+ }
+ if (shortcut.shift != event.shiftKey) {
+ // Shift is a special modifier, it may implicitely be required if the expected key
+ // is a special character accessible via shift.
+ let isAlphabetical = event.key && event.key.match(/[a-zA-Z]/);
+ // OSX: distinguish cmd+[key] from cmd+shift+[key] shortcuts (Bug 1300458)
+ let cmdShortcut = shortcut.meta && !shortcut.alt && !shortcut.ctrl;
+ if (isAlphabetical || cmdShortcut) {
+ return false;
+ }
+ }
+
+ if (shortcut.keyCode) {
+ return event.keyCode == shortcut.keyCode;
+ } else if (event.key in ElectronKeysMapping) {
+ return ElectronKeysMapping[event.key] === shortcut.key;
+ }
+
+ // get the key from the keyCode if key is not provided.
+ let key = event.key || String.fromCharCode(event.keyCode);
+
+ // For character keys, we match if the final character is the expected one.
+ // But for digits we also accept indirect match to please azerty keyboard,
+ // which requires Shift to be pressed to get digits.
+ return key.toLowerCase() == shortcut.key ||
+ (shortcut.key.match(/[0-9]/) &&
+ event.keyCode == shortcut.key.charCodeAt(0));
+ },
+
+ handleEvent(event) {
+ for (let [key, shortcut] of this.keys) {
+ if (this.doesEventMatchShortcut(event, shortcut)) {
+ this.eventEmitter.emit(key, event);
+ }
+ }
+ },
+
+ on(key, listener) {
+ if (typeof listener !== "function") {
+ throw new Error("KeyShortcuts.on() expects a function as " +
+ "second argument");
+ }
+ if (!this.keys.has(key)) {
+ let shortcut = KeyShortcuts.parseElectronKey(this.window, key);
+ // The key string is wrong and we were unable to compute the key shortcut
+ if (!shortcut) {
+ return;
+ }
+ this.keys.set(key, shortcut);
+ }
+ this.eventEmitter.on(key, listener);
+ },
+
+ off(key, listener) {
+ this.eventEmitter.off(key, listener);
+ },
+};
+exports.KeyShortcuts = KeyShortcuts;
diff --git a/devtools/client/shared/keycodes.js b/devtools/client/shared/keycodes.js
new file mode 100644
index 000000000..fe4764dbe
--- /dev/null
+++ b/devtools/client/shared/keycodes.js
@@ -0,0 +1,146 @@
+/* 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";
+
+// This was copied (and slightly modified) from
+// devtools/shared/gcli/source/lib/gcli/util/util.js, which in turn
+// says:
+
+/**
+ * Keyboard handling is a mess. http://unixpapa.com/js/key.html
+ * It would be good to use DOM L3 Keyboard events,
+ * http://www.w3.org/TR/2010/WD-DOM-Level-3-Events-20100907/#events-keyboardevents
+ * however only Webkit supports them, and there isn't a shim on Modernizr:
+ * https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills
+ * and when the code that uses this KeyEvent was written, nothing was clear,
+ * so instead, we're using this unmodern shim:
+ * http://stackoverflow.com/questions/5681146/chrome-10-keyevent-or-something-similar-to-firefoxs-keyevent
+ * See BUG 664991: GCLI's keyboard handling should be updated to use DOM-L3
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=664991
+ */
+
+exports.KeyCodes = {
+ DOM_VK_CANCEL: 3,
+ DOM_VK_HELP: 6,
+ DOM_VK_BACK_SPACE: 8,
+ DOM_VK_TAB: 9,
+ DOM_VK_CLEAR: 12,
+ DOM_VK_RETURN: 13,
+ DOM_VK_SHIFT: 16,
+ DOM_VK_CONTROL: 17,
+ DOM_VK_ALT: 18,
+ DOM_VK_PAUSE: 19,
+ DOM_VK_CAPS_LOCK: 20,
+ DOM_VK_ESCAPE: 27,
+ DOM_VK_SPACE: 32,
+ DOM_VK_PAGE_UP: 33,
+ DOM_VK_PAGE_DOWN: 34,
+ DOM_VK_END: 35,
+ DOM_VK_HOME: 36,
+ DOM_VK_LEFT: 37,
+ DOM_VK_UP: 38,
+ DOM_VK_RIGHT: 39,
+ DOM_VK_DOWN: 40,
+ DOM_VK_PRINTSCREEN: 44,
+ DOM_VK_INSERT: 45,
+ DOM_VK_DELETE: 46,
+ DOM_VK_0: 48,
+ DOM_VK_1: 49,
+ DOM_VK_2: 50,
+ DOM_VK_3: 51,
+ DOM_VK_4: 52,
+ DOM_VK_5: 53,
+ DOM_VK_6: 54,
+ DOM_VK_7: 55,
+ DOM_VK_8: 56,
+ DOM_VK_9: 57,
+ DOM_VK_SEMICOLON: 59,
+ DOM_VK_EQUALS: 61,
+ DOM_VK_A: 65,
+ DOM_VK_B: 66,
+ DOM_VK_C: 67,
+ DOM_VK_D: 68,
+ DOM_VK_E: 69,
+ DOM_VK_F: 70,
+ DOM_VK_G: 71,
+ DOM_VK_H: 72,
+ DOM_VK_I: 73,
+ DOM_VK_J: 74,
+ DOM_VK_K: 75,
+ DOM_VK_L: 76,
+ DOM_VK_M: 77,
+ DOM_VK_N: 78,
+ DOM_VK_O: 79,
+ DOM_VK_P: 80,
+ DOM_VK_Q: 81,
+ DOM_VK_R: 82,
+ DOM_VK_S: 83,
+ DOM_VK_T: 84,
+ DOM_VK_U: 85,
+ DOM_VK_V: 86,
+ DOM_VK_W: 87,
+ DOM_VK_X: 88,
+ DOM_VK_Y: 89,
+ DOM_VK_Z: 90,
+ DOM_VK_CONTEXT_MENU: 93,
+ DOM_VK_NUMPAD0: 96,
+ DOM_VK_NUMPAD1: 97,
+ DOM_VK_NUMPAD2: 98,
+ DOM_VK_NUMPAD3: 99,
+ DOM_VK_NUMPAD4: 100,
+ DOM_VK_NUMPAD5: 101,
+ DOM_VK_NUMPAD6: 102,
+ DOM_VK_NUMPAD7: 103,
+ DOM_VK_NUMPAD8: 104,
+ DOM_VK_NUMPAD9: 105,
+ DOM_VK_MULTIPLY: 106,
+ DOM_VK_ADD: 107,
+ DOM_VK_SEPARATOR: 108,
+ DOM_VK_SUBTRACT: 109,
+ DOM_VK_DECIMAL: 110,
+ DOM_VK_DIVIDE: 111,
+ DOM_VK_F1: 112,
+ DOM_VK_F2: 113,
+ DOM_VK_F3: 114,
+ DOM_VK_F4: 115,
+ DOM_VK_F5: 116,
+ DOM_VK_F6: 117,
+ DOM_VK_F7: 118,
+ DOM_VK_F8: 119,
+ DOM_VK_F9: 120,
+ DOM_VK_F10: 121,
+ DOM_VK_F11: 122,
+ DOM_VK_F12: 123,
+ DOM_VK_F13: 124,
+ DOM_VK_F14: 125,
+ DOM_VK_F15: 126,
+ DOM_VK_F16: 127,
+ DOM_VK_F17: 128,
+ DOM_VK_F18: 129,
+ DOM_VK_F19: 130,
+ DOM_VK_F20: 131,
+ DOM_VK_F21: 132,
+ DOM_VK_F22: 133,
+ DOM_VK_F23: 134,
+ DOM_VK_F24: 135,
+ DOM_VK_NUM_LOCK: 144,
+ DOM_VK_SCROLL_LOCK: 145,
+ DOM_VK_COMMA: 188,
+ DOM_VK_PERIOD: 190,
+ DOM_VK_SLASH: 191,
+ DOM_VK_BACK_QUOTE: 192,
+ DOM_VK_OPEN_BRACKET: 219,
+ DOM_VK_BACK_SLASH: 220,
+ DOM_VK_CLOSE_BRACKET: 221,
+ DOM_VK_QUOTE: 222,
+ DOM_VK_META: 224,
+
+ // A few that did not appear in gcli, but that are apparently used
+ // in devtools.
+ DOM_VK_COLON: 58,
+ DOM_VK_VOLUME_MUTE: 181,
+ DOM_VK_VOLUME_DOWN: 182,
+ DOM_VK_VOLUME_UP: 183,
+};
diff --git a/devtools/client/shared/moz.build b/devtools/client/shared/moz.build
new file mode 100644
index 000000000..1c61970c0
--- /dev/null
+++ b/devtools/client/shared/moz.build
@@ -0,0 +1,54 @@
+# -*- 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+
+DIRS += [
+ 'components',
+ 'redux',
+ 'shim',
+ 'vendor',
+ 'widgets',
+]
+
+DevToolsModules(
+ 'AppCacheUtils.jsm',
+ 'autocomplete-popup.js',
+ 'browser-loader.js',
+ 'css-angle.js',
+ 'css-reload.js',
+ 'curl.js',
+ 'demangle.js',
+ 'developer-toolbar.js',
+ 'devices.js',
+ 'devtools-file-watcher.js',
+ 'DOMHelpers.jsm',
+ 'doorhanger.js',
+ 'file-watcher-worker.js',
+ 'file-watcher.js',
+ 'getjson.js',
+ 'inplace-editor.js',
+ 'Jsbeautify.jsm',
+ 'key-shortcuts.js',
+ 'keycodes.js',
+ 'network-throttling-profiles.js',
+ 'node-attribute-parser.js',
+ 'options-view.js',
+ 'output-parser.js',
+ 'poller.js',
+ 'prefs.js',
+ 'scroll.js',
+ 'source-utils.js',
+ 'SplitView.jsm',
+ 'suggestion-picker.js',
+ 'telemetry.js',
+ 'theme.js',
+ 'undo.js',
+ 'view-source.js',
+ 'webgl-utils.js',
+ 'zoom-keys.js',
+)
diff --git a/devtools/client/shared/network-throttling-profiles.js b/devtools/client/shared/network-throttling-profiles.js
new file mode 100644
index 000000000..ef139fda6
--- /dev/null
+++ b/devtools/client/shared/network-throttling-profiles.js
@@ -0,0 +1,68 @@
+/* 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 K = 1024;
+const M = 1024 * 1024;
+const Bps = 1 / 8;
+const KBps = K * Bps;
+const MBps = M * Bps;
+
+/**
+ * Predefined network throttling profiles.
+ * Speeds are in bytes per second. Latency is in ms.
+ */
+/* eslint-disable key-spacing */
+module.exports = [
+ {
+ id: "GPRS",
+ download: 50 * KBps,
+ upload: 20 * KBps,
+ latency: 500,
+ },
+ {
+ id: "Regular 2G",
+ download: 250 * KBps,
+ upload: 50 * KBps,
+ latency: 300,
+ },
+ {
+ id: "Good 2G",
+ download: 450 * KBps,
+ upload: 150 * KBps,
+ latency: 150,
+ },
+ {
+ id: "Regular 3G",
+ download: 750 * KBps,
+ upload: 250 * KBps,
+ latency: 100,
+ },
+ {
+ id: "Good 3G",
+ download: 1.5 * MBps,
+ upload: 750 * KBps,
+ latency: 40,
+ },
+ {
+ id: "Regular 4G / LTE",
+ download: 4 * MBps,
+ upload: 3 * MBps,
+ latency: 20,
+ },
+ {
+ id: "DSL",
+ download: 2 * MBps,
+ upload: 1 * MBps,
+ latency: 5,
+ },
+ {
+ id: "Wi-Fi",
+ download: 30 * MBps,
+ upload: 15 * MBps,
+ latency: 2,
+ },
+];
+/* eslint-enable key-spacing */
diff --git a/devtools/client/shared/node-attribute-parser.js b/devtools/client/shared/node-attribute-parser.js
new file mode 100644
index 000000000..aaf866fca
--- /dev/null
+++ b/devtools/client/shared/node-attribute-parser.js
@@ -0,0 +1,294 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module contains a small element attribute value parser. It's primary
+ * goal is to extract link information from attribute values (like the href in
+ * <a href="/some/link.html"> for example).
+ *
+ * There are several types of linkable attribute values:
+ * - TYPE_URI: a URI (e.g. <a href="uri">).
+ * - TYPE_URI_LIST: a space separated list of URIs (e.g. <a ping="uri1 uri2">).
+ * - TYPE_IDREF: a reference to an other element in the same document via its id
+ * (e.g. <label for="input-id"> or <key command="command-id">).
+ * - TYPE_IDREF_LIST: a space separated list of IDREFs (e.g.
+ * <output for="id1 id2">).
+ * - TYPE_JS_RESOURCE_URI: a URI to a javascript resource that can be opened in
+ * the devtools (e.g. <script src="uri">).
+ * - TYPE_CSS_RESOURCE_URI: a URI to a css resource that can be opened in the
+ * devtools (e.g. <link href="uri">).
+ *
+ * parseAttribute is the parser entry function, exported on this module.
+ */
+
+const TYPE_STRING = "string";
+const TYPE_URI = "uri";
+const TYPE_URI_LIST = "uriList";
+const TYPE_IDREF = "idref";
+const TYPE_IDREF_LIST = "idrefList";
+const TYPE_JS_RESOURCE_URI = "jsresource";
+const TYPE_CSS_RESOURCE_URI = "cssresource";
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+/* eslint-disable max-len */
+const ATTRIBUTE_TYPES = [
+ {namespaceURI: HTML_NS, attributeName: "action", tagName: "form", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "background", tagName: "body", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "cite", tagName: "blockquote", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "cite", tagName: "q", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "cite", tagName: "del", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "cite", tagName: "ins", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "classid", tagName: "object", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "codebase", tagName: "object", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "codebase", tagName: "applet", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "command", tagName: "menuitem", type: TYPE_IDREF},
+ {namespaceURI: "*", attributeName: "contextmenu", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "data", tagName: "object", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "for", tagName: "label", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "for", tagName: "output", type: TYPE_IDREF_LIST},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "button", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "fieldset", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "input", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "keygen", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "label", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "object", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "output", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "select", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "form", tagName: "textarea", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "formaction", tagName: "button", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "formaction", tagName: "input", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "headers", tagName: "td", type: TYPE_IDREF_LIST},
+ {namespaceURI: HTML_NS, attributeName: "headers", tagName: "th", type: TYPE_IDREF_LIST},
+ {namespaceURI: HTML_NS, attributeName: "href", tagName: "a", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "href", tagName: "area", type: TYPE_URI},
+ {namespaceURI: "*", attributeName: "href", tagName: "link", type: TYPE_CSS_RESOURCE_URI,
+ /* eslint-enable */
+ isValid: (namespaceURI, tagName, attributes) => {
+ return getAttribute(attributes, "rel") === "stylesheet";
+ }},
+ /* eslint-disable max-len */
+ {namespaceURI: "*", attributeName: "href", tagName: "link", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "href", tagName: "base", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "icon", tagName: "menuitem", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "list", tagName: "input", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "img", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "frame", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "iframe", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "manifest", tagName: "html", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "menu", tagName: "button", type: TYPE_IDREF},
+ {namespaceURI: HTML_NS, attributeName: "ping", tagName: "a", type: TYPE_URI_LIST},
+ {namespaceURI: HTML_NS, attributeName: "ping", tagName: "area", type: TYPE_URI_LIST},
+ {namespaceURI: HTML_NS, attributeName: "poster", tagName: "video", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "profile", tagName: "head", type: TYPE_URI},
+ {namespaceURI: "*", attributeName: "src", tagName: "script", type: TYPE_JS_RESOURCE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "input", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "frame", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "iframe", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "img", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "audio", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "embed", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "source", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "track", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "src", tagName: "video", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "usemap", tagName: "img", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "usemap", tagName: "input", type: TYPE_URI},
+ {namespaceURI: HTML_NS, attributeName: "usemap", tagName: "object", type: TYPE_URI},
+ {namespaceURI: "*", attributeName: "xmlns", tagName: "*", type: TYPE_URI},
+ {namespaceURI: XUL_NS, attributeName: "command", tagName: "key", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "containment", tagName: "*", type: TYPE_URI},
+ {namespaceURI: XUL_NS, attributeName: "context", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "datasources", tagName: "*", type: TYPE_URI_LIST},
+ {namespaceURI: XUL_NS, attributeName: "insertafter", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "insertbefore", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "menu", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "observes", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "popup", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "ref", tagName: "*", type: TYPE_URI},
+ {namespaceURI: XUL_NS, attributeName: "removeelement", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "sortResource", tagName: "*", type: TYPE_URI},
+ {namespaceURI: XUL_NS, attributeName: "sortResource2", tagName: "*", type: TYPE_URI},
+ {namespaceURI: XUL_NS, attributeName: "src", tagName: "stringbundle", type: TYPE_URI},
+ {namespaceURI: XUL_NS, attributeName: "template", tagName: "*", type: TYPE_IDREF},
+ {namespaceURI: XUL_NS, attributeName: "tooltip", tagName: "*", type: TYPE_IDREF},
+ /* eslint-enable */
+ // SVG links aren't handled yet, see bug 1158831.
+ // {namespaceURI: SVG_NS, attributeName: "fill", tagName: "*", type: },
+ // {namespaceURI: SVG_NS, attributeName: "stroke", tagName: "*", type: },
+ // {namespaceURI: SVG_NS, attributeName: "markerstart", tagName: "*", type: },
+ // {namespaceURI: SVG_NS, attributeName: "markermid", tagName: "*", type: },
+ // {namespaceURI: SVG_NS, attributeName: "markerend", tagName: "*", type: },
+ // {namespaceURI: SVG_NS, attributeName: "xlink:href", tagName: "*", type: }
+];
+
+var parsers = {
+ [TYPE_URI]: function (attributeValue) {
+ return [{
+ type: TYPE_URI,
+ value: attributeValue
+ }];
+ },
+ [TYPE_URI_LIST]: function (attributeValue) {
+ let data = splitBy(attributeValue, " ");
+ for (let token of data) {
+ if (!token.type) {
+ token.type = TYPE_URI;
+ }
+ }
+ return data;
+ },
+ [TYPE_JS_RESOURCE_URI]: function (attributeValue) {
+ return [{
+ type: TYPE_JS_RESOURCE_URI,
+ value: attributeValue
+ }];
+ },
+ [TYPE_CSS_RESOURCE_URI]: function (attributeValue) {
+ return [{
+ type: TYPE_CSS_RESOURCE_URI,
+ value: attributeValue
+ }];
+ },
+ [TYPE_IDREF]: function (attributeValue) {
+ return [{
+ type: TYPE_IDREF,
+ value: attributeValue
+ }];
+ },
+ [TYPE_IDREF_LIST]: function (attributeValue) {
+ let data = splitBy(attributeValue, " ");
+ for (let token of data) {
+ if (!token.type) {
+ token.type = TYPE_IDREF;
+ }
+ }
+ return data;
+ }
+};
+
+/**
+ * Parse an attribute value.
+ * @param {String} namespaceURI The namespaceURI of the node that has the
+ * attribute.
+ * @param {String} tagName The tagName of the node that has the attribute.
+ * @param {Array} attributes The list of all attributes of the node. This should
+ * be an array of {name, value} objects.
+ * @param {String} attributeName The name of the attribute to parse.
+ * @return {Array} An array of tokens that represents the value. Each token is
+ * an object {type: [string|uri|jsresource|cssresource|idref], value}.
+ * For instance parsing the ping attribute in <a ping="uri1 uri2"> returns:
+ * [
+ * {type: "uri", value: "uri2"},
+ * {type: "string", value: " "},
+ * {type: "uri", value: "uri1"}
+ * ]
+ */
+function parseAttribute(namespaceURI, tagName, attributes, attributeName) {
+ if (!hasAttribute(attributes, attributeName)) {
+ throw new Error(`Attribute ${attributeName} isn't part of the ` +
+ "provided attributes");
+ }
+
+ let type = getType(namespaceURI, tagName, attributes, attributeName);
+ if (!type) {
+ return [{
+ type: TYPE_STRING,
+ value: getAttribute(attributes, attributeName)
+ }];
+ }
+
+ return parsers[type](getAttribute(attributes, attributeName));
+}
+
+/**
+ * Get the type for links in this attribute if any.
+ * @param {String} namespaceURI The node's namespaceURI.
+ * @param {String} tagName The node's tagName.
+ * @param {Array} attributes The node's attributes, as a list of {name, value}
+ * objects.
+ * @param {String} attributeName The name of the attribute to get the type for.
+ * @return {Object} null if no type exist for this attribute on this node, the
+ * type object otherwise.
+ */
+function getType(namespaceURI, tagName, attributes, attributeName) {
+ for (let typeData of ATTRIBUTE_TYPES) {
+ let containsAttribute = attributeName === typeData.attributeName ||
+ typeData.attributeName === "*";
+ let hasNamespace = namespaceURI === typeData.namespaceURI ||
+ typeData.namespaceURI === "*";
+ let hasTagName = tagName.toLowerCase() === typeData.tagName ||
+ typeData.tagName === "*";
+ let isValid = typeData.isValid
+ ? typeData.isValid(namespaceURI,
+ tagName,
+ attributes,
+ attributeName)
+ : true;
+
+ if (containsAttribute && hasNamespace && hasTagName && isValid) {
+ return typeData.type;
+ }
+ }
+
+ return null;
+}
+
+function getAttribute(attributes, attributeName) {
+ for (let {name, value} of attributes) {
+ if (name === attributeName) {
+ return value;
+ }
+ }
+ return null;
+}
+
+function hasAttribute(attributes, attributeName) {
+ for (let {name} of attributes) {
+ if (name === attributeName) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Split a string by a given character and return an array of objects parts.
+ * The array will contain objects for the split character too, marked with
+ * TYPE_STRING type.
+ * @param {String} value The string to parse.
+ * @param {String} splitChar A 1 length split character.
+ * @return {Array}
+ */
+function splitBy(value, splitChar) {
+ let data = [], i = 0, buffer = "";
+ while (i <= value.length) {
+ if (i === value.length && buffer) {
+ data.push({value: buffer});
+ }
+ if (value[i] === splitChar) {
+ if (buffer) {
+ data.push({value: buffer});
+ }
+ data.push({
+ type: TYPE_STRING,
+ value: splitChar
+ });
+ buffer = "";
+ } else {
+ buffer += value[i];
+ }
+
+ i++;
+ }
+ return data;
+}
+
+exports.parseAttribute = parseAttribute;
+// Exported for testing only.
+exports.splitBy = splitBy;
diff --git a/devtools/client/shared/options-view.js b/devtools/client/shared/options-view.js
new file mode 100644
index 000000000..bb583eaee
--- /dev/null
+++ b/devtools/client/shared/options-view.js
@@ -0,0 +1,186 @@
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const Services = require("Services");
+const { Preferences } = require("resource://gre/modules/Preferences.jsm");
+const OPTIONS_SHOWN_EVENT = "options-shown";
+const OPTIONS_HIDDEN_EVENT = "options-hidden";
+const PREF_CHANGE_EVENT = "pref-changed";
+
+/**
+ * OptionsView constructor. Takes several options, all required:
+ * - branchName: The name of the prefs branch, like "devtools.debugger."
+ * - menupopup: The XUL `menupopup` item that contains the pref buttons.
+ *
+ * Fires an event, PREF_CHANGE_EVENT, with the preference name that changed as
+ * the second argument. Fires events on opening/closing the XUL panel
+ * (OPTIONS_SHOW_EVENT, OPTIONS_HIDDEN_EVENT) as the second argument in the
+ * listener, used for tests mostly.
+ */
+const OptionsView = function (options = {}) {
+ this.branchName = options.branchName;
+ this.menupopup = options.menupopup;
+ this.window = this.menupopup.ownerDocument.defaultView;
+ let { document } = this.window;
+ this.$ = document.querySelector.bind(document);
+ this.$$ = (selector, parent = document) => parent.querySelectorAll(selector);
+ // Get the corresponding button that opens the popup by looking
+ // for an element with a `popup` attribute matching the menu's ID
+ this.button = this.$(`[popup=${this.menupopup.getAttribute("id")}]`);
+
+ this.prefObserver = new PrefObserver(this.branchName);
+
+ EventEmitter.decorate(this);
+};
+exports.OptionsView = OptionsView;
+
+OptionsView.prototype = {
+ /**
+ * Binds the events and observers for the OptionsView.
+ */
+ initialize: function () {
+ let { MutationObserver } = this.window;
+ this._onPrefChange = this._onPrefChange.bind(this);
+ this._onOptionChange = this._onOptionChange.bind(this);
+ this._onPopupShown = this._onPopupShown.bind(this);
+ this._onPopupHidden = this._onPopupHidden.bind(this);
+
+ // We use a mutation observer instead of a click handler
+ // because the click handler is fired before the XUL menuitem updates its
+ // checked status, which cascades incorrectly with the Preference observer.
+ this.mutationObserver = new MutationObserver(this._onOptionChange);
+ let observerConfig = { attributes: true, attributeFilter: ["checked"]};
+
+ // Sets observers and default options for all options
+ for (let $el of this.$$("menuitem", this.menupopup)) {
+ let prefName = $el.getAttribute("data-pref");
+
+ if (this.prefObserver.get(prefName)) {
+ $el.setAttribute("checked", "true");
+ } else {
+ $el.removeAttribute("checked");
+ }
+ this.mutationObserver.observe($el, observerConfig);
+ }
+
+ // Listen to any preference change in the specified branch
+ this.prefObserver.register();
+ this.prefObserver.on(PREF_CHANGE_EVENT, this._onPrefChange);
+
+ // Bind to menupopup's open and close event
+ this.menupopup.addEventListener("popupshown", this._onPopupShown);
+ this.menupopup.addEventListener("popuphidden", this._onPopupHidden);
+ },
+
+ /**
+ * Removes event handlers for all of the option buttons and
+ * preference observer.
+ */
+ destroy: function () {
+ this.mutationObserver.disconnect();
+ this.prefObserver.off(PREF_CHANGE_EVENT, this._onPrefChange);
+ this.menupopup.removeEventListener("popupshown", this._onPopupShown);
+ this.menupopup.removeEventListener("popuphidden", this._onPopupHidden);
+ },
+
+ /**
+ * Returns the value for the specified `prefName`
+ */
+ getPref: function (prefName) {
+ return this.prefObserver.get(prefName);
+ },
+
+ /**
+ * Called when a preference is changed (either via clicking an option
+ * button or by changing it in about:config). Updates the checked status
+ * of the corresponding button.
+ */
+ _onPrefChange: function (_, prefName) {
+ let $el = this.$(`menuitem[data-pref="${prefName}"]`, this.menupopup);
+ let value = this.prefObserver.get(prefName);
+
+ // If options panel does not contain a menuitem for the
+ // pref, emit an event and do nothing.
+ if (!$el) {
+ this.emit(PREF_CHANGE_EVENT, prefName);
+ return;
+ }
+
+ if (value) {
+ $el.setAttribute("checked", value);
+ } else {
+ $el.removeAttribute("checked");
+ }
+
+ this.emit(PREF_CHANGE_EVENT, prefName);
+ },
+
+ /**
+ * Mutation handler for handling a change on an options button.
+ * Sets the preference accordingly.
+ */
+ _onOptionChange: function (mutations) {
+ let { target } = mutations[0];
+ let prefName = target.getAttribute("data-pref");
+ let value = target.getAttribute("checked") === "true";
+
+ this.prefObserver.set(prefName, value);
+ },
+
+ /**
+ * Fired when the `menupopup` is opened, bound via XUL.
+ * Fires an event used in tests.
+ */
+ _onPopupShown: function () {
+ this.button.setAttribute("open", true);
+ this.emit(OPTIONS_SHOWN_EVENT);
+ },
+
+ /**
+ * Fired when the `menupopup` is closed, bound via XUL.
+ * Fires an event used in tests.
+ */
+ _onPopupHidden: function () {
+ this.button.removeAttribute("open");
+ this.emit(OPTIONS_HIDDEN_EVENT);
+ }
+};
+
+/**
+ * Constructor for PrefObserver. Small helper for observing changes
+ * on a preference branch. Takes a `branchName`, like "devtools.debugger."
+ *
+ * Fires an event of PREF_CHANGE_EVENT with the preference name that changed
+ * as the second argument in the listener.
+ */
+const PrefObserver = function (branchName) {
+ this.branchName = branchName;
+ this.branch = Services.prefs.getBranch(branchName);
+ EventEmitter.decorate(this);
+};
+
+PrefObserver.prototype = {
+ /**
+ * Returns `prefName`'s value. Does not require the branch name.
+ */
+ get: function (prefName) {
+ let fullName = this.branchName + prefName;
+ return Preferences.get(fullName);
+ },
+ /**
+ * Sets `prefName`'s `value`. Does not require the branch name.
+ */
+ set: function (prefName, value) {
+ let fullName = this.branchName + prefName;
+ Preferences.set(fullName, value);
+ },
+ register: function () {
+ this.branch.addObserver("", this, false);
+ },
+ unregister: function () {
+ this.branch.removeObserver("", this);
+ },
+ observe: function (subject, topic, prefName) {
+ this.emit(PREF_CHANGE_EVENT, prefName);
+ }
+};
diff --git a/devtools/client/shared/output-parser.js b/devtools/client/shared/output-parser.js
new file mode 100644
index 000000000..726c93b8b
--- /dev/null
+++ b/devtools/client/shared/output-parser.js
@@ -0,0 +1,695 @@
+/* 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 {angleUtils} = require("devtools/client/shared/css-angle");
+const {colorUtils} = require("devtools/shared/css/color");
+const {getCSSLexer} = require("devtools/shared/css/lexer");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {
+ ANGLE_TAKING_FUNCTIONS,
+ BEZIER_KEYWORDS,
+ COLOR_TAKING_FUNCTIONS,
+ CSS_TYPES
+} = require("devtools/shared/css/properties-db");
+const Services = require("Services");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
+
+/**
+ * This module is used to process text for output by developer tools. This means
+ * linking JS files with the debugger, CSS files with the style editor, JS
+ * functions with the debugger, placing color swatches next to colors and
+ * adding doorhanger previews where possible (images, angles, lengths,
+ * border radius, cubic-bezier etc.).
+ *
+ * Usage:
+ * const {OutputParser} = require("devtools/client/shared/output-parser");
+ *
+ * let parser = new OutputParser(document, supportsType);
+ *
+ * parser.parseCssProperty("color", "red"); // Returns document fragment.
+ *
+ * @param {Document} document Used to create DOM nodes.
+ * @param {Function} supportsTypes - A function that returns a boolean when asked if a css
+ * property name supports a given css type.
+ * The function is executed like supportsType("color", CSS_TYPES.COLOR)
+ * where CSS_TYPES is defined in devtools/shared/css/properties-db.js
+ * @param {Function} isValidOnClient - A function that checks if a css property
+ * name/value combo is valid.
+ */
+function OutputParser(document, {supportsType, isValidOnClient}) {
+ this.parsed = [];
+ this.doc = document;
+ this.supportsType = supportsType;
+ this.isValidOnClient = isValidOnClient;
+ this.colorSwatches = new WeakMap();
+ this.angleSwatches = new WeakMap();
+ this._onColorSwatchMouseDown = this._onColorSwatchMouseDown.bind(this);
+ this._onAngleSwatchMouseDown = this._onAngleSwatchMouseDown.bind(this);
+}
+
+exports.OutputParser = OutputParser;
+
+OutputParser.prototype = {
+ /**
+ * Parse a CSS property value given a property name.
+ *
+ * @param {String} name
+ * CSS Property Name
+ * @param {String} value
+ * CSS Property value
+ * @param {Object} [options]
+ * Options object. For valid options and default values see
+ * _mergeOptions().
+ * @return {DocumentFragment}
+ * A document fragment containing color swatches etc.
+ */
+ parseCssProperty: function (name, value, options = {}) {
+ options = this._mergeOptions(options);
+
+ options.expectCubicBezier = this.supportsType(name, CSS_TYPES.TIMING_FUNCTION);
+ options.expectDisplay = name === "display";
+ options.expectFilter = name === "filter";
+ options.supportsColor = this.supportsType(name, CSS_TYPES.COLOR) ||
+ this.supportsType(name, CSS_TYPES.GRADIENT);
+
+ // The filter property is special in that we want to show the
+ // swatch even if the value is invalid, because this way the user
+ // can easily use the editor to fix it.
+ if (options.expectFilter || this._cssPropertySupportsValue(name, value)) {
+ return this._parse(value, options);
+ }
+ this._appendTextNode(value);
+
+ return this._toDOM();
+ },
+
+ /**
+ * Given an initial FUNCTION token, read tokens from |tokenStream|
+ * and collect all the (non-comment) text. Return the collected
+ * text. The function token and the close paren are included in the
+ * result.
+ *
+ * @param {CSSToken} initialToken
+ * The FUNCTION token.
+ * @param {String} text
+ * The original CSS text.
+ * @param {CSSLexer} tokenStream
+ * The token stream from which to read.
+ * @return {String}
+ * The text of body of the function call.
+ */
+ _collectFunctionText: function (initialToken, text, tokenStream) {
+ let result = text.substring(initialToken.startOffset,
+ initialToken.endOffset);
+ let depth = 1;
+ while (depth > 0) {
+ let token = tokenStream.nextToken();
+ if (!token) {
+ break;
+ }
+ if (token.tokenType === "comment") {
+ continue;
+ }
+ result += text.substring(token.startOffset, token.endOffset);
+ if (token.tokenType === "symbol") {
+ if (token.text === "(") {
+ ++depth;
+ } else if (token.text === ")") {
+ --depth;
+ }
+ } else if (token.tokenType === "function") {
+ ++depth;
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Parse a string.
+ *
+ * @param {String} text
+ * Text to parse.
+ * @param {Object} [options]
+ * Options object. For valid options and default values see
+ * _mergeOptions().
+ * @return {DocumentFragment}
+ * A document fragment.
+ */
+ _parse: function (text, options = {}) {
+ text = text.trim();
+ this.parsed.length = 0;
+
+ let tokenStream = getCSSLexer(text);
+ let parenDepth = 0;
+ let outerMostFunctionTakesColor = false;
+
+ let colorOK = function () {
+ return options.supportsColor ||
+ (options.expectFilter && parenDepth === 1 &&
+ outerMostFunctionTakesColor);
+ };
+
+ let angleOK = function (angle) {
+ return (new angleUtils.CssAngle(angle)).valid;
+ };
+
+ while (true) {
+ let token = tokenStream.nextToken();
+ if (!token) {
+ break;
+ }
+ if (token.tokenType === "comment") {
+ continue;
+ }
+
+ switch (token.tokenType) {
+ case "function": {
+ if (COLOR_TAKING_FUNCTIONS.includes(token.text) ||
+ ANGLE_TAKING_FUNCTIONS.includes(token.text)) {
+ // The function can accept a color or an angle argument, and we know
+ // it isn't special in some other way. So, we let it
+ // through to the ordinary parsing loop so that the value
+ // can be handled in a single place.
+ this._appendTextNode(text.substring(token.startOffset,
+ token.endOffset));
+ if (parenDepth === 0) {
+ outerMostFunctionTakesColor = COLOR_TAKING_FUNCTIONS.includes(
+ token.text);
+ }
+ ++parenDepth;
+ } else {
+ let functionText = this._collectFunctionText(token, text,
+ tokenStream);
+
+ if (options.expectCubicBezier && token.text === "cubic-bezier") {
+ this._appendCubicBezier(functionText, options);
+ } else if (colorOK() && colorUtils.isValidCSSColor(functionText)) {
+ this._appendColor(functionText, options);
+ } else {
+ this._appendTextNode(functionText);
+ }
+ }
+ break;
+ }
+
+ case "ident":
+ if (options.expectCubicBezier &&
+ BEZIER_KEYWORDS.indexOf(token.text) >= 0) {
+ this._appendCubicBezier(token.text, options);
+ } else if (Services.prefs.getBoolPref(CSS_GRID_ENABLED_PREF) &&
+ options.expectDisplay && token.text === "grid" &&
+ text === token.text) {
+ this._appendGrid(token.text, options);
+ } else if (colorOK() && colorUtils.isValidCSSColor(token.text)) {
+ this._appendColor(token.text, options);
+ } else if (angleOK(token.text)) {
+ this._appendAngle(token.text, options);
+ } else {
+ this._appendTextNode(text.substring(token.startOffset,
+ token.endOffset));
+ }
+ break;
+
+ case "id":
+ case "hash": {
+ let original = text.substring(token.startOffset, token.endOffset);
+ if (colorOK() && colorUtils.isValidCSSColor(original)) {
+ this._appendColor(original, options);
+ } else {
+ this._appendTextNode(original);
+ }
+ break;
+ }
+ case "dimension":
+ let value = text.substring(token.startOffset, token.endOffset);
+ if (angleOK(value)) {
+ this._appendAngle(value, options);
+ } else {
+ this._appendTextNode(value);
+ }
+ break;
+ case "url":
+ case "bad_url":
+ this._appendURL(text.substring(token.startOffset, token.endOffset),
+ token.text, options);
+ break;
+
+ case "symbol":
+ if (token.text === "(") {
+ ++parenDepth;
+ } else if (token.text === ")") {
+ --parenDepth;
+ if (parenDepth === 0) {
+ outerMostFunctionTakesColor = false;
+ }
+ }
+ // falls through
+ default:
+ this._appendTextNode(
+ text.substring(token.startOffset, token.endOffset));
+ break;
+ }
+ }
+
+ let result = this._toDOM();
+
+ if (options.expectFilter && !options.filterSwatch) {
+ result = this._wrapFilter(text, options, result);
+ }
+
+ return result;
+ },
+
+ /**
+ * Append a cubic-bezier timing function value to the output
+ *
+ * @param {String} bezier
+ * The cubic-bezier timing function
+ * @param {Object} options
+ * Options object. For valid options and default values see
+ * _mergeOptions()
+ */
+ _appendCubicBezier: function (bezier, options) {
+ let container = this._createNode("span", {
+ "data-bezier": bezier
+ });
+
+ if (options.bezierSwatchClass) {
+ let swatch = this._createNode("span", {
+ class: options.bezierSwatchClass
+ });
+ container.appendChild(swatch);
+ }
+
+ let value = this._createNode("span", {
+ class: options.bezierClass
+ }, bezier);
+
+ container.appendChild(value);
+ this.parsed.push(container);
+ },
+
+ /**
+ * Append a CSS Grid highlighter toggle icon next to the value in a
+ * 'display: grid' declaration
+ *
+ * @param {String} grid
+ * The grid text value to append
+ * @param {Object} options
+ * Options object. For valid options and default values see
+ * _mergeOptions()
+ */
+ _appendGrid: function (grid, options) {
+ let container = this._createNode("span", {});
+
+ let toggle = this._createNode("span", {
+ class: options.gridClass
+ });
+
+ let value = this._createNode("span", {});
+ value.textContent = grid;
+
+ container.appendChild(toggle);
+ container.appendChild(value);
+ this.parsed.push(container);
+ },
+
+ /**
+ * Append a angle value to the output
+ *
+ * @param {String} angle
+ * angle to append
+ * @param {Object} options
+ * Options object. For valid options and default values see
+ * _mergeOptions()
+ */
+ _appendAngle: function (angle, options) {
+ let angleObj = new angleUtils.CssAngle(angle);
+ let container = this._createNode("span", {
+ "data-angle": angle
+ });
+
+ if (options.angleSwatchClass) {
+ let swatch = this._createNode("span", {
+ class: options.angleSwatchClass
+ });
+ this.angleSwatches.set(swatch, angleObj);
+ swatch.addEventListener("mousedown", this._onAngleSwatchMouseDown, false);
+
+ // Add click listener to stop event propagation when shift key is pressed
+ // in order to prevent the value input to be focused.
+ // Bug 711942 will add a tooltip to edit angle values and we should
+ // be able to move this listener to Tooltip.js when it'll be implemented.
+ swatch.addEventListener("click", function (event) {
+ if (event.shiftKey) {
+ event.stopPropagation();
+ }
+ }, false);
+ EventEmitter.decorate(swatch);
+ container.appendChild(swatch);
+ }
+
+ let value = this._createNode("span", {
+ class: options.angleClass
+ }, angle);
+
+ container.appendChild(value);
+ this.parsed.push(container);
+ },
+
+ /**
+ * Check if a CSS property supports a specific value.
+ *
+ * @param {String} name
+ * CSS Property name to check
+ * @param {String} value
+ * CSS Property value to check
+ */
+ _cssPropertySupportsValue: function (name, value) {
+ return this.isValidOnClient(name, value, this.doc);
+ },
+
+ /**
+ * Tests if a given colorObject output by CssColor is valid for parsing.
+ * Valid means it's really a color, not any of the CssColor SPECIAL_VALUES
+ * except transparent
+ */
+ _isValidColor: function (colorObj) {
+ return colorObj.valid &&
+ (!colorObj.specialValue || colorObj.specialValue === "transparent");
+ },
+
+ /**
+ * Append a color to the output.
+ *
+ * @param {String} color
+ * Color to append
+ * @param {Object} [options]
+ * Options object. For valid options and default values see
+ * _mergeOptions().
+ */
+ _appendColor: function (color, options = {}) {
+ let colorObj = new colorUtils.CssColor(color);
+
+ if (this._isValidColor(colorObj)) {
+ let container = this._createNode("span", {
+ "data-color": color
+ });
+
+ if (options.colorSwatchClass) {
+ let swatch = this._createNode("span", {
+ class: options.colorSwatchClass,
+ style: "background-color:" + color
+ });
+ this.colorSwatches.set(swatch, colorObj);
+ swatch.addEventListener("mousedown", this._onColorSwatchMouseDown,
+ false);
+ EventEmitter.decorate(swatch);
+ container.appendChild(swatch);
+ }
+
+ if (options.defaultColorType) {
+ color = colorObj.toString();
+ container.dataset.color = color;
+ }
+
+ let value = this._createNode("span", {
+ class: options.colorClass
+ }, color);
+
+ container.appendChild(value);
+ this.parsed.push(container);
+ } else {
+ this._appendTextNode(color);
+ }
+ },
+
+ /**
+ * Wrap some existing nodes in a filter editor.
+ *
+ * @param {String} filters
+ * The full text of the "filter" property.
+ * @param {object} options
+ * The options object passed to parseCssProperty().
+ * @param {object} nodes
+ * Nodes created by _toDOM().
+ *
+ * @returns {object}
+ * A new node that supplies a filter swatch and that wraps |nodes|.
+ */
+ _wrapFilter: function (filters, options, nodes) {
+ let container = this._createNode("span", {
+ "data-filters": filters
+ });
+
+ if (options.filterSwatchClass) {
+ let swatch = this._createNode("span", {
+ class: options.filterSwatchClass
+ });
+ container.appendChild(swatch);
+ }
+
+ let value = this._createNode("span", {
+ class: options.filterClass
+ });
+ value.appendChild(nodes);
+ container.appendChild(value);
+
+ return container;
+ },
+
+ _onColorSwatchMouseDown: function (event) {
+ if (!event.shiftKey) {
+ return;
+ }
+
+ // Prevent click event to be fired to not show the tooltip
+ event.stopPropagation();
+
+ let swatch = event.target;
+ let color = this.colorSwatches.get(swatch);
+ let val = color.nextColorUnit();
+
+ swatch.nextElementSibling.textContent = val;
+ swatch.emit("unit-change", val);
+ },
+
+ _onAngleSwatchMouseDown: function (event) {
+ if (!event.shiftKey) {
+ return;
+ }
+
+ event.stopPropagation();
+
+ let swatch = event.target;
+ let angle = this.angleSwatches.get(swatch);
+ let val = angle.nextAngleUnit();
+
+ swatch.nextElementSibling.textContent = val;
+ swatch.emit("unit-change", val);
+ },
+
+ /**
+ * A helper function that sanitizes a possibly-unterminated URL.
+ */
+ _sanitizeURL: function (url) {
+ // Re-lex the URL and add any needed termination characters.
+ let urlTokenizer = getCSSLexer(url);
+ // Just read until EOF; there will only be a single token.
+ while (urlTokenizer.nextToken()) {
+ // Nothing.
+ }
+
+ return urlTokenizer.performEOFFixup(url, true);
+ },
+
+ /**
+ * Append a URL to the output.
+ *
+ * @param {String} match
+ * Complete match that may include "url(xxx)"
+ * @param {String} url
+ * Actual URL
+ * @param {Object} [options]
+ * Options object. For valid options and default values see
+ * _mergeOptions().
+ */
+ _appendURL: function (match, url, options) {
+ if (options.urlClass) {
+ // Sanitize the URL. Note that if we modify the URL, we just
+ // leave the termination characters. This isn't strictly
+ // "as-authored", but it makes a bit more sense.
+ match = this._sanitizeURL(match);
+ // This regexp matches a URL token. It puts the "url(", any
+ // leading whitespace, and any opening quote into |leader|; the
+ // URL text itself into |body|, and any trailing quote, trailing
+ // whitespace, and the ")" into |trailer|. We considered adding
+ // functionality for this to CSSLexer, in some way, but this
+ // seemed simpler on the whole.
+ let [, leader, , body, trailer] =
+ /^(url\([ \t\r\n\f]*(["']?))(.*?)(\2[ \t\r\n\f]*\))$/i.exec(match);
+
+ this._appendTextNode(leader);
+
+ let href = url;
+ if (options.baseURI) {
+ try {
+ href = new URL(url, options.baseURI).href;
+ } catch (e) {
+ // Ignore.
+ }
+ }
+
+ this._appendNode("a", {
+ target: "_blank",
+ class: options.urlClass,
+ href: href
+ }, body);
+
+ this._appendTextNode(trailer);
+ } else {
+ this._appendTextNode(match);
+ }
+ },
+
+ /**
+ * Create a node.
+ *
+ * @param {String} tagName
+ * Tag type e.g. "div"
+ * @param {Object} attributes
+ * e.g. {class: "someClass", style: "cursor:pointer"};
+ * @param {String} [value]
+ * If a value is included it will be appended as a text node inside
+ * the tag. This is useful e.g. for span tags.
+ * @return {Node} Newly created Node.
+ */
+ _createNode: function (tagName, attributes, value = "") {
+ let node = this.doc.createElementNS(HTML_NS, tagName);
+ let attrs = Object.getOwnPropertyNames(attributes);
+
+ for (let attr of attrs) {
+ if (attributes[attr]) {
+ node.setAttribute(attr, attributes[attr]);
+ }
+ }
+
+ if (value) {
+ let textNode = this.doc.createTextNode(value);
+ node.appendChild(textNode);
+ }
+
+ return node;
+ },
+
+ /**
+ * Append a node to the output.
+ *
+ * @param {String} tagName
+ * Tag type e.g. "div"
+ * @param {Object} attributes
+ * e.g. {class: "someClass", style: "cursor:pointer"};
+ * @param {String} [value]
+ * If a value is included it will be appended as a text node inside
+ * the tag. This is useful e.g. for span tags.
+ */
+ _appendNode: function (tagName, attributes, value = "") {
+ let node = this._createNode(tagName, attributes, value);
+ this.parsed.push(node);
+ },
+
+ /**
+ * Append a text node to the output. If the previously output item was a text
+ * node then we append the text to that node.
+ *
+ * @param {String} text
+ * Text to append
+ */
+ _appendTextNode: function (text) {
+ let lastItem = this.parsed[this.parsed.length - 1];
+ if (typeof lastItem === "string") {
+ this.parsed[this.parsed.length - 1] = lastItem + text;
+ } else {
+ this.parsed.push(text);
+ }
+ },
+
+ /**
+ * Take all output and append it into a single DocumentFragment.
+ *
+ * @return {DocumentFragment}
+ * Document Fragment
+ */
+ _toDOM: function () {
+ let frag = this.doc.createDocumentFragment();
+
+ for (let item of this.parsed) {
+ if (typeof item === "string") {
+ frag.appendChild(this.doc.createTextNode(item));
+ } else {
+ frag.appendChild(item);
+ }
+ }
+
+ this.parsed.length = 0;
+ return frag;
+ },
+
+ /**
+ * Merges options objects. Default values are set here.
+ *
+ * @param {Object} overrides
+ * The option values to override e.g. _mergeOptions({colors: false})
+ *
+ * Valid options are:
+ * - defaultColorType: true // Convert colors to the default type
+ * // selected in the options panel.
+ * - angleClass: "" // The class to use for the angle value
+ * // that follows the swatch.
+ * - angleSwatchClass: "" // The class to use for angle swatches.
+ * - bezierClass: "" // The class to use for the bezier value
+ * // that follows the swatch.
+ * - bezierSwatchClass: "" // The class to use for bezier swatches.
+ * - colorClass: "" // The class to use for the color value
+ * // that follows the swatch.
+ * - colorSwatchClass: "" // The class to use for color swatches.
+ * - filterSwatch: false // A special case for parsing a
+ * // "filter" property, causing the
+ * // parser to skip the call to
+ * // _wrapFilter. Used only for
+ * // previewing with the filter swatch.
+ * - gridClass: "" // The class to use for the grid icon.
+ * - supportsColor: false // Does the CSS property support colors?
+ * - urlClass: "" // The class to be used for url() links.
+ * - baseURI: undefined // A string used to resolve
+ * // relative links.
+ * @return {Object}
+ * Overridden options object
+ */
+ _mergeOptions: function (overrides) {
+ let defaults = {
+ defaultColorType: true,
+ angleClass: "",
+ angleSwatchClass: "",
+ bezierClass: "",
+ bezierSwatchClass: "",
+ colorClass: "",
+ colorSwatchClass: "",
+ filterSwatch: false,
+ gridClass: "",
+ supportsColor: false,
+ urlClass: "",
+ baseURI: undefined,
+ };
+
+ for (let item in overrides) {
+ defaults[item] = overrides[item];
+ }
+ return defaults;
+ }
+};
diff --git a/devtools/client/shared/poller.js b/devtools/client/shared/poller.js
new file mode 100644
index 000000000..961f81a27
--- /dev/null
+++ b/devtools/client/shared/poller.js
@@ -0,0 +1,114 @@
+/* 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";
+loader.lazyRequireGetter(this, "defer",
+ "promise", true);
+
+/**
+ * @constructor Poller
+ * Takes a function that is to be called on an interval,
+ * and can be turned on and off via methods to execute `fn` on the interval
+ * specified during `on`. If `fn` returns a promise, the polling waits for
+ * that promise to resolve before waiting the interval to call again.
+ *
+ * Specify the `wait` duration between polling here, and optionally
+ * an `immediate` boolean, indicating whether the function should be called
+ * immediately when toggling on.
+ *
+ * @param {function} fn
+ * @param {number} wait
+ * @param {boolean?} immediate
+ */
+function Poller(fn, wait, immediate) {
+ this._fn = fn;
+ this._wait = wait;
+ this._immediate = immediate;
+ this._poll = this._poll.bind(this);
+ this._preparePoll = this._preparePoll.bind(this);
+}
+exports.Poller = Poller;
+
+/**
+ * Returns a boolean indicating whether or not poller
+ * is polling.
+ *
+ * @return {boolean}
+ */
+Poller.prototype.isPolling = function pollerIsPolling() {
+ return !!this._timer;
+};
+
+/**
+ * Turns polling on.
+ *
+ * @return {Poller}
+ */
+Poller.prototype.on = function pollerOn() {
+ if (this._destroyed) {
+ throw Error("Poller cannot be turned on after destruction.");
+ }
+ if (this._timer) {
+ this.off();
+ }
+ this._immediate ? this._poll() : this._preparePoll();
+ return this;
+};
+
+/**
+ * Turns off polling. Returns a promise that resolves when
+ * the last outstanding `fn` call finishes if it's an async function.
+ *
+ * @return {Promise}
+ */
+Poller.prototype.off = function pollerOff() {
+ let { resolve, promise } = defer();
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = null;
+ }
+
+ // Settle an inflight poll call before resolving
+ // if using a promise-backed poll function
+ if (this._inflight) {
+ this._inflight.then(resolve);
+ } else {
+ resolve();
+ }
+ return promise;
+};
+
+/**
+ * Turns off polling and removes the reference to the poller function.
+ * Resolves when the last outstanding `fn` call finishes if it's an async
+ * function.
+ */
+Poller.prototype.destroy = function pollerDestroy() {
+ return this.off().then(() => {
+ this._destroyed = true;
+ this._fn = null;
+ });
+};
+
+Poller.prototype._preparePoll = function pollerPrepare() {
+ this._timer = setTimeout(this._poll, this._wait);
+};
+
+Poller.prototype._poll = function pollerPoll() {
+ let response = this._fn();
+ if (response && typeof response.then === "function") {
+ // Store the most recent in-flight polling
+ // call so we can clean it up when disabling
+ this._inflight = response;
+ response.then(() => {
+ // Only queue up the next call if poller was not turned off
+ // while this async poll call was in flight.
+ if (this._timer) {
+ this._preparePoll();
+ }
+ });
+ } else {
+ this._preparePoll();
+ }
+};
diff --git a/devtools/client/shared/prefs.js b/devtools/client/shared/prefs.js
new file mode 100644
index 000000000..9b44d4d58
--- /dev/null
+++ b/devtools/client/shared/prefs.js
@@ -0,0 +1,178 @@
+/* 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 Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * Shortcuts for lazily accessing and setting various preferences.
+ * Usage:
+ * let prefs = new Prefs("root.path.to.branch", {
+ * myIntPref: ["Int", "leaf.path.to.my-int-pref"],
+ * myCharPref: ["Char", "leaf.path.to.my-char-pref"],
+ * myJsonPref: ["Json", "leaf.path.to.my-json-pref"],
+ * myFloatPref: ["Float", "leaf.path.to.my-float-pref"]
+ * ...
+ * });
+ *
+ * Get/set:
+ * prefs.myCharPref = "foo";
+ * let aux = prefs.myCharPref;
+ *
+ * Observe:
+ * prefs.registerObserver();
+ * prefs.on("pref-changed", (prefName, prefValue) => {
+ * ...
+ * });
+ *
+ * @param string prefsRoot
+ * The root path to the required preferences branch.
+ * @param object prefsBlueprint
+ * An object containing { accessorName: [prefType, prefName] } keys.
+ */
+function PrefsHelper(prefsRoot = "", prefsBlueprint = {}) {
+ EventEmitter.decorate(this);
+
+ let cache = new Map();
+
+ for (let accessorName in prefsBlueprint) {
+ let [prefType, prefName] = prefsBlueprint[accessorName];
+ map(this, cache, accessorName, prefType, prefsRoot, prefName);
+ }
+
+ let observer = makeObserver(this, cache, prefsRoot, prefsBlueprint);
+ this.registerObserver = () => observer.register();
+ this.unregisterObserver = () => observer.unregister();
+}
+
+/**
+ * Helper method for getting a pref value.
+ *
+ * @param Map cache
+ * @param string prefType
+ * @param string prefsRoot
+ * @param string prefName
+ * @return any
+ */
+function get(cache, prefType, prefsRoot, prefName) {
+ let cachedPref = cache.get(prefName);
+ if (cachedPref !== undefined) {
+ return cachedPref;
+ }
+ let value = Services.prefs["get" + prefType + "Pref"](
+ [prefsRoot, prefName].join(".")
+ );
+ cache.set(prefName, value);
+ return value;
+}
+
+/**
+ * Helper method for setting a pref value.
+ *
+ * @param Map cache
+ * @param string prefType
+ * @param string prefsRoot
+ * @param string prefName
+ * @param any value
+ */
+function set(cache, prefType, prefsRoot, prefName, value) {
+ Services.prefs["set" + prefType + "Pref"](
+ [prefsRoot, prefName].join("."),
+ value
+ );
+ cache.set(prefName, value);
+}
+
+/**
+ * Maps a property name to a pref, defining lazy getters and setters.
+ * Supported types are "Bool", "Char", "Int", "Float" (sugar around "Char"
+ * type and casting), and "Json" (which is basically just sugar for "Char"
+ * using the standard JSON serializer).
+ *
+ * @param PrefsHelper self
+ * @param Map cache
+ * @param string accessorName
+ * @param string prefType
+ * @param string prefsRoot
+ * @param string prefName
+ * @param array serializer [optional]
+ */
+function map(self, cache, accessorName, prefType, prefsRoot, prefName,
+ serializer = { in: e => e, out: e => e }) {
+ if (prefName in self) {
+ throw new Error(`Can't use ${prefName} because it overrides a property` +
+ "on the instance.");
+ }
+ if (prefType == "Json") {
+ map(self, cache, accessorName, "Char", prefsRoot, prefName, {
+ in: JSON.parse,
+ out: JSON.stringify
+ });
+ return;
+ }
+ if (prefType == "Float") {
+ map(self, cache, accessorName, "Char", prefsRoot, prefName, {
+ in: Number.parseFloat,
+ out: (n) => n + ""
+ });
+ return;
+ }
+
+ Object.defineProperty(self, accessorName, {
+ get: () => serializer.in(get(cache, prefType, prefsRoot, prefName)),
+ set: (e) => set(cache, prefType, prefsRoot, prefName, serializer.out(e))
+ });
+}
+
+/**
+ * Finds the accessor for the provided pref, based on the blueprint object
+ * used in the constructor.
+ *
+ * @param PrefsHelper self
+ * @param object prefsBlueprint
+ * @return string
+ */
+function accessorNameForPref(somePrefName, prefsBlueprint) {
+ for (let accessorName in prefsBlueprint) {
+ let [, prefName] = prefsBlueprint[accessorName];
+ if (somePrefName == prefName) {
+ return accessorName;
+ }
+ }
+ return "";
+}
+
+/**
+ * Creates a pref observer for `self`.
+ *
+ * @param PrefsHelper self
+ * @param Map cache
+ * @param string prefsRoot
+ * @param object prefsBlueprint
+ * @return object
+ */
+function makeObserver(self, cache, prefsRoot, prefsBlueprint) {
+ return {
+ register: function () {
+ this._branch = Services.prefs.getBranch(prefsRoot + ".");
+ this._branch.addObserver("", this, false);
+ },
+ unregister: function () {
+ this._branch.removeObserver("", this);
+ },
+ observe: function (subject, topic, prefName) {
+ // If this particular pref isn't handled by the blueprint object,
+ // even though it's in the specified branch, ignore it.
+ let accessorName = accessorNameForPref(prefName, prefsBlueprint);
+ if (!(accessorName in self)) {
+ return;
+ }
+ cache.delete(prefName);
+ self.emit("pref-changed", accessorName, self[accessorName]);
+ }
+ };
+}
+
+exports.PrefsHelper = PrefsHelper;
diff --git a/devtools/client/shared/redux/create-store.js b/devtools/client/shared/redux/create-store.js
new file mode 100644
index 000000000..baacf428e
--- /dev/null
+++ b/devtools/client/shared/redux/create-store.js
@@ -0,0 +1,51 @@
+/* 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 { createStore, applyMiddleware } = require("devtools/client/shared/vendor/redux");
+const { thunk } = require("./middleware/thunk");
+const { waitUntilService } = require("./middleware/wait-service");
+const { task } = require("./middleware/task");
+const { log } = require("./middleware/log");
+const { promise } = require("./middleware/promise");
+const { history } = require("./middleware/history");
+
+/**
+ * This creates a dispatcher with all the standard middleware in place
+ * that all code requires. It can also be optionally configured in
+ * various ways, such as logging and recording.
+ *
+ * @param {object} opts:
+ * - log: log all dispatched actions to console
+ * - history: an array to store every action in. Should only be
+ * used in tests.
+ * - middleware: array of middleware to be included in the redux store
+ */
+module.exports = (opts = {}) => {
+ const middleware = [
+ task,
+ thunk,
+ promise,
+
+ // Order is important: services must go last as they always
+ // operate on "already transformed" actions. Actions going through
+ // them shouldn't have any special fields like promises, they
+ // should just be normal JSON objects.
+ waitUntilService
+ ];
+
+ if (opts.history) {
+ middleware.push(history(opts.history));
+ }
+
+ if (opts.middleware) {
+ opts.middleware.forEach(fn => middleware.push(fn));
+ }
+
+ if (opts.log) {
+ middleware.push(log);
+ }
+
+ return applyMiddleware(...middleware)(createStore);
+};
diff --git a/devtools/client/shared/redux/middleware/history.js b/devtools/client/shared/redux/middleware/history.js
new file mode 100644
index 000000000..dba88c045
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/history.js
@@ -0,0 +1,23 @@
+/* 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 flags = require("devtools/shared/flags");
+
+/**
+ * A middleware that stores every action coming through the store in the passed
+ * in logging object. Should only be used for tests, as it collects all
+ * action information, which will cause memory bloat.
+ */
+exports.history = (log = []) => ({ dispatch, getState }) => {
+ if (!flags.testing) {
+ console.warn("Using history middleware stores all actions in state for " +
+ "testing and devtools is not currently running in test " +
+ "mode. Be sure this is intentional.");
+ }
+ return next => action => {
+ log.push(action);
+ next(action);
+ };
+};
diff --git a/devtools/client/shared/redux/middleware/log.js b/devtools/client/shared/redux/middleware/log.js
new file mode 100644
index 000000000..f812f793b
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/log.js
@@ -0,0 +1,17 @@
+/* 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";
+
+/**
+ * A middleware that logs all actions coming through the system
+ * to the console.
+ */
+function log({ dispatch, getState }) {
+ return next => action => {
+ console.log("[DISPATCH]", JSON.stringify(action, null, 2));
+ next(action);
+ };
+}
+
+exports.log = log;
diff --git a/devtools/client/shared/redux/middleware/moz.build b/devtools/client/shared/redux/middleware/moz.build
new file mode 100644
index 000000000..a25bfd518
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/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/.
+
+DevToolsModules(
+ 'history.js',
+ 'log.js',
+ 'promise.js',
+ 'task.js',
+ 'thunk.js',
+ 'wait-service.js',
+)
+
+XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
diff --git a/devtools/client/shared/redux/middleware/promise.js b/devtools/client/shared/redux/middleware/promise.js
new file mode 100644
index 000000000..237e41eef
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/promise.js
@@ -0,0 +1,54 @@
+/* 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 uuidgen = require("sdk/util/uuid").uuid;
+const defer = require("devtools/shared/defer");
+const {
+ entries, toObject, executeSoon
+} = require("devtools/shared/DevToolsUtils");
+const PROMISE = exports.PROMISE = "@@dispatch/promise";
+
+function promiseMiddleware({ dispatch, getState }) {
+ return next => action => {
+ if (!(PROMISE in action)) {
+ return next(action);
+ }
+
+ const promiseInst = action[PROMISE];
+ const seqId = uuidgen().toString();
+
+ // Create a new action that doesn't have the promise field and has
+ // the `seqId` field that represents the sequence id
+ action = Object.assign(
+ toObject(entries(action).filter(pair => pair[0] !== PROMISE)), { seqId }
+ );
+
+ dispatch(Object.assign({}, action, { status: "start" }));
+
+ // Return the promise so action creators can still compose if they
+ // want to.
+ const deferred = defer();
+ promiseInst.then(value => {
+ executeSoon(() => {
+ dispatch(Object.assign({}, action, {
+ status: "done",
+ value: value
+ }));
+ deferred.resolve(value);
+ });
+ }, error => {
+ executeSoon(() => {
+ dispatch(Object.assign({}, action, {
+ status: "error",
+ error: error.message || error
+ }));
+ deferred.reject(error);
+ });
+ });
+ return deferred.promise;
+ };
+}
+
+exports.promise = promiseMiddleware;
diff --git a/devtools/client/shared/redux/middleware/task.js b/devtools/client/shared/redux/middleware/task.js
new file mode 100644
index 000000000..c1dd262ee
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/task.js
@@ -0,0 +1,42 @@
+/* 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 { Task } = require("devtools/shared/task");
+const { executeSoon, isGenerator, reportException } = require("devtools/shared/DevToolsUtils");
+const ERROR_TYPE = exports.ERROR_TYPE = "@@redux/middleware/task#error";
+
+/**
+ * A middleware that allows generator thunks (functions) and promise
+ * to be dispatched. If it's a generator, it is called with `dispatch`
+ * and `getState`, allowing the action to create multiple actions (most likely
+ * asynchronously) and yield on each. If called with a promise, calls `dispatch`
+ * on the results.
+ */
+
+function task({ dispatch, getState }) {
+ return next => action => {
+ if (isGenerator(action)) {
+ return Task.spawn(action.bind(null, dispatch, getState))
+ .then(null, handleError.bind(null, dispatch));
+ }
+
+ /*
+ if (isPromise(action)) {
+ return action.then(dispatch, handleError.bind(null, dispatch));
+ }
+ */
+
+ return next(action);
+ };
+}
+
+function handleError(dispatch, error) {
+ executeSoon(() => {
+ reportException(ERROR_TYPE, error);
+ dispatch({ type: ERROR_TYPE, error });
+ });
+}
+
+exports.task = task;
diff --git a/devtools/client/shared/redux/middleware/test/.eslintrc.js b/devtools/client/shared/redux/middleware/test/.eslintrc.js
new file mode 100644
index 000000000..0d12cd9a3
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/.eslintrc.js
@@ -0,0 +1,17 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../../.eslintrc.mochitests.js",
+ "globals": {
+ "run_test": true,
+ "run_next_test": true,
+ "equal": true,
+ "do_print": true,
+ "waitUntilState": true
+ },
+ "rules": {
+ // Stop giving errors for run_test
+ "camelcase": "off"
+ }
+};
diff --git a/devtools/client/shared/redux/middleware/test/head.js b/devtools/client/shared/redux/middleware/test/head.js
new file mode 100644
index 000000000..1e5cbff7a
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/head.js
@@ -0,0 +1,27 @@
+/* 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/. */
+
+/* exported waitUntilState */
+
+"use strict";
+
+const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const flags = require("devtools/shared/flags");
+
+flags.testing = true;
+
+function waitUntilState(store, predicate) {
+ return new Promise(resolve => {
+ let unsubscribe = store.subscribe(check);
+ function check() {
+ if (predicate(store.getState())) {
+ unsubscribe();
+ resolve();
+ }
+ }
+
+ // Fire the check immediately incase the action has already occurred
+ check();
+ });
+}
diff --git a/devtools/client/shared/redux/middleware/test/test_middleware-task-01.js b/devtools/client/shared/redux/middleware/test/test_middleware-task-01.js
new file mode 100644
index 000000000..be94560cb
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/test_middleware-task-01.js
@@ -0,0 +1,56 @@
+/* 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 { createStore, applyMiddleware } = require("devtools/client/shared/vendor/redux");
+const { task } = require("devtools/client/shared/redux/middleware/task");
+
+/**
+ * Tests that task middleware allows dispatching generators, promises and objects
+ * that return actions;
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let store = applyMiddleware(task)(createStore)(reducer);
+
+ store.dispatch(fetch1("generator"));
+ yield waitUntilState(store, () => store.getState().length === 1);
+ equal(store.getState()[0].data, "generator",
+ "task middleware async dispatches an action via generator");
+
+ store.dispatch(fetch2("sync"));
+ yield waitUntilState(store, () => store.getState().length === 2);
+ equal(store.getState()[1].data, "sync",
+ "task middleware sync dispatches an action via sync");
+});
+
+function fetch1(data) {
+ return function* (dispatch, getState) {
+ equal(getState().length, 0, "`getState` is accessible in a generator action");
+ let moreData = yield new Promise(resolve => resolve(data));
+ // Ensure it handles more than one yield
+ moreData = yield new Promise(resolve => resolve(data));
+ dispatch({ type: "fetch1", data: moreData });
+ };
+}
+
+function fetch2(data) {
+ return {
+ type: "fetch2",
+ data
+ };
+}
+
+function reducer(state = [], action) {
+ do_print("Action called: " + action.type);
+ if (["fetch1", "fetch2"].includes(action.type)) {
+ state.push(action);
+ }
+ return [...state];
+}
diff --git a/devtools/client/shared/redux/middleware/test/test_middleware-task-02.js b/devtools/client/shared/redux/middleware/test/test_middleware-task-02.js
new file mode 100644
index 000000000..7e2a88d2c
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/test_middleware-task-02.js
@@ -0,0 +1,67 @@
+/* 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";
+
+/**
+ * Tests that task middleware allows dispatching generators that dispatch
+ * additional sync and async actions.
+ */
+
+const { createStore, applyMiddleware } = require("devtools/client/shared/vendor/redux");
+const { task } = require("devtools/client/shared/redux/middleware/task");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let store = applyMiddleware(task)(createStore)(reducer);
+
+ store.dispatch(comboAction());
+ yield waitUntilState(store, () => store.getState().length === 3);
+
+ equal(store.getState()[0].type, "fetchAsync-start",
+ "Async dispatched actions in a generator task are fired");
+ equal(store.getState()[1].type, "fetchAsync-end",
+ "Async dispatched actions in a generator task are fired");
+ equal(store.getState()[2].type, "fetchSync",
+ "Return values of yielded sync dispatched actions are correct");
+ equal(store.getState()[3].type, "fetch-done",
+ "Return values of yielded async dispatched actions are correct");
+ equal(store.getState()[3].data.sync.data, "sync",
+ "Return values of dispatched sync values are correct");
+ equal(store.getState()[3].data.async, "async",
+ "Return values of dispatched async values are correct");
+});
+
+function comboAction() {
+ return function* (dispatch, getState) {
+ let data = {};
+ data.async = yield dispatch(fetchAsync("async"));
+ data.sync = yield dispatch(fetchSync("sync"));
+ dispatch({ type: "fetch-done", data });
+ };
+}
+
+function fetchSync(data) {
+ return { type: "fetchSync", data };
+}
+
+function fetchAsync(data) {
+ return function* (dispatch) {
+ dispatch({ type: "fetchAsync-start" });
+ let val = yield new Promise(resolve => resolve(data));
+ dispatch({ type: "fetchAsync-end" });
+ return val;
+ };
+}
+
+function reducer(state = [], action) {
+ do_print("Action called: " + action.type);
+ if (/fetch/.test(action.type)) {
+ state.push(action);
+ }
+ return [...state];
+}
diff --git a/devtools/client/shared/redux/middleware/test/test_middleware-task-03.js b/devtools/client/shared/redux/middleware/test/test_middleware-task-03.js
new file mode 100644
index 000000000..7dc0e5c9d
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/test_middleware-task-03.js
@@ -0,0 +1,42 @@
+/* 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 { createStore, applyMiddleware } = require("devtools/client/shared/vendor/redux");
+const { task, ERROR_TYPE } = require("devtools/client/shared/redux/middleware/task");
+
+/**
+ * Tests that the middleware handles errors thrown in tasks, and rejected promises.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let store = applyMiddleware(task)(createStore)(reducer);
+
+ store.dispatch(generatorError());
+ yield waitUntilState(store, () => store.getState().length === 1);
+ equal(store.getState()[0].type, ERROR_TYPE,
+ "generator errors dispatch ERROR_TYPE actions");
+ equal(store.getState()[0].error, "task-middleware-error-generator",
+ "generator errors dispatch ERROR_TYPE actions with error");
+});
+
+function generatorError() {
+ return function* (dispatch, getState) {
+ let error = "task-middleware-error-generator";
+ throw error;
+ };
+}
+
+function reducer(state = [], action) {
+ do_print("Action called: " + action.type);
+ if (action.type === ERROR_TYPE) {
+ state.push(action);
+ }
+ return [...state];
+}
diff --git a/devtools/client/shared/redux/middleware/test/xpcshell.ini b/devtools/client/shared/redux/middleware/test/xpcshell.ini
new file mode 100644
index 000000000..3836ed1fd
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_middleware-task-01.js]
+[test_middleware-task-02.js]
+[test_middleware-task-03.js]
diff --git a/devtools/client/shared/redux/middleware/thunk.js b/devtools/client/shared/redux/middleware/thunk.js
new file mode 100644
index 000000000..8f564a033
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/thunk.js
@@ -0,0 +1,19 @@
+/* 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";
+
+/**
+ * A middleware that allows thunks (functions) to be dispatched.
+ * If it's a thunk, it is called with `dispatch` and `getState`,
+ * allowing the action to create multiple actions (most likely
+ * asynchronously).
+ */
+function thunk({ dispatch, getState }) {
+ return next => action => {
+ return (typeof action === "function")
+ ? action(dispatch, getState)
+ : next(action);
+ };
+}
+exports.thunk = thunk;
diff --git a/devtools/client/shared/redux/middleware/wait-service.js b/devtools/client/shared/redux/middleware/wait-service.js
new file mode 100644
index 000000000..93878a312
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/wait-service.js
@@ -0,0 +1,64 @@
+/* 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";
+
+/**
+ * A middleware which acts like a service, because it is stateful
+ * and "long-running" in the background. It provides the ability
+ * for actions to install a function to be run once when a specific
+ * condition is met by an action coming through the system. Think of
+ * it as a thunk that blocks until the condition is met. Example:
+ *
+ * ```js
+ * const services = { WAIT_UNTIL: require('wait-service').NAME };
+ *
+ * { type: services.WAIT_UNTIL,
+ * predicate: action => action.type === constants.ADD_ITEM,
+ * run: (dispatch, getState, action) => {
+ * // Do anything here. You only need to accept the arguments
+ * // if you need them. `action` is the action that satisfied
+ * // the predicate.
+ * }
+ * }
+ * ```
+ */
+const NAME = exports.NAME = "@@service/waitUntil";
+
+function waitUntilService({ dispatch, getState }) {
+ let pending = [];
+
+ function checkPending(action) {
+ let readyRequests = [];
+ let stillPending = [];
+
+ // Find the pending requests whose predicates are satisfied with
+ // this action. Wait to run the requests until after we update the
+ // pending queue because the request handler may synchronously
+ // dispatch again and run this service (that use case is
+ // completely valid).
+ for (let request of pending) {
+ if (request.predicate(action)) {
+ readyRequests.push(request);
+ } else {
+ stillPending.push(request);
+ }
+ }
+
+ pending = stillPending;
+ for (let request of readyRequests) {
+ request.run(dispatch, getState, action);
+ }
+ }
+
+ return next => action => {
+ if (action.type === NAME) {
+ pending.push(action);
+ return null;
+ }
+ let result = next(action);
+ checkPending(action);
+ return result;
+ };
+}
+exports.waitUntilService = waitUntilService;
diff --git a/devtools/client/shared/redux/moz.build b/devtools/client/shared/redux/moz.build
new file mode 100644
index 000000000..02b1f6bd6
--- /dev/null
+++ b/devtools/client/shared/redux/moz.build
@@ -0,0 +1,14 @@
+# -*- 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 += [
+ 'middleware',
+]
+
+DevToolsModules(
+ 'create-store.js',
+ 'non-react-subscriber.js',
+)
diff --git a/devtools/client/shared/redux/non-react-subscriber.js b/devtools/client/shared/redux/non-react-subscriber.js
new file mode 100644
index 000000000..0bb3f0b8f
--- /dev/null
+++ b/devtools/client/shared/redux/non-react-subscriber.js
@@ -0,0 +1,153 @@
+/* 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";
+
+/**
+ * This file defines functions to add the ability for redux reducers
+ * to broadcast specific state changes to a non-React UI. You should
+ * *never* use this for new code that uses React, as it violates the
+ * core principals of a functional UI. This should only be used when
+ * migrating old code to redux, because it allows you to use redux
+ * with event-listening UI elements. The typical way to set all of
+ * this up is this:
+ *
+ * const emitter = makeEmitter();
+ * let store = createStore(combineEmittingReducers(
+ * reducers,
+ * emitter.emit
+ * ));
+ * store = enhanceStoreWithEmitter(store, emitter);
+ *
+ * Now reducers will receive a 3rd argument, `emit`, for emitting
+ * events, and the store has an `on` function for listening to them.
+ * For example, a reducer can now do this:
+ *
+ * function update(state = initialState, action, emitChange) {
+ * if (action.type === constants.ADD_BREAKPOINT) {
+ * const id = action.breakpoint.id;
+ * emitChange('add-breakpoint', action.breakpoint);
+ * return state.merge({ [id]: action.breakpoint });
+ * }
+ * return state;
+ * }
+ *
+ * `emitChange` is *not* synchronous, the state changes will be
+ * broadcasted *after* all reducers are run and the state has been
+ * updated.
+ *
+ * Now, a non-React widget can do this:
+ *
+ * store.on('add-breakpoint', breakpoint => { ... });
+ */
+
+const { combineReducers } = require("devtools/client/shared/vendor/redux");
+
+/**
+ * Make an emitter that is meant to be used in redux reducers. This
+ * does not run listeners immediately when an event is emitted; it
+ * waits until all reducers have run and the store has updated the
+ * state, and then fires any enqueued events. Events *are* fired
+ * synchronously, but just later in the process.
+ *
+ * This is important because you never want the UI to be updating in
+ * the middle of a reducing process. Reducers will fire these events
+ * in the middle of generating new state, but the new state is *not*
+ * available from the store yet. So if the UI executes in the middle
+ * of the reducing process and calls `getState()` to get something
+ * from the state, it will get stale state.
+ *
+ * We want the reducing and the UI updating phases to execute
+ * atomically and independent from each other.
+ *
+ * @param {Function} stillAliveFunc
+ * A function that indicates the app is still active. If this
+ * returns false, changes will stop being broadcasted.
+ */
+function makeStateBroadcaster(stillAliveFunc) {
+ const listeners = {};
+ let enqueuedChanges = [];
+
+ return {
+ onChange: (name, cb) => {
+ if (!listeners[name]) {
+ listeners[name] = [];
+ }
+ listeners[name].push(cb);
+ },
+
+ offChange: (name, cb) => {
+ listeners[name] = listeners[name].filter(listener => listener !== cb);
+ },
+
+ emitChange: (name, payload) => {
+ enqueuedChanges.push([name, payload]);
+ },
+
+ subscribeToStore: store => {
+ store.subscribe(() => {
+ if (stillAliveFunc()) {
+ enqueuedChanges.forEach(([name, payload]) => {
+ if (listeners[name]) {
+ listeners[name].forEach(listener => {
+ listener(payload);
+ });
+ }
+ });
+ enqueuedChanges = [];
+ }
+ });
+ }
+ };
+}
+
+/**
+ * Make a store fire any enqueued events whenever the state changes,
+ * and add an `on` function to allow users to listen for specific
+ * events.
+ *
+ * @param {Object} store
+ * @param {Object} broadcaster
+ * @return {Object}
+ */
+function enhanceStoreWithBroadcaster(store, broadcaster) {
+ broadcaster.subscribeToStore(store);
+ store.onChange = broadcaster.onChange;
+ store.offChange = broadcaster.offChange;
+ return store;
+}
+
+/**
+ * Function that takes a hash of reducers, like `combineReducers`, and
+ * an `emitChange` function and returns a function to be used as a
+ * reducer for a Redux store. This allows all reducers defined here to
+ * receive a third argument, the `emitChange` function, for
+ * event-based subscriptions from within reducers.
+ *
+ * @param {Object} reducers
+ * @param {Function} emitChange
+ * @return {Function}
+ */
+function combineBroadcastingReducers(reducers, emitChange) {
+ // Wrap each reducer with a wrapper function that calls
+ // the reducer with a third argument, an `emitChange` function.
+ // Use this rather than a new custom top level reducer that would ultimately
+ // have to replicate redux's `combineReducers` so we only pass in correct
+ // state, the error checking, and other edge cases.
+ function wrapReduce(newReducers, key) {
+ newReducers[key] = (state, action) => {
+ return reducers[key](state, action, emitChange);
+ };
+ return newReducers;
+ }
+
+ return combineReducers(
+ Object.keys(reducers).reduce(wrapReduce, Object.create(null))
+ );
+}
+
+module.exports = {
+ makeStateBroadcaster,
+ enhanceStoreWithBroadcaster,
+ combineBroadcastingReducers
+};
diff --git a/devtools/client/shared/scroll.js b/devtools/client/shared/scroll.js
new file mode 100644
index 000000000..ee591e014
--- /dev/null
+++ b/devtools/client/shared/scroll.js
@@ -0,0 +1,52 @@
+/* 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";
+
+/**
+ * Scroll the document so that the element "elem" appears in the viewport.
+ *
+ * @param {DOMNode} elem
+ * The element that needs to appear in the viewport.
+ * @param {Boolean} centered
+ * true if you want it centered, false if you want it to appear on the
+ * top of the viewport. It is true by default, and that is usually what
+ * you want.
+ */
+function scrollIntoViewIfNeeded(elem, centered = true) {
+ let win = elem.ownerDocument.defaultView;
+ let clientRect = elem.getBoundingClientRect();
+
+ // The following are always from the {top, bottom}
+ // of the viewport, to the {top, …} of the box.
+ // Think of them as geometrical vectors, it helps.
+ // The origin is at the top left.
+
+ let topToBottom = clientRect.bottom;
+ let bottomToTop = clientRect.top - win.innerHeight;
+ // We allow one translation on the y axis.
+ let yAllowed = true;
+
+ // Whatever `centered` is, the behavior is the same if the box is
+ // (even partially) visible.
+ if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) {
+ win.scrollBy(0, topToBottom - elem.offsetHeight);
+ yAllowed = false;
+ } else if ((bottomToTop < 0 || !centered) &&
+ bottomToTop >= -elem.offsetHeight) {
+ win.scrollBy(0, bottomToTop + elem.offsetHeight);
+ yAllowed = false;
+ }
+
+ // If we want it centered, and the box is completely hidden,
+ // then we center it explicitly.
+ if (centered) {
+ if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) {
+ win.scroll(win.scrollX,
+ win.scrollY + clientRect.top
+ - (win.innerHeight - elem.offsetHeight) / 2);
+ }
+ }
+}
+exports.scrollIntoViewIfNeeded = scrollIntoViewIfNeeded;
diff --git a/devtools/client/shared/shim/Services.js b/devtools/client/shared/shim/Services.js
new file mode 100644
index 000000000..97ea51433
--- /dev/null
+++ b/devtools/client/shared/shim/Services.js
@@ -0,0 +1,620 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* globals localStorage, window, document, NodeFilter */
+
+// Some constants from nsIPrefBranch.idl.
+const PREF_INVALID = 0;
+const PREF_STRING = 32;
+const PREF_INT = 64;
+const PREF_BOOL = 128;
+const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
+
+// We prefix all our local storage items with this.
+const PREFIX = "Services.prefs:";
+
+/**
+ * Create a new preference branch. This object conforms largely to
+ * nsIPrefBranch and nsIPrefService, though it only implements the
+ * subset needed by devtools. A preference branch can hold child
+ * preferences while also holding a preference value itself.
+ *
+ * @param {PrefBranch} parent the parent branch, or null for the root
+ * branch.
+ * @param {String} name the base name of this branch
+ * @param {String} fullName the fully-qualified name of this branch
+ */
+function PrefBranch(parent, name, fullName) {
+ this._parent = parent;
+ this._name = name;
+ this._fullName = fullName;
+ this._observers = {};
+ this._children = {};
+
+ // Properties used when this branch has a value as well.
+ this._defaultValue = null;
+ this._hasUserValue = false;
+ this._userValue = null;
+ this._type = PREF_INVALID;
+}
+
+PrefBranch.prototype = {
+ PREF_INVALID: PREF_INVALID,
+ PREF_STRING: PREF_STRING,
+ PREF_INT: PREF_INT,
+ PREF_BOOL: PREF_BOOL,
+
+ /** @see nsIPrefBranch.root. */
+ get root() {
+ return this._fullName;
+ },
+
+ /** @see nsIPrefBranch.getPrefType. */
+ getPrefType: function (prefName) {
+ return this._findPref(prefName)._type;
+ },
+
+ /** @see nsIPrefBranch.getBoolPref. */
+ getBoolPref: function (prefName) {
+ let thePref = this._findPref(prefName);
+ if (thePref._type !== PREF_BOOL) {
+ throw new Error(`${prefName} does not have bool type`);
+ }
+ return thePref._get();
+ },
+
+ /** @see nsIPrefBranch.setBoolPref. */
+ setBoolPref: function (prefName, value) {
+ if (typeof value !== "boolean") {
+ throw new Error("non-bool passed to setBoolPref");
+ }
+ let thePref = this._findOrCreatePref(prefName, value, true, value);
+ if (thePref._type !== PREF_BOOL) {
+ throw new Error(`${prefName} does not have bool type`);
+ }
+ thePref._set(value);
+ },
+
+ /** @see nsIPrefBranch.getCharPref. */
+ getCharPref: function (prefName) {
+ let thePref = this._findPref(prefName);
+ if (thePref._type !== PREF_STRING) {
+ throw new Error(`${prefName} does not have string type`);
+ }
+ return thePref._get();
+ },
+
+ /** @see nsIPrefBranch.setCharPref. */
+ setCharPref: function (prefName, value) {
+ if (typeof value !== "string") {
+ throw new Error("non-string passed to setCharPref");
+ }
+ let thePref = this._findOrCreatePref(prefName, value, true, value);
+ if (thePref._type !== PREF_STRING) {
+ throw new Error(`${prefName} does not have string type`);
+ }
+ thePref._set(value);
+ },
+
+ /** @see nsIPrefBranch.getIntPref. */
+ getIntPref: function (prefName) {
+ let thePref = this._findPref(prefName);
+ if (thePref._type !== PREF_INT) {
+ throw new Error(`${prefName} does not have int type`);
+ }
+ return thePref._get();
+ },
+
+ /** @see nsIPrefBranch.setIntPref. */
+ setIntPref: function (prefName, value) {
+ if (typeof value !== "number") {
+ throw new Error("non-number passed to setIntPref");
+ }
+ let thePref = this._findOrCreatePref(prefName, value, true, value);
+ if (thePref._type !== PREF_INT) {
+ throw new Error(`${prefName} does not have int type`);
+ }
+ thePref._set(value);
+ },
+
+ /** @see nsIPrefBranch.clearUserPref */
+ clearUserPref: function (prefName) {
+ let thePref = this._findPref(prefName);
+ thePref._clearUserValue();
+ },
+
+ /** @see nsIPrefBranch.prefHasUserValue */
+ prefHasUserValue: function (prefName) {
+ let thePref = this._findPref(prefName);
+ return thePref._hasUserValue;
+ },
+
+ /** @see nsIPrefBranch.addObserver */
+ addObserver: function (domain, observer, holdWeak) {
+ if (holdWeak) {
+ throw new Error("shim prefs only supports strong observers");
+ }
+
+ if (!(domain in this._observers)) {
+ this._observers[domain] = [];
+ }
+ this._observers[domain].push(observer);
+ },
+
+ /** @see nsIPrefBranch.removeObserver */
+ removeObserver: function (domain, observer) {
+ if (!(domain in this._observers)) {
+ return;
+ }
+ let index = this._observers[domain].indexOf(observer);
+ if (index >= 0) {
+ this._observers[domain].splice(index, 1);
+ }
+ },
+
+ /** @see nsIPrefService.savePrefFile */
+ savePrefFile: function (file) {
+ if (file) {
+ throw new Error("shim prefs only supports null file in savePrefFile");
+ }
+ // Nothing to do - this implementation always writes back.
+ },
+
+ /** @see nsIPrefService.getBranch */
+ getBranch: function (prefRoot) {
+ if (!prefRoot) {
+ return this;
+ }
+ if (prefRoot.endsWith(".")) {
+ prefRoot = prefRoot.slice(0, -1);
+ }
+ // This is a bit weird since it could erroneously return a pref,
+ // not a pref branch.
+ return this._findPref(prefRoot);
+ },
+
+ /**
+ * Return this preference's current value.
+ *
+ * @return {Any} The current value of this preference. This may
+ * return a string, a number, or a boolean depending on the
+ * preference's type.
+ */
+ _get: function () {
+ if (this._hasUserValue) {
+ return this._userValue;
+ }
+ return this._defaultValue;
+ },
+
+ /**
+ * Set the preference's value. The new value is assumed to be a
+ * user value. After setting the value, this function emits a
+ * change notification.
+ *
+ * @param {Any} value the new value
+ */
+ _set: function (value) {
+ if (!this._hasUserValue || value !== this._userValue) {
+ this._userValue = value;
+ this._hasUserValue = true;
+ this._saveAndNotify();
+ }
+ },
+
+ /**
+ * Set the default value for this preference, and emit a
+ * notification if this results in a visible change.
+ *
+ * @param {Any} value the new default value
+ */
+ _setDefault: function (value) {
+ if (this._defaultValue !== value) {
+ this._defaultValue = value;
+ if (!this._hasUserValue) {
+ this._saveAndNotify();
+ }
+ }
+ },
+
+ /**
+ * If this preference has a user value, clear it. If a change was
+ * made, emit a change notification.
+ */
+ _clearUserValue: function () {
+ if (this._hasUserValue) {
+ this._userValue = null;
+ this._hasUserValue = false;
+ this._saveAndNotify();
+ }
+ },
+
+ /**
+ * Helper function to write the preference's value to local storage
+ * and then emit a change notification.
+ */
+ _saveAndNotify: function () {
+ let store = {
+ type: this._type,
+ defaultValue: this._defaultValue,
+ hasUserValue: this._hasUserValue,
+ userValue: this._userValue,
+ };
+
+ localStorage.setItem(PREFIX + this._fullName, JSON.stringify(store));
+ this._parent._notify(this._name);
+ },
+
+ /**
+ * Change this preference's value without writing it back to local
+ * storage. This is used to handle changes to local storage that
+ * were made externally.
+ *
+ * @param {Number} type one of the PREF_* values
+ * @param {Any} userValue the user value to use if the pref does not exist
+ * @param {Any} defaultValue the default value to use if the pref
+ * does not exist
+ * @param {Boolean} hasUserValue if a new pref is created, whether
+ * the default value is also a user value
+ * @param {Object} store the new value of the preference. It should
+ * be of the form {type, defaultValue, hasUserValue, userValue};
+ * where |type| is one of the PREF_* type constants; |defaultValue|
+ * and |userValue| are the default and user values, respectively;
+ * and |hasUserValue| is a boolean indicating whether the user value
+ * is valid
+ */
+ _storageUpdated: function (type, userValue, hasUserValue, defaultValue) {
+ this._type = type;
+ this._defaultValue = defaultValue;
+ this._hasUserValue = hasUserValue;
+ this._userValue = userValue;
+ // There's no need to write this back to local storage, since it
+ // came from there; and this avoids infinite event loops.
+ this._parent._notify(this._name);
+ },
+
+ /**
+ * Helper function to find either a Preference or PrefBranch object
+ * given its name. If the name is not found, throws an exception.
+ *
+ * @param {String} prefName the fully-qualified preference name
+ * @return {Object} Either a Preference or PrefBranch object
+ */
+ _findPref: function (prefName) {
+ let branchNames = prefName.split(".");
+ let branch = this;
+
+ for (let branchName of branchNames) {
+ branch = branch._children[branchName];
+ if (!branch) {
+ throw new Error("could not find pref branch " + prefName);
+ }
+ }
+
+ return branch;
+ },
+
+ /**
+ * Helper function to notify any observers when a preference has
+ * changed. This will also notify the parent branch for further
+ * reporting.
+ *
+ * @param {String} relativeName the name of the updated pref,
+ * relative to this branch
+ */
+ _notify: function (relativeName) {
+ for (let domain in this._observers) {
+ if (relativeName === domain || domain === "" ||
+ (domain.endsWith(".") && relativeName.startsWith(domain))) {
+ // Allow mutation while walking.
+ let localList = this._observers[domain].slice();
+ for (let observer of localList) {
+ try {
+ observer.observe(this, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID,
+ relativeName);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+ }
+
+ if (this._parent) {
+ this._parent._notify(this._name + "." + relativeName);
+ }
+ },
+
+ /**
+ * Helper function to create a branch given an array of branch names
+ * representing the path of the new branch.
+ *
+ * @param {Array} branchList an array of strings, one per component
+ * of the branch to be created
+ * @return {PrefBranch} the new branch
+ */
+ _createBranch: function (branchList) {
+ let parent = this;
+ for (let branch of branchList) {
+ if (!parent._children[branch]) {
+ let isParentRoot = !parent.parent;
+ let branchName = (isParentRoot ? "" : parent.root + ".") + branch;
+ parent._children[branch] = new PrefBranch(parent, branch, branchName);
+ }
+ parent = parent._children[branch];
+ }
+ return parent;
+ },
+
+ /**
+ * Create a new preference. The new preference is assumed to be in
+ * local storage already, and the new value is taken from there.
+ *
+ * @param {String} keyName the full-qualified name of the preference.
+ * This is also the name of the key in local storage.
+ * @param {Any} userValue the user value to use if the pref does not exist
+ * @param {Boolean} hasUserValue if a new pref is created, whether
+ * the default value is also a user value
+ * @param {Any} defaultValue the default value to use if the pref
+ * does not exist
+ * @param {Boolean} init if true, then this call is initialization
+ * from local storage and should override the default prefs
+ */
+ _findOrCreatePref: function (keyName, userValue, hasUserValue, defaultValue,
+ init = false) {
+ let branch = this._createBranch(keyName.split("."));
+
+ if (hasUserValue && typeof (userValue) !== typeof (defaultValue)) {
+ throw new Error("inconsistent values when creating " + keyName);
+ }
+
+ let type;
+ switch (typeof (defaultValue)) {
+ case "boolean":
+ type = PREF_BOOL;
+ break;
+ case "number":
+ type = PREF_INT;
+ break;
+ case "string":
+ type = PREF_STRING;
+ break;
+ default:
+ throw new Error("unhandled argument type: " + typeof (defaultValue));
+ }
+
+ if (init || branch._type === PREF_INVALID) {
+ branch._storageUpdated(type, userValue, hasUserValue, defaultValue);
+ } else if (branch._type !== type) {
+ throw new Error("attempt to change type of pref " + keyName);
+ }
+
+ return branch;
+ },
+
+ /**
+ * Helper function that is called when local storage changes. This
+ * updates the preferences and notifies pref observers as needed.
+ *
+ * @param {StorageEvent} event the event representing the local
+ * storage change
+ */
+ _onStorageChange: function (event) {
+ if (event.storageArea !== localStorage) {
+ return;
+ }
+ // Ignore delete events. Not clear what's correct.
+ if (event.key === null || event.newValue === null) {
+ return;
+ }
+
+ let {type, userValue, hasUserValue, defaultValue} =
+ JSON.parse(event.newValue);
+ if (event.oldValue === null) {
+ this._findOrCreatePref(event.key, userValue, hasUserValue, defaultValue);
+ } else {
+ let thePref = this._findPref(event.key);
+ thePref._storageUpdated(type, userValue, hasUserValue, defaultValue);
+ }
+ },
+
+ /**
+ * Helper function to initialize the root PrefBranch.
+ */
+ _initializeRoot: function () {
+ if (Services._defaultPrefsEnabled) {
+ /* eslint-disable no-eval */
+ let devtools = require("raw!prefs!devtools/client/preferences/devtools");
+ eval(devtools);
+ let all = require("raw!prefs!modules/libpref/init/all");
+ eval(all);
+ /* eslint-enable no-eval */
+ }
+
+ // Read the prefs from local storage and create the local
+ // representations.
+ for (let i = 0; i < localStorage.length; ++i) {
+ let keyName = localStorage.key(i);
+ if (keyName.startsWith(PREFIX)) {
+ let {userValue, hasUserValue, defaultValue} =
+ JSON.parse(localStorage.getItem(keyName));
+ this._findOrCreatePref(keyName.slice(PREFIX.length), userValue,
+ hasUserValue, defaultValue, true);
+ }
+ }
+
+ this._onStorageChange = this._onStorageChange.bind(this);
+ window.addEventListener("storage", this._onStorageChange);
+ },
+};
+
+const Services = {
+ _prefs: null,
+
+ // For use by tests. If set to false before Services.prefs is used,
+ // this will disable the reading of the default prefs.
+ _defaultPrefsEnabled: true,
+
+ /**
+ * An implementation of nsIPrefService that is based on local
+ * storage. Only the subset of nsIPrefService that is actually used
+ * by devtools is implemented here. This is lazily instantiated so
+ * that the tests have a chance to disable the loading of default
+ * prefs.
+ */
+ get prefs() {
+ if (!this._prefs) {
+ this._prefs = new PrefBranch(null, "", "");
+ this._prefs._initializeRoot();
+ }
+ return this._prefs;
+ },
+
+ /**
+ * An implementation of Services.appinfo that holds just the
+ * properties needed by devtools.
+ */
+ appinfo: {
+ get OS() {
+ const os = window.navigator.userAgent;
+ if (os) {
+ if (os.includes("Linux")) {
+ return "Linux";
+ } else if (os.includes("Windows")) {
+ return "WINNT";
+ } else if (os.includes("Mac")) {
+ return "Darwin";
+ }
+ }
+ return "Unknown";
+ },
+
+ // It's fine for this to be an approximation.
+ get name() {
+ return window.navigator.userAgent;
+ },
+
+ // It's fine for this to be an approximation.
+ get version() {
+ return window.navigator.appVersion;
+ },
+
+ // This is only used by telemetry, which is disabled for the
+ // content case. So, being totally wrong is ok.
+ get is64Bit() {
+ return true;
+ },
+ },
+
+ /**
+ * A no-op implementation of Services.telemetry. This supports just
+ * the subset of Services.telemetry that is used by devtools.
+ */
+ telemetry: {
+ getHistogramById: function (name) {
+ return {
+ add: () => {}
+ };
+ },
+
+ getKeyedHistogramById: function (name) {
+ return {
+ add: () => {}
+ };
+ },
+ },
+
+ /**
+ * An implementation of Services.focus that holds just the
+ * properties and methods needed by devtools.
+ * @see nsIFocusManager.idl for details.
+ */
+ focus: {
+ // These values match nsIFocusManager in order to make testing a
+ // bit simpler.
+ MOVEFOCUS_FORWARD: 1,
+ MOVEFOCUS_BACKWARD: 2,
+
+ get focusedElement() {
+ if (!document.hasFocus()) {
+ return null;
+ }
+ return document.activeElement;
+ },
+
+ moveFocus: function (window, startElement, type, flags) {
+ if (flags !== 0) {
+ throw new Error("shim Services.focus.moveFocus only accepts flags===0");
+ }
+ if (type !== Services.focus.MOVEFOCUS_FORWARD
+ && type !== Services.focus.MOVEFOCUS_BACKWARD) {
+ throw new Error("shim Services.focus.moveFocus only supports " +
+ " MOVEFOCUS_FORWARD and MOVEFOCUS_BACKWARD");
+ }
+
+ if (!startElement) {
+ startElement = document.activeElement || document;
+ }
+
+ let iter = document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT, {
+ acceptNode: function (node) {
+ let tabIndex = node.getAttribute("tabindex");
+ if (tabIndex === "-1") {
+ return NodeFilter.FILTER_SKIP;
+ }
+ node.focus();
+ if (document.activeElement == node) {
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ return NodeFilter.FILTER_SKIP;
+ }
+ });
+
+ iter.currentNode = startElement;
+
+ // Sets the focus via side effect in the filter.
+ if (type === Services.focus.MOVEFOCUS_FORWARD) {
+ iter.nextNode();
+ } else {
+ iter.previousNode();
+ }
+ },
+ },
+
+ /**
+ * An implementation of Services.wm that provides a shim for
+ * getMostRecentWindow.
+ */
+ wm: {
+ getMostRecentWindow: function () {
+ // Having the returned object implement openUILinkIn is
+ // sufficient for our purposes.
+ return {
+ openUILinkIn: function (url) {
+ window.open(url, "_blank");
+ },
+ };
+ },
+ },
+};
+
+/**
+ * Create a new preference. This is used during startup (see
+ * devtools/client/preferences/devtools.js) to install the
+ * default preferences.
+ *
+ * @param {String} name the name of the preference
+ * @param {Any} value the default value of the preference
+ */
+function pref(name, value) {
+ let thePref = Services.prefs._findOrCreatePref(name, value, true, value);
+ thePref._setDefault(value);
+}
+
+module.exports = Services;
+// This is exported to silence eslint.
+exports.pref = pref;
diff --git a/devtools/client/shared/shim/moz.build b/devtools/client/shared/shim/moz.build
new file mode 100644
index 000000000..dff3e903c
--- /dev/null
+++ b/devtools/client/shared/shim/moz.build
@@ -0,0 +1,13 @@
+# -*- 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/.
+
+DevToolsModules(
+ 'Services.js',
+)
+
+MOCHITEST_MANIFESTS += [
+ 'test/mochitest.ini',
+]
diff --git a/devtools/client/shared/shim/test/.eslintrc.js b/devtools/client/shared/shim/test/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/shared/shim/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/shared/shim/test/file_service_wm.html b/devtools/client/shared/shim/test/file_service_wm.html
new file mode 100644
index 000000000..24753e710
--- /dev/null
+++ b/devtools/client/shared/shim/test/file_service_wm.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script>
+// Silence eslint complaint about the onload below.
+/* eslint-disable no-unused-vars */
+
+"use strict";
+
+function load() {
+ (window.opener || window.parent).hurray();
+ window.close();
+}
+</script>
+</head>
+
+<body onload='load()'>
+</body>
+
+</html>
diff --git a/devtools/client/shared/shim/test/mochitest.ini b/devtools/client/shared/shim/test/mochitest.ini
new file mode 100644
index 000000000..4ba5cd6c1
--- /dev/null
+++ b/devtools/client/shared/shim/test/mochitest.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files =
+ file_service_wm.html
+ prefs-wrapper.js
+
+[test_service_appinfo.html]
+[test_service_focus.html]
+[test_service_prefs.html]
+[test_service_prefs_defaults.html]
+[test_service_wm.html]
diff --git a/devtools/client/shared/shim/test/prefs-wrapper.js b/devtools/client/shared/shim/test/prefs-wrapper.js
new file mode 100644
index 000000000..057e12d17
--- /dev/null
+++ b/devtools/client/shared/shim/test/prefs-wrapper.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// A wrapper for Services.prefs that compares our shim content
+// implementation with the real service.
+
+// We assume we're loaded in a global where Services was already loaded.
+/* globals isDeeply, Services */
+
+"use strict";
+
+function setMethod(methodName, prefName, value) {
+ let savedException;
+ let prefThrew = false;
+ try {
+ Services.prefs[methodName](prefName, value);
+ } catch (e) {
+ prefThrew = true;
+ savedException = e;
+ }
+
+ let realThrew = false;
+ try {
+ SpecialPowers[methodName](prefName, value);
+ } catch (e) {
+ realThrew = true;
+ savedException = e;
+ }
+
+ is(prefThrew, realThrew, methodName + " [throw check]");
+ if (prefThrew || realThrew) {
+ throw savedException;
+ }
+}
+
+function getMethod(methodName, prefName) {
+ let prefThrew = false;
+ let prefValue = undefined;
+ let savedException;
+ try {
+ prefValue = Services.prefs[methodName](prefName);
+ } catch (e) {
+ prefThrew = true;
+ savedException = e;
+ }
+
+ let realValue = undefined;
+ let realThrew = false;
+ try {
+ realValue = SpecialPowers[methodName](prefName);
+ } catch (e) {
+ realThrew = true;
+ savedException = e;
+ }
+
+ is(prefThrew, realThrew, methodName + " [throw check]");
+ isDeeply(prefValue, realValue, methodName + " [equality]");
+ if (prefThrew || realThrew) {
+ throw savedException;
+ }
+
+ return prefValue;
+}
+
+var WrappedPrefs = {};
+
+for (let method of ["getPrefType", "getBoolPref", "getCharPref", "getIntPref",
+ "clearUserPref"]) {
+ WrappedPrefs[method] = getMethod.bind(null, method);
+}
+
+for (let method of ["setBoolPref", "setCharPref", "setIntPref"]) {
+ WrappedPrefs[method] = setMethod.bind(null, method);
+}
+
+// Silence eslint.
+exports.WrappedPrefs = WrappedPrefs;
diff --git a/devtools/client/shared/shim/test/test_service_appinfo.html b/devtools/client/shared/shim/test/test_service_appinfo.html
new file mode 100644
index 000000000..bc659cb42
--- /dev/null
+++ b/devtools/client/shared/shim/test/test_service_appinfo.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1265802
+-->
+<head>
+ <title>Test for Bug 1265802 - replace Services.appinfo</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+ <script type="application/javascript;version=1.8">
+ "use strict";
+ var exports = {}
+ var module = {exports};
+ </script>
+
+ <script type="application/javascript;version=1.8"
+ src="resource://devtools/client/shared/shim/Services.js"></script>
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+is(Services.appinfo.OS, SpecialPowers.Services.appinfo.OS,
+ "check that Services.appinfo.OS shim matches platform");
+</script>
+</body>
diff --git a/devtools/client/shared/shim/test/test_service_focus.html b/devtools/client/shared/shim/test/test_service_focus.html
new file mode 100644
index 000000000..d720e0b53
--- /dev/null
+++ b/devtools/client/shared/shim/test/test_service_focus.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1278473
+-->
+<head>
+ <title>Test for Bug 1278473 - replace Services.focus</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+ <script type="application/javascript;version=1.8">
+ "use strict";
+ var exports = {}
+ var module = {exports};
+ </script>
+
+ <script type="application/javascript;version=1.8"
+ src="resource://devtools/client/shared/shim/Services.js"></script>
+</head>
+<body>
+ <span>
+ <span id="start" testvalue="0" tabindex="0"> </span>
+ <label>
+ <input testvalue="1" type="radio">Hi</input>
+ </label>
+ <label>
+ <input type="radio" tabindex="-1">Bye</input>
+ </label>
+ <label style="display: none">
+ <input id="button3" type="radio" tabindex="-1">Invisible</input>
+ </label>
+ <input id="button4" type="radio" disabled="true">Disabled</input>
+ <span testvalue="2" tabindex="0"> </span>
+ </span>
+
+<script type="application/javascript;version=1.8">
+ "use strict";
+
+ // The test assumes these are identical, so assert it here.
+ is(Services.focus.MOVEFOCUS_BACKWARD, SpecialPowers.Services.focus.MOVEFOCUS_BACKWARD,
+ "check MOVEFOCUS_BACKWARD");
+ is(Services.focus.MOVEFOCUS_FORWARD, SpecialPowers.Services.focus.MOVEFOCUS_FORWARD,
+ "check MOVEFOCUS_FORWARD");
+
+ function moveFocus(element, type, expect) {
+ let current = document.activeElement;
+ const suffix = "(type=" + type + ", to=" + expect + ")";
+
+ // First try with the platform implementation.
+ SpecialPowers.Services.focus.moveFocus(window, element, type, 0);
+ is(document.activeElement.getAttribute("testvalue"), expect,
+ "platform moveFocus " + suffix);
+
+ // Reset the focus and try again with the shim.
+ current.focus();
+ is(document.activeElement, current, "reset " + suffix);
+
+ Services.focus.moveFocus(window, element, type, 0);
+ is(document.activeElement.getAttribute("testvalue"), expect,
+ "shim moveFocus " + suffix);
+ }
+
+ let start = document.querySelector("#start");
+ start.focus();
+ is(document.activeElement.getAttribute("testvalue"), "0", "initial focus");
+
+ moveFocus(null, Services.focus.MOVEFOCUS_FORWARD, "1");
+ moveFocus(null, Services.focus.MOVEFOCUS_FORWARD, "2");
+ let end = document.activeElement;
+ moveFocus(null, Services.focus.MOVEFOCUS_BACKWARD, "1");
+ moveFocus(null, Services.focus.MOVEFOCUS_BACKWARD, "0");
+
+ moveFocus(start, Services.focus.MOVEFOCUS_FORWARD, "1");
+ moveFocus(end, Services.focus.MOVEFOCUS_BACKWARD, "1");
+</script>
+</body>
diff --git a/devtools/client/shared/shim/test/test_service_prefs.html b/devtools/client/shared/shim/test/test_service_prefs.html
new file mode 100644
index 000000000..99e827dfd
--- /dev/null
+++ b/devtools/client/shared/shim/test/test_service_prefs.html
@@ -0,0 +1,244 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1265808
+-->
+<head>
+ <title>Test for Bug 1265808 - replace Services.prefs</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+<script type="application/javascript;version=1.8">
+"use strict";
+var exports = {}
+var module = {exports};
+
+ // Add some starter prefs.
+localStorage.setItem("Services.prefs:devtools.branch1.somebool", JSON.stringify({
+ // bool
+ type: 128,
+ defaultValue: false,
+ hasUserValue: false,
+ userValue: false
+}));
+
+localStorage.setItem("Services.prefs:devtools.branch1.somestring", JSON.stringify({
+ // string
+ type: 32,
+ defaultValue: "dinosaurs",
+ hasUserValue: true,
+ userValue: "elephants"
+}));
+
+localStorage.setItem("Services.prefs:devtools.branch2.someint", JSON.stringify({
+ // string
+ type: 64,
+ defaultValue: -16,
+ hasUserValue: false,
+ userValue: null
+}));
+
+</script>
+
+ <script type="application/javascript;version=1.8"
+ src="prefs-wrapper.js"></script>
+ <script type="application/javascript;version=1.8"
+ src="resource://devtools/client/shared/shim/Services.js"></script>
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+function do_tests() {
+ // We can't load the defaults in this context.
+ Services._defaultPrefsEnabled = false;
+
+ is(Services.prefs.getBoolPref("devtools.branch1.somebool"), false,
+ "bool pref value");
+ Services.prefs.setBoolPref("devtools.branch1.somebool", true);
+ is(Services.prefs.getBoolPref("devtools.branch1.somebool"), true,
+ "bool pref value after setting");
+
+ let threw;
+
+ try {
+ threw = false;
+ WrappedPrefs.getIntPref("devtools.branch1.somebool");
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "type-checking for bool pref");
+
+ try {
+ threw = false;
+ Services.prefs.setIntPref("devtools.branch1.somebool", 27);
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "type-checking for setting bool pref");
+
+ try {
+ threw = false;
+ Services.prefs.setBoolPref("devtools.branch1.somebool", 27);
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "setting bool pref to wrong type");
+
+ try {
+ threw = false;
+ Services.prefs.getCharPref("devtools.branch2.someint");
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "type-checking for int pref");
+
+ try {
+ threw = false;
+ Services.prefs.setCharPref("devtools.branch2.someint", "whatever");
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "type-checking for setting int pref");
+
+ try {
+ threw = false;
+ Services.prefs.setIntPref("devtools.branch2.someint", "whatever");
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "setting int pref to wrong type");
+
+ try {
+ threw = false;
+ Services.prefs.getBoolPref("devtools.branch1.somestring");
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "type-checking for char pref");
+
+ try {
+ threw = false;
+ Services.prefs.setBoolPref("devtools.branch1.somestring", true);
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "type-checking for setting char pref");
+
+ try {
+ threw = false;
+ Services.prefs.setCharPref("devtools.branch1.somestring", true);
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "setting char pref to wrong type");
+
+ is(Services.prefs.getPrefType("devtools.branch1.somebool"),
+ Services.prefs.PREF_BOOL, "type of bool pref");
+ is(Services.prefs.getPrefType("devtools.branch2.someint"),
+ Services.prefs.PREF_INT, "type of int pref");
+ is(Services.prefs.getPrefType("devtools.branch1.somestring"),
+ Services.prefs.PREF_STRING, "type of string pref");
+
+ WrappedPrefs.setBoolPref("devtools.branch1.somebool", true);
+ ok(WrappedPrefs.getBoolPref("devtools.branch1.somebool"), "set bool pref");
+ WrappedPrefs.setIntPref("devtools.branch2.someint", -93);
+ is(WrappedPrefs.getIntPref("devtools.branch2.someint"), -93, "set int pref");
+ WrappedPrefs.setCharPref("devtools.branch1.somestring", "hello");
+ is(WrappedPrefs.getCharPref("devtools.branch1.somestring"), "hello",
+ "set string pref");
+
+ Services.prefs.clearUserPref("devtools.branch1.somestring");
+ is(Services.prefs.getCharPref("devtools.branch1.somestring"), "dinosaurs",
+ "clear string pref");
+
+ ok(Services.prefs.prefHasUserValue("devtools.branch1.somebool"),
+ "bool pref has user value");
+ ok(!Services.prefs.prefHasUserValue("devtools.branch1.somestring"),
+ "string pref does not have user value");
+
+
+ Services.prefs.savePrefFile(null);
+ ok(true, "saved pref file without error");
+
+
+ let branch0 = Services.prefs.getBranch(null);
+ let branch1 = Services.prefs.getBranch("devtools.branch1.");
+
+ branch1.setCharPref("somestring", "octopus");
+ Services.prefs.setCharPref("devtools.branch1.somestring", "octopus");
+ is(Services.prefs.getCharPref("devtools.branch1.somestring"), "octopus",
+ "set correctly via branch");
+ is(branch0.getCharPref("devtools.branch1.somestring"), "octopus",
+ "get via base branch");
+ is(branch1.getCharPref("somestring"), "octopus", "get via branch");
+
+
+ let notifications = {};
+ let clearNotificationList = () => { notifications = {}; }
+ let observer = {
+ observe: function (subject, topic, data) {
+ notifications[data] = true;
+ }
+ };
+
+ branch0.addObserver("devtools.branch1", null, null);
+ branch0.addObserver("devtools.branch1.", observer, false);
+ branch1.addObserver("", observer, false);
+
+ Services.prefs.setCharPref("devtools.branch1.somestring", "elf owl");
+ isDeeply(notifications, {
+ "devtools.branch1.somestring": true,
+ "somestring": true
+ }, "notifications sent to two listeners");
+
+ clearNotificationList();
+ Services.prefs.setIntPref("devtools.branch2.someint", 1729);
+ isDeeply(notifications, {}, "no notifications sent");
+
+ clearNotificationList();
+ branch0.removeObserver("devtools.branch1.", observer);
+ Services.prefs.setCharPref("devtools.branch1.somestring", "tapir");
+ isDeeply(notifications, {
+ "somestring": true
+ }, "removeObserver worked");
+
+ clearNotificationList();
+ branch0.addObserver("devtools.branch1.somestring", observer, false);
+ Services.prefs.setCharPref("devtools.branch1.somestring", "northern shoveler");
+ isDeeply(notifications, {
+ "devtools.branch1.somestring": true,
+ "somestring": true
+ }, "notifications sent to two listeners");
+ branch0.removeObserver("devtools.branch1.somestring", observer);
+
+ // Make sure we update if the pref change comes from somewhere else.
+ clearNotificationList();
+ pref("devtools.branch1.someotherstring", "lazuli bunting");
+ isDeeply(notifications, {
+ "someotherstring": true
+ }, "pref worked");
+
+ // Regression test for bug 1296427.
+ pref("devtools.hud.loglimit", 1000);
+ pref("devtools.hud.loglimit.network", 1000);
+
+ // Clean up.
+ localStorage.clear();
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ {"set": [
+ ["devtools.branch1.somestring", "elephants"],
+ ["devtools.branch1.somebool", false],
+ ["devtools.branch2.someint", "-16"],
+ ]},
+ do_tests);
+
+</script>
+</body>
diff --git a/devtools/client/shared/shim/test/test_service_prefs_defaults.html b/devtools/client/shared/shim/test/test_service_prefs_defaults.html
new file mode 100644
index 000000000..d8933b74a
--- /dev/null
+++ b/devtools/client/shared/shim/test/test_service_prefs_defaults.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1309384
+-->
+<head>
+ <title>Test for Bug 1309384 - Services.prefs replacement defaults handling</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+<script type="application/javascript;version=1.8">
+"use strict";
+var exports = {}
+var module = {exports};
+
+// Allow one require("raw!prefs...") to return some defaults, with the
+// others being ignored.
+var firstTime = true;
+function require(something) {
+ if (!something.startsWith("raw!prefs!")) {
+ throw new Error("whoops");
+ }
+ if (!firstTime) {
+ return "";
+ }
+ firstTime = false;
+ return "pref('pref1', 'pref1default');\n" +
+ "pref('pref2', 'pref2default');\n" +
+ "pref('pref3', 'pref3default');\n";
+}
+
+// Pretend that one of the prefs was modifed by the user in an earlier session.
+localStorage.setItem("Services.prefs:pref3", JSON.stringify({
+ // string
+ type: 32,
+ defaultValue: "pref3default",
+ hasUserValue: true,
+ userValue: "glass winged butterfly"
+}));
+
+</script>
+
+ <script type="application/javascript;version=1.8"
+ src="resource://devtools/client/shared/shim/Services.js"></script>
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+is(Services.prefs.getCharPref("pref1"), "pref1default", "pref1 value");
+is(Services.prefs.getCharPref("pref2"), "pref2default", "pref2 value");
+is(Services.prefs.getCharPref("pref3"), "glass winged butterfly", "pref3 value");
+
+// Only pref3 should be in local storage at this point.
+is(localStorage.length, 1, "local storage is correct");
+
+Services.prefs.setCharPref("pref2", "pref2override");
+
+// Check that a default pref can be overridden properly
+
+// Workaround to reset the prefs helper and force it to read defaults & overrides again.
+Services._prefs = null;
+is(Services.prefs.getCharPref("pref2"), "pref2override", "pref2 value overridden");
+
+// Clean up.
+localStorage.clear();
+
+</script>
+</body>
diff --git a/devtools/client/shared/shim/test/test_service_wm.html b/devtools/client/shared/shim/test/test_service_wm.html
new file mode 100644
index 000000000..4db602f7e
--- /dev/null
+++ b/devtools/client/shared/shim/test/test_service_wm.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1310279
+-->
+<head>
+ <title>Test for Bug 1310279 - replace Services.wm</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+ <script type="application/javascript;version=1.8">
+ "use strict";
+ var exports = {}
+ var module = {exports};
+ </script>
+
+ <script type="application/javascript;version=1.8"
+ src="resource://devtools/client/shared/shim/Services.js"></script>
+</head>
+<body>
+
+<script type="application/javascript;version=1.8">
+ "use strict";
+
+ function hurray(window) {
+ ok(true, "window loaded");
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ Services.wm.getMostRecentWindow().openUILinkIn("file_service_wm.html");
+
+</script>
+</body>
diff --git a/devtools/client/shared/source-utils.js b/devtools/client/shared/source-utils.js
new file mode 100644
index 000000000..974fd272d
--- /dev/null
+++ b/devtools/client/shared/source-utils.js
@@ -0,0 +1,328 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+
+const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
+const UNKNOWN_SOURCE_STRING = l10n.getStr("frame.unknownSource");
+
+// Character codes used in various parsing helper functions.
+const CHAR_CODE_A = "a".charCodeAt(0);
+const CHAR_CODE_C = "c".charCodeAt(0);
+const CHAR_CODE_D = "d".charCodeAt(0);
+const CHAR_CODE_E = "e".charCodeAt(0);
+const CHAR_CODE_F = "f".charCodeAt(0);
+const CHAR_CODE_H = "h".charCodeAt(0);
+const CHAR_CODE_I = "i".charCodeAt(0);
+const CHAR_CODE_J = "j".charCodeAt(0);
+const CHAR_CODE_L = "l".charCodeAt(0);
+const CHAR_CODE_M = "m".charCodeAt(0);
+const CHAR_CODE_O = "o".charCodeAt(0);
+const CHAR_CODE_P = "p".charCodeAt(0);
+const CHAR_CODE_R = "r".charCodeAt(0);
+const CHAR_CODE_S = "s".charCodeAt(0);
+const CHAR_CODE_T = "t".charCodeAt(0);
+const CHAR_CODE_U = "u".charCodeAt(0);
+const CHAR_CODE_COLON = ":".charCodeAt(0);
+const CHAR_CODE_SLASH = "/".charCodeAt(0);
+const CHAR_CODE_CAP_S = "S".charCodeAt(0);
+
+// The cache used in the `parseURL` function.
+const gURLStore = new Map();
+// The cache used in the `getSourceNames` function.
+const gSourceNamesStore = new Map();
+
+/**
+ * Takes a string and returns an object containing all the properties
+ * available on an URL instance, with additional properties (fileName),
+ * Leverages caching.
+ *
+ * @param {String} location
+ * @return {Object?} An object containing most properties available
+ * in https://developer.mozilla.org/en-US/docs/Web/API/URL
+ */
+
+function parseURL(location) {
+ let url = gURLStore.get(location);
+
+ if (url !== void 0) {
+ return url;
+ }
+
+ try {
+ url = new URL(location);
+ // The callers were generally written to expect a URL from
+ // sdk/url, which is subtly different. So, work around some
+ // important differences here.
+ url = {
+ href: url.href,
+ protocol: url.protocol,
+ host: url.host,
+ hostname: url.hostname,
+ port: url.port || null,
+ pathname: url.pathname,
+ search: url.search,
+ hash: url.hash,
+ username: url.username,
+ password: url.password,
+ origin: url.origin,
+ };
+
+ // Definitions:
+ // Example: https://foo.com:8888/file.js
+ // `hostname`: "foo.com"
+ // `host`: "foo.com:8888"
+ let isChrome = isChromeScheme(location);
+
+ url.fileName = url.pathname ?
+ (url.pathname.slice(url.pathname.lastIndexOf("/") + 1) || "/") :
+ "/";
+
+ if (isChrome) {
+ url.hostname = null;
+ url.host = null;
+ }
+
+ gURLStore.set(location, url);
+ return url;
+ } catch (e) {
+ gURLStore.set(location, null);
+ return null;
+ }
+}
+
+/**
+ * Parse a source into a short and long name as well as a host name.
+ *
+ * @param {String} source
+ * The source to parse. Can be a URI or names like "(eval)" or
+ * "self-hosted".
+ * @return {Object}
+ * An object with the following properties:
+ * - {String} short: A short name for the source.
+ * - "http://page.com/test.js#go?q=query" -> "test.js"
+ * - {String} long: The full, long name for the source, with
+ hash/query stripped.
+ * - "http://page.com/test.js#go?q=query" -> "http://page.com/test.js"
+ * - {String?} host: If available, the host name for the source.
+ * - "http://page.com/test.js#go?q=query" -> "page.com"
+ */
+function getSourceNames(source) {
+ let data = gSourceNamesStore.get(source);
+
+ if (data) {
+ return data;
+ }
+
+ let short, long, host;
+ const sourceStr = source ? String(source) : "";
+
+ // If `data:...` uri
+ if (isDataScheme(sourceStr)) {
+ let commaIndex = sourceStr.indexOf(",");
+ if (commaIndex > -1) {
+ // The `short` name for a data URI becomes `data:` followed by the actual
+ // encoded content, omitting the MIME type, and charset.
+ short = `data:${sourceStr.substring(commaIndex + 1)}`.slice(0, 100);
+ let result = { short, long: sourceStr };
+ gSourceNamesStore.set(source, result);
+ return result;
+ }
+ }
+
+ // If Scratchpad URI, like "Scratchpad/1"; no modifications,
+ // and short/long are the same.
+ if (isScratchpadScheme(sourceStr)) {
+ let result = { short: sourceStr, long: sourceStr };
+ gSourceNamesStore.set(source, result);
+ return result;
+ }
+
+ const parsedUrl = parseURL(sourceStr);
+
+ if (!parsedUrl) {
+ // Malformed URI.
+ long = sourceStr;
+ short = sourceStr.slice(0, 100);
+ } else {
+ host = parsedUrl.host;
+
+ long = parsedUrl.href;
+ if (parsedUrl.hash) {
+ long = long.replace(parsedUrl.hash, "");
+ }
+ if (parsedUrl.search) {
+ long = long.replace(parsedUrl.search, "");
+ }
+
+ short = parsedUrl.fileName;
+ // If `short` is just a slash, and we actually have a path,
+ // strip the slash and parse again to get a more useful short name.
+ // e.g. "http://foo.com/bar/" -> "bar", rather than "/"
+ if (short === "/" && parsedUrl.pathname !== "/") {
+ short = parseURL(long.replace(/\/$/, "")).fileName;
+ }
+ }
+
+ if (!short) {
+ if (!long) {
+ long = UNKNOWN_SOURCE_STRING;
+ }
+ short = long.slice(0, 100);
+ }
+
+ let result = { short, long, host };
+ gSourceNamesStore.set(source, result);
+ return result;
+}
+
+// For the functions below, we assume that we will never access the location
+// argument out of bounds, which is indeed the vast majority of cases.
+//
+// They are written this way because they are hot. Each frame is checked for
+// being content or chrome when processing the profile.
+
+function isColonSlashSlash(location, i = 0) {
+ return location.charCodeAt(++i) === CHAR_CODE_COLON &&
+ location.charCodeAt(++i) === CHAR_CODE_SLASH &&
+ location.charCodeAt(++i) === CHAR_CODE_SLASH;
+}
+
+/**
+ * Checks for a Scratchpad URI, like "Scratchpad/1"
+ */
+function isScratchpadScheme(location, i = 0) {
+ return location.charCodeAt(i) === CHAR_CODE_CAP_S &&
+ location.charCodeAt(++i) === CHAR_CODE_C &&
+ location.charCodeAt(++i) === CHAR_CODE_R &&
+ location.charCodeAt(++i) === CHAR_CODE_A &&
+ location.charCodeAt(++i) === CHAR_CODE_T &&
+ location.charCodeAt(++i) === CHAR_CODE_C &&
+ location.charCodeAt(++i) === CHAR_CODE_H &&
+ location.charCodeAt(++i) === CHAR_CODE_P &&
+ location.charCodeAt(++i) === CHAR_CODE_A &&
+ location.charCodeAt(++i) === CHAR_CODE_D &&
+ location.charCodeAt(++i) === CHAR_CODE_SLASH;
+}
+
+function isDataScheme(location, i = 0) {
+ return location.charCodeAt(i) === CHAR_CODE_D &&
+ location.charCodeAt(++i) === CHAR_CODE_A &&
+ location.charCodeAt(++i) === CHAR_CODE_T &&
+ location.charCodeAt(++i) === CHAR_CODE_A &&
+ location.charCodeAt(++i) === CHAR_CODE_COLON;
+}
+
+function isContentScheme(location, i = 0) {
+ let firstChar = location.charCodeAt(i);
+
+ switch (firstChar) {
+ // "http://" or "https://"
+ case CHAR_CODE_H:
+ if (location.charCodeAt(++i) === CHAR_CODE_T &&
+ location.charCodeAt(++i) === CHAR_CODE_T &&
+ location.charCodeAt(++i) === CHAR_CODE_P) {
+ if (location.charCodeAt(i + 1) === CHAR_CODE_S) {
+ ++i;
+ }
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ // "file://"
+ case CHAR_CODE_F:
+ if (location.charCodeAt(++i) === CHAR_CODE_I &&
+ location.charCodeAt(++i) === CHAR_CODE_L &&
+ location.charCodeAt(++i) === CHAR_CODE_E) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ // "app://"
+ case CHAR_CODE_A:
+ if (location.charCodeAt(++i) == CHAR_CODE_P &&
+ location.charCodeAt(++i) == CHAR_CODE_P) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ default:
+ return false;
+ }
+}
+
+function isChromeScheme(location, i = 0) {
+ let firstChar = location.charCodeAt(i);
+
+ switch (firstChar) {
+ // "chrome://"
+ case CHAR_CODE_C:
+ if (location.charCodeAt(++i) === CHAR_CODE_H &&
+ location.charCodeAt(++i) === CHAR_CODE_R &&
+ location.charCodeAt(++i) === CHAR_CODE_O &&
+ location.charCodeAt(++i) === CHAR_CODE_M &&
+ location.charCodeAt(++i) === CHAR_CODE_E) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ // "resource://"
+ case CHAR_CODE_R:
+ if (location.charCodeAt(++i) === CHAR_CODE_E &&
+ location.charCodeAt(++i) === CHAR_CODE_S &&
+ location.charCodeAt(++i) === CHAR_CODE_O &&
+ location.charCodeAt(++i) === CHAR_CODE_U &&
+ location.charCodeAt(++i) === CHAR_CODE_R &&
+ location.charCodeAt(++i) === CHAR_CODE_C &&
+ location.charCodeAt(++i) === CHAR_CODE_E) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ // "jar:file://"
+ case CHAR_CODE_J:
+ if (location.charCodeAt(++i) === CHAR_CODE_A &&
+ location.charCodeAt(++i) === CHAR_CODE_R &&
+ location.charCodeAt(++i) === CHAR_CODE_COLON &&
+ location.charCodeAt(++i) === CHAR_CODE_F &&
+ location.charCodeAt(++i) === CHAR_CODE_I &&
+ location.charCodeAt(++i) === CHAR_CODE_L &&
+ location.charCodeAt(++i) === CHAR_CODE_E) {
+ return isColonSlashSlash(location, i);
+ }
+ return false;
+
+ default:
+ return false;
+ }
+}
+
+/**
+ * A utility method to get the file name from a sourcemapped location
+ * The sourcemap location can be in any form. This method returns a
+ * formatted file name for different cases like Windows or OSX.
+ * @param source
+ * @returns String
+ */
+function getSourceMappedFile(source) {
+ // If sourcemapped source is a OSX path, return
+ // the characters after last "/".
+ // If sourcemapped source is a Windowss path, return
+ // the characters after last "\\".
+ if (source.lastIndexOf("/") >= 0) {
+ source = source.slice(source.lastIndexOf("/") + 1);
+ } else if (source.lastIndexOf("\\") >= 0) {
+ source = source.slice(source.lastIndexOf("\\") + 1);
+ }
+ return source;
+}
+
+exports.parseURL = parseURL;
+exports.getSourceNames = getSourceNames;
+exports.isScratchpadScheme = isScratchpadScheme;
+exports.isChromeScheme = isChromeScheme;
+exports.isContentScheme = isContentScheme;
+exports.isDataScheme = isDataScheme;
+exports.getSourceMappedFile = getSourceMappedFile;
diff --git a/devtools/client/shared/splitview.css b/devtools/client/shared/splitview.css
new file mode 100644
index 000000000..de9c4e330
--- /dev/null
+++ b/devtools/client/shared/splitview.css
@@ -0,0 +1,83 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+box,
+.splitview-nav {
+ -moz-box-flex: 1;
+ -moz-box-orient: vertical;
+}
+
+.splitview-nav-container {
+ -moz-box-pack: center;
+}
+
+.loading .splitview-nav-container > .placeholder {
+ display: none !important;
+}
+
+.splitview-controller,
+.splitview-main {
+ -moz-box-flex: 0;
+}
+
+.splitview-controller {
+ min-height: 3em;
+ max-height: 14em;
+ max-width: 400px;
+ min-width: 200px;
+}
+
+.splitview-nav {
+ display: -moz-box;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+/* only the active details pane is shown */
+.splitview-side-details > * {
+ display: none;
+}
+.splitview-side-details > .splitview-active {
+ display: -moz-box;
+}
+
+/* this is to keep in sync with SplitView.jsm's LANDSCAPE_MEDIA_QUERY */
+@media (min-width: 701px) {
+ .splitview-root {
+ -moz-box-orient: horizontal;
+ }
+ .splitview-controller {
+ max-height: none;
+ }
+ .splitview-details {
+ display: none;
+ }
+ .splitview-details.splitview-active {
+ display: -moz-box;
+ }
+}
+
+/* filtered items are hidden */
+ol.splitview-nav > li.splitview-filtered {
+ display: none;
+}
+
+/* "empty list" and "all filtered" placeholders are hidden */
+.splitview-nav:empty,
+.splitview-nav.splitview-all-filtered,
+.splitview-nav + .splitview-nav.placeholder {
+ display: none;
+}
+.splitview-nav.splitview-all-filtered ~ .splitview-nav.placeholder.all-filtered,
+.splitview-nav:empty ~ .splitview-nav.placeholder.empty {
+ display: -moz-box;
+}
+
+/* portrait mode */
+@media (max-width: 700px) {
+ .splitview-controller {
+ max-width: none;
+ }
+}
diff --git a/devtools/client/shared/suggestion-picker.js b/devtools/client/shared/suggestion-picker.js
new file mode 100644
index 000000000..6155eb3bf
--- /dev/null
+++ b/devtools/client/shared/suggestion-picker.js
@@ -0,0 +1,176 @@
+/* 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";
+
+/**
+ * Allows to find the lowest ranking index in an index
+ * of suggestions, by comparing it to another array of "most relevant" items
+ * which has been sorted by relevance.
+ *
+ * Example usage:
+ * let sortedBrowsers = ["firefox", "safari", "edge", "chrome"];
+ * let myBrowsers = ["brave", "chrome", "firefox"];
+ * let bestBrowserIndex = findMostRelevantIndex(myBrowsers, sortedBrowsers);
+ * // returns "2", the index of firefox in myBrowsers array
+ *
+ * @param {Array} items
+ * Array of items to compare against sortedItems.
+ * @param {Array} sortedItems
+ * Array of sorted items that suggestions are evaluated against. Array
+ * should be sorted by relevance, most relevant item first.
+ * @return {Number}
+ */
+function findMostRelevantIndex(items, sortedItems) {
+ if (!Array.isArray(items) || !Array.isArray(sortedItems)) {
+ throw new Error("Please provide valid items and sortedItems arrays.");
+ }
+
+ // If the items array is empty, no valid index can be found.
+ if (!items.length) {
+ return -1;
+ }
+
+ // Return 0 if no match was found in the suggestion list.
+ let bestIndex = 0;
+ let lowestIndex = Infinity;
+ items.forEach((item, i) => {
+ let index = sortedItems.indexOf(item);
+ if (index !== -1 && index <= lowestIndex) {
+ lowestIndex = index;
+ bestIndex = i;
+ }
+ });
+
+ return bestIndex;
+}
+
+/**
+ * Top 100 CSS property names sorted by relevance, most relevant first.
+ *
+ * List based on the one used by Chrome devtools :
+ * https://code.google.com/p/chromium/codesearch#chromium/src/third_party/
+ * WebKit/Source/devtools/front_end/sdk/CSSMetadata.js&q=CSSMetadata&
+ * sq=package:chromium&type=cs&l=676
+ *
+ * The data is a mix of https://www.chromestatus.com/metrics/css and usage
+ * metrics from popular sites collected via https://gist.github.com/NV/3751436
+ *
+ * @type {Array}
+ */
+const SORTED_CSS_PROPERTIES = [
+ "width",
+ "margin",
+ "height",
+ "padding",
+ "font-size",
+ "border",
+ "display",
+ "position",
+ "text-align",
+ "background",
+ "background-color",
+ "top",
+ "font-weight",
+ "color",
+ "overflow",
+ "font-family",
+ "margin-top",
+ "float",
+ "opacity",
+ "cursor",
+ "left",
+ "text-decoration",
+ "background-image",
+ "right",
+ "line-height",
+ "margin-left",
+ "visibility",
+ "margin-bottom",
+ "padding-top",
+ "z-index",
+ "margin-right",
+ "background-position",
+ "vertical-align",
+ "padding-left",
+ "background-repeat",
+ "border-bottom",
+ "padding-right",
+ "border-top",
+ "padding-bottom",
+ "clear",
+ "white-space",
+ "bottom",
+ "border-color",
+ "max-width",
+ "border-radius",
+ "border-right",
+ "outline",
+ "border-left",
+ "font-style",
+ "content",
+ "min-width",
+ "min-height",
+ "box-sizing",
+ "list-style",
+ "border-width",
+ "box-shadow",
+ "font",
+ "border-collapse",
+ "text-shadow",
+ "text-indent",
+ "border-style",
+ "max-height",
+ "text-overflow",
+ "background-size",
+ "text-transform",
+ "zoom",
+ "list-style-type",
+ "border-spacing",
+ "word-wrap",
+ "overflow-y",
+ "transition",
+ "border-top-color",
+ "border-bottom-color",
+ "border-top-right-radius",
+ "letter-spacing",
+ "border-top-left-radius",
+ "border-bottom-left-radius",
+ "border-bottom-right-radius",
+ "overflow-x",
+ "pointer-events",
+ "border-right-color",
+ "transform",
+ "border-top-width",
+ "border-bottom-width",
+ "border-right-width",
+ "direction",
+ "animation",
+ "border-left-color",
+ "clip",
+ "border-left-width",
+ "table-layout",
+ "src",
+ "resize",
+ "word-break",
+ "background-clip",
+ "transform-origin",
+ "font-variant",
+ "filter",
+ "quotes",
+ "word-spacing"
+];
+
+/**
+ * Helper to find the most relevant CSS property name in a provided array.
+ *
+ * @param items {Array}
+ * Array of CSS property names.
+ */
+function findMostRelevantCssPropertyIndex(items) {
+ return findMostRelevantIndex(items, SORTED_CSS_PROPERTIES);
+}
+
+exports.findMostRelevantIndex = findMostRelevantIndex;
+exports.findMostRelevantCssPropertyIndex = findMostRelevantCssPropertyIndex;
diff --git a/devtools/client/shared/telemetry.js b/devtools/client/shared/telemetry.js
new file mode 100644
index 000000000..64a299581
--- /dev/null
+++ b/devtools/client/shared/telemetry.js
@@ -0,0 +1,341 @@
+/* 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/. */
+
+/**
+ * Telemetry.
+ *
+ * To add metrics for a tool:
+ *
+ * 1. Create count, flag, and exponential entries in
+ * toolkit/components/telemetry/Histograms.json. Each type is optional but it
+ * is best if all three can be included.
+ *
+ * 2. Add your chart entries to devtools/client/shared/telemetry.js
+ * (Telemetry.prototype._histograms):
+ * mytoolname: {
+ * histogram: "DEVTOOLS_MYTOOLNAME_OPENED_COUNT",
+ * timerHistogram: "DEVTOOLS_MYTOOLNAME_TIME_ACTIVE_SECONDS"
+ * },
+ *
+ * 3. Include this module at the top of your tool. Use:
+ * let Telemetry = require("devtools/client/shared/telemetry")
+ *
+ * 4. Create a telemetry instance in your tool's constructor:
+ * this._telemetry = new Telemetry();
+ *
+ * 5. When your tool is opened call:
+ * this._telemetry.toolOpened("mytoolname");
+ *
+ * 6. When your tool is closed call:
+ * this._telemetry.toolClosed("mytoolname");
+ *
+ * Note:
+ * You can view telemetry stats for your local Firefox instance via
+ * about:telemetry.
+ *
+ * You can view telemetry stats for large groups of Firefox users at
+ * telemetry.mozilla.org.
+ */
+
+"use strict";
+
+const TOOLS_OPENED_PREF = "devtools.telemetry.tools.opened.version";
+
+function Telemetry() {
+ // Bind pretty much all functions so that callers do not need to.
+ this.toolOpened = this.toolOpened.bind(this);
+ this.toolClosed = this.toolClosed.bind(this);
+ this.log = this.log.bind(this);
+ this.logOncePerBrowserVersion = this.logOncePerBrowserVersion.bind(this);
+ this.destroy = this.destroy.bind(this);
+
+ this._timers = new Map();
+}
+
+module.exports = Telemetry;
+
+var Services = require("Services");
+
+Telemetry.prototype = {
+ _histograms: {
+ toolbox: {
+ histogram: "DEVTOOLS_TOOLBOX_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS"
+ },
+ options: {
+ histogram: "DEVTOOLS_OPTIONS_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_OPTIONS_TIME_ACTIVE_SECONDS"
+ },
+ webconsole: {
+ histogram: "DEVTOOLS_WEBCONSOLE_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_WEBCONSOLE_TIME_ACTIVE_SECONDS"
+ },
+ browserconsole: {
+ histogram: "DEVTOOLS_BROWSERCONSOLE_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_BROWSERCONSOLE_TIME_ACTIVE_SECONDS"
+ },
+ inspector: {
+ histogram: "DEVTOOLS_INSPECTOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_INSPECTOR_TIME_ACTIVE_SECONDS"
+ },
+ ruleview: {
+ histogram: "DEVTOOLS_RULEVIEW_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_RULEVIEW_TIME_ACTIVE_SECONDS"
+ },
+ computedview: {
+ histogram: "DEVTOOLS_COMPUTEDVIEW_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS"
+ },
+ fontinspector: {
+ histogram: "DEVTOOLS_FONTINSPECTOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS"
+ },
+ animationinspector: {
+ histogram: "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_ANIMATIONINSPECTOR_TIME_ACTIVE_SECONDS"
+ },
+ jsdebugger: {
+ histogram: "DEVTOOLS_JSDEBUGGER_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_JSDEBUGGER_TIME_ACTIVE_SECONDS"
+ },
+ jsbrowserdebugger: {
+ histogram: "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS"
+ },
+ styleeditor: {
+ histogram: "DEVTOOLS_STYLEEDITOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS"
+ },
+ shadereditor: {
+ histogram: "DEVTOOLS_SHADEREDITOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_SHADEREDITOR_TIME_ACTIVE_SECONDS"
+ },
+ webaudioeditor: {
+ histogram: "DEVTOOLS_WEBAUDIOEDITOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_WEBAUDIOEDITOR_TIME_ACTIVE_SECONDS"
+ },
+ canvasdebugger: {
+ histogram: "DEVTOOLS_CANVASDEBUGGER_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_CANVASDEBUGGER_TIME_ACTIVE_SECONDS"
+ },
+ performance: {
+ histogram: "DEVTOOLS_JSPROFILER_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS"
+ },
+ memory: {
+ histogram: "DEVTOOLS_MEMORY_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_MEMORY_TIME_ACTIVE_SECONDS"
+ },
+ netmonitor: {
+ histogram: "DEVTOOLS_NETMONITOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS"
+ },
+ storage: {
+ histogram: "DEVTOOLS_STORAGE_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_STORAGE_TIME_ACTIVE_SECONDS"
+ },
+ paintflashing: {
+ histogram: "DEVTOOLS_PAINTFLASHING_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_PAINTFLASHING_TIME_ACTIVE_SECONDS"
+ },
+ scratchpad: {
+ histogram: "DEVTOOLS_SCRATCHPAD_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_SCRATCHPAD_TIME_ACTIVE_SECONDS"
+ },
+ "scratchpad-window": {
+ histogram: "DEVTOOLS_SCRATCHPAD_WINDOW_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_SCRATCHPAD_WINDOW_TIME_ACTIVE_SECONDS"
+ },
+ responsive: {
+ histogram: "DEVTOOLS_RESPONSIVE_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS"
+ },
+ eyedropper: {
+ histogram: "DEVTOOLS_EYEDROPPER_OPENED_COUNT",
+ },
+ menueyedropper: {
+ histogram: "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT",
+ },
+ pickereyedropper: {
+ histogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT",
+ },
+ toolbareyedropper: {
+ histogram: "DEVTOOLS_TOOLBAR_EYEDROPPER_OPENED_COUNT",
+ },
+ developertoolbar: {
+ histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS"
+ },
+ aboutdebugging: {
+ histogram: "DEVTOOLS_ABOUTDEBUGGING_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_ABOUTDEBUGGING_TIME_ACTIVE_SECONDS"
+ },
+ webide: {
+ histogram: "DEVTOOLS_WEBIDE_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_WEBIDE_TIME_ACTIVE_SECONDS"
+ },
+ webideProjectEditor: {
+ histogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_TIME_ACTIVE_SECONDS"
+ },
+ webideProjectEditorSave: {
+ histogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_SAVE_COUNT",
+ },
+ webideNewProject: {
+ histogram: "DEVTOOLS_WEBIDE_NEW_PROJECT_COUNT",
+ },
+ webideImportProject: {
+ histogram: "DEVTOOLS_WEBIDE_IMPORT_PROJECT_COUNT",
+ },
+ custom: {
+ histogram: "DEVTOOLS_CUSTOM_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS"
+ },
+ reloadAddonInstalled: {
+ histogram: "DEVTOOLS_RELOAD_ADDON_INSTALLED_COUNT",
+ },
+ reloadAddonReload: {
+ histogram: "DEVTOOLS_RELOAD_ADDON_RELOAD_COUNT",
+ },
+ },
+
+ /**
+ * Add an entry to a histogram.
+ *
+ * @param {String} id
+ * Used to look up the relevant histogram ID and log true to that
+ * histogram.
+ */
+ toolOpened: function (id) {
+ let charts = this._histograms[id] || this._histograms.custom;
+
+ if (charts.histogram) {
+ this.log(charts.histogram, true);
+ }
+ if (charts.timerHistogram) {
+ this.startTimer(charts.timerHistogram);
+ }
+ },
+
+ /**
+ * Record that an action occurred. Aliases to `toolOpened`, so it's just for
+ * readability at the call site for cases where we aren't actually opening
+ * tools.
+ */
+ actionOccurred(id) {
+ this.toolOpened(id);
+ },
+
+ toolClosed: function (id) {
+ let charts = this._histograms[id];
+
+ if (!charts || !charts.timerHistogram) {
+ return;
+ }
+
+ this.stopTimer(charts.timerHistogram);
+ },
+
+ /**
+ * Record the start time for a timing-based histogram entry.
+ *
+ * @param String histogramId
+ * Histogram in which the data is to be stored.
+ */
+ startTimer: function (histogramId) {
+ this._timers.set(histogramId, new Date());
+ },
+
+ /**
+ * Stop the timer and log elasped time for a timing-based histogram entry.
+ *
+ * @param String histogramId
+ * Histogram in which the data is to be stored.
+ * @param String key [optional]
+ * Optional key for a keyed histogram.
+ */
+ stopTimer: function (histogramId, key) {
+ let startTime = this._timers.get(histogramId);
+ if (startTime) {
+ let time = (new Date() - startTime) / 1000;
+ if (!key) {
+ this.log(histogramId, time);
+ } else {
+ this.logKeyed(histogramId, key, time);
+ }
+ this._timers.delete(histogramId);
+ }
+ },
+
+ /**
+ * Log a value to a histogram.
+ *
+ * @param {String} histogramId
+ * Histogram in which the data is to be stored.
+ * @param value
+ * Value to store.
+ */
+ log: function (histogramId, value) {
+ if (histogramId) {
+ try {
+ let histogram = Services.telemetry.getHistogramById(histogramId);
+ histogram.add(value);
+ } catch (e) {
+ dump("Warning: An attempt was made to write to the " + histogramId +
+ " histogram, which is not defined in Histograms.json\n");
+ }
+ }
+ },
+
+ /**
+ * Log a value to a keyed histogram.
+ *
+ * @param {String} histogramId
+ * Histogram in which the data is to be stored.
+ * @param {String} key
+ * The key within the single histogram.
+ * @param value
+ * Value to store.
+ */
+ logKeyed: function (histogramId, key, value) {
+ if (histogramId) {
+ try {
+ let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
+ histogram.add(key, value);
+ } catch (e) {
+ dump("Warning: An attempt was made to write to the " + histogramId +
+ " histogram, which is not defined in Histograms.json\n");
+ }
+ }
+ },
+
+ /**
+ * Log info about usage once per browser version. This allows us to discover
+ * how many individual users are using our tools for each browser version.
+ *
+ * @param {String} perUserHistogram
+ * Histogram in which the data is to be stored.
+ */
+ logOncePerBrowserVersion: function (perUserHistogram, value) {
+ let currentVersion = Services.appinfo.version;
+ let latest = Services.prefs.getCharPref(TOOLS_OPENED_PREF);
+ let latestObj = JSON.parse(latest);
+
+ let lastVersionHistogramUpdated = latestObj[perUserHistogram];
+
+ if (typeof lastVersionHistogramUpdated == "undefined" ||
+ lastVersionHistogramUpdated !== currentVersion) {
+ latestObj[perUserHistogram] = currentVersion;
+ latest = JSON.stringify(latestObj);
+ Services.prefs.setCharPref(TOOLS_OPENED_PREF, latest);
+ this.log(perUserHistogram, value);
+ }
+ },
+
+ destroy: function () {
+ for (let histogramId of this._timers.keys()) {
+ this.stopTimer(histogramId);
+ }
+ }
+};
diff --git a/devtools/client/shared/test/.eslintrc.js b/devtools/client/shared/test/.eslintrc.js
new file mode 100644
index 000000000..ed80d6d12
--- /dev/null
+++ b/devtools/client/shared/test/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js",
+ "globals": {
+ "DeveloperToolbar": true
+ }
+};
diff --git a/devtools/client/shared/test/browser.ini b/devtools/client/shared/test/browser.ini
new file mode 100644
index 000000000..ad3f52fbd
--- /dev/null
+++ b/devtools/client/shared/test/browser.ini
@@ -0,0 +1,188 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ browser_layoutHelpers.html
+ browser_layoutHelpers-getBoxQuads.html
+ browser_templater_basic.html
+ browser_toolbar_basic.html
+ browser_toolbar_webconsole_errors_count.html
+ browser_devices.json
+ doc_options-view.xul
+ head.js
+ helper_color_data.js
+ helper_html_tooltip.js
+ helper_inplace_editor.js
+ html-mdn-css-basic-testing.html
+ html-mdn-css-no-summary.html
+ html-mdn-css-no-summary-or-syntax.html
+ html-mdn-css-no-syntax.html
+ html-mdn-css-syntax-old-style.html
+ leakhunt.js
+ test-actor.js
+ test-actor-registry.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_css_angle.js]
+[browser_css_color.js]
+[browser_cubic-bezier-01.js]
+[browser_cubic-bezier-02.js]
+[browser_cubic-bezier-03.js]
+[browser_cubic-bezier-04.js]
+[browser_cubic-bezier-05.js]
+[browser_cubic-bezier-06.js]
+[browser_filter-editor-01.js]
+[browser_filter-editor-02.js]
+[browser_filter-editor-03.js]
+[browser_filter-editor-04.js]
+[browser_filter-editor-05.js]
+[browser_filter-editor-06.js]
+[browser_filter-editor-07.js]
+[browser_filter-editor-08.js]
+[browser_filter-editor-09.js]
+[browser_filter-editor-10.js]
+[browser_filter-presets-01.js]
+[browser_filter-presets-02.js]
+[browser_filter-presets-03.js]
+[browser_flame-graph-01.js]
+[browser_flame-graph-02.js]
+[browser_flame-graph-03a.js]
+[browser_flame-graph-03b.js]
+[browser_flame-graph-03c.js]
+[browser_flame-graph-04.js]
+[browser_flame-graph-05.js]
+[browser_flame-graph-utils-01.js]
+[browser_flame-graph-utils-02.js]
+[browser_flame-graph-utils-03.js]
+[browser_flame-graph-utils-04.js]
+[browser_flame-graph-utils-05.js]
+[browser_flame-graph-utils-06.js]
+[browser_flame-graph-utils-hash.js]
+[browser_graphs-01.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-02.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-03.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-04.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-05.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-06.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07a.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07b.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07c.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07d.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-07e.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-08.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09a.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09b.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09c.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09d.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09e.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-09f.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-10a.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-10b.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-10c.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-11a.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-11b.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-12.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-13.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-14.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-15.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_graphs-16.js]
+skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
+[browser_html_tooltip-01.js]
+[browser_html_tooltip-02.js]
+[browser_html_tooltip-03.js]
+[browser_html_tooltip-04.js]
+[browser_html_tooltip-05.js]
+[browser_html_tooltip_arrow-01.js]
+[browser_html_tooltip_arrow-02.js]
+[browser_html_tooltip_consecutive-show.js]
+[browser_html_tooltip_hover.js]
+[browser_html_tooltip_offset.js]
+[browser_html_tooltip_rtl.js]
+[browser_html_tooltip_variable-height.js]
+[browser_html_tooltip_width-auto.js]
+[browser_html_tooltip_xul-wrapper.js]
+[browser_inplace-editor-01.js]
+[browser_inplace-editor-02.js]
+[browser_inplace-editor_autocomplete_01.js]
+[browser_inplace-editor_autocomplete_02.js]
+[browser_inplace-editor_autocomplete_offset.js]
+[browser_inplace-editor_maxwidth.js]
+[browser_keycodes.js]
+[browser_key_shortcuts.js]
+[browser_layoutHelpers.js]
+skip-if = e10s # Layouthelpers test should not run in a content page.
+[browser_layoutHelpers-getBoxQuads.js]
+skip-if = e10s # Layouthelpers test should not run in a content page.
+[browser_mdn-docs-01.js]
+[browser_mdn-docs-02.js]
+[browser_mdn-docs-03.js]
+[browser_num-l10n.js]
+[browser_options-view-01.js]
+[browser_outputparser.js]
+skip-if = e10s # Test intermittently fails with e10s. Bug 1124162.
+[browser_poller.js]
+[browser_prefs-01.js]
+[browser_prefs-02.js]
+[browser_require_raw.js]
+[browser_spectrum.js]
+[browser_theme.js]
+[browser_tableWidget_basic.js]
+[browser_tableWidget_keyboard_interaction.js]
+[browser_tableWidget_mouse_interaction.js]
+[browser_telemetry_button_eyedropper.js]
+[browser_telemetry_button_paintflashing.js]
+skip-if = e10s # Bug 937167 - e10s paintflashing
+[browser_telemetry_button_responsive.js]
+skip-if = e10s # Bug 1067145 - e10s responsiveview
+[browser_telemetry_button_scratchpad.js]
+[browser_telemetry_sidebar.js]
+[browser_telemetry_toolbox.js]
+[browser_telemetry_toolboxtabs_canvasdebugger.js]
+[browser_telemetry_toolboxtabs_inspector.js]
+[browser_telemetry_toolboxtabs_jsdebugger.js]
+[browser_telemetry_toolboxtabs_jsprofiler.js]
+[browser_telemetry_toolboxtabs_netmonitor.js]
+[browser_telemetry_toolboxtabs_options.js]
+[browser_telemetry_toolboxtabs_shadereditor.js]
+[browser_telemetry_toolboxtabs_storage.js]
+[browser_telemetry_toolboxtabs_styleeditor.js]
+[browser_telemetry_toolboxtabs_webaudioeditor.js]
+[browser_telemetry_toolboxtabs_webconsole.js]
+[browser_templater_basic.js]
+[browser_toolbar_basic.js]
+skip-if = (e10s && debug) # Bug 1253035
+[browser_toolbar_tooltip.js]
+[browser_toolbar_webconsole_errors_count.js]
+skip-if = e10s # The developertoolbar error count isn't correct with e10s
+[browser_treeWidget_basic.js]
+[browser_treeWidget_keyboard_interaction.js]
+[browser_treeWidget_mouse_interaction.js]
+[browser_devices.js]
+[browser_theme_switching.js]
diff --git a/devtools/client/shared/test/browser_css_angle.js b/devtools/client/shared/test/browser_css_angle.js
new file mode 100644
index 000000000..903be44d8
--- /dev/null
+++ b/devtools/client/shared/test/browser_css_angle.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from head.js */
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,browser_css_angle.js";
+var {angleUtils} = require("devtools/client/shared/css-angle");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host] = yield createHost("bottom", TEST_URI);
+
+ info("Starting the test");
+ testAngleUtils();
+ testAngleValidity();
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testAngleUtils() {
+ let data = getTestData();
+
+ for (let {authored, deg, rad, grad, turn} of data) {
+ let angle = new angleUtils.CssAngle(authored);
+
+ // Check all values.
+ info("Checking values for " + authored);
+ is(angle.deg, deg, "color.deg === deg");
+ is(angle.rad, rad, "color.rad === rad");
+ is(angle.grad, grad, "color.grad === grad");
+ is(angle.turn, turn, "color.turn === turn");
+
+ testToString(angle, deg, rad, grad, turn);
+ }
+}
+
+function testAngleValidity() {
+ let data = getAngleValidityData();
+
+ for (let {angle, result} of data) {
+ let testAngle = new angleUtils.CssAngle(angle);
+ let validString = testAngle.valid ? " a valid" : "an invalid";
+
+ is(testAngle.valid, result,
+ `Testing that "${angle}" is ${validString} angle`);
+ }
+}
+
+function testToString(angle, deg, rad, grad, turn) {
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.deg;
+ is(angle.toString(), deg, "toString() with deg type");
+
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.rad;
+ is(angle.toString(), rad, "toString() with rad type");
+
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.grad;
+ is(angle.toString(), grad, "toString() with grad type");
+
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.turn;
+ is(angle.toString(), turn, "toString() with turn type");
+}
+
+function getAngleValidityData() {
+ return [{
+ angle: "0.2turn",
+ result: true
+ }, {
+ angle: "-0.2turn",
+ result: true
+ }, {
+ angle: "-.2turn",
+ result: true
+ }, {
+ angle: "1e02turn",
+ result: true
+ }, {
+ angle: "-2e2turn",
+ result: true
+ }, {
+ angle: ".2turn",
+ result: true
+ }, {
+ angle: "0.2aaturn",
+ result: false
+ }, {
+ angle: "2dega",
+ result: false
+ }, {
+ angle: "0.deg",
+ result: false
+ }, {
+ angle: ".deg",
+ result: false
+ }, {
+ angle: "..2turn",
+ result: false
+ }];
+}
+
+function getTestData() {
+ return [{
+ authored: "0deg",
+ deg: "0deg",
+ rad: "0rad",
+ grad: "0grad",
+ turn: "0turn"
+ }, {
+ authored: "180deg",
+ deg: "180deg",
+ rad: `${Math.round(Math.PI * 10000) / 10000}rad`,
+ grad: "200grad",
+ turn: "0.5turn"
+ }, {
+ authored: "180DEG",
+ deg: "180DEG",
+ rad: `${Math.round(Math.PI * 10000) / 10000}RAD`,
+ grad: "200GRAD",
+ turn: "0.5TURN"
+ }, {
+ authored: `-${Math.PI}rad`,
+ deg: "-180deg",
+ rad: `-${Math.PI}rad`,
+ grad: "-200grad",
+ turn: "-0.5turn"
+ }, {
+ authored: `-${Math.PI}RAD`,
+ deg: "-180DEG",
+ rad: `-${Math.PI}RAD`,
+ grad: "-200GRAD",
+ turn: "-0.5TURN"
+ }, {
+ authored: "100grad",
+ deg: "90deg",
+ rad: `${Math.round(Math.PI / 2 * 10000) / 10000}rad`,
+ grad: "100grad",
+ turn: "0.25turn"
+ }, {
+ authored: "100GRAD",
+ deg: "90DEG",
+ rad: `${Math.round(Math.PI / 2 * 10000) / 10000}RAD`,
+ grad: "100GRAD",
+ turn: "0.25TURN"
+ }, {
+ authored: "-1turn",
+ deg: "-360deg",
+ rad: `${-1 * Math.round(Math.PI * 2 * 10000) / 10000}rad`,
+ grad: "-400grad",
+ turn: "-1turn"
+ }, {
+ authored: "-10TURN",
+ deg: "-3600DEG",
+ rad: `${-1 * Math.round(Math.PI * 2 * 10 * 10000) / 10000}RAD`,
+ grad: "-4000GRAD",
+ turn: "-10TURN"
+ }, {
+ authored: "inherit",
+ deg: "inherit",
+ rad: "inherit",
+ grad: "inherit",
+ turn: "inherit"
+ }, {
+ authored: "initial",
+ deg: "initial",
+ rad: "initial",
+ grad: "initial",
+ turn: "initial"
+ }, {
+ authored: "unset",
+ deg: "unset",
+ rad: "unset",
+ grad: "unset",
+ turn: "unset"
+ }];
+}
diff --git a/devtools/client/shared/test/browser_css_color.js b/devtools/client/shared/test/browser_css_color.js
new file mode 100644
index 000000000..c0846c362
--- /dev/null
+++ b/devtools/client/shared/test/browser_css_color.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,browser_css_color.js";
+var {colorUtils} = require("devtools/shared/css/color");
+/* global getFixtureColorData */
+loadHelperScript("helper_color_data.js");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Creating a test canvas element to test colors");
+ let canvas = createTestCanvas(doc);
+ info("Starting the test");
+ testColorUtils(canvas);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function createTestCanvas(doc) {
+ let canvas = doc.createElement("canvas");
+ canvas.width = canvas.height = 10;
+ doc.body.appendChild(canvas);
+ return canvas;
+}
+
+function testColorUtils(canvas) {
+ let data = getFixtureColorData();
+
+ for (let {authored, name, hex, hsl, rgb} of data) {
+ let color = new colorUtils.CssColor(authored);
+
+ // Check all values.
+ info("Checking values for " + authored);
+ is(color.name, name, "color.name === name");
+ is(color.hex, hex, "color.hex === hex");
+ is(color.hsl, hsl, "color.hsl === hsl");
+ is(color.rgb, rgb, "color.rgb === rgb");
+
+ testToString(color, name, hex, hsl, rgb);
+ testColorMatch(name, hex, hsl, rgb, color.rgba, canvas);
+ }
+
+ testSetAlpha();
+}
+
+function testToString(color, name, hex, hsl, rgb) {
+ color.colorUnit = colorUtils.CssColor.COLORUNIT.name;
+ is(color.toString(), name, "toString() with authored type");
+
+ color.colorUnit = colorUtils.CssColor.COLORUNIT.hex;
+ is(color.toString(), hex, "toString() with hex type");
+
+ color.colorUnit = colorUtils.CssColor.COLORUNIT.hsl;
+ is(color.toString(), hsl, "toString() with hsl type");
+
+ color.colorUnit = colorUtils.CssColor.COLORUNIT.rgb;
+ is(color.toString(), rgb, "toString() with rgb type");
+}
+
+function testColorMatch(name, hex, hsl, rgb, rgba, canvas) {
+ let target;
+ let ctx = canvas.getContext("2d");
+
+ let clearCanvas = function () {
+ canvas.width = 1;
+ };
+ let setColor = function (color) {
+ ctx.fillStyle = color;
+ ctx.fillRect(0, 0, 1, 1);
+ };
+ let setTargetColor = function () {
+ clearCanvas();
+ // All colors have rgba so we can use this to compare against.
+ setColor(rgba);
+ let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+ target = {r: r, g: g, b: b, a: a};
+ };
+ let test = function (color, type) {
+ // hsla -> rgba -> hsla produces inaccurate results so we
+ // need some tolerence here.
+ let tolerance = 3;
+ clearCanvas();
+
+ setColor(color);
+ let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+
+ let rgbFail = Math.abs(r - target.r) > tolerance ||
+ Math.abs(g - target.g) > tolerance ||
+ Math.abs(b - target.b) > tolerance;
+ ok(!rgbFail, "color " + rgba + " matches target. Type: " + type);
+ if (rgbFail) {
+ info(`target: ${target.toSource()}, color: [r: ${r}, g: ${g}, b: ${b}, a: ${a}]`);
+ }
+
+ let alphaFail = a !== target.a;
+ ok(!alphaFail, "color " + rgba + " alpha value matches target.");
+ };
+
+ setTargetColor();
+
+ test(name, "name");
+ test(hex, "hex");
+ test(hsl, "hsl");
+ test(rgb, "rgb");
+}
+
+function testSetAlpha() {
+ let values = [
+ ["longhex", "#ff0000", 0.5, "rgba(255, 0, 0, 0.5)"],
+ ["hex", "#f0f", 0.2, "rgba(255, 0, 255, 0.2)"],
+ ["rgba", "rgba(120, 34, 23, 1)", 0.25, "rgba(120, 34, 23, 0.25)"],
+ ["rgb", "rgb(120, 34, 23)", 0.25, "rgba(120, 34, 23, 0.25)"],
+ ["hsl", "hsl(208, 100%, 97%)", 0.75, "rgba(240, 248, 255, 0.75)"],
+ ["hsla", "hsla(208, 100%, 97%, 1)", 0.75, "rgba(240, 248, 255, 0.75)"],
+ ["alphahex", "#f08f", 0.6, "rgba(255, 0, 136, 0.6)"],
+ ["longalphahex", "#00ff80ff", 0.2, "rgba(0, 255, 128, 0.2)"]
+ ];
+ values.forEach(([type, value, alpha, expected]) => {
+ is(colorUtils.setAlpha(value, alpha), expected,
+ "correctly sets alpha value for " + type);
+ });
+
+ try {
+ colorUtils.setAlpha("rgb(24, 25%, 45, 1)", 1);
+ ok(false, "Should fail when passing in an invalid color.");
+ } catch (e) {
+ ok(true, "Fails when setAlpha receives an invalid color.");
+ }
+
+ is(colorUtils.setAlpha("#fff"), "rgba(255, 255, 255, 1)",
+ "sets alpha to 1 if invalid.");
+}
diff --git a/devtools/client/shared/test/browser_cubic-bezier-01.js b/devtools/client/shared/test/browser_cubic-bezier-01.js
new file mode 100644
index 000000000..4c32590b2
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-01.js
@@ -0,0 +1,38 @@
+/* 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";
+
+// Tests that the CubicBezierWidget generates content in a given parent node
+
+const {CubicBezierWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Checking that the graph markup is created in the parent");
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierWidget(container);
+
+ ok(container.querySelector(".display-wrap"),
+ "The display has been added");
+
+ ok(container.querySelector(".coordinate-plane"),
+ "The coordinate plane has been added");
+ let buttons = container.querySelectorAll("button");
+ is(buttons.length, 2,
+ "The 2 control points have been added");
+ is(buttons[0].className, "control-point");
+ is(buttons[1].className, "control-point");
+ ok(container.querySelector("canvas"), "The curve canvas has been added");
+
+ info("Destroying the widget");
+ w.destroy();
+ is(container.children.length, 0, "All nodes have been removed");
+
+ host.destroy();
+});
diff --git a/devtools/client/shared/test/browser_cubic-bezier-02.js b/devtools/client/shared/test/browser_cubic-bezier-02.js
new file mode 100644
index 000000000..f5e21e4d4
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-02.js
@@ -0,0 +1,200 @@
+/* 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";
+
+// Tests the CubicBezierWidget events
+
+const {CubicBezierWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+const {PREDEFINED} = require("devtools/client/shared/widgets/CubicBezierPresets");
+
+// In this test we have to use a slightly more complete HTML tree, with <body>
+// in order to remove its margin and prevent shifted positions
+const TEST_URI = `data:text/html,
+ <html><body>
+ <div id="cubic-bezier-container"/>
+ </body></html>`;
+
+add_task(function* () {
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ // Required or widget will be clipped inside of 'bottom'
+ // host by -14. Setting `fixed` zeroes this which is needed for
+ // calculating offsets. Occurs in test env only.
+ doc.body.setAttribute("style", "position: fixed; margin: 0;");
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+ let rect = w.curve.getBoundingClientRect();
+ rect.graphTop = rect.height * w.bezierCanvas.padding[0];
+ rect.graphBottom = rect.height - rect.graphTop;
+ rect.graphHeight = rect.graphBottom - rect.graphTop;
+
+ yield pointsCanBeDragged(w, win, doc, rect);
+ yield curveCanBeClicked(w, win, doc, rect);
+ yield pointsCanBeMovedWithKeyboard(w, win, doc, rect);
+
+ w.destroy();
+ host.destroy();
+});
+
+function* pointsCanBeDragged(widget, win, doc, offsets) {
+ info("Checking that the control points can be dragged with the mouse");
+
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Generating a mousedown/move/up on P1");
+ widget._onPointMouseDown({target: widget.p1});
+ doc.onmousemove({pageX: offsets.left, pageY: offsets.graphTop});
+ doc.onmouseup();
+
+ let bezier = yield onUpdated;
+ ok(true, "The widget fired the updated event");
+ ok(bezier, "The updated event contains a bezier argument");
+ is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
+
+ info("Listening for the update event");
+ onUpdated = widget.once("updated");
+
+ info("Generating a mousedown/move/up on P2");
+ widget._onPointMouseDown({target: widget.p2});
+ doc.onmousemove({pageX: offsets.right, pageY: offsets.graphBottom});
+ doc.onmouseup();
+
+ bezier = yield onUpdated;
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
+}
+
+function* curveCanBeClicked(widget, win, doc, offsets) {
+ info("Checking that clicking on the curve moves the closest control point");
+
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Click close to P1");
+ let x = offsets.left + (offsets.width / 4.0);
+ let y = offsets.graphTop + (offsets.graphHeight / 4.0);
+ widget._onCurveClick({pageX: x, pageY: y});
+
+ let bezier = yield onUpdated;
+ ok(true, "The widget fired the updated event");
+ is(bezier.P1[0], 0.25, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "P2 time coordinate remained unchanged");
+ is(bezier.P2[1], 0, "P2 progress coordinate remained unchanged");
+
+ info("Listening for the update event");
+ onUpdated = widget.once("updated");
+
+ info("Click close to P2");
+ x = offsets.right - (offsets.width / 4);
+ y = offsets.graphBottom - (offsets.graphHeight / 4);
+ widget._onCurveClick({pageX: x, pageY: y});
+
+ bezier = yield onUpdated;
+ is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
+ is(bezier.P1[0], 0.25, "P1 time coordinate remained unchanged");
+ is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged");
+}
+
+function* pointsCanBeMovedWithKeyboard(widget, win, doc, offsets) {
+ info("Checking that points respond to keyboard events");
+
+ let singleStep = 3;
+ let shiftStep = 30;
+
+ info("Moving P1 to the left");
+ let newOffset = parseInt(widget.p1.style.left, 10) - singleStep;
+ let x = widget.bezierCanvas
+ .offsetsToCoordinates({style: {left: newOffset}})[0];
+
+ let onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 37));
+ let bezier = yield onUpdated;
+
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the left, fast");
+ newOffset = parseInt(widget.p1.style.left, 10) - shiftStep;
+ x = widget.bezierCanvas
+ .offsetsToCoordinates({style: {left: newOffset}})[0];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 37, true));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the right, fast");
+ newOffset = parseInt(widget.p1.style.left, 10) + shiftStep;
+ x = widget.bezierCanvas
+ .offsetsToCoordinates({style: {left: newOffset}})[0];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 39, true));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the bottom");
+ newOffset = parseInt(widget.p1.style.top, 10) + singleStep;
+ let y = widget.bezierCanvas
+ .offsetsToCoordinates({style: {top: newOffset}})[1];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 40));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the bottom, fast");
+ newOffset = parseInt(widget.p1.style.top, 10) + shiftStep;
+ y = widget.bezierCanvas
+ .offsetsToCoordinates({style: {top: newOffset}})[1];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 40, true));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the top, fast");
+ newOffset = parseInt(widget.p1.style.top, 10) - shiftStep;
+ y = widget.bezierCanvas
+ .offsetsToCoordinates({style: {top: newOffset}})[1];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 38, true));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
+
+ info("Checking that keyboard events also work with P2");
+ info("Moving P2 to the left");
+ newOffset = parseInt(widget.p2.style.left, 10) - singleStep;
+ x = widget.bezierCanvas
+ .offsetsToCoordinates({style: {left: newOffset}})[0];
+
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p2, 37));
+ bezier = yield onUpdated;
+ is(bezier.P2[0], x, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
+}
+
+function getKeyEvent(target, keyCode, shift = false) {
+ return {
+ target: target,
+ keyCode: keyCode,
+ shiftKey: shift,
+ preventDefault: () => {}
+ };
+}
diff --git a/devtools/client/shared/test/browser_cubic-bezier-03.js b/devtools/client/shared/test/browser_cubic-bezier-03.js
new file mode 100644
index 000000000..274ed81ef
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-03.js
@@ -0,0 +1,68 @@
+/* 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";
+
+// Tests that coordinates can be changed programatically in the CubicBezierWidget
+
+const {CubicBezierWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+const {PREDEFINED} = require("devtools/client/shared/widgets/CubicBezierPresets");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+ yield coordinatesCanBeChangedByProvidingAnArray(w);
+ yield coordinatesCanBeChangedByProvidingAValue(w);
+
+ w.destroy();
+ host.destroy();
+});
+
+function* coordinatesCanBeChangedByProvidingAnArray(widget) {
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Setting new coordinates");
+ widget.coordinates = [0, 1, 1, 0];
+
+ let bezier = yield onUpdated;
+ ok(true, "The updated event was fired as a result of setting coordinates");
+
+ is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
+}
+
+function* coordinatesCanBeChangedByProvidingAValue(widget) {
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Setting linear css value");
+ widget.cssCubicBezierValue = "linear";
+ let bezier = yield onUpdated;
+ ok(true, "The updated event was fired as a result of setting cssValue");
+
+ is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 1, "The new P2 progress coordinate is correct");
+
+ info("Setting a custom cubic-bezier css value");
+ onUpdated = widget.once("updated");
+ widget.cssCubicBezierValue = "cubic-bezier(.25,-0.5, 1, 1.25)";
+ bezier = yield onUpdated;
+ ok(true, "The updated event was fired as a result of setting cssValue");
+
+ is(bezier.P1[0], .25, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], -.5, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 1.25, "The new P2 progress coordinate is correct");
+}
diff --git a/devtools/client/shared/test/browser_cubic-bezier-04.js b/devtools/client/shared/test/browser_cubic-bezier-04.js
new file mode 100644
index 000000000..102428035
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-04.js
@@ -0,0 +1,50 @@
+/* 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";
+
+// Tests that the CubicBezierPresetWidget generates markup.
+
+const {CubicBezierPresetWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+const {PRESETS} = require("devtools/client/shared/widgets/CubicBezierPresets");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierPresetWidget(container);
+
+ info("Checking that the presets are created in the parent");
+ ok(container.querySelector(".preset-pane"),
+ "The preset pane has been added");
+
+ ok(container.querySelector("#preset-categories"),
+ "The preset categories have been added");
+ let categories = container.querySelectorAll(".category");
+ is(categories.length, Object.keys(PRESETS).length,
+ "The preset categories have been added");
+ Object.keys(PRESETS).forEach(category => {
+ ok(container.querySelector("#" + category), `${category} has been added`);
+ ok(container.querySelector("#preset-category-" + category),
+ `The preset list for ${category} has been added.`);
+ });
+
+ info("Checking that each of the presets and its preview have been added");
+ Object.keys(PRESETS).forEach(category => {
+ Object.keys(PRESETS[category]).forEach(presetLabel => {
+ let preset = container.querySelector("#" + presetLabel);
+ ok(preset, `${presetLabel} has been added`);
+ ok(preset.querySelector("canvas"),
+ `${presetLabel}'s canvas preview has been added`);
+ ok(preset.querySelector("p"),
+ `${presetLabel}'s label has been added`);
+ });
+ });
+
+ w.destroy();
+ host.destroy();
+});
diff --git a/devtools/client/shared/test/browser_cubic-bezier-05.js b/devtools/client/shared/test/browser_cubic-bezier-05.js
new file mode 100644
index 000000000..b9cdab294
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-05.js
@@ -0,0 +1,48 @@
+/* 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";
+
+// Tests that the CubicBezierPresetWidget cycles menus
+
+const {CubicBezierPresetWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} =
+ require("devtools/client/shared/widgets/CubicBezierPresets");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierPresetWidget(container);
+
+ info("Checking that preset is selected if coordinates are known");
+
+ w.refreshMenu([0, 0, 0, 0]);
+ is(w.activeCategory, container.querySelector(`#${DEFAULT_PRESET_CATEGORY}`),
+ "The default category is selected");
+ is(w._activePreset, null, "There is no selected category");
+
+ w.refreshMenu(PREDEFINED.linear);
+ is(w.activeCategory, container.querySelector("#ease-in-out"),
+ "The ease-in-out category is active");
+ is(w._activePreset, container.querySelector("#ease-in-out-linear"),
+ "The ease-in-out-linear preset is active");
+
+ w.refreshMenu(PRESETS["ease-out"]["ease-out-sine"]);
+ is(w.activeCategory, container.querySelector("#ease-out"),
+ "The ease-out category is active");
+ is(w._activePreset, container.querySelector("#ease-out-sine"),
+ "The ease-out-sine preset is active");
+
+ w.refreshMenu([0, 0, 0, 0]);
+ is(w.activeCategory, container.querySelector("#ease-out"),
+ "The ease-out category is still active");
+ is(w._activePreset, null, "No preset is active");
+
+ w.destroy();
+ host.destroy();
+});
diff --git a/devtools/client/shared/test/browser_cubic-bezier-06.js b/devtools/client/shared/test/browser_cubic-bezier-06.js
new file mode 100644
index 000000000..150949929
--- /dev/null
+++ b/devtools/client/shared/test/browser_cubic-bezier-06.js
@@ -0,0 +1,79 @@
+
+/* 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";
+
+// Tests the integration between CubicBezierWidget and CubicBezierPresets
+
+const {CubicBezierWidget} =
+ require("devtools/client/shared/widgets/CubicBezierWidget");
+const {PRESETS} = require("devtools/client/shared/widgets/CubicBezierPresets");
+
+const TEST_URI = `data:text/html,<div id="cubic-bezier-container" />`;
+
+add_task(function* () {
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#cubic-bezier-container");
+ let w = new CubicBezierWidget(container,
+ PRESETS["ease-in"]["ease-in-sine"]);
+ w.presets.refreshMenu(PRESETS["ease-in"]["ease-in-sine"]);
+
+ let rect = w.curve.getBoundingClientRect();
+ rect.graphTop = rect.height * w.bezierCanvas.padding[0];
+
+ yield adjustingBezierUpdatesPreset(w, win, doc, rect);
+ yield selectingPresetUpdatesBezier(w, win, doc, rect);
+
+ w.destroy();
+ host.destroy();
+});
+
+function* adjustingBezierUpdatesPreset(widget, win, doc, rect) {
+ info("Checking that changing the bezier refreshes the preset menu");
+
+ is(widget.presets.activeCategory,
+ doc.querySelector("#ease-in"),
+ "The selected category is ease-in");
+
+ is(widget.presets._activePreset,
+ doc.querySelector("#ease-in-sine"),
+ "The selected preset is ease-in-sine");
+
+ info("Generating custom bezier curve by dragging");
+ widget._onPointMouseDown({target: widget.p1});
+ doc.onmousemove({pageX: rect.left, pageY: rect.graphTop});
+ doc.onmouseup();
+
+ is(widget.presets.activeCategory,
+ doc.querySelector("#ease-in"),
+ "The selected category is still ease-in");
+
+ is(widget.presets._activePreset, null,
+ "There is no active preset");
+}
+
+function* selectingPresetUpdatesBezier(widget, win, doc, rect) {
+ info("Checking that selecting a preset updates bezier curve");
+
+ info("Listening for the new coordinates event");
+ let onNewCoordinates = widget.presets.once("new-coordinates");
+ let onUpdated = widget.once("updated");
+
+ info("Click a preset");
+ let preset = doc.querySelector("#ease-in-sine");
+ widget.presets._onPresetClick({currentTarget: preset});
+
+ yield onNewCoordinates;
+ ok(true, "The preset widget fired the new-coordinates event");
+
+ let bezier = yield onUpdated;
+ ok(true, "The bezier canvas fired the updated event");
+
+ is(bezier.P1[0], preset.coordinates[0], "The new P1 time coordinate is correct");
+ is(bezier.P1[1], preset.coordinates[1], "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], preset.coordinates[2], "P2 time coordinate is correct ");
+ is(bezier.P2[1], preset.coordinates[3], "P2 progress coordinate is correct");
+}
diff --git a/devtools/client/shared/test/browser_devices.js b/devtools/client/shared/test/browser_devices.js
new file mode 100644
index 000000000..0bf52fe8e
--- /dev/null
+++ b/devtools/client/shared/test/browser_devices.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ getDevices,
+ getDeviceString,
+ addDevice
+} = require("devtools/client/shared/devices");
+
+add_task(function* () {
+ Services.prefs.setCharPref("devtools.devices.url",
+ TEST_URI_ROOT + "browser_devices.json");
+
+ let devices = yield getDevices();
+
+ is(devices.TYPES.length, 1, "Found 1 device type.");
+
+ let type1 = devices.TYPES[0];
+
+ is(devices[type1].length, 2, "Found 2 devices of type #1.");
+
+ let string = getDeviceString(type1);
+ ok(typeof string === "string" && string.length > 0, "Able to localize type #1.");
+
+ let device1 = {
+ name: "SquarePhone",
+ width: 320,
+ height: 320,
+ pixelRatio: 2,
+ userAgent: "Mozilla/5.0 (Mobile; rv:42.0)",
+ touch: true,
+ firefoxOS: true
+ };
+ addDevice(device1, type1);
+ devices = yield getDevices();
+
+ is(devices[type1].length, 3, "Added new device of type #1.");
+ ok(devices[type1].filter(d => d.name === device1.name), "Found the new device.");
+
+ let type2 = "appliances";
+ let device2 = {
+ name: "Mr Freezer",
+ width: 800,
+ height: 600,
+ pixelRatio: 5,
+ userAgent: "Mozilla/5.0 (Appliance; rv:42.0)",
+ touch: true,
+ firefoxOS: true
+ };
+ addDevice(device2, type2);
+ devices = yield getDevices();
+
+ is(devices.TYPES.length, 2, "Added device type #2.");
+ is(devices[type2].length, 1, "Added new device of type #2.");
+});
diff --git a/devtools/client/shared/test/browser_devices.json b/devtools/client/shared/test/browser_devices.json
new file mode 100644
index 000000000..cc7722a7f
--- /dev/null
+++ b/devtools/client/shared/test/browser_devices.json
@@ -0,0 +1,23 @@
+{
+ "TYPES": [ "phones" ],
+ "phones": [
+ {
+ "name": "Small Phone",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true
+ },
+ {
+ "name": "Big Phone",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true
+ }
+ ]
+}
diff --git a/devtools/client/shared/test/browser_filter-editor-01.js b/devtools/client/shared/test/browser_filter-editor-01.js
new file mode 100644
index 000000000..1a5beb454
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-01.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the Filter Editor Widget parses filter values correctly (setCssValue)
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const DOMUtils =
+ Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+// Verify that the given string consists of a valid CSS URL token.
+// Return true on success, false on error.
+function verifyURL(string) {
+ let lexer = DOMUtils.getCSSLexer(string);
+
+ let token = lexer.nextToken();
+ if (!token || token.tokenType !== "url") {
+ return false;
+ }
+
+ return lexer.nextToken() === null;
+}
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+
+ info("Test parsing of a valid CSS Filter value");
+ widget.setCssValue("blur(2px) contrast(200%)");
+ is(widget.getCssValue(),
+ "blur(2px) contrast(200%)",
+ "setCssValue should work for computed values");
+
+ info("Test parsing of space-filled value");
+ widget.setCssValue("blur( 2px ) contrast( 2 )");
+ is(widget.getCssValue(),
+ "blur(2px) contrast(200%)",
+ "setCssValue should work for spaced values");
+
+ info("Test parsing of string-typed values");
+ widget.setCssValue("drop-shadow( 2px 1px 5px black) url( example.svg#filter )");
+
+ is(widget.getCssValue(),
+ "drop-shadow(2px 1px 5px black) url(example.svg#filter)",
+ "setCssValue should work for string-typed values");
+
+ info("Test parsing of mixed-case function names");
+ widget.setCssValue("BLUR(2px) Contrast(200%) Drop-Shadow(2px 1px 5px Black)");
+ is(widget.getCssValue(),
+ "BLUR(2px) Contrast(200%) Drop-Shadow(2px 1px 5px Black)",
+ "setCssValue should work for mixed-case function names");
+
+ info("Test parsing of invalid filter value");
+ widget.setCssValue("totallyinvalid");
+ is(widget.getCssValue(), "none",
+ "setCssValue should turn completely invalid value to 'none'");
+
+ info("Test parsing of invalid function argument");
+ widget.setCssValue("blur('hello')");
+ is(widget.getCssValue(), "blur(0px)",
+ "setCssValue should replace invalid function argument with default");
+
+ info("Test parsing of invalid function argument #2");
+ widget.setCssValue("drop-shadow(whatever)");
+ is(widget.getCssValue(), "drop-shadow()",
+ "setCssValue should replace invalid drop-shadow argument with empty string");
+
+ info("Test parsing of mixed invalid argument");
+ widget.setCssValue("contrast(5%) whatever invert('xxx')");
+ is(widget.getCssValue(), "contrast(5%) invert(0%)",
+ "setCssValue should handle multiple errors");
+
+ info("Test parsing of 'unset'");
+ widget.setCssValue("unset");
+ is(widget.getCssValue(), "unset", "setCssValue should handle 'unset'");
+ info("Test parsing of 'initial'");
+ widget.setCssValue("initial");
+ is(widget.getCssValue(), "initial", "setCssValue should handle 'initial'");
+ info("Test parsing of 'inherit'");
+ widget.setCssValue("inherit");
+ is(widget.getCssValue(), "inherit", "setCssValue should handle 'inherit'");
+
+ info("Test parsing of quoted URL");
+ widget.setCssValue("url('invalid ) when ) unquoted')");
+ is(widget.getCssValue(), "url('invalid ) when ) unquoted')",
+ "setCssValue should re-quote single-quoted URL contents");
+ widget.setCssValue("url(\"invalid ) when ) unquoted\")");
+ is(widget.getCssValue(), "url(\"invalid ) when ) unquoted\")",
+ "setCssValue should re-quote double-quoted URL contents");
+ widget.setCssValue("url(ordinary)");
+ is(widget.getCssValue(), "url(ordinary)",
+ "setCssValue should not quote ordinary unquoted URL contents");
+
+ let quotedurl =
+ "url(invalid\\ \\)\\ {\\\twhen\\ }\\ ;\\ \\\\unquoted\\'\\\")";
+ ok(verifyURL(quotedurl), "weird URL is valid");
+ widget.setCssValue(quotedurl);
+ is(widget.getCssValue(), quotedurl,
+ "setCssValue should re-quote weird unquoted URL contents");
+
+ let dataurl = "url(data:image/svg+xml;utf8,<svg\\ " +
+ "xmlns=\\\"http://www.w3.org/2000/svg\\\"><filter\\ id=\\\"blur\\\">" +
+ "<feGaussianBlur\\ stdDeviation=\\\"3\\\"/></filter></svg>#blur)";
+ ok(verifyURL(dataurl), "data URL is valid");
+ widget.setCssValue(dataurl);
+ is(widget.getCssValue(), dataurl, "setCssValue should not mangle data urls");
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-02.js b/devtools/client/shared/test/browser_filter-editor-02.js
new file mode 100644
index 000000000..7c0ec270a
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-02.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the Filter Editor Widget renders filters correctly
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/filterwidget.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const TEST_DATA = [
+ {
+ cssValue: "blur(2px) contrast(200%) hue-rotate(20.2deg) drop-shadow(5px 5px black)",
+ expected: [
+ {
+ label: "blur",
+ value: "2",
+ unit: "px"
+ },
+ {
+ label: "contrast",
+ value: "200",
+ unit: "%"
+ },
+ {
+ label: "hue-rotate",
+ value: "20.2",
+ unit: "deg"
+ },
+ {
+ label: "drop-shadow",
+ value: "5px 5px black",
+ unit: null
+ }
+ ]
+ },
+ {
+ cssValue: "hue-rotate(420.2deg)",
+ expected: [
+ {
+ label: "hue-rotate",
+ value: "420.2",
+ unit: "deg"
+ }
+ ]
+ },
+ {
+ cssValue: "url(example.svg)",
+ expected: [
+ {
+ label: "url",
+ value: "example.svg",
+ unit: null
+ }
+ ]
+ },
+ {
+ cssValue: "none",
+ expected: []
+ }
+ ];
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+
+ info("Test rendering of different types");
+
+ for (let {cssValue, expected} of TEST_DATA) {
+ widget.setCssValue(cssValue);
+
+ if (cssValue === "none") {
+ const text = container.querySelector("#filters").textContent;
+ ok(text.indexOf(L10N.getStr("emptyFilterList")) > -1,
+ "Contains |emptyFilterList| string when given value 'none'");
+ ok(text.indexOf(L10N.getStr("addUsingList")) > -1,
+ "Contains |addUsingList| string when given value 'none'");
+ continue;
+ }
+ const filters = container.querySelectorAll(".filter");
+ testRenderedFilters(filters, expected);
+ }
+});
+
+function testRenderedFilters(filters, expected) {
+ for (let [index, filter] of [...filters].entries()) {
+ let [name, value] = filter.children,
+ label = name.children[1],
+ [input, unit] = value.children;
+
+ const eq = expected[index];
+ is(label.textContent, eq.label, "Label should match");
+ is(input.value, eq.value, "Values should match");
+ if (eq.unit) {
+ is(unit.textContent, eq.unit, "Unit should match");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_filter-editor-03.js b/devtools/client/shared/test/browser_filter-editor-03.js
new file mode 100644
index 000000000..67d36b6b2
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-03.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget add, removeAt, updateAt, getValueAt methods
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+const GRAYSCALE_MAX = 100;
+const INVERT_MIN = 0;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+
+ info("Test add method");
+ const blur = widget.add("blur", "10.2px");
+ is(widget.getCssValue(), "blur(10.2px)",
+ "Should add filters");
+
+ const url = widget.add("url", "test.svg");
+ is(widget.getCssValue(), "blur(10.2px) url(test.svg)",
+ "Should add filters in order");
+
+ info("Test updateValueAt method");
+ widget.updateValueAt(url, "test2.svg");
+ widget.updateValueAt(blur, 5);
+ is(widget.getCssValue(), "blur(5px) url(test2.svg)",
+ "Should update values correctly");
+
+ info("Test getValueAt method");
+ is(widget.getValueAt(blur), "5px",
+ "Should return value + unit");
+ is(widget.getValueAt(url), "test2.svg",
+ "Should return value for string-type filters");
+
+ info("Test removeAt method");
+ widget.removeAt(url);
+ is(widget.getCssValue(), "blur(5px)",
+ "Should remove the specified filter");
+
+ info("Test add method applying filter range to value");
+ const grayscale = widget.add("grayscale", GRAYSCALE_MAX + 1);
+ is(widget.getValueAt(grayscale), `${GRAYSCALE_MAX}%`,
+ "Shouldn't allow values higher than max");
+
+ const invert = widget.add("invert", INVERT_MIN - 1);
+ is(widget.getValueAt(invert), `${INVERT_MIN}%`,
+ "Shouldn't allow values less than INVERT_MIN");
+
+ info("Test updateValueAt method applying filter range to value");
+ widget.updateValueAt(grayscale, GRAYSCALE_MAX + 1);
+ is(widget.getValueAt(grayscale), `${GRAYSCALE_MAX}%`,
+ "Shouldn't allow values higher than max");
+
+ widget.updateValueAt(invert, INVERT_MIN - 1);
+ is(widget.getValueAt(invert), `${INVERT_MIN}%`,
+ "Shouldn't allow values less than INVERT_MIN");
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-04.js b/devtools/client/shared/test/browser_filter-editor-04.js
new file mode 100644
index 000000000..c1c8f4380
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-04.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget's drag-drop re-ordering
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+const LIST_ITEM_HEIGHT = 32;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "blur(2px) contrast(200%) brightness(200%)";
+ let widget = new CSSFilterEditorWidget(container, initialValue, cssIsValid);
+
+ const filters = widget.el.querySelector("#filters");
+ function first() {
+ return filters.children[0];
+ }
+ function mid() {
+ return filters.children[1];
+ }
+ function last() {
+ return filters.children[2];
+ }
+
+ info("Test re-ordering neighbour filters");
+ widget._mouseDown({
+ target: first().querySelector("i"),
+ pageY: 0
+ });
+ widget._mouseMove({ pageY: LIST_ITEM_HEIGHT });
+
+ // Element re-ordering should be instant
+ is(mid().querySelector("label").textContent, "blur",
+ "Should reorder elements correctly");
+
+ widget._mouseUp();
+
+ is(widget.getCssValue(), "contrast(200%) blur(2px) brightness(200%)",
+ "Should reorder filters objects correctly");
+
+ info("Test re-ordering first and last filters");
+ widget._mouseDown({
+ target: first().querySelector("i"),
+ pageY: 0
+ });
+ widget._mouseMove({ pageY: LIST_ITEM_HEIGHT * 2 });
+
+ // Element re-ordering should be instant
+ is(last().querySelector("label").textContent, "contrast",
+ "Should reorder elements correctly");
+ widget._mouseUp();
+
+ is(widget.getCssValue(), "brightness(200%) blur(2px) contrast(200%)",
+ "Should reorder filters objects correctly");
+
+ info("Test dragging first element out of list");
+ const boundaries = filters.getBoundingClientRect();
+
+ widget._mouseDown({
+ target: first().querySelector("i"),
+ pageY: 0
+ });
+ widget._mouseMove({ pageY: -LIST_ITEM_HEIGHT * 5 });
+ ok(first().getBoundingClientRect().top >= boundaries.top,
+ "First filter should not move outside filter list");
+
+ widget._mouseUp();
+
+ info("Test dragging last element out of list");
+ widget._mouseDown({
+ target: last().querySelector("i"),
+ pageY: 0
+ });
+ widget._mouseMove({ pageY: -LIST_ITEM_HEIGHT * 5 });
+ ok(last().getBoundingClientRect().bottom <= boundaries.bottom,
+ "Last filter should not move outside filter list");
+
+ widget._mouseUp();
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-05.js b/devtools/client/shared/test/browser_filter-editor-05.js
new file mode 100644
index 000000000..a18429542
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-05.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+// Tests the Filter Editor Widget's label-dragging
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const GRAYSCALE_MAX = 100,
+ GRAYSCALE_MIN = 0;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(
+ container, "grayscale(0%) url(test.svg)", cssIsValid
+ );
+
+ const filters = widget.el.querySelector("#filters");
+ const grayscale = filters.children[0];
+ const url = filters.children[1];
+
+ info("Test label-dragging on number-type filters without modifiers");
+ widget._mouseDown({
+ target: grayscale.querySelector("label"),
+ pageX: 0,
+ altKey: false,
+ shiftKey: false
+ });
+
+ widget._mouseMove({
+ pageX: 12,
+ altKey: false,
+ shiftKey: false
+ });
+ let expected = DEFAULT_VALUE_MULTIPLIER * 12;
+ is(widget.getValueAt(0),
+ `${expected}%`,
+ "Should update value correctly without modifiers");
+
+ info("Test label-dragging on number-type filters with alt");
+ widget._mouseMove({
+ // 20 - 12 = 8
+ pageX: 20,
+ altKey: true,
+ shiftKey: false
+ });
+
+ expected = expected + SLOW_VALUE_MULTIPLIER * 8;
+ is(widget.getValueAt(0),
+ `${expected}%`,
+ "Should update value correctly with alt key");
+
+ info("Test label-dragging on number-type filters with shift");
+ widget._mouseMove({
+ // 25 - 20 = 5
+ pageX: 25,
+ altKey: false,
+ shiftKey: true
+ });
+
+ expected = expected + FAST_VALUE_MULTIPLIER * 5;
+ is(widget.getValueAt(0),
+ `${expected}%`,
+ "Should update value correctly with shift key");
+
+ info("Test releasing mouse and dragging again");
+
+ widget._mouseUp();
+
+ widget._mouseDown({
+ target: grayscale.querySelector("label"),
+ pageX: 0,
+ altKey: false,
+ shiftKey: false
+ });
+
+ widget._mouseMove({
+ pageX: 5,
+ altKey: false,
+ shiftKey: false
+ });
+
+ expected = expected + DEFAULT_VALUE_MULTIPLIER * 5;
+ is(widget.getValueAt(0),
+ `${expected}%`,
+ "Should reset multiplier to default");
+
+ info("Test value ranges");
+
+ widget._mouseMove({
+ // 30 - 25 = 5
+ pageX: 30,
+ altKey: false,
+ shiftKey: true
+ });
+
+ expected = GRAYSCALE_MAX;
+ is(widget.getValueAt(0),
+ `${expected}%`,
+ "Shouldn't allow values higher than max");
+
+ widget._mouseMove({
+ pageX: -11,
+ altKey: false,
+ shiftKey: true
+ });
+
+ expected = GRAYSCALE_MIN;
+ is(widget.getValueAt(0),
+ `${expected}%`,
+ "Shouldn't allow values less than min");
+
+ widget._mouseUp();
+
+ info("Test label-dragging on string-type filters");
+ widget._mouseDown({
+ target: url.querySelector("label"),
+ pageX: 0,
+ altKey: false,
+ shiftKey: false
+ });
+
+ ok(!widget.isDraggingLabel,
+ "Label-dragging should not work for string-type filters");
+
+ widget._mouseMove({
+ pageX: -11,
+ altKey: false,
+ shiftKey: true
+ });
+
+ is(widget.getValueAt(1),
+ "test.svg",
+ "Label-dragging on string-type filters shouldn't affect their value");
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-06.js b/devtools/client/shared/test/browser_filter-editor-06.js
new file mode 100644
index 000000000..1e1a6c914
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-06.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget's add button
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/filterwidget.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+
+ const select = widget.el.querySelector("select"),
+ add = widget.el.querySelector("#add-filter");
+
+ const TEST_DATA = [
+ {
+ name: "blur",
+ unit: "px",
+ type: "length"
+ },
+ {
+ name: "contrast",
+ unit: "%",
+ type: "percentage"
+ },
+ {
+ name: "hue-rotate",
+ unit: "deg",
+ type: "angle"
+ },
+ {
+ name: "drop-shadow",
+ placeholder: L10N.getStr("dropShadowPlaceholder"),
+ type: "string"
+ },
+ {
+ name: "url",
+ placeholder: "example.svg#c1",
+ type: "string"
+ }
+ ];
+
+ info("Test adding new filters with different units");
+
+ for (let [index, filter] of TEST_DATA.entries()) {
+ select.value = filter.name;
+ add.click();
+
+ if (filter.unit) {
+ is(widget.getValueAt(index), `0${filter.unit}`,
+ `Should add ${filter.unit} to ${filter.type} filters`);
+ } else if (filter.placeholder) {
+ let i = index + 1;
+ const input = widget.el.querySelector(`.filter:nth-child(${i}) input`);
+ is(input.placeholder, filter.placeholder,
+ "Should set the appropriate placeholder for string-type filters");
+ }
+ }
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-07.js b/devtools/client/shared/test/browser_filter-editor-07.js
new file mode 100644
index 000000000..af2975d5c
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-07.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget's remove button
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(
+ container, "blur(2px) contrast(200%)", cssIsValid
+ );
+
+ info("Test removing filters with remove button");
+ widget.el.querySelector(".filter button").click();
+
+ is(widget.getCssValue(), "contrast(200%)",
+ "Should remove the clicked filter");
+});
diff --git a/devtools/client/shared/test/browser_filter-editor-08.js b/devtools/client/shared/test/browser_filter-editor-08.js
new file mode 100644
index 000000000..c30dbf299
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-08.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget inputs increase/decrease value using
+// arrow keys, applying multiplier using alt/shift on number-type filters
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "blur(2px)";
+ let widget = new CSSFilterEditorWidget(container, initialValue, cssIsValid);
+
+ let value = 2;
+
+ triggerKey = triggerKey.bind(widget);
+
+ info("Test simple arrow keys");
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), `${value}px`,
+ "Should decrease value using down arrow");
+
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), `${value}px`,
+ "Should decrease value using down arrow");
+
+ info("Test shift key multiplier");
+ triggerKey(38, "shiftKey");
+
+ value += FAST_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), `${value}px`,
+ "Should increase value by fast multiplier using up arrow");
+
+ triggerKey(40, "shiftKey");
+
+ value -= FAST_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), `${value}px`,
+ "Should decrease value by fast multiplier using down arrow");
+
+ info("Test alt key multiplier");
+ triggerKey(38, "altKey");
+
+ value += SLOW_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), `${value}px`,
+ "Should increase value by slow multiplier using up arrow");
+
+ triggerKey(40, "altKey");
+
+ value -= SLOW_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), `${value}px`,
+ "Should decrease value by slow multiplier using down arrow");
+
+ triggerKey = null;
+});
+
+// Triggers the specified keyCode and modifier key on
+// first filter's input
+function triggerKey(key, modifier) {
+ const filter = this.el.querySelector("#filters").children[0];
+ const input = filter.querySelector("input");
+
+ this._keyDown({
+ target: input,
+ keyCode: key,
+ [modifier]: true,
+ preventDefault: function () {}
+ });
+}
diff --git a/devtools/client/shared/test/browser_filter-editor-09.js b/devtools/client/shared/test/browser_filter-editor-09.js
new file mode 100644
index 000000000..1a358425e
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-09.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget inputs increase/decrease value when cursor is
+// on a number using arrow keys, applying multiplier using alt/shift on strings
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "drop-shadow(rgb(0, 0, 0) 1px 1px 0px)";
+ let widget = new CSSFilterEditorWidget(container, initialValue, cssIsValid);
+ widget.el.querySelector("#filters input").setSelectionRange(13, 13);
+
+ let value = 1;
+
+ triggerKey = triggerKey.bind(widget);
+
+ info("Test simple arrow keys");
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease value using down arrow");
+
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease value using down arrow");
+
+ info("Test shift key multiplier");
+ triggerKey(38, "shiftKey");
+
+ value += FAST_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should increase value by fast multiplier using up arrow");
+
+ triggerKey(40, "shiftKey");
+
+ value -= FAST_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease value by fast multiplier using down arrow");
+
+ info("Test alt key multiplier");
+ triggerKey(38, "altKey");
+
+ value += SLOW_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should increase value by slow multiplier using up arrow");
+
+ triggerKey(40, "altKey");
+
+ value -= SLOW_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease value by slow multiplier using down arrow");
+
+ triggerKey(40, "shiftKey");
+
+ value -= FAST_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease to negative");
+
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease negative numbers correctly");
+
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should increase negative values correctly");
+
+ triggerKey(40, "altKey");
+ triggerKey(40, "altKey");
+
+ value -= SLOW_VALUE_MULTIPLIER * 2;
+ is(widget.getValueAt(0), val(value),
+ "Should decrease float numbers correctly");
+
+ triggerKey(38, "altKey");
+
+ value += SLOW_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should increase float numbers correctly");
+
+ triggerKey = null;
+});
+
+// Triggers the specified keyCode and modifier key on
+// first filter's input
+function triggerKey(key, modifier) {
+ const filter = this.el.querySelector("#filters").children[0];
+ const input = filter.querySelector("input");
+
+ this._keyDown({
+ target: input,
+ keyCode: key,
+ [modifier]: true,
+ preventDefault: function () {}
+ });
+}
+
+function val(value) {
+ let v = value.toFixed(1);
+
+ if (v.indexOf(".0") > -1) {
+ v = v.slice(0, -2);
+ }
+ return `rgb(0, 0, 0) ${v}px 1px 0px`;
+}
diff --git a/devtools/client/shared/test/browser_filter-editor-10.js b/devtools/client/shared/test/browser_filter-editor-10.js
new file mode 100644
index 000000000..b73c53a83
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-editor-10.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Filter Editor Widget inputs increase/decrease value when cursor is
+// on a number using arrow keys if cursor is behind/mid/after the number strings
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ const initialValue = "drop-shadow(rgb(0, 0, 0) 10px 1px 0px)";
+ let widget = new CSSFilterEditorWidget(container, initialValue, cssIsValid);
+ const input = widget.el.querySelector("#filters input");
+
+ let value = 10;
+
+ triggerKey = triggerKey.bind(widget);
+
+ info("Test increment/decrement of string-type numbers without selection");
+
+ input.setSelectionRange(14, 14);
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should work with cursor in the middle of number");
+
+ input.setSelectionRange(13, 13);
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should work with cursor before the number");
+
+ input.setSelectionRange(15, 15);
+ triggerKey(40);
+
+ value -= DEFAULT_VALUE_MULTIPLIER;
+ is(widget.getValueAt(0), val(value),
+ "Should work with cursor after the number");
+
+ info("Test increment/decrement of string-type numbers with a selection");
+
+ input.setSelectionRange(13, 15);
+ triggerKey(38);
+ input.setSelectionRange(13, 18);
+ triggerKey(38);
+
+ value += DEFAULT_VALUE_MULTIPLIER * 2;
+ is(widget.getValueAt(0), val(value),
+ "Should work if a there is a selection, starting with the number");
+
+ triggerKey = null;
+});
+
+// Triggers the specified keyCode and modifier key on
+// first filter's input
+function triggerKey(key, modifier) {
+ const filter = this.el.querySelector("#filters").children[0];
+ const input = filter.querySelector("input");
+
+ this._keyDown({
+ target: input,
+ keyCode: key,
+ [modifier]: true,
+ preventDefault: function () {}
+ });
+}
+
+function val(value) {
+ let v = value.toFixed(1);
+
+ if (v.indexOf(".0") > -1) {
+ v = v.slice(0, -2);
+ }
+ return `rgb(0, 0, 0) ${v}px 1px 0px`;
+}
diff --git a/devtools/client/shared/test/browser_filter-presets-01.js b/devtools/client/shared/test/browser_filter-presets-01.js
new file mode 100644
index 000000000..859f5f63e
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-presets-01.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests saving presets
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+ // First render
+ yield widget.once("render");
+
+ const VALUE = "blur(2px) contrast(150%)";
+ const NAME = "Test";
+
+ yield showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
+
+ let preset = widget.el.querySelector(".preset");
+ is(preset.querySelector("label").textContent, NAME,
+ "Should show preset name correctly");
+ is(preset.querySelector("span").textContent, VALUE,
+ "Should show preset value preview correctly");
+
+ let list = yield widget.getPresets();
+ let input = widget.el.querySelector(".presets-list .footer input");
+ let data = list[0];
+
+ is(data.name, NAME,
+ "Should add the preset to asyncStorage - name property");
+ is(data.value, VALUE,
+ "Should add the preset to asyncStorage - name property");
+
+ info("Test overriding preset by using the same name");
+
+ const VALUE_2 = "saturate(50%) brightness(10%)";
+
+ widget.setCssValue(VALUE_2);
+
+ yield savePreset(widget);
+
+ is(widget.el.querySelectorAll(".preset").length, 1,
+ "Should override the preset with the same name - render");
+
+ list = yield widget.getPresets();
+ data = list[0];
+
+ is(list.length, 1,
+ "Should override the preset with the same name - asyncStorage");
+
+ is(data.name, NAME,
+ "Should override the preset with the same name - prop name");
+ is(data.value, VALUE_2,
+ "Should override the preset with the same name - prop value");
+
+ yield widget.setPresets([]);
+
+ info("Test saving a preset without name");
+ input.value = "";
+
+ yield savePreset(widget, "preset-save-error");
+
+ list = yield widget.getPresets();
+ is(list.length, 0,
+ "Should not add a preset without name");
+
+ info("Test saving a preset without filters");
+
+ input.value = NAME;
+ widget.setCssValue("none");
+
+ yield savePreset(widget, "preset-save-error");
+
+ list = yield widget.getPresets();
+ is(list.length, 0,
+ "Should not add a preset without filters (value: none)");
+});
+
+/**
+ * Call savePreset on widget and wait for the specified event to emit
+ * @param {CSSFilterWidget} widget
+ * @param {string} expectEvent="render" The event to listen on
+ * @return {Promise}
+ */
+function savePreset(widget, expectEvent = "render") {
+ let onEvent = widget.once(expectEvent);
+ widget._savePreset({
+ preventDefault: () => {},
+ });
+ return onEvent;
+}
diff --git a/devtools/client/shared/test/browser_filter-presets-02.js b/devtools/client/shared/test/browser_filter-presets-02.js
new file mode 100644
index 000000000..5e700ea94
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-presets-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests loading presets
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+ // First render
+ yield widget.once("render");
+
+ const VALUE = "blur(2px) contrast(150%)";
+ const NAME = "Test";
+
+ yield showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
+
+ let onRender = widget.once("render");
+ // reset value
+ widget.setCssValue("saturate(100%) brightness(150%)");
+ yield onRender;
+
+ let preset = widget.el.querySelector(".preset");
+
+ onRender = widget.once("render");
+ widget._presetClick({
+ target: preset
+ });
+
+ yield onRender;
+
+ is(widget.getCssValue(), VALUE,
+ "Should set widget's value correctly");
+ is(widget.el.querySelector(".presets-list .footer input").value, NAME,
+ "Should set input's value to name");
+});
diff --git a/devtools/client/shared/test/browser_filter-presets-03.js b/devtools/client/shared/test/browser_filter-presets-03.js
new file mode 100644
index 000000000..a61bf35db
--- /dev/null
+++ b/devtools/client/shared/test/browser_filter-presets-03.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests deleting presets
+
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const TEST_URI = `data:text/html,<div id="filter-container" />`;
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ const cssIsValid = getClientCssProperties().getValidityChecker(doc);
+
+ const container = doc.querySelector("#filter-container");
+ let widget = new CSSFilterEditorWidget(container, "none", cssIsValid);
+ // First render
+ yield widget.once("render");
+
+ const NAME = "Test";
+ const VALUE = "blur(2px) contrast(150%)";
+
+ yield showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
+
+ let removeButton = widget.el.querySelector(".preset .remove-button");
+ let onRender = widget.once("render");
+ widget._presetClick({
+ target: removeButton
+ });
+
+ yield onRender;
+ is(widget.el.querySelector(".preset"), null,
+ "Should re-render after removing preset");
+
+ let list = yield widget.getPresets();
+ is(list.length, 0,
+ "Should remove presets from asyncStorage");
+});
diff --git a/devtools/client/shared/test/browser_flame-graph-01.js b/devtools/client/shared/test/browser_flame-graph-01.js
new file mode 100644
index 000000000..a32fb9fd3
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-01.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that flame graph widget works properly.
+
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body);
+
+ let readyEventEmitted;
+ graph.once("ready", () => {
+ readyEventEmitted = true;
+ });
+
+ yield graph.ready();
+ ok(readyEventEmitted, "The 'ready' event should have been emitted");
+
+ testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ ok(graph._container.classList.contains("flame-graph-widget-container"),
+ "The correct graph container was created.");
+ ok(graph._canvas.classList.contains("flame-graph-widget-canvas"),
+ "The correct graph container was created.");
+
+ let bounds = host.frame.getBoundingClientRect();
+
+ is(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph has the correct height.");
+
+ ok(graph._selection.start === null,
+ "The graph's selection start value is initially null.");
+ ok(graph._selection.end === null,
+ "The graph's selection end value is initially null.");
+
+ ok(graph._selectionDragger.origin === null,
+ "The graph's dragger origin value is initially null.");
+ ok(graph._selectionDragger.anchor.start === null,
+ "The graph's dragger anchor start value is initially null.");
+ ok(graph._selectionDragger.anchor.end === null,
+ "The graph's dragger anchor end value is initially null.");
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-02.js b/devtools/client/shared/test/browser_flame-graph-02.js
new file mode 100644
index 000000000..e15c3efe0
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that flame graph widgets may have a fixed width or height.
+
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body);
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+
+ yield graph.ready();
+ testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ let bounds = host.frame.getBoundingClientRect();
+
+ isnot(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph should not span all the parent node's width.");
+ isnot(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph should not span all the parent node's height.");
+
+ is(graph.width, graph.fixedWidth * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, graph.fixedHeight * window.devicePixelRatio,
+ "The graph has the correct height.");
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-03a.js b/devtools/client/shared/test/browser_flame-graph-03a.js
new file mode 100644
index 000000000..10fcf9457
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-03a.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that selections in the flame graph widget work properly.
+
+const TEST_DATA = [
+ {
+ color: "#f00",
+ blocks: [
+ { x: 0, y: 0, width: 50, height: 20, text: "FOO" },
+ { x: 50, y: 0, width: 100, height: 20, text: "BAR" }
+ ]
+ },
+ {
+ color: "#00f",
+ blocks: [
+ { x: 0, y: 30, width: 30, height: 20, text: "BAZ" }
+ ]
+ }
+];
+const TEST_BOUNDS = { startTime: 0, endTime: 150 };
+const TEST_WIDTH = 200;
+const TEST_HEIGHT = 100;
+
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, 1);
+ graph.fixedWidth = TEST_WIDTH;
+ graph.fixedHeight = TEST_HEIGHT;
+ graph.horizontalPanThreshold = 0;
+ graph.verticalPanThreshold = 0;
+
+ yield graph.ready();
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ is(graph.getViewRange().startTime, 0,
+ "The selection start boundary is correct (1).");
+ is(graph.getViewRange().endTime, 150,
+ "The selection end boundary is correct (1).");
+
+ scroll(graph, 200, HORIZONTAL_AXIS, 10);
+ is(graph.getViewRange().startTime | 0, 75,
+ "The selection start boundary is correct (2).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (2).");
+
+ scroll(graph, -200, HORIZONTAL_AXIS, 10);
+ is(graph.getViewRange().startTime | 0, 37,
+ "The selection start boundary is correct (3).");
+ is(graph.getViewRange().endTime | 0, 112,
+ "The selection end boundary is correct (3).");
+
+ scroll(graph, 200, VERTICAL_AXIS, TEST_WIDTH / 2);
+ is(graph.getViewRange().startTime | 0, 34,
+ "The selection start boundary is correct (4).");
+ is(graph.getViewRange().endTime | 0, 115,
+ "The selection end boundary is correct (4).");
+
+ scroll(graph, -200, VERTICAL_AXIS, TEST_WIDTH / 2);
+ is(graph.getViewRange().startTime | 0, 37,
+ "The selection start boundary is correct (5).");
+ is(graph.getViewRange().endTime | 0, 112,
+ "The selection end boundary is correct (5).");
+
+ dragStart(graph, TEST_WIDTH / 2);
+ is(graph.getViewRange().startTime | 0, 37,
+ "The selection start boundary is correct (6).");
+ is(graph.getViewRange().endTime | 0, 112,
+ "The selection end boundary is correct (6).");
+
+ hover(graph, TEST_WIDTH / 2 - 10);
+ is(graph.getViewRange().startTime | 0, 41,
+ "The selection start boundary is correct (7).");
+ is(graph.getViewRange().endTime | 0, 116,
+ "The selection end boundary is correct (7).");
+
+ dragStop(graph, 10);
+ is(graph.getViewRange().startTime | 0, 71,
+ "The selection start boundary is correct (8).");
+ is(graph.getViewRange().endTime | 0, 145,
+ "The selection end boundary is correct (8).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
+
+var HORIZONTAL_AXIS = 1;
+var VERTICAL_AXIS = 2;
+
+function scroll(graph, wheel, axis, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseWheel({ testX: x, testY: y, axis, detail: wheel,
+ HORIZONTAL_AXIS,
+ VERTICAL_AXIS
+ });
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-03b.js b/devtools/client/shared/test/browser_flame-graph-03b.js
new file mode 100644
index 000000000..936eb831e
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-03b.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that selections in the flame graph widget work properly on HiDPI.
+
+const TEST_DATA = [
+ {
+ color: "#f00",
+ blocks: [
+ { x: 0, y: 0, width: 50, height: 20, text: "FOO" },
+ { x: 50, y: 0, width: 100, height: 20, text: "BAR" }
+ ]
+ },
+ {
+ color: "#00f",
+ blocks: [
+ { x: 0, y: 30, width: 30, height: 20, text: "BAZ" }
+ ]
+ }
+];
+const TEST_BOUNDS = { startTime: 0, endTime: 150 };
+const TEST_WIDTH = 200;
+const TEST_HEIGHT = 100;
+const TEST_DPI_DENSITIY = 2;
+
+var {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
+ graph.fixedWidth = TEST_WIDTH;
+ graph.fixedHeight = TEST_HEIGHT;
+
+ yield graph.ready();
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ is(graph.getViewRange().startTime, 0,
+ "The selection start boundary is correct on HiDPI (1).");
+ is(graph.getViewRange().endTime, 150,
+ "The selection end boundary is correct on HiDPI (1).");
+
+ is(graph.getOuterBounds().startTime, 0,
+ "The bounds start boundary is correct on HiDPI (1).");
+ is(graph.getOuterBounds().endTime, 150,
+ "The bounds end boundary is correct on HiDPI (1).");
+
+ scroll(graph, 10000, HORIZONTAL_AXIS, 1);
+
+ is(Math.round(graph.getViewRange().startTime), 150,
+ "The selection start boundary is correct on HiDPI (2).");
+ is(Math.round(graph.getViewRange().endTime), 150,
+ "The selection end boundary is correct on HiDPI (2).");
+
+ is(graph.getOuterBounds().startTime, 0,
+ "The bounds start boundary is correct on HiDPI (2).");
+ is(graph.getOuterBounds().endTime, 150,
+ "The bounds end boundary is correct on HiDPI (2).");
+}
+
+// EventUtils just doesn't work!
+
+var HORIZONTAL_AXIS = 1;
+var VERTICAL_AXIS = 2;
+
+function scroll(graph, wheel, axis, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseWheel({ testX: x, testY: y, axis, detail: wheel,
+ HORIZONTAL_AXIS,
+ VERTICAL_AXIS
+ });
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-03c.js b/devtools/client/shared/test/browser_flame-graph-03c.js
new file mode 100644
index 000000000..3a6bf80ae
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-03c.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that vertical panning in the flame graph widget works properly.
+
+const TEST_DATA = [
+ {
+ color: "#f00",
+ blocks: [
+ { x: 0, y: 0, width: 50, height: 20, text: "FOO" },
+ { x: 50, y: 0, width: 100, height: 20, text: "BAR" }
+ ]
+ },
+ {
+ color: "#00f",
+ blocks: [
+ { x: 0, y: 30, width: 30, height: 20, text: "BAZ" }
+ ]
+ }
+];
+const TEST_BOUNDS = { startTime: 0, endTime: 150 };
+const TEST_WIDTH = 200;
+const TEST_HEIGHT = 100;
+const TEST_DPI_DENSITIY = 2;
+
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
+ graph.fixedWidth = TEST_WIDTH;
+ graph.fixedHeight = TEST_HEIGHT;
+
+ yield graph.ready();
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ // Drag up vertically only.
+
+ dragStart(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (1).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (1).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (1).");
+
+ hover(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2 - 50);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (2).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (2).");
+ is(graph.getViewRange().verticalOffset | 0, 17,
+ "The vertical offset is correct (2).");
+
+ dragStop(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2 - 100);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (3).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (3).");
+ is(graph.getViewRange().verticalOffset | 0, 42,
+ "The vertical offset is correct (3).");
+
+ // Drag down strongly vertically and slightly horizontally.
+
+ dragStart(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (4).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (4).");
+ is(graph.getViewRange().verticalOffset | 0, 42,
+ "The vertical offset is correct (4).");
+
+ hover(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2 + 50);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (5).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (5).");
+ is(graph.getViewRange().verticalOffset | 0, 25,
+ "The vertical offset is correct (5).");
+
+ dragStop(graph, TEST_WIDTH / 2 + 100, TEST_HEIGHT / 2 + 500);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (6).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (6).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (6).");
+
+ // Drag up slightly vertically and strongly horizontally.
+
+ dragStart(graph, TEST_WIDTH / 2, TEST_HEIGHT / 2);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (7).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (7).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (7).");
+
+ hover(graph, TEST_WIDTH / 2 + 50, TEST_HEIGHT / 2);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (8).");
+ is(graph.getViewRange().endTime | 0, 116,
+ "The selection end boundary is correct (8).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (8).");
+
+ dragStop(graph, TEST_WIDTH / 2 + 500, TEST_HEIGHT / 2 + 100);
+ is(graph.getViewRange().startTime | 0, 0,
+ "The selection start boundary is correct (9).");
+ is(graph.getViewRange().endTime | 0, 0,
+ "The selection end boundary is correct (9).");
+ is(graph.getViewRange().verticalOffset | 0, 0,
+ "The vertical offset is correct (9).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-04.js b/devtools/client/shared/test/browser_flame-graph-04.js
new file mode 100644
index 000000000..5bcc112ec
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-04.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that text metrics in the flame graph widget work properly.
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const {ELLIPSIS} = require("devtools/shared/l10n");
+const {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+const {FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+const {FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new FlameGraph(doc.body, 1);
+ yield graph.ready();
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ is(graph._averageCharWidth, getAverageCharWidth(),
+ "The average char width was calculated correctly.");
+ is(graph._overflowCharWidth, getCharWidth(ELLIPSIS),
+ "The ellipsis char width was calculated correctly.");
+
+ let text = "This text is maybe overflowing";
+ let text1000px = graph._getFittedText(text, 1000);
+ let text50px = graph._getFittedText(text, 50);
+ let text10px = graph._getFittedText(text, 10);
+ let text1px = graph._getFittedText(text, 1);
+
+ is(graph._getTextWidthApprox(text), getAverageCharWidth() * text.length,
+ "The approximate width was calculated correctly.");
+
+ info("Text at 1000px width: " + text1000px);
+ info("Text at 50px width : " + text50px);
+ info("Text at 10px width : " + text10px);
+ info("Text at 1px width : " + text1px);
+
+ is(text1000px, text,
+ "The fitted text for 1000px width is correct.");
+
+ isnot(text50px, text,
+ "The fitted text for 50px width is correct (1).");
+
+ ok(text50px.includes(ELLIPSIS),
+ "The fitted text for 50px width is correct (2).");
+
+ is(graph._getFittedText(text, FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE + 1), ELLIPSIS,
+ "The fitted text for text font size width is correct.");
+
+ is(graph._getFittedText(text, 1), "",
+ "The fitted text for 1px width is correct.");
+}
+
+function getAverageCharWidth() {
+ let letterWidthsSum = 0;
+
+ let start = " ".charCodeAt(0);
+ let end = "z".charCodeAt(0) + 1;
+
+ for (let i = start; i < end; i++) {
+ let char = String.fromCharCode(i);
+ letterWidthsSum += getCharWidth(char);
+ }
+
+ return letterWidthsSum / (end - start);
+}
+
+function getCharWidth(char) {
+ let canvas = document.createElementNS(HTML_NS, "canvas");
+ let ctx = canvas.getContext("2d");
+
+ let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE;
+ let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
+ ctx.font = fontSize + "px " + fontFamily;
+
+ return ctx.measureText(char).width;
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-05.js b/devtools/client/shared/test/browser_flame-graph-05.js
new file mode 100644
index 000000000..1b30489dc
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-05.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that flame graph widget has proper keyboard support.
+
+const TEST_DATA = [
+ {
+ color: "#f00",
+ blocks: [
+ { x: 0, y: 0, width: 50, height: 20, text: "FOO" },
+ { x: 50, y: 0, width: 100, height: 20, text: "BAR" }
+ ]
+ },
+ {
+ color: "#00f",
+ blocks: [
+ { x: 0, y: 30, width: 30, height: 20, text: "BAZ" }
+ ]
+ }
+];
+const TEST_BOUNDS = { startTime: 0, endTime: 150 };
+const TEST_DPI_DENSITIY = 2;
+
+const KEY_CODE_UP = 38;
+const KEY_CODE_LEFT = 37;
+const KEY_CODE_RIGHT = 39;
+
+var {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
+ yield graph.ready();
+
+ yield testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ is(graph._selection.start, 0,
+ "The graph's selection start value is initially correct.");
+ is(graph._selection.end, TEST_BOUNDS.endTime * TEST_DPI_DENSITIY,
+ "The graph's selection end value is initially correct.");
+
+ yield pressKeyForTime(graph, KEY_CODE_LEFT, 1000);
+
+ is(graph._selection.start, 0,
+ "The graph's selection start value is correct after pressing LEFT.");
+ ok(graph._selection.end < TEST_BOUNDS.endTime * TEST_DPI_DENSITIY,
+ "The graph's selection end value is correct after pressing LEFT.");
+
+ graph._selection.start = 0;
+ graph._selection.end = TEST_BOUNDS.endTime * TEST_DPI_DENSITIY;
+ info("Graph selection was reset (1).");
+
+ yield pressKeyForTime(graph, KEY_CODE_RIGHT, 1000);
+
+ ok(graph._selection.start > 0,
+ "The graph's selection start value is correct after pressing RIGHT.");
+ is(graph._selection.end, TEST_BOUNDS.endTime * TEST_DPI_DENSITIY,
+ "The graph's selection end value is correct after pressing RIGHT.");
+
+ graph._selection.start = 0;
+ graph._selection.end = TEST_BOUNDS.endTime * TEST_DPI_DENSITIY;
+ info("Graph selection was reset (2).");
+
+ yield pressKeyForTime(graph, KEY_CODE_UP, 1000);
+
+ ok(graph._selection.start > 0,
+ "The graph's selection start value is correct after pressing UP.");
+ ok(graph._selection.end < TEST_BOUNDS.endTime * TEST_DPI_DENSITIY,
+ "The graph's selection end value is correct after pressing UP.");
+
+ let distanceLeft = graph._selection.start;
+ let distanceRight = TEST_BOUNDS.endTime * TEST_DPI_DENSITIY - graph._selection.end;
+
+ ok(Math.abs(distanceRight - distanceLeft) < 0.1,
+ "The graph zoomed correctly towards the center point.");
+}
+
+function pressKeyForTime(graph, keyCode, ms) {
+ graph._onKeyDown({
+ keyCode,
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ });
+
+ return new Promise(resolve => {
+ setTimeout(() => {
+ graph._onKeyUp({
+ keyCode,
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ });
+ resolve();
+ }, ms);
+ });
+}
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-01.js b/devtools/client/shared/test/browser_flame-graph-utils-01.js
new file mode 100644
index 000000000..6871e234c
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-01.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that text metrics and data conversion from profiler samples
+// widget work properly in the flame graph.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA);
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "M"
+ }, {
+ location: "N",
+ }, {
+ location: "P"
+ }],
+ time: 50,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 100,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "D"
+ }],
+ time: 210,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "E",
+ }, {
+ location: "F"
+ }],
+ time: 330,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 460,
+}, {
+ frames: [{
+ location: "X"
+ }, {
+ location: "Y",
+ }, {
+ location: "Z"
+ }],
+ time: 500
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 50,
+ frameKey: "A",
+ x: 50,
+ y: 0,
+ width: 410,
+ height: 15,
+ text: "A"
+ }]
+}, {
+ blocks: [{
+ startTime: 50,
+ frameKey: "B",
+ x: 50,
+ y: 15,
+ width: 160,
+ height: 15,
+ text: "B"
+ }, {
+ startTime: 330,
+ frameKey: "B",
+ x: 330,
+ y: 15,
+ width: 130,
+ height: 15,
+ text: "B"
+ }]
+}, {
+ blocks: [{
+ startTime: 50,
+ frameKey: "C",
+ x: 50,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "C"
+ }, {
+ startTime: 330,
+ frameKey: "C",
+ x: 330,
+ y: 30,
+ width: 130,
+ height: 15,
+ text: "C"
+ }]
+}, {
+ blocks: [{
+ startTime: 100,
+ frameKey: "D",
+ x: 100,
+ y: 30,
+ width: 110,
+ height: 15,
+ text: "D"
+ }, {
+ startTime: 460,
+ frameKey: "X",
+ x: 460,
+ y: 0,
+ width: 40,
+ height: 15,
+ text: "X"
+ }]
+}, {
+ blocks: [{
+ startTime: 210,
+ frameKey: "E",
+ x: 210,
+ y: 15,
+ width: 120,
+ height: 15,
+ text: "E"
+ }, {
+ startTime: 460,
+ frameKey: "Y",
+ x: 460,
+ y: 15,
+ width: 40,
+ height: 15,
+ text: "Y"
+ }]
+}, {
+ blocks: [{
+ startTime: 210,
+ frameKey: "F",
+ x: 210,
+ y: 30,
+ width: 120,
+ height: 15,
+ text: "F"
+ }, {
+ startTime: 460,
+ frameKey: "Z",
+ x: 460,
+ y: 30,
+ width: 40,
+ height: 15,
+ text: "Z"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "M",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "M"
+ }]
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "N",
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "N"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "P",
+ x: 0,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "P"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-02.js b/devtools/client/shared/test/browser_flame-graph-utils-02.js
new file mode 100644
index 000000000..15e9d1933
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-02.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests consecutive duplicate frames are removed from the flame graph data.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA, {
+ flattenRecursion: true
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 50,
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "A",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "A"
+ }]
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "B",
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "B"
+ }]
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "C",
+ x: 0,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "C"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-03.js b/devtools/client/shared/test/browser_flame-graph-utils-03.js
new file mode 100644
index 000000000..0f28c0afc
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-03.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if platform frames are removed from the flame graph data.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA, {
+ contentOnly: true
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "http://A"
+ }, {
+ location: "https://B"
+ }, {
+ location: "file://C",
+ }, {
+ location: "chrome://D"
+ }, {
+ location: "resource://E"
+ }],
+ time: 50,
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "http://A",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "http://A"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "Gecko",
+ x: 0,
+ y: 45,
+ width: 50,
+ height: 15,
+ text: "Gecko"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "https://B",
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "https://B"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "file://C",
+ x: 0,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "file://C"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-04.js b/devtools/client/shared/test/browser_flame-graph-utils-04.js
new file mode 100644
index 000000000..1bf6c1f59
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-04.js
@@ -0,0 +1,188 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if (idle) nodes are added when necessary in the flame graph data.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA, {
+ flattenRecursion: true,
+ contentOnly: true,
+ showIdleBlocks: "\m/"
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "http://A"
+ }, {
+ location: "http://A"
+ }, {
+ location: "http://A"
+ }, {
+ location: "https://B"
+ }, {
+ location: "https://B"
+ }, {
+ location: "file://C",
+ }, {
+ location: "chrome://D"
+ }, {
+ location: "resource://E"
+ }],
+ time: 50
+}, {
+ frames: [],
+ time: 100
+}, {
+ frames: [{
+ location: "http://A"
+ }, {
+ location: "https://B"
+ }, {
+ location: "file://C",
+ }],
+ time: 150
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "http://A",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "http://A"
+ }, {
+ startTime: 100,
+ frameKey: "http://A",
+ x: 100,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "http://A"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "Gecko",
+ x: 0,
+ y: 45,
+ width: 50,
+ height: 15,
+ text: "Gecko"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "https://B",
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "https://B"
+ }, {
+ startTime: 100,
+ frameKey: "https://B",
+ x: 100,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: "https://B"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "file://C",
+ x: 0,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "file://C"
+ }, {
+ startTime: 100,
+ frameKey: "file://C",
+ x: 100,
+ y: 30,
+ width: 50,
+ height: 15,
+ text: "file://C"
+ }]
+}, {
+ blocks: [{
+ startTime: 50,
+ frameKey: "m/",
+ x: 50,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "m/"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-05.js b/devtools/client/shared/test/browser_flame-graph-utils-05.js
new file mode 100644
index 000000000..5abdd708a
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-05.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that flame graph data is cached, and that the cache may be cleared.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out1 = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA);
+ let out2 = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA);
+ is(out1, out2, "The outputted data is identical.");
+
+ let out3 = FlameGraphUtils.createFlameGraphDataFromThread(
+ TEST_DATA, { flattenRecursion: true }
+ );
+ is(out2, out3, "The outputted data is still identical.");
+
+ FlameGraphUtils.removeFromCache(TEST_DATA);
+ let out4 = FlameGraphUtils.createFlameGraphDataFromThread(
+ TEST_DATA, { flattenRecursion: true }
+ );
+ isnot(out3, out4, "The outputted data is not identical anymore.");
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 50,
+}]);
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-06.js b/devtools/client/shared/test/browser_flame-graph-utils-06.js
new file mode 100644
index 000000000..886a1035b
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-06.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the text displayed is the function name, file name and line number
+// if applicable and demangling.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+const {PALLETTE_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
+const MANGLED_FN = "__Z3FooIiEvv";
+const UNMANGLED_FN = "void Foo<int>()";
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromThread(TEST_DATA, {
+ flattenRecursion: true
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, PALLETTE_SIZE, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+var TEST_DATA = synthesizeProfileForTest([{
+ frames: [{
+ location: "A (http://path/to/file.js:10:5)"
+ }, {
+ location: `${MANGLED_FN} (http://path/to/file.js:100:5)`
+ }],
+ time: 50,
+}]);
+
+var EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: "A (http://path/to/file.js:10:5)",
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 15,
+ text: "A (file.js:10)"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ startTime: 0,
+ frameKey: `${MANGLED_FN} (http://path/to/file.js:100:5)`,
+ x: 0,
+ y: 15,
+ width: 50,
+ height: 15,
+ text: `${UNMANGLED_FN} (file.js:100)`
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/devtools/client/shared/test/browser_flame-graph-utils-hash.js b/devtools/client/shared/test/browser_flame-graph-utils-hash.js
new file mode 100644
index 000000000..6b441bbf5
--- /dev/null
+++ b/devtools/client/shared/test/browser_flame-graph-utils-hash.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if (idle) nodes are added when necessary in the flame graph data.
+
+const {FlameGraphUtils} = require("devtools/client/shared/widgets/FlameGraph");
+
+add_task(function* () {
+ let hash1 = FlameGraphUtils._getStringHash("abc");
+ let hash2 = FlameGraphUtils._getStringHash("acb");
+ let hash3 = FlameGraphUtils._getStringHash(Array.from(Array(100000)).join("a"));
+ let hash4 = FlameGraphUtils._getStringHash(Array.from(Array(100000)).join("b"));
+
+ isnot(hash1, hash2, "The hashes should not be equal (1).");
+ isnot(hash2, hash3, "The hashes should not be equal (2).");
+ isnot(hash3, hash4, "The hashes should not be equal (3).");
+
+ ok(Number.isInteger(hash1), "The hashes should be integers, not Infinity or NaN (1).");
+ ok(Number.isInteger(hash2), "The hashes should be integers, not Infinity or NaN (2).");
+ ok(Number.isInteger(hash3), "The hashes should be integers, not Infinity or NaN (3).");
+ ok(Number.isInteger(hash4), "The hashes should be integers, not Infinity or NaN (4).");
+});
diff --git a/devtools/client/shared/test/browser_graphs-01.js b/devtools/client/shared/test/browser_graphs-01.js
new file mode 100644
index 000000000..c4f5640d9
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-01.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets works properly.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+ finish();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ let readyEventEmitted;
+ graph.once("ready", () => {
+ readyEventEmitted = true;
+ });
+
+ yield graph.ready();
+ ok(readyEventEmitted, "The 'ready' event should have been emitted");
+
+ testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ ok(graph._container.classList.contains("line-graph-widget-container"),
+ "The correct graph container was created.");
+ ok(graph._canvas.classList.contains("line-graph-widget-canvas"),
+ "The correct graph container was created.");
+
+ let bounds = host.frame.getBoundingClientRect();
+
+ is(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph has the correct height.");
+
+ ok(graph._cursor.x === null,
+ "The graph's cursor X coordinate is initially null.");
+ ok(graph._cursor.y === null,
+ "The graph's cursor Y coordinate is initially null.");
+
+ ok(graph._selection.start === null,
+ "The graph's selection start value is initially null.");
+ ok(graph._selection.end === null,
+ "The graph's selection end value is initially null.");
+
+ ok(graph._selectionDragger.origin === null,
+ "The graph's dragger origin value is initially null.");
+ ok(graph._selectionDragger.anchor.start === null,
+ "The graph's dragger anchor start value is initially null.");
+ ok(graph._selectionDragger.anchor.end === null,
+ "The graph's dragger anchor end value is initially null.");
+
+ ok(graph._selectionResizer.margin === null,
+ "The graph's resizer margin value is initially null.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-02.js b/devtools/client/shared/test/browser_graphs-02.js
new file mode 100644
index 000000000..def728722
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-02.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets can properly add data, regions and highlights.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testDataAndRegions(graph);
+ testHighlights(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testDataAndRegions(graph) {
+ let thrown1;
+ try {
+ graph.setRegions(TEST_REGIONS);
+ } catch (e) {
+ thrown1 = true;
+ }
+ ok(thrown1, "Setting regions before setting data shouldn't work.");
+
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ let thrown2;
+ try {
+ graph.setRegions(TEST_REGIONS);
+ } catch (e) {
+ thrown2 = true;
+ }
+ ok(thrown2, "Setting regions twice shouldn't work.");
+
+ ok(graph.hasData(), "The graph should now have the data source set.");
+ ok(graph.hasRegions(), "The graph should now have the regions set.");
+
+ is(graph.dataScaleX,
+ // last & first tick in TEST_DATA
+ graph.width / 4180,
+ "The data scale on the X axis is correct.");
+
+ is(graph.dataScaleY,
+ // max value in TEST_DATA * GRAPH_DAMPEN_VALUES
+ graph.height / 60 * 0.85,
+ "The data scale on the Y axis is correct.");
+
+ for (let i = 0; i < TEST_REGIONS.length; i++) {
+ let original = TEST_REGIONS[i];
+ let normalized = graph._regions[i];
+
+ is(original.start * graph.dataScaleX, normalized.start,
+ "The region's start value was properly normalized.");
+ is(original.end * graph.dataScaleX, normalized.end,
+ "The region's end value was properly normalized.");
+ }
+}
+
+function testHighlights(graph) {
+ graph.setMask(TEST_REGIONS);
+ ok(graph.hasMask(),
+ "The graph should now have the highlights set.");
+
+ graph.setMask([]);
+ ok(graph.hasMask(),
+ "The graph shouldn't have anything highlighted.");
+
+ graph.setMask(null);
+ ok(!graph.hasMask(),
+ "The graph should have everything highlighted.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-03.js b/devtools/client/shared/test/browser_graphs-03.js
new file mode 100644
index 000000000..b44d4620a
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-03.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets can handle clients getting/setting the
+// selection or cursor.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ yield testSelection(graph);
+ yield testCursor(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testSelection(graph) {
+ ok(graph.getSelection().start === null,
+ "The graph's selection should initially have a null start value.");
+ ok(graph.getSelection().end === null,
+ "The graph's selection should initially have a null end value.");
+ ok(!graph.hasSelection(),
+ "There shouldn't initially be any selection.");
+
+ let selected = graph.once("selecting");
+ graph.setSelection({ start: 100, end: 200 });
+
+ yield selected;
+ ok(true, "A 'selecting' event has been fired.");
+
+ ok(graph.hasSelection(),
+ "There should now be a selection.");
+ is(graph.getSelection().start, 100,
+ "The graph's selection now has an updated start value.");
+ is(graph.getSelection().end, 200,
+ "The graph's selection now has an updated end value.");
+
+ let thrown;
+ try {
+ graph.setSelection({ start: null, end: null });
+ } catch (e) {
+ thrown = true;
+ }
+ ok(thrown, "Setting a null selection shouldn't work.");
+
+ ok(graph.hasSelection(),
+ "There should still be a selection.");
+
+ let deselected = graph.once("deselecting");
+ graph.dropSelection();
+
+ yield deselected;
+ ok(true, "A 'deselecting' event has been fired.");
+
+ ok(!graph.hasSelection(),
+ "There shouldn't be any selection anymore.");
+ ok(graph.getSelection().start === null,
+ "The graph's selection now has a null start value.");
+ ok(graph.getSelection().end === null,
+ "The graph's selection now has a null end value.");
+}
+
+function* testCursor(graph) {
+ ok(graph.getCursor().x === null,
+ "The graph's cursor should initially have a null X value.");
+ ok(graph.getCursor().y === null,
+ "The graph's cursor should initially have a null Y value.");
+ ok(!graph.hasCursor(),
+ "There shouldn't initially be any cursor.");
+
+ graph.setCursor({ x: 100, y: 50 });
+
+ ok(graph.hasCursor(),
+ "There should now be a cursor.");
+ is(graph.getCursor().x, 100,
+ "The graph's cursor now has an updated start value.");
+ is(graph.getCursor().y, 50,
+ "The graph's cursor now has an updated end value.");
+
+ let thrown;
+ try {
+ graph.setCursor({ x: null, y: null });
+ } catch (e) {
+ thrown = true;
+ }
+ ok(thrown, "Setting a null cursor shouldn't work.");
+
+ ok(graph.hasCursor(),
+ "There should still be a cursor.");
+
+ graph.dropCursor();
+
+ ok(!graph.hasCursor(),
+ "There shouldn't be any cursor anymore.");
+ ok(graph.getCursor().x === null,
+ "The graph's cursor now has a null start value.");
+ ok(graph.getCursor().y === null,
+ "The graph's cursor now has a null end value.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-04.js b/devtools/client/shared/test/browser_graphs-04.js
new file mode 100644
index 000000000..452b27c4a
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-04.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets can correctly compare selections and cursors.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ ok(!graph.hasSelection(),
+ "There shouldn't initially be any selection.");
+ is(graph.getSelectionWidth(), 0,
+ "The selection width should be 0 when there's no selection.");
+
+ graph.setSelection({ start: 100, end: 200 });
+
+ ok(graph.hasSelection(),
+ "There should now be a selection.");
+ is(graph.getSelectionWidth(), 100,
+ "The selection width should now be 100.");
+
+ ok(graph.isSelectionDifferent({ start: 100, end: 201 }),
+ "The selection was correctly reported to be different (1).");
+ ok(graph.isSelectionDifferent({ start: 101, end: 200 }),
+ "The selection was correctly reported to be different (2).");
+ ok(graph.isSelectionDifferent({ start: null, end: null }),
+ "The selection was correctly reported to be different (3).");
+ ok(graph.isSelectionDifferent(null),
+ "The selection was correctly reported to be different (4).");
+
+ ok(!graph.isSelectionDifferent({ start: 100, end: 200 }),
+ "The selection was incorrectly reported to be different (1).");
+ ok(!graph.isSelectionDifferent(graph.getSelection()),
+ "The selection was incorrectly reported to be different (2).");
+
+ graph.setCursor({ x: 100, y: 50 });
+
+ ok(graph.isCursorDifferent({ x: 100, y: 51 }),
+ "The cursor was correctly reported to be different (1).");
+ ok(graph.isCursorDifferent({ x: 101, y: 50 }),
+ "The cursor was correctly reported to be different (2).");
+ ok(graph.isCursorDifferent({ x: null, y: null }),
+ "The cursor was correctly reported to be different (3).");
+ ok(graph.isCursorDifferent(null),
+ "The cursor was correctly reported to be different (4).");
+
+ ok(!graph.isCursorDifferent({ x: 100, y: 50 }),
+ "The cursor was incorrectly reported to be different (1).");
+ ok(!graph.isCursorDifferent(graph.getCursor()),
+ "The cursor was incorrectly reported to be different (2).");
+}
diff --git a/devtools/client/shared/test/browser_graphs-05.js b/devtools/client/shared/test/browser_graphs-05.js
new file mode 100644
index 000000000..bd3da9128
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-05.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets can correctly determine which regions are hovered.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ ok(!graph.getHoveredRegion(),
+ "There should be no hovered region yet because there's no regions.");
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(!graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should not be hovered.");
+
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ ok(!graph.getHoveredRegion(),
+ "There should be no hovered region yet because there's no cursor.");
+
+ graph.setCursor({ x: TEST_REGIONS[0].start * graph.dataScaleX - 1, y: 0 });
+ ok(!graph.getHoveredRegion(),
+ "There shouldn't be any hovered region yet.");
+
+ graph.setCursor({ x: TEST_REGIONS[0].start * graph.dataScaleX + 1, y: 0 });
+ ok(graph.getHoveredRegion(),
+ "There should be a hovered region now.");
+ is(graph.getHoveredRegion().start, 320 * graph.dataScaleX,
+ "The reported hovered region is correct (1).");
+ is(graph.getHoveredRegion().end, 460 * graph.dataScaleX,
+ "The reported hovered region is correct (2).");
+
+ graph.setSelection({ start: 100, end: 200 });
+
+ info("Setting cursor over the left boundary.");
+ graph.setCursor({ x: 100, y: 0 });
+
+ ok(graph._isHoveringStartBoundary(),
+ "The graph start boundary should be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor near the left boundary.");
+ graph.setCursor({ x: 105, y: 0 });
+
+ ok(graph._isHoveringStartBoundary(),
+ "The graph start boundary should be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(graph._isHoveringSelectionContents(),
+ "The graph contents should be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor over the selection.");
+ graph.setCursor({ x: 150, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(graph._isHoveringSelectionContents(),
+ "The graph contents should be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor near the right boundary.");
+ graph.setCursor({ x: 195, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(graph._isHoveringEndBoundary(),
+ "The graph end boundary should be hovered.");
+ ok(graph._isHoveringSelectionContents(),
+ "The graph contents should be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor over the right boundary.");
+ graph.setCursor({ x: 200, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(graph._isHoveringEndBoundary(),
+ "The graph end boundary should be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting away from the selection.");
+ graph.setCursor({ x: 300, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(!graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should not be hovered.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-06.js b/devtools/client/shared/test/browser_graphs-06.js
new file mode 100644
index 000000000..596fe7702
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-06.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if clicking on regions adds a selection spanning that region.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ click(graph, (graph._regions[0].start + graph._regions[0].end) / 2);
+ is(graph.getSelection().start, graph._regions[0].start,
+ "The first region is now selected (1).");
+ is(graph.getSelection().end, graph._regions[0].end,
+ "The first region is now selected (2).");
+
+ let min = map(graph.getSelection().start, 0, graph.width, 112, 4180);
+ let max = map(graph.getSelection().end, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (1).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (2).");
+
+ click(graph, (graph._regions[1].start + graph._regions[1].end) / 2);
+ is(graph.getSelection().start, graph._regions[1].start,
+ "The second region is now selected (1).");
+ is(graph.getSelection().end, graph._regions[1].end,
+ "The second region is now selected (2).");
+
+ min = map(graph.getSelection().start, 0, graph.width, 112, 4180);
+ max = map(graph.getSelection().end, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (3).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (4).");
+
+ graph.setSelection({ start: graph.width, end: 0 });
+ min = map(0, 0, graph.width, 112, 4180);
+ max = map(graph.width, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (5).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (6).");
+
+ graph.setSelection({ start: graph.width + 100, end: -100 });
+ min = map(0, 0, graph.width, 112, 4180);
+ max = map(graph.width, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (7).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (8).");
+}
+
+/**
+ * Maps a value from one range to another.
+ */
+function map(value, istart, istop, ostart, ostop) {
+ return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
+}
+
+// EventUtils just doesn't work!
+
+function click(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-07a.js b/devtools/client/shared/test/browser_graphs-07a.js
new file mode 100644
index 000000000..44e166cc5
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07a.js
@@ -0,0 +1,232 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if selecting, resizing, moving selections and zooming in/out works.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+ testGraph(graph, normalDragStop);
+ yield graph.destroy();
+
+ let graph2 = new LineGraphWidget(doc.body, "fps");
+ yield graph2.once("ready");
+ testGraph(graph2, buggyDragStop);
+ yield graph2.destroy();
+
+ host.destroy();
+}
+
+function testGraph(graph, dragStop) {
+ graph.setData(TEST_DATA);
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 400);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 500);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (3).");
+
+ info("Making a new selection.");
+
+ dragStart(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (4).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (4).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (4).");
+
+ hover(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (5).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (5).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (5).");
+
+ dragStop(graph, 400);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (6).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (6).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (6).");
+
+ info("Resizing by dragging the end handlebar.");
+
+ dragStart(graph, 400);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (7).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (7).");
+
+ dragStop(graph, 600);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (8).");
+ is(graph.getSelection().end, 600,
+ "The current selection end value is correct (8).");
+
+ info("Resizing by dragging the start handlebar.");
+
+ dragStart(graph, 200);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (9).");
+ is(graph.getSelection().end, 600,
+ "The current selection end value is correct (9).");
+
+ dragStop(graph, 100);
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (10).");
+ is(graph.getSelection().end, 600,
+ "The current selection end value is correct (10).");
+
+ info("Moving by dragging the selection.");
+
+ dragStart(graph, 300);
+ hover(graph, 400);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (11).");
+ is(graph.getSelection().end, 700,
+ "The current selection end value is correct (11).");
+
+ dragStop(graph, 500);
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (12).");
+ is(graph.getSelection().end, 800,
+ "The current selection end value is correct (12).");
+
+ info("Zooming in by scrolling inside the selection.");
+
+ scroll(graph, -1000, 600);
+ is(graph.getSelection().start, 525,
+ "The current selection start value is correct (13).");
+ is(graph.getSelection().end, 650,
+ "The current selection end value is correct (13).");
+
+ info("Zooming out by scrolling inside the selection.");
+
+ scroll(graph, 1000, 600);
+ is(graph.getSelection().start, 468.75,
+ "The current selection start value is correct (14).");
+ is(graph.getSelection().end, 687.5,
+ "The current selection end value is correct (14).");
+
+ info("Sliding left by scrolling outside the selection.");
+
+ scroll(graph, 100, 900);
+ is(graph.getSelection().start, 458.75,
+ "The current selection start value is correct (15).");
+ is(graph.getSelection().end, 677.5,
+ "The current selection end value is correct (15).");
+
+ info("Sliding right by scrolling outside the selection.");
+
+ scroll(graph, -100, 900);
+ is(graph.getSelection().start, 468.75,
+ "The current selection start value is correct (16).");
+ is(graph.getSelection().end, 687.5,
+ "The current selection end value is correct (16).");
+
+ info("Zooming out a lot.");
+
+ scroll(graph, Number.MAX_SAFE_INTEGER, 500);
+ is(graph.getSelection().start, 1,
+ "The current selection start value is correct (17).");
+ is(graph.getSelection().end, graph.width - 1,
+ "The current selection end value is correct (17).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function normalDragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
+
+function buggyDragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+
+ graph._onMouseMove({ testX: x, testY: y });
+
+ // Only fire a mousemove with no buttons instead of a mouseup.
+ // This happens when the mouseup happens outside of the window.
+ // Send different coordinates to make sure the selection is preserved,
+ // see Bugs 1066504 and 1144779.
+ graph._onMouseMove({ testX: x + 1, testY: y + 1, buttons: 0 });
+}
+
+function scroll(graph, wheel, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseWheel({ testX: x, testY: y, detail: wheel });
+}
diff --git a/devtools/client/shared/test/browser_graphs-07b.js b/devtools/client/shared/test/browser_graphs-07b.js
new file mode 100644
index 000000000..3b4bc5740
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07b.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if selections can't be added via clicking, while not allowed.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+var LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+ graph.selectionEnabled = false;
+
+ info("Attempting to make a selection.");
+
+ dragStart(graph, 300);
+ is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+ "The graph shouldn't have a selection (1).");
+
+ hover(graph, 400);
+ is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+ "The graph shouldn't have a selection (2).");
+
+ dragStop(graph, 500);
+ is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+ "The graph shouldn't have a selection (3).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-07c.js b/devtools/client/shared/test/browser_graphs-07c.js
new file mode 100644
index 000000000..1791ced12
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07c.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if movement via event dispatching using screenX / screenY
+// works. All of the other tests directly use the graph's mouse event
+// callbacks with textX / testY for convenience.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+ testGraph(graph);
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 400);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 500);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (3).");
+
+ info("Making a new selection.");
+
+ dragStart(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (4).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (4).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (4).");
+
+ hover(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (5).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (5).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (5).");
+
+ dragStop(graph, 400);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (6).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (6).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (6).");
+}
+
+// EventUtils just doesn't work!
+
+function dispatchEvent(graph, x, y, type) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ let quad = graph._canvas.getBoxQuads({
+ relativeTo: window.document
+ })[0];
+
+ let screenX = window.screenX + quad.p1.x + x;
+ let screenY = window.screenY + quad.p1.y + y;
+
+ graph._canvas.dispatchEvent(new MouseEvent(type, {
+ bubbles: true,
+ cancelable: true,
+ buttons: 1,
+ view: window,
+ screenX: screenX,
+ screenY: screenY,
+ }));
+}
+
+function hover(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, "mousemove");
+}
+
+function dragStart(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, "mousemove");
+ dispatchEvent(graph, x, y, "mousedown");
+}
+
+function dragStop(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, "mousemove");
+ dispatchEvent(graph, x, y, "mouseup");
+}
diff --git a/devtools/client/shared/test/browser_graphs-07d.js b/devtools/client/shared/test/browser_graphs-07d.js
new file mode 100644
index 000000000..8a64f7d75
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07d.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that selections are drawn onto the canvas.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ // Measure the color of the first pixel before any selection is made.
+ graph._onAnimationFrame();
+ let pixelNoSelection = graph._ctx.getImageData(1, 1, 1, 1).data;
+
+ graph.setSelection({ start: 0, end: 10 });
+ graph._onAnimationFrame();
+ let pixelNormalSelection = graph._ctx.getImageData(1, 1, 1, 1).data;
+
+ Assert.notDeepEqual(pixelNormalSelection, pixelNoSelection,
+ "The first pixel is part of the drawn selection.");
+
+ graph.setSelection({ start: graph.width + 100, end: -100 });
+ graph._onAnimationFrame();
+ let pixelFullSelection = graph._ctx.getImageData(1, 1, 1, 1).data;
+
+ Assert.deepEqual(pixelFullSelection, pixelNormalSelection,
+ "The first pixel is still part of the drawn selection.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-07e.js b/devtools/client/shared/test/browser_graphs-07e.js
new file mode 100644
index 000000000..814284b9d
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-07e.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that selections are drawn onto the canvas.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+let CURRENT_ZOOM = 1;
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+ graph.setData(TEST_DATA);
+
+ info("Testing with normal zoom.");
+ testGraph(graph);
+
+ info("Testing while zoomed out.");
+ setZoom(host.frame, .5);
+ testGraph(graph);
+
+ info("Testing while zoomed in.");
+ setZoom(host.frame, 2);
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.dropSelection();
+
+ info("Making a selection.");
+
+ dragStart(graph, 100);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 100,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 300);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (3).");
+}
+
+function setZoom(frame, zoomValue) {
+ let contViewer = frame.docShell.contentViewer;
+ CURRENT_ZOOM = contViewer.fullZoom = zoomValue;
+}
+
+// EventUtils just doesn't work!
+
+function dispatchEvent(graph, x, y, fn) {
+ x *= CURRENT_ZOOM;
+ y *= CURRENT_ZOOM;
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ let quad = graph._canvas.getBoxQuads({
+ relativeTo: window.document
+ })[0];
+
+ let screenX = (window.screenX + quad.p1.x + x);
+ let screenY = (window.screenY + quad.p1.y + y);
+
+ fn({
+ screenX: screenX,
+ screenY: screenY,
+ });
+}
+
+function hover(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, graph._onMouseMove);
+}
+
+function dragStart(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, graph._onMouseMove);
+ dispatchEvent(graph, x, y, graph._onMouseDown);
+}
+
+function dragStop(graph, x, y = 1) {
+ dispatchEvent(graph, x, y, graph._onMouseMove);
+ dispatchEvent(graph, x, y, graph._onMouseUp);
+}
diff --git a/devtools/client/shared/test/browser_graphs-08.js b/devtools/client/shared/test/browser_graphs-08.js
new file mode 100644
index 000000000..ae6b54fb5
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-08.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests if a selection is dropped when clicking outside of it.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+
+ dragStart(graph, 300);
+ dragStop(graph, 500);
+ ok(graph.hasSelection(),
+ "A selection should be available.");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct.");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct.");
+
+ click(graph, 600);
+ ok(!graph.hasSelection(),
+ "The selection should be dropped.");
+}
+
+// EventUtils just doesn't work!
+
+function click(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-09a.js b/devtools/client/shared/test/browser_graphs-09a.js
new file mode 100644
index 000000000..8e6e65c24
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09a.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs properly create the gutter and tooltips.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, { metric: "fps" });
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ info("Should be able to set the graph data before waiting for the ready event.");
+
+ yield graph.setDataWhenReady(TEST_DATA);
+ ok(graph.hasData(), "Data was set successfully.");
+
+ is(graph._gutter.hidden, false,
+ "The gutter should not be hidden because the tooltips have arrows.");
+ is(graph._maxTooltip.hidden, false,
+ "The max tooltip should not be hidden.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should not be hidden.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should not be hidden.");
+
+ is(graph._maxTooltip.getAttribute("with-arrows"), "true",
+ "The maximum tooltip has the correct 'with-arrows' attribute.");
+ is(graph._avgTooltip.getAttribute("with-arrows"), "true",
+ "The average tooltip has the correct 'with-arrows' attribute.");
+ is(graph._minTooltip.getAttribute("with-arrows"), "true",
+ "The minimum tooltip has the correct 'with-arrows' attribute.");
+
+ is(graph._maxTooltip.querySelector("[text=info]").textContent, "max",
+ "The maximum tooltip displays the correct info.");
+ is(graph._avgTooltip.querySelector("[text=info]").textContent, "avg",
+ "The average tooltip displays the correct info.");
+ is(graph._minTooltip.querySelector("[text=info]").textContent, "min",
+ "The minimum tooltip displays the correct info.");
+
+ is(graph._maxTooltip.querySelector("[text=value]").textContent, "60",
+ "The maximum tooltip displays the correct value.");
+ is(graph._avgTooltip.querySelector("[text=value]").textContent, "41.72",
+ "The average tooltip displays the correct value.");
+ is(graph._minTooltip.querySelector("[text=value]").textContent, "10",
+ "The minimum tooltip displays the correct value.");
+
+ is(graph._maxTooltip.querySelector("[text=metric]").textContent, "fps",
+ "The maximum tooltip displays the correct metric.");
+ is(graph._avgTooltip.querySelector("[text=metric]").textContent, "fps",
+ "The average tooltip displays the correct metric.");
+ is(graph._minTooltip.querySelector("[text=metric]").textContent, "fps",
+ "The minimum tooltip displays the correct metric.");
+
+ is(parseInt(graph._maxTooltip.style.top, 10), 22,
+ "The maximum tooltip is positioned correctly.");
+ is(parseInt(graph._avgTooltip.style.top, 10), 61,
+ "The average tooltip is positioned correctly.");
+ is(parseInt(graph._minTooltip.style.top, 10), 128,
+ "The minimum tooltip is positioned correctly.");
+
+ is(parseInt(graph._maxGutterLine.style.top, 10), 22,
+ "The maximum gutter line is positioned correctly.");
+ is(parseInt(graph._avgGutterLine.style.top, 10), 61,
+ "The average gutter line is positioned correctly.");
+ is(parseInt(graph._minGutterLine.style.top, 10), 128,
+ "The minimum gutter line is positioned correctly.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09b.js b/devtools/client/shared/test/browser_graphs-09b.js
new file mode 100644
index 000000000..58dc552fb
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09b.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs properly use the tooltips configuration properties.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ graph.withTooltipArrows = false;
+ graph.withFixedTooltipPositions = true;
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, false,
+ "The gutter should be visible even if the tooltips don't have arrows.");
+ is(graph._maxTooltip.hidden, false,
+ "The max tooltip should not be hidden.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should not be hidden.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should not be hidden.");
+
+ is(graph._maxTooltip.getAttribute("with-arrows"), "false",
+ "The maximum tooltip has the correct 'with-arrows' attribute.");
+ is(graph._avgTooltip.getAttribute("with-arrows"), "false",
+ "The average tooltip has the correct 'with-arrows' attribute.");
+ is(graph._minTooltip.getAttribute("with-arrows"), "false",
+ "The minimum tooltip has the correct 'with-arrows' attribute.");
+
+ is(parseInt(graph._maxTooltip.style.top, 10), 8,
+ "The maximum tooltip is positioned correctly.");
+ is(parseInt(graph._avgTooltip.style.top, 10), 8,
+ "The average tooltip is positioned correctly.");
+ is(parseInt(graph._minTooltip.style.top, 10), 142,
+ "The minimum tooltip is positioned correctly.");
+
+ is(parseInt(graph._maxGutterLine.style.top, 10), 22,
+ "The maximum gutter line is positioned correctly.");
+ is(parseInt(graph._avgGutterLine.style.top, 10), 61,
+ "The average gutter line is positioned correctly.");
+ is(parseInt(graph._minGutterLine.style.top, 10), 128,
+ "The minimum gutter line is positioned correctly.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09c.js b/devtools/client/shared/test/browser_graphs-09c.js
new file mode 100644
index 000000000..ba3d3c1c4
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09c.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs hide the tooltips when there's no data available.
+
+const TEST_DATA = [];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, true,
+ "The gutter should be hidden, since there's no data available.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden.");
+ is(graph._avgTooltip.hidden, true,
+ "The avg tooltip should be hidden.");
+ is(graph._minTooltip.hidden, true,
+ "The min tooltip should be hidden.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09d.js b/devtools/client/shared/test/browser_graphs-09d.js
new file mode 100644
index 000000000..7d0c01c15
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09d.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs hide the 'max' tooltip when the distance between
+// the 'min' and 'max' tooltip is too small.
+
+const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 59.9 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, false,
+ "The gutter should not be hidden.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should not be hidden.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should not be hidden.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09e.js b/devtools/client/shared/test/browser_graphs-09e.js
new file mode 100644
index 000000000..72f2f7bdd
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09e.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that line graphs hide the gutter and tooltips when there's no data,
+// but show them when there is.
+
+const NO_DATA = [];
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(NO_DATA);
+
+ is(graph._gutter.hidden, true,
+ "The gutter should be hidden when there's no data available.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden when there's no data available.");
+ is(graph._avgTooltip.hidden, true,
+ "The avg tooltip should be hidden when there's no data available.");
+ is(graph._minTooltip.hidden, true,
+ "The min tooltip should be hidden when there's no data available.");
+
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, false,
+ "The gutter should be visible now.");
+ is(graph._maxTooltip.hidden, false,
+ "The max tooltip should be visible now.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should be visible now.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should be visible now.");
+
+ yield graph.setDataWhenReady(NO_DATA);
+
+ is(graph._gutter.hidden, true,
+ "The gutter should be hidden again.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden again.");
+ is(graph._avgTooltip.hidden, true,
+ "The avg tooltip should be hidden again.");
+ is(graph._minTooltip.hidden, true,
+ "The min tooltip should be hidden again.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-09f.js b/devtools/client/shared/test/browser_graphs-09f.js
new file mode 100644
index 000000000..32b5819b2
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-09f.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the constructor options for `min`, `max` and `avg` on displaying the
+// gutter/tooltips and lines.
+
+const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 1 }];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+
+ yield testGraph(doc.body, { avg: false });
+ yield testGraph(doc.body, { min: false });
+ yield testGraph(doc.body, { max: false });
+ yield testGraph(doc.body, { min: false, max: false, avg: false });
+ yield testGraph(doc.body, {});
+
+ host.destroy();
+}
+
+function* testGraph(parent, options) {
+ options.metric = "fps";
+ let graph = new LineGraphWidget(parent, options);
+ yield graph.setDataWhenReady(TEST_DATA);
+ let shouldGutterShow = options.min === false && options.max === false;
+
+ is(graph._gutter.hidden, shouldGutterShow,
+ `The gutter should ${shouldGutterShow ? "" : "not "}be shown`);
+
+ is(graph._maxTooltip.hidden, options.max === false,
+ `The max tooltip should ${options.max === false ? "not " : ""}be shown`);
+ is(graph._maxGutterLine.hidden, options.max === false,
+ `The max gutter should ${options.max === false ? "not " : ""}be shown`);
+ is(graph._minTooltip.hidden, options.min === false,
+ `The min tooltip should ${options.min === false ? "not " : ""}be shown`);
+ is(graph._minGutterLine.hidden, options.min === false,
+ `The min gutter should ${options.min === false ? "not " : ""}be shown`);
+ is(graph._avgTooltip.hidden, options.avg === false,
+ `The avg tooltip should ${options.avg === false ? "not " : ""}be shown`);
+ is(graph._avgGutterLine.hidden, options.avg === false,
+ `The avg gutter should ${options.avg === false ? "not " : ""}be shown`);
+
+ yield graph.destroy();
+}
diff --git a/devtools/client/shared/test/browser_graphs-10a.js b/devtools/client/shared/test/browser_graphs-10a.js
new file mode 100644
index 000000000..7f66156f4
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-10a.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graphs properly handle resizing.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost("window");
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ let refreshCount = 0;
+ graph.on("refresh", () => refreshCount++);
+
+ yield testGraph(host, graph);
+
+ is(refreshCount, 2, "The graph should've been refreshed 2 times.");
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData(TEST_DATA);
+ let initialBounds = host.frame.getBoundingClientRect();
+
+ host._window.resizeBy(-100, -100);
+ yield graph.once("refresh");
+ let newBounds = host.frame.getBoundingClientRect();
+
+ is(initialBounds.width - newBounds.width, 100,
+ "The window was properly resized (1).");
+ is(initialBounds.height - newBounds.height, 100,
+ "The window was properly resized (2).");
+
+ is(graph.width, newBounds.width * window.devicePixelRatio,
+ "The graph has the correct width (1).");
+ is(graph.height, newBounds.height * window.devicePixelRatio,
+ "The graph has the correct height (1).");
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 400);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 500);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (3).");
+
+ host._window.resizeBy(100, 100);
+ yield graph.once("refresh");
+ let newerBounds = host.frame.getBoundingClientRect();
+
+ is(initialBounds.width - newerBounds.width, 0,
+ "The window was properly resized (3).");
+ is(initialBounds.height - newerBounds.height, 0,
+ "The window was properly resized (4).");
+
+ is(graph.width, newerBounds.width * window.devicePixelRatio,
+ "The graph has the correct width (2).");
+ is(graph.height, newerBounds.height * window.devicePixelRatio,
+ "The graph has the correct height (2).");
+
+ info("Making a new selection.");
+
+ dragStart(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (4).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (4).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (4).");
+
+ hover(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (5).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (5).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (5).");
+
+ dragStop(graph, 400);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (6).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (6).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (6).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-10b.js b/devtools/client/shared/test/browser_graphs-10b.js
new file mode 100644
index 000000000..a29bdfd25
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-10b.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graphs aren't refreshed when the owner window resizes but
+// the graph dimensions stay the same.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost("window");
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+ yield graph.once("ready");
+
+ let refreshCount = 0;
+ let refreshCancelledCount = 0;
+ graph.on("refresh", () => refreshCount++);
+ graph.on("refresh-cancelled", () => refreshCancelledCount++);
+
+ yield testGraph(host, graph);
+
+ is(refreshCount, 0, "The graph shouldn't have been refreshed at all.");
+ is(refreshCancelledCount, 2, "The graph should've had 2 refresh attempts.");
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData(TEST_DATA);
+
+ host._window.resizeBy(-100, -100);
+ yield graph.once("refresh-cancelled");
+
+ host._window.resizeBy(100, 100);
+ yield graph.once("refresh-cancelled");
+}
diff --git a/devtools/client/shared/test/browser_graphs-10c.js b/devtools/client/shared/test/browser_graphs-10c.js
new file mode 100644
index 000000000..f68a3e804
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-10c.js
@@ -0,0 +1,109 @@
+
+"use strict";
+
+// Tests that graphs properly handle resizing.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost("window");
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ let refreshCount = 0;
+ graph.on("refresh", () => refreshCount++);
+
+ yield testGraph(host, graph);
+
+ is(refreshCount, 2, "The graph should've been refreshed 2 times.");
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData(TEST_DATA);
+
+ host._window.resizeTo(500, 500);
+ yield graph.once("refresh");
+ let oldBounds = host.frame.getBoundingClientRect();
+
+ is(graph._width, oldBounds.width * window.devicePixelRatio,
+ "The window was properly resized (1).");
+ is(graph._height, oldBounds.height * window.devicePixelRatio,
+ "The window was properly resized (1).");
+
+ dragStart(graph, 100);
+ dragStop(graph, 400);
+
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (1).");
+
+ info("Making sure the selection updates when the window is resized");
+
+ host._window.resizeTo(250, 250);
+ yield graph.once("refresh");
+ let newBounds = host.frame.getBoundingClientRect();
+
+ is(graph._width, newBounds.width * window.devicePixelRatio,
+ "The window was properly resized (2).");
+ is(graph._height, newBounds.height * window.devicePixelRatio,
+ "The window was properly resized (2).");
+
+ let ratio = oldBounds.width / newBounds.width;
+ info("The window resize ratio is: " + ratio);
+
+ is(graph.getSelection().start, Math.round(100 / ratio),
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, Math.round(400 / ratio),
+ "The current selection end value is correct (2).");
+}
+
+// EventUtils just doesn't work!
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-11a.js b/devtools/client/shared/test/browser_graphs-11a.js
new file mode 100644
index 000000000..27e5b292c
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-11a.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that bar graph create a legend as expected.
+
+const BarGraphWidget = require("devtools/client/shared/widgets/BarGraphWidget");
+
+const CATEGORIES = [
+ { color: "#46afe3", label: "Foo" },
+ { color: "#eb5368", label: "Bar" },
+ { color: "#70bf53", label: "Baz" }
+];
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new BarGraphWidget(doc.body);
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.format = CATEGORIES;
+ graph.setData([{ delta: 0, values: [] }]);
+
+ let legendContainer = graph._document.querySelector(".bar-graph-widget-legend");
+ ok(legendContainer,
+ "A legend container should be available.");
+ is(legendContainer.childNodes.length, 3,
+ "Three legend items should have been created.");
+
+ let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item");
+ is(legendItems.length, 3,
+ "Three legend items should exist in the entire graph.");
+
+ is(legendItems[0].querySelector("[view=color]").style.backgroundColor,
+ "rgb(70, 175, 227)", "The first legend item has the correct color.");
+ is(legendItems[1].querySelector("[view=color]").style.backgroundColor,
+ "rgb(235, 83, 104)", "The second legend item has the correct color.");
+ is(legendItems[2].querySelector("[view=color]").style.backgroundColor,
+ "rgb(112, 191, 83)", "The third legend item has the correct color.");
+
+ is(legendItems[0].querySelector("[view=label]").textContent, "Foo",
+ "The first legend item has the correct label.");
+ is(legendItems[1].querySelector("[view=label]").textContent, "Bar",
+ "The second legend item has the correct label.");
+ is(legendItems[2].querySelector("[view=label]").textContent, "Baz",
+ "The third legend item has the correct label.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-11b.js b/devtools/client/shared/test/browser_graphs-11b.js
new file mode 100644
index 000000000..4df1c4495
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-11b.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that bar graph's legend items handle mouseover/mouseout.
+
+const BarGraphWidget = require("devtools/client/shared/widgets/BarGraphWidget");
+
+const CATEGORIES = [
+ { color: "#46afe3", label: "Foo" },
+ { color: "#eb5368", label: "Bar" },
+ { color: "#70bf53", label: "Baz" }
+];
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new BarGraphWidget(doc.body, 1);
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+
+ yield graph.once("ready");
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ graph.format = CATEGORIES;
+ graph.dataOffsetX = 1000;
+ graph.setData([{
+ delta: 1100, values: [0, 2, 3]
+ }, {
+ delta: 1200, values: [1, 0, 2]
+ }, {
+ delta: 1300, values: [2, 1, 0]
+ }, {
+ delta: 1400, values: [0, 3, 1]
+ }, {
+ delta: 1500, values: [3, 0, 2]
+ }, {
+ delta: 1600, values: [3, 2, 0]
+ }]);
+
+ /* eslint-disable max-len */
+ is(graph._blocksBoundingRects.toSource(), "[{type:1, start:0, end:33.33333333333333, top:70, bottom:100}, {type:2, start:0, end:33.33333333333333, top:24, bottom:69}, {type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100}, {type:2, start:34.33333333333333, end:66.66666666666666, top:54, bottom:84}, {type:0, start:67.66666666666666, end:100, top:70, bottom:100}, {type:1, start:67.66666666666666, end:100, top:54, bottom:69}, {type:1, start:101, end:133.33333333333331, top:55, bottom:100}, {type:2, start:101, end:133.33333333333331, top:39, bottom:54}, {type:0, start:134.33333333333331, end:166.66666666666666, top:55, bottom:100}, {type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54}, {type:0, start:167.66666666666666, end:200, top:55, bottom:100}, {type:1, start:167.66666666666666, end:200, top:24, bottom:54}]",
+ "The correct blocks bounding rects were calculated for the bar graph.");
+
+ let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item");
+ is(legendItems.length, 3,
+ "Three legend items should exist in the entire graph.");
+
+ yield testLegend(graph, 0, {
+ highlights: "[{type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100}, {type:0, start:67.66666666666666, end:100, top:70, bottom:100}, {type:0, start:134.33333333333331, end:166.66666666666666, top:55, bottom:100}, {type:0, start:167.66666666666666, end:200, top:55, bottom:100}]",
+ selection: "({start:34.33333333333333, end:200})",
+ leftmost: "({type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100})",
+ rightmost: "({type:0, start:167.66666666666666, end:200, top:55, bottom:100})"
+ });
+ yield testLegend(graph, 1, {
+ highlights: "[{type:1, start:0, end:33.33333333333333, top:70, bottom:100}, {type:1, start:67.66666666666666, end:100, top:54, bottom:69}, {type:1, start:101, end:133.33333333333331, top:55, bottom:100}, {type:1, start:167.66666666666666, end:200, top:24, bottom:54}]",
+ selection: "({start:0, end:200})",
+ leftmost: "({type:1, start:0, end:33.33333333333333, top:70, bottom:100})",
+ rightmost: "({type:1, start:167.66666666666666, end:200, top:24, bottom:54})"
+ });
+ yield testLegend(graph, 2, {
+ highlights: "[{type:2, start:0, end:33.33333333333333, top:24, bottom:69}, {type:2, start:34.33333333333333, end:66.66666666666666, top:54, bottom:84}, {type:2, start:101, end:133.33333333333331, top:39, bottom:54}, {type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54}]",
+ selection: "({start:0, end:166.66666666666666})",
+ leftmost: "({type:2, start:0, end:33.33333333333333, top:24, bottom:69})",
+ rightmost: "({type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54})"
+ });
+ /* eslint-enable max-len */
+}
+
+function* testLegend(graph, index, { highlights, selection, leftmost, rightmost }) {
+ // Hover.
+
+ let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item");
+ let colorBlock = legendItems[index].querySelector("[view=color]");
+
+ let debounced = graph.once("legend-hover");
+ graph._onLegendMouseOver({ target: colorBlock });
+ ok(!graph.hasMask(), "The graph shouldn't get highlights immediately.");
+
+ let [type, rects] = yield debounced;
+ ok(graph.hasMask(), "The graph should now have highlights.");
+
+ is(type, index,
+ "The legend item was correctly hovered.");
+ is(rects.toSource(), highlights,
+ "The legend item highlighted the correct regions.");
+
+ // Unhover.
+
+ let unhovered = graph.once("legend-unhover");
+ graph._onLegendMouseOut();
+ ok(!graph.hasMask(), "The graph shouldn't have highlights anymore.");
+
+ yield unhovered;
+ ok(true, "The 'legend-mouseout' event was emitted.");
+
+ // Select.
+
+ let selected = graph.once("legend-selection");
+ graph._onLegendMouseDown(mockEvent(colorBlock));
+ ok(graph.hasSelection(), "The graph should now have a selection.");
+ is(graph.getSelection().toSource(), selection, "The graph has a correct selection.");
+
+ let [left, right] = yield selected;
+ is(left.toSource(), leftmost, "The correct leftmost data block was found.");
+ is(right.toSource(), rightmost, "The correct rightmost data block was found.");
+
+ // Deselect.
+
+ graph.dropSelection();
+}
+
+function mockEvent(node) {
+ return {
+ target: node,
+ preventDefault: () => {},
+ stopPropagation: () => {}
+ };
+}
diff --git a/devtools/client/shared/test/browser_graphs-12.js b/devtools/client/shared/test/browser_graphs-12.js
new file mode 100644
index 000000000..1836d016c
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-12.js
@@ -0,0 +1,157 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that canvas graphs can have their selection linked.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+const BarGraphWidget = require("devtools/client/shared/widgets/BarGraphWidget");
+const {CanvasGraphUtils} = require("devtools/client/shared/widgets/Graphs");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let first = document.createElement("div");
+ first.setAttribute("style", "display: inline-block; width: 100%; height: 50%;");
+ doc.body.appendChild(first);
+
+ let second = document.createElement("div");
+ second.setAttribute("style", "display: inline-block; width: 100%; height: 50%;");
+ doc.body.appendChild(second);
+
+ let graph1 = new LineGraphWidget(first, "js");
+ let graph2 = new BarGraphWidget(second);
+
+ CanvasGraphUtils.linkAnimation(graph1, graph2);
+ CanvasGraphUtils.linkSelection(graph1, graph2);
+
+ yield graph1.ready();
+ yield graph2.ready();
+
+ testGraphs(graph1, graph2);
+
+ yield graph1.destroy();
+ yield graph2.destroy();
+ host.destroy();
+}
+
+function testGraphs(graph1, graph2) {
+ info("Making a selection in the first graph.");
+
+ dragStart(graph1, 300);
+ ok(graph1.hasSelectionInProgress(),
+ "The selection should start (1.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should not start yet in the second graph (1.2).");
+ is(graph1.getSelection().start, 300,
+ "The current selection start value is correct (1.1).");
+ is(graph2.getSelection().start, 300,
+ "The current selection start value is correct (1.2).");
+ is(graph1.getSelection().end, 300,
+ "The current selection end value is correct (1.1).");
+ is(graph2.getSelection().end, 300,
+ "The current selection end value is correct (1.2).");
+
+ hover(graph1, 400);
+ ok(graph1.hasSelectionInProgress(),
+ "The selection should still be in progress (2.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should not be in progress in the second graph (2.2).");
+ is(graph1.getSelection().start, 300,
+ "The current selection start value is correct (2.1).");
+ is(graph2.getSelection().start, 300,
+ "The current selection start value is correct (2.2).");
+ is(graph1.getSelection().end, 400,
+ "The current selection end value is correct (2.1).");
+ is(graph2.getSelection().end, 400,
+ "The current selection end value is correct (2.2).");
+
+ dragStop(graph1, 500);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should have stopped (3.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should have stopped (3.2).");
+ is(graph1.getSelection().start, 300,
+ "The current selection start value is correct (3.1).");
+ is(graph2.getSelection().start, 300,
+ "The current selection start value is correct (3.2).");
+ is(graph1.getSelection().end, 500,
+ "The current selection end value is correct (3.1).");
+ is(graph2.getSelection().end, 500,
+ "The current selection end value is correct (3.2).");
+
+ info("Making a new selection in the second graph.");
+
+ dragStart(graph2, 200);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should not start yet in the first graph (4.1).");
+ ok(graph2.hasSelectionInProgress(),
+ "The selection should start (4.2).");
+ is(graph1.getSelection().start, 200,
+ "The current selection start value is correct (4.1).");
+ is(graph2.getSelection().start, 200,
+ "The current selection start value is correct (4.2).");
+ is(graph1.getSelection().end, 200,
+ "The current selection end value is correct (4.1).");
+ is(graph2.getSelection().end, 200,
+ "The current selection end value is correct (4.2).");
+
+ hover(graph2, 300);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should not be in progress in the first graph (2.2).");
+ ok(graph2.hasSelectionInProgress(),
+ "The selection should still be in progress (5.2).");
+ is(graph1.getSelection().start, 200,
+ "The current selection start value is correct (5.1).");
+ is(graph2.getSelection().start, 200,
+ "The current selection start value is correct (5.2).");
+ is(graph1.getSelection().end, 300,
+ "The current selection end value is correct (5.1).");
+ is(graph2.getSelection().end, 300,
+ "The current selection end value is correct (5.2).");
+
+ dragStop(graph2, 400);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should have stopped (6.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should have stopped (6.2).");
+ is(graph1.getSelection().start, 200,
+ "The current selection start value is correct (6.1).");
+ is(graph2.getSelection().start, 200,
+ "The current selection start value is correct (6.2).");
+ is(graph1.getSelection().end, 400,
+ "The current selection end value is correct (6.1).");
+ is(graph2.getSelection().end, 400,
+ "The current selection end value is correct (6.2).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
diff --git a/devtools/client/shared/test/browser_graphs-13.js b/devtools/client/shared/test/browser_graphs-13.js
new file mode 100644
index 000000000..d671291ed
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-13.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets may have a fixed width or height.
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ doc.body.setAttribute("style",
+ "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+
+ yield graph.ready();
+ testGraph(host, graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ let bounds = host.frame.getBoundingClientRect();
+
+ isnot(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph should not span all the parent node's width.");
+ isnot(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph should not span all the parent node's height.");
+
+ is(graph.width, graph.fixedWidth * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, graph.fixedHeight * window.devicePixelRatio,
+ "The graph has the correct height.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-14.js b/devtools/client/shared/test/browser_graphs-14.js
new file mode 100644
index 000000000..4001f8e6d
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-14.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets correctly emit mouse input events.
+
+const TEST_DATA = [
+ { delta: 112, value: 48 }, { delta: 213, value: 59 },
+ { delta: 313, value: 60 }, { delta: 413, value: 59 },
+ { delta: 530, value: 59 }, { delta: 646, value: 58 },
+ { delta: 747, value: 60 }, { delta: 863, value: 48 },
+ { delta: 980, value: 37 }, { delta: 1097, value: 30 },
+ { delta: 1213, value: 29 }, { delta: 1330, value: 23 },
+ { delta: 1430, value: 10 }, { delta: 1534, value: 17 },
+ { delta: 1645, value: 20 }, { delta: 1746, value: 22 },
+ { delta: 1846, value: 39 }, { delta: 1963, value: 26 },
+ { delta: 2080, value: 27 }, { delta: 2197, value: 35 },
+ { delta: 2312, value: 47 }, { delta: 2412, value: 53 },
+ { delta: 2514, value: 60 }, { delta: 2630, value: 37 },
+ { delta: 2730, value: 36 }, { delta: 2830, value: 37 },
+ { delta: 2946, value: 36 }, { delta: 3046, value: 40 },
+ { delta: 3163, value: 47 }, { delta: 3280, value: 41 },
+ { delta: 3380, value: 35 }, { delta: 3480, value: 27 },
+ { delta: 3580, value: 39 }, { delta: 3680, value: 42 },
+ { delta: 3780, value: 49 }, { delta: 3880, value: 55 },
+ { delta: 3980, value: 60 }, { delta: 4080, value: 60 },
+ { delta: 4180, value: 60 }
+];
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ let mouseDownEvents = 0;
+ let mouseUpEvents = 0;
+ let scrollEvents = 0;
+ graph.on("mousedown", () => mouseDownEvents++);
+ graph.on("mouseup", () => mouseUpEvents++);
+ graph.on("scroll", () => scrollEvents++);
+
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ dragStop(graph, 500);
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (1).");
+
+ is(mouseDownEvents, 1,
+ "One mousedown event should have been fired.");
+ is(mouseUpEvents, 1,
+ "One mouseup event should have been fired.");
+ is(scrollEvents, 0,
+ "No scroll event should have been fired.");
+
+ info("Zooming in by scrolling inside the selection.");
+
+ scroll(graph, -1000, 400);
+ is(graph.getSelection().start, 375,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 425,
+ "The current selection end value is correct (2).");
+
+ is(mouseDownEvents, 1,
+ "No more mousedown events should have been fired.");
+ is(mouseUpEvents, 1,
+ "No more mouseup events should have been fired.");
+ is(scrollEvents, 1,
+ "One scroll event should have been fired.");
+}
+
+// EventUtils just doesn't work!
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+}
+
+function scroll(graph, wheel, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseWheel({ testX: x, testY: y, detail: wheel });
+}
diff --git a/devtools/client/shared/test/browser_graphs-15.js b/devtools/client/shared/test/browser_graphs-15.js
new file mode 100644
index 000000000..af2c9875e
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-15.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that graph widgets correctly emit mouse input events.
+
+const FAST_FPS = 60;
+const SLOW_FPS = 10;
+
+// Each element represents a second
+const FRAMES = [FAST_FPS, FAST_FPS, FAST_FPS, SLOW_FPS, FAST_FPS];
+const TEST_DATA = [];
+const INTERVAL = 100;
+const DURATION = 5000;
+var t = 0;
+for (let frameRate of FRAMES) {
+ for (let i = 0; i < frameRate; i++) {
+ // Duration between frames at this rate
+ let delta = Math.floor(1000 / frameRate);
+ t += delta;
+ TEST_DATA.push(t);
+ }
+}
+
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ console.log("test data", TEST_DATA);
+ yield graph.setDataFromTimestamps(TEST_DATA, INTERVAL, DURATION);
+ is(graph._avgTooltip.querySelector("[text=value]").textContent, "50",
+ "The average tooltip displays the correct value.");
+}
diff --git a/devtools/client/shared/test/browser_graphs-16.js b/devtools/client/shared/test/browser_graphs-16.js
new file mode 100644
index 000000000..194cb751c
--- /dev/null
+++ b/devtools/client/shared/test/browser_graphs-16.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that mounta graphs work as expected.
+
+const MountainGraphWidget = require("devtools/client/shared/widgets/MountainGraphWidget");
+
+const TEST_DATA = [
+ { delta: 0, values: [0.1, 0.5, 0.3] },
+ { delta: 1, values: [0.25, 0, 0.5] },
+ { delta: 2, values: [0.5, 0.25, 0.1] },
+ { delta: 3, values: [0, 0.75, 0] },
+ { delta: 4, values: [0.75, 0, 0.25] }
+];
+
+const SECTIONS = [
+ { color: "red" },
+ { color: "green" },
+ { color: "blue" }
+];
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host,, doc] = yield createHost();
+ let graph = new MountainGraphWidget(doc.body);
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ yield graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.format = SECTIONS;
+ graph.setData(TEST_DATA);
+ ok(true, "The graph didn't throw any erorrs.");
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip-01.js b/devtools/client/shared/test/browser_html_tooltip-01.js
new file mode 100644
index 000000000..7752881b9
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-01.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip show & hide methods.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+function getTooltipContent(doc) {
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "50px";
+ div.style.boxSizing = "border-box";
+ div.textContent = "tooltip";
+ return div;
+}
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ yield addTab("about:blank");
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper});
+
+ info("Set tooltip content");
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ info("Show the tooltip and check the expected events are fired.");
+
+ let shown = 0;
+ tooltip.on("shown", () => shown++);
+
+ let onShown = tooltip.once("shown");
+ tooltip.show(doc.getElementById("box1"));
+
+ yield onShown;
+ is(shown, 1, "Event shown was fired once");
+
+ yield waitForReflow(tooltip);
+ is(tooltip.isVisible(), true, "Tooltip is visible");
+
+ info("Hide the tooltip and check the expected events are fired.");
+
+ let hidden = 0;
+ tooltip.on("hidden", () => hidden++);
+
+ let onPopupHidden = tooltip.once("hidden");
+ tooltip.hide();
+
+ yield onPopupHidden;
+ is(hidden, 1, "Event hidden was fired once");
+
+ yield waitForReflow(tooltip);
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip-02.js b/devtools/client/shared/test/browser_html_tooltip-02.js
new file mode 100644
index 000000000..4ebe9185b
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-02.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+"use strict";
+
+/**
+ * Test the HTMLTooltip is closed when clicking outside of its container.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ htmlns="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ <iframe id="frame" width="200"></iframe>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ yield testClickInTooltipContent(doc);
+ yield testConsumeOutsideClicksFalse(doc);
+ yield testConsumeOutsideClicksTrue(doc);
+ yield testConsumeWithRightClick(doc);
+ yield testClickInOuterIframe(doc);
+ yield testClickInInnerIframe(doc);
+}
+
+function* testClickInTooltipContent(doc) {
+ info("Test a tooltip is not closed when clicking inside itself");
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onTooltipContainerClick = once(tooltip.container, "click");
+ EventUtils.synthesizeMouseAtCenter(tooltip.container, {}, doc.defaultView);
+ yield onTooltipContainerClick;
+ is(tooltip.isVisible(), true, "Tooltip is still visible");
+
+ tooltip.destroy();
+}
+
+function* testConsumeOutsideClicksFalse(doc) {
+ info("Test closing a tooltip via click with consumeOutsideClicks: false");
+ let box4 = doc.getElementById("box4");
+
+ let tooltip = new HTMLTooltip(doc, {consumeOutsideClicks: false, useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onBox4Clicked = once(box4, "click");
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(box4, {}, doc.defaultView);
+ yield onHidden;
+ yield onBox4Clicked;
+
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+
+ tooltip.destroy();
+}
+
+function* testConsumeOutsideClicksTrue(doc) {
+ info("Test closing a tooltip via click with consumeOutsideClicks: true");
+ let box4 = doc.getElementById("box4");
+
+ // Count clicks on box4
+ let box4clicks = 0;
+ box4.addEventListener("click", () => box4clicks++);
+
+ let tooltip = new HTMLTooltip(doc, {consumeOutsideClicks: true, useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(box4, {}, doc.defaultView);
+ yield onHidden;
+
+ is(box4clicks, 0, "box4 catched no click event");
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+
+ tooltip.destroy();
+}
+
+function* testConsumeWithRightClick(doc) {
+ info("Test closing a tooltip with a right-click, with consumeOutsideClicks: true");
+ let box4 = doc.getElementById("box4");
+
+ let tooltip = new HTMLTooltip(doc, {consumeOutsideClicks: true, useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ // Only left-click events should be consumed, so we expect to catch a click when using
+ // {button: 2}, which simulates a right-click.
+ info("Right click on box4, expect tooltip to be hidden, event should not be consumed");
+ let onBox4Clicked = once(box4, "click");
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(box4, {button: 2}, doc.defaultView);
+ yield onHidden;
+ yield onBox4Clicked;
+
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+
+ tooltip.destroy();
+}
+
+function* testClickInOuterIframe(doc) {
+ info("Test clicking an iframe outside of the tooltip closes the tooltip");
+ let frame = doc.getElementById("frame");
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper});
+ tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouseAtCenter(frame, {}, doc.defaultView);
+ yield onHidden;
+
+ is(tooltip.isVisible(), false, "Tooltip is hidden");
+ tooltip.destroy();
+}
+
+function* testClickInInnerIframe(doc) {
+ info("Test clicking an iframe inside the tooltip content does not close the tooltip");
+
+ let tooltip = new HTMLTooltip(doc, {consumeOutsideClicks: false, useXulWrapper});
+
+ let iframe = doc.createElementNS(HTML_NS, "iframe");
+ iframe.style.width = "100px";
+ iframe.style.height = "50px";
+ tooltip.setContent(iframe, {width: 100, height: 50});
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let onTooltipContainerClick = once(tooltip.container, "click");
+ EventUtils.synthesizeMouseAtCenter(tooltip.container, {}, doc.defaultView);
+ yield onTooltipContainerClick;
+
+ is(tooltip.isVisible(), true, "Tooltip is still visible");
+
+ tooltip.destroy();
+}
+
+function getTooltipContent(doc) {
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "50px";
+ div.style.boxSizing = "border-box";
+ div.textContent = "tooltip";
+ return div;
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip-03.js b/devtools/client/shared/test/browser_html_tooltip-03.js
new file mode 100644
index 000000000..6c189c127
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-03.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip autofocus configuration option.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">
+ <textbox></textbox>
+ </hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">
+ <textbox id="box3-input"></textbox>
+ </hbox>
+ <hbox id="box4" flex="1">
+ <textbox id="box4-input"></textbox>
+ </hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [, , doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ yield testNoAutoFocus(doc);
+ yield testAutoFocus(doc);
+ yield testAutoFocusPreservesFocusChange(doc);
+}
+
+function* testNoAutoFocus(doc) {
+ yield focusNode(doc, "#box4-input");
+ ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
+
+ info("Test a tooltip without autofocus will not take focus");
+ let tooltip = yield createTooltip(doc, false);
+
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+ ok(doc.activeElement.closest("#box4-input"), "Focus is still in the #box4-input");
+
+ yield hideTooltip(tooltip);
+ yield blurNode(doc, "#box4-input");
+
+ tooltip.destroy();
+}
+
+function* testAutoFocus(doc) {
+ yield focusNode(doc, "#box4-input");
+ ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
+
+ info("Test autofocus tooltip takes focus when displayed, " +
+ "and restores the focus when hidden");
+ let tooltip = yield createTooltip(doc, true);
+
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+ ok(doc.activeElement.closest(".tooltip-content"), "Focus is in the tooltip");
+
+ yield hideTooltip(tooltip);
+ ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
+
+ info("Blur the textbox before moving to the next test to reset the state.");
+ yield blurNode(doc, "#box4-input");
+
+ tooltip.destroy();
+}
+
+function* testAutoFocusPreservesFocusChange(doc) {
+ yield focusNode(doc, "#box4-input");
+ ok(doc.activeElement.closest("#box4-input"), "Focus is still in the #box3-input");
+
+ info("Test autofocus tooltip takes focus when displayed, " +
+ "but does not try to restore the active element if it is not focused when hidden");
+ let tooltip = yield createTooltip(doc, true);
+
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+ ok(doc.activeElement.closest(".tooltip-content"), "Focus is in the tooltip");
+
+ info("Move the focus to #box3-input while the tooltip is displayed");
+ yield focusNode(doc, "#box3-input");
+ ok(doc.activeElement.closest("#box3-input"), "Focus moved to the #box3-input");
+
+ yield hideTooltip(tooltip);
+ ok(doc.activeElement.closest("#box3-input"), "Focus is still in the #box3-input");
+
+ info("Blur the textbox before moving to the next test to reset the state.");
+ yield blurNode(doc, "#box3-input");
+
+ tooltip.destroy();
+}
+
+/**
+ * Fpcus the node corresponding to the provided selector in the provided document. Returns
+ * a promise that will resolve when receiving the focus event on the node.
+ */
+function focusNode(doc, selector) {
+ let node = doc.querySelector(selector);
+ let onFocus = once(node, "focus");
+ node.focus();
+ return onFocus;
+}
+
+/**
+ * Blur the node corresponding to the provided selector in the provided document. Returns
+ * a promise that will resolve when receiving the blur event on the node.
+ */
+function blurNode(doc, selector) {
+ let node = doc.querySelector(selector);
+ let onBlur = once(node, "blur");
+ node.blur();
+ return onBlur;
+}
+
+/**
+ * Create an HTMLTooltip instance with the provided autofocus setting.
+ *
+ * @param {Document} doc
+ * Document in which the tooltip should be created
+ * @param {Boolean} autofocus
+ * @return {Promise} promise that will resolve the HTMLTooltip instance created when the
+ * tooltip content will be ready.
+ */
+function* createTooltip(doc, autofocus) {
+ let tooltip = new HTMLTooltip(doc, {autofocus, useXulWrapper});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.classList.add("tooltip-content");
+ div.style.height = "50px";
+ div.innerHTML = '<input type="text"></input>';
+
+ tooltip.setContent(div, {width: 150, height: 50});
+ return tooltip;
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip-04.js b/devtools/client/shared/test/browser_html_tooltip-04.js
new file mode 100644
index 000000000..90164840e
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-04.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip positioning for a small tooltip element (should aways
+ * find a way to fit).
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox style="height: 10px">spacer</hbox>
+ <hbox id="box1" style="height: 50px">test1</hbox>
+ <hbox id="box2" style="height: 50px">test2</hbox>
+ <hbox flex="1">MIDDLE</hbox>
+ <hbox id="box3" style="height: 50px">test3</hbox>
+ <hbox id="box4" style="height: 50px">test4</hbox>
+ <hbox style="height: 10px">spacer</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLTIP_HEIGHT = 30;
+const TOOLTIP_WIDTH = 100;
+
+add_task(function* () {
+ // Force the toolbox to be 400px high;
+ yield pushPref("devtools.toolbox.footer.height", 400);
+
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "100%";
+ tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+ let height = TOOLTIP_HEIGHT, width = TOOLTIP_WIDTH;
+
+ // box1: Can only fit below box1
+ info("Display the tooltip on box1.");
+ yield showTooltip(tooltip, box1);
+ let expectedTooltipGeometry = {position: "bottom", height, width};
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box1.");
+ yield showTooltip(tooltip, box1, {position: "top"});
+ expectedTooltipGeometry = {position: "bottom", height, width};
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box2: Can fit above or below, will default to bottom, more height
+ // available.
+ info("Try to display the tooltip on box2.");
+ yield showTooltip(tooltip, box2);
+ expectedTooltipGeometry = {position: "bottom", height, width};
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box2.");
+ yield showTooltip(tooltip, box2, {position: "top"});
+ expectedTooltipGeometry = {position: "top", height, width};
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box3: Can fit above or below, will default to top, more height available.
+ info("Try to display the tooltip on box3.");
+ yield showTooltip(tooltip, box3);
+ expectedTooltipGeometry = {position: "top", height, width};
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box3.");
+ yield showTooltip(tooltip, box3, {position: "bottom"});
+ expectedTooltipGeometry = {position: "bottom", height, width};
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box4: Can only fit above box4
+ info("Display the tooltip on box4.");
+ yield showTooltip(tooltip, box4);
+ expectedTooltipGeometry = {position: "top", height, width};
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box4.");
+ yield showTooltip(tooltip, box4, {position: "bottom"});
+ expectedTooltipGeometry = {position: "top", height, width};
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip-05.js b/devtools/client/shared/test/browser_html_tooltip-05.js
new file mode 100644
index 000000000..58be4f831
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip-05.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip positioning for a huge tooltip element (can not fit in
+ * the viewport).
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" style="height: 50px">test1</hbox>
+ <hbox id="box2" style="height: 50px">test2</hbox>
+ <hbox id="box3" style="height: 50px">test3</hbox>
+ <hbox id="box4" style="height: 50px">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLTIP_HEIGHT = 200;
+const TOOLTIP_WIDTH = 200;
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "100%";
+ tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+ let width = TOOLTIP_WIDTH;
+
+ // box1: Can not fit above or below box1, default to bottom with a reduced
+ // height of 150px.
+ info("Display the tooltip on box1.");
+ yield showTooltip(tooltip, box1);
+ let expectedTooltipGeometry = {position: "bottom", height: 150, width};
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box1.");
+ yield showTooltip(tooltip, box1, {position: "top"});
+ expectedTooltipGeometry = {position: "bottom", height: 150, width};
+ checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box2: Can not fit above or below box2, default to bottom with a reduced
+ // height of 100px.
+ info("Try to display the tooltip on box2.");
+ yield showTooltip(tooltip, box2);
+ expectedTooltipGeometry = {position: "bottom", height: 100, width};
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on top of box2.");
+ yield showTooltip(tooltip, box2, {position: "top"});
+ expectedTooltipGeometry = {position: "bottom", height: 100, width};
+ checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box3: Can not fit above or below box3, default to top with a reduced height
+ // of 100px.
+ info("Try to display the tooltip on box3.");
+ yield showTooltip(tooltip, box3);
+ expectedTooltipGeometry = {position: "top", height: 100, width};
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box3.");
+ yield showTooltip(tooltip, box3, {position: "bottom"});
+ expectedTooltipGeometry = {position: "top", height: 100, width};
+ checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ // box4: Can not fit above or below box4, default to top with a reduced height
+ // of 150px.
+ info("Display the tooltip on box4.");
+ yield showTooltip(tooltip, box4);
+ expectedTooltipGeometry = {position: "top", height: 150, width};
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ info("Try to display the tooltip on bottom of box4.");
+ yield showTooltip(tooltip, box4, {position: "bottom"});
+ expectedTooltipGeometry = {position: "top", height: 150, width};
+ checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
+ yield hideTooltip(tooltip);
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_arrow-01.js b/devtools/client/shared/test/browser_html_tooltip_arrow-01.js
new file mode 100644
index 000000000..a20c67529
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_arrow-01.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "arrow" type on small anchors. The arrow should remain
+ * aligned with the anchors as much as possible
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const getAnchor = function (position) {
+ return `<html:div class="anchor" style="width:10px;
+ height: 10px;
+ position: absolute;
+ background: red;
+ ${position}"></html:div>`;
+};
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/light-theme.css"?>
+
+ <window class="theme-light"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <vbox flex="1" style="position: relative">
+ ${getAnchor("top: 0; left: 0;")}
+ ${getAnchor("top: 0; left: 25px;")}
+ ${getAnchor("top: 0; left: 50px;")}
+ ${getAnchor("top: 0; left: 75px;")}
+ ${getAnchor("bottom: 0; left: 0;")}
+ ${getAnchor("bottom: 0; left: 25px;")}
+ ${getAnchor("bottom: 0; left: 50px;")}
+ ${getAnchor("bottom: 0; left: 75px;")}
+ ${getAnchor("bottom: 0; right: 0;")}
+ ${getAnchor("bottom: 0; right: 25px;")}
+ ${getAnchor("bottom: 0; right: 50px;")}
+ ${getAnchor("bottom: 0; right: 75px;")}
+ ${getAnchor("top: 0; right: 0;")}
+ ${getAnchor("top: 0; right: 25px;")}
+ ${getAnchor("top: 0; right: 50px;")}
+ ${getAnchor("top: 0; right: 75px;")}
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {type: "arrow", useXulWrapper});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "35px";
+ tooltip.setContent(div, {width: 200, height: 35});
+
+ let {right: docRight} = doc.documentElement.getBoundingClientRect();
+
+ let elements = [...doc.querySelectorAll(".anchor")];
+ for (let el of elements) {
+ info("Display the tooltip on an anchor.");
+ yield showTooltip(tooltip, el);
+
+ let arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ // Get the geometry of the anchor, the tooltip panel & arrow.
+ let arrowBounds = arrow.getBoxQuads({relativeTo: doc})[0].bounds;
+ let panelBounds = tooltip.panel.getBoxQuads({relativeTo: doc})[0].bounds;
+ let anchorBounds = el.getBoxQuads({relativeTo: doc})[0].bounds;
+
+ let intersects = arrowBounds.left <= anchorBounds.right &&
+ arrowBounds.right >= anchorBounds.left;
+ let isBlockedByViewport = arrowBounds.left == 0 ||
+ arrowBounds.right == docRight;
+ ok(intersects || isBlockedByViewport,
+ "Tooltip arrow is aligned with the anchor, or stuck on viewport's edge.");
+
+ let isInPanel = arrowBounds.left >= panelBounds.left &&
+ arrowBounds.right <= panelBounds.right;
+ ok(isInPanel,
+ "The tooltip arrow remains inside the tooltip panel horizontally");
+
+ yield hideTooltip(tooltip);
+ }
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_arrow-02.js b/devtools/client/shared/test/browser_html_tooltip_arrow-02.js
new file mode 100644
index 000000000..098f1ac7b
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_arrow-02.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "arrow" type on wide anchors. The arrow should remain
+ * aligned with the anchors as much as possible
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const getAnchor = function (position) {
+ return `<html:div class="anchor" style="height: 5px;
+ position: absolute;
+ background: red;
+ ${position}"></html:div>`;
+};
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/light-theme.css"?>
+
+ <window class="theme-light"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <vbox flex="1" style="position: relative">
+ ${getAnchor("top: 0; left: 0; width: 50px;")}
+ ${getAnchor("top: 10px; left: 0; width: 100px;")}
+ ${getAnchor("top: 20px; left: 0; width: 150px;")}
+ ${getAnchor("top: 30px; left: 0; width: 200px;")}
+ ${getAnchor("top: 40px; left: 0; width: 250px;")}
+ ${getAnchor("top: 50px; left: 100px; width: 250px;")}
+ ${getAnchor("top: 100px; width: 50px; right: 0;")}
+ ${getAnchor("top: 110px; width: 100px; right: 0;")}
+ ${getAnchor("top: 120px; width: 150px; right: 0;")}
+ ${getAnchor("top: 130px; width: 200px; right: 0;")}
+ ${getAnchor("top: 140px; width: 250px; right: 0;")}
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {type: "arrow", useXulWrapper});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "35px";
+ tooltip.setContent(div, {width: 200, height: 35});
+
+ let {right: docRight} = doc.documentElement.getBoundingClientRect();
+
+ let elements = [...doc.querySelectorAll(".anchor")];
+ for (let el of elements) {
+ info("Display the tooltip on an anchor.");
+ yield showTooltip(tooltip, el);
+
+ let arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ // Get the geometry of the anchor, the tooltip panel & arrow.
+ let arrowBounds = arrow.getBoxQuads({relativeTo: doc})[0].bounds;
+ let panelBounds = tooltip.panel.getBoxQuads({relativeTo: doc})[0].bounds;
+ let anchorBounds = el.getBoxQuads({relativeTo: doc})[0].bounds;
+
+ let intersects = arrowBounds.left <= anchorBounds.right &&
+ arrowBounds.right >= anchorBounds.left;
+ let isBlockedByViewport = arrowBounds.left == 0 ||
+ arrowBounds.right == docRight;
+ ok(intersects || isBlockedByViewport,
+ "Tooltip arrow is aligned with the anchor, or stuck on viewport's edge.");
+
+ let isInPanel = arrowBounds.left >= panelBounds.left &&
+ arrowBounds.right <= panelBounds.right;
+ ok(isInPanel,
+ "The tooltip arrow remains inside the tooltip panel horizontally");
+ yield hideTooltip(tooltip);
+ }
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js b/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js
new file mode 100644
index 000000000..7ed1d6dc1
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip show can be called several times. It should move according to the
+ * new anchor/options and should not leak event listeners.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+function getTooltipContent(doc) {
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "50px";
+ div.textContent = "tooltip";
+ return div;
+}
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+
+ let width = 100, height = 50;
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ tooltip.setContent(getTooltipContent(doc), {width, height});
+
+ info("Show the tooltip on each of the 4 hbox, without calling hide in between");
+
+ info("Show tooltip on box1");
+ tooltip.show(box1);
+ checkTooltipGeometry(tooltip, box1, {position: "bottom", width, height});
+
+ info("Show tooltip on box2");
+ tooltip.show(box2);
+ checkTooltipGeometry(tooltip, box2, {position: "bottom", width, height});
+
+ info("Show tooltip on box3");
+ tooltip.show(box3);
+ checkTooltipGeometry(tooltip, box3, {position: "top", width, height});
+
+ info("Show tooltip on box4");
+ tooltip.show(box4);
+ checkTooltipGeometry(tooltip, box4, {position: "top", width, height});
+
+ info("Hide tooltip before leaving test");
+ yield hideTooltip(tooltip);
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_hover.js b/devtools/client/shared/test/browser_html_tooltip_hover.js
new file mode 100644
index 000000000..8a661bbe1
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_hover.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the TooltipToggle helper class for HTMLTooltip
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="resource://devtools/client/themes/variables.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ class="theme-light" title="Tooltip hover test">
+ <vbox id="container" flex="1">
+ <hbox id="box1" flex="1"><label>test1</label></hbox>
+ <hbox id="box2" flex="1"><label>test2</label></hbox>
+ <hbox id="box3" flex="1"><label>test3</label></hbox>
+ <hbox id="box4" flex="1"><label>test4</label></hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+ // Wait for full page load before synthesizing events on the page.
+ yield waitUntil(() => doc.readyState === "complete");
+
+ let width = 100, height = 50;
+ let tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.textContent = "tooltip";
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ tooltip.setContent(tooltipContent, {width, height});
+
+ let container = doc.getElementById("container");
+ tooltip.startTogglingOnHover(container, () => true);
+
+ info("Hover on each of the 4 boxes, expect the tooltip to appear");
+ function* showAndCheck(boxId, position) {
+ info(`Show tooltip on ${boxId}`);
+ let box = doc.getElementById(boxId);
+ let shown = tooltip.once("shown");
+ EventUtils.synthesizeMouseAtCenter(box, { type: "mousemove" }, doc.defaultView);
+ yield shown;
+ checkTooltipGeometry(tooltip, box, {position, width, height});
+ }
+
+ yield showAndCheck("box1", "bottom");
+ yield showAndCheck("box2", "bottom");
+ yield showAndCheck("box3", "top");
+ yield showAndCheck("box4", "top");
+
+ info("Move out of the container");
+ let hidden = tooltip.once("hidden");
+ EventUtils.synthesizeMouseAtCenter(container, { type: "mouseout" }, doc.defaultView);
+ yield hidden;
+
+ info("Destroy the tooltip and finish");
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_offset.js b/devtools/client/shared/test/browser_html_tooltip_offset.js
new file mode 100644
index 000000000..dfbdef723
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_offset.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+"use strict";
+
+/**
+ * Test the HTMLTooltip can be displayed with vertical and horizontal offsets.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ htmlns="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Test a tooltip is not closed when clicking inside itself");
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "100px";
+ div.style.boxSizing = "border-box";
+ div.textContent = "tooltip";
+ tooltip.setContent(div, {width: 50, height: 100});
+
+ info("Display the tooltip on box1.");
+ yield showTooltip(tooltip, box1, {x: 5, y: 10});
+
+ let panelRect = tooltip.container.getBoundingClientRect();
+ let anchorRect = box1.getBoundingClientRect();
+
+ // Tooltip will be displayed below box1
+ is(panelRect.top, anchorRect.bottom + 10, "Tooltip top has 10px offset");
+ is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+ is(panelRect.height, 100, "Tooltip height is at 100px as expected");
+
+ info("Display the tooltip on box2.");
+ yield showTooltip(tooltip, box2, {x: 5, y: 10});
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box2.getBoundingClientRect();
+
+ // Tooltip will be displayed below box2, but can't be fully displayed because of the
+ // offset
+ is(panelRect.top, anchorRect.bottom + 10, "Tooltip top has 10px offset");
+ is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+ is(panelRect.height, 90, "Tooltip height is only 90px");
+
+ info("Display the tooltip on box3.");
+ yield showTooltip(tooltip, box3, {x: 5, y: 10});
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box3.getBoundingClientRect();
+
+ // Tooltip will be displayed above box3, but can't be fully displayed because of the
+ // offset
+ is(panelRect.bottom, anchorRect.top - 10, "Tooltip bottom is 10px above anchor");
+ is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+ is(panelRect.height, 90, "Tooltip height is only 90px");
+
+ info("Display the tooltip on box4.");
+ yield showTooltip(tooltip, box4, {x: 5, y: 10});
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box4.getBoundingClientRect();
+
+ // Tooltip will be displayed above box4
+ is(panelRect.bottom, anchorRect.top - 10, "Tooltip bottom is 10px above anchor");
+ is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+ is(panelRect.height, 100, "Tooltip height is at 100px as expected");
+
+ yield hideTooltip(tooltip);
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_rtl.js b/devtools/client/shared/test/browser_html_tooltip_rtl.js
new file mode 100644
index 000000000..e41716c80
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_rtl.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+"use strict";
+
+/**
+ * Test the HTMLTooltip anchor alignment changes with the anchor direction.
+ * - should be aligned to the right of RTL anchors
+ * - should be aligned to the left of LTR anchors
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ htmlns="http://www.w3.org/1999/xhtml"
+ title="Tooltip test">
+ <hbox style="padding: 90px 0;" flex="1">
+ <hbox id="box1" flex="1" style="background:red; direction: rtl;">test1</hbox>
+ <hbox id="box2" flex="1" style="background:blue; direction: rtl;">test2</hbox>
+ <hbox id="box3" flex="1" style="background:red; direction: ltr;">test3</hbox>
+ <hbox id="box4" flex="1" style="background:blue; direction: ltr;">test4</hbox>
+ </hbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLBOX_WIDTH = 500;
+const TOOLTIP_WIDTH = 150;
+const TOOLTIP_HEIGHT = 30;
+
+add_task(function* () {
+ // Force the toolbox to be 500px wide (min width is 465px);
+ yield pushPref("devtools.toolbox.sidebar.width", TOOLBOX_WIDTH);
+
+ let [,, doc] = yield createHost("side", TEST_URI);
+
+ info("Test a tooltip is not closed when clicking inside itself");
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.textContent = "tooltip";
+ div.style.cssText = "box-sizing: border-box; border: 1px solid black";
+ tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+ yield testRtlAnchors(doc, tooltip);
+ yield testLtrAnchors(doc, tooltip);
+ yield hideTooltip(tooltip);
+
+ tooltip.destroy();
+});
+
+function* testRtlAnchors(doc, tooltip) {
+ /*
+ * The layout of the test page is as follows:
+ * _______________________________
+ * | toolbox |
+ * | _____ _____ _____ _____ |
+ * || | | | | | | ||
+ * || box1| | box2| | box3| | box4||
+ * ||_____| |_____| |_____| |_____||
+ * |_______________________________|
+ *
+ * - box1 is aligned with the left edge of the toolbox
+ * - box2 is displayed right after box1
+ * - total toolbox width is 500px so each box is 125px wide
+ */
+
+ let box1 = doc.getElementById("box1");
+ let box2 = doc.getElementById("box2");
+
+ info("Display the tooltip on box1.");
+ yield showTooltip(tooltip, box1, {position: "bottom"});
+
+ let panelRect = tooltip.container.getBoundingClientRect();
+ let anchorRect = box1.getBoundingClientRect();
+
+ // box1 uses RTL direction, so the tooltip should be aligned with the right edge of the
+ // anchor, but it is shifted to the right to fit in the toolbox.
+ is(panelRect.left, 0, "Tooltip is aligned with left edge of the toolbox");
+ is(panelRect.top, anchorRect.bottom, "Tooltip aligned with the anchor bottom edge");
+ is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip height is at 100px as expected");
+
+ info("Display the tooltip on box2.");
+ yield showTooltip(tooltip, box2, {position: "bottom"});
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box2.getBoundingClientRect();
+
+ // box2 uses RTL direction, so the tooltip is aligned with the right edge of the anchor
+ is(panelRect.right, anchorRect.right, "Tooltip is aligned with right edge of anchor");
+ is(panelRect.top, anchorRect.bottom, "Tooltip aligned with the anchor bottom edge");
+ is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip height is at 100px as expected");
+}
+
+function* testLtrAnchors(doc, tooltip) {
+ /*
+ * The layout of the test page is as follows:
+ * _______________________________
+ * | toolbox |
+ * | _____ _____ _____ _____ |
+ * || | | | | | | ||
+ * || box1| | box2| | box3| | box4||
+ * ||_____| |_____| |_____| |_____||
+ * |_______________________________|
+ *
+ * - box3 is is displayed right after box2
+ * - box4 is aligned with the right edge of the toolbox
+ * - total toolbox width is 500px so each box is 125px wide
+ */
+
+ let box3 = doc.getElementById("box3");
+ let box4 = doc.getElementById("box4");
+
+ info("Display the tooltip on box3.");
+ yield showTooltip(tooltip, box3, {position: "bottom"});
+
+ let panelRect = tooltip.container.getBoundingClientRect();
+ let anchorRect = box3.getBoundingClientRect();
+
+ // box3 uses LTR direction, so the tooltip is aligned with the left edge of the anchor.
+ is(panelRect.left, anchorRect.left, "Tooltip is aligned with left edge of anchor");
+ is(panelRect.top, anchorRect.bottom, "Tooltip aligned with the anchor bottom edge");
+ is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip height is at 100px as expected");
+
+ info("Display the tooltip on box4.");
+ yield showTooltip(tooltip, box4, {position: "bottom"});
+
+ panelRect = tooltip.container.getBoundingClientRect();
+ anchorRect = box4.getBoundingClientRect();
+
+ // box4 uses LTR direction, so the tooltip should be aligned with the left edge of the
+ // anchor, but it is shifted to the left to fit in the toolbox.
+ is(panelRect.right, TOOLBOX_WIDTH, "Tooltip is aligned with right edge of toolbox");
+ is(panelRect.top, anchorRect.bottom, "Tooltip aligned with the anchor bottom edge");
+ is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip height is at 100px as expected");
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_variable-height.js b/devtools/client/shared/test/browser_html_tooltip_variable-height.js
new file mode 100644
index 000000000..e64ec4a2e
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_variable-height.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip content can have a variable height.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const CONTAINER_HEIGHT = 300;
+const CONTAINER_WIDTH = 200;
+const TOOLTIP_HEIGHT = 50;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+ // Force the toolbox to be 400px tall => 50px for each box.
+ yield pushPref("devtools.toolbox.footer.height", 400);
+
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: false});
+ info("Set tooltip content 50px tall, but request a container 200px tall");
+ let tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.style.cssText = "height: " + TOOLTIP_HEIGHT + "px; background: red;";
+ tooltip.setContent(tooltipContent, {width: CONTAINER_WIDTH, height: Infinity});
+
+ info("Show the tooltip and check the container and panel height.");
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let containerRect = tooltip.container.getBoundingClientRect();
+ let panelRect = tooltip.panel.getBoundingClientRect();
+ is(containerRect.height, CONTAINER_HEIGHT,
+ "Tooltip container has the expected height.");
+ is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip panel has the expected height.");
+
+ info("Click below the tooltip panel but in the tooltip filler element.");
+ let onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouse(tooltip.container, 100, 100, {}, doc.defaultView);
+ yield onHidden;
+
+ info("Show the tooltip one more time, and increase the content height");
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+ tooltipContent.style.height = (2 * CONTAINER_HEIGHT) + "px";
+
+ info("Click at the same coordinates as earlier, this time it should hit the tooltip.");
+ let onPanelClick = once(tooltip.panel, "click");
+ EventUtils.synthesizeMouse(tooltip.container, 100, 100, {}, doc.defaultView);
+ yield onPanelClick;
+ is(tooltip.isVisible(), true, "Tooltip is still visible");
+
+ info("Click above the tooltip container, the tooltip should be closed.");
+ onHidden = once(tooltip, "hidden");
+ EventUtils.synthesizeMouse(tooltip.container, 100, -10, {}, doc.defaultView);
+ yield onHidden;
+
+ tooltip.destroy();
+});
diff --git a/devtools/client/shared/test/browser_html_tooltip_width-auto.js b/devtools/client/shared/test/browser_html_tooltip_width-auto.js
new file mode 100644
index 000000000..66e33673e
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_width-auto.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip content can automatically calculate its width based on content.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" flex="1">test1</hbox>
+ <hbox id="box2" flex="1">test2</hbox>
+ <hbox id="box3" flex="1">test3</hbox>
+ <hbox id="box4" flex="1">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Run tests for a Tooltip without using a XUL panel");
+ useXulWrapper = false;
+ yield runTests(doc);
+
+ info("Run tests for a Tooltip with a XUL panel");
+ useXulWrapper = true;
+ yield runTests(doc);
+});
+
+function* runTests(doc) {
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper});
+ info("Create tooltip content width to 150px");
+ let tooltipContent = doc.createElementNS(HTML_NS, "div");
+ tooltipContent.style.cssText = "height: 100%; width: 150px; background: red;";
+
+ info("Set tooltip content using width:auto");
+ tooltip.setContent(tooltipContent, {width: "auto", height: 50});
+
+ info("Show the tooltip and check the tooltip panel width.");
+ yield showTooltip(tooltip, doc.getElementById("box1"));
+
+ let panelRect = tooltip.panel.getBoundingClientRect();
+ is(panelRect.width, 150, "Tooltip panel has the expected width.");
+
+ yield hideTooltip(tooltip);
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js b/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js
new file mode 100644
index 000000000..5c21f21c3
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip can overflow out of the toolbox when using a XUL panel wrapper.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ <vbox flex="1">
+ <hbox id="box1" style="height: 50px">test1</hbox>
+ <hbox id="box2" style="height: 50px">test2</hbox>
+ <hbox id="box3" style="height: 50px">test3</hbox>
+ <hbox id="box4" style="height: 50px">test4</hbox>
+ </vbox>
+ </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+// The test toolbox will be 200px tall, the anchors are 50px tall, therefore, the maximum
+// tooltip height that could fit in the toolbox is 150px. Setting 160px, the tooltip will
+// either have to overflow or to be resized.
+const TOOLTIP_HEIGHT = 160;
+const TOOLTIP_WIDTH = 200;
+
+add_task(function* () {
+ // Force the toolbox to be 200px high;
+ yield pushPref("devtools.toolbox.footer.height", 200);
+
+ let [, win, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Resizing window to have some space below the window.");
+ let originalWidth = win.top.outerWidth;
+ let originalHeight = win.top.outerHeight;
+ win.top.resizeBy(0, -100);
+
+ info("Create HTML tooltip");
+ let tooltip = new HTMLTooltip(doc, {useXulWrapper: true});
+ let div = doc.createElementNS(HTML_NS, "div");
+ div.style.height = "200px";
+ div.style.background = "red";
+ tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+ let box1 = doc.getElementById("box1");
+
+ // Above box1: check that the tooltip can overflow onto the content page.
+ info("Display the tooltip above box1.");
+ yield showTooltip(tooltip, box1, {position: "top"});
+ checkTooltip(tooltip, "top", TOOLTIP_HEIGHT);
+ yield hideTooltip(tooltip);
+
+ // Below box1: check that the tooltip can overflow out of the browser window.
+ info("Display the tooltip below box1.");
+ yield showTooltip(tooltip, box1, {position: "bottom"});
+ checkTooltip(tooltip, "bottom", TOOLTIP_HEIGHT);
+ yield hideTooltip(tooltip);
+
+ is(tooltip.isVisible(), false, "Tooltip is not visible");
+
+ tooltip.destroy();
+
+ info("Restore original window dimensions.");
+ win.top.resizeTo(originalWidth, originalHeight);
+});
+
+function checkTooltip(tooltip, position, height) {
+ is(tooltip.position, position, "Actual tooltip position is " + position);
+ let rect = tooltip.container.getBoundingClientRect();
+ is(rect.height, height, "Actual tooltip height is " + height);
+ // Testing the actual left/top offsets is not relevant here as it is handled by the XUL
+ // panel.
+}
diff --git a/devtools/client/shared/test/browser_inplace-editor-01.js b/devtools/client/shared/test/browser_inplace-editor-01.js
new file mode 100644
index 000000000..6308602f1
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor-01.js
@@ -0,0 +1,150 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor behavior.
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,inline editor tests");
+ let [host, , doc] = yield createHost();
+
+ yield testMultipleInitialization(doc);
+ yield testReturnCommit(doc);
+ yield testBlurCommit(doc);
+ yield testAdvanceCharCommit(doc);
+ yield testAdvanceCharsFunction(doc);
+ yield testEscapeCancel(doc);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testMultipleInitialization(doc) {
+ doc.body.innerHTML = "";
+ let options = {};
+ let span = options.element = createSpan(doc);
+
+ info("Creating multiple inplace-editor fields");
+ editableField(options);
+ editableField(options);
+
+ info("Clicking on the inplace-editor field to turn to edit mode");
+ span.click();
+
+ is(span.style.display, "none", "The original <span> is hidden");
+ is(doc.querySelectorAll("input").length, 1, "Only one <input>");
+ is(doc.querySelectorAll("span").length, 2,
+ "Correct number of <span> elements");
+ is(doc.querySelectorAll("span.autosizer").length, 1,
+ "There is an autosizer element");
+}
+
+function testReturnCommit(doc) {
+ info("Testing that pressing return commits the new value");
+ let def = defer();
+
+ createInplaceEditorAndClick({
+ initial: "explicit initial",
+ start: function (editor) {
+ is(editor.input.value, "explicit initial",
+ "Explicit initial value should be used.");
+ editor.input.value = "Test Value";
+ EventUtils.sendKey("return");
+ },
+ done: onDone("Test Value", true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testBlurCommit(doc) {
+ info("Testing that bluring the field commits the new value");
+ let def = defer();
+
+ createInplaceEditorAndClick({
+ start: function (editor) {
+ is(editor.input.value, "Edit Me!", "textContent of the span used.");
+ editor.input.value = "Test Value";
+ editor.input.blur();
+ },
+ done: onDone("Test Value", true, def)
+ }, doc, "Edit Me!");
+
+ return def.promise;
+}
+
+function testAdvanceCharCommit(doc) {
+ info("Testing that configured advanceChars commit the new value");
+ let def = defer();
+
+ createInplaceEditorAndClick({
+ advanceChars: ":",
+ start: function (editor) {
+ EventUtils.sendString("Test:");
+ },
+ done: onDone("Test", true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testAdvanceCharsFunction(doc) {
+ info("Testing advanceChars as a function");
+ let def = defer();
+
+ let firstTime = true;
+
+ createInplaceEditorAndClick({
+ initial: "",
+ advanceChars: function (charCode, text, insertionPoint) {
+ if (charCode !== Components.interfaces.nsIDOMKeyEvent.DOM_VK_COLON) {
+ return false;
+ }
+ if (firstTime) {
+ firstTime = false;
+ return false;
+ }
+
+ // Just to make sure we check it somehow.
+ return text.length > 0;
+ },
+ start: function (editor) {
+ for (let ch of ":Test:") {
+ EventUtils.sendChar(ch);
+ }
+ },
+ done: onDone(":Test", true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testEscapeCancel(doc) {
+ info("Testing that escape cancels the new value");
+ let def = defer();
+
+ createInplaceEditorAndClick({
+ initial: "initial text",
+ start: function (editor) {
+ editor.input.value = "Test Value";
+ EventUtils.sendKey("escape");
+ },
+ done: onDone("initial text", false, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function onDone(value, isCommit, def) {
+ return function (actualValue, actualCommit) {
+ info("Inplace-editor's done callback executed, checking its state");
+ is(actualValue, value, "The value is correct");
+ is(actualCommit, isCommit, "The commit boolean is correct");
+ def.resolve();
+ };
+}
diff --git a/devtools/client/shared/test/browser_inplace-editor-02.js b/devtools/client/shared/test/browser_inplace-editor-02.js
new file mode 100644
index 000000000..811c30123
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor-02.js
@@ -0,0 +1,71 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+loadHelperScript("helper_inplace_editor.js");
+
+// Test that the trimOutput option for the inplace editor works correctly.
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,inline editor tests");
+ let [host, , doc] = yield createHost();
+
+ yield testNonTrimmed(doc);
+ yield testTrimmed(doc);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testNonTrimmed(doc) {
+ info("Testing the trimOutput=false option");
+ let def = defer();
+
+ let initial = "\nMultiple\nLines\n";
+ let changed = " \nMultiple\nLines\n with more whitespace ";
+ createInplaceEditorAndClick({
+ trimOutput: false,
+ multiline: true,
+ initial: initial,
+ start: function (editor) {
+ is(editor.input.value, initial, "Explicit initial value should be used.");
+ editor.input.value = changed;
+ EventUtils.sendKey("return");
+ },
+ done: onDone(changed, true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testTrimmed(doc) {
+ info("Testing the trimOutput=true option (default value)");
+ let def = defer();
+
+ let initial = "\nMultiple\nLines\n";
+ let changed = " \nMultiple\nLines\n with more whitespace ";
+ createInplaceEditorAndClick({
+ initial: initial,
+ multiline: true,
+ start: function (editor) {
+ is(editor.input.value, initial, "Explicit initial value should be used.");
+ editor.input.value = changed;
+ EventUtils.sendKey("return");
+ },
+ done: onDone(changed.trim(), true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function onDone(value, isCommit, def) {
+ return function (actualValue, actualCommit) {
+ info("Inplace-editor's done callback executed, checking its state");
+ is(actualValue, value, "The value is correct");
+ is(actualCommit, isCommit, "The commit boolean is correct");
+ def.resolve();
+ };
+}
diff --git a/devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js b/devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js
new file mode 100644
index 000000000..e9ceb11ad
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js
@@ -0,0 +1,75 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor autocomplete popup for CSS properties suggestions.
+// Using a mocked list of CSS properties to avoid test failures linked to
+// engine changes (new property, removed property, ...).
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selected suggestion index (-1 if popup is hidden),
+// number of suggestions in the popup (0 if popup is hidden),
+// ]
+const testData = [
+ ["b", "border", 1, 3],
+ ["VK_DOWN", "box-sizing", 2, 3],
+ ["VK_DOWN", "background", 0, 3],
+ ["VK_DOWN", "border", 1, 3],
+ ["VK_BACK_SPACE", "b", -1, 0],
+ ["VK_BACK_SPACE", "", -1, 0],
+ ["VK_DOWN", "background", 0, 6],
+ ["VK_LEFT", "background", -1, 0],
+];
+
+const mockGetCSSPropertyList = function () {
+ return [
+ "background",
+ "border",
+ "box-sizing",
+ "color",
+ "display",
+ "visibility",
+ ];
+};
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," +
+ "inplace editor CSS property autocomplete");
+ let [host, win, doc] = yield createHost();
+
+ let xulDocument = win.top.document;
+ let popup = new AutocompletePopup(xulDocument, { autoSelect: true });
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ start: runPropertyAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+ done: resolve,
+ popup: popup
+ }, doc);
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let runPropertyAutocompletionTest = Task.async(function* (editor) {
+ info("Starting to test for css property completion");
+ editor._getCSSPropertyList = mockGetCSSPropertyList;
+
+ for (let data of testData) {
+ yield testCompletion(data, editor);
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+});
diff --git a/devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js b/devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js
new file mode 100644
index 000000000..3100026b4
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js
@@ -0,0 +1,80 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor autocomplete popup for CSS values suggestions.
+// Using a mocked list of CSS properties to avoid test failures linked to
+// engine changes (new property, removed property, ...).
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selected suggestion index (-1 if popup is hidden),
+// number of suggestions in the popup (0 if popup is hidden),
+// ]
+const testData = [
+ ["b", "block", -1, 0],
+ ["VK_BACK_SPACE", "b", -1, 0],
+ ["VK_BACK_SPACE", "", -1, 0],
+ ["i", "inline", 0, 2],
+ ["VK_DOWN", "inline-block", 1, 2],
+ ["VK_DOWN", "inline", 0, 2],
+ ["VK_LEFT", "inline", -1, 0],
+];
+
+const mockGetCSSValuesForPropertyName = function (propertyName) {
+ let values = {
+ "display": [
+ "block",
+ "flex",
+ "inline",
+ "inline-block",
+ "none",
+ ]
+ };
+ return values[propertyName] || [];
+};
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," +
+ "inplace editor CSS value autocomplete");
+ let [host, win, doc] = yield createHost();
+
+ let xulDocument = win.top.document;
+ let popup = new AutocompletePopup(xulDocument, { autoSelect: true });
+
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ start: runAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: {
+ name: "display"
+ },
+ done: resolve,
+ popup: popup
+ }, doc);
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let runAutocompletionTest = Task.async(function* (editor) {
+ info("Starting to test for css property completion");
+ editor._getCSSValuesForPropertyName = mockGetCSSValuesForPropertyName;
+
+ for (let data of testData) {
+ yield testCompletion(data, editor);
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+});
diff --git a/devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js b/devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js
new file mode 100644
index 000000000..5d1737bfd
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js
@@ -0,0 +1,119 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
+loadHelperScript("helper_inplace_editor.js");
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+ <?xml-stylesheet href="chrome://global/skin/global.css"?>
+ <?xml-stylesheet href="resource://devtools/client/themes/common.css"?>
+ <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+ <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tooltip test">
+ </window>`;
+
+// Test the inplace-editor autocomplete popup is aligned with the completed query.
+// Which means when completing "style=display:flex; color:" the popup will aim to be
+// aligned with the ":" next to "color".
+
+// format :
+// [
+// what key to press,
+// expected input box value after keypress,
+// selected suggestion index (-1 if popup is hidden),
+// number of suggestions in the popup (0 if popup is hidden),
+// ]
+// or
+// ["checkPopupOffset"]
+// to measure and test the autocomplete popup left offset.
+const testData = [
+ ["VK_RIGHT", "style=", -1, 0],
+ ["d", "style=display", 1, 2],
+ ["checkPopupOffset"],
+ ["VK_RIGHT", "style=display", -1, 0],
+ [":", "style=display:block", 0, 3],
+ ["checkPopupOffset"],
+ ["f", "style=display:flex", -1, 0],
+ ["VK_RIGHT", "style=display:flex", -1, 0],
+ [";", "style=display:flex;", -1, 0],
+ ["c", "style=display:flex;color", 1, 2],
+ ["checkPopupOffset"],
+ ["VK_RIGHT", "style=display:flex;color", -1, 0],
+ [":", "style=display:flex;color:blue", 0, 2],
+ ["checkPopupOffset"],
+];
+
+const mockGetCSSPropertyList = function () {
+ return [
+ "clear",
+ "color",
+ "direction",
+ "display",
+ ];
+};
+
+const mockGetCSSValuesForPropertyName = function (propertyName) {
+ let values = {
+ "color": ["blue", "red"],
+ "display": ["block", "flex", "none"]
+ };
+ return values[propertyName] || [];
+};
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,inplace editor CSS value autocomplete");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let popup = new AutocompletePopup(doc, { autoSelect: true });
+
+ info("Create a CSS_MIXED type autocomplete");
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ initial: "style=",
+ start: runAutocompletionTest,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+ done: resolve,
+ popup: popup
+ }, doc);
+ });
+
+ popup.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let runAutocompletionTest = Task.async(function* (editor) {
+ info("Starting autocomplete test for inplace-editor popup offset");
+ editor._getCSSPropertyList = mockGetCSSPropertyList;
+ editor._getCSSValuesForPropertyName = mockGetCSSValuesForPropertyName;
+
+ let previousOffset = -1;
+ for (let data of testData) {
+ if (data[0] === "checkPopupOffset") {
+ info("Check the popup offset has been modified");
+ // We are not testing hard coded offset values here, which could be fragile. We only
+ // want to ensure the popup tries to match the position of the query in the editor
+ // input.
+ let offset = getPopupOffset(editor);
+ ok(offset > previousOffset, "New popup offset is greater than the previous one");
+ previousOffset = offset;
+ } else {
+ yield testCompletion(data, editor);
+ }
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+});
+
+/**
+ * Get the autocomplete panel left offset, relative to the provided input's left offset.
+ */
+function getPopupOffset({popup, input}) {
+ let popupQuads = popup._panel.getBoxQuads({relativeTo: input});
+ return popupQuads[0].bounds.left;
+}
diff --git a/devtools/client/shared/test/browser_inplace-editor_maxwidth.js b/devtools/client/shared/test/browser_inplace-editor_maxwidth.js
new file mode 100644
index 000000000..205f4418e
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_maxwidth.js
@@ -0,0 +1,114 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+loadHelperScript("helper_inplace_editor.js");
+
+const MAX_WIDTH = 300;
+const START_TEXT = "Start text";
+const LONG_TEXT = "I am a long text and I will not fit in a 300px container. " +
+ "I expect the inplace editor to wrap.";
+
+// Test the inplace-editor behavior with a maxWidth configuration option
+// defined.
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8,inplace editor max width tests");
+ let [host, , doc] = yield createHost();
+
+ info("Testing the maxWidth option in pixels, to precisely check the size");
+ yield new Promise(resolve => {
+ createInplaceEditorAndClick({
+ multiline: true,
+ maxWidth: MAX_WIDTH,
+ start: testMaxWidth,
+ done: resolve
+ }, doc, START_TEXT);
+ });
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+let testMaxWidth = Task.async(function* (editor) {
+ is(editor.input.value, START_TEXT, "Span text content should be used");
+ ok(editor.input.offsetWidth < MAX_WIDTH,
+ "Input width should be strictly smaller than MAX_WIDTH");
+ is(getLines(editor.input), 1, "Input should display 1 line of text");
+
+ info("Check a text is on several lines if it does not fit MAX_WIDTH");
+ for (let key of LONG_TEXT) {
+ EventUtils.sendChar(key);
+ checkScrollbars(editor.input);
+ }
+
+ is(editor.input.value, LONG_TEXT, "Long text should be the input value");
+ is(editor.input.offsetWidth, MAX_WIDTH,
+ "Input width should be the same as MAX_WIDTH");
+ is(getLines(editor.input), 3, "Input should display 3 lines of text");
+ checkScrollbars(editor.input);
+
+ info("Delete all characters on line 3.");
+ while (getLines(editor.input) === 3) {
+ EventUtils.sendKey("BACK_SPACE");
+ checkScrollbars(editor.input);
+ }
+
+ is(editor.input.offsetWidth, MAX_WIDTH,
+ "Input width should be the same as MAX_WIDTH");
+ is(getLines(editor.input), 2, "Input should display 2 lines of text");
+ checkScrollbars(editor.input);
+
+ info("Delete all characters on line 2.");
+ while (getLines(editor.input) === 2) {
+ EventUtils.sendKey("BACK_SPACE");
+ checkScrollbars(editor.input);
+ }
+
+ is(getLines(editor.input), 1, "Input should display 1 line of text");
+ checkScrollbars(editor.input);
+
+ info("Delete all characters.");
+ while (editor.input.value !== "") {
+ EventUtils.sendKey("BACK_SPACE");
+ checkScrollbars(editor.input);
+ }
+
+ ok(editor.input.offsetWidth < MAX_WIDTH,
+ "Input width should again be strictly smaller than MAX_WIDTH");
+ ok(editor.input.offsetWidth > 0,
+ "Even with no content, the input has a non-zero width");
+ is(getLines(editor.input), 1, "Input should display 1 line of text");
+ checkScrollbars(editor.input);
+
+ info("Leave the inplace-editor");
+ EventUtils.sendKey("RETURN");
+});
+
+/**
+ * Retrieve the current number of lines displayed in the provided textarea.
+ *
+ * @param {DOMNode} textarea
+ * @return {Number} the number of lines
+ */
+function getLines(textarea) {
+ let win = textarea.ownerDocument.defaultView;
+ let style = win.getComputedStyle(textarea);
+ let lineHeight = style.getPropertyCSSValue("line-height").cssText;
+ return Math.floor(textarea.clientHeight / parseFloat(lineHeight));
+}
+
+/**
+ * Verify that the provided textarea has no vertical or horizontal scrollbar.
+ *
+ * @param {DOMNode} textarea
+ */
+function checkScrollbars(textarea) {
+ is(textarea.scrollHeight, textarea.clientHeight,
+ "Textarea should never have vertical scrollbars");
+ is(textarea.scrollWidth, textarea.clientWidth,
+ "Textarea should never have horizontal scrollbars");
+}
diff --git a/devtools/client/shared/test/browser_key_shortcuts.js b/devtools/client/shared/test/browser_key_shortcuts.js
new file mode 100644
index 000000000..c88782f85
--- /dev/null
+++ b/devtools/client/shared/test/browser_key_shortcuts.js
@@ -0,0 +1,425 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var isOSX = Services.appinfo.OS === "Darwin";
+
+add_task(function* () {
+ let shortcuts = new KeyShortcuts({
+ window
+ });
+
+ yield testSimple(shortcuts);
+ yield testNonLetterCharacter(shortcuts);
+ yield testPlusCharacter(shortcuts);
+ yield testFunctionKey(shortcuts);
+ yield testMixup(shortcuts);
+ yield testLooseDigits(shortcuts);
+ yield testExactModifiers(shortcuts);
+ yield testLooseShiftModifier(shortcuts);
+ yield testStrictLetterShiftModifier(shortcuts);
+ yield testAltModifier(shortcuts);
+ yield testCommandOrControlModifier(shortcuts);
+ yield testCtrlModifier(shortcuts);
+ yield testInvalidShortcutString(shortcuts);
+ yield testCmdShiftShortcut(shortcuts);
+ shortcuts.destroy();
+
+ yield testTarget();
+});
+
+// Test helper to listen to the next key press for a given key,
+// returning a promise to help using Tasks.
+function once(shortcuts, key, listener) {
+ let called = false;
+ return new Promise(done => {
+ let onShortcut = (key2, event) => {
+ shortcuts.off(key, onShortcut);
+ ok(!called, "once listener called only once (i.e. off() works)");
+ is(key, key2, "listener first argument match the key we listen");
+ called = true;
+ listener(key2, event);
+ done();
+ };
+ shortcuts.on(key, onShortcut);
+ });
+}
+
+function* testSimple(shortcuts) {
+ info("Test simple key shortcuts");
+
+ let onKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "0");
+
+ // Display another key press to ensure that once() correctly stop listening
+ EventUtils.synthesizeKey("0", {}, window);
+ });
+
+ EventUtils.synthesizeKey("0", {}, window);
+ yield onKey;
+}
+
+function* testNonLetterCharacter(shortcuts) {
+ info("Test non-naive character key shortcuts");
+
+ let onKey = once(shortcuts, "[", (key, event) => {
+ is(event.key, "[");
+ });
+
+ EventUtils.synthesizeKey("[", {}, window);
+ yield onKey;
+}
+
+function* testFunctionKey(shortcuts) {
+ info("Test function key shortcuts");
+
+ let onKey = once(shortcuts, "F12", (key, event) => {
+ is(event.key, "F12");
+ });
+
+ EventUtils.synthesizeKey("F12", { keyCode: 123 }, window);
+ yield onKey;
+}
+
+// Plus is special. It's keycode is the one for "=". That's because it requires
+// shift to be pressed and is behind "=" key. So it should be considered as a
+// character key
+function* testPlusCharacter(shortcuts) {
+ info("Test 'Plus' key shortcuts");
+
+ let onKey = once(shortcuts, "Plus", (key, event) => {
+ is(event.key, "+");
+ });
+
+ EventUtils.synthesizeKey("+", { keyCode: 61, shiftKey: true }, window);
+ yield onKey;
+}
+
+// Test they listeners are not mixed up between shortcuts
+function* testMixup(shortcuts) {
+ info("Test possible listener mixup");
+
+ let hitFirst = false, hitSecond = false;
+ let onFirstKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "0");
+ hitFirst = true;
+ });
+ let onSecondKey = once(shortcuts, "Alt+A", (key, event) => {
+ is(event.key, "a");
+ ok(event.altKey);
+ hitSecond = true;
+ });
+
+ // Dispatch the first shortcut and expect only this one to be notified
+ ok(!hitFirst, "First shortcut isn't notified before firing the key event");
+ EventUtils.synthesizeKey("0", {}, window);
+ yield onFirstKey;
+ ok(hitFirst, "Got the first shortcut notified");
+ ok(!hitSecond, "No mixup, second shortcut is still not notified (1/2)");
+
+ // Wait an extra time, just to be sure this isn't racy
+ yield new Promise(done => {
+ window.setTimeout(done, 0);
+ });
+ ok(!hitSecond, "No mixup, second shortcut is still not notified (2/2)");
+
+ // Finally dispatch the second shortcut
+ EventUtils.synthesizeKey("a", { altKey: true }, window);
+ yield onSecondKey;
+ ok(hitSecond, "Got the second shortcut notified once it is actually fired");
+}
+
+// On azerty keyboard, digits are only available by pressing Shift/Capslock,
+// but we accept them even if we omit doing that.
+function* testLooseDigits(shortcuts) {
+ info("Test Loose digits");
+ let onKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "à");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ });
+ // Simulate a press on the "0" key, without shift pressed on a french
+ // keyboard
+ EventUtils.synthesizeKey(
+ "à",
+ { keyCode: 48 },
+ window);
+ yield onKey;
+
+ onKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "0");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(event.shiftKey);
+ });
+ // Simulate the same press with shift pressed
+ EventUtils.synthesizeKey(
+ "0",
+ { keyCode: 48, shiftKey: true },
+ window);
+ yield onKey;
+}
+
+// Test that shortcuts is notified only when the modifiers match exactly
+function* testExactModifiers(shortcuts) {
+ info("Test exact modifiers match");
+
+ let hit = false;
+ let onKey = once(shortcuts, "Alt+A", (key, event) => {
+ is(event.key, "a");
+ ok(event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ hit = true;
+ });
+
+ // Dispatch with unexpected set of modifiers
+ ok(!hit, "Shortcut isn't notified before firing the key event");
+ EventUtils.synthesizeKey("a",
+ { accelKey: true, altKey: true, shiftKey: true },
+ window);
+ EventUtils.synthesizeKey(
+ "a",
+ { accelKey: true, altKey: false, shiftKey: false },
+ window);
+ EventUtils.synthesizeKey(
+ "a",
+ { accelKey: false, altKey: false, shiftKey: true },
+ window);
+ EventUtils.synthesizeKey(
+ "a",
+ { accelKey: false, altKey: false, shiftKey: false },
+ window);
+
+ // Wait an extra time to let a chance to call the listener
+ yield new Promise(done => {
+ window.setTimeout(done, 0);
+ });
+ ok(!hit, "Listener isn't called when modifiers aren't exactly matching");
+
+ // Dispatch the expected modifiers
+ EventUtils.synthesizeKey("a", { accelKey: false, altKey: true, shiftKey: false},
+ window);
+ yield onKey;
+ ok(hit, "Got shortcut notified once it is actually fired");
+}
+
+// Some keys are only accessible via shift and listener should also be called
+// even if the key didn't explicitely requested Shift modifier.
+// For example, `%` on french keyboards is only accessible via Shift.
+// Same thing for `@` on US keybords.
+function* testLooseShiftModifier(shortcuts) {
+ info("Test Loose shift modifier");
+ let onKey = once(shortcuts, "%", (key, event) => {
+ is(event.key, "%");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "%",
+ { accelKey: false, altKey: false, ctrlKey: false, shiftKey: true},
+ window);
+ yield onKey;
+
+ onKey = once(shortcuts, "@", (key, event) => {
+ is(event.key, "@");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "@",
+ { accelKey: false, altKey: false, ctrlKey: false, shiftKey: true},
+ window);
+ yield onKey;
+}
+
+// But Shift modifier is strict on all letter characters (a to Z)
+function* testStrictLetterShiftModifier(shortcuts) {
+ info("Test strict shift modifier on letters");
+ let hitFirst = false;
+ let onKey = once(shortcuts, "a", (key, event) => {
+ is(event.key, "a");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ hitFirst = true;
+ });
+ let onShiftKey = once(shortcuts, "Shift+a", (key, event) => {
+ is(event.key, "a");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "a",
+ { shiftKey: true},
+ window);
+ yield onShiftKey;
+ ok(!hitFirst, "Didn't fire the explicit shift+a");
+
+ EventUtils.synthesizeKey(
+ "a",
+ { shiftKey: false},
+ window);
+ yield onKey;
+}
+
+function* testAltModifier(shortcuts) {
+ info("Test Alt modifier");
+ let onKey = once(shortcuts, "Alt+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(event.altKey);
+ ok(!event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "VK_F1",
+ { altKey: true },
+ window);
+ yield onKey;
+}
+
+function* testCommandOrControlModifier(shortcuts) {
+ info("Test CommandOrControl modifier");
+ let onKey = once(shortcuts, "CommandOrControl+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(!event.altKey);
+ if (isOSX) {
+ ok(!event.ctrlKey);
+ ok(event.metaKey);
+ } else {
+ ok(event.ctrlKey);
+ ok(!event.metaKey);
+ }
+ ok(!event.shiftKey);
+ });
+ let onKeyAlias = once(shortcuts, "CmdOrCtrl+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(!event.altKey);
+ if (isOSX) {
+ ok(!event.ctrlKey);
+ ok(event.metaKey);
+ } else {
+ ok(event.ctrlKey);
+ ok(!event.metaKey);
+ }
+ ok(!event.shiftKey);
+ });
+ if (isOSX) {
+ EventUtils.synthesizeKey(
+ "VK_F1",
+ { metaKey: true },
+ window);
+ } else {
+ EventUtils.synthesizeKey(
+ "VK_F1",
+ { ctrlKey: true },
+ window);
+ }
+ yield onKey;
+ yield onKeyAlias;
+}
+
+function* testCtrlModifier(shortcuts) {
+ info("Test Ctrl modifier");
+ let onKey = once(shortcuts, "Ctrl+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(!event.altKey);
+ ok(event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ });
+ let onKeyAlias = once(shortcuts, "Control+F1", (key, event) => {
+ is(event.keyCode, window.KeyboardEvent.DOM_VK_F1);
+ ok(!event.altKey);
+ ok(event.ctrlKey);
+ ok(!event.metaKey);
+ ok(!event.shiftKey);
+ });
+ EventUtils.synthesizeKey(
+ "VK_F1",
+ { ctrlKey: true },
+ window);
+ yield onKey;
+ yield onKeyAlias;
+}
+
+function* testCmdShiftShortcut(shortcuts) {
+ if (!isOSX) {
+ // This test is OSX only (Bug 1300458).
+ return;
+ }
+
+ let onCmdKey = once(shortcuts, "CmdOrCtrl+[", (key, event) => {
+ is(event.key, "[");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(event.metaKey);
+ ok(!event.shiftKey);
+ });
+ let onCmdShiftKey = once(shortcuts, "CmdOrCtrl+Shift+[", (key, event) => {
+ is(event.key, "[");
+ ok(!event.altKey);
+ ok(!event.ctrlKey);
+ ok(event.metaKey);
+ ok(event.shiftKey);
+ });
+
+ EventUtils.synthesizeKey(
+ "[",
+ { metaKey: true, shiftKey: true },
+ window);
+ EventUtils.synthesizeKey(
+ "[",
+ { metaKey: true },
+ window);
+
+ yield onCmdKey;
+ yield onCmdShiftKey;
+}
+
+function* testTarget() {
+ info("Test KeyShortcuts with target argument");
+
+ let target = document.createElementNS("http://www.w3.org/1999/xhtml",
+ "input");
+ document.documentElement.appendChild(target);
+ target.focus();
+
+ let shortcuts = new KeyShortcuts({
+ window,
+ target
+ });
+ let onKey = once(shortcuts, "0", (key, event) => {
+ is(event.key, "0");
+ is(event.target, target);
+ });
+ EventUtils.synthesizeKey("0", {}, window);
+ yield onKey;
+
+ target.remove();
+
+ shortcuts.destroy();
+}
+
+function testInvalidShortcutString(shortcuts) {
+ info("Test wrong shortcut string");
+
+ let shortcut = KeyShortcuts.parseElectronKey(window, "Cmmd+F");
+ ok(!shortcut, "Passing a invalid shortcut string should return a null object");
+
+ shortcuts.on("Cmmd+F", function () {});
+ ok(true, "on() shouldn't throw when passing invalid shortcut string");
+}
diff --git a/devtools/client/shared/test/browser_keycodes.js b/devtools/client/shared/test/browser_keycodes.js
new file mode 100644
index 000000000..9e6b4a4ee
--- /dev/null
+++ b/devtools/client/shared/test/browser_keycodes.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+add_task(function* () {
+ for (let key in KeyCodes) {
+ is(KeyCodes[key], Ci.nsIDOMKeyEvent[key], "checking value for " + key);
+ }
+});
diff --git a/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html b/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html
new file mode 100644
index 000000000..070792b9a
--- /dev/null
+++ b/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.html
@@ -0,0 +1,65 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Layout Helpers</title>
+<style id="styles">
+ body {
+ margin: 0;
+ padding: 0;
+ }
+
+ #hidden-node {
+ display: none;
+ }
+
+ #simple-node-with-margin-padding-border {
+ width: 200px;
+ height: 200px;
+ background: #f06;
+
+ padding: 20px;
+ margin: 50px;
+ border: 10px solid black;
+ }
+
+ #scrolled-node {
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ width: 300px;
+ height: 100px;
+ overflow: scroll;
+ background: linear-gradient(red, pink);
+ }
+
+ #sub-scrolled-node {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ background: linear-gradient(yellow, green);
+ }
+
+ #inner-scrolled-node {
+ width: 100px;
+ height: 400px;
+ background: linear-gradient(black, white);
+ }
+</style>
+<div id="hidden-node"></div>
+<div id="simple-node-with-margin-padding-border"></div>
+<!-- The inline encoded code below corresponds to:
+<iframe style="margin:10px;border:0;width:300px;height:300px;">
+ <iframe style="margin:10px;border:0;width:200px;height:200px;">
+ <div id="inner-node" style="width:100px;height:100px;border:10px solid red;margin:10px;padding:10px;"></div>
+ </iframe>
+</iframe>
+ -->
+<iframe src="data:text/html,%3Cstyle%3Ebody%7Bmargin:0;padding:0;%7D%3C/style%3E%3Ciframe%20src=%22data:text/html,%253Cstyle%253Ebody%257Bmargin:0;padding:0;%257D%253C/style%253E%253Cdiv%2520id='inner-node'%2520style='width:100px;height:100px;border:10px%2520solid%2520red;margin:10px;padding:10px;'%253E%253C/div%253E%22%20style=%22margin:10px;border:0;width:200px;height:200px;%22%3E%3C/iframe%3E" style="margin:10px;border:0;width:300px;height:300px;"></iframe>
+<div id="scrolled-node">
+ <div id="sub-scrolled-node">
+ <div id="inner-scrolled-node"></div>
+ </div>
+</div>
+<span id="inline">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus porttitor luctus sem id scelerisque. Cras quis velit sed risus euismod lacinia. Donec viverra enim eu ligula efficitur, quis vulputate metus cursus. Duis sed interdum risus. Ut blandit velit vitae faucibus efficitur. Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br/ >
+Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed vitae dolor metus. Aliquam sed velit sit amet libero vestibulum aliquam vel a lorem. Integer eget ex eget justo auctor ullamcorper.<br/ >
+Praesent tristique maximus lacus, nec ultricies neque ultrices non. Phasellus vel lobortis justo. </span> \ No newline at end of file
diff --git a/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js b/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js
new file mode 100644
index 000000000..221127a11
--- /dev/null
+++ b/devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js
@@ -0,0 +1,219 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests getAdjustedQuads works properly in a variety of use cases including
+// iframes, scroll and zoom
+
+"use strict";
+
+const {getAdjustedQuads} = require("devtools/shared/layout/utils");
+
+const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers-getBoxQuads.html";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URI);
+ let doc = tab.linkedBrowser.contentDocument;
+
+ ok(typeof getAdjustedQuads === "function", "getAdjustedQuads is defined");
+
+ info("Running tests");
+
+ returnsTheRightDataStructure(doc);
+ isEmptyForMissingNode(doc);
+ isEmptyForHiddenNodes(doc);
+ defaultsToBorderBoxIfNoneProvided(doc);
+ returnsLikeGetBoxQuadsInSimpleCase(doc);
+ takesIframesOffsetsIntoAccount(doc);
+ takesScrollingIntoAccount(doc);
+ yield takesZoomIntoAccount(doc);
+ returnsMultipleItemsForWrappingInlineElements(doc);
+
+ gBrowser.removeCurrentTab();
+});
+
+function returnsTheRightDataStructure(doc) {
+ info("Checks that the returned data contains bounds and 4 points");
+
+ let node = doc.querySelector("body");
+ let [res] = getAdjustedQuads(doc.defaultView, node, "content");
+
+ ok("bounds" in res, "The returned data has a bounds property");
+ ok("p1" in res, "The returned data has a p1 property");
+ ok("p2" in res, "The returned data has a p2 property");
+ ok("p3" in res, "The returned data has a p3 property");
+ ok("p4" in res, "The returned data has a p4 property");
+
+ for (let boundProp of
+ ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+ ok(boundProp in res.bounds, "The bounds has a " + boundProp + " property");
+ }
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ for (let pointProp of ["x", "y", "z", "w"]) {
+ ok(pointProp in res[point], point + " has a " + pointProp + " property");
+ }
+ }
+}
+
+function isEmptyForMissingNode(doc) {
+ info("Checks that null is returned for invalid nodes");
+
+ for (let input of [null, undefined, "", 0]) {
+ is(getAdjustedQuads(doc.defaultView, input).length, 0,
+ "A 0-length array is returned for input " + input);
+ }
+}
+
+function isEmptyForHiddenNodes(doc) {
+ info("Checks that null is returned for nodes that aren't rendered");
+
+ let style = doc.querySelector("#styles");
+ is(getAdjustedQuads(doc.defaultView, style).length, 0,
+ "null is returned for a <style> node");
+
+ let hidden = doc.querySelector("#hidden-node");
+ is(getAdjustedQuads(doc.defaultView, hidden).length, 0,
+ "null is returned for a hidden node");
+}
+
+function defaultsToBorderBoxIfNoneProvided(doc) {
+ info("Checks that if no boxtype is passed, then border is the default one");
+
+ let node = doc.querySelector("#simple-node-with-margin-padding-border");
+ let [withBoxType] = getAdjustedQuads(doc.defaultView, node, "border");
+ let [withoutBoxType] = getAdjustedQuads(doc.defaultView, node);
+
+ for (let boundProp of
+ ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+ is(withBoxType.bounds[boundProp], withoutBoxType.bounds[boundProp],
+ boundProp + " bound is equal with or without the border box type");
+ }
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ for (let pointProp of ["x", "y", "z", "w"]) {
+ is(withBoxType[point][pointProp], withoutBoxType[point][pointProp],
+ point + "." + pointProp +
+ " is equal with or without the border box type");
+ }
+ }
+}
+
+function returnsLikeGetBoxQuadsInSimpleCase(doc) {
+ info("Checks that for an element in the main frame, without scroll nor zoom" +
+ "that the returned value is similar to the returned value of getBoxQuads");
+
+ let node = doc.querySelector("#simple-node-with-margin-padding-border");
+
+ for (let region of ["content", "padding", "border", "margin"]) {
+ let expected = node.getBoxQuads({
+ box: region
+ })[0];
+ let [actual] = getAdjustedQuads(doc.defaultView, node, region);
+
+ for (let boundProp of
+ ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+ is(actual.bounds[boundProp], expected.bounds[boundProp],
+ boundProp + " bound is equal to the one returned by getBoxQuads for " +
+ region + " box");
+ }
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ for (let pointProp of ["x", "y", "z", "w"]) {
+ is(actual[point][pointProp], expected[point][pointProp],
+ point + "." + pointProp +
+ " is equal to the one returned by getBoxQuads for " + region + " box");
+ }
+ }
+ }
+}
+
+function takesIframesOffsetsIntoAccount(doc) {
+ info("Checks that the quad returned for a node inside iframes that have " +
+ "margins takes those offsets into account");
+
+ let rootIframe = doc.querySelector("iframe");
+ let subIframe = rootIframe.contentDocument.querySelector("iframe");
+ let innerNode = subIframe.contentDocument.querySelector("#inner-node");
+
+ let [quad] = getAdjustedQuads(doc.defaultView, innerNode, "content");
+
+ // rootIframe margin + subIframe margin + node margin + node border + node padding
+ let p1x = 10 + 10 + 10 + 10 + 10;
+ is(quad.p1.x, p1x, "The inner node's p1 x position is correct");
+
+ // Same as p1x + the inner node width
+ let p2x = p1x + 100;
+ is(quad.p2.x, p2x, "The inner node's p2 x position is correct");
+}
+
+function takesScrollingIntoAccount(doc) {
+ info("Checks that the quad returned for a node inside multiple scrolled " +
+ "containers takes the scroll values into account");
+
+ // For info, the container being tested here is absolutely positioned at 0 0
+ // to simplify asserting the coordinates
+
+ info("Scroll the container nodes down");
+ let scrolledNode = doc.querySelector("#scrolled-node");
+ scrolledNode.scrollTop = 100;
+ let subScrolledNode = doc.querySelector("#sub-scrolled-node");
+ subScrolledNode.scrollTop = 200;
+ let innerNode = doc.querySelector("#inner-scrolled-node");
+
+ let [quad] = getAdjustedQuads(doc.defaultView, innerNode, "content");
+ is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling down");
+ is(quad.p1.y, -300, "p1.y of the scrolled node is correct after scrolling down");
+
+ info("Scrolling back up");
+ scrolledNode.scrollTop = 0;
+ subScrolledNode.scrollTop = 0;
+
+ [quad] = getAdjustedQuads(doc.defaultView, innerNode, "content");
+ is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling up");
+ is(quad.p1.y, 0, "p1.y of the scrolled node is correct after scrolling up");
+}
+
+function* takesZoomIntoAccount(doc) {
+ info("Checks that if the page is zoomed in/out, the quad returned is correct");
+
+ // Hard-coding coordinates in this zoom test is a bad idea as it can vary
+ // depending on the platform, so we simply test that zooming in produces a
+ // bigger quad and zooming out produces a smaller quad
+
+ let node = doc.querySelector("#simple-node-with-margin-padding-border");
+ let [defaultQuad] = getAdjustedQuads(doc.defaultView, node);
+
+ info("Zoom in");
+ window.FullZoom.enlarge();
+ let [zoomedInQuad] = getAdjustedQuads(doc.defaultView, node);
+
+ ok(zoomedInQuad.bounds.width > defaultQuad.bounds.width,
+ "The zoomed in quad is bigger than the default one");
+ ok(zoomedInQuad.bounds.height > defaultQuad.bounds.height,
+ "The zoomed in quad is bigger than the default one");
+
+ info("Zoom out");
+ yield window.FullZoom.reset();
+ window.FullZoom.reduce();
+ let [zoomedOutQuad] = getAdjustedQuads(doc.defaultView, node);
+
+ ok(zoomedOutQuad.bounds.width < defaultQuad.bounds.width,
+ "The zoomed out quad is smaller than the default one");
+ ok(zoomedOutQuad.bounds.height < defaultQuad.bounds.height,
+ "The zoomed out quad is smaller than the default one");
+
+ yield window.FullZoom.reset();
+}
+
+function returnsMultipleItemsForWrappingInlineElements(doc) {
+ info("Checks that several quads are returned " +
+ "for inline elements that span line-breaks");
+
+ let node = doc.querySelector("#inline");
+ let quads = getAdjustedQuads(doc.defaultView, node, "content");
+ // At least 3 because of the 2 <br />, maybe more depending on the window size.
+ ok(quads.length >= 3, "Multiple quads were returned");
+
+ is(quads.length, node.getBoxQuads().length,
+ "The same number of boxes as getBoxQuads was returned");
+}
diff --git a/devtools/client/shared/test/browser_layoutHelpers.html b/devtools/client/shared/test/browser_layoutHelpers.html
new file mode 100644
index 000000000..f50bfffdf
--- /dev/null
+++ b/devtools/client/shared/test/browser_layoutHelpers.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset=utf-8>
+<title> Layout Helpers </title>
+
+<style>
+ html {
+ height: 300%;
+ width: 300%;
+ }
+ div#some {
+ position: absolute;
+ background: black;
+ width: 2px;
+ height: 2px;
+ }
+ iframe {
+ position: absolute;
+ width: 40px;
+ height: 40px;
+ border: 0;
+ }
+</style>
+
+<div id=some></div>
diff --git a/devtools/client/shared/test/browser_layoutHelpers.js b/devtools/client/shared/test/browser_layoutHelpers.js
new file mode 100644
index 000000000..3274ad686
--- /dev/null
+++ b/devtools/client/shared/test/browser_layoutHelpers.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that scrollIntoViewIfNeeded works properly.
+const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll");
+
+const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers.html";
+
+add_task(function* () {
+ let [host, win] = yield createHost("bottom", TEST_URI);
+ runTest(win);
+ host.destroy();
+});
+
+function runTest(win) {
+ let some = win.document.getElementById("some");
+
+ some.style.top = win.innerHeight + "px";
+ some.style.left = win.innerWidth + "px";
+ // The tests start with a black 2x2 pixels square below bottom right.
+ // Do not resize the window during the tests.
+
+ let xPos = Math.floor(win.innerWidth / 2);
+ // Above the viewport.
+ win.scroll(xPos, win.innerHeight + 2);
+ scrollIntoViewIfNeeded(some);
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+ "Element completely hidden above should appear centered.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // On the top edge.
+ win.scroll(win.innerWidth / 2, win.innerHeight + 1);
+ scrollIntoViewIfNeeded(some);
+ is(win.scrollY, win.innerHeight,
+ "Element partially visible above should appear above.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // Just below the viewport.
+ win.scroll(win.innerWidth / 2, 0);
+ scrollIntoViewIfNeeded(some);
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+ "Element completely hidden below should appear centered.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // On the bottom edge.
+ win.scroll(win.innerWidth / 2, 1);
+ scrollIntoViewIfNeeded(some);
+ is(win.scrollY, 2,
+ "Element partially visible below should appear below.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // Above the viewport.
+ win.scroll(win.innerWidth / 2, win.innerHeight + 2);
+ scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, win.innerHeight,
+ "Element completely hidden above should appear above " +
+ "if parameter is false.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // On the top edge.
+ win.scroll(win.innerWidth / 2, win.innerHeight + 1);
+ scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, win.innerHeight,
+ "Element partially visible above should appear above " +
+ "if parameter is false.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // Below the viewport.
+ win.scroll(win.innerWidth / 2, 0);
+ scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, 2,
+ "Element completely hidden below should appear below " +
+ "if parameter is false.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+
+ // On the bottom edge.
+ win.scroll(win.innerWidth / 2, 1);
+ scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, 2,
+ "Element partially visible below should appear below " +
+ "if parameter is false.");
+ is(win.scrollX, xPos,
+ "scrollX position has not changed.");
+}
diff --git a/devtools/client/shared/test/browser_mdn-docs-01.js b/devtools/client/shared/test/browser_mdn-docs-01.js
new file mode 100644
index 000000000..6490dfef7
--- /dev/null
+++ b/devtools/client/shared/test/browser_mdn-docs-01.js
@@ -0,0 +1,168 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the MdnDocsWidget object, and specifically its
+ * loadCssDocs() function.
+ *
+ * The MdnDocsWidget is initialized with a document which has a specific
+ * structure. You then call loadCssDocs(), passing in a CSS property name.
+ * MdnDocsWidget then fetches docs for that property by making an XHR to
+ * a docs page, and loads the results into the document. While the XHR is
+ * still not resolved the document is put into an "initializing" state in
+ * which the devtools throbber is displayed.
+ *
+ * In this file we test:
+ * - the initial state of the document before the docs have loaded
+ * - the state of the document after the docs have loaded
+ */
+
+"use strict";
+
+const {setBaseCssDocsUrl, MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
+
+/**
+ * Test properties
+ *
+ * In the real tooltip, a CSS property name is used to look up an MDN page
+ * for that property.
+ * In the test code, the names defined here is used to look up a page
+ * served by the test server.
+ */
+const BASIC_TESTING_PROPERTY = "html-mdn-css-basic-testing.html";
+
+const BASIC_EXPECTED_SUMMARY = "A summary of the property.";
+const BASIC_EXPECTED_SYNTAX = [{type: "comment", text: "/* The part we want */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "this"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "is-the-part-we-want"},
+ {type: "text", text: ";"}];
+
+const URI_PARAMS =
+ "?utm_source=mozilla&utm_medium=firefox-inspector&utm_campaign=default";
+
+add_task(function* () {
+ setBaseCssDocsUrl(TEST_URI_ROOT);
+
+ yield addTab("about:blank");
+ let [host, win] = yield createHost("bottom", "data:text/html," +
+ "<div class='mdn-container'></div>");
+ let widget = new MdnDocsWidget(win.document.querySelector("div"));
+
+ yield testTheBasics(widget);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * Test all the basics
+ * - initial content, before docs have loaded, is as expected
+ * - throbber is set before docs have loaded
+ * - contents are as expected after docs have loaded
+ * - throbber is gone after docs have loaded
+ * - mdn link text is correct and onclick behavior is correct
+ */
+function* testTheBasics(widget) {
+ info("Test all the basic functionality in the widget");
+
+ info("Get the widget state before docs have loaded");
+ let promise = widget.loadCssDocs(BASIC_TESTING_PROPERTY);
+
+ info("Check initial contents before docs have loaded");
+ checkTooltipContents(widget.elements, {
+ propertyName: BASIC_TESTING_PROPERTY,
+ summary: "",
+ syntax: ""
+ });
+
+ // throbber is set
+ ok(widget.elements.info.classList.contains("devtools-throbber"),
+ "Throbber is set");
+
+ info("Now let the widget finish loading");
+ yield promise;
+
+ info("Check contents after docs have loaded");
+ checkTooltipContents(widget.elements, {
+ propertyName: BASIC_TESTING_PROPERTY,
+ summary: BASIC_EXPECTED_SUMMARY,
+ syntax: BASIC_EXPECTED_SYNTAX
+ });
+
+ // throbber is gone
+ ok(!widget.elements.info.classList.contains("devtools-throbber"),
+ "Throbber is not set");
+
+ info("Check that MDN link text is correct and onclick behavior is correct");
+
+ let mdnLink = widget.elements.linkToMdn;
+ let expectedHref = TEST_URI_ROOT + BASIC_TESTING_PROPERTY + URI_PARAMS;
+ is(mdnLink.href, expectedHref, "MDN link href is correct");
+
+ let uri = yield checkLinkClick(mdnLink);
+ is(uri, expectedHref, "New tab opened with the expected URI");
+}
+
+ /**
+ * Clicking the "Visit MDN Page" in the tooltip panel
+ * should open a new browser tab with the page loaded.
+ *
+ * To test this we'll listen for a new tab opening, and
+ * when it does, add a listener to that new tab to tell
+ * us when it has loaded.
+ *
+ * Then we click the link.
+ *
+ * In the tab's load listener, we'll resolve the promise
+ * with the URI, which is expected to match the href
+ * in the orginal link.
+ *
+ * One complexity is that when you open a new tab,
+ * "about:blank" is first loaded into the tab before the
+ * actual page. So we ignore that first load event, and keep
+ * listening until "load" is triggered for a different URI.
+ */
+function checkLinkClick(link) {
+ function loadListener(tab) {
+ let browser = getBrowser().getBrowserForTab(tab);
+ let uri = browser.currentURI.spec;
+
+ info("New browser tab has loaded");
+ gBrowser.removeTab(tab);
+ info("Resolve promise with new tab URI");
+ deferred.resolve(uri);
+ }
+
+ function newTabListener(e) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", newTabListener);
+ let tab = e.target;
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url => url != "about:blank")
+ .then(url => loadListener(tab));
+ }
+
+ let deferred = defer();
+ info("Check that clicking the link opens a new tab with the correct URI");
+ gBrowser.tabContainer.addEventListener("TabOpen", newTabListener, false);
+ info("Click the link to MDN");
+ link.click();
+ return deferred.promise;
+}
+
+/**
+ * Utility function to check content of the tooltip.
+ */
+function checkTooltipContents(doc, expected) {
+ is(doc.heading.textContent,
+ expected.propertyName,
+ "Property name is correct");
+
+ is(doc.summary.textContent,
+ expected.summary,
+ "Summary is correct");
+
+ checkCssSyntaxHighlighterOutput(expected.syntax, doc.syntax);
+}
diff --git a/devtools/client/shared/test/browser_mdn-docs-02.js b/devtools/client/shared/test/browser_mdn-docs-02.js
new file mode 100644
index 000000000..000dc7261
--- /dev/null
+++ b/devtools/client/shared/test/browser_mdn-docs-02.js
@@ -0,0 +1,128 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the MdnDocsWidget object, and specifically its
+ * loadCssDocs() function.
+ *
+ * The MdnDocsWidget is initialized with a document which has a specific
+ * structure. You then call loadCssDocs(), passing in a CSS property name.
+ * MdnDocsWidget then fetches docs for that property by making an XHR to
+ * a docs page, and loads the results into the document.
+ *
+ * In this file we test that the tooltip can properly handle the different
+ * structures that the docs page might have, including variant structures and
+ * error conditions like parts of the document being missing.
+ *
+ * We also test that the tooltip properly handles the case where the page
+ * doesn't exist at all.
+ */
+
+"use strict";
+
+const {
+ setBaseCssDocsUrl,
+ MdnDocsWidget
+} = require("devtools/client/shared/widgets/MdnDocsWidget");
+
+const BASIC_EXPECTED_SUMMARY = "A summary of the property.";
+const BASIC_EXPECTED_SYNTAX = [{type: "comment", text: "/* The part we want */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "this"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "is-the-part-we-want"},
+ {type: "text", text: ";"}];
+
+const ERROR_MESSAGE = "Could not load docs page.";
+
+/**
+ * Test properties
+ *
+ * In the real tooltip, a CSS property name is used to look up an MDN page
+ * for that property.
+ * In the test code, the names defined here are used to look up a page
+ * served by the test server. We have different properties to test
+ * different ways that the docs pages might be constructed, including errors
+ * like pages that don't include docs where we expect.
+ */
+const SYNTAX_OLD_STYLE = "html-mdn-css-syntax-old-style.html";
+const NO_SUMMARY = "html-mdn-css-no-summary.html";
+const NO_SYNTAX = "html-mdn-css-no-syntax.html";
+const NO_SUMMARY_OR_SYNTAX = "html-mdn-css-no-summary-or-syntax.html";
+
+const TEST_DATA = [{
+ desc: "Test a property for which we don't have a page",
+ docsPageUrl: "i-dont-exist.html",
+ expectedContents: {
+ propertyName: "i-dont-exist.html",
+ summary: ERROR_MESSAGE,
+ syntax: []
+ }
+}, {
+ desc: "Test a property whose syntax section is specified using an old-style page",
+ docsPageUrl: SYNTAX_OLD_STYLE,
+ expectedContents: {
+ propertyName: SYNTAX_OLD_STYLE,
+ summary: BASIC_EXPECTED_SUMMARY,
+ syntax: BASIC_EXPECTED_SYNTAX
+ }
+}, {
+ desc: "Test a property whose page doesn't have a summary",
+ docsPageUrl: NO_SUMMARY,
+ expectedContents: {
+ propertyName: NO_SUMMARY,
+ summary: "",
+ syntax: BASIC_EXPECTED_SYNTAX
+ }
+}, {
+ desc: "Test a property whose page doesn't have a syntax",
+ docsPageUrl: NO_SYNTAX,
+ expectedContents: {
+ propertyName: NO_SYNTAX,
+ summary: BASIC_EXPECTED_SUMMARY,
+ syntax: []
+ }
+}, {
+ desc: "Test a property whose page doesn't have a summary or a syntax",
+ docsPageUrl: NO_SUMMARY_OR_SYNTAX,
+ expectedContents: {
+ propertyName: NO_SUMMARY_OR_SYNTAX,
+ summary: ERROR_MESSAGE,
+ syntax: []
+ }
+}
+];
+
+add_task(function* () {
+ setBaseCssDocsUrl(TEST_URI_ROOT);
+
+ yield addTab("about:blank");
+ let [host, win] = yield createHost("bottom", "data:text/html," +
+ "<div class='mdn-container'></div>");
+ let widget = new MdnDocsWidget(win.document.querySelector("div"));
+
+ for (let {desc, docsPageUrl, expectedContents} of TEST_DATA) {
+ info(desc);
+ yield widget.loadCssDocs(docsPageUrl);
+ checkTooltipContents(widget.elements, expectedContents);
+ }
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+/*
+ * Utility function to check content of the tooltip.
+ */
+function checkTooltipContents(doc, expected) {
+ is(doc.heading.textContent,
+ expected.propertyName,
+ "Property name is correct");
+
+ is(doc.summary.textContent,
+ expected.summary,
+ "Summary is correct");
+
+ checkCssSyntaxHighlighterOutput(expected.syntax, doc.syntax);
+}
diff --git a/devtools/client/shared/test/browser_mdn-docs-03.js b/devtools/client/shared/test/browser_mdn-docs-03.js
new file mode 100644
index 000000000..c686aa6a9
--- /dev/null
+++ b/devtools/client/shared/test/browser_mdn-docs-03.js
@@ -0,0 +1,277 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the CSS syntax highlighter in the MdnDocsWidget object.
+ *
+ * The CSS syntax highlighter accepts:
+ * - a string containing CSS
+ * - a DOM node
+ *
+ * It parses the string and creates a collection of DOM nodes for different
+ * CSS token types. These DOM nodes have CSS classes applied to them,
+ * to apply the right style for that particular token type. The DOM nodes
+ * are returned as children of the node that was passed to the function.
+ *
+ * This test code defines a number of different strings containing valid and
+ * invalid CSS in various forms. For each string it defines the DOM nodes
+ * that it expects to get from the syntax highlighter.
+ *
+ * It then calls the syntax highlighter, and checks that the resulting
+ * collection of DOM nodes is what we expected.
+ */
+
+"use strict";
+
+const {appendSyntaxHighlightedCSS} = require("devtools/client/shared/widgets/MdnDocsWidget");
+
+/**
+ * An array containing the actual test cases.
+ *
+ * The test code tests every case in the array. If you want to add more
+ * test cases, just add more items to the array.
+ *
+ * Each test case consists of:
+ * - description: string describing the salient features of this test case
+ * - example: the string to test
+ * - expected: an array of objects, one for each DOM node we expect, that
+ * captures the information about the node that we expect to test.
+ */
+const TEST_DATA = [{
+ description: "Valid syntax, string value.",
+ example: "name: stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, numeric value.",
+ example: "name: 1;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, url value.",
+ example: "name: url(./name);",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "url(./name)"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, space before ':'.",
+ example: "name : stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: " "},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, space before ';'.",
+ example: "name: stringValue ;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: " "},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, trailing space.",
+ example: "name: stringValue; ",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"},
+ {type: "text", text: " "}
+ ]}, {
+ description: "Valid syntax, leading space.",
+ example: " name: stringValue;",
+ expected: [{type: "text", text: " "},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, two spaces.",
+ example: "name: stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, no spaces.",
+ example: "name:stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, two-part value.",
+ example: "name: stringValue 1;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, two declarations.",
+ example: "name: stringValue;\n" +
+ "name: 1;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, commented, numeric value.",
+ example: "/* comment */\n" +
+ "name: 1;",
+ expected: [{type: "comment", text: "/* comment */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, multiline commented, string value.",
+ example: "/* multiline \n" +
+ "comment */\n" +
+ "name: stringValue;",
+ expected: [{type: "comment", text: "/* multiline \ncomment */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, commented, two declarations.",
+ example: "/* comment 1 */\n" +
+ "name: 1;\n" +
+ "/* comment 2 */\n" +
+ "name: stringValue;",
+ expected: [{type: "comment", text: "/* comment 1 */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "1"},
+ {type: "text", text: ";"},
+ {type: "text", text: "\n"},
+ {type: "comment", text: "/* comment 2 */"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, multiline.",
+ example: "name: \n" +
+ "stringValue;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " \n"},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Valid syntax, multiline, two declarations.",
+ example: "name: \n" +
+ "stringValue \n" +
+ "stringValue2;",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " \n"},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: " \n"},
+ {type: "property-value", text: "stringValue2"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Invalid: not CSS at all.",
+ example: "not CSS at all",
+ expected: [{type: "property-name", text: "not"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "CSS"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "at"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "all"}
+ ]}, {
+ description: "Invalid: switched ':' and ';'.",
+ example: "name; stringValue:",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ";"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "stringValue"},
+ {type: "text", text: ":"}
+ ]}, {
+ description: "Invalid: unterminated comment.",
+ example: "/* unterminated comment\n" +
+ "name: stringValue;",
+ expected: [{type: "comment", text: "/* unterminated comment\nname: stringValue;"}
+ ]}, {
+ description: "Invalid: bad comment syntax.",
+ example: "// invalid comment\n" +
+ "name: stringValue;",
+ expected: [{type: "text", text: "/"},
+ {type: "text", text: "/"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "invalid"},
+ {type: "text", text: " "},
+ {type: "property-name", text: "comment"},
+ {type: "text", text: "\n"},
+ {type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: ";"}
+ ]}, {
+ description: "Invalid: no trailing ';'.",
+ example: "name: stringValue\n" +
+ "name: stringValue2",
+ expected: [{type: "property-name", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue"},
+ {type: "text", text: "\n"},
+ {type: "property-value", text: "name"},
+ {type: "text", text: ":"},
+ {type: "text", text: " "},
+ {type: "property-value", text: "stringValue2"},
+ ]}
+];
+
+/**
+ * Iterate through every test case, calling the syntax highlighter,
+ * then calling a helper function to check the output.
+ */
+add_task(function* () {
+ let doc = gBrowser.selectedTab.ownerDocument;
+ let parent = doc.createElement("div");
+ info("Testing all CSS syntax highlighter test cases");
+ for (let {description, example, expected} of TEST_DATA) {
+ info("Testing: " + description);
+ appendSyntaxHighlightedCSS(example, parent);
+ checkCssSyntaxHighlighterOutput(expected, parent);
+ while (parent.firstChild) {
+ parent.firstChild.remove();
+ }
+ }
+});
diff --git a/devtools/client/shared/test/browser_num-l10n.js b/devtools/client/shared/test/browser_num-l10n.js
new file mode 100644
index 000000000..fb4ef6cc7
--- /dev/null
+++ b/devtools/client/shared/test/browser_num-l10n.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the localization utils work properly.
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+function test() {
+ let l10n = new LocalizationHelper();
+
+ is(l10n.numberWithDecimals(1234.56789, 2), "1,234.57",
+ "The first number was properly localized.");
+ is(l10n.numberWithDecimals(0.0001, 2), "0",
+ "The second number was properly localized.");
+ is(l10n.numberWithDecimals(1.0001, 2), "1",
+ "The third number was properly localized.");
+ is(l10n.numberWithDecimals(NaN, 2), "0",
+ "NaN was properly localized.");
+ is(l10n.numberWithDecimals(null, 2), "0",
+ "`null` was properly localized.");
+ is(l10n.numberWithDecimals(undefined, 2), "0",
+ "`undefined` was properly localized.");
+
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_options-view-01.js b/devtools/client/shared/test/browser_options-view-01.js
new file mode 100644
index 000000000..7dae05a3c
--- /dev/null
+++ b/devtools/client/shared/test/browser_options-view-01.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that options-view OptionsView responds to events correctly.
+
+const {OptionsView} = require("devtools/client/shared/options-view");
+
+const BRANCH = "devtools.debugger.";
+const BLACK_BOX_PREF = "auto-black-box";
+const PRETTY_PRINT_PREF = "auto-pretty-print";
+
+const originalBlackBox = Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF);
+const originalPrettyPrint = Services.prefs.getBoolPref(BRANCH + PRETTY_PRINT_PREF);
+
+add_task(function* () {
+ info("Setting a couple of preferences");
+ Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, false);
+ Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, true);
+
+ info("Opening a test tab and a toolbox host to create the options view in");
+ yield addTab("about:blank");
+ let [host, win] = yield createHost("bottom", OPTIONS_VIEW_URL);
+
+ yield testOptionsView(win);
+
+ info("Closing the host and current tab");
+ host.destroy();
+ gBrowser.removeCurrentTab();
+
+ info("Resetting the preferences");
+ Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, originalBlackBox);
+ Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, originalPrettyPrint);
+});
+
+function* testOptionsView(win) {
+ let events = [];
+ let options = createOptionsView(win);
+ yield options.initialize();
+
+ let $ = win.document.querySelector.bind(win.document);
+
+ options.on("pref-changed", (_, pref) => events.push(pref));
+
+ let ppEl = $("menuitem[data-pref='auto-pretty-print']");
+ let bbEl = $("menuitem[data-pref='auto-black-box']");
+
+ // Test default config
+ is(ppEl.getAttribute("checked"), "true", "`true` prefs are checked on start");
+ is(bbEl.getAttribute("checked"), "", "`false` prefs are unchecked on start");
+
+ // Test buttons update when preferences update outside of the menu
+ Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, false);
+ Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, true);
+
+ is(options.getPref(PRETTY_PRINT_PREF), false, "getPref returns correct value");
+ is(options.getPref(BLACK_BOX_PREF), true, "getPref returns correct value");
+
+ is(ppEl.getAttribute("checked"), "", "menuitems update when preferences change");
+ is(bbEl.getAttribute("checked"), "true", "menuitems update when preferences change");
+
+ // Tests events are fired when preferences update outside of the menu
+ is(events.length, 2, "two 'pref-changed' events fired");
+ is(events[0], "auto-pretty-print",
+ "correct pref passed in 'pref-changed' event (auto-pretty-print)");
+ is(events[1], "auto-black-box",
+ "correct pref passed in 'pref-changed' event (auto-black-box)");
+
+ // Test buttons update when clicked and preferences are updated
+ yield click(options, win, ppEl);
+ is(ppEl.getAttribute("checked"), "true", "menuitems update when clicked");
+ is(Services.prefs.getBoolPref(BRANCH + PRETTY_PRINT_PREF),
+ true, "preference updated via click");
+
+ yield click(options, win, bbEl);
+ is(bbEl.getAttribute("checked"), "", "menuitems update when clicked");
+ is(Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF),
+ false, "preference updated via click");
+
+ // Tests events are fired when preferences updated via click
+ is(events.length, 4, "two 'pref-changed' events fired");
+ is(events[2], "auto-pretty-print",
+ "correct pref passed in 'pref-changed' event (auto-pretty-print)");
+ is(events[3], "auto-black-box",
+ "correct pref passed in 'pref-changed' event (auto-black-box)");
+
+ yield options.destroy();
+}
+
+function createOptionsView(win) {
+ return new OptionsView({
+ branchName: BRANCH,
+ menupopup: win.document.querySelector("#options-menupopup")
+ });
+}
+
+function* click(view, win, menuitem) {
+ let opened = view.once("options-shown");
+ let closed = view.once("options-hidden");
+
+ let button = win.document.querySelector("#options-button");
+ EventUtils.synthesizeMouseAtCenter(button, {}, win);
+ yield opened;
+ is(button.getAttribute("open"), "true", "button has `open` attribute");
+
+ EventUtils.synthesizeMouseAtCenter(menuitem, {}, win);
+ yield closed;
+ ok(!button.hasAttribute("open"), "button does not have `open` attribute");
+}
diff --git a/devtools/client/shared/test/browser_outputparser.js b/devtools/client/shared/test/browser_outputparser.js
new file mode 100644
index 000000000..a231ad903
--- /dev/null
+++ b/devtools/client/shared/test/browser_outputparser.js
@@ -0,0 +1,292 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {OutputParser} = require("devtools/client/shared/output-parser");
+const {initCssProperties, getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, , doc] = yield createHost("bottom", "data:text/html," +
+ "<h1>browser_outputParser.js</h1><div></div>");
+
+ // Mock the toolbox that initCssProperties expect so we get the fallback css properties.
+ let toolbox = {target: {client: {}, hasActor: () => false}};
+ yield initCssProperties(toolbox);
+ let cssProperties = getCssProperties(toolbox);
+
+ let parser = new OutputParser(doc, cssProperties);
+ testParseCssProperty(doc, parser);
+ testParseCssVar(doc, parser);
+ testParseURL(doc, parser);
+ testParseFilter(doc, parser);
+ testParseAngle(doc, parser);
+
+ host.destroy();
+}
+
+// Class name used in color swatch.
+var COLOR_TEST_CLASS = "test-class";
+
+// Create a new CSS color-parsing test. |name| is the name of the CSS
+// property. |value| is the CSS text to use. |segments| is an array
+// describing the expected result. If an element of |segments| is a
+// string, it is simply appended to the expected string. Otherwise,
+// it must be an object with a |name| property, which is the color
+// name as it appears in the input.
+//
+// This approach is taken to reduce boilerplate and to make it simpler
+// to modify the test when the parseCssProperty output changes.
+function makeColorTest(name, value, segments) {
+ let result = {
+ name,
+ value,
+ expected: ""
+ };
+
+ for (let segment of segments) {
+ if (typeof (segment) === "string") {
+ result.expected += segment;
+ } else {
+ result.expected += "<span data-color=\"" + segment.name + "\">" +
+ "<span class=\"" + COLOR_TEST_CLASS + "\" style=\"background-color:" +
+ segment.name + "\"></span><span>" +
+ segment.name + "</span></span>";
+ }
+ }
+
+ result.desc = "Testing " + name + ": " + value;
+
+ return result;
+}
+
+function testParseCssProperty(doc, parser) {
+ let tests = [
+ makeColorTest("border", "1px solid red",
+ ["1px solid ", {name: "red"}]),
+
+ makeColorTest("background-image",
+ "linear-gradient(to right, #F60 10%, rgba(0,0,0,1))",
+ ["linear-gradient(to right, ", {name: "#F60"},
+ " 10%, ", {name: "rgba(0,0,0,1)"},
+ ")"]),
+
+ // In "arial black", "black" is a font, not a color.
+ makeColorTest("font-family", "arial black", ["arial black"]),
+
+ makeColorTest("box-shadow", "0 0 1em red",
+ ["0 0 1em ", {name: "red"}]),
+
+ makeColorTest("box-shadow",
+ "0 0 1em red, 2px 2px 0 0 rgba(0,0,0,.5)",
+ ["0 0 1em ", {name: "red"},
+ ", 2px 2px 0 0 ",
+ {name: "rgba(0,0,0,.5)"}]),
+
+ makeColorTest("content", "\"red\"", ["\"red\""]),
+
+ // Invalid property names should not cause exceptions.
+ makeColorTest("hellothere", "'red'", ["'red'"]),
+
+ makeColorTest("filter",
+ "blur(1px) drop-shadow(0 0 0 blue) url(red.svg#blue)",
+ ["<span data-filters=\"blur(1px) drop-shadow(0 0 0 blue) ",
+ "url(red.svg#blue)\"><span>",
+ "blur(1px) drop-shadow(0 0 0 ",
+ {name: "blue"},
+ ") url(red.svg#blue)</span></span>"]),
+
+ makeColorTest("color", "currentColor", ["currentColor"]),
+
+ // Test a very long property.
+ makeColorTest("background-image",
+ /* eslint-disable max-len */
+ "linear-gradient(to left, transparent 0, transparent 5%,#F00 0, #F00 10%,#FF0 0, #FF0 15%,#0F0 0, #0F0 20%,#0FF 0, #0FF 25%,#00F 0, #00F 30%,#800 0, #800 35%,#880 0, #880 40%,#080 0, #080 45%,#088 0, #088 50%,#008 0, #008 55%,#FFF 0, #FFF 60%,#EEE 0, #EEE 65%,#CCC 0, #CCC 70%,#999 0, #999 75%,#666 0, #666 80%,#333 0, #333 85%,#111 0, #111 90%,#000 0, #000 95%,transparent 0, transparent 100%)",
+ /* eslint-enable max-len */
+ ["linear-gradient(to left, ", {name: "transparent"},
+ " 0, ", {name: "transparent"},
+ " 5%,", {name: "#F00"},
+ " 0, ", {name: "#F00"},
+ " 10%,", {name: "#FF0"},
+ " 0, ", {name: "#FF0"},
+ " 15%,", {name: "#0F0"},
+ " 0, ", {name: "#0F0"},
+ " 20%,", {name: "#0FF"},
+ " 0, ", {name: "#0FF"},
+ " 25%,", {name: "#00F"},
+ " 0, ", {name: "#00F"},
+ " 30%,", {name: "#800"},
+ " 0, ", {name: "#800"},
+ " 35%,", {name: "#880"},
+ " 0, ", {name: "#880"},
+ " 40%,", {name: "#080"},
+ " 0, ", {name: "#080"},
+ " 45%,", {name: "#088"},
+ " 0, ", {name: "#088"},
+ " 50%,", {name: "#008"},
+ " 0, ", {name: "#008"},
+ " 55%,", {name: "#FFF"},
+ " 0, ", {name: "#FFF"},
+ " 60%,", {name: "#EEE"},
+ " 0, ", {name: "#EEE"},
+ " 65%,", {name: "#CCC"},
+ " 0, ", {name: "#CCC"},
+ " 70%,", {name: "#999"},
+ " 0, ", {name: "#999"},
+ " 75%,", {name: "#666"},
+ " 0, ", {name: "#666"},
+ " 80%,", {name: "#333"},
+ " 0, ", {name: "#333"},
+ " 85%,", {name: "#111"},
+ " 0, ", {name: "#111"},
+ " 90%,", {name: "#000"},
+ " 0, ", {name: "#000"},
+ " 95%,", {name: "transparent"},
+ " 0, ", {name: "transparent"},
+ " 100%)"]),
+ ];
+
+ let target = doc.querySelector("div");
+ ok(target, "captain, we have the div");
+
+ for (let test of tests) {
+ info(test.desc);
+
+ let frag = parser.parseCssProperty(test.name, test.value, {
+ colorSwatchClass: COLOR_TEST_CLASS
+ });
+
+ target.appendChild(frag);
+
+ is(target.innerHTML, test.expected,
+ "CSS property correctly parsed for " + test.name + ": " + test.value);
+
+ target.innerHTML = "";
+ }
+}
+
+function testParseCssVar(doc, parser) {
+ let frag = parser.parseCssProperty("color", "var(--some-kind-of-green)", {
+ colorSwatchClass: "test-colorswatch"
+ });
+
+ let target = doc.querySelector("div");
+ ok(target, "captain, we have the div");
+ target.appendChild(frag);
+
+ is(target.innerHTML, "var(--some-kind-of-green)",
+ "CSS property correctly parsed");
+
+ target.innerHTML = "";
+}
+
+function testParseURL(doc, parser) {
+ info("Test that URL parsing preserves quoting style");
+
+ const tests = [
+ {
+ desc: "simple test without quotes",
+ leader: "url(",
+ trailer: ")",
+ },
+ {
+ desc: "simple test with single quotes",
+ leader: "url('",
+ trailer: "')",
+ },
+ {
+ desc: "simple test with double quotes",
+ leader: "url(\"",
+ trailer: "\")",
+ },
+ {
+ desc: "test with single quotes and whitespace",
+ leader: "url( \t'",
+ trailer: "'\r\n\f)",
+ },
+ {
+ desc: "simple test with uppercase",
+ leader: "URL(",
+ trailer: ")",
+ },
+ {
+ desc: "bad url, missing paren",
+ leader: "url(",
+ trailer: "",
+ expectedTrailer: ")"
+ },
+ {
+ desc: "bad url, missing paren, with baseURI",
+ baseURI: "data:text/html,<style></style>",
+ leader: "url(",
+ trailer: "",
+ expectedTrailer: ")"
+ },
+ {
+ desc: "bad url, double quote, missing paren",
+ leader: "url(\"",
+ trailer: "\"",
+ expectedTrailer: "\")",
+ },
+ {
+ desc: "bad url, single quote, missing paren and quote",
+ leader: "url('",
+ trailer: "",
+ expectedTrailer: "')"
+ }
+ ];
+
+ for (let test of tests) {
+ let url = test.leader + "something.jpg" + test.trailer;
+ let frag = parser.parseCssProperty("background", url, {
+ urlClass: "test-urlclass",
+ baseURI: test.baseURI,
+ });
+
+ let target = doc.querySelector("div");
+ target.appendChild(frag);
+
+ let expectedTrailer = test.expectedTrailer || test.trailer;
+
+ let expected = test.leader +
+ "<a target=\"_blank\" class=\"test-urlclass\" " +
+ "href=\"something.jpg\">something.jpg</a>" +
+ expectedTrailer;
+
+ is(target.innerHTML, expected, test.desc);
+
+ target.innerHTML = "";
+ }
+}
+
+function testParseFilter(doc, parser) {
+ let frag = parser.parseCssProperty("filter", "something invalid", {
+ filterSwatchClass: "test-filterswatch"
+ });
+
+ let swatchCount = frag.querySelectorAll(".test-filterswatch").length;
+ is(swatchCount, 1, "filter swatch was created");
+}
+
+function testParseAngle(doc, parser) {
+ let frag = parser.parseCssProperty("image-orientation", "90deg", {
+ angleSwatchClass: "test-angleswatch"
+ });
+
+ let swatchCount = frag.querySelectorAll(".test-angleswatch").length;
+ is(swatchCount, 1, "angle swatch was created");
+
+ frag = parser.parseCssProperty("background-image",
+ "linear-gradient(90deg, red, blue", {
+ angleSwatchClass: "test-angleswatch"
+ });
+
+ swatchCount = frag.querySelectorAll(".test-angleswatch").length;
+ is(swatchCount, 1, "angle swatch was created");
+}
diff --git a/devtools/client/shared/test/browser_poller.js b/devtools/client/shared/test/browser_poller.js
new file mode 100644
index 000000000..281d055ff
--- /dev/null
+++ b/devtools/client/shared/test/browser_poller.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Poller class.
+
+const { Poller } = require("devtools/client/shared/poller");
+
+add_task(function* () {
+ let count1 = 0, count2 = 0, count3 = 0;
+
+ let poller1 = new Poller(function () {
+ count1++;
+ }, 1000000000, true);
+ let poller2 = new Poller(function () {
+ count2++;
+ }, 10);
+ let poller3 = new Poller(function () {
+ count3++;
+ }, 1000000000);
+
+ poller2.on();
+
+ ok(!poller1.isPolling(), "isPolling() returns false for an off poller");
+ ok(poller2.isPolling(), "isPolling() returns true for an on poller");
+
+ yield waitUntil(() => count2 > 10);
+
+ ok(count2 > 10, "poller that was turned on polled several times");
+ ok(count1 === 0, "poller that was never turned on never polled");
+
+ yield poller2.off();
+ let currentCount2 = count2;
+
+ // Really high poll time!
+ poller1.on();
+ poller3.on();
+
+ yield waitUntil(() => count1 === 1);
+ ok(true, "Poller calls fn immediately when `immediate` is true");
+ ok(count3 === 0, "Poller does not call fn immediately when `immediate` is not set");
+
+ ok(count2 === currentCount2, "a turned off poller does not continue to poll");
+ yield poller2.off();
+ yield poller2.off();
+ yield poller2.off();
+ ok(true, "Poller.prototype.off() is idempotent");
+
+ // This should still have not polled a second time
+ is(count1, 1, "wait time works");
+
+ ok(poller1.isPolling(), "isPolling() returns true for an on poller");
+ ok(!poller2.isPolling(), "isPolling() returns false for an off poller");
+});
+
+add_task(function* () {
+ let count = -1;
+ // Create a poller that returns a promise.
+ // The promise is resolved asynchronously after adding 9 to the count, ensuring
+ // that on every poll, we have a multiple of 10.
+ let asyncPoller = new Poller(function () {
+ count++;
+ ok(!(count % 10), `Async poller called with a multiple of 10: ${count}`);
+ return new Promise(function (resolve, reject) {
+ let add9 = 9;
+ let interval = setInterval(() => {
+ if (add9--) {
+ count++;
+ } else {
+ clearInterval(interval);
+ resolve();
+ }
+ }, 10);
+ });
+ });
+
+ asyncPoller.on(1);
+ yield waitUntil(() => count > 50);
+ yield asyncPoller.off();
+});
+
+add_task(function* () {
+ // Create a poller that returns a promise. This poll call
+ // is called immediately, and then subsequently turned off.
+ // The call to `off` should not resolve until the inflight call
+ // finishes.
+ let inflightFinished = null;
+ let pollCalls = 0;
+ let asyncPoller = new Poller(function () {
+ pollCalls++;
+ return new Promise(function (resolve, reject) {
+ setTimeout(() => {
+ inflightFinished = true;
+ resolve();
+ }, 1000);
+ });
+ }, 1, true);
+ asyncPoller.on();
+
+ yield asyncPoller.off();
+ ok(inflightFinished,
+ "off() method does not resolve until remaining inflight poll calls finish");
+ is(pollCalls, 1, "should only be one poll call to occur before turning off polling");
+});
+
+add_task(function* () {
+ // Create a poller that returns a promise. This poll call
+ // is called immediately, and then subsequently turned off.
+ // The call to `off` should not resolve until the inflight call
+ // finishes.
+ let inflightFinished = null;
+ let pollCalls = 0;
+ let asyncPoller = new Poller(function () {
+ pollCalls++;
+ return new Promise(function (resolve, reject) {
+ setTimeout(() => {
+ inflightFinished = true;
+ resolve();
+ }, 1000);
+ });
+ }, 1, true);
+ asyncPoller.on();
+
+ yield asyncPoller.destroy();
+ ok(inflightFinished,
+ "destroy() method does not resolve until remaining inflight poll calls finish");
+ is(pollCalls, 1, "should only be one poll call to occur before destroying polling");
+
+ try {
+ asyncPoller.on();
+ ok(false, "Calling on() after destruction should throw");
+ } catch (e) {
+ ok(true, "Calling on() after destruction should throw");
+ }
+});
diff --git a/devtools/client/shared/test/browser_prefs-01.js b/devtools/client/shared/test/browser_prefs-01.js
new file mode 100644
index 000000000..193348361
--- /dev/null
+++ b/devtools/client/shared/test/browser_prefs-01.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the preference helpers work properly.
+
+const { PrefsHelper } = require("devtools/client/shared/prefs");
+
+function test() {
+ let Prefs = new PrefsHelper("devtools.debugger", {
+ "foo": ["Bool", "enabled"]
+ });
+
+ let originalPrefValue = Services.prefs.getBoolPref("devtools.debugger.enabled");
+ is(Prefs.foo, originalPrefValue, "The pref value was correctly fetched.");
+
+ Prefs.foo = !originalPrefValue;
+ is(Prefs.foo, !originalPrefValue,
+ "The pref was was correctly changed (1).");
+ is(Services.prefs.getBoolPref("devtools.debugger.enabled"), !originalPrefValue,
+ "The pref was was correctly changed (2).");
+
+ Services.prefs.setBoolPref("devtools.debugger.enabled", originalPrefValue);
+ info("The pref value was reset (1).");
+ is(Prefs.foo, !originalPrefValue,
+ "The cached pref value hasn't changed yet (1).");
+
+ Services.prefs.setBoolPref("devtools.debugger.enabled", !originalPrefValue);
+ info("The pref value was reset (2).");
+ is(Prefs.foo, !originalPrefValue,
+ "The cached pref value hasn't changed yet (2).");
+
+ Prefs.registerObserver();
+
+ Services.prefs.setBoolPref("devtools.debugger.enabled", originalPrefValue);
+ info("The pref value was reset (3).");
+ is(Prefs.foo, originalPrefValue,
+ "The cached pref value has changed now.");
+
+ Prefs.unregisterObserver();
+
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_prefs-02.js b/devtools/client/shared/test/browser_prefs-02.js
new file mode 100644
index 000000000..f0f638d63
--- /dev/null
+++ b/devtools/client/shared/test/browser_prefs-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that preference helpers work properly with custom types of Float and Json.
+
+const { PrefsHelper } = require("devtools/client/shared/prefs");
+
+function test() {
+ let originalJson = Services.prefs.getCharPref(
+ "devtools.performance.timeline.hidden-markers");
+ let originalFloat = Services.prefs.getCharPref(
+ "devtools.performance.memory.sample-probability");
+
+ let Prefs = new PrefsHelper("devtools.performance", {
+ "float": ["Float", "memory.sample-probability"],
+ "json": ["Json", "timeline.hidden-markers"]
+ });
+
+ Prefs.registerObserver();
+
+ // Float
+ Services.prefs.setCharPref("devtools.performance.timeline.hidden-markers", "{\"a\":1}");
+ is(Prefs.json.a, 1, "The JSON pref value is correctly casted on get.");
+
+ Prefs.json = { b: 2 };
+ is(Prefs.json.a, undefined, "The JSON pref value is correctly casted on set (1).");
+ is(Prefs.json.b, 2, "The JSON pref value is correctly casted on set (2).");
+
+ // Float
+ Services.prefs.setCharPref("devtools.performance.memory.sample-probability", "3.14");
+ is(Prefs.float, 3.14, "The float pref value is correctly casted on get.");
+
+ Prefs.float = 6.28;
+ is(Prefs.float, 6.28, "The float pref value is correctly casted on set.");
+
+ Prefs.unregisterObserver();
+
+ Services.prefs.setCharPref("devtools.performance.timeline.hidden-markers",
+ originalJson);
+ Services.prefs.setCharPref("devtools.performance.memory.sample-probability",
+ originalFloat);
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_require_raw.js b/devtools/client/shared/test/browser_require_raw.js
new file mode 100644
index 000000000..d40b84c35
--- /dev/null
+++ b/devtools/client/shared/test/browser_require_raw.js
@@ -0,0 +1,20 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+
+const { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/shared/",
+ window
+});
+
+const variableFileContents = browserRequire("raw!devtools/client/themes/variables.css");
+
+function test() {
+ ok(variableFileContents.length > 0, "raw browserRequire worked");
+ finish();
+}
diff --git a/devtools/client/shared/test/browser_spectrum.js b/devtools/client/shared/test/browser_spectrum.js
new file mode 100644
index 000000000..9e72ef621
--- /dev/null
+++ b/devtools/client/shared/test/browser_spectrum.js
@@ -0,0 +1,114 @@
+/* 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";
+
+// Tests that the spectrum color picker works correctly
+
+const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
+
+const TEST_URI = `data:text/html,
+ <link rel="stylesheet" href="chrome://devtools/content/shared/widgets/spectrum.css" type="text/css"/>
+ <div id="spectrum-container" />`;
+
+add_task(function* () {
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.getElementById("spectrum-container");
+
+ yield testCreateAndDestroyShouldAppendAndRemoveElements(container);
+ yield testPassingAColorAtInitShouldSetThatColor(container);
+ yield testSettingAndGettingANewColor(container);
+ yield testChangingColorShouldEmitEvents(container);
+ yield testSettingColorShoudUpdateTheUI(container);
+
+ host.destroy();
+});
+
+function testCreateAndDestroyShouldAppendAndRemoveElements(container) {
+ ok(container, "We have the root node to append spectrum to");
+ is(container.childElementCount, 0, "Root node is empty");
+
+ let s = new Spectrum(container, [255, 126, 255, 1]);
+ s.show();
+ ok(container.childElementCount > 0, "Spectrum has appended elements");
+
+ s.destroy();
+ is(container.childElementCount, 0, "Destroying spectrum removed all nodes");
+}
+
+function testPassingAColorAtInitShouldSetThatColor(container) {
+ let initRgba = [255, 126, 255, 1];
+
+ let s = new Spectrum(container, initRgba);
+ s.show();
+
+ let setRgba = s.rgb;
+
+ is(initRgba[0], setRgba[0], "Spectrum initialized with the right color");
+ is(initRgba[1], setRgba[1], "Spectrum initialized with the right color");
+ is(initRgba[2], setRgba[2], "Spectrum initialized with the right color");
+ is(initRgba[3], setRgba[3], "Spectrum initialized with the right color");
+
+ s.destroy();
+}
+
+function testSettingAndGettingANewColor(container) {
+ let s = new Spectrum(container, [0, 0, 0, 1]);
+ s.show();
+
+ let colorToSet = [255, 255, 255, 1];
+ s.rgb = colorToSet;
+ let newColor = s.rgb;
+
+ is(colorToSet[0], newColor[0], "Spectrum set with the right color");
+ is(colorToSet[1], newColor[1], "Spectrum set with the right color");
+ is(colorToSet[2], newColor[2], "Spectrum set with the right color");
+ is(colorToSet[3], newColor[3], "Spectrum set with the right color");
+
+ s.destroy();
+}
+
+function testChangingColorShouldEmitEvents(container) {
+ return new Promise(resolve => {
+ let s = new Spectrum(container, [255, 255, 255, 1]);
+ s.show();
+
+ s.once("changed", (event, rgba, color) => {
+ ok(true, "Changed event was emitted on color change");
+ is(rgba[0], 128, "New color is correct");
+ is(rgba[1], 64, "New color is correct");
+ is(rgba[2], 64, "New color is correct");
+ is(rgba[3], 1, "New color is correct");
+ is(`rgba(${rgba.join(", ")})`, color, "RGBA and css color correspond");
+
+ s.destroy();
+ resolve();
+ });
+
+ // Simulate a drag move event by calling the handler directly.
+ s.onDraggerMove(s.dragger.offsetWidth / 2, s.dragger.offsetHeight / 2);
+ });
+}
+
+function testSettingColorShoudUpdateTheUI(container) {
+ let s = new Spectrum(container, [255, 255, 255, 1]);
+ s.show();
+ let dragHelperOriginalPos = [s.dragHelper.style.top, s.dragHelper.style.left];
+ let alphaHelperOriginalPos = s.alphaSliderHelper.style.left;
+
+ s.rgb = [50, 240, 234, .2];
+ s.updateUI();
+
+ ok(s.alphaSliderHelper.style.left != alphaHelperOriginalPos, "Alpha helper has moved");
+ ok(s.dragHelper.style.top !== dragHelperOriginalPos[0], "Drag helper has moved");
+ ok(s.dragHelper.style.left !== dragHelperOriginalPos[1], "Drag helper has moved");
+
+ s.rgb = [240, 32, 124, 0];
+ s.updateUI();
+ is(s.alphaSliderHelper.style.left, -(s.alphaSliderHelper.offsetWidth / 2) + "px",
+ "Alpha range UI has been updated again");
+
+ s.destroy();
+}
diff --git a/devtools/client/shared/test/browser_tableWidget_basic.js b/devtools/client/shared/test/browser_tableWidget_basic.js
new file mode 100644
index 000000000..684ca99bb
--- /dev/null
+++ b/devtools/client/shared/test/browser_tableWidget_basic.js
@@ -0,0 +1,390 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the table widget api works fine
+
+"use strict";
+
+const TEST_URI = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+
+ // Uncomment these lines to help with visual debugging. When uncommented they
+ // dump a couple of thousand errors in the log (bug 1258285)
+ // "<?xml-stylesheet href='chrome://devtools/skin/light-theme.css'?>" +
+ // "<?xml-stylesheet href='chrome://devtools/skin/widgets.css'?>" +
+
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Table Widget' width='600' height='500'>" +
+ "<box flex='1' class='theme-light'/></window>";
+
+const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host, , doc] = yield createHost("bottom", TEST_URI);
+
+ let table = new TableWidget(doc.querySelector("box"), {
+ initialColumns: {
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ },
+ uniqueId: "col1",
+ emptyText: "This is dummy empty text",
+ highlightUpdated: true,
+ removableColumns: true,
+ firstColumn: "col4"
+ });
+
+ startTests(doc, table);
+ endTests(doc, host, table);
+});
+
+function startTests(doc, table) {
+ populateTable(doc, table);
+
+ testTreeItemInsertedCorrectly(doc, table);
+ testAPI(doc, table);
+}
+
+function endTests(doc, host, table) {
+ table.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+ table = null;
+ finish();
+}
+
+function populateTable(doc, table) {
+ table.push({
+ col1: "id1",
+ col2: "value10",
+ col3: "value20",
+ col4: "value30"
+ });
+ table.push({
+ col1: "id2",
+ col2: "value14",
+ col3: "value29",
+ col4: "value32"
+ });
+ table.push({
+ col1: "id3",
+ col2: "value17",
+ col3: "value21",
+ col4: "value31",
+ extraData: "foobar",
+ extraData2: 42
+ });
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id5",
+ col2: "value19",
+ col3: "value26",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ });
+ table.push({
+ col1: "id8",
+ col2: "value11",
+ col3: "value27",
+ col4: "value34"
+ });
+
+ let span = doc.createElement("span");
+ span.textContent = "domnode";
+
+ table.push({
+ col1: "id9",
+ col2: "value11",
+ col3: "value23",
+ col4: span
+ });
+}
+
+/**
+ * Test if the nodes are inserted correctly in the table.
+ */
+function testTreeItemInsertedCorrectly(doc, table) {
+ // double because of splitters
+ is(table.tbody.children.length, 4 * 2, "4 columns exist");
+
+ // Test firstColumn option and check if the nodes are inserted correctly
+ is(table.tbody.children[0].firstChild.children.length, 9 + 1,
+ "Correct rows in column 4");
+ is(table.tbody.children[0].firstChild.firstChild.value, "Column 4",
+ "Correct column header value");
+
+ for (let i = 1; i < 4; i++) {
+ is(table.tbody.children[i * 2].firstChild.children.length, 9 + 1,
+ `Correct rows in column ${i}`);
+ is(table.tbody.children[i * 2].firstChild.firstChild.value, `Column ${i}`,
+ "Correct column header value");
+ }
+ for (let i = 1; i < 10; i++) {
+ is(table.tbody.children[2].firstChild.children[i].value, `id${i}`,
+ `Correct value in row ${i}`);
+ }
+
+ // Remove firstColumn option and reset the table
+ table.clear();
+ table.firstColumn = "";
+ table.setColumns({
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ });
+ populateTable(doc, table);
+
+ // Check if the nodes are inserted correctly without firstColumn option
+ for (let i = 0; i < 4; i++) {
+ is(table.tbody.children[i * 2].firstChild.children.length, 9 + 1,
+ `Correct rows in column ${i}`);
+ is(table.tbody.children[i * 2].firstChild.firstChild.value,
+ `Column ${i + 1}`,
+ "Correct column header value");
+ }
+
+ for (let i = 1; i < 10; i++) {
+ is(table.tbody.firstChild.firstChild.children[i].value, `id${i}`,
+ `Correct value in row ${i}`);
+ }
+}
+
+/**
+ * Tests if the API exposed by TableWidget works properly
+ */
+function testAPI(doc, table) {
+ info("Testing TableWidget API");
+ // Check if selectRow and selectedRow setter works as expected
+ // Nothing should be selected beforehand
+ ok(!doc.querySelector(".theme-selected"), "Nothing is selected");
+ table.selectRow("id4");
+ let node = doc.querySelector(".theme-selected");
+ ok(!!node, "Somthing got selected");
+ is(node.getAttribute("data-id"), "id4", "Correct node selected");
+
+ table.selectRow("id7");
+ let node2 = doc.querySelector(".theme-selected");
+ ok(!!node2, "Somthing is still selected");
+ isnot(node, node2, "Newly selected node is different from previous");
+ is(node2.getAttribute("data-id"), "id7", "Correct node selected");
+
+ // test if selectedIRow getter works
+ is(table.selectedRow.col1, "id7", "Correct result of selectedRow getter");
+
+ // test if isSelected works
+ ok(table.isSelected("id7"), "isSelected with column id works");
+ ok(table.isSelected({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ }), "isSelected with json works");
+
+ table.selectedRow = "id4";
+ let node3 = doc.querySelector(".theme-selected");
+ ok(!!node3, "Somthing is still selected");
+ isnot(node2, node3, "Newly selected node is different from previous");
+ is(node3, node, "First and third selected nodes should be same");
+ is(node3.getAttribute("data-id"), "id4", "Correct node selected");
+
+ // test if selectedRow getter works
+ is(table.selectedRow.col1, "id4", "Correct result of selectedRow getter");
+
+ // test if clear selection works
+ table.clearSelection();
+ ok(!doc.querySelector(".theme-selected"),
+ "Nothing selected after clear selection call");
+
+ // test if selectNextRow and selectPreviousRow work
+ table.selectedRow = "id7";
+ ok(table.isSelected("id7"), "Correct row selected");
+ table.selectNextRow();
+ ok(table.isSelected("id8"), "Correct row selected after selectNextRow call");
+
+ table.selectNextRow();
+ ok(table.isSelected("id9"), "Correct row selected after selectNextRow call");
+
+ table.selectNextRow();
+ ok(table.isSelected("id1"),
+ "Properly cycled to first row after selectNextRow call on last row");
+
+ table.selectNextRow();
+ ok(table.isSelected("id2"), "Correct row selected after selectNextRow call");
+
+ table.selectPreviousRow();
+ ok(table.isSelected("id1"),
+ "Correct row selected after selectPreviousRow call");
+
+ table.selectPreviousRow();
+ ok(table.isSelected("id9"),
+ "Properly cycled to last row after selectPreviousRow call on first row");
+
+ // test if remove works
+ ok(doc.querySelector("[data-id='id4']"), "id4 row exists before removal");
+ table.remove("id4");
+ ok(!doc.querySelector("[data-id='id4']"),
+ "id4 row does not exist after removal through id");
+
+ ok(doc.querySelector("[data-id='id6']"), "id6 row exists before removal");
+ table.remove({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ ok(!doc.querySelector("[data-id='id6']"),
+ "id6 row does not exist after removal through json");
+
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+
+ // test if selectedIndex getter setter works
+ table.selectedIndex = 2;
+ ok(table.isSelected("id3"), "Correct row selected by selectedIndex setter");
+
+ table.selectedIndex = 4;
+ ok(table.isSelected("id5"), "Correct row selected by selectedIndex setter");
+
+ table.selectRow("id8");
+ is(table.selectedIndex, 7, "Correct value of selectedIndex getter");
+
+ // testing if clear works
+ table.clear();
+ // double because splitters
+ is(table.tbody.children.length, 4 * 2,
+ "4 columns exist even after clear");
+ for (let i = 0; i < 4; i++) {
+ is(table.tbody.children[i * 2].firstChild.children.length, 1,
+ `Only header in the column ${i} after clear call`);
+ is(table.tbody.children[i * 2].firstChild.firstChild.value,
+ `Column ${i + 1}`,
+ "Correct column header value");
+ }
+
+ // testing if setColumns work
+ table.setColumns({
+ col1: "Foobar",
+ col2: "Testing"
+ });
+
+ // double because splitters
+ is(table.tbody.children.length, 2 * 2,
+ "2 columns exist after setColumn call");
+ is(table.tbody.children[0].firstChild.firstChild.value, "Foobar",
+ "Correct column header value for first column");
+ is(table.tbody.children[2].firstChild.firstChild.value, "Testing",
+ "Correct column header value for second column");
+
+ table.setColumns({
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ });
+ // double because splitters
+ is(table.tbody.children.length, 4 * 2,
+ "4 columns exist after second setColumn call");
+
+ populateTable(doc, table);
+
+ // testing if update works
+ is(doc.querySelectorAll("[data-id='id4']")[1].value, "value12",
+ "Correct value before update");
+ table.update({
+ col1: "id4",
+ col2: "UPDATED",
+ col3: "value26",
+ col4: "value33"
+ });
+ is(doc.querySelectorAll("[data-id='id4']")[1].value, "UPDATED",
+ "Correct value after update");
+
+ // testing if sorting works by calling it once on an already sorted column
+ // should sort descending
+ table.sortBy("col1");
+ for (let i = 1; i < 10; i++) {
+ is(table.tbody.firstChild.firstChild.children[i].value, `id${10 - i}`,
+ `Correct value in row ${i} after descending sort by on col1`);
+ }
+ // Calling it on an unsorted column should sort by it in ascending manner
+ table.sortBy("col2");
+ let cell = table.tbody.children[2].firstChild.children[2];
+ checkAscendingOrder(cell);
+
+ // Calling it again should sort by it in descending manner
+ table.sortBy("col2");
+ cell = table.tbody.children[2].firstChild.lastChild.previousSibling;
+ checkDescendingOrder(cell);
+
+ // Calling it again should sort by it in ascending manner
+ table.sortBy("col2");
+ cell = table.tbody.children[2].firstChild.children[2];
+ checkAscendingOrder(cell);
+
+ table.clear();
+ populateTable(doc, table);
+
+ // testing if sorting works should sort by ascending manner
+ table.sortBy("col4");
+ cell = table.tbody.children[6].firstChild.children[1];
+ is(cell.textContent, "domnode", "DOMNode sorted correctly");
+ checkAscendingOrder(cell.nextSibling);
+
+ // Calling it again should sort it in descending order
+ table.sortBy("col4");
+ cell = table.tbody.children[6].firstChild.children[9];
+ is(cell.textContent, "domnode", "DOMNode sorted correctly");
+ checkDescendingOrder(cell.previousSibling);
+}
+
+function checkAscendingOrder(cell) {
+ while (cell) {
+ let currentCell = cell.value || cell.textContent;
+ let prevCell = cell.previousSibling.value ||
+ cell.previousSibling.textContent;
+ ok(currentCell >= prevCell, "Sorting is in ascending order");
+ cell = cell.nextSibling;
+ }
+}
+
+function checkDescendingOrder(cell) {
+ while (cell != cell.parentNode.firstChild) {
+ let currentCell = cell.value || cell.textContent;
+ let nextCell = cell.nextSibling.value || cell.nextSibling.textContent;
+ ok(currentCell >= nextCell, "Sorting is in descending order");
+ cell = cell.previousSibling;
+ }
+}
diff --git a/devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js b/devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js
new file mode 100644
index 000000000..ce052bd88
--- /dev/null
+++ b/devtools/client/shared/test/browser_tableWidget_keyboard_interaction.js
@@ -0,0 +1,194 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that keyboard interaction works fine with the table widget
+
+"use strict";
+
+const TEST_URI = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+
+ // Uncomment these lines to help with visual debugging. When uncommented they
+ // dump a couple of thousand errors in the log (bug 1258285)
+ // "<?xml-stylesheet href='chrome://devtools/skin/light-theme.css'?>" +
+ // "<?xml-stylesheet href='chrome://devtools/skin/widgets.css'?>" +
+
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Table Widget' width='600' height='500'>" +
+ "<box flex='1' class='theme-light'/></window>";
+const TEST_OPT = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
+
+var doc, table;
+
+function test() {
+ waitForExplicitFinish();
+ let win = Services.ww.openWindow(null, TEST_URI, "_blank", TEST_OPT, null);
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ waitForFocus(function () {
+ doc = win.document;
+ table = new TableWidget(doc.querySelector("box"), {
+ initialColumns: {
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ },
+ uniqueId: "col1",
+ emptyText: "This is dummy empty text",
+ highlightUpdated: true,
+ removableColumns: true,
+ });
+ startTests();
+ });
+ });
+}
+
+function endTests() {
+ table.destroy();
+ doc.defaultView.close();
+ doc = table = null;
+ finish();
+}
+
+var startTests = Task.async(function* () {
+ populateTable();
+ yield testKeyboardInteraction();
+ endTests();
+});
+
+function populateTable() {
+ table.push({
+ col1: "id1",
+ col2: "value10",
+ col3: "value20",
+ col4: "value30"
+ });
+ table.push({
+ col1: "id2",
+ col2: "value14",
+ col3: "value29",
+ col4: "value32"
+ });
+ table.push({
+ col1: "id3",
+ col2: "value17",
+ col3: "value21",
+ col4: "value31",
+ extraData: "foobar",
+ extraData2: 42
+ });
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id5",
+ col2: "value19",
+ col3: "value26",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ });
+ table.push({
+ col1: "id8",
+ col2: "value11",
+ col3: "value27",
+ col4: "value34"
+ });
+ table.push({
+ col1: "id9",
+ col2: "value11",
+ col3: "value23",
+ col4: "value38"
+ });
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node, button = 0) {
+ if (button == 0) {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {},
+ doc.defaultView));
+ } else {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {
+ button: button,
+ type: "contextmenu"
+ }, doc.defaultView));
+ }
+}
+
+function getNodeByValue(value) {
+ return table.tbody.querySelector("[value=" + value + "]");
+}
+
+/**
+ * Tests if pressing navigation keys on the table items does the expected
+ * behavior.
+ */
+var testKeyboardInteraction = Task.async(function* () {
+ info("Testing keyboard interaction with the table");
+ info("clicking on the row containing id2");
+ let node = getNodeByValue("id2");
+ let event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ click(node);
+ yield event;
+
+ yield testRow("id3", "DOWN", "next row");
+ yield testRow("id4", "DOWN", "next row");
+ yield testRow("id3", "UP", "previous row");
+ yield testRow("id4", "DOWN", "next row");
+ yield testRow("id5", "DOWN", "next row");
+ yield testRow("id6", "DOWN", "next row");
+ yield testRow("id5", "UP", "previous row");
+ yield testRow("id4", "UP", "previous row");
+ yield testRow("id3", "UP", "previous row");
+
+ // selecting last item node to test edge navigation cycling case
+ table.selectedRow = "id9";
+
+ // pressing down on last row should move to first row.
+ yield testRow("id1", "DOWN", "first row");
+
+ // pressing up now should move to last row.
+ yield testRow("id9", "UP", "last row");
+});
+
+function* testRow(id, key, destination) {
+ let node = getNodeByValue(id);
+ // node should not have selected class
+ ok(!node.classList.contains("theme-selected"),
+ "Row should not have selected class");
+ info(`Pressing ${key} to select ${destination}`);
+
+ let event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ EventUtils.sendKey(key, doc.defaultView);
+
+ let uniqueId = yield event;
+ is(id, uniqueId, `Correct row was selected after pressing ${key}`);
+
+ ok(node.classList.contains("theme-selected"), "row has selected class");
+
+ let nodes = doc.querySelectorAll(".theme-selected");
+ for (let i = 0; i < nodes.length; i++) {
+ is(nodes[i].getAttribute("data-id"), id,
+ "Correct cell selected in all columns");
+ }
+}
diff --git a/devtools/client/shared/test/browser_tableWidget_mouse_interaction.js b/devtools/client/shared/test/browser_tableWidget_mouse_interaction.js
new file mode 100644
index 000000000..4d7de94e3
--- /dev/null
+++ b/devtools/client/shared/test/browser_tableWidget_mouse_interaction.js
@@ -0,0 +1,317 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that mosue interaction works fine with the table widget
+
+"use strict";
+
+const TEST_URI = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+
+ // Uncomment these lines to help with visual debugging. When uncommented they
+ // dump a couple of thousand errors in the log (bug 1258285)
+ // "<?xml-stylesheet href='chrome://devtools/skin/light-theme.css'?>" +
+ // "<?xml-stylesheet href='chrome://devtools/skin/widgets.css'?>" +
+
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Table Widget' width='600' height='500'>" +
+ "<box flex='1' class='theme-light'/></window>";
+const TEST_OPT = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
+
+var doc, table;
+
+function test() {
+ waitForExplicitFinish();
+ let win = Services.ww.openWindow(null, TEST_URI, "_blank", TEST_OPT, null);
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ waitForFocus(function () {
+ doc = win.document;
+ table = new TableWidget(doc.querySelector("box"), {
+ initialColumns: {
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ },
+ uniqueId: "col1",
+ emptyText: "This is dummy empty text",
+ highlightUpdated: true,
+ removableColumns: true,
+ wrapTextInElements: true,
+ });
+ startTests();
+ });
+ });
+}
+
+function endTests() {
+ table.destroy();
+ doc.defaultView.close();
+ doc = table = null;
+ finish();
+}
+
+var startTests = Task.async(function* () {
+ populateTable();
+ yield testMouseInteraction();
+ endTests();
+});
+
+function populateTable() {
+ table.push({
+ col1: "id1",
+ col2: "value10",
+ col3: "value20",
+ col4: "value30"
+ });
+ table.push({
+ col1: "id2",
+ col2: "value14",
+ col3: "value29",
+ col4: "value32"
+ });
+ table.push({
+ col1: "id3",
+ col2: "value17",
+ col3: "value21",
+ col4: "value31",
+ extraData: "foobar",
+ extraData2: 42
+ });
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id5",
+ col2: "value19",
+ col3: "value26",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ });
+ table.push({
+ col1: "id8",
+ col2: "value11",
+ col3: "value27",
+ col4: "value34"
+ });
+ table.push({
+ col1: "id9",
+ col2: "value11",
+ col3: "value23",
+ col4: "value38"
+ });
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node, button = 0) {
+ if (button == 0) {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {},
+ doc.defaultView));
+ } else {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {
+ button: button,
+ type: "contextmenu"
+ }, doc.defaultView));
+ }
+}
+
+/**
+ * Tests if clicking the table items does the expected behavior
+ */
+var testMouseInteraction = Task.async(function* () {
+ info("Testing mouse interaction with the table");
+ ok(!table.selectedRow, "Nothing should be selected beforehand");
+
+ let event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ let firstColumnFirstRowCell = table.tbody.firstChild.firstChild.children[1];
+ info("clicking on the first row");
+ ok(!firstColumnFirstRowCell.classList.contains("theme-selected"),
+ "Node should not have selected class before clicking");
+ click(firstColumnFirstRowCell);
+ let id = yield event;
+ ok(firstColumnFirstRowCell.classList.contains("theme-selected"),
+ "Node has selected class after click");
+ is(id, "id1", "Correct row was selected");
+
+ info("clicking on second row to select it");
+ event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ let firstColumnSecondRowCell = table.tbody.firstChild.firstChild.children[2];
+ // node should not have selected class
+ ok(!firstColumnSecondRowCell.classList.contains("theme-selected"),
+ "New node should not have selected class before clicking");
+ click(firstColumnSecondRowCell);
+ id = yield event;
+ ok(firstColumnSecondRowCell.classList.contains("theme-selected"),
+ "New node has selected class after clicking");
+ is(id, "id2", "Correct table path is emitted for new node");
+ isnot(firstColumnFirstRowCell, firstColumnSecondRowCell,
+ "Old and new node are different");
+ ok(!firstColumnFirstRowCell.classList.contains("theme-selected"),
+ "Old node should not have selected class after the click on new node");
+
+ info("clicking on the third row cell content to select third row");
+ event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ let firstColumnThirdRowCell = table.tbody.firstChild.firstChild.children[3];
+ let firstColumnThirdRowCellInnerNode = firstColumnThirdRowCell.querySelector("span");
+ // node should not have selected class
+ ok(!firstColumnThirdRowCell.classList.contains("theme-selected"),
+ "New node should not have selected class before clicking");
+ click(firstColumnThirdRowCellInnerNode);
+ id = yield event;
+ ok(firstColumnThirdRowCell.classList.contains("theme-selected"),
+ "New node has selected class after clicking the cell content");
+ is(id, "id3", "Correct table path is emitted for new node");
+
+ // clicking on table header to sort by it
+ event = table.once(TableWidget.EVENTS.COLUMN_SORTED);
+ let node = table.tbody.children[6].firstChild.children[0];
+ info("clicking on the 4th coulmn header to sort the table by it");
+ ok(!node.hasAttribute("sorted"),
+ "Node should not have sorted attribute before clicking");
+ ok(doc.querySelector("[sorted]"),
+ "Although, something else should be sorted on");
+ isnot(doc.querySelector("[sorted]"), node, "Which is not equal to this node");
+ click(node);
+ id = yield event;
+ is(id, "col4", "Correct column was sorted on");
+ ok(node.hasAttribute("sorted"),
+ "Node should now have sorted attribute after clicking");
+ is(doc.querySelectorAll("[sorted]").length, 1,
+ "Now only one column should be sorted on");
+ is(doc.querySelector("[sorted]"), node, "Which should be this column");
+
+ // test context menu opening.
+ // hiding second column
+ // event listener for popupshown
+ info("right click on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ let onPopupShown = once(table.menupopup, "popupshown");
+ click(node, 2);
+ yield onPopupShown;
+
+ is(table.menupopup.querySelectorAll("[disabled]").length, 1,
+ "Only 1 menuitem is disabled");
+ is(table.menupopup.querySelector("[disabled]"),
+ table.menupopup.querySelector("[data-id='col1']"),
+ "Which is the unique column");
+ // popup should be open now
+ // clicking on second column label
+ let onPopupHidden = once(table.menupopup, "popuphidden");
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col2']");
+ info("selecting to hide the second column");
+ ok(!table.tbody.children[2].hasAttribute("hidden"),
+ "Column is not hidden before hiding it");
+ click(node);
+ id = yield event;
+ yield onPopupHidden;
+ is(id, "col2", "Correct column was triggered to be hidden");
+ is(table.tbody.children[2].getAttribute("hidden"), "true",
+ "Column is hidden after hiding it");
+
+ // hiding third column
+ // event listener for popupshown
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ onPopupShown = once(table.menupopup, "popupshown");
+ click(node, 2);
+ yield onPopupShown;
+
+ is(table.menupopup.querySelectorAll("[disabled]").length, 1,
+ "Only 1 menuitem is disabled");
+ // popup should be open now
+ // clicking on second column label
+ onPopupHidden = once(table.menupopup, "popuphidden");
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col3']");
+ info("selecting to hide the second column");
+ ok(!table.tbody.children[4].hasAttribute("hidden"),
+ "Column is not hidden before hiding it");
+ click(node);
+ id = yield event;
+ yield onPopupHidden;
+ is(id, "col3", "Correct column was triggered to be hidden");
+ is(table.tbody.children[4].getAttribute("hidden"), "true",
+ "Column is hidden after hiding it");
+
+ // opening again to see if 2 items are disabled now
+ // event listener for popupshown
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ onPopupShown = once(table.menupopup, "popupshown");
+ click(node, 2);
+ yield onPopupShown;
+
+ is(table.menupopup.querySelectorAll("[disabled]").length, 2,
+ "2 menuitems are disabled now as only 2 columns remain visible");
+ is(table.menupopup.querySelectorAll("[disabled]")[0],
+ table.menupopup.querySelector("[data-id='col1']"),
+ "First is the unique column");
+ is(table.menupopup.querySelectorAll("[disabled]")[1],
+ table.menupopup.querySelector("[data-id='col4']"),
+ "Second is the last column");
+
+ // showing back 2nd column
+ // popup should be open now
+ // clicking on second column label
+ onPopupHidden = once(table.menupopup, "popuphidden");
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col2']");
+ info("selecting to hide the second column");
+ is(table.tbody.children[2].getAttribute("hidden"), "true",
+ "Column is hidden before unhiding it");
+ click(node);
+ id = yield event;
+ yield onPopupHidden;
+ is(id, "col2", "Correct column was triggered to be hidden");
+ ok(!table.tbody.children[2].hasAttribute("hidden"),
+ "Column is not hidden after unhiding it");
+
+ // showing back 3rd column
+ // event listener for popupshown
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ onPopupShown = once(table.menupopup, "popupshown");
+ click(node, 2);
+ yield onPopupShown;
+
+ // popup should be open now
+ // clicking on second column label
+ onPopupHidden = once(table.menupopup, "popuphidden");
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col3']");
+ info("selecting to hide the second column");
+ is(table.tbody.children[4].getAttribute("hidden"), "true",
+ "Column is hidden before unhiding it");
+ click(node);
+ id = yield event;
+ yield onPopupHidden;
+ is(id, "col3", "Correct column was triggered to be hidden");
+ ok(!table.tbody.children[4].hasAttribute("hidden"),
+ "Column is not hidden after unhiding it");
+
+ // reset table state
+ table.clearSelection();
+ table.sortBy("col1");
+});
diff --git a/devtools/client/shared/test/browser_telemetry_button_eyedropper.js b/devtools/client/shared/test/browser_telemetry_button_eyedropper.js
new file mode 100644
index 000000000..76546ce83
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_button_eyedropper.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the eyedropper button");
+ yield testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+ info("Calling the eyedropper button's callback");
+ // We call the button callback directly because we don't need to test the UI here, we're
+ // only concerned about testing the telemetry probe.
+ yield toolbox.getPanel("inspector").showEyeDropper();
+
+ checkResults("_EYEDROPPER_", Telemetry);
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.includes(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ is(value.length, 1, histId + " has one entry");
+
+ let okay = value.every(element => element === true);
+ ok(okay, "All " + histId + " entries are === true");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_button_paintflashing.js b/devtools/client/shared/test/browser_telemetry_button_paintflashing.js
new file mode 100644
index 000000000..dcce8f738
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_button_paintflashing.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_paintflashing.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the paintflashing button");
+ yield testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-paintflashing");
+
+ let button = toolbox.doc.querySelector("#command-button-paintflashing");
+ ok(button, "Captain, we have the button");
+
+ yield* delayedClicks(toolbox, button, 4);
+ checkResults("_PAINTFLASHING_", Telemetry);
+}
+
+function* delayedClicks(toolbox, node, clicks) {
+ for (let i = 0; i < clicks; i++) {
+ yield new Promise(resolve => {
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(() => resolve(), TOOL_DELAY);
+ });
+
+ // this event will fire once the command execution starts and
+ // the output object is created
+ let clicked = toolbox._requisition.commandOutputManager.onOutput.once();
+
+ info("Clicking button " + node.id);
+ node.click();
+
+ let outputEvent = yield clicked;
+ // promise gets resolved once execution finishes and output is ready
+ yield outputEvent.output.promise;
+ }
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.includes(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_button_responsive.js b/devtools/client/shared/test/browser_telemetry_button_responsive.js
new file mode 100644
index 000000000..41e53f0f8
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_button_responsive.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_responsive.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+const { ResponsiveUIManager } = Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the responsivedesign button");
+ yield testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-responsive");
+
+ let button = toolbox.doc.querySelector("#command-button-responsive");
+ ok(button, "Captain, we have the button");
+
+ yield delayedClicks(button, 4);
+
+ checkResults("_RESPONSIVE_", Telemetry);
+}
+
+function waitForToggle() {
+ return new Promise(resolve => {
+ let handler = () => {
+ ResponsiveUIManager.off("on", handler);
+ ResponsiveUIManager.off("off", handler);
+ resolve();
+ };
+ ResponsiveUIManager.on("on", handler);
+ ResponsiveUIManager.on("off", handler);
+ });
+}
+
+var delayedClicks = Task.async(function* (node, clicks) {
+ for (let i = 0; i < clicks; i++) {
+ info("Clicking button " + node.id);
+ let toggled = waitForToggle();
+ node.click();
+ yield toggled;
+ // See TOOL_DELAY for why we need setTimeout here
+ yield DevToolsUtils.waitForTime(TOOL_DELAY);
+ }
+});
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.includes(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_button_scratchpad.js b/devtools/client/shared/test/browser_telemetry_button_scratchpad.js
new file mode 100644
index 000000000..e191bb257
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_button_scratchpad.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_scratchpad.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ let onAllWindowsOpened = trackScratchpadWindows();
+
+ info("testing the scratchpad button");
+ yield testButton(toolbox, Telemetry);
+ yield onAllWindowsOpened;
+
+ checkResults("_SCRATCHPAD_", Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function trackScratchpadWindows() {
+ info("register the window observer to track when scratchpad windows open");
+
+ let numScratchpads = 0;
+
+ return new Promise(resolve => {
+ Services.ww.registerNotification(function observer(subject, topic) {
+ if (topic == "domwindowopened") {
+ let win = subject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ if (win.Scratchpad) {
+ win.Scratchpad.addObserver({
+ onReady: function () {
+ win.Scratchpad.removeObserver(this);
+ numScratchpads++;
+ win.close();
+
+ info("another scratchpad was opened and closed, " +
+ `count is now ${numScratchpads}`);
+
+ if (numScratchpads === 4) {
+ Services.ww.unregisterNotification(observer);
+ info("4 scratchpads have been opened and closed, checking results");
+ resolve();
+ }
+ },
+ });
+ }
+ }, false);
+ }
+ });
+ });
+}
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-scratchpad");
+ let button = toolbox.doc.querySelector("#command-button-scratchpad");
+ ok(button, "Captain, we have the button");
+
+ yield delayedClicks(button, 4);
+}
+
+function delayedClicks(node, clicks) {
+ return new Promise(resolve => {
+ let clicked = 0;
+
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(function delayedClick() {
+ info("Clicking button " + node.id);
+ node.click();
+ clicked++;
+
+ if (clicked >= clicks) {
+ resolve(node);
+ } else {
+ setTimeout(delayedClick, TOOL_DELAY);
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.includes(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_sidebar.js b/devtools/client/shared/test/browser_telemetry_sidebar.js
new file mode 100644
index 000000000..8a8f35578
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_sidebar.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_sidebar.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ yield testSidebar(toolbox);
+ checkResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testSidebar(toolbox) {
+ info("Testing sidebar");
+
+ let inspector = toolbox.getCurrentPanel();
+ let sidebarTools = ["ruleview", "computedview", "fontinspector",
+ "animationinspector"];
+
+ // Concatenate the array with itself so that we can open each tool twice.
+ sidebarTools.push.apply(sidebarTools, sidebarTools);
+
+ return new Promise(resolve => {
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(function selectSidebarTab() {
+ let tool = sidebarTools.pop();
+ if (tool) {
+ inspector.sidebar.select(tool);
+ setTimeout(function () {
+ setTimeout(selectSidebarTab, TOOL_DELAY);
+ }, TOOL_DELAY);
+ } else {
+ resolve();
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults(Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Object.entries(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_")) {
+ // Inspector stats are tested in browser_telemetry_toolboxtabs.js so we
+ // skip them here because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId === "DEVTOOLS_TOOLBOX_OPENED_COUNT") {
+ is(value.length, 1, histId + " has only one entry");
+ } else if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/devtools/client/shared/test/browser_telemetry_toolbox.js b/devtools/client/shared/test/browser_telemetry_toolbox.js
new file mode 100644
index 000000000..85328cf14
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolbox.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolbox.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(3, TOOL_DELAY, "inspector");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js
new file mode 100644
index 000000000..81ab9470c
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_canvasdebugger.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ info("Activate the canvasdebugger");
+ let originalPref = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled");
+ Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", true);
+
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "canvasdebugger");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activate the canvasdebugger");
+ Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", originalPref);
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js
new file mode 100644
index 000000000..a50c8d203
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_inspector.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_inspector.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "inspector");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
new file mode 100644
index 000000000..ba1c26643
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_jsdebugger.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "jsdebugger");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
new file mode 100644
index 000000000..0a5dfb048
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_jsprofiler.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "performance");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
new file mode 100644
index 000000000..6d5292b11
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_netmonitor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "netmonitor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js
new file mode 100644
index 000000000..26b3bf77c
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_options.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_options.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "options");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js
new file mode 100644
index 000000000..5cf1eb0a6
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+ "Error: Shader Editor is still waiting for a WebGL context to be created.");
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_shadereditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+const TOOL_PREF = "devtools.shadereditor.enabled";
+
+add_task(function* () {
+ info("Active the sharer editor");
+ let originalPref = Services.prefs.getBoolPref(TOOL_PREF);
+ Services.prefs.setBoolPref(TOOL_PREF, true);
+
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "shadereditor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activate the sharer editor");
+ Services.prefs.setBoolPref(TOOL_PREF, originalPref);
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js
new file mode 100644
index 000000000..838b06fcb
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_storage.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_storage.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 1000;
+
+add_task(function* () {
+ info("Activating the storage inspector");
+ Services.prefs.setBoolPref("devtools.storage.enabled", true);
+
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "storage");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activating the storage inspector");
+ Services.prefs.clearUserPref("devtools.storage.enabled");
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
new file mode 100644
index 000000000..cdd9e3fb3
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_styleeditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "styleeditor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js
new file mode 100644
index 000000000..a75ebad1d
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_webaudioeditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ info("Activating the webaudioeditor");
+ let originalPref = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled");
+ Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", true);
+
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "webaudioeditor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activating the webaudioeditor");
+ Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", originalPref);
+});
diff --git a/devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js b/devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js
new file mode 100644
index 000000000..4e15dbf4b
--- /dev/null
+++ b/devtools/client/shared/test/browser_telemetry_toolboxtabs_webconsole.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_styleeditor_webconsole.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "webconsole");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/shared/test/browser_templater_basic.html b/devtools/client/shared/test/browser_templater_basic.html
new file mode 100644
index 000000000..473c731f3
--- /dev/null
+++ b/devtools/client/shared/test/browser_templater_basic.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+<head>
+ <title>DOM Template Tests</title>
+</head>
+<body>
+
+</body>
+</html>
+
diff --git a/devtools/client/shared/test/browser_templater_basic.js b/devtools/client/shared/test/browser_templater_basic.js
new file mode 100644
index 000000000..256900cf5
--- /dev/null
+++ b/devtools/client/shared/test/browser_templater_basic.js
@@ -0,0 +1,286 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the DOM Template engine works properly
+
+/*
+ * These tests run both in Mozilla/Mochitest and plain browsers (as does
+ * domtemplate)
+ * We should endevour to keep the source in sync.
+ */
+
+const {template} = require("devtools/shared/gcli/templater");
+
+const TEST_URI = TEST_URI_ROOT + "browser_templater_basic.html";
+
+var test = Task.async(function* () {
+ yield addTab("about:blank");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Starting DOM Templater Tests");
+ runTest(0, host, doc);
+});
+
+function runTest(index, host, doc) {
+ let options = tests[index] = tests[index]();
+ let holder = doc.createElement("div");
+ holder.id = options.name;
+ let body = doc.body;
+ body.appendChild(holder);
+ holder.innerHTML = options.template;
+
+ info("Running " + options.name);
+ template(holder, options.data, options.options);
+
+ if (typeof options.result == "string") {
+ is(holder.innerHTML, options.result, options.name);
+ } else {
+ ok(holder.innerHTML.match(options.result) != null,
+ options.name + " result='" + holder.innerHTML + "'");
+ }
+
+ if (options.also) {
+ options.also(options);
+ }
+
+ function runNextTest() {
+ index++;
+ if (index < tests.length) {
+ runTest(index, host, doc);
+ } else {
+ finished(host);
+ }
+ }
+
+ if (options.later) {
+ let ais = is.bind(this);
+
+ function createTester(testHolder, testOptions) {
+ return () => {
+ ais(testHolder.innerHTML, testOptions.later, testOptions.name + " later");
+ runNextTest();
+ };
+ }
+
+ executeSoon(createTester(holder, options));
+ } else {
+ runNextTest();
+ }
+}
+
+function finished(host) {
+ host.destroy();
+ gBrowser.removeCurrentTab();
+ info("Finishing DOM Templater Tests");
+ tests = null;
+ finish();
+}
+
+/**
+ * Why have an array of functions that return data rather than just an array
+ * of the data itself? Some of these tests contain calls to delayReply() which
+ * sets up async processing using executeSoon(). Since the execution of these
+ * tests is asynchronous, the delayed reply will probably arrive before the
+ * test is executed, making the test be synchronous. So we wrap the data in a
+ * function so we only set it up just before we use it.
+ */
+var tests = [
+ () => ({
+ name: "simpleNesting",
+ template: '<div id="ex1">${nested.value}</div>',
+ data: { nested: { value: "pass 1" } },
+ result: '<div id="ex1">pass 1</div>'
+ }),
+
+ () => ({
+ name: "returnDom",
+ template: '<div id="ex2">${__element.ownerDocument.createTextNode(\'pass 2\')}</div>',
+ options: { allowEval: true },
+ data: {},
+ result: '<div id="ex2">pass 2</div>'
+ }),
+
+ () => ({
+ name: "srcChange",
+ template: '<img _src="${fred}" id="ex3">',
+ data: { fred: "green.png" },
+ result: /<img( id="ex3")? src="green.png"( id="ex3")?>/
+ }),
+
+ () => ({
+ name: "ifTrue",
+ template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+ options: { allowEval: true },
+ data: { name: "fred" },
+ result: "<p>hello fred</p>"
+ }),
+
+ () => ({
+ name: "ifFalse",
+ template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+ options: { allowEval: true },
+ data: { name: "jim" },
+ result: ""
+ }),
+
+ () => ({
+ name: "simpleLoop",
+ template: '<p foreach="index in ${[ 1, 2, 3 ]}">${index}</p>',
+ options: { allowEval: true },
+ data: {},
+ result: "<p>1</p><p>2</p><p>3</p>"
+ }),
+
+ () => ({
+ name: "loopElement",
+ template: '<loop foreach="i in ${array}">${i}</loop>',
+ data: { array: [ 1, 2, 3 ] },
+ result: "123"
+ }),
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ // Bug 692031: DOMTemplate async loops do not drop the loop element
+ () => ({
+ name: "asyncLoopElement",
+ template: '<loop foreach="i in ${array}">${i}</loop>',
+ data: { array: delayReply([1, 2, 3]) },
+ result: "<span></span>",
+ later: "123"
+ }),
+
+ () => ({
+ name: "saveElement",
+ template: '<p save="${element}">${name}</p>',
+ data: { name: "pass 8" },
+ result: "<p>pass 8</p>",
+ also: function (options) {
+ ok(options.data.element.innerHTML, "pass 9", "saveElement saved");
+ delete options.data.element;
+ }
+ }),
+
+ () => ({
+ name: "useElement",
+ template: '<p id="pass9">${adjust(__element)}</p>',
+ options: { allowEval: true },
+ data: {
+ adjust: function (element) {
+ is("pass9", element.id, "useElement adjust");
+ return "pass 9b";
+ }
+ },
+ result: '<p id="pass9">pass 9b</p>'
+ }),
+
+ () => ({
+ name: "asyncInline",
+ template: "${delayed}",
+ data: { delayed: delayReply("inline") },
+ result: "<span></span>",
+ later: "inline"
+ }),
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ () => ({
+ name: "asyncArray",
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: { delayed: delayReply([1, 2, 3]) },
+ result: "<span></span>",
+ later: "<p>1</p><p>2</p><p>3</p>"
+ }),
+
+ () => ({
+ name: "asyncMember",
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: { delayed: [delayReply(4), delayReply(5), delayReply(6)] },
+ result: "<span></span><span></span><span></span>",
+ later: "<p>4</p><p>5</p><p>6</p>"
+ }),
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ () => ({
+ name: "asyncBoth",
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: {
+ delayed: delayReply([
+ delayReply(4),
+ delayReply(5),
+ delayReply(6)
+ ])
+ },
+ result: "<span></span>",
+ later: "<p>4</p><p>5</p><p>6</p>"
+ }),
+
+ // Bug 701762: DOMTemplate fails when ${foo()} returns undefined
+ () => ({
+ name: "functionReturningUndefiend",
+ template: "<p>${foo()}</p>",
+ options: { allowEval: true },
+ data: {
+ foo: function () {}
+ },
+ result: "<p>undefined</p>"
+ }),
+
+ // Bug 702642: DOMTemplate is relatively slow when evaluating JS ${}
+ () => ({
+ name: "propertySimple",
+ template: "<p>${a.b.c}</p>",
+ data: { a: { b: { c: "hello" } } },
+ result: "<p>hello</p>"
+ }),
+
+ () => ({
+ name: "propertyPass",
+ template: "<p>${Math.max(1, 2)}</p>",
+ options: { allowEval: true },
+ result: "<p>2</p>"
+ }),
+
+ () => ({
+ name: "propertyFail",
+ template: "<p>${Math.max(1, 2)}</p>",
+ result: "<p>${Math.max(1, 2)}</p>"
+ }),
+
+ // Bug 723431: DOMTemplate should allow customisation of display of
+ // null/undefined values
+ () => ({
+ name: "propertyUndefAttrFull",
+ template: "<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>",
+ data: { nullvar: null, undefinedvar1: undefined },
+ result: "<p>null|undefined|undefined</p>"
+ }),
+
+ () => ({
+ name: "propertyUndefAttrBlank",
+ template: "<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>",
+ data: { nullvar: null, undefinedvar1: undefined },
+ options: { blankNullUndefined: true },
+ result: "<p>||</p>"
+ }),
+
+ /* eslint-disable max-len */
+ () => ({
+ name: "propertyUndefAttrFull",
+ template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ result: '<div><p value="null"></p><p value="undefined"></p><p value="undefined"></p></div>'
+ }),
+
+ () => ({
+ name: "propertyUndefAttrBlank",
+ template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ options: { blankNullUndefined: true },
+ result: '<div><p value=""></p><p value=""></p><p value=""></p></div>'
+ })
+ /* eslint-enable max-len */
+];
+
+function delayReply(data) {
+ return new Promise(resolve => resolve(data));
+}
diff --git a/devtools/client/shared/test/browser_theme.js b/devtools/client/shared/test/browser_theme.js
new file mode 100644
index 000000000..174e5aeec
--- /dev/null
+++ b/devtools/client/shared/test/browser_theme.js
@@ -0,0 +1,98 @@
+/* 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";
+
+// Tests that theme utilities work
+
+const {getColor, getTheme, setTheme} = require("devtools/client/shared/theme");
+
+add_task(function* () {
+ testGetTheme();
+ testSetTheme();
+ testGetColor();
+ testColorExistence();
+});
+
+function testGetTheme() {
+ let originalTheme = getTheme();
+ ok(originalTheme, "has some theme to start with.");
+ Services.prefs.setCharPref("devtools.theme", "light");
+ is(getTheme(), "light", "getTheme() correctly returns light theme");
+ Services.prefs.setCharPref("devtools.theme", "dark");
+ is(getTheme(), "dark", "getTheme() correctly returns dark theme");
+ Services.prefs.setCharPref("devtools.theme", "firebug");
+ is(getTheme(), "firebug", "getTheme() correctly returns firebug theme");
+ Services.prefs.setCharPref("devtools.theme", "unknown");
+ is(getTheme(), "unknown", "getTheme() correctly returns an unknown theme");
+ Services.prefs.setCharPref("devtools.theme", originalTheme);
+}
+
+function testSetTheme() {
+ let originalTheme = getTheme();
+ gDevTools.once("pref-changed", (_, { pref, oldValue, newValue }) => {
+ is(pref, "devtools.theme",
+ "The 'pref-changed' event triggered by setTheme has correct pref.");
+ is(oldValue, originalTheme,
+ "The 'pref-changed' event triggered by setTheme has correct oldValue.");
+ is(newValue, "dark",
+ "The 'pref-changed' event triggered by setTheme has correct newValue.");
+ });
+ setTheme("dark");
+ is(Services.prefs.getCharPref("devtools.theme"), "dark",
+ "setTheme() correctly sets dark theme.");
+ setTheme("light");
+ is(Services.prefs.getCharPref("devtools.theme"), "light",
+ "setTheme() correctly sets light theme.");
+ setTheme("firebug");
+ is(Services.prefs.getCharPref("devtools.theme"), "firebug",
+ "setTheme() correctly sets firebug theme.");
+ setTheme("unknown");
+ is(Services.prefs.getCharPref("devtools.theme"), "unknown",
+ "setTheme() correctly sets an unknown theme.");
+ Services.prefs.setCharPref("devtools.theme", originalTheme);
+}
+
+function testGetColor() {
+ let BLUE_DARK = "#46afe3";
+ let BLUE_LIGHT = "#0088cc";
+ let BLUE_FIREBUG = "#3455db";
+ let originalTheme = getTheme();
+
+ setTheme("dark");
+ is(getColor("highlight-blue"), BLUE_DARK, "correctly gets color for enabled theme.");
+ setTheme("light");
+ is(getColor("highlight-blue"), BLUE_LIGHT, "correctly gets color for enabled theme.");
+ setTheme("firebug");
+ is(getColor("highlight-blue"), BLUE_FIREBUG, "correctly gets color for enabled theme.");
+ setTheme("metal");
+ is(getColor("highlight-blue"), BLUE_LIGHT,
+ "correctly uses light for default theme if enabled theme not found");
+
+ is(getColor("highlight-blue", "dark"), BLUE_DARK,
+ "if provided and found, uses the provided theme.");
+ is(getColor("highlight-blue", "firebug"), BLUE_FIREBUG,
+ "if provided and found, uses the provided theme.");
+ is(getColor("highlight-blue", "metal"), BLUE_LIGHT,
+ "if provided and not found, defaults to light theme.");
+ is(getColor("somecomponents"), null, "if a type cannot be found, should return null.");
+
+ setTheme(originalTheme);
+}
+
+function testColorExistence() {
+ const vars = ["body-background", "sidebar-background", "contrast-background",
+ "tab-toolbar-background", "toolbar-background", "selection-background",
+ "selection-color", "selection-background-semitransparent", "splitter-color", "comment",
+ "body-color", "body-color-alt", "content-color1", "content-color2", "content-color3",
+ "highlight-green", "highlight-blue", "highlight-bluegrey", "highlight-purple",
+ "highlight-lightorange", "highlight-orange", "highlight-red", "highlight-pink"
+ ];
+
+ for (let type of vars) {
+ ok(getColor(type, "light"), `${type} is a valid color in light theme`);
+ ok(getColor(type, "dark"), `${type} is a valid color in light theme`);
+ ok(getColor(type, "firebug"), `${type} is a valid color in light theme`);
+ }
+}
diff --git a/devtools/client/shared/test/browser_theme_switching.js b/devtools/client/shared/test/browser_theme_switching.js
new file mode 100644
index 000000000..392462a67
--- /dev/null
+++ b/devtools/client/shared/test/browser_theme_switching.js
@@ -0,0 +1,53 @@
+/* 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";
+
+add_task(function* () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target);
+ let doc = toolbox.doc;
+ let root = doc.documentElement;
+
+ let platform = root.getAttribute("platform");
+ let expectedPlatform = getPlatform();
+ is(platform, expectedPlatform, ":root[platform] is correct");
+
+ let theme = Services.prefs.getCharPref("devtools.theme");
+ let className = "theme-" + theme;
+ ok(root.classList.contains(className),
+ ":root has " + className + " class (current theme)");
+
+ // Convert the xpath result into an array of strings
+ // like `href="{URL}" type="text/css"`
+ let sheetsIterator = doc.evaluate("processing-instruction('xml-stylesheet')",
+ doc, null, XPathResult.ANY_TYPE, null);
+ let sheetsInDOM = [];
+
+ /* eslint-disable no-cond-assign */
+ let sheet;
+ while (sheet = sheetsIterator.iterateNext()) {
+ sheetsInDOM.push(sheet.data);
+ }
+ /* eslint-enable no-cond-assign */
+
+ let sheetsFromTheme = gDevTools.getThemeDefinition(theme).stylesheets;
+ info("Checking for existence of " + sheetsInDOM.length + " sheets");
+ for (let themeSheet of sheetsFromTheme) {
+ ok(sheetsInDOM.some(s => s.includes(themeSheet)),
+ "There is a stylesheet for " + themeSheet);
+ }
+
+ yield toolbox.destroy();
+});
+
+function getPlatform() {
+ let {OS} = Services.appinfo;
+ if (OS == "WINNT") {
+ return "win";
+ } else if (OS == "Darwin") {
+ return "mac";
+ }
+ return "linux";
+}
diff --git a/devtools/client/shared/test/browser_toolbar_basic.html b/devtools/client/shared/test/browser_toolbar_basic.html
new file mode 100644
index 000000000..2ea3773b0
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_basic.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Developer Toolbar Tests</title>
+ <style type="text/css">
+ #single { color: red; }
+ </style>
+ <script type="text/javascript">
+ /* eslint-disable */
+ var a = 1;
+ </script>
+</head>
+<body>
+
+<p id=single>
+1
+</p>
+
+<p class=twin>
+2a
+</p>
+
+<p class=twin>
+2b
+</p>
+
+<style>
+.twin { color: blue; }
+</style>
+<script>
+/* eslint-disable */
+var b = 2;
+</script>
+
+</body>
+</html>
diff --git a/devtools/client/shared/test/browser_toolbar_basic.js b/devtools/client/shared/test/browser_toolbar_basic.js
new file mode 100644
index 000000000..12da27ab1
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_basic.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the developer toolbar works properly
+
+const TEST_URI = TEST_URI_ROOT + "browser_toolbar_basic.html";
+
+add_task(function* () {
+ info("Starting browser_toolbar_basic.js");
+ yield addTab(TEST_URI);
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in to start");
+
+ let shown = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield shown;
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in checkOpen");
+
+ let close = document.getElementById("developer-toolbar-closebutton");
+ ok(close, "Close button exists");
+
+ let toggleToolbox =
+ document.getElementById("menu_devToolbox");
+ ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.showToolbox(target, "inspector");
+ ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
+
+ yield addTab("about:blank");
+ info("Opened a new tab");
+
+ ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
+
+ gBrowser.removeCurrentTab();
+
+ let hidden = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield hidden;
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in hidden");
+
+ shown = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield shown;
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in after open");
+
+ ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
+
+ hidden = oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE);
+ document.getElementById("developer-toolbar-closebutton").doCommand();
+ yield hidden;
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible after re-close");
+});
+
+function isChecked(b) {
+ return b.getAttribute("checked") == "true";
+}
diff --git a/devtools/client/shared/test/browser_toolbar_tooltip.js b/devtools/client/shared/test/browser_toolbar_tooltip.js
new file mode 100644
index 000000000..bc09f705c
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_tooltip.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the developer toolbar works properly
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+ "Protocol error (unknownError): Error: Got an invalid root window in DocumentWalker"
+);
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>Tooltip Tests</p>";
+const PREF_DEVTOOLS_THEME = "devtools.theme";
+
+registerCleanupFunction(() => {
+ // Set preferences back to their original values
+ Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
+});
+
+add_task(function* showToolbar() {
+ yield addTab(TEST_URI);
+
+ info("Starting browser_toolbar_tooltip.js");
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in runTest");
+
+ let showPromise = observeOnce(DeveloperToolbar.NOTIFICATIONS.SHOW);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield showPromise;
+});
+
+add_task(function* testDimensions() {
+ let tooltipPanel = DeveloperToolbar.tooltipPanel;
+
+ DeveloperToolbar.focusManager.helpRequest();
+ yield DeveloperToolbar.inputter.setInput("help help");
+
+ DeveloperToolbar.inputter.setCursor({ start: "help help".length });
+ is(tooltipPanel._dimensions.start, "help ".length,
+ "search param start, when cursor at end");
+ ok(getLeftMargin() > 30, "tooltip offset, when cursor at end");
+
+ DeveloperToolbar.inputter.setCursor({ start: "help".length });
+ is(tooltipPanel._dimensions.start, 0,
+ "search param start, when cursor at end of command");
+ ok(getLeftMargin() > 9, "tooltip offset, when cursor at end of command");
+
+ DeveloperToolbar.inputter.setCursor({ start: "help help".length - 1 });
+ is(tooltipPanel._dimensions.start, "help ".length,
+ "search param start, when cursor at penultimate position");
+ ok(getLeftMargin() > 30, "tooltip offset, when cursor at penultimate position");
+
+ DeveloperToolbar.inputter.setCursor({ start: 0 });
+ is(tooltipPanel._dimensions.start, 0,
+ "search param start, when cursor at start");
+ ok(getLeftMargin() > 9, "tooltip offset, when cursor at start");
+});
+
+add_task(function* testThemes() {
+ let tooltipPanel = DeveloperToolbar.tooltipPanel;
+ ok(tooltipPanel.document, "Tooltip panel is initialized");
+
+ Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
+
+ yield DeveloperToolbar.inputter.setInput("");
+ yield DeveloperToolbar.inputter.setInput("help help");
+ is(tooltipPanel.document.documentElement.getAttribute("devtoolstheme"),
+ "dark", "Tooltip panel has correct theme");
+
+ Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
+
+ yield DeveloperToolbar.inputter.setInput("");
+ yield DeveloperToolbar.inputter.setInput("help help");
+ is(tooltipPanel.document.documentElement.getAttribute("devtoolstheme"),
+ "light", "Tooltip panel has correct theme");
+});
+
+add_task(function* hideToolbar() {
+ info("Ending browser_toolbar_tooltip.js");
+ yield DeveloperToolbar.inputter.setInput("");
+
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in hideToolbar");
+
+ info("Hide toolbar");
+ let hidePromise = observeOnce(DeveloperToolbar.NOTIFICATIONS.HIDE);
+ document.getElementById("menu_devToolbar").doCommand();
+ yield hidePromise;
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in hideToolbar");
+
+ info("Done test");
+});
+
+function getLeftMargin() {
+ let style = DeveloperToolbar.tooltipPanel._panel.style.marginLeft;
+ return parseInt(style.slice(0, -2), 10);
+}
+
+function observeOnce(topic, ownsWeak = false) {
+ return new Promise(function (resolve, reject) {
+ let resolver = function (subject) {
+ Services.obs.removeObserver(resolver, topic);
+ resolve(subject);
+ };
+ Services.obs.addObserver(resolver, topic, ownsWeak);
+ });
+}
diff --git a/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html b/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html
new file mode 100644
index 000000000..d09902af0
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Developer Toolbar Tests - errors count in the Web Console button</title>
+ <script type="text/javascript">
+ "use strict";
+ console.log("foobarBug762996consoleLog");
+ window.onload = function () {
+ window.foobarBug762996load();
+ };
+ window.foobarBug762996a();
+ </script>
+ <script type="text/javascript">
+ window.foobarBug762996b();
+ </script>
+</head>
+<body>
+ <p>Hello world! Test for errors count in the Web Console button (developer
+ toolbar).</p>
+ <p style="color: foobarBug762996css"><button>click me</button></p>
+ <script type="text/javascript;version=1.8">
+ "use strict";
+ let testObj = {};
+ document.querySelector("button").onclick = function() {
+ let test = testObj.fooBug788445 + "warning";
+ window.foobarBug762996click();
+ };
+ </script>
+</body>
+</html>
diff --git a/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js b/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js
new file mode 100644
index 000000000..c61666585
--- /dev/null
+++ b/devtools/client/shared/test/browser_toolbar_webconsole_errors_count.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-cpows-in-tests */
+
+"use strict";
+
+// Tests that the developer toolbar errors count works properly.
+
+// Use the old webconsole since this is directly accessing old DOM, and
+// the error count isn't reset when pressing the clear button in new one
+// See Bug 1304794.
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+function test() {
+ const TEST_URI = TEST_URI_ROOT + "browser_toolbar_webconsole_errors_count.html";
+
+ let tab1, tab2, webconsole;
+
+ Services.prefs.setBoolPref("javascript.options.strict", true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("javascript.options.strict");
+ });
+
+ ignoreAllUncaughtExceptions();
+ addTab(TEST_URI).then(openToolbar);
+
+ function openToolbar(tab) {
+ tab1 = tab;
+ ignoreAllUncaughtExceptions(false);
+
+ expectUncaughtException();
+
+ if (!DeveloperToolbar.visible) {
+ DeveloperToolbar.show(true).then(onOpenToolbar);
+ } else {
+ onOpenToolbar();
+ }
+ }
+
+ function onOpenToolbar() {
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible");
+ webconsole = document.getElementById("developer-toolbar-toolbox-button");
+
+ waitForButtonUpdate({
+ name: "web console button shows page errors",
+ errors: 3,
+ warnings: 0,
+ callback: addErrors,
+ });
+ }
+
+ function addErrors() {
+ expectUncaughtException();
+
+ waitForFocus(function () {
+ let button = content.document.querySelector("button");
+ executeSoon(function () {
+ EventUtils.synthesizeMouse(button, 3, 2, {}, content);
+ });
+ }, content);
+
+ waitForButtonUpdate({
+ name: "button shows one more error after click in page",
+ errors: 4,
+ warnings: 1,
+ callback: () => {
+ ignoreAllUncaughtExceptions();
+ addTab(TEST_URI).then(onOpenSecondTab);
+ },
+ });
+ }
+
+ function onOpenSecondTab(tab) {
+ tab2 = tab;
+
+ ignoreAllUncaughtExceptions(false);
+ expectUncaughtException();
+
+ waitForButtonUpdate({
+ name: "button shows correct number of errors after new tab is open",
+ errors: 3,
+ warnings: 0,
+ callback: switchToTab1,
+ });
+ }
+
+ function switchToTab1() {
+ gBrowser.selectedTab = tab1;
+ waitForButtonUpdate({
+ name: "button shows the page errors from tab 1",
+ errors: 4,
+ warnings: 1,
+ callback: openWebConsole.bind(null, tab1, onWebConsoleOpen),
+ });
+ }
+
+ function onWebConsoleOpen(hud) {
+ dump("lolz!!\n");
+ waitForValue({
+ name: "web console shows the page errors",
+ validator: function () {
+ let selector = ".message[category=exception][severity=error]";
+ return hud.outputNode.querySelectorAll(selector).length;
+ },
+ value: 4,
+ success: checkConsoleOutput.bind(null, hud),
+ failure: () => {
+ finish();
+ },
+ });
+ }
+
+ function checkConsoleOutput(hud) {
+ let msgs = ["foobarBug762996a", "foobarBug762996b", "foobarBug762996load",
+ "foobarBug762996click", "foobarBug762996consoleLog",
+ "foobarBug762996css", "fooBug788445"];
+ msgs.forEach(function (msg) {
+ isnot(hud.outputNode.textContent.indexOf(msg), -1,
+ msg + " found in the Web Console output");
+ });
+
+ hud.jsterm.clearOutput();
+
+ is(hud.outputNode.textContent.indexOf("foobarBug762996color"), -1,
+ "clearOutput() worked");
+
+ expectUncaughtException();
+ let button = content.document.querySelector("button");
+ EventUtils.synthesizeMouse(button, 2, 2, {}, content);
+
+ waitForButtonUpdate({
+ name: "button shows one more error after another click in page",
+ errors: 5,
+ // warnings are not repeated by the js engine
+ warnings: 1,
+ callback: () => waitForValue(waitForNewError),
+ });
+
+ let waitForNewError = {
+ name: "the Web Console displays the new error",
+ validator: function () {
+ return hud.outputNode.textContent.indexOf("foobarBug762996click") > -1;
+ },
+ success: doClearConsoleButton.bind(null, hud),
+ failure: finish,
+ };
+ }
+
+ function doClearConsoleButton(hud) {
+ let clearButton = hud.ui.rootElement
+ .querySelector(".webconsole-clear-console-button");
+ EventUtils.synthesizeMouse(clearButton, 2, 2, {}, hud.iframeWindow);
+
+ is(hud.outputNode.textContent.indexOf("foobarBug762996click"), -1,
+ "clear console button worked");
+ is(getErrorsCount(), 0, "page errors counter has been reset");
+ let tooltip = getTooltipValues();
+ is(tooltip[1], 0, "page warnings counter has been reset");
+
+ doPageReload(hud);
+ }
+
+ function doPageReload(hud) {
+ tab1.linkedBrowser.addEventListener("load", onReload, true);
+
+ ignoreAllUncaughtExceptions();
+ content.location.reload();
+
+ function onReload() {
+ tab1.linkedBrowser.removeEventListener("load", onReload, true);
+ ignoreAllUncaughtExceptions(false);
+ expectUncaughtException();
+
+ waitForButtonUpdate({
+ name: "the Web Console button count has been reset after page reload",
+ errors: 3,
+ warnings: 0,
+ callback: waitForValue.bind(null, waitForConsoleOutputAfterReload),
+ });
+ }
+
+ let waitForConsoleOutputAfterReload = {
+ name: "the Web Console displays the correct number of errors after reload",
+ validator: function () {
+ let selector = ".message[category=exception][severity=error]";
+ return hud.outputNode.querySelectorAll(selector).length;
+ },
+ value: 3,
+ success: function () {
+ isnot(hud.outputNode.textContent.indexOf("foobarBug762996load"), -1,
+ "foobarBug762996load found in console output after page reload");
+ testEnd();
+ },
+ failure: testEnd,
+ };
+ }
+
+ function testEnd() {
+ document.getElementById("developer-toolbar-closebutton").doCommand();
+ let target1 = TargetFactory.forTab(tab1);
+ gDevTools.closeToolbox(target1).then(() => {
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ finish();
+ });
+ }
+
+ // Utility functions
+
+ function getErrorsCount() {
+ let count = webconsole.getAttribute("error-count");
+ return count ? count : "0";
+ }
+
+ function getTooltipValues() {
+ let matches = webconsole.getAttribute("tooltiptext")
+ .match(/(\d+) errors?, (\d+) warnings?/);
+ return matches ? [matches[1], matches[2]] : [0, 0];
+ }
+
+ function waitForButtonUpdate(options) {
+ function check() {
+ let errors = getErrorsCount();
+ let tooltip = getTooltipValues();
+ let result = errors == options.errors && tooltip[1] == options.warnings;
+ if (result) {
+ ok(true, options.name);
+ is(errors, tooltip[0], "button error-count is the same as in the tooltip");
+
+ // Get out of the toolbar event execution loop.
+ executeSoon(options.callback);
+ }
+ return result;
+ }
+
+ if (!check()) {
+ info("wait for: " + options.name);
+ DeveloperToolbar.on("errors-counter-updated", function onUpdate(event) {
+ if (check()) {
+ DeveloperToolbar.off(event, onUpdate);
+ }
+ });
+ }
+ }
+
+ function openWebConsole(tab, callback) {
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target, "webconsole").then((toolbox) =>
+ callback(toolbox.getCurrentPanel().hud));
+ }
+}
diff --git a/devtools/client/shared/test/browser_treeWidget_basic.js b/devtools/client/shared/test/browser_treeWidget_basic.js
new file mode 100644
index 000000000..b1d7772f7
--- /dev/null
+++ b/devtools/client/shared/test/browser_treeWidget_basic.js
@@ -0,0 +1,267 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the tree widget api works fine
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<head>" +
+ "<link rel='stylesheet' type='text/css' href='chrome://devtools/skin/widg" +
+ "ets.css'></head><body><div></div><span></span></body>";
+const {TreeWidget} = require("devtools/client/shared/widgets/TreeWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let tree = new TreeWidget(doc.querySelector("div"), {
+ defaultType: "store"
+ });
+
+ populateTree(tree, doc);
+ testTreeItemInsertedCorrectly(tree, doc);
+ testAPI(tree, doc);
+ populateUnsortedTree(tree, doc);
+ testUnsortedTreeItemInsertedCorrectly(tree, doc);
+
+ tree.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function populateTree(tree, doc) {
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2-1",
+ label: "Level 2"
+ }, {
+ id: "level3-1",
+ label: "Level 3 - Child 1",
+ type: "dir"
+ }]);
+ tree.add(["level1", "level2-1", {
+ id: "level3-2",
+ label: "Level 3 - Child 2"
+ }]);
+ tree.add(["level1", "level2-1", {
+ id: "level3-3",
+ label: "Level 3 - Child 3"
+ }]);
+ tree.add(["level1", {
+ id: "level2-2",
+ label: "Level 2.1"
+ }, {
+ id: "level3-1",
+ label: "Level 3.1"
+ }]);
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+ tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
+}
+
+/**
+ * Test if the nodes are inserted correctly in the tree.
+ */
+function testTreeItemInsertedCorrectly(tree, doc) {
+ is(tree.root.children.children.length, 2,
+ "Number of top level elements match");
+ is(tree.root.children.firstChild.lastChild.children.length, 3,
+ "Number of first second level elements match");
+ is(tree.root.children.lastChild.lastChild.children.length, 1,
+ "Number of second second level elements match");
+
+ ok(tree.root.items.has("level1"), "Level1 top level element exists");
+ is(tree.root.children.firstChild.dataset.id, JSON.stringify(["level1"]),
+ "Data id of first top level element matches");
+ is(tree.root.children.firstChild.firstChild.textContent, "Level 1",
+ "Text content of first top level element matches");
+
+ ok(tree.root.items.has("level1.1"), "Level1.1 top level element exists");
+ is(tree.root.children.firstChild.nextSibling.dataset.id,
+ JSON.stringify(["level1.1"]),
+ "Data id of second top level element matches");
+ is(tree.root.children.firstChild.nextSibling.firstChild.textContent,
+ "level1.1",
+ "Text content of second top level element matches");
+
+ // Adding a new non text item in the tree.
+ let node = doc.createElement("div");
+ node.textContent = "Foo Bar";
+ node.className = "foo bar";
+ tree.add([{
+ id: "level1.2",
+ node: node,
+ attachment: {
+ foo: "bar"
+ }
+ }]);
+
+ is(tree.root.children.children.length, 3,
+ "Number of top level elements match after update");
+ ok(tree.root.items.has("level1.2"), "New level node got added");
+ ok(tree.attachments.has(JSON.stringify(["level1.2"])),
+ "Attachment is present for newly added node");
+ // The item should be added before level1 and level 1.1 as lexical sorting
+ is(tree.root.children.firstChild.dataset.id, JSON.stringify(["level1.2"]),
+ "Data id of last top level element matches");
+ is(tree.root.children.firstChild.firstChild.firstChild, node,
+ "Newly added node is inserted at the right location");
+}
+
+/**
+ * Populate the unsorted tree.
+ */
+function populateUnsortedTree(tree, doc) {
+ tree.sorted = false;
+
+ tree.add([{ id: "g-1", label: "g-1"}]);
+ tree.add(["g-1", { id: "d-2", label: "d-2.1"}]);
+ tree.add(["g-1", { id: "b-2", label: "b-2.2"}]);
+ tree.add(["g-1", { id: "a-2", label: "a-2.3"}]);
+}
+
+/**
+ * Test if the nodes are inserted correctly in the unsorted tree.
+ */
+function testUnsortedTreeItemInsertedCorrectly(tree, doc) {
+ ok(tree.root.items.has("g-1"), "g-1 top level element exists");
+
+ is(tree.root.children.firstChild.lastChild.children.length, 3,
+ "Number of children for g-1 matches");
+ is(tree.root.children.firstChild.dataset.id, JSON.stringify(["g-1"]),
+ "Data id of g-1 matches");
+ is(tree.root.children.firstChild.firstChild.textContent, "g-1",
+ "Text content of g-1 matches");
+ is(tree.root.children.firstChild.lastChild.firstChild.dataset.id,
+ JSON.stringify(["g-1", "d-2"]),
+ "Data id of d-2 matches");
+ is(tree.root.children.firstChild.lastChild.firstChild.textContent, "d-2.1",
+ "Text content of d-2 matches");
+ is(tree.root.children.firstChild.lastChild.firstChild.nextSibling.textContent,
+ "b-2.2", "Text content of b-2 matches");
+ is(tree.root.children.firstChild.lastChild.lastChild.textContent, "a-2.3",
+ "Text content of a-2 matches");
+}
+
+/**
+ * Tests if the API exposed by TreeWidget works properly
+ */
+function testAPI(tree, doc) {
+ info("Testing TreeWidget API");
+ // Check if selectItem and selectedItem setter works as expected
+ // Nothing should be selected beforehand
+ ok(!doc.querySelector(".theme-selected"), "Nothing is selected");
+ tree.selectItem(["level1"]);
+ let node = doc.querySelector(".theme-selected");
+ ok(!!node, "Something got selected");
+ is(node.parentNode.dataset.id, JSON.stringify(["level1"]),
+ "Correct node selected");
+
+ tree.selectItem(["level1", "level2"]);
+ let node2 = doc.querySelector(".theme-selected");
+ ok(!!node2, "Something is still selected");
+ isnot(node, node2, "Newly selected node is different from previous");
+ is(node2.parentNode.dataset.id, JSON.stringify(["level1", "level2"]),
+ "Correct node selected");
+
+ // test if selectedItem getter works
+ is(tree.selectedItem.length, 2, "Correct length of selected item");
+ is(tree.selectedItem[0], "level1", "Correct selected item");
+ is(tree.selectedItem[1], "level2", "Correct selected item");
+
+ // test if isSelected works
+ ok(tree.isSelected(["level1", "level2"]), "isSelected works");
+
+ tree.selectedItem = ["level1"];
+ let node3 = doc.querySelector(".theme-selected");
+ ok(!!node3, "Something is still selected");
+ isnot(node2, node3, "Newly selected node is different from previous");
+ is(node3, node, "First and third selected nodes should be same");
+ is(node3.parentNode.dataset.id, JSON.stringify(["level1"]),
+ "Correct node selected");
+
+ // test if selectedItem getter works
+ is(tree.selectedItem.length, 1, "Correct length of selected item");
+ is(tree.selectedItem[0], "level1", "Correct selected item");
+
+ // test if clear selection works
+ tree.clearSelection();
+ ok(!doc.querySelector(".theme-selected"),
+ "Nothing selected after clear selection call");
+
+ // test if collapseAll/expandAll work
+ ok(doc.querySelectorAll("[expanded]").length > 0,
+ "Some nodes are expanded");
+ tree.collapseAll();
+ is(doc.querySelectorAll("[expanded]").length, 0,
+ "Nothing is expanded after collapseAll call");
+ tree.expandAll();
+ is(doc.querySelectorAll("[expanded]").length, 13,
+ "All tree items expanded after expandAll call");
+
+ // test if selectNextItem and selectPreviousItem work
+ tree.selectedItem = ["level1", "level2"];
+ ok(tree.isSelected(["level1", "level2"]), "Correct item selected");
+ tree.selectNextItem();
+ ok(tree.isSelected(["level1", "level2", "level3"]),
+ "Correct item selected after selectNextItem call");
+
+ tree.selectNextItem();
+ ok(tree.isSelected(["level1", "level2-1"]),
+ "Correct item selected after second selectNextItem call");
+
+ tree.selectNextItem();
+ ok(tree.isSelected(["level1", "level2-1", "level3-1"]),
+ "Correct item selected after third selectNextItem call");
+
+ tree.selectPreviousItem();
+ ok(tree.isSelected(["level1", "level2-1"]),
+ "Correct item selected after selectPreviousItem call");
+
+ tree.selectPreviousItem();
+ ok(tree.isSelected(["level1", "level2", "level3"]),
+ "Correct item selected after second selectPreviousItem call");
+
+ // test if remove works
+ ok(doc.querySelector("[data-id='" +
+ JSON.stringify(["level1", "level2", "level3"]) + "']"),
+ "level1-level2-level3 item exists before removing");
+ tree.remove(["level1", "level2", "level3"]);
+ ok(!doc.querySelector("[data-id='" +
+ JSON.stringify(["level1", "level2", "level3"]) + "']"),
+ "level1-level2-level3 item does not exist after removing");
+ let level2item = doc.querySelector("[data-id='" +
+ JSON.stringify(["level1", "level2"]) + "'] > .tree-widget-item");
+ ok(level2item.hasAttribute("empty"),
+ "level1-level2 item is marked as empty after removing");
+
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+
+ // test if clearing the tree works
+ is(doc.querySelectorAll("[level='1']").length, 3,
+ "Correct number of top level items before clearing");
+ tree.clear();
+ is(doc.querySelectorAll("[level='1']").length, 0,
+ "No top level item after clearing the tree");
+}
diff --git a/devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js b/devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js
new file mode 100644
index 000000000..9b214fe3f
--- /dev/null
+++ b/devtools/client/shared/test/browser_treeWidget_keyboard_interaction.js
@@ -0,0 +1,228 @@
+/* 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";
+
+// Tests that keyboard interaction works fine with the tree widget
+
+const TEST_URI = "data:text/html;charset=utf-8,<head>" +
+ "<link rel='stylesheet' type='text/css' href='chrome://devtools/skin/widg" +
+ "ets.css'></head><body><div></div><span></span></body>";
+const {TreeWidget} = require("devtools/client/shared/widgets/TreeWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ let tree = new TreeWidget(doc.querySelector("div"), {
+ defaultType: "store"
+ });
+
+ populateTree(tree, doc);
+ yield testKeyboardInteraction(tree, win);
+
+ tree.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function populateTree(tree, doc) {
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2-1",
+ label: "Level 2"
+ }, {
+ id: "level3-1",
+ label: "Level 3 - Child 1",
+ type: "dir"
+ }]);
+ tree.add(["level1", "level2-1", { id: "level3-2", label: "Level 3 - Child 2"}]);
+ tree.add(["level1", "level2-1", { id: "level3-3", label: "Level 3 - Child 3"}]);
+ tree.add(["level1", {
+ id: "level2-2",
+ label: "Level 2.1"
+ }, {
+ id: "level3-1",
+ label: "Level 3.1"
+ }]);
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+ tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
+
+ // Adding a new non text item in the tree.
+ let node = doc.createElement("div");
+ node.textContent = "Foo Bar";
+ node.className = "foo bar";
+ tree.add([{
+ id: "level1.2",
+ node: node,
+ attachment: {
+ foo: "bar"
+ }
+ }]);
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node) {
+ let win = node.ownerDocument.defaultView;
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, win));
+}
+
+/**
+ * Tests if pressing navigation keys on the tree items does the expected behavior
+ */
+function* testKeyboardInteraction(tree, win) {
+ info("Testing keyboard interaction with the tree");
+ let event;
+ let pass = (e, d, a) => event.resolve([e, d, a]);
+
+ info("clicking on first top level item");
+ let node = tree.root.children.firstChild.firstChild;
+ event = defer();
+ tree.once("select", pass);
+ click(node);
+ yield event.promise;
+ node = tree.root.children.firstChild.nextSibling.firstChild;
+ // node should not have selected class
+ ok(!node.classList.contains("theme-selected"), "Node should not have selected class");
+ ok(!node.hasAttribute("expanded"), "Node is not expanded");
+
+ info("Pressing down key to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ let [name, data, attachment] = yield event.promise;
+ is(name, "select", "Select event was fired after pressing down");
+ is(data[0], "level1", "Correct item was selected after pressing down");
+ ok(!attachment, "null attachment was emitted");
+ ok(node.classList.contains("theme-selected"), "Node has selected class");
+ ok(node.hasAttribute("expanded"), "Node is expanded now");
+
+ info("Pressing down key again to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 2, "Correct level item was selected after second down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2", "Correct second level");
+
+ info("Pressing down key again to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 3, "Correct level item was selected after third down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2", "Correct second level");
+ is(data[2], "level3", "Correct third level");
+
+ info("Pressing down key again to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 2, "Correct level item was selected after fourth down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2-1", "Correct second level");
+
+ // pressing left to check expand collapse feature.
+ // This does not emit any event, so listening for keypress
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ // executeSoon so that other listeners on the same method are executed first
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing left key to collapse the item");
+ event = defer();
+ node = tree._selectedLabel;
+ ok(node.hasAttribute("expanded"), "Item is expanded before left keypress");
+ EventUtils.sendKey("LEFT", win);
+ yield event.promise;
+
+ ok(!node.hasAttribute("expanded"), "Item is not expanded after left keypress");
+
+ // pressing left on collapsed item should select the previous item
+
+ info("Pressing left key on collapsed item to select previous");
+ tree.once("select", pass);
+ event = defer();
+ // parent node should have no effect of this keypress
+ node = tree.root.children.firstChild.nextSibling.firstChild;
+ ok(node.hasAttribute("expanded"), "Parent is expanded");
+ EventUtils.sendKey("LEFT", win);
+ [name, data] = yield event.promise;
+ is(data.length, 3, "Correct level item was selected after second left keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2", "Correct second level");
+ is(data[2], "level3", "Correct third level");
+ ok(node.hasAttribute("expanded"), "Parent is still expanded after left keypress");
+
+ // pressing down again
+
+ info("Pressing down key to select next item");
+ event = defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 2, "Correct level item was selected after fifth down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2-1", "Correct second level");
+
+ // collapsing the item to check expand feature.
+
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing left key to collapse the item");
+ event = defer();
+ node = tree._selectedLabel;
+ ok(node.hasAttribute("expanded"), "Item is expanded before left keypress");
+ EventUtils.sendKey("LEFT", win);
+ yield event.promise;
+ ok(!node.hasAttribute("expanded"), "Item is collapsed after left keypress");
+
+ // pressing right should expand this now.
+
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing right key to expend the collapsed item");
+ event = defer();
+ node = tree._selectedLabel;
+ ok(!node.hasAttribute("expanded"), "Item is collapsed before right keypress");
+ EventUtils.sendKey("RIGHT", win);
+ yield event.promise;
+ ok(node.hasAttribute("expanded"), "Item is expanded after right keypress");
+
+ // selecting last item node to test edge navigation case
+
+ tree.selectedItem = ["level1.1", "level2", "level3"];
+ node = tree._selectedLabel;
+ // pressing down again should not change selection
+ event = defer();
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing down key on last item of the tree");
+ EventUtils.sendKey("DOWN", win);
+ yield event.promise;
+
+ ok(tree.isSelected(["level1.1", "level2", "level3"]),
+ "Last item is still selected after pressing down on last item of the tree");
+}
diff --git a/devtools/client/shared/test/browser_treeWidget_mouse_interaction.js b/devtools/client/shared/test/browser_treeWidget_mouse_interaction.js
new file mode 100644
index 000000000..f42fd16ad
--- /dev/null
+++ b/devtools/client/shared/test/browser_treeWidget_mouse_interaction.js
@@ -0,0 +1,135 @@
+/* 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";
+
+// Tests that mouse interaction works fine with tree widget
+
+const TEST_URI = "data:text/html;charset=utf-8,<head>" +
+ "<link rel='stylesheet' type='text/css' href='chrome://devtools/skin/widg" +
+ "ets.css'></head><body><div></div><span></span></body>";
+const {TreeWidget} = require("devtools/client/shared/widgets/TreeWidget");
+
+add_task(function* () {
+ yield addTab("about:blank");
+ let [host,, doc] = yield createHost("bottom", TEST_URI);
+
+ let tree = new TreeWidget(doc.querySelector("div"), {
+ defaultType: "store"
+ });
+
+ populateTree(tree, doc);
+ yield testMouseInteraction(tree);
+
+ tree.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function populateTree(tree, doc) {
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2-1",
+ label: "Level 2"
+ }, {
+ id: "level3-1",
+ label: "Level 3 - Child 1",
+ type: "dir"
+ }]);
+ tree.add(["level1", "level2-1", { id: "level3-2", label: "Level 3 - Child 2"}]);
+ tree.add(["level1", "level2-1", { id: "level3-3", label: "Level 3 - Child 3"}]);
+ tree.add(["level1", {
+ id: "level2-2",
+ label: "Level 2.1"
+ }, {
+ id: "level3-1",
+ label: "Level 3.1"
+ }]);
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+ tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
+
+ // Adding a new non text item in the tree.
+ let node = doc.createElement("div");
+ node.textContent = "Foo Bar";
+ node.className = "foo bar";
+ tree.add([{
+ id: "level1.2",
+ node: node,
+ attachment: {
+ foo: "bar"
+ }
+ }]);
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node) {
+ let win = node.ownerDocument.defaultView;
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, win));
+}
+
+/**
+ * Tests if clicking the tree items does the expected behavior
+ */
+function* testMouseInteraction(tree) {
+ info("Testing mouse interaction with the tree");
+ let event;
+ let pass = (e, d, a) => event.resolve([e, d, a]);
+
+ ok(!tree.selectedItem, "Nothing should be selected beforehand");
+
+ tree.once("select", pass);
+ let node = tree.root.children.firstChild.firstChild;
+ info("clicking on first top level item");
+ event = defer();
+ ok(!node.classList.contains("theme-selected"),
+ "Node should not have selected class before clicking");
+ click(node);
+ let [, data, attachment] = yield event.promise;
+ ok(node.classList.contains("theme-selected"),
+ "Node has selected class after click");
+ is(data[0], "level1.2", "Correct tree path is emitted");
+ ok(attachment && attachment.foo, "Correct attachment is emitted");
+ is(attachment.foo, "bar", "Correct attachment value is emitted");
+
+ info("clicking second top level item with children to check if it expands");
+ let node2 = tree.root.children.firstChild.nextSibling.firstChild;
+ event = defer();
+ // node should not have selected class
+ ok(!node2.classList.contains("theme-selected"),
+ "New node should not have selected class before clicking");
+ ok(!node2.hasAttribute("expanded"), "New node is not expanded before clicking");
+ tree.once("select", pass);
+ click(node2);
+ [, data, attachment] = yield event.promise;
+ ok(node2.classList.contains("theme-selected"),
+ "New node has selected class after clicking");
+ is(data[0], "level1", "Correct tree path is emitted for new node");
+ ok(!attachment, "null attachment should be emitted for new node");
+ ok(node2.hasAttribute("expanded"), "New node expanded after click");
+
+ ok(!node.classList.contains("theme-selected"),
+ "Old node should not have selected class after the click on new node");
+
+ // clicking again should just collapse
+ // this will not emit "select" event
+ event = defer();
+ node2.addEventListener("click", () => {
+ executeSoon(() => event.resolve(null));
+ }, { once: true });
+ click(node2);
+ yield event.promise;
+ ok(!node2.hasAttribute("expanded"), "New node collapsed after click again");
+}
diff --git a/devtools/client/shared/test/doc_options-view.xul b/devtools/client/shared/test/doc_options-view.xul
new file mode 100644
index 000000000..06ae1cbf1
--- /dev/null
+++ b/devtools/client/shared/test/doc_options-view.xul
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<!DOCTYPE window []>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <popupset id="options-popupset">
+ <menupopup id="options-menupopup" position="before_end">
+ <menuitem id="option-autoprettyprint"
+ type="checkbox"
+ data-pref="auto-pretty-print"
+ label="pretty print"/>
+ <menuitem id="option-autoblackbox"
+ type="checkbox"
+ data-pref="auto-black-box"
+ label="black box"/>
+ </menupopup>
+ </popupset>
+ <button id="options-button"
+ popup="options-menupopup"/>
+</window>
diff --git a/devtools/client/shared/test/head.js b/devtools/client/shared/test/head.js
new file mode 100644
index 000000000..fbf0e84af
--- /dev/null
+++ b/devtools/client/shared/test/head.js
@@ -0,0 +1,346 @@
+/* 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+const {DOMHelpers} = Cu.import("resource://devtools/client/shared/DOMHelpers.jsm", {});
+const {Hosts} = require("devtools/client/framework/toolbox-hosts");
+
+const TEST_URI_ROOT = "http://example.com/browser/devtools/client/shared/test/";
+const OPTIONS_VIEW_URL = TEST_URI_ROOT + "doc_options-view.xul";
+
+function catchFail(func) {
+ return function () {
+ try {
+ return func.apply(null, arguments);
+ } catch (ex) {
+ ok(false, ex);
+ console.error(ex);
+ finish();
+ throw ex;
+ }
+ };
+}
+
+/**
+ * Polls a given function waiting for the given value.
+ *
+ * @param object options
+ * Options object with the following properties:
+ * - validator
+ * A validator function that should return the expected value. This is
+ * called every few milliseconds to check if the result is the expected
+ * one. When the returned result is the expected one, then the |success|
+ * function is called and polling stops. If |validator| never returns
+ * the expected value, then polling timeouts after several tries and
+ * a failure is recorded - the given |failure| function is invoked.
+ * - success
+ * A function called when the validator function returns the expected
+ * value.
+ * - failure
+ * A function called if the validator function timeouts - fails to return
+ * the expected value in the given time.
+ * - name
+ * Name of test. This is used to generate the success and failure
+ * messages.
+ * - timeout
+ * Timeout for validator function, in milliseconds. Default is 5000 ms.
+ * - value
+ * The expected value. If this option is omitted then the |validator|
+ * function must return a trueish value.
+ * Each of the provided callback functions will receive two arguments:
+ * the |options| object and the last value returned by |validator|.
+ */
+function waitForValue(options) {
+ let start = Date.now();
+ let timeout = options.timeout || 5000;
+ let lastValue;
+
+ function wait(validatorFn, successFn, failureFn) {
+ if ((Date.now() - start) > timeout) {
+ // Log the failure.
+ ok(false, "Timed out while waiting for: " + options.name);
+ let expected = "value" in options ?
+ "'" + options.value + "'" :
+ "a trueish value";
+ info("timeout info :: got '" + lastValue + "', expected " + expected);
+ failureFn(options, lastValue);
+ return;
+ }
+
+ lastValue = validatorFn(options, lastValue);
+ let successful = "value" in options ?
+ lastValue == options.value :
+ lastValue;
+ if (successful) {
+ ok(true, options.name);
+ successFn(options, lastValue);
+ } else {
+ setTimeout(() => {
+ wait(validatorFn, successFn, failureFn);
+ }, 100);
+ }
+ }
+
+ wait(options.validator, options.success, options.failure);
+}
+
+function oneTimeObserve(name, callback) {
+ return new Promise((resolve) => {
+ let func = function () {
+ Services.obs.removeObserver(func, name);
+ if (callback) {
+ callback();
+ }
+ resolve();
+ };
+ Services.obs.addObserver(func, name, false);
+ });
+}
+
+let createHost =
+Task.async(function* (type = "bottom", src = "data:text/html;charset=utf-8,") {
+ let host = new Hosts[type](gBrowser.selectedTab);
+ let iframe = yield host.create();
+
+ yield new Promise(resolve => {
+ let domHelper = new DOMHelpers(iframe.contentWindow);
+ iframe.setAttribute("src", src);
+ domHelper.onceDOMReady(resolve);
+ });
+
+ return [host, iframe.contentWindow, iframe.contentDocument];
+});
+
+/**
+ * Check the correctness of the data recorded in Telemetry after
+ * loadTelemetryAndRecordLogs was called.
+ */
+function checkTelemetryResults(Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let histId in result) {
+ let value = result[histId];
+
+ if (histId.endsWith("OPENED_COUNT")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function (element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
+
+/**
+ * Open and close the toolbox in the current browser tab, several times, waiting
+ * some amount of time in between.
+ * @param {Number} nbOfTimes
+ * @param {Number} usageTime in milliseconds
+ * @param {String} toolId
+ */
+function* openAndCloseToolbox(nbOfTimes, usageTime, toolId) {
+ for (let i = 0; i < nbOfTimes; i++) {
+ info("Opening toolbox " + (i + 1));
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.showToolbox(target, toolId);
+
+ // We use a timeout to check the toolbox's active time
+ yield new Promise(resolve => setTimeout(resolve, usageTime));
+
+ info("Closing toolbox " + (i + 1));
+ yield gDevTools.closeToolbox(target);
+ }
+}
+
+/**
+ * Synthesize a profile for testing.
+ */
+function synthesizeProfileForTest(samples) {
+ const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+ samples.unshift({
+ time: 0,
+ frames: []
+ });
+
+ let uniqueStacks = new RecordingUtils.UniqueStacks();
+ return RecordingUtils.deflateThread({
+ samples: samples,
+ markers: []
+ }, uniqueStacks);
+}
+
+/**
+ * Waits until a predicate returns true.
+ *
+ * @param function predicate
+ * Invoked once in a while until it returns true.
+ * @param number interval [optional]
+ * How often the predicate is invoked, in milliseconds.
+ */
+function waitUntil(predicate, interval = 10) {
+ if (predicate()) {
+ return Promise.resolve(true);
+ }
+ return new Promise(resolve => {
+ setTimeout(function () {
+ waitUntil(predicate).then(() => resolve(true));
+ }, interval);
+ });
+}
+
+/**
+ * Show the presets list sidebar in the cssfilter widget popup
+ * @param {CSSFilterWidget} widget
+ * @return {Promise}
+ */
+function showFilterPopupPresets(widget) {
+ let onRender = widget.once("render");
+ widget._togglePresets();
+ return onRender;
+}
+
+/**
+ * Show presets list and create a sample preset with the name and value provided
+ * @param {CSSFilterWidget} widget
+ * @param {string} name
+ * @param {string} value
+ * @return {Promise}
+ */
+let showFilterPopupPresetsAndCreatePreset =
+Task.async(function* (widget, name, value) {
+ yield showFilterPopupPresets(widget);
+
+ let onRender = widget.once("render");
+ widget.setCssValue(value);
+ yield onRender;
+
+ let footer = widget.el.querySelector(".presets-list .footer");
+ footer.querySelector("input").value = name;
+
+ onRender = widget.once("render");
+ widget._savePreset({
+ preventDefault: () => {}
+ });
+
+ yield onRender;
+});
+
+/**
+ * Utility function for testing CSS code samples that have been
+ * syntax-highlighted.
+ *
+ * The CSS syntax highlighter emits a collection of DOM nodes that have
+ * CSS classes applied to them. This function checks that those nodes
+ * are what we expect.
+ *
+ * @param {array} expectedNodes
+ * A representation of the nodes we expect to see.
+ * Each node is an object containing two properties:
+ * - type: a string which can be one of:
+ * - text, comment, property-name, property-value
+ * - text: the textContent of the node
+ *
+ * For example, given a string like this:
+ * "<comment> The part we want </comment>\n this: is-the-part-we-want;"
+ *
+ * we would represent the expected output like this:
+ * [{type: "comment", text: "<comment> The part we want </comment>"},
+ * {type: "text", text: "\n"},
+ * {type: "property-name", text: "this"},
+ * {type: "text", text: ":"},
+ * {type: "text", text: " "},
+ * {type: "property-value", text: "is-the-part-we-want"},
+ * {type: "text", text: ";"}];
+ *
+ * @param {Node} parent
+ * The DOM node whose children are the output of the syntax highlighter.
+ */
+function checkCssSyntaxHighlighterOutput(expectedNodes, parent) {
+ /**
+ * The classes applied to the output nodes by the syntax highlighter.
+ * These must be same as the definitions in MdnDocsWidget.js.
+ */
+ const PROPERTY_NAME_COLOR = "theme-fg-color5";
+ const PROPERTY_VALUE_COLOR = "theme-fg-color1";
+ const COMMENT_COLOR = "theme-comment";
+
+ /**
+ * Check the type and content of a single node.
+ */
+ function checkNode(expected, actual) {
+ ok(actual.textContent == expected.text,
+ "Check that node has the expected textContent");
+ info("Expected text content: [" + expected.text + "]");
+ info("Actual text content: [" + actual.textContent + "]");
+
+ info("Check that node has the expected type");
+ if (expected.type == "text") {
+ ok(actual.nodeType == 3, "Check that node is a text node");
+ } else {
+ ok(actual.tagName.toUpperCase() == "SPAN", "Check that node is a SPAN");
+ }
+
+ info("Check that node has the expected className");
+
+ let expectedClassName = null;
+ let actualClassName = null;
+
+ switch (expected.type) {
+ case "property-name":
+ expectedClassName = PROPERTY_NAME_COLOR;
+ break;
+ case "property-value":
+ expectedClassName = PROPERTY_VALUE_COLOR;
+ break;
+ case "comment":
+ expectedClassName = COMMENT_COLOR;
+ break;
+ default:
+ ok(!actual.classList, "No className expected");
+ return;
+ }
+
+ ok(actual.classList.length == 1, "One className expected");
+ actualClassName = actual.classList[0];
+
+ ok(expectedClassName == actualClassName, "Check className value");
+ info("Expected className: " + expectedClassName);
+ info("Actual className: " + actualClassName);
+ }
+
+ info("Logging the actual nodes we have:");
+ for (let j = 0; j < parent.childNodes.length; j++) {
+ let n = parent.childNodes[j];
+ info(j + " / " +
+ "nodeType: " + n.nodeType + " / " +
+ "textContent: " + n.textContent);
+ }
+
+ ok(parent.childNodes.length == parent.childNodes.length,
+ "Check we have the expected number of nodes");
+ info("Expected node count " + expectedNodes.length);
+ info("Actual node count " + expectedNodes.length);
+
+ for (let i = 0; i < expectedNodes.length; i++) {
+ info("Check node " + i);
+ checkNode(expectedNodes[i], parent.childNodes[i]);
+ }
+}
diff --git a/devtools/client/shared/test/helper_color_data.js b/devtools/client/shared/test/helper_color_data.js
new file mode 100644
index 000000000..3407102ae
--- /dev/null
+++ b/devtools/client/shared/test/helper_color_data.js
@@ -0,0 +1,175 @@
+"use strict";
+
+/* eslint-disable max-len */
+function getFixtureColorData() {
+ return [
+ {authored: "aliceblue", name: "aliceblue", hex: "#f0f8ff", hsl: "hsl(208, 100%, 97.1%)", rgb: "rgb(240, 248, 255)", cycle: 4},
+ {authored: "antiquewhite", name: "antiquewhite", hex: "#faebd7", hsl: "hsl(34.3, 77.8%, 91.2%)", rgb: "rgb(250, 235, 215)", cycle: 4},
+ {authored: "aqua", name: "aqua", hex: "#0ff", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)", cycle: 4},
+ {authored: "aquamarine", name: "aquamarine", hex: "#7fffd4", hsl: "hsl(159.8, 100%, 74.9%)", rgb: "rgb(127, 255, 212)", cycle: 4},
+ {authored: "azure", name: "azure", hex: "#f0ffff", hsl: "hsl(180, 100%, 97.1%)", rgb: "rgb(240, 255, 255)", cycle: 4},
+ {authored: "beige", name: "beige", hex: "#f5f5dc", hsl: "hsl(60, 55.6%, 91.2%)", rgb: "rgb(245, 245, 220)", cycle: 4},
+ {authored: "bisque", name: "bisque", hex: "#ffe4c4", hsl: "hsl(32.5, 100%, 88.4%)", rgb: "rgb(255, 228, 196)", cycle: 4},
+ {authored: "black", name: "black", hex: "#000", hsl: "hsl(0, 0%, 0%)", rgb: "rgb(0, 0, 0)", cycle: 4},
+ {authored: "blanchedalmond", name: "blanchedalmond", hex: "#ffebcd", hsl: "hsl(36, 100%, 90.2%)", rgb: "rgb(255, 235, 205)", cycle: 4},
+ {authored: "blue", name: "blue", hex: "#00f", hsl: "hsl(240, 100%, 50%)", rgb: "rgb(0, 0, 255)", cycle: 4},
+ {authored: "blueviolet", name: "blueviolet", hex: "#8a2be2", hsl: "hsl(271.1, 75.9%, 52.7%)", rgb: "rgb(138, 43, 226)", cycle: 4},
+ {authored: "brown", name: "brown", hex: "#a52a2a", hsl: "hsl(0, 59.4%, 40.6%)", rgb: "rgb(165, 42, 42)", cycle: 4},
+ {authored: "burlywood", name: "burlywood", hex: "#deb887", hsl: "hsl(33.8, 56.9%, 70%)", rgb: "rgb(222, 184, 135)", cycle: 4},
+ {authored: "cadetblue", name: "cadetblue", hex: "#5f9ea0", hsl: "hsl(181.8, 25.5%, 50%)", rgb: "rgb(95, 158, 160)", cycle: 4},
+ {authored: "chartreuse", name: "chartreuse", hex: "#7fff00", hsl: "hsl(90.1, 100%, 50%)", rgb: "rgb(127, 255, 0)", cycle: 4},
+ {authored: "chocolate", name: "chocolate", hex: "#d2691e", hsl: "hsl(25, 75%, 47.1%)", rgb: "rgb(210, 105, 30)", cycle: 4},
+ {authored: "coral", name: "coral", hex: "#ff7f50", hsl: "hsl(16.1, 100%, 65.7%)", rgb: "rgb(255, 127, 80)", cycle: 4},
+ {authored: "cornflowerblue", name: "cornflowerblue", hex: "#6495ed", hsl: "hsl(218.5, 79.2%, 66.1%)", rgb: "rgb(100, 149, 237)", cycle: 4},
+ {authored: "cornsilk", name: "cornsilk", hex: "#fff8dc", hsl: "hsl(48, 100%, 93.1%)", rgb: "rgb(255, 248, 220)", cycle: 4},
+ {authored: "crimson", name: "crimson", hex: "#dc143c", hsl: "hsl(348, 83.3%, 47.1%)", rgb: "rgb(220, 20, 60)", cycle: 4},
+ {authored: "cyan", name: "aqua", hex: "#0ff", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)", cycle: 4},
+ {authored: "darkblue", name: "darkblue", hex: "#00008b", hsl: "hsl(240, 100%, 27.3%)", rgb: "rgb(0, 0, 139)", cycle: 4},
+ {authored: "darkcyan", name: "darkcyan", hex: "#008b8b", hsl: "hsl(180, 100%, 27.3%)", rgb: "rgb(0, 139, 139)", cycle: 4},
+ {authored: "darkgoldenrod", name: "darkgoldenrod", hex: "#b8860b", hsl: "hsl(42.7, 88.7%, 38.2%)", rgb: "rgb(184, 134, 11)", cycle: 4},
+ {authored: "darkgray", name: "darkgray", hex: "#a9a9a9", hsl: "hsl(0, 0%, 66.3%)", rgb: "rgb(169, 169, 169)", cycle: 4},
+ {authored: "darkgreen", name: "darkgreen", hex: "#006400", hsl: "hsl(120, 100%, 19.6%)", rgb: "rgb(0, 100, 0)", cycle: 4},
+ {authored: "darkgrey", name: "darkgray", hex: "#a9a9a9", hsl: "hsl(0, 0%, 66.3%)", rgb: "rgb(169, 169, 169)", cycle: 4},
+ {authored: "darkkhaki", name: "darkkhaki", hex: "#bdb76b", hsl: "hsl(55.6, 38.3%, 58%)", rgb: "rgb(189, 183, 107)", cycle: 4},
+ {authored: "darkmagenta", name: "darkmagenta", hex: "#8b008b", hsl: "hsl(300, 100%, 27.3%)", rgb: "rgb(139, 0, 139)", cycle: 4},
+ {authored: "darkolivegreen", name: "darkolivegreen", hex: "#556b2f", hsl: "hsl(82, 39%, 30.2%)", rgb: "rgb(85, 107, 47)", cycle: 4},
+ {authored: "darkorange", name: "darkorange", hex: "#ff8c00", hsl: "hsl(32.9, 100%, 50%)", rgb: "rgb(255, 140, 0)", cycle: 4},
+ {authored: "darkorchid", name: "darkorchid", hex: "#9932cc", hsl: "hsl(280.1, 60.6%, 49.8%)", rgb: "rgb(153, 50, 204)", cycle: 4},
+ {authored: "darkred", name: "darkred", hex: "#8b0000", hsl: "hsl(0, 100%, 27.3%)", rgb: "rgb(139, 0, 0)", cycle: 4},
+ {authored: "darksalmon", name: "darksalmon", hex: "#e9967a", hsl: "hsl(15.1, 71.6%, 69.6%)", rgb: "rgb(233, 150, 122)", cycle: 4},
+ {authored: "darkseagreen", name: "darkseagreen", hex: "#8fbc8f", hsl: "hsl(120, 25.1%, 64.9%)", rgb: "rgb(143, 188, 143)", cycle: 4},
+ {authored: "darkslateblue", name: "darkslateblue", hex: "#483d8b", hsl: "hsl(248.5, 39%, 39.2%)", rgb: "rgb(72, 61, 139)", cycle: 4},
+ {authored: "darkslategray", name: "darkslategray", hex: "#2f4f4f", hsl: "hsl(180, 25.4%, 24.7%)", rgb: "rgb(47, 79, 79)", cycle: 4},
+ {authored: "darkslategrey", name: "darkslategray", hex: "#2f4f4f", hsl: "hsl(180, 25.4%, 24.7%)", rgb: "rgb(47, 79, 79)", cycle: 4},
+ {authored: "darkturquoise", name: "darkturquoise", hex: "#00ced1", hsl: "hsl(180.9, 100%, 41%)", rgb: "rgb(0, 206, 209)", cycle: 4},
+ {authored: "darkviolet", name: "darkviolet", hex: "#9400d3", hsl: "hsl(282.1, 100%, 41.4%)", rgb: "rgb(148, 0, 211)", cycle: 4},
+ {authored: "deeppink", name: "deeppink", hex: "#ff1493", hsl: "hsl(327.6, 100%, 53.9%)", rgb: "rgb(255, 20, 147)", cycle: 4},
+ {authored: "deepskyblue", name: "deepskyblue", hex: "#00bfff", hsl: "hsl(195.1, 100%, 50%)", rgb: "rgb(0, 191, 255)", cycle: 4},
+ {authored: "dimgray", name: "dimgray", hex: "#696969", hsl: "hsl(0, 0%, 41.2%)", rgb: "rgb(105, 105, 105)", cycle: 4},
+ {authored: "dodgerblue", name: "dodgerblue", hex: "#1e90ff", hsl: "hsl(209.6, 100%, 55.9%)", rgb: "rgb(30, 144, 255)", cycle: 4},
+ {authored: "firebrick", name: "firebrick", hex: "#b22222", hsl: "hsl(0, 67.9%, 41.6%)", rgb: "rgb(178, 34, 34)", cycle: 4},
+ {authored: "floralwhite", name: "floralwhite", hex: "#fffaf0", hsl: "hsl(40, 100%, 97.1%)", rgb: "rgb(255, 250, 240)", cycle: 4},
+ {authored: "forestgreen", name: "forestgreen", hex: "#228b22", hsl: "hsl(120, 60.7%, 33.9%)", rgb: "rgb(34, 139, 34)", cycle: 4},
+ {authored: "fuchsia", name: "fuchsia", hex: "#f0f", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)", cycle: 4},
+ {authored: "gainsboro", name: "gainsboro", hex: "#dcdcdc", hsl: "hsl(0, 0%, 86.3%)", rgb: "rgb(220, 220, 220)", cycle: 4},
+ {authored: "ghostwhite", name: "ghostwhite", hex: "#f8f8ff", hsl: "hsl(240, 100%, 98.6%)", rgb: "rgb(248, 248, 255)", cycle: 4},
+ {authored: "gold", name: "gold", hex: "#ffd700", hsl: "hsl(50.6, 100%, 50%)", rgb: "rgb(255, 215, 0)", cycle: 4},
+ {authored: "goldenrod", name: "goldenrod", hex: "#daa520", hsl: "hsl(42.9, 74.4%, 49%)", rgb: "rgb(218, 165, 32)", cycle: 4},
+ {authored: "gray", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50.2%)", rgb: "rgb(128, 128, 128)", cycle: 4},
+ {authored: "green", name: "green", hex: "#008000", hsl: "hsl(120, 100%, 25.1%)", rgb: "rgb(0, 128, 0)", cycle: 4},
+ {authored: "greenyellow", name: "greenyellow", hex: "#adff2f", hsl: "hsl(83.7, 100%, 59.2%)", rgb: "rgb(173, 255, 47)", cycle: 4},
+ {authored: "grey", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50.2%)", rgb: "rgb(128, 128, 128)", cycle: 4},
+ {authored: "honeydew", name: "honeydew", hex: "#f0fff0", hsl: "hsl(120, 100%, 97.1%)", rgb: "rgb(240, 255, 240)", cycle: 4},
+ {authored: "hotpink", name: "hotpink", hex: "#ff69b4", hsl: "hsl(330, 100%, 70.6%)", rgb: "rgb(255, 105, 180)", cycle: 4},
+ {authored: "indianred", name: "indianred", hex: "#cd5c5c", hsl: "hsl(0, 53.1%, 58.2%)", rgb: "rgb(205, 92, 92)", cycle: 4},
+ {authored: "indigo", name: "indigo", hex: "#4b0082", hsl: "hsl(274.6, 100%, 25.5%)", rgb: "rgb(75, 0, 130)", cycle: 4},
+ {authored: "ivory", name: "ivory", hex: "#fffff0", hsl: "hsl(60, 100%, 97.1%)", rgb: "rgb(255, 255, 240)", cycle: 4},
+ {authored: "khaki", name: "khaki", hex: "#f0e68c", hsl: "hsl(54, 76.9%, 74.5%)", rgb: "rgb(240, 230, 140)", cycle: 4},
+ {authored: "lavender", name: "lavender", hex: "#e6e6fa", hsl: "hsl(240, 66.7%, 94.1%)", rgb: "rgb(230, 230, 250)", cycle: 4},
+ {authored: "lavenderblush", name: "lavenderblush", hex: "#fff0f5", hsl: "hsl(340, 100%, 97.1%)", rgb: "rgb(255, 240, 245)", cycle: 4},
+ {authored: "lawngreen", name: "lawngreen", hex: "#7cfc00", hsl: "hsl(90.5, 100%, 49.4%)", rgb: "rgb(124, 252, 0)", cycle: 4},
+ {authored: "lemonchiffon", name: "lemonchiffon", hex: "#fffacd", hsl: "hsl(54, 100%, 90.2%)", rgb: "rgb(255, 250, 205)", cycle: 4},
+ {authored: "lightblue", name: "lightblue", hex: "#add8e6", hsl: "hsl(194.7, 53.3%, 79%)", rgb: "rgb(173, 216, 230)", cycle: 4},
+ {authored: "lightcoral", name: "lightcoral", hex: "#f08080", hsl: "hsl(0, 78.9%, 72.2%)", rgb: "rgb(240, 128, 128)", cycle: 4},
+ {authored: "lightcyan", name: "lightcyan", hex: "#e0ffff", hsl: "hsl(180, 100%, 93.9%)", rgb: "rgb(224, 255, 255)", cycle: 4},
+ {authored: "lightgoldenrodyellow", name: "lightgoldenrodyellow", hex: "#fafad2", hsl: "hsl(60, 80%, 90.2%)", rgb: "rgb(250, 250, 210)", cycle: 4},
+ {authored: "lightgray", name: "lightgray", hex: "#d3d3d3", hsl: "hsl(0, 0%, 82.7%)", rgb: "rgb(211, 211, 211)", cycle: 4},
+ {authored: "lightgreen", name: "lightgreen", hex: "#90ee90", hsl: "hsl(120, 73.4%, 74.9%)", rgb: "rgb(144, 238, 144)", cycle: 4},
+ {authored: "lightgrey", name: "lightgray", hex: "#d3d3d3", hsl: "hsl(0, 0%, 82.7%)", rgb: "rgb(211, 211, 211)", cycle: 4},
+ {authored: "lightpink", name: "lightpink", hex: "#ffb6c1", hsl: "hsl(351, 100%, 85.7%)", rgb: "rgb(255, 182, 193)", cycle: 4},
+ {authored: "lightsalmon", name: "lightsalmon", hex: "#ffa07a", hsl: "hsl(17.1, 100%, 73.9%)", rgb: "rgb(255, 160, 122)", cycle: 4},
+ {authored: "lightseagreen", name: "lightseagreen", hex: "#20b2aa", hsl: "hsl(176.7, 69.5%, 41.2%)", rgb: "rgb(32, 178, 170)", cycle: 4},
+ {authored: "lightskyblue", name: "lightskyblue", hex: "#87cefa", hsl: "hsl(203, 92%, 75.5%)", rgb: "rgb(135, 206, 250)", cycle: 4},
+ {authored: "lightslategray", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14.3%, 53.3%)", rgb: "rgb(119, 136, 153)", cycle: 4},
+ {authored: "lightslategrey", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14.3%, 53.3%)", rgb: "rgb(119, 136, 153)", cycle: 4},
+ {authored: "lightsteelblue", name: "lightsteelblue", hex: "#b0c4de", hsl: "hsl(213.9, 41.1%, 78%)", rgb: "rgb(176, 196, 222)", cycle: 4},
+ {authored: "lightyellow", name: "lightyellow", hex: "#ffffe0", hsl: "hsl(60, 100%, 93.9%)", rgb: "rgb(255, 255, 224)", cycle: 4},
+ {authored: "lime", name: "lime", hex: "#0f0", hsl: "hsl(120, 100%, 50%)", rgb: "rgb(0, 255, 0)", cycle: 4},
+ {authored: "limegreen", name: "limegreen", hex: "#32cd32", hsl: "hsl(120, 60.8%, 50%)", rgb: "rgb(50, 205, 50)", cycle: 4},
+ {authored: "linen", name: "linen", hex: "#faf0e6", hsl: "hsl(30, 66.7%, 94.1%)", rgb: "rgb(250, 240, 230)", cycle: 4},
+ {authored: "magenta", name: "fuchsia", hex: "#f0f", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)", cycle: 4},
+ {authored: "maroon", name: "maroon", hex: "#800000", hsl: "hsl(0, 100%, 25.1%)", rgb: "rgb(128, 0, 0)", cycle: 4},
+ {authored: "mediumaquamarine", name: "mediumaquamarine", hex: "#66cdaa", hsl: "hsl(159.6, 50.7%, 60.2%)", rgb: "rgb(102, 205, 170)", cycle: 4},
+ {authored: "mediumblue", name: "mediumblue", hex: "#0000cd", hsl: "hsl(240, 100%, 40.2%)", rgb: "rgb(0, 0, 205)", cycle: 4},
+ {authored: "mediumorchid", name: "mediumorchid", hex: "#ba55d3", hsl: "hsl(288.1, 58.9%, 58%)", rgb: "rgb(186, 85, 211)", cycle: 4},
+ {authored: "mediumpurple", name: "mediumpurple", hex: "#9370db", hsl: "hsl(259.6, 59.8%, 64.9%)", rgb: "rgb(147, 112, 219)", cycle: 4},
+ {authored: "mediumseagreen", name: "mediumseagreen", hex: "#3cb371", hsl: "hsl(146.7, 49.8%, 46.9%)", rgb: "rgb(60, 179, 113)", cycle: 4},
+ {authored: "mediumslateblue", name: "mediumslateblue", hex: "#7b68ee", hsl: "hsl(248.5, 79.8%, 67.1%)", rgb: "rgb(123, 104, 238)", cycle: 4},
+ {authored: "mediumspringgreen", name: "mediumspringgreen", hex: "#00fa9a", hsl: "hsl(157, 100%, 49%)", rgb: "rgb(0, 250, 154)", cycle: 4},
+ {authored: "mediumturquoise", name: "mediumturquoise", hex: "#48d1cc", hsl: "hsl(177.8, 59.8%, 55.1%)", rgb: "rgb(72, 209, 204)", cycle: 4},
+ {authored: "mediumvioletred", name: "mediumvioletred", hex: "#c71585", hsl: "hsl(322.2, 80.9%, 43.1%)", rgb: "rgb(199, 21, 133)", cycle: 4},
+ {authored: "midnightblue", name: "midnightblue", hex: "#191970", hsl: "hsl(240, 63.5%, 26.9%)", rgb: "rgb(25, 25, 112)", cycle: 4},
+ {authored: "mintcream", name: "mintcream", hex: "#f5fffa", hsl: "hsl(150, 100%, 98%)", rgb: "rgb(245, 255, 250)", cycle: 4},
+ {authored: "mistyrose", name: "mistyrose", hex: "#ffe4e1", hsl: "hsl(6, 100%, 94.1%)", rgb: "rgb(255, 228, 225)", cycle: 4},
+ {authored: "moccasin", name: "moccasin", hex: "#ffe4b5", hsl: "hsl(38.1, 100%, 85.5%)", rgb: "rgb(255, 228, 181)", cycle: 4},
+ {authored: "navajowhite", name: "navajowhite", hex: "#ffdead", hsl: "hsl(35.9, 100%, 83.9%)", rgb: "rgb(255, 222, 173)", cycle: 4},
+ {authored: "navy", name: "navy", hex: "#000080", hsl: "hsl(240, 100%, 25.1%)", rgb: "rgb(0, 0, 128)", cycle: 4},
+ {authored: "oldlace", name: "oldlace", hex: "#fdf5e6", hsl: "hsl(39.1, 85.2%, 94.7%)", rgb: "rgb(253, 245, 230)", cycle: 4},
+ {authored: "olive", name: "olive", hex: "#808000", hsl: "hsl(60, 100%, 25.1%)", rgb: "rgb(128, 128, 0)", cycle: 4},
+ {authored: "olivedrab", name: "olivedrab", hex: "#6b8e23", hsl: "hsl(79.6, 60.5%, 34.7%)", rgb: "rgb(107, 142, 35)", cycle: 4},
+ {authored: "orange", name: "orange", hex: "#ffa500", hsl: "hsl(38.8, 100%, 50%)", rgb: "rgb(255, 165, 0)", cycle: 4},
+ {authored: "orangered", name: "orangered", hex: "#ff4500", hsl: "hsl(16.2, 100%, 50%)", rgb: "rgb(255, 69, 0)", cycle: 4},
+ {authored: "orchid", name: "orchid", hex: "#da70d6", hsl: "hsl(302.3, 58.9%, 64.7%)", rgb: "rgb(218, 112, 214)", cycle: 4},
+ {authored: "palegoldenrod", name: "palegoldenrod", hex: "#eee8aa", hsl: "hsl(54.7, 66.7%, 80%)", rgb: "rgb(238, 232, 170)", cycle: 4},
+ {authored: "palegreen", name: "palegreen", hex: "#98fb98", hsl: "hsl(120, 92.5%, 79%)", rgb: "rgb(152, 251, 152)", cycle: 4},
+ {authored: "paleturquoise", name: "paleturquoise", hex: "#afeeee", hsl: "hsl(180, 64.9%, 81%)", rgb: "rgb(175, 238, 238)", cycle: 4},
+ {authored: "palevioletred", name: "palevioletred", hex: "#db7093", hsl: "hsl(340.4, 59.8%, 64.9%)", rgb: "rgb(219, 112, 147)", cycle: 4},
+ {authored: "papayawhip", name: "papayawhip", hex: "#ffefd5", hsl: "hsl(37.1, 100%, 91.8%)", rgb: "rgb(255, 239, 213)", cycle: 4},
+ {authored: "peachpuff", name: "peachpuff", hex: "#ffdab9", hsl: "hsl(28.3, 100%, 86.3%)", rgb: "rgb(255, 218, 185)", cycle: 4},
+ {authored: "peru", name: "peru", hex: "#cd853f", hsl: "hsl(29.6, 58.7%, 52.5%)", rgb: "rgb(205, 133, 63)", cycle: 4},
+ {authored: "pink", name: "pink", hex: "#ffc0cb", hsl: "hsl(349.5, 100%, 87.6%)", rgb: "rgb(255, 192, 203)", cycle: 4},
+ {authored: "plum", name: "plum", hex: "#dda0dd", hsl: "hsl(300, 47.3%, 74.7%)", rgb: "rgb(221, 160, 221)", cycle: 4},
+ {authored: "powderblue", name: "powderblue", hex: "#b0e0e6", hsl: "hsl(186.7, 51.9%, 79.6%)", rgb: "rgb(176, 224, 230)", cycle: 4},
+ {authored: "purple", name: "purple", hex: "#800080", hsl: "hsl(300, 100%, 25.1%)", rgb: "rgb(128, 0, 128)", cycle: 4},
+ {authored: "rebeccapurple", name: "rebeccapurple", hex: "#639", hsl: "hsl(270, 50%, 40%)", rgb: "rgb(102, 51, 153)", cycle: 4},
+ {authored: "red", name: "red", hex: "#f00", hsl: "hsl(0, 100%, 50%)", rgb: "rgb(255, 0, 0)", cycle: 4},
+ {authored: "rosybrown", name: "rosybrown", hex: "#bc8f8f", hsl: "hsl(0, 25.1%, 64.9%)", rgb: "rgb(188, 143, 143)", cycle: 4},
+ {authored: "royalblue", name: "royalblue", hex: "#4169e1", hsl: "hsl(225, 72.7%, 56.9%)", rgb: "rgb(65, 105, 225)", cycle: 4},
+ {authored: "saddlebrown", name: "saddlebrown", hex: "#8b4513", hsl: "hsl(25, 75.9%, 31%)", rgb: "rgb(139, 69, 19)", cycle: 4},
+ {authored: "salmon", name: "salmon", hex: "#fa8072", hsl: "hsl(6.2, 93.2%, 71.4%)", rgb: "rgb(250, 128, 114)", cycle: 4},
+ {authored: "sandybrown", name: "sandybrown", hex: "#f4a460", hsl: "hsl(27.6, 87.1%, 66.7%)", rgb: "rgb(244, 164, 96)", cycle: 4},
+ {authored: "seagreen", name: "seagreen", hex: "#2e8b57", hsl: "hsl(146.5, 50.3%, 36.3%)", rgb: "rgb(46, 139, 87)", cycle: 4},
+ {authored: "seashell", name: "seashell", hex: "#fff5ee", hsl: "hsl(24.7, 100%, 96.7%)", rgb: "rgb(255, 245, 238)", cycle: 4},
+ {authored: "sienna", name: "sienna", hex: "#a0522d", hsl: "hsl(19.3, 56.1%, 40.2%)", rgb: "rgb(160, 82, 45)", cycle: 4},
+ {authored: "silver", name: "silver", hex: "#c0c0c0", hsl: "hsl(0, 0%, 75.3%)", rgb: "rgb(192, 192, 192)", cycle: 4},
+ {authored: "skyblue", name: "skyblue", hex: "#87ceeb", hsl: "hsl(197.4, 71.4%, 72.5%)", rgb: "rgb(135, 206, 235)", cycle: 4},
+ {authored: "slateblue", name: "slateblue", hex: "#6a5acd", hsl: "hsl(248.3, 53.5%, 57.8%)", rgb: "rgb(106, 90, 205)", cycle: 4},
+ {authored: "slategray", name: "slategray", hex: "#708090", hsl: "hsl(210, 12.6%, 50.2%)", rgb: "rgb(112, 128, 144)", cycle: 4},
+ {authored: "slategrey", name: "slategray", hex: "#708090", hsl: "hsl(210, 12.6%, 50.2%)", rgb: "rgb(112, 128, 144)", cycle: 4},
+ {authored: "snow", name: "snow", hex: "#fffafa", hsl: "hsl(0, 100%, 99%)", rgb: "rgb(255, 250, 250)", cycle: 4},
+ {authored: "springgreen", name: "springgreen", hex: "#00ff7f", hsl: "hsl(149.9, 100%, 50%)", rgb: "rgb(0, 255, 127)", cycle: 4},
+ {authored: "steelblue", name: "steelblue", hex: "#4682b4", hsl: "hsl(207.3, 44%, 49%)", rgb: "rgb(70, 130, 180)", cycle: 4},
+ {authored: "tan", name: "tan", hex: "#d2b48c", hsl: "hsl(34.3, 43.7%, 68.6%)", rgb: "rgb(210, 180, 140)", cycle: 4},
+ {authored: "teal", name: "teal", hex: "#008080", hsl: "hsl(180, 100%, 25.1%)", rgb: "rgb(0, 128, 128)", cycle: 4},
+ {authored: "thistle", name: "thistle", hex: "#d8bfd8", hsl: "hsl(300, 24.3%, 79.8%)", rgb: "rgb(216, 191, 216)", cycle: 4},
+ {authored: "tomato", name: "tomato", hex: "#ff6347", hsl: "hsl(9.1, 100%, 63.9%)", rgb: "rgb(255, 99, 71)", cycle: 4},
+ {authored: "turquoise", name: "turquoise", hex: "#40e0d0", hsl: "hsl(174, 72.1%, 56.5%)", rgb: "rgb(64, 224, 208)", cycle: 4},
+ {authored: "violet", name: "violet", hex: "#ee82ee", hsl: "hsl(300, 76.1%, 72.2%)", rgb: "rgb(238, 130, 238)", cycle: 4},
+ {authored: "wheat", name: "wheat", hex: "#f5deb3", hsl: "hsl(39.1, 76.7%, 83.1%)", rgb: "rgb(245, 222, 179)", cycle: 4},
+ {authored: "white", name: "white", hex: "#fff", hsl: "hsl(0, 0%, 100%)", rgb: "rgb(255, 255, 255)", cycle: 4},
+ {authored: "whitesmoke", name: "whitesmoke", hex: "#f5f5f5", hsl: "hsl(0, 0%, 96.1%)", rgb: "rgb(245, 245, 245)", cycle: 4},
+ {authored: "yellow", name: "yellow", hex: "#ff0", hsl: "hsl(60, 100%, 50%)", rgb: "rgb(255, 255, 0)", cycle: 4},
+ {authored: "yellowgreen", name: "yellowgreen", hex: "#9acd32", hsl: "hsl(79.7, 60.8%, 50%)", rgb: "rgb(154, 205, 50)", cycle: 4},
+ {authored: "rgba(0, 0, 0, 0)", name: "#0000", hex: "#0000", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)", cycle: 3},
+ {authored: "hsla(0, 0%, 0%, 0)", name: "#0000", hex: "#0000", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)", cycle: 3},
+ {authored: "rgba(50, 60, 70, 0.5)", name: "#323c4680", hex: "#323c4680", hsl: "hsla(210, 16.7%, 23.5%, 0.5)", rgb: "rgba(50, 60, 70, 0.5)", cycle: 3},
+ {authored: "rgba(0, 0, 0, 0.3)", name: "#0000004d", hex: "#0000004d", hsl: "hsla(0, 0%, 0%, 0.3)", rgb: "rgba(0, 0, 0, 0.3)", cycle: 3},
+ {authored: "rgba(255, 255, 255, 0.6)", name: "#fff9", hex: "#fff9", hsl: "hsla(0, 0%, 100%, 0.6)", rgb: "rgba(255, 255, 255, 0.6)", cycle: 3},
+ {authored: "rgba(127, 89, 45, 1)", name: "#7f592d", hex: "#7f592d", hsl: "hsl(32.2, 47.7%, 33.7%)", rgb: "rgb(127, 89, 45)", cycle: 3},
+ {authored: "hsla(19.304, 56%, 40%, 1)", name: "#9f522d", hex: "#9f522d", hsl: "hsl(19.5, 55.9%, 40%)", rgb: "rgb(159, 82, 45)", cycle: 3},
+ {authored: "#f089", name: "#f089", hex: "#f089", hsl: "hsla(328, 100%, 50%, 0.6)", rgb: "rgba(255, 0, 136, 0.6)", cycle: 3},
+ {authored: "#00ff8080", name: "#00ff8080", hex: "#00ff8080", hsl: "hsla(150.1, 100%, 50%, 0.5)", rgb: "rgba(0, 255, 128, 0.5)", cycle: 3},
+ {authored: "currentcolor", name: "currentcolor", hex: "currentcolor", hsl: "currentcolor", rgb: "currentcolor", cycle: false},
+ {authored: "inherit", name: "inherit", hex: "inherit", hsl: "inherit", rgb: "inherit", cycle: false},
+ {authored: "initial", name: "initial", hex: "initial", hsl: "initial", rgb: "initial", cycle: false},
+ {authored: "invalidColor", name: "", hex: "", hsl: "", rgb: "", cycle: false},
+ {authored: "transparent", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent", cycle: false},
+ {authored: "unset", name: "unset", hex: "unset", hsl: "unset", rgb: "unset", cycle: false},
+ ];
+}
+/* eslint-enable max-len */
+
+// Allow this function to be shared on mochitests and xpcshell tests.
+if (typeof module === "object") {
+ module.exports = getFixtureColorData;
+}
diff --git a/devtools/client/shared/test/helper_html_tooltip.js b/devtools/client/shared/test/helper_html_tooltip.js
new file mode 100644
index 000000000..ffc6945f3
--- /dev/null
+++ b/devtools/client/shared/test/helper_html_tooltip.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+
+"use strict";
+
+/**
+ * Helper methods for the HTMLTooltip integration tests.
+ */
+
+/**
+ * Display an existing HTMLTooltip on an anchor. After the tooltip "shown"
+ * event has been fired a reflow will be triggered.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The tooltip instance to display
+ * @param {Node} anchor
+ * The anchor that should be used to display the tooltip
+ * @param {Object} see HTMLTooltip:show documentation
+ * @return {Promise} promise that resolves when "shown" has been fired, reflow
+ * and repaint done.
+ */
+function* showTooltip(tooltip, anchor, {position, x, y} = {}) {
+ let onShown = tooltip.once("shown");
+ tooltip.show(anchor, {position, x, y});
+ yield onShown;
+ return waitForReflow(tooltip);
+}
+
+/**
+ * Hide an existing HTMLTooltip. After the tooltip "hidden" event has been fired
+ * a reflow will be triggered.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The tooltip instance to hide
+ * @return {Promise} promise that resolves when "hidden" has been fired, reflow
+ * and repaint done.
+ */
+function* hideTooltip(tooltip) {
+ let onPopupHidden = tooltip.once("hidden");
+ tooltip.hide();
+ yield onPopupHidden;
+ return waitForReflow(tooltip);
+}
+
+/**
+ * Forces the reflow of an HTMLTooltip document and waits for the next repaint.
+ *
+ * @param {HTMLTooltip} the tooltip to reflow
+ * @return {Promise} a promise that will resolve after the reflow and repaint
+ * have been executed.
+ */
+function waitForReflow(tooltip) {
+ let {doc} = tooltip;
+ return new Promise(resolve => {
+ doc.documentElement.offsetWidth;
+ doc.defaultView.requestAnimationFrame(resolve);
+ });
+}
+
+/**
+ * Test helper designed to check that a tooltip is displayed at the expected
+ * position relative to an anchor, given a set of expectations.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The HTMLTooltip instance to check
+ * @param {Node} anchor
+ * The tooltip's anchor
+ * @param {Object} expected
+ * - {String} position : "top" or "bottom"
+ * - {Boolean} leftAligned
+ * - {Number} width: expected tooltip width
+ * - {Number} height: expected tooltip height
+ */
+function checkTooltipGeometry(tooltip, anchor,
+ {position, leftAligned = true, height, width} = {}) {
+ info("Check the tooltip geometry matches expected position and dimensions");
+ let tooltipRect = tooltip.container.getBoundingClientRect();
+ let anchorRect = anchor.getBoundingClientRect();
+
+ if (position === "top") {
+ is(tooltipRect.bottom, anchorRect.top, "Tooltip is above the anchor");
+ } else if (position === "bottom") {
+ is(tooltipRect.top, anchorRect.bottom, "Tooltip is below the anchor");
+ } else {
+ ok(false, "Invalid position provided to checkTooltipGeometry");
+ }
+
+ if (leftAligned) {
+ is(tooltipRect.left, anchorRect.left,
+ "Tooltip left-aligned with the anchor");
+ }
+
+ is(tooltipRect.height, height, "Tooltip has the expected height");
+ is(tooltipRect.width, width, "Tooltip has the expected width");
+}
diff --git a/devtools/client/shared/test/helper_inplace_editor.js b/devtools/client/shared/test/helper_inplace_editor.js
new file mode 100644
index 000000000..ec6b79e00
--- /dev/null
+++ b/devtools/client/shared/test/helper_inplace_editor.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from head.js */
+
+"use strict";
+
+/**
+ * Helper methods for the HTMLTooltip integration tests.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const { editableField } = require("devtools/client/shared/inplace-editor");
+
+/**
+ * Create an inplace editor linked to a span element and click on the span to
+ * to turn to edit mode.
+ *
+ * @param {Object} options
+ * Options passed to the InplaceEditor/editableField constructor.
+ * @param {Document} doc
+ * Document where the span element will be created.
+ * @param {String} textContent
+ * (optional) String that will be used as the text content of the span.
+ */
+const createInplaceEditorAndClick = Task.async(function* (options, doc, textContent) {
+ let span = options.element = createSpan(doc);
+ if (textContent) {
+ span.textContent = textContent;
+ }
+
+ info("Creating an inplace-editor field");
+ editableField(options);
+
+ info("Clicking on the inplace-editor field to turn to edit mode");
+ span.click();
+});
+
+/**
+ * Helper to create a span in the provided document.
+ *
+ * @param {Document} doc
+ * Document where the span element will be created.
+ * @return {Element} the created span element.
+ */
+function createSpan(doc) {
+ info("Creating a new span element");
+ let div = doc.createElementNS(HTML_NS, "div");
+ let span = doc.createElementNS(HTML_NS, "span");
+ span.setAttribute("tabindex", "0");
+ span.style.fontSize = "11px";
+ span.style.display = "inline-block";
+ span.style.width = "100px";
+ span.style.border = "1px solid red";
+ span.style.fontFamily = "monospace";
+
+ div.style.height = "100%";
+ div.style.position = "absolute";
+ div.appendChild(span);
+
+ let parent = doc.querySelector("window") || doc.body;
+ parent.appendChild(div);
+ return span;
+}
+
+/**
+ * Test helper simulating a key event in an InplaceEditor and checking that the
+ * autocompletion works as expected.
+ *
+ * @param {Array} testData
+ * - {String} key, the key to send
+ * - {String} completion, the expected value of the auto-completion
+ * - {Number} index, the index of the selected suggestion in the popup
+ * - {Number} total, the total number of suggestions in the popup
+ * @param {InplaceEditor} editor
+ * The InplaceEditor instance being tested
+ */
+function* testCompletion([key, completion, index, total], editor) {
+ info("Pressing key " + key);
+ info("Expecting " + completion);
+
+ let onVisibilityChange = null;
+ let open = total > 0;
+ if (editor.popup.isOpen != open) {
+ onVisibilityChange = editor.popup.once(open ? "popup-opened" : "popup-closed");
+ }
+
+ let onSuggest;
+ if (/(left|right|back_space|escape)/ig.test(key)) {
+ info("Adding event listener for right|back_space|escape keys");
+ onSuggest = once(editor.input, "keypress");
+ } else {
+ info("Waiting for after-suggest event on the editor");
+ onSuggest = editor.once("after-suggest");
+ }
+
+ info("Synthesizing key " + key);
+ EventUtils.synthesizeKey(key, {}, editor.input.defaultView);
+
+ yield onSuggest;
+ yield onVisibilityChange;
+ yield waitForTick();
+
+ info("Checking the state");
+ if (completion !== null) {
+ is(editor.input.value, completion, "Correct value is autocompleted");
+ }
+ if (total === 0) {
+ ok(!(editor.popup && editor.popup.isOpen), "Popup is closed");
+ } else {
+ ok(editor.popup.isOpen, "Popup is open");
+ is(editor.popup.getItems().length, total, "Number of suggestions match");
+ is(editor.popup.selectedIndex, index, "Expected item is selected");
+ }
+}
diff --git a/devtools/client/shared/test/html-mdn-css-basic-testing.html b/devtools/client/shared/test/html-mdn-css-basic-testing.html
new file mode 100644
index 000000000..182fa6d9b
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-basic-testing.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+ <h2 id="Summary">Summary</h2>
+
+ <p>A summary of the property.</p>
+
+ <h2 id="Syntax">Syntax</h2>
+
+ <pre>/* The part we want */
+this: is-the-part-we-want;</pre>
+
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html b/devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html
new file mode 100644
index 000000000..8e1a7c3f6
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-no-summary-or-syntax.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+<p>This is not the summary or the syntax.</p>
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/shared/test/html-mdn-css-no-summary.html b/devtools/client/shared/test/html-mdn-css-no-summary.html
new file mode 100644
index 000000000..3af52b4f6
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-no-summary.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+ <p>This is not the summary.</p>
+
+ <h2 id="Syntax">Syntax</h2>
+
+ <pre>To be ignored.</pre>
+
+ <pre>/* The part we want */
+this: is-the-part-we-want;</pre>
+
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/shared/test/html-mdn-css-no-syntax.html b/devtools/client/shared/test/html-mdn-css-no-syntax.html
new file mode 100644
index 000000000..4e43bc434
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-no-syntax.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+ <h2 id="Summary">Summary</h2>
+
+ <p>A summary of the property.</p>
+
+ <p>This is not the syntax.</p>
+
+
+</body>
+</html>
diff --git a/devtools/client/shared/test/html-mdn-css-syntax-old-style.html b/devtools/client/shared/test/html-mdn-css-syntax-old-style.html
new file mode 100644
index 000000000..281267cc4
--- /dev/null
+++ b/devtools/client/shared/test/html-mdn-css-syntax-old-style.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+</head>
+
+<body>
+
+ <h2 id="Summary">Summary</h2>
+
+ <p>A summary of the property.</p>
+
+ <h2 id="Syntax">Syntax</h2>
+
+ <pre>The part we should ignore</pre>
+
+ <pre>/* The part we want */
+this: is-the-part-we-want;</pre>
+
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/shared/test/leakhunt.js b/devtools/client/shared/test/leakhunt.js
new file mode 100644
index 000000000..e71244955
--- /dev/null
+++ b/devtools/client/shared/test/leakhunt.js
@@ -0,0 +1,165 @@
+/* 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";
+
+/**
+ * Memory leak hunter. Walks a tree of objects looking for DOM nodes.
+ * Usage:
+ * leakHunt({
+ * thing: thing,
+ * otherthing: otherthing
+ * });
+ */
+function leakHunt(root) {
+ let path = [];
+ let seen = [];
+
+ try {
+ let output = leakHunt.inner(root, path, seen);
+ output.forEach(function (line) {
+ dump(line + "\n");
+ });
+ } catch (ex) {
+ dump(ex + "\n");
+ }
+}
+
+leakHunt.inner = function (root, path, seen) {
+ let prefix = new Array(path.length).join(" ");
+
+ let reply = [];
+ function log(msg) {
+ reply.push(msg);
+ }
+
+ let direct;
+ try {
+ direct = Object.keys(root);
+ } catch (ex) {
+ log(prefix + " Error enumerating: " + ex);
+ return reply;
+ }
+
+ try {
+ let index = 0;
+ for (let data of root) {
+ let prop = "" + index;
+ leakHunt.digProperty(prop, data, path, seen, direct, log);
+ index++;
+ }
+ } catch (ex) {
+ /* Ignore things that are not enumerable */
+ }
+
+ for (let prop in root) {
+ let data;
+ try {
+ data = root[prop];
+ } catch (ex) {
+ log(prefix + " " + prop + " = Error: " + ex.toString().substring(0, 30));
+ continue;
+ }
+
+ leakHunt.digProperty(prop, data, path, seen, direct, log);
+ }
+
+ return reply;
+};
+
+leakHunt.hide = [ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/ ];
+
+leakHunt.noRecurse = [
+ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/,
+ /^Window$/, /^Document$/,
+ /^XULDocument$/, /^XULElement$/,
+ /^DOMWindow$/, /^HTMLDocument$/, /^HTML.*Element$/, /^ChromeWindow$/
+];
+
+leakHunt.digProperty = function (prop, data, path, seen, direct, log) {
+ let newPath = path.slice();
+ newPath.push(prop);
+ let prefix = new Array(newPath.length).join(" ");
+
+ let recurse = true;
+ let message = leakHunt.getType(data);
+
+ if (leakHunt.matchesAnyPattern(message, leakHunt.hide)) {
+ return;
+ }
+
+ if (message === "function" && direct.indexOf(prop) == -1) {
+ return;
+ }
+
+ if (message === "string") {
+ let extra = data.length > 10 ? data.substring(0, 9) + "_" : data;
+ message += ' "' + extra.replace(/\n/g, "|") + '"';
+ recurse = false;
+ } else if (leakHunt.matchesAnyPattern(message, leakHunt.noRecurse)) {
+ message += " (no recurse)";
+ recurse = false;
+ } else if (seen.indexOf(data) !== -1) {
+ message += " (already seen)";
+ recurse = false;
+ }
+
+ if (recurse) {
+ seen.push(data);
+ let lines = leakHunt.inner(data, newPath, seen);
+ if (lines.length == 0) {
+ if (message !== "function") {
+ log(prefix + prop + " = " + message + " { }");
+ }
+ } else {
+ log(prefix + prop + " = " + message + " {");
+ lines.forEach(function (line) {
+ log(line);
+ });
+ log(prefix + "}");
+ }
+ } else {
+ log(prefix + prop + " = " + message);
+ }
+};
+
+leakHunt.matchesAnyPattern = function (str, patterns) {
+ let match = false;
+ patterns.forEach(function (pattern) {
+ if (str.match(pattern)) {
+ match = true;
+ }
+ });
+ return match;
+};
+
+leakHunt.getType = function (data) {
+ if (data === null) {
+ return "null";
+ }
+ if (data === undefined) {
+ return "undefined";
+ }
+
+ let type = typeof data;
+ if (type === "object" || type === "Object") {
+ type = leakHunt.getCtorName(data);
+ }
+
+ return type;
+};
+
+leakHunt.getCtorName = function (obj) {
+ try {
+ if (obj.constructor && obj.constructor.name) {
+ return obj.constructor.name;
+ }
+ } catch (ex) {
+ return "UnknownObject";
+ }
+
+ // If that fails, use Objects toString which sometimes gives something
+ // better than 'Object', and at least defaults to Object if nothing better
+ return Object.prototype.toString.call(obj).slice(8, -1);
+};
diff --git a/devtools/client/shared/test/test-actor-registry.js b/devtools/client/shared/test/test-actor-registry.js
new file mode 100644
index 000000000..e3b47c154
--- /dev/null
+++ b/devtools/client/shared/test/test-actor-registry.js
@@ -0,0 +1,97 @@
+/* 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";
+
+(function (exports) {
+ const Cu = Components.utils;
+ const CC = Components.Constructor;
+
+ const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ const { fetch } = require("devtools/shared/DevToolsUtils");
+ const defer = require("devtools/shared/defer");
+ const { Task } = require("devtools/shared/task");
+
+ const TEST_URL_ROOT = "http://example.com/browser/devtools/client/shared/test/";
+ const ACTOR_URL = TEST_URL_ROOT + "test-actor.js";
+
+ // Register a test actor that can operate on the remote document
+ exports.registerTestActor = Task.async(function* (client) {
+ // First, instanciate ActorRegistryFront to be able to dynamically register an actor
+ let deferred = defer();
+ client.listTabs(deferred.resolve);
+ let response = yield deferred.promise;
+ let { ActorRegistryFront } = require("devtools/shared/fronts/actor-registry");
+ let registryFront = ActorRegistryFront(client, response);
+
+ // Then ask to register our test-actor to retrieve its front
+ let options = {
+ type: { tab: true },
+ constructor: "TestActor",
+ prefix: "testActor"
+ };
+ let testActorFront = yield registryFront.registerActor(ACTOR_URL, options);
+ return testActorFront;
+ });
+
+ // Load the test actor in a custom sandbox as we can't use SDK module loader with URIs
+ let loadFront = Task.async(function* () {
+ let sourceText = yield request(ACTOR_URL);
+ const principal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
+ const sandbox = Cu.Sandbox(principal);
+ sandbox.exports = {};
+ sandbox.require = require;
+ Cu.evalInSandbox(sourceText, sandbox, "1.8", ACTOR_URL, 1);
+ return sandbox.exports;
+ });
+
+ // Ensure fetching a live TabActor form for the targeted app
+ // (helps fetching the test actor registered dynamically)
+ let getUpdatedForm = function (client, tab) {
+ return client.getTab({tab: tab})
+ .then(response => response.tab);
+ };
+
+ // Spawn an instance of the test actor for the given toolbox
+ exports.getTestActor = Task.async(function* (toolbox) {
+ let client = toolbox.target.client;
+ return getTestActor(client, toolbox.target.tab, toolbox);
+ });
+
+ // Sometimes, we need the test actor before opening or without a toolbox then just
+ // create a front for the given `tab`
+ exports.getTestActorWithoutToolbox = Task.async(function* (tab) {
+ let { DebuggerServer } = require("devtools/server/main");
+ let { DebuggerClient } = require("devtools/shared/client/main");
+
+ // We need to spawn a client instance,
+ // but for that we have to first ensure a server is running
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+
+ yield client.connect();
+
+ // We also need to make sure the test actor is registered on the server.
+ yield exports.registerTestActor(client);
+
+ return getTestActor(client, tab);
+ });
+
+ // Fetch the content of a URI
+ let request = function (uri) {
+ return fetch(uri).then(({ content }) => content);
+ };
+
+ let getTestActor = Task.async(function* (client, tab, toolbox) {
+ // We may have to update the form in order to get the dynamically registered
+ // test actor.
+ let form = yield getUpdatedForm(client, tab);
+
+ let { TestActorFront } = yield loadFront();
+
+ return new TestActorFront(client, form, toolbox);
+ });
+})(this);
diff --git a/devtools/client/shared/test/test-actor.js b/devtools/client/shared/test/test-actor.js
new file mode 100644
index 000000000..3aab5287b
--- /dev/null
+++ b/devtools/client/shared/test/test-actor.js
@@ -0,0 +1,1138 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* exported TestActor, TestActorFront */
+
+"use strict";
+
+// A helper actor for inspector and markupview tests.
+
+const { Cc, Ci, Cu } = require("chrome");
+const {getRect, getElementFromPoint, getAdjustedQuads} = require("devtools/shared/layout/utils");
+const defer = require("devtools/shared/defer");
+const {Task} = require("devtools/shared/task");
+const {isContentStylesheet} = require("devtools/shared/inspector/css-logic");
+const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+const loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader);
+
+// Set up a dummy environment so that EventUtils works. We need to be careful to
+// pass a window object into each EventUtils method we call rather than having
+// it rely on the |window| global.
+let EventUtils = {};
+EventUtils.window = {};
+EventUtils.parent = {};
+/* eslint-disable camelcase */
+EventUtils._EU_Ci = Components.interfaces;
+EventUtils._EU_Cc = Components.classes;
+/* eslint-disable camelcase */
+loader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+const protocol = require("devtools/shared/protocol");
+const {Arg, RetVal} = protocol;
+
+const dumpn = msg => {
+ dump(msg + "\n");
+};
+
+/**
+ * Get the instance of CanvasFrameAnonymousContentHelper used by a given
+ * highlighter actor.
+ * The instance provides methods to get/set attributes/text/style on nodes of
+ * the highlighter, inserted into the nsCanvasFrame.
+ * @see /devtools/server/actors/highlighters.js
+ * @param {String} actorID
+ */
+function getHighlighterCanvasFrameHelper(conn, actorID) {
+ let actor = conn.getActor(actorID);
+ if (actor && actor._highlighter) {
+ return actor._highlighter.markup;
+ }
+ return null;
+}
+
+var testSpec = protocol.generateActorSpec({
+ typeName: "testActor",
+
+ methods: {
+ getNumberOfElementMatches: {
+ request: {
+ selector: Arg(0, "string"),
+ },
+ response: {
+ value: RetVal("number")
+ }
+ },
+ getHighlighterAttribute: {
+ request: {
+ nodeID: Arg(0, "string"),
+ name: Arg(1, "string"),
+ actorID: Arg(2, "string")
+ },
+ response: {
+ value: RetVal("string")
+ }
+ },
+ getHighlighterNodeTextContent: {
+ request: {
+ nodeID: Arg(0, "string"),
+ actorID: Arg(1, "string")
+ },
+ response: {
+ value: RetVal("string")
+ }
+ },
+ getSelectorHighlighterBoxNb: {
+ request: {
+ highlighter: Arg(0, "string"),
+ },
+ response: {
+ value: RetVal("number")
+ }
+ },
+ changeHighlightedNodeWaitForUpdate: {
+ request: {
+ name: Arg(0, "string"),
+ value: Arg(1, "string"),
+ actorID: Arg(2, "string")
+ },
+ response: {}
+ },
+ waitForHighlighterEvent: {
+ request: {
+ event: Arg(0, "string"),
+ actorID: Arg(1, "string")
+ },
+ response: {}
+ },
+ waitForEventOnNode: {
+ request: {
+ eventName: Arg(0, "string"),
+ selector: Arg(1, "nullable:string")
+ },
+ response: {}
+ },
+ changeZoomLevel: {
+ request: {
+ level: Arg(0, "string"),
+ actorID: Arg(1, "string"),
+ },
+ response: {}
+ },
+ assertElementAtPoint: {
+ request: {
+ x: Arg(0, "number"),
+ y: Arg(1, "number"),
+ selector: Arg(2, "string")
+ },
+ response: {
+ value: RetVal("boolean")
+ }
+ },
+ getAllAdjustedQuads: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ synthesizeMouse: {
+ request: {
+ object: Arg(0, "json")
+ },
+ response: {}
+ },
+ synthesizeKey: {
+ request: {
+ args: Arg(0, "json")
+ },
+ response: {}
+ },
+ scrollIntoView: {
+ request: {
+ args: Arg(0, "string")
+ },
+ response: {}
+ },
+ hasPseudoClassLock: {
+ request: {
+ selector: Arg(0, "string"),
+ pseudo: Arg(1, "string")
+ },
+ response: {
+ value: RetVal("boolean")
+ }
+ },
+ loadAndWaitForCustomEvent: {
+ request: {
+ url: Arg(0, "string")
+ },
+ response: {}
+ },
+ hasNode: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("boolean")
+ }
+ },
+ getBoundingClientRect: {
+ request: {
+ selector: Arg(0, "string"),
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ setProperty: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string"),
+ value: Arg(2, "string")
+ },
+ response: {}
+ },
+ getProperty: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string")
+ },
+ response: {
+ value: RetVal("string")
+ }
+ },
+ getAttribute: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string")
+ },
+ response: {
+ value: RetVal("string")
+ }
+ },
+ setAttribute: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string"),
+ value: Arg(2, "string")
+ },
+ response: {}
+ },
+ removeAttribute: {
+ request: {
+ selector: Arg(0, "string"),
+ property: Arg(1, "string")
+ },
+ response: {}
+ },
+ reload: {
+ request: {},
+ response: {}
+ },
+ reloadFrame: {
+ request: {
+ selector: Arg(0, "string"),
+ },
+ response: {}
+ },
+ eval: {
+ request: {
+ js: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("nullable:json")
+ }
+ },
+ scrollWindow: {
+ request: {
+ x: Arg(0, "number"),
+ y: Arg(1, "number"),
+ relative: Arg(2, "nullable:boolean"),
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ reflow: {},
+ getNodeRect: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ getTextNodeRect: {
+ request: {
+ parentSelector: Arg(0, "string"),
+ childNodeIndex: Arg(1, "number")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ getNodeInfo: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
+ getStyleSheetsInfoForNode: {
+ request: {
+ selector: Arg(0, "string")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ }
+ }
+});
+
+var TestActor = exports.TestActor = protocol.ActorClassWithSpec(testSpec, {
+ initialize: function (conn, tabActor, options) {
+ this.conn = conn;
+ this.tabActor = tabActor;
+ },
+
+ get content() {
+ return this.tabActor.window;
+ },
+
+ /**
+ * Helper to retrieve a DOM element.
+ * @param {string | array} selector Either a regular selector string
+ * or a selector array. If an array, each item, except the last one
+ * are considered matching an iframe, so that we can query element
+ * within deep iframes.
+ */
+ _querySelector: function (selector) {
+ let document = this.content.document;
+ if (Array.isArray(selector)) {
+ let fullSelector = selector.join(" >> ");
+ while (selector.length > 1) {
+ let str = selector.shift();
+ let iframe = document.querySelector(str);
+ if (!iframe) {
+ throw new Error("Unable to find element with selector \"" + str + "\"" +
+ " (full selector:" + fullSelector + ")");
+ }
+ if (!iframe.contentWindow) {
+ throw new Error("Iframe selector doesn't target an iframe \"" + str + "\"" +
+ " (full selector:" + fullSelector + ")");
+ }
+ document = iframe.contentWindow.document;
+ }
+ selector = selector.shift();
+ }
+ let node = document.querySelector(selector);
+ if (!node) {
+ throw new Error("Unable to find element with selector \"" + selector + "\"");
+ }
+ return node;
+ },
+ /**
+ * Helper to get the number of elements matching a selector
+ * @param {string} CSS selector.
+ */
+ getNumberOfElementMatches: function (selector, root = this.content.document) {
+ return root.querySelectorAll(selector).length;
+ },
+
+ /**
+ * Get a value for a given attribute name, on one of the elements of the box
+ * model highlighter, given its ID.
+ * @param {Object} msg The msg.data part expects the following properties
+ * - {String} nodeID The full ID of the element to get the attribute for
+ * - {String} name The name of the attribute to get
+ * - {String} actorID The highlighter actor ID
+ * @return {String} The value, if found, null otherwise
+ */
+ getHighlighterAttribute: function (nodeID, name, actorID) {
+ let helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
+ if (helper) {
+ return helper.getAttributeForElement(nodeID, name);
+ }
+ return null;
+ },
+
+ /**
+ * Get the textcontent of one of the elements of the box model highlighter,
+ * given its ID.
+ * @param {String} nodeID The full ID of the element to get the attribute for
+ * @param {String} actorID The highlighter actor ID
+ * @return {String} The textcontent value
+ */
+ getHighlighterNodeTextContent: function (nodeID, actorID) {
+ let value;
+ let helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
+ if (helper) {
+ value = helper.getTextContentForElement(nodeID);
+ }
+ return value;
+ },
+
+ /**
+ * Get the number of box-model highlighters created by the SelectorHighlighter
+ * @param {String} actorID The highlighter actor ID
+ * @return {Number} The number of box-model highlighters created, or null if the
+ * SelectorHighlighter was not found.
+ */
+ getSelectorHighlighterBoxNb: function (actorID) {
+ let highlighter = this.conn.getActor(actorID);
+ let {_highlighter: h} = highlighter;
+ if (!h || !h._highlighters) {
+ return null;
+ }
+ return h._highlighters.length;
+ },
+
+ /**
+ * Subscribe to the box-model highlighter's update event, modify an attribute of
+ * the currently highlighted node and send a message when the highlighter has
+ * updated.
+ * @param {String} the name of the attribute to be changed
+ * @param {String} the new value for the attribute
+ * @param {String} actorID The highlighter actor ID
+ */
+ changeHighlightedNodeWaitForUpdate: function (name, value, actorID) {
+ return new Promise(resolve => {
+ let highlighter = this.conn.getActor(actorID);
+ let {_highlighter: h} = highlighter;
+
+ h.once("updated", resolve);
+
+ h.currentNode.setAttribute(name, value);
+ });
+ },
+
+ /**
+ * Subscribe to a given highlighter event and respond when the event is received.
+ * @param {String} event The name of the highlighter event to listen to
+ * @param {String} actorID The highlighter actor ID
+ */
+ waitForHighlighterEvent: function (event, actorID) {
+ let highlighter = this.conn.getActor(actorID);
+ let {_highlighter: h} = highlighter;
+
+ return h.once(event);
+ },
+
+ /**
+ * Wait for a specific event on a node matching the provided selector.
+ * @param {String} eventName The name of the event to listen to
+ * @param {String} selector Optional: css selector of the node which should
+ * trigger the event. If ommitted, target will be the content window
+ */
+ waitForEventOnNode: function (eventName, selector) {
+ return new Promise(resolve => {
+ let node = selector ? this._querySelector(selector) : this.content;
+ node.addEventListener(eventName, function onEvent() {
+ node.removeEventListener(eventName, onEvent);
+ resolve();
+ });
+ });
+ },
+
+ /**
+ * Change the zoom level of the page.
+ * Optionally subscribe to the box-model highlighter's update event and waiting
+ * for it to refresh before responding.
+ * @param {Number} level The new zoom level
+ * @param {String} actorID Optional. The highlighter actor ID
+ */
+ changeZoomLevel: function (level, actorID) {
+ dumpn("Zooming page to " + level);
+ return new Promise(resolve => {
+ if (actorID) {
+ let actor = this.conn.getActor(actorID);
+ let {_highlighter: h} = actor;
+ h.once("updated", resolve);
+ } else {
+ resolve();
+ }
+
+ let docShell = this.content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ docShell.contentViewer.fullZoom = level;
+ });
+ },
+
+ assertElementAtPoint: function (x, y, selector) {
+ let elementAtPoint = getElementFromPoint(this.content.document, x, y);
+ if (!elementAtPoint) {
+ throw new Error("Unable to find element at (" + x + ", " + y + ")");
+ }
+ let node = this._querySelector(selector);
+ return node == elementAtPoint;
+ },
+
+ /**
+ * Get all box-model regions' adjusted boxquads for the given element
+ * @param {String} selector The node selector to target a given element
+ * @return {Object} An object with each property being a box-model region, each
+ * of them being an object with the p1/p2/p3/p4 properties
+ */
+ getAllAdjustedQuads: function (selector) {
+ let regions = {};
+ let node = this._querySelector(selector);
+ for (let boxType of ["content", "padding", "border", "margin"]) {
+ regions[boxType] = getAdjustedQuads(this.content, node, boxType);
+ }
+
+ return regions;
+ },
+
+ /**
+ * Synthesize a mouse event on an element, after ensuring that it is visible
+ * in the viewport. This handler doesn't send a message back. Consumers
+ * should listen to specific events on the inspector/highlighter to know when
+ * the event got synthesized.
+ * @param {String} selector The node selector to get the node target for the event
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Boolean} center If set to true, x/y will be ignored and
+ * synthesizeMouseAtCenter will be used instead
+ * @param {Object} options Other event options
+ */
+ synthesizeMouse: function ({ selector, x, y, center, options }) {
+ let node = this._querySelector(selector);
+ node.scrollIntoView();
+ if (center) {
+ EventUtils.synthesizeMouseAtCenter(node, options, node.ownerDocument.defaultView);
+ } else {
+ EventUtils.synthesizeMouse(node, x, y, options, node.ownerDocument.defaultView);
+ }
+ },
+
+ /**
+ * Synthesize a key event for an element. This handler doesn't send a message
+ * back. Consumers should listen to specific events on the inspector/highlighter
+ * to know when the event got synthesized.
+ */
+ synthesizeKey: function ({key, options, content}) {
+ EventUtils.synthesizeKey(key, options, this.content);
+ },
+
+ /**
+ * Scroll an element into view.
+ * @param {String} selector The selector for the node to scroll into view.
+ */
+ scrollIntoView: function (selector) {
+ let node = this._querySelector(selector);
+ node.scrollIntoView();
+ },
+
+ /**
+ * Check that an element currently has a pseudo-class lock.
+ * @param {String} selector The node selector to get the pseudo-class from
+ * @param {String} pseudo The pseudoclass to check for
+ * @return {Boolean}
+ */
+ hasPseudoClassLock: function (selector, pseudo) {
+ let node = this._querySelector(selector);
+ return DOMUtils.hasPseudoClassLock(node, pseudo);
+ },
+
+ loadAndWaitForCustomEvent: function (url) {
+ return new Promise(resolve => {
+ // Wait for DOMWindowCreated first, as listening on the current outerwindow
+ // doesn't allow receiving test-page-processing-done.
+ this.tabActor.chromeEventHandler.addEventListener("DOMWindowCreated", () => {
+ this.content.addEventListener(
+ "test-page-processing-done", resolve, { once: true }
+ );
+ }, { once: true });
+
+ this.content.location = url;
+ });
+ },
+
+ hasNode: function (selector) {
+ try {
+ // _querySelector throws if the node doesn't exists
+ this._querySelector(selector);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ /**
+ * Get the bounding rect for a given DOM node once.
+ * @param {String} selector selector identifier to select the DOM node
+ * @return {json} the bounding rect info
+ */
+ getBoundingClientRect: function (selector) {
+ let node = this._querySelector(selector);
+ let rect = node.getBoundingClientRect();
+ // DOMRect can't be stringified directly, so return a simple object instead.
+ return {
+ x: rect.x,
+ y: rect.y,
+ width: rect.width,
+ height: rect.height,
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left
+ };
+ },
+
+ /**
+ * Set a JS property on a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} property The property name
+ * @param {String} value The attribute value
+ */
+ setProperty: function (selector, property, value) {
+ let node = this._querySelector(selector);
+ node[property] = value;
+ },
+
+ /**
+ * Get a JS property on a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} property The property name
+ * @return {String} value The attribute value
+ */
+ getProperty: function (selector, property) {
+ let node = this._querySelector(selector);
+ return node[property];
+ },
+
+ /**
+ * Get an attribute on a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} attribute The attribute name
+ * @return {String} value The attribute value
+ */
+ getAttribute: function (selector, attribute) {
+ let node = this._querySelector(selector);
+ return node.getAttribute(attribute);
+ },
+
+ /**
+ * Set an attribute on a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} attribute The attribute name
+ * @param {String} value The attribute value
+ */
+ setAttribute: function (selector, attribute, value) {
+ let node = this._querySelector(selector);
+ node.setAttribute(attribute, value);
+ },
+
+ /**
+ * Remove an attribute from a DOM Node.
+ * @param {String} selector The node selector
+ * @param {String} attribute The attribute name
+ */
+ removeAttribute: function (selector, attribute) {
+ let node = this._querySelector(selector);
+ node.removeAttribute(attribute);
+ },
+
+ /**
+ * Reload the content window.
+ */
+ reload: function () {
+ this.content.location.reload();
+ },
+
+ /**
+ * Reload an iframe and wait for its load event.
+ * @param {String} selector The node selector
+ */
+ reloadFrame: function (selector) {
+ let node = this._querySelector(selector);
+
+ let deferred = defer();
+
+ let onLoad = function () {
+ node.removeEventListener("load", onLoad);
+ deferred.resolve();
+ };
+ node.addEventListener("load", onLoad);
+
+ node.contentWindow.location.reload();
+ return deferred.promise;
+ },
+
+ /**
+ * Evaluate a JS string in the context of the content document.
+ * @param {String} js JS string to evaluate
+ * @return {json} The evaluation result
+ */
+ eval: function (js) {
+ // We have to use a sandbox, as CSP prevent us from using eval on apps...
+ let sb = Cu.Sandbox(this.content, { sandboxPrototype: this.content });
+ return Cu.evalInSandbox(js, sb);
+ },
+
+ /**
+ * Scrolls the window to a particular set of coordinates in the document, or
+ * by the given amount if `relative` is set to `true`.
+ *
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Boolean} relative
+ *
+ * @return {Object} An object with x / y properties, representing the number
+ * of pixels that the document has been scrolled horizontally and vertically.
+ */
+ scrollWindow: function (x, y, relative) {
+ if (isNaN(x) || isNaN(y)) {
+ return {};
+ }
+
+ let deferred = defer();
+ this.content.addEventListener("scroll", function onScroll(event) {
+ this.removeEventListener("scroll", onScroll);
+
+ let data = {x: this.content.scrollX, y: this.content.scrollY};
+ deferred.resolve(data);
+ });
+
+ this.content[relative ? "scrollBy" : "scrollTo"](x, y);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Forces the reflow and waits for the next repaint.
+ */
+ reflow: function () {
+ let deferred = defer();
+ this.content.document.documentElement.offsetWidth;
+ this.content.requestAnimationFrame(deferred.resolve);
+
+ return deferred.promise;
+ },
+
+ getNodeRect: Task.async(function* (selector) {
+ let node = this._querySelector(selector);
+ return getRect(this.content, node, this.content);
+ }),
+
+ getTextNodeRect: Task.async(function* (parentSelector, childNodeIndex) {
+ let parentNode = this._querySelector(parentSelector);
+ let node = parentNode.childNodes[childNodeIndex];
+ return getAdjustedQuads(this.content, node)[0].bounds;
+ }),
+
+ /**
+ * Get information about a DOM element, identified by a selector.
+ * @param {String} selector The CSS selector to get the node (can be an array
+ * of selectors to get elements in an iframe).
+ * @return {Object} data Null if selector didn't match any node, otherwise:
+ * - {String} tagName.
+ * - {String} namespaceURI.
+ * - {Number} numChildren The number of children in the element.
+ * - {Array} attributes An array of {name, value, namespaceURI} objects.
+ * - {String} outerHTML.
+ * - {String} innerHTML.
+ * - {String} textContent.
+ */
+ getNodeInfo: function (selector) {
+ let node = this._querySelector(selector);
+ let info = null;
+
+ if (node) {
+ info = {
+ tagName: node.tagName,
+ namespaceURI: node.namespaceURI,
+ numChildren: node.children.length,
+ numNodes: node.childNodes.length,
+ attributes: [...node.attributes].map(({name, value, namespaceURI}) => {
+ return {name, value, namespaceURI};
+ }),
+ outerHTML: node.outerHTML,
+ innerHTML: node.innerHTML,
+ textContent: node.textContent
+ };
+ }
+
+ return info;
+ },
+
+ /**
+ * Get information about the stylesheets which have CSS rules that apply to a given DOM
+ * element, identified by a selector.
+ * @param {String} selector The CSS selector to get the node (can be an array
+ * of selectors to get elements in an iframe).
+ * @return {Array} A list of stylesheet objects, each having the following properties:
+ * - {String} href.
+ * - {Boolean} isContentSheet.
+ */
+ getStyleSheetsInfoForNode: function (selector) {
+ let node = this._querySelector(selector);
+ let domRules = DOMUtils.getCSSStyleRules(node);
+
+ let sheets = [];
+
+ for (let i = 0, n = domRules.Count(); i < n; i++) {
+ let sheet = domRules.GetElementAt(i).parentStyleSheet;
+ sheets.push({
+ href: sheet.href,
+ isContentSheet: isContentStylesheet(sheet)
+ });
+ }
+
+ return sheets;
+ }
+});
+
+var TestActorFront = exports.TestActorFront = protocol.FrontClassWithSpec(testSpec, {
+ initialize: function (client, { testActor }, toolbox) {
+ protocol.Front.prototype.initialize.call(this, client, { actor: testActor });
+ this.manage(this);
+ this.toolbox = toolbox;
+ },
+
+ /**
+ * Zoom the current page to a given level.
+ * @param {Number} level The new zoom level.
+ * @return {Promise} The returned promise will only resolve when the
+ * highlighter has updated to the new zoom level.
+ */
+ zoomPageTo: function (level) {
+ return this.changeZoomLevel(level, this.toolbox.highlighter.actorID);
+ },
+
+ /* eslint-disable max-len */
+ changeHighlightedNodeWaitForUpdate: protocol.custom(function (name, value, highlighter) {
+ /* eslint-enable max-len */
+ return this._changeHighlightedNodeWaitForUpdate(
+ name, value, (highlighter || this.toolbox.highlighter).actorID
+ );
+ }, {
+ impl: "_changeHighlightedNodeWaitForUpdate"
+ }),
+
+ /**
+ * Get the value of an attribute on one of the highlighter's node.
+ * @param {String} nodeID The Id of the node in the highlighter.
+ * @param {String} name The name of the attribute.
+ * @param {Object} highlighter Optional custom highlither to target
+ * @return {String} value
+ */
+ getHighlighterNodeAttribute: function (nodeID, name, highlighter) {
+ return this.getHighlighterAttribute(
+ nodeID, name, (highlighter || this.toolbox.highlighter).actorID
+ );
+ },
+
+ getHighlighterNodeTextContent: protocol.custom(function (nodeID, highlighter) {
+ return this._getHighlighterNodeTextContent(
+ nodeID, (highlighter || this.toolbox.highlighter).actorID
+ );
+ }, {
+ impl: "_getHighlighterNodeTextContent"
+ }),
+
+ /**
+ * Is the highlighter currently visible on the page?
+ */
+ isHighlighting: function () {
+ return this.getHighlighterNodeAttribute("box-model-elements", "hidden")
+ .then(value => value === null);
+ },
+
+ /**
+ * Assert that the box-model highlighter's current position corresponds to the
+ * given node boxquads.
+ * @param {String} selector The node selector to get the boxQuads from
+ * @param {Function} is assertion function to call for equality checks
+ * @param {String} prefix An optional prefix for logging information to the
+ * console.
+ */
+ isNodeCorrectlyHighlighted: Task.async(function* (selector, is, prefix = "") {
+ prefix += (prefix ? " " : "") + selector + " ";
+
+ let boxModel = yield this._getBoxModelStatus();
+ let regions = yield this.getAllAdjustedQuads(selector);
+
+ for (let boxType of ["content", "padding", "border", "margin"]) {
+ let [quad] = regions[boxType];
+ for (let point in boxModel[boxType].points) {
+ is(boxModel[boxType].points[point].x, quad[point].x,
+ prefix + boxType + " point " + point + " x coordinate is correct");
+ is(boxModel[boxType].points[point].y, quad[point].y,
+ prefix + boxType + " point " + point + " y coordinate is correct");
+ }
+ }
+ }),
+
+ /**
+ * Get the current rect of the border region of the box-model highlighter
+ */
+ getSimpleBorderRect: Task.async(function* (toolbox) {
+ let {border} = yield this._getBoxModelStatus(toolbox);
+ let {p1, p2, p4} = border.points;
+
+ return {
+ top: p1.y,
+ left: p1.x,
+ width: p2.x - p1.x,
+ height: p4.y - p1.y
+ };
+ }),
+
+ /**
+ * Get the current positions and visibility of the various box-model highlighter
+ * elements.
+ */
+ _getBoxModelStatus: Task.async(function* () {
+ let isVisible = yield this.isHighlighting();
+
+ let ret = {
+ visible: isVisible
+ };
+
+ for (let region of ["margin", "border", "padding", "content"]) {
+ let points = yield this._getPointsForRegion(region);
+ let visible = yield this._isRegionHidden(region);
+ ret[region] = {points, visible};
+ }
+
+ ret.guides = {};
+ for (let guide of ["top", "right", "bottom", "left"]) {
+ ret.guides[guide] = yield this._getGuideStatus(guide);
+ }
+
+ return ret;
+ }),
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the node matching the
+ * given selector.
+ * @param {String} selector
+ * @return {Boolean}
+ */
+ assertHighlightedNode: Task.async(function* (selector) {
+ let rect = yield this.getNodeRect(selector);
+ return yield this.isNodeRectHighlighted(rect);
+ }),
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the text node that can
+ * be found at a given index within the list of childNodes of a parent element matching
+ * the given selector.
+ * @param {String} parentSelector
+ * @param {Number} childNodeIndex
+ * @return {Boolean}
+ */
+ assertHighlightedTextNode: Task.async(function* (parentSelector, childNodeIndex) {
+ let rect = yield this.getTextNodeRect(parentSelector, childNodeIndex);
+ return yield this.isNodeRectHighlighted(rect);
+ }),
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the given rect.
+ * @param {Object} rect
+ * @return {Boolean}
+ */
+ isNodeRectHighlighted: Task.async(function* ({ left, top, width, height }) {
+ let {visible, border} = yield this._getBoxModelStatus();
+ let points = border.points;
+ if (!visible) {
+ return false;
+ }
+
+ // Check that the node is within the box model
+ let right = left + width;
+ let bottom = top + height;
+
+ // Converts points dictionnary into an array
+ let list = [];
+ for (let i = 1; i <= 4; i++) {
+ let p = points["p" + i];
+ list.push([p.x, p.y]);
+ }
+ points = list;
+
+ // Check that each point of the node is within the box model
+ return isInside([left, top], points) &&
+ isInside([right, top], points) &&
+ isInside([right, bottom], points) &&
+ isInside([left, bottom], points);
+ }),
+
+ /**
+ * Get the coordinate (points attribute) from one of the polygon elements in the
+ * box model highlighter.
+ */
+ _getPointsForRegion: Task.async(function* (region) {
+ let d = yield this.getHighlighterNodeAttribute("box-model-" + region, "d");
+
+ let polygons = d.match(/M[^M]+/g);
+ if (!polygons) {
+ return null;
+ }
+
+ let points = polygons[0].trim().split(" ").map(i => {
+ return i.replace(/M|L/, "").split(",");
+ });
+
+ return {
+ p1: {
+ x: parseFloat(points[0][0]),
+ y: parseFloat(points[0][1])
+ },
+ p2: {
+ x: parseFloat(points[1][0]),
+ y: parseFloat(points[1][1])
+ },
+ p3: {
+ x: parseFloat(points[2][0]),
+ y: parseFloat(points[2][1])
+ },
+ p4: {
+ x: parseFloat(points[3][0]),
+ y: parseFloat(points[3][1])
+ }
+ };
+ }),
+
+ /**
+ * Is a given region polygon element of the box-model highlighter currently
+ * hidden?
+ */
+ _isRegionHidden: Task.async(function* (region) {
+ let value = yield this.getHighlighterNodeAttribute("box-model-" + region, "hidden");
+ return value !== null;
+ }),
+
+ _getGuideStatus: Task.async(function* (location) {
+ let id = "box-model-guide-" + location;
+
+ let hidden = yield this.getHighlighterNodeAttribute(id, "hidden");
+ let x1 = yield this.getHighlighterNodeAttribute(id, "x1");
+ let y1 = yield this.getHighlighterNodeAttribute(id, "y1");
+ let x2 = yield this.getHighlighterNodeAttribute(id, "x2");
+ let y2 = yield this.getHighlighterNodeAttribute(id, "y2");
+
+ return {
+ visible: !hidden,
+ x1: x1,
+ y1: y1,
+ x2: x2,
+ y2: y2
+ };
+ }),
+
+ /**
+ * Get the coordinates of the rectangle that is defined by the 4 guides displayed
+ * in the toolbox box-model highlighter.
+ * @return {Object} Null if at least one guide is hidden. Otherwise an object
+ * with p1, p2, p3, p4 properties being {x, y} objects.
+ */
+ getGuidesRectangle: Task.async(function* () {
+ let tGuide = yield this._getGuideStatus("top");
+ let rGuide = yield this._getGuideStatus("right");
+ let bGuide = yield this._getGuideStatus("bottom");
+ let lGuide = yield this._getGuideStatus("left");
+
+ if (!tGuide.visible || !rGuide.visible || !bGuide.visible || !lGuide.visible) {
+ return null;
+ }
+
+ return {
+ p1: {x: lGuide.x1, y: tGuide.y1},
+ p2: {x: rGuide.x1, y: tGuide. y1},
+ p3: {x: rGuide.x1, y: bGuide.y1},
+ p4: {x: lGuide.x1, y: bGuide.y1}
+ };
+ }),
+
+ waitForHighlighterEvent: protocol.custom(function (event) {
+ return this._waitForHighlighterEvent(event, this.toolbox.highlighter.actorID);
+ }, {
+ impl: "_waitForHighlighterEvent"
+ }),
+
+ /**
+ * Get the "d" attribute value for one of the box-model highlighter's region
+ * <path> elements, and parse it to a list of points.
+ * @param {String} region The box model region name.
+ * @param {Front} highlighter The front of the highlighter.
+ * @return {Object} The object returned has the following form:
+ * - d {String} the d attribute value
+ * - points {Array} an array of all the polygons defined by the path. Each box
+ * is itself an Array of points, themselves being [x,y] coordinates arrays.
+ */
+ getHighlighterRegionPath: Task.async(function* (region, highlighter) {
+ let d = yield this.getHighlighterNodeAttribute(
+ `box-model-${region}`, "d", highlighter
+ );
+ if (!d) {
+ return {d: null};
+ }
+
+ let polygons = d.match(/M[^M]+/g);
+ if (!polygons) {
+ return {d};
+ }
+
+ let points = [];
+ for (let polygon of polygons) {
+ points.push(polygon.trim().split(" ").map(i => {
+ return i.replace(/M|L/, "").split(",");
+ }));
+ }
+
+ return {d, points};
+ })
+});
+
+/**
+ * Check whether a point is included in a polygon.
+ * Taken and tweaked from:
+ * https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85
+ * @param {Array} point [x,y] coordinates
+ * @param {Array} polygon An array of [x,y] points
+ * @return {Boolean}
+ */
+function isInside(point, polygon) {
+ if (polygon.length === 0) {
+ return false;
+ }
+
+ const n = polygon.length;
+ const newPoints = polygon.slice(0);
+ newPoints.push(polygon[0]);
+ let wn = 0;
+
+ // loop through all edges of the polygon
+ for (let i = 0; i < n; i++) {
+ // Accept points on the edges
+ let r = isLeft(newPoints[i], newPoints[i + 1], point);
+ if (r === 0) {
+ return true;
+ }
+ if (newPoints[i][1] <= point[1]) {
+ if (newPoints[i + 1][1] > point[1] && r > 0) {
+ wn++;
+ }
+ } else if (newPoints[i + 1][1] <= point[1] && r < 0) {
+ wn--;
+ }
+ }
+ if (wn === 0) {
+ dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
+ }
+ // the point is outside only when this winding number wn===0, otherwise it's inside
+ return wn !== 0;
+}
+
+function isLeft(p0, p1, p2) {
+ let l = ((p1[0] - p0[0]) * (p2[1] - p0[1])) -
+ ((p2[0] - p0[0]) * (p1[1] - p0[1]));
+ return l;
+}
diff --git a/devtools/client/shared/test/unit/.eslintrc.js b/devtools/client/shared/test/unit/.eslintrc.js
new file mode 100644
index 000000000..59adf410a
--- /dev/null
+++ b/devtools/client/shared/test/unit/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ "extends": "../../../../.eslintrc.xpcshell.js"
+};
diff --git a/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js b/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js
new file mode 100644
index 000000000..5f4438234
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 that VariablesView._doSearch() works even without an attached
+// VariablesViewController (bug 1196341).
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+const DOMParser = Cc["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Ci.nsIDOMParser);
+const { VariablesView } =
+ Cu.import("resource://devtools/client/shared/widgets/VariablesView.jsm", {});
+
+function run_test() {
+ let doc = DOMParser.parseFromString("<div>", "text/html");
+ let container = doc.body.firstChild;
+ ok(container, "Got a container.");
+
+ let vv = new VariablesView(container, { searchEnabled: true });
+ let scope = vv.addScope("Test scope");
+ let item1 = scope.addItem("a", { value: "1" });
+ let item2 = scope.addItem("b", { value: "2" });
+
+ do_print("Performing a search without a controller.");
+ vv._doSearch("a");
+
+ equal(item1.target.hasAttribute("unmatched"), false,
+ "First item that matched the filter is visible.");
+ equal(item2.target.hasAttribute("unmatched"), true,
+ "The second item that did not match the filter is hidden.");
+}
diff --git a/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js b/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js
new file mode 100644
index 000000000..a70c870bb
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+const { VariablesView } = Components.utils.import("resource://devtools/client/shared/widgets/VariablesView.jsm", {});
+
+const PENDING = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "pending"
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+const FULFILLED = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "fulfilled",
+ "value": 10
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+const REJECTED = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "rejected",
+ "reason": 10
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+function run_test() {
+ equal(VariablesView.getString(PENDING, { concise: true }), "Promise");
+ equal(VariablesView.getString(PENDING), 'Promise {<state>: "pending"}');
+
+ equal(VariablesView.getString(FULFILLED, { concise: true }), "Promise");
+ equal(VariablesView.getString(FULFILLED),
+ 'Promise {<state>: "fulfilled", <value>: 10}');
+
+ equal(VariablesView.getString(REJECTED, { concise: true }), "Promise");
+ equal(VariablesView.getString(REJECTED), 'Promise {<state>: "rejected", <reason>: 10}');
+}
diff --git a/devtools/client/shared/test/unit/test_advanceValidate.js b/devtools/client/shared/test/unit/test_advanceValidate.js
new file mode 100644
index 000000000..2b3122a6f
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_advanceValidate.js
@@ -0,0 +1,31 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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";
+
+// Tests the advanceValidate function from rule-view.js.
+
+const {utils: Cu, interfaces: Ci} = Components;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {advanceValidate} = require("devtools/client/inspector/shared/utils");
+
+// 1 2 3
+// 0123456789012345678901234567890
+const sampleInput = '\\symbol "string" url(somewhere)';
+
+function testInsertion(where, result, testName) {
+ do_print(testName);
+ equal(advanceValidate(Ci.nsIDOMKeyEvent.DOM_VK_SEMICOLON, sampleInput, where),
+ result, "testing advanceValidate at " + where);
+}
+
+function run_test() {
+ testInsertion(4, true, "inside a symbol");
+ testInsertion(1, false, "after a backslash");
+ testInsertion(8, true, "after whitespace");
+ testInsertion(11, false, "inside a string");
+ testInsertion(24, false, "inside a URL");
+ testInsertion(31, true, "at the end");
+}
diff --git a/devtools/client/shared/test/unit/test_attribute-parsing-01.js b/devtools/client/shared/test/unit/test_attribute-parsing-01.js
new file mode 100644
index 000000000..b6b1a301d
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_attribute-parsing-01.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test splitBy from node-attribute-parser.js
+
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {splitBy} = require("devtools/client/shared/node-attribute-parser");
+
+const TEST_DATA = [{
+ value: "this is a test",
+ splitChar: " ",
+ expected: [
+ {value: "this"},
+ {value: " ", type: "string"},
+ {value: "is"},
+ {value: " ", type: "string"},
+ {value: "a"},
+ {value: " ", type: "string"},
+ {value: "test"}
+ ]
+}, {
+ value: "/path/to/handler",
+ splitChar: " ",
+ expected: [
+ {value: "/path/to/handler"}
+ ]
+}, {
+ value: "test",
+ splitChar: " ",
+ expected: [
+ {value: "test"}
+ ]
+}, {
+ value: " test ",
+ splitChar: " ",
+ expected: [
+ {value: " ", type: "string"},
+ {value: "test"},
+ {value: " ", type: "string"}
+ ]
+}, {
+ value: "",
+ splitChar: " ",
+ expected: []
+}, {
+ value: " ",
+ splitChar: " ",
+ expected: [
+ {value: " ", type: "string"},
+ {value: " ", type: "string"},
+ {value: " ", type: "string"}
+ ]
+}];
+
+function run_test() {
+ for (let {value, splitChar, expected} of TEST_DATA) {
+ do_print("Splitting string: " + value);
+ let tokens = splitBy(value, splitChar);
+
+ do_print("Checking that the number of parsed tokens is correct");
+ do_check_eq(tokens.length, expected.length);
+
+ for (let i = 0; i < tokens.length; i++) {
+ do_print("Checking the data in token " + i);
+ do_check_eq(tokens[i].value, expected[i].value);
+ if (expected[i].type) {
+ do_check_eq(tokens[i].type, expected[i].type);
+ }
+ }
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_attribute-parsing-02.js b/devtools/client/shared/test/unit/test_attribute-parsing-02.js
new file mode 100644
index 000000000..2c24d8f05
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_attribute-parsing-02.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test parseAttribute from node-attribute-parser.js
+
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {parseAttribute} = require("devtools/client/shared/node-attribute-parser");
+
+const TEST_DATA = [{
+ tagName: "body",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "class",
+ attributeValue: "some css class names",
+ expected: [
+ {value: "some css class names", type: "string"}
+ ]
+}, {
+ tagName: "box",
+ namespaceURI: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ attributeName: "datasources",
+ attributeValue: "/url/1?test=1#test http://mozilla.org/wow",
+ expected: [
+ {value: "/url/1?test=1#test", type: "uri"},
+ {value: " ", type: "string"},
+ {value: "http://mozilla.org/wow", type: "uri"}
+ ]
+}, {
+ tagName: "form",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "action",
+ attributeValue: "/path/to/handler",
+ expected: [
+ {value: "/path/to/handler", type: "uri"}
+ ]
+}, {
+ tagName: "a",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "ping",
+ attributeValue: "http://analytics.com/track?id=54 http://analytics.com/track?id=55",
+ expected: [
+ {value: "http://analytics.com/track?id=54", type: "uri"},
+ {value: " ", type: "string"},
+ {value: "http://analytics.com/track?id=55", type: "uri"}
+ ]
+}, {
+ tagName: "link",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "href",
+ attributeValue: "styles.css",
+ otherAttributes: [{name: "rel", value: "stylesheet"}],
+ expected: [
+ {value: "styles.css", type: "cssresource"}
+ ]
+}, {
+ tagName: "link",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "href",
+ attributeValue: "styles.css",
+ expected: [
+ {value: "styles.css", type: "uri"}
+ ]
+}, {
+ tagName: "output",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "for",
+ attributeValue: "element-id something id",
+ expected: [
+ {value: "element-id", type: "idref"},
+ {value: " ", type: "string"},
+ {value: "something", type: "idref"},
+ {value: " ", type: "string"},
+ {value: "id", type: "idref"}
+ ]
+}, {
+ tagName: "img",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "contextmenu",
+ attributeValue: "id-of-menu",
+ expected: [
+ {value: "id-of-menu", type: "idref"}
+ ]
+}, {
+ tagName: "img",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "src",
+ attributeValue: "omg-thats-so-funny.gif",
+ expected: [
+ {value: "omg-thats-so-funny.gif", type: "uri"}
+ ]
+}, {
+ tagName: "key",
+ namespaceURI: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ attributeName: "command",
+ attributeValue: "some_command_id",
+ expected: [
+ {value: "some_command_id", type: "idref"}
+ ]
+}, {
+ tagName: "script",
+ namespaceURI: "whatever",
+ attributeName: "src",
+ attributeValue: "script.js",
+ expected: [
+ {value: "script.js", type: "jsresource"}
+ ]
+}];
+
+function run_test() {
+ for (let {tagName, namespaceURI, attributeName,
+ otherAttributes, attributeValue, expected} of TEST_DATA) {
+ do_print("Testing <" + tagName + " " + attributeName + "='" + attributeValue + "'>");
+
+ let attributes = [
+ ...otherAttributes || [],
+ { name: attributeName, value: attributeValue }
+ ];
+ let tokens = parseAttribute(namespaceURI, tagName, attributes, attributeName);
+ if (!expected) {
+ do_check_true(!tokens);
+ continue;
+ }
+
+ do_print("Checking that the number of parsed tokens is correct");
+ do_check_eq(tokens.length, expected.length);
+
+ for (let i = 0; i < tokens.length; i++) {
+ do_print("Checking the data in token " + i);
+ do_check_eq(tokens[i].value, expected[i].value);
+ do_check_eq(tokens[i].type, expected[i].type);
+ }
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_bezierCanvas.js b/devtools/client/shared/test/unit/test_bezierCanvas.js
new file mode 100644
index 000000000..1decceebb
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_bezierCanvas.js
@@ -0,0 +1,117 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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";
+
+// Tests the BezierCanvas API in the CubicBezierWidget module
+
+var Cu = Components.utils;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var {CubicBezier, BezierCanvas} = require("devtools/client/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+ offsetsGetterReturnsData();
+ convertsOffsetsToCoordinates();
+ plotsCanvas();
+}
+
+function offsetsGetterReturnsData() {
+ do_print("offsets getter returns an array of 2 offset objects");
+
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+ let offsets = b.offsets;
+
+ do_check_eq(offsets.length, 2);
+
+ do_check_true("top" in offsets[0]);
+ do_check_true("left" in offsets[0]);
+ do_check_true("top" in offsets[1]);
+ do_check_true("left" in offsets[1]);
+
+ do_check_eq(offsets[0].top, "300px");
+ do_check_eq(offsets[0].left, "0px");
+ do_check_eq(offsets[1].top, "100px");
+ do_check_eq(offsets[1].left, "200px");
+
+ do_print("offsets getter returns data according to current padding");
+
+ b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [0, 0]);
+ offsets = b.offsets;
+
+ do_check_eq(offsets[0].top, "400px");
+ do_check_eq(offsets[0].left, "0px");
+ do_check_eq(offsets[1].top, "0px");
+ do_check_eq(offsets[1].left, "200px");
+}
+
+function convertsOffsetsToCoordinates() {
+ do_print("Converts offsets to coordinates");
+
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+
+ let coordinates = b.offsetsToCoordinates({style: {
+ left: "0px",
+ top: "0px"
+ }});
+ do_check_eq(coordinates.length, 2);
+ do_check_eq(coordinates[0], 0);
+ do_check_eq(coordinates[1], 1.5);
+
+ coordinates = b.offsetsToCoordinates({style: {
+ left: "0px",
+ top: "300px"
+ }});
+ do_check_eq(coordinates[0], 0);
+ do_check_eq(coordinates[1], 0);
+
+ coordinates = b.offsetsToCoordinates({style: {
+ left: "200px",
+ top: "100px"
+ }});
+ do_check_eq(coordinates[0], 1);
+ do_check_eq(coordinates[1], 1);
+}
+
+function plotsCanvas() {
+ do_print("Plots the curve to the canvas");
+
+ let hasDrawnCurve = false;
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+ b.ctx.bezierCurveTo = () => {
+ hasDrawnCurve = true;
+ };
+ b.plot();
+
+ do_check_true(hasDrawnCurve);
+}
+
+function getCubicBezier() {
+ return new CubicBezier([0, 0, 1, 1]);
+}
+
+function getCanvasMock(w = 200, h = 400) {
+ return {
+ getContext: function () {
+ return {
+ scale: () => {},
+ translate: () => {},
+ clearRect: () => {},
+ beginPath: () => {},
+ closePath: () => {},
+ moveTo: () => {},
+ lineTo: () => {},
+ stroke: () => {},
+ arc: () => {},
+ fill: () => {},
+ bezierCurveTo: () => {},
+ save: () => {},
+ restore: () => {},
+ setTransform: () => {}
+ };
+ },
+ width: w,
+ height: h
+ };
+}
diff --git a/devtools/client/shared/test/unit/test_cssAngle.js b/devtools/client/shared/test/unit/test_cssAngle.js
new file mode 100644
index 000000000..ecb93bc8f
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssAngle.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test classifyAngle.
+
+"use strict";
+
+var Cu = Components.utils;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+const {angleUtils} = require("devtools/client/shared/css-angle");
+
+const CLASSIFY_TESTS = [
+ { input: "180deg", output: "deg" },
+ { input: "-180deg", output: "deg" },
+ { input: "180DEG", output: "deg" },
+ { input: "200rad", output: "rad" },
+ { input: "-200rad", output: "rad" },
+ { input: "200RAD", output: "rad" },
+ { input: "0.5grad", output: "grad" },
+ { input: "-0.5grad", output: "grad" },
+ { input: "0.5GRAD", output: "grad" },
+ { input: "0.33turn", output: "turn" },
+ { input: "0.33TURN", output: "turn" },
+ { input: "-0.33turn", output: "turn" }
+];
+
+function run_test() {
+ for (let test of CLASSIFY_TESTS) {
+ let result = angleUtils.classifyAngle(test.input);
+ equal(result, test.output, "test classifyAngle(" + test.input + ")");
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_cssColor-01.js b/devtools/client/shared/test/unit/test_cssColor-01.js
new file mode 100644
index 000000000..13b9b5fa0
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColor-01.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test classifyColor.
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {colorUtils} = require("devtools/shared/css/color");
+
+loader.lazyGetter(this, "DOMUtils", function () {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+const CLASSIFY_TESTS = [
+ { input: "rgb(255,0,192)", output: "rgb" },
+ { input: "RGB(255,0,192)", output: "rgb" },
+ { input: "RGB(100%,0%,83%)", output: "rgb" },
+ { input: "rgba(255,0,192, 0.25)", output: "rgb" },
+ { input: "hsl(5, 5%, 5%)", output: "hsl" },
+ { input: "hsla(5, 5%, 5%, 0.25)", output: "hsl" },
+ { input: "hSlA(5, 5%, 5%, 0.25)", output: "hsl" },
+ { input: "#f0c", output: "hex" },
+ { input: "#f0c0", output: "hex" },
+ { input: "#fe01cb", output: "hex" },
+ { input: "#fe01cb80", output: "hex" },
+ { input: "#FE01CB", output: "hex" },
+ { input: "#FE01CB80", output: "hex" },
+ { input: "blue", output: "name" },
+ { input: "orange", output: "name" }
+];
+
+function compareWithDomutils(input, isColor) {
+ let ours = colorUtils.colorToRGBA(input);
+ let platform = DOMUtils.colorToRGBA(input);
+ deepEqual(ours, platform, "color " + input + " matches DOMUtils");
+ if (isColor) {
+ ok(ours !== null, "'" + input + "' is a color");
+ } else {
+ ok(ours === null, "'" + input + "' is not a color");
+ }
+}
+
+function run_test() {
+ for (let test of CLASSIFY_TESTS) {
+ let result = colorUtils.classifyColor(test.input);
+ equal(result, test.output, "test classifyColor(" + test.input + ")");
+
+ let obj = new colorUtils.CssColor("purple");
+ obj.setAuthoredUnitFromColor(test.input);
+ equal(obj.colorUnit, test.output,
+ "test setAuthoredUnitFromColor(" + test.input + ")");
+
+ // Check that our implementation matches DOMUtils.
+ compareWithDomutils(test.input, true);
+
+ // And check some obvious errors.
+ compareWithDomutils("mumble" + test.input, false);
+ compareWithDomutils(test.input + "trailingstuff", false);
+ }
+
+ // Regression test for bug 1303826.
+ let black = new colorUtils.CssColor("#000");
+ black.colorUnit = "name";
+ equal(black.toString(), "black", "test non-upper-case color cycling");
+
+ let upper = new colorUtils.CssColor("BLACK");
+ upper.colorUnit = "hex";
+ equal(upper.toString(), "#000", "test upper-case color cycling");
+ upper.colorUnit = "name";
+ equal(upper.toString(), "BLACK", "test upper-case color preservation");
+}
diff --git a/devtools/client/shared/test/unit/test_cssColor-02.js b/devtools/client/shared/test/unit/test_cssColor-02.js
new file mode 100644
index 000000000..c6a039028
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColor-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test color cycling regression - Bug 1303748.
+ *
+ * Values should cycle from a starting value, back to their original values. This can
+ * potentially be a little flaky due to the precision of different color representations.
+ */
+
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {colorUtils} = require("devtools/shared/css/color");
+const getFixtureColorData = require("resource://test/helper_color_data.js");
+
+function run_test() {
+ getFixtureColorData().forEach(({authored, name, hex, hsl, rgb, cycle}) => {
+ if (cycle) {
+ const nameCycled = runCycle(name, cycle);
+ const hexCycled = runCycle(hex, cycle);
+ const hslCycled = runCycle(hsl, cycle);
+ const rgbCycled = runCycle(rgb, cycle);
+ // Cut down on log output by only reporting a single pass/fail for the color.
+ ok(nameCycled && hexCycled && hslCycled && rgbCycled,
+ `${authored} was able to cycle back to the original value`);
+ }
+ });
+}
+
+/**
+ * Test a color cycle to see if a color cycles back to its original value in a fixed
+ * number of steps.
+ *
+ * @param {string} value - The color value, e.g. "#000".
+ * @param {integer) times - The number of times it takes to cycle back to the
+ * original color.
+ */
+function runCycle(value, times) {
+ let color = new colorUtils.CssColor(value);
+ for (let i = 0; i < times; i++) {
+ color.nextColorUnit();
+ color = new colorUtils.CssColor(color.toString());
+ }
+ return color.toString() === value;
+}
diff --git a/devtools/client/shared/test/unit/test_cssColor-03.js b/devtools/client/shared/test/unit/test_cssColor-03.js
new file mode 100644
index 000000000..c3ef5a5c2
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColor-03.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test css-color-4 color function syntax and old-style syntax.
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {colorUtils} = require("devtools/shared/css/color");
+
+loader.lazyGetter(this, "DOMUtils", function () {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+const OLD_STYLE_TESTS = [
+ "rgb(255,0,192)",
+ "RGB(255,0,192)",
+ "RGB(100%,0%,83%)",
+ "rgba(255,0,192,0.25)",
+ "hsl(120, 100%, 40%)",
+ "hsla(120, 100%, 40%, 0.25)",
+ "hSlA(240, 100%, 50%, 0.25)",
+];
+
+const CSS_COLOR_4_TESTS = [
+ "rgb(255.0,0.0,192.0)",
+ "RGB(255 0 192)",
+ "RGB(100% 0% 83% / 0.5)",
+ "RGB(100%,0%,83%,0.5)",
+ "RGB(100%,0%,83%,50%)",
+ "rgba(255,0,192)",
+ "hsl(50deg,15%,25%)",
+ "hsl(240 25% 33%)",
+ "hsl(50deg 25% 33% / 0.25)",
+ "hsl(60 120% 60% / 0.25)",
+ "hSlA(5turn 40% 4%)",
+];
+
+function run_test() {
+ for (let test of OLD_STYLE_TESTS) {
+ let ours = colorUtils.colorToRGBA(test, true);
+ let platform = DOMUtils.colorToRGBA(test);
+ deepEqual(ours, platform, "color " + test + " matches DOMUtils");
+ ok(ours !== null, "'" + test + "' is a color");
+ }
+
+ for (let test of CSS_COLOR_4_TESTS) {
+ let oursOld = colorUtils.colorToRGBA(test, true);
+ let oursNew = colorUtils.colorToRGBA(test, false);
+ let platform = DOMUtils.colorToRGBA(test);
+ notEqual(oursOld, platform, "old style parser for color " + test +
+ " should not match DOMUtils");
+ ok(oursOld === null, "'" + test + "' is not a color with old parser");
+ deepEqual(oursNew, platform, `css-color-4 parser for color ${test} matches DOMUtils`);
+ ok(oursNew !== null, "'" + test + "' is a color with css-color-4 parser");
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_cssColorDatabase.js b/devtools/client/shared/test/unit/test_cssColorDatabase.js
new file mode 100644
index 000000000..eb6363ba4
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColorDatabase.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that css-color-db matches platform.
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+const {colorUtils} = require("devtools/shared/css/color");
+const {cssColors} = require("devtools/shared/css/color-db");
+
+function isValid(colorName) {
+ ok(colorUtils.isValidCSSColor(colorName),
+ colorName + " is valid in database");
+ ok(DOMUtils.isValidCSSColor(colorName),
+ colorName + " is valid in DOMUtils");
+}
+
+function checkOne(colorName, checkName) {
+ let ours = colorUtils.colorToRGBA(colorName);
+ let fromDom = DOMUtils.colorToRGBA(colorName);
+ deepEqual(ours, fromDom, colorName + " agrees with DOMUtils");
+
+ isValid(colorName);
+
+ if (checkName) {
+ let {r, g, b} = ours;
+
+ // The color we got might not map back to the same name; but our
+ // implementation should agree with DOMUtils about which name is
+ // canonical.
+ let ourName = colorUtils.rgbToColorName(r, g, b);
+ let domName = DOMUtils.rgbToColorName(r, g, b);
+
+ equal(ourName, domName,
+ colorName + " canonical name agrees with DOMUtils");
+ }
+}
+
+function run_test() {
+ for (let name in cssColors) {
+ checkOne(name, true);
+ }
+ checkOne("transparent", false);
+
+ // Now check that platform didn't add a new name when we weren't
+ // looking.
+ let names = DOMUtils.getCSSValuesForProperty("background-color");
+ for (let name of names) {
+ if (name !== "hsl" && name !== "hsla" &&
+ name !== "rgb" && name !== "rgba" &&
+ name !== "inherit" && name !== "initial" && name !== "unset") {
+ checkOne(name, true);
+ }
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_cubicBezier.js b/devtools/client/shared/test/unit/test_cubicBezier.js
new file mode 100644
index 000000000..9ed6c4eb1
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cubicBezier.js
@@ -0,0 +1,146 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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";
+
+// Tests the CubicBezier API in the CubicBezierWidget module
+
+var Cu = Components.utils;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var {CubicBezier, _parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+ throwsWhenMissingCoordinates();
+ throwsWhenIncorrectCoordinates();
+ convertsStringCoordinates();
+ coordinatesToStringOutputsAString();
+ pointGettersReturnPointCoordinatesArrays();
+ toStringOutputsCubicBezierValue();
+ toStringOutputsCssPresetValues();
+ testParseTimingFunction();
+}
+
+function throwsWhenMissingCoordinates() {
+ do_check_throws(() => {
+ new CubicBezier();
+ }, "Throws an exception when coordinates are missing");
+}
+
+function throwsWhenIncorrectCoordinates() {
+ do_check_throws(() => {
+ new CubicBezier([]);
+ }, "Throws an exception when coordinates are incorrect (empty array)");
+
+ do_check_throws(() => {
+ new CubicBezier([0, 0]);
+ }, "Throws an exception when coordinates are incorrect (incomplete array)");
+
+ do_check_throws(() => {
+ new CubicBezier(["a", "b", "c", "d"]);
+ }, "Throws an exception when coordinates are incorrect (invalid type)");
+
+ do_check_throws(() => {
+ new CubicBezier([1.5, 0, 1.5, 0]);
+ }, "Throws an exception when coordinates are incorrect (time range invalid)");
+
+ do_check_throws(() => {
+ new CubicBezier([-0.5, 0, -0.5, 0]);
+ }, "Throws an exception when coordinates are incorrect (time range invalid)");
+}
+
+function convertsStringCoordinates() {
+ do_print("Converts string coordinates to numbers");
+ let c = new CubicBezier(["0", "1", ".5", "-2"]);
+
+ do_check_eq(c.coordinates[0], 0);
+ do_check_eq(c.coordinates[1], 1);
+ do_check_eq(c.coordinates[2], .5);
+ do_check_eq(c.coordinates[3], -2);
+}
+
+function coordinatesToStringOutputsAString() {
+ do_print("coordinates.toString() outputs a string representation");
+
+ let c = new CubicBezier(["0", "1", "0.5", "-2"]);
+ let string = c.coordinates.toString();
+ do_check_eq(string, "0,1,.5,-2");
+
+ c = new CubicBezier([1, 1, 1, 1]);
+ string = c.coordinates.toString();
+ do_check_eq(string, "1,1,1,1");
+}
+
+function pointGettersReturnPointCoordinatesArrays() {
+ do_print("Points getters return arrays of coordinates");
+
+ let c = new CubicBezier([0, .2, .5, 1]);
+ do_check_eq(c.P1[0], 0);
+ do_check_eq(c.P1[1], .2);
+ do_check_eq(c.P2[0], .5);
+ do_check_eq(c.P2[1], 1);
+}
+
+function toStringOutputsCubicBezierValue() {
+ do_print("toString() outputs the cubic-bezier() value");
+
+ let c = new CubicBezier([0, 1, 1, 0]);
+ do_check_eq(c.toString(), "cubic-bezier(0,1,1,0)");
+}
+
+function toStringOutputsCssPresetValues() {
+ do_print("toString() outputs the css predefined values");
+
+ let c = new CubicBezier([0, 0, 1, 1]);
+ do_check_eq(c.toString(), "linear");
+
+ c = new CubicBezier([0.25, 0.1, 0.25, 1]);
+ do_check_eq(c.toString(), "ease");
+
+ c = new CubicBezier([0.42, 0, 1, 1]);
+ do_check_eq(c.toString(), "ease-in");
+
+ c = new CubicBezier([0, 0, 0.58, 1]);
+ do_check_eq(c.toString(), "ease-out");
+
+ c = new CubicBezier([0.42, 0, 0.58, 1]);
+ do_check_eq(c.toString(), "ease-in-out");
+}
+
+function testParseTimingFunction() {
+ do_print("test parseTimingFunction");
+
+ for (let test of ["ease", "linear", "ease-in", "ease-out", "ease-in-out"]) {
+ ok(_parseTimingFunction(test), test);
+ }
+
+ ok(!_parseTimingFunction("something"), "non-function token");
+ ok(!_parseTimingFunction("something()"), "non-cubic-bezier function");
+ ok(!_parseTimingFunction("cubic-bezier(something)",
+ "cubic-bezier with non-numeric argument"));
+ ok(!_parseTimingFunction("cubic-bezier(1,2,3:7)",
+ "did not see comma"));
+ ok(!_parseTimingFunction("cubic-bezier(1,2,3,7:",
+ "did not see close paren"));
+ ok(!_parseTimingFunction("cubic-bezier(1,2", "early EOF after number"));
+ ok(!_parseTimingFunction("cubic-bezier(1,2,", "early EOF after comma"));
+ deepEqual(_parseTimingFunction("cubic-bezier(1,2,3,7)"), [1, 2, 3, 7],
+ "correct invocation");
+ deepEqual(_parseTimingFunction("cubic-bezier(1, /* */ 2,3, 7 )"),
+ [1, 2, 3, 7],
+ "correct with comments and whitespace");
+}
+
+function do_check_throws(cb, info) {
+ do_print(info);
+
+ let hasThrown = false;
+ try {
+ cb();
+ } catch (e) {
+ hasThrown = true;
+ }
+
+ do_check_true(hasThrown);
+}
diff --git a/devtools/client/shared/test/unit/test_escapeCSSComment.js b/devtools/client/shared/test/unit/test_escapeCSSComment.js
new file mode 100644
index 000000000..19d8a2902
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_escapeCSSComment.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {escapeCSSComment, _unescapeCSSComment} = require("devtools/shared/css/parsing-utils");
+
+const TEST_DATA = [
+ {
+ input: "simple",
+ expected: "simple"
+ },
+ {
+ input: "/* comment */",
+ expected: "/\\* comment *\\/"
+ },
+ {
+ input: "/* two *//* comments */",
+ expected: "/\\* two *\\//\\* comments *\\/"
+ },
+ {
+ input: "/* nested /\\* comment *\\/ */",
+ expected: "/\\* nested /\\\\* comment *\\\\/ *\\/",
+ }
+];
+
+function run_test() {
+ let i = 0;
+ for (let test of TEST_DATA) {
+ ++i;
+ do_print("Test #" + i);
+
+ let escaped = escapeCSSComment(test.input);
+ equal(escaped, test.expected);
+ let unescaped = _unescapeCSSComment(escaped);
+ equal(unescaped, test.input);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_parseDeclarations.js b/devtools/client/shared/test/unit/test_parseDeclarations.js
new file mode 100644
index 000000000..d400a5359
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_parseDeclarations.js
@@ -0,0 +1,439 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {parseDeclarations, _parseCommentDeclarations} = require("devtools/shared/css/parsing-utils");
+const {isCssPropertyKnown} = require("devtools/server/actors/css-properties");
+
+const TEST_DATA = [
+ // Simple test
+ {
+ input: "p:v;",
+ expected: [{name: "p", value: "v", priority: "", offsets: [0, 4]}]
+ },
+ // Simple test
+ {
+ input: "this:is;a:test;",
+ expected: [
+ {name: "this", value: "is", priority: "", offsets: [0, 8]},
+ {name: "a", value: "test", priority: "", offsets: [8, 15]}
+ ]
+ },
+ // Test a single declaration with semi-colon
+ {
+ input: "name:value;",
+ expected: [{name: "name", value: "value", priority: "", offsets: [0, 11]}]
+ },
+ // Test a single declaration without semi-colon
+ {
+ input: "name:value",
+ expected: [{name: "name", value: "value", priority: "", offsets: [0, 10]}]
+ },
+ // Test multiple declarations separated by whitespaces and carriage
+ // returns and tabs
+ {
+ input: "p1 : v1 ; \t\t \n p2:v2; \n\n\n\n\t p3 : v3;",
+ expected: [
+ {name: "p1", value: "v1", priority: "", offsets: [0, 9]},
+ {name: "p2", value: "v2", priority: "", offsets: [16, 22]},
+ {name: "p3", value: "v3", priority: "", offsets: [32, 45]},
+ ]
+ },
+ // Test simple priority
+ {
+ input: "p1: v1; p2: v2 !important;",
+ expected: [
+ {name: "p1", value: "v1", priority: "", offsets: [0, 7]},
+ {name: "p2", value: "v2", priority: "important", offsets: [8, 26]}
+ ]
+ },
+ // Test simple priority
+ {
+ input: "p1: v1 !important; p2: v2",
+ expected: [
+ {name: "p1", value: "v1", priority: "important", offsets: [0, 18]},
+ {name: "p2", value: "v2", priority: "", offsets: [19, 25]}
+ ]
+ },
+ // Test simple priority
+ {
+ input: "p1: v1 ! important; p2: v2 ! important;",
+ expected: [
+ {name: "p1", value: "v1", priority: "important", offsets: [0, 20]},
+ {name: "p2", value: "v2", priority: "important", offsets: [21, 40]}
+ ]
+ },
+ // Test invalid priority
+ {
+ input: "p1: v1 important;",
+ expected: [
+ {name: "p1", value: "v1 important", priority: "", offsets: [0, 17]}
+ ]
+ },
+ // Test various types of background-image urls
+ {
+ input: "background-image: url(../../relative/image.png)",
+ expected: [{
+ name: "background-image",
+ value: "url(../../relative/image.png)",
+ priority: "",
+ offsets: [0, 47]
+ }]
+ },
+ {
+ input: "background-image: url(http://site.com/test.png)",
+ expected: [{
+ name: "background-image",
+ value: "url(http://site.com/test.png)",
+ priority: "",
+ offsets: [0, 47]
+ }]
+ },
+ {
+ input: "background-image: url(wow.gif)",
+ expected: [{
+ name: "background-image",
+ value: "url(wow.gif)",
+ priority: "",
+ offsets: [0, 30]
+ }]
+ },
+ // Test that urls with :;{} characters in them are parsed correctly
+ {
+ input: "background: red url(\"http://site.com/image{}:;.png?id=4#wat\") "
+ + "repeat top right",
+ expected: [{
+ name: "background",
+ value: "red url(\"http://site.com/image{}:;.png?id=4#wat\") " +
+ "repeat top right",
+ priority: "",
+ offsets: [0, 78]
+ }]
+ },
+ // Test that an empty string results in an empty array
+ {input: "", expected: []},
+ // Test that a string comprised only of whitespaces results in an empty array
+ {input: " \n \n \n \n \t \t\t\t ", expected: []},
+ // Test that a null input throws an exception
+ {input: null, throws: true},
+ // Test that a undefined input throws an exception
+ {input: undefined, throws: true},
+ // Test that :;{} characters in quoted content are not parsed as multiple
+ // declarations
+ {
+ input: "content: \";color:red;}selector{color:yellow;\"",
+ expected: [{
+ name: "content",
+ value: "\";color:red;}selector{color:yellow;\"",
+ priority: "",
+ offsets: [0, 45]
+ }]
+ },
+ // Test that rules aren't parsed, just declarations. So { and } found after a
+ // property name should be part of the property name, same for values.
+ {
+ input: "body {color:red;} p {color: blue;}",
+ expected: [
+ {name: "body {color", value: "red", priority: "", offsets: [0, 16]},
+ {name: "} p {color", value: "blue", priority: "", offsets: [16, 33]},
+ {name: "}", value: "", priority: "", offsets: [33, 34]}
+ ]
+ },
+ // Test unbalanced : and ;
+ {
+ input: "color :red : font : arial;",
+ expected: [
+ {name: "color", value: "red : font : arial", priority: "",
+ offsets: [0, 26]}
+ ]
+ },
+ {
+ input: "background: red;;;;;",
+ expected: [{name: "background", value: "red", priority: "",
+ offsets: [0, 16]}]
+ },
+ {
+ input: "background:;",
+ expected: [{name: "background", value: "", priority: "",
+ offsets: [0, 12]}]
+ },
+ {input: ";;;;;", expected: []},
+ {input: ":;:;", expected: []},
+ // Test name only
+ {input: "color", expected: [
+ {name: "color", value: "", priority: "", offsets: [0, 5]}
+ ]},
+ // Test trailing name without :
+ {input: "color:blue;font", expected: [
+ {name: "color", value: "blue", priority: "", offsets: [0, 11]},
+ {name: "font", value: "", priority: "", offsets: [11, 15]}
+ ]},
+ // Test trailing name with :
+ {input: "color:blue;font:", expected: [
+ {name: "color", value: "blue", priority: "", offsets: [0, 11]},
+ {name: "font", value: "", priority: "", offsets: [11, 16]}
+ ]},
+ // Test leading value
+ {input: "Arial;color:blue;", expected: [
+ {name: "", value: "Arial", priority: "", offsets: [0, 6]},
+ {name: "color", value: "blue", priority: "", offsets: [6, 17]}
+ ]},
+ // Test hex colors
+ {
+ input: "color: #333",
+ expected: [{name: "color", value: "#333", priority: "", offsets: [0, 11]}]
+ },
+ {
+ input: "color: #456789",
+ expected: [{name: "color", value: "#456789", priority: "",
+ offsets: [0, 14]}]
+ },
+ {
+ input: "wat: #XYZ",
+ expected: [{name: "wat", value: "#XYZ", priority: "", offsets: [0, 9]}]
+ },
+ // Test string/url quotes escaping
+ {
+ input: "content: \"this is a 'string'\"",
+ expected: [{name: "content", value: "\"this is a 'string'\"", priority: "",
+ offsets: [0, 29]}]
+ },
+ {
+ input: 'content: "this is a \\"string\\""',
+ expected: [{
+ name: "content",
+ value: '"this is a \\"string\\""',
+ priority: "",
+ offsets: [0, 31]}]
+ },
+ {
+ input: "content: 'this is a \"string\"'",
+ expected: [{
+ name: "content",
+ value: '\'this is a "string"\'',
+ priority: "",
+ offsets: [0, 29]
+ }]
+ },
+ {
+ input: "content: 'this is a \\'string\\''",
+ expected: [{
+ name: "content",
+ value: "'this is a \\'string\\''",
+ priority: "",
+ offsets: [0, 31],
+ }]
+ },
+ {
+ input: "content: 'this \\' is a \" really strange string'",
+ expected: [{
+ name: "content",
+ value: "'this \\' is a \" really strange string'",
+ priority: "",
+ offsets: [0, 47]
+ }]
+ },
+ {
+ input: "content: \"a not s\\ o very long title\"",
+ expected: [{
+ name: "content",
+ value: '"a not s\\ o very long title"',
+ priority: "",
+ offsets: [0, 46]
+ }]
+ },
+ // Test calc with nested parentheses
+ {
+ input: "width: calc((100% - 3em) / 2)",
+ expected: [{name: "width", value: "calc((100% - 3em) / 2)", priority: "",
+ offsets: [0, 29]}]
+ },
+
+ // Simple embedded comment test.
+ {
+ parseComments: true,
+ input: "width: 5; /* background: green; */ background: red;",
+ expected: [{name: "width", value: "5", priority: "", offsets: [0, 9]},
+ {name: "background", value: "green", priority: "",
+ offsets: [13, 31], commentOffsets: [10, 34]},
+ {name: "background", value: "red", priority: "",
+ offsets: [35, 51]}]
+ },
+
+ // Embedded comment where the parsing heuristic fails.
+ {
+ parseComments: true,
+ input: "width: 5; /* background something: green; */ background: red;",
+ expected: [{name: "width", value: "5", priority: "", offsets: [0, 9]},
+ {name: "background", value: "red", priority: "",
+ offsets: [45, 61]}]
+ },
+
+ // Embedded comment where the parsing heuristic is a bit funny.
+ {
+ parseComments: true,
+ input: "width: 5; /* background: */ background: red;",
+ expected: [{name: "width", value: "5", priority: "", offsets: [0, 9]},
+ {name: "background", value: "", priority: "",
+ offsets: [13, 24], commentOffsets: [10, 27]},
+ {name: "background", value: "red", priority: "",
+ offsets: [28, 44]}]
+ },
+
+ // Another case where the parsing heuristic says not to bother; note
+ // that there is no ";" in the comment.
+ {
+ parseComments: true,
+ input: "width: 5; /* background: yellow */ background: red;",
+ expected: [{name: "width", value: "5", priority: "", offsets: [0, 9]},
+ {name: "background", value: "yellow", priority: "",
+ offsets: [13, 31], commentOffsets: [10, 34]},
+ {name: "background", value: "red", priority: "",
+ offsets: [35, 51]}]
+ },
+
+ // Parsing a comment should yield text that has been unescaped, and
+ // the offsets should refer to the original text.
+ {
+ parseComments: true,
+ input: "/* content: '*\\/'; */",
+ expected: [{name: "content", value: "'*/'", priority: "",
+ offsets: [3, 18], commentOffsets: [0, 21]}]
+ },
+
+ // Parsing a comment should yield text that has been unescaped, and
+ // the offsets should refer to the original text. This variant
+ // tests the no-semicolon path.
+ {
+ parseComments: true,
+ input: "/* content: '*\\/' */",
+ expected: [{name: "content", value: "'*/'", priority: "",
+ offsets: [3, 17], commentOffsets: [0, 20]}]
+ },
+
+ // A comment-in-a-comment should yield the correct offsets.
+ {
+ parseComments: true,
+ input: "/* color: /\\* comment *\\/ red; */",
+ expected: [{name: "color", value: "red", priority: "",
+ offsets: [3, 30], commentOffsets: [0, 33]}]
+ },
+
+ // HTML comments are ignored.
+ {
+ parseComments: true,
+ input: "<!-- color: red; --> color: blue;",
+ expected: [{name: "color", value: "red", priority: "",
+ offsets: [5, 16]},
+ {name: "color", value: "blue", priority: "",
+ offsets: [21, 33]}]
+ },
+
+ // Don't error on an empty comment.
+ {
+ parseComments: true,
+ input: "/**/",
+ expected: []
+ },
+
+ // Parsing our special comments skips the name-check heuristic.
+ {
+ parseComments: true,
+ input: "/*! walrus: zebra; */",
+ expected: [{name: "walrus", value: "zebra", priority: "",
+ offsets: [4, 18], commentOffsets: [0, 21]}]
+ },
+
+ // Regression test for bug 1287620.
+ {
+ input: "color: blue \\9 no\\_need",
+ expected: [{name: "color", value: "blue \\9 no_need", priority: "", offsets: [0, 23]}]
+ },
+
+ // Regression test for bug 1297890 - don't paste tokens.
+ {
+ parseComments: true,
+ input: "stroke-dasharray: 1/*ThisIsAComment*/2;",
+ expected: [{name: "stroke-dasharray", value: "1 2", priority: "", offsets: [0, 39]}]
+ },
+];
+
+function run_test() {
+ run_basic_tests();
+ run_comment_tests();
+}
+
+// Test parseDeclarations.
+function run_basic_tests() {
+ for (let test of TEST_DATA) {
+ do_print("Test input string " + test.input);
+ let output;
+ try {
+ output = parseDeclarations(isCssPropertyKnown, test.input,
+ test.parseComments);
+ } catch (e) {
+ do_print("parseDeclarations threw an exception with the given input " +
+ "string");
+ if (test.throws) {
+ do_print("Exception expected");
+ do_check_true(true);
+ } else {
+ do_print("Exception unexpected\n" + e);
+ do_check_true(false);
+ }
+ }
+ if (output) {
+ assertOutput(output, test.expected);
+ }
+ }
+}
+
+const COMMENT_DATA = [
+ {
+ input: "content: 'hi",
+ expected: [{name: "content", value: "'hi", priority: "", terminator: "';",
+ offsets: [2, 14], colonOffsets: [9, 11],
+ commentOffsets: [0, 16]}],
+ },
+ {
+ input: "text that once confounded the parser;",
+ expected: []
+ },
+];
+
+// Test parseCommentDeclarations.
+function run_comment_tests() {
+ for (let test of COMMENT_DATA) {
+ do_print("Test input string " + test.input);
+ let output = _parseCommentDeclarations(isCssPropertyKnown, test.input, 0,
+ test.input.length + 4);
+ deepEqual(output, test.expected);
+ }
+}
+
+function assertOutput(actual, expected) {
+ if (actual.length === expected.length) {
+ for (let i = 0; i < expected.length; i++) {
+ do_check_true(!!actual[i]);
+ do_print("Check that the output item has the expected name, " +
+ "value and priority");
+ do_check_eq(expected[i].name, actual[i].name);
+ do_check_eq(expected[i].value, actual[i].value);
+ do_check_eq(expected[i].priority, actual[i].priority);
+ deepEqual(expected[i].offsets, actual[i].offsets);
+ if ("commentOffsets" in expected[i]) {
+ deepEqual(expected[i].commentOffsets, actual[i].commentOffsets);
+ }
+ }
+ } else {
+ for (let prop of actual) {
+ do_print("Actual output contained: {name: " + prop.name + ", value: " +
+ prop.value + ", priority: " + prop.priority + "}");
+ }
+ do_check_eq(actual.length, expected.length);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js b/devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js
new file mode 100644
index 000000000..ccd778c4a
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js
@@ -0,0 +1,213 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {
+ parsePseudoClassesAndAttributes,
+ SELECTOR_ATTRIBUTE,
+ SELECTOR_ELEMENT,
+ SELECTOR_PSEUDO_CLASS
+} = require("devtools/shared/css/parsing-utils");
+
+const TEST_DATA = [
+ // Test that a null input throws an exception
+ {
+ input: null,
+ throws: true
+ },
+ // Test that a undefined input throws an exception
+ {
+ input: undefined,
+ throws: true
+ },
+ {
+ input: ":root",
+ expected: [
+ { value: ":root", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: ".testclass",
+ expected: [
+ { value: ".testclass", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: "div p",
+ expected: [
+ { value: "div p", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: "div > p",
+ expected: [
+ { value: "div > p", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: "a[hidden]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[hidden]", type: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ input: "a[hidden=true]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[hidden=true]", type: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ input: "a[hidden=true] p:hover",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[hidden=true]", type: SELECTOR_ATTRIBUTE },
+ { value: " p", type: SELECTOR_ELEMENT },
+ { value: ":hover", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "a[checked=\"true\"]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[checked=\"true\"]", type: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ input: "a[title~=test]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[title~=test]", type: SELECTOR_ATTRIBUTE }
+ ]
+ },
+ {
+ input: "h1[hidden=\"true\"][title^=\"Important\"]",
+ expected: [
+ { value: "h1", type: SELECTOR_ELEMENT },
+ { value: "[hidden=\"true\"]", type: SELECTOR_ATTRIBUTE },
+ { value: "[title^=\"Important\"]", type: SELECTOR_ATTRIBUTE}
+ ]
+ },
+ {
+ input: "p:hover",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":hover", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p + .testclass:hover",
+ expected: [
+ { value: "p + .testclass", type: SELECTOR_ELEMENT },
+ { value: ":hover", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p::before",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: "::before", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p:nth-child(2)",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":nth-child(2)", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p:not([title=\"test\"]) .testclass",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":not([title=\"test\"])", type: SELECTOR_PSEUDO_CLASS },
+ { value: " .testclass", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: "a\\:hover",
+ expected: [
+ { value: "a\\:hover", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: ":not(:lang(it))",
+ expected: [
+ { value: ":not(:lang(it))", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p:not(:lang(it))",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":not(:lang(it))", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: "p:not(p:lang(it))",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":not(p:lang(it))", type: SELECTOR_PSEUDO_CLASS }
+ ]
+ },
+ {
+ input: ":not(:lang(it)",
+ expected: [
+ { value: ":not(:lang(it)", type: SELECTOR_ELEMENT }
+ ]
+ },
+ {
+ input: ":not(:lang(it)))",
+ expected: [
+ { value: ":not(:lang(it))", type: SELECTOR_PSEUDO_CLASS },
+ { value: ")", type: SELECTOR_ELEMENT }
+ ]
+ }
+];
+
+function run_test() {
+ for (let test of TEST_DATA) {
+ dump("Test input string " + test.input + "\n");
+ let output;
+
+ try {
+ output = parsePseudoClassesAndAttributes(test.input);
+ } catch (e) {
+ dump("parsePseudoClassesAndAttributes threw an exception with the " +
+ "given input string\n");
+ if (test.throws) {
+ ok(true, "Exception expected");
+ } else {
+ dump();
+ ok(false, "Exception unexpected\n" + e);
+ }
+ }
+
+ if (output) {
+ assertOutput(output, test.expected);
+ }
+ }
+}
+
+function assertOutput(actual, expected) {
+ if (actual.length === expected.length) {
+ for (let i = 0; i < expected.length; i++) {
+ dump("Check that the output item has the expected value and type\n");
+ ok(!!actual[i]);
+ equal(expected[i].value, actual[i].value);
+ equal(expected[i].type, actual[i].type);
+ }
+ } else {
+ for (let prop of actual) {
+ dump("Actual output contained: {value: " + prop.value + ", type: " +
+ prop.type + "}\n");
+ }
+ equal(actual.length, expected.length);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_parseSingleValue.js b/devtools/client/shared/test/unit/test_parseSingleValue.js
new file mode 100644
index 000000000..73e4f0ac4
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_parseSingleValue.js
@@ -0,0 +1,93 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {parseSingleValue} = require("devtools/shared/css/parsing-utils");
+const {isCssPropertyKnown} = require("devtools/server/actors/css-properties");
+
+const TEST_DATA = [
+ {input: null, throws: true},
+ {input: undefined, throws: true},
+ {input: "", expected: {value: "", priority: ""}},
+ {input: " \t \t \n\n ", expected: {value: "", priority: ""}},
+ {input: "blue", expected: {value: "blue", priority: ""}},
+ {input: "blue !important", expected: {value: "blue", priority: "important"}},
+ {input: "blue!important", expected: {value: "blue", priority: "important"}},
+ {input: "blue ! important", expected: {value: "blue", priority: "important"}},
+ {
+ input: "blue ! important",
+ expected: {value: "blue", priority: "important"}
+ },
+ {input: "blue !", expected: {value: "blue", priority: ""}},
+ {input: "blue !mportant", expected: {value: "blue !mportant", priority: ""}},
+ {
+ input: " blue !important ",
+ expected: {value: "blue", priority: "important"}
+ },
+ {
+ input: "url(\"http://url.com/whyWouldYouDoThat!important.png\") !important",
+ expected: {
+ value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+ priority: "important"
+ }
+ },
+ {
+ input: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+ expected: {
+ value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+ priority: ""
+ }
+ },
+ {
+ input: "\"content!important\" !important",
+ expected: {
+ value: "\"content!important\"",
+ priority: "important"
+ }
+ },
+ {
+ input: "\"content!important\"",
+ expected: {
+ value: "\"content!important\"",
+ priority: ""
+ }
+ },
+ {
+ input: "\"all the \\\"'\\\\ special characters\"",
+ expected: {
+ value: "\"all the \\\"'\\\\ special characters\"",
+ priority: ""
+ }
+ }
+];
+
+function run_test() {
+ for (let test of TEST_DATA) {
+ do_print("Test input value " + test.input);
+ try {
+ let output = parseSingleValue(isCssPropertyKnown, test.input);
+ assertOutput(output, test.expected);
+ } catch (e) {
+ do_print("parseSingleValue threw an exception with the given input " +
+ "value");
+ if (test.throws) {
+ do_print("Exception expected");
+ do_check_true(true);
+ } else {
+ do_print("Exception unexpected\n" + e);
+ do_check_true(false);
+ }
+ }
+ }
+}
+
+function assertOutput(actual, expected) {
+ do_print("Check that the output has the expected value and priority");
+ do_check_eq(expected.value, actual.value);
+ do_check_eq(expected.priority, actual.priority);
+}
diff --git a/devtools/client/shared/test/unit/test_rewriteDeclarations.js b/devtools/client/shared/test/unit/test_rewriteDeclarations.js
new file mode 100644
index 000000000..0183ea3c5
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_rewriteDeclarations.js
@@ -0,0 +1,529 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {RuleRewriter} = require("devtools/shared/css/parsing-utils");
+const {isCssPropertyKnown} = require("devtools/server/actors/css-properties");
+
+const TEST_DATA = [
+ {
+ desc: "simple set",
+ input: "p:v;",
+ instruction: {type: "set", name: "p", value: "N", priority: "",
+ index: 0},
+ expected: "p:N;"
+ },
+ {
+ desc: "simple set clearing !important",
+ input: "p:v !important;",
+ instruction: {type: "set", name: "p", value: "N", priority: "",
+ index: 0},
+ expected: "p:N;"
+ },
+ {
+ desc: "simple set adding !important",
+ input: "p:v;",
+ instruction: {type: "set", name: "p", value: "N", priority: "important",
+ index: 0},
+ expected: "p:N !important;"
+ },
+ {
+ desc: "simple set between comments",
+ input: "/*color:red;*/ p:v; /*color:green;*/",
+ instruction: {type: "set", name: "p", value: "N", priority: "",
+ index: 1},
+ expected: "/*color:red;*/ p:N; /*color:green;*/"
+ },
+ // The rule view can generate a "set" with a previously unknown
+ // property index; which should work like "create".
+ {
+ desc: "set at unknown index",
+ input: "a:b; e: f;",
+ instruction: {type: "set", name: "c", value: "d", priority: "",
+ index: 2},
+ expected: "a:b; e: f;c: d;"
+ },
+ {
+ desc: "simple rename",
+ input: "p:v;",
+ instruction: {type: "rename", name: "p", newName: "q", index: 0},
+ expected: "q:v;"
+ },
+ // "rename" is passed the name that the user entered, and must do
+ // any escaping necessary to ensure that this is an identifier.
+ {
+ desc: "rename requiring escape",
+ input: "p:v;",
+ instruction: {type: "rename", name: "p", newName: "a b", index: 0},
+ expected: "a\\ b:v;"
+ },
+ {
+ desc: "simple create",
+ input: "",
+ instruction: {type: "create", name: "p", value: "v", priority: "important",
+ index: 0, enabled: true},
+ expected: "p: v !important;"
+ },
+ {
+ desc: "create between two properties",
+ input: "a:b; e: f;",
+ instruction: {type: "create", name: "c", value: "d", priority: "",
+ index: 1, enabled: true},
+ expected: "a:b; c: d;e: f;"
+ },
+ // "create" is passed the name that the user entered, and must do
+ // any escaping necessary to ensure that this is an identifier.
+ {
+ desc: "create requiring escape",
+ input: "",
+ instruction: {type: "create", name: "a b", value: "d", priority: "",
+ index: 1, enabled: true},
+ expected: "a\\ b: d;"
+ },
+ {
+ desc: "simple disable",
+ input: "p:v;",
+ instruction: {type: "enable", name: "p", value: false, index: 0},
+ expected: "/*! p:v; */"
+ },
+ {
+ desc: "simple enable",
+ input: "/* color:v; */",
+ instruction: {type: "enable", name: "color", value: true, index: 0},
+ expected: "color:v;"
+ },
+ {
+ desc: "enable with following property in comment",
+ input: "/* color:red; color: blue; */",
+ instruction: {type: "enable", name: "color", value: true, index: 0},
+ expected: "color:red; /* color: blue; */"
+ },
+ {
+ desc: "enable with preceding property in comment",
+ input: "/* color:red; color: blue; */",
+ instruction: {type: "enable", name: "color", value: true, index: 1},
+ expected: "/* color:red; */ color: blue;"
+ },
+ {
+ desc: "simple remove",
+ input: "a:b;c:d;e:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "a:b;e:f;"
+ },
+ {
+ desc: "disable with comment ender in string",
+ input: "content: '*/';",
+ instruction: {type: "enable", name: "content", value: false, index: 0},
+ expected: "/*! content: '*\\/'; */"
+ },
+ {
+ desc: "enable with comment ender in string",
+ input: "/* content: '*\\/'; */",
+ instruction: {type: "enable", name: "content", value: true, index: 0},
+ expected: "content: '*/';"
+ },
+ {
+ desc: "enable requiring semicolon insertion",
+ // Note the lack of a trailing semicolon in the comment.
+ input: "/* color:red */ color: blue;",
+ instruction: {type: "enable", name: "color", value: true, index: 0},
+ expected: "color:red; color: blue;"
+ },
+ {
+ desc: "create requiring semicolon insertion",
+ // Note the lack of a trailing semicolon.
+ input: "color: red",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "color: red;a: b;"
+ },
+
+ // Newline insertion.
+ {
+ desc: "simple newline insertion",
+ input: "\ncolor: red;\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\ncolor: red;\na: b;\n"
+ },
+ // Newline insertion.
+ {
+ desc: "semicolon insertion before newline",
+ // Note the lack of a trailing semicolon.
+ input: "\ncolor: red\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\ncolor: red;\na: b;\n"
+ },
+ // Newline insertion.
+ {
+ desc: "newline and semicolon insertion",
+ // Note the lack of a trailing semicolon and newline.
+ input: "\ncolor: red",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\ncolor: red;\na: b;\n"
+ },
+
+ // Newline insertion and indentation.
+ {
+ desc: "indentation with create",
+ input: "\n color: red;\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\n color: red;\n a: b;\n"
+ },
+ // Newline insertion and indentation.
+ {
+ desc: "indentation plus semicolon insertion before newline",
+ // Note the lack of a trailing semicolon.
+ input: "\n color: red\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\n color: red;\n a: b;\n"
+ },
+ {
+ desc: "indentation inserted before trailing whitespace",
+ // Note the trailing whitespace. This could come from a rule
+ // like:
+ // @supports (mumble) {
+ // body {
+ // color: red;
+ // }
+ // }
+ // Here if we create a rule we don't want it to follow
+ // the indentation of the "}".
+ input: "\n color: red;\n ",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\n color: red;\n a: b;\n "
+ },
+ // Newline insertion and indentation.
+ {
+ desc: "indentation comes from preceding comment",
+ // Note how the comment comes before the declaration.
+ input: "\n /* comment */ color: red\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 1, enabled: true},
+ expected: "\n /* comment */ color: red;\n a: b;\n"
+ },
+ // Default indentation.
+ {
+ desc: "use of default indentation",
+ input: "\n",
+ instruction: {type: "create", name: "a", value: "b", priority: "",
+ index: 0, enabled: true},
+ expected: "\n\ta: b;\n"
+ },
+
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion removes newline",
+ input: "a:b;\nc:d;\ne:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "a:b;\ne:f;"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion remove blank line",
+ input: "\n a:b;\n c:d; \ne:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "\n a:b;\ne:f;"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion leaves comment",
+ input: "\n a:b;\n /* something */ c:d; \ne:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "\n a:b;\n /* something */ \ne:f;"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion leaves previous newline",
+ input: "\n a:b;\n c:d; \ne:f;",
+ instruction: {type: "remove", name: "e", index: 2},
+ expected: "\n a:b;\n c:d; \n"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion removes trailing whitespace",
+ input: "\n a:b;\n c:d; \n e:f;",
+ instruction: {type: "remove", name: "e", index: 2},
+ expected: "\n a:b;\n c:d; \n"
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion preserves indentation",
+ input: " a:b;\n c:d; \n e:f;",
+ instruction: {type: "remove", name: "a", index: 0},
+ expected: " c:d; \n e:f;"
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable single quote termination",
+ input: "/* content: 'hi */ color: red;",
+ instruction: {type: "enable", name: "content", value: true, index: 0},
+ expected: "content: 'hi'; color: red;",
+ changed: {0: "'hi'"}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create single quote termination",
+ input: "content: 'hi",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "content: 'hi';color: red;",
+ changed: {0: "'hi'"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable double quote termination",
+ input: "/* content: \"hi */ color: red;",
+ instruction: {type: "enable", name: "content", value: true, index: 0},
+ expected: "content: \"hi\"; color: red;",
+ changed: {0: "\"hi\""}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create double quote termination",
+ input: "content: \"hi",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "content: \"hi\";color: red;",
+ changed: {0: "\"hi\""}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable url termination",
+ input: "/* background-image: url(something.jpg */ color: red;",
+ instruction: {type: "enable", name: "background-image", value: true,
+ index: 0},
+ expected: "background-image: url(something.jpg); color: red;",
+ changed: {0: "url(something.jpg)"}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create url termination",
+ input: "background-image: url(something.jpg",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "background-image: url(something.jpg);color: red;",
+ changed: {0: "url(something.jpg)"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable url single quote termination",
+ input: "/* background-image: url('something.jpg */ color: red;",
+ instruction: {type: "enable", name: "background-image", value: true,
+ index: 0},
+ expected: "background-image: url('something.jpg'); color: red;",
+ changed: {0: "url('something.jpg')"}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create url single quote termination",
+ input: "background-image: url('something.jpg",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "background-image: url('something.jpg');color: red;",
+ changed: {0: "url('something.jpg')"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "create url double quote termination",
+ input: "/* background-image: url(\"something.jpg */ color: red;",
+ instruction: {type: "enable", name: "background-image", value: true,
+ index: 0},
+ expected: "background-image: url(\"something.jpg\"); color: red;",
+ changed: {0: "url(\"something.jpg\")"}
+ },
+ // Termination insertion corner case.
+ {
+ desc: "enable url double quote termination",
+ input: "background-image: url(\"something.jpg",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "background-image: url(\"something.jpg\");color: red;",
+ changed: {0: "url(\"something.jpg\")"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "create backslash termination",
+ input: "something: \\",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "something: \\\\;color: red;",
+ // The lexer rewrites the token before we see it. However this is
+ // so obscure as to be inconsequential.
+ changed: {0: "\uFFFD\\"}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable backslash single quote termination",
+ input: "something: '\\",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "something: '\\\\';color: red;",
+ changed: {0: "'\\\\'"}
+ },
+ {
+ desc: "enable backslash double quote termination",
+ input: "something: \"\\",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "something: \"\\\\\";color: red;",
+ changed: {0: "\"\\\\\""}
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable comment termination",
+ input: "something: blah /* comment ",
+ instruction: {type: "create", name: "color", value: "red", priority: "",
+ index: 1, enabled: true},
+ expected: "something: blah /* comment*/; color: red;"
+ },
+
+ // Rewrite a "heuristic override" comment.
+ {
+ desc: "enable with heuristic override comment",
+ input: "/*! walrus: zebra; */",
+ instruction: {type: "enable", name: "walrus", value: true, index: 0},
+ expected: "walrus: zebra;"
+ },
+
+ // Sanitize a bad value.
+ {
+ desc: "create sanitize unpaired brace",
+ input: "",
+ instruction: {type: "create", name: "p", value: "}", priority: "",
+ index: 0, enabled: true},
+ expected: "p: \\};",
+ changed: {0: "\\}"}
+ },
+ // Sanitize a bad value.
+ {
+ desc: "set sanitize unpaired brace",
+ input: "walrus: zebra;",
+ instruction: {type: "set", name: "walrus", value: "{{}}}", priority: "",
+ index: 0},
+ expected: "walrus: {{}}\\};",
+ changed: {0: "{{}}\\}"}
+ },
+ // Sanitize a bad value.
+ {
+ desc: "enable sanitize unpaired brace",
+ input: "/*! walrus: }*/",
+ instruction: {type: "enable", name: "walrus", value: true, index: 0},
+ expected: "walrus: \\};",
+ changed: {0: "\\}"}
+ },
+
+ // Creating a new declaration does not require an attempt to
+ // terminate a previous commented declaration.
+ {
+ desc: "disabled declaration does not need semicolon insertion",
+ input: "/*! no: semicolon */\n",
+ instruction: {type: "create", name: "walrus", value: "zebra", priority: "",
+ index: 1, enabled: true},
+ expected: "/*! no: semicolon */\nwalrus: zebra;\n",
+ changed: {}
+ },
+
+ {
+ desc: "create commented-out property",
+ input: "p: v",
+ instruction: {type: "create", name: "shoveler", value: "duck", priority: "",
+ index: 1, enabled: false},
+ expected: "p: v;/*! shoveler: duck; */",
+ },
+ {
+ desc: "disabled create with comment ender in string",
+ input: "",
+ instruction: {type: "create", name: "content", value: "'*/'", priority: "",
+ index: 0, enabled: false},
+ expected: "/*! content: '*\\/'; */"
+ },
+
+ {
+ desc: "delete disabled property",
+ input: "\n a:b;\n /* color:#f0c; */\n e:f;",
+ instruction: {type: "remove", name: "color", index: 1},
+ expected: "\n a:b;\n e:f;",
+ },
+ {
+ desc: "delete heuristic-disabled property",
+ input: "\n a:b;\n /*! c:d; */\n e:f;",
+ instruction: {type: "remove", name: "c", index: 1},
+ expected: "\n a:b;\n e:f;",
+ },
+ {
+ desc: "delete disabled property leaving other disabled property",
+ input: "\n a:b;\n /* color:#f0c; background-color: seagreen; */\n e:f;",
+ instruction: {type: "remove", name: "color", index: 1},
+ expected: "\n a:b;\n /* background-color: seagreen; */\n e:f;",
+ },
+];
+
+function rewriteDeclarations(inputString, instruction, defaultIndentation) {
+ let rewriter = new RuleRewriter(isCssPropertyKnown, null, inputString);
+ rewriter.defaultIndentation = defaultIndentation;
+
+ switch (instruction.type) {
+ case "rename":
+ rewriter.renameProperty(instruction.index, instruction.name,
+ instruction.newName);
+ break;
+
+ case "enable":
+ rewriter.setPropertyEnabled(instruction.index, instruction.name,
+ instruction.value);
+ break;
+
+ case "create":
+ rewriter.createProperty(instruction.index, instruction.name,
+ instruction.value, instruction.priority,
+ instruction.enabled);
+ break;
+
+ case "set":
+ rewriter.setProperty(instruction.index, instruction.name,
+ instruction.value, instruction.priority);
+ break;
+
+ case "remove":
+ rewriter.removeProperty(instruction.index, instruction.name);
+ break;
+
+ default:
+ throw new Error("unrecognized instruction");
+ }
+
+ return rewriter.getResult();
+}
+
+function run_test() {
+ for (let test of TEST_DATA) {
+ let {changed, text} = rewriteDeclarations(test.input, test.instruction,
+ "\t");
+ equal(text, test.expected, "output for " + test.desc);
+
+ let expectChanged;
+ if ("changed" in test) {
+ expectChanged = test.changed;
+ } else {
+ expectChanged = {};
+ }
+ deepEqual(changed, expectChanged, "changed result for " + test.desc);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_source-utils.js b/devtools/client/shared/test/unit/test_source-utils.js
new file mode 100644
index 000000000..2ff55b92e
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_source-utils.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests utility functions contained in `source-utils.js`
+ */
+
+const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const sourceUtils = require("devtools/client/shared/source-utils");
+
+function run_test() {
+ run_next_test();
+}
+
+const CHROME_URLS = [
+ "chrome://foo", "resource://baz", "jar:file:///Users/root"
+];
+
+const CONTENT_URLS = [
+ "http://mozilla.org", "https://mozilla.org", "file:///Users/root", "app://fxosapp"
+];
+
+// Test `sourceUtils.parseURL`
+add_task(function* () {
+ let parsed = sourceUtils.parseURL("https://foo.com:8888/boo/bar.js?q=query");
+ equal(parsed.fileName, "bar.js", "parseURL parsed valid fileName");
+ equal(parsed.host, "foo.com:8888", "parseURL parsed valid host");
+ equal(parsed.hostname, "foo.com", "parseURL parsed valid hostname");
+ equal(parsed.port, "8888", "parseURL parsed valid port");
+ equal(parsed.href, "https://foo.com:8888/boo/bar.js?q=query", "parseURL parsed valid href");
+
+ parsed = sourceUtils.parseURL("https://foo.com");
+ equal(parsed.host, "foo.com", "parseURL parsed valid host when no port given");
+ equal(parsed.hostname, "foo.com", "parseURL parsed valid hostname when no port given");
+
+ equal(sourceUtils.parseURL("self-hosted"), null,
+ "parseURL returns `null` for invalid URLs");
+});
+
+// Test `sourceUtils.isContentScheme`.
+add_task(function* () {
+ for (let url of CHROME_URLS) {
+ ok(!sourceUtils.isContentScheme(url),
+ `${url} correctly identified as not content scheme`);
+ }
+ for (let url of CONTENT_URLS) {
+ ok(sourceUtils.isContentScheme(url), `${url} correctly identified as content scheme`);
+ }
+});
+
+// Test `sourceUtils.isChromeScheme`.
+add_task(function* () {
+ for (let url of CHROME_URLS) {
+ ok(sourceUtils.isChromeScheme(url), `${url} correctly identified as chrome scheme`);
+ }
+ for (let url of CONTENT_URLS) {
+ ok(!sourceUtils.isChromeScheme(url),
+ `${url} correctly identified as not chrome scheme`);
+ }
+});
+
+// Test `sourceUtils.isDataScheme`.
+add_task(function* () {
+ let dataURI = "data:text/html;charset=utf-8,<!DOCTYPE html></html>";
+ ok(sourceUtils.isDataScheme(dataURI), `${dataURI} correctly identified as data scheme`);
+
+ for (let url of CHROME_URLS) {
+ ok(!sourceUtils.isDataScheme(url), `${url} correctly identified as not data scheme`);
+ }
+ for (let url of CONTENT_URLS) {
+ ok(!sourceUtils.isDataScheme(url), `${url} correctly identified as not data scheme`);
+ }
+});
+
+// Test `sourceUtils.getSourceNames`.
+add_task(function* () {
+ testAbbreviation("http://example.com/foo/bar/baz/boo.js",
+ "boo.js",
+ "http://example.com/foo/bar/baz/boo.js",
+ "example.com");
+});
+
+// Test `sourceUtils.isScratchpadTheme`
+add_task(function* () {
+ ok(sourceUtils.isScratchpadScheme("Scratchpad/1"),
+ "Scratchpad/1 identified as scratchpad");
+ ok(sourceUtils.isScratchpadScheme("Scratchpad/20"),
+ "Scratchpad/20 identified as scratchpad");
+ ok(!sourceUtils.isScratchpadScheme("http://www.mozilla.org"), "http://www.mozilla.org not identified as scratchpad");
+});
+
+// Test `sourceUtils.getSourceNames`.
+add_task(function* () {
+ // Check length
+ let longMalformedURL = `example.com${new Array(100).fill("/a").join("")}/file.js`;
+ ok(sourceUtils.getSourceNames(longMalformedURL).short.length <= 100,
+ "`short` names are capped at 100 characters");
+
+ testAbbreviation("self-hosted", "self-hosted", "self-hosted");
+ testAbbreviation("", "(unknown)", "(unknown)");
+
+ // Test shortening data URIs, stripping mime/charset
+ testAbbreviation("data:text/html;charset=utf-8,<!DOCTYPE html></html>",
+ "data:<!DOCTYPE html></html>",
+ "data:text/html;charset=utf-8,<!DOCTYPE html></html>");
+
+ let longDataURI = `data:image/png;base64,${new Array(100).fill("a").join("")}`;
+ let longDataURIShort = sourceUtils.getSourceNames(longDataURI).short;
+
+ // Test shortening data URIs and that the `short` result is capped
+ ok(longDataURIShort.length <= 100,
+ "`short` names are capped at 100 characters for data URIs");
+ equal(longDataURIShort.substr(0, 10), "data:aaaaa",
+ "truncated data URI short names still have `data:...`");
+
+ // Test simple URL and cache retrieval by calling the same input multiple times.
+ let testUrl = "http://example.com/foo/bar/baz/boo.js";
+ testAbbreviation(testUrl, "boo.js", testUrl, "example.com");
+ testAbbreviation(testUrl, "boo.js", testUrl, "example.com");
+
+ // Check query and hash and port
+ testAbbreviation("http://example.com:8888/foo/bar/baz.js?q=query#go",
+ "baz.js",
+ "http://example.com:8888/foo/bar/baz.js",
+ "example.com:8888");
+
+ // Trailing "/" with nothing beyond host
+ testAbbreviation("http://example.com/",
+ "/",
+ "http://example.com/",
+ "example.com");
+
+ // Trailing "/"
+ testAbbreviation("http://example.com/foo/bar/",
+ "bar",
+ "http://example.com/foo/bar/",
+ "example.com");
+
+ // Non-extension ending
+ testAbbreviation("http://example.com/bar",
+ "bar",
+ "http://example.com/bar",
+ "example.com");
+
+ // Check query
+ testAbbreviation("http://example.com/foo.js?bar=1&baz=2",
+ "foo.js",
+ "http://example.com/foo.js",
+ "example.com");
+
+ // Check query with trailing slash
+ testAbbreviation("http://example.com/foo/?bar=1&baz=2",
+ "foo",
+ "http://example.com/foo/",
+ "example.com");
+});
+
+// Test for source mapped file name
+add_task(function* () {
+ const { getSourceMappedFile } = sourceUtils;
+ const source = "baz.js";
+ const output = getSourceMappedFile(source);
+ equal(output, "baz.js", "correctly formats file name");
+ // Test for OSX file path
+ const source1 = "/foo/bar/baz.js";
+ const output1 = getSourceMappedFile(source1);
+ equal(output1, "baz.js", "correctly formats Linux file path");
+ // Test for Windows file path
+ const source2 = "Z:\\foo\\bar\\baz.js";
+ const output2 = getSourceMappedFile(source2);
+ equal(output2, "baz.js", "correctly formats Windows file path");
+});
+
+function testAbbreviation(source, short, long, host) {
+ let results = sourceUtils.getSourceNames(source);
+ equal(results.short, short, `${source} has correct "short" name`);
+ equal(results.long, long, `${source} has correct "long" name`);
+ equal(results.host, host, `${source} has correct "host" name`);
+}
diff --git a/devtools/client/shared/test/unit/test_suggestion-picker.js b/devtools/client/shared/test/unit/test_suggestion-picker.js
new file mode 100644
index 000000000..28c9df13b
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_suggestion-picker.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test the suggestion-picker helper methods.
+ */
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {
+ findMostRelevantIndex,
+ findMostRelevantCssPropertyIndex
+} = require("devtools/client/shared/suggestion-picker");
+
+/**
+ * Run all tests defined below.
+ */
+function run_test() {
+ ensureMostRelevantIndexProvidedByHelperFunction();
+ ensureMostRelevantIndexProvidedByClassMethod();
+ ensureErrorThrownWithInvalidArguments();
+}
+
+/**
+ * Generic test data.
+ */
+const TEST_DATA = [
+ {
+ // Match in sortedItems array.
+ items: ["chrome", "edge", "firefox"],
+ sortedItems: ["firefox", "chrome", "edge"],
+ expectedIndex: 2
+ }, {
+ // No match in sortedItems array.
+ items: ["apple", "oranges", "banana"],
+ sortedItems: ["kiwi", "pear", "peach"],
+ expectedIndex: 0
+ }, {
+ // Empty items array.
+ items: [],
+ sortedItems: ["empty", "arrays", "can't", "have", "relevant", "indexes"],
+ expectedIndex: -1
+ }
+];
+
+function ensureMostRelevantIndexProvidedByHelperFunction() {
+ do_print("Running ensureMostRelevantIndexProvidedByHelperFunction()");
+
+ for (let testData of TEST_DATA) {
+ let { items, sortedItems, expectedIndex } = testData;
+ let mostRelevantIndex = findMostRelevantIndex(items, sortedItems);
+ strictEqual(mostRelevantIndex, expectedIndex);
+ }
+}
+
+/**
+ * CSS properties test data.
+ */
+const CSS_TEST_DATA = [
+ {
+ items: [
+ "backface-visibility",
+ "background",
+ "background-attachment",
+ "background-blend-mode",
+ "background-clip",
+ "background-color",
+ "background-image",
+ "background-origin",
+ "background-position",
+ "background-repeat"
+ ],
+ expectedIndex: 1
+ },
+ {
+ items: [
+ "caption-side",
+ "clear",
+ "clip",
+ "clip-path",
+ "clip-rule",
+ "color",
+ "color-interpolation",
+ "color-interpolation-filters",
+ "content",
+ "counter-increment"
+ ],
+ expectedIndex: 5
+ },
+ {
+ items: [
+ "direction",
+ "display",
+ "dominant-baseline"
+ ],
+ expectedIndex: 1
+ },
+ {
+ items: [
+ "object-fit",
+ "object-position",
+ "offset-block-end",
+ "offset-block-start",
+ "offset-inline-end",
+ "offset-inline-start",
+ "opacity",
+ "order",
+ "orphans",
+ "outline"
+ ],
+ expectedIndex: 6
+ },
+ {
+ items: [
+ "white-space",
+ "widows",
+ "width",
+ "will-change",
+ "word-break",
+ "word-spacing",
+ "word-wrap",
+ "writing-mode"
+ ],
+ expectedIndex: 2
+ }
+];
+
+function ensureMostRelevantIndexProvidedByClassMethod() {
+ do_print("Running ensureMostRelevantIndexProvidedByClassMethod()");
+
+ for (let testData of CSS_TEST_DATA) {
+ let { items, expectedIndex } = testData;
+ let mostRelevantIndex = findMostRelevantCssPropertyIndex(items);
+ strictEqual(mostRelevantIndex, expectedIndex);
+ }
+}
+
+function ensureErrorThrownWithInvalidArguments() {
+ do_print("Running ensureErrorThrownWithInvalidTypeArgument()");
+
+ let expectedError = "Please provide valid items and sortedItems arrays.";
+ // No arguments passed.
+ throws(() => findMostRelevantIndex(), expectedError);
+ // Invalid arguments passed.
+ throws(() => findMostRelevantIndex([]), expectedError);
+ throws(() => findMostRelevantIndex(null, []), expectedError);
+ throws(() => findMostRelevantIndex([], "string"), expectedError);
+ throws(() => findMostRelevantIndex("string", []), expectedError);
+}
diff --git a/devtools/client/shared/test/unit/test_undoStack.js b/devtools/client/shared/test/unit/test_undoStack.js
new file mode 100644
index 000000000..7499614fd
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_undoStack.js
@@ -0,0 +1,98 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+const {Loader} = Components.utils.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+
+const loader = new Loader.Loader({
+ paths: {
+ "": "resource://gre/modules/commonjs/",
+ "devtools": "resource://devtools",
+ },
+ globals: {},
+});
+const require = Loader.Require(loader, { id: "undo-test" });
+
+const {UndoStack} = require("devtools/client/shared/undo");
+
+const MAX_SIZE = 5;
+
+function run_test() {
+ let str = "";
+ let stack = new UndoStack(MAX_SIZE);
+
+ function add(ch) {
+ stack.do(function () {
+ str += ch;
+ }, function () {
+ str = str.slice(0, -1);
+ });
+ }
+
+ do_check_false(stack.canUndo());
+ do_check_false(stack.canRedo());
+
+ // Check adding up to the limit of the size
+ add("a");
+ do_check_true(stack.canUndo());
+ do_check_false(stack.canRedo());
+
+ add("b");
+ add("c");
+ add("d");
+ add("e");
+
+ do_check_eq(str, "abcde");
+
+ // Check a simple undo+redo
+ stack.undo();
+
+ do_check_eq(str, "abcd");
+ do_check_true(stack.canRedo());
+
+ stack.redo();
+ do_check_eq(str, "abcde");
+ do_check_false(stack.canRedo());
+
+ // Check an undo followed by a new action
+ stack.undo();
+ do_check_eq(str, "abcd");
+
+ add("q");
+ do_check_eq(str, "abcdq");
+ do_check_false(stack.canRedo());
+
+ stack.undo();
+ do_check_eq(str, "abcd");
+ stack.redo();
+ do_check_eq(str, "abcdq");
+
+ // Revert back to the beginning of the queue...
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+ do_check_eq(str, "");
+
+ // Now put it all back....
+ while (stack.canRedo()) {
+ stack.redo();
+ }
+ do_check_eq(str, "abcdq");
+
+ // Now go over the undo limit...
+ add("1");
+ add("2");
+ add("3");
+
+ do_check_eq(str, "abcdq123");
+
+ // And now undoing the whole stack should only undo 5 actions.
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+
+ do_check_eq(str, "abc");
+}
diff --git a/devtools/client/shared/test/unit/xpcshell.ini b/devtools/client/shared/test/unit/xpcshell.ini
new file mode 100644
index 000000000..b3c5791ec
--- /dev/null
+++ b/devtools/client/shared/test/unit/xpcshell.ini
@@ -0,0 +1,30 @@
+[DEFAULT]
+tags = devtools
+head =
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+support-files =
+ ../helper_color_data.js
+
+[test_advanceValidate.js]
+[test_attribute-parsing-01.js]
+[test_attribute-parsing-02.js]
+[test_bezierCanvas.js]
+[test_cssAngle.js]
+[test_cssColor-01.js]
+[test_cssColor-02.js]
+[test_cssColor-03.js]
+[test_cssColorDatabase.js]
+[test_cubicBezier.js]
+[test_escapeCSSComment.js]
+[test_parseDeclarations.js]
+[test_parsePseudoClassesAndAttributes.js]
+[test_parseSingleValue.js]
+[test_rewriteDeclarations.js]
+[test_source-utils.js]
+[test_suggestion-picker.js]
+[test_undoStack.js]
+[test_VariablesView_filtering-without-controller.js]
+[test_VariablesView_getString_promise.js]
diff --git a/devtools/client/shared/theme-switching.js b/devtools/client/shared/theme-switching.js
new file mode 100644
index 000000000..29f93c460
--- /dev/null
+++ b/devtools/client/shared/theme-switching.js
@@ -0,0 +1,185 @@
+/* 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/. */
+
+/* eslint-env browser */
+"use strict";
+(function () {
+ const { utils: Cu } = Components;
+ const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ const Services = require("Services");
+ const { gDevTools } = require("devtools/client/framework/devtools");
+ const { watchCSS } = require("devtools/client/shared/css-reload");
+ let documentElement = document.documentElement;
+
+ let os;
+ let platform = navigator.platform;
+ if (platform.startsWith("Win")) {
+ os = "win";
+ } else if (platform.startsWith("Mac")) {
+ os = "mac";
+ } else {
+ os = "linux";
+ }
+
+ documentElement.setAttribute("platform", os);
+
+ // no-theme attributes allows to just est the platform attribute
+ // to have per-platform CSS working correctly.
+ if (documentElement.getAttribute("no-theme") === "true") {
+ return;
+ }
+
+ let devtoolsStyleSheets = new WeakMap();
+ let gOldTheme = "";
+
+ function forceStyle() {
+ let computedStyle = window.getComputedStyle(documentElement);
+ if (!computedStyle) {
+ // Null when documentElement is not ready. This method is anyways not
+ // required then as scrollbars would be in their state without flushing.
+ return;
+ }
+ // Save display value
+ let display = computedStyle.display;
+ documentElement.style.display = "none";
+ // Flush
+ window.getComputedStyle(documentElement).display;
+ // Restore
+ documentElement.style.display = display;
+ }
+
+ /*
+ * Append a new processing instruction and return an object with
+ * - styleSheet: DOMNode
+ * - loadPromise: Promise that resolves once the sheets loads or errors
+ */
+ function appendStyleSheet(url) {
+ let styleSheetAttr = `href="${url}" type="text/css"`;
+ let styleSheet = document.createProcessingInstruction(
+ "xml-stylesheet", styleSheetAttr);
+ let loadPromise = new Promise((resolve, reject) => {
+ function onload() {
+ styleSheet.removeEventListener("load", onload);
+ styleSheet.removeEventListener("error", onerror);
+ resolve();
+ }
+ function onerror() {
+ styleSheet.removeEventListener("load", onload);
+ styleSheet.removeEventListener("error", onerror);
+ reject("Failed to load theme file " + url);
+ }
+
+ styleSheet.addEventListener("load", onload);
+ styleSheet.addEventListener("error", onerror);
+ });
+ document.insertBefore(styleSheet, documentElement);
+ return {styleSheet, loadPromise};
+ }
+
+ /*
+ * Notify the window that a theme switch finished so tests can check the DOM
+ */
+ function notifyWindow() {
+ window.dispatchEvent(new CustomEvent("theme-switch-complete", {}));
+ }
+
+ /*
+ * Apply all the sheets from `newTheme` and remove all of the sheets
+ * from `oldTheme`
+ */
+ function switchTheme(newTheme) {
+ if (newTheme === gOldTheme) {
+ return;
+ }
+ let oldTheme = gOldTheme;
+ gOldTheme = newTheme;
+
+ let oldThemeDef = gDevTools.getThemeDefinition(oldTheme);
+ let newThemeDef = gDevTools.getThemeDefinition(newTheme);
+
+ // The theme might not be available anymore (e.g. uninstalled)
+ // Use the default one.
+ if (!newThemeDef) {
+ newThemeDef = gDevTools.getThemeDefinition("light");
+ }
+
+ // Store the sheets in a WeakMap for access later when the theme gets
+ // unapplied. It's hard to query for processing instructions so this
+ // is an easy way to access them later without storing a property on
+ // the window
+ devtoolsStyleSheets.set(newThemeDef, []);
+
+ let loadEvents = [];
+ for (let url of newThemeDef.stylesheets) {
+ let {styleSheet, loadPromise} = appendStyleSheet(url);
+ devtoolsStyleSheets.get(newThemeDef).push(styleSheet);
+ loadEvents.push(loadPromise);
+ }
+
+ try {
+ const StylesheetUtils = require("sdk/stylesheet/utils");
+ const SCROLLBARS_URL = "chrome://devtools/skin/floating-scrollbars-dark-theme.css";
+
+ // TODO: extensions might want to customize scrollbar styles too.
+ if (!Services.appShell.hiddenDOMWindow
+ .matchMedia("(-moz-overlay-scrollbars)").matches) {
+ if (newTheme == "dark") {
+ StylesheetUtils.loadSheet(window, SCROLLBARS_URL, "agent");
+ } else if (oldTheme == "dark") {
+ StylesheetUtils.removeSheet(window, SCROLLBARS_URL, "agent");
+ }
+ forceStyle();
+ }
+ } catch (e) {
+ console.warn("customize scrollbar styles is only supported in firefox");
+ }
+
+ Promise.all(loadEvents).then(() => {
+ // Unload all stylesheets and classes from the old theme.
+ if (oldThemeDef) {
+ for (let name of oldThemeDef.classList) {
+ documentElement.classList.remove(name);
+ }
+
+ for (let sheet of devtoolsStyleSheets.get(oldThemeDef) || []) {
+ sheet.remove();
+ }
+
+ if (oldThemeDef.onUnapply) {
+ oldThemeDef.onUnapply(window, newTheme);
+ }
+ }
+
+ // Load all stylesheets and classes from the new theme.
+ for (let name of newThemeDef.classList) {
+ documentElement.classList.add(name);
+ }
+
+ if (newThemeDef.onApply) {
+ newThemeDef.onApply(window, oldTheme);
+ }
+
+ // Final notification for further theme-switching related logic.
+ gDevTools.emit("theme-switched", window, newTheme, oldTheme);
+ notifyWindow();
+ }, console.error.bind(console));
+ }
+
+ function handlePrefChange() {
+ switchTheme(Services.prefs.getCharPref("devtools.theme"));
+ }
+
+ if (documentElement.hasAttribute("force-theme")) {
+ switchTheme(documentElement.getAttribute("force-theme"));
+ } else {
+ switchTheme(Services.prefs.getCharPref("devtools.theme"));
+
+ Services.prefs.addObserver("devtools.theme", handlePrefChange, false);
+ window.addEventListener("unload", function () {
+ Services.prefs.removeObserver("devtools.theme", handlePrefChange);
+ }, { once: true });
+ }
+
+ watchCSS(window);
+})();
diff --git a/devtools/client/shared/theme.js b/devtools/client/shared/theme.js
new file mode 100644
index 000000000..6ba956f64
--- /dev/null
+++ b/devtools/client/shared/theme.js
@@ -0,0 +1,84 @@
+/* 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";
+
+/**
+ * Colors for themes taken from:
+ * https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
+ */
+
+const Services = require("Services");
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+const variableFileContents = require("raw!devtools/client/themes/variables.css");
+
+const THEME_SELECTOR_STRINGS = {
+ light: ":root.theme-light {",
+ dark: ":root.theme-dark {",
+ firebug: ":root.theme-firebug {"
+};
+
+/**
+ * Takes a theme name and returns the contents of its variable rule block.
+ * The first time this runs fetches the variables CSS file and caches it.
+ */
+function getThemeFile(name) {
+ // If there's no theme expected for this name, use `light` as default.
+ let selector = THEME_SELECTOR_STRINGS[name] ||
+ THEME_SELECTOR_STRINGS.light;
+
+ // This is a pretty naive way to find the contents between:
+ // selector {
+ // name: val;
+ // }
+ // There is test coverage for this feature (browser_theme.js)
+ // so if an } is introduced in the variables file it will catch that.
+ let theme = variableFileContents;
+ theme = theme.substring(theme.indexOf(selector));
+ theme = theme.substring(0, theme.indexOf("}"));
+
+ return theme;
+}
+
+/**
+ * Returns the string value of the current theme,
+ * like "dark" or "light".
+ */
+const getTheme = exports.getTheme = () => {
+ return Services.prefs.getCharPref("devtools.theme");
+};
+
+/**
+ * Returns a color indicated by `type` (like "toolbar-background", or
+ * "highlight-red"), with the ability to specify a theme, or use whatever the
+ * current theme is if left unset. If theme not found, falls back to "light"
+ * theme. Returns null if the type cannot be found for the theme given.
+ */
+/* eslint-disable no-unused-vars */
+const getColor = exports.getColor = (type, theme) => {
+ let themeName = theme || getTheme();
+ let themeFile = getThemeFile(themeName);
+ let match = themeFile.match(new RegExp("--theme-" + type + ": (.*);"));
+
+ // Return the appropriate variable in the theme, or otherwise, null.
+ return match ? match[1] : null;
+};
+
+/**
+ * Mimics selecting the theme selector in the toolbox;
+ * sets the preference and emits an event on gDevTools to trigger
+ * the themeing.
+ */
+const setTheme = exports.setTheme = (newTheme) => {
+ let oldTheme = getTheme();
+
+ Services.prefs.setCharPref("devtools.theme", newTheme);
+ gDevTools.emit("pref-changed", {
+ pref: "devtools.theme",
+ newValue: newTheme,
+ oldValue: oldTheme
+ });
+};
+/* eslint-enable */
diff --git a/devtools/client/shared/undo.js b/devtools/client/shared/undo.js
new file mode 100644
index 000000000..65791f50d
--- /dev/null
+++ b/devtools/client/shared/undo.js
@@ -0,0 +1,192 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * A simple undo stack manager.
+ *
+ * Actions are added along with the necessary code to
+ * reverse the action.
+ *
+ * @param integer maxUndo Maximum number of undo steps.
+ * defaults to 50.
+ */
+function UndoStack(maxUndo) {
+ this.maxUndo = maxUndo || 50;
+ this._stack = [];
+}
+
+exports.UndoStack = UndoStack;
+
+UndoStack.prototype = {
+ // Current index into the undo stack. Is positioned after the last
+ // currently-applied change.
+ _index: 0,
+
+ // The current batch depth (see startBatch() for details)
+ _batchDepth: 0,
+
+ destroy: function () {
+ this.uninstallController();
+ delete this._stack;
+ },
+
+ /**
+ * Start a collection of related changes. Changes will be batched
+ * together into one undo/redo item until endBatch() is called.
+ *
+ * Batches can be nested, in which case the outer batch will contain
+ * all items from the inner batches. This allows larger user
+ * actions made up of a collection of smaller actions to be
+ * undone as a single action.
+ */
+ startBatch: function () {
+ if (this._batchDepth++ === 0) {
+ this._batch = [];
+ }
+ },
+
+ /**
+ * End a batch of related changes, performing its action and adding
+ * it to the undo stack.
+ */
+ endBatch: function () {
+ if (--this._batchDepth > 0) {
+ return;
+ }
+
+ // Cut off the end of the undo stack at the current index,
+ // and the beginning to prevent a stack larger than maxUndo.
+ let start = Math.max((this._index + 1) - this.maxUndo, 0);
+ this._stack = this._stack.slice(start, this._index);
+
+ let batch = this._batch;
+ delete this._batch;
+ let entry = {
+ do: function () {
+ for (let item of batch) {
+ item.do();
+ }
+ },
+ undo: function () {
+ for (let i = batch.length - 1; i >= 0; i--) {
+ batch[i].undo();
+ }
+ }
+ };
+ this._stack.push(entry);
+ this._index = this._stack.length;
+ entry.do();
+ this._change();
+ },
+
+ /**
+ * Perform an action, adding it to the undo stack.
+ *
+ * @param function toDo Called to perform the action.
+ * @param function undo Called to reverse the action.
+ */
+ do: function (toDo, undo) {
+ this.startBatch();
+ this._batch.push({ do: toDo, undo });
+ this.endBatch();
+ },
+
+ /*
+ * Returns true if undo() will do anything.
+ */
+ canUndo: function () {
+ return this._index > 0;
+ },
+
+ /**
+ * Undo the top of the undo stack.
+ *
+ * @return true if an action was undone.
+ */
+ undo: function () {
+ if (!this.canUndo()) {
+ return false;
+ }
+ this._stack[--this._index].undo();
+ this._change();
+ return true;
+ },
+
+ /**
+ * Returns true if redo() will do anything.
+ */
+ canRedo: function () {
+ return this._stack.length > this._index;
+ },
+
+ /**
+ * Redo the most recently undone action.
+ *
+ * @return true if an action was redone.
+ */
+ redo: function () {
+ if (!this.canRedo()) {
+ return false;
+ }
+ this._stack[this._index++].do();
+ this._change();
+ return true;
+ },
+
+ _change: function () {
+ if (this._controllerWindow) {
+ this._controllerWindow.goUpdateCommand("cmd_undo");
+ this._controllerWindow.goUpdateCommand("cmd_redo");
+ }
+ },
+
+ /**
+ * ViewController implementation for undo/redo.
+ */
+
+ /**
+ * Install this object as a command controller.
+ */
+ installController: function (controllerWindow) {
+ this._controllerWindow = controllerWindow;
+ controllerWindow.controllers.appendController(this);
+ },
+
+ /**
+ * Uninstall this object from the command controller.
+ */
+ uninstallController: function () {
+ if (!this._controllerWindow) {
+ return;
+ }
+ this._controllerWindow.controllers.removeController(this);
+ },
+
+ supportsCommand: function (command) {
+ return (command == "cmd_undo" ||
+ command == "cmd_redo");
+ },
+
+ isCommandEnabled: function (command) {
+ switch (command) {
+ case "cmd_undo": return this.canUndo();
+ case "cmd_redo": return this.canRedo();
+ }
+ return false;
+ },
+
+ doCommand: function (command) {
+ switch (command) {
+ case "cmd_undo": return this.undo();
+ case "cmd_redo": return this.redo();
+ default: return null;
+ }
+ },
+
+ onEvent: function (event) {},
+};
diff --git a/devtools/client/shared/vendor/D3_LICENSE b/devtools/client/shared/vendor/D3_LICENSE
new file mode 100644
index 000000000..fb7d95d70
--- /dev/null
+++ b/devtools/client/shared/vendor/D3_LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2014, Michael Bostock
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* The name Michael Bostock may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/devtools/client/shared/vendor/DAGRE_D3_LICENSE b/devtools/client/shared/vendor/DAGRE_D3_LICENSE
new file mode 100644
index 000000000..1d64ed68c
--- /dev/null
+++ b/devtools/client/shared/vendor/DAGRE_D3_LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2013 Chris Pettitt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/devtools/client/shared/vendor/REACT_REDUX_LICENSE b/devtools/client/shared/vendor/REACT_REDUX_LICENSE
new file mode 100644
index 000000000..af2353dca
--- /dev/null
+++ b/devtools/client/shared/vendor/REACT_REDUX_LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Dan Abramov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/devtools/client/shared/vendor/REACT_REDUX_UPGRADING b/devtools/client/shared/vendor/REACT_REDUX_UPGRADING
new file mode 100644
index 000000000..7739751db
--- /dev/null
+++ b/devtools/client/shared/vendor/REACT_REDUX_UPGRADING
@@ -0,0 +1,9 @@
+"react-redux" uses UMD style loading to work in many different environments.
+It assumes that "react" and "redux" are both included via `require("react")`
+as in node or browserify, but the paths to our react and redux installation are different.
+
+If upgrading react-redux, define the correct paths and replace the require statements
+for the module.exports case with the correct paths.
+
+Path to react: "devtools/client/shared/vendor/react"
+Path to redux: "devtools/client/shared/vendor/redux"
diff --git a/devtools/client/shared/vendor/REACT_UPGRADING b/devtools/client/shared/vendor/REACT_UPGRADING
new file mode 100644
index 000000000..a82b4fa8c
--- /dev/null
+++ b/devtools/client/shared/vendor/REACT_UPGRADING
@@ -0,0 +1,54 @@
+We use a version of React that has a few minor tweaks. We want to use
+an un-minified production version anyway, and because of all of this
+you need to build React yourself to upgrade it for devtools.
+
+First, clone the repo and get ready to build it. Replace `<version>`
+with the version tag you are targetting:
+
+* git clone https://github.com/facebook/react.git
+* cd react
+* git checkout <version>
+* In `src/addons/ReactWithAddons.js`, move the
+ `React.addons.TestUtils = ...` line outside of the `if`
+ block to force it be include in the production build
+
+Next, build React:
+
+* npm install
+* grunt build
+
+Unfortunately, you need to manually patch the generated JS file. We
+need to force React to always create HTML elements, and we do this by
+changing all `document.createElement` calls to `createElementNS`. It's
+much easier to do this on the generated file to make sure you update
+all dependencies as well.
+
+Open `build/react-with-addons.js` and search for all
+`document.createElement` calls and replace them with
+`document.createElementNS('http://www.w3.org/1999/xhtml', ...)`. Note
+that some code is `ownerDocument.createElement` so don't do a blind
+search/replace. There is only about ~12 places to change.
+
+Now move into our repo (note the naming of `react-dev.js`, it's the dev version):
+
+* cp build/react-with-addons.js <gecko-dev>/devtools/client/shared/vendor/react-dev.js
+
+Now we need to generate a production version of React:
+
+* NODE_ENV=production grunt build
+
+Unfortunately, you need to manually replace all the `createElement`
+calls in this version again. We know this is not ideal but WE NEED TO
+MOVE OFF XUL and we don't need to do this anymore once that happens.
+
+After patching `build/react-with-addons.js` again, copy the production
+version over:
+
+* cp build/react-with-addons.js <gecko-dev>/devtools/client/shared/vendor/react.js
+
+You also need to copy the ReactDOM package. It requires React, so
+right now we are just manually changing the path from `react` to
+`devtools/client/shared/vendor/react`.
+
+* cp build/react-dom.js <gecko-dev>/devtools/client/shared/vendor/react-dom.js
+* (change `require('react')` at the top of the file to the right path)
diff --git a/devtools/client/shared/vendor/REACT_VIRTUALIZED_UPGRADING b/devtools/client/shared/vendor/REACT_VIRTUALIZED_UPGRADING
new file mode 100644
index 000000000..50e0c1817
--- /dev/null
+++ b/devtools/client/shared/vendor/REACT_VIRTUALIZED_UPGRADING
@@ -0,0 +1,14 @@
+"react-virtualized" uses UMD style loading to work in many different environments.
+It assumes that "react", "react-addons-shallow-compare", and "react-dom" are all included
+separately via require statements. The paths to our installations are different.
+
+If upgrading:
+
+- Define the correct paths for React, etc and replace the require statements for the
+ module.exports case with the correct paths.
+- Replace any references to React.addons.shallowCompare with the webpack module id.
+- To support use in XUL documents, replace calls to createElement with
+ createElementNS("http://www.w3.org/1999/xhtml", but make sure that you aren't replacing
+ any calls to React.createElement.
+- Also required for XUL, replace document.head and document.body with
+ document.firstElementChild
diff --git a/devtools/client/shared/vendor/REDUX_LICENSE b/devtools/client/shared/vendor/REDUX_LICENSE
new file mode 100644
index 000000000..af2353dca
--- /dev/null
+++ b/devtools/client/shared/vendor/REDUX_LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Dan Abramov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/devtools/client/shared/vendor/REDUX_UPGRADING b/devtools/client/shared/vendor/REDUX_UPGRADING
new file mode 100644
index 000000000..fbbbfc8e3
--- /dev/null
+++ b/devtools/client/shared/vendor/REDUX_UPGRADING
@@ -0,0 +1,10 @@
+REDUX_UPGRADING
+
+Current version of redux : 3.3.0
+
+1 - grab the unminified version of redux on npm. For release 3.3.0 for instance,
+https://npmcdn.com/redux@3.3.0/dist/redux.js
+
+2 - replace the content of devtools/client/shared/vendor
+
+3 - update the current version in this file \ No newline at end of file
diff --git a/devtools/client/shared/vendor/RESELECT_LICENSE b/devtools/client/shared/vendor/RESELECT_LICENSE
new file mode 100644
index 000000000..1ca1e449f
--- /dev/null
+++ b/devtools/client/shared/vendor/RESELECT_LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2016 Reselect Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/devtools/client/shared/vendor/RESELECT_UPGRADING b/devtools/client/shared/vendor/RESELECT_UPGRADING
new file mode 100644
index 000000000..b846d05be
--- /dev/null
+++ b/devtools/client/shared/vendor/RESELECT_UPGRADING
@@ -0,0 +1,7 @@
+Follow these steps when adding/upgrading the reselect.js module:
+
+1. git clone https://github.com/reactjs/reselect - clone the repo
+2. npm install - compile the sources to a compiled JS module file
+3. cp dist/reselect.js $DEST_DIR - copy the compiled file to Firefox source tree
+
+The package version used it currently 2.5.4 (last update in bug 1310573)
diff --git a/devtools/client/shared/vendor/d3.js b/devtools/client/shared/vendor/d3.js
new file mode 100644
index 000000000..2f645354c
--- /dev/null
+++ b/devtools/client/shared/vendor/d3.js
@@ -0,0 +1,9275 @@
+!function() {
+ var d3 = {
+ version: "3.4.2"
+ };
+ if (!Date.now) Date.now = function() {
+ return +new Date();
+ };
+ var d3_arraySlice = [].slice, d3_array = function(list) {
+ return d3_arraySlice.call(list);
+ };
+ var d3_document = document, d3_documentElement = d3_document.documentElement, d3_window = window;
+ try {
+ d3_array(d3_documentElement.childNodes)[0].nodeType;
+ } catch (e) {
+ d3_array = function(list) {
+ var i = list.length, array = new Array(i);
+ while (i--) array[i] = list[i];
+ return array;
+ };
+ }
+ try {
+ d3_document.createElement("div").style.setProperty("opacity", 0, "");
+ } catch (error) {
+ var d3_element_prototype = d3_window.Element.prototype, d3_element_setAttribute = d3_element_prototype.setAttribute, d3_element_setAttributeNS = d3_element_prototype.setAttributeNS, d3_style_prototype = d3_window.CSSStyleDeclaration.prototype, d3_style_setProperty = d3_style_prototype.setProperty;
+ d3_element_prototype.setAttribute = function(name, value) {
+ d3_element_setAttribute.call(this, name, value + "");
+ };
+ d3_element_prototype.setAttributeNS = function(space, local, value) {
+ d3_element_setAttributeNS.call(this, space, local, value + "");
+ };
+ d3_style_prototype.setProperty = function(name, value, priority) {
+ d3_style_setProperty.call(this, name, value + "", priority);
+ };
+ }
+ d3.ascending = function(a, b) {
+ return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
+ };
+ d3.descending = function(a, b) {
+ return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN;
+ };
+ d3.min = function(array, f) {
+ var i = -1, n = array.length, a, b;
+ if (arguments.length === 1) {
+ while (++i < n && !((a = array[i]) != null && a <= a)) a = undefined;
+ while (++i < n) if ((b = array[i]) != null && a > b) a = b;
+ } else {
+ while (++i < n && !((a = f.call(array, array[i], i)) != null && a <= a)) a = undefined;
+ while (++i < n) if ((b = f.call(array, array[i], i)) != null && a > b) a = b;
+ }
+ return a;
+ };
+ d3.max = function(array, f) {
+ var i = -1, n = array.length, a, b;
+ if (arguments.length === 1) {
+ while (++i < n && !((a = array[i]) != null && a <= a)) a = undefined;
+ while (++i < n) if ((b = array[i]) != null && b > a) a = b;
+ } else {
+ while (++i < n && !((a = f.call(array, array[i], i)) != null && a <= a)) a = undefined;
+ while (++i < n) if ((b = f.call(array, array[i], i)) != null && b > a) a = b;
+ }
+ return a;
+ };
+ d3.extent = function(array, f) {
+ var i = -1, n = array.length, a, b, c;
+ if (arguments.length === 1) {
+ while (++i < n && !((a = c = array[i]) != null && a <= a)) a = c = undefined;
+ while (++i < n) if ((b = array[i]) != null) {
+ if (a > b) a = b;
+ if (c < b) c = b;
+ }
+ } else {
+ while (++i < n && !((a = c = f.call(array, array[i], i)) != null && a <= a)) a = undefined;
+ while (++i < n) if ((b = f.call(array, array[i], i)) != null) {
+ if (a > b) a = b;
+ if (c < b) c = b;
+ }
+ }
+ return [ a, c ];
+ };
+ d3.sum = function(array, f) {
+ var s = 0, n = array.length, a, i = -1;
+ if (arguments.length === 1) {
+ while (++i < n) if (!isNaN(a = +array[i])) s += a;
+ } else {
+ while (++i < n) if (!isNaN(a = +f.call(array, array[i], i))) s += a;
+ }
+ return s;
+ };
+ function d3_number(x) {
+ return x != null && !isNaN(x);
+ }
+ d3.mean = function(array, f) {
+ var n = array.length, a, m = 0, i = -1, j = 0;
+ if (arguments.length === 1) {
+ while (++i < n) if (d3_number(a = array[i])) m += (a - m) / ++j;
+ } else {
+ while (++i < n) if (d3_number(a = f.call(array, array[i], i))) m += (a - m) / ++j;
+ }
+ return j ? m : undefined;
+ };
+ d3.quantile = function(values, p) {
+ var H = (values.length - 1) * p + 1, h = Math.floor(H), v = +values[h - 1], e = H - h;
+ return e ? v + e * (values[h] - v) : v;
+ };
+ d3.median = function(array, f) {
+ if (arguments.length > 1) array = array.map(f);
+ array = array.filter(d3_number);
+ return array.length ? d3.quantile(array.sort(d3.ascending), .5) : undefined;
+ };
+ d3.bisector = function(f) {
+ return {
+ left: function(a, x, lo, hi) {
+ if (arguments.length < 3) lo = 0;
+ if (arguments.length < 4) hi = a.length;
+ while (lo < hi) {
+ var mid = lo + hi >>> 1;
+ if (f.call(a, a[mid], mid) < x) lo = mid + 1; else hi = mid;
+ }
+ return lo;
+ },
+ right: function(a, x, lo, hi) {
+ if (arguments.length < 3) lo = 0;
+ if (arguments.length < 4) hi = a.length;
+ while (lo < hi) {
+ var mid = lo + hi >>> 1;
+ if (x < f.call(a, a[mid], mid)) hi = mid; else lo = mid + 1;
+ }
+ return lo;
+ }
+ };
+ };
+ var d3_bisector = d3.bisector(function(d) {
+ return d;
+ });
+ d3.bisectLeft = d3_bisector.left;
+ d3.bisect = d3.bisectRight = d3_bisector.right;
+ d3.shuffle = function(array) {
+ var m = array.length, t, i;
+ while (m) {
+ i = Math.random() * m-- | 0;
+ t = array[m], array[m] = array[i], array[i] = t;
+ }
+ return array;
+ };
+ d3.permute = function(array, indexes) {
+ var i = indexes.length, permutes = new Array(i);
+ while (i--) permutes[i] = array[indexes[i]];
+ return permutes;
+ };
+ d3.pairs = function(array) {
+ var i = 0, n = array.length - 1, p0, p1 = array[0], pairs = new Array(n < 0 ? 0 : n);
+ while (i < n) pairs[i] = [ p0 = p1, p1 = array[++i] ];
+ return pairs;
+ };
+ d3.zip = function() {
+ if (!(n = arguments.length)) return [];
+ for (var i = -1, m = d3.min(arguments, d3_zipLength), zips = new Array(m); ++i < m; ) {
+ for (var j = -1, n, zip = zips[i] = new Array(n); ++j < n; ) {
+ zip[j] = arguments[j][i];
+ }
+ }
+ return zips;
+ };
+ function d3_zipLength(d) {
+ return d.length;
+ }
+ d3.transpose = function(matrix) {
+ return d3.zip.apply(d3, matrix);
+ };
+ d3.keys = function(map) {
+ var keys = [];
+ for (var key in map) keys.push(key);
+ return keys;
+ };
+ d3.values = function(map) {
+ var values = [];
+ for (var key in map) values.push(map[key]);
+ return values;
+ };
+ d3.entries = function(map) {
+ var entries = [];
+ for (var key in map) entries.push({
+ key: key,
+ value: map[key]
+ });
+ return entries;
+ };
+ d3.merge = function(arrays) {
+ var n = arrays.length, m, i = -1, j = 0, merged, array;
+ while (++i < n) j += arrays[i].length;
+ merged = new Array(j);
+ while (--n >= 0) {
+ array = arrays[n];
+ m = array.length;
+ while (--m >= 0) {
+ merged[--j] = array[m];
+ }
+ }
+ return merged;
+ };
+ var abs = Math.abs;
+ d3.range = function(start, stop, step) {
+ if (arguments.length < 3) {
+ step = 1;
+ if (arguments.length < 2) {
+ stop = start;
+ start = 0;
+ }
+ }
+ if ((stop - start) / step === Infinity) throw new Error("infinite range");
+ var range = [], k = d3_range_integerScale(abs(step)), i = -1, j;
+ start *= k, stop *= k, step *= k;
+ if (step < 0) while ((j = start + step * ++i) > stop) range.push(j / k); else while ((j = start + step * ++i) < stop) range.push(j / k);
+ return range;
+ };
+ function d3_range_integerScale(x) {
+ var k = 1;
+ while (x * k % 1) k *= 10;
+ return k;
+ }
+ function d3_class(ctor, properties) {
+ try {
+ for (var key in properties) {
+ Object.defineProperty(ctor.prototype, key, {
+ value: properties[key],
+ enumerable: false
+ });
+ }
+ } catch (e) {
+ ctor.prototype = properties;
+ }
+ }
+ d3.map = function(object) {
+ var map = new d3_Map();
+ if (object instanceof d3_Map) object.forEach(function(key, value) {
+ map.set(key, value);
+ }); else for (var key in object) map.set(key, object[key]);
+ return map;
+ };
+ function d3_Map() {}
+ d3_class(d3_Map, {
+ has: d3_map_has,
+ get: function(key) {
+ return this[d3_map_prefix + key];
+ },
+ set: function(key, value) {
+ return this[d3_map_prefix + key] = value;
+ },
+ remove: d3_map_remove,
+ keys: d3_map_keys,
+ values: function() {
+ var values = [];
+ this.forEach(function(key, value) {
+ values.push(value);
+ });
+ return values;
+ },
+ entries: function() {
+ var entries = [];
+ this.forEach(function(key, value) {
+ entries.push({
+ key: key,
+ value: value
+ });
+ });
+ return entries;
+ },
+ size: d3_map_size,
+ empty: d3_map_empty,
+ forEach: function(f) {
+ for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) f.call(this, key.substring(1), this[key]);
+ }
+ });
+ var d3_map_prefix = "\x00", d3_map_prefixCode = d3_map_prefix.charCodeAt(0);
+ function d3_map_has(key) {
+ return d3_map_prefix + key in this;
+ }
+ function d3_map_remove(key) {
+ key = d3_map_prefix + key;
+ return key in this && delete this[key];
+ }
+ function d3_map_keys() {
+ var keys = [];
+ this.forEach(function(key) {
+ keys.push(key);
+ });
+ return keys;
+ }
+ function d3_map_size() {
+ var size = 0;
+ for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) ++size;
+ return size;
+ }
+ function d3_map_empty() {
+ for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) return false;
+ return true;
+ }
+ d3.nest = function() {
+ var nest = {}, keys = [], sortKeys = [], sortValues, rollup;
+ function map(mapType, array, depth) {
+ if (depth >= keys.length) return rollup ? rollup.call(nest, array) : sortValues ? array.sort(sortValues) : array;
+ var i = -1, n = array.length, key = keys[depth++], keyValue, object, setter, valuesByKey = new d3_Map(), values;
+ while (++i < n) {
+ if (values = valuesByKey.get(keyValue = key(object = array[i]))) {
+ values.push(object);
+ } else {
+ valuesByKey.set(keyValue, [ object ]);
+ }
+ }
+ if (mapType) {
+ object = mapType();
+ setter = function(keyValue, values) {
+ object.set(keyValue, map(mapType, values, depth));
+ };
+ } else {
+ object = {};
+ setter = function(keyValue, values) {
+ object[keyValue] = map(mapType, values, depth);
+ };
+ }
+ valuesByKey.forEach(setter);
+ return object;
+ }
+ function entries(map, depth) {
+ if (depth >= keys.length) return map;
+ var array = [], sortKey = sortKeys[depth++];
+ map.forEach(function(key, keyMap) {
+ array.push({
+ key: key,
+ values: entries(keyMap, depth)
+ });
+ });
+ return sortKey ? array.sort(function(a, b) {
+ return sortKey(a.key, b.key);
+ }) : array;
+ }
+ nest.map = function(array, mapType) {
+ return map(mapType, array, 0);
+ };
+ nest.entries = function(array) {
+ return entries(map(d3.map, array, 0), 0);
+ };
+ nest.key = function(d) {
+ keys.push(d);
+ return nest;
+ };
+ nest.sortKeys = function(order) {
+ sortKeys[keys.length - 1] = order;
+ return nest;
+ };
+ nest.sortValues = function(order) {
+ sortValues = order;
+ return nest;
+ };
+ nest.rollup = function(f) {
+ rollup = f;
+ return nest;
+ };
+ return nest;
+ };
+ d3.set = function(array) {
+ var set = new d3_Set();
+ if (array) for (var i = 0, n = array.length; i < n; ++i) set.add(array[i]);
+ return set;
+ };
+ function d3_Set() {}
+ d3_class(d3_Set, {
+ has: d3_map_has,
+ add: function(value) {
+ this[d3_map_prefix + value] = true;
+ return value;
+ },
+ remove: function(value) {
+ value = d3_map_prefix + value;
+ return value in this && delete this[value];
+ },
+ values: d3_map_keys,
+ size: d3_map_size,
+ empty: d3_map_empty,
+ forEach: function(f) {
+ for (var value in this) if (value.charCodeAt(0) === d3_map_prefixCode) f.call(this, value.substring(1));
+ }
+ });
+ d3.behavior = {};
+ d3.rebind = function(target, source) {
+ var i = 1, n = arguments.length, method;
+ while (++i < n) target[method = arguments[i]] = d3_rebind(target, source, source[method]);
+ return target;
+ };
+ function d3_rebind(target, source, method) {
+ return function() {
+ var value = method.apply(source, arguments);
+ return value === source ? target : value;
+ };
+ }
+ function d3_vendorSymbol(object, name) {
+ if (name in object) return name;
+ name = name.charAt(0).toUpperCase() + name.substring(1);
+ for (var i = 0, n = d3_vendorPrefixes.length; i < n; ++i) {
+ var prefixName = d3_vendorPrefixes[i] + name;
+ if (prefixName in object) return prefixName;
+ }
+ }
+ var d3_vendorPrefixes = [ "webkit", "ms", "moz", "Moz", "o", "O" ];
+ function d3_noop() {}
+ d3.dispatch = function() {
+ var dispatch = new d3_dispatch(), i = -1, n = arguments.length;
+ while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch);
+ return dispatch;
+ };
+ function d3_dispatch() {}
+ d3_dispatch.prototype.on = function(type, listener) {
+ var i = type.indexOf("."), name = "";
+ if (i >= 0) {
+ name = type.substring(i + 1);
+ type = type.substring(0, i);
+ }
+ if (type) return arguments.length < 2 ? this[type].on(name) : this[type].on(name, listener);
+ if (arguments.length === 2) {
+ if (listener == null) for (type in this) {
+ if (this.hasOwnProperty(type)) this[type].on(name, null);
+ }
+ return this;
+ }
+ };
+ function d3_dispatch_event(dispatch) {
+ var listeners = [], listenerByName = new d3_Map();
+ function event() {
+ var z = listeners, i = -1, n = z.length, l;
+ while (++i < n) if (l = z[i].on) l.apply(this, arguments);
+ return dispatch;
+ }
+ event.on = function(name, listener) {
+ var l = listenerByName.get(name), i;
+ if (arguments.length < 2) return l && l.on;
+ if (l) {
+ l.on = null;
+ listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1));
+ listenerByName.remove(name);
+ }
+ if (listener) listeners.push(listenerByName.set(name, {
+ on: listener
+ }));
+ return dispatch;
+ };
+ return event;
+ }
+ d3.event = null;
+ function d3_eventPreventDefault() {
+ d3.event.preventDefault();
+ }
+ function d3_eventSource() {
+ var e = d3.event, s;
+ while (s = e.sourceEvent) e = s;
+ return e;
+ }
+ function d3_eventDispatch(target) {
+ var dispatch = new d3_dispatch(), i = 0, n = arguments.length;
+ while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch);
+ dispatch.of = function(thiz, argumentz) {
+ return function(e1) {
+ try {
+ var e0 = e1.sourceEvent = d3.event;
+ e1.target = target;
+ d3.event = e1;
+ dispatch[e1.type].apply(thiz, argumentz);
+ } finally {
+ d3.event = e0;
+ }
+ };
+ };
+ return dispatch;
+ }
+ d3.requote = function(s) {
+ return s.replace(d3_requote_re, "\\$&");
+ };
+ var d3_requote_re = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
+ var d3_subclass = {}.__proto__ ? function(object, prototype) {
+ object.__proto__ = prototype;
+ } : function(object, prototype) {
+ for (var property in prototype) object[property] = prototype[property];
+ };
+ function d3_selection(groups) {
+ d3_subclass(groups, d3_selectionPrototype);
+ return groups;
+ }
+ var d3_select = function(s, n) {
+ return n.querySelector(s);
+ }, d3_selectAll = function(s, n) {
+ return n.querySelectorAll(s);
+ }, d3_selectMatcher = d3_documentElement[d3_vendorSymbol(d3_documentElement, "matchesSelector")], d3_selectMatches = function(n, s) {
+ return d3_selectMatcher.call(n, s);
+ };
+ if (typeof Sizzle === "function") {
+ d3_select = function(s, n) {
+ return Sizzle(s, n)[0] || null;
+ };
+ d3_selectAll = function(s, n) {
+ return Sizzle.uniqueSort(Sizzle(s, n));
+ };
+ d3_selectMatches = Sizzle.matchesSelector;
+ }
+ d3.selection = function() {
+ return d3_selectionRoot;
+ };
+ var d3_selectionPrototype = d3.selection.prototype = [];
+ d3_selectionPrototype.select = function(selector) {
+ var subgroups = [], subgroup, subnode, group, node;
+ selector = d3_selection_selector(selector);
+ for (var j = -1, m = this.length; ++j < m; ) {
+ subgroups.push(subgroup = []);
+ subgroup.parentNode = (group = this[j]).parentNode;
+ for (var i = -1, n = group.length; ++i < n; ) {
+ if (node = group[i]) {
+ subgroup.push(subnode = selector.call(node, node.__data__, i, j));
+ if (subnode && "__data__" in node) subnode.__data__ = node.__data__;
+ } else {
+ subgroup.push(null);
+ }
+ }
+ }
+ return d3_selection(subgroups);
+ };
+ function d3_selection_selector(selector) {
+ return typeof selector === "function" ? selector : function() {
+ return d3_select(selector, this);
+ };
+ }
+ d3_selectionPrototype.selectAll = function(selector) {
+ var subgroups = [], subgroup, node;
+ selector = d3_selection_selectorAll(selector);
+ for (var j = -1, m = this.length; ++j < m; ) {
+ for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
+ if (node = group[i]) {
+ subgroups.push(subgroup = d3_array(selector.call(node, node.__data__, i, j)));
+ subgroup.parentNode = node;
+ }
+ }
+ }
+ return d3_selection(subgroups);
+ };
+ function d3_selection_selectorAll(selector) {
+ return typeof selector === "function" ? selector : function() {
+ return d3_selectAll(selector, this);
+ };
+ }
+ var d3_nsPrefix = {
+ svg: "http://www.w3.org/2000/svg",
+ xhtml: "http://www.w3.org/1999/xhtml",
+ xlink: "http://www.w3.org/1999/xlink",
+ xml: "http://www.w3.org/XML/1998/namespace",
+ xmlns: "http://www.w3.org/2000/xmlns/"
+ };
+ d3.ns = {
+ prefix: d3_nsPrefix,
+ qualify: function(name) {
+ var i = name.indexOf(":"), prefix = name;
+ if (i >= 0) {
+ prefix = name.substring(0, i);
+ name = name.substring(i + 1);
+ }
+ return d3_nsPrefix.hasOwnProperty(prefix) ? {
+ space: d3_nsPrefix[prefix],
+ local: name
+ } : name;
+ }
+ };
+ d3_selectionPrototype.attr = function(name, value) {
+ if (arguments.length < 2) {
+ if (typeof name === "string") {
+ var node = this.node();
+ name = d3.ns.qualify(name);
+ return name.local ? node.getAttributeNS(name.space, name.local) : node.getAttribute(name);
+ }
+ for (value in name) this.each(d3_selection_attr(value, name[value]));
+ return this;
+ }
+ return this.each(d3_selection_attr(name, value));
+ };
+ function d3_selection_attr(name, value) {
+ name = d3.ns.qualify(name);
+ function attrNull() {
+ this.removeAttribute(name);
+ }
+ function attrNullNS() {
+ this.removeAttributeNS(name.space, name.local);
+ }
+ function attrConstant() {
+ this.setAttribute(name, value);
+ }
+ function attrConstantNS() {
+ this.setAttributeNS(name.space, name.local, value);
+ }
+ function attrFunction() {
+ var x = value.apply(this, arguments);
+ if (x == null) this.removeAttribute(name); else this.setAttribute(name, x);
+ }
+ function attrFunctionNS() {
+ var x = value.apply(this, arguments);
+ if (x == null) this.removeAttributeNS(name.space, name.local); else this.setAttributeNS(name.space, name.local, x);
+ }
+ return value == null ? name.local ? attrNullNS : attrNull : typeof value === "function" ? name.local ? attrFunctionNS : attrFunction : name.local ? attrConstantNS : attrConstant;
+ }
+ function d3_collapse(s) {
+ return s.trim().replace(/\s+/g, " ");
+ }
+ d3_selectionPrototype.classed = function(name, value) {
+ if (arguments.length < 2) {
+ if (typeof name === "string") {
+ var node = this.node(), n = (name = d3_selection_classes(name)).length, i = -1;
+ if (value = node.classList) {
+ while (++i < n) if (!value.contains(name[i])) return false;
+ } else {
+ value = node.getAttribute("class");
+ while (++i < n) if (!d3_selection_classedRe(name[i]).test(value)) return false;
+ }
+ return true;
+ }
+ for (value in name) this.each(d3_selection_classed(value, name[value]));
+ return this;
+ }
+ return this.each(d3_selection_classed(name, value));
+ };
+ function d3_selection_classedRe(name) {
+ return new RegExp("(?:^|\\s+)" + d3.requote(name) + "(?:\\s+|$)", "g");
+ }
+ function d3_selection_classes(name) {
+ return name.trim().split(/^|\s+/);
+ }
+ function d3_selection_classed(name, value) {
+ name = d3_selection_classes(name).map(d3_selection_classedName);
+ var n = name.length;
+ function classedConstant() {
+ var i = -1;
+ while (++i < n) name[i](this, value);
+ }
+ function classedFunction() {
+ var i = -1, x = value.apply(this, arguments);
+ while (++i < n) name[i](this, x);
+ }
+ return typeof value === "function" ? classedFunction : classedConstant;
+ }
+ function d3_selection_classedName(name) {
+ var re = d3_selection_classedRe(name);
+ return function(node, value) {
+ if (c = node.classList) return value ? c.add(name) : c.remove(name);
+ var c = node.getAttribute("class") || "";
+ if (value) {
+ re.lastIndex = 0;
+ if (!re.test(c)) node.setAttribute("class", d3_collapse(c + " " + name));
+ } else {
+ node.setAttribute("class", d3_collapse(c.replace(re, " ")));
+ }
+ };
+ }
+ d3_selectionPrototype.style = function(name, value, priority) {
+ var n = arguments.length;
+ if (n < 3) {
+ if (typeof name !== "string") {
+ if (n < 2) value = "";
+ for (priority in name) this.each(d3_selection_style(priority, name[priority], value));
+ return this;
+ }
+ if (n < 2) return d3_window.getComputedStyle(this.node(), null).getPropertyValue(name);
+ priority = "";
+ }
+ return this.each(d3_selection_style(name, value, priority));
+ };
+ function d3_selection_style(name, value, priority) {
+ function styleNull() {
+ this.style.removeProperty(name);
+ }
+ function styleConstant() {
+ this.style.setProperty(name, value, priority);
+ }
+ function styleFunction() {
+ var x = value.apply(this, arguments);
+ if (x == null) this.style.removeProperty(name); else this.style.setProperty(name, x, priority);
+ }
+ return value == null ? styleNull : typeof value === "function" ? styleFunction : styleConstant;
+ }
+ d3_selectionPrototype.property = function(name, value) {
+ if (arguments.length < 2) {
+ if (typeof name === "string") return this.node()[name];
+ for (value in name) this.each(d3_selection_property(value, name[value]));
+ return this;
+ }
+ return this.each(d3_selection_property(name, value));
+ };
+ function d3_selection_property(name, value) {
+ function propertyNull() {
+ delete this[name];
+ }
+ function propertyConstant() {
+ this[name] = value;
+ }
+ function propertyFunction() {
+ var x = value.apply(this, arguments);
+ if (x == null) delete this[name]; else this[name] = x;
+ }
+ return value == null ? propertyNull : typeof value === "function" ? propertyFunction : propertyConstant;
+ }
+ d3_selectionPrototype.text = function(value) {
+ return arguments.length ? this.each(typeof value === "function" ? function() {
+ var v = value.apply(this, arguments);
+ this.textContent = v == null ? "" : v;
+ } : value == null ? function() {
+ this.textContent = "";
+ } : function() {
+ this.textContent = value;
+ }) : this.node().textContent;
+ };
+ d3_selectionPrototype.html = function(value) {
+ return arguments.length ? this.each(typeof value === "function" ? function() {
+ var v = value.apply(this, arguments);
+ this.innerHTML = v == null ? "" : v;
+ } : value == null ? function() {
+ this.innerHTML = "";
+ } : function() {
+ this.innerHTML = value;
+ }) : this.node().innerHTML;
+ };
+ d3_selectionPrototype.append = function(name) {
+ name = d3_selection_creator(name);
+ return this.select(function() {
+ return this.appendChild(name.apply(this, arguments));
+ });
+ };
+ function d3_selection_creator(name) {
+ return typeof name === "function" ? name : (name = d3.ns.qualify(name)).local ? function() {
+ return this.ownerDocument.createElementNS(name.space, name.local);
+ } : function() {
+ return this.ownerDocument.createElementNS(this.namespaceURI, name);
+ };
+ }
+ d3_selectionPrototype.insert = function(name, before) {
+ name = d3_selection_creator(name);
+ before = d3_selection_selector(before);
+ return this.select(function() {
+ return this.insertBefore(name.apply(this, arguments), before.apply(this, arguments) || null);
+ });
+ };
+ d3_selectionPrototype.remove = function() {
+ return this.each(function() {
+ var parent = this.parentNode;
+ if (parent) parent.removeChild(this);
+ });
+ };
+ d3_selectionPrototype.data = function(value, key) {
+ var i = -1, n = this.length, group, node;
+ if (!arguments.length) {
+ value = new Array(n = (group = this[0]).length);
+ while (++i < n) {
+ if (node = group[i]) {
+ value[i] = node.__data__;
+ }
+ }
+ return value;
+ }
+ function bind(group, groupData) {
+ var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData;
+ if (key) {
+ var nodeByKeyValue = new d3_Map(), dataByKeyValue = new d3_Map(), keyValues = [], keyValue;
+ for (i = -1; ++i < n; ) {
+ keyValue = key.call(node = group[i], node.__data__, i);
+ if (nodeByKeyValue.has(keyValue)) {
+ exitNodes[i] = node;
+ } else {
+ nodeByKeyValue.set(keyValue, node);
+ }
+ keyValues.push(keyValue);
+ }
+ for (i = -1; ++i < m; ) {
+ keyValue = key.call(groupData, nodeData = groupData[i], i);
+ if (node = nodeByKeyValue.get(keyValue)) {
+ updateNodes[i] = node;
+ node.__data__ = nodeData;
+ } else if (!dataByKeyValue.has(keyValue)) {
+ enterNodes[i] = d3_selection_dataNode(nodeData);
+ }
+ dataByKeyValue.set(keyValue, nodeData);
+ nodeByKeyValue.remove(keyValue);
+ }
+ for (i = -1; ++i < n; ) {
+ if (nodeByKeyValue.has(keyValues[i])) {
+ exitNodes[i] = group[i];
+ }
+ }
+ } else {
+ for (i = -1; ++i < n0; ) {
+ node = group[i];
+ nodeData = groupData[i];
+ if (node) {
+ node.__data__ = nodeData;
+ updateNodes[i] = node;
+ } else {
+ enterNodes[i] = d3_selection_dataNode(nodeData);
+ }
+ }
+ for (;i < m; ++i) {
+ enterNodes[i] = d3_selection_dataNode(groupData[i]);
+ }
+ for (;i < n; ++i) {
+ exitNodes[i] = group[i];
+ }
+ }
+ enterNodes.update = updateNodes;
+ enterNodes.parentNode = updateNodes.parentNode = exitNodes.parentNode = group.parentNode;
+ enter.push(enterNodes);
+ update.push(updateNodes);
+ exit.push(exitNodes);
+ }
+ var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]);
+ if (typeof value === "function") {
+ while (++i < n) {
+ bind(group = this[i], value.call(group, group.parentNode.__data__, i));
+ }
+ } else {
+ while (++i < n) {
+ bind(group = this[i], value);
+ }
+ }
+ update.enter = function() {
+ return enter;
+ };
+ update.exit = function() {
+ return exit;
+ };
+ return update;
+ };
+ function d3_selection_dataNode(data) {
+ return {
+ __data__: data
+ };
+ }
+ d3_selectionPrototype.datum = function(value) {
+ return arguments.length ? this.property("__data__", value) : this.property("__data__");
+ };
+ d3_selectionPrototype.filter = function(filter) {
+ var subgroups = [], subgroup, group, node;
+ if (typeof filter !== "function") filter = d3_selection_filter(filter);
+ for (var j = 0, m = this.length; j < m; j++) {
+ subgroups.push(subgroup = []);
+ subgroup.parentNode = (group = this[j]).parentNode;
+ for (var i = 0, n = group.length; i < n; i++) {
+ if ((node = group[i]) && filter.call(node, node.__data__, i, j)) {
+ subgroup.push(node);
+ }
+ }
+ }
+ return d3_selection(subgroups);
+ };
+ function d3_selection_filter(selector) {
+ return function() {
+ return d3_selectMatches(this, selector);
+ };
+ }
+ d3_selectionPrototype.order = function() {
+ for (var j = -1, m = this.length; ++j < m; ) {
+ for (var group = this[j], i = group.length - 1, next = group[i], node; --i >= 0; ) {
+ if (node = group[i]) {
+ if (next && next !== node.nextSibling) next.parentNode.insertBefore(node, next);
+ next = node;
+ }
+ }
+ }
+ return this;
+ };
+ d3_selectionPrototype.sort = function(comparator) {
+ comparator = d3_selection_sortComparator.apply(this, arguments);
+ for (var j = -1, m = this.length; ++j < m; ) this[j].sort(comparator);
+ return this.order();
+ };
+ function d3_selection_sortComparator(comparator) {
+ if (!arguments.length) comparator = d3.ascending;
+ return function(a, b) {
+ return a && b ? comparator(a.__data__, b.__data__) : !a - !b;
+ };
+ }
+ d3_selectionPrototype.each = function(callback) {
+ return d3_selection_each(this, function(node, i, j) {
+ callback.call(node, node.__data__, i, j);
+ });
+ };
+ function d3_selection_each(groups, callback) {
+ for (var j = 0, m = groups.length; j < m; j++) {
+ for (var group = groups[j], i = 0, n = group.length, node; i < n; i++) {
+ if (node = group[i]) callback(node, i, j);
+ }
+ }
+ return groups;
+ }
+ d3_selectionPrototype.call = function(callback) {
+ var args = d3_array(arguments);
+ callback.apply(args[0] = this, args);
+ return this;
+ };
+ d3_selectionPrototype.empty = function() {
+ return !this.node();
+ };
+ d3_selectionPrototype.node = function() {
+ for (var j = 0, m = this.length; j < m; j++) {
+ for (var group = this[j], i = 0, n = group.length; i < n; i++) {
+ var node = group[i];
+ if (node) return node;
+ }
+ }
+ return null;
+ };
+ d3_selectionPrototype.size = function() {
+ var n = 0;
+ this.each(function() {
+ ++n;
+ });
+ return n;
+ };
+ function d3_selection_enter(selection) {
+ d3_subclass(selection, d3_selection_enterPrototype);
+ return selection;
+ }
+ var d3_selection_enterPrototype = [];
+ d3.selection.enter = d3_selection_enter;
+ d3.selection.enter.prototype = d3_selection_enterPrototype;
+ d3_selection_enterPrototype.append = d3_selectionPrototype.append;
+ d3_selection_enterPrototype.empty = d3_selectionPrototype.empty;
+ d3_selection_enterPrototype.node = d3_selectionPrototype.node;
+ d3_selection_enterPrototype.call = d3_selectionPrototype.call;
+ d3_selection_enterPrototype.size = d3_selectionPrototype.size;
+ d3_selection_enterPrototype.select = function(selector) {
+ var subgroups = [], subgroup, subnode, upgroup, group, node;
+ for (var j = -1, m = this.length; ++j < m; ) {
+ upgroup = (group = this[j]).update;
+ subgroups.push(subgroup = []);
+ subgroup.parentNode = group.parentNode;
+ for (var i = -1, n = group.length; ++i < n; ) {
+ if (node = group[i]) {
+ subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i, j));
+ subnode.__data__ = node.__data__;
+ } else {
+ subgroup.push(null);
+ }
+ }
+ }
+ return d3_selection(subgroups);
+ };
+ d3_selection_enterPrototype.insert = function(name, before) {
+ if (arguments.length < 2) before = d3_selection_enterInsertBefore(this);
+ return d3_selectionPrototype.insert.call(this, name, before);
+ };
+ function d3_selection_enterInsertBefore(enter) {
+ var i0, j0;
+ return function(d, i, j) {
+ var group = enter[j].update, n = group.length, node;
+ if (j != j0) j0 = j, i0 = 0;
+ if (i >= i0) i0 = i + 1;
+ while (!(node = group[i0]) && ++i0 < n) ;
+ return node;
+ };
+ }
+ d3_selectionPrototype.transition = function() {
+ var id = d3_transitionInheritId || ++d3_transitionId, subgroups = [], subgroup, node, transition = d3_transitionInherit || {
+ time: Date.now(),
+ ease: d3_ease_cubicInOut,
+ delay: 0,
+ duration: 250
+ };
+ for (var j = -1, m = this.length; ++j < m; ) {
+ subgroups.push(subgroup = []);
+ for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
+ if (node = group[i]) d3_transitionNode(node, i, id, transition);
+ subgroup.push(node);
+ }
+ }
+ return d3_transition(subgroups, id);
+ };
+ d3_selectionPrototype.interrupt = function() {
+ return this.each(d3_selection_interrupt);
+ };
+ function d3_selection_interrupt() {
+ var lock = this.__transition__;
+ if (lock) ++lock.active;
+ }
+ d3.select = function(node) {
+ var group = [ typeof node === "string" ? d3_select(node, d3_document) : node ];
+ group.parentNode = d3_documentElement;
+ return d3_selection([ group ]);
+ };
+ d3.selectAll = function(nodes) {
+ var group = d3_array(typeof nodes === "string" ? d3_selectAll(nodes, d3_document) : nodes);
+ group.parentNode = d3_documentElement;
+ return d3_selection([ group ]);
+ };
+ var d3_selectionRoot = d3.select(d3_documentElement);
+ d3_selectionPrototype.on = function(type, listener, capture) {
+ var n = arguments.length;
+ if (n < 3) {
+ if (typeof type !== "string") {
+ if (n < 2) listener = false;
+ for (capture in type) this.each(d3_selection_on(capture, type[capture], listener));
+ return this;
+ }
+ if (n < 2) return (n = this.node()["__on" + type]) && n._;
+ capture = false;
+ }
+ return this.each(d3_selection_on(type, listener, capture));
+ };
+ function d3_selection_on(type, listener, capture) {
+ var name = "__on" + type, i = type.indexOf("."), wrap = d3_selection_onListener;
+ if (i > 0) type = type.substring(0, i);
+ var filter = d3_selection_onFilters.get(type);
+ if (filter) type = filter, wrap = d3_selection_onFilter;
+ function onRemove() {
+ var l = this[name];
+ if (l) {
+ this.removeEventListener(type, l, l.$);
+ delete this[name];
+ }
+ }
+ function onAdd() {
+ var l = wrap(listener, d3_array(arguments));
+ onRemove.call(this);
+ this.addEventListener(type, this[name] = l, l.$ = capture);
+ l._ = listener;
+ }
+ function removeAll() {
+ var re = new RegExp("^__on([^.]+)" + d3.requote(type) + "$"), match;
+ for (var name in this) {
+ if (match = name.match(re)) {
+ var l = this[name];
+ this.removeEventListener(match[1], l, l.$);
+ delete this[name];
+ }
+ }
+ }
+ return i ? listener ? onAdd : onRemove : listener ? d3_noop : removeAll;
+ }
+ var d3_selection_onFilters = d3.map({
+ mouseenter: "mouseover",
+ mouseleave: "mouseout"
+ });
+ d3_selection_onFilters.forEach(function(k) {
+ if ("on" + k in d3_document) d3_selection_onFilters.remove(k);
+ });
+ function d3_selection_onListener(listener, argumentz) {
+ return function(e) {
+ var o = d3.event;
+ d3.event = e;
+ argumentz[0] = this.__data__;
+ try {
+ listener.apply(this, argumentz);
+ } finally {
+ d3.event = o;
+ }
+ };
+ }
+ function d3_selection_onFilter(listener, argumentz) {
+ var l = d3_selection_onListener(listener, argumentz);
+ return function(e) {
+ var target = this, related = e.relatedTarget;
+ if (!related || related !== target && !(related.compareDocumentPosition(target) & 8)) {
+ l.call(target, e);
+ }
+ };
+ }
+ var d3_event_dragSelect = "onselectstart" in d3_document ? null : d3_vendorSymbol(d3_documentElement.style, "userSelect"), d3_event_dragId = 0;
+ function d3_event_dragSuppress() {
+ var name = ".dragsuppress-" + ++d3_event_dragId, click = "click" + name, w = d3.select(d3_window).on("touchmove" + name, d3_eventPreventDefault).on("dragstart" + name, d3_eventPreventDefault).on("selectstart" + name, d3_eventPreventDefault);
+ if (d3_event_dragSelect) {
+ var style = d3_documentElement.style, select = style[d3_event_dragSelect];
+ style[d3_event_dragSelect] = "none";
+ }
+ return function(suppressClick) {
+ w.on(name, null);
+ if (d3_event_dragSelect) style[d3_event_dragSelect] = select;
+ if (suppressClick) {
+ function off() {
+ w.on(click, null);
+ }
+ w.on(click, function() {
+ d3_eventPreventDefault();
+ off();
+ }, true);
+ setTimeout(off, 0);
+ }
+ };
+ }
+ d3.mouse = function(container) {
+ return d3_mousePoint(container, d3_eventSource());
+ };
+ var d3_mouse_bug44083 = /WebKit/.test(d3_window.navigator.userAgent) ? -1 : 0;
+ function d3_mousePoint(container, e) {
+ if (e.changedTouches) e = e.changedTouches[0];
+ var svg = container.ownerSVGElement || container;
+ if (svg.createSVGPoint) {
+ var point = svg.createSVGPoint();
+ if (d3_mouse_bug44083 < 0 && (d3_window.scrollX || d3_window.scrollY)) {
+ svg = d3.select("body").append("svg").style({
+ position: "absolute",
+ top: 0,
+ left: 0,
+ margin: 0,
+ padding: 0,
+ border: "none"
+ }, "important");
+ var ctm = svg[0][0].getScreenCTM();
+ d3_mouse_bug44083 = !(ctm.f || ctm.e);
+ svg.remove();
+ }
+ if (d3_mouse_bug44083) point.x = e.pageX, point.y = e.pageY; else point.x = e.clientX,
+ point.y = e.clientY;
+ point = point.matrixTransform(container.getScreenCTM().inverse());
+ return [ point.x, point.y ];
+ }
+ var rect = container.getBoundingClientRect();
+ return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ];
+ }
+ d3.touches = function(container, touches) {
+ if (arguments.length < 2) touches = d3_eventSource().touches;
+ return touches ? d3_array(touches).map(function(touch) {
+ var point = d3_mousePoint(container, touch);
+ point.identifier = touch.identifier;
+ return point;
+ }) : [];
+ };
+ d3.behavior.drag = function() {
+ var event = d3_eventDispatch(drag, "drag", "dragstart", "dragend"), origin = null, mousedown = dragstart(d3_noop, d3.mouse, "mousemove", "mouseup"), touchstart = dragstart(touchid, touchposition, "touchmove", "touchend");
+ function drag() {
+ this.on("mousedown.drag", mousedown).on("touchstart.drag", touchstart);
+ }
+ function touchid() {
+ return d3.event.changedTouches[0].identifier;
+ }
+ function touchposition(parent, id) {
+ return d3.touches(parent).filter(function(p) {
+ return p.identifier === id;
+ })[0];
+ }
+ function dragstart(id, position, move, end) {
+ return function() {
+ var target = this, parent = target.parentNode, event_ = event.of(target, arguments), eventTarget = d3.event.target, eventId = id(), drag = eventId == null ? "drag" : "drag-" + eventId, origin_ = position(parent, eventId), dragged = 0, offset, w = d3.select(d3_window).on(move + "." + drag, moved).on(end + "." + drag, ended), dragRestore = d3_event_dragSuppress();
+ if (origin) {
+ offset = origin.apply(target, arguments);
+ offset = [ offset.x - origin_[0], offset.y - origin_[1] ];
+ } else {
+ offset = [ 0, 0 ];
+ }
+ event_({
+ type: "dragstart"
+ });
+ function moved() {
+ var p = position(parent, eventId), dx = p[0] - origin_[0], dy = p[1] - origin_[1];
+ dragged |= dx | dy;
+ origin_ = p;
+ event_({
+ type: "drag",
+ x: p[0] + offset[0],
+ y: p[1] + offset[1],
+ dx: dx,
+ dy: dy
+ });
+ }
+ function ended() {
+ w.on(move + "." + drag, null).on(end + "." + drag, null);
+ dragRestore(dragged && d3.event.target === eventTarget);
+ event_({
+ type: "dragend"
+ });
+ }
+ };
+ }
+ drag.origin = function(x) {
+ if (!arguments.length) return origin;
+ origin = x;
+ return drag;
+ };
+ return d3.rebind(drag, event, "on");
+ };
+ var π = Math.PI, τ = 2 * π, halfπ = π / 2, ε = 1e-6, ε2 = ε * ε, d3_radians = π / 180, d3_degrees = 180 / π;
+ function d3_sgn(x) {
+ return x > 0 ? 1 : x < 0 ? -1 : 0;
+ }
+ function d3_cross2d(a, b, c) {
+ return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
+ }
+ function d3_acos(x) {
+ return x > 1 ? 0 : x < -1 ? π : Math.acos(x);
+ }
+ function d3_asin(x) {
+ return x > 1 ? halfπ : x < -1 ? -halfπ : Math.asin(x);
+ }
+ function d3_sinh(x) {
+ return ((x = Math.exp(x)) - 1 / x) / 2;
+ }
+ function d3_cosh(x) {
+ return ((x = Math.exp(x)) + 1 / x) / 2;
+ }
+ function d3_tanh(x) {
+ return ((x = Math.exp(2 * x)) - 1) / (x + 1);
+ }
+ function d3_haversin(x) {
+ return (x = Math.sin(x / 2)) * x;
+ }
+ var ρ = Math.SQRT2, ρ2 = 2, ρ4 = 4;
+ d3.interpolateZoom = function(p0, p1) {
+ var ux0 = p0[0], uy0 = p0[1], w0 = p0[2], ux1 = p1[0], uy1 = p1[1], w1 = p1[2];
+ var dx = ux1 - ux0, dy = uy1 - uy0, d2 = dx * dx + dy * dy, d1 = Math.sqrt(d2), b0 = (w1 * w1 - w0 * w0 + ρ4 * d2) / (2 * w0 * ρ2 * d1), b1 = (w1 * w1 - w0 * w0 - ρ4 * d2) / (2 * w1 * ρ2 * d1), r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0), r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1), dr = r1 - r0, S = (dr || Math.log(w1 / w0)) / ρ;
+ function interpolate(t) {
+ var s = t * S;
+ if (dr) {
+ var coshr0 = d3_cosh(r0), u = w0 / (ρ2 * d1) * (coshr0 * d3_tanh(ρ * s + r0) - d3_sinh(r0));
+ return [ ux0 + u * dx, uy0 + u * dy, w0 * coshr0 / d3_cosh(ρ * s + r0) ];
+ }
+ return [ ux0 + t * dx, uy0 + t * dy, w0 * Math.exp(ρ * s) ];
+ }
+ interpolate.duration = S * 1e3;
+ return interpolate;
+ };
+ d3.behavior.zoom = function() {
+ var view = {
+ x: 0,
+ y: 0,
+ k: 1
+ }, translate0, center, size = [ 960, 500 ], scaleExtent = d3_behavior_zoomInfinity, mousedown = "mousedown.zoom", mousemove = "mousemove.zoom", mouseup = "mouseup.zoom", mousewheelTimer, touchstart = "touchstart.zoom", touchtime, event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"), x0, x1, y0, y1;
+ function zoom(g) {
+ g.on(mousedown, mousedowned).on(d3_behavior_zoomWheel + ".zoom", mousewheeled).on(mousemove, mousewheelreset).on("dblclick.zoom", dblclicked).on(touchstart, touchstarted);
+ }
+ zoom.event = function(g) {
+ g.each(function() {
+ var event_ = event.of(this, arguments), view1 = view;
+ if (d3_transitionInheritId) {
+ d3.select(this).transition().each("start.zoom", function() {
+ view = this.__chart__ || {
+ x: 0,
+ y: 0,
+ k: 1
+ };
+ zoomstarted(event_);
+ }).tween("zoom:zoom", function() {
+ var dx = size[0], dy = size[1], cx = dx / 2, cy = dy / 2, i = d3.interpolateZoom([ (cx - view.x) / view.k, (cy - view.y) / view.k, dx / view.k ], [ (cx - view1.x) / view1.k, (cy - view1.y) / view1.k, dx / view1.k ]);
+ return function(t) {
+ var l = i(t), k = dx / l[2];
+ this.__chart__ = view = {
+ x: cx - l[0] * k,
+ y: cy - l[1] * k,
+ k: k
+ };
+ zoomed(event_);
+ };
+ }).each("end.zoom", function() {
+ zoomended(event_);
+ });
+ } else {
+ this.__chart__ = view;
+ zoomstarted(event_);
+ zoomed(event_);
+ zoomended(event_);
+ }
+ });
+ };
+ zoom.translate = function(_) {
+ if (!arguments.length) return [ view.x, view.y ];
+ view = {
+ x: +_[0],
+ y: +_[1],
+ k: view.k
+ };
+ rescale();
+ return zoom;
+ };
+ zoom.scale = function(_) {
+ if (!arguments.length) return view.k;
+ view = {
+ x: view.x,
+ y: view.y,
+ k: +_
+ };
+ rescale();
+ return zoom;
+ };
+ zoom.scaleExtent = function(_) {
+ if (!arguments.length) return scaleExtent;
+ scaleExtent = _ == null ? d3_behavior_zoomInfinity : [ +_[0], +_[1] ];
+ return zoom;
+ };
+ zoom.center = function(_) {
+ if (!arguments.length) return center;
+ center = _ && [ +_[0], +_[1] ];
+ return zoom;
+ };
+ zoom.size = function(_) {
+ if (!arguments.length) return size;
+ size = _ && [ +_[0], +_[1] ];
+ return zoom;
+ };
+ zoom.x = function(z) {
+ if (!arguments.length) return x1;
+ x1 = z;
+ x0 = z.copy();
+ view = {
+ x: 0,
+ y: 0,
+ k: 1
+ };
+ return zoom;
+ };
+ zoom.y = function(z) {
+ if (!arguments.length) return y1;
+ y1 = z;
+ y0 = z.copy();
+ view = {
+ x: 0,
+ y: 0,
+ k: 1
+ };
+ return zoom;
+ };
+ function location(p) {
+ return [ (p[0] - view.x) / view.k, (p[1] - view.y) / view.k ];
+ }
+ function point(l) {
+ return [ l[0] * view.k + view.x, l[1] * view.k + view.y ];
+ }
+ function scaleTo(s) {
+ view.k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], s));
+ }
+ function translateTo(p, l) {
+ l = point(l);
+ view.x += p[0] - l[0];
+ view.y += p[1] - l[1];
+ }
+ function rescale() {
+ if (x1) x1.domain(x0.range().map(function(x) {
+ return (x - view.x) / view.k;
+ }).map(x0.invert));
+ if (y1) y1.domain(y0.range().map(function(y) {
+ return (y - view.y) / view.k;
+ }).map(y0.invert));
+ }
+ function zoomstarted(event) {
+ event({
+ type: "zoomstart"
+ });
+ }
+ function zoomed(event) {
+ rescale();
+ event({
+ type: "zoom",
+ scale: view.k,
+ translate: [ view.x, view.y ]
+ });
+ }
+ function zoomended(event) {
+ event({
+ type: "zoomend"
+ });
+ }
+ function mousedowned() {
+ var target = this, event_ = event.of(target, arguments), eventTarget = d3.event.target, dragged = 0, w = d3.select(d3_window).on(mousemove, moved).on(mouseup, ended), l = location(d3.mouse(target)), dragRestore = d3_event_dragSuppress();
+ d3_selection_interrupt.call(target);
+ zoomstarted(event_);
+ function moved() {
+ dragged = 1;
+ translateTo(d3.mouse(target), l);
+ zoomed(event_);
+ }
+ function ended() {
+ w.on(mousemove, d3_window === target ? mousewheelreset : null).on(mouseup, null);
+ dragRestore(dragged && d3.event.target === eventTarget);
+ zoomended(event_);
+ }
+ }
+ function touchstarted() {
+ var target = this, event_ = event.of(target, arguments), locations0 = {}, distance0 = 0, scale0, eventId = d3.event.changedTouches[0].identifier, touchmove = "touchmove.zoom-" + eventId, touchend = "touchend.zoom-" + eventId, w = d3.select(d3_window).on(touchmove, moved).on(touchend, ended), t = d3.select(target).on(mousedown, null).on(touchstart, started), dragRestore = d3_event_dragSuppress();
+ d3_selection_interrupt.call(target);
+ started();
+ zoomstarted(event_);
+ function relocate() {
+ var touches = d3.touches(target);
+ scale0 = view.k;
+ touches.forEach(function(t) {
+ if (t.identifier in locations0) locations0[t.identifier] = location(t);
+ });
+ return touches;
+ }
+ function started() {
+ var changed = d3.event.changedTouches;
+ for (var i = 0, n = changed.length; i < n; ++i) {
+ locations0[changed[i].identifier] = null;
+ }
+ var touches = relocate(), now = Date.now();
+ if (touches.length === 1) {
+ if (now - touchtime < 500) {
+ var p = touches[0], l = locations0[p.identifier];
+ scaleTo(view.k * 2);
+ translateTo(p, l);
+ d3_eventPreventDefault();
+ zoomed(event_);
+ }
+ touchtime = now;
+ } else if (touches.length > 1) {
+ var p = touches[0], q = touches[1], dx = p[0] - q[0], dy = p[1] - q[1];
+ distance0 = dx * dx + dy * dy;
+ }
+ }
+ function moved() {
+ var touches = d3.touches(target), p0, l0, p1, l1;
+ for (var i = 0, n = touches.length; i < n; ++i, l1 = null) {
+ p1 = touches[i];
+ if (l1 = locations0[p1.identifier]) {
+ if (l0) break;
+ p0 = p1, l0 = l1;
+ }
+ }
+ if (l1) {
+ var distance1 = (distance1 = p1[0] - p0[0]) * distance1 + (distance1 = p1[1] - p0[1]) * distance1, scale1 = distance0 && Math.sqrt(distance1 / distance0);
+ p0 = [ (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2 ];
+ l0 = [ (l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2 ];
+ scaleTo(scale1 * scale0);
+ }
+ touchtime = null;
+ translateTo(p0, l0);
+ zoomed(event_);
+ }
+ function ended() {
+ if (d3.event.touches.length) {
+ var changed = d3.event.changedTouches;
+ for (var i = 0, n = changed.length; i < n; ++i) {
+ delete locations0[changed[i].identifier];
+ }
+ for (var identifier in locations0) {
+ return void relocate();
+ }
+ }
+ w.on(touchmove, null).on(touchend, null);
+ t.on(mousedown, mousedowned).on(touchstart, touchstarted);
+ dragRestore();
+ zoomended(event_);
+ }
+ }
+ function mousewheeled() {
+ var event_ = event.of(this, arguments);
+ if (mousewheelTimer) clearTimeout(mousewheelTimer); else d3_selection_interrupt.call(this),
+ zoomstarted(event_);
+ mousewheelTimer = setTimeout(function() {
+ mousewheelTimer = null;
+ zoomended(event_);
+ }, 50);
+ d3_eventPreventDefault();
+ var point = center || d3.mouse(this);
+ if (!translate0) translate0 = location(point);
+ scaleTo(Math.pow(2, d3_behavior_zoomDelta() * .002) * view.k);
+ translateTo(point, translate0);
+ zoomed(event_);
+ }
+ function mousewheelreset() {
+ translate0 = null;
+ }
+ function dblclicked() {
+ var event_ = event.of(this, arguments), p = d3.mouse(this), l = location(p), k = Math.log(view.k) / Math.LN2;
+ zoomstarted(event_);
+ scaleTo(Math.pow(2, d3.event.shiftKey ? Math.ceil(k) - 1 : Math.floor(k) + 1));
+ translateTo(p, l);
+ zoomed(event_);
+ zoomended(event_);
+ }
+ return d3.rebind(zoom, event, "on");
+ };
+ var d3_behavior_zoomInfinity = [ 0, Infinity ];
+ var d3_behavior_zoomDelta, d3_behavior_zoomWheel = "onwheel" in d3_document ? (d3_behavior_zoomDelta = function() {
+ return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1);
+ }, "wheel") : "onmousewheel" in d3_document ? (d3_behavior_zoomDelta = function() {
+ return d3.event.wheelDelta;
+ }, "mousewheel") : (d3_behavior_zoomDelta = function() {
+ return -d3.event.detail;
+ }, "MozMousePixelScroll");
+ function d3_Color() {}
+ d3_Color.prototype.toString = function() {
+ return this.rgb() + "";
+ };
+ d3.hsl = function(h, s, l) {
+ return arguments.length === 1 ? h instanceof d3_Hsl ? d3_hsl(h.h, h.s, h.l) : d3_rgb_parse("" + h, d3_rgb_hsl, d3_hsl) : d3_hsl(+h, +s, +l);
+ };
+ function d3_hsl(h, s, l) {
+ return new d3_Hsl(h, s, l);
+ }
+ function d3_Hsl(h, s, l) {
+ this.h = h;
+ this.s = s;
+ this.l = l;
+ }
+ var d3_hslPrototype = d3_Hsl.prototype = new d3_Color();
+ d3_hslPrototype.brighter = function(k) {
+ k = Math.pow(.7, arguments.length ? k : 1);
+ return d3_hsl(this.h, this.s, this.l / k);
+ };
+ d3_hslPrototype.darker = function(k) {
+ k = Math.pow(.7, arguments.length ? k : 1);
+ return d3_hsl(this.h, this.s, k * this.l);
+ };
+ d3_hslPrototype.rgb = function() {
+ return d3_hsl_rgb(this.h, this.s, this.l);
+ };
+ function d3_hsl_rgb(h, s, l) {
+ var m1, m2;
+ h = isNaN(h) ? 0 : (h %= 360) < 0 ? h + 360 : h;
+ s = isNaN(s) ? 0 : s < 0 ? 0 : s > 1 ? 1 : s;
+ l = l < 0 ? 0 : l > 1 ? 1 : l;
+ m2 = l <= .5 ? l * (1 + s) : l + s - l * s;
+ m1 = 2 * l - m2;
+ function v(h) {
+ if (h > 360) h -= 360; else if (h < 0) h += 360;
+ if (h < 60) return m1 + (m2 - m1) * h / 60;
+ if (h < 180) return m2;
+ if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60;
+ return m1;
+ }
+ function vv(h) {
+ return Math.round(v(h) * 255);
+ }
+ return d3_rgb(vv(h + 120), vv(h), vv(h - 120));
+ }
+ d3.hcl = function(h, c, l) {
+ return arguments.length === 1 ? h instanceof d3_Hcl ? d3_hcl(h.h, h.c, h.l) : h instanceof d3_Lab ? d3_lab_hcl(h.l, h.a, h.b) : d3_lab_hcl((h = d3_rgb_lab((h = d3.rgb(h)).r, h.g, h.b)).l, h.a, h.b) : d3_hcl(+h, +c, +l);
+ };
+ function d3_hcl(h, c, l) {
+ return new d3_Hcl(h, c, l);
+ }
+ function d3_Hcl(h, c, l) {
+ this.h = h;
+ this.c = c;
+ this.l = l;
+ }
+ var d3_hclPrototype = d3_Hcl.prototype = new d3_Color();
+ d3_hclPrototype.brighter = function(k) {
+ return d3_hcl(this.h, this.c, Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)));
+ };
+ d3_hclPrototype.darker = function(k) {
+ return d3_hcl(this.h, this.c, Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)));
+ };
+ d3_hclPrototype.rgb = function() {
+ return d3_hcl_lab(this.h, this.c, this.l).rgb();
+ };
+ function d3_hcl_lab(h, c, l) {
+ if (isNaN(h)) h = 0;
+ if (isNaN(c)) c = 0;
+ return d3_lab(l, Math.cos(h *= d3_radians) * c, Math.sin(h) * c);
+ }
+ d3.lab = function(l, a, b) {
+ return arguments.length === 1 ? l instanceof d3_Lab ? d3_lab(l.l, l.a, l.b) : l instanceof d3_Hcl ? d3_hcl_lab(l.l, l.c, l.h) : d3_rgb_lab((l = d3.rgb(l)).r, l.g, l.b) : d3_lab(+l, +a, +b);
+ };
+ function d3_lab(l, a, b) {
+ return new d3_Lab(l, a, b);
+ }
+ function d3_Lab(l, a, b) {
+ this.l = l;
+ this.a = a;
+ this.b = b;
+ }
+ var d3_lab_K = 18;
+ var d3_lab_X = .95047, d3_lab_Y = 1, d3_lab_Z = 1.08883;
+ var d3_labPrototype = d3_Lab.prototype = new d3_Color();
+ d3_labPrototype.brighter = function(k) {
+ return d3_lab(Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)), this.a, this.b);
+ };
+ d3_labPrototype.darker = function(k) {
+ return d3_lab(Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)), this.a, this.b);
+ };
+ d3_labPrototype.rgb = function() {
+ return d3_lab_rgb(this.l, this.a, this.b);
+ };
+ function d3_lab_rgb(l, a, b) {
+ var y = (l + 16) / 116, x = y + a / 500, z = y - b / 200;
+ x = d3_lab_xyz(x) * d3_lab_X;
+ y = d3_lab_xyz(y) * d3_lab_Y;
+ z = d3_lab_xyz(z) * d3_lab_Z;
+ return d3_rgb(d3_xyz_rgb(3.2404542 * x - 1.5371385 * y - .4985314 * z), d3_xyz_rgb(-.969266 * x + 1.8760108 * y + .041556 * z), d3_xyz_rgb(.0556434 * x - .2040259 * y + 1.0572252 * z));
+ }
+ function d3_lab_hcl(l, a, b) {
+ return l > 0 ? d3_hcl(Math.atan2(b, a) * d3_degrees, Math.sqrt(a * a + b * b), l) : d3_hcl(NaN, NaN, l);
+ }
+ function d3_lab_xyz(x) {
+ return x > .206893034 ? x * x * x : (x - 4 / 29) / 7.787037;
+ }
+ function d3_xyz_lab(x) {
+ return x > .008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29;
+ }
+ function d3_xyz_rgb(r) {
+ return Math.round(255 * (r <= .00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - .055));
+ }
+ d3.rgb = function(r, g, b) {
+ return arguments.length === 1 ? r instanceof d3_Rgb ? d3_rgb(r.r, r.g, r.b) : d3_rgb_parse("" + r, d3_rgb, d3_hsl_rgb) : d3_rgb(~~r, ~~g, ~~b);
+ };
+ function d3_rgbNumber(value) {
+ return d3_rgb(value >> 16, value >> 8 & 255, value & 255);
+ }
+ function d3_rgbString(value) {
+ return d3_rgbNumber(value) + "";
+ }
+ function d3_rgb(r, g, b) {
+ return new d3_Rgb(r, g, b);
+ }
+ function d3_Rgb(r, g, b) {
+ this.r = r;
+ this.g = g;
+ this.b = b;
+ }
+ var d3_rgbPrototype = d3_Rgb.prototype = new d3_Color();
+ d3_rgbPrototype.brighter = function(k) {
+ k = Math.pow(.7, arguments.length ? k : 1);
+ var r = this.r, g = this.g, b = this.b, i = 30;
+ if (!r && !g && !b) return d3_rgb(i, i, i);
+ if (r && r < i) r = i;
+ if (g && g < i) g = i;
+ if (b && b < i) b = i;
+ return d3_rgb(Math.min(255, ~~(r / k)), Math.min(255, ~~(g / k)), Math.min(255, ~~(b / k)));
+ };
+ d3_rgbPrototype.darker = function(k) {
+ k = Math.pow(.7, arguments.length ? k : 1);
+ return d3_rgb(~~(k * this.r), ~~(k * this.g), ~~(k * this.b));
+ };
+ d3_rgbPrototype.hsl = function() {
+ return d3_rgb_hsl(this.r, this.g, this.b);
+ };
+ d3_rgbPrototype.toString = function() {
+ return "#" + d3_rgb_hex(this.r) + d3_rgb_hex(this.g) + d3_rgb_hex(this.b);
+ };
+ function d3_rgb_hex(v) {
+ return v < 16 ? "0" + Math.max(0, v).toString(16) : Math.min(255, v).toString(16);
+ }
+ function d3_rgb_parse(format, rgb, hsl) {
+ var r = 0, g = 0, b = 0, m1, m2, name;
+ m1 = /([a-z]+)\((.*)\)/i.exec(format);
+ if (m1) {
+ m2 = m1[2].split(",");
+ switch (m1[1]) {
+ case "hsl":
+ {
+ return hsl(parseFloat(m2[0]), parseFloat(m2[1]) / 100, parseFloat(m2[2]) / 100);
+ }
+
+ case "rgb":
+ {
+ return rgb(d3_rgb_parseNumber(m2[0]), d3_rgb_parseNumber(m2[1]), d3_rgb_parseNumber(m2[2]));
+ }
+ }
+ }
+ if (name = d3_rgb_names.get(format)) return rgb(name.r, name.g, name.b);
+ if (format != null && format.charAt(0) === "#") {
+ if (format.length === 4) {
+ r = format.charAt(1);
+ r += r;
+ g = format.charAt(2);
+ g += g;
+ b = format.charAt(3);
+ b += b;
+ } else if (format.length === 7) {
+ r = format.substring(1, 3);
+ g = format.substring(3, 5);
+ b = format.substring(5, 7);
+ }
+ r = parseInt(r, 16);
+ g = parseInt(g, 16);
+ b = parseInt(b, 16);
+ }
+ return rgb(r, g, b);
+ }
+ function d3_rgb_hsl(r, g, b) {
+ var min = Math.min(r /= 255, g /= 255, b /= 255), max = Math.max(r, g, b), d = max - min, h, s, l = (max + min) / 2;
+ if (d) {
+ s = l < .5 ? d / (max + min) : d / (2 - max - min);
+ if (r == max) h = (g - b) / d + (g < b ? 6 : 0); else if (g == max) h = (b - r) / d + 2; else h = (r - g) / d + 4;
+ h *= 60;
+ } else {
+ h = NaN;
+ s = l > 0 && l < 1 ? 0 : h;
+ }
+ return d3_hsl(h, s, l);
+ }
+ function d3_rgb_lab(r, g, b) {
+ r = d3_rgb_xyz(r);
+ g = d3_rgb_xyz(g);
+ b = d3_rgb_xyz(b);
+ var x = d3_xyz_lab((.4124564 * r + .3575761 * g + .1804375 * b) / d3_lab_X), y = d3_xyz_lab((.2126729 * r + .7151522 * g + .072175 * b) / d3_lab_Y), z = d3_xyz_lab((.0193339 * r + .119192 * g + .9503041 * b) / d3_lab_Z);
+ return d3_lab(116 * y - 16, 500 * (x - y), 200 * (y - z));
+ }
+ function d3_rgb_xyz(r) {
+ return (r /= 255) <= .04045 ? r / 12.92 : Math.pow((r + .055) / 1.055, 2.4);
+ }
+ function d3_rgb_parseNumber(c) {
+ var f = parseFloat(c);
+ return c.charAt(c.length - 1) === "%" ? Math.round(f * 2.55) : f;
+ }
+ var d3_rgb_names = d3.map({
+ aliceblue: 15792383,
+ antiquewhite: 16444375,
+ aqua: 65535,
+ aquamarine: 8388564,
+ azure: 15794175,
+ beige: 16119260,
+ bisque: 16770244,
+ black: 0,
+ blanchedalmond: 16772045,
+ blue: 255,
+ blueviolet: 9055202,
+ brown: 10824234,
+ burlywood: 14596231,
+ cadetblue: 6266528,
+ chartreuse: 8388352,
+ chocolate: 13789470,
+ coral: 16744272,
+ cornflowerblue: 6591981,
+ cornsilk: 16775388,
+ crimson: 14423100,
+ cyan: 65535,
+ darkblue: 139,
+ darkcyan: 35723,
+ darkgoldenrod: 12092939,
+ darkgray: 11119017,
+ darkgreen: 25600,
+ darkgrey: 11119017,
+ darkkhaki: 12433259,
+ darkmagenta: 9109643,
+ darkolivegreen: 5597999,
+ darkorange: 16747520,
+ darkorchid: 10040012,
+ darkred: 9109504,
+ darksalmon: 15308410,
+ darkseagreen: 9419919,
+ darkslateblue: 4734347,
+ darkslategray: 3100495,
+ darkslategrey: 3100495,
+ darkturquoise: 52945,
+ darkviolet: 9699539,
+ deeppink: 16716947,
+ deepskyblue: 49151,
+ dimgray: 6908265,
+ dimgrey: 6908265,
+ dodgerblue: 2003199,
+ firebrick: 11674146,
+ floralwhite: 16775920,
+ forestgreen: 2263842,
+ fuchsia: 16711935,
+ gainsboro: 14474460,
+ ghostwhite: 16316671,
+ gold: 16766720,
+ goldenrod: 14329120,
+ gray: 8421504,
+ green: 32768,
+ greenyellow: 11403055,
+ grey: 8421504,
+ honeydew: 15794160,
+ hotpink: 16738740,
+ indianred: 13458524,
+ indigo: 4915330,
+ ivory: 16777200,
+ khaki: 15787660,
+ lavender: 15132410,
+ lavenderblush: 16773365,
+ lawngreen: 8190976,
+ lemonchiffon: 16775885,
+ lightblue: 11393254,
+ lightcoral: 15761536,
+ lightcyan: 14745599,
+ lightgoldenrodyellow: 16448210,
+ lightgray: 13882323,
+ lightgreen: 9498256,
+ lightgrey: 13882323,
+ lightpink: 16758465,
+ lightsalmon: 16752762,
+ lightseagreen: 2142890,
+ lightskyblue: 8900346,
+ lightslategray: 7833753,
+ lightslategrey: 7833753,
+ lightsteelblue: 11584734,
+ lightyellow: 16777184,
+ lime: 65280,
+ limegreen: 3329330,
+ linen: 16445670,
+ magenta: 16711935,
+ maroon: 8388608,
+ mediumaquamarine: 6737322,
+ mediumblue: 205,
+ mediumorchid: 12211667,
+ mediumpurple: 9662683,
+ mediumseagreen: 3978097,
+ mediumslateblue: 8087790,
+ mediumspringgreen: 64154,
+ mediumturquoise: 4772300,
+ mediumvioletred: 13047173,
+ midnightblue: 1644912,
+ mintcream: 16121850,
+ mistyrose: 16770273,
+ moccasin: 16770229,
+ navajowhite: 16768685,
+ navy: 128,
+ oldlace: 16643558,
+ olive: 8421376,
+ olivedrab: 7048739,
+ orange: 16753920,
+ orangered: 16729344,
+ orchid: 14315734,
+ palegoldenrod: 15657130,
+ palegreen: 10025880,
+ paleturquoise: 11529966,
+ palevioletred: 14381203,
+ papayawhip: 16773077,
+ peachpuff: 16767673,
+ peru: 13468991,
+ pink: 16761035,
+ plum: 14524637,
+ powderblue: 11591910,
+ purple: 8388736,
+ red: 16711680,
+ rosybrown: 12357519,
+ royalblue: 4286945,
+ saddlebrown: 9127187,
+ salmon: 16416882,
+ sandybrown: 16032864,
+ seagreen: 3050327,
+ seashell: 16774638,
+ sienna: 10506797,
+ silver: 12632256,
+ skyblue: 8900331,
+ slateblue: 6970061,
+ slategray: 7372944,
+ slategrey: 7372944,
+ snow: 16775930,
+ springgreen: 65407,
+ steelblue: 4620980,
+ tan: 13808780,
+ teal: 32896,
+ thistle: 14204888,
+ tomato: 16737095,
+ turquoise: 4251856,
+ violet: 15631086,
+ wheat: 16113331,
+ white: 16777215,
+ whitesmoke: 16119285,
+ yellow: 16776960,
+ yellowgreen: 10145074
+ });
+ d3_rgb_names.forEach(function(key, value) {
+ d3_rgb_names.set(key, d3_rgbNumber(value));
+ });
+ function d3_functor(v) {
+ return typeof v === "function" ? v : function() {
+ return v;
+ };
+ }
+ d3.functor = d3_functor;
+ function d3_identity(d) {
+ return d;
+ }
+ d3.xhr = d3_xhrType(d3_identity);
+ function d3_xhrType(response) {
+ return function(url, mimeType, callback) {
+ if (arguments.length === 2 && typeof mimeType === "function") callback = mimeType,
+ mimeType = null;
+ return d3_xhr(url, mimeType, response, callback);
+ };
+ }
+ function d3_xhr(url, mimeType, response, callback) {
+ var xhr = {}, dispatch = d3.dispatch("beforesend", "progress", "load", "error"), headers = {}, request = new XMLHttpRequest(), responseType = null;
+ if (d3_window.XDomainRequest && !("withCredentials" in request) && /^(http(s)?:)?\/\//.test(url)) request = new XDomainRequest();
+ "onload" in request ? request.onload = request.onerror = respond : request.onreadystatechange = function() {
+ request.readyState > 3 && respond();
+ };
+ function respond() {
+ var status = request.status, result;
+ if (!status && request.responseText || status >= 200 && status < 300 || status === 304) {
+ try {
+ result = response.call(xhr, request);
+ } catch (e) {
+ dispatch.error.call(xhr, e);
+ return;
+ }
+ dispatch.load.call(xhr, result);
+ } else {
+ dispatch.error.call(xhr, request);
+ }
+ }
+ request.onprogress = function(event) {
+ var o = d3.event;
+ d3.event = event;
+ try {
+ dispatch.progress.call(xhr, request);
+ } finally {
+ d3.event = o;
+ }
+ };
+ xhr.header = function(name, value) {
+ name = (name + "").toLowerCase();
+ if (arguments.length < 2) return headers[name];
+ if (value == null) delete headers[name]; else headers[name] = value + "";
+ return xhr;
+ };
+ xhr.mimeType = function(value) {
+ if (!arguments.length) return mimeType;
+ mimeType = value == null ? null : value + "";
+ return xhr;
+ };
+ xhr.responseType = function(value) {
+ if (!arguments.length) return responseType;
+ responseType = value;
+ return xhr;
+ };
+ xhr.response = function(value) {
+ response = value;
+ return xhr;
+ };
+ [ "get", "post" ].forEach(function(method) {
+ xhr[method] = function() {
+ return xhr.send.apply(xhr, [ method ].concat(d3_array(arguments)));
+ };
+ });
+ xhr.send = function(method, data, callback) {
+ if (arguments.length === 2 && typeof data === "function") callback = data, data = null;
+ request.open(method, url, true);
+ if (mimeType != null && !("accept" in headers)) headers["accept"] = mimeType + ",*/*";
+ if (request.setRequestHeader) for (var name in headers) request.setRequestHeader(name, headers[name]);
+ if (mimeType != null && request.overrideMimeType) request.overrideMimeType(mimeType);
+ if (responseType != null) request.responseType = responseType;
+ if (callback != null) xhr.on("error", callback).on("load", function(request) {
+ callback(null, request);
+ });
+ dispatch.beforesend.call(xhr, request);
+ request.send(data == null ? null : data);
+ return xhr;
+ };
+ xhr.abort = function() {
+ request.abort();
+ return xhr;
+ };
+ d3.rebind(xhr, dispatch, "on");
+ return callback == null ? xhr : xhr.get(d3_xhr_fixCallback(callback));
+ }
+ function d3_xhr_fixCallback(callback) {
+ return callback.length === 1 ? function(error, request) {
+ callback(error == null ? request : null);
+ } : callback;
+ }
+ d3.dsv = function(delimiter, mimeType) {
+ var reFormat = new RegExp('["' + delimiter + "\n]"), delimiterCode = delimiter.charCodeAt(0);
+ function dsv(url, row, callback) {
+ if (arguments.length < 3) callback = row, row = null;
+ var xhr = d3_xhr(url, mimeType, row == null ? response : typedResponse(row), callback);
+ xhr.row = function(_) {
+ return arguments.length ? xhr.response((row = _) == null ? response : typedResponse(_)) : row;
+ };
+ return xhr;
+ }
+ function response(request) {
+ return dsv.parse(request.responseText);
+ }
+ function typedResponse(f) {
+ return function(request) {
+ return dsv.parse(request.responseText, f);
+ };
+ }
+ dsv.parse = function(text, f) {
+ var o;
+ return dsv.parseRows(text, function(row, i) {
+ if (o) return o(row, i - 1);
+ var a = new Function("d", "return {" + row.map(function(name, i) {
+ return JSON.stringify(name) + ": d[" + i + "]";
+ }).join(",") + "}");
+ o = f ? function(row, i) {
+ return f(a(row), i);
+ } : a;
+ });
+ };
+ dsv.parseRows = function(text, f) {
+ var EOL = {}, EOF = {}, rows = [], N = text.length, I = 0, n = 0, t, eol;
+ function token() {
+ if (I >= N) return EOF;
+ if (eol) return eol = false, EOL;
+ var j = I;
+ if (text.charCodeAt(j) === 34) {
+ var i = j;
+ while (i++ < N) {
+ if (text.charCodeAt(i) === 34) {
+ if (text.charCodeAt(i + 1) !== 34) break;
+ ++i;
+ }
+ }
+ I = i + 2;
+ var c = text.charCodeAt(i + 1);
+ if (c === 13) {
+ eol = true;
+ if (text.charCodeAt(i + 2) === 10) ++I;
+ } else if (c === 10) {
+ eol = true;
+ }
+ return text.substring(j + 1, i).replace(/""/g, '"');
+ }
+ while (I < N) {
+ var c = text.charCodeAt(I++), k = 1;
+ if (c === 10) eol = true; else if (c === 13) {
+ eol = true;
+ if (text.charCodeAt(I) === 10) ++I, ++k;
+ } else if (c !== delimiterCode) continue;
+ return text.substring(j, I - k);
+ }
+ return text.substring(j);
+ }
+ while ((t = token()) !== EOF) {
+ var a = [];
+ while (t !== EOL && t !== EOF) {
+ a.push(t);
+ t = token();
+ }
+ if (f && !(a = f(a, n++))) continue;
+ rows.push(a);
+ }
+ return rows;
+ };
+ dsv.format = function(rows) {
+ if (Array.isArray(rows[0])) return dsv.formatRows(rows);
+ var fieldSet = new d3_Set(), fields = [];
+ rows.forEach(function(row) {
+ for (var field in row) {
+ if (!fieldSet.has(field)) {
+ fields.push(fieldSet.add(field));
+ }
+ }
+ });
+ return [ fields.map(formatValue).join(delimiter) ].concat(rows.map(function(row) {
+ return fields.map(function(field) {
+ return formatValue(row[field]);
+ }).join(delimiter);
+ })).join("\n");
+ };
+ dsv.formatRows = function(rows) {
+ return rows.map(formatRow).join("\n");
+ };
+ function formatRow(row) {
+ return row.map(formatValue).join(delimiter);
+ }
+ function formatValue(text) {
+ return reFormat.test(text) ? '"' + text.replace(/\"/g, '""') + '"' : text;
+ }
+ return dsv;
+ };
+ d3.csv = d3.dsv(",", "text/csv");
+ d3.tsv = d3.dsv(" ", "text/tab-separated-values");
+ var d3_timer_queueHead, d3_timer_queueTail, d3_timer_interval, d3_timer_timeout, d3_timer_active, d3_timer_frame = d3_window[d3_vendorSymbol(d3_window, "requestAnimationFrame")] || function(callback) {
+ setTimeout(callback, 17);
+ };
+ d3.timer = function(callback, delay, then) {
+ var n = arguments.length;
+ if (n < 2) delay = 0;
+ if (n < 3) then = Date.now();
+ var time = then + delay, timer = {
+ c: callback,
+ t: time,
+ f: false,
+ n: null
+ };
+ if (d3_timer_queueTail) d3_timer_queueTail.n = timer; else d3_timer_queueHead = timer;
+ d3_timer_queueTail = timer;
+ if (!d3_timer_interval) {
+ d3_timer_timeout = clearTimeout(d3_timer_timeout);
+ d3_timer_interval = 1;
+ d3_timer_frame(d3_timer_step);
+ }
+ };
+ function d3_timer_step() {
+ var now = d3_timer_mark(), delay = d3_timer_sweep() - now;
+ if (delay > 24) {
+ if (isFinite(delay)) {
+ clearTimeout(d3_timer_timeout);
+ d3_timer_timeout = setTimeout(d3_timer_step, delay);
+ }
+ d3_timer_interval = 0;
+ } else {
+ d3_timer_interval = 1;
+ d3_timer_frame(d3_timer_step);
+ }
+ }
+ d3.timer.flush = function() {
+ d3_timer_mark();
+ d3_timer_sweep();
+ };
+ function d3_timer_mark() {
+ var now = Date.now();
+ d3_timer_active = d3_timer_queueHead;
+ while (d3_timer_active) {
+ if (now >= d3_timer_active.t) d3_timer_active.f = d3_timer_active.c(now - d3_timer_active.t);
+ d3_timer_active = d3_timer_active.n;
+ }
+ return now;
+ }
+ function d3_timer_sweep() {
+ var t0, t1 = d3_timer_queueHead, time = Infinity;
+ while (t1) {
+ if (t1.f) {
+ t1 = t0 ? t0.n = t1.n : d3_timer_queueHead = t1.n;
+ } else {
+ if (t1.t < time) time = t1.t;
+ t1 = (t0 = t1).n;
+ }
+ }
+ d3_timer_queueTail = t0;
+ return time;
+ }
+ function d3_format_precision(x, p) {
+ return p - (x ? Math.ceil(Math.log(x) / Math.LN10) : 1);
+ }
+ d3.round = function(x, n) {
+ return n ? Math.round(x * (n = Math.pow(10, n))) / n : Math.round(x);
+ };
+ var d3_formatPrefixes = [ "y", "z", "a", "f", "p", "n", "µ", "m", "", "k", "M", "G", "T", "P", "E", "Z", "Y" ].map(d3_formatPrefix);
+ d3.formatPrefix = function(value, precision) {
+ var i = 0;
+ if (value) {
+ if (value < 0) value *= -1;
+ if (precision) value = d3.round(value, d3_format_precision(value, precision));
+ i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10);
+ i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3));
+ }
+ return d3_formatPrefixes[8 + i / 3];
+ };
+ function d3_formatPrefix(d, i) {
+ var k = Math.pow(10, abs(8 - i) * 3);
+ return {
+ scale: i > 8 ? function(d) {
+ return d / k;
+ } : function(d) {
+ return d * k;
+ },
+ symbol: d
+ };
+ }
+ function d3_locale_numberFormat(locale) {
+ var locale_decimal = locale.decimal, locale_thousands = locale.thousands, locale_grouping = locale.grouping, locale_currency = locale.currency, formatGroup = locale_grouping ? function(value) {
+ var i = value.length, t = [], j = 0, g = locale_grouping[0];
+ while (i > 0 && g > 0) {
+ t.push(value.substring(i -= g, i + g));
+ g = locale_grouping[j = (j + 1) % locale_grouping.length];
+ }
+ return t.reverse().join(locale_thousands);
+ } : d3_identity;
+ return function(specifier) {
+ var match = d3_format_re.exec(specifier), fill = match[1] || " ", align = match[2] || ">", sign = match[3] || "", symbol = match[4] || "", zfill = match[5], width = +match[6], comma = match[7], precision = match[8], type = match[9], scale = 1, prefix = "", suffix = "", integer = false;
+ if (precision) precision = +precision.substring(1);
+ if (zfill || fill === "0" && align === "=") {
+ zfill = fill = "0";
+ align = "=";
+ if (comma) width -= Math.floor((width - 1) / 4);
+ }
+ switch (type) {
+ case "n":
+ comma = true;
+ type = "g";
+ break;
+
+ case "%":
+ scale = 100;
+ suffix = "%";
+ type = "f";
+ break;
+
+ case "p":
+ scale = 100;
+ suffix = "%";
+ type = "r";
+ break;
+
+ case "b":
+ case "o":
+ case "x":
+ case "X":
+ if (symbol === "#") prefix = "0" + type.toLowerCase();
+
+ case "c":
+ case "d":
+ integer = true;
+ precision = 0;
+ break;
+
+ case "s":
+ scale = -1;
+ type = "r";
+ break;
+ }
+ if (symbol === "$") prefix = locale_currency[0], suffix = locale_currency[1];
+ if (type == "r" && !precision) type = "g";
+ if (precision != null) {
+ if (type == "g") precision = Math.max(1, Math.min(21, precision)); else if (type == "e" || type == "f") precision = Math.max(0, Math.min(20, precision));
+ }
+ type = d3_format_types.get(type) || d3_format_typeDefault;
+ var zcomma = zfill && comma;
+ return function(value) {
+ var fullSuffix = suffix;
+ if (integer && value % 1) return "";
+ var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, "-") : sign;
+ if (scale < 0) {
+ var unit = d3.formatPrefix(value, precision);
+ value = unit.scale(value);
+ fullSuffix = unit.symbol + suffix;
+ } else {
+ value *= scale;
+ }
+ value = type(value, precision);
+ var i = value.lastIndexOf("."), before = i < 0 ? value : value.substring(0, i), after = i < 0 ? "" : locale_decimal + value.substring(i + 1);
+ if (!zfill && comma) before = formatGroup(before);
+ var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length), padding = length < width ? new Array(length = width - length + 1).join(fill) : "";
+ if (zcomma) before = formatGroup(padding + before);
+ negative += prefix;
+ value = before + after;
+ return (align === "<" ? negative + value + padding : align === ">" ? padding + negative + value : align === "^" ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) : negative + (zcomma ? value : padding + value)) + fullSuffix;
+ };
+ };
+ }
+ var d3_format_re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i;
+ var d3_format_types = d3.map({
+ b: function(x) {
+ return x.toString(2);
+ },
+ c: function(x) {
+ return String.fromCharCode(x);
+ },
+ o: function(x) {
+ return x.toString(8);
+ },
+ x: function(x) {
+ return x.toString(16);
+ },
+ X: function(x) {
+ return x.toString(16).toUpperCase();
+ },
+ g: function(x, p) {
+ return x.toPrecision(p);
+ },
+ e: function(x, p) {
+ return x.toExponential(p);
+ },
+ f: function(x, p) {
+ return x.toFixed(p);
+ },
+ r: function(x, p) {
+ return (x = d3.round(x, d3_format_precision(x, p))).toFixed(Math.max(0, Math.min(20, d3_format_precision(x * (1 + 1e-15), p))));
+ }
+ });
+ function d3_format_typeDefault(x) {
+ return x + "";
+ }
+ var d3_time = d3.time = {}, d3_date = Date;
+ function d3_date_utc() {
+ this._ = new Date(arguments.length > 1 ? Date.UTC.apply(this, arguments) : arguments[0]);
+ }
+ d3_date_utc.prototype = {
+ getDate: function() {
+ return this._.getUTCDate();
+ },
+ getDay: function() {
+ return this._.getUTCDay();
+ },
+ getFullYear: function() {
+ return this._.getUTCFullYear();
+ },
+ getHours: function() {
+ return this._.getUTCHours();
+ },
+ getMilliseconds: function() {
+ return this._.getUTCMilliseconds();
+ },
+ getMinutes: function() {
+ return this._.getUTCMinutes();
+ },
+ getMonth: function() {
+ return this._.getUTCMonth();
+ },
+ getSeconds: function() {
+ return this._.getUTCSeconds();
+ },
+ getTime: function() {
+ return this._.getTime();
+ },
+ getTimezoneOffset: function() {
+ return 0;
+ },
+ valueOf: function() {
+ return this._.valueOf();
+ },
+ setDate: function() {
+ d3_time_prototype.setUTCDate.apply(this._, arguments);
+ },
+ setDay: function() {
+ d3_time_prototype.setUTCDay.apply(this._, arguments);
+ },
+ setFullYear: function() {
+ d3_time_prototype.setUTCFullYear.apply(this._, arguments);
+ },
+ setHours: function() {
+ d3_time_prototype.setUTCHours.apply(this._, arguments);
+ },
+ setMilliseconds: function() {
+ d3_time_prototype.setUTCMilliseconds.apply(this._, arguments);
+ },
+ setMinutes: function() {
+ d3_time_prototype.setUTCMinutes.apply(this._, arguments);
+ },
+ setMonth: function() {
+ d3_time_prototype.setUTCMonth.apply(this._, arguments);
+ },
+ setSeconds: function() {
+ d3_time_prototype.setUTCSeconds.apply(this._, arguments);
+ },
+ setTime: function() {
+ d3_time_prototype.setTime.apply(this._, arguments);
+ }
+ };
+ var d3_time_prototype = Date.prototype;
+ function d3_time_interval(local, step, number) {
+ function round(date) {
+ var d0 = local(date), d1 = offset(d0, 1);
+ return date - d0 < d1 - date ? d0 : d1;
+ }
+ function ceil(date) {
+ step(date = local(new d3_date(date - 1)), 1);
+ return date;
+ }
+ function offset(date, k) {
+ step(date = new d3_date(+date), k);
+ return date;
+ }
+ function range(t0, t1, dt) {
+ var time = ceil(t0), times = [];
+ if (dt > 1) {
+ while (time < t1) {
+ if (!(number(time) % dt)) times.push(new Date(+time));
+ step(time, 1);
+ }
+ } else {
+ while (time < t1) times.push(new Date(+time)), step(time, 1);
+ }
+ return times;
+ }
+ function range_utc(t0, t1, dt) {
+ try {
+ d3_date = d3_date_utc;
+ var utc = new d3_date_utc();
+ utc._ = t0;
+ return range(utc, t1, dt);
+ } finally {
+ d3_date = Date;
+ }
+ }
+ local.floor = local;
+ local.round = round;
+ local.ceil = ceil;
+ local.offset = offset;
+ local.range = range;
+ var utc = local.utc = d3_time_interval_utc(local);
+ utc.floor = utc;
+ utc.round = d3_time_interval_utc(round);
+ utc.ceil = d3_time_interval_utc(ceil);
+ utc.offset = d3_time_interval_utc(offset);
+ utc.range = range_utc;
+ return local;
+ }
+ function d3_time_interval_utc(method) {
+ return function(date, k) {
+ try {
+ d3_date = d3_date_utc;
+ var utc = new d3_date_utc();
+ utc._ = date;
+ return method(utc, k)._;
+ } finally {
+ d3_date = Date;
+ }
+ };
+ }
+ d3_time.year = d3_time_interval(function(date) {
+ date = d3_time.day(date);
+ date.setMonth(0, 1);
+ return date;
+ }, function(date, offset) {
+ date.setFullYear(date.getFullYear() + offset);
+ }, function(date) {
+ return date.getFullYear();
+ });
+ d3_time.years = d3_time.year.range;
+ d3_time.years.utc = d3_time.year.utc.range;
+ d3_time.day = d3_time_interval(function(date) {
+ var day = new d3_date(2e3, 0);
+ day.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
+ return day;
+ }, function(date, offset) {
+ date.setDate(date.getDate() + offset);
+ }, function(date) {
+ return date.getDate() - 1;
+ });
+ d3_time.days = d3_time.day.range;
+ d3_time.days.utc = d3_time.day.utc.range;
+ d3_time.dayOfYear = function(date) {
+ var year = d3_time.year(date);
+ return Math.floor((date - year - (date.getTimezoneOffset() - year.getTimezoneOffset()) * 6e4) / 864e5);
+ };
+ [ "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" ].forEach(function(day, i) {
+ i = 7 - i;
+ var interval = d3_time[day] = d3_time_interval(function(date) {
+ (date = d3_time.day(date)).setDate(date.getDate() - (date.getDay() + i) % 7);
+ return date;
+ }, function(date, offset) {
+ date.setDate(date.getDate() + Math.floor(offset) * 7);
+ }, function(date) {
+ var day = d3_time.year(date).getDay();
+ return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7) - (day !== i);
+ });
+ d3_time[day + "s"] = interval.range;
+ d3_time[day + "s"].utc = interval.utc.range;
+ d3_time[day + "OfYear"] = function(date) {
+ var day = d3_time.year(date).getDay();
+ return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7);
+ };
+ });
+ d3_time.week = d3_time.sunday;
+ d3_time.weeks = d3_time.sunday.range;
+ d3_time.weeks.utc = d3_time.sunday.utc.range;
+ d3_time.weekOfYear = d3_time.sundayOfYear;
+ function d3_locale_timeFormat(locale) {
+ var locale_dateTime = locale.dateTime, locale_date = locale.date, locale_time = locale.time, locale_periods = locale.periods, locale_days = locale.days, locale_shortDays = locale.shortDays, locale_months = locale.months, locale_shortMonths = locale.shortMonths;
+ function d3_time_format(template) {
+ var n = template.length;
+ function format(date) {
+ var string = [], i = -1, j = 0, c, p, f;
+ while (++i < n) {
+ if (template.charCodeAt(i) === 37) {
+ string.push(template.substring(j, i));
+ if ((p = d3_time_formatPads[c = template.charAt(++i)]) != null) c = template.charAt(++i);
+ if (f = d3_time_formats[c]) c = f(date, p == null ? c === "e" ? " " : "0" : p);
+ string.push(c);
+ j = i + 1;
+ }
+ }
+ string.push(template.substring(j, i));
+ return string.join("");
+ }
+ format.parse = function(string) {
+ var d = {
+ y: 1900,
+ m: 0,
+ d: 1,
+ H: 0,
+ M: 0,
+ S: 0,
+ L: 0,
+ Z: null
+ }, i = d3_time_parse(d, template, string, 0);
+ if (i != string.length) return null;
+ if ("p" in d) d.H = d.H % 12 + d.p * 12;
+ var localZ = d.Z != null && d3_date !== d3_date_utc, date = new (localZ ? d3_date_utc : d3_date)();
+ if ("j" in d) date.setFullYear(d.y, 0, d.j); else if ("w" in d && ("W" in d || "U" in d)) {
+ date.setFullYear(d.y, 0, 1);
+ date.setFullYear(d.y, 0, "W" in d ? (d.w + 6) % 7 + d.W * 7 - (date.getDay() + 5) % 7 : d.w + d.U * 7 - (date.getDay() + 6) % 7);
+ } else date.setFullYear(d.y, d.m, d.d);
+ date.setHours(d.H + Math.floor(d.Z / 100), d.M + d.Z % 100, d.S, d.L);
+ return localZ ? date._ : date;
+ };
+ format.toString = function() {
+ return template;
+ };
+ return format;
+ }
+ function d3_time_parse(date, template, string, j) {
+ var c, p, t, i = 0, n = template.length, m = string.length;
+ while (i < n) {
+ if (j >= m) return -1;
+ c = template.charCodeAt(i++);
+ if (c === 37) {
+ t = template.charAt(i++);
+ p = d3_time_parsers[t in d3_time_formatPads ? template.charAt(i++) : t];
+ if (!p || (j = p(date, string, j)) < 0) return -1;
+ } else if (c != string.charCodeAt(j++)) {
+ return -1;
+ }
+ }
+ return j;
+ }
+ d3_time_format.utc = function(template) {
+ var local = d3_time_format(template);
+ function format(date) {
+ try {
+ d3_date = d3_date_utc;
+ var utc = new d3_date();
+ utc._ = date;
+ return local(utc);
+ } finally {
+ d3_date = Date;
+ }
+ }
+ format.parse = function(string) {
+ try {
+ d3_date = d3_date_utc;
+ var date = local.parse(string);
+ return date && date._;
+ } finally {
+ d3_date = Date;
+ }
+ };
+ format.toString = local.toString;
+ return format;
+ };
+ d3_time_format.multi = d3_time_format.utc.multi = d3_time_formatMulti;
+ var d3_time_periodLookup = d3.map(), d3_time_dayRe = d3_time_formatRe(locale_days), d3_time_dayLookup = d3_time_formatLookup(locale_days), d3_time_dayAbbrevRe = d3_time_formatRe(locale_shortDays), d3_time_dayAbbrevLookup = d3_time_formatLookup(locale_shortDays), d3_time_monthRe = d3_time_formatRe(locale_months), d3_time_monthLookup = d3_time_formatLookup(locale_months), d3_time_monthAbbrevRe = d3_time_formatRe(locale_shortMonths), d3_time_monthAbbrevLookup = d3_time_formatLookup(locale_shortMonths);
+ locale_periods.forEach(function(p, i) {
+ d3_time_periodLookup.set(p.toLowerCase(), i);
+ });
+ var d3_time_formats = {
+ a: function(d) {
+ return locale_shortDays[d.getDay()];
+ },
+ A: function(d) {
+ return locale_days[d.getDay()];
+ },
+ b: function(d) {
+ return locale_shortMonths[d.getMonth()];
+ },
+ B: function(d) {
+ return locale_months[d.getMonth()];
+ },
+ c: d3_time_format(locale_dateTime),
+ d: function(d, p) {
+ return d3_time_formatPad(d.getDate(), p, 2);
+ },
+ e: function(d, p) {
+ return d3_time_formatPad(d.getDate(), p, 2);
+ },
+ H: function(d, p) {
+ return d3_time_formatPad(d.getHours(), p, 2);
+ },
+ I: function(d, p) {
+ return d3_time_formatPad(d.getHours() % 12 || 12, p, 2);
+ },
+ j: function(d, p) {
+ return d3_time_formatPad(1 + d3_time.dayOfYear(d), p, 3);
+ },
+ L: function(d, p) {
+ return d3_time_formatPad(d.getMilliseconds(), p, 3);
+ },
+ m: function(d, p) {
+ return d3_time_formatPad(d.getMonth() + 1, p, 2);
+ },
+ M: function(d, p) {
+ return d3_time_formatPad(d.getMinutes(), p, 2);
+ },
+ p: function(d) {
+ return locale_periods[+(d.getHours() >= 12)];
+ },
+ S: function(d, p) {
+ return d3_time_formatPad(d.getSeconds(), p, 2);
+ },
+ U: function(d, p) {
+ return d3_time_formatPad(d3_time.sundayOfYear(d), p, 2);
+ },
+ w: function(d) {
+ return d.getDay();
+ },
+ W: function(d, p) {
+ return d3_time_formatPad(d3_time.mondayOfYear(d), p, 2);
+ },
+ x: d3_time_format(locale_date),
+ X: d3_time_format(locale_time),
+ y: function(d, p) {
+ return d3_time_formatPad(d.getFullYear() % 100, p, 2);
+ },
+ Y: function(d, p) {
+ return d3_time_formatPad(d.getFullYear() % 1e4, p, 4);
+ },
+ Z: d3_time_zone,
+ "%": function() {
+ return "%";
+ }
+ };
+ var d3_time_parsers = {
+ a: d3_time_parseWeekdayAbbrev,
+ A: d3_time_parseWeekday,
+ b: d3_time_parseMonthAbbrev,
+ B: d3_time_parseMonth,
+ c: d3_time_parseLocaleFull,
+ d: d3_time_parseDay,
+ e: d3_time_parseDay,
+ H: d3_time_parseHour24,
+ I: d3_time_parseHour24,
+ j: d3_time_parseDayOfYear,
+ L: d3_time_parseMilliseconds,
+ m: d3_time_parseMonthNumber,
+ M: d3_time_parseMinutes,
+ p: d3_time_parseAmPm,
+ S: d3_time_parseSeconds,
+ U: d3_time_parseWeekNumberSunday,
+ w: d3_time_parseWeekdayNumber,
+ W: d3_time_parseWeekNumberMonday,
+ x: d3_time_parseLocaleDate,
+ X: d3_time_parseLocaleTime,
+ y: d3_time_parseYear,
+ Y: d3_time_parseFullYear,
+ Z: d3_time_parseZone,
+ "%": d3_time_parseLiteralPercent
+ };
+ function d3_time_parseWeekdayAbbrev(date, string, i) {
+ d3_time_dayAbbrevRe.lastIndex = 0;
+ var n = d3_time_dayAbbrevRe.exec(string.substring(i));
+ return n ? (date.w = d3_time_dayAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+ }
+ function d3_time_parseWeekday(date, string, i) {
+ d3_time_dayRe.lastIndex = 0;
+ var n = d3_time_dayRe.exec(string.substring(i));
+ return n ? (date.w = d3_time_dayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+ }
+ function d3_time_parseMonthAbbrev(date, string, i) {
+ d3_time_monthAbbrevRe.lastIndex = 0;
+ var n = d3_time_monthAbbrevRe.exec(string.substring(i));
+ return n ? (date.m = d3_time_monthAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+ }
+ function d3_time_parseMonth(date, string, i) {
+ d3_time_monthRe.lastIndex = 0;
+ var n = d3_time_monthRe.exec(string.substring(i));
+ return n ? (date.m = d3_time_monthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+ }
+ function d3_time_parseLocaleFull(date, string, i) {
+ return d3_time_parse(date, d3_time_formats.c.toString(), string, i);
+ }
+ function d3_time_parseLocaleDate(date, string, i) {
+ return d3_time_parse(date, d3_time_formats.x.toString(), string, i);
+ }
+ function d3_time_parseLocaleTime(date, string, i) {
+ return d3_time_parse(date, d3_time_formats.X.toString(), string, i);
+ }
+ function d3_time_parseAmPm(date, string, i) {
+ var n = d3_time_periodLookup.get(string.substring(i, i += 2).toLowerCase());
+ return n == null ? -1 : (date.p = n, i);
+ }
+ return d3_time_format;
+ }
+ var d3_time_formatPads = {
+ "-": "",
+ _: " ",
+ "0": "0"
+ }, d3_time_numberRe = /^\s*\d+/, d3_time_percentRe = /^%/;
+ function d3_time_formatPad(value, fill, width) {
+ var sign = value < 0 ? "-" : "", string = (sign ? -value : value) + "", length = string.length;
+ return sign + (length < width ? new Array(width - length + 1).join(fill) + string : string);
+ }
+ function d3_time_formatRe(names) {
+ return new RegExp("^(?:" + names.map(d3.requote).join("|") + ")", "i");
+ }
+ function d3_time_formatLookup(names) {
+ var map = new d3_Map(), i = -1, n = names.length;
+ while (++i < n) map.set(names[i].toLowerCase(), i);
+ return map;
+ }
+ function d3_time_parseWeekdayNumber(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 1));
+ return n ? (date.w = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseWeekNumberSunday(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i));
+ return n ? (date.U = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseWeekNumberMonday(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i));
+ return n ? (date.W = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseFullYear(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 4));
+ return n ? (date.y = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseYear(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+ return n ? (date.y = d3_time_expandYear(+n[0]), i + n[0].length) : -1;
+ }
+ function d3_time_parseZone(date, string, i) {
+ return /^[+-]\d{4}$/.test(string = string.substring(i, i + 5)) ? (date.Z = +string,
+ i + 5) : -1;
+ }
+ function d3_time_expandYear(d) {
+ return d + (d > 68 ? 1900 : 2e3);
+ }
+ function d3_time_parseMonthNumber(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+ return n ? (date.m = n[0] - 1, i + n[0].length) : -1;
+ }
+ function d3_time_parseDay(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+ return n ? (date.d = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseDayOfYear(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 3));
+ return n ? (date.j = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseHour24(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+ return n ? (date.H = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseMinutes(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+ return n ? (date.M = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseSeconds(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+ return n ? (date.S = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_parseMilliseconds(date, string, i) {
+ d3_time_numberRe.lastIndex = 0;
+ var n = d3_time_numberRe.exec(string.substring(i, i + 3));
+ return n ? (date.L = +n[0], i + n[0].length) : -1;
+ }
+ function d3_time_zone(d) {
+ var z = d.getTimezoneOffset(), zs = z > 0 ? "-" : "+", zh = ~~(abs(z) / 60), zm = abs(z) % 60;
+ return zs + d3_time_formatPad(zh, "0", 2) + d3_time_formatPad(zm, "0", 2);
+ }
+ function d3_time_parseLiteralPercent(date, string, i) {
+ d3_time_percentRe.lastIndex = 0;
+ var n = d3_time_percentRe.exec(string.substring(i, i + 1));
+ return n ? i + n[0].length : -1;
+ }
+ function d3_time_formatMulti(formats) {
+ var n = formats.length, i = -1;
+ while (++i < n) formats[i][0] = this(formats[i][0]);
+ return function(date) {
+ var i = 0, f = formats[i];
+ while (!f[1](date)) f = formats[++i];
+ return f[0](date);
+ };
+ }
+ d3.locale = function(locale) {
+ return {
+ numberFormat: d3_locale_numberFormat(locale),
+ timeFormat: d3_locale_timeFormat(locale)
+ };
+ };
+ var d3_locale_enUS = d3.locale({
+ decimal: ".",
+ thousands: ",",
+ grouping: [ 3 ],
+ currency: [ "$", "" ],
+ dateTime: "%a %b %e %X %Y",
+ date: "%m/%d/%Y",
+ time: "%H:%M:%S",
+ periods: [ "AM", "PM" ],
+ days: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ],
+ shortDays: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ],
+ months: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ],
+ shortMonths: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]
+ });
+ d3.format = d3_locale_enUS.numberFormat;
+ d3.geo = {};
+ function d3_adder() {}
+ d3_adder.prototype = {
+ s: 0,
+ t: 0,
+ add: function(y) {
+ d3_adderSum(y, this.t, d3_adderTemp);
+ d3_adderSum(d3_adderTemp.s, this.s, this);
+ if (this.s) this.t += d3_adderTemp.t; else this.s = d3_adderTemp.t;
+ },
+ reset: function() {
+ this.s = this.t = 0;
+ },
+ valueOf: function() {
+ return this.s;
+ }
+ };
+ var d3_adderTemp = new d3_adder();
+ function d3_adderSum(a, b, o) {
+ var x = o.s = a + b, bv = x - a, av = x - bv;
+ o.t = a - av + (b - bv);
+ }
+ d3.geo.stream = function(object, listener) {
+ if (object && d3_geo_streamObjectType.hasOwnProperty(object.type)) {
+ d3_geo_streamObjectType[object.type](object, listener);
+ } else {
+ d3_geo_streamGeometry(object, listener);
+ }
+ };
+ function d3_geo_streamGeometry(geometry, listener) {
+ if (geometry && d3_geo_streamGeometryType.hasOwnProperty(geometry.type)) {
+ d3_geo_streamGeometryType[geometry.type](geometry, listener);
+ }
+ }
+ var d3_geo_streamObjectType = {
+ Feature: function(feature, listener) {
+ d3_geo_streamGeometry(feature.geometry, listener);
+ },
+ FeatureCollection: function(object, listener) {
+ var features = object.features, i = -1, n = features.length;
+ while (++i < n) d3_geo_streamGeometry(features[i].geometry, listener);
+ }
+ };
+ var d3_geo_streamGeometryType = {
+ Sphere: function(object, listener) {
+ listener.sphere();
+ },
+ Point: function(object, listener) {
+ object = object.coordinates;
+ listener.point(object[0], object[1], object[2]);
+ },
+ MultiPoint: function(object, listener) {
+ var coordinates = object.coordinates, i = -1, n = coordinates.length;
+ while (++i < n) object = coordinates[i], listener.point(object[0], object[1], object[2]);
+ },
+ LineString: function(object, listener) {
+ d3_geo_streamLine(object.coordinates, listener, 0);
+ },
+ MultiLineString: function(object, listener) {
+ var coordinates = object.coordinates, i = -1, n = coordinates.length;
+ while (++i < n) d3_geo_streamLine(coordinates[i], listener, 0);
+ },
+ Polygon: function(object, listener) {
+ d3_geo_streamPolygon(object.coordinates, listener);
+ },
+ MultiPolygon: function(object, listener) {
+ var coordinates = object.coordinates, i = -1, n = coordinates.length;
+ while (++i < n) d3_geo_streamPolygon(coordinates[i], listener);
+ },
+ GeometryCollection: function(object, listener) {
+ var geometries = object.geometries, i = -1, n = geometries.length;
+ while (++i < n) d3_geo_streamGeometry(geometries[i], listener);
+ }
+ };
+ function d3_geo_streamLine(coordinates, listener, closed) {
+ var i = -1, n = coordinates.length - closed, coordinate;
+ listener.lineStart();
+ while (++i < n) coordinate = coordinates[i], listener.point(coordinate[0], coordinate[1], coordinate[2]);
+ listener.lineEnd();
+ }
+ function d3_geo_streamPolygon(coordinates, listener) {
+ var i = -1, n = coordinates.length;
+ listener.polygonStart();
+ while (++i < n) d3_geo_streamLine(coordinates[i], listener, 1);
+ listener.polygonEnd();
+ }
+ d3.geo.area = function(object) {
+ d3_geo_areaSum = 0;
+ d3.geo.stream(object, d3_geo_area);
+ return d3_geo_areaSum;
+ };
+ var d3_geo_areaSum, d3_geo_areaRingSum = new d3_adder();
+ var d3_geo_area = {
+ sphere: function() {
+ d3_geo_areaSum += 4 * π;
+ },
+ point: d3_noop,
+ lineStart: d3_noop,
+ lineEnd: d3_noop,
+ polygonStart: function() {
+ d3_geo_areaRingSum.reset();
+ d3_geo_area.lineStart = d3_geo_areaRingStart;
+ },
+ polygonEnd: function() {
+ var area = 2 * d3_geo_areaRingSum;
+ d3_geo_areaSum += area < 0 ? 4 * π + area : area;
+ d3_geo_area.lineStart = d3_geo_area.lineEnd = d3_geo_area.point = d3_noop;
+ }
+ };
+ function d3_geo_areaRingStart() {
+ var λ00, φ00, λ0, cosφ0, sinφ0;
+ d3_geo_area.point = function(λ, φ) {
+ d3_geo_area.point = nextPoint;
+ λ0 = (λ00 = λ) * d3_radians, cosφ0 = Math.cos(φ = (φ00 = φ) * d3_radians / 2 + π / 4),
+ sinφ0 = Math.sin(φ);
+ };
+ function nextPoint(λ, φ) {
+ λ *= d3_radians;
+ φ = φ * d3_radians / 2 + π / 4;
+ var dλ = λ - λ0, cosφ = Math.cos(φ), sinφ = Math.sin(φ), k = sinφ0 * sinφ, u = cosφ0 * cosφ + k * Math.cos(dλ), v = k * Math.sin(dλ);
+ d3_geo_areaRingSum.add(Math.atan2(v, u));
+ λ0 = λ, cosφ0 = cosφ, sinφ0 = sinφ;
+ }
+ d3_geo_area.lineEnd = function() {
+ nextPoint(λ00, φ00);
+ };
+ }
+ function d3_geo_cartesian(spherical) {
+ var λ = spherical[0], φ = spherical[1], cosφ = Math.cos(φ);
+ return [ cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ) ];
+ }
+ function d3_geo_cartesianDot(a, b) {
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+ }
+ function d3_geo_cartesianCross(a, b) {
+ return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ];
+ }
+ function d3_geo_cartesianAdd(a, b) {
+ a[0] += b[0];
+ a[1] += b[1];
+ a[2] += b[2];
+ }
+ function d3_geo_cartesianScale(vector, k) {
+ return [ vector[0] * k, vector[1] * k, vector[2] * k ];
+ }
+ function d3_geo_cartesianNormalize(d) {
+ var l = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
+ d[0] /= l;
+ d[1] /= l;
+ d[2] /= l;
+ }
+ function d3_geo_spherical(cartesian) {
+ return [ Math.atan2(cartesian[1], cartesian[0]), d3_asin(cartesian[2]) ];
+ }
+ function d3_geo_sphericalEqual(a, b) {
+ return abs(a[0] - b[0]) < ε && abs(a[1] - b[1]) < ε;
+ }
+ d3.geo.bounds = function() {
+ var λ0, φ0, λ1, φ1, λ_, λ__, φ__, p0, dλSum, ranges, range;
+ var bound = {
+ point: point,
+ lineStart: lineStart,
+ lineEnd: lineEnd,
+ polygonStart: function() {
+ bound.point = ringPoint;
+ bound.lineStart = ringStart;
+ bound.lineEnd = ringEnd;
+ dλSum = 0;
+ d3_geo_area.polygonStart();
+ },
+ polygonEnd: function() {
+ d3_geo_area.polygonEnd();
+ bound.point = point;
+ bound.lineStart = lineStart;
+ bound.lineEnd = lineEnd;
+ if (d3_geo_areaRingSum < 0) λ0 = -(λ1 = 180), φ0 = -(φ1 = 90); else if (dλSum > ε) φ1 = 90; else if (dλSum < -ε) φ0 = -90;
+ range[0] = λ0, range[1] = λ1;
+ }
+ };
+ function point(λ, φ) {
+ ranges.push(range = [ λ0 = λ, λ1 = λ ]);
+ if (φ < φ0) φ0 = φ;
+ if (φ > φ1) φ1 = φ;
+ }
+ function linePoint(λ, φ) {
+ var p = d3_geo_cartesian([ λ * d3_radians, φ * d3_radians ]);
+ if (p0) {
+ var normal = d3_geo_cartesianCross(p0, p), equatorial = [ normal[1], -normal[0], 0 ], inflection = d3_geo_cartesianCross(equatorial, normal);
+ d3_geo_cartesianNormalize(inflection);
+ inflection = d3_geo_spherical(inflection);
+ var dλ = λ - λ_, s = dλ > 0 ? 1 : -1, λi = inflection[0] * d3_degrees * s, antimeridian = abs(dλ) > 180;
+ if (antimeridian ^ (s * λ_ < λi && λi < s * λ)) {
+ var φi = inflection[1] * d3_degrees;
+ if (φi > φ1) φ1 = φi;
+ } else if (λi = (λi + 360) % 360 - 180, antimeridian ^ (s * λ_ < λi && λi < s * λ)) {
+ var φi = -inflection[1] * d3_degrees;
+ if (φi < φ0) φ0 = φi;
+ } else {
+ if (φ < φ0) φ0 = φ;
+ if (φ > φ1) φ1 = φ;
+ }
+ if (antimeridian) {
+ if (λ < λ_) {
+ if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ;
+ } else {
+ if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ;
+ }
+ } else {
+ if (λ1 >= λ0) {
+ if (λ < λ0) λ0 = λ;
+ if (λ > λ1) λ1 = λ;
+ } else {
+ if (λ > λ_) {
+ if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ;
+ } else {
+ if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ;
+ }
+ }
+ }
+ } else {
+ point(λ, φ);
+ }
+ p0 = p, λ_ = λ;
+ }
+ function lineStart() {
+ bound.point = linePoint;
+ }
+ function lineEnd() {
+ range[0] = λ0, range[1] = λ1;
+ bound.point = point;
+ p0 = null;
+ }
+ function ringPoint(λ, φ) {
+ if (p0) {
+ var dλ = λ - λ_;
+ dλSum += abs(dλ) > 180 ? dλ + (dλ > 0 ? 360 : -360) : dλ;
+ } else λ__ = λ, φ__ = φ;
+ d3_geo_area.point(λ, φ);
+ linePoint(λ, φ);
+ }
+ function ringStart() {
+ d3_geo_area.lineStart();
+ }
+ function ringEnd() {
+ ringPoint(λ__, φ__);
+ d3_geo_area.lineEnd();
+ if (abs(dλSum) > ε) λ0 = -(λ1 = 180);
+ range[0] = λ0, range[1] = λ1;
+ p0 = null;
+ }
+ function angle(λ0, λ1) {
+ return (λ1 -= λ0) < 0 ? λ1 + 360 : λ1;
+ }
+ function compareRanges(a, b) {
+ return a[0] - b[0];
+ }
+ function withinRange(x, range) {
+ return range[0] <= range[1] ? range[0] <= x && x <= range[1] : x < range[0] || range[1] < x;
+ }
+ return function(feature) {
+ φ1 = λ1 = -(λ0 = φ0 = Infinity);
+ ranges = [];
+ d3.geo.stream(feature, bound);
+ var n = ranges.length;
+ if (n) {
+ ranges.sort(compareRanges);
+ for (var i = 1, a = ranges[0], b, merged = [ a ]; i < n; ++i) {
+ b = ranges[i];
+ if (withinRange(b[0], a) || withinRange(b[1], a)) {
+ if (angle(a[0], b[1]) > angle(a[0], a[1])) a[1] = b[1];
+ if (angle(b[0], a[1]) > angle(a[0], a[1])) a[0] = b[0];
+ } else {
+ merged.push(a = b);
+ }
+ }
+ var best = -Infinity, dλ;
+ for (var n = merged.length - 1, i = 0, a = merged[n], b; i <= n; a = b, ++i) {
+ b = merged[i];
+ if ((dλ = angle(a[1], b[0])) > best) best = dλ, λ0 = b[0], λ1 = a[1];
+ }
+ }
+ ranges = range = null;
+ return λ0 === Infinity || φ0 === Infinity ? [ [ NaN, NaN ], [ NaN, NaN ] ] : [ [ λ0, φ0 ], [ λ1, φ1 ] ];
+ };
+ }();
+ d3.geo.centroid = function(object) {
+ d3_geo_centroidW0 = d3_geo_centroidW1 = d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0;
+ d3.geo.stream(object, d3_geo_centroid);
+ var x = d3_geo_centroidX2, y = d3_geo_centroidY2, z = d3_geo_centroidZ2, m = x * x + y * y + z * z;
+ if (m < ε2) {
+ x = d3_geo_centroidX1, y = d3_geo_centroidY1, z = d3_geo_centroidZ1;
+ if (d3_geo_centroidW1 < ε) x = d3_geo_centroidX0, y = d3_geo_centroidY0, z = d3_geo_centroidZ0;
+ m = x * x + y * y + z * z;
+ if (m < ε2) return [ NaN, NaN ];
+ }
+ return [ Math.atan2(y, x) * d3_degrees, d3_asin(z / Math.sqrt(m)) * d3_degrees ];
+ };
+ var d3_geo_centroidW0, d3_geo_centroidW1, d3_geo_centroidX0, d3_geo_centroidY0, d3_geo_centroidZ0, d3_geo_centroidX1, d3_geo_centroidY1, d3_geo_centroidZ1, d3_geo_centroidX2, d3_geo_centroidY2, d3_geo_centroidZ2;
+ var d3_geo_centroid = {
+ sphere: d3_noop,
+ point: d3_geo_centroidPoint,
+ lineStart: d3_geo_centroidLineStart,
+ lineEnd: d3_geo_centroidLineEnd,
+ polygonStart: function() {
+ d3_geo_centroid.lineStart = d3_geo_centroidRingStart;
+ },
+ polygonEnd: function() {
+ d3_geo_centroid.lineStart = d3_geo_centroidLineStart;
+ }
+ };
+ function d3_geo_centroidPoint(λ, φ) {
+ λ *= d3_radians;
+ var cosφ = Math.cos(φ *= d3_radians);
+ d3_geo_centroidPointXYZ(cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ));
+ }
+ function d3_geo_centroidPointXYZ(x, y, z) {
+ ++d3_geo_centroidW0;
+ d3_geo_centroidX0 += (x - d3_geo_centroidX0) / d3_geo_centroidW0;
+ d3_geo_centroidY0 += (y - d3_geo_centroidY0) / d3_geo_centroidW0;
+ d3_geo_centroidZ0 += (z - d3_geo_centroidZ0) / d3_geo_centroidW0;
+ }
+ function d3_geo_centroidLineStart() {
+ var x0, y0, z0;
+ d3_geo_centroid.point = function(λ, φ) {
+ λ *= d3_radians;
+ var cosφ = Math.cos(φ *= d3_radians);
+ x0 = cosφ * Math.cos(λ);
+ y0 = cosφ * Math.sin(λ);
+ z0 = Math.sin(φ);
+ d3_geo_centroid.point = nextPoint;
+ d3_geo_centroidPointXYZ(x0, y0, z0);
+ };
+ function nextPoint(λ, φ) {
+ λ *= d3_radians;
+ var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), w = Math.atan2(Math.sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z);
+ d3_geo_centroidW1 += w;
+ d3_geo_centroidX1 += w * (x0 + (x0 = x));
+ d3_geo_centroidY1 += w * (y0 + (y0 = y));
+ d3_geo_centroidZ1 += w * (z0 + (z0 = z));
+ d3_geo_centroidPointXYZ(x0, y0, z0);
+ }
+ }
+ function d3_geo_centroidLineEnd() {
+ d3_geo_centroid.point = d3_geo_centroidPoint;
+ }
+ function d3_geo_centroidRingStart() {
+ var λ00, φ00, x0, y0, z0;
+ d3_geo_centroid.point = function(λ, φ) {
+ λ00 = λ, φ00 = φ;
+ d3_geo_centroid.point = nextPoint;
+ λ *= d3_radians;
+ var cosφ = Math.cos(φ *= d3_radians);
+ x0 = cosφ * Math.cos(λ);
+ y0 = cosφ * Math.sin(λ);
+ z0 = Math.sin(φ);
+ d3_geo_centroidPointXYZ(x0, y0, z0);
+ };
+ d3_geo_centroid.lineEnd = function() {
+ nextPoint(λ00, φ00);
+ d3_geo_centroid.lineEnd = d3_geo_centroidLineEnd;
+ d3_geo_centroid.point = d3_geo_centroidPoint;
+ };
+ function nextPoint(λ, φ) {
+ λ *= d3_radians;
+ var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), cx = y0 * z - z0 * y, cy = z0 * x - x0 * z, cz = x0 * y - y0 * x, m = Math.sqrt(cx * cx + cy * cy + cz * cz), u = x0 * x + y0 * y + z0 * z, v = m && -d3_acos(u) / m, w = Math.atan2(m, u);
+ d3_geo_centroidX2 += v * cx;
+ d3_geo_centroidY2 += v * cy;
+ d3_geo_centroidZ2 += v * cz;
+ d3_geo_centroidW1 += w;
+ d3_geo_centroidX1 += w * (x0 + (x0 = x));
+ d3_geo_centroidY1 += w * (y0 + (y0 = y));
+ d3_geo_centroidZ1 += w * (z0 + (z0 = z));
+ d3_geo_centroidPointXYZ(x0, y0, z0);
+ }
+ }
+ function d3_true() {
+ return true;
+ }
+ function d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener) {
+ var subject = [], clip = [];
+ segments.forEach(function(segment) {
+ if ((n = segment.length - 1) <= 0) return;
+ var n, p0 = segment[0], p1 = segment[n];
+ if (d3_geo_sphericalEqual(p0, p1)) {
+ listener.lineStart();
+ for (var i = 0; i < n; ++i) listener.point((p0 = segment[i])[0], p0[1]);
+ listener.lineEnd();
+ return;
+ }
+ var a = new d3_geo_clipPolygonIntersection(p0, segment, null, true), b = new d3_geo_clipPolygonIntersection(p0, null, a, false);
+ a.o = b;
+ subject.push(a);
+ clip.push(b);
+ a = new d3_geo_clipPolygonIntersection(p1, segment, null, false);
+ b = new d3_geo_clipPolygonIntersection(p1, null, a, true);
+ a.o = b;
+ subject.push(a);
+ clip.push(b);
+ });
+ clip.sort(compare);
+ d3_geo_clipPolygonLinkCircular(subject);
+ d3_geo_clipPolygonLinkCircular(clip);
+ if (!subject.length) return;
+ for (var i = 0, entry = clipStartInside, n = clip.length; i < n; ++i) {
+ clip[i].e = entry = !entry;
+ }
+ var start = subject[0], points, point;
+ while (1) {
+ var current = start, isSubject = true;
+ while (current.v) if ((current = current.n) === start) return;
+ points = current.z;
+ listener.lineStart();
+ do {
+ current.v = current.o.v = true;
+ if (current.e) {
+ if (isSubject) {
+ for (var i = 0, n = points.length; i < n; ++i) listener.point((point = points[i])[0], point[1]);
+ } else {
+ interpolate(current.x, current.n.x, 1, listener);
+ }
+ current = current.n;
+ } else {
+ if (isSubject) {
+ points = current.p.z;
+ for (var i = points.length - 1; i >= 0; --i) listener.point((point = points[i])[0], point[1]);
+ } else {
+ interpolate(current.x, current.p.x, -1, listener);
+ }
+ current = current.p;
+ }
+ current = current.o;
+ points = current.z;
+ isSubject = !isSubject;
+ } while (!current.v);
+ listener.lineEnd();
+ }
+ }
+ function d3_geo_clipPolygonLinkCircular(array) {
+ if (!(n = array.length)) return;
+ var n, i = 0, a = array[0], b;
+ while (++i < n) {
+ a.n = b = array[i];
+ b.p = a;
+ a = b;
+ }
+ a.n = b = array[0];
+ b.p = a;
+ }
+ function d3_geo_clipPolygonIntersection(point, points, other, entry) {
+ this.x = point;
+ this.z = points;
+ this.o = other;
+ this.e = entry;
+ this.v = false;
+ this.n = this.p = null;
+ }
+ function d3_geo_clip(pointVisible, clipLine, interpolate, clipStart) {
+ return function(rotate, listener) {
+ var line = clipLine(listener), rotatedClipStart = rotate.invert(clipStart[0], clipStart[1]);
+ var clip = {
+ point: point,
+ lineStart: lineStart,
+ lineEnd: lineEnd,
+ polygonStart: function() {
+ clip.point = pointRing;
+ clip.lineStart = ringStart;
+ clip.lineEnd = ringEnd;
+ segments = [];
+ polygon = [];
+ listener.polygonStart();
+ },
+ polygonEnd: function() {
+ clip.point = point;
+ clip.lineStart = lineStart;
+ clip.lineEnd = lineEnd;
+ segments = d3.merge(segments);
+ var clipStartInside = d3_geo_pointInPolygon(rotatedClipStart, polygon);
+ if (segments.length) {
+ d3_geo_clipPolygon(segments, d3_geo_clipSort, clipStartInside, interpolate, listener);
+ } else if (clipStartInside) {
+ listener.lineStart();
+ interpolate(null, null, 1, listener);
+ listener.lineEnd();
+ }
+ listener.polygonEnd();
+ segments = polygon = null;
+ },
+ sphere: function() {
+ listener.polygonStart();
+ listener.lineStart();
+ interpolate(null, null, 1, listener);
+ listener.lineEnd();
+ listener.polygonEnd();
+ }
+ };
+ function point(λ, φ) {
+ var point = rotate(λ, φ);
+ if (pointVisible(λ = point[0], φ = point[1])) listener.point(λ, φ);
+ }
+ function pointLine(λ, φ) {
+ var point = rotate(λ, φ);
+ line.point(point[0], point[1]);
+ }
+ function lineStart() {
+ clip.point = pointLine;
+ line.lineStart();
+ }
+ function lineEnd() {
+ clip.point = point;
+ line.lineEnd();
+ }
+ var segments;
+ var buffer = d3_geo_clipBufferListener(), ringListener = clipLine(buffer), polygon, ring;
+ function pointRing(λ, φ) {
+ ring.push([ λ, φ ]);
+ var point = rotate(λ, φ);
+ ringListener.point(point[0], point[1]);
+ }
+ function ringStart() {
+ ringListener.lineStart();
+ ring = [];
+ }
+ function ringEnd() {
+ pointRing(ring[0][0], ring[0][1]);
+ ringListener.lineEnd();
+ var clean = ringListener.clean(), ringSegments = buffer.buffer(), segment, n = ringSegments.length;
+ ring.pop();
+ polygon.push(ring);
+ ring = null;
+ if (!n) return;
+ if (clean & 1) {
+ segment = ringSegments[0];
+ var n = segment.length - 1, i = -1, point;
+ listener.lineStart();
+ while (++i < n) listener.point((point = segment[i])[0], point[1]);
+ listener.lineEnd();
+ return;
+ }
+ if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift()));
+ segments.push(ringSegments.filter(d3_geo_clipSegmentLength1));
+ }
+ return clip;
+ };
+ }
+ function d3_geo_clipSegmentLength1(segment) {
+ return segment.length > 1;
+ }
+ function d3_geo_clipBufferListener() {
+ var lines = [], line;
+ return {
+ lineStart: function() {
+ lines.push(line = []);
+ },
+ point: function(λ, φ) {
+ line.push([ λ, φ ]);
+ },
+ lineEnd: d3_noop,
+ buffer: function() {
+ var buffer = lines;
+ lines = [];
+ line = null;
+ return buffer;
+ },
+ rejoin: function() {
+ if (lines.length > 1) lines.push(lines.pop().concat(lines.shift()));
+ }
+ };
+ }
+ function d3_geo_clipSort(a, b) {
+ return ((a = a.x)[0] < 0 ? a[1] - halfπ - ε : halfπ - a[1]) - ((b = b.x)[0] < 0 ? b[1] - halfπ - ε : halfπ - b[1]);
+ }
+ function d3_geo_pointInPolygon(point, polygon) {
+ var meridian = point[0], parallel = point[1], meridianNormal = [ Math.sin(meridian), -Math.cos(meridian), 0 ], polarAngle = 0, winding = 0;
+ d3_geo_areaRingSum.reset();
+ for (var i = 0, n = polygon.length; i < n; ++i) {
+ var ring = polygon[i], m = ring.length;
+ if (!m) continue;
+ var point0 = ring[0], λ0 = point0[0], φ0 = point0[1] / 2 + π / 4, sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), j = 1;
+ while (true) {
+ if (j === m) j = 0;
+ point = ring[j];
+ var λ = point[0], φ = point[1] / 2 + π / 4, sinφ = Math.sin(φ), cosφ = Math.cos(φ), dλ = λ - λ0, antimeridian = abs(dλ) > π, k = sinφ0 * sinφ;
+ d3_geo_areaRingSum.add(Math.atan2(k * Math.sin(dλ), cosφ0 * cosφ + k * Math.cos(dλ)));
+ polarAngle += antimeridian ? dλ + (dλ >= 0 ? τ : -τ) : dλ;
+ if (antimeridian ^ λ0 >= meridian ^ λ >= meridian) {
+ var arc = d3_geo_cartesianCross(d3_geo_cartesian(point0), d3_geo_cartesian(point));
+ d3_geo_cartesianNormalize(arc);
+ var intersection = d3_geo_cartesianCross(meridianNormal, arc);
+ d3_geo_cartesianNormalize(intersection);
+ var φarc = (antimeridian ^ dλ >= 0 ? -1 : 1) * d3_asin(intersection[2]);
+ if (parallel > φarc || parallel === φarc && (arc[0] || arc[1])) {
+ winding += antimeridian ^ dλ >= 0 ? 1 : -1;
+ }
+ }
+ if (!j++) break;
+ λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ, point0 = point;
+ }
+ }
+ return (polarAngle < -ε || polarAngle < ε && d3_geo_areaRingSum < 0) ^ winding & 1;
+ }
+ var d3_geo_clipAntimeridian = d3_geo_clip(d3_true, d3_geo_clipAntimeridianLine, d3_geo_clipAntimeridianInterpolate, [ -π, -π / 2 ]);
+ function d3_geo_clipAntimeridianLine(listener) {
+ var λ0 = NaN, φ0 = NaN, sλ0 = NaN, clean;
+ return {
+ lineStart: function() {
+ listener.lineStart();
+ clean = 1;
+ },
+ point: function(λ1, φ1) {
+ var sλ1 = λ1 > 0 ? π : -π, dλ = abs(λ1 - λ0);
+ if (abs(dλ - π) < ε) {
+ listener.point(λ0, φ0 = (φ0 + φ1) / 2 > 0 ? halfπ : -halfπ);
+ listener.point(sλ0, φ0);
+ listener.lineEnd();
+ listener.lineStart();
+ listener.point(sλ1, φ0);
+ listener.point(λ1, φ0);
+ clean = 0;
+ } else if (sλ0 !== sλ1 && dλ >= π) {
+ if (abs(λ0 - sλ0) < ε) λ0 -= sλ0 * ε;
+ if (abs(λ1 - sλ1) < ε) λ1 -= sλ1 * ε;
+ φ0 = d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1);
+ listener.point(sλ0, φ0);
+ listener.lineEnd();
+ listener.lineStart();
+ listener.point(sλ1, φ0);
+ clean = 0;
+ }
+ listener.point(λ0 = λ1, φ0 = φ1);
+ sλ0 = sλ1;
+ },
+ lineEnd: function() {
+ listener.lineEnd();
+ λ0 = φ0 = NaN;
+ },
+ clean: function() {
+ return 2 - clean;
+ }
+ };
+ }
+ function d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1) {
+ var cosφ0, cosφ1, sinλ0_λ1 = Math.sin(λ0 - λ1);
+ return abs(sinλ0_λ1) > ε ? Math.atan((Math.sin(φ0) * (cosφ1 = Math.cos(φ1)) * Math.sin(λ1) - Math.sin(φ1) * (cosφ0 = Math.cos(φ0)) * Math.sin(λ0)) / (cosφ0 * cosφ1 * sinλ0_λ1)) : (φ0 + φ1) / 2;
+ }
+ function d3_geo_clipAntimeridianInterpolate(from, to, direction, listener) {
+ var φ;
+ if (from == null) {
+ φ = direction * halfπ;
+ listener.point(-π, φ);
+ listener.point(0, φ);
+ listener.point(π, φ);
+ listener.point(π, 0);
+ listener.point(π, -φ);
+ listener.point(0, -φ);
+ listener.point(-π, -φ);
+ listener.point(-π, 0);
+ listener.point(-π, φ);
+ } else if (abs(from[0] - to[0]) > ε) {
+ var s = from[0] < to[0] ? π : -π;
+ φ = direction * s / 2;
+ listener.point(-s, φ);
+ listener.point(0, φ);
+ listener.point(s, φ);
+ } else {
+ listener.point(to[0], to[1]);
+ }
+ }
+ function d3_geo_clipCircle(radius) {
+ var cr = Math.cos(radius), smallRadius = cr > 0, notHemisphere = abs(cr) > ε, interpolate = d3_geo_circleInterpolate(radius, 6 * d3_radians);
+ return d3_geo_clip(visible, clipLine, interpolate, smallRadius ? [ 0, -radius ] : [ -π, radius - π ]);
+ function visible(λ, φ) {
+ return Math.cos(λ) * Math.cos(φ) > cr;
+ }
+ function clipLine(listener) {
+ var point0, c0, v0, v00, clean;
+ return {
+ lineStart: function() {
+ v00 = v0 = false;
+ clean = 1;
+ },
+ point: function(λ, φ) {
+ var point1 = [ λ, φ ], point2, v = visible(λ, φ), c = smallRadius ? v ? 0 : code(λ, φ) : v ? code(λ + (λ < 0 ? π : -π), φ) : 0;
+ if (!point0 && (v00 = v0 = v)) listener.lineStart();
+ if (v !== v0) {
+ point2 = intersect(point0, point1);
+ if (d3_geo_sphericalEqual(point0, point2) || d3_geo_sphericalEqual(point1, point2)) {
+ point1[0] += ε;
+ point1[1] += ε;
+ v = visible(point1[0], point1[1]);
+ }
+ }
+ if (v !== v0) {
+ clean = 0;
+ if (v) {
+ listener.lineStart();
+ point2 = intersect(point1, point0);
+ listener.point(point2[0], point2[1]);
+ } else {
+ point2 = intersect(point0, point1);
+ listener.point(point2[0], point2[1]);
+ listener.lineEnd();
+ }
+ point0 = point2;
+ } else if (notHemisphere && point0 && smallRadius ^ v) {
+ var t;
+ if (!(c & c0) && (t = intersect(point1, point0, true))) {
+ clean = 0;
+ if (smallRadius) {
+ listener.lineStart();
+ listener.point(t[0][0], t[0][1]);
+ listener.point(t[1][0], t[1][1]);
+ listener.lineEnd();
+ } else {
+ listener.point(t[1][0], t[1][1]);
+ listener.lineEnd();
+ listener.lineStart();
+ listener.point(t[0][0], t[0][1]);
+ }
+ }
+ }
+ if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) {
+ listener.point(point1[0], point1[1]);
+ }
+ point0 = point1, v0 = v, c0 = c;
+ },
+ lineEnd: function() {
+ if (v0) listener.lineEnd();
+ point0 = null;
+ },
+ clean: function() {
+ return clean | (v00 && v0) << 1;
+ }
+ };
+ }
+ function intersect(a, b, two) {
+ var pa = d3_geo_cartesian(a), pb = d3_geo_cartesian(b);
+ var n1 = [ 1, 0, 0 ], n2 = d3_geo_cartesianCross(pa, pb), n2n2 = d3_geo_cartesianDot(n2, n2), n1n2 = n2[0], determinant = n2n2 - n1n2 * n1n2;
+ if (!determinant) return !two && a;
+ var c1 = cr * n2n2 / determinant, c2 = -cr * n1n2 / determinant, n1xn2 = d3_geo_cartesianCross(n1, n2), A = d3_geo_cartesianScale(n1, c1), B = d3_geo_cartesianScale(n2, c2);
+ d3_geo_cartesianAdd(A, B);
+ var u = n1xn2, w = d3_geo_cartesianDot(A, u), uu = d3_geo_cartesianDot(u, u), t2 = w * w - uu * (d3_geo_cartesianDot(A, A) - 1);
+ if (t2 < 0) return;
+ var t = Math.sqrt(t2), q = d3_geo_cartesianScale(u, (-w - t) / uu);
+ d3_geo_cartesianAdd(q, A);
+ q = d3_geo_spherical(q);
+ if (!two) return q;
+ var λ0 = a[0], λ1 = b[0], φ0 = a[1], φ1 = b[1], z;
+ if (λ1 < λ0) z = λ0, λ0 = λ1, λ1 = z;
+ var δλ = λ1 - λ0, polar = abs(δλ - π) < ε, meridian = polar || δλ < ε;
+ if (!polar && φ1 < φ0) z = φ0, φ0 = φ1, φ1 = z;
+ if (meridian ? polar ? φ0 + φ1 > 0 ^ q[1] < (abs(q[0] - λ0) < ε ? φ0 : φ1) : φ0 <= q[1] && q[1] <= φ1 : δλ > π ^ (λ0 <= q[0] && q[0] <= λ1)) {
+ var q1 = d3_geo_cartesianScale(u, (-w + t) / uu);
+ d3_geo_cartesianAdd(q1, A);
+ return [ q, d3_geo_spherical(q1) ];
+ }
+ }
+ function code(λ, φ) {
+ var r = smallRadius ? radius : π - radius, code = 0;
+ if (λ < -r) code |= 1; else if (λ > r) code |= 2;
+ if (φ < -r) code |= 4; else if (φ > r) code |= 8;
+ return code;
+ }
+ }
+ function d3_geom_clipLine(x0, y0, x1, y1) {
+ return function(line) {
+ var a = line.a, b = line.b, ax = a.x, ay = a.y, bx = b.x, by = b.y, t0 = 0, t1 = 1, dx = bx - ax, dy = by - ay, r;
+ r = x0 - ax;
+ if (!dx && r > 0) return;
+ r /= dx;
+ if (dx < 0) {
+ if (r < t0) return;
+ if (r < t1) t1 = r;
+ } else if (dx > 0) {
+ if (r > t1) return;
+ if (r > t0) t0 = r;
+ }
+ r = x1 - ax;
+ if (!dx && r < 0) return;
+ r /= dx;
+ if (dx < 0) {
+ if (r > t1) return;
+ if (r > t0) t0 = r;
+ } else if (dx > 0) {
+ if (r < t0) return;
+ if (r < t1) t1 = r;
+ }
+ r = y0 - ay;
+ if (!dy && r > 0) return;
+ r /= dy;
+ if (dy < 0) {
+ if (r < t0) return;
+ if (r < t1) t1 = r;
+ } else if (dy > 0) {
+ if (r > t1) return;
+ if (r > t0) t0 = r;
+ }
+ r = y1 - ay;
+ if (!dy && r < 0) return;
+ r /= dy;
+ if (dy < 0) {
+ if (r > t1) return;
+ if (r > t0) t0 = r;
+ } else if (dy > 0) {
+ if (r < t0) return;
+ if (r < t1) t1 = r;
+ }
+ if (t0 > 0) line.a = {
+ x: ax + t0 * dx,
+ y: ay + t0 * dy
+ };
+ if (t1 < 1) line.b = {
+ x: ax + t1 * dx,
+ y: ay + t1 * dy
+ };
+ return line;
+ };
+ }
+ var d3_geo_clipExtentMAX = 1e9;
+ d3.geo.clipExtent = function() {
+ var x0, y0, x1, y1, stream, clip, clipExtent = {
+ stream: function(output) {
+ if (stream) stream.valid = false;
+ stream = clip(output);
+ stream.valid = true;
+ return stream;
+ },
+ extent: function(_) {
+ if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ];
+ clip = d3_geo_clipExtent(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]);
+ if (stream) stream.valid = false, stream = null;
+ return clipExtent;
+ }
+ };
+ return clipExtent.extent([ [ 0, 0 ], [ 960, 500 ] ]);
+ };
+ function d3_geo_clipExtent(x0, y0, x1, y1) {
+ return function(listener) {
+ var listener_ = listener, bufferListener = d3_geo_clipBufferListener(), clipLine = d3_geom_clipLine(x0, y0, x1, y1), segments, polygon, ring;
+ var clip = {
+ point: point,
+ lineStart: lineStart,
+ lineEnd: lineEnd,
+ polygonStart: function() {
+ listener = bufferListener;
+ segments = [];
+ polygon = [];
+ clean = true;
+ },
+ polygonEnd: function() {
+ listener = listener_;
+ segments = d3.merge(segments);
+ var clipStartInside = insidePolygon([ x0, y1 ]), inside = clean && clipStartInside, visible = segments.length;
+ if (inside || visible) {
+ listener.polygonStart();
+ if (inside) {
+ listener.lineStart();
+ interpolate(null, null, 1, listener);
+ listener.lineEnd();
+ }
+ if (visible) {
+ d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener);
+ }
+ listener.polygonEnd();
+ }
+ segments = polygon = ring = null;
+ }
+ };
+ function insidePolygon(p) {
+ var wn = 0, n = polygon.length, y = p[1];
+ for (var i = 0; i < n; ++i) {
+ for (var j = 1, v = polygon[i], m = v.length, a = v[0], b; j < m; ++j) {
+ b = v[j];
+ if (a[1] <= y) {
+ if (b[1] > y && d3_cross2d(a, b, p) > 0) ++wn;
+ } else {
+ if (b[1] <= y && d3_cross2d(a, b, p) < 0) --wn;
+ }
+ a = b;
+ }
+ }
+ return wn !== 0;
+ }
+ function interpolate(from, to, direction, listener) {
+ var a = 0, a1 = 0;
+ if (from == null || (a = corner(from, direction)) !== (a1 = corner(to, direction)) || comparePoints(from, to) < 0 ^ direction > 0) {
+ do {
+ listener.point(a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0);
+ } while ((a = (a + direction + 4) % 4) !== a1);
+ } else {
+ listener.point(to[0], to[1]);
+ }
+ }
+ function pointVisible(x, y) {
+ return x0 <= x && x <= x1 && y0 <= y && y <= y1;
+ }
+ function point(x, y) {
+ if (pointVisible(x, y)) listener.point(x, y);
+ }
+ var x__, y__, v__, x_, y_, v_, first, clean;
+ function lineStart() {
+ clip.point = linePoint;
+ if (polygon) polygon.push(ring = []);
+ first = true;
+ v_ = false;
+ x_ = y_ = NaN;
+ }
+ function lineEnd() {
+ if (segments) {
+ linePoint(x__, y__);
+ if (v__ && v_) bufferListener.rejoin();
+ segments.push(bufferListener.buffer());
+ }
+ clip.point = point;
+ if (v_) listener.lineEnd();
+ }
+ function linePoint(x, y) {
+ x = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, x));
+ y = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, y));
+ var v = pointVisible(x, y);
+ if (polygon) ring.push([ x, y ]);
+ if (first) {
+ x__ = x, y__ = y, v__ = v;
+ first = false;
+ if (v) {
+ listener.lineStart();
+ listener.point(x, y);
+ }
+ } else {
+ if (v && v_) listener.point(x, y); else {
+ var l = {
+ a: {
+ x: x_,
+ y: y_
+ },
+ b: {
+ x: x,
+ y: y
+ }
+ };
+ if (clipLine(l)) {
+ if (!v_) {
+ listener.lineStart();
+ listener.point(l.a.x, l.a.y);
+ }
+ listener.point(l.b.x, l.b.y);
+ if (!v) listener.lineEnd();
+ clean = false;
+ } else if (v) {
+ listener.lineStart();
+ listener.point(x, y);
+ clean = false;
+ }
+ }
+ }
+ x_ = x, y_ = y, v_ = v;
+ }
+ return clip;
+ };
+ function corner(p, direction) {
+ return abs(p[0] - x0) < ε ? direction > 0 ? 0 : 3 : abs(p[0] - x1) < ε ? direction > 0 ? 2 : 1 : abs(p[1] - y0) < ε ? direction > 0 ? 1 : 0 : direction > 0 ? 3 : 2;
+ }
+ function compare(a, b) {
+ return comparePoints(a.x, b.x);
+ }
+ function comparePoints(a, b) {
+ var ca = corner(a, 1), cb = corner(b, 1);
+ return ca !== cb ? ca - cb : ca === 0 ? b[1] - a[1] : ca === 1 ? a[0] - b[0] : ca === 2 ? a[1] - b[1] : b[0] - a[0];
+ }
+ }
+ function d3_geo_compose(a, b) {
+ function compose(x, y) {
+ return x = a(x, y), b(x[0], x[1]);
+ }
+ if (a.invert && b.invert) compose.invert = function(x, y) {
+ return x = b.invert(x, y), x && a.invert(x[0], x[1]);
+ };
+ return compose;
+ }
+ function d3_geo_conic(projectAt) {
+ var φ0 = 0, φ1 = π / 3, m = d3_geo_projectionMutator(projectAt), p = m(φ0, φ1);
+ p.parallels = function(_) {
+ if (!arguments.length) return [ φ0 / π * 180, φ1 / π * 180 ];
+ return m(φ0 = _[0] * π / 180, φ1 = _[1] * π / 180);
+ };
+ return p;
+ }
+ function d3_geo_conicEqualArea(φ0, φ1) {
+ var sinφ0 = Math.sin(φ0), n = (sinφ0 + Math.sin(φ1)) / 2, C = 1 + sinφ0 * (2 * n - sinφ0), ρ0 = Math.sqrt(C) / n;
+ function forward(λ, φ) {
+ var ρ = Math.sqrt(C - 2 * n * Math.sin(φ)) / n;
+ return [ ρ * Math.sin(λ *= n), ρ0 - ρ * Math.cos(λ) ];
+ }
+ forward.invert = function(x, y) {
+ var ρ0_y = ρ0 - y;
+ return [ Math.atan2(x, ρ0_y) / n, d3_asin((C - (x * x + ρ0_y * ρ0_y) * n * n) / (2 * n)) ];
+ };
+ return forward;
+ }
+ (d3.geo.conicEqualArea = function() {
+ return d3_geo_conic(d3_geo_conicEqualArea);
+ }).raw = d3_geo_conicEqualArea;
+ d3.geo.albers = function() {
+ return d3.geo.conicEqualArea().rotate([ 96, 0 ]).center([ -.6, 38.7 ]).parallels([ 29.5, 45.5 ]).scale(1070);
+ };
+ d3.geo.albersUsa = function() {
+ var lower48 = d3.geo.albers();
+ var alaska = d3.geo.conicEqualArea().rotate([ 154, 0 ]).center([ -2, 58.5 ]).parallels([ 55, 65 ]);
+ var hawaii = d3.geo.conicEqualArea().rotate([ 157, 0 ]).center([ -3, 19.9 ]).parallels([ 8, 18 ]);
+ var point, pointStream = {
+ point: function(x, y) {
+ point = [ x, y ];
+ }
+ }, lower48Point, alaskaPoint, hawaiiPoint;
+ function albersUsa(coordinates) {
+ var x = coordinates[0], y = coordinates[1];
+ point = null;
+ (lower48Point(x, y), point) || (alaskaPoint(x, y), point) || hawaiiPoint(x, y);
+ return point;
+ }
+ albersUsa.invert = function(coordinates) {
+ var k = lower48.scale(), t = lower48.translate(), x = (coordinates[0] - t[0]) / k, y = (coordinates[1] - t[1]) / k;
+ return (y >= .12 && y < .234 && x >= -.425 && x < -.214 ? alaska : y >= .166 && y < .234 && x >= -.214 && x < -.115 ? hawaii : lower48).invert(coordinates);
+ };
+ albersUsa.stream = function(stream) {
+ var lower48Stream = lower48.stream(stream), alaskaStream = alaska.stream(stream), hawaiiStream = hawaii.stream(stream);
+ return {
+ point: function(x, y) {
+ lower48Stream.point(x, y);
+ alaskaStream.point(x, y);
+ hawaiiStream.point(x, y);
+ },
+ sphere: function() {
+ lower48Stream.sphere();
+ alaskaStream.sphere();
+ hawaiiStream.sphere();
+ },
+ lineStart: function() {
+ lower48Stream.lineStart();
+ alaskaStream.lineStart();
+ hawaiiStream.lineStart();
+ },
+ lineEnd: function() {
+ lower48Stream.lineEnd();
+ alaskaStream.lineEnd();
+ hawaiiStream.lineEnd();
+ },
+ polygonStart: function() {
+ lower48Stream.polygonStart();
+ alaskaStream.polygonStart();
+ hawaiiStream.polygonStart();
+ },
+ polygonEnd: function() {
+ lower48Stream.polygonEnd();
+ alaskaStream.polygonEnd();
+ hawaiiStream.polygonEnd();
+ }
+ };
+ };
+ albersUsa.precision = function(_) {
+ if (!arguments.length) return lower48.precision();
+ lower48.precision(_);
+ alaska.precision(_);
+ hawaii.precision(_);
+ return albersUsa;
+ };
+ albersUsa.scale = function(_) {
+ if (!arguments.length) return lower48.scale();
+ lower48.scale(_);
+ alaska.scale(_ * .35);
+ hawaii.scale(_);
+ return albersUsa.translate(lower48.translate());
+ };
+ albersUsa.translate = function(_) {
+ if (!arguments.length) return lower48.translate();
+ var k = lower48.scale(), x = +_[0], y = +_[1];
+ lower48Point = lower48.translate(_).clipExtent([ [ x - .455 * k, y - .238 * k ], [ x + .455 * k, y + .238 * k ] ]).stream(pointStream).point;
+ alaskaPoint = alaska.translate([ x - .307 * k, y + .201 * k ]).clipExtent([ [ x - .425 * k + ε, y + .12 * k + ε ], [ x - .214 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point;
+ hawaiiPoint = hawaii.translate([ x - .205 * k, y + .212 * k ]).clipExtent([ [ x - .214 * k + ε, y + .166 * k + ε ], [ x - .115 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point;
+ return albersUsa;
+ };
+ return albersUsa.scale(1070);
+ };
+ var d3_geo_pathAreaSum, d3_geo_pathAreaPolygon, d3_geo_pathArea = {
+ point: d3_noop,
+ lineStart: d3_noop,
+ lineEnd: d3_noop,
+ polygonStart: function() {
+ d3_geo_pathAreaPolygon = 0;
+ d3_geo_pathArea.lineStart = d3_geo_pathAreaRingStart;
+ },
+ polygonEnd: function() {
+ d3_geo_pathArea.lineStart = d3_geo_pathArea.lineEnd = d3_geo_pathArea.point = d3_noop;
+ d3_geo_pathAreaSum += abs(d3_geo_pathAreaPolygon / 2);
+ }
+ };
+ function d3_geo_pathAreaRingStart() {
+ var x00, y00, x0, y0;
+ d3_geo_pathArea.point = function(x, y) {
+ d3_geo_pathArea.point = nextPoint;
+ x00 = x0 = x, y00 = y0 = y;
+ };
+ function nextPoint(x, y) {
+ d3_geo_pathAreaPolygon += y0 * x - x0 * y;
+ x0 = x, y0 = y;
+ }
+ d3_geo_pathArea.lineEnd = function() {
+ nextPoint(x00, y00);
+ };
+ }
+ var d3_geo_pathBoundsX0, d3_geo_pathBoundsY0, d3_geo_pathBoundsX1, d3_geo_pathBoundsY1;
+ var d3_geo_pathBounds = {
+ point: d3_geo_pathBoundsPoint,
+ lineStart: d3_noop,
+ lineEnd: d3_noop,
+ polygonStart: d3_noop,
+ polygonEnd: d3_noop
+ };
+ function d3_geo_pathBoundsPoint(x, y) {
+ if (x < d3_geo_pathBoundsX0) d3_geo_pathBoundsX0 = x;
+ if (x > d3_geo_pathBoundsX1) d3_geo_pathBoundsX1 = x;
+ if (y < d3_geo_pathBoundsY0) d3_geo_pathBoundsY0 = y;
+ if (y > d3_geo_pathBoundsY1) d3_geo_pathBoundsY1 = y;
+ }
+ function d3_geo_pathBuffer() {
+ var pointCircle = d3_geo_pathBufferCircle(4.5), buffer = [];
+ var stream = {
+ point: point,
+ lineStart: function() {
+ stream.point = pointLineStart;
+ },
+ lineEnd: lineEnd,
+ polygonStart: function() {
+ stream.lineEnd = lineEndPolygon;
+ },
+ polygonEnd: function() {
+ stream.lineEnd = lineEnd;
+ stream.point = point;
+ },
+ pointRadius: function(_) {
+ pointCircle = d3_geo_pathBufferCircle(_);
+ return stream;
+ },
+ result: function() {
+ if (buffer.length) {
+ var result = buffer.join("");
+ buffer = [];
+ return result;
+ }
+ }
+ };
+ function point(x, y) {
+ buffer.push("M", x, ",", y, pointCircle);
+ }
+ function pointLineStart(x, y) {
+ buffer.push("M", x, ",", y);
+ stream.point = pointLine;
+ }
+ function pointLine(x, y) {
+ buffer.push("L", x, ",", y);
+ }
+ function lineEnd() {
+ stream.point = point;
+ }
+ function lineEndPolygon() {
+ buffer.push("Z");
+ }
+ return stream;
+ }
+ function d3_geo_pathBufferCircle(radius) {
+ return "m0," + radius + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius + "a" + radius + "," + radius + " 0 1,1 0," + 2 * radius + "z";
+ }
+ var d3_geo_pathCentroid = {
+ point: d3_geo_pathCentroidPoint,
+ lineStart: d3_geo_pathCentroidLineStart,
+ lineEnd: d3_geo_pathCentroidLineEnd,
+ polygonStart: function() {
+ d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidRingStart;
+ },
+ polygonEnd: function() {
+ d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint;
+ d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidLineStart;
+ d3_geo_pathCentroid.lineEnd = d3_geo_pathCentroidLineEnd;
+ }
+ };
+ function d3_geo_pathCentroidPoint(x, y) {
+ d3_geo_centroidX0 += x;
+ d3_geo_centroidY0 += y;
+ ++d3_geo_centroidZ0;
+ }
+ function d3_geo_pathCentroidLineStart() {
+ var x0, y0;
+ d3_geo_pathCentroid.point = function(x, y) {
+ d3_geo_pathCentroid.point = nextPoint;
+ d3_geo_pathCentroidPoint(x0 = x, y0 = y);
+ };
+ function nextPoint(x, y) {
+ var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy);
+ d3_geo_centroidX1 += z * (x0 + x) / 2;
+ d3_geo_centroidY1 += z * (y0 + y) / 2;
+ d3_geo_centroidZ1 += z;
+ d3_geo_pathCentroidPoint(x0 = x, y0 = y);
+ }
+ }
+ function d3_geo_pathCentroidLineEnd() {
+ d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint;
+ }
+ function d3_geo_pathCentroidRingStart() {
+ var x00, y00, x0, y0;
+ d3_geo_pathCentroid.point = function(x, y) {
+ d3_geo_pathCentroid.point = nextPoint;
+ d3_geo_pathCentroidPoint(x00 = x0 = x, y00 = y0 = y);
+ };
+ function nextPoint(x, y) {
+ var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy);
+ d3_geo_centroidX1 += z * (x0 + x) / 2;
+ d3_geo_centroidY1 += z * (y0 + y) / 2;
+ d3_geo_centroidZ1 += z;
+ z = y0 * x - x0 * y;
+ d3_geo_centroidX2 += z * (x0 + x);
+ d3_geo_centroidY2 += z * (y0 + y);
+ d3_geo_centroidZ2 += z * 3;
+ d3_geo_pathCentroidPoint(x0 = x, y0 = y);
+ }
+ d3_geo_pathCentroid.lineEnd = function() {
+ nextPoint(x00, y00);
+ };
+ }
+ function d3_geo_pathContext(context) {
+ var pointRadius = 4.5;
+ var stream = {
+ point: point,
+ lineStart: function() {
+ stream.point = pointLineStart;
+ },
+ lineEnd: lineEnd,
+ polygonStart: function() {
+ stream.lineEnd = lineEndPolygon;
+ },
+ polygonEnd: function() {
+ stream.lineEnd = lineEnd;
+ stream.point = point;
+ },
+ pointRadius: function(_) {
+ pointRadius = _;
+ return stream;
+ },
+ result: d3_noop
+ };
+ function point(x, y) {
+ context.moveTo(x, y);
+ context.arc(x, y, pointRadius, 0, τ);
+ }
+ function pointLineStart(x, y) {
+ context.moveTo(x, y);
+ stream.point = pointLine;
+ }
+ function pointLine(x, y) {
+ context.lineTo(x, y);
+ }
+ function lineEnd() {
+ stream.point = point;
+ }
+ function lineEndPolygon() {
+ context.closePath();
+ }
+ return stream;
+ }
+ function d3_geo_resample(project) {
+ var δ2 = .5, cosMinDistance = Math.cos(30 * d3_radians), maxDepth = 16;
+ function resample(stream) {
+ return (maxDepth ? resampleRecursive : resampleNone)(stream);
+ }
+ function resampleNone(stream) {
+ return d3_geo_transformPoint(stream, function(x, y) {
+ x = project(x, y);
+ stream.point(x[0], x[1]);
+ });
+ }
+ function resampleRecursive(stream) {
+ var λ00, φ00, x00, y00, a00, b00, c00, λ0, x0, y0, a0, b0, c0;
+ var resample = {
+ point: point,
+ lineStart: lineStart,
+ lineEnd: lineEnd,
+ polygonStart: function() {
+ stream.polygonStart();
+ resample.lineStart = ringStart;
+ },
+ polygonEnd: function() {
+ stream.polygonEnd();
+ resample.lineStart = lineStart;
+ }
+ };
+ function point(x, y) {
+ x = project(x, y);
+ stream.point(x[0], x[1]);
+ }
+ function lineStart() {
+ x0 = NaN;
+ resample.point = linePoint;
+ stream.lineStart();
+ }
+ function linePoint(λ, φ) {
+ var c = d3_geo_cartesian([ λ, φ ]), p = project(λ, φ);
+ resampleLineTo(x0, y0, λ0, a0, b0, c0, x0 = p[0], y0 = p[1], λ0 = λ, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream);
+ stream.point(x0, y0);
+ }
+ function lineEnd() {
+ resample.point = point;
+ stream.lineEnd();
+ }
+ function ringStart() {
+ lineStart();
+ resample.point = ringPoint;
+ resample.lineEnd = ringEnd;
+ }
+ function ringPoint(λ, φ) {
+ linePoint(λ00 = λ, φ00 = φ), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0;
+ resample.point = linePoint;
+ }
+ function ringEnd() {
+ resampleLineTo(x0, y0, λ0, a0, b0, c0, x00, y00, λ00, a00, b00, c00, maxDepth, stream);
+ resample.lineEnd = lineEnd;
+ lineEnd();
+ }
+ return resample;
+ }
+ function resampleLineTo(x0, y0, λ0, a0, b0, c0, x1, y1, λ1, a1, b1, c1, depth, stream) {
+ var dx = x1 - x0, dy = y1 - y0, d2 = dx * dx + dy * dy;
+ if (d2 > 4 * δ2 && depth--) {
+ var a = a0 + a1, b = b0 + b1, c = c0 + c1, m = Math.sqrt(a * a + b * b + c * c), φ2 = Math.asin(c /= m), λ2 = abs(abs(c) - 1) < ε || abs(λ0 - λ1) < ε ? (λ0 + λ1) / 2 : Math.atan2(b, a), p = project(λ2, φ2), x2 = p[0], y2 = p[1], dx2 = x2 - x0, dy2 = y2 - y0, dz = dy * dx2 - dx * dy2;
+ if (dz * dz / d2 > δ2 || abs((dx * dx2 + dy * dy2) / d2 - .5) > .3 || a0 * a1 + b0 * b1 + c0 * c1 < cosMinDistance) {
+ resampleLineTo(x0, y0, λ0, a0, b0, c0, x2, y2, λ2, a /= m, b /= m, c, depth, stream);
+ stream.point(x2, y2);
+ resampleLineTo(x2, y2, λ2, a, b, c, x1, y1, λ1, a1, b1, c1, depth, stream);
+ }
+ }
+ }
+ resample.precision = function(_) {
+ if (!arguments.length) return Math.sqrt(δ2);
+ maxDepth = (δ2 = _ * _) > 0 && 16;
+ return resample;
+ };
+ return resample;
+ }
+ d3.geo.path = function() {
+ var pointRadius = 4.5, projection, context, projectStream, contextStream, cacheStream;
+ function path(object) {
+ if (object) {
+ if (typeof pointRadius === "function") contextStream.pointRadius(+pointRadius.apply(this, arguments));
+ if (!cacheStream || !cacheStream.valid) cacheStream = projectStream(contextStream);
+ d3.geo.stream(object, cacheStream);
+ }
+ return contextStream.result();
+ }
+ path.area = function(object) {
+ d3_geo_pathAreaSum = 0;
+ d3.geo.stream(object, projectStream(d3_geo_pathArea));
+ return d3_geo_pathAreaSum;
+ };
+ path.centroid = function(object) {
+ d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0;
+ d3.geo.stream(object, projectStream(d3_geo_pathCentroid));
+ return d3_geo_centroidZ2 ? [ d3_geo_centroidX2 / d3_geo_centroidZ2, d3_geo_centroidY2 / d3_geo_centroidZ2 ] : d3_geo_centroidZ1 ? [ d3_geo_centroidX1 / d3_geo_centroidZ1, d3_geo_centroidY1 / d3_geo_centroidZ1 ] : d3_geo_centroidZ0 ? [ d3_geo_centroidX0 / d3_geo_centroidZ0, d3_geo_centroidY0 / d3_geo_centroidZ0 ] : [ NaN, NaN ];
+ };
+ path.bounds = function(object) {
+ d3_geo_pathBoundsX1 = d3_geo_pathBoundsY1 = -(d3_geo_pathBoundsX0 = d3_geo_pathBoundsY0 = Infinity);
+ d3.geo.stream(object, projectStream(d3_geo_pathBounds));
+ return [ [ d3_geo_pathBoundsX0, d3_geo_pathBoundsY0 ], [ d3_geo_pathBoundsX1, d3_geo_pathBoundsY1 ] ];
+ };
+ path.projection = function(_) {
+ if (!arguments.length) return projection;
+ projectStream = (projection = _) ? _.stream || d3_geo_pathProjectStream(_) : d3_identity;
+ return reset();
+ };
+ path.context = function(_) {
+ if (!arguments.length) return context;
+ contextStream = (context = _) == null ? new d3_geo_pathBuffer() : new d3_geo_pathContext(_);
+ if (typeof pointRadius !== "function") contextStream.pointRadius(pointRadius);
+ return reset();
+ };
+ path.pointRadius = function(_) {
+ if (!arguments.length) return pointRadius;
+ pointRadius = typeof _ === "function" ? _ : (contextStream.pointRadius(+_), +_);
+ return path;
+ };
+ function reset() {
+ cacheStream = null;
+ return path;
+ }
+ return path.projection(d3.geo.albersUsa()).context(null);
+ };
+ function d3_geo_pathProjectStream(project) {
+ var resample = d3_geo_resample(function(x, y) {
+ return project([ x * d3_degrees, y * d3_degrees ]);
+ });
+ return function(stream) {
+ return d3_geo_projectionRadians(resample(stream));
+ };
+ }
+ d3.geo.transform = function(methods) {
+ return {
+ stream: function(stream) {
+ var transform = new d3_geo_transform(stream);
+ for (var k in methods) transform[k] = methods[k];
+ return transform;
+ }
+ };
+ };
+ function d3_geo_transform(stream) {
+ this.stream = stream;
+ }
+ d3_geo_transform.prototype = {
+ point: function(x, y) {
+ this.stream.point(x, y);
+ },
+ sphere: function() {
+ this.stream.sphere();
+ },
+ lineStart: function() {
+ this.stream.lineStart();
+ },
+ lineEnd: function() {
+ this.stream.lineEnd();
+ },
+ polygonStart: function() {
+ this.stream.polygonStart();
+ },
+ polygonEnd: function() {
+ this.stream.polygonEnd();
+ }
+ };
+ function d3_geo_transformPoint(stream, point) {
+ return {
+ point: point,
+ sphere: function() {
+ stream.sphere();
+ },
+ lineStart: function() {
+ stream.lineStart();
+ },
+ lineEnd: function() {
+ stream.lineEnd();
+ },
+ polygonStart: function() {
+ stream.polygonStart();
+ },
+ polygonEnd: function() {
+ stream.polygonEnd();
+ }
+ };
+ }
+ d3.geo.projection = d3_geo_projection;
+ d3.geo.projectionMutator = d3_geo_projectionMutator;
+ function d3_geo_projection(project) {
+ return d3_geo_projectionMutator(function() {
+ return project;
+ })();
+ }
+ function d3_geo_projectionMutator(projectAt) {
+ var project, rotate, projectRotate, projectResample = d3_geo_resample(function(x, y) {
+ x = project(x, y);
+ return [ x[0] * k + δx, δy - x[1] * k ];
+ }), k = 150, x = 480, y = 250, λ = 0, φ = 0, δλ = 0, δφ = 0, δγ = 0, δx, δy, preclip = d3_geo_clipAntimeridian, postclip = d3_identity, clipAngle = null, clipExtent = null, stream;
+ function projection(point) {
+ point = projectRotate(point[0] * d3_radians, point[1] * d3_radians);
+ return [ point[0] * k + δx, δy - point[1] * k ];
+ }
+ function invert(point) {
+ point = projectRotate.invert((point[0] - δx) / k, (δy - point[1]) / k);
+ return point && [ point[0] * d3_degrees, point[1] * d3_degrees ];
+ }
+ projection.stream = function(output) {
+ if (stream) stream.valid = false;
+ stream = d3_geo_projectionRadians(preclip(rotate, projectResample(postclip(output))));
+ stream.valid = true;
+ return stream;
+ };
+ projection.clipAngle = function(_) {
+ if (!arguments.length) return clipAngle;
+ preclip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle((clipAngle = +_) * d3_radians);
+ return invalidate();
+ };
+ projection.clipExtent = function(_) {
+ if (!arguments.length) return clipExtent;
+ clipExtent = _;
+ postclip = _ ? d3_geo_clipExtent(_[0][0], _[0][1], _[1][0], _[1][1]) : d3_identity;
+ return invalidate();
+ };
+ projection.scale = function(_) {
+ if (!arguments.length) return k;
+ k = +_;
+ return reset();
+ };
+ projection.translate = function(_) {
+ if (!arguments.length) return [ x, y ];
+ x = +_[0];
+ y = +_[1];
+ return reset();
+ };
+ projection.center = function(_) {
+ if (!arguments.length) return [ λ * d3_degrees, φ * d3_degrees ];
+ λ = _[0] % 360 * d3_radians;
+ φ = _[1] % 360 * d3_radians;
+ return reset();
+ };
+ projection.rotate = function(_) {
+ if (!arguments.length) return [ δλ * d3_degrees, δφ * d3_degrees, δγ * d3_degrees ];
+ δλ = _[0] % 360 * d3_radians;
+ δφ = _[1] % 360 * d3_radians;
+ δγ = _.length > 2 ? _[2] % 360 * d3_radians : 0;
+ return reset();
+ };
+ d3.rebind(projection, projectResample, "precision");
+ function reset() {
+ projectRotate = d3_geo_compose(rotate = d3_geo_rotation(δλ, δφ, δγ), project);
+ var center = project(λ, φ);
+ δx = x - center[0] * k;
+ δy = y + center[1] * k;
+ return invalidate();
+ }
+ function invalidate() {
+ if (stream) stream.valid = false, stream = null;
+ return projection;
+ }
+ return function() {
+ project = projectAt.apply(this, arguments);
+ projection.invert = project.invert && invert;
+ return reset();
+ };
+ }
+ function d3_geo_projectionRadians(stream) {
+ return d3_geo_transformPoint(stream, function(x, y) {
+ stream.point(x * d3_radians, y * d3_radians);
+ });
+ }
+ function d3_geo_equirectangular(λ, φ) {
+ return [ λ, φ ];
+ }
+ (d3.geo.equirectangular = function() {
+ return d3_geo_projection(d3_geo_equirectangular);
+ }).raw = d3_geo_equirectangular.invert = d3_geo_equirectangular;
+ d3.geo.rotation = function(rotate) {
+ rotate = d3_geo_rotation(rotate[0] % 360 * d3_radians, rotate[1] * d3_radians, rotate.length > 2 ? rotate[2] * d3_radians : 0);
+ function forward(coordinates) {
+ coordinates = rotate(coordinates[0] * d3_radians, coordinates[1] * d3_radians);
+ return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates;
+ }
+ forward.invert = function(coordinates) {
+ coordinates = rotate.invert(coordinates[0] * d3_radians, coordinates[1] * d3_radians);
+ return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates;
+ };
+ return forward;
+ };
+ function d3_geo_identityRotation(λ, φ) {
+ return [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ];
+ }
+ d3_geo_identityRotation.invert = d3_geo_equirectangular;
+ function d3_geo_rotation(δλ, δφ, δγ) {
+ return δλ ? δφ || δγ ? d3_geo_compose(d3_geo_rotationλ(δλ), d3_geo_rotationφγ(δφ, δγ)) : d3_geo_rotationλ(δλ) : δφ || δγ ? d3_geo_rotationφγ(δφ, δγ) : d3_geo_identityRotation;
+ }
+ function d3_geo_forwardRotationλ(δλ) {
+ return function(λ, φ) {
+ return λ += δλ, [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ];
+ };
+ }
+ function d3_geo_rotationλ(δλ) {
+ var rotation = d3_geo_forwardRotationλ(δλ);
+ rotation.invert = d3_geo_forwardRotationλ(-δλ);
+ return rotation;
+ }
+ function d3_geo_rotationφγ(δφ, δγ) {
+ var cosδφ = Math.cos(δφ), sinδφ = Math.sin(δφ), cosδγ = Math.cos(δγ), sinδγ = Math.sin(δγ);
+ function rotation(λ, φ) {
+ var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδφ + x * sinδφ;
+ return [ Math.atan2(y * cosδγ - k * sinδγ, x * cosδφ - z * sinδφ), d3_asin(k * cosδγ + y * sinδγ) ];
+ }
+ rotation.invert = function(λ, φ) {
+ var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδγ - y * sinδγ;
+ return [ Math.atan2(y * cosδγ + z * sinδγ, x * cosδφ + k * sinδφ), d3_asin(k * cosδφ - x * sinδφ) ];
+ };
+ return rotation;
+ }
+ d3.geo.circle = function() {
+ var origin = [ 0, 0 ], angle, precision = 6, interpolate;
+ function circle() {
+ var center = typeof origin === "function" ? origin.apply(this, arguments) : origin, rotate = d3_geo_rotation(-center[0] * d3_radians, -center[1] * d3_radians, 0).invert, ring = [];
+ interpolate(null, null, 1, {
+ point: function(x, y) {
+ ring.push(x = rotate(x, y));
+ x[0] *= d3_degrees, x[1] *= d3_degrees;
+ }
+ });
+ return {
+ type: "Polygon",
+ coordinates: [ ring ]
+ };
+ }
+ circle.origin = function(x) {
+ if (!arguments.length) return origin;
+ origin = x;
+ return circle;
+ };
+ circle.angle = function(x) {
+ if (!arguments.length) return angle;
+ interpolate = d3_geo_circleInterpolate((angle = +x) * d3_radians, precision * d3_radians);
+ return circle;
+ };
+ circle.precision = function(_) {
+ if (!arguments.length) return precision;
+ interpolate = d3_geo_circleInterpolate(angle * d3_radians, (precision = +_) * d3_radians);
+ return circle;
+ };
+ return circle.angle(90);
+ };
+ function d3_geo_circleInterpolate(radius, precision) {
+ var cr = Math.cos(radius), sr = Math.sin(radius);
+ return function(from, to, direction, listener) {
+ var step = direction * precision;
+ if (from != null) {
+ from = d3_geo_circleAngle(cr, from);
+ to = d3_geo_circleAngle(cr, to);
+ if (direction > 0 ? from < to : from > to) from += direction * τ;
+ } else {
+ from = radius + direction * τ;
+ to = radius - .5 * step;
+ }
+ for (var point, t = from; direction > 0 ? t > to : t < to; t -= step) {
+ listener.point((point = d3_geo_spherical([ cr, -sr * Math.cos(t), -sr * Math.sin(t) ]))[0], point[1]);
+ }
+ };
+ }
+ function d3_geo_circleAngle(cr, point) {
+ var a = d3_geo_cartesian(point);
+ a[0] -= cr;
+ d3_geo_cartesianNormalize(a);
+ var angle = d3_acos(-a[1]);
+ return ((-a[2] < 0 ? -angle : angle) + 2 * Math.PI - ε) % (2 * Math.PI);
+ }
+ d3.geo.distance = function(a, b) {
+ var Δλ = (b[0] - a[0]) * d3_radians, φ0 = a[1] * d3_radians, φ1 = b[1] * d3_radians, sinΔλ = Math.sin(Δλ), cosΔλ = Math.cos(Δλ), sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), sinφ1 = Math.sin(φ1), cosφ1 = Math.cos(φ1), t;
+ return Math.atan2(Math.sqrt((t = cosφ1 * sinΔλ) * t + (t = cosφ0 * sinφ1 - sinφ0 * cosφ1 * cosΔλ) * t), sinφ0 * sinφ1 + cosφ0 * cosφ1 * cosΔλ);
+ };
+ d3.geo.graticule = function() {
+ var x1, x0, X1, X0, y1, y0, Y1, Y0, dx = 10, dy = dx, DX = 90, DY = 360, x, y, X, Y, precision = 2.5;
+ function graticule() {
+ return {
+ type: "MultiLineString",
+ coordinates: lines()
+ };
+ }
+ function lines() {
+ return d3.range(Math.ceil(X0 / DX) * DX, X1, DX).map(X).concat(d3.range(Math.ceil(Y0 / DY) * DY, Y1, DY).map(Y)).concat(d3.range(Math.ceil(x0 / dx) * dx, x1, dx).filter(function(x) {
+ return abs(x % DX) > ε;
+ }).map(x)).concat(d3.range(Math.ceil(y0 / dy) * dy, y1, dy).filter(function(y) {
+ return abs(y % DY) > ε;
+ }).map(y));
+ }
+ graticule.lines = function() {
+ return lines().map(function(coordinates) {
+ return {
+ type: "LineString",
+ coordinates: coordinates
+ };
+ });
+ };
+ graticule.outline = function() {
+ return {
+ type: "Polygon",
+ coordinates: [ X(X0).concat(Y(Y1).slice(1), X(X1).reverse().slice(1), Y(Y0).reverse().slice(1)) ]
+ };
+ };
+ graticule.extent = function(_) {
+ if (!arguments.length) return graticule.minorExtent();
+ return graticule.majorExtent(_).minorExtent(_);
+ };
+ graticule.majorExtent = function(_) {
+ if (!arguments.length) return [ [ X0, Y0 ], [ X1, Y1 ] ];
+ X0 = +_[0][0], X1 = +_[1][0];
+ Y0 = +_[0][1], Y1 = +_[1][1];
+ if (X0 > X1) _ = X0, X0 = X1, X1 = _;
+ if (Y0 > Y1) _ = Y0, Y0 = Y1, Y1 = _;
+ return graticule.precision(precision);
+ };
+ graticule.minorExtent = function(_) {
+ if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ];
+ x0 = +_[0][0], x1 = +_[1][0];
+ y0 = +_[0][1], y1 = +_[1][1];
+ if (x0 > x1) _ = x0, x0 = x1, x1 = _;
+ if (y0 > y1) _ = y0, y0 = y1, y1 = _;
+ return graticule.precision(precision);
+ };
+ graticule.step = function(_) {
+ if (!arguments.length) return graticule.minorStep();
+ return graticule.majorStep(_).minorStep(_);
+ };
+ graticule.majorStep = function(_) {
+ if (!arguments.length) return [ DX, DY ];
+ DX = +_[0], DY = +_[1];
+ return graticule;
+ };
+ graticule.minorStep = function(_) {
+ if (!arguments.length) return [ dx, dy ];
+ dx = +_[0], dy = +_[1];
+ return graticule;
+ };
+ graticule.precision = function(_) {
+ if (!arguments.length) return precision;
+ precision = +_;
+ x = d3_geo_graticuleX(y0, y1, 90);
+ y = d3_geo_graticuleY(x0, x1, precision);
+ X = d3_geo_graticuleX(Y0, Y1, 90);
+ Y = d3_geo_graticuleY(X0, X1, precision);
+ return graticule;
+ };
+ return graticule.majorExtent([ [ -180, -90 + ε ], [ 180, 90 - ε ] ]).minorExtent([ [ -180, -80 - ε ], [ 180, 80 + ε ] ]);
+ };
+ function d3_geo_graticuleX(y0, y1, dy) {
+ var y = d3.range(y0, y1 - ε, dy).concat(y1);
+ return function(x) {
+ return y.map(function(y) {
+ return [ x, y ];
+ });
+ };
+ }
+ function d3_geo_graticuleY(x0, x1, dx) {
+ var x = d3.range(x0, x1 - ε, dx).concat(x1);
+ return function(y) {
+ return x.map(function(x) {
+ return [ x, y ];
+ });
+ };
+ }
+ function d3_source(d) {
+ return d.source;
+ }
+ function d3_target(d) {
+ return d.target;
+ }
+ d3.geo.greatArc = function() {
+ var source = d3_source, source_, target = d3_target, target_;
+ function greatArc() {
+ return {
+ type: "LineString",
+ coordinates: [ source_ || source.apply(this, arguments), target_ || target.apply(this, arguments) ]
+ };
+ }
+ greatArc.distance = function() {
+ return d3.geo.distance(source_ || source.apply(this, arguments), target_ || target.apply(this, arguments));
+ };
+ greatArc.source = function(_) {
+ if (!arguments.length) return source;
+ source = _, source_ = typeof _ === "function" ? null : _;
+ return greatArc;
+ };
+ greatArc.target = function(_) {
+ if (!arguments.length) return target;
+ target = _, target_ = typeof _ === "function" ? null : _;
+ return greatArc;
+ };
+ greatArc.precision = function() {
+ return arguments.length ? greatArc : 0;
+ };
+ return greatArc;
+ };
+ d3.geo.interpolate = function(source, target) {
+ return d3_geo_interpolate(source[0] * d3_radians, source[1] * d3_radians, target[0] * d3_radians, target[1] * d3_radians);
+ };
+ function d3_geo_interpolate(x0, y0, x1, y1) {
+ var cy0 = Math.cos(y0), sy0 = Math.sin(y0), cy1 = Math.cos(y1), sy1 = Math.sin(y1), kx0 = cy0 * Math.cos(x0), ky0 = cy0 * Math.sin(x0), kx1 = cy1 * Math.cos(x1), ky1 = cy1 * Math.sin(x1), d = 2 * Math.asin(Math.sqrt(d3_haversin(y1 - y0) + cy0 * cy1 * d3_haversin(x1 - x0))), k = 1 / Math.sin(d);
+ var interpolate = d ? function(t) {
+ var B = Math.sin(t *= d) * k, A = Math.sin(d - t) * k, x = A * kx0 + B * kx1, y = A * ky0 + B * ky1, z = A * sy0 + B * sy1;
+ return [ Math.atan2(y, x) * d3_degrees, Math.atan2(z, Math.sqrt(x * x + y * y)) * d3_degrees ];
+ } : function() {
+ return [ x0 * d3_degrees, y0 * d3_degrees ];
+ };
+ interpolate.distance = d;
+ return interpolate;
+ }
+ d3.geo.length = function(object) {
+ d3_geo_lengthSum = 0;
+ d3.geo.stream(object, d3_geo_length);
+ return d3_geo_lengthSum;
+ };
+ var d3_geo_lengthSum;
+ var d3_geo_length = {
+ sphere: d3_noop,
+ point: d3_noop,
+ lineStart: d3_geo_lengthLineStart,
+ lineEnd: d3_noop,
+ polygonStart: d3_noop,
+ polygonEnd: d3_noop
+ };
+ function d3_geo_lengthLineStart() {
+ var λ0, sinφ0, cosφ0;
+ d3_geo_length.point = function(λ, φ) {
+ λ0 = λ * d3_radians, sinφ0 = Math.sin(φ *= d3_radians), cosφ0 = Math.cos(φ);
+ d3_geo_length.point = nextPoint;
+ };
+ d3_geo_length.lineEnd = function() {
+ d3_geo_length.point = d3_geo_length.lineEnd = d3_noop;
+ };
+ function nextPoint(λ, φ) {
+ var sinφ = Math.sin(φ *= d3_radians), cosφ = Math.cos(φ), t = abs((λ *= d3_radians) - λ0), cosΔλ = Math.cos(t);
+ d3_geo_lengthSum += Math.atan2(Math.sqrt((t = cosφ * Math.sin(t)) * t + (t = cosφ0 * sinφ - sinφ0 * cosφ * cosΔλ) * t), sinφ0 * sinφ + cosφ0 * cosφ * cosΔλ);
+ λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ;
+ }
+ }
+ function d3_geo_azimuthal(scale, angle) {
+ function azimuthal(λ, φ) {
+ var cosλ = Math.cos(λ), cosφ = Math.cos(φ), k = scale(cosλ * cosφ);
+ return [ k * cosφ * Math.sin(λ), k * Math.sin(φ) ];
+ }
+ azimuthal.invert = function(x, y) {
+ var ρ = Math.sqrt(x * x + y * y), c = angle(ρ), sinc = Math.sin(c), cosc = Math.cos(c);
+ return [ Math.atan2(x * sinc, ρ * cosc), Math.asin(ρ && y * sinc / ρ) ];
+ };
+ return azimuthal;
+ }
+ var d3_geo_azimuthalEqualArea = d3_geo_azimuthal(function(cosλcosφ) {
+ return Math.sqrt(2 / (1 + cosλcosφ));
+ }, function(ρ) {
+ return 2 * Math.asin(ρ / 2);
+ });
+ (d3.geo.azimuthalEqualArea = function() {
+ return d3_geo_projection(d3_geo_azimuthalEqualArea);
+ }).raw = d3_geo_azimuthalEqualArea;
+ var d3_geo_azimuthalEquidistant = d3_geo_azimuthal(function(cosλcosφ) {
+ var c = Math.acos(cosλcosφ);
+ return c && c / Math.sin(c);
+ }, d3_identity);
+ (d3.geo.azimuthalEquidistant = function() {
+ return d3_geo_projection(d3_geo_azimuthalEquidistant);
+ }).raw = d3_geo_azimuthalEquidistant;
+ function d3_geo_conicConformal(φ0, φ1) {
+ var cosφ0 = Math.cos(φ0), t = function(φ) {
+ return Math.tan(π / 4 + φ / 2);
+ }, n = φ0 === φ1 ? Math.sin(φ0) : Math.log(cosφ0 / Math.cos(φ1)) / Math.log(t(φ1) / t(φ0)), F = cosφ0 * Math.pow(t(φ0), n) / n;
+ if (!n) return d3_geo_mercator;
+ function forward(λ, φ) {
+ var ρ = abs(abs(φ) - halfπ) < ε ? 0 : F / Math.pow(t(φ), n);
+ return [ ρ * Math.sin(n * λ), F - ρ * Math.cos(n * λ) ];
+ }
+ forward.invert = function(x, y) {
+ var ρ0_y = F - y, ρ = d3_sgn(n) * Math.sqrt(x * x + ρ0_y * ρ0_y);
+ return [ Math.atan2(x, ρ0_y) / n, 2 * Math.atan(Math.pow(F / ρ, 1 / n)) - halfπ ];
+ };
+ return forward;
+ }
+ (d3.geo.conicConformal = function() {
+ return d3_geo_conic(d3_geo_conicConformal);
+ }).raw = d3_geo_conicConformal;
+ function d3_geo_conicEquidistant(φ0, φ1) {
+ var cosφ0 = Math.cos(φ0), n = φ0 === φ1 ? Math.sin(φ0) : (cosφ0 - Math.cos(φ1)) / (φ1 - φ0), G = cosφ0 / n + φ0;
+ if (abs(n) < ε) return d3_geo_equirectangular;
+ function forward(λ, φ) {
+ var ρ = G - φ;
+ return [ ρ * Math.sin(n * λ), G - ρ * Math.cos(n * λ) ];
+ }
+ forward.invert = function(x, y) {
+ var ρ0_y = G - y;
+ return [ Math.atan2(x, ρ0_y) / n, G - d3_sgn(n) * Math.sqrt(x * x + ρ0_y * ρ0_y) ];
+ };
+ return forward;
+ }
+ (d3.geo.conicEquidistant = function() {
+ return d3_geo_conic(d3_geo_conicEquidistant);
+ }).raw = d3_geo_conicEquidistant;
+ var d3_geo_gnomonic = d3_geo_azimuthal(function(cosλcosφ) {
+ return 1 / cosλcosφ;
+ }, Math.atan);
+ (d3.geo.gnomonic = function() {
+ return d3_geo_projection(d3_geo_gnomonic);
+ }).raw = d3_geo_gnomonic;
+ function d3_geo_mercator(λ, φ) {
+ return [ λ, Math.log(Math.tan(π / 4 + φ / 2)) ];
+ }
+ d3_geo_mercator.invert = function(x, y) {
+ return [ x, 2 * Math.atan(Math.exp(y)) - halfπ ];
+ };
+ function d3_geo_mercatorProjection(project) {
+ var m = d3_geo_projection(project), scale = m.scale, translate = m.translate, clipExtent = m.clipExtent, clipAuto;
+ m.scale = function() {
+ var v = scale.apply(m, arguments);
+ return v === m ? clipAuto ? m.clipExtent(null) : m : v;
+ };
+ m.translate = function() {
+ var v = translate.apply(m, arguments);
+ return v === m ? clipAuto ? m.clipExtent(null) : m : v;
+ };
+ m.clipExtent = function(_) {
+ var v = clipExtent.apply(m, arguments);
+ if (v === m) {
+ if (clipAuto = _ == null) {
+ var k = π * scale(), t = translate();
+ clipExtent([ [ t[0] - k, t[1] - k ], [ t[0] + k, t[1] + k ] ]);
+ }
+ } else if (clipAuto) {
+ v = null;
+ }
+ return v;
+ };
+ return m.clipExtent(null);
+ }
+ (d3.geo.mercator = function() {
+ return d3_geo_mercatorProjection(d3_geo_mercator);
+ }).raw = d3_geo_mercator;
+ var d3_geo_orthographic = d3_geo_azimuthal(function() {
+ return 1;
+ }, Math.asin);
+ (d3.geo.orthographic = function() {
+ return d3_geo_projection(d3_geo_orthographic);
+ }).raw = d3_geo_orthographic;
+ var d3_geo_stereographic = d3_geo_azimuthal(function(cosλcosφ) {
+ return 1 / (1 + cosλcosφ);
+ }, function(ρ) {
+ return 2 * Math.atan(ρ);
+ });
+ (d3.geo.stereographic = function() {
+ return d3_geo_projection(d3_geo_stereographic);
+ }).raw = d3_geo_stereographic;
+ function d3_geo_transverseMercator(λ, φ) {
+ return [ Math.log(Math.tan(π / 4 + φ / 2)), -λ ];
+ }
+ d3_geo_transverseMercator.invert = function(x, y) {
+ return [ -y, 2 * Math.atan(Math.exp(x)) - halfπ ];
+ };
+ (d3.geo.transverseMercator = function() {
+ var projection = d3_geo_mercatorProjection(d3_geo_transverseMercator), center = projection.center, rotate = projection.rotate;
+ projection.center = function(_) {
+ return _ ? center([ -_[1], _[0] ]) : (_ = center(), [ -_[1], _[0] ]);
+ };
+ projection.rotate = function(_) {
+ return _ ? rotate([ _[0], _[1], _.length > 2 ? _[2] + 90 : 90 ]) : (_ = rotate(),
+ [ _[0], _[1], _[2] - 90 ]);
+ };
+ return projection.rotate([ 0, 0 ]);
+ }).raw = d3_geo_transverseMercator;
+ d3.geom = {};
+ function d3_geom_pointX(d) {
+ return d[0];
+ }
+ function d3_geom_pointY(d) {
+ return d[1];
+ }
+ d3.geom.hull = function(vertices) {
+ var x = d3_geom_pointX, y = d3_geom_pointY;
+ if (arguments.length) return hull(vertices);
+ function hull(data) {
+ if (data.length < 3) return [];
+ var fx = d3_functor(x), fy = d3_functor(y), i, n = data.length, points = [], flippedPoints = [];
+ for (i = 0; i < n; i++) {
+ points.push([ +fx.call(this, data[i], i), +fy.call(this, data[i], i), i ]);
+ }
+ points.sort(d3_geom_hullOrder);
+ for (i = 0; i < n; i++) flippedPoints.push([ points[i][0], -points[i][1] ]);
+ var upper = d3_geom_hullUpper(points), lower = d3_geom_hullUpper(flippedPoints);
+ var skipLeft = lower[0] === upper[0], skipRight = lower[lower.length - 1] === upper[upper.length - 1], polygon = [];
+ for (i = upper.length - 1; i >= 0; --i) polygon.push(data[points[upper[i]][2]]);
+ for (i = +skipLeft; i < lower.length - skipRight; ++i) polygon.push(data[points[lower[i]][2]]);
+ return polygon;
+ }
+ hull.x = function(_) {
+ return arguments.length ? (x = _, hull) : x;
+ };
+ hull.y = function(_) {
+ return arguments.length ? (y = _, hull) : y;
+ };
+ return hull;
+ };
+ function d3_geom_hullUpper(points) {
+ var n = points.length, hull = [ 0, 1 ], hs = 2;
+ for (var i = 2; i < n; i++) {
+ while (hs > 1 && d3_cross2d(points[hull[hs - 2]], points[hull[hs - 1]], points[i]) <= 0) --hs;
+ hull[hs++] = i;
+ }
+ return hull.slice(0, hs);
+ }
+ function d3_geom_hullOrder(a, b) {
+ return a[0] - b[0] || a[1] - b[1];
+ }
+ d3.geom.polygon = function(coordinates) {
+ d3_subclass(coordinates, d3_geom_polygonPrototype);
+ return coordinates;
+ };
+ var d3_geom_polygonPrototype = d3.geom.polygon.prototype = [];
+ d3_geom_polygonPrototype.area = function() {
+ var i = -1, n = this.length, a, b = this[n - 1], area = 0;
+ while (++i < n) {
+ a = b;
+ b = this[i];
+ area += a[1] * b[0] - a[0] * b[1];
+ }
+ return area * .5;
+ };
+ d3_geom_polygonPrototype.centroid = function(k) {
+ var i = -1, n = this.length, x = 0, y = 0, a, b = this[n - 1], c;
+ if (!arguments.length) k = -1 / (6 * this.area());
+ while (++i < n) {
+ a = b;
+ b = this[i];
+ c = a[0] * b[1] - b[0] * a[1];
+ x += (a[0] + b[0]) * c;
+ y += (a[1] + b[1]) * c;
+ }
+ return [ x * k, y * k ];
+ };
+ d3_geom_polygonPrototype.clip = function(subject) {
+ var input, closed = d3_geom_polygonClosed(subject), i = -1, n = this.length - d3_geom_polygonClosed(this), j, m, a = this[n - 1], b, c, d;
+ while (++i < n) {
+ input = subject.slice();
+ subject.length = 0;
+ b = this[i];
+ c = input[(m = input.length - closed) - 1];
+ j = -1;
+ while (++j < m) {
+ d = input[j];
+ if (d3_geom_polygonInside(d, a, b)) {
+ if (!d3_geom_polygonInside(c, a, b)) {
+ subject.push(d3_geom_polygonIntersect(c, d, a, b));
+ }
+ subject.push(d);
+ } else if (d3_geom_polygonInside(c, a, b)) {
+ subject.push(d3_geom_polygonIntersect(c, d, a, b));
+ }
+ c = d;
+ }
+ if (closed) subject.push(subject[0]);
+ a = b;
+ }
+ return subject;
+ };
+ function d3_geom_polygonInside(p, a, b) {
+ return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]);
+ }
+ function d3_geom_polygonIntersect(c, d, a, b) {
+ var x1 = c[0], x3 = a[0], x21 = d[0] - x1, x43 = b[0] - x3, y1 = c[1], y3 = a[1], y21 = d[1] - y1, y43 = b[1] - y3, ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21);
+ return [ x1 + ua * x21, y1 + ua * y21 ];
+ }
+ function d3_geom_polygonClosed(coordinates) {
+ var a = coordinates[0], b = coordinates[coordinates.length - 1];
+ return !(a[0] - b[0] || a[1] - b[1]);
+ }
+ var d3_geom_voronoiEdges, d3_geom_voronoiCells, d3_geom_voronoiBeaches, d3_geom_voronoiBeachPool = [], d3_geom_voronoiFirstCircle, d3_geom_voronoiCircles, d3_geom_voronoiCirclePool = [];
+ function d3_geom_voronoiBeach() {
+ d3_geom_voronoiRedBlackNode(this);
+ this.edge = this.site = this.circle = null;
+ }
+ function d3_geom_voronoiCreateBeach(site) {
+ var beach = d3_geom_voronoiBeachPool.pop() || new d3_geom_voronoiBeach();
+ beach.site = site;
+ return beach;
+ }
+ function d3_geom_voronoiDetachBeach(beach) {
+ d3_geom_voronoiDetachCircle(beach);
+ d3_geom_voronoiBeaches.remove(beach);
+ d3_geom_voronoiBeachPool.push(beach);
+ d3_geom_voronoiRedBlackNode(beach);
+ }
+ function d3_geom_voronoiRemoveBeach(beach) {
+ var circle = beach.circle, x = circle.x, y = circle.cy, vertex = {
+ x: x,
+ y: y
+ }, previous = beach.P, next = beach.N, disappearing = [ beach ];
+ d3_geom_voronoiDetachBeach(beach);
+ var lArc = previous;
+ while (lArc.circle && abs(x - lArc.circle.x) < ε && abs(y - lArc.circle.cy) < ε) {
+ previous = lArc.P;
+ disappearing.unshift(lArc);
+ d3_geom_voronoiDetachBeach(lArc);
+ lArc = previous;
+ }
+ disappearing.unshift(lArc);
+ d3_geom_voronoiDetachCircle(lArc);
+ var rArc = next;
+ while (rArc.circle && abs(x - rArc.circle.x) < ε && abs(y - rArc.circle.cy) < ε) {
+ next = rArc.N;
+ disappearing.push(rArc);
+ d3_geom_voronoiDetachBeach(rArc);
+ rArc = next;
+ }
+ disappearing.push(rArc);
+ d3_geom_voronoiDetachCircle(rArc);
+ var nArcs = disappearing.length, iArc;
+ for (iArc = 1; iArc < nArcs; ++iArc) {
+ rArc = disappearing[iArc];
+ lArc = disappearing[iArc - 1];
+ d3_geom_voronoiSetEdgeEnd(rArc.edge, lArc.site, rArc.site, vertex);
+ }
+ lArc = disappearing[0];
+ rArc = disappearing[nArcs - 1];
+ rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, rArc.site, null, vertex);
+ d3_geom_voronoiAttachCircle(lArc);
+ d3_geom_voronoiAttachCircle(rArc);
+ }
+ function d3_geom_voronoiAddBeach(site) {
+ var x = site.x, directrix = site.y, lArc, rArc, dxl, dxr, node = d3_geom_voronoiBeaches._;
+ while (node) {
+ dxl = d3_geom_voronoiLeftBreakPoint(node, directrix) - x;
+ if (dxl > ε) node = node.L; else {
+ dxr = x - d3_geom_voronoiRightBreakPoint(node, directrix);
+ if (dxr > ε) {
+ if (!node.R) {
+ lArc = node;
+ break;
+ }
+ node = node.R;
+ } else {
+ if (dxl > -ε) {
+ lArc = node.P;
+ rArc = node;
+ } else if (dxr > -ε) {
+ lArc = node;
+ rArc = node.N;
+ } else {
+ lArc = rArc = node;
+ }
+ break;
+ }
+ }
+ }
+ var newArc = d3_geom_voronoiCreateBeach(site);
+ d3_geom_voronoiBeaches.insert(lArc, newArc);
+ if (!lArc && !rArc) return;
+ if (lArc === rArc) {
+ d3_geom_voronoiDetachCircle(lArc);
+ rArc = d3_geom_voronoiCreateBeach(lArc.site);
+ d3_geom_voronoiBeaches.insert(newArc, rArc);
+ newArc.edge = rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site);
+ d3_geom_voronoiAttachCircle(lArc);
+ d3_geom_voronoiAttachCircle(rArc);
+ return;
+ }
+ if (!rArc) {
+ newArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site);
+ return;
+ }
+ d3_geom_voronoiDetachCircle(lArc);
+ d3_geom_voronoiDetachCircle(rArc);
+ var lSite = lArc.site, ax = lSite.x, ay = lSite.y, bx = site.x - ax, by = site.y - ay, rSite = rArc.site, cx = rSite.x - ax, cy = rSite.y - ay, d = 2 * (bx * cy - by * cx), hb = bx * bx + by * by, hc = cx * cx + cy * cy, vertex = {
+ x: (cy * hb - by * hc) / d + ax,
+ y: (bx * hc - cx * hb) / d + ay
+ };
+ d3_geom_voronoiSetEdgeEnd(rArc.edge, lSite, rSite, vertex);
+ newArc.edge = d3_geom_voronoiCreateEdge(lSite, site, null, vertex);
+ rArc.edge = d3_geom_voronoiCreateEdge(site, rSite, null, vertex);
+ d3_geom_voronoiAttachCircle(lArc);
+ d3_geom_voronoiAttachCircle(rArc);
+ }
+ function d3_geom_voronoiLeftBreakPoint(arc, directrix) {
+ var site = arc.site, rfocx = site.x, rfocy = site.y, pby2 = rfocy - directrix;
+ if (!pby2) return rfocx;
+ var lArc = arc.P;
+ if (!lArc) return -Infinity;
+ site = lArc.site;
+ var lfocx = site.x, lfocy = site.y, plby2 = lfocy - directrix;
+ if (!plby2) return lfocx;
+ var hl = lfocx - rfocx, aby2 = 1 / pby2 - 1 / plby2, b = hl / plby2;
+ if (aby2) return (-b + Math.sqrt(b * b - 2 * aby2 * (hl * hl / (-2 * plby2) - lfocy + plby2 / 2 + rfocy - pby2 / 2))) / aby2 + rfocx;
+ return (rfocx + lfocx) / 2;
+ }
+ function d3_geom_voronoiRightBreakPoint(arc, directrix) {
+ var rArc = arc.N;
+ if (rArc) return d3_geom_voronoiLeftBreakPoint(rArc, directrix);
+ var site = arc.site;
+ return site.y === directrix ? site.x : Infinity;
+ }
+ function d3_geom_voronoiCell(site) {
+ this.site = site;
+ this.edges = [];
+ }
+ d3_geom_voronoiCell.prototype.prepare = function() {
+ var halfEdges = this.edges, iHalfEdge = halfEdges.length, edge;
+ while (iHalfEdge--) {
+ edge = halfEdges[iHalfEdge].edge;
+ if (!edge.b || !edge.a) halfEdges.splice(iHalfEdge, 1);
+ }
+ halfEdges.sort(d3_geom_voronoiHalfEdgeOrder);
+ return halfEdges.length;
+ };
+ function d3_geom_voronoiCloseCells(extent) {
+ var x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], x2, y2, x3, y3, cells = d3_geom_voronoiCells, iCell = cells.length, cell, iHalfEdge, halfEdges, nHalfEdges, start, end;
+ while (iCell--) {
+ cell = cells[iCell];
+ if (!cell || !cell.prepare()) continue;
+ halfEdges = cell.edges;
+ nHalfEdges = halfEdges.length;
+ iHalfEdge = 0;
+ while (iHalfEdge < nHalfEdges) {
+ end = halfEdges[iHalfEdge].end(), x3 = end.x, y3 = end.y;
+ start = halfEdges[++iHalfEdge % nHalfEdges].start(), x2 = start.x, y2 = start.y;
+ if (abs(x3 - x2) > ε || abs(y3 - y2) > ε) {
+ halfEdges.splice(iHalfEdge, 0, new d3_geom_voronoiHalfEdge(d3_geom_voronoiCreateBorderEdge(cell.site, end, abs(x3 - x0) < ε && y1 - y3 > ε ? {
+ x: x0,
+ y: abs(x2 - x0) < ε ? y2 : y1
+ } : abs(y3 - y1) < ε && x1 - x3 > ε ? {
+ x: abs(y2 - y1) < ε ? x2 : x1,
+ y: y1
+ } : abs(x3 - x1) < ε && y3 - y0 > ε ? {
+ x: x1,
+ y: abs(x2 - x1) < ε ? y2 : y0
+ } : abs(y3 - y0) < ε && x3 - x0 > ε ? {
+ x: abs(y2 - y0) < ε ? x2 : x0,
+ y: y0
+ } : null), cell.site, null));
+ ++nHalfEdges;
+ }
+ }
+ }
+ }
+ function d3_geom_voronoiHalfEdgeOrder(a, b) {
+ return b.angle - a.angle;
+ }
+ function d3_geom_voronoiCircle() {
+ d3_geom_voronoiRedBlackNode(this);
+ this.x = this.y = this.arc = this.site = this.cy = null;
+ }
+ function d3_geom_voronoiAttachCircle(arc) {
+ var lArc = arc.P, rArc = arc.N;
+ if (!lArc || !rArc) return;
+ var lSite = lArc.site, cSite = arc.site, rSite = rArc.site;
+ if (lSite === rSite) return;
+ var bx = cSite.x, by = cSite.y, ax = lSite.x - bx, ay = lSite.y - by, cx = rSite.x - bx, cy = rSite.y - by;
+ var d = 2 * (ax * cy - ay * cx);
+ if (d >= -ε2) return;
+ var ha = ax * ax + ay * ay, hc = cx * cx + cy * cy, x = (cy * ha - ay * hc) / d, y = (ax * hc - cx * ha) / d, cy = y + by;
+ var circle = d3_geom_voronoiCirclePool.pop() || new d3_geom_voronoiCircle();
+ circle.arc = arc;
+ circle.site = cSite;
+ circle.x = x + bx;
+ circle.y = cy + Math.sqrt(x * x + y * y);
+ circle.cy = cy;
+ arc.circle = circle;
+ var before = null, node = d3_geom_voronoiCircles._;
+ while (node) {
+ if (circle.y < node.y || circle.y === node.y && circle.x <= node.x) {
+ if (node.L) node = node.L; else {
+ before = node.P;
+ break;
+ }
+ } else {
+ if (node.R) node = node.R; else {
+ before = node;
+ break;
+ }
+ }
+ }
+ d3_geom_voronoiCircles.insert(before, circle);
+ if (!before) d3_geom_voronoiFirstCircle = circle;
+ }
+ function d3_geom_voronoiDetachCircle(arc) {
+ var circle = arc.circle;
+ if (circle) {
+ if (!circle.P) d3_geom_voronoiFirstCircle = circle.N;
+ d3_geom_voronoiCircles.remove(circle);
+ d3_geom_voronoiCirclePool.push(circle);
+ d3_geom_voronoiRedBlackNode(circle);
+ arc.circle = null;
+ }
+ }
+ function d3_geom_voronoiClipEdges(extent) {
+ var edges = d3_geom_voronoiEdges, clip = d3_geom_clipLine(extent[0][0], extent[0][1], extent[1][0], extent[1][1]), i = edges.length, e;
+ while (i--) {
+ e = edges[i];
+ if (!d3_geom_voronoiConnectEdge(e, extent) || !clip(e) || abs(e.a.x - e.b.x) < ε && abs(e.a.y - e.b.y) < ε) {
+ e.a = e.b = null;
+ edges.splice(i, 1);
+ }
+ }
+ }
+ function d3_geom_voronoiConnectEdge(edge, extent) {
+ var vb = edge.b;
+ if (vb) return true;
+ var va = edge.a, x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], lSite = edge.l, rSite = edge.r, lx = lSite.x, ly = lSite.y, rx = rSite.x, ry = rSite.y, fx = (lx + rx) / 2, fy = (ly + ry) / 2, fm, fb;
+ if (ry === ly) {
+ if (fx < x0 || fx >= x1) return;
+ if (lx > rx) {
+ if (!va) va = {
+ x: fx,
+ y: y0
+ }; else if (va.y >= y1) return;
+ vb = {
+ x: fx,
+ y: y1
+ };
+ } else {
+ if (!va) va = {
+ x: fx,
+ y: y1
+ }; else if (va.y < y0) return;
+ vb = {
+ x: fx,
+ y: y0
+ };
+ }
+ } else {
+ fm = (lx - rx) / (ry - ly);
+ fb = fy - fm * fx;
+ if (fm < -1 || fm > 1) {
+ if (lx > rx) {
+ if (!va) va = {
+ x: (y0 - fb) / fm,
+ y: y0
+ }; else if (va.y >= y1) return;
+ vb = {
+ x: (y1 - fb) / fm,
+ y: y1
+ };
+ } else {
+ if (!va) va = {
+ x: (y1 - fb) / fm,
+ y: y1
+ }; else if (va.y < y0) return;
+ vb = {
+ x: (y0 - fb) / fm,
+ y: y0
+ };
+ }
+ } else {
+ if (ly < ry) {
+ if (!va) va = {
+ x: x0,
+ y: fm * x0 + fb
+ }; else if (va.x >= x1) return;
+ vb = {
+ x: x1,
+ y: fm * x1 + fb
+ };
+ } else {
+ if (!va) va = {
+ x: x1,
+ y: fm * x1 + fb
+ }; else if (va.x < x0) return;
+ vb = {
+ x: x0,
+ y: fm * x0 + fb
+ };
+ }
+ }
+ }
+ edge.a = va;
+ edge.b = vb;
+ return true;
+ }
+ function d3_geom_voronoiEdge(lSite, rSite) {
+ this.l = lSite;
+ this.r = rSite;
+ this.a = this.b = null;
+ }
+ function d3_geom_voronoiCreateEdge(lSite, rSite, va, vb) {
+ var edge = new d3_geom_voronoiEdge(lSite, rSite);
+ d3_geom_voronoiEdges.push(edge);
+ if (va) d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, va);
+ if (vb) d3_geom_voronoiSetEdgeEnd(edge, rSite, lSite, vb);
+ d3_geom_voronoiCells[lSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, lSite, rSite));
+ d3_geom_voronoiCells[rSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, rSite, lSite));
+ return edge;
+ }
+ function d3_geom_voronoiCreateBorderEdge(lSite, va, vb) {
+ var edge = new d3_geom_voronoiEdge(lSite, null);
+ edge.a = va;
+ edge.b = vb;
+ d3_geom_voronoiEdges.push(edge);
+ return edge;
+ }
+ function d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, vertex) {
+ if (!edge.a && !edge.b) {
+ edge.a = vertex;
+ edge.l = lSite;
+ edge.r = rSite;
+ } else if (edge.l === rSite) {
+ edge.b = vertex;
+ } else {
+ edge.a = vertex;
+ }
+ }
+ function d3_geom_voronoiHalfEdge(edge, lSite, rSite) {
+ var va = edge.a, vb = edge.b;
+ this.edge = edge;
+ this.site = lSite;
+ this.angle = rSite ? Math.atan2(rSite.y - lSite.y, rSite.x - lSite.x) : edge.l === lSite ? Math.atan2(vb.x - va.x, va.y - vb.y) : Math.atan2(va.x - vb.x, vb.y - va.y);
+ }
+ d3_geom_voronoiHalfEdge.prototype = {
+ start: function() {
+ return this.edge.l === this.site ? this.edge.a : this.edge.b;
+ },
+ end: function() {
+ return this.edge.l === this.site ? this.edge.b : this.edge.a;
+ }
+ };
+ function d3_geom_voronoiRedBlackTree() {
+ this._ = null;
+ }
+ function d3_geom_voronoiRedBlackNode(node) {
+ node.U = node.C = node.L = node.R = node.P = node.N = null;
+ }
+ d3_geom_voronoiRedBlackTree.prototype = {
+ insert: function(after, node) {
+ var parent, grandpa, uncle;
+ if (after) {
+ node.P = after;
+ node.N = after.N;
+ if (after.N) after.N.P = node;
+ after.N = node;
+ if (after.R) {
+ after = after.R;
+ while (after.L) after = after.L;
+ after.L = node;
+ } else {
+ after.R = node;
+ }
+ parent = after;
+ } else if (this._) {
+ after = d3_geom_voronoiRedBlackFirst(this._);
+ node.P = null;
+ node.N = after;
+ after.P = after.L = node;
+ parent = after;
+ } else {
+ node.P = node.N = null;
+ this._ = node;
+ parent = null;
+ }
+ node.L = node.R = null;
+ node.U = parent;
+ node.C = true;
+ after = node;
+ while (parent && parent.C) {
+ grandpa = parent.U;
+ if (parent === grandpa.L) {
+ uncle = grandpa.R;
+ if (uncle && uncle.C) {
+ parent.C = uncle.C = false;
+ grandpa.C = true;
+ after = grandpa;
+ } else {
+ if (after === parent.R) {
+ d3_geom_voronoiRedBlackRotateLeft(this, parent);
+ after = parent;
+ parent = after.U;
+ }
+ parent.C = false;
+ grandpa.C = true;
+ d3_geom_voronoiRedBlackRotateRight(this, grandpa);
+ }
+ } else {
+ uncle = grandpa.L;
+ if (uncle && uncle.C) {
+ parent.C = uncle.C = false;
+ grandpa.C = true;
+ after = grandpa;
+ } else {
+ if (after === parent.L) {
+ d3_geom_voronoiRedBlackRotateRight(this, parent);
+ after = parent;
+ parent = after.U;
+ }
+ parent.C = false;
+ grandpa.C = true;
+ d3_geom_voronoiRedBlackRotateLeft(this, grandpa);
+ }
+ }
+ parent = after.U;
+ }
+ this._.C = false;
+ },
+ remove: function(node) {
+ if (node.N) node.N.P = node.P;
+ if (node.P) node.P.N = node.N;
+ node.N = node.P = null;
+ var parent = node.U, sibling, left = node.L, right = node.R, next, red;
+ if (!left) next = right; else if (!right) next = left; else next = d3_geom_voronoiRedBlackFirst(right);
+ if (parent) {
+ if (parent.L === node) parent.L = next; else parent.R = next;
+ } else {
+ this._ = next;
+ }
+ if (left && right) {
+ red = next.C;
+ next.C = node.C;
+ next.L = left;
+ left.U = next;
+ if (next !== right) {
+ parent = next.U;
+ next.U = node.U;
+ node = next.R;
+ parent.L = node;
+ next.R = right;
+ right.U = next;
+ } else {
+ next.U = parent;
+ parent = next;
+ node = next.R;
+ }
+ } else {
+ red = node.C;
+ node = next;
+ }
+ if (node) node.U = parent;
+ if (red) return;
+ if (node && node.C) {
+ node.C = false;
+ return;
+ }
+ do {
+ if (node === this._) break;
+ if (node === parent.L) {
+ sibling = parent.R;
+ if (sibling.C) {
+ sibling.C = false;
+ parent.C = true;
+ d3_geom_voronoiRedBlackRotateLeft(this, parent);
+ sibling = parent.R;
+ }
+ if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) {
+ if (!sibling.R || !sibling.R.C) {
+ sibling.L.C = false;
+ sibling.C = true;
+ d3_geom_voronoiRedBlackRotateRight(this, sibling);
+ sibling = parent.R;
+ }
+ sibling.C = parent.C;
+ parent.C = sibling.R.C = false;
+ d3_geom_voronoiRedBlackRotateLeft(this, parent);
+ node = this._;
+ break;
+ }
+ } else {
+ sibling = parent.L;
+ if (sibling.C) {
+ sibling.C = false;
+ parent.C = true;
+ d3_geom_voronoiRedBlackRotateRight(this, parent);
+ sibling = parent.L;
+ }
+ if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) {
+ if (!sibling.L || !sibling.L.C) {
+ sibling.R.C = false;
+ sibling.C = true;
+ d3_geom_voronoiRedBlackRotateLeft(this, sibling);
+ sibling = parent.L;
+ }
+ sibling.C = parent.C;
+ parent.C = sibling.L.C = false;
+ d3_geom_voronoiRedBlackRotateRight(this, parent);
+ node = this._;
+ break;
+ }
+ }
+ sibling.C = true;
+ node = parent;
+ parent = parent.U;
+ } while (!node.C);
+ if (node) node.C = false;
+ }
+ };
+ function d3_geom_voronoiRedBlackRotateLeft(tree, node) {
+ var p = node, q = node.R, parent = p.U;
+ if (parent) {
+ if (parent.L === p) parent.L = q; else parent.R = q;
+ } else {
+ tree._ = q;
+ }
+ q.U = parent;
+ p.U = q;
+ p.R = q.L;
+ if (p.R) p.R.U = p;
+ q.L = p;
+ }
+ function d3_geom_voronoiRedBlackRotateRight(tree, node) {
+ var p = node, q = node.L, parent = p.U;
+ if (parent) {
+ if (parent.L === p) parent.L = q; else parent.R = q;
+ } else {
+ tree._ = q;
+ }
+ q.U = parent;
+ p.U = q;
+ p.L = q.R;
+ if (p.L) p.L.U = p;
+ q.R = p;
+ }
+ function d3_geom_voronoiRedBlackFirst(node) {
+ while (node.L) node = node.L;
+ return node;
+ }
+ function d3_geom_voronoi(sites, bbox) {
+ var site = sites.sort(d3_geom_voronoiVertexOrder).pop(), x0, y0, circle;
+ d3_geom_voronoiEdges = [];
+ d3_geom_voronoiCells = new Array(sites.length);
+ d3_geom_voronoiBeaches = new d3_geom_voronoiRedBlackTree();
+ d3_geom_voronoiCircles = new d3_geom_voronoiRedBlackTree();
+ while (true) {
+ circle = d3_geom_voronoiFirstCircle;
+ if (site && (!circle || site.y < circle.y || site.y === circle.y && site.x < circle.x)) {
+ if (site.x !== x0 || site.y !== y0) {
+ d3_geom_voronoiCells[site.i] = new d3_geom_voronoiCell(site);
+ d3_geom_voronoiAddBeach(site);
+ x0 = site.x, y0 = site.y;
+ }
+ site = sites.pop();
+ } else if (circle) {
+ d3_geom_voronoiRemoveBeach(circle.arc);
+ } else {
+ break;
+ }
+ }
+ if (bbox) d3_geom_voronoiClipEdges(bbox), d3_geom_voronoiCloseCells(bbox);
+ var diagram = {
+ cells: d3_geom_voronoiCells,
+ edges: d3_geom_voronoiEdges
+ };
+ d3_geom_voronoiBeaches = d3_geom_voronoiCircles = d3_geom_voronoiEdges = d3_geom_voronoiCells = null;
+ return diagram;
+ }
+ function d3_geom_voronoiVertexOrder(a, b) {
+ return b.y - a.y || b.x - a.x;
+ }
+ d3.geom.voronoi = function(points) {
+ var x = d3_geom_pointX, y = d3_geom_pointY, fx = x, fy = y, clipExtent = d3_geom_voronoiClipExtent;
+ if (points) return voronoi(points);
+ function voronoi(data) {
+ var polygons = new Array(data.length), x0 = clipExtent[0][0], y0 = clipExtent[0][1], x1 = clipExtent[1][0], y1 = clipExtent[1][1];
+ d3_geom_voronoi(sites(data), clipExtent).cells.forEach(function(cell, i) {
+ var edges = cell.edges, site = cell.site, polygon = polygons[i] = edges.length ? edges.map(function(e) {
+ var s = e.start();
+ return [ s.x, s.y ];
+ }) : site.x >= x0 && site.x <= x1 && site.y >= y0 && site.y <= y1 ? [ [ x0, y1 ], [ x1, y1 ], [ x1, y0 ], [ x0, y0 ] ] : [];
+ polygon.point = data[i];
+ });
+ return polygons;
+ }
+ function sites(data) {
+ return data.map(function(d, i) {
+ return {
+ x: Math.round(fx(d, i) / ε) * ε,
+ y: Math.round(fy(d, i) / ε) * ε,
+ i: i
+ };
+ });
+ }
+ voronoi.links = function(data) {
+ return d3_geom_voronoi(sites(data)).edges.filter(function(edge) {
+ return edge.l && edge.r;
+ }).map(function(edge) {
+ return {
+ source: data[edge.l.i],
+ target: data[edge.r.i]
+ };
+ });
+ };
+ voronoi.triangles = function(data) {
+ var triangles = [];
+ d3_geom_voronoi(sites(data)).cells.forEach(function(cell, i) {
+ var site = cell.site, edges = cell.edges.sort(d3_geom_voronoiHalfEdgeOrder), j = -1, m = edges.length, e0, s0, e1 = edges[m - 1].edge, s1 = e1.l === site ? e1.r : e1.l;
+ while (++j < m) {
+ e0 = e1;
+ s0 = s1;
+ e1 = edges[j].edge;
+ s1 = e1.l === site ? e1.r : e1.l;
+ if (i < s0.i && i < s1.i && d3_geom_voronoiTriangleArea(site, s0, s1) < 0) {
+ triangles.push([ data[i], data[s0.i], data[s1.i] ]);
+ }
+ }
+ });
+ return triangles;
+ };
+ voronoi.x = function(_) {
+ return arguments.length ? (fx = d3_functor(x = _), voronoi) : x;
+ };
+ voronoi.y = function(_) {
+ return arguments.length ? (fy = d3_functor(y = _), voronoi) : y;
+ };
+ voronoi.clipExtent = function(_) {
+ if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent;
+ clipExtent = _ == null ? d3_geom_voronoiClipExtent : _;
+ return voronoi;
+ };
+ voronoi.size = function(_) {
+ if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent && clipExtent[1];
+ return voronoi.clipExtent(_ && [ [ 0, 0 ], _ ]);
+ };
+ return voronoi;
+ };
+ var d3_geom_voronoiClipExtent = [ [ -1e6, -1e6 ], [ 1e6, 1e6 ] ];
+ function d3_geom_voronoiTriangleArea(a, b, c) {
+ return (a.x - c.x) * (b.y - a.y) - (a.x - b.x) * (c.y - a.y);
+ }
+ d3.geom.delaunay = function(vertices) {
+ return d3.geom.voronoi().triangles(vertices);
+ };
+ d3.geom.quadtree = function(points, x1, y1, x2, y2) {
+ var x = d3_geom_pointX, y = d3_geom_pointY, compat;
+ if (compat = arguments.length) {
+ x = d3_geom_quadtreeCompatX;
+ y = d3_geom_quadtreeCompatY;
+ if (compat === 3) {
+ y2 = y1;
+ x2 = x1;
+ y1 = x1 = 0;
+ }
+ return quadtree(points);
+ }
+ function quadtree(data) {
+ var d, fx = d3_functor(x), fy = d3_functor(y), xs, ys, i, n, x1_, y1_, x2_, y2_;
+ if (x1 != null) {
+ x1_ = x1, y1_ = y1, x2_ = x2, y2_ = y2;
+ } else {
+ x2_ = y2_ = -(x1_ = y1_ = Infinity);
+ xs = [], ys = [];
+ n = data.length;
+ if (compat) for (i = 0; i < n; ++i) {
+ d = data[i];
+ if (d.x < x1_) x1_ = d.x;
+ if (d.y < y1_) y1_ = d.y;
+ if (d.x > x2_) x2_ = d.x;
+ if (d.y > y2_) y2_ = d.y;
+ xs.push(d.x);
+ ys.push(d.y);
+ } else for (i = 0; i < n; ++i) {
+ var x_ = +fx(d = data[i], i), y_ = +fy(d, i);
+ if (x_ < x1_) x1_ = x_;
+ if (y_ < y1_) y1_ = y_;
+ if (x_ > x2_) x2_ = x_;
+ if (y_ > y2_) y2_ = y_;
+ xs.push(x_);
+ ys.push(y_);
+ }
+ }
+ var dx = x2_ - x1_, dy = y2_ - y1_;
+ if (dx > dy) y2_ = y1_ + dx; else x2_ = x1_ + dy;
+ function insert(n, d, x, y, x1, y1, x2, y2) {
+ if (isNaN(x) || isNaN(y)) return;
+ if (n.leaf) {
+ var nx = n.x, ny = n.y;
+ if (nx != null) {
+ if (abs(nx - x) + abs(ny - y) < .01) {
+ insertChild(n, d, x, y, x1, y1, x2, y2);
+ } else {
+ var nPoint = n.point;
+ n.x = n.y = n.point = null;
+ insertChild(n, nPoint, nx, ny, x1, y1, x2, y2);
+ insertChild(n, d, x, y, x1, y1, x2, y2);
+ }
+ } else {
+ n.x = x, n.y = y, n.point = d;
+ }
+ } else {
+ insertChild(n, d, x, y, x1, y1, x2, y2);
+ }
+ }
+ function insertChild(n, d, x, y, x1, y1, x2, y2) {
+ var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5, right = x >= sx, bottom = y >= sy, i = (bottom << 1) + right;
+ n.leaf = false;
+ n = n.nodes[i] || (n.nodes[i] = d3_geom_quadtreeNode());
+ if (right) x1 = sx; else x2 = sx;
+ if (bottom) y1 = sy; else y2 = sy;
+ insert(n, d, x, y, x1, y1, x2, y2);
+ }
+ var root = d3_geom_quadtreeNode();
+ root.add = function(d) {
+ insert(root, d, +fx(d, ++i), +fy(d, i), x1_, y1_, x2_, y2_);
+ };
+ root.visit = function(f) {
+ d3_geom_quadtreeVisit(f, root, x1_, y1_, x2_, y2_);
+ };
+ i = -1;
+ if (x1 == null) {
+ while (++i < n) {
+ insert(root, data[i], xs[i], ys[i], x1_, y1_, x2_, y2_);
+ }
+ --i;
+ } else data.forEach(root.add);
+ xs = ys = data = d = null;
+ return root;
+ }
+ quadtree.x = function(_) {
+ return arguments.length ? (x = _, quadtree) : x;
+ };
+ quadtree.y = function(_) {
+ return arguments.length ? (y = _, quadtree) : y;
+ };
+ quadtree.extent = function(_) {
+ if (!arguments.length) return x1 == null ? null : [ [ x1, y1 ], [ x2, y2 ] ];
+ if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = +_[0][0], y1 = +_[0][1], x2 = +_[1][0],
+ y2 = +_[1][1];
+ return quadtree;
+ };
+ quadtree.size = function(_) {
+ if (!arguments.length) return x1 == null ? null : [ x2 - x1, y2 - y1 ];
+ if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = y1 = 0, x2 = +_[0], y2 = +_[1];
+ return quadtree;
+ };
+ return quadtree;
+ };
+ function d3_geom_quadtreeCompatX(d) {
+ return d.x;
+ }
+ function d3_geom_quadtreeCompatY(d) {
+ return d.y;
+ }
+ function d3_geom_quadtreeNode() {
+ return {
+ leaf: true,
+ nodes: [],
+ point: null,
+ x: null,
+ y: null
+ };
+ }
+ function d3_geom_quadtreeVisit(f, node, x1, y1, x2, y2) {
+ if (!f(node, x1, y1, x2, y2)) {
+ var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5, children = node.nodes;
+ if (children[0]) d3_geom_quadtreeVisit(f, children[0], x1, y1, sx, sy);
+ if (children[1]) d3_geom_quadtreeVisit(f, children[1], sx, y1, x2, sy);
+ if (children[2]) d3_geom_quadtreeVisit(f, children[2], x1, sy, sx, y2);
+ if (children[3]) d3_geom_quadtreeVisit(f, children[3], sx, sy, x2, y2);
+ }
+ }
+ d3.interpolateRgb = d3_interpolateRgb;
+ function d3_interpolateRgb(a, b) {
+ a = d3.rgb(a);
+ b = d3.rgb(b);
+ var ar = a.r, ag = a.g, ab = a.b, br = b.r - ar, bg = b.g - ag, bb = b.b - ab;
+ return function(t) {
+ return "#" + d3_rgb_hex(Math.round(ar + br * t)) + d3_rgb_hex(Math.round(ag + bg * t)) + d3_rgb_hex(Math.round(ab + bb * t));
+ };
+ }
+ d3.interpolateObject = d3_interpolateObject;
+ function d3_interpolateObject(a, b) {
+ var i = {}, c = {}, k;
+ for (k in a) {
+ if (k in b) {
+ i[k] = d3_interpolate(a[k], b[k]);
+ } else {
+ c[k] = a[k];
+ }
+ }
+ for (k in b) {
+ if (!(k in a)) {
+ c[k] = b[k];
+ }
+ }
+ return function(t) {
+ for (k in i) c[k] = i[k](t);
+ return c;
+ };
+ }
+ d3.interpolateNumber = d3_interpolateNumber;
+ function d3_interpolateNumber(a, b) {
+ b -= a = +a;
+ return function(t) {
+ return a + b * t;
+ };
+ }
+ d3.interpolateString = d3_interpolateString;
+ function d3_interpolateString(a, b) {
+ var m, i, j, s0 = 0, s1 = 0, s = [], q = [], n, o;
+ a = a + "", b = b + "";
+ d3_interpolate_number.lastIndex = 0;
+ for (i = 0; m = d3_interpolate_number.exec(b); ++i) {
+ if (m.index) s.push(b.substring(s0, s1 = m.index));
+ q.push({
+ i: s.length,
+ x: m[0]
+ });
+ s.push(null);
+ s0 = d3_interpolate_number.lastIndex;
+ }
+ if (s0 < b.length) s.push(b.substring(s0));
+ for (i = 0, n = q.length; (m = d3_interpolate_number.exec(a)) && i < n; ++i) {
+ o = q[i];
+ if (o.x == m[0]) {
+ if (o.i) {
+ if (s[o.i + 1] == null) {
+ s[o.i - 1] += o.x;
+ s.splice(o.i, 1);
+ for (j = i + 1; j < n; ++j) q[j].i--;
+ } else {
+ s[o.i - 1] += o.x + s[o.i + 1];
+ s.splice(o.i, 2);
+ for (j = i + 1; j < n; ++j) q[j].i -= 2;
+ }
+ } else {
+ if (s[o.i + 1] == null) {
+ s[o.i] = o.x;
+ } else {
+ s[o.i] = o.x + s[o.i + 1];
+ s.splice(o.i + 1, 1);
+ for (j = i + 1; j < n; ++j) q[j].i--;
+ }
+ }
+ q.splice(i, 1);
+ n--;
+ i--;
+ } else {
+ o.x = d3_interpolateNumber(parseFloat(m[0]), parseFloat(o.x));
+ }
+ }
+ while (i < n) {
+ o = q.pop();
+ if (s[o.i + 1] == null) {
+ s[o.i] = o.x;
+ } else {
+ s[o.i] = o.x + s[o.i + 1];
+ s.splice(o.i + 1, 1);
+ }
+ n--;
+ }
+ if (s.length === 1) {
+ return s[0] == null ? (o = q[0].x, function(t) {
+ return o(t) + "";
+ }) : function() {
+ return b;
+ };
+ }
+ return function(t) {
+ for (i = 0; i < n; ++i) s[(o = q[i]).i] = o.x(t);
+ return s.join("");
+ };
+ }
+ var d3_interpolate_number = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g;
+ d3.interpolate = d3_interpolate;
+ function d3_interpolate(a, b) {
+ var i = d3.interpolators.length, f;
+ while (--i >= 0 && !(f = d3.interpolators[i](a, b))) ;
+ return f;
+ }
+ d3.interpolators = [ function(a, b) {
+ var t = typeof b;
+ return (t === "string" ? d3_rgb_names.has(b) || /^(#|rgb\(|hsl\()/.test(b) ? d3_interpolateRgb : d3_interpolateString : b instanceof d3_Color ? d3_interpolateRgb : t === "object" ? Array.isArray(b) ? d3_interpolateArray : d3_interpolateObject : d3_interpolateNumber)(a, b);
+ } ];
+ d3.interpolateArray = d3_interpolateArray;
+ function d3_interpolateArray(a, b) {
+ var x = [], c = [], na = a.length, nb = b.length, n0 = Math.min(a.length, b.length), i;
+ for (i = 0; i < n0; ++i) x.push(d3_interpolate(a[i], b[i]));
+ for (;i < na; ++i) c[i] = a[i];
+ for (;i < nb; ++i) c[i] = b[i];
+ return function(t) {
+ for (i = 0; i < n0; ++i) c[i] = x[i](t);
+ return c;
+ };
+ }
+ var d3_ease_default = function() {
+ return d3_identity;
+ };
+ var d3_ease = d3.map({
+ linear: d3_ease_default,
+ poly: d3_ease_poly,
+ quad: function() {
+ return d3_ease_quad;
+ },
+ cubic: function() {
+ return d3_ease_cubic;
+ },
+ sin: function() {
+ return d3_ease_sin;
+ },
+ exp: function() {
+ return d3_ease_exp;
+ },
+ circle: function() {
+ return d3_ease_circle;
+ },
+ elastic: d3_ease_elastic,
+ back: d3_ease_back,
+ bounce: function() {
+ return d3_ease_bounce;
+ }
+ });
+ var d3_ease_mode = d3.map({
+ "in": d3_identity,
+ out: d3_ease_reverse,
+ "in-out": d3_ease_reflect,
+ "out-in": function(f) {
+ return d3_ease_reflect(d3_ease_reverse(f));
+ }
+ });
+ d3.ease = function(name) {
+ var i = name.indexOf("-"), t = i >= 0 ? name.substring(0, i) : name, m = i >= 0 ? name.substring(i + 1) : "in";
+ t = d3_ease.get(t) || d3_ease_default;
+ m = d3_ease_mode.get(m) || d3_identity;
+ return d3_ease_clamp(m(t.apply(null, d3_arraySlice.call(arguments, 1))));
+ };
+ function d3_ease_clamp(f) {
+ return function(t) {
+ return t <= 0 ? 0 : t >= 1 ? 1 : f(t);
+ };
+ }
+ function d3_ease_reverse(f) {
+ return function(t) {
+ return 1 - f(1 - t);
+ };
+ }
+ function d3_ease_reflect(f) {
+ return function(t) {
+ return .5 * (t < .5 ? f(2 * t) : 2 - f(2 - 2 * t));
+ };
+ }
+ function d3_ease_quad(t) {
+ return t * t;
+ }
+ function d3_ease_cubic(t) {
+ return t * t * t;
+ }
+ function d3_ease_cubicInOut(t) {
+ if (t <= 0) return 0;
+ if (t >= 1) return 1;
+ var t2 = t * t, t3 = t2 * t;
+ return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75);
+ }
+ function d3_ease_poly(e) {
+ return function(t) {
+ return Math.pow(t, e);
+ };
+ }
+ function d3_ease_sin(t) {
+ return 1 - Math.cos(t * halfπ);
+ }
+ function d3_ease_exp(t) {
+ return Math.pow(2, 10 * (t - 1));
+ }
+ function d3_ease_circle(t) {
+ return 1 - Math.sqrt(1 - t * t);
+ }
+ function d3_ease_elastic(a, p) {
+ var s;
+ if (arguments.length < 2) p = .45;
+ if (arguments.length) s = p / τ * Math.asin(1 / a); else a = 1, s = p / 4;
+ return function(t) {
+ return 1 + a * Math.pow(2, -10 * t) * Math.sin((t - s) * τ / p);
+ };
+ }
+ function d3_ease_back(s) {
+ if (!s) s = 1.70158;
+ return function(t) {
+ return t * t * ((s + 1) * t - s);
+ };
+ }
+ function d3_ease_bounce(t) {
+ return t < 1 / 2.75 ? 7.5625 * t * t : t < 2 / 2.75 ? 7.5625 * (t -= 1.5 / 2.75) * t + .75 : t < 2.5 / 2.75 ? 7.5625 * (t -= 2.25 / 2.75) * t + .9375 : 7.5625 * (t -= 2.625 / 2.75) * t + .984375;
+ }
+ d3.interpolateHcl = d3_interpolateHcl;
+ function d3_interpolateHcl(a, b) {
+ a = d3.hcl(a);
+ b = d3.hcl(b);
+ var ah = a.h, ac = a.c, al = a.l, bh = b.h - ah, bc = b.c - ac, bl = b.l - al;
+ if (isNaN(bc)) bc = 0, ac = isNaN(ac) ? b.c : ac;
+ if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360;
+ return function(t) {
+ return d3_hcl_lab(ah + bh * t, ac + bc * t, al + bl * t) + "";
+ };
+ }
+ d3.interpolateHsl = d3_interpolateHsl;
+ function d3_interpolateHsl(a, b) {
+ a = d3.hsl(a);
+ b = d3.hsl(b);
+ var ah = a.h, as = a.s, al = a.l, bh = b.h - ah, bs = b.s - as, bl = b.l - al;
+ if (isNaN(bs)) bs = 0, as = isNaN(as) ? b.s : as;
+ if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360;
+ return function(t) {
+ return d3_hsl_rgb(ah + bh * t, as + bs * t, al + bl * t) + "";
+ };
+ }
+ d3.interpolateLab = d3_interpolateLab;
+ function d3_interpolateLab(a, b) {
+ a = d3.lab(a);
+ b = d3.lab(b);
+ var al = a.l, aa = a.a, ab = a.b, bl = b.l - al, ba = b.a - aa, bb = b.b - ab;
+ return function(t) {
+ return d3_lab_rgb(al + bl * t, aa + ba * t, ab + bb * t) + "";
+ };
+ }
+ d3.interpolateRound = d3_interpolateRound;
+ function d3_interpolateRound(a, b) {
+ b -= a;
+ return function(t) {
+ return Math.round(a + b * t);
+ };
+ }
+ d3.transform = function(string) {
+ var g = d3_document.createElementNS(d3.ns.prefix.svg, "g");
+ return (d3.transform = function(string) {
+ if (string != null) {
+ g.setAttribute("transform", string);
+ var t = g.transform.baseVal.consolidate();
+ }
+ return new d3_transform(t ? t.matrix : d3_transformIdentity);
+ })(string);
+ };
+ function d3_transform(m) {
+ var r0 = [ m.a, m.b ], r1 = [ m.c, m.d ], kx = d3_transformNormalize(r0), kz = d3_transformDot(r0, r1), ky = d3_transformNormalize(d3_transformCombine(r1, r0, -kz)) || 0;
+ if (r0[0] * r1[1] < r1[0] * r0[1]) {
+ r0[0] *= -1;
+ r0[1] *= -1;
+ kx *= -1;
+ kz *= -1;
+ }
+ this.rotate = (kx ? Math.atan2(r0[1], r0[0]) : Math.atan2(-r1[0], r1[1])) * d3_degrees;
+ this.translate = [ m.e, m.f ];
+ this.scale = [ kx, ky ];
+ this.skew = ky ? Math.atan2(kz, ky) * d3_degrees : 0;
+ }
+ d3_transform.prototype.toString = function() {
+ return "translate(" + this.translate + ")rotate(" + this.rotate + ")skewX(" + this.skew + ")scale(" + this.scale + ")";
+ };
+ function d3_transformDot(a, b) {
+ return a[0] * b[0] + a[1] * b[1];
+ }
+ function d3_transformNormalize(a) {
+ var k = Math.sqrt(d3_transformDot(a, a));
+ if (k) {
+ a[0] /= k;
+ a[1] /= k;
+ }
+ return k;
+ }
+ function d3_transformCombine(a, b, k) {
+ a[0] += k * b[0];
+ a[1] += k * b[1];
+ return a;
+ }
+ var d3_transformIdentity = {
+ a: 1,
+ b: 0,
+ c: 0,
+ d: 1,
+ e: 0,
+ f: 0
+ };
+ d3.interpolateTransform = d3_interpolateTransform;
+ function d3_interpolateTransform(a, b) {
+ var s = [], q = [], n, A = d3.transform(a), B = d3.transform(b), ta = A.translate, tb = B.translate, ra = A.rotate, rb = B.rotate, wa = A.skew, wb = B.skew, ka = A.scale, kb = B.scale;
+ if (ta[0] != tb[0] || ta[1] != tb[1]) {
+ s.push("translate(", null, ",", null, ")");
+ q.push({
+ i: 1,
+ x: d3_interpolateNumber(ta[0], tb[0])
+ }, {
+ i: 3,
+ x: d3_interpolateNumber(ta[1], tb[1])
+ });
+ } else if (tb[0] || tb[1]) {
+ s.push("translate(" + tb + ")");
+ } else {
+ s.push("");
+ }
+ if (ra != rb) {
+ if (ra - rb > 180) rb += 360; else if (rb - ra > 180) ra += 360;
+ q.push({
+ i: s.push(s.pop() + "rotate(", null, ")") - 2,
+ x: d3_interpolateNumber(ra, rb)
+ });
+ } else if (rb) {
+ s.push(s.pop() + "rotate(" + rb + ")");
+ }
+ if (wa != wb) {
+ q.push({
+ i: s.push(s.pop() + "skewX(", null, ")") - 2,
+ x: d3_interpolateNumber(wa, wb)
+ });
+ } else if (wb) {
+ s.push(s.pop() + "skewX(" + wb + ")");
+ }
+ if (ka[0] != kb[0] || ka[1] != kb[1]) {
+ n = s.push(s.pop() + "scale(", null, ",", null, ")");
+ q.push({
+ i: n - 4,
+ x: d3_interpolateNumber(ka[0], kb[0])
+ }, {
+ i: n - 2,
+ x: d3_interpolateNumber(ka[1], kb[1])
+ });
+ } else if (kb[0] != 1 || kb[1] != 1) {
+ s.push(s.pop() + "scale(" + kb + ")");
+ }
+ n = q.length;
+ return function(t) {
+ var i = -1, o;
+ while (++i < n) s[(o = q[i]).i] = o.x(t);
+ return s.join("");
+ };
+ }
+ function d3_uninterpolateNumber(a, b) {
+ b = b - (a = +a) ? 1 / (b - a) : 0;
+ return function(x) {
+ return (x - a) * b;
+ };
+ }
+ function d3_uninterpolateClamp(a, b) {
+ b = b - (a = +a) ? 1 / (b - a) : 0;
+ return function(x) {
+ return Math.max(0, Math.min(1, (x - a) * b));
+ };
+ }
+ d3.layout = {};
+ d3.layout.bundle = function() {
+ return function(links) {
+ var paths = [], i = -1, n = links.length;
+ while (++i < n) paths.push(d3_layout_bundlePath(links[i]));
+ return paths;
+ };
+ };
+ function d3_layout_bundlePath(link) {
+ var start = link.source, end = link.target, lca = d3_layout_bundleLeastCommonAncestor(start, end), points = [ start ];
+ while (start !== lca) {
+ start = start.parent;
+ points.push(start);
+ }
+ var k = points.length;
+ while (end !== lca) {
+ points.splice(k, 0, end);
+ end = end.parent;
+ }
+ return points;
+ }
+ function d3_layout_bundleAncestors(node) {
+ var ancestors = [], parent = node.parent;
+ while (parent != null) {
+ ancestors.push(node);
+ node = parent;
+ parent = parent.parent;
+ }
+ ancestors.push(node);
+ return ancestors;
+ }
+ function d3_layout_bundleLeastCommonAncestor(a, b) {
+ if (a === b) return a;
+ var aNodes = d3_layout_bundleAncestors(a), bNodes = d3_layout_bundleAncestors(b), aNode = aNodes.pop(), bNode = bNodes.pop(), sharedNode = null;
+ while (aNode === bNode) {
+ sharedNode = aNode;
+ aNode = aNodes.pop();
+ bNode = bNodes.pop();
+ }
+ return sharedNode;
+ }
+ d3.layout.chord = function() {
+ var chord = {}, chords, groups, matrix, n, padding = 0, sortGroups, sortSubgroups, sortChords;
+ function relayout() {
+ var subgroups = {}, groupSums = [], groupIndex = d3.range(n), subgroupIndex = [], k, x, x0, i, j;
+ chords = [];
+ groups = [];
+ k = 0, i = -1;
+ while (++i < n) {
+ x = 0, j = -1;
+ while (++j < n) {
+ x += matrix[i][j];
+ }
+ groupSums.push(x);
+ subgroupIndex.push(d3.range(n));
+ k += x;
+ }
+ if (sortGroups) {
+ groupIndex.sort(function(a, b) {
+ return sortGroups(groupSums[a], groupSums[b]);
+ });
+ }
+ if (sortSubgroups) {
+ subgroupIndex.forEach(function(d, i) {
+ d.sort(function(a, b) {
+ return sortSubgroups(matrix[i][a], matrix[i][b]);
+ });
+ });
+ }
+ k = (τ - padding * n) / k;
+ x = 0, i = -1;
+ while (++i < n) {
+ x0 = x, j = -1;
+ while (++j < n) {
+ var di = groupIndex[i], dj = subgroupIndex[di][j], v = matrix[di][dj], a0 = x, a1 = x += v * k;
+ subgroups[di + "-" + dj] = {
+ index: di,
+ subindex: dj,
+ startAngle: a0,
+ endAngle: a1,
+ value: v
+ };
+ }
+ groups[di] = {
+ index: di,
+ startAngle: x0,
+ endAngle: x,
+ value: (x - x0) / k
+ };
+ x += padding;
+ }
+ i = -1;
+ while (++i < n) {
+ j = i - 1;
+ while (++j < n) {
+ var source = subgroups[i + "-" + j], target = subgroups[j + "-" + i];
+ if (source.value || target.value) {
+ chords.push(source.value < target.value ? {
+ source: target,
+ target: source
+ } : {
+ source: source,
+ target: target
+ });
+ }
+ }
+ }
+ if (sortChords) resort();
+ }
+ function resort() {
+ chords.sort(function(a, b) {
+ return sortChords((a.source.value + a.target.value) / 2, (b.source.value + b.target.value) / 2);
+ });
+ }
+ chord.matrix = function(x) {
+ if (!arguments.length) return matrix;
+ n = (matrix = x) && matrix.length;
+ chords = groups = null;
+ return chord;
+ };
+ chord.padding = function(x) {
+ if (!arguments.length) return padding;
+ padding = x;
+ chords = groups = null;
+ return chord;
+ };
+ chord.sortGroups = function(x) {
+ if (!arguments.length) return sortGroups;
+ sortGroups = x;
+ chords = groups = null;
+ return chord;
+ };
+ chord.sortSubgroups = function(x) {
+ if (!arguments.length) return sortSubgroups;
+ sortSubgroups = x;
+ chords = null;
+ return chord;
+ };
+ chord.sortChords = function(x) {
+ if (!arguments.length) return sortChords;
+ sortChords = x;
+ if (chords) resort();
+ return chord;
+ };
+ chord.chords = function() {
+ if (!chords) relayout();
+ return chords;
+ };
+ chord.groups = function() {
+ if (!groups) relayout();
+ return groups;
+ };
+ return chord;
+ };
+ d3.layout.force = function() {
+ var force = {}, event = d3.dispatch("start", "tick", "end"), size = [ 1, 1 ], drag, alpha, friction = .9, linkDistance = d3_layout_forceLinkDistance, linkStrength = d3_layout_forceLinkStrength, charge = -30, chargeDistance2 = d3_layout_forceChargeDistance2, gravity = .1, theta2 = .64, nodes = [], links = [], distances, strengths, charges;
+ function repulse(node) {
+ return function(quad, x1, _, x2) {
+ if (quad.point !== node) {
+ var dx = quad.cx - node.x, dy = quad.cy - node.y, dw = x2 - x1, dn = dx * dx + dy * dy;
+ if (dw * dw / theta2 < dn) {
+ if (dn < chargeDistance2) {
+ var k = quad.charge / dn;
+ node.px -= dx * k;
+ node.py -= dy * k;
+ }
+ return true;
+ }
+ if (quad.point && dn && dn < chargeDistance2) {
+ var k = quad.pointCharge / dn;
+ node.px -= dx * k;
+ node.py -= dy * k;
+ }
+ }
+ return !quad.charge;
+ };
+ }
+ force.tick = function() {
+ if ((alpha *= .99) < .005) {
+ event.end({
+ type: "end",
+ alpha: alpha = 0
+ });
+ return true;
+ }
+ var n = nodes.length, m = links.length, q, i, o, s, t, l, k, x, y;
+ for (i = 0; i < m; ++i) {
+ o = links[i];
+ s = o.source;
+ t = o.target;
+ x = t.x - s.x;
+ y = t.y - s.y;
+ if (l = x * x + y * y) {
+ l = alpha * strengths[i] * ((l = Math.sqrt(l)) - distances[i]) / l;
+ x *= l;
+ y *= l;
+ t.x -= x * (k = s.weight / (t.weight + s.weight));
+ t.y -= y * k;
+ s.x += x * (k = 1 - k);
+ s.y += y * k;
+ }
+ }
+ if (k = alpha * gravity) {
+ x = size[0] / 2;
+ y = size[1] / 2;
+ i = -1;
+ if (k) while (++i < n) {
+ o = nodes[i];
+ o.x += (x - o.x) * k;
+ o.y += (y - o.y) * k;
+ }
+ }
+ if (charge) {
+ d3_layout_forceAccumulate(q = d3.geom.quadtree(nodes), alpha, charges);
+ i = -1;
+ while (++i < n) {
+ if (!(o = nodes[i]).fixed) {
+ q.visit(repulse(o));
+ }
+ }
+ }
+ i = -1;
+ while (++i < n) {
+ o = nodes[i];
+ if (o.fixed) {
+ o.x = o.px;
+ o.y = o.py;
+ } else {
+ o.x -= (o.px - (o.px = o.x)) * friction;
+ o.y -= (o.py - (o.py = o.y)) * friction;
+ }
+ }
+ event.tick({
+ type: "tick",
+ alpha: alpha
+ });
+ };
+ force.nodes = function(x) {
+ if (!arguments.length) return nodes;
+ nodes = x;
+ return force;
+ };
+ force.links = function(x) {
+ if (!arguments.length) return links;
+ links = x;
+ return force;
+ };
+ force.size = function(x) {
+ if (!arguments.length) return size;
+ size = x;
+ return force;
+ };
+ force.linkDistance = function(x) {
+ if (!arguments.length) return linkDistance;
+ linkDistance = typeof x === "function" ? x : +x;
+ return force;
+ };
+ force.distance = force.linkDistance;
+ force.linkStrength = function(x) {
+ if (!arguments.length) return linkStrength;
+ linkStrength = typeof x === "function" ? x : +x;
+ return force;
+ };
+ force.friction = function(x) {
+ if (!arguments.length) return friction;
+ friction = +x;
+ return force;
+ };
+ force.charge = function(x) {
+ if (!arguments.length) return charge;
+ charge = typeof x === "function" ? x : +x;
+ return force;
+ };
+ force.chargeDistance = function(x) {
+ if (!arguments.length) return Math.sqrt(chargeDistance2);
+ chargeDistance2 = x * x;
+ return force;
+ };
+ force.gravity = function(x) {
+ if (!arguments.length) return gravity;
+ gravity = +x;
+ return force;
+ };
+ force.theta = function(x) {
+ if (!arguments.length) return Math.sqrt(theta2);
+ theta2 = x * x;
+ return force;
+ };
+ force.alpha = function(x) {
+ if (!arguments.length) return alpha;
+ x = +x;
+ if (alpha) {
+ if (x > 0) alpha = x; else alpha = 0;
+ } else if (x > 0) {
+ event.start({
+ type: "start",
+ alpha: alpha = x
+ });
+ d3.timer(force.tick);
+ }
+ return force;
+ };
+ force.start = function() {
+ var i, n = nodes.length, m = links.length, w = size[0], h = size[1], neighbors, o;
+ for (i = 0; i < n; ++i) {
+ (o = nodes[i]).index = i;
+ o.weight = 0;
+ }
+ for (i = 0; i < m; ++i) {
+ o = links[i];
+ if (typeof o.source == "number") o.source = nodes[o.source];
+ if (typeof o.target == "number") o.target = nodes[o.target];
+ ++o.source.weight;
+ ++o.target.weight;
+ }
+ for (i = 0; i < n; ++i) {
+ o = nodes[i];
+ if (isNaN(o.x)) o.x = position("x", w);
+ if (isNaN(o.y)) o.y = position("y", h);
+ if (isNaN(o.px)) o.px = o.x;
+ if (isNaN(o.py)) o.py = o.y;
+ }
+ distances = [];
+ if (typeof linkDistance === "function") for (i = 0; i < m; ++i) distances[i] = +linkDistance.call(this, links[i], i); else for (i = 0; i < m; ++i) distances[i] = linkDistance;
+ strengths = [];
+ if (typeof linkStrength === "function") for (i = 0; i < m; ++i) strengths[i] = +linkStrength.call(this, links[i], i); else for (i = 0; i < m; ++i) strengths[i] = linkStrength;
+ charges = [];
+ if (typeof charge === "function") for (i = 0; i < n; ++i) charges[i] = +charge.call(this, nodes[i], i); else for (i = 0; i < n; ++i) charges[i] = charge;
+ function position(dimension, size) {
+ if (!neighbors) {
+ neighbors = new Array(n);
+ for (j = 0; j < n; ++j) {
+ neighbors[j] = [];
+ }
+ for (j = 0; j < m; ++j) {
+ var o = links[j];
+ neighbors[o.source.index].push(o.target);
+ neighbors[o.target.index].push(o.source);
+ }
+ }
+ var candidates = neighbors[i], j = -1, m = candidates.length, x;
+ while (++j < m) if (!isNaN(x = candidates[j][dimension])) return x;
+ return Math.random() * size;
+ }
+ return force.resume();
+ };
+ force.resume = function() {
+ return force.alpha(.1);
+ };
+ force.stop = function() {
+ return force.alpha(0);
+ };
+ force.drag = function() {
+ if (!drag) drag = d3.behavior.drag().origin(d3_identity).on("dragstart.force", d3_layout_forceDragstart).on("drag.force", dragmove).on("dragend.force", d3_layout_forceDragend);
+ if (!arguments.length) return drag;
+ this.on("mouseover.force", d3_layout_forceMouseover).on("mouseout.force", d3_layout_forceMouseout).call(drag);
+ };
+ function dragmove(d) {
+ d.px = d3.event.x, d.py = d3.event.y;
+ force.resume();
+ }
+ return d3.rebind(force, event, "on");
+ };
+ function d3_layout_forceDragstart(d) {
+ d.fixed |= 2;
+ }
+ function d3_layout_forceDragend(d) {
+ d.fixed &= ~6;
+ }
+ function d3_layout_forceMouseover(d) {
+ d.fixed |= 4;
+ d.px = d.x, d.py = d.y;
+ }
+ function d3_layout_forceMouseout(d) {
+ d.fixed &= ~4;
+ }
+ function d3_layout_forceAccumulate(quad, alpha, charges) {
+ var cx = 0, cy = 0;
+ quad.charge = 0;
+ if (!quad.leaf) {
+ var nodes = quad.nodes, n = nodes.length, i = -1, c;
+ while (++i < n) {
+ c = nodes[i];
+ if (c == null) continue;
+ d3_layout_forceAccumulate(c, alpha, charges);
+ quad.charge += c.charge;
+ cx += c.charge * c.cx;
+ cy += c.charge * c.cy;
+ }
+ }
+ if (quad.point) {
+ if (!quad.leaf) {
+ quad.point.x += Math.random() - .5;
+ quad.point.y += Math.random() - .5;
+ }
+ var k = alpha * charges[quad.point.index];
+ quad.charge += quad.pointCharge = k;
+ cx += k * quad.point.x;
+ cy += k * quad.point.y;
+ }
+ quad.cx = cx / quad.charge;
+ quad.cy = cy / quad.charge;
+ }
+ var d3_layout_forceLinkDistance = 20, d3_layout_forceLinkStrength = 1, d3_layout_forceChargeDistance2 = Infinity;
+ d3.layout.hierarchy = function() {
+ var sort = d3_layout_hierarchySort, children = d3_layout_hierarchyChildren, value = d3_layout_hierarchyValue;
+ function recurse(node, depth, nodes) {
+ var childs = children.call(hierarchy, node, depth);
+ node.depth = depth;
+ nodes.push(node);
+ if (childs && (n = childs.length)) {
+ var i = -1, n, c = node.children = new Array(n), v = 0, j = depth + 1, d;
+ while (++i < n) {
+ d = c[i] = recurse(childs[i], j, nodes);
+ d.parent = node;
+ v += d.value;
+ }
+ if (sort) c.sort(sort);
+ if (value) node.value = v;
+ } else {
+ delete node.children;
+ if (value) {
+ node.value = +value.call(hierarchy, node, depth) || 0;
+ }
+ }
+ return node;
+ }
+ function revalue(node, depth) {
+ var children = node.children, v = 0;
+ if (children && (n = children.length)) {
+ var i = -1, n, j = depth + 1;
+ while (++i < n) v += revalue(children[i], j);
+ } else if (value) {
+ v = +value.call(hierarchy, node, depth) || 0;
+ }
+ if (value) node.value = v;
+ return v;
+ }
+ function hierarchy(d) {
+ var nodes = [];
+ recurse(d, 0, nodes);
+ return nodes;
+ }
+ hierarchy.sort = function(x) {
+ if (!arguments.length) return sort;
+ sort = x;
+ return hierarchy;
+ };
+ hierarchy.children = function(x) {
+ if (!arguments.length) return children;
+ children = x;
+ return hierarchy;
+ };
+ hierarchy.value = function(x) {
+ if (!arguments.length) return value;
+ value = x;
+ return hierarchy;
+ };
+ hierarchy.revalue = function(root) {
+ revalue(root, 0);
+ return root;
+ };
+ return hierarchy;
+ };
+ function d3_layout_hierarchyRebind(object, hierarchy) {
+ d3.rebind(object, hierarchy, "sort", "children", "value");
+ object.nodes = object;
+ object.links = d3_layout_hierarchyLinks;
+ return object;
+ }
+ function d3_layout_hierarchyChildren(d) {
+ return d.children;
+ }
+ function d3_layout_hierarchyValue(d) {
+ return d.value;
+ }
+ function d3_layout_hierarchySort(a, b) {
+ return b.value - a.value;
+ }
+ function d3_layout_hierarchyLinks(nodes) {
+ return d3.merge(nodes.map(function(parent) {
+ return (parent.children || []).map(function(child) {
+ return {
+ source: parent,
+ target: child
+ };
+ });
+ }));
+ }
+ d3.layout.partition = function() {
+ var hierarchy = d3.layout.hierarchy(), size = [ 1, 1 ];
+ function position(node, x, dx, dy) {
+ var children = node.children;
+ node.x = x;
+ node.y = node.depth * dy;
+ node.dx = dx;
+ node.dy = dy;
+ if (children && (n = children.length)) {
+ var i = -1, n, c, d;
+ dx = node.value ? dx / node.value : 0;
+ while (++i < n) {
+ position(c = children[i], x, d = c.value * dx, dy);
+ x += d;
+ }
+ }
+ }
+ function depth(node) {
+ var children = node.children, d = 0;
+ if (children && (n = children.length)) {
+ var i = -1, n;
+ while (++i < n) d = Math.max(d, depth(children[i]));
+ }
+ return 1 + d;
+ }
+ function partition(d, i) {
+ var nodes = hierarchy.call(this, d, i);
+ position(nodes[0], 0, size[0], size[1] / depth(nodes[0]));
+ return nodes;
+ }
+ partition.size = function(x) {
+ if (!arguments.length) return size;
+ size = x;
+ return partition;
+ };
+ return d3_layout_hierarchyRebind(partition, hierarchy);
+ };
+ d3.layout.pie = function() {
+ var value = Number, sort = d3_layout_pieSortByValue, startAngle = 0, endAngle = τ;
+ function pie(data) {
+ var values = data.map(function(d, i) {
+ return +value.call(pie, d, i);
+ });
+ var a = +(typeof startAngle === "function" ? startAngle.apply(this, arguments) : startAngle);
+ var k = ((typeof endAngle === "function" ? endAngle.apply(this, arguments) : endAngle) - a) / d3.sum(values);
+ var index = d3.range(data.length);
+ if (sort != null) index.sort(sort === d3_layout_pieSortByValue ? function(i, j) {
+ return values[j] - values[i];
+ } : function(i, j) {
+ return sort(data[i], data[j]);
+ });
+ var arcs = [];
+ index.forEach(function(i) {
+ var d;
+ arcs[i] = {
+ data: data[i],
+ value: d = values[i],
+ startAngle: a,
+ endAngle: a += d * k
+ };
+ });
+ return arcs;
+ }
+ pie.value = function(x) {
+ if (!arguments.length) return value;
+ value = x;
+ return pie;
+ };
+ pie.sort = function(x) {
+ if (!arguments.length) return sort;
+ sort = x;
+ return pie;
+ };
+ pie.startAngle = function(x) {
+ if (!arguments.length) return startAngle;
+ startAngle = x;
+ return pie;
+ };
+ pie.endAngle = function(x) {
+ if (!arguments.length) return endAngle;
+ endAngle = x;
+ return pie;
+ };
+ return pie;
+ };
+ var d3_layout_pieSortByValue = {};
+ d3.layout.stack = function() {
+ var values = d3_identity, order = d3_layout_stackOrderDefault, offset = d3_layout_stackOffsetZero, out = d3_layout_stackOut, x = d3_layout_stackX, y = d3_layout_stackY;
+ function stack(data, index) {
+ var series = data.map(function(d, i) {
+ return values.call(stack, d, i);
+ });
+ var points = series.map(function(d) {
+ return d.map(function(v, i) {
+ return [ x.call(stack, v, i), y.call(stack, v, i) ];
+ });
+ });
+ var orders = order.call(stack, points, index);
+ series = d3.permute(series, orders);
+ points = d3.permute(points, orders);
+ var offsets = offset.call(stack, points, index);
+ var n = series.length, m = series[0].length, i, j, o;
+ for (j = 0; j < m; ++j) {
+ out.call(stack, series[0][j], o = offsets[j], points[0][j][1]);
+ for (i = 1; i < n; ++i) {
+ out.call(stack, series[i][j], o += points[i - 1][j][1], points[i][j][1]);
+ }
+ }
+ return data;
+ }
+ stack.values = function(x) {
+ if (!arguments.length) return values;
+ values = x;
+ return stack;
+ };
+ stack.order = function(x) {
+ if (!arguments.length) return order;
+ order = typeof x === "function" ? x : d3_layout_stackOrders.get(x) || d3_layout_stackOrderDefault;
+ return stack;
+ };
+ stack.offset = function(x) {
+ if (!arguments.length) return offset;
+ offset = typeof x === "function" ? x : d3_layout_stackOffsets.get(x) || d3_layout_stackOffsetZero;
+ return stack;
+ };
+ stack.x = function(z) {
+ if (!arguments.length) return x;
+ x = z;
+ return stack;
+ };
+ stack.y = function(z) {
+ if (!arguments.length) return y;
+ y = z;
+ return stack;
+ };
+ stack.out = function(z) {
+ if (!arguments.length) return out;
+ out = z;
+ return stack;
+ };
+ return stack;
+ };
+ function d3_layout_stackX(d) {
+ return d.x;
+ }
+ function d3_layout_stackY(d) {
+ return d.y;
+ }
+ function d3_layout_stackOut(d, y0, y) {
+ d.y0 = y0;
+ d.y = y;
+ }
+ var d3_layout_stackOrders = d3.map({
+ "inside-out": function(data) {
+ var n = data.length, i, j, max = data.map(d3_layout_stackMaxIndex), sums = data.map(d3_layout_stackReduceSum), index = d3.range(n).sort(function(a, b) {
+ return max[a] - max[b];
+ }), top = 0, bottom = 0, tops = [], bottoms = [];
+ for (i = 0; i < n; ++i) {
+ j = index[i];
+ if (top < bottom) {
+ top += sums[j];
+ tops.push(j);
+ } else {
+ bottom += sums[j];
+ bottoms.push(j);
+ }
+ }
+ return bottoms.reverse().concat(tops);
+ },
+ reverse: function(data) {
+ return d3.range(data.length).reverse();
+ },
+ "default": d3_layout_stackOrderDefault
+ });
+ var d3_layout_stackOffsets = d3.map({
+ silhouette: function(data) {
+ var n = data.length, m = data[0].length, sums = [], max = 0, i, j, o, y0 = [];
+ for (j = 0; j < m; ++j) {
+ for (i = 0, o = 0; i < n; i++) o += data[i][j][1];
+ if (o > max) max = o;
+ sums.push(o);
+ }
+ for (j = 0; j < m; ++j) {
+ y0[j] = (max - sums[j]) / 2;
+ }
+ return y0;
+ },
+ wiggle: function(data) {
+ var n = data.length, x = data[0], m = x.length, i, j, k, s1, s2, s3, dx, o, o0, y0 = [];
+ y0[0] = o = o0 = 0;
+ for (j = 1; j < m; ++j) {
+ for (i = 0, s1 = 0; i < n; ++i) s1 += data[i][j][1];
+ for (i = 0, s2 = 0, dx = x[j][0] - x[j - 1][0]; i < n; ++i) {
+ for (k = 0, s3 = (data[i][j][1] - data[i][j - 1][1]) / (2 * dx); k < i; ++k) {
+ s3 += (data[k][j][1] - data[k][j - 1][1]) / dx;
+ }
+ s2 += s3 * data[i][j][1];
+ }
+ y0[j] = o -= s1 ? s2 / s1 * dx : 0;
+ if (o < o0) o0 = o;
+ }
+ for (j = 0; j < m; ++j) y0[j] -= o0;
+ return y0;
+ },
+ expand: function(data) {
+ var n = data.length, m = data[0].length, k = 1 / n, i, j, o, y0 = [];
+ for (j = 0; j < m; ++j) {
+ for (i = 0, o = 0; i < n; i++) o += data[i][j][1];
+ if (o) for (i = 0; i < n; i++) data[i][j][1] /= o; else for (i = 0; i < n; i++) data[i][j][1] = k;
+ }
+ for (j = 0; j < m; ++j) y0[j] = 0;
+ return y0;
+ },
+ zero: d3_layout_stackOffsetZero
+ });
+ function d3_layout_stackOrderDefault(data) {
+ return d3.range(data.length);
+ }
+ function d3_layout_stackOffsetZero(data) {
+ var j = -1, m = data[0].length, y0 = [];
+ while (++j < m) y0[j] = 0;
+ return y0;
+ }
+ function d3_layout_stackMaxIndex(array) {
+ var i = 1, j = 0, v = array[0][1], k, n = array.length;
+ for (;i < n; ++i) {
+ if ((k = array[i][1]) > v) {
+ j = i;
+ v = k;
+ }
+ }
+ return j;
+ }
+ function d3_layout_stackReduceSum(d) {
+ return d.reduce(d3_layout_stackSum, 0);
+ }
+ function d3_layout_stackSum(p, d) {
+ return p + d[1];
+ }
+ d3.layout.histogram = function() {
+ var frequency = true, valuer = Number, ranger = d3_layout_histogramRange, binner = d3_layout_histogramBinSturges;
+ function histogram(data, i) {
+ var bins = [], values = data.map(valuer, this), range = ranger.call(this, values, i), thresholds = binner.call(this, range, values, i), bin, i = -1, n = values.length, m = thresholds.length - 1, k = frequency ? 1 : 1 / n, x;
+ while (++i < m) {
+ bin = bins[i] = [];
+ bin.dx = thresholds[i + 1] - (bin.x = thresholds[i]);
+ bin.y = 0;
+ }
+ if (m > 0) {
+ i = -1;
+ while (++i < n) {
+ x = values[i];
+ if (x >= range[0] && x <= range[1]) {
+ bin = bins[d3.bisect(thresholds, x, 1, m) - 1];
+ bin.y += k;
+ bin.push(data[i]);
+ }
+ }
+ }
+ return bins;
+ }
+ histogram.value = function(x) {
+ if (!arguments.length) return valuer;
+ valuer = x;
+ return histogram;
+ };
+ histogram.range = function(x) {
+ if (!arguments.length) return ranger;
+ ranger = d3_functor(x);
+ return histogram;
+ };
+ histogram.bins = function(x) {
+ if (!arguments.length) return binner;
+ binner = typeof x === "number" ? function(range) {
+ return d3_layout_histogramBinFixed(range, x);
+ } : d3_functor(x);
+ return histogram;
+ };
+ histogram.frequency = function(x) {
+ if (!arguments.length) return frequency;
+ frequency = !!x;
+ return histogram;
+ };
+ return histogram;
+ };
+ function d3_layout_histogramBinSturges(range, values) {
+ return d3_layout_histogramBinFixed(range, Math.ceil(Math.log(values.length) / Math.LN2 + 1));
+ }
+ function d3_layout_histogramBinFixed(range, n) {
+ var x = -1, b = +range[0], m = (range[1] - b) / n, f = [];
+ while (++x <= n) f[x] = m * x + b;
+ return f;
+ }
+ function d3_layout_histogramRange(values) {
+ return [ d3.min(values), d3.max(values) ];
+ }
+ d3.layout.tree = function() {
+ var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = false;
+ function tree(d, i) {
+ var nodes = hierarchy.call(this, d, i), root = nodes[0];
+ function firstWalk(node, previousSibling) {
+ var children = node.children, layout = node._tree;
+ if (children && (n = children.length)) {
+ var n, firstChild = children[0], previousChild, ancestor = firstChild, child, i = -1;
+ while (++i < n) {
+ child = children[i];
+ firstWalk(child, previousChild);
+ ancestor = apportion(child, previousChild, ancestor);
+ previousChild = child;
+ }
+ d3_layout_treeShift(node);
+ var midpoint = .5 * (firstChild._tree.prelim + child._tree.prelim);
+ if (previousSibling) {
+ layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling);
+ layout.mod = layout.prelim - midpoint;
+ } else {
+ layout.prelim = midpoint;
+ }
+ } else {
+ if (previousSibling) {
+ layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling);
+ }
+ }
+ }
+ function secondWalk(node, x) {
+ node.x = node._tree.prelim + x;
+ var children = node.children;
+ if (children && (n = children.length)) {
+ var i = -1, n;
+ x += node._tree.mod;
+ while (++i < n) {
+ secondWalk(children[i], x);
+ }
+ }
+ }
+ function apportion(node, previousSibling, ancestor) {
+ if (previousSibling) {
+ var vip = node, vop = node, vim = previousSibling, vom = node.parent.children[0], sip = vip._tree.mod, sop = vop._tree.mod, sim = vim._tree.mod, som = vom._tree.mod, shift;
+ while (vim = d3_layout_treeRight(vim), vip = d3_layout_treeLeft(vip), vim && vip) {
+ vom = d3_layout_treeLeft(vom);
+ vop = d3_layout_treeRight(vop);
+ vop._tree.ancestor = node;
+ shift = vim._tree.prelim + sim - vip._tree.prelim - sip + separation(vim, vip);
+ if (shift > 0) {
+ d3_layout_treeMove(d3_layout_treeAncestor(vim, node, ancestor), node, shift);
+ sip += shift;
+ sop += shift;
+ }
+ sim += vim._tree.mod;
+ sip += vip._tree.mod;
+ som += vom._tree.mod;
+ sop += vop._tree.mod;
+ }
+ if (vim && !d3_layout_treeRight(vop)) {
+ vop._tree.thread = vim;
+ vop._tree.mod += sim - sop;
+ }
+ if (vip && !d3_layout_treeLeft(vom)) {
+ vom._tree.thread = vip;
+ vom._tree.mod += sip - som;
+ ancestor = node;
+ }
+ }
+ return ancestor;
+ }
+ d3_layout_treeVisitAfter(root, function(node, previousSibling) {
+ node._tree = {
+ ancestor: node,
+ prelim: 0,
+ mod: 0,
+ change: 0,
+ shift: 0,
+ number: previousSibling ? previousSibling._tree.number + 1 : 0
+ };
+ });
+ firstWalk(root);
+ secondWalk(root, -root._tree.prelim);
+ var left = d3_layout_treeSearch(root, d3_layout_treeLeftmost), right = d3_layout_treeSearch(root, d3_layout_treeRightmost), deep = d3_layout_treeSearch(root, d3_layout_treeDeepest), x0 = left.x - separation(left, right) / 2, x1 = right.x + separation(right, left) / 2, y1 = deep.depth || 1;
+ d3_layout_treeVisitAfter(root, nodeSize ? function(node) {
+ node.x *= size[0];
+ node.y = node.depth * size[1];
+ delete node._tree;
+ } : function(node) {
+ node.x = (node.x - x0) / (x1 - x0) * size[0];
+ node.y = node.depth / y1 * size[1];
+ delete node._tree;
+ });
+ return nodes;
+ }
+ tree.separation = function(x) {
+ if (!arguments.length) return separation;
+ separation = x;
+ return tree;
+ };
+ tree.size = function(x) {
+ if (!arguments.length) return nodeSize ? null : size;
+ nodeSize = (size = x) == null;
+ return tree;
+ };
+ tree.nodeSize = function(x) {
+ if (!arguments.length) return nodeSize ? size : null;
+ nodeSize = (size = x) != null;
+ return tree;
+ };
+ return d3_layout_hierarchyRebind(tree, hierarchy);
+ };
+ function d3_layout_treeSeparation(a, b) {
+ return a.parent == b.parent ? 1 : 2;
+ }
+ function d3_layout_treeLeft(node) {
+ var children = node.children;
+ return children && children.length ? children[0] : node._tree.thread;
+ }
+ function d3_layout_treeRight(node) {
+ var children = node.children, n;
+ return children && (n = children.length) ? children[n - 1] : node._tree.thread;
+ }
+ function d3_layout_treeSearch(node, compare) {
+ var children = node.children;
+ if (children && (n = children.length)) {
+ var child, n, i = -1;
+ while (++i < n) {
+ if (compare(child = d3_layout_treeSearch(children[i], compare), node) > 0) {
+ node = child;
+ }
+ }
+ }
+ return node;
+ }
+ function d3_layout_treeRightmost(a, b) {
+ return a.x - b.x;
+ }
+ function d3_layout_treeLeftmost(a, b) {
+ return b.x - a.x;
+ }
+ function d3_layout_treeDeepest(a, b) {
+ return a.depth - b.depth;
+ }
+ function d3_layout_treeVisitAfter(node, callback) {
+ function visit(node, previousSibling) {
+ var children = node.children;
+ if (children && (n = children.length)) {
+ var child, previousChild = null, i = -1, n;
+ while (++i < n) {
+ child = children[i];
+ visit(child, previousChild);
+ previousChild = child;
+ }
+ }
+ callback(node, previousSibling);
+ }
+ visit(node, null);
+ }
+ function d3_layout_treeShift(node) {
+ var shift = 0, change = 0, children = node.children, i = children.length, child;
+ while (--i >= 0) {
+ child = children[i]._tree;
+ child.prelim += shift;
+ child.mod += shift;
+ shift += child.shift + (change += child.change);
+ }
+ }
+ function d3_layout_treeMove(ancestor, node, shift) {
+ ancestor = ancestor._tree;
+ node = node._tree;
+ var change = shift / (node.number - ancestor.number);
+ ancestor.change += change;
+ node.change -= change;
+ node.shift += shift;
+ node.prelim += shift;
+ node.mod += shift;
+ }
+ function d3_layout_treeAncestor(vim, node, ancestor) {
+ return vim._tree.ancestor.parent == node.parent ? vim._tree.ancestor : ancestor;
+ }
+ d3.layout.pack = function() {
+ var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), padding = 0, size = [ 1, 1 ], radius;
+ function pack(d, i) {
+ var nodes = hierarchy.call(this, d, i), root = nodes[0], w = size[0], h = size[1], r = radius == null ? Math.sqrt : typeof radius === "function" ? radius : function() {
+ return radius;
+ };
+ root.x = root.y = 0;
+ d3_layout_treeVisitAfter(root, function(d) {
+ d.r = +r(d.value);
+ });
+ d3_layout_treeVisitAfter(root, d3_layout_packSiblings);
+ if (padding) {
+ var dr = padding * (radius ? 1 : Math.max(2 * root.r / w, 2 * root.r / h)) / 2;
+ d3_layout_treeVisitAfter(root, function(d) {
+ d.r += dr;
+ });
+ d3_layout_treeVisitAfter(root, d3_layout_packSiblings);
+ d3_layout_treeVisitAfter(root, function(d) {
+ d.r -= dr;
+ });
+ }
+ d3_layout_packTransform(root, w / 2, h / 2, radius ? 1 : 1 / Math.max(2 * root.r / w, 2 * root.r / h));
+ return nodes;
+ }
+ pack.size = function(_) {
+ if (!arguments.length) return size;
+ size = _;
+ return pack;
+ };
+ pack.radius = function(_) {
+ if (!arguments.length) return radius;
+ radius = _ == null || typeof _ === "function" ? _ : +_;
+ return pack;
+ };
+ pack.padding = function(_) {
+ if (!arguments.length) return padding;
+ padding = +_;
+ return pack;
+ };
+ return d3_layout_hierarchyRebind(pack, hierarchy);
+ };
+ function d3_layout_packSort(a, b) {
+ return a.value - b.value;
+ }
+ function d3_layout_packInsert(a, b) {
+ var c = a._pack_next;
+ a._pack_next = b;
+ b._pack_prev = a;
+ b._pack_next = c;
+ c._pack_prev = b;
+ }
+ function d3_layout_packSplice(a, b) {
+ a._pack_next = b;
+ b._pack_prev = a;
+ }
+ function d3_layout_packIntersects(a, b) {
+ var dx = b.x - a.x, dy = b.y - a.y, dr = a.r + b.r;
+ return .999 * dr * dr > dx * dx + dy * dy;
+ }
+ function d3_layout_packSiblings(node) {
+ if (!(nodes = node.children) || !(n = nodes.length)) return;
+ var nodes, xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity, a, b, c, i, j, k, n;
+ function bound(node) {
+ xMin = Math.min(node.x - node.r, xMin);
+ xMax = Math.max(node.x + node.r, xMax);
+ yMin = Math.min(node.y - node.r, yMin);
+ yMax = Math.max(node.y + node.r, yMax);
+ }
+ nodes.forEach(d3_layout_packLink);
+ a = nodes[0];
+ a.x = -a.r;
+ a.y = 0;
+ bound(a);
+ if (n > 1) {
+ b = nodes[1];
+ b.x = b.r;
+ b.y = 0;
+ bound(b);
+ if (n > 2) {
+ c = nodes[2];
+ d3_layout_packPlace(a, b, c);
+ bound(c);
+ d3_layout_packInsert(a, c);
+ a._pack_prev = c;
+ d3_layout_packInsert(c, b);
+ b = a._pack_next;
+ for (i = 3; i < n; i++) {
+ d3_layout_packPlace(a, b, c = nodes[i]);
+ var isect = 0, s1 = 1, s2 = 1;
+ for (j = b._pack_next; j !== b; j = j._pack_next, s1++) {
+ if (d3_layout_packIntersects(j, c)) {
+ isect = 1;
+ break;
+ }
+ }
+ if (isect == 1) {
+ for (k = a._pack_prev; k !== j._pack_prev; k = k._pack_prev, s2++) {
+ if (d3_layout_packIntersects(k, c)) {
+ break;
+ }
+ }
+ }
+ if (isect) {
+ if (s1 < s2 || s1 == s2 && b.r < a.r) d3_layout_packSplice(a, b = j); else d3_layout_packSplice(a = k, b);
+ i--;
+ } else {
+ d3_layout_packInsert(a, c);
+ b = c;
+ bound(c);
+ }
+ }
+ }
+ }
+ var cx = (xMin + xMax) / 2, cy = (yMin + yMax) / 2, cr = 0;
+ for (i = 0; i < n; i++) {
+ c = nodes[i];
+ c.x -= cx;
+ c.y -= cy;
+ cr = Math.max(cr, c.r + Math.sqrt(c.x * c.x + c.y * c.y));
+ }
+ node.r = cr;
+ nodes.forEach(d3_layout_packUnlink);
+ }
+ function d3_layout_packLink(node) {
+ node._pack_next = node._pack_prev = node;
+ }
+ function d3_layout_packUnlink(node) {
+ delete node._pack_next;
+ delete node._pack_prev;
+ }
+ function d3_layout_packTransform(node, x, y, k) {
+ var children = node.children;
+ node.x = x += k * node.x;
+ node.y = y += k * node.y;
+ node.r *= k;
+ if (children) {
+ var i = -1, n = children.length;
+ while (++i < n) d3_layout_packTransform(children[i], x, y, k);
+ }
+ }
+ function d3_layout_packPlace(a, b, c) {
+ var db = a.r + c.r, dx = b.x - a.x, dy = b.y - a.y;
+ if (db && (dx || dy)) {
+ var da = b.r + c.r, dc = dx * dx + dy * dy;
+ da *= da;
+ db *= db;
+ var x = .5 + (db - da) / (2 * dc), y = Math.sqrt(Math.max(0, 2 * da * (db + dc) - (db -= dc) * db - da * da)) / (2 * dc);
+ c.x = a.x + x * dx + y * dy;
+ c.y = a.y + x * dy - y * dx;
+ } else {
+ c.x = a.x + db;
+ c.y = a.y;
+ }
+ }
+ d3.layout.cluster = function() {
+ var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = false;
+ function cluster(d, i) {
+ var nodes = hierarchy.call(this, d, i), root = nodes[0], previousNode, x = 0;
+ d3_layout_treeVisitAfter(root, function(node) {
+ var children = node.children;
+ if (children && children.length) {
+ node.x = d3_layout_clusterX(children);
+ node.y = d3_layout_clusterY(children);
+ } else {
+ node.x = previousNode ? x += separation(node, previousNode) : 0;
+ node.y = 0;
+ previousNode = node;
+ }
+ });
+ var left = d3_layout_clusterLeft(root), right = d3_layout_clusterRight(root), x0 = left.x - separation(left, right) / 2, x1 = right.x + separation(right, left) / 2;
+ d3_layout_treeVisitAfter(root, nodeSize ? function(node) {
+ node.x = (node.x - root.x) * size[0];
+ node.y = (root.y - node.y) * size[1];
+ } : function(node) {
+ node.x = (node.x - x0) / (x1 - x0) * size[0];
+ node.y = (1 - (root.y ? node.y / root.y : 1)) * size[1];
+ });
+ return nodes;
+ }
+ cluster.separation = function(x) {
+ if (!arguments.length) return separation;
+ separation = x;
+ return cluster;
+ };
+ cluster.size = function(x) {
+ if (!arguments.length) return nodeSize ? null : size;
+ nodeSize = (size = x) == null;
+ return cluster;
+ };
+ cluster.nodeSize = function(x) {
+ if (!arguments.length) return nodeSize ? size : null;
+ nodeSize = (size = x) != null;
+ return cluster;
+ };
+ return d3_layout_hierarchyRebind(cluster, hierarchy);
+ };
+ function d3_layout_clusterY(children) {
+ return 1 + d3.max(children, function(child) {
+ return child.y;
+ });
+ }
+ function d3_layout_clusterX(children) {
+ return children.reduce(function(x, child) {
+ return x + child.x;
+ }, 0) / children.length;
+ }
+ function d3_layout_clusterLeft(node) {
+ var children = node.children;
+ return children && children.length ? d3_layout_clusterLeft(children[0]) : node;
+ }
+ function d3_layout_clusterRight(node) {
+ var children = node.children, n;
+ return children && (n = children.length) ? d3_layout_clusterRight(children[n - 1]) : node;
+ }
+ d3.layout.treemap = function() {
+ var hierarchy = d3.layout.hierarchy(), round = Math.round, size = [ 1, 1 ], padding = null, pad = d3_layout_treemapPadNull, sticky = false, stickies, mode = "squarify", ratio = .5 * (1 + Math.sqrt(5));
+ function scale(children, k) {
+ var i = -1, n = children.length, child, area;
+ while (++i < n) {
+ area = (child = children[i]).value * (k < 0 ? 0 : k);
+ child.area = isNaN(area) || area <= 0 ? 0 : area;
+ }
+ }
+ function squarify(node) {
+ var children = node.children;
+ if (children && children.length) {
+ var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = mode === "slice" ? rect.dx : mode === "dice" ? rect.dy : mode === "slice-dice" ? node.depth & 1 ? rect.dy : rect.dx : Math.min(rect.dx, rect.dy), n;
+ scale(remaining, rect.dx * rect.dy / node.value);
+ row.area = 0;
+ while ((n = remaining.length) > 0) {
+ row.push(child = remaining[n - 1]);
+ row.area += child.area;
+ if (mode !== "squarify" || (score = worst(row, u)) <= best) {
+ remaining.pop();
+ best = score;
+ } else {
+ row.area -= row.pop().area;
+ position(row, u, rect, false);
+ u = Math.min(rect.dx, rect.dy);
+ row.length = row.area = 0;
+ best = Infinity;
+ }
+ }
+ if (row.length) {
+ position(row, u, rect, true);
+ row.length = row.area = 0;
+ }
+ children.forEach(squarify);
+ }
+ }
+ function stickify(node) {
+ var children = node.children;
+ if (children && children.length) {
+ var rect = pad(node), remaining = children.slice(), child, row = [];
+ scale(remaining, rect.dx * rect.dy / node.value);
+ row.area = 0;
+ while (child = remaining.pop()) {
+ row.push(child);
+ row.area += child.area;
+ if (child.z != null) {
+ position(row, child.z ? rect.dx : rect.dy, rect, !remaining.length);
+ row.length = row.area = 0;
+ }
+ }
+ children.forEach(stickify);
+ }
+ }
+ function worst(row, u) {
+ var s = row.area, r, rmax = 0, rmin = Infinity, i = -1, n = row.length;
+ while (++i < n) {
+ if (!(r = row[i].area)) continue;
+ if (r < rmin) rmin = r;
+ if (r > rmax) rmax = r;
+ }
+ s *= s;
+ u *= u;
+ return s ? Math.max(u * rmax * ratio / s, s / (u * rmin * ratio)) : Infinity;
+ }
+ function position(row, u, rect, flush) {
+ var i = -1, n = row.length, x = rect.x, y = rect.y, v = u ? round(row.area / u) : 0, o;
+ if (u == rect.dx) {
+ if (flush || v > rect.dy) v = rect.dy;
+ while (++i < n) {
+ o = row[i];
+ o.x = x;
+ o.y = y;
+ o.dy = v;
+ x += o.dx = Math.min(rect.x + rect.dx - x, v ? round(o.area / v) : 0);
+ }
+ o.z = true;
+ o.dx += rect.x + rect.dx - x;
+ rect.y += v;
+ rect.dy -= v;
+ } else {
+ if (flush || v > rect.dx) v = rect.dx;
+ while (++i < n) {
+ o = row[i];
+ o.x = x;
+ o.y = y;
+ o.dx = v;
+ y += o.dy = Math.min(rect.y + rect.dy - y, v ? round(o.area / v) : 0);
+ }
+ o.z = false;
+ o.dy += rect.y + rect.dy - y;
+ rect.x += v;
+ rect.dx -= v;
+ }
+ }
+ function treemap(d) {
+ var nodes = stickies || hierarchy(d), root = nodes[0];
+ root.x = 0;
+ root.y = 0;
+ root.dx = size[0];
+ root.dy = size[1];
+ if (stickies) hierarchy.revalue(root);
+ scale([ root ], root.dx * root.dy / root.value);
+ (stickies ? stickify : squarify)(root);
+ if (sticky) stickies = nodes;
+ return nodes;
+ }
+ treemap.size = function(x) {
+ if (!arguments.length) return size;
+ size = x;
+ return treemap;
+ };
+ treemap.padding = function(x) {
+ if (!arguments.length) return padding;
+ function padFunction(node) {
+ var p = x.call(treemap, node, node.depth);
+ return p == null ? d3_layout_treemapPadNull(node) : d3_layout_treemapPad(node, typeof p === "number" ? [ p, p, p, p ] : p);
+ }
+ function padConstant(node) {
+ return d3_layout_treemapPad(node, x);
+ }
+ var type;
+ pad = (padding = x) == null ? d3_layout_treemapPadNull : (type = typeof x) === "function" ? padFunction : type === "number" ? (x = [ x, x, x, x ],
+ padConstant) : padConstant;
+ return treemap;
+ };
+ treemap.round = function(x) {
+ if (!arguments.length) return round != Number;
+ round = x ? Math.round : Number;
+ return treemap;
+ };
+ treemap.sticky = function(x) {
+ if (!arguments.length) return sticky;
+ sticky = x;
+ stickies = null;
+ return treemap;
+ };
+ treemap.ratio = function(x) {
+ if (!arguments.length) return ratio;
+ ratio = x;
+ return treemap;
+ };
+ treemap.mode = function(x) {
+ if (!arguments.length) return mode;
+ mode = x + "";
+ return treemap;
+ };
+ return d3_layout_hierarchyRebind(treemap, hierarchy);
+ };
+ function d3_layout_treemapPadNull(node) {
+ return {
+ x: node.x,
+ y: node.y,
+ dx: node.dx,
+ dy: node.dy
+ };
+ }
+ function d3_layout_treemapPad(node, padding) {
+ var x = node.x + padding[3], y = node.y + padding[0], dx = node.dx - padding[1] - padding[3], dy = node.dy - padding[0] - padding[2];
+ if (dx < 0) {
+ x += dx / 2;
+ dx = 0;
+ }
+ if (dy < 0) {
+ y += dy / 2;
+ dy = 0;
+ }
+ return {
+ x: x,
+ y: y,
+ dx: dx,
+ dy: dy
+ };
+ }
+ d3.random = {
+ normal: function(µ, σ) {
+ var n = arguments.length;
+ if (n < 2) σ = 1;
+ if (n < 1) µ = 0;
+ return function() {
+ var x, y, r;
+ do {
+ x = Math.random() * 2 - 1;
+ y = Math.random() * 2 - 1;
+ r = x * x + y * y;
+ } while (!r || r > 1);
+ return µ + σ * x * Math.sqrt(-2 * Math.log(r) / r);
+ };
+ },
+ logNormal: function() {
+ var random = d3.random.normal.apply(d3, arguments);
+ return function() {
+ return Math.exp(random());
+ };
+ },
+ bates: function(m) {
+ var random = d3.random.irwinHall(m);
+ return function() {
+ return random() / m;
+ };
+ },
+ irwinHall: function(m) {
+ return function() {
+ for (var s = 0, j = 0; j < m; j++) s += Math.random();
+ return s;
+ };
+ }
+ };
+ d3.scale = {};
+ function d3_scaleExtent(domain) {
+ var start = domain[0], stop = domain[domain.length - 1];
+ return start < stop ? [ start, stop ] : [ stop, start ];
+ }
+ function d3_scaleRange(scale) {
+ return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range());
+ }
+ function d3_scale_bilinear(domain, range, uninterpolate, interpolate) {
+ var u = uninterpolate(domain[0], domain[1]), i = interpolate(range[0], range[1]);
+ return function(x) {
+ return i(u(x));
+ };
+ }
+ function d3_scale_nice(domain, nice) {
+ var i0 = 0, i1 = domain.length - 1, x0 = domain[i0], x1 = domain[i1], dx;
+ if (x1 < x0) {
+ dx = i0, i0 = i1, i1 = dx;
+ dx = x0, x0 = x1, x1 = dx;
+ }
+ domain[i0] = nice.floor(x0);
+ domain[i1] = nice.ceil(x1);
+ return domain;
+ }
+ function d3_scale_niceStep(step) {
+ return step ? {
+ floor: function(x) {
+ return Math.floor(x / step) * step;
+ },
+ ceil: function(x) {
+ return Math.ceil(x / step) * step;
+ }
+ } : d3_scale_niceIdentity;
+ }
+ var d3_scale_niceIdentity = {
+ floor: d3_identity,
+ ceil: d3_identity
+ };
+ function d3_scale_polylinear(domain, range, uninterpolate, interpolate) {
+ var u = [], i = [], j = 0, k = Math.min(domain.length, range.length) - 1;
+ if (domain[k] < domain[0]) {
+ domain = domain.slice().reverse();
+ range = range.slice().reverse();
+ }
+ while (++j <= k) {
+ u.push(uninterpolate(domain[j - 1], domain[j]));
+ i.push(interpolate(range[j - 1], range[j]));
+ }
+ return function(x) {
+ var j = d3.bisect(domain, x, 1, k) - 1;
+ return i[j](u[j](x));
+ };
+ }
+ d3.scale.linear = function() {
+ return d3_scale_linear([ 0, 1 ], [ 0, 1 ], d3_interpolate, false);
+ };
+ function d3_scale_linear(domain, range, interpolate, clamp) {
+ var output, input;
+ function rescale() {
+ var linear = Math.min(domain.length, range.length) > 2 ? d3_scale_polylinear : d3_scale_bilinear, uninterpolate = clamp ? d3_uninterpolateClamp : d3_uninterpolateNumber;
+ output = linear(domain, range, uninterpolate, interpolate);
+ input = linear(range, domain, uninterpolate, d3_interpolate);
+ return scale;
+ }
+ function scale(x) {
+ return output(x);
+ }
+ scale.invert = function(y) {
+ return input(y);
+ };
+ scale.domain = function(x) {
+ if (!arguments.length) return domain;
+ domain = x.map(Number);
+ return rescale();
+ };
+ scale.range = function(x) {
+ if (!arguments.length) return range;
+ range = x;
+ return rescale();
+ };
+ scale.rangeRound = function(x) {
+ return scale.range(x).interpolate(d3_interpolateRound);
+ };
+ scale.clamp = function(x) {
+ if (!arguments.length) return clamp;
+ clamp = x;
+ return rescale();
+ };
+ scale.interpolate = function(x) {
+ if (!arguments.length) return interpolate;
+ interpolate = x;
+ return rescale();
+ };
+ scale.ticks = function(m) {
+ return d3_scale_linearTicks(domain, m);
+ };
+ scale.tickFormat = function(m, format) {
+ return d3_scale_linearTickFormat(domain, m, format);
+ };
+ scale.nice = function(m) {
+ d3_scale_linearNice(domain, m);
+ return rescale();
+ };
+ scale.copy = function() {
+ return d3_scale_linear(domain, range, interpolate, clamp);
+ };
+ return rescale();
+ }
+ function d3_scale_linearRebind(scale, linear) {
+ return d3.rebind(scale, linear, "range", "rangeRound", "interpolate", "clamp");
+ }
+ function d3_scale_linearNice(domain, m) {
+ return d3_scale_nice(domain, d3_scale_niceStep(d3_scale_linearTickRange(domain, m)[2]));
+ }
+ function d3_scale_linearTickRange(domain, m) {
+ if (m == null) m = 10;
+ var extent = d3_scaleExtent(domain), span = extent[1] - extent[0], step = Math.pow(10, Math.floor(Math.log(span / m) / Math.LN10)), err = m / span * step;
+ if (err <= .15) step *= 10; else if (err <= .35) step *= 5; else if (err <= .75) step *= 2;
+ extent[0] = Math.ceil(extent[0] / step) * step;
+ extent[1] = Math.floor(extent[1] / step) * step + step * .5;
+ extent[2] = step;
+ return extent;
+ }
+ function d3_scale_linearTicks(domain, m) {
+ return d3.range.apply(d3, d3_scale_linearTickRange(domain, m));
+ }
+ function d3_scale_linearTickFormat(domain, m, format) {
+ var range = d3_scale_linearTickRange(domain, m);
+ return d3.format(format ? format.replace(d3_format_re, function(a, b, c, d, e, f, g, h, i, j) {
+ return [ b, c, d, e, f, g, h, i || "." + d3_scale_linearFormatPrecision(j, range), j ].join("");
+ }) : ",." + d3_scale_linearPrecision(range[2]) + "f");
+ }
+ var d3_scale_linearFormatSignificant = {
+ s: 1,
+ g: 1,
+ p: 1,
+ r: 1,
+ e: 1
+ };
+ function d3_scale_linearPrecision(value) {
+ return -Math.floor(Math.log(value) / Math.LN10 + .01);
+ }
+ function d3_scale_linearFormatPrecision(type, range) {
+ var p = d3_scale_linearPrecision(range[2]);
+ return type in d3_scale_linearFormatSignificant ? Math.abs(p - d3_scale_linearPrecision(Math.max(Math.abs(range[0]), Math.abs(range[1])))) + +(type !== "e") : p - (type === "%") * 2;
+ }
+ d3.scale.log = function() {
+ return d3_scale_log(d3.scale.linear().domain([ 0, 1 ]), 10, true, [ 1, 10 ]);
+ };
+ function d3_scale_log(linear, base, positive, domain) {
+ function log(x) {
+ return (positive ? Math.log(x < 0 ? 0 : x) : -Math.log(x > 0 ? 0 : -x)) / Math.log(base);
+ }
+ function pow(x) {
+ return positive ? Math.pow(base, x) : -Math.pow(base, -x);
+ }
+ function scale(x) {
+ return linear(log(x));
+ }
+ scale.invert = function(x) {
+ return pow(linear.invert(x));
+ };
+ scale.domain = function(x) {
+ if (!arguments.length) return domain;
+ positive = x[0] >= 0;
+ linear.domain((domain = x.map(Number)).map(log));
+ return scale;
+ };
+ scale.base = function(_) {
+ if (!arguments.length) return base;
+ base = +_;
+ linear.domain(domain.map(log));
+ return scale;
+ };
+ scale.nice = function() {
+ var niced = d3_scale_nice(domain.map(log), positive ? Math : d3_scale_logNiceNegative);
+ linear.domain(niced);
+ domain = niced.map(pow);
+ return scale;
+ };
+ scale.ticks = function() {
+ var extent = d3_scaleExtent(domain), ticks = [], u = extent[0], v = extent[1], i = Math.floor(log(u)), j = Math.ceil(log(v)), n = base % 1 ? 2 : base;
+ if (isFinite(j - i)) {
+ if (positive) {
+ for (;i < j; i++) for (var k = 1; k < n; k++) ticks.push(pow(i) * k);
+ ticks.push(pow(i));
+ } else {
+ ticks.push(pow(i));
+ for (;i++ < j; ) for (var k = n - 1; k > 0; k--) ticks.push(pow(i) * k);
+ }
+ for (i = 0; ticks[i] < u; i++) {}
+ for (j = ticks.length; ticks[j - 1] > v; j--) {}
+ ticks = ticks.slice(i, j);
+ }
+ return ticks;
+ };
+ scale.tickFormat = function(n, format) {
+ if (!arguments.length) return d3_scale_logFormat;
+ if (arguments.length < 2) format = d3_scale_logFormat; else if (typeof format !== "function") format = d3.format(format);
+ var k = Math.max(.1, n / scale.ticks().length), f = positive ? (e = 1e-12, Math.ceil) : (e = -1e-12,
+ Math.floor), e;
+ return function(d) {
+ return d / pow(f(log(d) + e)) <= k ? format(d) : "";
+ };
+ };
+ scale.copy = function() {
+ return d3_scale_log(linear.copy(), base, positive, domain);
+ };
+ return d3_scale_linearRebind(scale, linear);
+ }
+ var d3_scale_logFormat = d3.format(".0e"), d3_scale_logNiceNegative = {
+ floor: function(x) {
+ return -Math.ceil(-x);
+ },
+ ceil: function(x) {
+ return -Math.floor(-x);
+ }
+ };
+ d3.scale.pow = function() {
+ return d3_scale_pow(d3.scale.linear(), 1, [ 0, 1 ]);
+ };
+ function d3_scale_pow(linear, exponent, domain) {
+ var powp = d3_scale_powPow(exponent), powb = d3_scale_powPow(1 / exponent);
+ function scale(x) {
+ return linear(powp(x));
+ }
+ scale.invert = function(x) {
+ return powb(linear.invert(x));
+ };
+ scale.domain = function(x) {
+ if (!arguments.length) return domain;
+ linear.domain((domain = x.map(Number)).map(powp));
+ return scale;
+ };
+ scale.ticks = function(m) {
+ return d3_scale_linearTicks(domain, m);
+ };
+ scale.tickFormat = function(m, format) {
+ return d3_scale_linearTickFormat(domain, m, format);
+ };
+ scale.nice = function(m) {
+ return scale.domain(d3_scale_linearNice(domain, m));
+ };
+ scale.exponent = function(x) {
+ if (!arguments.length) return exponent;
+ powp = d3_scale_powPow(exponent = x);
+ powb = d3_scale_powPow(1 / exponent);
+ linear.domain(domain.map(powp));
+ return scale;
+ };
+ scale.copy = function() {
+ return d3_scale_pow(linear.copy(), exponent, domain);
+ };
+ return d3_scale_linearRebind(scale, linear);
+ }
+ function d3_scale_powPow(e) {
+ return function(x) {
+ return x < 0 ? -Math.pow(-x, e) : Math.pow(x, e);
+ };
+ }
+ d3.scale.sqrt = function() {
+ return d3.scale.pow().exponent(.5);
+ };
+ d3.scale.ordinal = function() {
+ return d3_scale_ordinal([], {
+ t: "range",
+ a: [ [] ]
+ });
+ };
+ function d3_scale_ordinal(domain, ranger) {
+ var index, range, rangeBand;
+ function scale(x) {
+ return range[((index.get(x) || ranger.t === "range" && index.set(x, domain.push(x))) - 1) % range.length];
+ }
+ function steps(start, step) {
+ return d3.range(domain.length).map(function(i) {
+ return start + step * i;
+ });
+ }
+ scale.domain = function(x) {
+ if (!arguments.length) return domain;
+ domain = [];
+ index = new d3_Map();
+ var i = -1, n = x.length, xi;
+ while (++i < n) if (!index.has(xi = x[i])) index.set(xi, domain.push(xi));
+ return scale[ranger.t].apply(scale, ranger.a);
+ };
+ scale.range = function(x) {
+ if (!arguments.length) return range;
+ range = x;
+ rangeBand = 0;
+ ranger = {
+ t: "range",
+ a: arguments
+ };
+ return scale;
+ };
+ scale.rangePoints = function(x, padding) {
+ if (arguments.length < 2) padding = 0;
+ var start = x[0], stop = x[1], step = (stop - start) / (Math.max(1, domain.length - 1) + padding);
+ range = steps(domain.length < 2 ? (start + stop) / 2 : start + step * padding / 2, step);
+ rangeBand = 0;
+ ranger = {
+ t: "rangePoints",
+ a: arguments
+ };
+ return scale;
+ };
+ scale.rangeBands = function(x, padding, outerPadding) {
+ if (arguments.length < 2) padding = 0;
+ if (arguments.length < 3) outerPadding = padding;
+ var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = (stop - start) / (domain.length - padding + 2 * outerPadding);
+ range = steps(start + step * outerPadding, step);
+ if (reverse) range.reverse();
+ rangeBand = step * (1 - padding);
+ ranger = {
+ t: "rangeBands",
+ a: arguments
+ };
+ return scale;
+ };
+ scale.rangeRoundBands = function(x, padding, outerPadding) {
+ if (arguments.length < 2) padding = 0;
+ if (arguments.length < 3) outerPadding = padding;
+ var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = Math.floor((stop - start) / (domain.length - padding + 2 * outerPadding)), error = stop - start - (domain.length - padding) * step;
+ range = steps(start + Math.round(error / 2), step);
+ if (reverse) range.reverse();
+ rangeBand = Math.round(step * (1 - padding));
+ ranger = {
+ t: "rangeRoundBands",
+ a: arguments
+ };
+ return scale;
+ };
+ scale.rangeBand = function() {
+ return rangeBand;
+ };
+ scale.rangeExtent = function() {
+ return d3_scaleExtent(ranger.a[0]);
+ };
+ scale.copy = function() {
+ return d3_scale_ordinal(domain, ranger);
+ };
+ return scale.domain(domain);
+ }
+ d3.scale.category10 = function() {
+ return d3.scale.ordinal().range(d3_category10);
+ };
+ d3.scale.category20 = function() {
+ return d3.scale.ordinal().range(d3_category20);
+ };
+ d3.scale.category20b = function() {
+ return d3.scale.ordinal().range(d3_category20b);
+ };
+ d3.scale.category20c = function() {
+ return d3.scale.ordinal().range(d3_category20c);
+ };
+ var d3_category10 = [ 2062260, 16744206, 2924588, 14034728, 9725885, 9197131, 14907330, 8355711, 12369186, 1556175 ].map(d3_rgbString);
+ var d3_category20 = [ 2062260, 11454440, 16744206, 16759672, 2924588, 10018698, 14034728, 16750742, 9725885, 12955861, 9197131, 12885140, 14907330, 16234194, 8355711, 13092807, 12369186, 14408589, 1556175, 10410725 ].map(d3_rgbString);
+ var d3_category20b = [ 3750777, 5395619, 7040719, 10264286, 6519097, 9216594, 11915115, 13556636, 9202993, 12426809, 15186514, 15190932, 8666169, 11356490, 14049643, 15177372, 8077683, 10834324, 13528509, 14589654 ].map(d3_rgbString);
+ var d3_category20c = [ 3244733, 7057110, 10406625, 13032431, 15095053, 16616764, 16625259, 16634018, 3253076, 7652470, 10607003, 13101504, 7695281, 10394312, 12369372, 14342891, 6513507, 9868950, 12434877, 14277081 ].map(d3_rgbString);
+ d3.scale.quantile = function() {
+ return d3_scale_quantile([], []);
+ };
+ function d3_scale_quantile(domain, range) {
+ var thresholds;
+ function rescale() {
+ var k = 0, q = range.length;
+ thresholds = [];
+ while (++k < q) thresholds[k - 1] = d3.quantile(domain, k / q);
+ return scale;
+ }
+ function scale(x) {
+ if (!isNaN(x = +x)) return range[d3.bisect(thresholds, x)];
+ }
+ scale.domain = function(x) {
+ if (!arguments.length) return domain;
+ domain = x.filter(function(d) {
+ return !isNaN(d);
+ }).sort(d3.ascending);
+ return rescale();
+ };
+ scale.range = function(x) {
+ if (!arguments.length) return range;
+ range = x;
+ return rescale();
+ };
+ scale.quantiles = function() {
+ return thresholds;
+ };
+ scale.invertExtent = function(y) {
+ y = range.indexOf(y);
+ return y < 0 ? [ NaN, NaN ] : [ y > 0 ? thresholds[y - 1] : domain[0], y < thresholds.length ? thresholds[y] : domain[domain.length - 1] ];
+ };
+ scale.copy = function() {
+ return d3_scale_quantile(domain, range);
+ };
+ return rescale();
+ }
+ d3.scale.quantize = function() {
+ return d3_scale_quantize(0, 1, [ 0, 1 ]);
+ };
+ function d3_scale_quantize(x0, x1, range) {
+ var kx, i;
+ function scale(x) {
+ return range[Math.max(0, Math.min(i, Math.floor(kx * (x - x0))))];
+ }
+ function rescale() {
+ kx = range.length / (x1 - x0);
+ i = range.length - 1;
+ return scale;
+ }
+ scale.domain = function(x) {
+ if (!arguments.length) return [ x0, x1 ];
+ x0 = +x[0];
+ x1 = +x[x.length - 1];
+ return rescale();
+ };
+ scale.range = function(x) {
+ if (!arguments.length) return range;
+ range = x;
+ return rescale();
+ };
+ scale.invertExtent = function(y) {
+ y = range.indexOf(y);
+ y = y < 0 ? NaN : y / kx + x0;
+ return [ y, y + 1 / kx ];
+ };
+ scale.copy = function() {
+ return d3_scale_quantize(x0, x1, range);
+ };
+ return rescale();
+ }
+ d3.scale.threshold = function() {
+ return d3_scale_threshold([ .5 ], [ 0, 1 ]);
+ };
+ function d3_scale_threshold(domain, range) {
+ function scale(x) {
+ if (x <= x) return range[d3.bisect(domain, x)];
+ }
+ scale.domain = function(_) {
+ if (!arguments.length) return domain;
+ domain = _;
+ return scale;
+ };
+ scale.range = function(_) {
+ if (!arguments.length) return range;
+ range = _;
+ return scale;
+ };
+ scale.invertExtent = function(y) {
+ y = range.indexOf(y);
+ return [ domain[y - 1], domain[y] ];
+ };
+ scale.copy = function() {
+ return d3_scale_threshold(domain, range);
+ };
+ return scale;
+ }
+ d3.scale.identity = function() {
+ return d3_scale_identity([ 0, 1 ]);
+ };
+ function d3_scale_identity(domain) {
+ function identity(x) {
+ return +x;
+ }
+ identity.invert = identity;
+ identity.domain = identity.range = function(x) {
+ if (!arguments.length) return domain;
+ domain = x.map(identity);
+ return identity;
+ };
+ identity.ticks = function(m) {
+ return d3_scale_linearTicks(domain, m);
+ };
+ identity.tickFormat = function(m, format) {
+ return d3_scale_linearTickFormat(domain, m, format);
+ };
+ identity.copy = function() {
+ return d3_scale_identity(domain);
+ };
+ return identity;
+ }
+ d3.svg = {};
+ d3.svg.arc = function() {
+ var innerRadius = d3_svg_arcInnerRadius, outerRadius = d3_svg_arcOuterRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle;
+ function arc() {
+ var r0 = innerRadius.apply(this, arguments), r1 = outerRadius.apply(this, arguments), a0 = startAngle.apply(this, arguments) + d3_svg_arcOffset, a1 = endAngle.apply(this, arguments) + d3_svg_arcOffset, da = (a1 < a0 && (da = a0,
+ a0 = a1, a1 = da), a1 - a0), df = da < π ? "0" : "1", c0 = Math.cos(a0), s0 = Math.sin(a0), c1 = Math.cos(a1), s1 = Math.sin(a1);
+ return da >= d3_svg_arcMax ? r0 ? "M0," + r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + -r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 + "M0," + r0 + "A" + r0 + "," + r0 + " 0 1,0 0," + -r0 + "A" + r0 + "," + r0 + " 0 1,0 0," + r0 + "Z" : "M0," + r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + -r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 + "Z" : r0 ? "M" + r1 * c0 + "," + r1 * s0 + "A" + r1 + "," + r1 + " 0 " + df + ",1 " + r1 * c1 + "," + r1 * s1 + "L" + r0 * c1 + "," + r0 * s1 + "A" + r0 + "," + r0 + " 0 " + df + ",0 " + r0 * c0 + "," + r0 * s0 + "Z" : "M" + r1 * c0 + "," + r1 * s0 + "A" + r1 + "," + r1 + " 0 " + df + ",1 " + r1 * c1 + "," + r1 * s1 + "L0,0" + "Z";
+ }
+ arc.innerRadius = function(v) {
+ if (!arguments.length) return innerRadius;
+ innerRadius = d3_functor(v);
+ return arc;
+ };
+ arc.outerRadius = function(v) {
+ if (!arguments.length) return outerRadius;
+ outerRadius = d3_functor(v);
+ return arc;
+ };
+ arc.startAngle = function(v) {
+ if (!arguments.length) return startAngle;
+ startAngle = d3_functor(v);
+ return arc;
+ };
+ arc.endAngle = function(v) {
+ if (!arguments.length) return endAngle;
+ endAngle = d3_functor(v);
+ return arc;
+ };
+ arc.centroid = function() {
+ var r = (innerRadius.apply(this, arguments) + outerRadius.apply(this, arguments)) / 2, a = (startAngle.apply(this, arguments) + endAngle.apply(this, arguments)) / 2 + d3_svg_arcOffset;
+ return [ Math.cos(a) * r, Math.sin(a) * r ];
+ };
+ return arc;
+ };
+ var d3_svg_arcOffset = -halfπ, d3_svg_arcMax = τ - ε;
+ function d3_svg_arcInnerRadius(d) {
+ return d.innerRadius;
+ }
+ function d3_svg_arcOuterRadius(d) {
+ return d.outerRadius;
+ }
+ function d3_svg_arcStartAngle(d) {
+ return d.startAngle;
+ }
+ function d3_svg_arcEndAngle(d) {
+ return d.endAngle;
+ }
+ function d3_svg_line(projection) {
+ var x = d3_geom_pointX, y = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, tension = .7;
+ function line(data) {
+ var segments = [], points = [], i = -1, n = data.length, d, fx = d3_functor(x), fy = d3_functor(y);
+ function segment() {
+ segments.push("M", interpolate(projection(points), tension));
+ }
+ while (++i < n) {
+ if (defined.call(this, d = data[i], i)) {
+ points.push([ +fx.call(this, d, i), +fy.call(this, d, i) ]);
+ } else if (points.length) {
+ segment();
+ points = [];
+ }
+ }
+ if (points.length) segment();
+ return segments.length ? segments.join("") : null;
+ }
+ line.x = function(_) {
+ if (!arguments.length) return x;
+ x = _;
+ return line;
+ };
+ line.y = function(_) {
+ if (!arguments.length) return y;
+ y = _;
+ return line;
+ };
+ line.defined = function(_) {
+ if (!arguments.length) return defined;
+ defined = _;
+ return line;
+ };
+ line.interpolate = function(_) {
+ if (!arguments.length) return interpolateKey;
+ if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key;
+ return line;
+ };
+ line.tension = function(_) {
+ if (!arguments.length) return tension;
+ tension = _;
+ return line;
+ };
+ return line;
+ }
+ d3.svg.line = function() {
+ return d3_svg_line(d3_identity);
+ };
+ var d3_svg_lineInterpolators = d3.map({
+ linear: d3_svg_lineLinear,
+ "linear-closed": d3_svg_lineLinearClosed,
+ step: d3_svg_lineStep,
+ "step-before": d3_svg_lineStepBefore,
+ "step-after": d3_svg_lineStepAfter,
+ basis: d3_svg_lineBasis,
+ "basis-open": d3_svg_lineBasisOpen,
+ "basis-closed": d3_svg_lineBasisClosed,
+ bundle: d3_svg_lineBundle,
+ cardinal: d3_svg_lineCardinal,
+ "cardinal-open": d3_svg_lineCardinalOpen,
+ "cardinal-closed": d3_svg_lineCardinalClosed,
+ monotone: d3_svg_lineMonotone
+ });
+ d3_svg_lineInterpolators.forEach(function(key, value) {
+ value.key = key;
+ value.closed = /-closed$/.test(key);
+ });
+ function d3_svg_lineLinear(points) {
+ return points.join("L");
+ }
+ function d3_svg_lineLinearClosed(points) {
+ return d3_svg_lineLinear(points) + "Z";
+ }
+ function d3_svg_lineStep(points) {
+ var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ];
+ while (++i < n) path.push("H", (p[0] + (p = points[i])[0]) / 2, "V", p[1]);
+ if (n > 1) path.push("H", p[0]);
+ return path.join("");
+ }
+ function d3_svg_lineStepBefore(points) {
+ var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ];
+ while (++i < n) path.push("V", (p = points[i])[1], "H", p[0]);
+ return path.join("");
+ }
+ function d3_svg_lineStepAfter(points) {
+ var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ];
+ while (++i < n) path.push("H", (p = points[i])[0], "V", p[1]);
+ return path.join("");
+ }
+ function d3_svg_lineCardinalOpen(points, tension) {
+ return points.length < 4 ? d3_svg_lineLinear(points) : points[1] + d3_svg_lineHermite(points.slice(1, points.length - 1), d3_svg_lineCardinalTangents(points, tension));
+ }
+ function d3_svg_lineCardinalClosed(points, tension) {
+ return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite((points.push(points[0]),
+ points), d3_svg_lineCardinalTangents([ points[points.length - 2] ].concat(points, [ points[1] ]), tension));
+ }
+ function d3_svg_lineCardinal(points, tension) {
+ return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineCardinalTangents(points, tension));
+ }
+ function d3_svg_lineHermite(points, tangents) {
+ if (tangents.length < 1 || points.length != tangents.length && points.length != tangents.length + 2) {
+ return d3_svg_lineLinear(points);
+ }
+ var quad = points.length != tangents.length, path = "", p0 = points[0], p = points[1], t0 = tangents[0], t = t0, pi = 1;
+ if (quad) {
+ path += "Q" + (p[0] - t0[0] * 2 / 3) + "," + (p[1] - t0[1] * 2 / 3) + "," + p[0] + "," + p[1];
+ p0 = points[1];
+ pi = 2;
+ }
+ if (tangents.length > 1) {
+ t = tangents[1];
+ p = points[pi];
+ pi++;
+ path += "C" + (p0[0] + t0[0]) + "," + (p0[1] + t0[1]) + "," + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1];
+ for (var i = 2; i < tangents.length; i++, pi++) {
+ p = points[pi];
+ t = tangents[i];
+ path += "S" + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1];
+ }
+ }
+ if (quad) {
+ var lp = points[pi];
+ path += "Q" + (p[0] + t[0] * 2 / 3) + "," + (p[1] + t[1] * 2 / 3) + "," + lp[0] + "," + lp[1];
+ }
+ return path;
+ }
+ function d3_svg_lineCardinalTangents(points, tension) {
+ var tangents = [], a = (1 - tension) / 2, p0, p1 = points[0], p2 = points[1], i = 1, n = points.length;
+ while (++i < n) {
+ p0 = p1;
+ p1 = p2;
+ p2 = points[i];
+ tangents.push([ a * (p2[0] - p0[0]), a * (p2[1] - p0[1]) ]);
+ }
+ return tangents;
+ }
+ function d3_svg_lineBasis(points) {
+ if (points.length < 3) return d3_svg_lineLinear(points);
+ var i = 1, n = points.length, pi = points[0], x0 = pi[0], y0 = pi[1], px = [ x0, x0, x0, (pi = points[1])[0] ], py = [ y0, y0, y0, pi[1] ], path = [ x0, ",", y0, "L", d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ];
+ points.push(points[n - 1]);
+ while (++i <= n) {
+ pi = points[i];
+ px.shift();
+ px.push(pi[0]);
+ py.shift();
+ py.push(pi[1]);
+ d3_svg_lineBasisBezier(path, px, py);
+ }
+ points.pop();
+ path.push("L", pi);
+ return path.join("");
+ }
+ function d3_svg_lineBasisOpen(points) {
+ if (points.length < 4) return d3_svg_lineLinear(points);
+ var path = [], i = -1, n = points.length, pi, px = [ 0 ], py = [ 0 ];
+ while (++i < 3) {
+ pi = points[i];
+ px.push(pi[0]);
+ py.push(pi[1]);
+ }
+ path.push(d3_svg_lineDot4(d3_svg_lineBasisBezier3, px) + "," + d3_svg_lineDot4(d3_svg_lineBasisBezier3, py));
+ --i;
+ while (++i < n) {
+ pi = points[i];
+ px.shift();
+ px.push(pi[0]);
+ py.shift();
+ py.push(pi[1]);
+ d3_svg_lineBasisBezier(path, px, py);
+ }
+ return path.join("");
+ }
+ function d3_svg_lineBasisClosed(points) {
+ var path, i = -1, n = points.length, m = n + 4, pi, px = [], py = [];
+ while (++i < 4) {
+ pi = points[i % n];
+ px.push(pi[0]);
+ py.push(pi[1]);
+ }
+ path = [ d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ];
+ --i;
+ while (++i < m) {
+ pi = points[i % n];
+ px.shift();
+ px.push(pi[0]);
+ py.shift();
+ py.push(pi[1]);
+ d3_svg_lineBasisBezier(path, px, py);
+ }
+ return path.join("");
+ }
+ function d3_svg_lineBundle(points, tension) {
+ var n = points.length - 1;
+ if (n) {
+ var x0 = points[0][0], y0 = points[0][1], dx = points[n][0] - x0, dy = points[n][1] - y0, i = -1, p, t;
+ while (++i <= n) {
+ p = points[i];
+ t = i / n;
+ p[0] = tension * p[0] + (1 - tension) * (x0 + t * dx);
+ p[1] = tension * p[1] + (1 - tension) * (y0 + t * dy);
+ }
+ }
+ return d3_svg_lineBasis(points);
+ }
+ function d3_svg_lineDot4(a, b) {
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
+ }
+ var d3_svg_lineBasisBezier1 = [ 0, 2 / 3, 1 / 3, 0 ], d3_svg_lineBasisBezier2 = [ 0, 1 / 3, 2 / 3, 0 ], d3_svg_lineBasisBezier3 = [ 0, 1 / 6, 2 / 3, 1 / 6 ];
+ function d3_svg_lineBasisBezier(path, x, y) {
+ path.push("C", d3_svg_lineDot4(d3_svg_lineBasisBezier1, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier1, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, y));
+ }
+ function d3_svg_lineSlope(p0, p1) {
+ return (p1[1] - p0[1]) / (p1[0] - p0[0]);
+ }
+ function d3_svg_lineFiniteDifferences(points) {
+ var i = 0, j = points.length - 1, m = [], p0 = points[0], p1 = points[1], d = m[0] = d3_svg_lineSlope(p0, p1);
+ while (++i < j) {
+ m[i] = (d + (d = d3_svg_lineSlope(p0 = p1, p1 = points[i + 1]))) / 2;
+ }
+ m[i] = d;
+ return m;
+ }
+ function d3_svg_lineMonotoneTangents(points) {
+ var tangents = [], d, a, b, s, m = d3_svg_lineFiniteDifferences(points), i = -1, j = points.length - 1;
+ while (++i < j) {
+ d = d3_svg_lineSlope(points[i], points[i + 1]);
+ if (abs(d) < ε) {
+ m[i] = m[i + 1] = 0;
+ } else {
+ a = m[i] / d;
+ b = m[i + 1] / d;
+ s = a * a + b * b;
+ if (s > 9) {
+ s = d * 3 / Math.sqrt(s);
+ m[i] = s * a;
+ m[i + 1] = s * b;
+ }
+ }
+ }
+ i = -1;
+ while (++i <= j) {
+ s = (points[Math.min(j, i + 1)][0] - points[Math.max(0, i - 1)][0]) / (6 * (1 + m[i] * m[i]));
+ tangents.push([ s || 0, m[i] * s || 0 ]);
+ }
+ return tangents;
+ }
+ function d3_svg_lineMonotone(points) {
+ return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points));
+ }
+ d3.svg.line.radial = function() {
+ var line = d3_svg_line(d3_svg_lineRadial);
+ line.radius = line.x, delete line.x;
+ line.angle = line.y, delete line.y;
+ return line;
+ };
+ function d3_svg_lineRadial(points) {
+ var point, i = -1, n = points.length, r, a;
+ while (++i < n) {
+ point = points[i];
+ r = point[0];
+ a = point[1] + d3_svg_arcOffset;
+ point[0] = r * Math.cos(a);
+ point[1] = r * Math.sin(a);
+ }
+ return points;
+ }
+ function d3_svg_area(projection) {
+ var x0 = d3_geom_pointX, x1 = d3_geom_pointX, y0 = 0, y1 = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, interpolateReverse = interpolate, L = "L", tension = .7;
+ function area(data) {
+ var segments = [], points0 = [], points1 = [], i = -1, n = data.length, d, fx0 = d3_functor(x0), fy0 = d3_functor(y0), fx1 = x0 === x1 ? function() {
+ return x;
+ } : d3_functor(x1), fy1 = y0 === y1 ? function() {
+ return y;
+ } : d3_functor(y1), x, y;
+ function segment() {
+ segments.push("M", interpolate(projection(points1), tension), L, interpolateReverse(projection(points0.reverse()), tension), "Z");
+ }
+ while (++i < n) {
+ if (defined.call(this, d = data[i], i)) {
+ points0.push([ x = +fx0.call(this, d, i), y = +fy0.call(this, d, i) ]);
+ points1.push([ +fx1.call(this, d, i), +fy1.call(this, d, i) ]);
+ } else if (points0.length) {
+ segment();
+ points0 = [];
+ points1 = [];
+ }
+ }
+ if (points0.length) segment();
+ return segments.length ? segments.join("") : null;
+ }
+ area.x = function(_) {
+ if (!arguments.length) return x1;
+ x0 = x1 = _;
+ return area;
+ };
+ area.x0 = function(_) {
+ if (!arguments.length) return x0;
+ x0 = _;
+ return area;
+ };
+ area.x1 = function(_) {
+ if (!arguments.length) return x1;
+ x1 = _;
+ return area;
+ };
+ area.y = function(_) {
+ if (!arguments.length) return y1;
+ y0 = y1 = _;
+ return area;
+ };
+ area.y0 = function(_) {
+ if (!arguments.length) return y0;
+ y0 = _;
+ return area;
+ };
+ area.y1 = function(_) {
+ if (!arguments.length) return y1;
+ y1 = _;
+ return area;
+ };
+ area.defined = function(_) {
+ if (!arguments.length) return defined;
+ defined = _;
+ return area;
+ };
+ area.interpolate = function(_) {
+ if (!arguments.length) return interpolateKey;
+ if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key;
+ interpolateReverse = interpolate.reverse || interpolate;
+ L = interpolate.closed ? "M" : "L";
+ return area;
+ };
+ area.tension = function(_) {
+ if (!arguments.length) return tension;
+ tension = _;
+ return area;
+ };
+ return area;
+ }
+ d3_svg_lineStepBefore.reverse = d3_svg_lineStepAfter;
+ d3_svg_lineStepAfter.reverse = d3_svg_lineStepBefore;
+ d3.svg.area = function() {
+ return d3_svg_area(d3_identity);
+ };
+ d3.svg.area.radial = function() {
+ var area = d3_svg_area(d3_svg_lineRadial);
+ area.radius = area.x, delete area.x;
+ area.innerRadius = area.x0, delete area.x0;
+ area.outerRadius = area.x1, delete area.x1;
+ area.angle = area.y, delete area.y;
+ area.startAngle = area.y0, delete area.y0;
+ area.endAngle = area.y1, delete area.y1;
+ return area;
+ };
+ d3.svg.chord = function() {
+ var source = d3_source, target = d3_target, radius = d3_svg_chordRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle;
+ function chord(d, i) {
+ var s = subgroup(this, source, d, i), t = subgroup(this, target, d, i);
+ return "M" + s.p0 + arc(s.r, s.p1, s.a1 - s.a0) + (equals(s, t) ? curve(s.r, s.p1, s.r, s.p0) : curve(s.r, s.p1, t.r, t.p0) + arc(t.r, t.p1, t.a1 - t.a0) + curve(t.r, t.p1, s.r, s.p0)) + "Z";
+ }
+ function subgroup(self, f, d, i) {
+ var subgroup = f.call(self, d, i), r = radius.call(self, subgroup, i), a0 = startAngle.call(self, subgroup, i) + d3_svg_arcOffset, a1 = endAngle.call(self, subgroup, i) + d3_svg_arcOffset;
+ return {
+ r: r,
+ a0: a0,
+ a1: a1,
+ p0: [ r * Math.cos(a0), r * Math.sin(a0) ],
+ p1: [ r * Math.cos(a1), r * Math.sin(a1) ]
+ };
+ }
+ function equals(a, b) {
+ return a.a0 == b.a0 && a.a1 == b.a1;
+ }
+ function arc(r, p, a) {
+ return "A" + r + "," + r + " 0 " + +(a > π) + ",1 " + p;
+ }
+ function curve(r0, p0, r1, p1) {
+ return "Q 0,0 " + p1;
+ }
+ chord.radius = function(v) {
+ if (!arguments.length) return radius;
+ radius = d3_functor(v);
+ return chord;
+ };
+ chord.source = function(v) {
+ if (!arguments.length) return source;
+ source = d3_functor(v);
+ return chord;
+ };
+ chord.target = function(v) {
+ if (!arguments.length) return target;
+ target = d3_functor(v);
+ return chord;
+ };
+ chord.startAngle = function(v) {
+ if (!arguments.length) return startAngle;
+ startAngle = d3_functor(v);
+ return chord;
+ };
+ chord.endAngle = function(v) {
+ if (!arguments.length) return endAngle;
+ endAngle = d3_functor(v);
+ return chord;
+ };
+ return chord;
+ };
+ function d3_svg_chordRadius(d) {
+ return d.radius;
+ }
+ d3.svg.diagonal = function() {
+ var source = d3_source, target = d3_target, projection = d3_svg_diagonalProjection;
+ function diagonal(d, i) {
+ var p0 = source.call(this, d, i), p3 = target.call(this, d, i), m = (p0.y + p3.y) / 2, p = [ p0, {
+ x: p0.x,
+ y: m
+ }, {
+ x: p3.x,
+ y: m
+ }, p3 ];
+ p = p.map(projection);
+ return "M" + p[0] + "C" + p[1] + " " + p[2] + " " + p[3];
+ }
+ diagonal.source = function(x) {
+ if (!arguments.length) return source;
+ source = d3_functor(x);
+ return diagonal;
+ };
+ diagonal.target = function(x) {
+ if (!arguments.length) return target;
+ target = d3_functor(x);
+ return diagonal;
+ };
+ diagonal.projection = function(x) {
+ if (!arguments.length) return projection;
+ projection = x;
+ return diagonal;
+ };
+ return diagonal;
+ };
+ function d3_svg_diagonalProjection(d) {
+ return [ d.x, d.y ];
+ }
+ d3.svg.diagonal.radial = function() {
+ var diagonal = d3.svg.diagonal(), projection = d3_svg_diagonalProjection, projection_ = diagonal.projection;
+ diagonal.projection = function(x) {
+ return arguments.length ? projection_(d3_svg_diagonalRadialProjection(projection = x)) : projection;
+ };
+ return diagonal;
+ };
+ function d3_svg_diagonalRadialProjection(projection) {
+ return function() {
+ var d = projection.apply(this, arguments), r = d[0], a = d[1] + d3_svg_arcOffset;
+ return [ r * Math.cos(a), r * Math.sin(a) ];
+ };
+ }
+ d3.svg.symbol = function() {
+ var type = d3_svg_symbolType, size = d3_svg_symbolSize;
+ function symbol(d, i) {
+ return (d3_svg_symbols.get(type.call(this, d, i)) || d3_svg_symbolCircle)(size.call(this, d, i));
+ }
+ symbol.type = function(x) {
+ if (!arguments.length) return type;
+ type = d3_functor(x);
+ return symbol;
+ };
+ symbol.size = function(x) {
+ if (!arguments.length) return size;
+ size = d3_functor(x);
+ return symbol;
+ };
+ return symbol;
+ };
+ function d3_svg_symbolSize() {
+ return 64;
+ }
+ function d3_svg_symbolType() {
+ return "circle";
+ }
+ function d3_svg_symbolCircle(size) {
+ var r = Math.sqrt(size / π);
+ return "M0," + r + "A" + r + "," + r + " 0 1,1 0," + -r + "A" + r + "," + r + " 0 1,1 0," + r + "Z";
+ }
+ var d3_svg_symbols = d3.map({
+ circle: d3_svg_symbolCircle,
+ cross: function(size) {
+ var r = Math.sqrt(size / 5) / 2;
+ return "M" + -3 * r + "," + -r + "H" + -r + "V" + -3 * r + "H" + r + "V" + -r + "H" + 3 * r + "V" + r + "H" + r + "V" + 3 * r + "H" + -r + "V" + r + "H" + -3 * r + "Z";
+ },
+ diamond: function(size) {
+ var ry = Math.sqrt(size / (2 * d3_svg_symbolTan30)), rx = ry * d3_svg_symbolTan30;
+ return "M0," + -ry + "L" + rx + ",0" + " 0," + ry + " " + -rx + ",0" + "Z";
+ },
+ square: function(size) {
+ var r = Math.sqrt(size) / 2;
+ return "M" + -r + "," + -r + "L" + r + "," + -r + " " + r + "," + r + " " + -r + "," + r + "Z";
+ },
+ "triangle-down": function(size) {
+ var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2;
+ return "M0," + ry + "L" + rx + "," + -ry + " " + -rx + "," + -ry + "Z";
+ },
+ "triangle-up": function(size) {
+ var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2;
+ return "M0," + -ry + "L" + rx + "," + ry + " " + -rx + "," + ry + "Z";
+ }
+ });
+ d3.svg.symbolTypes = d3_svg_symbols.keys();
+ var d3_svg_symbolSqrt3 = Math.sqrt(3), d3_svg_symbolTan30 = Math.tan(30 * d3_radians);
+ function d3_transition(groups, id) {
+ d3_subclass(groups, d3_transitionPrototype);
+ groups.id = id;
+ return groups;
+ }
+ var d3_transitionPrototype = [], d3_transitionId = 0, d3_transitionInheritId, d3_transitionInherit;
+ d3_transitionPrototype.call = d3_selectionPrototype.call;
+ d3_transitionPrototype.empty = d3_selectionPrototype.empty;
+ d3_transitionPrototype.node = d3_selectionPrototype.node;
+ d3_transitionPrototype.size = d3_selectionPrototype.size;
+ d3.transition = function(selection) {
+ return arguments.length ? d3_transitionInheritId ? selection.transition() : selection : d3_selectionRoot.transition();
+ };
+ d3.transition.prototype = d3_transitionPrototype;
+ d3_transitionPrototype.select = function(selector) {
+ var id = this.id, subgroups = [], subgroup, subnode, node;
+ selector = d3_selection_selector(selector);
+ for (var j = -1, m = this.length; ++j < m; ) {
+ subgroups.push(subgroup = []);
+ for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
+ if ((node = group[i]) && (subnode = selector.call(node, node.__data__, i, j))) {
+ if ("__data__" in node) subnode.__data__ = node.__data__;
+ d3_transitionNode(subnode, i, id, node.__transition__[id]);
+ subgroup.push(subnode);
+ } else {
+ subgroup.push(null);
+ }
+ }
+ }
+ return d3_transition(subgroups, id);
+ };
+ d3_transitionPrototype.selectAll = function(selector) {
+ var id = this.id, subgroups = [], subgroup, subnodes, node, subnode, transition;
+ selector = d3_selection_selectorAll(selector);
+ for (var j = -1, m = this.length; ++j < m; ) {
+ for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
+ if (node = group[i]) {
+ transition = node.__transition__[id];
+ subnodes = selector.call(node, node.__data__, i, j);
+ subgroups.push(subgroup = []);
+ for (var k = -1, o = subnodes.length; ++k < o; ) {
+ if (subnode = subnodes[k]) d3_transitionNode(subnode, k, id, transition);
+ subgroup.push(subnode);
+ }
+ }
+ }
+ }
+ return d3_transition(subgroups, id);
+ };
+ d3_transitionPrototype.filter = function(filter) {
+ var subgroups = [], subgroup, group, node;
+ if (typeof filter !== "function") filter = d3_selection_filter(filter);
+ for (var j = 0, m = this.length; j < m; j++) {
+ subgroups.push(subgroup = []);
+ for (var group = this[j], i = 0, n = group.length; i < n; i++) {
+ if ((node = group[i]) && filter.call(node, node.__data__, i, j)) {
+ subgroup.push(node);
+ }
+ }
+ }
+ return d3_transition(subgroups, this.id);
+ };
+ d3_transitionPrototype.tween = function(name, tween) {
+ var id = this.id;
+ if (arguments.length < 2) return this.node().__transition__[id].tween.get(name);
+ return d3_selection_each(this, tween == null ? function(node) {
+ node.__transition__[id].tween.remove(name);
+ } : function(node) {
+ node.__transition__[id].tween.set(name, tween);
+ });
+ };
+ function d3_transition_tween(groups, name, value, tween) {
+ var id = groups.id;
+ return d3_selection_each(groups, typeof value === "function" ? function(node, i, j) {
+ node.__transition__[id].tween.set(name, tween(value.call(node, node.__data__, i, j)));
+ } : (value = tween(value), function(node) {
+ node.__transition__[id].tween.set(name, value);
+ }));
+ }
+ d3_transitionPrototype.attr = function(nameNS, value) {
+ if (arguments.length < 2) {
+ for (value in nameNS) this.attr(value, nameNS[value]);
+ return this;
+ }
+ var interpolate = nameNS == "transform" ? d3_interpolateTransform : d3_interpolate, name = d3.ns.qualify(nameNS);
+ function attrNull() {
+ this.removeAttribute(name);
+ }
+ function attrNullNS() {
+ this.removeAttributeNS(name.space, name.local);
+ }
+ function attrTween(b) {
+ return b == null ? attrNull : (b += "", function() {
+ var a = this.getAttribute(name), i;
+ return a !== b && (i = interpolate(a, b), function(t) {
+ this.setAttribute(name, i(t));
+ });
+ });
+ }
+ function attrTweenNS(b) {
+ return b == null ? attrNullNS : (b += "", function() {
+ var a = this.getAttributeNS(name.space, name.local), i;
+ return a !== b && (i = interpolate(a, b), function(t) {
+ this.setAttributeNS(name.space, name.local, i(t));
+ });
+ });
+ }
+ return d3_transition_tween(this, "attr." + nameNS, value, name.local ? attrTweenNS : attrTween);
+ };
+ d3_transitionPrototype.attrTween = function(nameNS, tween) {
+ var name = d3.ns.qualify(nameNS);
+ function attrTween(d, i) {
+ var f = tween.call(this, d, i, this.getAttribute(name));
+ return f && function(t) {
+ this.setAttribute(name, f(t));
+ };
+ }
+ function attrTweenNS(d, i) {
+ var f = tween.call(this, d, i, this.getAttributeNS(name.space, name.local));
+ return f && function(t) {
+ this.setAttributeNS(name.space, name.local, f(t));
+ };
+ }
+ return this.tween("attr." + nameNS, name.local ? attrTweenNS : attrTween);
+ };
+ d3_transitionPrototype.style = function(name, value, priority) {
+ var n = arguments.length;
+ if (n < 3) {
+ if (typeof name !== "string") {
+ if (n < 2) value = "";
+ for (priority in name) this.style(priority, name[priority], value);
+ return this;
+ }
+ priority = "";
+ }
+ function styleNull() {
+ this.style.removeProperty(name);
+ }
+ function styleString(b) {
+ return b == null ? styleNull : (b += "", function() {
+ var a = d3_window.getComputedStyle(this, null).getPropertyValue(name), i;
+ return a !== b && (i = d3_interpolate(a, b), function(t) {
+ this.style.setProperty(name, i(t), priority);
+ });
+ });
+ }
+ return d3_transition_tween(this, "style." + name, value, styleString);
+ };
+ d3_transitionPrototype.styleTween = function(name, tween, priority) {
+ if (arguments.length < 3) priority = "";
+ function styleTween(d, i) {
+ var f = tween.call(this, d, i, d3_window.getComputedStyle(this, null).getPropertyValue(name));
+ return f && function(t) {
+ this.style.setProperty(name, f(t), priority);
+ };
+ }
+ return this.tween("style." + name, styleTween);
+ };
+ d3_transitionPrototype.text = function(value) {
+ return d3_transition_tween(this, "text", value, d3_transition_text);
+ };
+ function d3_transition_text(b) {
+ if (b == null) b = "";
+ return function() {
+ this.textContent = b;
+ };
+ }
+ d3_transitionPrototype.remove = function() {
+ return this.each("end.transition", function() {
+ var p;
+ if (this.__transition__.count < 2 && (p = this.parentNode)) p.removeChild(this);
+ });
+ };
+ d3_transitionPrototype.ease = function(value) {
+ var id = this.id;
+ if (arguments.length < 1) return this.node().__transition__[id].ease;
+ if (typeof value !== "function") value = d3.ease.apply(d3, arguments);
+ return d3_selection_each(this, function(node) {
+ node.__transition__[id].ease = value;
+ });
+ };
+ d3_transitionPrototype.delay = function(value) {
+ var id = this.id;
+ return d3_selection_each(this, typeof value === "function" ? function(node, i, j) {
+ node.__transition__[id].delay = +value.call(node, node.__data__, i, j);
+ } : (value = +value, function(node) {
+ node.__transition__[id].delay = value;
+ }));
+ };
+ d3_transitionPrototype.duration = function(value) {
+ var id = this.id;
+ return d3_selection_each(this, typeof value === "function" ? function(node, i, j) {
+ node.__transition__[id].duration = Math.max(1, value.call(node, node.__data__, i, j));
+ } : (value = Math.max(1, value), function(node) {
+ node.__transition__[id].duration = value;
+ }));
+ };
+ d3_transitionPrototype.each = function(type, listener) {
+ var id = this.id;
+ if (arguments.length < 2) {
+ var inherit = d3_transitionInherit, inheritId = d3_transitionInheritId;
+ d3_transitionInheritId = id;
+ d3_selection_each(this, function(node, i, j) {
+ d3_transitionInherit = node.__transition__[id];
+ type.call(node, node.__data__, i, j);
+ });
+ d3_transitionInherit = inherit;
+ d3_transitionInheritId = inheritId;
+ } else {
+ d3_selection_each(this, function(node) {
+ var transition = node.__transition__[id];
+ (transition.event || (transition.event = d3.dispatch("start", "end"))).on(type, listener);
+ });
+ }
+ return this;
+ };
+ d3_transitionPrototype.transition = function() {
+ var id0 = this.id, id1 = ++d3_transitionId, subgroups = [], subgroup, group, node, transition;
+ for (var j = 0, m = this.length; j < m; j++) {
+ subgroups.push(subgroup = []);
+ for (var group = this[j], i = 0, n = group.length; i < n; i++) {
+ if (node = group[i]) {
+ transition = Object.create(node.__transition__[id0]);
+ transition.delay += transition.duration;
+ d3_transitionNode(node, i, id1, transition);
+ }
+ subgroup.push(node);
+ }
+ }
+ return d3_transition(subgroups, id1);
+ };
+ function d3_transitionNode(node, i, id, inherit) {
+ var lock = node.__transition__ || (node.__transition__ = {
+ active: 0,
+ count: 0
+ }), transition = lock[id];
+ if (!transition) {
+ var time = inherit.time;
+ transition = lock[id] = {
+ tween: new d3_Map(),
+ time: time,
+ ease: inherit.ease,
+ delay: inherit.delay,
+ duration: inherit.duration
+ };
+ ++lock.count;
+ d3.timer(function(elapsed) {
+ var d = node.__data__, ease = transition.ease, delay = transition.delay, duration = transition.duration, timer = d3_timer_active, tweened = [];
+ timer.t = delay + time;
+ if (delay <= elapsed) return start(elapsed - delay);
+ timer.c = start;
+ function start(elapsed) {
+ if (lock.active > id) return stop();
+ lock.active = id;
+ transition.event && transition.event.start.call(node, d, i);
+ transition.tween.forEach(function(key, value) {
+ if (value = value.call(node, d, i)) {
+ tweened.push(value);
+ }
+ });
+ d3.timer(function() {
+ timer.c = tick(elapsed || 1) ? d3_true : tick;
+ return 1;
+ }, 0, time);
+ }
+ function tick(elapsed) {
+ if (lock.active !== id) return stop();
+ var t = elapsed / duration, e = ease(t), n = tweened.length;
+ while (n > 0) {
+ tweened[--n].call(node, e);
+ }
+ if (t >= 1) {
+ transition.event && transition.event.end.call(node, d, i);
+ return stop();
+ }
+ }
+ function stop() {
+ if (--lock.count) delete lock[id]; else delete node.__transition__;
+ return 1;
+ }
+ }, 0, time);
+ }
+ }
+ d3.svg.axis = function() {
+ var scale = d3.scale.linear(), orient = d3_svg_axisDefaultOrient, innerTickSize = 6, outerTickSize = 6, tickPadding = 3, tickArguments_ = [ 10 ], tickValues = null, tickFormat_;
+ function axis(g) {
+ g.each(function() {
+ var g = d3.select(this);
+ var scale0 = this.__chart__ || scale, scale1 = this.__chart__ = scale.copy();
+ var ticks = tickValues == null ? scale1.ticks ? scale1.ticks.apply(scale1, tickArguments_) : scale1.domain() : tickValues, tickFormat = tickFormat_ == null ? scale1.tickFormat ? scale1.tickFormat.apply(scale1, tickArguments_) : d3_identity : tickFormat_, tick = g.selectAll(".tick").data(ticks, scale1), tickEnter = tick.enter().insert("g", ".domain").attr("class", "tick").style("opacity", ε), tickExit = d3.transition(tick.exit()).style("opacity", ε).remove(), tickUpdate = d3.transition(tick).style("opacity", 1), tickTransform;
+ var range = d3_scaleRange(scale1), path = g.selectAll(".domain").data([ 0 ]), pathUpdate = (path.enter().append("path").attr("class", "domain"),
+ d3.transition(path));
+ tickEnter.append("line");
+ tickEnter.append("text");
+ var lineEnter = tickEnter.select("line"), lineUpdate = tickUpdate.select("line"), text = tick.select("text").text(tickFormat), textEnter = tickEnter.select("text"), textUpdate = tickUpdate.select("text");
+ switch (orient) {
+ case "bottom":
+ {
+ tickTransform = d3_svg_axisX;
+ lineEnter.attr("y2", innerTickSize);
+ textEnter.attr("y", Math.max(innerTickSize, 0) + tickPadding);
+ lineUpdate.attr("x2", 0).attr("y2", innerTickSize);
+ textUpdate.attr("x", 0).attr("y", Math.max(innerTickSize, 0) + tickPadding);
+ text.attr("dy", ".71em").style("text-anchor", "middle");
+ pathUpdate.attr("d", "M" + range[0] + "," + outerTickSize + "V0H" + range[1] + "V" + outerTickSize);
+ break;
+ }
+
+ case "top":
+ {
+ tickTransform = d3_svg_axisX;
+ lineEnter.attr("y2", -innerTickSize);
+ textEnter.attr("y", -(Math.max(innerTickSize, 0) + tickPadding));
+ lineUpdate.attr("x2", 0).attr("y2", -innerTickSize);
+ textUpdate.attr("x", 0).attr("y", -(Math.max(innerTickSize, 0) + tickPadding));
+ text.attr("dy", "0em").style("text-anchor", "middle");
+ pathUpdate.attr("d", "M" + range[0] + "," + -outerTickSize + "V0H" + range[1] + "V" + -outerTickSize);
+ break;
+ }
+
+ case "left":
+ {
+ tickTransform = d3_svg_axisY;
+ lineEnter.attr("x2", -innerTickSize);
+ textEnter.attr("x", -(Math.max(innerTickSize, 0) + tickPadding));
+ lineUpdate.attr("x2", -innerTickSize).attr("y2", 0);
+ textUpdate.attr("x", -(Math.max(innerTickSize, 0) + tickPadding)).attr("y", 0);
+ text.attr("dy", ".32em").style("text-anchor", "end");
+ pathUpdate.attr("d", "M" + -outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + -outerTickSize);
+ break;
+ }
+
+ case "right":
+ {
+ tickTransform = d3_svg_axisY;
+ lineEnter.attr("x2", innerTickSize);
+ textEnter.attr("x", Math.max(innerTickSize, 0) + tickPadding);
+ lineUpdate.attr("x2", innerTickSize).attr("y2", 0);
+ textUpdate.attr("x", Math.max(innerTickSize, 0) + tickPadding).attr("y", 0);
+ text.attr("dy", ".32em").style("text-anchor", "start");
+ pathUpdate.attr("d", "M" + outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + outerTickSize);
+ break;
+ }
+ }
+ if (scale1.rangeBand) {
+ var x = scale1, dx = x.rangeBand() / 2;
+ scale0 = scale1 = function(d) {
+ return x(d) + dx;
+ };
+ } else if (scale0.rangeBand) {
+ scale0 = scale1;
+ } else {
+ tickExit.call(tickTransform, scale1);
+ }
+ tickEnter.call(tickTransform, scale0);
+ tickUpdate.call(tickTransform, scale1);
+ });
+ }
+ axis.scale = function(x) {
+ if (!arguments.length) return scale;
+ scale = x;
+ return axis;
+ };
+ axis.orient = function(x) {
+ if (!arguments.length) return orient;
+ orient = x in d3_svg_axisOrients ? x + "" : d3_svg_axisDefaultOrient;
+ return axis;
+ };
+ axis.ticks = function() {
+ if (!arguments.length) return tickArguments_;
+ tickArguments_ = arguments;
+ return axis;
+ };
+ axis.tickValues = function(x) {
+ if (!arguments.length) return tickValues;
+ tickValues = x;
+ return axis;
+ };
+ axis.tickFormat = function(x) {
+ if (!arguments.length) return tickFormat_;
+ tickFormat_ = x;
+ return axis;
+ };
+ axis.tickSize = function(x) {
+ var n = arguments.length;
+ if (!n) return innerTickSize;
+ innerTickSize = +x;
+ outerTickSize = +arguments[n - 1];
+ return axis;
+ };
+ axis.innerTickSize = function(x) {
+ if (!arguments.length) return innerTickSize;
+ innerTickSize = +x;
+ return axis;
+ };
+ axis.outerTickSize = function(x) {
+ if (!arguments.length) return outerTickSize;
+ outerTickSize = +x;
+ return axis;
+ };
+ axis.tickPadding = function(x) {
+ if (!arguments.length) return tickPadding;
+ tickPadding = +x;
+ return axis;
+ };
+ axis.tickSubdivide = function() {
+ return arguments.length && axis;
+ };
+ return axis;
+ };
+ var d3_svg_axisDefaultOrient = "bottom", d3_svg_axisOrients = {
+ top: 1,
+ right: 1,
+ bottom: 1,
+ left: 1
+ };
+ function d3_svg_axisX(selection, x) {
+ selection.attr("transform", function(d) {
+ return "translate(" + x(d) + ",0)";
+ });
+ }
+ function d3_svg_axisY(selection, y) {
+ selection.attr("transform", function(d) {
+ return "translate(0," + y(d) + ")";
+ });
+ }
+ d3.svg.brush = function() {
+ var event = d3_eventDispatch(brush, "brushstart", "brush", "brushend"), x = null, y = null, xExtent = [ 0, 0 ], yExtent = [ 0, 0 ], xExtentDomain, yExtentDomain, xClamp = true, yClamp = true, resizes = d3_svg_brushResizes[0];
+ function brush(g) {
+ g.each(function() {
+ var g = d3.select(this).style("pointer-events", "all").style("-webkit-tap-highlight-color", "rgba(0,0,0,0)").on("mousedown.brush", brushstart).on("touchstart.brush", brushstart);
+ var background = g.selectAll(".background").data([ 0 ]);
+ background.enter().append("rect").attr("class", "background").style("visibility", "hidden").style("cursor", "crosshair");
+ g.selectAll(".extent").data([ 0 ]).enter().append("rect").attr("class", "extent").style("cursor", "move");
+ var resize = g.selectAll(".resize").data(resizes, d3_identity);
+ resize.exit().remove();
+ resize.enter().append("g").attr("class", function(d) {
+ return "resize " + d;
+ }).style("cursor", function(d) {
+ return d3_svg_brushCursor[d];
+ }).append("rect").attr("x", function(d) {
+ return /[ew]$/.test(d) ? -3 : null;
+ }).attr("y", function(d) {
+ return /^[ns]/.test(d) ? -3 : null;
+ }).attr("width", 6).attr("height", 6).style("visibility", "hidden");
+ resize.style("display", brush.empty() ? "none" : null);
+ var gUpdate = d3.transition(g), backgroundUpdate = d3.transition(background), range;
+ if (x) {
+ range = d3_scaleRange(x);
+ backgroundUpdate.attr("x", range[0]).attr("width", range[1] - range[0]);
+ redrawX(gUpdate);
+ }
+ if (y) {
+ range = d3_scaleRange(y);
+ backgroundUpdate.attr("y", range[0]).attr("height", range[1] - range[0]);
+ redrawY(gUpdate);
+ }
+ redraw(gUpdate);
+ });
+ }
+ brush.event = function(g) {
+ g.each(function() {
+ var event_ = event.of(this, arguments), extent1 = {
+ x: xExtent,
+ y: yExtent,
+ i: xExtentDomain,
+ j: yExtentDomain
+ }, extent0 = this.__chart__ || extent1;
+ this.__chart__ = extent1;
+ if (d3_transitionInheritId) {
+ d3.select(this).transition().each("start.brush", function() {
+ xExtentDomain = extent0.i;
+ yExtentDomain = extent0.j;
+ xExtent = extent0.x;
+ yExtent = extent0.y;
+ event_({
+ type: "brushstart"
+ });
+ }).tween("brush:brush", function() {
+ var xi = d3_interpolateArray(xExtent, extent1.x), yi = d3_interpolateArray(yExtent, extent1.y);
+ xExtentDomain = yExtentDomain = null;
+ return function(t) {
+ xExtent = extent1.x = xi(t);
+ yExtent = extent1.y = yi(t);
+ event_({
+ type: "brush",
+ mode: "resize"
+ });
+ };
+ }).each("end.brush", function() {
+ xExtentDomain = extent1.i;
+ yExtentDomain = extent1.j;
+ event_({
+ type: "brush",
+ mode: "resize"
+ });
+ event_({
+ type: "brushend"
+ });
+ });
+ } else {
+ event_({
+ type: "brushstart"
+ });
+ event_({
+ type: "brush",
+ mode: "resize"
+ });
+ event_({
+ type: "brushend"
+ });
+ }
+ });
+ };
+ function redraw(g) {
+ g.selectAll(".resize").attr("transform", function(d) {
+ return "translate(" + xExtent[+/e$/.test(d)] + "," + yExtent[+/^s/.test(d)] + ")";
+ });
+ }
+ function redrawX(g) {
+ g.select(".extent").attr("x", xExtent[0]);
+ g.selectAll(".extent,.n>rect,.s>rect").attr("width", xExtent[1] - xExtent[0]);
+ }
+ function redrawY(g) {
+ g.select(".extent").attr("y", yExtent[0]);
+ g.selectAll(".extent,.e>rect,.w>rect").attr("height", yExtent[1] - yExtent[0]);
+ }
+ function brushstart() {
+ var target = this, eventTarget = d3.select(d3.event.target), event_ = event.of(target, arguments), g = d3.select(target), resizing = eventTarget.datum(), resizingX = !/^(n|s)$/.test(resizing) && x, resizingY = !/^(e|w)$/.test(resizing) && y, dragging = eventTarget.classed("extent"), dragRestore = d3_event_dragSuppress(), center, origin = d3.mouse(target), offset;
+ var w = d3.select(d3_window).on("keydown.brush", keydown).on("keyup.brush", keyup);
+ if (d3.event.changedTouches) {
+ w.on("touchmove.brush", brushmove).on("touchend.brush", brushend);
+ } else {
+ w.on("mousemove.brush", brushmove).on("mouseup.brush", brushend);
+ }
+ g.interrupt().selectAll("*").interrupt();
+ if (dragging) {
+ origin[0] = xExtent[0] - origin[0];
+ origin[1] = yExtent[0] - origin[1];
+ } else if (resizing) {
+ var ex = +/w$/.test(resizing), ey = +/^n/.test(resizing);
+ offset = [ xExtent[1 - ex] - origin[0], yExtent[1 - ey] - origin[1] ];
+ origin[0] = xExtent[ex];
+ origin[1] = yExtent[ey];
+ } else if (d3.event.altKey) center = origin.slice();
+ g.style("pointer-events", "none").selectAll(".resize").style("display", null);
+ d3.select("body").style("cursor", eventTarget.style("cursor"));
+ event_({
+ type: "brushstart"
+ });
+ brushmove();
+ function keydown() {
+ if (d3.event.keyCode == 32) {
+ if (!dragging) {
+ center = null;
+ origin[0] -= xExtent[1];
+ origin[1] -= yExtent[1];
+ dragging = 2;
+ }
+ d3_eventPreventDefault();
+ }
+ }
+ function keyup() {
+ if (d3.event.keyCode == 32 && dragging == 2) {
+ origin[0] += xExtent[1];
+ origin[1] += yExtent[1];
+ dragging = 0;
+ d3_eventPreventDefault();
+ }
+ }
+ function brushmove() {
+ var point = d3.mouse(target), moved = false;
+ if (offset) {
+ point[0] += offset[0];
+ point[1] += offset[1];
+ }
+ if (!dragging) {
+ if (d3.event.altKey) {
+ if (!center) center = [ (xExtent[0] + xExtent[1]) / 2, (yExtent[0] + yExtent[1]) / 2 ];
+ origin[0] = xExtent[+(point[0] < center[0])];
+ origin[1] = yExtent[+(point[1] < center[1])];
+ } else center = null;
+ }
+ if (resizingX && move1(point, x, 0)) {
+ redrawX(g);
+ moved = true;
+ }
+ if (resizingY && move1(point, y, 1)) {
+ redrawY(g);
+ moved = true;
+ }
+ if (moved) {
+ redraw(g);
+ event_({
+ type: "brush",
+ mode: dragging ? "move" : "resize"
+ });
+ }
+ }
+ function move1(point, scale, i) {
+ var range = d3_scaleRange(scale), r0 = range[0], r1 = range[1], position = origin[i], extent = i ? yExtent : xExtent, size = extent[1] - extent[0], min, max;
+ if (dragging) {
+ r0 -= position;
+ r1 -= size + position;
+ }
+ min = (i ? yClamp : xClamp) ? Math.max(r0, Math.min(r1, point[i])) : point[i];
+ if (dragging) {
+ max = (min += position) + size;
+ } else {
+ if (center) position = Math.max(r0, Math.min(r1, 2 * center[i] - min));
+ if (position < min) {
+ max = min;
+ min = position;
+ } else {
+ max = position;
+ }
+ }
+ if (extent[0] != min || extent[1] != max) {
+ if (i) yExtentDomain = null; else xExtentDomain = null;
+ extent[0] = min;
+ extent[1] = max;
+ return true;
+ }
+ }
+ function brushend() {
+ brushmove();
+ g.style("pointer-events", "all").selectAll(".resize").style("display", brush.empty() ? "none" : null);
+ d3.select("body").style("cursor", null);
+ w.on("mousemove.brush", null).on("mouseup.brush", null).on("touchmove.brush", null).on("touchend.brush", null).on("keydown.brush", null).on("keyup.brush", null);
+ dragRestore();
+ event_({
+ type: "brushend"
+ });
+ }
+ }
+ brush.x = function(z) {
+ if (!arguments.length) return x;
+ x = z;
+ resizes = d3_svg_brushResizes[!x << 1 | !y];
+ return brush;
+ };
+ brush.y = function(z) {
+ if (!arguments.length) return y;
+ y = z;
+ resizes = d3_svg_brushResizes[!x << 1 | !y];
+ return brush;
+ };
+ brush.clamp = function(z) {
+ if (!arguments.length) return x && y ? [ xClamp, yClamp ] : x ? xClamp : y ? yClamp : null;
+ if (x && y) xClamp = !!z[0], yClamp = !!z[1]; else if (x) xClamp = !!z; else if (y) yClamp = !!z;
+ return brush;
+ };
+ brush.extent = function(z) {
+ var x0, x1, y0, y1, t;
+ if (!arguments.length) {
+ if (x) {
+ if (xExtentDomain) {
+ x0 = xExtentDomain[0], x1 = xExtentDomain[1];
+ } else {
+ x0 = xExtent[0], x1 = xExtent[1];
+ if (x.invert) x0 = x.invert(x0), x1 = x.invert(x1);
+ if (x1 < x0) t = x0, x0 = x1, x1 = t;
+ }
+ }
+ if (y) {
+ if (yExtentDomain) {
+ y0 = yExtentDomain[0], y1 = yExtentDomain[1];
+ } else {
+ y0 = yExtent[0], y1 = yExtent[1];
+ if (y.invert) y0 = y.invert(y0), y1 = y.invert(y1);
+ if (y1 < y0) t = y0, y0 = y1, y1 = t;
+ }
+ }
+ return x && y ? [ [ x0, y0 ], [ x1, y1 ] ] : x ? [ x0, x1 ] : y && [ y0, y1 ];
+ }
+ if (x) {
+ x0 = z[0], x1 = z[1];
+ if (y) x0 = x0[0], x1 = x1[0];
+ xExtentDomain = [ x0, x1 ];
+ if (x.invert) x0 = x(x0), x1 = x(x1);
+ if (x1 < x0) t = x0, x0 = x1, x1 = t;
+ if (x0 != xExtent[0] || x1 != xExtent[1]) xExtent = [ x0, x1 ];
+ }
+ if (y) {
+ y0 = z[0], y1 = z[1];
+ if (x) y0 = y0[1], y1 = y1[1];
+ yExtentDomain = [ y0, y1 ];
+ if (y.invert) y0 = y(y0), y1 = y(y1);
+ if (y1 < y0) t = y0, y0 = y1, y1 = t;
+ if (y0 != yExtent[0] || y1 != yExtent[1]) yExtent = [ y0, y1 ];
+ }
+ return brush;
+ };
+ brush.clear = function() {
+ if (!brush.empty()) {
+ xExtent = [ 0, 0 ], yExtent = [ 0, 0 ];
+ xExtentDomain = yExtentDomain = null;
+ }
+ return brush;
+ };
+ brush.empty = function() {
+ return !!x && xExtent[0] == xExtent[1] || !!y && yExtent[0] == yExtent[1];
+ };
+ return d3.rebind(brush, event, "on");
+ };
+ var d3_svg_brushCursor = {
+ n: "ns-resize",
+ e: "ew-resize",
+ s: "ns-resize",
+ w: "ew-resize",
+ nw: "nwse-resize",
+ ne: "nesw-resize",
+ se: "nwse-resize",
+ sw: "nesw-resize"
+ };
+ var d3_svg_brushResizes = [ [ "n", "e", "s", "w", "nw", "ne", "se", "sw" ], [ "e", "w" ], [ "n", "s" ], [] ];
+ var d3_time_format = d3_time.format = d3_locale_enUS.timeFormat;
+ var d3_time_formatUtc = d3_time_format.utc;
+ var d3_time_formatIso = d3_time_formatUtc("%Y-%m-%dT%H:%M:%S.%LZ");
+ d3_time_format.iso = Date.prototype.toISOString && +new Date("2000-01-01T00:00:00.000Z") ? d3_time_formatIsoNative : d3_time_formatIso;
+ function d3_time_formatIsoNative(date) {
+ return date.toISOString();
+ }
+ d3_time_formatIsoNative.parse = function(string) {
+ var date = new Date(string);
+ return isNaN(date) ? null : date;
+ };
+ d3_time_formatIsoNative.toString = d3_time_formatIso.toString;
+ d3_time.second = d3_time_interval(function(date) {
+ return new d3_date(Math.floor(date / 1e3) * 1e3);
+ }, function(date, offset) {
+ date.setTime(date.getTime() + Math.floor(offset) * 1e3);
+ }, function(date) {
+ return date.getSeconds();
+ });
+ d3_time.seconds = d3_time.second.range;
+ d3_time.seconds.utc = d3_time.second.utc.range;
+ d3_time.minute = d3_time_interval(function(date) {
+ return new d3_date(Math.floor(date / 6e4) * 6e4);
+ }, function(date, offset) {
+ date.setTime(date.getTime() + Math.floor(offset) * 6e4);
+ }, function(date) {
+ return date.getMinutes();
+ });
+ d3_time.minutes = d3_time.minute.range;
+ d3_time.minutes.utc = d3_time.minute.utc.range;
+ d3_time.hour = d3_time_interval(function(date) {
+ var timezone = date.getTimezoneOffset() / 60;
+ return new d3_date((Math.floor(date / 36e5 - timezone) + timezone) * 36e5);
+ }, function(date, offset) {
+ date.setTime(date.getTime() + Math.floor(offset) * 36e5);
+ }, function(date) {
+ return date.getHours();
+ });
+ d3_time.hours = d3_time.hour.range;
+ d3_time.hours.utc = d3_time.hour.utc.range;
+ d3_time.month = d3_time_interval(function(date) {
+ date = d3_time.day(date);
+ date.setDate(1);
+ return date;
+ }, function(date, offset) {
+ date.setMonth(date.getMonth() + offset);
+ }, function(date) {
+ return date.getMonth();
+ });
+ d3_time.months = d3_time.month.range;
+ d3_time.months.utc = d3_time.month.utc.range;
+ function d3_time_scale(linear, methods, format) {
+ function scale(x) {
+ return linear(x);
+ }
+ scale.invert = function(x) {
+ return d3_time_scaleDate(linear.invert(x));
+ };
+ scale.domain = function(x) {
+ if (!arguments.length) return linear.domain().map(d3_time_scaleDate);
+ linear.domain(x);
+ return scale;
+ };
+ function tickMethod(extent, count) {
+ var span = extent[1] - extent[0], target = span / count, i = d3.bisect(d3_time_scaleSteps, target);
+ return i == d3_time_scaleSteps.length ? [ methods.year, d3_scale_linearTickRange(extent.map(function(d) {
+ return d / 31536e6;
+ }), count)[2] ] : !i ? [ d3_time_scaleMilliseconds, d3_scale_linearTickRange(extent, count)[2] ] : methods[target / d3_time_scaleSteps[i - 1] < d3_time_scaleSteps[i] / target ? i - 1 : i];
+ }
+ scale.nice = function(interval, skip) {
+ var domain = scale.domain(), extent = d3_scaleExtent(domain), method = interval == null ? tickMethod(extent, 10) : typeof interval === "number" && tickMethod(extent, interval);
+ if (method) interval = method[0], skip = method[1];
+ function skipped(date) {
+ return !isNaN(date) && !interval.range(date, d3_time_scaleDate(+date + 1), skip).length;
+ }
+ return scale.domain(d3_scale_nice(domain, skip > 1 ? {
+ floor: function(date) {
+ while (skipped(date = interval.floor(date))) date = d3_time_scaleDate(date - 1);
+ return date;
+ },
+ ceil: function(date) {
+ while (skipped(date = interval.ceil(date))) date = d3_time_scaleDate(+date + 1);
+ return date;
+ }
+ } : interval));
+ };
+ scale.ticks = function(interval, skip) {
+ var extent = d3_scaleExtent(scale.domain()), method = interval == null ? tickMethod(extent, 10) : typeof interval === "number" ? tickMethod(extent, interval) : !interval.range && [ {
+ range: interval
+ }, skip ];
+ if (method) interval = method[0], skip = method[1];
+ return interval.range(extent[0], d3_time_scaleDate(+extent[1] + 1), skip < 1 ? 1 : skip);
+ };
+ scale.tickFormat = function() {
+ return format;
+ };
+ scale.copy = function() {
+ return d3_time_scale(linear.copy(), methods, format);
+ };
+ return d3_scale_linearRebind(scale, linear);
+ }
+ function d3_time_scaleDate(t) {
+ return new Date(t);
+ }
+ var d3_time_scaleSteps = [ 1e3, 5e3, 15e3, 3e4, 6e4, 3e5, 9e5, 18e5, 36e5, 108e5, 216e5, 432e5, 864e5, 1728e5, 6048e5, 2592e6, 7776e6, 31536e6 ];
+ var d3_time_scaleLocalMethods = [ [ d3_time.second, 1 ], [ d3_time.second, 5 ], [ d3_time.second, 15 ], [ d3_time.second, 30 ], [ d3_time.minute, 1 ], [ d3_time.minute, 5 ], [ d3_time.minute, 15 ], [ d3_time.minute, 30 ], [ d3_time.hour, 1 ], [ d3_time.hour, 3 ], [ d3_time.hour, 6 ], [ d3_time.hour, 12 ], [ d3_time.day, 1 ], [ d3_time.day, 2 ], [ d3_time.week, 1 ], [ d3_time.month, 1 ], [ d3_time.month, 3 ], [ d3_time.year, 1 ] ];
+ var d3_time_scaleLocalFormat = d3_time_format.multi([ [ ".%L", function(d) {
+ return d.getMilliseconds();
+ } ], [ ":%S", function(d) {
+ return d.getSeconds();
+ } ], [ "%I:%M", function(d) {
+ return d.getMinutes();
+ } ], [ "%I %p", function(d) {
+ return d.getHours();
+ } ], [ "%a %d", function(d) {
+ return d.getDay() && d.getDate() != 1;
+ } ], [ "%b %d", function(d) {
+ return d.getDate() != 1;
+ } ], [ "%B", function(d) {
+ return d.getMonth();
+ } ], [ "%Y", d3_true ] ]);
+ var d3_time_scaleMilliseconds = {
+ range: function(start, stop, step) {
+ return d3.range(+start, +stop, step).map(d3_time_scaleDate);
+ },
+ floor: d3_identity,
+ ceil: d3_identity
+ };
+ d3_time_scaleLocalMethods.year = d3_time.year;
+ d3_time.scale = function() {
+ return d3_time_scale(d3.scale.linear(), d3_time_scaleLocalMethods, d3_time_scaleLocalFormat);
+ };
+ var d3_time_scaleUtcMethods = d3_time_scaleLocalMethods.map(function(m) {
+ return [ m[0].utc, m[1] ];
+ });
+ var d3_time_scaleUtcFormat = d3_time_formatUtc.multi([ [ ".%L", function(d) {
+ return d.getUTCMilliseconds();
+ } ], [ ":%S", function(d) {
+ return d.getUTCSeconds();
+ } ], [ "%I:%M", function(d) {
+ return d.getUTCMinutes();
+ } ], [ "%I %p", function(d) {
+ return d.getUTCHours();
+ } ], [ "%a %d", function(d) {
+ return d.getUTCDay() && d.getUTCDate() != 1;
+ } ], [ "%b %d", function(d) {
+ return d.getUTCDate() != 1;
+ } ], [ "%B", function(d) {
+ return d.getUTCMonth();
+ } ], [ "%Y", d3_true ] ]);
+ d3_time_scaleUtcMethods.year = d3_time.year.utc;
+ d3_time.scale.utc = function() {
+ return d3_time_scale(d3.scale.linear(), d3_time_scaleUtcMethods, d3_time_scaleUtcFormat);
+ };
+ d3.text = d3_xhrType(function(request) {
+ return request.responseText;
+ });
+ d3.json = function(url, callback) {
+ return d3_xhr(url, "application/json", d3_json, callback);
+ };
+ function d3_json(request) {
+ return JSON.parse(request.responseText);
+ }
+ d3.html = function(url, callback) {
+ return d3_xhr(url, "text/html", d3_html, callback);
+ };
+ function d3_html(request) {
+ var range = d3_document.createRange();
+ range.selectNode(d3_document.body);
+ return range.createContextualFragment(request.responseText);
+ }
+ d3.xml = d3_xhrType(function(request) {
+ return request.responseXML;
+ });
+ if (typeof define === "function" && define.amd) {
+ define(d3);
+ } else if (typeof module === "object" && module.exports) {
+ module.exports = d3;
+ } else {
+ this.d3 = d3;
+ }
+}(); \ No newline at end of file
diff --git a/devtools/client/shared/vendor/dagre-d3.js b/devtools/client/shared/vendor/dagre-d3.js
new file mode 100644
index 000000000..482ce827f
--- /dev/null
+++ b/devtools/client/shared/vendor/dagre-d3.js
@@ -0,0 +1,4560 @@
+;(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+var global=self;/**
+ * @license
+ * Copyright (c) 2012-2013 Chris Pettitt
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+global.dagreD3 = require('./index');
+
+},{"./index":2}],2:[function(require,module,exports){
+/**
+ * @license
+ * Copyright (c) 2012-2013 Chris Pettitt
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+module.exports = {
+ Digraph: require('graphlib').Digraph,
+ Renderer: require('./lib/Renderer'),
+ json: require('graphlib').converter.json,
+ layout: require('dagre').layout,
+ version: require('./lib/version')
+};
+
+},{"./lib/Renderer":3,"./lib/version":4,"dagre":11,"graphlib":28}],3:[function(require,module,exports){
+var layout = require('dagre').layout;
+
+var d3;
+try { d3 = require('d3'); } catch (_) { d3 = window.d3; }
+
+module.exports = Renderer;
+
+function Renderer() {
+ // Set up defaults...
+ this._layout = layout();
+
+ this.drawNodes(defaultDrawNodes);
+ this.drawEdgeLabels(defaultDrawEdgeLabels);
+ this.drawEdgePaths(defaultDrawEdgePaths);
+ this.positionNodes(defaultPositionNodes);
+ this.positionEdgeLabels(defaultPositionEdgeLabels);
+ this.positionEdgePaths(defaultPositionEdgePaths);
+ this.transition(defaultTransition);
+ this.postLayout(defaultPostLayout);
+ this.postRender(defaultPostRender);
+
+ this.edgeInterpolate('bundle');
+ this.edgeTension(0.95);
+}
+
+Renderer.prototype.layout = function(layout) {
+ if (!arguments.length) { return this._layout; }
+ this._layout = layout;
+ return this;
+};
+
+Renderer.prototype.drawNodes = function(drawNodes) {
+ if (!arguments.length) { return this._drawNodes; }
+ this._drawNodes = bind(drawNodes, this);
+ return this;
+};
+
+Renderer.prototype.drawEdgeLabels = function(drawEdgeLabels) {
+ if (!arguments.length) { return this._drawEdgeLabels; }
+ this._drawEdgeLabels = bind(drawEdgeLabels, this);
+ return this;
+};
+
+Renderer.prototype.drawEdgePaths = function(drawEdgePaths) {
+ if (!arguments.length) { return this._drawEdgePaths; }
+ this._drawEdgePaths = bind(drawEdgePaths, this);
+ return this;
+};
+
+Renderer.prototype.positionNodes = function(positionNodes) {
+ if (!arguments.length) { return this._positionNodes; }
+ this._positionNodes = bind(positionNodes, this);
+ return this;
+};
+
+Renderer.prototype.positionEdgeLabels = function(positionEdgeLabels) {
+ if (!arguments.length) { return this._positionEdgeLabels; }
+ this._positionEdgeLabels = bind(positionEdgeLabels, this);
+ return this;
+};
+
+Renderer.prototype.positionEdgePaths = function(positionEdgePaths) {
+ if (!arguments.length) { return this._positionEdgePaths; }
+ this._positionEdgePaths = bind(positionEdgePaths, this);
+ return this;
+};
+
+Renderer.prototype.transition = function(transition) {
+ if (!arguments.length) { return this._transition; }
+ this._transition = bind(transition, this);
+ return this;
+};
+
+Renderer.prototype.postLayout = function(postLayout) {
+ if (!arguments.length) { return this._postLayout; }
+ this._postLayout = bind(postLayout, this);
+ return this;
+};
+
+Renderer.prototype.postRender = function(postRender) {
+ if (!arguments.length) { return this._postRender; }
+ this._postRender = bind(postRender, this);
+ return this;
+};
+
+Renderer.prototype.edgeInterpolate = function(edgeInterpolate) {
+ if (!arguments.length) { return this._edgeInterpolate; }
+ this._edgeInterpolate = edgeInterpolate;
+ return this;
+};
+
+Renderer.prototype.edgeTension = function(edgeTension) {
+ if (!arguments.length) { return this._edgeTension; }
+ this._edgeTension = edgeTension;
+ return this;
+};
+
+Renderer.prototype.run = function(graph, svg) {
+ // First copy the input graph so that it is not changed by the rendering
+ // process.
+ graph = copyAndInitGraph(graph);
+
+ // Create layers
+ svg
+ .selectAll('g.edgePaths, g.edgeLabels, g.nodes')
+ .data(['edgePaths', 'edgeLabels', 'nodes'])
+ .enter()
+ .append('g')
+ .attr('class', function(d) { return d; });
+
+
+ // Create node and edge roots, attach labels, and capture dimension
+ // information for use with layout.
+ var svgNodes = this._drawNodes(graph, svg.select('g.nodes'));
+ var svgEdgeLabels = this._drawEdgeLabels(graph, svg.select('g.edgeLabels'));
+
+ svgNodes.each(function(u) { calculateDimensions(this, graph.node(u)); });
+ svgEdgeLabels.each(function(e) { calculateDimensions(this, graph.edge(e)); });
+
+ // Now apply the layout function
+ var result = runLayout(graph, this._layout);
+
+ // Run any user-specified post layout processing
+ this._postLayout(result, svg);
+
+ var svgEdgePaths = this._drawEdgePaths(graph, svg.select('g.edgePaths'));
+
+ // Apply the layout information to the graph
+ this._positionNodes(result, svgNodes);
+ this._positionEdgeLabels(result, svgEdgeLabels);
+ this._positionEdgePaths(result, svgEdgePaths);
+
+ this._postRender(result, svg);
+
+ return result;
+};
+
+function copyAndInitGraph(graph) {
+ var copy = graph.copy();
+
+ // Init labels if they were not present in the source graph
+ copy.nodes().forEach(function(u) {
+ var value = copy.node(u);
+ if (value === undefined) {
+ value = {};
+ copy.node(u, value);
+ }
+ if (!('label' in value)) { value.label = ''; }
+ });
+
+ copy.edges().forEach(function(e) {
+ var value = copy.edge(e);
+ if (value === undefined) {
+ value = {};
+ copy.edge(e, value);
+ }
+ if (!('label' in value)) { value.label = ''; }
+ });
+
+ return copy;
+}
+
+function calculateDimensions(group, value) {
+ var bbox = group.getBBox();
+ value.width = bbox.width;
+ value.height = bbox.height;
+}
+
+function runLayout(graph, layout) {
+ var result = layout.run(graph);
+
+ // Copy labels to the result graph
+ graph.eachNode(function(u, value) { result.node(u).label = value.label; });
+ graph.eachEdge(function(e, u, v, value) { result.edge(e).label = value.label; });
+
+ return result;
+}
+
+function defaultDrawNodes(g, root) {
+ var nodes = g.nodes().filter(function(u) { return !isComposite(g, u); });
+
+ var svgNodes = root
+ .selectAll('g.node')
+ .classed('enter', false)
+ .data(nodes, function(u) { return u; });
+
+ svgNodes.selectAll('*').remove();
+
+ svgNodes
+ .enter()
+ .append('g')
+ .style('opacity', 0)
+ .attr('class', 'node enter');
+
+ svgNodes.each(function(u) { addLabel(g.node(u), d3.select(this), 10, 10); });
+
+ this._transition(svgNodes.exit())
+ .style('opacity', 0)
+ .remove();
+
+ return svgNodes;
+}
+
+function defaultDrawEdgeLabels(g, root) {
+ var svgEdgeLabels = root
+ .selectAll('g.edgeLabel')
+ .classed('enter', false)
+ .data(g.edges(), function (e) { return e; });
+
+ svgEdgeLabels.selectAll('*').remove();
+
+ svgEdgeLabels
+ .enter()
+ .append('g')
+ .style('opacity', 0)
+ .attr('class', 'edgeLabel enter');
+
+ svgEdgeLabels.each(function(e) { addLabel(g.edge(e), d3.select(this), 0, 0); });
+
+ this._transition(svgEdgeLabels.exit())
+ .style('opacity', 0)
+ .remove();
+
+ return svgEdgeLabels;
+}
+
+var defaultDrawEdgePaths = function(g, root) {
+ var svgEdgePaths = root
+ .selectAll('g.edgePath')
+ .classed('enter', false)
+ .data(g.edges(), function(e) { return e; });
+
+ svgEdgePaths
+ .enter()
+ .append('g')
+ .attr('class', 'edgePath enter')
+ .append('path')
+ .style('opacity', 0)
+ .attr('marker-end', 'url(#arrowhead)');
+
+ this._transition(svgEdgePaths.exit())
+ .style('opacity', 0)
+ .remove();
+
+ return svgEdgePaths;
+};
+
+function defaultPositionNodes(g, svgNodes, svgNodesEnter) {
+ function transform(u) {
+ var value = g.node(u);
+ return 'translate(' + value.x + ',' + value.y + ')';
+ }
+
+ // For entering nodes, position immediately without transition
+ svgNodes.filter('.enter').attr('transform', transform);
+
+ this._transition(svgNodes)
+ .style('opacity', 1)
+ .attr('transform', transform);
+}
+
+function defaultPositionEdgeLabels(g, svgEdgeLabels) {
+ function transform(e) {
+ var value = g.edge(e);
+ var point = findMidPoint(value.points);
+ return 'translate(' + point.x + ',' + point.y + ')';
+ }
+
+ // For entering edge labels, position immediately without transition
+ svgEdgeLabels.filter('.enter').attr('transform', transform);
+
+ this._transition(svgEdgeLabels)
+ .style('opacity', 1)
+ .attr('transform', transform);
+}
+
+function defaultPositionEdgePaths(g, svgEdgePaths) {
+ var interpolate = this._edgeInterpolate,
+ tension = this._edgeTension;
+
+ function calcPoints(e) {
+ var value = g.edge(e);
+ var source = g.node(g.incidentNodes(e)[0]);
+ var target = g.node(g.incidentNodes(e)[1]);
+ var points = value.points.slice();
+
+ var p0 = points.length === 0 ? target : points[0];
+ var p1 = points.length === 0 ? source : points[points.length - 1];
+
+ points.unshift(intersectRect(source, p0));
+ // TODO: use bpodgursky's shortening algorithm here
+ points.push(intersectRect(target, p1));
+
+ return d3.svg.line()
+ .x(function(d) { return d.x; })
+ .y(function(d) { return d.y; })
+ .interpolate(interpolate)
+ .tension(tension)
+ (points);
+ }
+
+ svgEdgePaths.filter('.enter').selectAll('path')
+ .attr('d', calcPoints);
+
+ this._transition(svgEdgePaths.selectAll('path'))
+ .attr('d', calcPoints)
+ .style('opacity', 1);
+}
+
+// By default we do not use transitions
+function defaultTransition(selection) {
+ return selection;
+}
+
+function defaultPostLayout() {
+ // Do nothing
+}
+
+function defaultPostRender(graph, root) {
+ if (graph.isDirected() && root.select('#arrowhead').empty()) {
+ root
+ .append('svg:defs')
+ .append('svg:marker')
+ .attr('id', 'arrowhead')
+ .attr('viewBox', '0 0 10 10')
+ .attr('refX', 8)
+ .attr('refY', 5)
+ .attr('markerUnits', 'strokewidth')
+ .attr('markerWidth', 8)
+ .attr('markerHeight', 5)
+ .attr('orient', 'auto')
+ .attr('style', 'fill: #333')
+ .append('svg:path')
+ .attr('d', 'M 0 0 L 10 5 L 0 10 z');
+ }
+}
+
+function addLabel(node, root, marginX, marginY) {
+ // Add the rect first so that it appears behind the label
+ var label = node.label;
+ var rect = root.append('rect');
+ var labelSvg = root.append('g');
+
+ if (label[0] === '<') {
+ addForeignObjectLabel(label, labelSvg);
+ // No margin for HTML elements
+ marginX = marginY = 0;
+ } else {
+ addTextLabel(label,
+ labelSvg,
+ Math.floor(node.labelCols),
+ node.labelCut);
+ }
+
+ var bbox = root.node().getBBox();
+
+ labelSvg.attr('transform',
+ 'translate(' + (-bbox.width / 2) + ',' + (-bbox.height / 2) + ')');
+
+ rect
+ .attr('rx', 5)
+ .attr('ry', 5)
+ .attr('x', -(bbox.width / 2 + marginX))
+ .attr('y', -(bbox.height / 2 + marginY))
+ .attr('width', bbox.width + 2 * marginX)
+ .attr('height', bbox.height + 2 * marginY);
+}
+
+function addForeignObjectLabel(label, root) {
+ var fo = root
+ .append('foreignObject')
+ .attr('width', '100000');
+
+ var w, h;
+ fo
+ .append('xhtml:div')
+ .style('float', 'left')
+ // TODO find a better way to get dimensions for foreignObjects...
+ .html(function() { return label; })
+ .each(function() {
+ w = this.clientWidth;
+ h = this.clientHeight;
+ });
+
+ fo
+ .attr('width', w)
+ .attr('height', h);
+}
+
+function addTextLabel(label, root, labelCols, labelCut) {
+ if (labelCut === undefined) labelCut = "false";
+ labelCut = (labelCut.toString().toLowerCase() === "true");
+
+ var node = root
+ .append('text')
+ .attr('text-anchor', 'left');
+
+ label = label.replace(/\\n/g, "\n");
+
+ var arr = labelCols ? wordwrap(label, labelCols, labelCut) : label;
+ arr = arr.split("\n");
+ for (var i = 0; i < arr.length; i++) {
+ node
+ .append('tspan')
+ .attr('dy', '1em')
+ .attr('x', '1')
+ .text(arr[i]);
+ }
+}
+
+// Thanks to
+// http://james.padolsey.com/javascript/wordwrap-for-javascript/
+function wordwrap (str, width, cut, brk) {
+ brk = brk || '\n';
+ width = width || 75;
+ cut = cut || false;
+
+ if (!str) { return str; }
+
+ var regex = '.{1,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)');
+
+ return str.match( RegExp(regex, 'g') ).join( brk );
+}
+
+function findMidPoint(points) {
+ var midIdx = points.length / 2;
+ if (points.length % 2) {
+ return points[Math.floor(midIdx)];
+ } else {
+ var p0 = points[midIdx - 1];
+ var p1 = points[midIdx];
+ return {x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2};
+ }
+}
+
+function intersectRect(rect, point) {
+ var x = rect.x;
+ var y = rect.y;
+
+ // For now we only support rectangles
+
+ // Rectangle intersection algorithm from:
+ // http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
+ var dx = point.x - x;
+ var dy = point.y - y;
+ var w = rect.width / 2;
+ var h = rect.height / 2;
+
+ var sx, sy;
+ if (Math.abs(dy) * w > Math.abs(dx) * h) {
+ // Intersection is top or bottom of rect.
+ if (dy < 0) {
+ h = -h;
+ }
+ sx = dy === 0 ? 0 : h * dx / dy;
+ sy = h;
+ } else {
+ // Intersection is left or right of rect.
+ if (dx < 0) {
+ w = -w;
+ }
+ sx = w;
+ sy = dx === 0 ? 0 : w * dy / dx;
+ }
+
+ return {x: x + sx, y: y + sy};
+}
+
+function isComposite(g, u) {
+ return 'children' in g && g.children(u).length;
+}
+
+function bind(func, thisArg) {
+ // For some reason PhantomJS occassionally fails when using the builtin bind,
+ // so we check if it is available and if not, use a degenerate polyfill.
+ if (func.bind) {
+ return func.bind(thisArg);
+ }
+
+ return function() {
+ return func.apply(thisArg, arguments);
+ };
+}
+
+},{"d3":10,"dagre":11}],4:[function(require,module,exports){
+module.exports = '0.1.5';
+
+},{}],5:[function(require,module,exports){
+exports.Set = require('./lib/Set');
+exports.PriorityQueue = require('./lib/PriorityQueue');
+exports.version = require('./lib/version');
+
+},{"./lib/PriorityQueue":6,"./lib/Set":7,"./lib/version":9}],6:[function(require,module,exports){
+module.exports = PriorityQueue;
+
+/**
+ * A min-priority queue data structure. This algorithm is derived from Cormen,
+ * et al., "Introduction to Algorithms". The basic idea of a min-priority
+ * queue is that you can efficiently (in O(1) time) get the smallest key in
+ * the queue. Adding and removing elements takes O(log n) time. A key can
+ * have its priority decreased in O(log n) time.
+ */
+function PriorityQueue() {
+ this._arr = [];
+ this._keyIndices = {};
+}
+
+/**
+ * Returns the number of elements in the queue. Takes `O(1)` time.
+ */
+PriorityQueue.prototype.size = function() {
+ return this._arr.length;
+};
+
+/**
+ * Returns the keys that are in the queue. Takes `O(n)` time.
+ */
+PriorityQueue.prototype.keys = function() {
+ return this._arr.map(function(x) { return x.key; });
+};
+
+/**
+ * Returns `true` if **key** is in the queue and `false` if not.
+ */
+PriorityQueue.prototype.has = function(key) {
+ return key in this._keyIndices;
+};
+
+/**
+ * Returns the priority for **key**. If **key** is not present in the queue
+ * then this function returns `undefined`. Takes `O(1)` time.
+ *
+ * @param {Object} key
+ */
+PriorityQueue.prototype.priority = function(key) {
+ var index = this._keyIndices[key];
+ if (index !== undefined) {
+ return this._arr[index].priority;
+ }
+};
+
+/**
+ * Returns the key for the minimum element in this queue. If the queue is
+ * empty this function throws an Error. Takes `O(1)` time.
+ */
+PriorityQueue.prototype.min = function() {
+ if (this.size() === 0) {
+ throw new Error("Queue underflow");
+ }
+ return this._arr[0].key;
+};
+
+/**
+ * Inserts a new key into the priority queue. If the key already exists in
+ * the queue this function returns `false`; otherwise it will return `true`.
+ * Takes `O(n)` time.
+ *
+ * @param {Object} key the key to add
+ * @param {Number} priority the initial priority for the key
+ */
+PriorityQueue.prototype.add = function(key, priority) {
+ var keyIndices = this._keyIndices;
+ if (!(key in keyIndices)) {
+ var arr = this._arr;
+ var index = arr.length;
+ keyIndices[key] = index;
+ arr.push({key: key, priority: priority});
+ this._decrease(index);
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Removes and returns the smallest key in the queue. Takes `O(log n)` time.
+ */
+PriorityQueue.prototype.removeMin = function() {
+ this._swap(0, this._arr.length - 1);
+ var min = this._arr.pop();
+ delete this._keyIndices[min.key];
+ this._heapify(0);
+ return min.key;
+};
+
+/**
+ * Decreases the priority for **key** to **priority**. If the new priority is
+ * greater than the previous priority, this function will throw an Error.
+ *
+ * @param {Object} key the key for which to raise priority
+ * @param {Number} priority the new priority for the key
+ */
+PriorityQueue.prototype.decrease = function(key, priority) {
+ var index = this._keyIndices[key];
+ if (priority > this._arr[index].priority) {
+ throw new Error("New priority is greater than current priority. " +
+ "Key: " + key + " Old: " + this._arr[index].priority + " New: " + priority);
+ }
+ this._arr[index].priority = priority;
+ this._decrease(index);
+};
+
+PriorityQueue.prototype._heapify = function(i) {
+ var arr = this._arr;
+ var l = 2 * i,
+ r = l + 1,
+ largest = i;
+ if (l < arr.length) {
+ largest = arr[l].priority < arr[largest].priority ? l : largest;
+ if (r < arr.length) {
+ largest = arr[r].priority < arr[largest].priority ? r : largest;
+ }
+ if (largest !== i) {
+ this._swap(i, largest);
+ this._heapify(largest);
+ }
+ }
+};
+
+PriorityQueue.prototype._decrease = function(index) {
+ var arr = this._arr;
+ var priority = arr[index].priority;
+ var parent;
+ while (index !== 0) {
+ parent = index >> 1;
+ if (arr[parent].priority < priority) {
+ break;
+ }
+ this._swap(index, parent);
+ index = parent;
+ }
+};
+
+PriorityQueue.prototype._swap = function(i, j) {
+ var arr = this._arr;
+ var keyIndices = this._keyIndices;
+ var origArrI = arr[i];
+ var origArrJ = arr[j];
+ arr[i] = origArrJ;
+ arr[j] = origArrI;
+ keyIndices[origArrJ.key] = i;
+ keyIndices[origArrI.key] = j;
+};
+
+},{}],7:[function(require,module,exports){
+var util = require('./util');
+
+module.exports = Set;
+
+/**
+ * Constructs a new Set with an optional set of `initialKeys`.
+ *
+ * It is important to note that keys are coerced to String for most purposes
+ * with this object, similar to the behavior of JavaScript's Object. For
+ * example, the following will add only one key:
+ *
+ * var s = new Set();
+ * s.add(1);
+ * s.add("1");
+ *
+ * However, the type of the key is preserved internally so that `keys` returns
+ * the original key set uncoerced. For the above example, `keys` would return
+ * `[1]`.
+ */
+function Set(initialKeys) {
+ this._size = 0;
+ this._keys = {};
+
+ if (initialKeys) {
+ for (var i = 0, il = initialKeys.length; i < il; ++i) {
+ this.add(initialKeys[i]);
+ }
+ }
+}
+
+/**
+ * Returns a new Set that represents the set intersection of the array of given
+ * sets.
+ */
+Set.intersect = function(sets) {
+ if (sets.length === 0) {
+ return new Set();
+ }
+
+ var result = new Set(!util.isArray(sets[0]) ? sets[0].keys() : sets[0]);
+ for (var i = 1, il = sets.length; i < il; ++i) {
+ var resultKeys = result.keys(),
+ other = !util.isArray(sets[i]) ? sets[i] : new Set(sets[i]);
+ for (var j = 0, jl = resultKeys.length; j < jl; ++j) {
+ var key = resultKeys[j];
+ if (!other.has(key)) {
+ result.remove(key);
+ }
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Returns a new Set that represents the set union of the array of given sets.
+ */
+Set.union = function(sets) {
+ var totalElems = util.reduce(sets, function(lhs, rhs) {
+ return lhs + (rhs.size ? rhs.size() : rhs.length);
+ }, 0);
+ var arr = new Array(totalElems);
+
+ var k = 0;
+ for (var i = 0, il = sets.length; i < il; ++i) {
+ var cur = sets[i],
+ keys = !util.isArray(cur) ? cur.keys() : cur;
+ for (var j = 0, jl = keys.length; j < jl; ++j) {
+ arr[k++] = keys[j];
+ }
+ }
+
+ return new Set(arr);
+};
+
+/**
+ * Returns the size of this set in `O(1)` time.
+ */
+Set.prototype.size = function() {
+ return this._size;
+};
+
+/**
+ * Returns the keys in this set. Takes `O(n)` time.
+ */
+Set.prototype.keys = function() {
+ return values(this._keys);
+};
+
+/**
+ * Tests if a key is present in this Set. Returns `true` if it is and `false`
+ * if not. Takes `O(1)` time.
+ */
+Set.prototype.has = function(key) {
+ return key in this._keys;
+};
+
+/**
+ * Adds a new key to this Set if it is not already present. Returns `true` if
+ * the key was added and `false` if it was already present. Takes `O(1)` time.
+ */
+Set.prototype.add = function(key) {
+ if (!(key in this._keys)) {
+ this._keys[key] = key;
+ ++this._size;
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Removes a key from this Set. If the key was removed this function returns
+ * `true`. If not, it returns `false`. Takes `O(1)` time.
+ */
+Set.prototype.remove = function(key) {
+ if (key in this._keys) {
+ delete this._keys[key];
+ --this._size;
+ return true;
+ }
+ return false;
+};
+
+/*
+ * Returns an array of all values for properties of **o**.
+ */
+function values(o) {
+ var ks = Object.keys(o),
+ len = ks.length,
+ result = new Array(len),
+ i;
+ for (i = 0; i < len; ++i) {
+ result[i] = o[ks[i]];
+ }
+ return result;
+}
+
+},{"./util":8}],8:[function(require,module,exports){
+/*
+ * This polyfill comes from
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
+ */
+if(!Array.isArray) {
+ exports.isArray = function (vArg) {
+ return Object.prototype.toString.call(vArg) === '[object Array]';
+ };
+} else {
+ exports.isArray = Array.isArray;
+}
+
+/*
+ * Slightly adapted polyfill from
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
+ */
+if ('function' !== typeof Array.prototype.reduce) {
+ exports.reduce = function(array, callback, opt_initialValue) {
+ 'use strict';
+ if (null === array || 'undefined' === typeof array) {
+ // At the moment all modern browsers, that support strict mode, have
+ // native implementation of Array.prototype.reduce. For instance, IE8
+ // does not support strict mode, so this check is actually useless.
+ throw new TypeError(
+ 'Array.prototype.reduce called on null or undefined');
+ }
+ if ('function' !== typeof callback) {
+ throw new TypeError(callback + ' is not a function');
+ }
+ var index, value,
+ length = array.length >>> 0,
+ isValueSet = false;
+ if (1 < arguments.length) {
+ value = opt_initialValue;
+ isValueSet = true;
+ }
+ for (index = 0; length > index; ++index) {
+ if (array.hasOwnProperty(index)) {
+ if (isValueSet) {
+ value = callback(value, array[index], index, array);
+ }
+ else {
+ value = array[index];
+ isValueSet = true;
+ }
+ }
+ }
+ if (!isValueSet) {
+ throw new TypeError('Reduce of empty array with no initial value');
+ }
+ return value;
+ };
+} else {
+ exports.reduce = function(array, callback, opt_initialValue) {
+ return array.reduce(callback, opt_initialValue);
+ };
+}
+
+},{}],9:[function(require,module,exports){
+module.exports = '1.1.3';
+
+},{}],10:[function(require,module,exports){
+require("./d3");
+module.exports = d3;
+(function () { delete this.d3; })(); // unset global
+
+},{}],11:[function(require,module,exports){
+/*
+Copyright (c) 2012-2013 Chris Pettitt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+exports.Digraph = require("graphlib").Digraph;
+exports.Graph = require("graphlib").Graph;
+exports.layout = require("./lib/layout");
+exports.version = require("./lib/version");
+
+},{"./lib/layout":12,"./lib/version":27,"graphlib":28}],12:[function(require,module,exports){
+var util = require('./util'),
+ rank = require('./rank'),
+ order = require('./order'),
+ CGraph = require('graphlib').CGraph,
+ CDigraph = require('graphlib').CDigraph;
+
+module.exports = function() {
+ // External configuration
+ var config = {
+ // How much debug information to include?
+ debugLevel: 0,
+ // Max number of sweeps to perform in order phase
+ orderMaxSweeps: order.DEFAULT_MAX_SWEEPS,
+ // Use network simplex algorithm in ranking
+ rankSimplex: false,
+ // Rank direction. Valid values are (TB, LR)
+ rankDir: 'TB'
+ };
+
+ // Phase functions
+ var position = require('./position')();
+
+ // This layout object
+ var self = {};
+
+ self.orderIters = util.propertyAccessor(self, config, 'orderMaxSweeps');
+
+ self.rankSimplex = util.propertyAccessor(self, config, 'rankSimplex');
+
+ self.nodeSep = delegateProperty(position.nodeSep);
+ self.edgeSep = delegateProperty(position.edgeSep);
+ self.universalSep = delegateProperty(position.universalSep);
+ self.rankSep = delegateProperty(position.rankSep);
+ self.rankDir = util.propertyAccessor(self, config, 'rankDir');
+ self.debugAlignment = delegateProperty(position.debugAlignment);
+
+ self.debugLevel = util.propertyAccessor(self, config, 'debugLevel', function(x) {
+ util.log.level = x;
+ position.debugLevel(x);
+ });
+
+ self.run = util.time('Total layout', run);
+
+ self._normalize = normalize;
+
+ return self;
+
+ /*
+ * Constructs an adjacency graph using the nodes and edges specified through
+ * config. For each node and edge we add a property `dagre` that contains an
+ * object that will hold intermediate and final layout information. Some of
+ * the contents include:
+ *
+ * 1) A generated ID that uniquely identifies the object.
+ * 2) Dimension information for nodes (copied from the source node).
+ * 3) Optional dimension information for edges.
+ *
+ * After the adjacency graph is constructed the code no longer needs to use
+ * the original nodes and edges passed in via config.
+ */
+ function initLayoutGraph(inputGraph) {
+ var g = new CDigraph();
+
+ inputGraph.eachNode(function(u, value) {
+ if (value === undefined) value = {};
+ g.addNode(u, {
+ width: value.width,
+ height: value.height
+ });
+ if (value.hasOwnProperty('rank')) {
+ g.node(u).prefRank = value.rank;
+ }
+ });
+
+ // Set up subgraphs
+ if (inputGraph.parent) {
+ inputGraph.nodes().forEach(function(u) {
+ g.parent(u, inputGraph.parent(u));
+ });
+ }
+
+ inputGraph.eachEdge(function(e, u, v, value) {
+ if (value === undefined) value = {};
+ var newValue = {
+ e: e,
+ minLen: value.minLen || 1,
+ width: value.width || 0,
+ height: value.height || 0,
+ points: []
+ };
+
+ g.addEdge(null, u, v, newValue);
+ });
+
+ // Initial graph attributes
+ var graphValue = inputGraph.graph() || {};
+ g.graph({
+ rankDir: graphValue.rankDir || config.rankDir,
+ orderRestarts: graphValue.orderRestarts
+ });
+
+ return g;
+ }
+
+ function run(inputGraph) {
+ var rankSep = self.rankSep();
+ var g;
+ try {
+ // Build internal graph
+ g = util.time('initLayoutGraph', initLayoutGraph)(inputGraph);
+
+ if (g.order() === 0) {
+ return g;
+ }
+
+ // Make space for edge labels
+ g.eachEdge(function(e, s, t, a) {
+ a.minLen *= 2;
+ });
+ self.rankSep(rankSep / 2);
+
+ // Determine the rank for each node. Nodes with a lower rank will appear
+ // above nodes of higher rank.
+ util.time('rank.run', rank.run)(g, config.rankSimplex);
+
+ // Normalize the graph by ensuring that every edge is proper (each edge has
+ // a length of 1). We achieve this by adding dummy nodes to long edges,
+ // thus shortening them.
+ util.time('normalize', normalize)(g);
+
+ // Order the nodes so that edge crossings are minimized.
+ util.time('order', order)(g, config.orderMaxSweeps);
+
+ // Find the x and y coordinates for every node in the graph.
+ util.time('position', position.run)(g);
+
+ // De-normalize the graph by removing dummy nodes and augmenting the
+ // original long edges with coordinate information.
+ util.time('undoNormalize', undoNormalize)(g);
+
+ // Reverses points for edges that are in a reversed state.
+ util.time('fixupEdgePoints', fixupEdgePoints)(g);
+
+ // Restore delete edges and reverse edges that were reversed in the rank
+ // phase.
+ util.time('rank.restoreEdges', rank.restoreEdges)(g);
+
+ // Construct final result graph and return it
+ return util.time('createFinalGraph', createFinalGraph)(g, inputGraph.isDirected());
+ } finally {
+ self.rankSep(rankSep);
+ }
+ }
+
+ /*
+ * This function is responsible for 'normalizing' the graph. The process of
+ * normalization ensures that no edge in the graph has spans more than one
+ * rank. To do this it inserts dummy nodes as needed and links them by adding
+ * dummy edges. This function keeps enough information in the dummy nodes and
+ * edges to ensure that the original graph can be reconstructed later.
+ *
+ * This method assumes that the input graph is cycle free.
+ */
+ function normalize(g) {
+ var dummyCount = 0;
+ g.eachEdge(function(e, s, t, a) {
+ var sourceRank = g.node(s).rank;
+ var targetRank = g.node(t).rank;
+ if (sourceRank + 1 < targetRank) {
+ for (var u = s, rank = sourceRank + 1, i = 0; rank < targetRank; ++rank, ++i) {
+ var v = '_D' + (++dummyCount);
+ var node = {
+ width: a.width,
+ height: a.height,
+ edge: { id: e, source: s, target: t, attrs: a },
+ rank: rank,
+ dummy: true
+ };
+
+ // If this node represents a bend then we will use it as a control
+ // point. For edges with 2 segments this will be the center dummy
+ // node. For edges with more than two segments, this will be the
+ // first and last dummy node.
+ if (i === 0) node.index = 0;
+ else if (rank + 1 === targetRank) node.index = 1;
+
+ g.addNode(v, node);
+ g.addEdge(null, u, v, {});
+ u = v;
+ }
+ g.addEdge(null, u, t, {});
+ g.delEdge(e);
+ }
+ });
+ }
+
+ /*
+ * Reconstructs the graph as it was before normalization. The positions of
+ * dummy nodes are used to build an array of points for the original 'long'
+ * edge. Dummy nodes and edges are removed.
+ */
+ function undoNormalize(g) {
+ g.eachNode(function(u, a) {
+ if (a.dummy) {
+ if ('index' in a) {
+ var edge = a.edge;
+ if (!g.hasEdge(edge.id)) {
+ g.addEdge(edge.id, edge.source, edge.target, edge.attrs);
+ }
+ var points = g.edge(edge.id).points;
+ points[a.index] = { x: a.x, y: a.y, ul: a.ul, ur: a.ur, dl: a.dl, dr: a.dr };
+ }
+ g.delNode(u);
+ }
+ });
+ }
+
+ /*
+ * For each edge that was reversed during the `acyclic` step, reverse its
+ * array of points.
+ */
+ function fixupEdgePoints(g) {
+ g.eachEdge(function(e, s, t, a) { if (a.reversed) a.points.reverse(); });
+ }
+
+ function createFinalGraph(g, isDirected) {
+ var out = isDirected ? new CDigraph() : new CGraph();
+ out.graph(g.graph());
+ g.eachNode(function(u, value) { out.addNode(u, value); });
+ g.eachNode(function(u) { out.parent(u, g.parent(u)); });
+ g.eachEdge(function(e, u, v, value) {
+ out.addEdge(value.e, u, v, value);
+ });
+
+ // Attach bounding box information
+ var maxX = 0, maxY = 0;
+ g.eachNode(function(u, value) {
+ if (!g.children(u).length) {
+ maxX = Math.max(maxX, value.x + value.width / 2);
+ maxY = Math.max(maxY, value.y + value.height / 2);
+ }
+ });
+ g.eachEdge(function(e, u, v, value) {
+ var maxXPoints = Math.max.apply(Math, value.points.map(function(p) { return p.x; }));
+ var maxYPoints = Math.max.apply(Math, value.points.map(function(p) { return p.y; }));
+ maxX = Math.max(maxX, maxXPoints + value.width / 2);
+ maxY = Math.max(maxY, maxYPoints + value.height / 2);
+ });
+ out.graph().width = maxX;
+ out.graph().height = maxY;
+
+ return out;
+ }
+
+ /*
+ * Given a function, a new function is returned that invokes the given
+ * function. The return value from the function is always the `self` object.
+ */
+ function delegateProperty(f) {
+ return function() {
+ if (!arguments.length) return f();
+ f.apply(null, arguments);
+ return self;
+ };
+ }
+};
+
+
+},{"./order":13,"./position":18,"./rank":19,"./util":26,"graphlib":28}],13:[function(require,module,exports){
+var util = require('./util'),
+ crossCount = require('./order/crossCount'),
+ initLayerGraphs = require('./order/initLayerGraphs'),
+ initOrder = require('./order/initOrder'),
+ sortLayer = require('./order/sortLayer');
+
+module.exports = order;
+
+// The maximum number of sweeps to perform before finishing the order phase.
+var DEFAULT_MAX_SWEEPS = 24;
+order.DEFAULT_MAX_SWEEPS = DEFAULT_MAX_SWEEPS;
+
+/*
+ * Runs the order phase with the specified `graph, `maxSweeps`, and
+ * `debugLevel`. If `maxSweeps` is not specified we use `DEFAULT_MAX_SWEEPS`.
+ * If `debugLevel` is not set we assume 0.
+ */
+function order(g, maxSweeps) {
+ if (arguments.length < 2) {
+ maxSweeps = DEFAULT_MAX_SWEEPS;
+ }
+
+ var restarts = g.graph().orderRestarts || 0;
+
+ var layerGraphs = initLayerGraphs(g);
+ // TODO: remove this when we add back support for ordering clusters
+ layerGraphs.forEach(function(lg) {
+ lg = lg.filterNodes(function(u) { return !g.children(u).length; });
+ });
+
+ var iters = 0,
+ currentBestCC,
+ allTimeBestCC = Number.MAX_VALUE,
+ allTimeBest = {};
+
+ function saveAllTimeBest() {
+ g.eachNode(function(u, value) { allTimeBest[u] = value.order; });
+ }
+
+ for (var j = 0; j < Number(restarts) + 1 && allTimeBestCC !== 0; ++j) {
+ currentBestCC = Number.MAX_VALUE;
+ initOrder(g, restarts > 0);
+
+ util.log(2, 'Order phase start cross count: ' + g.graph().orderInitCC);
+
+ var i, lastBest, cc;
+ for (i = 0, lastBest = 0; lastBest < 4 && i < maxSweeps && currentBestCC > 0; ++i, ++lastBest, ++iters) {
+ sweep(g, layerGraphs, i);
+ cc = crossCount(g);
+ if (cc < currentBestCC) {
+ lastBest = 0;
+ currentBestCC = cc;
+ if (cc < allTimeBestCC) {
+ saveAllTimeBest();
+ allTimeBestCC = cc;
+ }
+ }
+ util.log(3, 'Order phase start ' + j + ' iter ' + i + ' cross count: ' + cc);
+ }
+ }
+
+ Object.keys(allTimeBest).forEach(function(u) {
+ if (!g.children || !g.children(u).length) {
+ g.node(u).order = allTimeBest[u];
+ }
+ });
+ g.graph().orderCC = allTimeBestCC;
+
+ util.log(2, 'Order iterations: ' + iters);
+ util.log(2, 'Order phase best cross count: ' + g.graph().orderCC);
+}
+
+function predecessorWeights(g, nodes) {
+ var weights = {};
+ nodes.forEach(function(u) {
+ weights[u] = g.inEdges(u).map(function(e) {
+ return g.node(g.source(e)).order;
+ });
+ });
+ return weights;
+}
+
+function successorWeights(g, nodes) {
+ var weights = {};
+ nodes.forEach(function(u) {
+ weights[u] = g.outEdges(u).map(function(e) {
+ return g.node(g.target(e)).order;
+ });
+ });
+ return weights;
+}
+
+function sweep(g, layerGraphs, iter) {
+ if (iter % 2 === 0) {
+ sweepDown(g, layerGraphs, iter);
+ } else {
+ sweepUp(g, layerGraphs, iter);
+ }
+}
+
+function sweepDown(g, layerGraphs) {
+ var cg;
+ for (i = 1; i < layerGraphs.length; ++i) {
+ cg = sortLayer(layerGraphs[i], cg, predecessorWeights(g, layerGraphs[i].nodes()));
+ }
+}
+
+function sweepUp(g, layerGraphs) {
+ var cg;
+ for (i = layerGraphs.length - 2; i >= 0; --i) {
+ sortLayer(layerGraphs[i], cg, successorWeights(g, layerGraphs[i].nodes()));
+ }
+}
+
+},{"./order/crossCount":14,"./order/initLayerGraphs":15,"./order/initOrder":16,"./order/sortLayer":17,"./util":26}],14:[function(require,module,exports){
+var util = require('../util');
+
+module.exports = crossCount;
+
+/*
+ * Returns the cross count for the given graph.
+ */
+function crossCount(g) {
+ var cc = 0;
+ var ordering = util.ordering(g);
+ for (var i = 1; i < ordering.length; ++i) {
+ cc += twoLayerCrossCount(g, ordering[i-1], ordering[i]);
+ }
+ return cc;
+}
+
+/*
+ * This function searches through a ranked and ordered graph and counts the
+ * number of edges that cross. This algorithm is derived from:
+ *
+ * W. Barth et al., Bilayer Cross Counting, JGAA, 8(2) 179–194 (2004)
+ */
+function twoLayerCrossCount(g, layer1, layer2) {
+ var indices = [];
+ layer1.forEach(function(u) {
+ var nodeIndices = [];
+ g.outEdges(u).forEach(function(e) { nodeIndices.push(g.node(g.target(e)).order); });
+ nodeIndices.sort(function(x, y) { return x - y; });
+ indices = indices.concat(nodeIndices);
+ });
+
+ var firstIndex = 1;
+ while (firstIndex < layer2.length) firstIndex <<= 1;
+
+ var treeSize = 2 * firstIndex - 1;
+ firstIndex -= 1;
+
+ var tree = [];
+ for (var i = 0; i < treeSize; ++i) { tree[i] = 0; }
+
+ var cc = 0;
+ indices.forEach(function(i) {
+ var treeIndex = i + firstIndex;
+ ++tree[treeIndex];
+ while (treeIndex > 0) {
+ if (treeIndex % 2) {
+ cc += tree[treeIndex + 1];
+ }
+ treeIndex = (treeIndex - 1) >> 1;
+ ++tree[treeIndex];
+ }
+ });
+
+ return cc;
+}
+
+},{"../util":26}],15:[function(require,module,exports){
+var nodesFromList = require('graphlib').filter.nodesFromList,
+ /* jshint -W079 */
+ Set = require('cp-data').Set;
+
+module.exports = initLayerGraphs;
+
+/*
+ * This function takes a compound layered graph, g, and produces an array of
+ * layer graphs. Each entry in the array represents a subgraph of nodes
+ * relevant for performing crossing reduction on that layer.
+ */
+function initLayerGraphs(g) {
+ var ranks = [];
+
+ function dfs(u) {
+ if (u === null) {
+ g.children(u).forEach(function(v) { dfs(v); });
+ return;
+ }
+
+ var value = g.node(u);
+ value.minRank = ('rank' in value) ? value.rank : Number.MAX_VALUE;
+ value.maxRank = ('rank' in value) ? value.rank : Number.MIN_VALUE;
+ var uRanks = new Set();
+ g.children(u).forEach(function(v) {
+ var rs = dfs(v);
+ uRanks = Set.union([uRanks, rs]);
+ value.minRank = Math.min(value.minRank, g.node(v).minRank);
+ value.maxRank = Math.max(value.maxRank, g.node(v).maxRank);
+ });
+
+ if ('rank' in value) uRanks.add(value.rank);
+
+ uRanks.keys().forEach(function(r) {
+ if (!(r in ranks)) ranks[r] = [];
+ ranks[r].push(u);
+ });
+
+ return uRanks;
+ }
+ dfs(null);
+
+ var layerGraphs = [];
+ ranks.forEach(function(us, rank) {
+ layerGraphs[rank] = g.filterNodes(nodesFromList(us));
+ });
+
+ return layerGraphs;
+}
+
+},{"cp-data":5,"graphlib":28}],16:[function(require,module,exports){
+var crossCount = require('./crossCount'),
+ util = require('../util');
+
+module.exports = initOrder;
+
+/*
+ * Given a graph with a set of layered nodes (i.e. nodes that have a `rank`
+ * attribute) this function attaches an `order` attribute that uniquely
+ * arranges each node of each rank. If no constraint graph is provided the
+ * order of the nodes in each rank is entirely arbitrary.
+ */
+function initOrder(g, random) {
+ var layers = [];
+
+ g.eachNode(function(u, value) {
+ var layer = layers[value.rank];
+ if (g.children && g.children(u).length > 0) return;
+ if (!layer) {
+ layer = layers[value.rank] = [];
+ }
+ layer.push(u);
+ });
+
+ layers.forEach(function(layer) {
+ if (random) {
+ util.shuffle(layer);
+ }
+ layer.forEach(function(u, i) {
+ g.node(u).order = i;
+ });
+ });
+
+ var cc = crossCount(g);
+ g.graph().orderInitCC = cc;
+ g.graph().orderCC = Number.MAX_VALUE;
+}
+
+},{"../util":26,"./crossCount":14}],17:[function(require,module,exports){
+var util = require('../util');
+/*
+ Digraph = require('graphlib').Digraph,
+ topsort = require('graphlib').alg.topsort,
+ nodesFromList = require('graphlib').filter.nodesFromList;
+*/
+
+module.exports = sortLayer;
+
+/*
+function sortLayer(g, cg, weights) {
+ var result = sortLayerSubgraph(g, null, cg, weights);
+ result.list.forEach(function(u, i) {
+ g.node(u).order = i;
+ });
+ return result.constraintGraph;
+}
+*/
+
+function sortLayer(g, cg, weights) {
+ var ordering = [];
+ var bs = {};
+ g.eachNode(function(u, value) {
+ ordering[value.order] = u;
+ var ws = weights[u];
+ if (ws.length) {
+ bs[u] = util.sum(ws) / ws.length;
+ }
+ });
+
+ var toSort = g.nodes().filter(function(u) { return bs[u] !== undefined; });
+ toSort.sort(function(x, y) {
+ return bs[x] - bs[y] || g.node(x).order - g.node(y).order;
+ });
+
+ for (var i = 0, j = 0, jl = toSort.length; j < jl; ++i) {
+ if (bs[ordering[i]] !== undefined) {
+ g.node(toSort[j++]).order = i;
+ }
+ }
+}
+
+// TOOD: re-enable constrained sorting once we have a strategy for handling
+// undefined barycenters.
+/*
+function sortLayerSubgraph(g, sg, cg, weights) {
+ cg = cg ? cg.filterNodes(nodesFromList(g.children(sg))) : new Digraph();
+
+ var nodeData = {};
+ g.children(sg).forEach(function(u) {
+ if (g.children(u).length) {
+ nodeData[u] = sortLayerSubgraph(g, u, cg, weights);
+ nodeData[u].firstSG = u;
+ nodeData[u].lastSG = u;
+ } else {
+ var ws = weights[u];
+ nodeData[u] = {
+ degree: ws.length,
+ barycenter: ws.length > 0 ? util.sum(ws) / ws.length : 0,
+ list: [u]
+ };
+ }
+ });
+
+ resolveViolatedConstraints(g, cg, nodeData);
+
+ var keys = Object.keys(nodeData);
+ keys.sort(function(x, y) {
+ return nodeData[x].barycenter - nodeData[y].barycenter;
+ });
+
+ var result = keys.map(function(u) { return nodeData[u]; })
+ .reduce(function(lhs, rhs) { return mergeNodeData(g, lhs, rhs); });
+ return result;
+}
+
+/*
+function mergeNodeData(g, lhs, rhs) {
+ var cg = mergeDigraphs(lhs.constraintGraph, rhs.constraintGraph);
+
+ if (lhs.lastSG !== undefined && rhs.firstSG !== undefined) {
+ if (cg === undefined) {
+ cg = new Digraph();
+ }
+ if (!cg.hasNode(lhs.lastSG)) { cg.addNode(lhs.lastSG); }
+ cg.addNode(rhs.firstSG);
+ cg.addEdge(null, lhs.lastSG, rhs.firstSG);
+ }
+
+ return {
+ degree: lhs.degree + rhs.degree,
+ barycenter: (lhs.barycenter * lhs.degree + rhs.barycenter * rhs.degree) /
+ (lhs.degree + rhs.degree),
+ list: lhs.list.concat(rhs.list),
+ firstSG: lhs.firstSG !== undefined ? lhs.firstSG : rhs.firstSG,
+ lastSG: rhs.lastSG !== undefined ? rhs.lastSG : lhs.lastSG,
+ constraintGraph: cg
+ };
+}
+
+function mergeDigraphs(lhs, rhs) {
+ if (lhs === undefined) return rhs;
+ if (rhs === undefined) return lhs;
+
+ lhs = lhs.copy();
+ rhs.nodes().forEach(function(u) { lhs.addNode(u); });
+ rhs.edges().forEach(function(e, u, v) { lhs.addEdge(null, u, v); });
+ return lhs;
+}
+
+function resolveViolatedConstraints(g, cg, nodeData) {
+ // Removes nodes `u` and `v` from `cg` and makes any edges incident on them
+ // incident on `w` instead.
+ function collapseNodes(u, v, w) {
+ // TODO original paper removes self loops, but it is not obvious when this would happen
+ cg.inEdges(u).forEach(function(e) {
+ cg.delEdge(e);
+ cg.addEdge(null, cg.source(e), w);
+ });
+
+ cg.outEdges(v).forEach(function(e) {
+ cg.delEdge(e);
+ cg.addEdge(null, w, cg.target(e));
+ });
+
+ cg.delNode(u);
+ cg.delNode(v);
+ }
+
+ var violated;
+ while ((violated = findViolatedConstraint(cg, nodeData)) !== undefined) {
+ var source = cg.source(violated),
+ target = cg.target(violated);
+
+ var v;
+ while ((v = cg.addNode(null)) && g.hasNode(v)) {
+ cg.delNode(v);
+ }
+
+ // Collapse barycenter and list
+ nodeData[v] = mergeNodeData(g, nodeData[source], nodeData[target]);
+ delete nodeData[source];
+ delete nodeData[target];
+
+ collapseNodes(source, target, v);
+ if (cg.incidentEdges(v).length === 0) { cg.delNode(v); }
+ }
+}
+
+function findViolatedConstraint(cg, nodeData) {
+ var us = topsort(cg);
+ for (var i = 0; i < us.length; ++i) {
+ var u = us[i];
+ var inEdges = cg.inEdges(u);
+ for (var j = 0; j < inEdges.length; ++j) {
+ var e = inEdges[j];
+ if (nodeData[cg.source(e)].barycenter >= nodeData[u].barycenter) {
+ return e;
+ }
+ }
+ }
+}
+*/
+
+},{"../util":26}],18:[function(require,module,exports){
+var util = require('./util');
+
+/*
+ * The algorithms here are based on Brandes and Köpf, "Fast and Simple
+ * Horizontal Coordinate Assignment".
+ */
+module.exports = function() {
+ // External configuration
+ var config = {
+ nodeSep: 50,
+ edgeSep: 10,
+ universalSep: null,
+ rankSep: 30
+ };
+
+ var self = {};
+
+ self.nodeSep = util.propertyAccessor(self, config, 'nodeSep');
+ self.edgeSep = util.propertyAccessor(self, config, 'edgeSep');
+ // If not null this separation value is used for all nodes and edges
+ // regardless of their widths. `nodeSep` and `edgeSep` are ignored with this
+ // option.
+ self.universalSep = util.propertyAccessor(self, config, 'universalSep');
+ self.rankSep = util.propertyAccessor(self, config, 'rankSep');
+ self.debugLevel = util.propertyAccessor(self, config, 'debugLevel');
+
+ self.run = run;
+
+ return self;
+
+ function run(g) {
+ g = g.filterNodes(util.filterNonSubgraphs(g));
+
+ var layering = util.ordering(g);
+
+ var conflicts = findConflicts(g, layering);
+
+ var xss = {};
+ ['u', 'd'].forEach(function(vertDir) {
+ if (vertDir === 'd') layering.reverse();
+
+ ['l', 'r'].forEach(function(horizDir) {
+ if (horizDir === 'r') reverseInnerOrder(layering);
+
+ var dir = vertDir + horizDir;
+ var align = verticalAlignment(g, layering, conflicts, vertDir === 'u' ? 'predecessors' : 'successors');
+ xss[dir]= horizontalCompaction(g, layering, align.pos, align.root, align.align);
+
+ if (config.debugLevel >= 3)
+ debugPositioning(vertDir + horizDir, g, layering, xss[dir]);
+
+ if (horizDir === 'r') flipHorizontally(xss[dir]);
+
+ if (horizDir === 'r') reverseInnerOrder(layering);
+ });
+
+ if (vertDir === 'd') layering.reverse();
+ });
+
+ balance(g, layering, xss);
+
+ g.eachNode(function(v) {
+ var xs = [];
+ for (var alignment in xss) {
+ var alignmentX = xss[alignment][v];
+ posXDebug(alignment, g, v, alignmentX);
+ xs.push(alignmentX);
+ }
+ xs.sort(function(x, y) { return x - y; });
+ posX(g, v, (xs[1] + xs[2]) / 2);
+ });
+
+ // Align y coordinates with ranks
+ var y = 0, reverseY = g.graph().rankDir === 'BT' || g.graph().rankDir === 'RL';
+ layering.forEach(function(layer) {
+ var maxHeight = util.max(layer.map(function(u) { return height(g, u); }));
+ y += maxHeight / 2;
+ layer.forEach(function(u) {
+ posY(g, u, reverseY ? -y : y);
+ });
+ y += maxHeight / 2 + config.rankSep;
+ });
+
+ // Translate layout so that top left corner of bounding rectangle has
+ // coordinate (0, 0).
+ var minX = util.min(g.nodes().map(function(u) { return posX(g, u) - width(g, u) / 2; }));
+ var minY = util.min(g.nodes().map(function(u) { return posY(g, u) - height(g, u) / 2; }));
+ g.eachNode(function(u) {
+ posX(g, u, posX(g, u) - minX);
+ posY(g, u, posY(g, u) - minY);
+ });
+ }
+
+ /*
+ * Generate an ID that can be used to represent any undirected edge that is
+ * incident on `u` and `v`.
+ */
+ function undirEdgeId(u, v) {
+ return u < v
+ ? u.toString().length + ':' + u + '-' + v
+ : v.toString().length + ':' + v + '-' + u;
+ }
+
+ function findConflicts(g, layering) {
+ var conflicts = {}, // Set of conflicting edge ids
+ pos = {}, // Position of node in its layer
+ prevLayer,
+ currLayer,
+ k0, // Position of the last inner segment in the previous layer
+ l, // Current position in the current layer (for iteration up to `l1`)
+ k1; // Position of the next inner segment in the previous layer or
+ // the position of the last element in the previous layer
+
+ if (layering.length <= 2) return conflicts;
+
+ function updateConflicts(v) {
+ var k = pos[v];
+ if (k < k0 || k > k1) {
+ conflicts[undirEdgeId(currLayer[l], v)] = true;
+ }
+ }
+
+ layering[1].forEach(function(u, i) { pos[u] = i; });
+ for (var i = 1; i < layering.length - 1; ++i) {
+ prevLayer = layering[i];
+ currLayer = layering[i+1];
+ k0 = 0;
+ l = 0;
+
+ // Scan current layer for next node that is incident to an inner segement
+ // between layering[i+1] and layering[i].
+ for (var l1 = 0; l1 < currLayer.length; ++l1) {
+ var u = currLayer[l1]; // Next inner segment in the current layer or
+ // last node in the current layer
+ pos[u] = l1;
+ k1 = undefined;
+
+ if (g.node(u).dummy) {
+ var uPred = g.predecessors(u)[0];
+ // Note: In the case of self loops and sideways edges it is possible
+ // for a dummy not to have a predecessor.
+ if (uPred !== undefined && g.node(uPred).dummy)
+ k1 = pos[uPred];
+ }
+ if (k1 === undefined && l1 === currLayer.length - 1)
+ k1 = prevLayer.length - 1;
+
+ if (k1 !== undefined) {
+ for (; l <= l1; ++l) {
+ g.predecessors(currLayer[l]).forEach(updateConflicts);
+ }
+ k0 = k1;
+ }
+ }
+ }
+
+ return conflicts;
+ }
+
+ function verticalAlignment(g, layering, conflicts, relationship) {
+ var pos = {}, // Position for a node in its layer
+ root = {}, // Root of the block that the node participates in
+ align = {}; // Points to the next node in the block or, if the last
+ // element in the block, points to the first block's root
+
+ layering.forEach(function(layer) {
+ layer.forEach(function(u, i) {
+ root[u] = u;
+ align[u] = u;
+ pos[u] = i;
+ });
+ });
+
+ layering.forEach(function(layer) {
+ var prevIdx = -1;
+ layer.forEach(function(v) {
+ var related = g[relationship](v), // Adjacent nodes from the previous layer
+ mid; // The mid point in the related array
+
+ if (related.length > 0) {
+ related.sort(function(x, y) { return pos[x] - pos[y]; });
+ mid = (related.length - 1) / 2;
+ related.slice(Math.floor(mid), Math.ceil(mid) + 1).forEach(function(u) {
+ if (align[v] === v) {
+ if (!conflicts[undirEdgeId(u, v)] && prevIdx < pos[u]) {
+ align[u] = v;
+ align[v] = root[v] = root[u];
+ prevIdx = pos[u];
+ }
+ }
+ });
+ }
+ });
+ });
+
+ return { pos: pos, root: root, align: align };
+ }
+
+ // This function deviates from the standard BK algorithm in two ways. First
+ // it takes into account the size of the nodes. Second it includes a fix to
+ // the original algorithm that is described in Carstens, "Node and Label
+ // Placement in a Layered Layout Algorithm".
+ function horizontalCompaction(g, layering, pos, root, align) {
+ var sink = {}, // Mapping of node id -> sink node id for class
+ maybeShift = {}, // Mapping of sink node id -> { class node id, min shift }
+ shift = {}, // Mapping of sink node id -> shift
+ pred = {}, // Mapping of node id -> predecessor node (or null)
+ xs = {}; // Calculated X positions
+
+ layering.forEach(function(layer) {
+ layer.forEach(function(u, i) {
+ sink[u] = u;
+ maybeShift[u] = {};
+ if (i > 0)
+ pred[u] = layer[i - 1];
+ });
+ });
+
+ function updateShift(toShift, neighbor, delta) {
+ if (!(neighbor in maybeShift[toShift])) {
+ maybeShift[toShift][neighbor] = delta;
+ } else {
+ maybeShift[toShift][neighbor] = Math.min(maybeShift[toShift][neighbor], delta);
+ }
+ }
+
+ function placeBlock(v) {
+ if (!(v in xs)) {
+ xs[v] = 0;
+ var w = v;
+ do {
+ if (pos[w] > 0) {
+ var u = root[pred[w]];
+ placeBlock(u);
+ if (sink[v] === v) {
+ sink[v] = sink[u];
+ }
+ var delta = sep(g, pred[w]) + sep(g, w);
+ if (sink[v] !== sink[u]) {
+ updateShift(sink[u], sink[v], xs[v] - xs[u] - delta);
+ } else {
+ xs[v] = Math.max(xs[v], xs[u] + delta);
+ }
+ }
+ w = align[w];
+ } while (w !== v);
+ }
+ }
+
+ // Root coordinates relative to sink
+ util.values(root).forEach(function(v) {
+ placeBlock(v);
+ });
+
+ // Absolute coordinates
+ // There is an assumption here that we've resolved shifts for any classes
+ // that begin at an earlier layer. We guarantee this by visiting layers in
+ // order.
+ layering.forEach(function(layer) {
+ layer.forEach(function(v) {
+ xs[v] = xs[root[v]];
+ if (v === root[v] && v === sink[v]) {
+ var minShift = 0;
+ if (v in maybeShift && Object.keys(maybeShift[v]).length > 0) {
+ minShift = util.min(Object.keys(maybeShift[v])
+ .map(function(u) {
+ return maybeShift[v][u] + (u in shift ? shift[u] : 0);
+ }
+ ));
+ }
+ shift[v] = minShift;
+ }
+ });
+ });
+
+ layering.forEach(function(layer) {
+ layer.forEach(function(v) {
+ xs[v] += shift[sink[root[v]]] || 0;
+ });
+ });
+
+ return xs;
+ }
+
+ function findMinCoord(g, layering, xs) {
+ return util.min(layering.map(function(layer) {
+ var u = layer[0];
+ return xs[u];
+ }));
+ }
+
+ function findMaxCoord(g, layering, xs) {
+ return util.max(layering.map(function(layer) {
+ var u = layer[layer.length - 1];
+ return xs[u];
+ }));
+ }
+
+ function balance(g, layering, xss) {
+ var min = {}, // Min coordinate for the alignment
+ max = {}, // Max coordinate for the alginment
+ smallestAlignment,
+ shift = {}; // Amount to shift a given alignment
+
+ function updateAlignment(v) {
+ xss[alignment][v] += shift[alignment];
+ }
+
+ var smallest = Number.POSITIVE_INFINITY;
+ for (var alignment in xss) {
+ var xs = xss[alignment];
+ min[alignment] = findMinCoord(g, layering, xs);
+ max[alignment] = findMaxCoord(g, layering, xs);
+ var w = max[alignment] - min[alignment];
+ if (w < smallest) {
+ smallest = w;
+ smallestAlignment = alignment;
+ }
+ }
+
+ // Determine how much to adjust positioning for each alignment
+ ['u', 'd'].forEach(function(vertDir) {
+ ['l', 'r'].forEach(function(horizDir) {
+ var alignment = vertDir + horizDir;
+ shift[alignment] = horizDir === 'l'
+ ? min[smallestAlignment] - min[alignment]
+ : max[smallestAlignment] - max[alignment];
+ });
+ });
+
+ // Find average of medians for xss array
+ for (alignment in xss) {
+ g.eachNode(updateAlignment);
+ }
+ }
+
+ function flipHorizontally(xs) {
+ for (var u in xs) {
+ xs[u] = -xs[u];
+ }
+ }
+
+ function reverseInnerOrder(layering) {
+ layering.forEach(function(layer) {
+ layer.reverse();
+ });
+ }
+
+ function width(g, u) {
+ switch (g.graph().rankDir) {
+ case 'LR': return g.node(u).height;
+ case 'RL': return g.node(u).height;
+ default: return g.node(u).width;
+ }
+ }
+
+ function height(g, u) {
+ switch(g.graph().rankDir) {
+ case 'LR': return g.node(u).width;
+ case 'RL': return g.node(u).width;
+ default: return g.node(u).height;
+ }
+ }
+
+ function sep(g, u) {
+ if (config.universalSep !== null) {
+ return config.universalSep;
+ }
+ var w = width(g, u);
+ var s = g.node(u).dummy ? config.edgeSep : config.nodeSep;
+ return (w + s) / 2;
+ }
+
+ function posX(g, u, x) {
+ if (g.graph().rankDir === 'LR' || g.graph().rankDir === 'RL') {
+ if (arguments.length < 3) {
+ return g.node(u).y;
+ } else {
+ g.node(u).y = x;
+ }
+ } else {
+ if (arguments.length < 3) {
+ return g.node(u).x;
+ } else {
+ g.node(u).x = x;
+ }
+ }
+ }
+
+ function posXDebug(name, g, u, x) {
+ if (g.graph().rankDir === 'LR' || g.graph().rankDir === 'RL') {
+ if (arguments.length < 3) {
+ return g.node(u)[name];
+ } else {
+ g.node(u)[name] = x;
+ }
+ } else {
+ if (arguments.length < 3) {
+ return g.node(u)[name];
+ } else {
+ g.node(u)[name] = x;
+ }
+ }
+ }
+
+ function posY(g, u, y) {
+ if (g.graph().rankDir === 'LR' || g.graph().rankDir === 'RL') {
+ if (arguments.length < 3) {
+ return g.node(u).x;
+ } else {
+ g.node(u).x = y;
+ }
+ } else {
+ if (arguments.length < 3) {
+ return g.node(u).y;
+ } else {
+ g.node(u).y = y;
+ }
+ }
+ }
+
+ function debugPositioning(align, g, layering, xs) {
+ layering.forEach(function(l, li) {
+ var u, xU;
+ l.forEach(function(v) {
+ var xV = xs[v];
+ if (u) {
+ var s = sep(g, u) + sep(g, v);
+ if (xV - xU < s)
+ console.log('Position phase: sep violation. Align: ' + align + '. Layer: ' + li + '. ' +
+ 'U: ' + u + ' V: ' + v + '. Actual sep: ' + (xV - xU) + ' Expected sep: ' + s);
+ }
+ u = v;
+ xU = xV;
+ });
+ });
+ }
+};
+
+},{"./util":26}],19:[function(require,module,exports){
+var util = require('./util'),
+ acyclic = require('./rank/acyclic'),
+ initRank = require('./rank/initRank'),
+ feasibleTree = require('./rank/feasibleTree'),
+ constraints = require('./rank/constraints'),
+ simplex = require('./rank/simplex'),
+ components = require('graphlib').alg.components,
+ filter = require('graphlib').filter;
+
+exports.run = run;
+exports.restoreEdges = restoreEdges;
+
+/*
+ * Heuristic function that assigns a rank to each node of the input graph with
+ * the intent of minimizing edge lengths, while respecting the `minLen`
+ * attribute of incident edges.
+ *
+ * Prerequisites:
+ *
+ * * Each edge in the input graph must have an assigned 'minLen' attribute
+ */
+function run(g, useSimplex) {
+ expandSelfLoops(g);
+
+ // If there are rank constraints on nodes, then build a new graph that
+ // encodes the constraints.
+ util.time('constraints.apply', constraints.apply)(g);
+
+ expandSidewaysEdges(g);
+
+ // Reverse edges to get an acyclic graph, we keep the graph in an acyclic
+ // state until the very end.
+ util.time('acyclic', acyclic)(g);
+
+ // Convert the graph into a flat graph for ranking
+ var flatGraph = g.filterNodes(util.filterNonSubgraphs(g));
+
+ // Assign an initial ranking using DFS.
+ initRank(flatGraph);
+
+ // For each component improve the assigned ranks.
+ components(flatGraph).forEach(function(cmpt) {
+ var subgraph = flatGraph.filterNodes(filter.nodesFromList(cmpt));
+ rankComponent(subgraph, useSimplex);
+ });
+
+ // Relax original constraints
+ util.time('constraints.relax', constraints.relax(g));
+
+ // When handling nodes with constrained ranks it is possible to end up with
+ // edges that point to previous ranks. Most of the subsequent algorithms assume
+ // that edges are pointing to successive ranks only. Here we reverse any "back
+ // edges" and mark them as such. The acyclic algorithm will reverse them as a
+ // post processing step.
+ util.time('reorientEdges', reorientEdges)(g);
+}
+
+function restoreEdges(g) {
+ acyclic.undo(g);
+}
+
+/*
+ * Expand self loops into three dummy nodes. One will sit above the incident
+ * node, one will be at the same level, and one below. The result looks like:
+ *
+ * /--<--x--->--\
+ * node y
+ * \--<--z--->--/
+ *
+ * Dummy nodes x, y, z give us the shape of a loop and node y is where we place
+ * the label.
+ *
+ * TODO: consolidate knowledge of dummy node construction.
+ * TODO: support minLen = 2
+ */
+function expandSelfLoops(g) {
+ g.eachEdge(function(e, u, v, a) {
+ if (u === v) {
+ var x = addDummyNode(g, e, u, v, a, 0, false),
+ y = addDummyNode(g, e, u, v, a, 1, true),
+ z = addDummyNode(g, e, u, v, a, 2, false);
+ g.addEdge(null, x, u, {minLen: 1, selfLoop: true});
+ g.addEdge(null, x, y, {minLen: 1, selfLoop: true});
+ g.addEdge(null, u, z, {minLen: 1, selfLoop: true});
+ g.addEdge(null, y, z, {minLen: 1, selfLoop: true});
+ g.delEdge(e);
+ }
+ });
+}
+
+function expandSidewaysEdges(g) {
+ g.eachEdge(function(e, u, v, a) {
+ if (u === v) {
+ var origEdge = a.originalEdge,
+ dummy = addDummyNode(g, origEdge.e, origEdge.u, origEdge.v, origEdge.value, 0, true);
+ g.addEdge(null, u, dummy, {minLen: 1});
+ g.addEdge(null, dummy, v, {minLen: 1});
+ g.delEdge(e);
+ }
+ });
+}
+
+function addDummyNode(g, e, u, v, a, index, isLabel) {
+ return g.addNode(null, {
+ width: isLabel ? a.width : 0,
+ height: isLabel ? a.height : 0,
+ edge: { id: e, source: u, target: v, attrs: a },
+ dummy: true,
+ index: index
+ });
+}
+
+function reorientEdges(g) {
+ g.eachEdge(function(e, u, v, value) {
+ if (g.node(u).rank > g.node(v).rank) {
+ g.delEdge(e);
+ value.reversed = true;
+ g.addEdge(e, v, u, value);
+ }
+ });
+}
+
+function rankComponent(subgraph, useSimplex) {
+ var spanningTree = feasibleTree(subgraph);
+
+ if (useSimplex) {
+ util.log(1, 'Using network simplex for ranking');
+ simplex(subgraph, spanningTree);
+ }
+ normalize(subgraph);
+}
+
+function normalize(g) {
+ var m = util.min(g.nodes().map(function(u) { return g.node(u).rank; }));
+ g.eachNode(function(u, node) { node.rank -= m; });
+}
+
+},{"./rank/acyclic":20,"./rank/constraints":21,"./rank/feasibleTree":22,"./rank/initRank":23,"./rank/simplex":25,"./util":26,"graphlib":28}],20:[function(require,module,exports){
+var util = require('../util');
+
+module.exports = acyclic;
+module.exports.undo = undo;
+
+/*
+ * This function takes a directed graph that may have cycles and reverses edges
+ * as appropriate to break these cycles. Each reversed edge is assigned a
+ * `reversed` attribute with the value `true`.
+ *
+ * There should be no self loops in the graph.
+ */
+function acyclic(g) {
+ var onStack = {},
+ visited = {},
+ reverseCount = 0;
+
+ function dfs(u) {
+ if (u in visited) return;
+ visited[u] = onStack[u] = true;
+ g.outEdges(u).forEach(function(e) {
+ var t = g.target(e),
+ value;
+
+ if (u === t) {
+ console.error('Warning: found self loop "' + e + '" for node "' + u + '"');
+ } else if (t in onStack) {
+ value = g.edge(e);
+ g.delEdge(e);
+ value.reversed = true;
+ ++reverseCount;
+ g.addEdge(e, t, u, value);
+ } else {
+ dfs(t);
+ }
+ });
+
+ delete onStack[u];
+ }
+
+ g.eachNode(function(u) { dfs(u); });
+
+ util.log(2, 'Acyclic Phase: reversed ' + reverseCount + ' edge(s)');
+
+ return reverseCount;
+}
+
+/*
+ * Given a graph that has had the acyclic operation applied, this function
+ * undoes that operation. More specifically, any edge with the `reversed`
+ * attribute is again reversed to restore the original direction of the edge.
+ */
+function undo(g) {
+ g.eachEdge(function(e, s, t, a) {
+ if (a.reversed) {
+ delete a.reversed;
+ g.delEdge(e);
+ g.addEdge(e, t, s, a);
+ }
+ });
+}
+
+},{"../util":26}],21:[function(require,module,exports){
+exports.apply = function(g) {
+ function dfs(sg) {
+ var rankSets = {};
+ g.children(sg).forEach(function(u) {
+ if (g.children(u).length) {
+ dfs(u);
+ return;
+ }
+
+ var value = g.node(u),
+ prefRank = value.prefRank;
+ if (prefRank !== undefined) {
+ if (!checkSupportedPrefRank(prefRank)) { return; }
+
+ if (!(prefRank in rankSets)) {
+ rankSets.prefRank = [u];
+ } else {
+ rankSets.prefRank.push(u);
+ }
+
+ var newU = rankSets[prefRank];
+ if (newU === undefined) {
+ newU = rankSets[prefRank] = g.addNode(null, { originalNodes: [] });
+ g.parent(newU, sg);
+ }
+
+ redirectInEdges(g, u, newU, prefRank === 'min');
+ redirectOutEdges(g, u, newU, prefRank === 'max');
+
+ // Save original node and remove it from reduced graph
+ g.node(newU).originalNodes.push({ u: u, value: value, parent: sg });
+ g.delNode(u);
+ }
+ });
+
+ addLightEdgesFromMinNode(g, sg, rankSets.min);
+ addLightEdgesToMaxNode(g, sg, rankSets.max);
+ }
+
+ dfs(null);
+};
+
+function checkSupportedPrefRank(prefRank) {
+ if (prefRank !== 'min' && prefRank !== 'max' && prefRank.indexOf('same_') !== 0) {
+ console.error('Unsupported rank type: ' + prefRank);
+ return false;
+ }
+ return true;
+}
+
+function redirectInEdges(g, u, newU, reverse) {
+ g.inEdges(u).forEach(function(e) {
+ var origValue = g.edge(e),
+ value;
+ if (origValue.originalEdge) {
+ value = origValue;
+ } else {
+ value = {
+ originalEdge: { e: e, u: g.source(e), v: g.target(e), value: origValue },
+ minLen: g.edge(e).minLen
+ };
+ }
+
+ // Do not reverse edges for self-loops.
+ if (origValue.selfLoop) {
+ reverse = false;
+ }
+
+ if (reverse) {
+ // Ensure that all edges to min are reversed
+ g.addEdge(null, newU, g.source(e), value);
+ value.reversed = true;
+ } else {
+ g.addEdge(null, g.source(e), newU, value);
+ }
+ });
+}
+
+function redirectOutEdges(g, u, newU, reverse) {
+ g.outEdges(u).forEach(function(e) {
+ var origValue = g.edge(e),
+ value;
+ if (origValue.originalEdge) {
+ value = origValue;
+ } else {
+ value = {
+ originalEdge: { e: e, u: g.source(e), v: g.target(e), value: origValue },
+ minLen: g.edge(e).minLen
+ };
+ }
+
+ // Do not reverse edges for self-loops.
+ if (origValue.selfLoop) {
+ reverse = false;
+ }
+
+ if (reverse) {
+ // Ensure that all edges from max are reversed
+ g.addEdge(null, g.target(e), newU, value);
+ value.reversed = true;
+ } else {
+ g.addEdge(null, newU, g.target(e), value);
+ }
+ });
+}
+
+function addLightEdgesFromMinNode(g, sg, minNode) {
+ if (minNode !== undefined) {
+ g.children(sg).forEach(function(u) {
+ // The dummy check ensures we don't add an edge if the node is involved
+ // in a self loop or sideways edge.
+ if (u !== minNode && !g.outEdges(minNode, u).length && !g.node(u).dummy) {
+ g.addEdge(null, minNode, u, { minLen: 0 });
+ }
+ });
+ }
+}
+
+function addLightEdgesToMaxNode(g, sg, maxNode) {
+ if (maxNode !== undefined) {
+ g.children(sg).forEach(function(u) {
+ // The dummy check ensures we don't add an edge if the node is involved
+ // in a self loop or sideways edge.
+ if (u !== maxNode && !g.outEdges(u, maxNode).length && !g.node(u).dummy) {
+ g.addEdge(null, u, maxNode, { minLen: 0 });
+ }
+ });
+ }
+}
+
+/*
+ * This function "relaxes" the constraints applied previously by the "apply"
+ * function. It expands any nodes that were collapsed and assigns the rank of
+ * the collapsed node to each of the expanded nodes. It also restores the
+ * original edges and removes any dummy edges pointing at the collapsed nodes.
+ *
+ * Note that the process of removing collapsed nodes also removes dummy edges
+ * automatically.
+ */
+exports.relax = function(g) {
+ // Save original edges
+ var originalEdges = [];
+ g.eachEdge(function(e, u, v, value) {
+ var originalEdge = value.originalEdge;
+ if (originalEdge) {
+ originalEdges.push(originalEdge);
+ }
+ });
+
+ // Expand collapsed nodes
+ g.eachNode(function(u, value) {
+ var originalNodes = value.originalNodes;
+ if (originalNodes) {
+ originalNodes.forEach(function(originalNode) {
+ originalNode.value.rank = value.rank;
+ g.addNode(originalNode.u, originalNode.value);
+ g.parent(originalNode.u, originalNode.parent);
+ });
+ g.delNode(u);
+ }
+ });
+
+ // Restore original edges
+ originalEdges.forEach(function(edge) {
+ g.addEdge(edge.e, edge.u, edge.v, edge.value);
+ });
+};
+
+},{}],22:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require('cp-data').Set,
+/* jshint +W079 */
+ Digraph = require('graphlib').Digraph,
+ util = require('../util');
+
+module.exports = feasibleTree;
+
+/*
+ * Given an acyclic graph with each node assigned a `rank` attribute, this
+ * function constructs and returns a spanning tree. This function may reduce
+ * the length of some edges from the initial rank assignment while maintaining
+ * the `minLen` specified by each edge.
+ *
+ * Prerequisites:
+ *
+ * * The input graph is acyclic
+ * * Each node in the input graph has an assigned `rank` attribute
+ * * Each edge in the input graph has an assigned `minLen` attribute
+ *
+ * Outputs:
+ *
+ * A feasible spanning tree for the input graph (i.e. a spanning tree that
+ * respects each graph edge's `minLen` attribute) represented as a Digraph with
+ * a `root` attribute on graph.
+ *
+ * Nodes have the same id and value as that in the input graph.
+ *
+ * Edges in the tree have arbitrarily assigned ids. The attributes for edges
+ * include `reversed`. `reversed` indicates that the edge is a
+ * back edge in the input graph.
+ */
+function feasibleTree(g) {
+ var remaining = new Set(g.nodes()),
+ tree = new Digraph();
+
+ if (remaining.size() === 1) {
+ var root = g.nodes()[0];
+ tree.addNode(root, {});
+ tree.graph({ root: root });
+ return tree;
+ }
+
+ function addTightEdges(v) {
+ var continueToScan = true;
+ g.predecessors(v).forEach(function(u) {
+ if (remaining.has(u) && !slack(g, u, v)) {
+ if (remaining.has(v)) {
+ tree.addNode(v, {});
+ remaining.remove(v);
+ tree.graph({ root: v });
+ }
+
+ tree.addNode(u, {});
+ tree.addEdge(null, u, v, { reversed: true });
+ remaining.remove(u);
+ addTightEdges(u);
+ continueToScan = false;
+ }
+ });
+
+ g.successors(v).forEach(function(w) {
+ if (remaining.has(w) && !slack(g, v, w)) {
+ if (remaining.has(v)) {
+ tree.addNode(v, {});
+ remaining.remove(v);
+ tree.graph({ root: v });
+ }
+
+ tree.addNode(w, {});
+ tree.addEdge(null, v, w, {});
+ remaining.remove(w);
+ addTightEdges(w);
+ continueToScan = false;
+ }
+ });
+ return continueToScan;
+ }
+
+ function createTightEdge() {
+ var minSlack = Number.MAX_VALUE;
+ remaining.keys().forEach(function(v) {
+ g.predecessors(v).forEach(function(u) {
+ if (!remaining.has(u)) {
+ var edgeSlack = slack(g, u, v);
+ if (Math.abs(edgeSlack) < Math.abs(minSlack)) {
+ minSlack = -edgeSlack;
+ }
+ }
+ });
+
+ g.successors(v).forEach(function(w) {
+ if (!remaining.has(w)) {
+ var edgeSlack = slack(g, v, w);
+ if (Math.abs(edgeSlack) < Math.abs(minSlack)) {
+ minSlack = edgeSlack;
+ }
+ }
+ });
+ });
+
+ tree.eachNode(function(u) { g.node(u).rank -= minSlack; });
+ }
+
+ while (remaining.size()) {
+ var nodesToSearch = !tree.order() ? remaining.keys() : tree.nodes();
+ for (var i = 0, il = nodesToSearch.length;
+ i < il && addTightEdges(nodesToSearch[i]);
+ ++i);
+ if (remaining.size()) {
+ createTightEdge();
+ }
+ }
+
+ return tree;
+}
+
+function slack(g, u, v) {
+ var rankDiff = g.node(v).rank - g.node(u).rank;
+ var maxMinLen = util.max(g.outEdges(u, v)
+ .map(function(e) { return g.edge(e).minLen; }));
+ return rankDiff - maxMinLen;
+}
+
+},{"../util":26,"cp-data":5,"graphlib":28}],23:[function(require,module,exports){
+var util = require('../util'),
+ topsort = require('graphlib').alg.topsort;
+
+module.exports = initRank;
+
+/*
+ * Assigns a `rank` attribute to each node in the input graph and ensures that
+ * this rank respects the `minLen` attribute of incident edges.
+ *
+ * Prerequisites:
+ *
+ * * The input graph must be acyclic
+ * * Each edge in the input graph must have an assigned 'minLen' attribute
+ */
+function initRank(g) {
+ var sorted = topsort(g);
+
+ sorted.forEach(function(u) {
+ var inEdges = g.inEdges(u);
+ if (inEdges.length === 0) {
+ g.node(u).rank = 0;
+ return;
+ }
+
+ var minLens = inEdges.map(function(e) {
+ return g.node(g.source(e)).rank + g.edge(e).minLen;
+ });
+ g.node(u).rank = util.max(minLens);
+ });
+}
+
+},{"../util":26,"graphlib":28}],24:[function(require,module,exports){
+module.exports = {
+ slack: slack
+};
+
+/*
+ * A helper to calculate the slack between two nodes (`u` and `v`) given a
+ * `minLen` constraint. The slack represents how much the distance between `u`
+ * and `v` could shrink while maintaining the `minLen` constraint. If the value
+ * is negative then the constraint is currently violated.
+ *
+ This function requires that `u` and `v` are in `graph` and they both have a
+ `rank` attribute.
+ */
+function slack(graph, u, v, minLen) {
+ return Math.abs(graph.node(u).rank - graph.node(v).rank) - minLen;
+}
+
+},{}],25:[function(require,module,exports){
+var util = require('../util'),
+ rankUtil = require('./rankUtil');
+
+module.exports = simplex;
+
+function simplex(graph, spanningTree) {
+ // The network simplex algorithm repeatedly replaces edges of
+ // the spanning tree with negative cut values until no such
+ // edge exists.
+ initCutValues(graph, spanningTree);
+ while (true) {
+ var e = leaveEdge(spanningTree);
+ if (e === null) break;
+ var f = enterEdge(graph, spanningTree, e);
+ exchange(graph, spanningTree, e, f);
+ }
+}
+
+/*
+ * Set the cut values of edges in the spanning tree by a depth-first
+ * postorder traversal. The cut value corresponds to the cost, in
+ * terms of a ranking's edge length sum, of lengthening an edge.
+ * Negative cut values typically indicate edges that would yield a
+ * smaller edge length sum if they were lengthened.
+ */
+function initCutValues(graph, spanningTree) {
+ computeLowLim(spanningTree);
+
+ spanningTree.eachEdge(function(id, u, v, treeValue) {
+ treeValue.cutValue = 0;
+ });
+
+ // Propagate cut values up the tree.
+ function dfs(n) {
+ var children = spanningTree.successors(n);
+ for (var c in children) {
+ var child = children[c];
+ dfs(child);
+ }
+ if (n !== spanningTree.graph().root) {
+ setCutValue(graph, spanningTree, n);
+ }
+ }
+ dfs(spanningTree.graph().root);
+}
+
+/*
+ * Perform a DFS postorder traversal, labeling each node v with
+ * its traversal order 'lim(v)' and the minimum traversal number
+ * of any of its descendants 'low(v)'. This provides an efficient
+ * way to test whether u is an ancestor of v since
+ * low(u) <= lim(v) <= lim(u) if and only if u is an ancestor.
+ */
+function computeLowLim(tree) {
+ var postOrderNum = 0;
+
+ function dfs(n) {
+ var children = tree.successors(n);
+ var low = postOrderNum;
+ for (var c in children) {
+ var child = children[c];
+ dfs(child);
+ low = Math.min(low, tree.node(child).low);
+ }
+ tree.node(n).low = low;
+ tree.node(n).lim = postOrderNum++;
+ }
+
+ dfs(tree.graph().root);
+}
+
+/*
+ * To compute the cut value of the edge parent -> child, we consider
+ * it and any other graph edges to or from the child.
+ * parent
+ * |
+ * child
+ * / \
+ * u v
+ */
+function setCutValue(graph, tree, child) {
+ var parentEdge = tree.inEdges(child)[0];
+
+ // List of child's children in the spanning tree.
+ var grandchildren = [];
+ var grandchildEdges = tree.outEdges(child);
+ for (var gce in grandchildEdges) {
+ grandchildren.push(tree.target(grandchildEdges[gce]));
+ }
+
+ var cutValue = 0;
+
+ // TODO: Replace unit increment/decrement with edge weights.
+ var E = 0; // Edges from child to grandchild's subtree.
+ var F = 0; // Edges to child from grandchild's subtree.
+ var G = 0; // Edges from child to nodes outside of child's subtree.
+ var H = 0; // Edges from nodes outside of child's subtree to child.
+
+ // Consider all graph edges from child.
+ var outEdges = graph.outEdges(child);
+ var gc;
+ for (var oe in outEdges) {
+ var succ = graph.target(outEdges[oe]);
+ for (gc in grandchildren) {
+ if (inSubtree(tree, succ, grandchildren[gc])) {
+ E++;
+ }
+ }
+ if (!inSubtree(tree, succ, child)) {
+ G++;
+ }
+ }
+
+ // Consider all graph edges to child.
+ var inEdges = graph.inEdges(child);
+ for (var ie in inEdges) {
+ var pred = graph.source(inEdges[ie]);
+ for (gc in grandchildren) {
+ if (inSubtree(tree, pred, grandchildren[gc])) {
+ F++;
+ }
+ }
+ if (!inSubtree(tree, pred, child)) {
+ H++;
+ }
+ }
+
+ // Contributions depend on the alignment of the parent -> child edge
+ // and the child -> u or v edges.
+ var grandchildCutSum = 0;
+ for (gc in grandchildren) {
+ var cv = tree.edge(grandchildEdges[gc]).cutValue;
+ if (!tree.edge(grandchildEdges[gc]).reversed) {
+ grandchildCutSum += cv;
+ } else {
+ grandchildCutSum -= cv;
+ }
+ }
+
+ if (!tree.edge(parentEdge).reversed) {
+ cutValue += grandchildCutSum - E + F - G + H;
+ } else {
+ cutValue -= grandchildCutSum - E + F - G + H;
+ }
+
+ tree.edge(parentEdge).cutValue = cutValue;
+}
+
+/*
+ * Return whether n is a node in the subtree with the given
+ * root.
+ */
+function inSubtree(tree, n, root) {
+ return (tree.node(root).low <= tree.node(n).lim &&
+ tree.node(n).lim <= tree.node(root).lim);
+}
+
+/*
+ * Return an edge from the tree with a negative cut value, or null if there
+ * is none.
+ */
+function leaveEdge(tree) {
+ var edges = tree.edges();
+ for (var n in edges) {
+ var e = edges[n];
+ var treeValue = tree.edge(e);
+ if (treeValue.cutValue < 0) {
+ return e;
+ }
+ }
+ return null;
+}
+
+/*
+ * The edge e should be an edge in the tree, with an underlying edge
+ * in the graph, with a negative cut value. Of the two nodes incident
+ * on the edge, take the lower one. enterEdge returns an edge with
+ * minimum slack going from outside of that node's subtree to inside
+ * of that node's subtree.
+ */
+function enterEdge(graph, tree, e) {
+ var source = tree.source(e);
+ var target = tree.target(e);
+ var lower = tree.node(target).lim < tree.node(source).lim ? target : source;
+
+ // Is the tree edge aligned with the graph edge?
+ var aligned = !tree.edge(e).reversed;
+
+ var minSlack = Number.POSITIVE_INFINITY;
+ var minSlackEdge;
+ if (aligned) {
+ graph.eachEdge(function(id, u, v, value) {
+ if (id !== e && inSubtree(tree, u, lower) && !inSubtree(tree, v, lower)) {
+ var slack = rankUtil.slack(graph, u, v, value.minLen);
+ if (slack < minSlack) {
+ minSlack = slack;
+ minSlackEdge = id;
+ }
+ }
+ });
+ } else {
+ graph.eachEdge(function(id, u, v, value) {
+ if (id !== e && !inSubtree(tree, u, lower) && inSubtree(tree, v, lower)) {
+ var slack = rankUtil.slack(graph, u, v, value.minLen);
+ if (slack < minSlack) {
+ minSlack = slack;
+ minSlackEdge = id;
+ }
+ }
+ });
+ }
+
+ if (minSlackEdge === undefined) {
+ var outside = [];
+ var inside = [];
+ graph.eachNode(function(id) {
+ if (!inSubtree(tree, id, lower)) {
+ outside.push(id);
+ } else {
+ inside.push(id);
+ }
+ });
+ throw new Error('No edge found from outside of tree to inside');
+ }
+
+ return minSlackEdge;
+}
+
+/*
+ * Replace edge e with edge f in the tree, recalculating the tree root,
+ * the nodes' low and lim properties and the edges' cut values.
+ */
+function exchange(graph, tree, e, f) {
+ tree.delEdge(e);
+ var source = graph.source(f);
+ var target = graph.target(f);
+
+ // Redirect edges so that target is the root of its subtree.
+ function redirect(v) {
+ var edges = tree.inEdges(v);
+ for (var i in edges) {
+ var e = edges[i];
+ var u = tree.source(e);
+ var value = tree.edge(e);
+ redirect(u);
+ tree.delEdge(e);
+ value.reversed = !value.reversed;
+ tree.addEdge(e, v, u, value);
+ }
+ }
+
+ redirect(target);
+
+ var root = source;
+ var edges = tree.inEdges(root);
+ while (edges.length > 0) {
+ root = tree.source(edges[0]);
+ edges = tree.inEdges(root);
+ }
+
+ tree.graph().root = root;
+
+ tree.addEdge(null, source, target, {cutValue: 0});
+
+ initCutValues(graph, tree);
+
+ adjustRanks(graph, tree);
+}
+
+/*
+ * Reset the ranks of all nodes based on the current spanning tree.
+ * The rank of the tree's root remains unchanged, while all other
+ * nodes are set to the sum of minimum length constraints along
+ * the path from the root.
+ */
+function adjustRanks(graph, tree) {
+ function dfs(p) {
+ var children = tree.successors(p);
+ children.forEach(function(c) {
+ var minLen = minimumLength(graph, p, c);
+ graph.node(c).rank = graph.node(p).rank + minLen;
+ dfs(c);
+ });
+ }
+
+ dfs(tree.graph().root);
+}
+
+/*
+ * If u and v are connected by some edges in the graph, return the
+ * minimum length of those edges, as a positive number if v succeeds
+ * u and as a negative number if v precedes u.
+ */
+function minimumLength(graph, u, v) {
+ var outEdges = graph.outEdges(u, v);
+ if (outEdges.length > 0) {
+ return util.max(outEdges.map(function(e) {
+ return graph.edge(e).minLen;
+ }));
+ }
+
+ var inEdges = graph.inEdges(u, v);
+ if (inEdges.length > 0) {
+ return -util.max(inEdges.map(function(e) {
+ return graph.edge(e).minLen;
+ }));
+ }
+}
+
+},{"../util":26,"./rankUtil":24}],26:[function(require,module,exports){
+/*
+ * Returns the smallest value in the array.
+ */
+exports.min = function(values) {
+ return Math.min.apply(Math, values);
+};
+
+/*
+ * Returns the largest value in the array.
+ */
+exports.max = function(values) {
+ return Math.max.apply(Math, values);
+};
+
+/*
+ * Returns `true` only if `f(x)` is `true` for all `x` in `xs`. Otherwise
+ * returns `false`. This function will return immediately if it finds a
+ * case where `f(x)` does not hold.
+ */
+exports.all = function(xs, f) {
+ for (var i = 0; i < xs.length; ++i) {
+ if (!f(xs[i])) {
+ return false;
+ }
+ }
+ return true;
+};
+
+/*
+ * Accumulates the sum of elements in the given array using the `+` operator.
+ */
+exports.sum = function(values) {
+ return values.reduce(function(acc, x) { return acc + x; }, 0);
+};
+
+/*
+ * Returns an array of all values in the given object.
+ */
+exports.values = function(obj) {
+ return Object.keys(obj).map(function(k) { return obj[k]; });
+};
+
+exports.shuffle = function(array) {
+ for (i = array.length - 1; i > 0; --i) {
+ var j = Math.floor(Math.random() * (i + 1));
+ var aj = array[j];
+ array[j] = array[i];
+ array[i] = aj;
+ }
+};
+
+exports.propertyAccessor = function(self, config, field, setHook) {
+ return function(x) {
+ if (!arguments.length) return config[field];
+ config[field] = x;
+ if (setHook) setHook(x);
+ return self;
+ };
+};
+
+/*
+ * Given a layered, directed graph with `rank` and `order` node attributes,
+ * this function returns an array of ordered ranks. Each rank contains an array
+ * of the ids of the nodes in that rank in the order specified by the `order`
+ * attribute.
+ */
+exports.ordering = function(g) {
+ var ordering = [];
+ g.eachNode(function(u, value) {
+ var rank = ordering[value.rank] || (ordering[value.rank] = []);
+ rank[value.order] = u;
+ });
+ return ordering;
+};
+
+/*
+ * A filter that can be used with `filterNodes` to get a graph that only
+ * includes nodes that do not contain others nodes.
+ */
+exports.filterNonSubgraphs = function(g) {
+ return function(u) {
+ return g.children(u).length === 0;
+ };
+};
+
+/*
+ * Returns a new function that wraps `func` with a timer. The wrapper logs the
+ * time it takes to execute the function.
+ *
+ * The timer will be enabled provided `log.level >= 1`.
+ */
+function time(name, func) {
+ return function() {
+ var start = new Date().getTime();
+ try {
+ return func.apply(null, arguments);
+ } finally {
+ log(1, name + ' time: ' + (new Date().getTime() - start) + 'ms');
+ }
+ };
+}
+time.enabled = false;
+
+exports.time = time;
+
+/*
+ * A global logger with the specification `log(level, message, ...)` that
+ * will log a message to the console if `log.level >= level`.
+ */
+function log(level) {
+ if (log.level >= level) {
+ console.log.apply(console, Array.prototype.slice.call(arguments, 1));
+ }
+}
+log.level = 0;
+
+exports.log = log;
+
+},{}],27:[function(require,module,exports){
+module.exports = '0.4.5';
+
+},{}],28:[function(require,module,exports){
+exports.Graph = require("./lib/Graph");
+exports.Digraph = require("./lib/Digraph");
+exports.CGraph = require("./lib/CGraph");
+exports.CDigraph = require("./lib/CDigraph");
+require("./lib/graph-converters");
+
+exports.alg = {
+ isAcyclic: require("./lib/alg/isAcyclic"),
+ components: require("./lib/alg/components"),
+ dijkstra: require("./lib/alg/dijkstra"),
+ dijkstraAll: require("./lib/alg/dijkstraAll"),
+ findCycles: require("./lib/alg/findCycles"),
+ floydWarshall: require("./lib/alg/floydWarshall"),
+ postorder: require("./lib/alg/postorder"),
+ preorder: require("./lib/alg/preorder"),
+ prim: require("./lib/alg/prim"),
+ tarjan: require("./lib/alg/tarjan"),
+ topsort: require("./lib/alg/topsort")
+};
+
+exports.converter = {
+ json: require("./lib/converter/json.js")
+};
+
+var filter = require("./lib/filter");
+exports.filter = {
+ all: filter.all,
+ nodesFromList: filter.nodesFromList
+};
+
+exports.version = require("./lib/version");
+
+},{"./lib/CDigraph":30,"./lib/CGraph":31,"./lib/Digraph":32,"./lib/Graph":33,"./lib/alg/components":34,"./lib/alg/dijkstra":35,"./lib/alg/dijkstraAll":36,"./lib/alg/findCycles":37,"./lib/alg/floydWarshall":38,"./lib/alg/isAcyclic":39,"./lib/alg/postorder":40,"./lib/alg/preorder":41,"./lib/alg/prim":42,"./lib/alg/tarjan":43,"./lib/alg/topsort":44,"./lib/converter/json.js":46,"./lib/filter":47,"./lib/graph-converters":48,"./lib/version":50}],29:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = BaseGraph;
+
+function BaseGraph() {
+ // The value assigned to the graph itself.
+ this._value = undefined;
+
+ // Map of node id -> { id, value }
+ this._nodes = {};
+
+ // Map of edge id -> { id, u, v, value }
+ this._edges = {};
+
+ // Used to generate a unique id in the graph
+ this._nextId = 0;
+}
+
+// Number of nodes
+BaseGraph.prototype.order = function() {
+ return Object.keys(this._nodes).length;
+};
+
+// Number of edges
+BaseGraph.prototype.size = function() {
+ return Object.keys(this._edges).length;
+};
+
+// Accessor for graph level value
+BaseGraph.prototype.graph = function(value) {
+ if (arguments.length === 0) {
+ return this._value;
+ }
+ this._value = value;
+};
+
+BaseGraph.prototype.hasNode = function(u) {
+ return u in this._nodes;
+};
+
+BaseGraph.prototype.node = function(u, value) {
+ var node = this._strictGetNode(u);
+ if (arguments.length === 1) {
+ return node.value;
+ }
+ node.value = value;
+};
+
+BaseGraph.prototype.nodes = function() {
+ var nodes = [];
+ this.eachNode(function(id) { nodes.push(id); });
+ return nodes;
+};
+
+BaseGraph.prototype.eachNode = function(func) {
+ for (var k in this._nodes) {
+ var node = this._nodes[k];
+ func(node.id, node.value);
+ }
+};
+
+BaseGraph.prototype.hasEdge = function(e) {
+ return e in this._edges;
+};
+
+BaseGraph.prototype.edge = function(e, value) {
+ var edge = this._strictGetEdge(e);
+ if (arguments.length === 1) {
+ return edge.value;
+ }
+ edge.value = value;
+};
+
+BaseGraph.prototype.edges = function() {
+ var es = [];
+ this.eachEdge(function(id) { es.push(id); });
+ return es;
+};
+
+BaseGraph.prototype.eachEdge = function(func) {
+ for (var k in this._edges) {
+ var edge = this._edges[k];
+ func(edge.id, edge.u, edge.v, edge.value);
+ }
+};
+
+BaseGraph.prototype.incidentNodes = function(e) {
+ var edge = this._strictGetEdge(e);
+ return [edge.u, edge.v];
+};
+
+BaseGraph.prototype.addNode = function(u, value) {
+ if (u === undefined || u === null) {
+ do {
+ u = "_" + (++this._nextId);
+ } while (this.hasNode(u));
+ } else if (this.hasNode(u)) {
+ throw new Error("Graph already has node '" + u + "'");
+ }
+ this._nodes[u] = { id: u, value: value };
+ return u;
+};
+
+BaseGraph.prototype.delNode = function(u) {
+ this._strictGetNode(u);
+ this.incidentEdges(u).forEach(function(e) { this.delEdge(e); }, this);
+ delete this._nodes[u];
+};
+
+// inMap and outMap are opposite sides of an incidence map. For example, for
+// Graph these would both come from the _incidentEdges map, while for Digraph
+// they would come from _inEdges and _outEdges.
+BaseGraph.prototype._addEdge = function(e, u, v, value, inMap, outMap) {
+ this._strictGetNode(u);
+ this._strictGetNode(v);
+
+ if (e === undefined || e === null) {
+ do {
+ e = "_" + (++this._nextId);
+ } while (this.hasEdge(e));
+ }
+ else if (this.hasEdge(e)) {
+ throw new Error("Graph already has edge '" + e + "'");
+ }
+
+ this._edges[e] = { id: e, u: u, v: v, value: value };
+ addEdgeToMap(inMap[v], u, e);
+ addEdgeToMap(outMap[u], v, e);
+
+ return e;
+};
+
+// See note for _addEdge regarding inMap and outMap.
+BaseGraph.prototype._delEdge = function(e, inMap, outMap) {
+ var edge = this._strictGetEdge(e);
+ delEdgeFromMap(inMap[edge.v], edge.u, e);
+ delEdgeFromMap(outMap[edge.u], edge.v, e);
+ delete this._edges[e];
+};
+
+BaseGraph.prototype.copy = function() {
+ var copy = new this.constructor();
+ copy.graph(this.graph());
+ this.eachNode(function(u, value) { copy.addNode(u, value); });
+ this.eachEdge(function(e, u, v, value) { copy.addEdge(e, u, v, value); });
+ copy._nextId = this._nextId;
+ return copy;
+};
+
+BaseGraph.prototype.filterNodes = function(filter) {
+ var copy = new this.constructor();
+ copy.graph(this.graph());
+ this.eachNode(function(u, value) {
+ if (filter(u)) {
+ copy.addNode(u, value);
+ }
+ });
+ this.eachEdge(function(e, u, v, value) {
+ if (copy.hasNode(u) && copy.hasNode(v)) {
+ copy.addEdge(e, u, v, value);
+ }
+ });
+ return copy;
+};
+
+BaseGraph.prototype._strictGetNode = function(u) {
+ var node = this._nodes[u];
+ if (node === undefined) {
+ throw new Error("Node '" + u + "' is not in graph");
+ }
+ return node;
+};
+
+BaseGraph.prototype._strictGetEdge = function(e) {
+ var edge = this._edges[e];
+ if (edge === undefined) {
+ throw new Error("Edge '" + e + "' is not in graph");
+ }
+ return edge;
+};
+
+function addEdgeToMap(map, v, e) {
+ (map[v] || (map[v] = new Set())).add(e);
+}
+
+function delEdgeFromMap(map, v, e) {
+ var vEntry = map[v];
+ vEntry.remove(e);
+ if (vEntry.size() === 0) {
+ delete map[v];
+ }
+}
+
+
+},{"cp-data":5}],30:[function(require,module,exports){
+var Digraph = require("./Digraph"),
+ compoundify = require("./compoundify");
+
+var CDigraph = compoundify(Digraph);
+
+module.exports = CDigraph;
+
+CDigraph.fromDigraph = function(src) {
+ var g = new CDigraph(),
+ graphValue = src.graph();
+
+ if (graphValue !== undefined) {
+ g.graph(graphValue);
+ }
+
+ src.eachNode(function(u, value) {
+ if (value === undefined) {
+ g.addNode(u);
+ } else {
+ g.addNode(u, value);
+ }
+ });
+ src.eachEdge(function(e, u, v, value) {
+ if (value === undefined) {
+ g.addEdge(null, u, v);
+ } else {
+ g.addEdge(null, u, v, value);
+ }
+ });
+ return g;
+};
+
+CDigraph.prototype.toString = function() {
+ return "CDigraph " + JSON.stringify(this, null, 2);
+};
+
+},{"./Digraph":32,"./compoundify":45}],31:[function(require,module,exports){
+var Graph = require("./Graph"),
+ compoundify = require("./compoundify");
+
+var CGraph = compoundify(Graph);
+
+module.exports = CGraph;
+
+CGraph.fromGraph = function(src) {
+ var g = new CGraph(),
+ graphValue = src.graph();
+
+ if (graphValue !== undefined) {
+ g.graph(graphValue);
+ }
+
+ src.eachNode(function(u, value) {
+ if (value === undefined) {
+ g.addNode(u);
+ } else {
+ g.addNode(u, value);
+ }
+ });
+ src.eachEdge(function(e, u, v, value) {
+ if (value === undefined) {
+ g.addEdge(null, u, v);
+ } else {
+ g.addEdge(null, u, v, value);
+ }
+ });
+ return g;
+};
+
+CGraph.prototype.toString = function() {
+ return "CGraph " + JSON.stringify(this, null, 2);
+};
+
+},{"./Graph":33,"./compoundify":45}],32:[function(require,module,exports){
+/*
+ * This file is organized with in the following order:
+ *
+ * Exports
+ * Graph constructors
+ * Graph queries (e.g. nodes(), edges()
+ * Graph mutators
+ * Helper functions
+ */
+
+var util = require("./util"),
+ BaseGraph = require("./BaseGraph"),
+/* jshint -W079 */
+ Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = Digraph;
+
+/*
+ * Constructor to create a new directed multi-graph.
+ */
+function Digraph() {
+ BaseGraph.call(this);
+
+ /*! Map of sourceId -> {targetId -> Set of edge ids} */
+ this._inEdges = {};
+
+ /*! Map of targetId -> {sourceId -> Set of edge ids} */
+ this._outEdges = {};
+}
+
+Digraph.prototype = new BaseGraph();
+Digraph.prototype.constructor = Digraph;
+
+/*
+ * Always returns `true`.
+ */
+Digraph.prototype.isDirected = function() {
+ return true;
+};
+
+/*
+ * Returns all successors of the node with the id `u`. That is, all nodes
+ * that have the node `u` as their source are returned.
+ *
+ * If no node `u` exists in the graph this function throws an Error.
+ *
+ * @param {String} u a node id
+ */
+Digraph.prototype.successors = function(u) {
+ this._strictGetNode(u);
+ return Object.keys(this._outEdges[u])
+ .map(function(v) { return this._nodes[v].id; }, this);
+};
+
+/*
+ * Returns all predecessors of the node with the id `u`. That is, all nodes
+ * that have the node `u` as their target are returned.
+ *
+ * If no node `u` exists in the graph this function throws an Error.
+ *
+ * @param {String} u a node id
+ */
+Digraph.prototype.predecessors = function(u) {
+ this._strictGetNode(u);
+ return Object.keys(this._inEdges[u])
+ .map(function(v) { return this._nodes[v].id; }, this);
+};
+
+/*
+ * Returns all nodes that are adjacent to the node with the id `u`. In other
+ * words, this function returns the set of all successors and predecessors of
+ * node `u`.
+ *
+ * @param {String} u a node id
+ */
+Digraph.prototype.neighbors = function(u) {
+ return Set.union([this.successors(u), this.predecessors(u)]).keys();
+};
+
+/*
+ * Returns all nodes in the graph that have no in-edges.
+ */
+Digraph.prototype.sources = function() {
+ var self = this;
+ return this._filterNodes(function(u) {
+ // This could have better space characteristics if we had an inDegree function.
+ return self.inEdges(u).length === 0;
+ });
+};
+
+/*
+ * Returns all nodes in the graph that have no out-edges.
+ */
+Digraph.prototype.sinks = function() {
+ var self = this;
+ return this._filterNodes(function(u) {
+ // This could have better space characteristics if we have an outDegree function.
+ return self.outEdges(u).length === 0;
+ });
+};
+
+/*
+ * Returns the source node incident on the edge identified by the id `e`. If no
+ * such edge exists in the graph this function throws an Error.
+ *
+ * @param {String} e an edge id
+ */
+Digraph.prototype.source = function(e) {
+ return this._strictGetEdge(e).u;
+};
+
+/*
+ * Returns the target node incident on the edge identified by the id `e`. If no
+ * such edge exists in the graph this function throws an Error.
+ *
+ * @param {String} e an edge id
+ */
+Digraph.prototype.target = function(e) {
+ return this._strictGetEdge(e).v;
+};
+
+/*
+ * Returns an array of ids for all edges in the graph that have the node
+ * `target` as their target. If the node `target` is not in the graph this
+ * function raises an Error.
+ *
+ * Optionally a `source` node can also be specified. This causes the results
+ * to be filtered such that only edges from `source` to `target` are included.
+ * If the node `source` is specified but is not in the graph then this function
+ * raises an Error.
+ *
+ * @param {String} target the target node id
+ * @param {String} [source] an optional source node id
+ */
+Digraph.prototype.inEdges = function(target, source) {
+ this._strictGetNode(target);
+ var results = Set.union(util.values(this._inEdges[target])).keys();
+ if (arguments.length > 1) {
+ this._strictGetNode(source);
+ results = results.filter(function(e) { return this.source(e) === source; }, this);
+ }
+ return results;
+};
+
+/*
+ * Returns an array of ids for all edges in the graph that have the node
+ * `source` as their source. If the node `source` is not in the graph this
+ * function raises an Error.
+ *
+ * Optionally a `target` node may also be specified. This causes the results
+ * to be filtered such that only edges from `source` to `target` are included.
+ * If the node `target` is specified but is not in the graph then this function
+ * raises an Error.
+ *
+ * @param {String} source the source node id
+ * @param {String} [target] an optional target node id
+ */
+Digraph.prototype.outEdges = function(source, target) {
+ this._strictGetNode(source);
+ var results = Set.union(util.values(this._outEdges[source])).keys();
+ if (arguments.length > 1) {
+ this._strictGetNode(target);
+ results = results.filter(function(e) { return this.target(e) === target; }, this);
+ }
+ return results;
+};
+
+/*
+ * Returns an array of ids for all edges in the graph that have the `u` as
+ * their source or their target. If the node `u` is not in the graph this
+ * function raises an Error.
+ *
+ * Optionally a `v` node may also be specified. This causes the results to be
+ * filtered such that only edges between `u` and `v` - in either direction -
+ * are included. IF the node `v` is specified but not in the graph then this
+ * function raises an Error.
+ *
+ * @param {String} u the node for which to find incident edges
+ * @param {String} [v] option node that must be adjacent to `u`
+ */
+Digraph.prototype.incidentEdges = function(u, v) {
+ if (arguments.length > 1) {
+ return Set.union([this.outEdges(u, v), this.outEdges(v, u)]).keys();
+ } else {
+ return Set.union([this.inEdges(u), this.outEdges(u)]).keys();
+ }
+};
+
+/*
+ * Returns a string representation of this graph.
+ */
+Digraph.prototype.toString = function() {
+ return "Digraph " + JSON.stringify(this, null, 2);
+};
+
+/*
+ * Adds a new node with the id `u` to the graph and assigns it the value
+ * `value`. If a node with the id is already a part of the graph this function
+ * throws an Error.
+ *
+ * @param {String} u a node id
+ * @param {Object} [value] an optional value to attach to the node
+ */
+Digraph.prototype.addNode = function(u, value) {
+ u = BaseGraph.prototype.addNode.call(this, u, value);
+ this._inEdges[u] = {};
+ this._outEdges[u] = {};
+ return u;
+};
+
+/*
+ * Removes a node from the graph that has the id `u`. Any edges incident on the
+ * node are also removed. If the graph does not contain a node with the id this
+ * function will throw an Error.
+ *
+ * @param {String} u a node id
+ */
+Digraph.prototype.delNode = function(u) {
+ BaseGraph.prototype.delNode.call(this, u);
+ delete this._inEdges[u];
+ delete this._outEdges[u];
+};
+
+/*
+ * Adds a new edge to the graph with the id `e` from a node with the id `source`
+ * to a node with an id `target` and assigns it the value `value`. This graph
+ * allows more than one edge from `source` to `target` as long as the id `e`
+ * is unique in the set of edges. If `e` is `null` the graph will assign a
+ * unique identifier to the edge.
+ *
+ * If `source` or `target` are not present in the graph this function will
+ * throw an Error.
+ *
+ * @param {String} [e] an edge id
+ * @param {String} source the source node id
+ * @param {String} target the target node id
+ * @param {Object} [value] an optional value to attach to the edge
+ */
+Digraph.prototype.addEdge = function(e, source, target, value) {
+ return BaseGraph.prototype._addEdge.call(this, e, source, target, value,
+ this._inEdges, this._outEdges);
+};
+
+/*
+ * Removes an edge in the graph with the id `e`. If no edge in the graph has
+ * the id `e` this function will throw an Error.
+ *
+ * @param {String} e an edge id
+ */
+Digraph.prototype.delEdge = function(e) {
+ BaseGraph.prototype._delEdge.call(this, e, this._inEdges, this._outEdges);
+};
+
+// Unlike BaseGraph.filterNodes, this helper just returns nodes that
+// satisfy a predicate.
+Digraph.prototype._filterNodes = function(pred) {
+ var filtered = [];
+ this.eachNode(function(u) {
+ if (pred(u)) {
+ filtered.push(u);
+ }
+ });
+ return filtered;
+};
+
+
+},{"./BaseGraph":29,"./util":49,"cp-data":5}],33:[function(require,module,exports){
+/*
+ * This file is organized with in the following order:
+ *
+ * Exports
+ * Graph constructors
+ * Graph queries (e.g. nodes(), edges()
+ * Graph mutators
+ * Helper functions
+ */
+
+var util = require("./util"),
+ BaseGraph = require("./BaseGraph"),
+/* jshint -W079 */
+ Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = Graph;
+
+/*
+ * Constructor to create a new undirected multi-graph.
+ */
+function Graph() {
+ BaseGraph.call(this);
+
+ /*! Map of nodeId -> { otherNodeId -> Set of edge ids } */
+ this._incidentEdges = {};
+}
+
+Graph.prototype = new BaseGraph();
+Graph.prototype.constructor = Graph;
+
+/*
+ * Always returns `false`.
+ */
+Graph.prototype.isDirected = function() {
+ return false;
+};
+
+/*
+ * Returns all nodes that are adjacent to the node with the id `u`.
+ *
+ * @param {String} u a node id
+ */
+Graph.prototype.neighbors = function(u) {
+ this._strictGetNode(u);
+ return Object.keys(this._incidentEdges[u])
+ .map(function(v) { return this._nodes[v].id; }, this);
+};
+
+/*
+ * Returns an array of ids for all edges in the graph that are incident on `u`.
+ * If the node `u` is not in the graph this function raises an Error.
+ *
+ * Optionally a `v` node may also be specified. This causes the results to be
+ * filtered such that only edges between `u` and `v` are included. If the node
+ * `v` is specified but not in the graph then this function raises an Error.
+ *
+ * @param {String} u the node for which to find incident edges
+ * @param {String} [v] option node that must be adjacent to `u`
+ */
+Graph.prototype.incidentEdges = function(u, v) {
+ this._strictGetNode(u);
+ if (arguments.length > 1) {
+ this._strictGetNode(v);
+ return v in this._incidentEdges[u] ? this._incidentEdges[u][v].keys() : [];
+ } else {
+ return Set.union(util.values(this._incidentEdges[u])).keys();
+ }
+};
+
+/*
+ * Returns a string representation of this graph.
+ */
+Graph.prototype.toString = function() {
+ return "Graph " + JSON.stringify(this, null, 2);
+};
+
+/*
+ * Adds a new node with the id `u` to the graph and assigns it the value
+ * `value`. If a node with the id is already a part of the graph this function
+ * throws an Error.
+ *
+ * @param {String} u a node id
+ * @param {Object} [value] an optional value to attach to the node
+ */
+Graph.prototype.addNode = function(u, value) {
+ u = BaseGraph.prototype.addNode.call(this, u, value);
+ this._incidentEdges[u] = {};
+ return u;
+};
+
+/*
+ * Removes a node from the graph that has the id `u`. Any edges incident on the
+ * node are also removed. If the graph does not contain a node with the id this
+ * function will throw an Error.
+ *
+ * @param {String} u a node id
+ */
+Graph.prototype.delNode = function(u) {
+ BaseGraph.prototype.delNode.call(this, u);
+ delete this._incidentEdges[u];
+};
+
+/*
+ * Adds a new edge to the graph with the id `e` between a node with the id `u`
+ * and a node with an id `v` and assigns it the value `value`. This graph
+ * allows more than one edge between `u` and `v` as long as the id `e`
+ * is unique in the set of edges. If `e` is `null` the graph will assign a
+ * unique identifier to the edge.
+ *
+ * If `u` or `v` are not present in the graph this function will throw an
+ * Error.
+ *
+ * @param {String} [e] an edge id
+ * @param {String} u the node id of one of the adjacent nodes
+ * @param {String} v the node id of the other adjacent node
+ * @param {Object} [value] an optional value to attach to the edge
+ */
+Graph.prototype.addEdge = function(e, u, v, value) {
+ return BaseGraph.prototype._addEdge.call(this, e, u, v, value,
+ this._incidentEdges, this._incidentEdges);
+};
+
+/*
+ * Removes an edge in the graph with the id `e`. If no edge in the graph has
+ * the id `e` this function will throw an Error.
+ *
+ * @param {String} e an edge id
+ */
+Graph.prototype.delEdge = function(e) {
+ BaseGraph.prototype._delEdge.call(this, e, this._incidentEdges, this._incidentEdges);
+};
+
+
+},{"./BaseGraph":29,"./util":49,"cp-data":5}],34:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = components;
+
+/**
+ * Finds all [connected components][] in a graph and returns an array of these
+ * components. Each component is itself an array that contains the ids of nodes
+ * in the component.
+ *
+ * This function only works with undirected Graphs.
+ *
+ * [connected components]: http://en.wikipedia.org/wiki/Connected_component_(graph_theory)
+ *
+ * @param {Graph} g the graph to search for components
+ */
+function components(g) {
+ var results = [];
+ var visited = new Set();
+
+ function dfs(v, component) {
+ if (!visited.has(v)) {
+ visited.add(v);
+ component.push(v);
+ g.neighbors(v).forEach(function(w) {
+ dfs(w, component);
+ });
+ }
+ }
+
+ g.nodes().forEach(function(v) {
+ var component = [];
+ dfs(v, component);
+ if (component.length > 0) {
+ results.push(component);
+ }
+ });
+
+ return results;
+}
+
+},{"cp-data":5}],35:[function(require,module,exports){
+var PriorityQueue = require("cp-data").PriorityQueue;
+
+module.exports = dijkstra;
+
+/**
+ * This function is an implementation of [Dijkstra's algorithm][] which finds
+ * the shortest path from **source** to all other nodes in **g**. This
+ * function returns a map of `u -> { distance, predecessor }`. The distance
+ * property holds the sum of the weights from **source** to `u` along the
+ * shortest path or `Number.POSITIVE_INFINITY` if there is no path from
+ * **source**. The predecessor property can be used to walk the individual
+ * elements of the path from **source** to **u** in reverse order.
+ *
+ * This function takes an optional `weightFunc(e)` which returns the
+ * weight of the edge `e`. If no weightFunc is supplied then each edge is
+ * assumed to have a weight of 1. This function throws an Error if any of
+ * the traversed edges have a negative edge weight.
+ *
+ * This function takes an optional `incidentFunc(u)` which returns the ids of
+ * all edges incident to the node `u` for the purposes of shortest path
+ * traversal. By default this function uses the `g.outEdges` for Digraphs and
+ * `g.incidentEdges` for Graphs.
+ *
+ * This function takes `O((|E| + |V|) * log |V|)` time.
+ *
+ * [Dijkstra's algorithm]: http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
+ *
+ * @param {Graph} g the graph to search for shortest paths from **source**
+ * @param {Object} source the source from which to start the search
+ * @param {Function} [weightFunc] optional weight function
+ * @param {Function} [incidentFunc] optional incident function
+ */
+function dijkstra(g, source, weightFunc, incidentFunc) {
+ var results = {},
+ pq = new PriorityQueue();
+
+ function updateNeighbors(e) {
+ var incidentNodes = g.incidentNodes(e),
+ v = incidentNodes[0] !== u ? incidentNodes[0] : incidentNodes[1],
+ vEntry = results[v],
+ weight = weightFunc(e),
+ distance = uEntry.distance + weight;
+
+ if (weight < 0) {
+ throw new Error("dijkstra does not allow negative edge weights. Bad edge: " + e + " Weight: " + weight);
+ }
+
+ if (distance < vEntry.distance) {
+ vEntry.distance = distance;
+ vEntry.predecessor = u;
+ pq.decrease(v, distance);
+ }
+ }
+
+ weightFunc = weightFunc || function() { return 1; };
+ incidentFunc = incidentFunc || (g.isDirected()
+ ? function(u) { return g.outEdges(u); }
+ : function(u) { return g.incidentEdges(u); });
+
+ g.eachNode(function(u) {
+ var distance = u === source ? 0 : Number.POSITIVE_INFINITY;
+ results[u] = { distance: distance };
+ pq.add(u, distance);
+ });
+
+ var u, uEntry;
+ while (pq.size() > 0) {
+ u = pq.removeMin();
+ uEntry = results[u];
+ if (uEntry.distance === Number.POSITIVE_INFINITY) {
+ break;
+ }
+
+ incidentFunc(u).forEach(updateNeighbors);
+ }
+
+ return results;
+}
+
+},{"cp-data":5}],36:[function(require,module,exports){
+var dijkstra = require("./dijkstra");
+
+module.exports = dijkstraAll;
+
+/**
+ * This function finds the shortest path from each node to every other
+ * reachable node in the graph. It is similar to [alg.dijkstra][], but
+ * instead of returning a single-source array, it returns a mapping of
+ * of `source -> alg.dijksta(g, source, weightFunc, incidentFunc)`.
+ *
+ * This function takes an optional `weightFunc(e)` which returns the
+ * weight of the edge `e`. If no weightFunc is supplied then each edge is
+ * assumed to have a weight of 1. This function throws an Error if any of
+ * the traversed edges have a negative edge weight.
+ *
+ * This function takes an optional `incidentFunc(u)` which returns the ids of
+ * all edges incident to the node `u` for the purposes of shortest path
+ * traversal. By default this function uses the `outEdges` function on the
+ * supplied graph.
+ *
+ * This function takes `O(|V| * (|E| + |V|) * log |V|)` time.
+ *
+ * [alg.dijkstra]: dijkstra.js.html#dijkstra
+ *
+ * @param {Graph} g the graph to search for shortest paths from **source**
+ * @param {Function} [weightFunc] optional weight function
+ * @param {Function} [incidentFunc] optional incident function
+ */
+function dijkstraAll(g, weightFunc, incidentFunc) {
+ var results = {};
+ g.eachNode(function(u) {
+ results[u] = dijkstra(g, u, weightFunc, incidentFunc);
+ });
+ return results;
+}
+
+},{"./dijkstra":35}],37:[function(require,module,exports){
+var tarjan = require("./tarjan");
+
+module.exports = findCycles;
+
+/*
+ * Given a Digraph **g** this function returns all nodes that are part of a
+ * cycle. Since there may be more than one cycle in a graph this function
+ * returns an array of these cycles, where each cycle is itself represented
+ * by an array of ids for each node involved in that cycle.
+ *
+ * [alg.isAcyclic][] is more efficient if you only need to determine whether
+ * a graph has a cycle or not.
+ *
+ * [alg.isAcyclic]: isAcyclic.js.html#isAcyclic
+ *
+ * @param {Digraph} g the graph to search for cycles.
+ */
+function findCycles(g) {
+ return tarjan(g).filter(function(cmpt) { return cmpt.length > 1; });
+}
+
+},{"./tarjan":43}],38:[function(require,module,exports){
+module.exports = floydWarshall;
+
+/**
+ * This function is an implementation of the [Floyd-Warshall algorithm][],
+ * which finds the shortest path from each node to every other reachable node
+ * in the graph. It is similar to [alg.dijkstraAll][], but it handles negative
+ * edge weights and is more efficient for some types of graphs. This function
+ * returns a map of `source -> { target -> { distance, predecessor }`. The
+ * distance property holds the sum of the weights from `source` to `target`
+ * along the shortest path of `Number.POSITIVE_INFINITY` if there is no path
+ * from `source`. The predecessor property can be used to walk the individual
+ * elements of the path from `source` to `target` in reverse order.
+ *
+ * This function takes an optional `weightFunc(e)` which returns the
+ * weight of the edge `e`. If no weightFunc is supplied then each edge is
+ * assumed to have a weight of 1.
+ *
+ * This function takes an optional `incidentFunc(u)` which returns the ids of
+ * all edges incident to the node `u` for the purposes of shortest path
+ * traversal. By default this function uses the `outEdges` function on the
+ * supplied graph.
+ *
+ * This algorithm takes O(|V|^3) time.
+ *
+ * [Floyd-Warshall algorithm]: https://en.wikipedia.org/wiki/Floyd-Warshall_algorithm
+ * [alg.dijkstraAll]: dijkstraAll.js.html#dijkstraAll
+ *
+ * @param {Graph} g the graph to search for shortest paths from **source**
+ * @param {Function} [weightFunc] optional weight function
+ * @param {Function} [incidentFunc] optional incident function
+ */
+function floydWarshall(g, weightFunc, incidentFunc) {
+ var results = {},
+ nodes = g.nodes();
+
+ weightFunc = weightFunc || function() { return 1; };
+ incidentFunc = incidentFunc || (g.isDirected()
+ ? function(u) { return g.outEdges(u); }
+ : function(u) { return g.incidentEdges(u); });
+
+ nodes.forEach(function(u) {
+ results[u] = {};
+ results[u][u] = { distance: 0 };
+ nodes.forEach(function(v) {
+ if (u !== v) {
+ results[u][v] = { distance: Number.POSITIVE_INFINITY };
+ }
+ });
+ incidentFunc(u).forEach(function(e) {
+ var incidentNodes = g.incidentNodes(e),
+ v = incidentNodes[0] !== u ? incidentNodes[0] : incidentNodes[1],
+ d = weightFunc(e);
+ if (d < results[u][v].distance) {
+ results[u][v] = { distance: d, predecessor: u };
+ }
+ });
+ });
+
+ nodes.forEach(function(k) {
+ var rowK = results[k];
+ nodes.forEach(function(i) {
+ var rowI = results[i];
+ nodes.forEach(function(j) {
+ var ik = rowI[k];
+ var kj = rowK[j];
+ var ij = rowI[j];
+ var altDistance = ik.distance + kj.distance;
+ if (altDistance < ij.distance) {
+ ij.distance = altDistance;
+ ij.predecessor = kj.predecessor;
+ }
+ });
+ });
+ });
+
+ return results;
+}
+
+},{}],39:[function(require,module,exports){
+var topsort = require("./topsort");
+
+module.exports = isAcyclic;
+
+/*
+ * Given a Digraph **g** this function returns `true` if the graph has no
+ * cycles and returns `false` if it does. This algorithm returns as soon as it
+ * detects the first cycle.
+ *
+ * Use [alg.findCycles][] if you need the actual list of cycles in a graph.
+ *
+ * [alg.findCycles]: findCycles.js.html#findCycles
+ *
+ * @param {Digraph} g the graph to test for cycles
+ */
+function isAcyclic(g) {
+ try {
+ topsort(g);
+ } catch (e) {
+ if (e instanceof topsort.CycleException) return false;
+ throw e;
+ }
+ return true;
+}
+
+},{"./topsort":44}],40:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = postorder;
+
+// Postorder traversal of g, calling f for each visited node. Assumes the graph
+// is a tree.
+function postorder(g, root, f) {
+ var visited = new Set();
+ if (g.isDirected()) {
+ throw new Error("This function only works for undirected graphs");
+ }
+ function dfs(u, prev) {
+ if (visited.has(u)) {
+ throw new Error("The input graph is not a tree: " + g);
+ }
+ visited.add(u);
+ g.neighbors(u).forEach(function(v) {
+ if (v !== prev) dfs(v, u);
+ });
+ f(u);
+ }
+ dfs(root);
+}
+
+},{"cp-data":5}],41:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = preorder;
+
+// Preorder traversal of g, calling f for each visited node. Assumes the graph
+// is a tree.
+function preorder(g, root, f) {
+ var visited = new Set();
+ if (g.isDirected()) {
+ throw new Error("This function only works for undirected graphs");
+ }
+ function dfs(u, prev) {
+ if (visited.has(u)) {
+ throw new Error("The input graph is not a tree: " + g);
+ }
+ visited.add(u);
+ f(u);
+ g.neighbors(u).forEach(function(v) {
+ if (v !== prev) dfs(v, u);
+ });
+ }
+ dfs(root);
+}
+
+},{"cp-data":5}],42:[function(require,module,exports){
+var Graph = require("../Graph"),
+ PriorityQueue = require("cp-data").PriorityQueue;
+
+module.exports = prim;
+
+/**
+ * [Prim's algorithm][] takes a connected undirected graph and generates a
+ * [minimum spanning tree][]. This function returns the minimum spanning
+ * tree as an undirected graph. This algorithm is derived from the description
+ * in "Introduction to Algorithms", Third Edition, Cormen, et al., Pg 634.
+ *
+ * This function takes a `weightFunc(e)` which returns the weight of the edge
+ * `e`. It throws an Error if the graph is not connected.
+ *
+ * This function takes `O(|E| log |V|)` time.
+ *
+ * [Prim's algorithm]: https://en.wikipedia.org/wiki/Prim's_algorithm
+ * [minimum spanning tree]: https://en.wikipedia.org/wiki/Minimum_spanning_tree
+ *
+ * @param {Graph} g the graph used to generate the minimum spanning tree
+ * @param {Function} weightFunc the weight function to use
+ */
+function prim(g, weightFunc) {
+ var result = new Graph(),
+ parents = {},
+ pq = new PriorityQueue(),
+ u;
+
+ function updateNeighbors(e) {
+ var incidentNodes = g.incidentNodes(e),
+ v = incidentNodes[0] !== u ? incidentNodes[0] : incidentNodes[1],
+ pri = pq.priority(v);
+ if (pri !== undefined) {
+ var edgeWeight = weightFunc(e);
+ if (edgeWeight < pri) {
+ parents[v] = u;
+ pq.decrease(v, edgeWeight);
+ }
+ }
+ }
+
+ if (g.order() === 0) {
+ return result;
+ }
+
+ g.eachNode(function(u) {
+ pq.add(u, Number.POSITIVE_INFINITY);
+ result.addNode(u);
+ });
+
+ // Start from an arbitrary node
+ pq.decrease(g.nodes()[0], 0);
+
+ var init = false;
+ while (pq.size() > 0) {
+ u = pq.removeMin();
+ if (u in parents) {
+ result.addEdge(null, u, parents[u]);
+ } else if (init) {
+ throw new Error("Input graph is not connected: " + g);
+ } else {
+ init = true;
+ }
+
+ g.incidentEdges(u).forEach(updateNeighbors);
+ }
+
+ return result;
+}
+
+},{"../Graph":33,"cp-data":5}],43:[function(require,module,exports){
+module.exports = tarjan;
+
+/**
+ * This function is an implementation of [Tarjan's algorithm][] which finds
+ * all [strongly connected components][] in the directed graph **g**. Each
+ * strongly connected component is composed of nodes that can reach all other
+ * nodes in the component via directed edges. A strongly connected component
+ * can consist of a single node if that node cannot both reach and be reached
+ * by any other specific node in the graph. Components of more than one node
+ * are guaranteed to have at least one cycle.
+ *
+ * This function returns an array of components. Each component is itself an
+ * array that contains the ids of all nodes in the component.
+ *
+ * [Tarjan's algorithm]: http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
+ * [strongly connected components]: http://en.wikipedia.org/wiki/Strongly_connected_component
+ *
+ * @param {Digraph} g the graph to search for strongly connected components
+ */
+function tarjan(g) {
+ if (!g.isDirected()) {
+ throw new Error("tarjan can only be applied to a directed graph. Bad input: " + g);
+ }
+
+ var index = 0,
+ stack = [],
+ visited = {}, // node id -> { onStack, lowlink, index }
+ results = [];
+
+ function dfs(u) {
+ var entry = visited[u] = {
+ onStack: true,
+ lowlink: index,
+ index: index++
+ };
+ stack.push(u);
+
+ g.successors(u).forEach(function(v) {
+ if (!(v in visited)) {
+ dfs(v);
+ entry.lowlink = Math.min(entry.lowlink, visited[v].lowlink);
+ } else if (visited[v].onStack) {
+ entry.lowlink = Math.min(entry.lowlink, visited[v].index);
+ }
+ });
+
+ if (entry.lowlink === entry.index) {
+ var cmpt = [],
+ v;
+ do {
+ v = stack.pop();
+ visited[v].onStack = false;
+ cmpt.push(v);
+ } while (u !== v);
+ results.push(cmpt);
+ }
+ }
+
+ g.nodes().forEach(function(u) {
+ if (!(u in visited)) {
+ dfs(u);
+ }
+ });
+
+ return results;
+}
+
+},{}],44:[function(require,module,exports){
+module.exports = topsort;
+topsort.CycleException = CycleException;
+
+/*
+ * Given a graph **g**, this function returns an ordered list of nodes such
+ * that for each edge `u -> v`, `u` appears before `v` in the list. If the
+ * graph has a cycle it is impossible to generate such a list and
+ * **CycleException** is thrown.
+ *
+ * See [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting)
+ * for more details about how this algorithm works.
+ *
+ * @param {Digraph} g the graph to sort
+ */
+function topsort(g) {
+ if (!g.isDirected()) {
+ throw new Error("topsort can only be applied to a directed graph. Bad input: " + g);
+ }
+
+ var visited = {};
+ var stack = {};
+ var results = [];
+
+ function visit(node) {
+ if (node in stack) {
+ throw new CycleException();
+ }
+
+ if (!(node in visited)) {
+ stack[node] = true;
+ visited[node] = true;
+ g.predecessors(node).forEach(function(pred) {
+ visit(pred);
+ });
+ delete stack[node];
+ results.push(node);
+ }
+ }
+
+ var sinks = g.sinks();
+ if (g.order() !== 0 && sinks.length === 0) {
+ throw new CycleException();
+ }
+
+ g.sinks().forEach(function(sink) {
+ visit(sink);
+ });
+
+ return results;
+}
+
+function CycleException() {}
+
+CycleException.prototype.toString = function() {
+ return "Graph has at least one cycle";
+};
+
+},{}],45:[function(require,module,exports){
+// This file provides a helper function that mixes-in Dot behavior to an
+// existing graph prototype.
+
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+module.exports = compoundify;
+
+// Extends the given SuperConstructor with the ability for nodes to contain
+// other nodes. A special node id `null` is used to indicate the root graph.
+function compoundify(SuperConstructor) {
+ function Constructor() {
+ SuperConstructor.call(this);
+
+ // Map of object id -> parent id (or null for root graph)
+ this._parents = {};
+
+ // Map of id (or null) -> children set
+ this._children = {};
+ this._children[null] = new Set();
+ }
+
+ Constructor.prototype = new SuperConstructor();
+ Constructor.prototype.constructor = Constructor;
+
+ Constructor.prototype.parent = function(u, parent) {
+ this._strictGetNode(u);
+
+ if (arguments.length < 2) {
+ return this._parents[u];
+ }
+
+ if (u === parent) {
+ throw new Error("Cannot make " + u + " a parent of itself");
+ }
+ if (parent !== null) {
+ this._strictGetNode(parent);
+ }
+
+ this._children[this._parents[u]].remove(u);
+ this._parents[u] = parent;
+ this._children[parent].add(u);
+ };
+
+ Constructor.prototype.children = function(u) {
+ if (u !== null) {
+ this._strictGetNode(u);
+ }
+ return this._children[u].keys();
+ };
+
+ Constructor.prototype.addNode = function(u, value) {
+ u = SuperConstructor.prototype.addNode.call(this, u, value);
+ this._parents[u] = null;
+ this._children[u] = new Set();
+ this._children[null].add(u);
+ return u;
+ };
+
+ Constructor.prototype.delNode = function(u) {
+ // Promote all children to the parent of the subgraph
+ var parent = this.parent(u);
+ this._children[u].keys().forEach(function(child) {
+ this.parent(child, parent);
+ }, this);
+
+ this._children[parent].remove(u);
+ delete this._parents[u];
+ delete this._children[u];
+
+ return SuperConstructor.prototype.delNode.call(this, u);
+ };
+
+ Constructor.prototype.copy = function() {
+ var copy = SuperConstructor.prototype.copy.call(this);
+ this.nodes().forEach(function(u) {
+ copy.parent(u, this.parent(u));
+ }, this);
+ return copy;
+ };
+
+ Constructor.prototype.filterNodes = function(filter) {
+ var self = this,
+ copy = SuperConstructor.prototype.filterNodes.call(this, filter);
+
+ var parents = {};
+ function findParent(u) {
+ var parent = self.parent(u);
+ if (parent === null || copy.hasNode(parent)) {
+ parents[u] = parent;
+ return parent;
+ } else if (parent in parents) {
+ return parents[parent];
+ } else {
+ return findParent(parent);
+ }
+ }
+
+ copy.eachNode(function(u) { copy.parent(u, findParent(u)); });
+
+ return copy;
+ };
+
+ return Constructor;
+}
+
+},{"cp-data":5}],46:[function(require,module,exports){
+var Graph = require("../Graph"),
+ Digraph = require("../Digraph"),
+ CGraph = require("../CGraph"),
+ CDigraph = require("../CDigraph");
+
+exports.decode = function(nodes, edges, Ctor) {
+ Ctor = Ctor || Digraph;
+
+ if (typeOf(nodes) !== "Array") {
+ throw new Error("nodes is not an Array");
+ }
+
+ if (typeOf(edges) !== "Array") {
+ throw new Error("edges is not an Array");
+ }
+
+ if (typeof Ctor === "string") {
+ switch(Ctor) {
+ case "graph": Ctor = Graph; break;
+ case "digraph": Ctor = Digraph; break;
+ case "cgraph": Ctor = CGraph; break;
+ case "cdigraph": Ctor = CDigraph; break;
+ default: throw new Error("Unrecognized graph type: " + Ctor);
+ }
+ }
+
+ var graph = new Ctor();
+
+ nodes.forEach(function(u) {
+ graph.addNode(u.id, u.value);
+ });
+
+ // If the graph is compound, set up children...
+ if (graph.parent) {
+ nodes.forEach(function(u) {
+ if (u.children) {
+ u.children.forEach(function(v) {
+ graph.parent(v, u.id);
+ });
+ }
+ });
+ }
+
+ edges.forEach(function(e) {
+ graph.addEdge(e.id, e.u, e.v, e.value);
+ });
+
+ return graph;
+};
+
+exports.encode = function(graph) {
+ var nodes = [];
+ var edges = [];
+
+ graph.eachNode(function(u, value) {
+ var node = {id: u, value: value};
+ if (graph.children) {
+ var children = graph.children(u);
+ if (children.length) {
+ node.children = children;
+ }
+ }
+ nodes.push(node);
+ });
+
+ graph.eachEdge(function(e, u, v, value) {
+ edges.push({id: e, u: u, v: v, value: value});
+ });
+
+ var type;
+ if (graph instanceof CDigraph) {
+ type = "cdigraph";
+ } else if (graph instanceof CGraph) {
+ type = "cgraph";
+ } else if (graph instanceof Digraph) {
+ type = "digraph";
+ } else if (graph instanceof Graph) {
+ type = "graph";
+ } else {
+ throw new Error("Couldn't determine type of graph: " + graph);
+ }
+
+ return { nodes: nodes, edges: edges, type: type };
+};
+
+function typeOf(obj) {
+ return Object.prototype.toString.call(obj).slice(8, -1);
+}
+
+},{"../CDigraph":30,"../CGraph":31,"../Digraph":32,"../Graph":33}],47:[function(require,module,exports){
+/* jshint -W079 */
+var Set = require("cp-data").Set;
+/* jshint +W079 */
+
+exports.all = function() {
+ return function() { return true; };
+};
+
+exports.nodesFromList = function(nodes) {
+ var set = new Set(nodes);
+ return function(u) {
+ return set.has(u);
+ };
+};
+
+},{"cp-data":5}],48:[function(require,module,exports){
+var Graph = require("./Graph"),
+ Digraph = require("./Digraph");
+
+// Side-effect based changes are lousy, but node doesn't seem to resolve the
+// requires cycle.
+
+/**
+ * Returns a new directed graph using the nodes and edges from this graph. The
+ * new graph will have the same nodes, but will have twice the number of edges:
+ * each edge is split into two edges with opposite directions. Edge ids,
+ * consequently, are not preserved by this transformation.
+ */
+Graph.prototype.toDigraph =
+Graph.prototype.asDirected = function() {
+ var g = new Digraph();
+ this.eachNode(function(u, value) { g.addNode(u, value); });
+ this.eachEdge(function(e, u, v, value) {
+ g.addEdge(null, u, v, value);
+ g.addEdge(null, v, u, value);
+ });
+ return g;
+};
+
+/**
+ * Returns a new undirected graph using the nodes and edges from this graph.
+ * The new graph will have the same nodes, but the edges will be made
+ * undirected. Edge ids are preserved in this transformation.
+ */
+Digraph.prototype.toGraph =
+Digraph.prototype.asUndirected = function() {
+ var g = new Graph();
+ this.eachNode(function(u, value) { g.addNode(u, value); });
+ this.eachEdge(function(e, u, v, value) {
+ g.addEdge(e, u, v, value);
+ });
+ return g;
+};
+
+},{"./Digraph":32,"./Graph":33}],49:[function(require,module,exports){
+// Returns an array of all values for properties of **o**.
+exports.values = function(o) {
+ var ks = Object.keys(o),
+ len = ks.length,
+ result = new Array(len),
+ i;
+ for (i = 0; i < len; ++i) {
+ result[i] = o[ks[i]];
+ }
+ return result;
+};
+
+},{}],50:[function(require,module,exports){
+module.exports = '0.7.4';
+
+},{}]},{},[1])
+; \ No newline at end of file
diff --git a/devtools/client/shared/vendor/immutable.js b/devtools/client/shared/vendor/immutable.js
new file mode 100644
index 000000000..4903f34b3
--- /dev/null
+++ b/devtools/client/shared/vendor/immutable.js
@@ -0,0 +1,4997 @@
+/**
+ * Copyright (c) 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.Immutable = factory());
+}(this, function () { 'use strict';var SLICE$0 = Array.prototype.slice;
+
+ function createClass(ctor, superClass) {
+ if (superClass) {
+ ctor.prototype = Object.create(superClass.prototype);
+ }
+ ctor.prototype.constructor = ctor;
+ }
+
+ function Iterable(value) {
+ return isIterable(value) ? value : Seq(value);
+ }
+
+
+ createClass(KeyedIterable, Iterable);
+ function KeyedIterable(value) {
+ return isKeyed(value) ? value : KeyedSeq(value);
+ }
+
+
+ createClass(IndexedIterable, Iterable);
+ function IndexedIterable(value) {
+ return isIndexed(value) ? value : IndexedSeq(value);
+ }
+
+
+ createClass(SetIterable, Iterable);
+ function SetIterable(value) {
+ return isIterable(value) && !isAssociative(value) ? value : SetSeq(value);
+ }
+
+
+
+ function isIterable(maybeIterable) {
+ return !!(maybeIterable && maybeIterable[IS_ITERABLE_SENTINEL]);
+ }
+
+ function isKeyed(maybeKeyed) {
+ return !!(maybeKeyed && maybeKeyed[IS_KEYED_SENTINEL]);
+ }
+
+ function isIndexed(maybeIndexed) {
+ return !!(maybeIndexed && maybeIndexed[IS_INDEXED_SENTINEL]);
+ }
+
+ function isAssociative(maybeAssociative) {
+ return isKeyed(maybeAssociative) || isIndexed(maybeAssociative);
+ }
+
+ function isOrdered(maybeOrdered) {
+ return !!(maybeOrdered && maybeOrdered[IS_ORDERED_SENTINEL]);
+ }
+
+ Iterable.isIterable = isIterable;
+ Iterable.isKeyed = isKeyed;
+ Iterable.isIndexed = isIndexed;
+ Iterable.isAssociative = isAssociative;
+ Iterable.isOrdered = isOrdered;
+
+ Iterable.Keyed = KeyedIterable;
+ Iterable.Indexed = IndexedIterable;
+ Iterable.Set = SetIterable;
+
+
+ var IS_ITERABLE_SENTINEL = '@@__IMMUTABLE_ITERABLE__@@';
+ var IS_KEYED_SENTINEL = '@@__IMMUTABLE_KEYED__@@';
+ var IS_INDEXED_SENTINEL = '@@__IMMUTABLE_INDEXED__@@';
+ var IS_ORDERED_SENTINEL = '@@__IMMUTABLE_ORDERED__@@';
+
+ // Used for setting prototype methods that IE8 chokes on.
+ var DELETE = 'delete';
+
+ // Constants describing the size of trie nodes.
+ var SHIFT = 5; // Resulted in best performance after ______?
+ var SIZE = 1 << SHIFT;
+ var MASK = SIZE - 1;
+
+ // A consistent shared value representing "not set" which equals nothing other
+ // than itself, and nothing that could be provided externally.
+ var NOT_SET = {};
+
+ // Boolean references, Rough equivalent of `bool &`.
+ var CHANGE_LENGTH = { value: false };
+ var DID_ALTER = { value: false };
+
+ function MakeRef(ref) {
+ ref.value = false;
+ return ref;
+ }
+
+ function SetRef(ref) {
+ ref && (ref.value = true);
+ }
+
+ // A function which returns a value representing an "owner" for transient writes
+ // to tries. The return value will only ever equal itself, and will not equal
+ // the return of any subsequent call of this function.
+ function OwnerID() {}
+
+ // http://jsperf.com/copy-array-inline
+ function arrCopy(arr, offset) {
+ offset = offset || 0;
+ var len = Math.max(0, arr.length - offset);
+ var newArr = new Array(len);
+ for (var ii = 0; ii < len; ii++) {
+ newArr[ii] = arr[ii + offset];
+ }
+ return newArr;
+ }
+
+ function ensureSize(iter) {
+ if (iter.size === undefined) {
+ iter.size = iter.__iterate(returnTrue);
+ }
+ return iter.size;
+ }
+
+ function wrapIndex(iter, index) {
+ // This implements "is array index" which the ECMAString spec defines as:
+ //
+ // A String property name P is an array index if and only if
+ // ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal
+ // to 2^32−1.
+ //
+ // http://www.ecma-international.org/ecma-262/6.0/#sec-array-exotic-objects
+ if (typeof index !== 'number') {
+ var uint32Index = index >>> 0; // N >>> 0 is shorthand for ToUint32
+ if ('' + uint32Index !== index || uint32Index === 4294967295) {
+ return NaN;
+ }
+ index = uint32Index;
+ }
+ return index < 0 ? ensureSize(iter) + index : index;
+ }
+
+ function returnTrue() {
+ return true;
+ }
+
+ function wholeSlice(begin, end, size) {
+ return (begin === 0 || (size !== undefined && begin <= -size)) &&
+ (end === undefined || (size !== undefined && end >= size));
+ }
+
+ function resolveBegin(begin, size) {
+ return resolveIndex(begin, size, 0);
+ }
+
+ function resolveEnd(end, size) {
+ return resolveIndex(end, size, size);
+ }
+
+ function resolveIndex(index, size, defaultIndex) {
+ return index === undefined ?
+ defaultIndex :
+ index < 0 ?
+ Math.max(0, size + index) :
+ size === undefined ?
+ index :
+ Math.min(size, index);
+ }
+
+ /* global Symbol */
+
+ var ITERATE_KEYS = 0;
+ var ITERATE_VALUES = 1;
+ var ITERATE_ENTRIES = 2;
+
+ var REAL_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
+ var FAUX_ITERATOR_SYMBOL = '@@iterator';
+
+ var ITERATOR_SYMBOL = REAL_ITERATOR_SYMBOL || FAUX_ITERATOR_SYMBOL;
+
+
+ function Iterator(next) {
+ this.next = next;
+ }
+
+ Iterator.prototype.toString = function() {
+ return '[Iterator]';
+ };
+
+
+ Iterator.KEYS = ITERATE_KEYS;
+ Iterator.VALUES = ITERATE_VALUES;
+ Iterator.ENTRIES = ITERATE_ENTRIES;
+
+ Iterator.prototype.inspect =
+ Iterator.prototype.toSource = function () { return this.toString(); }
+ Iterator.prototype[ITERATOR_SYMBOL] = function () {
+ return this;
+ };
+
+
+ function iteratorValue(type, k, v, iteratorResult) {
+ var value = type === 0 ? k : type === 1 ? v : [k, v];
+ iteratorResult ? (iteratorResult.value = value) : (iteratorResult = {
+ value: value, done: false
+ });
+ return iteratorResult;
+ }
+
+ function iteratorDone() {
+ return { value: undefined, done: true };
+ }
+
+ function hasIterator(maybeIterable) {
+ return !!getIteratorFn(maybeIterable);
+ }
+
+ function isIterator(maybeIterator) {
+ return maybeIterator && typeof maybeIterator.next === 'function';
+ }
+
+ function getIterator(iterable) {
+ var iteratorFn = getIteratorFn(iterable);
+ return iteratorFn && iteratorFn.call(iterable);
+ }
+
+ function getIteratorFn(iterable) {
+ var iteratorFn = iterable && (
+ (REAL_ITERATOR_SYMBOL && iterable[REAL_ITERATOR_SYMBOL]) ||
+ iterable[FAUX_ITERATOR_SYMBOL]
+ );
+ if (typeof iteratorFn === 'function') {
+ return iteratorFn;
+ }
+ }
+
+ function isArrayLike(value) {
+ return value && typeof value.length === 'number';
+ }
+
+ createClass(Seq, Iterable);
+ function Seq(value) {
+ return value === null || value === undefined ? emptySequence() :
+ isIterable(value) ? value.toSeq() : seqFromValue(value);
+ }
+
+ Seq.of = function(/*...values*/) {
+ return Seq(arguments);
+ };
+
+ Seq.prototype.toSeq = function() {
+ return this;
+ };
+
+ Seq.prototype.toString = function() {
+ return this.__toString('Seq {', '}');
+ };
+
+ Seq.prototype.cacheResult = function() {
+ if (!this._cache && this.__iterateUncached) {
+ this._cache = this.entrySeq().toArray();
+ this.size = this._cache.length;
+ }
+ return this;
+ };
+
+ // abstract __iterateUncached(fn, reverse)
+
+ Seq.prototype.__iterate = function(fn, reverse) {
+ return seqIterate(this, fn, reverse, true);
+ };
+
+ // abstract __iteratorUncached(type, reverse)
+
+ Seq.prototype.__iterator = function(type, reverse) {
+ return seqIterator(this, type, reverse, true);
+ };
+
+
+
+ createClass(KeyedSeq, Seq);
+ function KeyedSeq(value) {
+ return value === null || value === undefined ?
+ emptySequence().toKeyedSeq() :
+ isIterable(value) ?
+ (isKeyed(value) ? value.toSeq() : value.fromEntrySeq()) :
+ keyedSeqFromValue(value);
+ }
+
+ KeyedSeq.prototype.toKeyedSeq = function() {
+ return this;
+ };
+
+
+
+ createClass(IndexedSeq, Seq);
+ function IndexedSeq(value) {
+ return value === null || value === undefined ? emptySequence() :
+ !isIterable(value) ? indexedSeqFromValue(value) :
+ isKeyed(value) ? value.entrySeq() : value.toIndexedSeq();
+ }
+
+ IndexedSeq.of = function(/*...values*/) {
+ return IndexedSeq(arguments);
+ };
+
+ IndexedSeq.prototype.toIndexedSeq = function() {
+ return this;
+ };
+
+ IndexedSeq.prototype.toString = function() {
+ return this.__toString('Seq [', ']');
+ };
+
+ IndexedSeq.prototype.__iterate = function(fn, reverse) {
+ return seqIterate(this, fn, reverse, false);
+ };
+
+ IndexedSeq.prototype.__iterator = function(type, reverse) {
+ return seqIterator(this, type, reverse, false);
+ };
+
+
+
+ createClass(SetSeq, Seq);
+ function SetSeq(value) {
+ return (
+ value === null || value === undefined ? emptySequence() :
+ !isIterable(value) ? indexedSeqFromValue(value) :
+ isKeyed(value) ? value.entrySeq() : value
+ ).toSetSeq();
+ }
+
+ SetSeq.of = function(/*...values*/) {
+ return SetSeq(arguments);
+ };
+
+ SetSeq.prototype.toSetSeq = function() {
+ return this;
+ };
+
+
+
+ Seq.isSeq = isSeq;
+ Seq.Keyed = KeyedSeq;
+ Seq.Set = SetSeq;
+ Seq.Indexed = IndexedSeq;
+
+ var IS_SEQ_SENTINEL = '@@__IMMUTABLE_SEQ__@@';
+
+ Seq.prototype[IS_SEQ_SENTINEL] = true;
+
+
+
+ createClass(ArraySeq, IndexedSeq);
+ function ArraySeq(array) {
+ this._array = array;
+ this.size = array.length;
+ }
+
+ ArraySeq.prototype.get = function(index, notSetValue) {
+ return this.has(index) ? this._array[wrapIndex(this, index)] : notSetValue;
+ };
+
+ ArraySeq.prototype.__iterate = function(fn, reverse) {
+ var array = this._array;
+ var maxIndex = array.length - 1;
+ for (var ii = 0; ii <= maxIndex; ii++) {
+ if (fn(array[reverse ? maxIndex - ii : ii], ii, this) === false) {
+ return ii + 1;
+ }
+ }
+ return ii;
+ };
+
+ ArraySeq.prototype.__iterator = function(type, reverse) {
+ var array = this._array;
+ var maxIndex = array.length - 1;
+ var ii = 0;
+ return new Iterator(function()
+ {return ii > maxIndex ?
+ iteratorDone() :
+ iteratorValue(type, ii, array[reverse ? maxIndex - ii++ : ii++])}
+ );
+ };
+
+
+
+ createClass(ObjectSeq, KeyedSeq);
+ function ObjectSeq(object) {
+ var keys = Object.keys(object);
+ this._object = object;
+ this._keys = keys;
+ this.size = keys.length;
+ }
+
+ ObjectSeq.prototype.get = function(key, notSetValue) {
+ if (notSetValue !== undefined && !this.has(key)) {
+ return notSetValue;
+ }
+ return this._object[key];
+ };
+
+ ObjectSeq.prototype.has = function(key) {
+ return this._object.hasOwnProperty(key);
+ };
+
+ ObjectSeq.prototype.__iterate = function(fn, reverse) {
+ var object = this._object;
+ var keys = this._keys;
+ var maxIndex = keys.length - 1;
+ for (var ii = 0; ii <= maxIndex; ii++) {
+ var key = keys[reverse ? maxIndex - ii : ii];
+ if (fn(object[key], key, this) === false) {
+ return ii + 1;
+ }
+ }
+ return ii;
+ };
+
+ ObjectSeq.prototype.__iterator = function(type, reverse) {
+ var object = this._object;
+ var keys = this._keys;
+ var maxIndex = keys.length - 1;
+ var ii = 0;
+ return new Iterator(function() {
+ var key = keys[reverse ? maxIndex - ii : ii];
+ return ii++ > maxIndex ?
+ iteratorDone() :
+ iteratorValue(type, key, object[key]);
+ });
+ };
+
+ ObjectSeq.prototype[IS_ORDERED_SENTINEL] = true;
+
+
+ createClass(IterableSeq, IndexedSeq);
+ function IterableSeq(iterable) {
+ this._iterable = iterable;
+ this.size = iterable.length || iterable.size;
+ }
+
+ IterableSeq.prototype.__iterateUncached = function(fn, reverse) {
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var iterable = this._iterable;
+ var iterator = getIterator(iterable);
+ var iterations = 0;
+ if (isIterator(iterator)) {
+ var step;
+ while (!(step = iterator.next()).done) {
+ if (fn(step.value, iterations++, this) === false) {
+ break;
+ }
+ }
+ }
+ return iterations;
+ };
+
+ IterableSeq.prototype.__iteratorUncached = function(type, reverse) {
+ if (reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ var iterable = this._iterable;
+ var iterator = getIterator(iterable);
+ if (!isIterator(iterator)) {
+ return new Iterator(iteratorDone);
+ }
+ var iterations = 0;
+ return new Iterator(function() {
+ var step = iterator.next();
+ return step.done ? step : iteratorValue(type, iterations++, step.value);
+ });
+ };
+
+
+
+ createClass(IteratorSeq, IndexedSeq);
+ function IteratorSeq(iterator) {
+ this._iterator = iterator;
+ this._iteratorCache = [];
+ }
+
+ IteratorSeq.prototype.__iterateUncached = function(fn, reverse) {
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var iterator = this._iterator;
+ var cache = this._iteratorCache;
+ var iterations = 0;
+ while (iterations < cache.length) {
+ if (fn(cache[iterations], iterations++, this) === false) {
+ return iterations;
+ }
+ }
+ var step;
+ while (!(step = iterator.next()).done) {
+ var val = step.value;
+ cache[iterations] = val;
+ if (fn(val, iterations++, this) === false) {
+ break;
+ }
+ }
+ return iterations;
+ };
+
+ IteratorSeq.prototype.__iteratorUncached = function(type, reverse) {
+ if (reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ var iterator = this._iterator;
+ var cache = this._iteratorCache;
+ var iterations = 0;
+ return new Iterator(function() {
+ if (iterations >= cache.length) {
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ cache[iterations] = step.value;
+ }
+ return iteratorValue(type, iterations, cache[iterations++]);
+ });
+ };
+
+
+
+
+ // # pragma Helper functions
+
+ function isSeq(maybeSeq) {
+ return !!(maybeSeq && maybeSeq[IS_SEQ_SENTINEL]);
+ }
+
+ var EMPTY_SEQ;
+
+ function emptySequence() {
+ return EMPTY_SEQ || (EMPTY_SEQ = new ArraySeq([]));
+ }
+
+ function keyedSeqFromValue(value) {
+ var seq =
+ Array.isArray(value) ? new ArraySeq(value).fromEntrySeq() :
+ isIterator(value) ? new IteratorSeq(value).fromEntrySeq() :
+ hasIterator(value) ? new IterableSeq(value).fromEntrySeq() :
+ typeof value === 'object' ? new ObjectSeq(value) :
+ undefined;
+ if (!seq) {
+ throw new TypeError(
+ 'Expected Array or iterable object of [k, v] entries, '+
+ 'or keyed object: ' + value
+ );
+ }
+ return seq;
+ }
+
+ function indexedSeqFromValue(value) {
+ var seq = maybeIndexedSeqFromValue(value);
+ if (!seq) {
+ throw new TypeError(
+ 'Expected Array or iterable object of values: ' + value
+ );
+ }
+ return seq;
+ }
+
+ function seqFromValue(value) {
+ var seq = maybeIndexedSeqFromValue(value) ||
+ (typeof value === 'object' && new ObjectSeq(value));
+ if (!seq) {
+ throw new TypeError(
+ 'Expected Array or iterable object of values, or keyed object: ' + value
+ );
+ }
+ return seq;
+ }
+
+ function maybeIndexedSeqFromValue(value) {
+ return (
+ isArrayLike(value) ? new ArraySeq(value) :
+ isIterator(value) ? new IteratorSeq(value) :
+ hasIterator(value) ? new IterableSeq(value) :
+ undefined
+ );
+ }
+
+ function seqIterate(seq, fn, reverse, useKeys) {
+ var cache = seq._cache;
+ if (cache) {
+ var maxIndex = cache.length - 1;
+ for (var ii = 0; ii <= maxIndex; ii++) {
+ var entry = cache[reverse ? maxIndex - ii : ii];
+ if (fn(entry[1], useKeys ? entry[0] : ii, seq) === false) {
+ return ii + 1;
+ }
+ }
+ return ii;
+ }
+ return seq.__iterateUncached(fn, reverse);
+ }
+
+ function seqIterator(seq, type, reverse, useKeys) {
+ var cache = seq._cache;
+ if (cache) {
+ var maxIndex = cache.length - 1;
+ var ii = 0;
+ return new Iterator(function() {
+ var entry = cache[reverse ? maxIndex - ii : ii];
+ return ii++ > maxIndex ?
+ iteratorDone() :
+ iteratorValue(type, useKeys ? entry[0] : ii - 1, entry[1]);
+ });
+ }
+ return seq.__iteratorUncached(type, reverse);
+ }
+
+ function fromJS(json, converter) {
+ return converter ?
+ fromJSWith(converter, json, '', {'': json}) :
+ fromJSDefault(json);
+ }
+
+ function fromJSWith(converter, json, key, parentJSON) {
+ if (Array.isArray(json)) {
+ return converter.call(parentJSON, key, IndexedSeq(json).map(function(v, k) {return fromJSWith(converter, v, k, json)}));
+ }
+ if (isPlainObj(json)) {
+ return converter.call(parentJSON, key, KeyedSeq(json).map(function(v, k) {return fromJSWith(converter, v, k, json)}));
+ }
+ return json;
+ }
+
+ function fromJSDefault(json) {
+ if (Array.isArray(json)) {
+ return IndexedSeq(json).map(fromJSDefault).toList();
+ }
+ if (isPlainObj(json)) {
+ return KeyedSeq(json).map(fromJSDefault).toMap();
+ }
+ return json;
+ }
+
+ function isPlainObj(value) {
+ return value && (value.constructor === Object || value.constructor === undefined);
+ }
+
+ /**
+ * An extension of the "same-value" algorithm as [described for use by ES6 Map
+ * and Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Key_equality)
+ *
+ * NaN is considered the same as NaN, however -0 and 0 are considered the same
+ * value, which is different from the algorithm described by
+ * [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is).
+ *
+ * This is extended further to allow Objects to describe the values they
+ * represent, by way of `valueOf` or `equals` (and `hashCode`).
+ *
+ * Note: because of this extension, the key equality of Immutable.Map and the
+ * value equality of Immutable.Set will differ from ES6 Map and Set.
+ *
+ * ### Defining custom values
+ *
+ * The easiest way to describe the value an object represents is by implementing
+ * `valueOf`. For example, `Date` represents a value by returning a unix
+ * timestamp for `valueOf`:
+ *
+ * var date1 = new Date(1234567890000); // Fri Feb 13 2009 ...
+ * var date2 = new Date(1234567890000);
+ * date1.valueOf(); // 1234567890000
+ * assert( date1 !== date2 );
+ * assert( Immutable.is( date1, date2 ) );
+ *
+ * Note: overriding `valueOf` may have other implications if you use this object
+ * where JavaScript expects a primitive, such as implicit string coercion.
+ *
+ * For more complex types, especially collections, implementing `valueOf` may
+ * not be performant. An alternative is to implement `equals` and `hashCode`.
+ *
+ * `equals` takes another object, presumably of similar type, and returns true
+ * if the it is equal. Equality is symmetrical, so the same result should be
+ * returned if this and the argument are flipped.
+ *
+ * assert( a.equals(b) === b.equals(a) );
+ *
+ * `hashCode` returns a 32bit integer number representing the object which will
+ * be used to determine how to store the value object in a Map or Set. You must
+ * provide both or neither methods, one must not exist without the other.
+ *
+ * Also, an important relationship between these methods must be upheld: if two
+ * values are equal, they *must* return the same hashCode. If the values are not
+ * equal, they might have the same hashCode; this is called a hash collision,
+ * and while undesirable for performance reasons, it is acceptable.
+ *
+ * if (a.equals(b)) {
+ * assert( a.hashCode() === b.hashCode() );
+ * }
+ *
+ * All Immutable collections implement `equals` and `hashCode`.
+ *
+ */
+ function is(valueA, valueB) {
+ if (valueA === valueB || (valueA !== valueA && valueB !== valueB)) {
+ return true;
+ }
+ if (!valueA || !valueB) {
+ return false;
+ }
+ if (typeof valueA.valueOf === 'function' &&
+ typeof valueB.valueOf === 'function') {
+ valueA = valueA.valueOf();
+ valueB = valueB.valueOf();
+ if (valueA === valueB || (valueA !== valueA && valueB !== valueB)) {
+ return true;
+ }
+ if (!valueA || !valueB) {
+ return false;
+ }
+ }
+ if (typeof valueA.equals === 'function' &&
+ typeof valueB.equals === 'function' &&
+ valueA.equals(valueB)) {
+ return true;
+ }
+ return false;
+ }
+
+ function deepEqual(a, b) {
+ if (a === b) {
+ return true;
+ }
+
+ if (
+ !isIterable(b) ||
+ a.size !== undefined && b.size !== undefined && a.size !== b.size ||
+ a.__hash !== undefined && b.__hash !== undefined && a.__hash !== b.__hash ||
+ isKeyed(a) !== isKeyed(b) ||
+ isIndexed(a) !== isIndexed(b) ||
+ isOrdered(a) !== isOrdered(b)
+ ) {
+ return false;
+ }
+
+ if (a.size === 0 && b.size === 0) {
+ return true;
+ }
+
+ var notAssociative = !isAssociative(a);
+
+ if (isOrdered(a)) {
+ var entries = a.entries();
+ return b.every(function(v, k) {
+ var entry = entries.next().value;
+ return entry && is(entry[1], v) && (notAssociative || is(entry[0], k));
+ }) && entries.next().done;
+ }
+
+ var flipped = false;
+
+ if (a.size === undefined) {
+ if (b.size === undefined) {
+ if (typeof a.cacheResult === 'function') {
+ a.cacheResult();
+ }
+ } else {
+ flipped = true;
+ var _ = a;
+ a = b;
+ b = _;
+ }
+ }
+
+ var allEqual = true;
+ var bSize = b.__iterate(function(v, k) {
+ if (notAssociative ? !a.has(v) :
+ flipped ? !is(v, a.get(k, NOT_SET)) : !is(a.get(k, NOT_SET), v)) {
+ allEqual = false;
+ return false;
+ }
+ });
+
+ return allEqual && a.size === bSize;
+ }
+
+ createClass(Repeat, IndexedSeq);
+
+ function Repeat(value, times) {
+ if (!(this instanceof Repeat)) {
+ return new Repeat(value, times);
+ }
+ this._value = value;
+ this.size = times === undefined ? Infinity : Math.max(0, times);
+ if (this.size === 0) {
+ if (EMPTY_REPEAT) {
+ return EMPTY_REPEAT;
+ }
+ EMPTY_REPEAT = this;
+ }
+ }
+
+ Repeat.prototype.toString = function() {
+ if (this.size === 0) {
+ return 'Repeat []';
+ }
+ return 'Repeat [ ' + this._value + ' ' + this.size + ' times ]';
+ };
+
+ Repeat.prototype.get = function(index, notSetValue) {
+ return this.has(index) ? this._value : notSetValue;
+ };
+
+ Repeat.prototype.includes = function(searchValue) {
+ return is(this._value, searchValue);
+ };
+
+ Repeat.prototype.slice = function(begin, end) {
+ var size = this.size;
+ return wholeSlice(begin, end, size) ? this :
+ new Repeat(this._value, resolveEnd(end, size) - resolveBegin(begin, size));
+ };
+
+ Repeat.prototype.reverse = function() {
+ return this;
+ };
+
+ Repeat.prototype.indexOf = function(searchValue) {
+ if (is(this._value, searchValue)) {
+ return 0;
+ }
+ return -1;
+ };
+
+ Repeat.prototype.lastIndexOf = function(searchValue) {
+ if (is(this._value, searchValue)) {
+ return this.size;
+ }
+ return -1;
+ };
+
+ Repeat.prototype.__iterate = function(fn, reverse) {
+ for (var ii = 0; ii < this.size; ii++) {
+ if (fn(this._value, ii, this) === false) {
+ return ii + 1;
+ }
+ }
+ return ii;
+ };
+
+ Repeat.prototype.__iterator = function(type, reverse) {var this$0 = this;
+ var ii = 0;
+ return new Iterator(function()
+ {return ii < this$0.size ? iteratorValue(type, ii++, this$0._value) : iteratorDone()}
+ );
+ };
+
+ Repeat.prototype.equals = function(other) {
+ return other instanceof Repeat ?
+ is(this._value, other._value) :
+ deepEqual(other);
+ };
+
+
+ var EMPTY_REPEAT;
+
+ function invariant(condition, error) {
+ if (!condition) throw new Error(error);
+ }
+
+ createClass(Range, IndexedSeq);
+
+ function Range(start, end, step) {
+ if (!(this instanceof Range)) {
+ return new Range(start, end, step);
+ }
+ invariant(step !== 0, 'Cannot step a Range by 0');
+ start = start || 0;
+ if (end === undefined) {
+ end = Infinity;
+ }
+ step = step === undefined ? 1 : Math.abs(step);
+ if (end < start) {
+ step = -step;
+ }
+ this._start = start;
+ this._end = end;
+ this._step = step;
+ this.size = Math.max(0, Math.ceil((end - start) / step - 1) + 1);
+ if (this.size === 0) {
+ if (EMPTY_RANGE) {
+ return EMPTY_RANGE;
+ }
+ EMPTY_RANGE = this;
+ }
+ }
+
+ Range.prototype.toString = function() {
+ if (this.size === 0) {
+ return 'Range []';
+ }
+ return 'Range [ ' +
+ this._start + '...' + this._end +
+ (this._step !== 1 ? ' by ' + this._step : '') +
+ ' ]';
+ };
+
+ Range.prototype.get = function(index, notSetValue) {
+ return this.has(index) ?
+ this._start + wrapIndex(this, index) * this._step :
+ notSetValue;
+ };
+
+ Range.prototype.includes = function(searchValue) {
+ var possibleIndex = (searchValue - this._start) / this._step;
+ return possibleIndex >= 0 &&
+ possibleIndex < this.size &&
+ possibleIndex === Math.floor(possibleIndex);
+ };
+
+ Range.prototype.slice = function(begin, end) {
+ if (wholeSlice(begin, end, this.size)) {
+ return this;
+ }
+ begin = resolveBegin(begin, this.size);
+ end = resolveEnd(end, this.size);
+ if (end <= begin) {
+ return new Range(0, 0);
+ }
+ return new Range(this.get(begin, this._end), this.get(end, this._end), this._step);
+ };
+
+ Range.prototype.indexOf = function(searchValue) {
+ var offsetValue = searchValue - this._start;
+ if (offsetValue % this._step === 0) {
+ var index = offsetValue / this._step;
+ if (index >= 0 && index < this.size) {
+ return index
+ }
+ }
+ return -1;
+ };
+
+ Range.prototype.lastIndexOf = function(searchValue) {
+ return this.indexOf(searchValue);
+ };
+
+ Range.prototype.__iterate = function(fn, reverse) {
+ var maxIndex = this.size - 1;
+ var step = this._step;
+ var value = reverse ? this._start + maxIndex * step : this._start;
+ for (var ii = 0; ii <= maxIndex; ii++) {
+ if (fn(value, ii, this) === false) {
+ return ii + 1;
+ }
+ value += reverse ? -step : step;
+ }
+ return ii;
+ };
+
+ Range.prototype.__iterator = function(type, reverse) {
+ var maxIndex = this.size - 1;
+ var step = this._step;
+ var value = reverse ? this._start + maxIndex * step : this._start;
+ var ii = 0;
+ return new Iterator(function() {
+ var v = value;
+ value += reverse ? -step : step;
+ return ii > maxIndex ? iteratorDone() : iteratorValue(type, ii++, v);
+ });
+ };
+
+ Range.prototype.equals = function(other) {
+ return other instanceof Range ?
+ this._start === other._start &&
+ this._end === other._end &&
+ this._step === other._step :
+ deepEqual(this, other);
+ };
+
+
+ var EMPTY_RANGE;
+
+ createClass(Collection, Iterable);
+ function Collection() {
+ throw TypeError('Abstract');
+ }
+
+
+ createClass(KeyedCollection, Collection);function KeyedCollection() {}
+
+ createClass(IndexedCollection, Collection);function IndexedCollection() {}
+
+ createClass(SetCollection, Collection);function SetCollection() {}
+
+
+ Collection.Keyed = KeyedCollection;
+ Collection.Indexed = IndexedCollection;
+ Collection.Set = SetCollection;
+
+ var imul =
+ typeof Math.imul === 'function' && Math.imul(0xffffffff, 2) === -2 ?
+ Math.imul :
+ function imul(a, b) {
+ a = a | 0; // int
+ b = b | 0; // int
+ var c = a & 0xffff;
+ var d = b & 0xffff;
+ // Shift by 0 fixes the sign on the high part.
+ return (c * d) + ((((a >>> 16) * d + c * (b >>> 16)) << 16) >>> 0) | 0; // int
+ };
+
+ // v8 has an optimization for storing 31-bit signed numbers.
+ // Values which have either 00 or 11 as the high order bits qualify.
+ // This function drops the highest order bit in a signed number, maintaining
+ // the sign bit.
+ function smi(i32) {
+ return ((i32 >>> 1) & 0x40000000) | (i32 & 0xBFFFFFFF);
+ }
+
+ function hash(o) {
+ if (o === false || o === null || o === undefined) {
+ return 0;
+ }
+ if (typeof o.valueOf === 'function') {
+ o = o.valueOf();
+ if (o === false || o === null || o === undefined) {
+ return 0;
+ }
+ }
+ if (o === true) {
+ return 1;
+ }
+ var type = typeof o;
+ if (type === 'number') {
+ var h = o | 0;
+ if (h !== o) {
+ h ^= o * 0xFFFFFFFF;
+ }
+ while (o > 0xFFFFFFFF) {
+ o /= 0xFFFFFFFF;
+ h ^= o;
+ }
+ return smi(h);
+ }
+ if (type === 'string') {
+ return o.length > STRING_HASH_CACHE_MIN_STRLEN ? cachedHashString(o) : hashString(o);
+ }
+ if (typeof o.hashCode === 'function') {
+ return o.hashCode();
+ }
+ if (type === 'object') {
+ return hashJSObj(o);
+ }
+ if (typeof o.toString === 'function') {
+ return hashString(o.toString());
+ }
+ throw new Error('Value type ' + type + ' cannot be hashed.');
+ }
+
+ function cachedHashString(string) {
+ var hash = stringHashCache[string];
+ if (hash === undefined) {
+ hash = hashString(string);
+ if (STRING_HASH_CACHE_SIZE === STRING_HASH_CACHE_MAX_SIZE) {
+ STRING_HASH_CACHE_SIZE = 0;
+ stringHashCache = {};
+ }
+ STRING_HASH_CACHE_SIZE++;
+ stringHashCache[string] = hash;
+ }
+ return hash;
+ }
+
+ // http://jsperf.com/hashing-strings
+ function hashString(string) {
+ // This is the hash from JVM
+ // The hash code for a string is computed as
+ // s[0] * 31 ^ (n - 1) + s[1] * 31 ^ (n - 2) + ... + s[n - 1],
+ // where s[i] is the ith character of the string and n is the length of
+ // the string. We "mod" the result to make it between 0 (inclusive) and 2^31
+ // (exclusive) by dropping high bits.
+ var hash = 0;
+ for (var ii = 0; ii < string.length; ii++) {
+ hash = 31 * hash + string.charCodeAt(ii) | 0;
+ }
+ return smi(hash);
+ }
+
+ function hashJSObj(obj) {
+ var hash;
+ if (usingWeakMap) {
+ hash = weakMap.get(obj);
+ if (hash !== undefined) {
+ return hash;
+ }
+ }
+
+ hash = obj[UID_HASH_KEY];
+ if (hash !== undefined) {
+ return hash;
+ }
+
+ if (!canDefineProperty) {
+ hash = obj.propertyIsEnumerable && obj.propertyIsEnumerable[UID_HASH_KEY];
+ if (hash !== undefined) {
+ return hash;
+ }
+
+ hash = getIENodeHash(obj);
+ if (hash !== undefined) {
+ return hash;
+ }
+ }
+
+ hash = ++objHashUID;
+ if (objHashUID & 0x40000000) {
+ objHashUID = 0;
+ }
+
+ if (usingWeakMap) {
+ weakMap.set(obj, hash);
+ } else if (isExtensible !== undefined && isExtensible(obj) === false) {
+ throw new Error('Non-extensible objects are not allowed as keys.');
+ } else if (canDefineProperty) {
+ Object.defineProperty(obj, UID_HASH_KEY, {
+ 'enumerable': false,
+ 'configurable': false,
+ 'writable': false,
+ 'value': hash
+ });
+ } else if (obj.propertyIsEnumerable !== undefined &&
+ obj.propertyIsEnumerable === obj.constructor.prototype.propertyIsEnumerable) {
+ // Since we can't define a non-enumerable property on the object
+ // we'll hijack one of the less-used non-enumerable properties to
+ // save our hash on it. Since this is a function it will not show up in
+ // `JSON.stringify` which is what we want.
+ obj.propertyIsEnumerable = function() {
+ return this.constructor.prototype.propertyIsEnumerable.apply(this, arguments);
+ };
+ obj.propertyIsEnumerable[UID_HASH_KEY] = hash;
+ } else if (obj.nodeType !== undefined) {
+ // At this point we couldn't get the IE `uniqueID` to use as a hash
+ // and we couldn't use a non-enumerable property to exploit the
+ // dontEnum bug so we simply add the `UID_HASH_KEY` on the node
+ // itself.
+ obj[UID_HASH_KEY] = hash;
+ } else {
+ throw new Error('Unable to set a non-enumerable property on object.');
+ }
+
+ return hash;
+ }
+
+ // Get references to ES5 object methods.
+ var isExtensible = Object.isExtensible;
+
+ // True if Object.defineProperty works as expected. IE8 fails this test.
+ var canDefineProperty = (function() {
+ try {
+ Object.defineProperty({}, '@', {});
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }());
+
+ // IE has a `uniqueID` property on DOM nodes. We can construct the hash from it
+ // and avoid memory leaks from the IE cloneNode bug.
+ function getIENodeHash(node) {
+ if (node && node.nodeType > 0) {
+ switch (node.nodeType) {
+ case 1: // Element
+ return node.uniqueID;
+ case 9: // Document
+ return node.documentElement && node.documentElement.uniqueID;
+ }
+ }
+ }
+
+ // If possible, use a WeakMap.
+ var usingWeakMap = typeof WeakMap === 'function';
+ var weakMap;
+ if (usingWeakMap) {
+ weakMap = new WeakMap();
+ }
+
+ var objHashUID = 0;
+
+ var UID_HASH_KEY = '__immutablehash__';
+ if (typeof Symbol === 'function') {
+ UID_HASH_KEY = Symbol(UID_HASH_KEY);
+ }
+
+ var STRING_HASH_CACHE_MIN_STRLEN = 16;
+ var STRING_HASH_CACHE_MAX_SIZE = 255;
+ var STRING_HASH_CACHE_SIZE = 0;
+ var stringHashCache = {};
+
+ function assertNotInfinite(size) {
+ invariant(
+ size !== Infinity,
+ 'Cannot perform this action with an infinite size.'
+ );
+ }
+
+ createClass(Map, KeyedCollection);
+
+ // @pragma Construction
+
+ function Map(value) {
+ return value === null || value === undefined ? emptyMap() :
+ isMap(value) && !isOrdered(value) ? value :
+ emptyMap().withMutations(function(map ) {
+ var iter = KeyedIterable(value);
+ assertNotInfinite(iter.size);
+ iter.forEach(function(v, k) {return map.set(k, v)});
+ });
+ }
+
+ Map.of = function() {var keyValues = SLICE$0.call(arguments, 0);
+ return emptyMap().withMutations(function(map ) {
+ for (var i = 0; i < keyValues.length; i += 2) {
+ if (i + 1 >= keyValues.length) {
+ throw new Error('Missing value for key: ' + keyValues[i]);
+ }
+ map.set(keyValues[i], keyValues[i + 1]);
+ }
+ });
+ };
+
+ Map.prototype.toString = function() {
+ return this.__toString('Map {', '}');
+ };
+
+ // @pragma Access
+
+ Map.prototype.get = function(k, notSetValue) {
+ return this._root ?
+ this._root.get(0, undefined, k, notSetValue) :
+ notSetValue;
+ };
+
+ // @pragma Modification
+
+ Map.prototype.set = function(k, v) {
+ return updateMap(this, k, v);
+ };
+
+ Map.prototype.setIn = function(keyPath, v) {
+ return this.updateIn(keyPath, NOT_SET, function() {return v});
+ };
+
+ Map.prototype.remove = function(k) {
+ return updateMap(this, k, NOT_SET);
+ };
+
+ Map.prototype.deleteIn = function(keyPath) {
+ return this.updateIn(keyPath, function() {return NOT_SET});
+ };
+
+ Map.prototype.update = function(k, notSetValue, updater) {
+ return arguments.length === 1 ?
+ k(this) :
+ this.updateIn([k], notSetValue, updater);
+ };
+
+ Map.prototype.updateIn = function(keyPath, notSetValue, updater) {
+ if (!updater) {
+ updater = notSetValue;
+ notSetValue = undefined;
+ }
+ var updatedValue = updateInDeepMap(
+ this,
+ forceIterator(keyPath),
+ notSetValue,
+ updater
+ );
+ return updatedValue === NOT_SET ? undefined : updatedValue;
+ };
+
+ Map.prototype.clear = function() {
+ if (this.size === 0) {
+ return this;
+ }
+ if (this.__ownerID) {
+ this.size = 0;
+ this._root = null;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return emptyMap();
+ };
+
+ // @pragma Composition
+
+ Map.prototype.merge = function(/*...iters*/) {
+ return mergeIntoMapWith(this, undefined, arguments);
+ };
+
+ Map.prototype.mergeWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return mergeIntoMapWith(this, merger, iters);
+ };
+
+ Map.prototype.mergeIn = function(keyPath) {var iters = SLICE$0.call(arguments, 1);
+ return this.updateIn(
+ keyPath,
+ emptyMap(),
+ function(m ) {return typeof m.merge === 'function' ?
+ m.merge.apply(m, iters) :
+ iters[iters.length - 1]}
+ );
+ };
+
+ Map.prototype.mergeDeep = function(/*...iters*/) {
+ return mergeIntoMapWith(this, deepMerger, arguments);
+ };
+
+ Map.prototype.mergeDeepWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return mergeIntoMapWith(this, deepMergerWith(merger), iters);
+ };
+
+ Map.prototype.mergeDeepIn = function(keyPath) {var iters = SLICE$0.call(arguments, 1);
+ return this.updateIn(
+ keyPath,
+ emptyMap(),
+ function(m ) {return typeof m.mergeDeep === 'function' ?
+ m.mergeDeep.apply(m, iters) :
+ iters[iters.length - 1]}
+ );
+ };
+
+ Map.prototype.sort = function(comparator) {
+ // Late binding
+ return OrderedMap(sortFactory(this, comparator));
+ };
+
+ Map.prototype.sortBy = function(mapper, comparator) {
+ // Late binding
+ return OrderedMap(sortFactory(this, comparator, mapper));
+ };
+
+ // @pragma Mutability
+
+ Map.prototype.withMutations = function(fn) {
+ var mutable = this.asMutable();
+ fn(mutable);
+ return mutable.wasAltered() ? mutable.__ensureOwner(this.__ownerID) : this;
+ };
+
+ Map.prototype.asMutable = function() {
+ return this.__ownerID ? this : this.__ensureOwner(new OwnerID());
+ };
+
+ Map.prototype.asImmutable = function() {
+ return this.__ensureOwner();
+ };
+
+ Map.prototype.wasAltered = function() {
+ return this.__altered;
+ };
+
+ Map.prototype.__iterator = function(type, reverse) {
+ return new MapIterator(this, type, reverse);
+ };
+
+ Map.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ var iterations = 0;
+ this._root && this._root.iterate(function(entry ) {
+ iterations++;
+ return fn(entry[1], entry[0], this$0);
+ }, reverse);
+ return iterations;
+ };
+
+ Map.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this.__altered = false;
+ return this;
+ }
+ return makeMap(this.size, this._root, ownerID, this.__hash);
+ };
+
+
+ function isMap(maybeMap) {
+ return !!(maybeMap && maybeMap[IS_MAP_SENTINEL]);
+ }
+
+ Map.isMap = isMap;
+
+ var IS_MAP_SENTINEL = '@@__IMMUTABLE_MAP__@@';
+
+ var MapPrototype = Map.prototype;
+ MapPrototype[IS_MAP_SENTINEL] = true;
+ MapPrototype[DELETE] = MapPrototype.remove;
+ MapPrototype.removeIn = MapPrototype.deleteIn;
+
+
+ // #pragma Trie Nodes
+
+
+
+ function ArrayMapNode(ownerID, entries) {
+ this.ownerID = ownerID;
+ this.entries = entries;
+ }
+
+ ArrayMapNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ var entries = this.entries;
+ for (var ii = 0, len = entries.length; ii < len; ii++) {
+ if (is(key, entries[ii][0])) {
+ return entries[ii][1];
+ }
+ }
+ return notSetValue;
+ };
+
+ ArrayMapNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ var removed = value === NOT_SET;
+
+ var entries = this.entries;
+ var idx = 0;
+ for (var len = entries.length; idx < len; idx++) {
+ if (is(key, entries[idx][0])) {
+ break;
+ }
+ }
+ var exists = idx < len;
+
+ if (exists ? entries[idx][1] === value : removed) {
+ return this;
+ }
+
+ SetRef(didAlter);
+ (removed || !exists) && SetRef(didChangeSize);
+
+ if (removed && entries.length === 1) {
+ return; // undefined
+ }
+
+ if (!exists && !removed && entries.length >= MAX_ARRAY_MAP_SIZE) {
+ return createNodes(ownerID, entries, key, value);
+ }
+
+ var isEditable = ownerID && ownerID === this.ownerID;
+ var newEntries = isEditable ? entries : arrCopy(entries);
+
+ if (exists) {
+ if (removed) {
+ idx === len - 1 ? newEntries.pop() : (newEntries[idx] = newEntries.pop());
+ } else {
+ newEntries[idx] = [key, value];
+ }
+ } else {
+ newEntries.push([key, value]);
+ }
+
+ if (isEditable) {
+ this.entries = newEntries;
+ return this;
+ }
+
+ return new ArrayMapNode(ownerID, newEntries);
+ };
+
+
+
+
+ function BitmapIndexedNode(ownerID, bitmap, nodes) {
+ this.ownerID = ownerID;
+ this.bitmap = bitmap;
+ this.nodes = nodes;
+ }
+
+ BitmapIndexedNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+ var bit = (1 << ((shift === 0 ? keyHash : keyHash >>> shift) & MASK));
+ var bitmap = this.bitmap;
+ return (bitmap & bit) === 0 ? notSetValue :
+ this.nodes[popCount(bitmap & (bit - 1))].get(shift + SHIFT, keyHash, key, notSetValue);
+ };
+
+ BitmapIndexedNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+ var keyHashFrag = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
+ var bit = 1 << keyHashFrag;
+ var bitmap = this.bitmap;
+ var exists = (bitmap & bit) !== 0;
+
+ if (!exists && value === NOT_SET) {
+ return this;
+ }
+
+ var idx = popCount(bitmap & (bit - 1));
+ var nodes = this.nodes;
+ var node = exists ? nodes[idx] : undefined;
+ var newNode = updateNode(node, ownerID, shift + SHIFT, keyHash, key, value, didChangeSize, didAlter);
+
+ if (newNode === node) {
+ return this;
+ }
+
+ if (!exists && newNode && nodes.length >= MAX_BITMAP_INDEXED_SIZE) {
+ return expandNodes(ownerID, nodes, bitmap, keyHashFrag, newNode);
+ }
+
+ if (exists && !newNode && nodes.length === 2 && isLeafNode(nodes[idx ^ 1])) {
+ return nodes[idx ^ 1];
+ }
+
+ if (exists && newNode && nodes.length === 1 && isLeafNode(newNode)) {
+ return newNode;
+ }
+
+ var isEditable = ownerID && ownerID === this.ownerID;
+ var newBitmap = exists ? newNode ? bitmap : bitmap ^ bit : bitmap | bit;
+ var newNodes = exists ? newNode ?
+ setIn(nodes, idx, newNode, isEditable) :
+ spliceOut(nodes, idx, isEditable) :
+ spliceIn(nodes, idx, newNode, isEditable);
+
+ if (isEditable) {
+ this.bitmap = newBitmap;
+ this.nodes = newNodes;
+ return this;
+ }
+
+ return new BitmapIndexedNode(ownerID, newBitmap, newNodes);
+ };
+
+
+
+
+ function HashArrayMapNode(ownerID, count, nodes) {
+ this.ownerID = ownerID;
+ this.count = count;
+ this.nodes = nodes;
+ }
+
+ HashArrayMapNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+ var idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
+ var node = this.nodes[idx];
+ return node ? node.get(shift + SHIFT, keyHash, key, notSetValue) : notSetValue;
+ };
+
+ HashArrayMapNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+ var idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
+ var removed = value === NOT_SET;
+ var nodes = this.nodes;
+ var node = nodes[idx];
+
+ if (removed && !node) {
+ return this;
+ }
+
+ var newNode = updateNode(node, ownerID, shift + SHIFT, keyHash, key, value, didChangeSize, didAlter);
+ if (newNode === node) {
+ return this;
+ }
+
+ var newCount = this.count;
+ if (!node) {
+ newCount++;
+ } else if (!newNode) {
+ newCount--;
+ if (newCount < MIN_HASH_ARRAY_MAP_SIZE) {
+ return packNodes(ownerID, nodes, newCount, idx);
+ }
+ }
+
+ var isEditable = ownerID && ownerID === this.ownerID;
+ var newNodes = setIn(nodes, idx, newNode, isEditable);
+
+ if (isEditable) {
+ this.count = newCount;
+ this.nodes = newNodes;
+ return this;
+ }
+
+ return new HashArrayMapNode(ownerID, newCount, newNodes);
+ };
+
+
+
+
+ function HashCollisionNode(ownerID, keyHash, entries) {
+ this.ownerID = ownerID;
+ this.keyHash = keyHash;
+ this.entries = entries;
+ }
+
+ HashCollisionNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ var entries = this.entries;
+ for (var ii = 0, len = entries.length; ii < len; ii++) {
+ if (is(key, entries[ii][0])) {
+ return entries[ii][1];
+ }
+ }
+ return notSetValue;
+ };
+
+ HashCollisionNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ if (keyHash === undefined) {
+ keyHash = hash(key);
+ }
+
+ var removed = value === NOT_SET;
+
+ if (keyHash !== this.keyHash) {
+ if (removed) {
+ return this;
+ }
+ SetRef(didAlter);
+ SetRef(didChangeSize);
+ return mergeIntoNode(this, ownerID, shift, keyHash, [key, value]);
+ }
+
+ var entries = this.entries;
+ var idx = 0;
+ for (var len = entries.length; idx < len; idx++) {
+ if (is(key, entries[idx][0])) {
+ break;
+ }
+ }
+ var exists = idx < len;
+
+ if (exists ? entries[idx][1] === value : removed) {
+ return this;
+ }
+
+ SetRef(didAlter);
+ (removed || !exists) && SetRef(didChangeSize);
+
+ if (removed && len === 2) {
+ return new ValueNode(ownerID, this.keyHash, entries[idx ^ 1]);
+ }
+
+ var isEditable = ownerID && ownerID === this.ownerID;
+ var newEntries = isEditable ? entries : arrCopy(entries);
+
+ if (exists) {
+ if (removed) {
+ idx === len - 1 ? newEntries.pop() : (newEntries[idx] = newEntries.pop());
+ } else {
+ newEntries[idx] = [key, value];
+ }
+ } else {
+ newEntries.push([key, value]);
+ }
+
+ if (isEditable) {
+ this.entries = newEntries;
+ return this;
+ }
+
+ return new HashCollisionNode(ownerID, this.keyHash, newEntries);
+ };
+
+
+
+
+ function ValueNode(ownerID, keyHash, entry) {
+ this.ownerID = ownerID;
+ this.keyHash = keyHash;
+ this.entry = entry;
+ }
+
+ ValueNode.prototype.get = function(shift, keyHash, key, notSetValue) {
+ return is(key, this.entry[0]) ? this.entry[1] : notSetValue;
+ };
+
+ ValueNode.prototype.update = function(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ var removed = value === NOT_SET;
+ var keyMatch = is(key, this.entry[0]);
+ if (keyMatch ? value === this.entry[1] : removed) {
+ return this;
+ }
+
+ SetRef(didAlter);
+
+ if (removed) {
+ SetRef(didChangeSize);
+ return; // undefined
+ }
+
+ if (keyMatch) {
+ if (ownerID && ownerID === this.ownerID) {
+ this.entry[1] = value;
+ return this;
+ }
+ return new ValueNode(ownerID, this.keyHash, [key, value]);
+ }
+
+ SetRef(didChangeSize);
+ return mergeIntoNode(this, ownerID, shift, hash(key), [key, value]);
+ };
+
+
+
+ // #pragma Iterators
+
+ ArrayMapNode.prototype.iterate =
+ HashCollisionNode.prototype.iterate = function (fn, reverse) {
+ var entries = this.entries;
+ for (var ii = 0, maxIndex = entries.length - 1; ii <= maxIndex; ii++) {
+ if (fn(entries[reverse ? maxIndex - ii : ii]) === false) {
+ return false;
+ }
+ }
+ }
+
+ BitmapIndexedNode.prototype.iterate =
+ HashArrayMapNode.prototype.iterate = function (fn, reverse) {
+ var nodes = this.nodes;
+ for (var ii = 0, maxIndex = nodes.length - 1; ii <= maxIndex; ii++) {
+ var node = nodes[reverse ? maxIndex - ii : ii];
+ if (node && node.iterate(fn, reverse) === false) {
+ return false;
+ }
+ }
+ }
+
+ ValueNode.prototype.iterate = function (fn, reverse) {
+ return fn(this.entry);
+ }
+
+ createClass(MapIterator, Iterator);
+
+ function MapIterator(map, type, reverse) {
+ this._type = type;
+ this._reverse = reverse;
+ this._stack = map._root && mapIteratorFrame(map._root);
+ }
+
+ MapIterator.prototype.next = function() {
+ var type = this._type;
+ var stack = this._stack;
+ while (stack) {
+ var node = stack.node;
+ var index = stack.index++;
+ var maxIndex;
+ if (node.entry) {
+ if (index === 0) {
+ return mapIteratorValue(type, node.entry);
+ }
+ } else if (node.entries) {
+ maxIndex = node.entries.length - 1;
+ if (index <= maxIndex) {
+ return mapIteratorValue(type, node.entries[this._reverse ? maxIndex - index : index]);
+ }
+ } else {
+ maxIndex = node.nodes.length - 1;
+ if (index <= maxIndex) {
+ var subNode = node.nodes[this._reverse ? maxIndex - index : index];
+ if (subNode) {
+ if (subNode.entry) {
+ return mapIteratorValue(type, subNode.entry);
+ }
+ stack = this._stack = mapIteratorFrame(subNode, stack);
+ }
+ continue;
+ }
+ }
+ stack = this._stack = this._stack.__prev;
+ }
+ return iteratorDone();
+ };
+
+
+ function mapIteratorValue(type, entry) {
+ return iteratorValue(type, entry[0], entry[1]);
+ }
+
+ function mapIteratorFrame(node, prev) {
+ return {
+ node: node,
+ index: 0,
+ __prev: prev
+ };
+ }
+
+ function makeMap(size, root, ownerID, hash) {
+ var map = Object.create(MapPrototype);
+ map.size = size;
+ map._root = root;
+ map.__ownerID = ownerID;
+ map.__hash = hash;
+ map.__altered = false;
+ return map;
+ }
+
+ var EMPTY_MAP;
+ function emptyMap() {
+ return EMPTY_MAP || (EMPTY_MAP = makeMap(0));
+ }
+
+ function updateMap(map, k, v) {
+ var newRoot;
+ var newSize;
+ if (!map._root) {
+ if (v === NOT_SET) {
+ return map;
+ }
+ newSize = 1;
+ newRoot = new ArrayMapNode(map.__ownerID, [[k, v]]);
+ } else {
+ var didChangeSize = MakeRef(CHANGE_LENGTH);
+ var didAlter = MakeRef(DID_ALTER);
+ newRoot = updateNode(map._root, map.__ownerID, 0, undefined, k, v, didChangeSize, didAlter);
+ if (!didAlter.value) {
+ return map;
+ }
+ newSize = map.size + (didChangeSize.value ? v === NOT_SET ? -1 : 1 : 0);
+ }
+ if (map.__ownerID) {
+ map.size = newSize;
+ map._root = newRoot;
+ map.__hash = undefined;
+ map.__altered = true;
+ return map;
+ }
+ return newRoot ? makeMap(newSize, newRoot) : emptyMap();
+ }
+
+ function updateNode(node, ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
+ if (!node) {
+ if (value === NOT_SET) {
+ return node;
+ }
+ SetRef(didAlter);
+ SetRef(didChangeSize);
+ return new ValueNode(ownerID, keyHash, [key, value]);
+ }
+ return node.update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter);
+ }
+
+ function isLeafNode(node) {
+ return node.constructor === ValueNode || node.constructor === HashCollisionNode;
+ }
+
+ function mergeIntoNode(node, ownerID, shift, keyHash, entry) {
+ if (node.keyHash === keyHash) {
+ return new HashCollisionNode(ownerID, keyHash, [node.entry, entry]);
+ }
+
+ var idx1 = (shift === 0 ? node.keyHash : node.keyHash >>> shift) & MASK;
+ var idx2 = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
+
+ var newNode;
+ var nodes = idx1 === idx2 ?
+ [mergeIntoNode(node, ownerID, shift + SHIFT, keyHash, entry)] :
+ ((newNode = new ValueNode(ownerID, keyHash, entry)), idx1 < idx2 ? [node, newNode] : [newNode, node]);
+
+ return new BitmapIndexedNode(ownerID, (1 << idx1) | (1 << idx2), nodes);
+ }
+
+ function createNodes(ownerID, entries, key, value) {
+ if (!ownerID) {
+ ownerID = new OwnerID();
+ }
+ var node = new ValueNode(ownerID, hash(key), [key, value]);
+ for (var ii = 0; ii < entries.length; ii++) {
+ var entry = entries[ii];
+ node = node.update(ownerID, 0, undefined, entry[0], entry[1]);
+ }
+ return node;
+ }
+
+ function packNodes(ownerID, nodes, count, excluding) {
+ var bitmap = 0;
+ var packedII = 0;
+ var packedNodes = new Array(count);
+ for (var ii = 0, bit = 1, len = nodes.length; ii < len; ii++, bit <<= 1) {
+ var node = nodes[ii];
+ if (node !== undefined && ii !== excluding) {
+ bitmap |= bit;
+ packedNodes[packedII++] = node;
+ }
+ }
+ return new BitmapIndexedNode(ownerID, bitmap, packedNodes);
+ }
+
+ function expandNodes(ownerID, nodes, bitmap, including, node) {
+ var count = 0;
+ var expandedNodes = new Array(SIZE);
+ for (var ii = 0; bitmap !== 0; ii++, bitmap >>>= 1) {
+ expandedNodes[ii] = bitmap & 1 ? nodes[count++] : undefined;
+ }
+ expandedNodes[including] = node;
+ return new HashArrayMapNode(ownerID, count + 1, expandedNodes);
+ }
+
+ function mergeIntoMapWith(map, merger, iterables) {
+ var iters = [];
+ for (var ii = 0; ii < iterables.length; ii++) {
+ var value = iterables[ii];
+ var iter = KeyedIterable(value);
+ if (!isIterable(value)) {
+ iter = iter.map(function(v ) {return fromJS(v)});
+ }
+ iters.push(iter);
+ }
+ return mergeIntoCollectionWith(map, merger, iters);
+ }
+
+ function deepMerger(existing, value, key) {
+ return existing && existing.mergeDeep && isIterable(value) ?
+ existing.mergeDeep(value) :
+ is(existing, value) ? existing : value;
+ }
+
+ function deepMergerWith(merger) {
+ return function(existing, value, key) {
+ if (existing && existing.mergeDeepWith && isIterable(value)) {
+ return existing.mergeDeepWith(merger, value);
+ }
+ var nextValue = merger(existing, value, key);
+ return is(existing, nextValue) ? existing : nextValue;
+ };
+ }
+
+ function mergeIntoCollectionWith(collection, merger, iters) {
+ iters = iters.filter(function(x ) {return x.size !== 0});
+ if (iters.length === 0) {
+ return collection;
+ }
+ if (collection.size === 0 && !collection.__ownerID && iters.length === 1) {
+ return collection.constructor(iters[0]);
+ }
+ return collection.withMutations(function(collection ) {
+ var mergeIntoMap = merger ?
+ function(value, key) {
+ collection.update(key, NOT_SET, function(existing )
+ {return existing === NOT_SET ? value : merger(existing, value, key)}
+ );
+ } :
+ function(value, key) {
+ collection.set(key, value);
+ }
+ for (var ii = 0; ii < iters.length; ii++) {
+ iters[ii].forEach(mergeIntoMap);
+ }
+ });
+ }
+
+ function updateInDeepMap(existing, keyPathIter, notSetValue, updater) {
+ var isNotSet = existing === NOT_SET;
+ var step = keyPathIter.next();
+ if (step.done) {
+ var existingValue = isNotSet ? notSetValue : existing;
+ var newValue = updater(existingValue);
+ return newValue === existingValue ? existing : newValue;
+ }
+ invariant(
+ isNotSet || (existing && existing.set),
+ 'invalid keyPath'
+ );
+ var key = step.value;
+ var nextExisting = isNotSet ? NOT_SET : existing.get(key, NOT_SET);
+ var nextUpdated = updateInDeepMap(
+ nextExisting,
+ keyPathIter,
+ notSetValue,
+ updater
+ );
+ return nextUpdated === nextExisting ? existing :
+ nextUpdated === NOT_SET ? existing.remove(key) :
+ (isNotSet ? emptyMap() : existing).set(key, nextUpdated);
+ }
+
+ function popCount(x) {
+ x = x - ((x >> 1) & 0x55555555);
+ x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
+ x = (x + (x >> 4)) & 0x0f0f0f0f;
+ x = x + (x >> 8);
+ x = x + (x >> 16);
+ return x & 0x7f;
+ }
+
+ function setIn(array, idx, val, canEdit) {
+ var newArray = canEdit ? array : arrCopy(array);
+ newArray[idx] = val;
+ return newArray;
+ }
+
+ function spliceIn(array, idx, val, canEdit) {
+ var newLen = array.length + 1;
+ if (canEdit && idx + 1 === newLen) {
+ array[idx] = val;
+ return array;
+ }
+ var newArray = new Array(newLen);
+ var after = 0;
+ for (var ii = 0; ii < newLen; ii++) {
+ if (ii === idx) {
+ newArray[ii] = val;
+ after = -1;
+ } else {
+ newArray[ii] = array[ii + after];
+ }
+ }
+ return newArray;
+ }
+
+ function spliceOut(array, idx, canEdit) {
+ var newLen = array.length - 1;
+ if (canEdit && idx === newLen) {
+ array.pop();
+ return array;
+ }
+ var newArray = new Array(newLen);
+ var after = 0;
+ for (var ii = 0; ii < newLen; ii++) {
+ if (ii === idx) {
+ after = 1;
+ }
+ newArray[ii] = array[ii + after];
+ }
+ return newArray;
+ }
+
+ var MAX_ARRAY_MAP_SIZE = SIZE / 4;
+ var MAX_BITMAP_INDEXED_SIZE = SIZE / 2;
+ var MIN_HASH_ARRAY_MAP_SIZE = SIZE / 4;
+
+ createClass(List, IndexedCollection);
+
+ // @pragma Construction
+
+ function List(value) {
+ var empty = emptyList();
+ if (value === null || value === undefined) {
+ return empty;
+ }
+ if (isList(value)) {
+ return value;
+ }
+ var iter = IndexedIterable(value);
+ var size = iter.size;
+ if (size === 0) {
+ return empty;
+ }
+ assertNotInfinite(size);
+ if (size > 0 && size < SIZE) {
+ return makeList(0, size, SHIFT, null, new VNode(iter.toArray()));
+ }
+ return empty.withMutations(function(list ) {
+ list.setSize(size);
+ iter.forEach(function(v, i) {return list.set(i, v)});
+ });
+ }
+
+ List.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ List.prototype.toString = function() {
+ return this.__toString('List [', ']');
+ };
+
+ // @pragma Access
+
+ List.prototype.get = function(index, notSetValue) {
+ index = wrapIndex(this, index);
+ if (index >= 0 && index < this.size) {
+ index += this._origin;
+ var node = listNodeFor(this, index);
+ return node && node.array[index & MASK];
+ }
+ return notSetValue;
+ };
+
+ // @pragma Modification
+
+ List.prototype.set = function(index, value) {
+ return updateList(this, index, value);
+ };
+
+ List.prototype.remove = function(index) {
+ return !this.has(index) ? this :
+ index === 0 ? this.shift() :
+ index === this.size - 1 ? this.pop() :
+ this.splice(index, 1);
+ };
+
+ List.prototype.insert = function(index, value) {
+ return this.splice(index, 0, value);
+ };
+
+ List.prototype.clear = function() {
+ if (this.size === 0) {
+ return this;
+ }
+ if (this.__ownerID) {
+ this.size = this._origin = this._capacity = 0;
+ this._level = SHIFT;
+ this._root = this._tail = null;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return emptyList();
+ };
+
+ List.prototype.push = function(/*...values*/) {
+ var values = arguments;
+ var oldSize = this.size;
+ return this.withMutations(function(list ) {
+ setListBounds(list, 0, oldSize + values.length);
+ for (var ii = 0; ii < values.length; ii++) {
+ list.set(oldSize + ii, values[ii]);
+ }
+ });
+ };
+
+ List.prototype.pop = function() {
+ return setListBounds(this, 0, -1);
+ };
+
+ List.prototype.unshift = function(/*...values*/) {
+ var values = arguments;
+ return this.withMutations(function(list ) {
+ setListBounds(list, -values.length);
+ for (var ii = 0; ii < values.length; ii++) {
+ list.set(ii, values[ii]);
+ }
+ });
+ };
+
+ List.prototype.shift = function() {
+ return setListBounds(this, 1);
+ };
+
+ // @pragma Composition
+
+ List.prototype.merge = function(/*...iters*/) {
+ return mergeIntoListWith(this, undefined, arguments);
+ };
+
+ List.prototype.mergeWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return mergeIntoListWith(this, merger, iters);
+ };
+
+ List.prototype.mergeDeep = function(/*...iters*/) {
+ return mergeIntoListWith(this, deepMerger, arguments);
+ };
+
+ List.prototype.mergeDeepWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return mergeIntoListWith(this, deepMergerWith(merger), iters);
+ };
+
+ List.prototype.setSize = function(size) {
+ return setListBounds(this, 0, size);
+ };
+
+ // @pragma Iteration
+
+ List.prototype.slice = function(begin, end) {
+ var size = this.size;
+ if (wholeSlice(begin, end, size)) {
+ return this;
+ }
+ return setListBounds(
+ this,
+ resolveBegin(begin, size),
+ resolveEnd(end, size)
+ );
+ };
+
+ List.prototype.__iterator = function(type, reverse) {
+ var index = 0;
+ var values = iterateList(this, reverse);
+ return new Iterator(function() {
+ var value = values();
+ return value === DONE ?
+ iteratorDone() :
+ iteratorValue(type, index++, value);
+ });
+ };
+
+ List.prototype.__iterate = function(fn, reverse) {
+ var index = 0;
+ var values = iterateList(this, reverse);
+ var value;
+ while ((value = values()) !== DONE) {
+ if (fn(value, index++, this) === false) {
+ break;
+ }
+ }
+ return index;
+ };
+
+ List.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ return this;
+ }
+ return makeList(this._origin, this._capacity, this._level, this._root, this._tail, ownerID, this.__hash);
+ };
+
+
+ function isList(maybeList) {
+ return !!(maybeList && maybeList[IS_LIST_SENTINEL]);
+ }
+
+ List.isList = isList;
+
+ var IS_LIST_SENTINEL = '@@__IMMUTABLE_LIST__@@';
+
+ var ListPrototype = List.prototype;
+ ListPrototype[IS_LIST_SENTINEL] = true;
+ ListPrototype[DELETE] = ListPrototype.remove;
+ ListPrototype.setIn = MapPrototype.setIn;
+ ListPrototype.deleteIn =
+ ListPrototype.removeIn = MapPrototype.removeIn;
+ ListPrototype.update = MapPrototype.update;
+ ListPrototype.updateIn = MapPrototype.updateIn;
+ ListPrototype.mergeIn = MapPrototype.mergeIn;
+ ListPrototype.mergeDeepIn = MapPrototype.mergeDeepIn;
+ ListPrototype.withMutations = MapPrototype.withMutations;
+ ListPrototype.asMutable = MapPrototype.asMutable;
+ ListPrototype.asImmutable = MapPrototype.asImmutable;
+ ListPrototype.wasAltered = MapPrototype.wasAltered;
+
+
+
+ function VNode(array, ownerID) {
+ this.array = array;
+ this.ownerID = ownerID;
+ }
+
+ // TODO: seems like these methods are very similar
+
+ VNode.prototype.removeBefore = function(ownerID, level, index) {
+ if (index === level ? 1 << level : 0 || this.array.length === 0) {
+ return this;
+ }
+ var originIndex = (index >>> level) & MASK;
+ if (originIndex >= this.array.length) {
+ return new VNode([], ownerID);
+ }
+ var removingFirst = originIndex === 0;
+ var newChild;
+ if (level > 0) {
+ var oldChild = this.array[originIndex];
+ newChild = oldChild && oldChild.removeBefore(ownerID, level - SHIFT, index);
+ if (newChild === oldChild && removingFirst) {
+ return this;
+ }
+ }
+ if (removingFirst && !newChild) {
+ return this;
+ }
+ var editable = editableVNode(this, ownerID);
+ if (!removingFirst) {
+ for (var ii = 0; ii < originIndex; ii++) {
+ editable.array[ii] = undefined;
+ }
+ }
+ if (newChild) {
+ editable.array[originIndex] = newChild;
+ }
+ return editable;
+ };
+
+ VNode.prototype.removeAfter = function(ownerID, level, index) {
+ if (index === (level ? 1 << level : 0) || this.array.length === 0) {
+ return this;
+ }
+ var sizeIndex = ((index - 1) >>> level) & MASK;
+ if (sizeIndex >= this.array.length) {
+ return this;
+ }
+
+ var newChild;
+ if (level > 0) {
+ var oldChild = this.array[sizeIndex];
+ newChild = oldChild && oldChild.removeAfter(ownerID, level - SHIFT, index);
+ if (newChild === oldChild && sizeIndex === this.array.length - 1) {
+ return this;
+ }
+ }
+
+ var editable = editableVNode(this, ownerID);
+ editable.array.splice(sizeIndex + 1);
+ if (newChild) {
+ editable.array[sizeIndex] = newChild;
+ }
+ return editable;
+ };
+
+
+
+ var DONE = {};
+
+ function iterateList(list, reverse) {
+ var left = list._origin;
+ var right = list._capacity;
+ var tailPos = getTailOffset(right);
+ var tail = list._tail;
+
+ return iterateNodeOrLeaf(list._root, list._level, 0);
+
+ function iterateNodeOrLeaf(node, level, offset) {
+ return level === 0 ?
+ iterateLeaf(node, offset) :
+ iterateNode(node, level, offset);
+ }
+
+ function iterateLeaf(node, offset) {
+ var array = offset === tailPos ? tail && tail.array : node && node.array;
+ var from = offset > left ? 0 : left - offset;
+ var to = right - offset;
+ if (to > SIZE) {
+ to = SIZE;
+ }
+ return function() {
+ if (from === to) {
+ return DONE;
+ }
+ var idx = reverse ? --to : from++;
+ return array && array[idx];
+ };
+ }
+
+ function iterateNode(node, level, offset) {
+ var values;
+ var array = node && node.array;
+ var from = offset > left ? 0 : (left - offset) >> level;
+ var to = ((right - offset) >> level) + 1;
+ if (to > SIZE) {
+ to = SIZE;
+ }
+ return function() {
+ do {
+ if (values) {
+ var value = values();
+ if (value !== DONE) {
+ return value;
+ }
+ values = null;
+ }
+ if (from === to) {
+ return DONE;
+ }
+ var idx = reverse ? --to : from++;
+ values = iterateNodeOrLeaf(
+ array && array[idx], level - SHIFT, offset + (idx << level)
+ );
+ } while (true);
+ };
+ }
+ }
+
+ function makeList(origin, capacity, level, root, tail, ownerID, hash) {
+ var list = Object.create(ListPrototype);
+ list.size = capacity - origin;
+ list._origin = origin;
+ list._capacity = capacity;
+ list._level = level;
+ list._root = root;
+ list._tail = tail;
+ list.__ownerID = ownerID;
+ list.__hash = hash;
+ list.__altered = false;
+ return list;
+ }
+
+ var EMPTY_LIST;
+ function emptyList() {
+ return EMPTY_LIST || (EMPTY_LIST = makeList(0, 0, SHIFT));
+ }
+
+ function updateList(list, index, value) {
+ index = wrapIndex(list, index);
+
+ if (index !== index) {
+ return list;
+ }
+
+ if (index >= list.size || index < 0) {
+ return list.withMutations(function(list ) {
+ index < 0 ?
+ setListBounds(list, index).set(0, value) :
+ setListBounds(list, 0, index + 1).set(index, value)
+ });
+ }
+
+ index += list._origin;
+
+ var newTail = list._tail;
+ var newRoot = list._root;
+ var didAlter = MakeRef(DID_ALTER);
+ if (index >= getTailOffset(list._capacity)) {
+ newTail = updateVNode(newTail, list.__ownerID, 0, index, value, didAlter);
+ } else {
+ newRoot = updateVNode(newRoot, list.__ownerID, list._level, index, value, didAlter);
+ }
+
+ if (!didAlter.value) {
+ return list;
+ }
+
+ if (list.__ownerID) {
+ list._root = newRoot;
+ list._tail = newTail;
+ list.__hash = undefined;
+ list.__altered = true;
+ return list;
+ }
+ return makeList(list._origin, list._capacity, list._level, newRoot, newTail);
+ }
+
+ function updateVNode(node, ownerID, level, index, value, didAlter) {
+ var idx = (index >>> level) & MASK;
+ var nodeHas = node && idx < node.array.length;
+ if (!nodeHas && value === undefined) {
+ return node;
+ }
+
+ var newNode;
+
+ if (level > 0) {
+ var lowerNode = node && node.array[idx];
+ var newLowerNode = updateVNode(lowerNode, ownerID, level - SHIFT, index, value, didAlter);
+ if (newLowerNode === lowerNode) {
+ return node;
+ }
+ newNode = editableVNode(node, ownerID);
+ newNode.array[idx] = newLowerNode;
+ return newNode;
+ }
+
+ if (nodeHas && node.array[idx] === value) {
+ return node;
+ }
+
+ SetRef(didAlter);
+
+ newNode = editableVNode(node, ownerID);
+ if (value === undefined && idx === newNode.array.length - 1) {
+ newNode.array.pop();
+ } else {
+ newNode.array[idx] = value;
+ }
+ return newNode;
+ }
+
+ function editableVNode(node, ownerID) {
+ if (ownerID && node && ownerID === node.ownerID) {
+ return node;
+ }
+ return new VNode(node ? node.array.slice() : [], ownerID);
+ }
+
+ function listNodeFor(list, rawIndex) {
+ if (rawIndex >= getTailOffset(list._capacity)) {
+ return list._tail;
+ }
+ if (rawIndex < 1 << (list._level + SHIFT)) {
+ var node = list._root;
+ var level = list._level;
+ while (node && level > 0) {
+ node = node.array[(rawIndex >>> level) & MASK];
+ level -= SHIFT;
+ }
+ return node;
+ }
+ }
+
+ function setListBounds(list, begin, end) {
+ // Sanitize begin & end using this shorthand for ToInt32(argument)
+ // http://www.ecma-international.org/ecma-262/6.0/#sec-toint32
+ if (begin !== undefined) {
+ begin = begin | 0;
+ }
+ if (end !== undefined) {
+ end = end | 0;
+ }
+ var owner = list.__ownerID || new OwnerID();
+ var oldOrigin = list._origin;
+ var oldCapacity = list._capacity;
+ var newOrigin = oldOrigin + begin;
+ var newCapacity = end === undefined ? oldCapacity : end < 0 ? oldCapacity + end : oldOrigin + end;
+ if (newOrigin === oldOrigin && newCapacity === oldCapacity) {
+ return list;
+ }
+
+ // If it's going to end after it starts, it's empty.
+ if (newOrigin >= newCapacity) {
+ return list.clear();
+ }
+
+ var newLevel = list._level;
+ var newRoot = list._root;
+
+ // New origin might need creating a higher root.
+ var offsetShift = 0;
+ while (newOrigin + offsetShift < 0) {
+ newRoot = new VNode(newRoot && newRoot.array.length ? [undefined, newRoot] : [], owner);
+ newLevel += SHIFT;
+ offsetShift += 1 << newLevel;
+ }
+ if (offsetShift) {
+ newOrigin += offsetShift;
+ oldOrigin += offsetShift;
+ newCapacity += offsetShift;
+ oldCapacity += offsetShift;
+ }
+
+ var oldTailOffset = getTailOffset(oldCapacity);
+ var newTailOffset = getTailOffset(newCapacity);
+
+ // New size might need creating a higher root.
+ while (newTailOffset >= 1 << (newLevel + SHIFT)) {
+ newRoot = new VNode(newRoot && newRoot.array.length ? [newRoot] : [], owner);
+ newLevel += SHIFT;
+ }
+
+ // Locate or create the new tail.
+ var oldTail = list._tail;
+ var newTail = newTailOffset < oldTailOffset ?
+ listNodeFor(list, newCapacity - 1) :
+ newTailOffset > oldTailOffset ? new VNode([], owner) : oldTail;
+
+ // Merge Tail into tree.
+ if (oldTail && newTailOffset > oldTailOffset && newOrigin < oldCapacity && oldTail.array.length) {
+ newRoot = editableVNode(newRoot, owner);
+ var node = newRoot;
+ for (var level = newLevel; level > SHIFT; level -= SHIFT) {
+ var idx = (oldTailOffset >>> level) & MASK;
+ node = node.array[idx] = editableVNode(node.array[idx], owner);
+ }
+ node.array[(oldTailOffset >>> SHIFT) & MASK] = oldTail;
+ }
+
+ // If the size has been reduced, there's a chance the tail needs to be trimmed.
+ if (newCapacity < oldCapacity) {
+ newTail = newTail && newTail.removeAfter(owner, 0, newCapacity);
+ }
+
+ // If the new origin is within the tail, then we do not need a root.
+ if (newOrigin >= newTailOffset) {
+ newOrigin -= newTailOffset;
+ newCapacity -= newTailOffset;
+ newLevel = SHIFT;
+ newRoot = null;
+ newTail = newTail && newTail.removeBefore(owner, 0, newOrigin);
+
+ // Otherwise, if the root has been trimmed, garbage collect.
+ } else if (newOrigin > oldOrigin || newTailOffset < oldTailOffset) {
+ offsetShift = 0;
+
+ // Identify the new top root node of the subtree of the old root.
+ while (newRoot) {
+ var beginIndex = (newOrigin >>> newLevel) & MASK;
+ if (beginIndex !== (newTailOffset >>> newLevel) & MASK) {
+ break;
+ }
+ if (beginIndex) {
+ offsetShift += (1 << newLevel) * beginIndex;
+ }
+ newLevel -= SHIFT;
+ newRoot = newRoot.array[beginIndex];
+ }
+
+ // Trim the new sides of the new root.
+ if (newRoot && newOrigin > oldOrigin) {
+ newRoot = newRoot.removeBefore(owner, newLevel, newOrigin - offsetShift);
+ }
+ if (newRoot && newTailOffset < oldTailOffset) {
+ newRoot = newRoot.removeAfter(owner, newLevel, newTailOffset - offsetShift);
+ }
+ if (offsetShift) {
+ newOrigin -= offsetShift;
+ newCapacity -= offsetShift;
+ }
+ }
+
+ if (list.__ownerID) {
+ list.size = newCapacity - newOrigin;
+ list._origin = newOrigin;
+ list._capacity = newCapacity;
+ list._level = newLevel;
+ list._root = newRoot;
+ list._tail = newTail;
+ list.__hash = undefined;
+ list.__altered = true;
+ return list;
+ }
+ return makeList(newOrigin, newCapacity, newLevel, newRoot, newTail);
+ }
+
+ function mergeIntoListWith(list, merger, iterables) {
+ var iters = [];
+ var maxSize = 0;
+ for (var ii = 0; ii < iterables.length; ii++) {
+ var value = iterables[ii];
+ var iter = IndexedIterable(value);
+ if (iter.size > maxSize) {
+ maxSize = iter.size;
+ }
+ if (!isIterable(value)) {
+ iter = iter.map(function(v ) {return fromJS(v)});
+ }
+ iters.push(iter);
+ }
+ if (maxSize > list.size) {
+ list = list.setSize(maxSize);
+ }
+ return mergeIntoCollectionWith(list, merger, iters);
+ }
+
+ function getTailOffset(size) {
+ return size < SIZE ? 0 : (((size - 1) >>> SHIFT) << SHIFT);
+ }
+
+ createClass(OrderedMap, Map);
+
+ // @pragma Construction
+
+ function OrderedMap(value) {
+ return value === null || value === undefined ? emptyOrderedMap() :
+ isOrderedMap(value) ? value :
+ emptyOrderedMap().withMutations(function(map ) {
+ var iter = KeyedIterable(value);
+ assertNotInfinite(iter.size);
+ iter.forEach(function(v, k) {return map.set(k, v)});
+ });
+ }
+
+ OrderedMap.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ OrderedMap.prototype.toString = function() {
+ return this.__toString('OrderedMap {', '}');
+ };
+
+ // @pragma Access
+
+ OrderedMap.prototype.get = function(k, notSetValue) {
+ var index = this._map.get(k);
+ return index !== undefined ? this._list.get(index)[1] : notSetValue;
+ };
+
+ // @pragma Modification
+
+ OrderedMap.prototype.clear = function() {
+ if (this.size === 0) {
+ return this;
+ }
+ if (this.__ownerID) {
+ this.size = 0;
+ this._map.clear();
+ this._list.clear();
+ return this;
+ }
+ return emptyOrderedMap();
+ };
+
+ OrderedMap.prototype.set = function(k, v) {
+ return updateOrderedMap(this, k, v);
+ };
+
+ OrderedMap.prototype.remove = function(k) {
+ return updateOrderedMap(this, k, NOT_SET);
+ };
+
+ OrderedMap.prototype.wasAltered = function() {
+ return this._map.wasAltered() || this._list.wasAltered();
+ };
+
+ OrderedMap.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return this._list.__iterate(
+ function(entry ) {return entry && fn(entry[1], entry[0], this$0)},
+ reverse
+ );
+ };
+
+ OrderedMap.prototype.__iterator = function(type, reverse) {
+ return this._list.fromEntrySeq().__iterator(type, reverse);
+ };
+
+ OrderedMap.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ var newMap = this._map.__ensureOwner(ownerID);
+ var newList = this._list.__ensureOwner(ownerID);
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this._map = newMap;
+ this._list = newList;
+ return this;
+ }
+ return makeOrderedMap(newMap, newList, ownerID, this.__hash);
+ };
+
+
+ function isOrderedMap(maybeOrderedMap) {
+ return isMap(maybeOrderedMap) && isOrdered(maybeOrderedMap);
+ }
+
+ OrderedMap.isOrderedMap = isOrderedMap;
+
+ OrderedMap.prototype[IS_ORDERED_SENTINEL] = true;
+ OrderedMap.prototype[DELETE] = OrderedMap.prototype.remove;
+
+
+
+ function makeOrderedMap(map, list, ownerID, hash) {
+ var omap = Object.create(OrderedMap.prototype);
+ omap.size = map ? map.size : 0;
+ omap._map = map;
+ omap._list = list;
+ omap.__ownerID = ownerID;
+ omap.__hash = hash;
+ return omap;
+ }
+
+ var EMPTY_ORDERED_MAP;
+ function emptyOrderedMap() {
+ return EMPTY_ORDERED_MAP || (EMPTY_ORDERED_MAP = makeOrderedMap(emptyMap(), emptyList()));
+ }
+
+ function updateOrderedMap(omap, k, v) {
+ var map = omap._map;
+ var list = omap._list;
+ var i = map.get(k);
+ var has = i !== undefined;
+ var newMap;
+ var newList;
+ if (v === NOT_SET) { // removed
+ if (!has) {
+ return omap;
+ }
+ if (list.size >= SIZE && list.size >= map.size * 2) {
+ newList = list.filter(function(entry, idx) {return entry !== undefined && i !== idx});
+ newMap = newList.toKeyedSeq().map(function(entry ) {return entry[0]}).flip().toMap();
+ if (omap.__ownerID) {
+ newMap.__ownerID = newList.__ownerID = omap.__ownerID;
+ }
+ } else {
+ newMap = map.remove(k);
+ newList = i === list.size - 1 ? list.pop() : list.set(i, undefined);
+ }
+ } else {
+ if (has) {
+ if (v === list.get(i)[1]) {
+ return omap;
+ }
+ newMap = map;
+ newList = list.set(i, [k, v]);
+ } else {
+ newMap = map.set(k, list.size);
+ newList = list.set(list.size, [k, v]);
+ }
+ }
+ if (omap.__ownerID) {
+ omap.size = newMap.size;
+ omap._map = newMap;
+ omap._list = newList;
+ omap.__hash = undefined;
+ return omap;
+ }
+ return makeOrderedMap(newMap, newList);
+ }
+
+ createClass(ToKeyedSequence, KeyedSeq);
+ function ToKeyedSequence(indexed, useKeys) {
+ this._iter = indexed;
+ this._useKeys = useKeys;
+ this.size = indexed.size;
+ }
+
+ ToKeyedSequence.prototype.get = function(key, notSetValue) {
+ return this._iter.get(key, notSetValue);
+ };
+
+ ToKeyedSequence.prototype.has = function(key) {
+ return this._iter.has(key);
+ };
+
+ ToKeyedSequence.prototype.valueSeq = function() {
+ return this._iter.valueSeq();
+ };
+
+ ToKeyedSequence.prototype.reverse = function() {var this$0 = this;
+ var reversedSequence = reverseFactory(this, true);
+ if (!this._useKeys) {
+ reversedSequence.valueSeq = function() {return this$0._iter.toSeq().reverse()};
+ }
+ return reversedSequence;
+ };
+
+ ToKeyedSequence.prototype.map = function(mapper, context) {var this$0 = this;
+ var mappedSequence = mapFactory(this, mapper, context);
+ if (!this._useKeys) {
+ mappedSequence.valueSeq = function() {return this$0._iter.toSeq().map(mapper, context)};
+ }
+ return mappedSequence;
+ };
+
+ ToKeyedSequence.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ var ii;
+ return this._iter.__iterate(
+ this._useKeys ?
+ function(v, k) {return fn(v, k, this$0)} :
+ ((ii = reverse ? resolveSize(this) : 0),
+ function(v ) {return fn(v, reverse ? --ii : ii++, this$0)}),
+ reverse
+ );
+ };
+
+ ToKeyedSequence.prototype.__iterator = function(type, reverse) {
+ if (this._useKeys) {
+ return this._iter.__iterator(type, reverse);
+ }
+ var iterator = this._iter.__iterator(ITERATE_VALUES, reverse);
+ var ii = reverse ? resolveSize(this) : 0;
+ return new Iterator(function() {
+ var step = iterator.next();
+ return step.done ? step :
+ iteratorValue(type, reverse ? --ii : ii++, step.value, step);
+ });
+ };
+
+ ToKeyedSequence.prototype[IS_ORDERED_SENTINEL] = true;
+
+
+ createClass(ToIndexedSequence, IndexedSeq);
+ function ToIndexedSequence(iter) {
+ this._iter = iter;
+ this.size = iter.size;
+ }
+
+ ToIndexedSequence.prototype.includes = function(value) {
+ return this._iter.includes(value);
+ };
+
+ ToIndexedSequence.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ var iterations = 0;
+ return this._iter.__iterate(function(v ) {return fn(v, iterations++, this$0)}, reverse);
+ };
+
+ ToIndexedSequence.prototype.__iterator = function(type, reverse) {
+ var iterator = this._iter.__iterator(ITERATE_VALUES, reverse);
+ var iterations = 0;
+ return new Iterator(function() {
+ var step = iterator.next();
+ return step.done ? step :
+ iteratorValue(type, iterations++, step.value, step)
+ });
+ };
+
+
+
+ createClass(ToSetSequence, SetSeq);
+ function ToSetSequence(iter) {
+ this._iter = iter;
+ this.size = iter.size;
+ }
+
+ ToSetSequence.prototype.has = function(key) {
+ return this._iter.includes(key);
+ };
+
+ ToSetSequence.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return this._iter.__iterate(function(v ) {return fn(v, v, this$0)}, reverse);
+ };
+
+ ToSetSequence.prototype.__iterator = function(type, reverse) {
+ var iterator = this._iter.__iterator(ITERATE_VALUES, reverse);
+ return new Iterator(function() {
+ var step = iterator.next();
+ return step.done ? step :
+ iteratorValue(type, step.value, step.value, step);
+ });
+ };
+
+
+
+ createClass(FromEntriesSequence, KeyedSeq);
+ function FromEntriesSequence(entries) {
+ this._iter = entries;
+ this.size = entries.size;
+ }
+
+ FromEntriesSequence.prototype.entrySeq = function() {
+ return this._iter.toSeq();
+ };
+
+ FromEntriesSequence.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return this._iter.__iterate(function(entry ) {
+ // Check if entry exists first so array access doesn't throw for holes
+ // in the parent iteration.
+ if (entry) {
+ validateEntry(entry);
+ var indexedIterable = isIterable(entry);
+ return fn(
+ indexedIterable ? entry.get(1) : entry[1],
+ indexedIterable ? entry.get(0) : entry[0],
+ this$0
+ );
+ }
+ }, reverse);
+ };
+
+ FromEntriesSequence.prototype.__iterator = function(type, reverse) {
+ var iterator = this._iter.__iterator(ITERATE_VALUES, reverse);
+ return new Iterator(function() {
+ while (true) {
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ var entry = step.value;
+ // Check if entry exists first so array access doesn't throw for holes
+ // in the parent iteration.
+ if (entry) {
+ validateEntry(entry);
+ var indexedIterable = isIterable(entry);
+ return iteratorValue(
+ type,
+ indexedIterable ? entry.get(0) : entry[0],
+ indexedIterable ? entry.get(1) : entry[1],
+ step
+ );
+ }
+ }
+ });
+ };
+
+
+ ToIndexedSequence.prototype.cacheResult =
+ ToKeyedSequence.prototype.cacheResult =
+ ToSetSequence.prototype.cacheResult =
+ FromEntriesSequence.prototype.cacheResult =
+ cacheResultThrough;
+
+
+ function flipFactory(iterable) {
+ var flipSequence = makeSequence(iterable);
+ flipSequence._iter = iterable;
+ flipSequence.size = iterable.size;
+ flipSequence.flip = function() {return iterable};
+ flipSequence.reverse = function () {
+ var reversedSequence = iterable.reverse.apply(this); // super.reverse()
+ reversedSequence.flip = function() {return iterable.reverse()};
+ return reversedSequence;
+ };
+ flipSequence.has = function(key ) {return iterable.includes(key)};
+ flipSequence.includes = function(key ) {return iterable.has(key)};
+ flipSequence.cacheResult = cacheResultThrough;
+ flipSequence.__iterateUncached = function (fn, reverse) {var this$0 = this;
+ return iterable.__iterate(function(v, k) {return fn(k, v, this$0) !== false}, reverse);
+ }
+ flipSequence.__iteratorUncached = function(type, reverse) {
+ if (type === ITERATE_ENTRIES) {
+ var iterator = iterable.__iterator(type, reverse);
+ return new Iterator(function() {
+ var step = iterator.next();
+ if (!step.done) {
+ var k = step.value[0];
+ step.value[0] = step.value[1];
+ step.value[1] = k;
+ }
+ return step;
+ });
+ }
+ return iterable.__iterator(
+ type === ITERATE_VALUES ? ITERATE_KEYS : ITERATE_VALUES,
+ reverse
+ );
+ }
+ return flipSequence;
+ }
+
+
+ function mapFactory(iterable, mapper, context) {
+ var mappedSequence = makeSequence(iterable);
+ mappedSequence.size = iterable.size;
+ mappedSequence.has = function(key ) {return iterable.has(key)};
+ mappedSequence.get = function(key, notSetValue) {
+ var v = iterable.get(key, NOT_SET);
+ return v === NOT_SET ?
+ notSetValue :
+ mapper.call(context, v, key, iterable);
+ };
+ mappedSequence.__iterateUncached = function (fn, reverse) {var this$0 = this;
+ return iterable.__iterate(
+ function(v, k, c) {return fn(mapper.call(context, v, k, c), k, this$0) !== false},
+ reverse
+ );
+ }
+ mappedSequence.__iteratorUncached = function (type, reverse) {
+ var iterator = iterable.__iterator(ITERATE_ENTRIES, reverse);
+ return new Iterator(function() {
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ var entry = step.value;
+ var key = entry[0];
+ return iteratorValue(
+ type,
+ key,
+ mapper.call(context, entry[1], key, iterable),
+ step
+ );
+ });
+ }
+ return mappedSequence;
+ }
+
+
+ function reverseFactory(iterable, useKeys) {
+ var reversedSequence = makeSequence(iterable);
+ reversedSequence._iter = iterable;
+ reversedSequence.size = iterable.size;
+ reversedSequence.reverse = function() {return iterable};
+ if (iterable.flip) {
+ reversedSequence.flip = function () {
+ var flipSequence = flipFactory(iterable);
+ flipSequence.reverse = function() {return iterable.flip()};
+ return flipSequence;
+ };
+ }
+ reversedSequence.get = function(key, notSetValue)
+ {return iterable.get(useKeys ? key : -1 - key, notSetValue)};
+ reversedSequence.has = function(key )
+ {return iterable.has(useKeys ? key : -1 - key)};
+ reversedSequence.includes = function(value ) {return iterable.includes(value)};
+ reversedSequence.cacheResult = cacheResultThrough;
+ reversedSequence.__iterate = function (fn, reverse) {var this$0 = this;
+ return iterable.__iterate(function(v, k) {return fn(v, k, this$0)}, !reverse);
+ };
+ reversedSequence.__iterator =
+ function(type, reverse) {return iterable.__iterator(type, !reverse)};
+ return reversedSequence;
+ }
+
+
+ function filterFactory(iterable, predicate, context, useKeys) {
+ var filterSequence = makeSequence(iterable);
+ if (useKeys) {
+ filterSequence.has = function(key ) {
+ var v = iterable.get(key, NOT_SET);
+ return v !== NOT_SET && !!predicate.call(context, v, key, iterable);
+ };
+ filterSequence.get = function(key, notSetValue) {
+ var v = iterable.get(key, NOT_SET);
+ return v !== NOT_SET && predicate.call(context, v, key, iterable) ?
+ v : notSetValue;
+ };
+ }
+ filterSequence.__iterateUncached = function (fn, reverse) {var this$0 = this;
+ var iterations = 0;
+ iterable.__iterate(function(v, k, c) {
+ if (predicate.call(context, v, k, c)) {
+ iterations++;
+ return fn(v, useKeys ? k : iterations - 1, this$0);
+ }
+ }, reverse);
+ return iterations;
+ };
+ filterSequence.__iteratorUncached = function (type, reverse) {
+ var iterator = iterable.__iterator(ITERATE_ENTRIES, reverse);
+ var iterations = 0;
+ return new Iterator(function() {
+ while (true) {
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ var entry = step.value;
+ var key = entry[0];
+ var value = entry[1];
+ if (predicate.call(context, value, key, iterable)) {
+ return iteratorValue(type, useKeys ? key : iterations++, value, step);
+ }
+ }
+ });
+ }
+ return filterSequence;
+ }
+
+
+ function countByFactory(iterable, grouper, context) {
+ var groups = Map().asMutable();
+ iterable.__iterate(function(v, k) {
+ groups.update(
+ grouper.call(context, v, k, iterable),
+ 0,
+ function(a ) {return a + 1}
+ );
+ });
+ return groups.asImmutable();
+ }
+
+
+ function groupByFactory(iterable, grouper, context) {
+ var isKeyedIter = isKeyed(iterable);
+ var groups = (isOrdered(iterable) ? OrderedMap() : Map()).asMutable();
+ iterable.__iterate(function(v, k) {
+ groups.update(
+ grouper.call(context, v, k, iterable),
+ function(a ) {return (a = a || [], a.push(isKeyedIter ? [k, v] : v), a)}
+ );
+ });
+ var coerce = iterableClass(iterable);
+ return groups.map(function(arr ) {return reify(iterable, coerce(arr))});
+ }
+
+
+ function sliceFactory(iterable, begin, end, useKeys) {
+ var originalSize = iterable.size;
+
+ // Sanitize begin & end using this shorthand for ToInt32(argument)
+ // http://www.ecma-international.org/ecma-262/6.0/#sec-toint32
+ if (begin !== undefined) {
+ begin = begin | 0;
+ }
+ if (end !== undefined) {
+ end = end | 0;
+ }
+
+ if (wholeSlice(begin, end, originalSize)) {
+ return iterable;
+ }
+
+ var resolvedBegin = resolveBegin(begin, originalSize);
+ var resolvedEnd = resolveEnd(end, originalSize);
+
+ // begin or end will be NaN if they were provided as negative numbers and
+ // this iterable's size is unknown. In that case, cache first so there is
+ // a known size and these do not resolve to NaN.
+ if (resolvedBegin !== resolvedBegin || resolvedEnd !== resolvedEnd) {
+ return sliceFactory(iterable.toSeq().cacheResult(), begin, end, useKeys);
+ }
+
+ // Note: resolvedEnd is undefined when the original sequence's length is
+ // unknown and this slice did not supply an end and should contain all
+ // elements after resolvedBegin.
+ // In that case, resolvedSize will be NaN and sliceSize will remain undefined.
+ var resolvedSize = resolvedEnd - resolvedBegin;
+ var sliceSize;
+ if (resolvedSize === resolvedSize) {
+ sliceSize = resolvedSize < 0 ? 0 : resolvedSize;
+ }
+
+ var sliceSeq = makeSequence(iterable);
+
+ // If iterable.size is undefined, the size of the realized sliceSeq is
+ // unknown at this point unless the number of items to slice is 0
+ sliceSeq.size = sliceSize === 0 ? sliceSize : iterable.size && sliceSize || undefined;
+
+ if (!useKeys && isSeq(iterable) && sliceSize >= 0) {
+ sliceSeq.get = function (index, notSetValue) {
+ index = wrapIndex(this, index);
+ return index >= 0 && index < sliceSize ?
+ iterable.get(index + resolvedBegin, notSetValue) :
+ notSetValue;
+ }
+ }
+
+ sliceSeq.__iterateUncached = function(fn, reverse) {var this$0 = this;
+ if (sliceSize === 0) {
+ return 0;
+ }
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var skipped = 0;
+ var isSkipping = true;
+ var iterations = 0;
+ iterable.__iterate(function(v, k) {
+ if (!(isSkipping && (isSkipping = skipped++ < resolvedBegin))) {
+ iterations++;
+ return fn(v, useKeys ? k : iterations - 1, this$0) !== false &&
+ iterations !== sliceSize;
+ }
+ });
+ return iterations;
+ };
+
+ sliceSeq.__iteratorUncached = function(type, reverse) {
+ if (sliceSize !== 0 && reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ // Don't bother instantiating parent iterator if taking 0.
+ var iterator = sliceSize !== 0 && iterable.__iterator(type, reverse);
+ var skipped = 0;
+ var iterations = 0;
+ return new Iterator(function() {
+ while (skipped++ < resolvedBegin) {
+ iterator.next();
+ }
+ if (++iterations > sliceSize) {
+ return iteratorDone();
+ }
+ var step = iterator.next();
+ if (useKeys || type === ITERATE_VALUES) {
+ return step;
+ } else if (type === ITERATE_KEYS) {
+ return iteratorValue(type, iterations - 1, undefined, step);
+ } else {
+ return iteratorValue(type, iterations - 1, step.value[1], step);
+ }
+ });
+ }
+
+ return sliceSeq;
+ }
+
+
+ function takeWhileFactory(iterable, predicate, context) {
+ var takeSequence = makeSequence(iterable);
+ takeSequence.__iterateUncached = function(fn, reverse) {var this$0 = this;
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var iterations = 0;
+ iterable.__iterate(function(v, k, c)
+ {return predicate.call(context, v, k, c) && ++iterations && fn(v, k, this$0)}
+ );
+ return iterations;
+ };
+ takeSequence.__iteratorUncached = function(type, reverse) {var this$0 = this;
+ if (reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ var iterator = iterable.__iterator(ITERATE_ENTRIES, reverse);
+ var iterating = true;
+ return new Iterator(function() {
+ if (!iterating) {
+ return iteratorDone();
+ }
+ var step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ var entry = step.value;
+ var k = entry[0];
+ var v = entry[1];
+ if (!predicate.call(context, v, k, this$0)) {
+ iterating = false;
+ return iteratorDone();
+ }
+ return type === ITERATE_ENTRIES ? step :
+ iteratorValue(type, k, v, step);
+ });
+ };
+ return takeSequence;
+ }
+
+
+ function skipWhileFactory(iterable, predicate, context, useKeys) {
+ var skipSequence = makeSequence(iterable);
+ skipSequence.__iterateUncached = function (fn, reverse) {var this$0 = this;
+ if (reverse) {
+ return this.cacheResult().__iterate(fn, reverse);
+ }
+ var isSkipping = true;
+ var iterations = 0;
+ iterable.__iterate(function(v, k, c) {
+ if (!(isSkipping && (isSkipping = predicate.call(context, v, k, c)))) {
+ iterations++;
+ return fn(v, useKeys ? k : iterations - 1, this$0);
+ }
+ });
+ return iterations;
+ };
+ skipSequence.__iteratorUncached = function(type, reverse) {var this$0 = this;
+ if (reverse) {
+ return this.cacheResult().__iterator(type, reverse);
+ }
+ var iterator = iterable.__iterator(ITERATE_ENTRIES, reverse);
+ var skipping = true;
+ var iterations = 0;
+ return new Iterator(function() {
+ var step, k, v;
+ do {
+ step = iterator.next();
+ if (step.done) {
+ if (useKeys || type === ITERATE_VALUES) {
+ return step;
+ } else if (type === ITERATE_KEYS) {
+ return iteratorValue(type, iterations++, undefined, step);
+ } else {
+ return iteratorValue(type, iterations++, step.value[1], step);
+ }
+ }
+ var entry = step.value;
+ k = entry[0];
+ v = entry[1];
+ skipping && (skipping = predicate.call(context, v, k, this$0));
+ } while (skipping);
+ return type === ITERATE_ENTRIES ? step :
+ iteratorValue(type, k, v, step);
+ });
+ };
+ return skipSequence;
+ }
+
+
+ function concatFactory(iterable, values) {
+ var isKeyedIterable = isKeyed(iterable);
+ var iters = [iterable].concat(values).map(function(v ) {
+ if (!isIterable(v)) {
+ v = isKeyedIterable ?
+ keyedSeqFromValue(v) :
+ indexedSeqFromValue(Array.isArray(v) ? v : [v]);
+ } else if (isKeyedIterable) {
+ v = KeyedIterable(v);
+ }
+ return v;
+ }).filter(function(v ) {return v.size !== 0});
+
+ if (iters.length === 0) {
+ return iterable;
+ }
+
+ if (iters.length === 1) {
+ var singleton = iters[0];
+ if (singleton === iterable ||
+ isKeyedIterable && isKeyed(singleton) ||
+ isIndexed(iterable) && isIndexed(singleton)) {
+ return singleton;
+ }
+ }
+
+ var concatSeq = new ArraySeq(iters);
+ if (isKeyedIterable) {
+ concatSeq = concatSeq.toKeyedSeq();
+ } else if (!isIndexed(iterable)) {
+ concatSeq = concatSeq.toSetSeq();
+ }
+ concatSeq = concatSeq.flatten(true);
+ concatSeq.size = iters.reduce(
+ function(sum, seq) {
+ if (sum !== undefined) {
+ var size = seq.size;
+ if (size !== undefined) {
+ return sum + size;
+ }
+ }
+ },
+ 0
+ );
+ return concatSeq;
+ }
+
+
+ function flattenFactory(iterable, depth, useKeys) {
+ var flatSequence = makeSequence(iterable);
+ flatSequence.__iterateUncached = function(fn, reverse) {
+ var iterations = 0;
+ var stopped = false;
+ function flatDeep(iter, currentDepth) {var this$0 = this;
+ iter.__iterate(function(v, k) {
+ if ((!depth || currentDepth < depth) && isIterable(v)) {
+ flatDeep(v, currentDepth + 1);
+ } else if (fn(v, useKeys ? k : iterations++, this$0) === false) {
+ stopped = true;
+ }
+ return !stopped;
+ }, reverse);
+ }
+ flatDeep(iterable, 0);
+ return iterations;
+ }
+ flatSequence.__iteratorUncached = function(type, reverse) {
+ var iterator = iterable.__iterator(type, reverse);
+ var stack = [];
+ var iterations = 0;
+ return new Iterator(function() {
+ while (iterator) {
+ var step = iterator.next();
+ if (step.done !== false) {
+ iterator = stack.pop();
+ continue;
+ }
+ var v = step.value;
+ if (type === ITERATE_ENTRIES) {
+ v = v[1];
+ }
+ if ((!depth || stack.length < depth) && isIterable(v)) {
+ stack.push(iterator);
+ iterator = v.__iterator(type, reverse);
+ } else {
+ return useKeys ? step : iteratorValue(type, iterations++, v, step);
+ }
+ }
+ return iteratorDone();
+ });
+ }
+ return flatSequence;
+ }
+
+
+ function flatMapFactory(iterable, mapper, context) {
+ var coerce = iterableClass(iterable);
+ return iterable.toSeq().map(
+ function(v, k) {return coerce(mapper.call(context, v, k, iterable))}
+ ).flatten(true);
+ }
+
+
+ function interposeFactory(iterable, separator) {
+ var interposedSequence = makeSequence(iterable);
+ interposedSequence.size = iterable.size && iterable.size * 2 -1;
+ interposedSequence.__iterateUncached = function(fn, reverse) {var this$0 = this;
+ var iterations = 0;
+ iterable.__iterate(function(v, k)
+ {return (!iterations || fn(separator, iterations++, this$0) !== false) &&
+ fn(v, iterations++, this$0) !== false},
+ reverse
+ );
+ return iterations;
+ };
+ interposedSequence.__iteratorUncached = function(type, reverse) {
+ var iterator = iterable.__iterator(ITERATE_VALUES, reverse);
+ var iterations = 0;
+ var step;
+ return new Iterator(function() {
+ if (!step || iterations % 2) {
+ step = iterator.next();
+ if (step.done) {
+ return step;
+ }
+ }
+ return iterations % 2 ?
+ iteratorValue(type, iterations++, separator) :
+ iteratorValue(type, iterations++, step.value, step);
+ });
+ };
+ return interposedSequence;
+ }
+
+
+ function sortFactory(iterable, comparator, mapper) {
+ if (!comparator) {
+ comparator = defaultComparator;
+ }
+ var isKeyedIterable = isKeyed(iterable);
+ var index = 0;
+ var entries = iterable.toSeq().map(
+ function(v, k) {return [k, v, index++, mapper ? mapper(v, k, iterable) : v]}
+ ).toArray();
+ entries.sort(function(a, b) {return comparator(a[3], b[3]) || a[2] - b[2]}).forEach(
+ isKeyedIterable ?
+ function(v, i) { entries[i].length = 2; } :
+ function(v, i) { entries[i] = v[1]; }
+ );
+ return isKeyedIterable ? KeyedSeq(entries) :
+ isIndexed(iterable) ? IndexedSeq(entries) :
+ SetSeq(entries);
+ }
+
+
+ function maxFactory(iterable, comparator, mapper) {
+ if (!comparator) {
+ comparator = defaultComparator;
+ }
+ if (mapper) {
+ var entry = iterable.toSeq()
+ .map(function(v, k) {return [v, mapper(v, k, iterable)]})
+ .reduce(function(a, b) {return maxCompare(comparator, a[1], b[1]) ? b : a});
+ return entry && entry[0];
+ } else {
+ return iterable.reduce(function(a, b) {return maxCompare(comparator, a, b) ? b : a});
+ }
+ }
+
+ function maxCompare(comparator, a, b) {
+ var comp = comparator(b, a);
+ // b is considered the new max if the comparator declares them equal, but
+ // they are not equal and b is in fact a nullish value.
+ return (comp === 0 && b !== a && (b === undefined || b === null || b !== b)) || comp > 0;
+ }
+
+
+ function zipWithFactory(keyIter, zipper, iters) {
+ var zipSequence = makeSequence(keyIter);
+ zipSequence.size = new ArraySeq(iters).map(function(i ) {return i.size}).min();
+ // Note: this a generic base implementation of __iterate in terms of
+ // __iterator which may be more generically useful in the future.
+ zipSequence.__iterate = function(fn, reverse) {
+ /* generic:
+ var iterator = this.__iterator(ITERATE_ENTRIES, reverse);
+ var step;
+ var iterations = 0;
+ while (!(step = iterator.next()).done) {
+ iterations++;
+ if (fn(step.value[1], step.value[0], this) === false) {
+ break;
+ }
+ }
+ return iterations;
+ */
+ // indexed:
+ var iterator = this.__iterator(ITERATE_VALUES, reverse);
+ var step;
+ var iterations = 0;
+ while (!(step = iterator.next()).done) {
+ if (fn(step.value, iterations++, this) === false) {
+ break;
+ }
+ }
+ return iterations;
+ };
+ zipSequence.__iteratorUncached = function(type, reverse) {
+ var iterators = iters.map(function(i )
+ {return (i = Iterable(i), getIterator(reverse ? i.reverse() : i))}
+ );
+ var iterations = 0;
+ var isDone = false;
+ return new Iterator(function() {
+ var steps;
+ if (!isDone) {
+ steps = iterators.map(function(i ) {return i.next()});
+ isDone = steps.some(function(s ) {return s.done});
+ }
+ if (isDone) {
+ return iteratorDone();
+ }
+ return iteratorValue(
+ type,
+ iterations++,
+ zipper.apply(null, steps.map(function(s ) {return s.value}))
+ );
+ });
+ };
+ return zipSequence
+ }
+
+
+ // #pragma Helper Functions
+
+ function reify(iter, seq) {
+ return isSeq(iter) ? seq : iter.constructor(seq);
+ }
+
+ function validateEntry(entry) {
+ if (entry !== Object(entry)) {
+ throw new TypeError('Expected [K, V] tuple: ' + entry);
+ }
+ }
+
+ function resolveSize(iter) {
+ assertNotInfinite(iter.size);
+ return ensureSize(iter);
+ }
+
+ function iterableClass(iterable) {
+ return isKeyed(iterable) ? KeyedIterable :
+ isIndexed(iterable) ? IndexedIterable :
+ SetIterable;
+ }
+
+ function makeSequence(iterable) {
+ return Object.create(
+ (
+ isKeyed(iterable) ? KeyedSeq :
+ isIndexed(iterable) ? IndexedSeq :
+ SetSeq
+ ).prototype
+ );
+ }
+
+ function cacheResultThrough() {
+ if (this._iter.cacheResult) {
+ this._iter.cacheResult();
+ this.size = this._iter.size;
+ return this;
+ } else {
+ return Seq.prototype.cacheResult.call(this);
+ }
+ }
+
+ function defaultComparator(a, b) {
+ return a > b ? 1 : a < b ? -1 : 0;
+ }
+
+ function forceIterator(keyPath) {
+ var iter = getIterator(keyPath);
+ if (!iter) {
+ // Array might not be iterable in this environment, so we need a fallback
+ // to our wrapped type.
+ if (!isArrayLike(keyPath)) {
+ throw new TypeError('Expected iterable or array-like: ' + keyPath);
+ }
+ iter = getIterator(Iterable(keyPath));
+ }
+ return iter;
+ }
+
+ createClass(Record, KeyedCollection);
+
+ function Record(defaultValues, name) {
+ var hasInitialized;
+
+ var RecordType = function Record(values) {
+ if (values instanceof RecordType) {
+ return values;
+ }
+ if (!(this instanceof RecordType)) {
+ return new RecordType(values);
+ }
+ if (!hasInitialized) {
+ hasInitialized = true;
+ var keys = Object.keys(defaultValues);
+ setProps(RecordTypePrototype, keys);
+ RecordTypePrototype.size = keys.length;
+ RecordTypePrototype._name = name;
+ RecordTypePrototype._keys = keys;
+ RecordTypePrototype._defaultValues = defaultValues;
+ }
+ this._map = Map(values);
+ };
+
+ var RecordTypePrototype = RecordType.prototype = Object.create(RecordPrototype);
+ RecordTypePrototype.constructor = RecordType;
+
+ return RecordType;
+ }
+
+ Record.prototype.toString = function() {
+ return this.__toString(recordName(this) + ' {', '}');
+ };
+
+ // @pragma Access
+
+ Record.prototype.has = function(k) {
+ return this._defaultValues.hasOwnProperty(k);
+ };
+
+ Record.prototype.get = function(k, notSetValue) {
+ if (!this.has(k)) {
+ return notSetValue;
+ }
+ var defaultVal = this._defaultValues[k];
+ return this._map ? this._map.get(k, defaultVal) : defaultVal;
+ };
+
+ // @pragma Modification
+
+ Record.prototype.clear = function() {
+ if (this.__ownerID) {
+ this._map && this._map.clear();
+ return this;
+ }
+ var RecordType = this.constructor;
+ return RecordType._empty || (RecordType._empty = makeRecord(this, emptyMap()));
+ };
+
+ Record.prototype.set = function(k, v) {
+ if (!this.has(k)) {
+ throw new Error('Cannot set unknown key "' + k + '" on ' + recordName(this));
+ }
+ if (this._map && !this._map.has(k)) {
+ var defaultVal = this._defaultValues[k];
+ if (v === defaultVal) {
+ return this;
+ }
+ }
+ var newMap = this._map && this._map.set(k, v);
+ if (this.__ownerID || newMap === this._map) {
+ return this;
+ }
+ return makeRecord(this, newMap);
+ };
+
+ Record.prototype.remove = function(k) {
+ if (!this.has(k)) {
+ return this;
+ }
+ var newMap = this._map && this._map.remove(k);
+ if (this.__ownerID || newMap === this._map) {
+ return this;
+ }
+ return makeRecord(this, newMap);
+ };
+
+ Record.prototype.wasAltered = function() {
+ return this._map.wasAltered();
+ };
+
+ Record.prototype.__iterator = function(type, reverse) {var this$0 = this;
+ return KeyedIterable(this._defaultValues).map(function(_, k) {return this$0.get(k)}).__iterator(type, reverse);
+ };
+
+ Record.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return KeyedIterable(this._defaultValues).map(function(_, k) {return this$0.get(k)}).__iterate(fn, reverse);
+ };
+
+ Record.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ var newMap = this._map && this._map.__ensureOwner(ownerID);
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this._map = newMap;
+ return this;
+ }
+ return makeRecord(this, newMap, ownerID);
+ };
+
+
+ var RecordPrototype = Record.prototype;
+ RecordPrototype[DELETE] = RecordPrototype.remove;
+ RecordPrototype.deleteIn =
+ RecordPrototype.removeIn = MapPrototype.removeIn;
+ RecordPrototype.merge = MapPrototype.merge;
+ RecordPrototype.mergeWith = MapPrototype.mergeWith;
+ RecordPrototype.mergeIn = MapPrototype.mergeIn;
+ RecordPrototype.mergeDeep = MapPrototype.mergeDeep;
+ RecordPrototype.mergeDeepWith = MapPrototype.mergeDeepWith;
+ RecordPrototype.mergeDeepIn = MapPrototype.mergeDeepIn;
+ RecordPrototype.setIn = MapPrototype.setIn;
+ RecordPrototype.update = MapPrototype.update;
+ RecordPrototype.updateIn = MapPrototype.updateIn;
+ RecordPrototype.withMutations = MapPrototype.withMutations;
+ RecordPrototype.asMutable = MapPrototype.asMutable;
+ RecordPrototype.asImmutable = MapPrototype.asImmutable;
+
+
+ function makeRecord(likeRecord, map, ownerID) {
+ var record = Object.create(Object.getPrototypeOf(likeRecord));
+ record._map = map;
+ record.__ownerID = ownerID;
+ return record;
+ }
+
+ function recordName(record) {
+ return record._name || record.constructor.name || 'Record';
+ }
+
+ function setProps(prototype, names) {
+ try {
+ names.forEach(setProp.bind(undefined, prototype));
+ } catch (error) {
+ // Object.defineProperty failed. Probably IE8.
+ }
+ }
+
+ function setProp(prototype, name) {
+ Object.defineProperty(prototype, name, {
+ get: function() {
+ return this.get(name);
+ },
+ set: function(value) {
+ invariant(this.__ownerID, 'Cannot set on an immutable record.');
+ this.set(name, value);
+ }
+ });
+ }
+
+ createClass(Set, SetCollection);
+
+ // @pragma Construction
+
+ function Set(value) {
+ return value === null || value === undefined ? emptySet() :
+ isSet(value) && !isOrdered(value) ? value :
+ emptySet().withMutations(function(set ) {
+ var iter = SetIterable(value);
+ assertNotInfinite(iter.size);
+ iter.forEach(function(v ) {return set.add(v)});
+ });
+ }
+
+ Set.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ Set.fromKeys = function(value) {
+ return this(KeyedIterable(value).keySeq());
+ };
+
+ Set.prototype.toString = function() {
+ return this.__toString('Set {', '}');
+ };
+
+ // @pragma Access
+
+ Set.prototype.has = function(value) {
+ return this._map.has(value);
+ };
+
+ // @pragma Modification
+
+ Set.prototype.add = function(value) {
+ return updateSet(this, this._map.set(value, true));
+ };
+
+ Set.prototype.remove = function(value) {
+ return updateSet(this, this._map.remove(value));
+ };
+
+ Set.prototype.clear = function() {
+ return updateSet(this, this._map.clear());
+ };
+
+ // @pragma Composition
+
+ Set.prototype.union = function() {var iters = SLICE$0.call(arguments, 0);
+ iters = iters.filter(function(x ) {return x.size !== 0});
+ if (iters.length === 0) {
+ return this;
+ }
+ if (this.size === 0 && !this.__ownerID && iters.length === 1) {
+ return this.constructor(iters[0]);
+ }
+ return this.withMutations(function(set ) {
+ for (var ii = 0; ii < iters.length; ii++) {
+ SetIterable(iters[ii]).forEach(function(value ) {return set.add(value)});
+ }
+ });
+ };
+
+ Set.prototype.intersect = function() {var iters = SLICE$0.call(arguments, 0);
+ if (iters.length === 0) {
+ return this;
+ }
+ iters = iters.map(function(iter ) {return SetIterable(iter)});
+ var originalSet = this;
+ return this.withMutations(function(set ) {
+ originalSet.forEach(function(value ) {
+ if (!iters.every(function(iter ) {return iter.includes(value)})) {
+ set.remove(value);
+ }
+ });
+ });
+ };
+
+ Set.prototype.subtract = function() {var iters = SLICE$0.call(arguments, 0);
+ if (iters.length === 0) {
+ return this;
+ }
+ iters = iters.map(function(iter ) {return SetIterable(iter)});
+ var originalSet = this;
+ return this.withMutations(function(set ) {
+ originalSet.forEach(function(value ) {
+ if (iters.some(function(iter ) {return iter.includes(value)})) {
+ set.remove(value);
+ }
+ });
+ });
+ };
+
+ Set.prototype.merge = function() {
+ return this.union.apply(this, arguments);
+ };
+
+ Set.prototype.mergeWith = function(merger) {var iters = SLICE$0.call(arguments, 1);
+ return this.union.apply(this, iters);
+ };
+
+ Set.prototype.sort = function(comparator) {
+ // Late binding
+ return OrderedSet(sortFactory(this, comparator));
+ };
+
+ Set.prototype.sortBy = function(mapper, comparator) {
+ // Late binding
+ return OrderedSet(sortFactory(this, comparator, mapper));
+ };
+
+ Set.prototype.wasAltered = function() {
+ return this._map.wasAltered();
+ };
+
+ Set.prototype.__iterate = function(fn, reverse) {var this$0 = this;
+ return this._map.__iterate(function(_, k) {return fn(k, k, this$0)}, reverse);
+ };
+
+ Set.prototype.__iterator = function(type, reverse) {
+ return this._map.map(function(_, k) {return k}).__iterator(type, reverse);
+ };
+
+ Set.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ var newMap = this._map.__ensureOwner(ownerID);
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this._map = newMap;
+ return this;
+ }
+ return this.__make(newMap, ownerID);
+ };
+
+
+ function isSet(maybeSet) {
+ return !!(maybeSet && maybeSet[IS_SET_SENTINEL]);
+ }
+
+ Set.isSet = isSet;
+
+ var IS_SET_SENTINEL = '@@__IMMUTABLE_SET__@@';
+
+ var SetPrototype = Set.prototype;
+ SetPrototype[IS_SET_SENTINEL] = true;
+ SetPrototype[DELETE] = SetPrototype.remove;
+ SetPrototype.mergeDeep = SetPrototype.merge;
+ SetPrototype.mergeDeepWith = SetPrototype.mergeWith;
+ SetPrototype.withMutations = MapPrototype.withMutations;
+ SetPrototype.asMutable = MapPrototype.asMutable;
+ SetPrototype.asImmutable = MapPrototype.asImmutable;
+
+ SetPrototype.__empty = emptySet;
+ SetPrototype.__make = makeSet;
+
+ function updateSet(set, newMap) {
+ if (set.__ownerID) {
+ set.size = newMap.size;
+ set._map = newMap;
+ return set;
+ }
+ return newMap === set._map ? set :
+ newMap.size === 0 ? set.__empty() :
+ set.__make(newMap);
+ }
+
+ function makeSet(map, ownerID) {
+ var set = Object.create(SetPrototype);
+ set.size = map ? map.size : 0;
+ set._map = map;
+ set.__ownerID = ownerID;
+ return set;
+ }
+
+ var EMPTY_SET;
+ function emptySet() {
+ return EMPTY_SET || (EMPTY_SET = makeSet(emptyMap()));
+ }
+
+ createClass(OrderedSet, Set);
+
+ // @pragma Construction
+
+ function OrderedSet(value) {
+ return value === null || value === undefined ? emptyOrderedSet() :
+ isOrderedSet(value) ? value :
+ emptyOrderedSet().withMutations(function(set ) {
+ var iter = SetIterable(value);
+ assertNotInfinite(iter.size);
+ iter.forEach(function(v ) {return set.add(v)});
+ });
+ }
+
+ OrderedSet.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ OrderedSet.fromKeys = function(value) {
+ return this(KeyedIterable(value).keySeq());
+ };
+
+ OrderedSet.prototype.toString = function() {
+ return this.__toString('OrderedSet {', '}');
+ };
+
+
+ function isOrderedSet(maybeOrderedSet) {
+ return isSet(maybeOrderedSet) && isOrdered(maybeOrderedSet);
+ }
+
+ OrderedSet.isOrderedSet = isOrderedSet;
+
+ var OrderedSetPrototype = OrderedSet.prototype;
+ OrderedSetPrototype[IS_ORDERED_SENTINEL] = true;
+
+ OrderedSetPrototype.__empty = emptyOrderedSet;
+ OrderedSetPrototype.__make = makeOrderedSet;
+
+ function makeOrderedSet(map, ownerID) {
+ var set = Object.create(OrderedSetPrototype);
+ set.size = map ? map.size : 0;
+ set._map = map;
+ set.__ownerID = ownerID;
+ return set;
+ }
+
+ var EMPTY_ORDERED_SET;
+ function emptyOrderedSet() {
+ return EMPTY_ORDERED_SET || (EMPTY_ORDERED_SET = makeOrderedSet(emptyOrderedMap()));
+ }
+
+ createClass(Stack, IndexedCollection);
+
+ // @pragma Construction
+
+ function Stack(value) {
+ return value === null || value === undefined ? emptyStack() :
+ isStack(value) ? value :
+ emptyStack().unshiftAll(value);
+ }
+
+ Stack.of = function(/*...values*/) {
+ return this(arguments);
+ };
+
+ Stack.prototype.toString = function() {
+ return this.__toString('Stack [', ']');
+ };
+
+ // @pragma Access
+
+ Stack.prototype.get = function(index, notSetValue) {
+ var head = this._head;
+ index = wrapIndex(this, index);
+ while (head && index--) {
+ head = head.next;
+ }
+ return head ? head.value : notSetValue;
+ };
+
+ Stack.prototype.peek = function() {
+ return this._head && this._head.value;
+ };
+
+ // @pragma Modification
+
+ Stack.prototype.push = function(/*...values*/) {
+ if (arguments.length === 0) {
+ return this;
+ }
+ var newSize = this.size + arguments.length;
+ var head = this._head;
+ for (var ii = arguments.length - 1; ii >= 0; ii--) {
+ head = {
+ value: arguments[ii],
+ next: head
+ };
+ }
+ if (this.__ownerID) {
+ this.size = newSize;
+ this._head = head;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return makeStack(newSize, head);
+ };
+
+ Stack.prototype.pushAll = function(iter) {
+ iter = IndexedIterable(iter);
+ if (iter.size === 0) {
+ return this;
+ }
+ assertNotInfinite(iter.size);
+ var newSize = this.size;
+ var head = this._head;
+ iter.reverse().forEach(function(value ) {
+ newSize++;
+ head = {
+ value: value,
+ next: head
+ };
+ });
+ if (this.__ownerID) {
+ this.size = newSize;
+ this._head = head;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return makeStack(newSize, head);
+ };
+
+ Stack.prototype.pop = function() {
+ return this.slice(1);
+ };
+
+ Stack.prototype.unshift = function(/*...values*/) {
+ return this.push.apply(this, arguments);
+ };
+
+ Stack.prototype.unshiftAll = function(iter) {
+ return this.pushAll(iter);
+ };
+
+ Stack.prototype.shift = function() {
+ return this.pop.apply(this, arguments);
+ };
+
+ Stack.prototype.clear = function() {
+ if (this.size === 0) {
+ return this;
+ }
+ if (this.__ownerID) {
+ this.size = 0;
+ this._head = undefined;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return emptyStack();
+ };
+
+ Stack.prototype.slice = function(begin, end) {
+ if (wholeSlice(begin, end, this.size)) {
+ return this;
+ }
+ var resolvedBegin = resolveBegin(begin, this.size);
+ var resolvedEnd = resolveEnd(end, this.size);
+ if (resolvedEnd !== this.size) {
+ // super.slice(begin, end);
+ return IndexedCollection.prototype.slice.call(this, begin, end);
+ }
+ var newSize = this.size - resolvedBegin;
+ var head = this._head;
+ while (resolvedBegin--) {
+ head = head.next;
+ }
+ if (this.__ownerID) {
+ this.size = newSize;
+ this._head = head;
+ this.__hash = undefined;
+ this.__altered = true;
+ return this;
+ }
+ return makeStack(newSize, head);
+ };
+
+ // @pragma Mutability
+
+ Stack.prototype.__ensureOwner = function(ownerID) {
+ if (ownerID === this.__ownerID) {
+ return this;
+ }
+ if (!ownerID) {
+ this.__ownerID = ownerID;
+ this.__altered = false;
+ return this;
+ }
+ return makeStack(this.size, this._head, ownerID, this.__hash);
+ };
+
+ // @pragma Iteration
+
+ Stack.prototype.__iterate = function(fn, reverse) {
+ if (reverse) {
+ return this.reverse().__iterate(fn);
+ }
+ var iterations = 0;
+ var node = this._head;
+ while (node) {
+ if (fn(node.value, iterations++, this) === false) {
+ break;
+ }
+ node = node.next;
+ }
+ return iterations;
+ };
+
+ Stack.prototype.__iterator = function(type, reverse) {
+ if (reverse) {
+ return this.reverse().__iterator(type);
+ }
+ var iterations = 0;
+ var node = this._head;
+ return new Iterator(function() {
+ if (node) {
+ var value = node.value;
+ node = node.next;
+ return iteratorValue(type, iterations++, value);
+ }
+ return iteratorDone();
+ });
+ };
+
+
+ function isStack(maybeStack) {
+ return !!(maybeStack && maybeStack[IS_STACK_SENTINEL]);
+ }
+
+ Stack.isStack = isStack;
+
+ var IS_STACK_SENTINEL = '@@__IMMUTABLE_STACK__@@';
+
+ var StackPrototype = Stack.prototype;
+ StackPrototype[IS_STACK_SENTINEL] = true;
+ StackPrototype.withMutations = MapPrototype.withMutations;
+ StackPrototype.asMutable = MapPrototype.asMutable;
+ StackPrototype.asImmutable = MapPrototype.asImmutable;
+ StackPrototype.wasAltered = MapPrototype.wasAltered;
+
+
+ function makeStack(size, head, ownerID, hash) {
+ var map = Object.create(StackPrototype);
+ map.size = size;
+ map._head = head;
+ map.__ownerID = ownerID;
+ map.__hash = hash;
+ map.__altered = false;
+ return map;
+ }
+
+ var EMPTY_STACK;
+ function emptyStack() {
+ return EMPTY_STACK || (EMPTY_STACK = makeStack(0));
+ }
+
+ /**
+ * Contributes additional methods to a constructor
+ */
+ function mixin(ctor, methods) {
+ var keyCopier = function(key ) { ctor.prototype[key] = methods[key]; };
+ Object.keys(methods).forEach(keyCopier);
+ Object.getOwnPropertySymbols &&
+ Object.getOwnPropertySymbols(methods).forEach(keyCopier);
+ return ctor;
+ }
+
+ Iterable.Iterator = Iterator;
+
+ mixin(Iterable, {
+
+ // ### Conversion to other types
+
+ toArray: function() {
+ assertNotInfinite(this.size);
+ var array = new Array(this.size || 0);
+ this.valueSeq().__iterate(function(v, i) { array[i] = v; });
+ return array;
+ },
+
+ toIndexedSeq: function() {
+ return new ToIndexedSequence(this);
+ },
+
+ toJS: function() {
+ return this.toSeq().map(
+ function(value ) {return value && typeof value.toJS === 'function' ? value.toJS() : value}
+ ).__toJS();
+ },
+
+ toJSON: function() {
+ return this.toSeq().map(
+ function(value ) {return value && typeof value.toJSON === 'function' ? value.toJSON() : value}
+ ).__toJS();
+ },
+
+ toKeyedSeq: function() {
+ return new ToKeyedSequence(this, true);
+ },
+
+ toMap: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return Map(this.toKeyedSeq());
+ },
+
+ toObject: function() {
+ assertNotInfinite(this.size);
+ var object = {};
+ this.__iterate(function(v, k) { object[k] = v; });
+ return object;
+ },
+
+ toOrderedMap: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return OrderedMap(this.toKeyedSeq());
+ },
+
+ toOrderedSet: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return OrderedSet(isKeyed(this) ? this.valueSeq() : this);
+ },
+
+ toSet: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return Set(isKeyed(this) ? this.valueSeq() : this);
+ },
+
+ toSetSeq: function() {
+ return new ToSetSequence(this);
+ },
+
+ toSeq: function() {
+ return isIndexed(this) ? this.toIndexedSeq() :
+ isKeyed(this) ? this.toKeyedSeq() :
+ this.toSetSeq();
+ },
+
+ toStack: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return Stack(isKeyed(this) ? this.valueSeq() : this);
+ },
+
+ toList: function() {
+ // Use Late Binding here to solve the circular dependency.
+ return List(isKeyed(this) ? this.valueSeq() : this);
+ },
+
+
+ // ### Common JavaScript methods and properties
+
+ toString: function() {
+ return '[Iterable]';
+ },
+
+ __toString: function(head, tail) {
+ if (this.size === 0) {
+ return head + tail;
+ }
+ return head + ' ' + this.toSeq().map(this.__toStringMapper).join(', ') + ' ' + tail;
+ },
+
+
+ // ### ES6 Collection methods (ES6 Array and Map)
+
+ concat: function() {var values = SLICE$0.call(arguments, 0);
+ return reify(this, concatFactory(this, values));
+ },
+
+ includes: function(searchValue) {
+ return this.some(function(value ) {return is(value, searchValue)});
+ },
+
+ entries: function() {
+ return this.__iterator(ITERATE_ENTRIES);
+ },
+
+ every: function(predicate, context) {
+ assertNotInfinite(this.size);
+ var returnValue = true;
+ this.__iterate(function(v, k, c) {
+ if (!predicate.call(context, v, k, c)) {
+ returnValue = false;
+ return false;
+ }
+ });
+ return returnValue;
+ },
+
+ filter: function(predicate, context) {
+ return reify(this, filterFactory(this, predicate, context, true));
+ },
+
+ find: function(predicate, context, notSetValue) {
+ var entry = this.findEntry(predicate, context);
+ return entry ? entry[1] : notSetValue;
+ },
+
+ findEntry: function(predicate, context) {
+ var found;
+ this.__iterate(function(v, k, c) {
+ if (predicate.call(context, v, k, c)) {
+ found = [k, v];
+ return false;
+ }
+ });
+ return found;
+ },
+
+ findLastEntry: function(predicate, context) {
+ return this.toSeq().reverse().findEntry(predicate, context);
+ },
+
+ forEach: function(sideEffect, context) {
+ assertNotInfinite(this.size);
+ return this.__iterate(context ? sideEffect.bind(context) : sideEffect);
+ },
+
+ join: function(separator) {
+ assertNotInfinite(this.size);
+ separator = separator !== undefined ? '' + separator : ',';
+ var joined = '';
+ var isFirst = true;
+ this.__iterate(function(v ) {
+ isFirst ? (isFirst = false) : (joined += separator);
+ joined += v !== null && v !== undefined ? v.toString() : '';
+ });
+ return joined;
+ },
+
+ keys: function() {
+ return this.__iterator(ITERATE_KEYS);
+ },
+
+ map: function(mapper, context) {
+ return reify(this, mapFactory(this, mapper, context));
+ },
+
+ reduce: function(reducer, initialReduction, context) {
+ assertNotInfinite(this.size);
+ var reduction;
+ var useFirst;
+ if (arguments.length < 2) {
+ useFirst = true;
+ } else {
+ reduction = initialReduction;
+ }
+ this.__iterate(function(v, k, c) {
+ if (useFirst) {
+ useFirst = false;
+ reduction = v;
+ } else {
+ reduction = reducer.call(context, reduction, v, k, c);
+ }
+ });
+ return reduction;
+ },
+
+ reduceRight: function(reducer, initialReduction, context) {
+ var reversed = this.toKeyedSeq().reverse();
+ return reversed.reduce.apply(reversed, arguments);
+ },
+
+ reverse: function() {
+ return reify(this, reverseFactory(this, true));
+ },
+
+ slice: function(begin, end) {
+ return reify(this, sliceFactory(this, begin, end, true));
+ },
+
+ some: function(predicate, context) {
+ return !this.every(not(predicate), context);
+ },
+
+ sort: function(comparator) {
+ return reify(this, sortFactory(this, comparator));
+ },
+
+ values: function() {
+ return this.__iterator(ITERATE_VALUES);
+ },
+
+
+ // ### More sequential methods
+
+ butLast: function() {
+ return this.slice(0, -1);
+ },
+
+ isEmpty: function() {
+ return this.size !== undefined ? this.size === 0 : !this.some(function() {return true});
+ },
+
+ count: function(predicate, context) {
+ return ensureSize(
+ predicate ? this.toSeq().filter(predicate, context) : this
+ );
+ },
+
+ countBy: function(grouper, context) {
+ return countByFactory(this, grouper, context);
+ },
+
+ equals: function(other) {
+ return deepEqual(this, other);
+ },
+
+ entrySeq: function() {
+ var iterable = this;
+ if (iterable._cache) {
+ // We cache as an entries array, so we can just return the cache!
+ return new ArraySeq(iterable._cache);
+ }
+ var entriesSequence = iterable.toSeq().map(entryMapper).toIndexedSeq();
+ entriesSequence.fromEntrySeq = function() {return iterable.toSeq()};
+ return entriesSequence;
+ },
+
+ filterNot: function(predicate, context) {
+ return this.filter(not(predicate), context);
+ },
+
+ findLast: function(predicate, context, notSetValue) {
+ return this.toKeyedSeq().reverse().find(predicate, context, notSetValue);
+ },
+
+ first: function() {
+ return this.find(returnTrue);
+ },
+
+ flatMap: function(mapper, context) {
+ return reify(this, flatMapFactory(this, mapper, context));
+ },
+
+ flatten: function(depth) {
+ return reify(this, flattenFactory(this, depth, true));
+ },
+
+ fromEntrySeq: function() {
+ return new FromEntriesSequence(this);
+ },
+
+ get: function(searchKey, notSetValue) {
+ return this.find(function(_, key) {return is(key, searchKey)}, undefined, notSetValue);
+ },
+
+ getIn: function(searchKeyPath, notSetValue) {
+ var nested = this;
+ // Note: in an ES6 environment, we would prefer:
+ // for (var key of searchKeyPath) {
+ var iter = forceIterator(searchKeyPath);
+ var step;
+ while (!(step = iter.next()).done) {
+ var key = step.value;
+ nested = nested && nested.get ? nested.get(key, NOT_SET) : NOT_SET;
+ if (nested === NOT_SET) {
+ return notSetValue;
+ }
+ }
+ return nested;
+ },
+
+ groupBy: function(grouper, context) {
+ return groupByFactory(this, grouper, context);
+ },
+
+ has: function(searchKey) {
+ return this.get(searchKey, NOT_SET) !== NOT_SET;
+ },
+
+ hasIn: function(searchKeyPath) {
+ return this.getIn(searchKeyPath, NOT_SET) !== NOT_SET;
+ },
+
+ isSubset: function(iter) {
+ iter = typeof iter.includes === 'function' ? iter : Iterable(iter);
+ return this.every(function(value ) {return iter.includes(value)});
+ },
+
+ isSuperset: function(iter) {
+ iter = typeof iter.isSubset === 'function' ? iter : Iterable(iter);
+ return iter.isSubset(this);
+ },
+
+ keySeq: function() {
+ return this.toSeq().map(keyMapper).toIndexedSeq();
+ },
+
+ last: function() {
+ return this.toSeq().reverse().first();
+ },
+
+ max: function(comparator) {
+ return maxFactory(this, comparator);
+ },
+
+ maxBy: function(mapper, comparator) {
+ return maxFactory(this, comparator, mapper);
+ },
+
+ min: function(comparator) {
+ return maxFactory(this, comparator ? neg(comparator) : defaultNegComparator);
+ },
+
+ minBy: function(mapper, comparator) {
+ return maxFactory(this, comparator ? neg(comparator) : defaultNegComparator, mapper);
+ },
+
+ rest: function() {
+ return this.slice(1);
+ },
+
+ skip: function(amount) {
+ return this.slice(Math.max(0, amount));
+ },
+
+ skipLast: function(amount) {
+ return reify(this, this.toSeq().reverse().skip(amount).reverse());
+ },
+
+ skipWhile: function(predicate, context) {
+ return reify(this, skipWhileFactory(this, predicate, context, true));
+ },
+
+ skipUntil: function(predicate, context) {
+ return this.skipWhile(not(predicate), context);
+ },
+
+ sortBy: function(mapper, comparator) {
+ return reify(this, sortFactory(this, comparator, mapper));
+ },
+
+ take: function(amount) {
+ return this.slice(0, Math.max(0, amount));
+ },
+
+ takeLast: function(amount) {
+ return reify(this, this.toSeq().reverse().take(amount).reverse());
+ },
+
+ takeWhile: function(predicate, context) {
+ return reify(this, takeWhileFactory(this, predicate, context));
+ },
+
+ takeUntil: function(predicate, context) {
+ return this.takeWhile(not(predicate), context);
+ },
+
+ valueSeq: function() {
+ return this.toIndexedSeq();
+ },
+
+
+ // ### Hashable Object
+
+ hashCode: function() {
+ return this.__hash || (this.__hash = hashIterable(this));
+ }
+
+
+ // ### Internal
+
+ // abstract __iterate(fn, reverse)
+
+ // abstract __iterator(type, reverse)
+ });
+
+ // var IS_ITERABLE_SENTINEL = '@@__IMMUTABLE_ITERABLE__@@';
+ // var IS_KEYED_SENTINEL = '@@__IMMUTABLE_KEYED__@@';
+ // var IS_INDEXED_SENTINEL = '@@__IMMUTABLE_INDEXED__@@';
+ // var IS_ORDERED_SENTINEL = '@@__IMMUTABLE_ORDERED__@@';
+
+ var IterablePrototype = Iterable.prototype;
+ IterablePrototype[IS_ITERABLE_SENTINEL] = true;
+ IterablePrototype[ITERATOR_SYMBOL] = IterablePrototype.values;
+ IterablePrototype.__toJS = IterablePrototype.toArray;
+ IterablePrototype.__toStringMapper = quoteString;
+ IterablePrototype.inspect =
+ IterablePrototype.toSource = function() { return this.toString(); };
+ IterablePrototype.chain = IterablePrototype.flatMap;
+ IterablePrototype.contains = IterablePrototype.includes;
+
+ // Temporary warning about using length
+ (function () {
+ try {
+ Object.defineProperty(IterablePrototype, 'length', {
+ get: function () {
+ if (!Iterable.noLengthWarning) {
+ var stack;
+ try {
+ throw new Error();
+ } catch (error) {
+ stack = error.stack;
+ }
+ if (stack.indexOf('_wrapObject') === -1) {
+ console && console.warn && console.warn(
+ 'iterable.length has been deprecated, '+
+ 'use iterable.size or iterable.count(). '+
+ 'This warning will become a silent error in a future version. ' +
+ stack
+ );
+ return this.size;
+ }
+ }
+ }
+ });
+ } catch (e) {}
+ })();
+
+
+
+ mixin(KeyedIterable, {
+
+ // ### More sequential methods
+
+ flip: function() {
+ return reify(this, flipFactory(this));
+ },
+
+ findKey: function(predicate, context) {
+ var entry = this.findEntry(predicate, context);
+ return entry && entry[0];
+ },
+
+ findLastKey: function(predicate, context) {
+ return this.toSeq().reverse().findKey(predicate, context);
+ },
+
+ keyOf: function(searchValue) {
+ return this.findKey(function(value ) {return is(value, searchValue)});
+ },
+
+ lastKeyOf: function(searchValue) {
+ return this.findLastKey(function(value ) {return is(value, searchValue)});
+ },
+
+ mapEntries: function(mapper, context) {var this$0 = this;
+ var iterations = 0;
+ return reify(this,
+ this.toSeq().map(
+ function(v, k) {return mapper.call(context, [k, v], iterations++, this$0)}
+ ).fromEntrySeq()
+ );
+ },
+
+ mapKeys: function(mapper, context) {var this$0 = this;
+ return reify(this,
+ this.toSeq().flip().map(
+ function(k, v) {return mapper.call(context, k, v, this$0)}
+ ).flip()
+ );
+ }
+
+ });
+
+ var KeyedIterablePrototype = KeyedIterable.prototype;
+ KeyedIterablePrototype[IS_KEYED_SENTINEL] = true;
+ KeyedIterablePrototype[ITERATOR_SYMBOL] = IterablePrototype.entries;
+ KeyedIterablePrototype.__toJS = IterablePrototype.toObject;
+ KeyedIterablePrototype.__toStringMapper = function(v, k) {return JSON.stringify(k) + ': ' + quoteString(v)};
+
+
+
+ mixin(IndexedIterable, {
+
+ // ### Conversion to other types
+
+ toKeyedSeq: function() {
+ return new ToKeyedSequence(this, false);
+ },
+
+
+ // ### ES6 Collection methods (ES6 Array and Map)
+
+ filter: function(predicate, context) {
+ return reify(this, filterFactory(this, predicate, context, false));
+ },
+
+ findIndex: function(predicate, context) {
+ var entry = this.findEntry(predicate, context);
+ return entry ? entry[0] : -1;
+ },
+
+ indexOf: function(searchValue) {
+ var key = this.toKeyedSeq().keyOf(searchValue);
+ return key === undefined ? -1 : key;
+ },
+
+ lastIndexOf: function(searchValue) {
+ var key = this.toKeyedSeq().reverse().keyOf(searchValue);
+ return key === undefined ? -1 : key;
+ },
+
+ reverse: function() {
+ return reify(this, reverseFactory(this, false));
+ },
+
+ slice: function(begin, end) {
+ return reify(this, sliceFactory(this, begin, end, false));
+ },
+
+ splice: function(index, removeNum /*, ...values*/) {
+ var numArgs = arguments.length;
+ removeNum = Math.max(removeNum | 0, 0);
+ if (numArgs === 0 || (numArgs === 2 && !removeNum)) {
+ return this;
+ }
+ // If index is negative, it should resolve relative to the size of the
+ // collection. However size may be expensive to compute if not cached, so
+ // only call count() if the number is in fact negative.
+ index = resolveBegin(index, index < 0 ? this.count() : this.size);
+ var spliced = this.slice(0, index);
+ return reify(
+ this,
+ numArgs === 1 ?
+ spliced :
+ spliced.concat(arrCopy(arguments, 2), this.slice(index + removeNum))
+ );
+ },
+
+
+ // ### More collection methods
+
+ findLastIndex: function(predicate, context) {
+ var key = this.toKeyedSeq().findLastKey(predicate, context);
+ return key === undefined ? -1 : key;
+ },
+
+ first: function() {
+ return this.get(0);
+ },
+
+ flatten: function(depth) {
+ return reify(this, flattenFactory(this, depth, false));
+ },
+
+ get: function(index, notSetValue) {
+ index = wrapIndex(this, index);
+ return (index < 0 || (this.size === Infinity ||
+ (this.size !== undefined && index > this.size))) ?
+ notSetValue :
+ this.find(function(_, key) {return key === index}, undefined, notSetValue);
+ },
+
+ has: function(index) {
+ index = wrapIndex(this, index);
+ return index >= 0 && (this.size !== undefined ?
+ this.size === Infinity || index < this.size :
+ this.indexOf(index) !== -1
+ );
+ },
+
+ interpose: function(separator) {
+ return reify(this, interposeFactory(this, separator));
+ },
+
+ interleave: function(/*...iterables*/) {
+ var iterables = [this].concat(arrCopy(arguments));
+ var zipped = zipWithFactory(this.toSeq(), IndexedSeq.of, iterables);
+ var interleaved = zipped.flatten(true);
+ if (zipped.size) {
+ interleaved.size = zipped.size * iterables.length;
+ }
+ return reify(this, interleaved);
+ },
+
+ last: function() {
+ return this.get(-1);
+ },
+
+ skipWhile: function(predicate, context) {
+ return reify(this, skipWhileFactory(this, predicate, context, false));
+ },
+
+ zip: function(/*, ...iterables */) {
+ var iterables = [this].concat(arrCopy(arguments));
+ return reify(this, zipWithFactory(this, defaultZipper, iterables));
+ },
+
+ zipWith: function(zipper/*, ...iterables */) {
+ var iterables = arrCopy(arguments);
+ iterables[0] = this;
+ return reify(this, zipWithFactory(this, zipper, iterables));
+ }
+
+ });
+
+ IndexedIterable.prototype[IS_INDEXED_SENTINEL] = true;
+ IndexedIterable.prototype[IS_ORDERED_SENTINEL] = true;
+
+
+
+ mixin(SetIterable, {
+
+ // ### ES6 Collection methods (ES6 Array and Map)
+
+ get: function(value, notSetValue) {
+ return this.has(value) ? value : notSetValue;
+ },
+
+ includes: function(value) {
+ return this.has(value);
+ },
+
+
+ // ### More sequential methods
+
+ keySeq: function() {
+ return this.valueSeq();
+ }
+
+ });
+
+ SetIterable.prototype.has = IterablePrototype.includes;
+ SetIterable.prototype.contains = SetIterable.prototype.includes;
+
+
+ // Mixin subclasses
+
+ mixin(KeyedSeq, KeyedIterable.prototype);
+ mixin(IndexedSeq, IndexedIterable.prototype);
+ mixin(SetSeq, SetIterable.prototype);
+
+ mixin(KeyedCollection, KeyedIterable.prototype);
+ mixin(IndexedCollection, IndexedIterable.prototype);
+ mixin(SetCollection, SetIterable.prototype);
+
+
+ // #pragma Helper functions
+
+ function keyMapper(v, k) {
+ return k;
+ }
+
+ function entryMapper(v, k) {
+ return [k, v];
+ }
+
+ function not(predicate) {
+ return function() {
+ return !predicate.apply(this, arguments);
+ }
+ }
+
+ function neg(predicate) {
+ return function() {
+ return -predicate.apply(this, arguments);
+ }
+ }
+
+ function quoteString(value) {
+ return typeof value === 'string' ? JSON.stringify(value) : value;
+ }
+
+ function defaultZipper() {
+ return arrCopy(arguments);
+ }
+
+ function defaultNegComparator(a, b) {
+ return a < b ? 1 : a > b ? -1 : 0;
+ }
+
+ function hashIterable(iterable) {
+ if (iterable.size === Infinity) {
+ return 0;
+ }
+ var ordered = isOrdered(iterable);
+ var keyed = isKeyed(iterable);
+ var h = ordered ? 1 : 0;
+ var size = iterable.__iterate(
+ keyed ?
+ ordered ?
+ function(v, k) { h = 31 * h + hashMerge(hash(v), hash(k)) | 0; } :
+ function(v, k) { h = h + hashMerge(hash(v), hash(k)) | 0; } :
+ ordered ?
+ function(v ) { h = 31 * h + hash(v) | 0; } :
+ function(v ) { h = h + hash(v) | 0; }
+ );
+ return murmurHashOfSize(size, h);
+ }
+
+ function murmurHashOfSize(size, h) {
+ h = imul(h, 0xCC9E2D51);
+ h = imul(h << 15 | h >>> -15, 0x1B873593);
+ h = imul(h << 13 | h >>> -13, 5);
+ h = (h + 0xE6546B64 | 0) ^ size;
+ h = imul(h ^ h >>> 16, 0x85EBCA6B);
+ h = imul(h ^ h >>> 13, 0xC2B2AE35);
+ h = smi(h ^ h >>> 16);
+ return h;
+ }
+
+ function hashMerge(a, b) {
+ return a ^ b + 0x9E3779B9 + (a << 6) + (a >> 2) | 0; // int
+ }
+
+ var Immutable = {
+
+ Iterable: Iterable,
+
+ Seq: Seq,
+ Collection: Collection,
+ Map: Map,
+ OrderedMap: OrderedMap,
+ List: List,
+ Stack: Stack,
+ Set: Set,
+ OrderedSet: OrderedSet,
+
+ Record: Record,
+ Range: Range,
+ Repeat: Repeat,
+
+ is: is,
+ fromJS: fromJS
+
+ };
+
+ return Immutable;
+
+})); \ No newline at end of file
diff --git a/devtools/client/shared/vendor/jsol.js b/devtools/client/shared/vendor/jsol.js
new file mode 100755
index 000000000..a87948c43
--- /dev/null
+++ b/devtools/client/shared/vendor/jsol.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2010, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+(function () {
+ /**
+ JSOL stands for JavaScript Object Literal which is a string representing
+ an object in JavaScript syntax.
+
+ For example:
+
+ {foo:"bar"} is equivalent to {"foo":"bar"} in JavaScript. Both are valid JSOL.
+
+ Note that {"foo":"bar"} is proper JSON[1] therefore you can use one of the many
+ JSON parsers out there like json2.js[2] or even the native browser's JSON parser,
+ if available.
+
+ However, {foo:"bar"} is NOT proper JSON but valid Javascript syntax for
+ representing an object with one key, "foo" and its value, "bar".
+ Using a JSON parser is not an option since this is NOT proper JSON.
+
+ You can use JSOL.parse to safely parse any string that reprsents a JavaScript Object Literal.
+ JSOL.parse will throw an Invalid JSOL exception on function calls, function declarations and variable references.
+
+ Examples:
+
+ JSOL.parse('{foo:"bar"}'); // valid
+
+ JSOL.parse('{evil:(function(){alert("I\'m evil");})()}'); // invalid function calls
+
+ JSOL.parse('{fn:function() { }}'); // invalid function declarations
+
+ var bar = "bar";
+ JSOL.parse('{foo:bar}'); // invalid variable references
+
+ [1] http://www.json.org
+ [2] http://www.json.org/json2.js
+ */
+ var trim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g; // Used for trimming whitespace
+ var JSOL = {
+ parse: function(text) {
+ // make sure text is a "string"
+ if (typeof text !== "string" || !text) {
+ return null;
+ }
+ // Make sure leading/trailing whitespace is removed
+ text = text.replace(trim, "");
+ // Make sure the incoming text is actual JSOL (or Javascript Object Literal)
+ // Logic borrowed from http://json.org/json2.js
+ if ( /^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@")
+ .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]")
+ .replace(/(?:^|:|,)(?:\s*\[)+/g, ":")
+ /** everything up to this point is json2.js **/
+ /** this is the 5th stage where it accepts unquoted keys **/
+ .replace(/\w*\s*\:/g, ":")) ) {
+ return (new Function("return " + text))();
+ }
+ else {
+ throw("Invalid JSOL: " + text);
+ }
+ }
+ };
+
+ if (typeof define === "function" && define.amd) {
+ define(JSOL);
+ } else if (typeof module === "object" && module.exports) {
+ module.exports = JSOL;
+ } else {
+ this.JSOL = JSOL;
+ }
+})();
diff --git a/devtools/client/shared/vendor/moz.build b/devtools/client/shared/vendor/moz.build
new file mode 100644
index 000000000..e04221293
--- /dev/null
+++ b/devtools/client/shared/vendor/moz.build
@@ -0,0 +1,29 @@
+# -*- 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/.
+modules = []
+modules += [
+ 'immutable.js',
+ 'jsol.js',
+ 'react-addons-shallow-compare.js',
+]
+
+# react-dev is used if either debug mode is enabled,
+# so include it for both
+if CONFIG['DEBUG_JS_MODULES'] or CONFIG['MOZ_DEBUG']:
+ modules += ['react-dev.js']
+
+modules += [
+ 'react-dom.js',
+ 'react-proxy.js',
+ 'react-redux.js',
+ 'react-virtualized.js',
+ 'react.js',
+ 'redux.js',
+ 'reselect.js',
+ 'seamless-immutable.js',
+]
+
+DevToolsModules(*modules)
diff --git a/devtools/client/shared/vendor/react-addons-shallow-compare.js b/devtools/client/shared/vendor/react-addons-shallow-compare.js
new file mode 100644
index 000000000..6a1c723cc
--- /dev/null
+++ b/devtools/client/shared/vendor/react-addons-shallow-compare.js
@@ -0,0 +1,9 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = require("devtools/client/shared/vendor/react").addons.shallowCompare;
diff --git a/devtools/client/shared/vendor/react-dev.js b/devtools/client/shared/vendor/react-dev.js
new file mode 100644
index 000000000..90e5e177a
--- /dev/null
+++ b/devtools/client/shared/vendor/react-dev.js
@@ -0,0 +1,20763 @@
+ /**
+ * React (with addons) v0.14.6
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.React = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactWithAddons
+ */
+
+/**
+ * This module exists purely in the open source project, and is meant as a way
+ * to create a separate standalone build of React. This build has "addons", or
+ * functionality we've built and think might be useful but doesn't have a good
+ * place to live inside React core.
+ */
+
+'use strict';
+
+var LinkedStateMixin = _dereq_(22);
+var React = _dereq_(26);
+var ReactComponentWithPureRenderMixin = _dereq_(37);
+var ReactCSSTransitionGroup = _dereq_(29);
+var ReactFragment = _dereq_(64);
+var ReactTransitionGroup = _dereq_(94);
+var ReactUpdates = _dereq_(96);
+
+var cloneWithProps = _dereq_(118);
+var shallowCompare = _dereq_(140);
+var update = _dereq_(143);
+var warning = _dereq_(173);
+
+var warnedAboutBatchedUpdates = false;
+
+React.addons = {
+ CSSTransitionGroup: ReactCSSTransitionGroup,
+ LinkedStateMixin: LinkedStateMixin,
+ PureRenderMixin: ReactComponentWithPureRenderMixin,
+ TransitionGroup: ReactTransitionGroup,
+
+ batchedUpdates: function () {
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(warnedAboutBatchedUpdates, 'React.addons.batchedUpdates is deprecated. Use ' + 'ReactDOM.unstable_batchedUpdates instead.') : undefined;
+ warnedAboutBatchedUpdates = true;
+ }
+ return ReactUpdates.batchedUpdates.apply(this, arguments);
+ },
+ cloneWithProps: cloneWithProps,
+ createFragment: ReactFragment.create,
+ shallowCompare: shallowCompare,
+ update: update
+};
+
+React.addons.TestUtils = _dereq_(91);
+
+if ("development" !== 'production') {
+ React.addons.Perf = _dereq_(55);
+}
+
+module.exports = React;
+},{"118":118,"140":140,"143":143,"173":173,"22":22,"26":26,"29":29,"37":37,"55":55,"64":64,"91":91,"94":94,"96":96}],2:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule AutoFocusUtils
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactMount = _dereq_(72);
+
+var findDOMNode = _dereq_(122);
+var focusNode = _dereq_(155);
+
+var Mixin = {
+ componentDidMount: function () {
+ if (this.props.autoFocus) {
+ focusNode(findDOMNode(this));
+ }
+ }
+};
+
+var AutoFocusUtils = {
+ Mixin: Mixin,
+
+ focusDOMComponent: function () {
+ focusNode(ReactMount.getNode(this._rootNodeID));
+ }
+};
+
+module.exports = AutoFocusUtils;
+},{"122":122,"155":155,"72":72}],3:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015 Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule BeforeInputEventPlugin
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPropagators = _dereq_(19);
+var ExecutionEnvironment = _dereq_(147);
+var FallbackCompositionState = _dereq_(20);
+var SyntheticCompositionEvent = _dereq_(103);
+var SyntheticInputEvent = _dereq_(107);
+
+var keyOf = _dereq_(166);
+
+var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
+var START_KEYCODE = 229;
+
+var canUseCompositionEvent = ExecutionEnvironment.canUseDOM && 'CompositionEvent' in window;
+
+var documentMode = null;
+if (ExecutionEnvironment.canUseDOM && 'documentMode' in document) {
+ documentMode = document.documentMode;
+}
+
+// Webkit offers a very useful `textInput` event that can be used to
+// directly represent `beforeInput`. The IE `textinput` event is not as
+// useful, so we don't use it.
+var canUseTextInputEvent = ExecutionEnvironment.canUseDOM && 'TextEvent' in window && !documentMode && !isPresto();
+
+// In IE9+, we have access to composition events, but the data supplied
+// by the native compositionend event may be incorrect. Japanese ideographic
+// spaces, for instance (\u3000) are not recorded correctly.
+var useFallbackCompositionData = ExecutionEnvironment.canUseDOM && (!canUseCompositionEvent || documentMode && documentMode > 8 && documentMode <= 11);
+
+/**
+ * Opera <= 12 includes TextEvent in window, but does not fire
+ * text input events. Rely on keypress instead.
+ */
+function isPresto() {
+ var opera = window.opera;
+ return typeof opera === 'object' && typeof opera.version === 'function' && parseInt(opera.version(), 10) <= 12;
+}
+
+var SPACEBAR_CODE = 32;
+var SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+// Events and their corresponding property names.
+var eventTypes = {
+ beforeInput: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onBeforeInput: null }),
+ captured: keyOf({ onBeforeInputCapture: null })
+ },
+ dependencies: [topLevelTypes.topCompositionEnd, topLevelTypes.topKeyPress, topLevelTypes.topTextInput, topLevelTypes.topPaste]
+ },
+ compositionEnd: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCompositionEnd: null }),
+ captured: keyOf({ onCompositionEndCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionEnd, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown]
+ },
+ compositionStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCompositionStart: null }),
+ captured: keyOf({ onCompositionStartCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionStart, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown]
+ },
+ compositionUpdate: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCompositionUpdate: null }),
+ captured: keyOf({ onCompositionUpdateCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionUpdate, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown]
+ }
+};
+
+// Track whether we've ever handled a keypress on the space key.
+var hasSpaceKeypress = false;
+
+/**
+ * Return whether a native keypress event is assumed to be a command.
+ * This is required because Firefox fires `keypress` events for key commands
+ * (cut, copy, select-all, etc.) even though no character is inserted.
+ */
+function isKeypressCommand(nativeEvent) {
+ return (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&
+ // ctrlKey && altKey is equivalent to AltGr, and is not a command.
+ !(nativeEvent.ctrlKey && nativeEvent.altKey);
+}
+
+/**
+ * Translate native top level events into event types.
+ *
+ * @param {string} topLevelType
+ * @return {object}
+ */
+function getCompositionEventType(topLevelType) {
+ switch (topLevelType) {
+ case topLevelTypes.topCompositionStart:
+ return eventTypes.compositionStart;
+ case topLevelTypes.topCompositionEnd:
+ return eventTypes.compositionEnd;
+ case topLevelTypes.topCompositionUpdate:
+ return eventTypes.compositionUpdate;
+ }
+}
+
+/**
+ * Does our fallback best-guess model think this event signifies that
+ * composition has begun?
+ *
+ * @param {string} topLevelType
+ * @param {object} nativeEvent
+ * @return {boolean}
+ */
+function isFallbackCompositionStart(topLevelType, nativeEvent) {
+ return topLevelType === topLevelTypes.topKeyDown && nativeEvent.keyCode === START_KEYCODE;
+}
+
+/**
+ * Does our fallback mode think that this event is the end of composition?
+ *
+ * @param {string} topLevelType
+ * @param {object} nativeEvent
+ * @return {boolean}
+ */
+function isFallbackCompositionEnd(topLevelType, nativeEvent) {
+ switch (topLevelType) {
+ case topLevelTypes.topKeyUp:
+ // Command keys insert or clear IME input.
+ return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
+ case topLevelTypes.topKeyDown:
+ // Expect IME keyCode on each keydown. If we get any other
+ // code we must have exited earlier.
+ return nativeEvent.keyCode !== START_KEYCODE;
+ case topLevelTypes.topKeyPress:
+ case topLevelTypes.topMouseDown:
+ case topLevelTypes.topBlur:
+ // Events are not possible without cancelling IME.
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * Google Input Tools provides composition data via a CustomEvent,
+ * with the `data` property populated in the `detail` object. If this
+ * is available on the event object, use it. If not, this is a plain
+ * composition event and we have nothing special to extract.
+ *
+ * @param {object} nativeEvent
+ * @return {?string}
+ */
+function getDataFromCustomEvent(nativeEvent) {
+ var detail = nativeEvent.detail;
+ if (typeof detail === 'object' && 'data' in detail) {
+ return detail.data;
+ }
+ return null;
+}
+
+// Track the current IME composition fallback object, if any.
+var currentComposition = null;
+
+/**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?object} A SyntheticCompositionEvent.
+ */
+function extractCompositionEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var eventType;
+ var fallbackData;
+
+ if (canUseCompositionEvent) {
+ eventType = getCompositionEventType(topLevelType);
+ } else if (!currentComposition) {
+ if (isFallbackCompositionStart(topLevelType, nativeEvent)) {
+ eventType = eventTypes.compositionStart;
+ }
+ } else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) {
+ eventType = eventTypes.compositionEnd;
+ }
+
+ if (!eventType) {
+ return null;
+ }
+
+ if (useFallbackCompositionData) {
+ // The current composition is stored statically and must not be
+ // overwritten while composition continues.
+ if (!currentComposition && eventType === eventTypes.compositionStart) {
+ currentComposition = FallbackCompositionState.getPooled(topLevelTarget);
+ } else if (eventType === eventTypes.compositionEnd) {
+ if (currentComposition) {
+ fallbackData = currentComposition.getData();
+ }
+ }
+ }
+
+ var event = SyntheticCompositionEvent.getPooled(eventType, topLevelTargetID, nativeEvent, nativeEventTarget);
+
+ if (fallbackData) {
+ // Inject data generated from fallback path into the synthetic event.
+ // This matches the property of native CompositionEventInterface.
+ event.data = fallbackData;
+ } else {
+ var customData = getDataFromCustomEvent(nativeEvent);
+ if (customData !== null) {
+ event.data = customData;
+ }
+ }
+
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+}
+
+/**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?string} The string corresponding to this `beforeInput` event.
+ */
+function getNativeBeforeInputChars(topLevelType, nativeEvent) {
+ switch (topLevelType) {
+ case topLevelTypes.topCompositionEnd:
+ return getDataFromCustomEvent(nativeEvent);
+ case topLevelTypes.topKeyPress:
+ /**
+ * If native `textInput` events are available, our goal is to make
+ * use of them. However, there is a special case: the spacebar key.
+ * In Webkit, preventing default on a spacebar `textInput` event
+ * cancels character insertion, but it *also* causes the browser
+ * to fall back to its default spacebar behavior of scrolling the
+ * page.
+ *
+ * Tracking at:
+ * https://code.google.com/p/chromium/issues/detail?id=355103
+ *
+ * To avoid this issue, use the keypress event as if no `textInput`
+ * event is available.
+ */
+ var which = nativeEvent.which;
+ if (which !== SPACEBAR_CODE) {
+ return null;
+ }
+
+ hasSpaceKeypress = true;
+ return SPACEBAR_CHAR;
+
+ case topLevelTypes.topTextInput:
+ // Record the characters to be added to the DOM.
+ var chars = nativeEvent.data;
+
+ // If it's a spacebar character, assume that we have already handled
+ // it at the keypress level and bail immediately. Android Chrome
+ // doesn't give us keycodes, so we need to blacklist it.
+ if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {
+ return null;
+ }
+
+ return chars;
+
+ default:
+ // For other native event types, do nothing.
+ return null;
+ }
+}
+
+/**
+ * For browsers that do not provide the `textInput` event, extract the
+ * appropriate string to use for SyntheticInputEvent.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?string} The fallback string for this `beforeInput` event.
+ */
+function getFallbackBeforeInputChars(topLevelType, nativeEvent) {
+ // If we are currently composing (IME) and using a fallback to do so,
+ // try to extract the composed characters from the fallback object.
+ if (currentComposition) {
+ if (topLevelType === topLevelTypes.topCompositionEnd || isFallbackCompositionEnd(topLevelType, nativeEvent)) {
+ var chars = currentComposition.getData();
+ FallbackCompositionState.release(currentComposition);
+ currentComposition = null;
+ return chars;
+ }
+ return null;
+ }
+
+ switch (topLevelType) {
+ case topLevelTypes.topPaste:
+ // If a paste event occurs after a keypress, throw out the input
+ // chars. Paste events should not lead to BeforeInput events.
+ return null;
+ case topLevelTypes.topKeyPress:
+ /**
+ * As of v27, Firefox may fire keypress events even when no character
+ * will be inserted. A few possibilities:
+ *
+ * - `which` is `0`. Arrow keys, Esc key, etc.
+ *
+ * - `which` is the pressed key code, but no char is available.
+ * Ex: 'AltGr + d` in Polish. There is no modified character for
+ * this key combination and no character is inserted into the
+ * document, but FF fires the keypress for char code `100` anyway.
+ * No `input` event will occur.
+ *
+ * - `which` is the pressed key code, but a command combination is
+ * being used. Ex: `Cmd+C`. No character is inserted, and no
+ * `input` event will occur.
+ */
+ if (nativeEvent.which && !isKeypressCommand(nativeEvent)) {
+ return String.fromCharCode(nativeEvent.which);
+ }
+ return null;
+ case topLevelTypes.topCompositionEnd:
+ return useFallbackCompositionData ? null : nativeEvent.data;
+ default:
+ return null;
+ }
+}
+
+/**
+ * Extract a SyntheticInputEvent for `beforeInput`, based on either native
+ * `textInput` or fallback behavior.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?object} A SyntheticInputEvent.
+ */
+function extractBeforeInputEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var chars;
+
+ if (canUseTextInputEvent) {
+ chars = getNativeBeforeInputChars(topLevelType, nativeEvent);
+ } else {
+ chars = getFallbackBeforeInputChars(topLevelType, nativeEvent);
+ }
+
+ // If no characters are being inserted, no BeforeInput event should
+ // be fired.
+ if (!chars) {
+ return null;
+ }
+
+ var event = SyntheticInputEvent.getPooled(eventTypes.beforeInput, topLevelTargetID, nativeEvent, nativeEventTarget);
+
+ event.data = chars;
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+}
+
+/**
+ * Create an `onBeforeInput` event to match
+ * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.
+ *
+ * This event plugin is based on the native `textInput` event
+ * available in Chrome, Safari, Opera, and IE. This event fires after
+ * `onKeyPress` and `onCompositionEnd`, but before `onInput`.
+ *
+ * `beforeInput` is spec'd but not implemented in any browsers, and
+ * the `input` event does not provide any useful information about what has
+ * actually been added, contrary to the spec. Thus, `textInput` is the best
+ * available event to identify the characters that have actually been inserted
+ * into the target node.
+ *
+ * This plugin is also responsible for emitting `composition` events, thus
+ * allowing us to share composition fallback code for both `beforeInput` and
+ * `composition` event types.
+ */
+var BeforeInputEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ return [extractCompositionEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget), extractBeforeInputEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget)];
+ }
+};
+
+module.exports = BeforeInputEventPlugin;
+},{"103":103,"107":107,"147":147,"15":15,"166":166,"19":19,"20":20}],4:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSProperty
+ */
+
+'use strict';
+
+/**
+ * CSS properties which accept numbers but are not in units of "px".
+ */
+var isUnitlessNumber = {
+ animationIterationCount: true,
+ boxFlex: true,
+ boxFlexGroup: true,
+ boxOrdinalGroup: true,
+ columnCount: true,
+ flex: true,
+ flexGrow: true,
+ flexPositive: true,
+ flexShrink: true,
+ flexNegative: true,
+ flexOrder: true,
+ fontWeight: true,
+ lineClamp: true,
+ lineHeight: true,
+ opacity: true,
+ order: true,
+ orphans: true,
+ tabSize: true,
+ widows: true,
+ zIndex: true,
+ zoom: true,
+
+ // SVG-related properties
+ fillOpacity: true,
+ stopOpacity: true,
+ strokeDashoffset: true,
+ strokeOpacity: true,
+ strokeWidth: true
+};
+
+/**
+ * @param {string} prefix vendor-specific prefix, eg: Webkit
+ * @param {string} key style name, eg: transitionDuration
+ * @return {string} style name prefixed with `prefix`, properly camelCased, eg:
+ * WebkitTransitionDuration
+ */
+function prefixKey(prefix, key) {
+ return prefix + key.charAt(0).toUpperCase() + key.substring(1);
+}
+
+/**
+ * Support style names that may come passed in prefixed by adding permutations
+ * of vendor prefixes.
+ */
+var prefixes = ['Webkit', 'ms', 'Moz', 'O'];
+
+// Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an
+// infinite loop, because it iterates over the newly added props too.
+Object.keys(isUnitlessNumber).forEach(function (prop) {
+ prefixes.forEach(function (prefix) {
+ isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop];
+ });
+});
+
+/**
+ * Most style properties can be unset by doing .style[prop] = '' but IE8
+ * doesn't like doing that with shorthand properties so for the properties that
+ * IE8 breaks on, which are listed here, we instead unset each of the
+ * individual properties. See http://bugs.jquery.com/ticket/12385.
+ * The 4-value 'clock' properties like margin, padding, border-width seem to
+ * behave without any problems. Curiously, list-style works too without any
+ * special prodding.
+ */
+var shorthandPropertyExpansions = {
+ background: {
+ backgroundAttachment: true,
+ backgroundColor: true,
+ backgroundImage: true,
+ backgroundPositionX: true,
+ backgroundPositionY: true,
+ backgroundRepeat: true
+ },
+ backgroundPosition: {
+ backgroundPositionX: true,
+ backgroundPositionY: true
+ },
+ border: {
+ borderWidth: true,
+ borderStyle: true,
+ borderColor: true
+ },
+ borderBottom: {
+ borderBottomWidth: true,
+ borderBottomStyle: true,
+ borderBottomColor: true
+ },
+ borderLeft: {
+ borderLeftWidth: true,
+ borderLeftStyle: true,
+ borderLeftColor: true
+ },
+ borderRight: {
+ borderRightWidth: true,
+ borderRightStyle: true,
+ borderRightColor: true
+ },
+ borderTop: {
+ borderTopWidth: true,
+ borderTopStyle: true,
+ borderTopColor: true
+ },
+ font: {
+ fontStyle: true,
+ fontVariant: true,
+ fontWeight: true,
+ fontSize: true,
+ lineHeight: true,
+ fontFamily: true
+ },
+ outline: {
+ outlineWidth: true,
+ outlineStyle: true,
+ outlineColor: true
+ }
+};
+
+var CSSProperty = {
+ isUnitlessNumber: isUnitlessNumber,
+ shorthandPropertyExpansions: shorthandPropertyExpansions
+};
+
+module.exports = CSSProperty;
+},{}],5:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSPropertyOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var CSSProperty = _dereq_(4);
+var ExecutionEnvironment = _dereq_(147);
+var ReactPerf = _dereq_(78);
+
+var camelizeStyleName = _dereq_(149);
+var dangerousStyleValue = _dereq_(119);
+var hyphenateStyleName = _dereq_(160);
+var memoizeStringOnly = _dereq_(168);
+var warning = _dereq_(173);
+
+var processStyleName = memoizeStringOnly(function (styleName) {
+ return hyphenateStyleName(styleName);
+});
+
+var hasShorthandPropertyBug = false;
+var styleFloatAccessor = 'cssFloat';
+if (ExecutionEnvironment.canUseDOM) {
+ var tempStyle = document.createElementNS('http://www.w3.org/1999/xhtml', 'div').style;
+ try {
+ // IE8 throws "Invalid argument." if resetting shorthand style properties.
+ tempStyle.font = '';
+ } catch (e) {
+ hasShorthandPropertyBug = true;
+ }
+ // IE8 only supports accessing cssFloat (standard) as styleFloat
+ if (document.documentElement.style.cssFloat === undefined) {
+ styleFloatAccessor = 'styleFloat';
+ }
+}
+
+if ("development" !== 'production') {
+ // 'msTransform' is correct, but the other prefixes should be capitalized
+ var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/;
+
+ // style values shouldn't contain a semicolon
+ var badStyleValueWithSemicolonPattern = /;\s*$/;
+
+ var warnedStyleNames = {};
+ var warnedStyleValues = {};
+
+ var warnHyphenatedStyleName = function (name) {
+ if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
+ return;
+ }
+
+ warnedStyleNames[name] = true;
+ "development" !== 'production' ? warning(false, 'Unsupported style property %s. Did you mean %s?', name, camelizeStyleName(name)) : undefined;
+ };
+
+ var warnBadVendoredStyleName = function (name) {
+ if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
+ return;
+ }
+
+ warnedStyleNames[name] = true;
+ "development" !== 'production' ? warning(false, 'Unsupported vendor-prefixed style property %s. Did you mean %s?', name, name.charAt(0).toUpperCase() + name.slice(1)) : undefined;
+ };
+
+ var warnStyleValueWithSemicolon = function (name, value) {
+ if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) {
+ return;
+ }
+
+ warnedStyleValues[value] = true;
+ "development" !== 'production' ? warning(false, 'Style property values shouldn\'t contain a semicolon. ' + 'Try "%s: %s" instead.', name, value.replace(badStyleValueWithSemicolonPattern, '')) : undefined;
+ };
+
+ /**
+ * @param {string} name
+ * @param {*} value
+ */
+ var warnValidStyle = function (name, value) {
+ if (name.indexOf('-') > -1) {
+ warnHyphenatedStyleName(name);
+ } else if (badVendoredStyleNamePattern.test(name)) {
+ warnBadVendoredStyleName(name);
+ } else if (badStyleValueWithSemicolonPattern.test(value)) {
+ warnStyleValueWithSemicolon(name, value);
+ }
+ };
+}
+
+/**
+ * Operations for dealing with CSS properties.
+ */
+var CSSPropertyOperations = {
+
+ /**
+ * Serializes a mapping of style properties for use as inline styles:
+ *
+ * > createMarkupForStyles({width: '200px', height: 0})
+ * "width:200px;height:0;"
+ *
+ * Undefined values are ignored so that declarative programming is easier.
+ * The result should be HTML-escaped before insertion into the DOM.
+ *
+ * @param {object} styles
+ * @return {?string}
+ */
+ createMarkupForStyles: function (styles) {
+ var serialized = '';
+ for (var styleName in styles) {
+ if (!styles.hasOwnProperty(styleName)) {
+ continue;
+ }
+ var styleValue = styles[styleName];
+ if ("development" !== 'production') {
+ warnValidStyle(styleName, styleValue);
+ }
+ if (styleValue != null) {
+ serialized += processStyleName(styleName) + ':';
+ serialized += dangerousStyleValue(styleName, styleValue) + ';';
+ }
+ }
+ return serialized || null;
+ },
+
+ /**
+ * Sets the value for multiple styles on a node. If a value is specified as
+ * '' (empty string), the corresponding style property will be unset.
+ *
+ * @param {DOMElement} node
+ * @param {object} styles
+ */
+ setValueForStyles: function (node, styles) {
+ var style = node.style;
+ for (var styleName in styles) {
+ if (!styles.hasOwnProperty(styleName)) {
+ continue;
+ }
+ if ("development" !== 'production') {
+ warnValidStyle(styleName, styles[styleName]);
+ }
+ var styleValue = dangerousStyleValue(styleName, styles[styleName]);
+ if (styleName === 'float') {
+ styleName = styleFloatAccessor;
+ }
+ if (styleValue) {
+ style[styleName] = styleValue;
+ } else {
+ var expansion = hasShorthandPropertyBug && CSSProperty.shorthandPropertyExpansions[styleName];
+ if (expansion) {
+ // Shorthand property that IE8 won't like unsetting, so unset each
+ // component to placate it
+ for (var individualStyleName in expansion) {
+ style[individualStyleName] = '';
+ }
+ } else {
+ style[styleName] = '';
+ }
+ }
+ }
+ }
+
+};
+
+ReactPerf.measureMethods(CSSPropertyOperations, 'CSSPropertyOperations', {
+ setValueForStyles: 'setValueForStyles'
+});
+
+module.exports = CSSPropertyOperations;
+},{"119":119,"147":147,"149":149,"160":160,"168":168,"173":173,"4":4,"78":78}],6:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CallbackQueue
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+/**
+ * A specialized pseudo-event module to help keep track of components waiting to
+ * be notified when their DOM representations are available for use.
+ *
+ * This implements `PooledClass`, so you should never need to instantiate this.
+ * Instead, use `CallbackQueue.getPooled()`.
+ *
+ * @class ReactMountReady
+ * @implements PooledClass
+ * @internal
+ */
+function CallbackQueue() {
+ this._callbacks = null;
+ this._contexts = null;
+}
+
+assign(CallbackQueue.prototype, {
+
+ /**
+ * Enqueues a callback to be invoked when `notifyAll` is invoked.
+ *
+ * @param {function} callback Invoked when `notifyAll` is invoked.
+ * @param {?object} context Context to call `callback` with.
+ * @internal
+ */
+ enqueue: function (callback, context) {
+ this._callbacks = this._callbacks || [];
+ this._contexts = this._contexts || [];
+ this._callbacks.push(callback);
+ this._contexts.push(context);
+ },
+
+ /**
+ * Invokes all enqueued callbacks and clears the queue. This is invoked after
+ * the DOM representation of a component has been created or updated.
+ *
+ * @internal
+ */
+ notifyAll: function () {
+ var callbacks = this._callbacks;
+ var contexts = this._contexts;
+ if (callbacks) {
+ !(callbacks.length === contexts.length) ? "development" !== 'production' ? invariant(false, 'Mismatched list of contexts in callback queue') : invariant(false) : undefined;
+ this._callbacks = null;
+ this._contexts = null;
+ for (var i = 0; i < callbacks.length; i++) {
+ callbacks[i].call(contexts[i]);
+ }
+ callbacks.length = 0;
+ contexts.length = 0;
+ }
+ },
+
+ /**
+ * Resets the internal queue.
+ *
+ * @internal
+ */
+ reset: function () {
+ this._callbacks = null;
+ this._contexts = null;
+ },
+
+ /**
+ * `PooledClass` looks for this.
+ */
+ destructor: function () {
+ this.reset();
+ }
+
+});
+
+PooledClass.addPoolingTo(CallbackQueue);
+
+module.exports = CallbackQueue;
+},{"161":161,"24":24,"25":25}],7:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ChangeEventPlugin
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+var EventPropagators = _dereq_(19);
+var ExecutionEnvironment = _dereq_(147);
+var ReactUpdates = _dereq_(96);
+var SyntheticEvent = _dereq_(105);
+
+var getEventTarget = _dereq_(128);
+var isEventSupported = _dereq_(133);
+var isTextInputElement = _dereq_(134);
+var keyOf = _dereq_(166);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+var eventTypes = {
+ change: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onChange: null }),
+ captured: keyOf({ onChangeCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topChange, topLevelTypes.topClick, topLevelTypes.topFocus, topLevelTypes.topInput, topLevelTypes.topKeyDown, topLevelTypes.topKeyUp, topLevelTypes.topSelectionChange]
+ }
+};
+
+/**
+ * For IE shims
+ */
+var activeElement = null;
+var activeElementID = null;
+var activeElementValue = null;
+var activeElementValueProp = null;
+
+/**
+ * SECTION: handle `change` event
+ */
+function shouldUseChangeEvent(elem) {
+ var nodeName = elem.nodeName && elem.nodeName.toLowerCase();
+ return nodeName === 'select' || nodeName === 'input' && elem.type === 'file';
+}
+
+var doesChangeEventBubble = false;
+if (ExecutionEnvironment.canUseDOM) {
+ // See `handleChange` comment below
+ doesChangeEventBubble = isEventSupported('change') && (!('documentMode' in document) || document.documentMode > 8);
+}
+
+function manualDispatchChangeEvent(nativeEvent) {
+ var event = SyntheticEvent.getPooled(eventTypes.change, activeElementID, nativeEvent, getEventTarget(nativeEvent));
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+
+ // If change and propertychange bubbled, we'd just bind to it like all the
+ // other events and have it go through ReactBrowserEventEmitter. Since it
+ // doesn't, we manually listen for the events and so we have to enqueue and
+ // process the abstract event manually.
+ //
+ // Batching is necessary here in order to ensure that all event handlers run
+ // before the next rerender (including event handlers attached to ancestor
+ // elements instead of directly on the input). Without this, controlled
+ // components don't work properly in conjunction with event bubbling because
+ // the component is rerendered and the value reverted before all the event
+ // handlers can run. See https://github.com/facebook/react/issues/708.
+ ReactUpdates.batchedUpdates(runEventInBatch, event);
+}
+
+function runEventInBatch(event) {
+ EventPluginHub.enqueueEvents(event);
+ EventPluginHub.processEventQueue(false);
+}
+
+function startWatchingForChangeEventIE8(target, targetID) {
+ activeElement = target;
+ activeElementID = targetID;
+ activeElement.attachEvent('onchange', manualDispatchChangeEvent);
+}
+
+function stopWatchingForChangeEventIE8() {
+ if (!activeElement) {
+ return;
+ }
+ activeElement.detachEvent('onchange', manualDispatchChangeEvent);
+ activeElement = null;
+ activeElementID = null;
+}
+
+function getTargetIDForChangeEvent(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topChange) {
+ return topLevelTargetID;
+ }
+}
+function handleEventsForChangeEventIE8(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topFocus) {
+ // stopWatching() should be a noop here but we call it just in case we
+ // missed a blur event somehow.
+ stopWatchingForChangeEventIE8();
+ startWatchingForChangeEventIE8(topLevelTarget, topLevelTargetID);
+ } else if (topLevelType === topLevelTypes.topBlur) {
+ stopWatchingForChangeEventIE8();
+ }
+}
+
+/**
+ * SECTION: handle `input` event
+ */
+var isInputEventSupported = false;
+if (ExecutionEnvironment.canUseDOM) {
+ // IE9 claims to support the input event but fails to trigger it when
+ // deleting text, so we ignore its input events
+ isInputEventSupported = isEventSupported('input') && (!('documentMode' in document) || document.documentMode > 9);
+}
+
+/**
+ * (For old IE.) Replacement getter/setter for the `value` property that gets
+ * set on the active element.
+ */
+var newValueProp = {
+ get: function () {
+ return activeElementValueProp.get.call(this);
+ },
+ set: function (val) {
+ // Cast to a string so we can do equality checks.
+ activeElementValue = '' + val;
+ activeElementValueProp.set.call(this, val);
+ }
+};
+
+/**
+ * (For old IE.) Starts tracking propertychange events on the passed-in element
+ * and override the value property so that we can distinguish user events from
+ * value changes in JS.
+ */
+function startWatchingForValueChange(target, targetID) {
+ activeElement = target;
+ activeElementID = targetID;
+ activeElementValue = target.value;
+ activeElementValueProp = Object.getOwnPropertyDescriptor(target.constructor.prototype, 'value');
+
+ // Not guarded in a canDefineProperty check: IE8 supports defineProperty only
+ // on DOM elements
+ Object.defineProperty(activeElement, 'value', newValueProp);
+ activeElement.attachEvent('onpropertychange', handlePropertyChange);
+}
+
+/**
+ * (For old IE.) Removes the event listeners from the currently-tracked element,
+ * if any exists.
+ */
+function stopWatchingForValueChange() {
+ if (!activeElement) {
+ return;
+ }
+
+ // delete restores the original property definition
+ delete activeElement.value;
+ activeElement.detachEvent('onpropertychange', handlePropertyChange);
+
+ activeElement = null;
+ activeElementID = null;
+ activeElementValue = null;
+ activeElementValueProp = null;
+}
+
+/**
+ * (For old IE.) Handles a propertychange event, sending a `change` event if
+ * the value of the active element has changed.
+ */
+function handlePropertyChange(nativeEvent) {
+ if (nativeEvent.propertyName !== 'value') {
+ return;
+ }
+ var value = nativeEvent.srcElement.value;
+ if (value === activeElementValue) {
+ return;
+ }
+ activeElementValue = value;
+
+ manualDispatchChangeEvent(nativeEvent);
+}
+
+/**
+ * If a `change` event should be fired, returns the target's ID.
+ */
+function getTargetIDForInputEvent(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topInput) {
+ // In modern browsers (i.e., not IE8 or IE9), the input event is exactly
+ // what we want so fall through here and trigger an abstract event
+ return topLevelTargetID;
+ }
+}
+
+// For IE8 and IE9.
+function handleEventsForInputEventIE(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topFocus) {
+ // In IE8, we can capture almost all .value changes by adding a
+ // propertychange handler and looking for events with propertyName
+ // equal to 'value'
+ // In IE9, propertychange fires for most input events but is buggy and
+ // doesn't fire when text is deleted, but conveniently, selectionchange
+ // appears to fire in all of the remaining cases so we catch those and
+ // forward the event if the value has changed
+ // In either case, we don't want to call the event handler if the value
+ // is changed from JS so we redefine a setter for `.value` that updates
+ // our activeElementValue variable, allowing us to ignore those changes
+ //
+ // stopWatching() should be a noop here but we call it just in case we
+ // missed a blur event somehow.
+ stopWatchingForValueChange();
+ startWatchingForValueChange(topLevelTarget, topLevelTargetID);
+ } else if (topLevelType === topLevelTypes.topBlur) {
+ stopWatchingForValueChange();
+ }
+}
+
+// For IE8 and IE9.
+function getTargetIDForInputEventIE(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topSelectionChange || topLevelType === topLevelTypes.topKeyUp || topLevelType === topLevelTypes.topKeyDown) {
+ // On the selectionchange event, the target is just document which isn't
+ // helpful for us so just check activeElement instead.
+ //
+ // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
+ // propertychange on the first input event after setting `value` from a
+ // script and fires only keydown, keypress, keyup. Catching keyup usually
+ // gets it and catching keydown lets us fire an event for the first
+ // keystroke if user does a key repeat (it'll be a little delayed: right
+ // before the second keystroke). Other input methods (e.g., paste) seem to
+ // fire selectionchange normally.
+ if (activeElement && activeElement.value !== activeElementValue) {
+ activeElementValue = activeElement.value;
+ return activeElementID;
+ }
+ }
+}
+
+/**
+ * SECTION: handle `click` event
+ */
+function shouldUseClickEvent(elem) {
+ // Use the `click` event to detect changes to checkbox and radio inputs.
+ // This approach works across all browsers, whereas `change` does not fire
+ // until `blur` in IE8.
+ return elem.nodeName && elem.nodeName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio');
+}
+
+function getTargetIDForClickEvent(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topClick) {
+ return topLevelTargetID;
+ }
+}
+
+/**
+ * This plugin creates an `onChange` event that normalizes change events
+ * across form elements. This event fires at a time when it's possible to
+ * change the element's value without seeing a flicker.
+ *
+ * Supported elements are:
+ * - input (see `isTextInputElement`)
+ * - textarea
+ * - select
+ */
+var ChangeEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+
+ var getTargetIDFunc, handleEventFunc;
+ if (shouldUseChangeEvent(topLevelTarget)) {
+ if (doesChangeEventBubble) {
+ getTargetIDFunc = getTargetIDForChangeEvent;
+ } else {
+ handleEventFunc = handleEventsForChangeEventIE8;
+ }
+ } else if (isTextInputElement(topLevelTarget)) {
+ if (isInputEventSupported) {
+ getTargetIDFunc = getTargetIDForInputEvent;
+ } else {
+ getTargetIDFunc = getTargetIDForInputEventIE;
+ handleEventFunc = handleEventsForInputEventIE;
+ }
+ } else if (shouldUseClickEvent(topLevelTarget)) {
+ getTargetIDFunc = getTargetIDForClickEvent;
+ }
+
+ if (getTargetIDFunc) {
+ var targetID = getTargetIDFunc(topLevelType, topLevelTarget, topLevelTargetID);
+ if (targetID) {
+ var event = SyntheticEvent.getPooled(eventTypes.change, targetID, nativeEvent, nativeEventTarget);
+ event.type = 'change';
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+ }
+ }
+
+ if (handleEventFunc) {
+ handleEventFunc(topLevelType, topLevelTarget, topLevelTargetID);
+ }
+ }
+
+};
+
+module.exports = ChangeEventPlugin;
+},{"105":105,"128":128,"133":133,"134":134,"147":147,"15":15,"16":16,"166":166,"19":19,"96":96}],8:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ClientReactRootIndex
+ * @typechecks
+ */
+
+'use strict';
+
+var nextReactRootIndex = 0;
+
+var ClientReactRootIndex = {
+ createReactRootIndex: function () {
+ return nextReactRootIndex++;
+ }
+};
+
+module.exports = ClientReactRootIndex;
+},{}],9:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMChildrenOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var Danger = _dereq_(12);
+var ReactMultiChildUpdateTypes = _dereq_(74);
+var ReactPerf = _dereq_(78);
+
+var setInnerHTML = _dereq_(138);
+var setTextContent = _dereq_(139);
+var invariant = _dereq_(161);
+
+/**
+ * Inserts `childNode` as a child of `parentNode` at the `index`.
+ *
+ * @param {DOMElement} parentNode Parent node in which to insert.
+ * @param {DOMElement} childNode Child node to insert.
+ * @param {number} index Index at which to insert the child.
+ * @internal
+ */
+function insertChildAt(parentNode, childNode, index) {
+ // By exploiting arrays returning `undefined` for an undefined index, we can
+ // rely exclusively on `insertBefore(node, null)` instead of also using
+ // `appendChild(node)`. However, using `undefined` is not allowed by all
+ // browsers so we must replace it with `null`.
+
+ // fix render order error in safari
+ // IE8 will throw error when index out of list size.
+ var beforeChild = index >= parentNode.childNodes.length ? null : parentNode.childNodes.item(index);
+
+ parentNode.insertBefore(childNode, beforeChild);
+}
+
+/**
+ * Operations for updating with DOM children.
+ */
+var DOMChildrenOperations = {
+
+ dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup,
+
+ updateTextContent: setTextContent,
+
+ /**
+ * Updates a component's children by processing a series of updates. The
+ * update configurations are each expected to have a `parentNode` property.
+ *
+ * @param {array<object>} updates List of update configurations.
+ * @param {array<string>} markupList List of markup strings.
+ * @internal
+ */
+ processUpdates: function (updates, markupList) {
+ var update;
+ // Mapping from parent IDs to initial child orderings.
+ var initialChildren = null;
+ // List of children that will be moved or removed.
+ var updatedChildren = null;
+
+ for (var i = 0; i < updates.length; i++) {
+ update = updates[i];
+ if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING || update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) {
+ var updatedIndex = update.fromIndex;
+ var updatedChild = update.parentNode.childNodes[updatedIndex];
+ var parentID = update.parentID;
+
+ !updatedChild ? "development" !== 'production' ? invariant(false, 'processUpdates(): Unable to find child %s of element. This ' + 'probably means the DOM was unexpectedly mutated (e.g., by the ' + 'browser), usually due to forgetting a <tbody> when using tables, ' + 'nesting tags like <form>, <p>, or <a>, or using non-SVG elements ' + 'in an <svg> parent. Try inspecting the child nodes of the element ' + 'with React ID `%s`.', updatedIndex, parentID) : invariant(false) : undefined;
+
+ initialChildren = initialChildren || {};
+ initialChildren[parentID] = initialChildren[parentID] || [];
+ initialChildren[parentID][updatedIndex] = updatedChild;
+
+ updatedChildren = updatedChildren || [];
+ updatedChildren.push(updatedChild);
+ }
+ }
+
+ var renderedMarkup;
+ // markupList is either a list of markup or just a list of elements
+ if (markupList.length && typeof markupList[0] === 'string') {
+ renderedMarkup = Danger.dangerouslyRenderMarkup(markupList);
+ } else {
+ renderedMarkup = markupList;
+ }
+
+ // Remove updated children first so that `toIndex` is consistent.
+ if (updatedChildren) {
+ for (var j = 0; j < updatedChildren.length; j++) {
+ updatedChildren[j].parentNode.removeChild(updatedChildren[j]);
+ }
+ }
+
+ for (var k = 0; k < updates.length; k++) {
+ update = updates[k];
+ switch (update.type) {
+ case ReactMultiChildUpdateTypes.INSERT_MARKUP:
+ insertChildAt(update.parentNode, renderedMarkup[update.markupIndex], update.toIndex);
+ break;
+ case ReactMultiChildUpdateTypes.MOVE_EXISTING:
+ insertChildAt(update.parentNode, initialChildren[update.parentID][update.fromIndex], update.toIndex);
+ break;
+ case ReactMultiChildUpdateTypes.SET_MARKUP:
+ setInnerHTML(update.parentNode, update.content);
+ break;
+ case ReactMultiChildUpdateTypes.TEXT_CONTENT:
+ setTextContent(update.parentNode, update.content);
+ break;
+ case ReactMultiChildUpdateTypes.REMOVE_NODE:
+ // Already removed by the for-loop above.
+ break;
+ }
+ }
+ }
+
+};
+
+ReactPerf.measureMethods(DOMChildrenOperations, 'DOMChildrenOperations', {
+ updateTextContent: 'updateTextContent'
+});
+
+module.exports = DOMChildrenOperations;
+},{"12":12,"138":138,"139":139,"161":161,"74":74,"78":78}],10:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMProperty
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+function checkMask(value, bitmask) {
+ return (value & bitmask) === bitmask;
+}
+
+var DOMPropertyInjection = {
+ /**
+ * Mapping from normalized, camelcased property names to a configuration that
+ * specifies how the associated DOM property should be accessed or rendered.
+ */
+ MUST_USE_ATTRIBUTE: 0x1,
+ MUST_USE_PROPERTY: 0x2,
+ HAS_SIDE_EFFECTS: 0x4,
+ HAS_BOOLEAN_VALUE: 0x8,
+ HAS_NUMERIC_VALUE: 0x10,
+ HAS_POSITIVE_NUMERIC_VALUE: 0x20 | 0x10,
+ HAS_OVERLOADED_BOOLEAN_VALUE: 0x40,
+
+ /**
+ * Inject some specialized knowledge about the DOM. This takes a config object
+ * with the following properties:
+ *
+ * isCustomAttribute: function that given an attribute name will return true
+ * if it can be inserted into the DOM verbatim. Useful for data-* or aria-*
+ * attributes where it's impossible to enumerate all of the possible
+ * attribute names,
+ *
+ * Properties: object mapping DOM property name to one of the
+ * DOMPropertyInjection constants or null. If your attribute isn't in here,
+ * it won't get written to the DOM.
+ *
+ * DOMAttributeNames: object mapping React attribute name to the DOM
+ * attribute name. Attribute names not specified use the **lowercase**
+ * normalized name.
+ *
+ * DOMAttributeNamespaces: object mapping React attribute name to the DOM
+ * attribute namespace URL. (Attribute names not specified use no namespace.)
+ *
+ * DOMPropertyNames: similar to DOMAttributeNames but for DOM properties.
+ * Property names not specified use the normalized name.
+ *
+ * DOMMutationMethods: Properties that require special mutation methods. If
+ * `value` is undefined, the mutation method should unset the property.
+ *
+ * @param {object} domPropertyConfig the config as described above.
+ */
+ injectDOMPropertyConfig: function (domPropertyConfig) {
+ var Injection = DOMPropertyInjection;
+ var Properties = domPropertyConfig.Properties || {};
+ var DOMAttributeNamespaces = domPropertyConfig.DOMAttributeNamespaces || {};
+ var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {};
+ var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {};
+ var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {};
+
+ if (domPropertyConfig.isCustomAttribute) {
+ DOMProperty._isCustomAttributeFunctions.push(domPropertyConfig.isCustomAttribute);
+ }
+
+ for (var propName in Properties) {
+ !!DOMProperty.properties.hasOwnProperty(propName) ? "development" !== 'production' ? invariant(false, 'injectDOMPropertyConfig(...): You\'re trying to inject DOM property ' + '\'%s\' which has already been injected. You may be accidentally ' + 'injecting the same DOM property config twice, or you may be ' + 'injecting two configs that have conflicting property names.', propName) : invariant(false) : undefined;
+
+ var lowerCased = propName.toLowerCase();
+ var propConfig = Properties[propName];
+
+ var propertyInfo = {
+ attributeName: lowerCased,
+ attributeNamespace: null,
+ propertyName: propName,
+ mutationMethod: null,
+
+ mustUseAttribute: checkMask(propConfig, Injection.MUST_USE_ATTRIBUTE),
+ mustUseProperty: checkMask(propConfig, Injection.MUST_USE_PROPERTY),
+ hasSideEffects: checkMask(propConfig, Injection.HAS_SIDE_EFFECTS),
+ hasBooleanValue: checkMask(propConfig, Injection.HAS_BOOLEAN_VALUE),
+ hasNumericValue: checkMask(propConfig, Injection.HAS_NUMERIC_VALUE),
+ hasPositiveNumericValue: checkMask(propConfig, Injection.HAS_POSITIVE_NUMERIC_VALUE),
+ hasOverloadedBooleanValue: checkMask(propConfig, Injection.HAS_OVERLOADED_BOOLEAN_VALUE)
+ };
+
+ !(!propertyInfo.mustUseAttribute || !propertyInfo.mustUseProperty) ? "development" !== 'production' ? invariant(false, 'DOMProperty: Cannot require using both attribute and property: %s', propName) : invariant(false) : undefined;
+ !(propertyInfo.mustUseProperty || !propertyInfo.hasSideEffects) ? "development" !== 'production' ? invariant(false, 'DOMProperty: Properties that have side effects must use property: %s', propName) : invariant(false) : undefined;
+ !(propertyInfo.hasBooleanValue + propertyInfo.hasNumericValue + propertyInfo.hasOverloadedBooleanValue <= 1) ? "development" !== 'production' ? invariant(false, 'DOMProperty: Value can be one of boolean, overloaded boolean, or ' + 'numeric value, but not a combination: %s', propName) : invariant(false) : undefined;
+
+ if ("development" !== 'production') {
+ DOMProperty.getPossibleStandardName[lowerCased] = propName;
+ }
+
+ if (DOMAttributeNames.hasOwnProperty(propName)) {
+ var attributeName = DOMAttributeNames[propName];
+ propertyInfo.attributeName = attributeName;
+ if ("development" !== 'production') {
+ DOMProperty.getPossibleStandardName[attributeName] = propName;
+ }
+ }
+
+ if (DOMAttributeNamespaces.hasOwnProperty(propName)) {
+ propertyInfo.attributeNamespace = DOMAttributeNamespaces[propName];
+ }
+
+ if (DOMPropertyNames.hasOwnProperty(propName)) {
+ propertyInfo.propertyName = DOMPropertyNames[propName];
+ }
+
+ if (DOMMutationMethods.hasOwnProperty(propName)) {
+ propertyInfo.mutationMethod = DOMMutationMethods[propName];
+ }
+
+ DOMProperty.properties[propName] = propertyInfo;
+ }
+ }
+};
+var defaultValueCache = {};
+
+/**
+ * DOMProperty exports lookup objects that can be used like functions:
+ *
+ * > DOMProperty.isValid['id']
+ * true
+ * > DOMProperty.isValid['foobar']
+ * undefined
+ *
+ * Although this may be confusing, it performs better in general.
+ *
+ * @see http://jsperf.com/key-exists
+ * @see http://jsperf.com/key-missing
+ */
+var DOMProperty = {
+
+ ID_ATTRIBUTE_NAME: 'data-reactid',
+
+ /**
+ * Map from property "standard name" to an object with info about how to set
+ * the property in the DOM. Each object contains:
+ *
+ * attributeName:
+ * Used when rendering markup or with `*Attribute()`.
+ * attributeNamespace
+ * propertyName:
+ * Used on DOM node instances. (This includes properties that mutate due to
+ * external factors.)
+ * mutationMethod:
+ * If non-null, used instead of the property or `setAttribute()` after
+ * initial render.
+ * mustUseAttribute:
+ * Whether the property must be accessed and mutated using `*Attribute()`.
+ * (This includes anything that fails `<propName> in <element>`.)
+ * mustUseProperty:
+ * Whether the property must be accessed and mutated as an object property.
+ * hasSideEffects:
+ * Whether or not setting a value causes side effects such as triggering
+ * resources to be loaded or text selection changes. If true, we read from
+ * the DOM before updating to ensure that the value is only set if it has
+ * changed.
+ * hasBooleanValue:
+ * Whether the property should be removed when set to a falsey value.
+ * hasNumericValue:
+ * Whether the property must be numeric or parse as a numeric and should be
+ * removed when set to a falsey value.
+ * hasPositiveNumericValue:
+ * Whether the property must be positive numeric or parse as a positive
+ * numeric and should be removed when set to a falsey value.
+ * hasOverloadedBooleanValue:
+ * Whether the property can be used as a flag as well as with a value.
+ * Removed when strictly equal to false; present without a value when
+ * strictly equal to true; present with a value otherwise.
+ */
+ properties: {},
+
+ /**
+ * Mapping from lowercase property names to the properly cased version, used
+ * to warn in the case of missing properties. Available only in __DEV__.
+ * @type {Object}
+ */
+ getPossibleStandardName: "development" !== 'production' ? {} : null,
+
+ /**
+ * All of the isCustomAttribute() functions that have been injected.
+ */
+ _isCustomAttributeFunctions: [],
+
+ /**
+ * Checks whether a property name is a custom attribute.
+ * @method
+ */
+ isCustomAttribute: function (attributeName) {
+ for (var i = 0; i < DOMProperty._isCustomAttributeFunctions.length; i++) {
+ var isCustomAttributeFn = DOMProperty._isCustomAttributeFunctions[i];
+ if (isCustomAttributeFn(attributeName)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Returns the default property value for a DOM property (i.e., not an
+ * attribute). Most default values are '' or false, but not all. Worse yet,
+ * some (in particular, `type`) vary depending on the type of element.
+ *
+ * TODO: Is it better to grab all the possible properties when creating an
+ * element to avoid having to create the same element twice?
+ */
+ getDefaultValueForProperty: function (nodeName, prop) {
+ var nodeDefaults = defaultValueCache[nodeName];
+ var testElement;
+ if (!nodeDefaults) {
+ defaultValueCache[nodeName] = nodeDefaults = {};
+ }
+ if (!(prop in nodeDefaults)) {
+ testElement = document.createElementNS('http://www.w3.org/1999/xhtml', nodeName);
+ nodeDefaults[prop] = testElement[prop];
+ }
+ return nodeDefaults[prop];
+ },
+
+ injection: DOMPropertyInjection
+};
+
+module.exports = DOMProperty;
+},{"161":161}],11:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMPropertyOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ReactPerf = _dereq_(78);
+
+var quoteAttributeValueForBrowser = _dereq_(136);
+var warning = _dereq_(173);
+
+// Simplified subset
+var VALID_ATTRIBUTE_NAME_REGEX = /^[a-zA-Z_][\w\.\-]*$/;
+var illegalAttributeNameCache = {};
+var validatedAttributeNameCache = {};
+
+function isAttributeNameSafe(attributeName) {
+ if (validatedAttributeNameCache.hasOwnProperty(attributeName)) {
+ return true;
+ }
+ if (illegalAttributeNameCache.hasOwnProperty(attributeName)) {
+ return false;
+ }
+ if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) {
+ validatedAttributeNameCache[attributeName] = true;
+ return true;
+ }
+ illegalAttributeNameCache[attributeName] = true;
+ "development" !== 'production' ? warning(false, 'Invalid attribute name: `%s`', attributeName) : undefined;
+ return false;
+}
+
+function shouldIgnoreValue(propertyInfo, value) {
+ return value == null || propertyInfo.hasBooleanValue && !value || propertyInfo.hasNumericValue && isNaN(value) || propertyInfo.hasPositiveNumericValue && value < 1 || propertyInfo.hasOverloadedBooleanValue && value === false;
+}
+
+if ("development" !== 'production') {
+ var reactProps = {
+ children: true,
+ dangerouslySetInnerHTML: true,
+ key: true,
+ ref: true
+ };
+ var warnedProperties = {};
+
+ var warnUnknownProperty = function (name) {
+ if (reactProps.hasOwnProperty(name) && reactProps[name] || warnedProperties.hasOwnProperty(name) && warnedProperties[name]) {
+ return;
+ }
+
+ warnedProperties[name] = true;
+ var lowerCasedName = name.toLowerCase();
+
+ // data-* attributes should be lowercase; suggest the lowercase version
+ var standardName = DOMProperty.isCustomAttribute(lowerCasedName) ? lowerCasedName : DOMProperty.getPossibleStandardName.hasOwnProperty(lowerCasedName) ? DOMProperty.getPossibleStandardName[lowerCasedName] : null;
+
+ // For now, only warn when we have a suggested correction. This prevents
+ // logging too much when using transferPropsTo.
+ "development" !== 'production' ? warning(standardName == null, 'Unknown DOM property %s. Did you mean %s?', name, standardName) : undefined;
+ };
+}
+
+/**
+ * Operations for dealing with DOM properties.
+ */
+var DOMPropertyOperations = {
+
+ /**
+ * Creates markup for the ID property.
+ *
+ * @param {string} id Unescaped ID.
+ * @return {string} Markup string.
+ */
+ createMarkupForID: function (id) {
+ return DOMProperty.ID_ATTRIBUTE_NAME + '=' + quoteAttributeValueForBrowser(id);
+ },
+
+ setAttributeForID: function (node, id) {
+ node.setAttribute(DOMProperty.ID_ATTRIBUTE_NAME, id);
+ },
+
+ /**
+ * Creates markup for a property.
+ *
+ * @param {string} name
+ * @param {*} value
+ * @return {?string} Markup string, or null if the property was invalid.
+ */
+ createMarkupForProperty: function (name, value) {
+ var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null;
+ if (propertyInfo) {
+ if (shouldIgnoreValue(propertyInfo, value)) {
+ return '';
+ }
+ var attributeName = propertyInfo.attributeName;
+ if (propertyInfo.hasBooleanValue || propertyInfo.hasOverloadedBooleanValue && value === true) {
+ return attributeName + '=""';
+ }
+ return attributeName + '=' + quoteAttributeValueForBrowser(value);
+ } else if (DOMProperty.isCustomAttribute(name)) {
+ if (value == null) {
+ return '';
+ }
+ return name + '=' + quoteAttributeValueForBrowser(value);
+ } else if ("development" !== 'production') {
+ warnUnknownProperty(name);
+ }
+ return null;
+ },
+
+ /**
+ * Creates markup for a custom property.
+ *
+ * @param {string} name
+ * @param {*} value
+ * @return {string} Markup string, or empty string if the property was invalid.
+ */
+ createMarkupForCustomAttribute: function (name, value) {
+ if (!isAttributeNameSafe(name) || value == null) {
+ return '';
+ }
+ return name + '=' + quoteAttributeValueForBrowser(value);
+ },
+
+ /**
+ * Sets the value for a property on a node.
+ *
+ * @param {DOMElement} node
+ * @param {string} name
+ * @param {*} value
+ */
+ setValueForProperty: function (node, name, value) {
+ var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null;
+ if (propertyInfo) {
+ var mutationMethod = propertyInfo.mutationMethod;
+ if (mutationMethod) {
+ mutationMethod(node, value);
+ } else if (shouldIgnoreValue(propertyInfo, value)) {
+ this.deleteValueForProperty(node, name);
+ } else if (propertyInfo.mustUseAttribute) {
+ var attributeName = propertyInfo.attributeName;
+ var namespace = propertyInfo.attributeNamespace;
+ // `setAttribute` with objects becomes only `[object]` in IE8/9,
+ // ('' + value) makes it output the correct toString()-value.
+ if (namespace) {
+ node.setAttributeNS(namespace, attributeName, '' + value);
+ } else if (propertyInfo.hasBooleanValue || propertyInfo.hasOverloadedBooleanValue && value === true) {
+ node.setAttribute(attributeName, '');
+ } else {
+ node.setAttribute(attributeName, '' + value);
+ }
+ } else {
+ var propName = propertyInfo.propertyName;
+ // Must explicitly cast values for HAS_SIDE_EFFECTS-properties to the
+ // property type before comparing; only `value` does and is string.
+ if (!propertyInfo.hasSideEffects || '' + node[propName] !== '' + value) {
+ // Contrary to `setAttribute`, object properties are properly
+ // `toString`ed by IE8/9.
+ node[propName] = value;
+ }
+ }
+ } else if (DOMProperty.isCustomAttribute(name)) {
+ DOMPropertyOperations.setValueForAttribute(node, name, value);
+ } else if ("development" !== 'production') {
+ warnUnknownProperty(name);
+ }
+ },
+
+ setValueForAttribute: function (node, name, value) {
+ if (!isAttributeNameSafe(name)) {
+ return;
+ }
+ if (value == null) {
+ node.removeAttribute(name);
+ } else {
+ node.setAttribute(name, '' + value);
+ }
+ },
+
+ /**
+ * Deletes the value for a property on a node.
+ *
+ * @param {DOMElement} node
+ * @param {string} name
+ */
+ deleteValueForProperty: function (node, name) {
+ var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null;
+ if (propertyInfo) {
+ var mutationMethod = propertyInfo.mutationMethod;
+ if (mutationMethod) {
+ mutationMethod(node, undefined);
+ } else if (propertyInfo.mustUseAttribute) {
+ node.removeAttribute(propertyInfo.attributeName);
+ } else {
+ var propName = propertyInfo.propertyName;
+ var defaultValue = DOMProperty.getDefaultValueForProperty(node.nodeName, propName);
+ if (!propertyInfo.hasSideEffects || '' + node[propName] !== defaultValue) {
+ node[propName] = defaultValue;
+ }
+ }
+ } else if (DOMProperty.isCustomAttribute(name)) {
+ node.removeAttribute(name);
+ } else if ("development" !== 'production') {
+ warnUnknownProperty(name);
+ }
+ }
+
+};
+
+ReactPerf.measureMethods(DOMPropertyOperations, 'DOMPropertyOperations', {
+ setValueForProperty: 'setValueForProperty',
+ setValueForAttribute: 'setValueForAttribute',
+ deleteValueForProperty: 'deleteValueForProperty'
+});
+
+module.exports = DOMPropertyOperations;
+},{"10":10,"136":136,"173":173,"78":78}],12:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Danger
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var createNodesFromMarkup = _dereq_(152);
+var emptyFunction = _dereq_(153);
+var getMarkupWrap = _dereq_(157);
+var invariant = _dereq_(161);
+
+var OPEN_TAG_NAME_EXP = /^(<[^ \/>]+)/;
+var RESULT_INDEX_ATTR = 'data-danger-index';
+
+/**
+ * Extracts the `nodeName` from a string of markup.
+ *
+ * NOTE: Extracting the `nodeName` does not require a regular expression match
+ * because we make assumptions about React-generated markup (i.e. there are no
+ * spaces surrounding the opening tag and there is at least one attribute).
+ *
+ * @param {string} markup String of markup.
+ * @return {string} Node name of the supplied markup.
+ * @see http://jsperf.com/extract-nodename
+ */
+function getNodeName(markup) {
+ return markup.substring(1, markup.indexOf(' '));
+}
+
+var Danger = {
+
+ /**
+ * Renders markup into an array of nodes. The markup is expected to render
+ * into a list of root nodes. Also, the length of `resultList` and
+ * `markupList` should be the same.
+ *
+ * @param {array<string>} markupList List of markup strings to render.
+ * @return {array<DOMElement>} List of rendered nodes.
+ * @internal
+ */
+ dangerouslyRenderMarkup: function (markupList) {
+ !ExecutionEnvironment.canUseDOM ? "development" !== 'production' ? invariant(false, 'dangerouslyRenderMarkup(...): Cannot render markup in a worker ' + 'thread. Make sure `window` and `document` are available globally ' + 'before requiring React when unit testing or use ' + 'ReactDOMServer.renderToString for server rendering.') : invariant(false) : undefined;
+ var nodeName;
+ var markupByNodeName = {};
+ // Group markup by `nodeName` if a wrap is necessary, else by '*'.
+ for (var i = 0; i < markupList.length; i++) {
+ !markupList[i] ? "development" !== 'production' ? invariant(false, 'dangerouslyRenderMarkup(...): Missing markup.') : invariant(false) : undefined;
+ nodeName = getNodeName(markupList[i]);
+ nodeName = getMarkupWrap(nodeName) ? nodeName : '*';
+ markupByNodeName[nodeName] = markupByNodeName[nodeName] || [];
+ markupByNodeName[nodeName][i] = markupList[i];
+ }
+ var resultList = [];
+ var resultListAssignmentCount = 0;
+ for (nodeName in markupByNodeName) {
+ if (!markupByNodeName.hasOwnProperty(nodeName)) {
+ continue;
+ }
+ var markupListByNodeName = markupByNodeName[nodeName];
+
+ // This for-in loop skips the holes of the sparse array. The order of
+ // iteration should follow the order of assignment, which happens to match
+ // numerical index order, but we don't rely on that.
+ var resultIndex;
+ for (resultIndex in markupListByNodeName) {
+ if (markupListByNodeName.hasOwnProperty(resultIndex)) {
+ var markup = markupListByNodeName[resultIndex];
+
+ // Push the requested markup with an additional RESULT_INDEX_ATTR
+ // attribute. If the markup does not start with a < character, it
+ // will be discarded below (with an appropriate console.error).
+ markupListByNodeName[resultIndex] = markup.replace(OPEN_TAG_NAME_EXP,
+ // This index will be parsed back out below.
+ '$1 ' + RESULT_INDEX_ATTR + '="' + resultIndex + '" ');
+ }
+ }
+
+ // Render each group of markup with similar wrapping `nodeName`.
+ var renderNodes = createNodesFromMarkup(markupListByNodeName.join(''), emptyFunction // Do nothing special with <script> tags.
+ );
+
+ for (var j = 0; j < renderNodes.length; ++j) {
+ var renderNode = renderNodes[j];
+ if (renderNode.hasAttribute && renderNode.hasAttribute(RESULT_INDEX_ATTR)) {
+
+ resultIndex = +renderNode.getAttribute(RESULT_INDEX_ATTR);
+ renderNode.removeAttribute(RESULT_INDEX_ATTR);
+
+ !!resultList.hasOwnProperty(resultIndex) ? "development" !== 'production' ? invariant(false, 'Danger: Assigning to an already-occupied result index.') : invariant(false) : undefined;
+
+ resultList[resultIndex] = renderNode;
+
+ // This should match resultList.length and markupList.length when
+ // we're done.
+ resultListAssignmentCount += 1;
+ } else if ("development" !== 'production') {
+ console.error('Danger: Discarding unexpected node:', renderNode);
+ }
+ }
+ }
+
+ // Although resultList was populated out of order, it should now be a dense
+ // array.
+ !(resultListAssignmentCount === resultList.length) ? "development" !== 'production' ? invariant(false, 'Danger: Did not assign to every index of resultList.') : invariant(false) : undefined;
+
+ !(resultList.length === markupList.length) ? "development" !== 'production' ? invariant(false, 'Danger: Expected markup to render %s nodes, but rendered %s.', markupList.length, resultList.length) : invariant(false) : undefined;
+
+ return resultList;
+ },
+
+ /**
+ * Replaces a node with a string of markup at its current position within its
+ * parent. The markup must render into a single root node.
+ *
+ * @param {DOMElement} oldChild Child node to replace.
+ * @param {string} markup Markup to render in place of the child node.
+ * @internal
+ */
+ dangerouslyReplaceNodeWithMarkup: function (oldChild, markup) {
+ !ExecutionEnvironment.canUseDOM ? "development" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Cannot render markup in a ' + 'worker thread. Make sure `window` and `document` are available ' + 'globally before requiring React when unit testing or use ' + 'ReactDOMServer.renderToString() for server rendering.') : invariant(false) : undefined;
+ !markup ? "development" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.') : invariant(false) : undefined;
+ !(oldChild.tagName.toLowerCase() !== 'html') ? "development" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Cannot replace markup of the ' + '<html> node. This is because browser quirks make this unreliable ' + 'and/or slow. If you want to render to the root you must use ' + 'server rendering. See ReactDOMServer.renderToString().') : invariant(false) : undefined;
+
+ var newChild;
+ if (typeof markup === 'string') {
+ newChild = createNodesFromMarkup(markup, emptyFunction)[0];
+ } else {
+ newChild = markup;
+ }
+ oldChild.parentNode.replaceChild(newChild, oldChild);
+ }
+
+};
+
+module.exports = Danger;
+},{"147":147,"152":152,"153":153,"157":157,"161":161}],13:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DefaultEventPluginOrder
+ */
+
+'use strict';
+
+var keyOf = _dereq_(166);
+
+/**
+ * Module that is injectable into `EventPluginHub`, that specifies a
+ * deterministic ordering of `EventPlugin`s. A convenient way to reason about
+ * plugins, without having to package every one of them. This is better than
+ * having plugins be ordered in the same order that they are injected because
+ * that ordering would be influenced by the packaging order.
+ * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that
+ * preventing default on events is convenient in `SimpleEventPlugin` handlers.
+ */
+var DefaultEventPluginOrder = [keyOf({ ResponderEventPlugin: null }), keyOf({ SimpleEventPlugin: null }), keyOf({ TapEventPlugin: null }), keyOf({ EnterLeaveEventPlugin: null }), keyOf({ ChangeEventPlugin: null }), keyOf({ SelectEventPlugin: null }), keyOf({ BeforeInputEventPlugin: null })];
+
+module.exports = DefaultEventPluginOrder;
+},{"166":166}],14:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EnterLeaveEventPlugin
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPropagators = _dereq_(19);
+var SyntheticMouseEvent = _dereq_(109);
+
+var ReactMount = _dereq_(72);
+var keyOf = _dereq_(166);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+var getFirstReactDOM = ReactMount.getFirstReactDOM;
+
+var eventTypes = {
+ mouseEnter: {
+ registrationName: keyOf({ onMouseEnter: null }),
+ dependencies: [topLevelTypes.topMouseOut, topLevelTypes.topMouseOver]
+ },
+ mouseLeave: {
+ registrationName: keyOf({ onMouseLeave: null }),
+ dependencies: [topLevelTypes.topMouseOut, topLevelTypes.topMouseOver]
+ }
+};
+
+var extractedEvents = [null, null];
+
+var EnterLeaveEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * For almost every interaction we care about, there will be both a top-level
+ * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that
+ * we do not extract duplicate events. However, moving the mouse into the
+ * browser from outside will not fire a `mouseout` event. In this case, we use
+ * the `mouseover` top-level event.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ if (topLevelType === topLevelTypes.topMouseOver && (nativeEvent.relatedTarget || nativeEvent.fromElement)) {
+ return null;
+ }
+ if (topLevelType !== topLevelTypes.topMouseOut && topLevelType !== topLevelTypes.topMouseOver) {
+ // Must not be a mouse in or mouse out - ignoring.
+ return null;
+ }
+
+ var win;
+ if (topLevelTarget.window === topLevelTarget) {
+ // `topLevelTarget` is probably a window object.
+ win = topLevelTarget;
+ } else {
+ // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.
+ var doc = topLevelTarget.ownerDocument;
+ if (doc) {
+ win = doc.defaultView || doc.parentWindow;
+ } else {
+ win = window;
+ }
+ }
+
+ var from;
+ var to;
+ var fromID = '';
+ var toID = '';
+ if (topLevelType === topLevelTypes.topMouseOut) {
+ from = topLevelTarget;
+ fromID = topLevelTargetID;
+ to = getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement);
+ if (to) {
+ toID = ReactMount.getID(to);
+ } else {
+ to = win;
+ }
+ to = to || win;
+ } else {
+ from = win;
+ to = topLevelTarget;
+ toID = topLevelTargetID;
+ }
+
+ if (from === to) {
+ // Nothing pertains to our managed components.
+ return null;
+ }
+
+ var leave = SyntheticMouseEvent.getPooled(eventTypes.mouseLeave, fromID, nativeEvent, nativeEventTarget);
+ leave.type = 'mouseleave';
+ leave.target = from;
+ leave.relatedTarget = to;
+
+ var enter = SyntheticMouseEvent.getPooled(eventTypes.mouseEnter, toID, nativeEvent, nativeEventTarget);
+ enter.type = 'mouseenter';
+ enter.target = to;
+ enter.relatedTarget = from;
+
+ EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID);
+
+ extractedEvents[0] = leave;
+ extractedEvents[1] = enter;
+
+ return extractedEvents;
+ }
+
+};
+
+module.exports = EnterLeaveEventPlugin;
+},{"109":109,"15":15,"166":166,"19":19,"72":72}],15:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventConstants
+ */
+
+'use strict';
+
+var keyMirror = _dereq_(165);
+
+var PropagationPhases = keyMirror({ bubbled: null, captured: null });
+
+/**
+ * Types of raw signals from the browser caught at the top level.
+ */
+var topLevelTypes = keyMirror({
+ topAbort: null,
+ topBlur: null,
+ topCanPlay: null,
+ topCanPlayThrough: null,
+ topChange: null,
+ topClick: null,
+ topCompositionEnd: null,
+ topCompositionStart: null,
+ topCompositionUpdate: null,
+ topContextMenu: null,
+ topCopy: null,
+ topCut: null,
+ topDoubleClick: null,
+ topDrag: null,
+ topDragEnd: null,
+ topDragEnter: null,
+ topDragExit: null,
+ topDragLeave: null,
+ topDragOver: null,
+ topDragStart: null,
+ topDrop: null,
+ topDurationChange: null,
+ topEmptied: null,
+ topEncrypted: null,
+ topEnded: null,
+ topError: null,
+ topFocus: null,
+ topInput: null,
+ topKeyDown: null,
+ topKeyPress: null,
+ topKeyUp: null,
+ topLoad: null,
+ topLoadedData: null,
+ topLoadedMetadata: null,
+ topLoadStart: null,
+ topMouseDown: null,
+ topMouseMove: null,
+ topMouseOut: null,
+ topMouseOver: null,
+ topMouseUp: null,
+ topPaste: null,
+ topPause: null,
+ topPlay: null,
+ topPlaying: null,
+ topProgress: null,
+ topRateChange: null,
+ topReset: null,
+ topScroll: null,
+ topSeeked: null,
+ topSeeking: null,
+ topSelectionChange: null,
+ topStalled: null,
+ topSubmit: null,
+ topSuspend: null,
+ topTextInput: null,
+ topTimeUpdate: null,
+ topTouchCancel: null,
+ topTouchEnd: null,
+ topTouchMove: null,
+ topTouchStart: null,
+ topVolumeChange: null,
+ topWaiting: null,
+ topWheel: null
+});
+
+var EventConstants = {
+ topLevelTypes: topLevelTypes,
+ PropagationPhases: PropagationPhases
+};
+
+module.exports = EventConstants;
+},{"165":165}],16:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPluginHub
+ */
+
+'use strict';
+
+var EventPluginRegistry = _dereq_(17);
+var EventPluginUtils = _dereq_(18);
+var ReactErrorUtils = _dereq_(61);
+
+var accumulateInto = _dereq_(115);
+var forEachAccumulated = _dereq_(124);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Internal store for event listeners
+ */
+var listenerBank = {};
+
+/**
+ * Internal queue of events that have accumulated their dispatches and are
+ * waiting to have their dispatches executed.
+ */
+var eventQueue = null;
+
+/**
+ * Dispatches an event and releases it back into the pool, unless persistent.
+ *
+ * @param {?object} event Synthetic event to be dispatched.
+ * @param {boolean} simulated If the event is simulated (changes exn behavior)
+ * @private
+ */
+var executeDispatchesAndRelease = function (event, simulated) {
+ if (event) {
+ EventPluginUtils.executeDispatchesInOrder(event, simulated);
+
+ if (!event.isPersistent()) {
+ event.constructor.release(event);
+ }
+ }
+};
+var executeDispatchesAndReleaseSimulated = function (e) {
+ return executeDispatchesAndRelease(e, true);
+};
+var executeDispatchesAndReleaseTopLevel = function (e) {
+ return executeDispatchesAndRelease(e, false);
+};
+
+/**
+ * - `InstanceHandle`: [required] Module that performs logical traversals of DOM
+ * hierarchy given ids of the logical DOM elements involved.
+ */
+var InstanceHandle = null;
+
+function validateInstanceHandle() {
+ var valid = InstanceHandle && InstanceHandle.traverseTwoPhase && InstanceHandle.traverseEnterLeave;
+ "development" !== 'production' ? warning(valid, 'InstanceHandle not injected before use!') : undefined;
+}
+
+/**
+ * This is a unified interface for event plugins to be installed and configured.
+ *
+ * Event plugins can implement the following properties:
+ *
+ * `extractEvents` {function(string, DOMEventTarget, string, object): *}
+ * Required. When a top-level event is fired, this method is expected to
+ * extract synthetic events that will in turn be queued and dispatched.
+ *
+ * `eventTypes` {object}
+ * Optional, plugins that fire events must publish a mapping of registration
+ * names that are used to register listeners. Values of this mapping must
+ * be objects that contain `registrationName` or `phasedRegistrationNames`.
+ *
+ * `executeDispatch` {function(object, function, string)}
+ * Optional, allows plugins to override how an event gets dispatched. By
+ * default, the listener is simply invoked.
+ *
+ * Each plugin that is injected into `EventsPluginHub` is immediately operable.
+ *
+ * @public
+ */
+var EventPluginHub = {
+
+ /**
+ * Methods for injecting dependencies.
+ */
+ injection: {
+
+ /**
+ * @param {object} InjectedMount
+ * @public
+ */
+ injectMount: EventPluginUtils.injection.injectMount,
+
+ /**
+ * @param {object} InjectedInstanceHandle
+ * @public
+ */
+ injectInstanceHandle: function (InjectedInstanceHandle) {
+ InstanceHandle = InjectedInstanceHandle;
+ if ("development" !== 'production') {
+ validateInstanceHandle();
+ }
+ },
+
+ getInstanceHandle: function () {
+ if ("development" !== 'production') {
+ validateInstanceHandle();
+ }
+ return InstanceHandle;
+ },
+
+ /**
+ * @param {array} InjectedEventPluginOrder
+ * @public
+ */
+ injectEventPluginOrder: EventPluginRegistry.injectEventPluginOrder,
+
+ /**
+ * @param {object} injectedNamesToPlugins Map from names to plugin modules.
+ */
+ injectEventPluginsByName: EventPluginRegistry.injectEventPluginsByName
+
+ },
+
+ eventNameDispatchConfigs: EventPluginRegistry.eventNameDispatchConfigs,
+
+ registrationNameModules: EventPluginRegistry.registrationNameModules,
+
+ /**
+ * Stores `listener` at `listenerBank[registrationName][id]`. Is idempotent.
+ *
+ * @param {string} id ID of the DOM element.
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ * @param {?function} listener The callback to store.
+ */
+ putListener: function (id, registrationName, listener) {
+ !(typeof listener === 'function') ? "development" !== 'production' ? invariant(false, 'Expected %s listener to be a function, instead got type %s', registrationName, typeof listener) : invariant(false) : undefined;
+
+ var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
+ bankForRegistrationName[id] = listener;
+
+ var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
+ if (PluginModule && PluginModule.didPutListener) {
+ PluginModule.didPutListener(id, registrationName, listener);
+ }
+ },
+
+ /**
+ * @param {string} id ID of the DOM element.
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ * @return {?function} The stored callback.
+ */
+ getListener: function (id, registrationName) {
+ var bankForRegistrationName = listenerBank[registrationName];
+ return bankForRegistrationName && bankForRegistrationName[id];
+ },
+
+ /**
+ * Deletes a listener from the registration bank.
+ *
+ * @param {string} id ID of the DOM element.
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ */
+ deleteListener: function (id, registrationName) {
+ var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
+ if (PluginModule && PluginModule.willDeleteListener) {
+ PluginModule.willDeleteListener(id, registrationName);
+ }
+
+ var bankForRegistrationName = listenerBank[registrationName];
+ // TODO: This should never be null -- when is it?
+ if (bankForRegistrationName) {
+ delete bankForRegistrationName[id];
+ }
+ },
+
+ /**
+ * Deletes all listeners for the DOM element with the supplied ID.
+ *
+ * @param {string} id ID of the DOM element.
+ */
+ deleteAllListeners: function (id) {
+ for (var registrationName in listenerBank) {
+ if (!listenerBank[registrationName][id]) {
+ continue;
+ }
+
+ var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
+ if (PluginModule && PluginModule.willDeleteListener) {
+ PluginModule.willDeleteListener(id, registrationName);
+ }
+
+ delete listenerBank[registrationName][id];
+ }
+ },
+
+ /**
+ * Allows registered plugins an opportunity to extract events from top-level
+ * native browser events.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @internal
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var events;
+ var plugins = EventPluginRegistry.plugins;
+ for (var i = 0; i < plugins.length; i++) {
+ // Not every plugin in the ordering may be loaded at runtime.
+ var possiblePlugin = plugins[i];
+ if (possiblePlugin) {
+ var extractedEvents = possiblePlugin.extractEvents(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget);
+ if (extractedEvents) {
+ events = accumulateInto(events, extractedEvents);
+ }
+ }
+ }
+ return events;
+ },
+
+ /**
+ * Enqueues a synthetic event that should be dispatched when
+ * `processEventQueue` is invoked.
+ *
+ * @param {*} events An accumulation of synthetic events.
+ * @internal
+ */
+ enqueueEvents: function (events) {
+ if (events) {
+ eventQueue = accumulateInto(eventQueue, events);
+ }
+ },
+
+ /**
+ * Dispatches all synthetic events on the event queue.
+ *
+ * @internal
+ */
+ processEventQueue: function (simulated) {
+ // Set `eventQueue` to null before processing it so that we can tell if more
+ // events get enqueued while processing.
+ var processingEventQueue = eventQueue;
+ eventQueue = null;
+ if (simulated) {
+ forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
+ } else {
+ forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
+ }
+ !!eventQueue ? "development" !== 'production' ? invariant(false, 'processEventQueue(): Additional events were enqueued while processing ' + 'an event queue. Support for this has not yet been implemented.') : invariant(false) : undefined;
+ // This would be a good time to rethrow if any of the event handlers threw.
+ ReactErrorUtils.rethrowCaughtError();
+ },
+
+ /**
+ * These are needed for tests only. Do not use!
+ */
+ __purge: function () {
+ listenerBank = {};
+ },
+
+ __getListenerBank: function () {
+ return listenerBank;
+ }
+
+};
+
+module.exports = EventPluginHub;
+},{"115":115,"124":124,"161":161,"17":17,"173":173,"18":18,"61":61}],17:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPluginRegistry
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Injectable ordering of event plugins.
+ */
+var EventPluginOrder = null;
+
+/**
+ * Injectable mapping from names to event plugin modules.
+ */
+var namesToPlugins = {};
+
+/**
+ * Recomputes the plugin list using the injected plugins and plugin ordering.
+ *
+ * @private
+ */
+function recomputePluginOrdering() {
+ if (!EventPluginOrder) {
+ // Wait until an `EventPluginOrder` is injected.
+ return;
+ }
+ for (var pluginName in namesToPlugins) {
+ var PluginModule = namesToPlugins[pluginName];
+ var pluginIndex = EventPluginOrder.indexOf(pluginName);
+ !(pluginIndex > -1) ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject event plugins that do not exist in ' + 'the plugin ordering, `%s`.', pluginName) : invariant(false) : undefined;
+ if (EventPluginRegistry.plugins[pluginIndex]) {
+ continue;
+ }
+ !PluginModule.extractEvents ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Event plugins must implement an `extractEvents` ' + 'method, but `%s` does not.', pluginName) : invariant(false) : undefined;
+ EventPluginRegistry.plugins[pluginIndex] = PluginModule;
+ var publishedEvents = PluginModule.eventTypes;
+ for (var eventName in publishedEvents) {
+ !publishEventForPlugin(publishedEvents[eventName], PluginModule, eventName) ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.', eventName, pluginName) : invariant(false) : undefined;
+ }
+ }
+}
+
+/**
+ * Publishes an event so that it can be dispatched by the supplied plugin.
+ *
+ * @param {object} dispatchConfig Dispatch configuration for the event.
+ * @param {object} PluginModule Plugin publishing the event.
+ * @return {boolean} True if the event was successfully published.
+ * @private
+ */
+function publishEventForPlugin(dispatchConfig, PluginModule, eventName) {
+ !!EventPluginRegistry.eventNameDispatchConfigs.hasOwnProperty(eventName) ? "development" !== 'production' ? invariant(false, 'EventPluginHub: More than one plugin attempted to publish the same ' + 'event name, `%s`.', eventName) : invariant(false) : undefined;
+ EventPluginRegistry.eventNameDispatchConfigs[eventName] = dispatchConfig;
+
+ var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames;
+ if (phasedRegistrationNames) {
+ for (var phaseName in phasedRegistrationNames) {
+ if (phasedRegistrationNames.hasOwnProperty(phaseName)) {
+ var phasedRegistrationName = phasedRegistrationNames[phaseName];
+ publishRegistrationName(phasedRegistrationName, PluginModule, eventName);
+ }
+ }
+ return true;
+ } else if (dispatchConfig.registrationName) {
+ publishRegistrationName(dispatchConfig.registrationName, PluginModule, eventName);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Publishes a registration name that is used to identify dispatched events and
+ * can be used with `EventPluginHub.putListener` to register listeners.
+ *
+ * @param {string} registrationName Registration name to add.
+ * @param {object} PluginModule Plugin publishing the event.
+ * @private
+ */
+function publishRegistrationName(registrationName, PluginModule, eventName) {
+ !!EventPluginRegistry.registrationNameModules[registrationName] ? "development" !== 'production' ? invariant(false, 'EventPluginHub: More than one plugin attempted to publish the same ' + 'registration name, `%s`.', registrationName) : invariant(false) : undefined;
+ EventPluginRegistry.registrationNameModules[registrationName] = PluginModule;
+ EventPluginRegistry.registrationNameDependencies[registrationName] = PluginModule.eventTypes[eventName].dependencies;
+}
+
+/**
+ * Registers plugins so that they can extract and dispatch events.
+ *
+ * @see {EventPluginHub}
+ */
+var EventPluginRegistry = {
+
+ /**
+ * Ordered list of injected plugins.
+ */
+ plugins: [],
+
+ /**
+ * Mapping from event name to dispatch config
+ */
+ eventNameDispatchConfigs: {},
+
+ /**
+ * Mapping from registration name to plugin module
+ */
+ registrationNameModules: {},
+
+ /**
+ * Mapping from registration name to event name
+ */
+ registrationNameDependencies: {},
+
+ /**
+ * Injects an ordering of plugins (by plugin name). This allows the ordering
+ * to be decoupled from injection of the actual plugins so that ordering is
+ * always deterministic regardless of packaging, on-the-fly injection, etc.
+ *
+ * @param {array} InjectedEventPluginOrder
+ * @internal
+ * @see {EventPluginHub.injection.injectEventPluginOrder}
+ */
+ injectEventPluginOrder: function (InjectedEventPluginOrder) {
+ !!EventPluginOrder ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject event plugin ordering more than ' + 'once. You are likely trying to load more than one copy of React.') : invariant(false) : undefined;
+ // Clone the ordering so it cannot be dynamically mutated.
+ EventPluginOrder = Array.prototype.slice.call(InjectedEventPluginOrder);
+ recomputePluginOrdering();
+ },
+
+ /**
+ * Injects plugins to be used by `EventPluginHub`. The plugin names must be
+ * in the ordering injected by `injectEventPluginOrder`.
+ *
+ * Plugins can be injected as part of page initialization or on-the-fly.
+ *
+ * @param {object} injectedNamesToPlugins Map from names to plugin modules.
+ * @internal
+ * @see {EventPluginHub.injection.injectEventPluginsByName}
+ */
+ injectEventPluginsByName: function (injectedNamesToPlugins) {
+ var isOrderingDirty = false;
+ for (var pluginName in injectedNamesToPlugins) {
+ if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {
+ continue;
+ }
+ var PluginModule = injectedNamesToPlugins[pluginName];
+ if (!namesToPlugins.hasOwnProperty(pluginName) || namesToPlugins[pluginName] !== PluginModule) {
+ !!namesToPlugins[pluginName] ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject two different event plugins ' + 'using the same name, `%s`.', pluginName) : invariant(false) : undefined;
+ namesToPlugins[pluginName] = PluginModule;
+ isOrderingDirty = true;
+ }
+ }
+ if (isOrderingDirty) {
+ recomputePluginOrdering();
+ }
+ },
+
+ /**
+ * Looks up the plugin for the supplied event.
+ *
+ * @param {object} event A synthetic event.
+ * @return {?object} The plugin that created the supplied event.
+ * @internal
+ */
+ getPluginModuleForEvent: function (event) {
+ var dispatchConfig = event.dispatchConfig;
+ if (dispatchConfig.registrationName) {
+ return EventPluginRegistry.registrationNameModules[dispatchConfig.registrationName] || null;
+ }
+ for (var phase in dispatchConfig.phasedRegistrationNames) {
+ if (!dispatchConfig.phasedRegistrationNames.hasOwnProperty(phase)) {
+ continue;
+ }
+ var PluginModule = EventPluginRegistry.registrationNameModules[dispatchConfig.phasedRegistrationNames[phase]];
+ if (PluginModule) {
+ return PluginModule;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Exposed for unit testing.
+ * @private
+ */
+ _resetEventPlugins: function () {
+ EventPluginOrder = null;
+ for (var pluginName in namesToPlugins) {
+ if (namesToPlugins.hasOwnProperty(pluginName)) {
+ delete namesToPlugins[pluginName];
+ }
+ }
+ EventPluginRegistry.plugins.length = 0;
+
+ var eventNameDispatchConfigs = EventPluginRegistry.eventNameDispatchConfigs;
+ for (var eventName in eventNameDispatchConfigs) {
+ if (eventNameDispatchConfigs.hasOwnProperty(eventName)) {
+ delete eventNameDispatchConfigs[eventName];
+ }
+ }
+
+ var registrationNameModules = EventPluginRegistry.registrationNameModules;
+ for (var registrationName in registrationNameModules) {
+ if (registrationNameModules.hasOwnProperty(registrationName)) {
+ delete registrationNameModules[registrationName];
+ }
+ }
+ }
+
+};
+
+module.exports = EventPluginRegistry;
+},{"161":161}],18:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPluginUtils
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var ReactErrorUtils = _dereq_(61);
+
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Injected dependencies:
+ */
+
+/**
+ * - `Mount`: [required] Module that can convert between React dom IDs and
+ * actual node references.
+ */
+var injection = {
+ Mount: null,
+ injectMount: function (InjectedMount) {
+ injection.Mount = InjectedMount;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(InjectedMount && InjectedMount.getNode && InjectedMount.getID, 'EventPluginUtils.injection.injectMount(...): Injected Mount ' + 'module is missing getNode or getID.') : undefined;
+ }
+ }
+};
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+function isEndish(topLevelType) {
+ return topLevelType === topLevelTypes.topMouseUp || topLevelType === topLevelTypes.topTouchEnd || topLevelType === topLevelTypes.topTouchCancel;
+}
+
+function isMoveish(topLevelType) {
+ return topLevelType === topLevelTypes.topMouseMove || topLevelType === topLevelTypes.topTouchMove;
+}
+function isStartish(topLevelType) {
+ return topLevelType === topLevelTypes.topMouseDown || topLevelType === topLevelTypes.topTouchStart;
+}
+
+var validateEventDispatches;
+if ("development" !== 'production') {
+ validateEventDispatches = function (event) {
+ var dispatchListeners = event._dispatchListeners;
+ var dispatchIDs = event._dispatchIDs;
+
+ var listenersIsArr = Array.isArray(dispatchListeners);
+ var idsIsArr = Array.isArray(dispatchIDs);
+ var IDsLen = idsIsArr ? dispatchIDs.length : dispatchIDs ? 1 : 0;
+ var listenersLen = listenersIsArr ? dispatchListeners.length : dispatchListeners ? 1 : 0;
+
+ "development" !== 'production' ? warning(idsIsArr === listenersIsArr && IDsLen === listenersLen, 'EventPluginUtils: Invalid `event`.') : undefined;
+ };
+}
+
+/**
+ * Dispatch the event to the listener.
+ * @param {SyntheticEvent} event SyntheticEvent to handle
+ * @param {boolean} simulated If the event is simulated (changes exn behavior)
+ * @param {function} listener Application-level callback
+ * @param {string} domID DOM id to pass to the callback.
+ */
+function executeDispatch(event, simulated, listener, domID) {
+ var type = event.type || 'unknown-event';
+ event.currentTarget = injection.Mount.getNode(domID);
+ if (simulated) {
+ ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event, domID);
+ } else {
+ ReactErrorUtils.invokeGuardedCallback(type, listener, event, domID);
+ }
+ event.currentTarget = null;
+}
+
+/**
+ * Standard/simple iteration through an event's collected dispatches.
+ */
+function executeDispatchesInOrder(event, simulated) {
+ var dispatchListeners = event._dispatchListeners;
+ var dispatchIDs = event._dispatchIDs;
+ if ("development" !== 'production') {
+ validateEventDispatches(event);
+ }
+ if (Array.isArray(dispatchListeners)) {
+ for (var i = 0; i < dispatchListeners.length; i++) {
+ if (event.isPropagationStopped()) {
+ break;
+ }
+ // Listeners and IDs are two parallel arrays that are always in sync.
+ executeDispatch(event, simulated, dispatchListeners[i], dispatchIDs[i]);
+ }
+ } else if (dispatchListeners) {
+ executeDispatch(event, simulated, dispatchListeners, dispatchIDs);
+ }
+ event._dispatchListeners = null;
+ event._dispatchIDs = null;
+}
+
+/**
+ * Standard/simple iteration through an event's collected dispatches, but stops
+ * at the first dispatch execution returning true, and returns that id.
+ *
+ * @return {?string} id of the first dispatch execution who's listener returns
+ * true, or null if no listener returned true.
+ */
+function executeDispatchesInOrderStopAtTrueImpl(event) {
+ var dispatchListeners = event._dispatchListeners;
+ var dispatchIDs = event._dispatchIDs;
+ if ("development" !== 'production') {
+ validateEventDispatches(event);
+ }
+ if (Array.isArray(dispatchListeners)) {
+ for (var i = 0; i < dispatchListeners.length; i++) {
+ if (event.isPropagationStopped()) {
+ break;
+ }
+ // Listeners and IDs are two parallel arrays that are always in sync.
+ if (dispatchListeners[i](event, dispatchIDs[i])) {
+ return dispatchIDs[i];
+ }
+ }
+ } else if (dispatchListeners) {
+ if (dispatchListeners(event, dispatchIDs)) {
+ return dispatchIDs;
+ }
+ }
+ return null;
+}
+
+/**
+ * @see executeDispatchesInOrderStopAtTrueImpl
+ */
+function executeDispatchesInOrderStopAtTrue(event) {
+ var ret = executeDispatchesInOrderStopAtTrueImpl(event);
+ event._dispatchIDs = null;
+ event._dispatchListeners = null;
+ return ret;
+}
+
+/**
+ * Execution of a "direct" dispatch - there must be at most one dispatch
+ * accumulated on the event or it is considered an error. It doesn't really make
+ * sense for an event with multiple dispatches (bubbled) to keep track of the
+ * return values at each dispatch execution, but it does tend to make sense when
+ * dealing with "direct" dispatches.
+ *
+ * @return {*} The return value of executing the single dispatch.
+ */
+function executeDirectDispatch(event) {
+ if ("development" !== 'production') {
+ validateEventDispatches(event);
+ }
+ var dispatchListener = event._dispatchListeners;
+ var dispatchID = event._dispatchIDs;
+ !!Array.isArray(dispatchListener) ? "development" !== 'production' ? invariant(false, 'executeDirectDispatch(...): Invalid `event`.') : invariant(false) : undefined;
+ var res = dispatchListener ? dispatchListener(event, dispatchID) : null;
+ event._dispatchListeners = null;
+ event._dispatchIDs = null;
+ return res;
+}
+
+/**
+ * @param {SyntheticEvent} event
+ * @return {boolean} True iff number of dispatches accumulated is greater than 0.
+ */
+function hasDispatches(event) {
+ return !!event._dispatchListeners;
+}
+
+/**
+ * General utilities that are useful in creating custom Event Plugins.
+ */
+var EventPluginUtils = {
+ isEndish: isEndish,
+ isMoveish: isMoveish,
+ isStartish: isStartish,
+
+ executeDirectDispatch: executeDirectDispatch,
+ executeDispatchesInOrder: executeDispatchesInOrder,
+ executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue,
+ hasDispatches: hasDispatches,
+
+ getNode: function (id) {
+ return injection.Mount.getNode(id);
+ },
+ getID: function (node) {
+ return injection.Mount.getID(node);
+ },
+
+ injection: injection
+};
+
+module.exports = EventPluginUtils;
+},{"15":15,"161":161,"173":173,"61":61}],19:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPropagators
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+
+var warning = _dereq_(173);
+
+var accumulateInto = _dereq_(115);
+var forEachAccumulated = _dereq_(124);
+
+var PropagationPhases = EventConstants.PropagationPhases;
+var getListener = EventPluginHub.getListener;
+
+/**
+ * Some event types have a notion of different registration names for different
+ * "phases" of propagation. This finds listeners by a given phase.
+ */
+function listenerAtPhase(id, event, propagationPhase) {
+ var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
+ return getListener(id, registrationName);
+}
+
+/**
+ * Tags a `SyntheticEvent` with dispatched listeners. Creating this function
+ * here, allows us to not have to bind or create functions for each event.
+ * Mutating the event's members allows us to not have to create a wrapping
+ * "dispatch" object that pairs the event with the listener.
+ */
+function accumulateDirectionalDispatches(domID, upwards, event) {
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(domID, 'Dispatching id must not be null') : undefined;
+ }
+ var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;
+ var listener = listenerAtPhase(domID, event, phase);
+ if (listener) {
+ event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
+ event._dispatchIDs = accumulateInto(event._dispatchIDs, domID);
+ }
+}
+
+/**
+ * Collect dispatches (must be entirely collected before dispatching - see unit
+ * tests). Lazily allocate the array to conserve memory. We must loop through
+ * each event and perform the traversal for each one. We cannot perform a
+ * single traversal for the entire collection of events because each event may
+ * have a different target.
+ */
+function accumulateTwoPhaseDispatchesSingle(event) {
+ if (event && event.dispatchConfig.phasedRegistrationNames) {
+ EventPluginHub.injection.getInstanceHandle().traverseTwoPhase(event.dispatchMarker, accumulateDirectionalDispatches, event);
+ }
+}
+
+/**
+ * Same as `accumulateTwoPhaseDispatchesSingle`, but skips over the targetID.
+ */
+function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
+ if (event && event.dispatchConfig.phasedRegistrationNames) {
+ EventPluginHub.injection.getInstanceHandle().traverseTwoPhaseSkipTarget(event.dispatchMarker, accumulateDirectionalDispatches, event);
+ }
+}
+
+/**
+ * Accumulates without regard to direction, does not look for phased
+ * registration names. Same as `accumulateDirectDispatchesSingle` but without
+ * requiring that the `dispatchMarker` be the same as the dispatched ID.
+ */
+function accumulateDispatches(id, ignoredDirection, event) {
+ if (event && event.dispatchConfig.registrationName) {
+ var registrationName = event.dispatchConfig.registrationName;
+ var listener = getListener(id, registrationName);
+ if (listener) {
+ event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
+ event._dispatchIDs = accumulateInto(event._dispatchIDs, id);
+ }
+ }
+}
+
+/**
+ * Accumulates dispatches on an `SyntheticEvent`, but only for the
+ * `dispatchMarker`.
+ * @param {SyntheticEvent} event
+ */
+function accumulateDirectDispatchesSingle(event) {
+ if (event && event.dispatchConfig.registrationName) {
+ accumulateDispatches(event.dispatchMarker, null, event);
+ }
+}
+
+function accumulateTwoPhaseDispatches(events) {
+ forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
+}
+
+function accumulateTwoPhaseDispatchesSkipTarget(events) {
+ forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget);
+}
+
+function accumulateEnterLeaveDispatches(leave, enter, fromID, toID) {
+ EventPluginHub.injection.getInstanceHandle().traverseEnterLeave(fromID, toID, accumulateDispatches, leave, enter);
+}
+
+function accumulateDirectDispatches(events) {
+ forEachAccumulated(events, accumulateDirectDispatchesSingle);
+}
+
+/**
+ * A small set of propagation patterns, each of which will accept a small amount
+ * of information, and generate a set of "dispatch ready event objects" - which
+ * are sets of events that have already been annotated with a set of dispatched
+ * listener functions/ids. The API is designed this way to discourage these
+ * propagation strategies from actually executing the dispatches, since we
+ * always want to collect the entire set of dispatches before executing event a
+ * single one.
+ *
+ * @constructor EventPropagators
+ */
+var EventPropagators = {
+ accumulateTwoPhaseDispatches: accumulateTwoPhaseDispatches,
+ accumulateTwoPhaseDispatchesSkipTarget: accumulateTwoPhaseDispatchesSkipTarget,
+ accumulateDirectDispatches: accumulateDirectDispatches,
+ accumulateEnterLeaveDispatches: accumulateEnterLeaveDispatches
+};
+
+module.exports = EventPropagators;
+},{"115":115,"124":124,"15":15,"16":16,"173":173}],20:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule FallbackCompositionState
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+
+var assign = _dereq_(24);
+var getTextContentAccessor = _dereq_(131);
+
+/**
+ * This helper class stores information about text content of a target node,
+ * allowing comparison of content before and after a given event.
+ *
+ * Identify the node where selection currently begins, then observe
+ * both its text content and its current position in the DOM. Since the
+ * browser may natively replace the target node during composition, we can
+ * use its position to find its replacement.
+ *
+ * @param {DOMEventTarget} root
+ */
+function FallbackCompositionState(root) {
+ this._root = root;
+ this._startText = this.getText();
+ this._fallbackText = null;
+}
+
+assign(FallbackCompositionState.prototype, {
+ destructor: function () {
+ this._root = null;
+ this._startText = null;
+ this._fallbackText = null;
+ },
+
+ /**
+ * Get current text of input.
+ *
+ * @return {string}
+ */
+ getText: function () {
+ if ('value' in this._root) {
+ return this._root.value;
+ }
+ return this._root[getTextContentAccessor()];
+ },
+
+ /**
+ * Determine the differing substring between the initially stored
+ * text content and the current content.
+ *
+ * @return {string}
+ */
+ getData: function () {
+ if (this._fallbackText) {
+ return this._fallbackText;
+ }
+
+ var start;
+ var startValue = this._startText;
+ var startLength = startValue.length;
+ var end;
+ var endValue = this.getText();
+ var endLength = endValue.length;
+
+ for (start = 0; start < startLength; start++) {
+ if (startValue[start] !== endValue[start]) {
+ break;
+ }
+ }
+
+ var minEnd = startLength - start;
+ for (end = 1; end <= minEnd; end++) {
+ if (startValue[startLength - end] !== endValue[endLength - end]) {
+ break;
+ }
+ }
+
+ var sliceTail = end > 1 ? 1 - end : undefined;
+ this._fallbackText = endValue.slice(start, sliceTail);
+ return this._fallbackText;
+ }
+});
+
+PooledClass.addPoolingTo(FallbackCompositionState);
+
+module.exports = FallbackCompositionState;
+},{"131":131,"24":24,"25":25}],21:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule HTMLDOMPropertyConfig
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ExecutionEnvironment = _dereq_(147);
+
+var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
+var MUST_USE_PROPERTY = DOMProperty.injection.MUST_USE_PROPERTY;
+var HAS_BOOLEAN_VALUE = DOMProperty.injection.HAS_BOOLEAN_VALUE;
+var HAS_SIDE_EFFECTS = DOMProperty.injection.HAS_SIDE_EFFECTS;
+var HAS_NUMERIC_VALUE = DOMProperty.injection.HAS_NUMERIC_VALUE;
+var HAS_POSITIVE_NUMERIC_VALUE = DOMProperty.injection.HAS_POSITIVE_NUMERIC_VALUE;
+var HAS_OVERLOADED_BOOLEAN_VALUE = DOMProperty.injection.HAS_OVERLOADED_BOOLEAN_VALUE;
+
+var hasSVG;
+if (ExecutionEnvironment.canUseDOM) {
+ var implementation = document.implementation;
+ hasSVG = implementation && implementation.hasFeature && implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1');
+}
+
+var HTMLDOMPropertyConfig = {
+ isCustomAttribute: RegExp.prototype.test.bind(/^(data|aria)-[a-z_][a-z\d_.\-]*$/),
+ Properties: {
+ /**
+ * Standard Properties
+ */
+ accept: null,
+ acceptCharset: null,
+ accessKey: null,
+ action: null,
+ allowFullScreen: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ allowTransparency: MUST_USE_ATTRIBUTE,
+ alt: null,
+ async: HAS_BOOLEAN_VALUE,
+ autoComplete: null,
+ // autoFocus is polyfilled/normalized by AutoFocusUtils
+ // autoFocus: HAS_BOOLEAN_VALUE,
+ autoPlay: HAS_BOOLEAN_VALUE,
+ capture: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ cellPadding: null,
+ cellSpacing: null,
+ charSet: MUST_USE_ATTRIBUTE,
+ challenge: MUST_USE_ATTRIBUTE,
+ checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ classID: MUST_USE_ATTRIBUTE,
+ // To set className on SVG elements, it's necessary to use .setAttribute;
+ // this works on HTML elements too in all browsers except IE8. Conveniently,
+ // IE8 doesn't support SVG and so we can simply use the attribute in
+ // browsers that support SVG and the property in browsers that don't,
+ // regardless of whether the element is HTML or SVG.
+ className: hasSVG ? MUST_USE_ATTRIBUTE : MUST_USE_PROPERTY,
+ cols: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
+ colSpan: null,
+ content: null,
+ contentEditable: null,
+ contextMenu: MUST_USE_ATTRIBUTE,
+ controls: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ coords: null,
+ crossOrigin: null,
+ data: null, // For `<object />` acts as `src`.
+ dateTime: MUST_USE_ATTRIBUTE,
+ 'default': HAS_BOOLEAN_VALUE,
+ defer: HAS_BOOLEAN_VALUE,
+ dir: null,
+ disabled: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ download: HAS_OVERLOADED_BOOLEAN_VALUE,
+ draggable: null,
+ encType: null,
+ form: MUST_USE_ATTRIBUTE,
+ formAction: MUST_USE_ATTRIBUTE,
+ formEncType: MUST_USE_ATTRIBUTE,
+ formMethod: MUST_USE_ATTRIBUTE,
+ formNoValidate: HAS_BOOLEAN_VALUE,
+ formTarget: MUST_USE_ATTRIBUTE,
+ frameBorder: MUST_USE_ATTRIBUTE,
+ headers: null,
+ height: MUST_USE_ATTRIBUTE,
+ hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ high: null,
+ href: null,
+ hrefLang: null,
+ htmlFor: null,
+ httpEquiv: null,
+ icon: null,
+ id: MUST_USE_PROPERTY,
+ inputMode: MUST_USE_ATTRIBUTE,
+ integrity: null,
+ is: MUST_USE_ATTRIBUTE,
+ keyParams: MUST_USE_ATTRIBUTE,
+ keyType: MUST_USE_ATTRIBUTE,
+ kind: null,
+ label: null,
+ lang: null,
+ list: MUST_USE_ATTRIBUTE,
+ loop: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ low: null,
+ manifest: MUST_USE_ATTRIBUTE,
+ marginHeight: null,
+ marginWidth: null,
+ max: null,
+ maxLength: MUST_USE_ATTRIBUTE,
+ media: MUST_USE_ATTRIBUTE,
+ mediaGroup: null,
+ method: null,
+ min: null,
+ minLength: MUST_USE_ATTRIBUTE,
+ multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ name: null,
+ nonce: MUST_USE_ATTRIBUTE,
+ noValidate: HAS_BOOLEAN_VALUE,
+ open: HAS_BOOLEAN_VALUE,
+ optimum: null,
+ pattern: null,
+ placeholder: null,
+ poster: null,
+ preload: null,
+ radioGroup: null,
+ readOnly: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ rel: null,
+ required: HAS_BOOLEAN_VALUE,
+ reversed: HAS_BOOLEAN_VALUE,
+ role: MUST_USE_ATTRIBUTE,
+ rows: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
+ rowSpan: null,
+ sandbox: null,
+ scope: null,
+ scoped: HAS_BOOLEAN_VALUE,
+ scrolling: null,
+ seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ shape: null,
+ size: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
+ sizes: MUST_USE_ATTRIBUTE,
+ span: HAS_POSITIVE_NUMERIC_VALUE,
+ spellCheck: null,
+ src: null,
+ srcDoc: MUST_USE_PROPERTY,
+ srcLang: null,
+ srcSet: MUST_USE_ATTRIBUTE,
+ start: HAS_NUMERIC_VALUE,
+ step: null,
+ style: null,
+ summary: null,
+ tabIndex: null,
+ target: null,
+ title: null,
+ type: null,
+ useMap: null,
+ value: MUST_USE_PROPERTY | HAS_SIDE_EFFECTS,
+ width: MUST_USE_ATTRIBUTE,
+ wmode: MUST_USE_ATTRIBUTE,
+ wrap: null,
+
+ /**
+ * RDFa Properties
+ */
+ about: MUST_USE_ATTRIBUTE,
+ datatype: MUST_USE_ATTRIBUTE,
+ inlist: MUST_USE_ATTRIBUTE,
+ prefix: MUST_USE_ATTRIBUTE,
+ // property is also supported for OpenGraph in meta tags.
+ property: MUST_USE_ATTRIBUTE,
+ resource: MUST_USE_ATTRIBUTE,
+ 'typeof': MUST_USE_ATTRIBUTE,
+ vocab: MUST_USE_ATTRIBUTE,
+
+ /**
+ * Non-standard Properties
+ */
+ // autoCapitalize and autoCorrect are supported in Mobile Safari for
+ // keyboard hints.
+ autoCapitalize: MUST_USE_ATTRIBUTE,
+ autoCorrect: MUST_USE_ATTRIBUTE,
+ // autoSave allows WebKit/Blink to persist values of input fields on page reloads
+ autoSave: null,
+ // color is for Safari mask-icon link
+ color: null,
+ // itemProp, itemScope, itemType are for
+ // Microdata support. See http://schema.org/docs/gs.html
+ itemProp: MUST_USE_ATTRIBUTE,
+ itemScope: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ itemType: MUST_USE_ATTRIBUTE,
+ // itemID and itemRef are for Microdata support as well but
+ // only specified in the the WHATWG spec document. See
+ // https://html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api
+ itemID: MUST_USE_ATTRIBUTE,
+ itemRef: MUST_USE_ATTRIBUTE,
+ // results show looking glass icon and recent searches on input
+ // search fields in WebKit/Blink
+ results: null,
+ // IE-only attribute that specifies security restrictions on an iframe
+ // as an alternative to the sandbox attribute on IE<10
+ security: MUST_USE_ATTRIBUTE,
+ // IE-only attribute that controls focus behavior
+ unselectable: MUST_USE_ATTRIBUTE
+ },
+ DOMAttributeNames: {
+ acceptCharset: 'accept-charset',
+ className: 'class',
+ htmlFor: 'for',
+ httpEquiv: 'http-equiv'
+ },
+ DOMPropertyNames: {
+ autoComplete: 'autocomplete',
+ autoFocus: 'autofocus',
+ autoPlay: 'autoplay',
+ autoSave: 'autosave',
+ // `encoding` is equivalent to `enctype`, IE8 lacks an `enctype` setter.
+ // http://www.w3.org/TR/html5/forms.html#dom-fs-encoding
+ encType: 'encoding',
+ hrefLang: 'hreflang',
+ radioGroup: 'radiogroup',
+ spellCheck: 'spellcheck',
+ srcDoc: 'srcdoc',
+ srcSet: 'srcset'
+ }
+};
+
+module.exports = HTMLDOMPropertyConfig;
+},{"10":10,"147":147}],22:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule LinkedStateMixin
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactLink = _dereq_(70);
+var ReactStateSetters = _dereq_(90);
+
+/**
+ * A simple mixin around ReactLink.forState().
+ */
+var LinkedStateMixin = {
+ /**
+ * Create a ReactLink that's linked to part of this component's state. The
+ * ReactLink will have the current value of this.state[key] and will call
+ * setState() when a change is requested.
+ *
+ * @param {string} key state key to update. Note: you may want to use keyOf()
+ * if you're using Google Closure Compiler advanced mode.
+ * @return {ReactLink} ReactLink instance linking to the state.
+ */
+ linkState: function (key) {
+ return new ReactLink(this.state[key], ReactStateSetters.createStateKeySetter(this, key));
+ }
+};
+
+module.exports = LinkedStateMixin;
+},{"70":70,"90":90}],23:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule LinkedValueUtils
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactPropTypes = _dereq_(82);
+var ReactPropTypeLocations = _dereq_(81);
+
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+var hasReadOnlyValue = {
+ 'button': true,
+ 'checkbox': true,
+ 'image': true,
+ 'hidden': true,
+ 'radio': true,
+ 'reset': true,
+ 'submit': true
+};
+
+function _assertSingleLink(inputProps) {
+ !(inputProps.checkedLink == null || inputProps.valueLink == null) ? "development" !== 'production' ? invariant(false, 'Cannot provide a checkedLink and a valueLink. If you want to use ' + 'checkedLink, you probably don\'t want to use valueLink and vice versa.') : invariant(false) : undefined;
+}
+function _assertValueLink(inputProps) {
+ _assertSingleLink(inputProps);
+ !(inputProps.value == null && inputProps.onChange == null) ? "development" !== 'production' ? invariant(false, 'Cannot provide a valueLink and a value or onChange event. If you want ' + 'to use value or onChange, you probably don\'t want to use valueLink.') : invariant(false) : undefined;
+}
+
+function _assertCheckedLink(inputProps) {
+ _assertSingleLink(inputProps);
+ !(inputProps.checked == null && inputProps.onChange == null) ? "development" !== 'production' ? invariant(false, 'Cannot provide a checkedLink and a checked property or onChange event. ' + 'If you want to use checked or onChange, you probably don\'t want to ' + 'use checkedLink') : invariant(false) : undefined;
+}
+
+var propTypes = {
+ value: function (props, propName, componentName) {
+ if (!props[propName] || hasReadOnlyValue[props.type] || props.onChange || props.readOnly || props.disabled) {
+ return null;
+ }
+ return new Error('You provided a `value` prop to a form field without an ' + '`onChange` handler. This will render a read-only field. If ' + 'the field should be mutable use `defaultValue`. Otherwise, ' + 'set either `onChange` or `readOnly`.');
+ },
+ checked: function (props, propName, componentName) {
+ if (!props[propName] || props.onChange || props.readOnly || props.disabled) {
+ return null;
+ }
+ return new Error('You provided a `checked` prop to a form field without an ' + '`onChange` handler. This will render a read-only field. If ' + 'the field should be mutable use `defaultChecked`. Otherwise, ' + 'set either `onChange` or `readOnly`.');
+ },
+ onChange: ReactPropTypes.func
+};
+
+var loggedTypeFailures = {};
+function getDeclarationErrorAddendum(owner) {
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+/**
+ * Provide a linked `value` attribute for controlled forms. You should not use
+ * this outside of the ReactDOM controlled form components.
+ */
+var LinkedValueUtils = {
+ checkPropTypes: function (tagName, props, owner) {
+ for (var propName in propTypes) {
+ if (propTypes.hasOwnProperty(propName)) {
+ var error = propTypes[propName](props, propName, tagName, ReactPropTypeLocations.prop);
+ }
+ if (error instanceof Error && !(error.message in loggedTypeFailures)) {
+ // Only monitor this failure once because there tends to be a lot of the
+ // same error.
+ loggedTypeFailures[error.message] = true;
+
+ var addendum = getDeclarationErrorAddendum(owner);
+ "development" !== 'production' ? warning(false, 'Failed form propType: %s%s', error.message, addendum) : undefined;
+ }
+ }
+ },
+
+ /**
+ * @param {object} inputProps Props for form component
+ * @return {*} current value of the input either from value prop or link.
+ */
+ getValue: function (inputProps) {
+ if (inputProps.valueLink) {
+ _assertValueLink(inputProps);
+ return inputProps.valueLink.value;
+ }
+ return inputProps.value;
+ },
+
+ /**
+ * @param {object} inputProps Props for form component
+ * @return {*} current checked status of the input either from checked prop
+ * or link.
+ */
+ getChecked: function (inputProps) {
+ if (inputProps.checkedLink) {
+ _assertCheckedLink(inputProps);
+ return inputProps.checkedLink.value;
+ }
+ return inputProps.checked;
+ },
+
+ /**
+ * @param {object} inputProps Props for form component
+ * @param {SyntheticEvent} event change event to handle
+ */
+ executeOnChange: function (inputProps, event) {
+ if (inputProps.valueLink) {
+ _assertValueLink(inputProps);
+ return inputProps.valueLink.requestChange(event.target.value);
+ } else if (inputProps.checkedLink) {
+ _assertCheckedLink(inputProps);
+ return inputProps.checkedLink.requestChange(event.target.checked);
+ } else if (inputProps.onChange) {
+ return inputProps.onChange.call(undefined, event);
+ }
+ }
+};
+
+module.exports = LinkedValueUtils;
+},{"161":161,"173":173,"81":81,"82":82}],24:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Object.assign
+ */
+
+// https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.assign
+
+'use strict';
+
+function assign(target, sources) {
+ if (target == null) {
+ throw new TypeError('Object.assign target cannot be null or undefined');
+ }
+
+ var to = Object(target);
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+ for (var nextIndex = 1; nextIndex < arguments.length; nextIndex++) {
+ var nextSource = arguments[nextIndex];
+ if (nextSource == null) {
+ continue;
+ }
+
+ var from = Object(nextSource);
+
+ // We don't currently support accessors nor proxies. Therefore this
+ // copy cannot throw. If we ever supported this then we must handle
+ // exceptions and side-effects. We don't support symbols so they won't
+ // be transferred.
+
+ for (var key in from) {
+ if (hasOwnProperty.call(from, key)) {
+ to[key] = from[key];
+ }
+ }
+ }
+
+ return to;
+}
+
+module.exports = assign;
+},{}],25:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule PooledClass
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Static poolers. Several custom versions for each potential number of
+ * arguments. A completely generic pooler is easy to implement, but would
+ * require accessing the `arguments` object. In each of these, `this` refers to
+ * the Class itself, not an instance. If any others are needed, simply add them
+ * here, or in their own files.
+ */
+var oneArgumentPooler = function (copyFieldsFrom) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, copyFieldsFrom);
+ return instance;
+ } else {
+ return new Klass(copyFieldsFrom);
+ }
+};
+
+var twoArgumentPooler = function (a1, a2) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2);
+ return instance;
+ } else {
+ return new Klass(a1, a2);
+ }
+};
+
+var threeArgumentPooler = function (a1, a2, a3) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2, a3);
+ return instance;
+ } else {
+ return new Klass(a1, a2, a3);
+ }
+};
+
+var fourArgumentPooler = function (a1, a2, a3, a4) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2, a3, a4);
+ return instance;
+ } else {
+ return new Klass(a1, a2, a3, a4);
+ }
+};
+
+var fiveArgumentPooler = function (a1, a2, a3, a4, a5) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2, a3, a4, a5);
+ return instance;
+ } else {
+ return new Klass(a1, a2, a3, a4, a5);
+ }
+};
+
+var standardReleaser = function (instance) {
+ var Klass = this;
+ !(instance instanceof Klass) ? "development" !== 'production' ? invariant(false, 'Trying to release an instance into a pool of a different type.') : invariant(false) : undefined;
+ instance.destructor();
+ if (Klass.instancePool.length < Klass.poolSize) {
+ Klass.instancePool.push(instance);
+ }
+};
+
+var DEFAULT_POOL_SIZE = 10;
+var DEFAULT_POOLER = oneArgumentPooler;
+
+/**
+ * Augments `CopyConstructor` to be a poolable class, augmenting only the class
+ * itself (statically) not adding any prototypical fields. Any CopyConstructor
+ * you give this may have a `poolSize` property, and will look for a
+ * prototypical `destructor` on instances (optional).
+ *
+ * @param {Function} CopyConstructor Constructor that can be used to reset.
+ * @param {Function} pooler Customizable pooler.
+ */
+var addPoolingTo = function (CopyConstructor, pooler) {
+ var NewKlass = CopyConstructor;
+ NewKlass.instancePool = [];
+ NewKlass.getPooled = pooler || DEFAULT_POOLER;
+ if (!NewKlass.poolSize) {
+ NewKlass.poolSize = DEFAULT_POOL_SIZE;
+ }
+ NewKlass.release = standardReleaser;
+ return NewKlass;
+};
+
+var PooledClass = {
+ addPoolingTo: addPoolingTo,
+ oneArgumentPooler: oneArgumentPooler,
+ twoArgumentPooler: twoArgumentPooler,
+ threeArgumentPooler: threeArgumentPooler,
+ fourArgumentPooler: fourArgumentPooler,
+ fiveArgumentPooler: fiveArgumentPooler
+};
+
+module.exports = PooledClass;
+},{"161":161}],26:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule React
+ */
+
+'use strict';
+
+var ReactDOM = _dereq_(40);
+var ReactDOMServer = _dereq_(50);
+var ReactIsomorphic = _dereq_(69);
+
+var assign = _dereq_(24);
+var deprecated = _dereq_(120);
+
+// `version` will be added here by ReactIsomorphic.
+var React = {};
+
+assign(React, ReactIsomorphic);
+
+assign(React, {
+ // ReactDOM
+ findDOMNode: deprecated('findDOMNode', 'ReactDOM', 'react-dom', ReactDOM, ReactDOM.findDOMNode),
+ render: deprecated('render', 'ReactDOM', 'react-dom', ReactDOM, ReactDOM.render),
+ unmountComponentAtNode: deprecated('unmountComponentAtNode', 'ReactDOM', 'react-dom', ReactDOM, ReactDOM.unmountComponentAtNode),
+
+ // ReactDOMServer
+ renderToString: deprecated('renderToString', 'ReactDOMServer', 'react-dom/server', ReactDOMServer, ReactDOMServer.renderToString),
+ renderToStaticMarkup: deprecated('renderToStaticMarkup', 'ReactDOMServer', 'react-dom/server', ReactDOMServer, ReactDOMServer.renderToStaticMarkup)
+});
+
+React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactDOM;
+React.__SECRET_DOM_SERVER_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactDOMServer;
+
+module.exports = React;
+},{"120":120,"24":24,"40":40,"50":50,"69":69}],27:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactBrowserComponentMixin
+ */
+
+'use strict';
+
+var ReactInstanceMap = _dereq_(68);
+
+var findDOMNode = _dereq_(122);
+var warning = _dereq_(173);
+
+var didWarnKey = '_getDOMNodeDidWarn';
+
+var ReactBrowserComponentMixin = {
+ /**
+ * Returns the DOM node rendered by this component.
+ *
+ * @return {DOMElement} The root node of this component.
+ * @final
+ * @protected
+ */
+ getDOMNode: function () {
+ "development" !== 'production' ? warning(this.constructor[didWarnKey], '%s.getDOMNode(...) is deprecated. Please use ' + 'ReactDOM.findDOMNode(instance) instead.', ReactInstanceMap.get(this).getName() || this.tagName || 'Unknown') : undefined;
+ this.constructor[didWarnKey] = true;
+ return findDOMNode(this);
+ }
+};
+
+module.exports = ReactBrowserComponentMixin;
+},{"122":122,"173":173,"68":68}],28:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactBrowserEventEmitter
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+var EventPluginRegistry = _dereq_(17);
+var ReactEventEmitterMixin = _dereq_(62);
+var ReactPerf = _dereq_(78);
+var ViewportMetrics = _dereq_(114);
+
+var assign = _dereq_(24);
+var isEventSupported = _dereq_(133);
+
+/**
+ * Summary of `ReactBrowserEventEmitter` event handling:
+ *
+ * - Top-level delegation is used to trap most native browser events. This
+ * may only occur in the main thread and is the responsibility of
+ * ReactEventListener, which is injected and can therefore support pluggable
+ * event sources. This is the only work that occurs in the main thread.
+ *
+ * - We normalize and de-duplicate events to account for browser quirks. This
+ * may be done in the worker thread.
+ *
+ * - Forward these native events (with the associated top-level type used to
+ * trap it) to `EventPluginHub`, which in turn will ask plugins if they want
+ * to extract any synthetic events.
+ *
+ * - The `EventPluginHub` will then process each event by annotating them with
+ * "dispatches", a sequence of listeners and IDs that care about that event.
+ *
+ * - The `EventPluginHub` then dispatches the events.
+ *
+ * Overview of React and the event system:
+ *
+ * +------------+ .
+ * | DOM | .
+ * +------------+ .
+ * | .
+ * v .
+ * +------------+ .
+ * | ReactEvent | .
+ * | Listener | .
+ * +------------+ . +-----------+
+ * | . +--------+|SimpleEvent|
+ * | . | |Plugin |
+ * +-----|------+ . v +-----------+
+ * | | | . +--------------+ +------------+
+ * | +-----------.--->|EventPluginHub| | Event |
+ * | | . | | +-----------+ | Propagators|
+ * | ReactEvent | . | | |TapEvent | |------------|
+ * | Emitter | . | |<---+|Plugin | |other plugin|
+ * | | . | | +-----------+ | utilities |
+ * | +-----------.--->| | +------------+
+ * | | | . +--------------+
+ * +-----|------+ . ^ +-----------+
+ * | . | |Enter/Leave|
+ * + . +-------+|Plugin |
+ * +-------------+ . +-----------+
+ * | application | .
+ * |-------------| .
+ * | | .
+ * | | .
+ * +-------------+ .
+ * .
+ * React Core . General Purpose Event Plugin System
+ */
+
+var alreadyListeningTo = {};
+var isMonitoringScrollValue = false;
+var reactTopListenersCounter = 0;
+
+// For events like 'submit' which don't consistently bubble (which we trap at a
+// lower node than `document`), binding at `document` would cause duplicate
+// events so we don't include them here
+var topEventMapping = {
+ topAbort: 'abort',
+ topBlur: 'blur',
+ topCanPlay: 'canplay',
+ topCanPlayThrough: 'canplaythrough',
+ topChange: 'change',
+ topClick: 'click',
+ topCompositionEnd: 'compositionend',
+ topCompositionStart: 'compositionstart',
+ topCompositionUpdate: 'compositionupdate',
+ topContextMenu: 'contextmenu',
+ topCopy: 'copy',
+ topCut: 'cut',
+ topDoubleClick: 'dblclick',
+ topDrag: 'drag',
+ topDragEnd: 'dragend',
+ topDragEnter: 'dragenter',
+ topDragExit: 'dragexit',
+ topDragLeave: 'dragleave',
+ topDragOver: 'dragover',
+ topDragStart: 'dragstart',
+ topDrop: 'drop',
+ topDurationChange: 'durationchange',
+ topEmptied: 'emptied',
+ topEncrypted: 'encrypted',
+ topEnded: 'ended',
+ topError: 'error',
+ topFocus: 'focus',
+ topInput: 'input',
+ topKeyDown: 'keydown',
+ topKeyPress: 'keypress',
+ topKeyUp: 'keyup',
+ topLoadedData: 'loadeddata',
+ topLoadedMetadata: 'loadedmetadata',
+ topLoadStart: 'loadstart',
+ topMouseDown: 'mousedown',
+ topMouseMove: 'mousemove',
+ topMouseOut: 'mouseout',
+ topMouseOver: 'mouseover',
+ topMouseUp: 'mouseup',
+ topPaste: 'paste',
+ topPause: 'pause',
+ topPlay: 'play',
+ topPlaying: 'playing',
+ topProgress: 'progress',
+ topRateChange: 'ratechange',
+ topScroll: 'scroll',
+ topSeeked: 'seeked',
+ topSeeking: 'seeking',
+ topSelectionChange: 'selectionchange',
+ topStalled: 'stalled',
+ topSuspend: 'suspend',
+ topTextInput: 'textInput',
+ topTimeUpdate: 'timeupdate',
+ topTouchCancel: 'touchcancel',
+ topTouchEnd: 'touchend',
+ topTouchMove: 'touchmove',
+ topTouchStart: 'touchstart',
+ topVolumeChange: 'volumechange',
+ topWaiting: 'waiting',
+ topWheel: 'wheel'
+};
+
+/**
+ * To ensure no conflicts with other potential React instances on the page
+ */
+var topListenersIDKey = '_reactListenersID' + String(Math.random()).slice(2);
+
+function getListeningForDocument(mountAt) {
+ // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`
+ // directly.
+ if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) {
+ mountAt[topListenersIDKey] = reactTopListenersCounter++;
+ alreadyListeningTo[mountAt[topListenersIDKey]] = {};
+ }
+ return alreadyListeningTo[mountAt[topListenersIDKey]];
+}
+
+/**
+ * `ReactBrowserEventEmitter` is used to attach top-level event listeners. For
+ * example:
+ *
+ * ReactBrowserEventEmitter.putListener('myID', 'onClick', myFunction);
+ *
+ * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
+ *
+ * @internal
+ */
+var ReactBrowserEventEmitter = assign({}, ReactEventEmitterMixin, {
+
+ /**
+ * Injectable event backend
+ */
+ ReactEventListener: null,
+
+ injection: {
+ /**
+ * @param {object} ReactEventListener
+ */
+ injectReactEventListener: function (ReactEventListener) {
+ ReactEventListener.setHandleTopLevel(ReactBrowserEventEmitter.handleTopLevel);
+ ReactBrowserEventEmitter.ReactEventListener = ReactEventListener;
+ }
+ },
+
+ /**
+ * Sets whether or not any created callbacks should be enabled.
+ *
+ * @param {boolean} enabled True if callbacks should be enabled.
+ */
+ setEnabled: function (enabled) {
+ if (ReactBrowserEventEmitter.ReactEventListener) {
+ ReactBrowserEventEmitter.ReactEventListener.setEnabled(enabled);
+ }
+ },
+
+ /**
+ * @return {boolean} True if callbacks are enabled.
+ */
+ isEnabled: function () {
+ return !!(ReactBrowserEventEmitter.ReactEventListener && ReactBrowserEventEmitter.ReactEventListener.isEnabled());
+ },
+
+ /**
+ * We listen for bubbled touch events on the document object.
+ *
+ * Firefox v8.01 (and possibly others) exhibited strange behavior when
+ * mounting `onmousemove` events at some node that was not the document
+ * element. The symptoms were that if your mouse is not moving over something
+ * contained within that mount point (for example on the background) the
+ * top-level listeners for `onmousemove` won't be called. However, if you
+ * register the `mousemove` on the document object, then it will of course
+ * catch all `mousemove`s. This along with iOS quirks, justifies restricting
+ * top-level listeners to the document object only, at least for these
+ * movement types of events and possibly all events.
+ *
+ * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
+ *
+ * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
+ * they bubble to document.
+ *
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ * @param {object} contentDocumentHandle Document which owns the container
+ */
+ listenTo: function (registrationName, contentDocumentHandle) {
+ var mountAt = contentDocumentHandle;
+ var isListening = getListeningForDocument(mountAt);
+ var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];
+
+ var topLevelTypes = EventConstants.topLevelTypes;
+ for (var i = 0; i < dependencies.length; i++) {
+ var dependency = dependencies[i];
+ if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
+ if (dependency === topLevelTypes.topWheel) {
+ if (isEventSupported('wheel')) {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt);
+ } else if (isEventSupported('mousewheel')) {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt);
+ } else {
+ // Firefox needs to capture a different mouse scroll event.
+ // @see http://www.quirksmode.org/dom/events/tests/scroll.html
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt);
+ }
+ } else if (dependency === topLevelTypes.topScroll) {
+
+ if (isEventSupported('scroll', true)) {
+ ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt);
+ } else {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topScroll, 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE);
+ }
+ } else if (dependency === topLevelTypes.topFocus || dependency === topLevelTypes.topBlur) {
+
+ if (isEventSupported('focus', true)) {
+ ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt);
+ ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt);
+ } else if (isEventSupported('focusin')) {
+ // IE has `focusin` and `focusout` events which bubble.
+ // @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt);
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt);
+ }
+
+ // to make sure blur and focus event listeners are only attached once
+ isListening[topLevelTypes.topBlur] = true;
+ isListening[topLevelTypes.topFocus] = true;
+ } else if (topEventMapping.hasOwnProperty(dependency)) {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
+ }
+
+ isListening[dependency] = true;
+ }
+ }
+ },
+
+ trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
+ return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelType, handlerBaseName, handle);
+ },
+
+ trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
+ return ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelType, handlerBaseName, handle);
+ },
+
+ /**
+ * Listens to window scroll and resize events. We cache scroll values so that
+ * application code can access them without triggering reflows.
+ *
+ * NOTE: Scroll events do not bubble.
+ *
+ * @see http://www.quirksmode.org/dom/events/scroll.html
+ */
+ ensureScrollValueMonitoring: function () {
+ if (!isMonitoringScrollValue) {
+ var refresh = ViewportMetrics.refreshScrollValues;
+ ReactBrowserEventEmitter.ReactEventListener.monitorScrollValue(refresh);
+ isMonitoringScrollValue = true;
+ }
+ },
+
+ eventNameDispatchConfigs: EventPluginHub.eventNameDispatchConfigs,
+
+ registrationNameModules: EventPluginHub.registrationNameModules,
+
+ putListener: EventPluginHub.putListener,
+
+ getListener: EventPluginHub.getListener,
+
+ deleteListener: EventPluginHub.deleteListener,
+
+ deleteAllListeners: EventPluginHub.deleteAllListeners
+
+});
+
+ReactPerf.measureMethods(ReactBrowserEventEmitter, 'ReactBrowserEventEmitter', {
+ putListener: 'putListener',
+ deleteListener: 'deleteListener'
+});
+
+module.exports = ReactBrowserEventEmitter;
+},{"114":114,"133":133,"15":15,"16":16,"17":17,"24":24,"62":62,"78":78}],29:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks
+ * @providesModule ReactCSSTransitionGroup
+ */
+
+'use strict';
+
+var React = _dereq_(26);
+
+var assign = _dereq_(24);
+
+var ReactTransitionGroup = _dereq_(94);
+var ReactCSSTransitionGroupChild = _dereq_(30);
+
+function createTransitionTimeoutPropValidator(transitionType) {
+ var timeoutPropName = 'transition' + transitionType + 'Timeout';
+ var enabledPropName = 'transition' + transitionType;
+
+ return function (props) {
+ // If the transition is enabled
+ if (props[enabledPropName]) {
+ // If no timeout duration is provided
+ if (props[timeoutPropName] == null) {
+ return new Error(timeoutPropName + ' wasn\'t supplied to ReactCSSTransitionGroup: ' + 'this can cause unreliable animations and won\'t be supported in ' + 'a future version of React. See ' + 'https://fb.me/react-animation-transition-group-timeout for more ' + 'information.');
+
+ // If the duration isn't a number
+ } else if (typeof props[timeoutPropName] !== 'number') {
+ return new Error(timeoutPropName + ' must be a number (in milliseconds)');
+ }
+ }
+ };
+}
+
+var ReactCSSTransitionGroup = React.createClass({
+ displayName: 'ReactCSSTransitionGroup',
+
+ propTypes: {
+ transitionName: ReactCSSTransitionGroupChild.propTypes.name,
+
+ transitionAppear: React.PropTypes.bool,
+ transitionEnter: React.PropTypes.bool,
+ transitionLeave: React.PropTypes.bool,
+ transitionAppearTimeout: createTransitionTimeoutPropValidator('Appear'),
+ transitionEnterTimeout: createTransitionTimeoutPropValidator('Enter'),
+ transitionLeaveTimeout: createTransitionTimeoutPropValidator('Leave')
+ },
+
+ getDefaultProps: function () {
+ return {
+ transitionAppear: false,
+ transitionEnter: true,
+ transitionLeave: true
+ };
+ },
+
+ _wrapChild: function (child) {
+ // We need to provide this childFactory so that
+ // ReactCSSTransitionGroupChild can receive updates to name, enter, and
+ // leave while it is leaving.
+ return React.createElement(ReactCSSTransitionGroupChild, {
+ name: this.props.transitionName,
+ appear: this.props.transitionAppear,
+ enter: this.props.transitionEnter,
+ leave: this.props.transitionLeave,
+ appearTimeout: this.props.transitionAppearTimeout,
+ enterTimeout: this.props.transitionEnterTimeout,
+ leaveTimeout: this.props.transitionLeaveTimeout
+ }, child);
+ },
+
+ render: function () {
+ return React.createElement(ReactTransitionGroup, assign({}, this.props, { childFactory: this._wrapChild }));
+ }
+});
+
+module.exports = ReactCSSTransitionGroup;
+},{"24":24,"26":26,"30":30,"94":94}],30:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks
+ * @providesModule ReactCSSTransitionGroupChild
+ */
+
+'use strict';
+
+var React = _dereq_(26);
+var ReactDOM = _dereq_(40);
+
+var CSSCore = _dereq_(145);
+var ReactTransitionEvents = _dereq_(93);
+
+var onlyChild = _dereq_(135);
+
+// We don't remove the element from the DOM until we receive an animationend or
+// transitionend event. If the user screws up and forgets to add an animation
+// their node will be stuck in the DOM forever, so we detect if an animation
+// does not start and if it doesn't, we just call the end listener immediately.
+var TICK = 17;
+
+var ReactCSSTransitionGroupChild = React.createClass({
+ displayName: 'ReactCSSTransitionGroupChild',
+
+ propTypes: {
+ name: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.shape({
+ enter: React.PropTypes.string,
+ leave: React.PropTypes.string,
+ active: React.PropTypes.string
+ }), React.PropTypes.shape({
+ enter: React.PropTypes.string,
+ enterActive: React.PropTypes.string,
+ leave: React.PropTypes.string,
+ leaveActive: React.PropTypes.string,
+ appear: React.PropTypes.string,
+ appearActive: React.PropTypes.string
+ })]).isRequired,
+
+ // Once we require timeouts to be specified, we can remove the
+ // boolean flags (appear etc.) and just accept a number
+ // or a bool for the timeout flags (appearTimeout etc.)
+ appear: React.PropTypes.bool,
+ enter: React.PropTypes.bool,
+ leave: React.PropTypes.bool,
+ appearTimeout: React.PropTypes.number,
+ enterTimeout: React.PropTypes.number,
+ leaveTimeout: React.PropTypes.number
+ },
+
+ transition: function (animationType, finishCallback, userSpecifiedDelay) {
+ var node = ReactDOM.findDOMNode(this);
+
+ if (!node) {
+ if (finishCallback) {
+ finishCallback();
+ }
+ return;
+ }
+
+ var className = this.props.name[animationType] || this.props.name + '-' + animationType;
+ var activeClassName = this.props.name[animationType + 'Active'] || className + '-active';
+ var timeout = null;
+
+ var endListener = function (e) {
+ if (e && e.target !== node) {
+ return;
+ }
+
+ clearTimeout(timeout);
+
+ CSSCore.removeClass(node, className);
+ CSSCore.removeClass(node, activeClassName);
+
+ ReactTransitionEvents.removeEndEventListener(node, endListener);
+
+ // Usually this optional callback is used for informing an owner of
+ // a leave animation and telling it to remove the child.
+ if (finishCallback) {
+ finishCallback();
+ }
+ };
+
+ CSSCore.addClass(node, className);
+
+ // Need to do this to actually trigger a transition.
+ this.queueClass(activeClassName);
+
+ // If the user specified a timeout delay.
+ if (userSpecifiedDelay) {
+ // Clean-up the animation after the specified delay
+ timeout = setTimeout(endListener, userSpecifiedDelay);
+ this.transitionTimeouts.push(timeout);
+ } else {
+ // DEPRECATED: this listener will be removed in a future version of react
+ ReactTransitionEvents.addEndEventListener(node, endListener);
+ }
+ },
+
+ queueClass: function (className) {
+ this.classNameQueue.push(className);
+
+ if (!this.timeout) {
+ this.timeout = setTimeout(this.flushClassNameQueue, TICK);
+ }
+ },
+
+ flushClassNameQueue: function () {
+ if (this.isMounted()) {
+ this.classNameQueue.forEach(CSSCore.addClass.bind(CSSCore, ReactDOM.findDOMNode(this)));
+ }
+ this.classNameQueue.length = 0;
+ this.timeout = null;
+ },
+
+ componentWillMount: function () {
+ this.classNameQueue = [];
+ this.transitionTimeouts = [];
+ },
+
+ componentWillUnmount: function () {
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+ this.transitionTimeouts.forEach(function (timeout) {
+ clearTimeout(timeout);
+ });
+ },
+
+ componentWillAppear: function (done) {
+ if (this.props.appear) {
+ this.transition('appear', done, this.props.appearTimeout);
+ } else {
+ done();
+ }
+ },
+
+ componentWillEnter: function (done) {
+ if (this.props.enter) {
+ this.transition('enter', done, this.props.enterTimeout);
+ } else {
+ done();
+ }
+ },
+
+ componentWillLeave: function (done) {
+ if (this.props.leave) {
+ this.transition('leave', done, this.props.leaveTimeout);
+ } else {
+ done();
+ }
+ },
+
+ render: function () {
+ return onlyChild(this.props.children);
+ }
+});
+
+module.exports = ReactCSSTransitionGroupChild;
+},{"135":135,"145":145,"26":26,"40":40,"93":93}],31:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactChildReconciler
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactReconciler = _dereq_(84);
+
+var instantiateReactComponent = _dereq_(132);
+var shouldUpdateReactComponent = _dereq_(141);
+var traverseAllChildren = _dereq_(142);
+var warning = _dereq_(173);
+
+function instantiateChild(childInstances, child, name) {
+ // We found a component instance.
+ var keyUnique = childInstances[name] === undefined;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(keyUnique, 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.', name) : undefined;
+ }
+ if (child != null && keyUnique) {
+ childInstances[name] = instantiateReactComponent(child, null);
+ }
+}
+
+/**
+ * ReactChildReconciler provides helpers for initializing or updating a set of
+ * children. Its output is suitable for passing it onto ReactMultiChild which
+ * does diffed reordering and insertion.
+ */
+var ReactChildReconciler = {
+ /**
+ * Generates a "mount image" for each of the supplied children. In the case
+ * of `ReactDOMComponent`, a mount image is a string of markup.
+ *
+ * @param {?object} nestedChildNodes Nested child maps.
+ * @return {?object} A set of child instances.
+ * @internal
+ */
+ instantiateChildren: function (nestedChildNodes, transaction, context) {
+ if (nestedChildNodes == null) {
+ return null;
+ }
+ var childInstances = {};
+ traverseAllChildren(nestedChildNodes, instantiateChild, childInstances);
+ return childInstances;
+ },
+
+ /**
+ * Updates the rendered children and returns a new set of children.
+ *
+ * @param {?object} prevChildren Previously initialized set of children.
+ * @param {?object} nextChildren Flat child element maps.
+ * @param {ReactReconcileTransaction} transaction
+ * @param {object} context
+ * @return {?object} A new set of child instances.
+ * @internal
+ */
+ updateChildren: function (prevChildren, nextChildren, transaction, context) {
+ // We currently don't have a way to track moves here but if we use iterators
+ // instead of for..in we can zip the iterators and check if an item has
+ // moved.
+ // TODO: If nothing has changed, return the prevChildren object so that we
+ // can quickly bailout if nothing has changed.
+ if (!nextChildren && !prevChildren) {
+ return null;
+ }
+ var name;
+ for (name in nextChildren) {
+ if (!nextChildren.hasOwnProperty(name)) {
+ continue;
+ }
+ var prevChild = prevChildren && prevChildren[name];
+ var prevElement = prevChild && prevChild._currentElement;
+ var nextElement = nextChildren[name];
+ if (prevChild != null && shouldUpdateReactComponent(prevElement, nextElement)) {
+ ReactReconciler.receiveComponent(prevChild, nextElement, transaction, context);
+ nextChildren[name] = prevChild;
+ } else {
+ if (prevChild) {
+ ReactReconciler.unmountComponent(prevChild, name);
+ }
+ // The child must be instantiated before it's mounted.
+ var nextChildInstance = instantiateReactComponent(nextElement, null);
+ nextChildren[name] = nextChildInstance;
+ }
+ }
+ // Unmount children that are no longer present.
+ for (name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
+ ReactReconciler.unmountComponent(prevChildren[name]);
+ }
+ }
+ return nextChildren;
+ },
+
+ /**
+ * Unmounts all rendered children. This should be used to clean up children
+ * when this component is unmounted.
+ *
+ * @param {?object} renderedChildren Previously initialized set of children.
+ * @internal
+ */
+ unmountChildren: function (renderedChildren) {
+ for (var name in renderedChildren) {
+ if (renderedChildren.hasOwnProperty(name)) {
+ var renderedChild = renderedChildren[name];
+ ReactReconciler.unmountComponent(renderedChild);
+ }
+ }
+ }
+
+};
+
+module.exports = ReactChildReconciler;
+},{"132":132,"141":141,"142":142,"173":173,"84":84}],32:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactChildren
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+var ReactElement = _dereq_(57);
+
+var emptyFunction = _dereq_(153);
+var traverseAllChildren = _dereq_(142);
+
+var twoArgumentPooler = PooledClass.twoArgumentPooler;
+var fourArgumentPooler = PooledClass.fourArgumentPooler;
+
+var userProvidedKeyEscapeRegex = /\/(?!\/)/g;
+function escapeUserProvidedKey(text) {
+ return ('' + text).replace(userProvidedKeyEscapeRegex, '//');
+}
+
+/**
+ * PooledClass representing the bookkeeping associated with performing a child
+ * traversal. Allows avoiding binding callbacks.
+ *
+ * @constructor ForEachBookKeeping
+ * @param {!function} forEachFunction Function to perform traversal with.
+ * @param {?*} forEachContext Context to perform context with.
+ */
+function ForEachBookKeeping(forEachFunction, forEachContext) {
+ this.func = forEachFunction;
+ this.context = forEachContext;
+ this.count = 0;
+}
+ForEachBookKeeping.prototype.destructor = function () {
+ this.func = null;
+ this.context = null;
+ this.count = 0;
+};
+PooledClass.addPoolingTo(ForEachBookKeeping, twoArgumentPooler);
+
+function forEachSingleChild(bookKeeping, child, name) {
+ var func = bookKeeping.func;
+ var context = bookKeeping.context;
+
+ func.call(context, child, bookKeeping.count++);
+}
+
+/**
+ * Iterates through children that are typically specified as `props.children`.
+ *
+ * The provided forEachFunc(child, index) will be called for each
+ * leaf child.
+ *
+ * @param {?*} children Children tree container.
+ * @param {function(*, int)} forEachFunc
+ * @param {*} forEachContext Context for forEachContext.
+ */
+function forEachChildren(children, forEachFunc, forEachContext) {
+ if (children == null) {
+ return children;
+ }
+ var traverseContext = ForEachBookKeeping.getPooled(forEachFunc, forEachContext);
+ traverseAllChildren(children, forEachSingleChild, traverseContext);
+ ForEachBookKeeping.release(traverseContext);
+}
+
+/**
+ * PooledClass representing the bookkeeping associated with performing a child
+ * mapping. Allows avoiding binding callbacks.
+ *
+ * @constructor MapBookKeeping
+ * @param {!*} mapResult Object containing the ordered map of results.
+ * @param {!function} mapFunction Function to perform mapping with.
+ * @param {?*} mapContext Context to perform mapping with.
+ */
+function MapBookKeeping(mapResult, keyPrefix, mapFunction, mapContext) {
+ this.result = mapResult;
+ this.keyPrefix = keyPrefix;
+ this.func = mapFunction;
+ this.context = mapContext;
+ this.count = 0;
+}
+MapBookKeeping.prototype.destructor = function () {
+ this.result = null;
+ this.keyPrefix = null;
+ this.func = null;
+ this.context = null;
+ this.count = 0;
+};
+PooledClass.addPoolingTo(MapBookKeeping, fourArgumentPooler);
+
+function mapSingleChildIntoContext(bookKeeping, child, childKey) {
+ var result = bookKeeping.result;
+ var keyPrefix = bookKeeping.keyPrefix;
+ var func = bookKeeping.func;
+ var context = bookKeeping.context;
+
+ var mappedChild = func.call(context, child, bookKeeping.count++);
+ if (Array.isArray(mappedChild)) {
+ mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, emptyFunction.thatReturnsArgument);
+ } else if (mappedChild != null) {
+ if (ReactElement.isValidElement(mappedChild)) {
+ mappedChild = ReactElement.cloneAndReplaceKey(mappedChild,
+ // Keep both the (mapped) and old keys if they differ, just as
+ // traverseAllChildren used to do for objects as children
+ keyPrefix + (mappedChild !== child ? escapeUserProvidedKey(mappedChild.key || '') + '/' : '') + childKey);
+ }
+ result.push(mappedChild);
+ }
+}
+
+function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
+ var escapedPrefix = '';
+ if (prefix != null) {
+ escapedPrefix = escapeUserProvidedKey(prefix) + '/';
+ }
+ var traverseContext = MapBookKeeping.getPooled(array, escapedPrefix, func, context);
+ traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
+ MapBookKeeping.release(traverseContext);
+}
+
+/**
+ * Maps children that are typically specified as `props.children`.
+ *
+ * The provided mapFunction(child, key, index) will be called for each
+ * leaf child.
+ *
+ * @param {?*} children Children tree container.
+ * @param {function(*, int)} func The map function.
+ * @param {*} context Context for mapFunction.
+ * @return {object} Object containing the ordered map of results.
+ */
+function mapChildren(children, func, context) {
+ if (children == null) {
+ return children;
+ }
+ var result = [];
+ mapIntoWithKeyPrefixInternal(children, result, null, func, context);
+ return result;
+}
+
+function forEachSingleChildDummy(traverseContext, child, name) {
+ return null;
+}
+
+/**
+ * Count the number of children that are typically specified as
+ * `props.children`.
+ *
+ * @param {?*} children Children tree container.
+ * @return {number} The number of children.
+ */
+function countChildren(children, context) {
+ return traverseAllChildren(children, forEachSingleChildDummy, null);
+}
+
+/**
+ * Flatten a children object (typically specified as `props.children`) and
+ * return an array with appropriately re-keyed children.
+ */
+function toArray(children) {
+ var result = [];
+ mapIntoWithKeyPrefixInternal(children, result, null, emptyFunction.thatReturnsArgument);
+ return result;
+}
+
+var ReactChildren = {
+ forEach: forEachChildren,
+ map: mapChildren,
+ mapIntoWithKeyPrefixInternal: mapIntoWithKeyPrefixInternal,
+ count: countChildren,
+ toArray: toArray
+};
+
+module.exports = ReactChildren;
+},{"142":142,"153":153,"25":25,"57":57}],33:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactClass
+ */
+
+'use strict';
+
+var ReactComponent = _dereq_(34);
+var ReactElement = _dereq_(57);
+var ReactPropTypeLocations = _dereq_(81);
+var ReactPropTypeLocationNames = _dereq_(80);
+var ReactNoopUpdateQueue = _dereq_(76);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var invariant = _dereq_(161);
+var keyMirror = _dereq_(165);
+var keyOf = _dereq_(166);
+var warning = _dereq_(173);
+
+var MIXINS_KEY = keyOf({ mixins: null });
+
+/**
+ * Policies that describe methods in `ReactClassInterface`.
+ */
+var SpecPolicy = keyMirror({
+ /**
+ * These methods may be defined only once by the class specification or mixin.
+ */
+ DEFINE_ONCE: null,
+ /**
+ * These methods may be defined by both the class specification and mixins.
+ * Subsequent definitions will be chained. These methods must return void.
+ */
+ DEFINE_MANY: null,
+ /**
+ * These methods are overriding the base class.
+ */
+ OVERRIDE_BASE: null,
+ /**
+ * These methods are similar to DEFINE_MANY, except we assume they return
+ * objects. We try to merge the keys of the return values of all the mixed in
+ * functions. If there is a key conflict we throw.
+ */
+ DEFINE_MANY_MERGED: null
+});
+
+var injectedMixins = [];
+
+var warnedSetProps = false;
+function warnSetProps() {
+ if (!warnedSetProps) {
+ warnedSetProps = true;
+ "development" !== 'production' ? warning(false, 'setProps(...) and replaceProps(...) are deprecated. ' + 'Instead, call render again at the top level.') : undefined;
+ }
+}
+
+/**
+ * Composite components are higher-level components that compose other composite
+ * or native components.
+ *
+ * To create a new type of `ReactClass`, pass a specification of
+ * your new class to `React.createClass`. The only requirement of your class
+ * specification is that you implement a `render` method.
+ *
+ * var MyComponent = React.createClass({
+ * render: function() {
+ * return <div>Hello World</div>;
+ * }
+ * });
+ *
+ * The class specification supports a specific protocol of methods that have
+ * special meaning (e.g. `render`). See `ReactClassInterface` for
+ * more the comprehensive protocol. Any other properties and methods in the
+ * class specification will be available on the prototype.
+ *
+ * @interface ReactClassInterface
+ * @internal
+ */
+var ReactClassInterface = {
+
+ /**
+ * An array of Mixin objects to include when defining your component.
+ *
+ * @type {array}
+ * @optional
+ */
+ mixins: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * An object containing properties and methods that should be defined on
+ * the component's constructor instead of its prototype (static methods).
+ *
+ * @type {object}
+ * @optional
+ */
+ statics: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Definition of prop types for this component.
+ *
+ * @type {object}
+ * @optional
+ */
+ propTypes: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Definition of context types for this component.
+ *
+ * @type {object}
+ * @optional
+ */
+ contextTypes: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Definition of context types this component sets for its children.
+ *
+ * @type {object}
+ * @optional
+ */
+ childContextTypes: SpecPolicy.DEFINE_MANY,
+
+ // ==== Definition methods ====
+
+ /**
+ * Invoked when the component is mounted. Values in the mapping will be set on
+ * `this.props` if that prop is not specified (i.e. using an `in` check).
+ *
+ * This method is invoked before `getInitialState` and therefore cannot rely
+ * on `this.state` or use `this.setState`.
+ *
+ * @return {object}
+ * @optional
+ */
+ getDefaultProps: SpecPolicy.DEFINE_MANY_MERGED,
+
+ /**
+ * Invoked once before the component is mounted. The return value will be used
+ * as the initial value of `this.state`.
+ *
+ * getInitialState: function() {
+ * return {
+ * isOn: false,
+ * fooBaz: new BazFoo()
+ * }
+ * }
+ *
+ * @return {object}
+ * @optional
+ */
+ getInitialState: SpecPolicy.DEFINE_MANY_MERGED,
+
+ /**
+ * @return {object}
+ * @optional
+ */
+ getChildContext: SpecPolicy.DEFINE_MANY_MERGED,
+
+ /**
+ * Uses props from `this.props` and state from `this.state` to render the
+ * structure of the component.
+ *
+ * No guarantees are made about when or how often this method is invoked, so
+ * it must not have side effects.
+ *
+ * render: function() {
+ * var name = this.props.name;
+ * return <div>Hello, {name}!</div>;
+ * }
+ *
+ * @return {ReactComponent}
+ * @nosideeffects
+ * @required
+ */
+ render: SpecPolicy.DEFINE_ONCE,
+
+ // ==== Delegate methods ====
+
+ /**
+ * Invoked when the component is initially created and about to be mounted.
+ * This may have side effects, but any external subscriptions or data created
+ * by this method must be cleaned up in `componentWillUnmount`.
+ *
+ * @optional
+ */
+ componentWillMount: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked when the component has been mounted and has a DOM representation.
+ * However, there is no guarantee that the DOM node is in the document.
+ *
+ * Use this as an opportunity to operate on the DOM when the component has
+ * been mounted (initialized and rendered) for the first time.
+ *
+ * @param {DOMElement} rootNode DOM element representing the component.
+ * @optional
+ */
+ componentDidMount: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked before the component receives new props.
+ *
+ * Use this as an opportunity to react to a prop transition by updating the
+ * state using `this.setState`. Current props are accessed via `this.props`.
+ *
+ * componentWillReceiveProps: function(nextProps, nextContext) {
+ * this.setState({
+ * likesIncreasing: nextProps.likeCount > this.props.likeCount
+ * });
+ * }
+ *
+ * NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop
+ * transition may cause a state change, but the opposite is not true. If you
+ * need it, you are probably looking for `componentWillUpdate`.
+ *
+ * @param {object} nextProps
+ * @optional
+ */
+ componentWillReceiveProps: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked while deciding if the component should be updated as a result of
+ * receiving new props, state and/or context.
+ *
+ * Use this as an opportunity to `return false` when you're certain that the
+ * transition to the new props/state/context will not require a component
+ * update.
+ *
+ * shouldComponentUpdate: function(nextProps, nextState, nextContext) {
+ * return !equal(nextProps, this.props) ||
+ * !equal(nextState, this.state) ||
+ * !equal(nextContext, this.context);
+ * }
+ *
+ * @param {object} nextProps
+ * @param {?object} nextState
+ * @param {?object} nextContext
+ * @return {boolean} True if the component should update.
+ * @optional
+ */
+ shouldComponentUpdate: SpecPolicy.DEFINE_ONCE,
+
+ /**
+ * Invoked when the component is about to update due to a transition from
+ * `this.props`, `this.state` and `this.context` to `nextProps`, `nextState`
+ * and `nextContext`.
+ *
+ * Use this as an opportunity to perform preparation before an update occurs.
+ *
+ * NOTE: You **cannot** use `this.setState()` in this method.
+ *
+ * @param {object} nextProps
+ * @param {?object} nextState
+ * @param {?object} nextContext
+ * @param {ReactReconcileTransaction} transaction
+ * @optional
+ */
+ componentWillUpdate: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked when the component's DOM representation has been updated.
+ *
+ * Use this as an opportunity to operate on the DOM when the component has
+ * been updated.
+ *
+ * @param {object} prevProps
+ * @param {?object} prevState
+ * @param {?object} prevContext
+ * @param {DOMElement} rootNode DOM element representing the component.
+ * @optional
+ */
+ componentDidUpdate: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked when the component is about to be removed from its parent and have
+ * its DOM representation destroyed.
+ *
+ * Use this as an opportunity to deallocate any external resources.
+ *
+ * NOTE: There is no `componentDidUnmount` since your component will have been
+ * destroyed by that point.
+ *
+ * @optional
+ */
+ componentWillUnmount: SpecPolicy.DEFINE_MANY,
+
+ // ==== Advanced methods ====
+
+ /**
+ * Updates the component's currently mounted DOM representation.
+ *
+ * By default, this implements React's rendering and reconciliation algorithm.
+ * Sophisticated clients may wish to override this.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ * @overridable
+ */
+ updateComponent: SpecPolicy.OVERRIDE_BASE
+
+};
+
+/**
+ * Mapping from class specification keys to special processing functions.
+ *
+ * Although these are declared like instance properties in the specification
+ * when defining classes using `React.createClass`, they are actually static
+ * and are accessible on the constructor instead of the prototype. Despite
+ * being static, they must be defined outside of the "statics" key under
+ * which all other static methods are defined.
+ */
+var RESERVED_SPEC_KEYS = {
+ displayName: function (Constructor, displayName) {
+ Constructor.displayName = displayName;
+ },
+ mixins: function (Constructor, mixins) {
+ if (mixins) {
+ for (var i = 0; i < mixins.length; i++) {
+ mixSpecIntoComponent(Constructor, mixins[i]);
+ }
+ }
+ },
+ childContextTypes: function (Constructor, childContextTypes) {
+ if ("development" !== 'production') {
+ validateTypeDef(Constructor, childContextTypes, ReactPropTypeLocations.childContext);
+ }
+ Constructor.childContextTypes = assign({}, Constructor.childContextTypes, childContextTypes);
+ },
+ contextTypes: function (Constructor, contextTypes) {
+ if ("development" !== 'production') {
+ validateTypeDef(Constructor, contextTypes, ReactPropTypeLocations.context);
+ }
+ Constructor.contextTypes = assign({}, Constructor.contextTypes, contextTypes);
+ },
+ /**
+ * Special case getDefaultProps which should move into statics but requires
+ * automatic merging.
+ */
+ getDefaultProps: function (Constructor, getDefaultProps) {
+ if (Constructor.getDefaultProps) {
+ Constructor.getDefaultProps = createMergedResultFunction(Constructor.getDefaultProps, getDefaultProps);
+ } else {
+ Constructor.getDefaultProps = getDefaultProps;
+ }
+ },
+ propTypes: function (Constructor, propTypes) {
+ if ("development" !== 'production') {
+ validateTypeDef(Constructor, propTypes, ReactPropTypeLocations.prop);
+ }
+ Constructor.propTypes = assign({}, Constructor.propTypes, propTypes);
+ },
+ statics: function (Constructor, statics) {
+ mixStaticSpecIntoComponent(Constructor, statics);
+ },
+ autobind: function () {} };
+
+// noop
+function validateTypeDef(Constructor, typeDef, location) {
+ for (var propName in typeDef) {
+ if (typeDef.hasOwnProperty(propName)) {
+ // use a warning instead of an invariant so components
+ // don't show up in prod but not in __DEV__
+ "development" !== 'production' ? warning(typeof typeDef[propName] === 'function', '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', Constructor.displayName || 'ReactClass', ReactPropTypeLocationNames[location], propName) : undefined;
+ }
+ }
+}
+
+function validateMethodOverride(proto, name) {
+ var specPolicy = ReactClassInterface.hasOwnProperty(name) ? ReactClassInterface[name] : null;
+
+ // Disallow overriding of base class methods unless explicitly allowed.
+ if (ReactClassMixin.hasOwnProperty(name)) {
+ !(specPolicy === SpecPolicy.OVERRIDE_BASE) ? "development" !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to override ' + '`%s` from your class specification. Ensure that your method names ' + 'do not overlap with React methods.', name) : invariant(false) : undefined;
+ }
+
+ // Disallow defining methods more than once unless explicitly allowed.
+ if (proto.hasOwnProperty(name)) {
+ !(specPolicy === SpecPolicy.DEFINE_MANY || specPolicy === SpecPolicy.DEFINE_MANY_MERGED) ? "development" !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to define ' + '`%s` on your component more than once. This conflict may be due ' + 'to a mixin.', name) : invariant(false) : undefined;
+ }
+}
+
+/**
+ * Mixin helper which handles policy validation and reserved
+ * specification keys when building React classses.
+ */
+function mixSpecIntoComponent(Constructor, spec) {
+ if (!spec) {
+ return;
+ }
+
+ !(typeof spec !== 'function') ? "development" !== 'production' ? invariant(false, 'ReactClass: You\'re attempting to ' + 'use a component class as a mixin. Instead, just use a regular object.') : invariant(false) : undefined;
+ !!ReactElement.isValidElement(spec) ? "development" !== 'production' ? invariant(false, 'ReactClass: You\'re attempting to ' + 'use a component as a mixin. Instead, just use a regular object.') : invariant(false) : undefined;
+
+ var proto = Constructor.prototype;
+
+ // By handling mixins before any other properties, we ensure the same
+ // chaining order is applied to methods with DEFINE_MANY policy, whether
+ // mixins are listed before or after these methods in the spec.
+ if (spec.hasOwnProperty(MIXINS_KEY)) {
+ RESERVED_SPEC_KEYS.mixins(Constructor, spec.mixins);
+ }
+
+ for (var name in spec) {
+ if (!spec.hasOwnProperty(name)) {
+ continue;
+ }
+
+ if (name === MIXINS_KEY) {
+ // We have already handled mixins in a special case above.
+ continue;
+ }
+
+ var property = spec[name];
+ validateMethodOverride(proto, name);
+
+ if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) {
+ RESERVED_SPEC_KEYS[name](Constructor, property);
+ } else {
+ // Setup methods on prototype:
+ // The following member methods should not be automatically bound:
+ // 1. Expected ReactClass methods (in the "interface").
+ // 2. Overridden methods (that were mixed in).
+ var isReactClassMethod = ReactClassInterface.hasOwnProperty(name);
+ var isAlreadyDefined = proto.hasOwnProperty(name);
+ var isFunction = typeof property === 'function';
+ var shouldAutoBind = isFunction && !isReactClassMethod && !isAlreadyDefined && spec.autobind !== false;
+
+ if (shouldAutoBind) {
+ if (!proto.__reactAutoBindMap) {
+ proto.__reactAutoBindMap = {};
+ }
+ proto.__reactAutoBindMap[name] = property;
+ proto[name] = property;
+ } else {
+ if (isAlreadyDefined) {
+ var specPolicy = ReactClassInterface[name];
+
+ // These cases should already be caught by validateMethodOverride.
+ !(isReactClassMethod && (specPolicy === SpecPolicy.DEFINE_MANY_MERGED || specPolicy === SpecPolicy.DEFINE_MANY)) ? "development" !== 'production' ? invariant(false, 'ReactClass: Unexpected spec policy %s for key %s ' + 'when mixing in component specs.', specPolicy, name) : invariant(false) : undefined;
+
+ // For methods which are defined more than once, call the existing
+ // methods before calling the new property, merging if appropriate.
+ if (specPolicy === SpecPolicy.DEFINE_MANY_MERGED) {
+ proto[name] = createMergedResultFunction(proto[name], property);
+ } else if (specPolicy === SpecPolicy.DEFINE_MANY) {
+ proto[name] = createChainedFunction(proto[name], property);
+ }
+ } else {
+ proto[name] = property;
+ if ("development" !== 'production') {
+ // Add verbose displayName to the function, which helps when looking
+ // at profiling tools.
+ if (typeof property === 'function' && spec.displayName) {
+ proto[name].displayName = spec.displayName + '_' + name;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+function mixStaticSpecIntoComponent(Constructor, statics) {
+ if (!statics) {
+ return;
+ }
+ for (var name in statics) {
+ var property = statics[name];
+ if (!statics.hasOwnProperty(name)) {
+ continue;
+ }
+
+ var isReserved = (name in RESERVED_SPEC_KEYS);
+ !!isReserved ? "development" !== 'production' ? invariant(false, 'ReactClass: You are attempting to define a reserved ' + 'property, `%s`, that shouldn\'t be on the "statics" key. Define it ' + 'as an instance property instead; it will still be accessible on the ' + 'constructor.', name) : invariant(false) : undefined;
+
+ var isInherited = (name in Constructor);
+ !!isInherited ? "development" !== 'production' ? invariant(false, 'ReactClass: You are attempting to define ' + '`%s` on your component more than once. This conflict may be ' + 'due to a mixin.', name) : invariant(false) : undefined;
+ Constructor[name] = property;
+ }
+}
+
+/**
+ * Merge two objects, but throw if both contain the same key.
+ *
+ * @param {object} one The first object, which is mutated.
+ * @param {object} two The second object
+ * @return {object} one after it has been mutated to contain everything in two.
+ */
+function mergeIntoWithNoDuplicateKeys(one, two) {
+ !(one && two && typeof one === 'object' && typeof two === 'object') ? "development" !== 'production' ? invariant(false, 'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.') : invariant(false) : undefined;
+
+ for (var key in two) {
+ if (two.hasOwnProperty(key)) {
+ !(one[key] === undefined) ? "development" !== 'production' ? invariant(false, 'mergeIntoWithNoDuplicateKeys(): ' + 'Tried to merge two objects with the same key: `%s`. This conflict ' + 'may be due to a mixin; in particular, this may be caused by two ' + 'getInitialState() or getDefaultProps() methods returning objects ' + 'with clashing keys.', key) : invariant(false) : undefined;
+ one[key] = two[key];
+ }
+ }
+ return one;
+}
+
+/**
+ * Creates a function that invokes two functions and merges their return values.
+ *
+ * @param {function} one Function to invoke first.
+ * @param {function} two Function to invoke second.
+ * @return {function} Function that invokes the two argument functions.
+ * @private
+ */
+function createMergedResultFunction(one, two) {
+ return function mergedResult() {
+ var a = one.apply(this, arguments);
+ var b = two.apply(this, arguments);
+ if (a == null) {
+ return b;
+ } else if (b == null) {
+ return a;
+ }
+ var c = {};
+ mergeIntoWithNoDuplicateKeys(c, a);
+ mergeIntoWithNoDuplicateKeys(c, b);
+ return c;
+ };
+}
+
+/**
+ * Creates a function that invokes two functions and ignores their return vales.
+ *
+ * @param {function} one Function to invoke first.
+ * @param {function} two Function to invoke second.
+ * @return {function} Function that invokes the two argument functions.
+ * @private
+ */
+function createChainedFunction(one, two) {
+ return function chainedFunction() {
+ one.apply(this, arguments);
+ two.apply(this, arguments);
+ };
+}
+
+/**
+ * Binds a method to the component.
+ *
+ * @param {object} component Component whose method is going to be bound.
+ * @param {function} method Method to be bound.
+ * @return {function} The bound method.
+ */
+function bindAutoBindMethod(component, method) {
+ var boundMethod = method.bind(component);
+ if ("development" !== 'production') {
+ boundMethod.__reactBoundContext = component;
+ boundMethod.__reactBoundMethod = method;
+ boundMethod.__reactBoundArguments = null;
+ var componentName = component.constructor.displayName;
+ var _bind = boundMethod.bind;
+ /* eslint-disable block-scoped-var, no-undef */
+ boundMethod.bind = function (newThis) {
+ for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ args[_key - 1] = arguments[_key];
+ }
+
+ // User is trying to bind() an autobound method; we effectively will
+ // ignore the value of "this" that the user is trying to use, so
+ // let's warn.
+ if (newThis !== component && newThis !== null) {
+ "development" !== 'production' ? warning(false, 'bind(): React component methods may only be bound to the ' + 'component instance. See %s', componentName) : undefined;
+ } else if (!args.length) {
+ "development" !== 'production' ? warning(false, 'bind(): You are binding a component method to the component. ' + 'React does this for you automatically in a high-performance ' + 'way, so you can safely remove this call. See %s', componentName) : undefined;
+ return boundMethod;
+ }
+ var reboundMethod = _bind.apply(boundMethod, arguments);
+ reboundMethod.__reactBoundContext = component;
+ reboundMethod.__reactBoundMethod = method;
+ reboundMethod.__reactBoundArguments = args;
+ return reboundMethod;
+ /* eslint-enable */
+ };
+ }
+ return boundMethod;
+}
+
+/**
+ * Binds all auto-bound methods in a component.
+ *
+ * @param {object} component Component whose method is going to be bound.
+ */
+function bindAutoBindMethods(component) {
+ for (var autoBindKey in component.__reactAutoBindMap) {
+ if (component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
+ var method = component.__reactAutoBindMap[autoBindKey];
+ component[autoBindKey] = bindAutoBindMethod(component, method);
+ }
+ }
+}
+
+/**
+ * Add more to the ReactClass base class. These are all legacy features and
+ * therefore not already part of the modern ReactComponent.
+ */
+var ReactClassMixin = {
+
+ /**
+ * TODO: This will be deprecated because state should always keep a consistent
+ * type signature and the only use case for this, is to avoid that.
+ */
+ replaceState: function (newState, callback) {
+ this.updater.enqueueReplaceState(this, newState);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+ },
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function () {
+ return this.updater.isMounted(this);
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {object} partialProps Subset of the next props.
+ * @param {?function} callback Called after props are updated.
+ * @final
+ * @public
+ * @deprecated
+ */
+ setProps: function (partialProps, callback) {
+ if ("development" !== 'production') {
+ warnSetProps();
+ }
+ this.updater.enqueueSetProps(this, partialProps);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+ },
+
+ /**
+ * Replace all the props.
+ *
+ * @param {object} newProps Subset of the next props.
+ * @param {?function} callback Called after props are updated.
+ * @final
+ * @public
+ * @deprecated
+ */
+ replaceProps: function (newProps, callback) {
+ if ("development" !== 'production') {
+ warnSetProps();
+ }
+ this.updater.enqueueReplaceProps(this, newProps);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+ }
+};
+
+var ReactClassComponent = function () {};
+assign(ReactClassComponent.prototype, ReactComponent.prototype, ReactClassMixin);
+
+/**
+ * Module for creating composite components.
+ *
+ * @class ReactClass
+ */
+var ReactClass = {
+
+ /**
+ * Creates a composite component class given a class specification.
+ *
+ * @param {object} spec Class specification (which must define `render`).
+ * @return {function} Component constructor function.
+ * @public
+ */
+ createClass: function (spec) {
+ var Constructor = function (props, context, updater) {
+ // This constructor is overridden by mocks. The argument is used
+ // by mocks to assert on what gets mounted.
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(this instanceof Constructor, 'Something is calling a React component directly. Use a factory or ' + 'JSX instead. See: https://fb.me/react-legacyfactory') : undefined;
+ }
+
+ // Wire up auto-binding
+ if (this.__reactAutoBindMap) {
+ bindAutoBindMethods(this);
+ }
+
+ this.props = props;
+ this.context = context;
+ this.refs = emptyObject;
+ this.updater = updater || ReactNoopUpdateQueue;
+
+ this.state = null;
+
+ // ReactClasses doesn't have constructors. Instead, they use the
+ // getInitialState and componentWillMount methods for initialization.
+
+ var initialState = this.getInitialState ? this.getInitialState() : null;
+ if ("development" !== 'production') {
+ // We allow auto-mocks to proceed as if they're returning null.
+ if (typeof initialState === 'undefined' && this.getInitialState._isMockFunction) {
+ // This is probably bad practice. Consider warning here and
+ // deprecating this convenience.
+ initialState = null;
+ }
+ }
+ !(typeof initialState === 'object' && !Array.isArray(initialState)) ? "development" !== 'production' ? invariant(false, '%s.getInitialState(): must return an object or null', Constructor.displayName || 'ReactCompositeComponent') : invariant(false) : undefined;
+
+ this.state = initialState;
+ };
+ Constructor.prototype = new ReactClassComponent();
+ Constructor.prototype.constructor = Constructor;
+
+ injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor));
+
+ mixSpecIntoComponent(Constructor, spec);
+
+ // Initialize the defaultProps property after all mixins have been merged.
+ if (Constructor.getDefaultProps) {
+ Constructor.defaultProps = Constructor.getDefaultProps();
+ }
+
+ if ("development" !== 'production') {
+ // This is a tag to indicate that the use of these method names is ok,
+ // since it's used with createClass. If it's not, then it's likely a
+ // mistake so we'll warn you to use the static property, property
+ // initializer or constructor respectively.
+ if (Constructor.getDefaultProps) {
+ Constructor.getDefaultProps.isReactClassApproved = {};
+ }
+ if (Constructor.prototype.getInitialState) {
+ Constructor.prototype.getInitialState.isReactClassApproved = {};
+ }
+ }
+
+ !Constructor.prototype.render ? "development" !== 'production' ? invariant(false, 'createClass(...): Class specification must implement a `render` method.') : invariant(false) : undefined;
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(!Constructor.prototype.componentShouldUpdate, '%s has a method called ' + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 'The name is phrased as a question because the function is ' + 'expected to return a value.', spec.displayName || 'A component') : undefined;
+ "development" !== 'production' ? warning(!Constructor.prototype.componentWillRecieveProps, '%s has a method called ' + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', spec.displayName || 'A component') : undefined;
+ }
+
+ // Reduce time spent doing lookups by setting these on the prototype.
+ for (var methodName in ReactClassInterface) {
+ if (!Constructor.prototype[methodName]) {
+ Constructor.prototype[methodName] = null;
+ }
+ }
+
+ return Constructor;
+ },
+
+ injection: {
+ injectMixin: function (mixin) {
+ injectedMixins.push(mixin);
+ }
+ }
+
+};
+
+module.exports = ReactClass;
+},{"154":154,"161":161,"165":165,"166":166,"173":173,"24":24,"34":34,"57":57,"76":76,"80":80,"81":81}],34:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponent
+ */
+
+'use strict';
+
+var ReactNoopUpdateQueue = _dereq_(76);
+
+var canDefineProperty = _dereq_(117);
+var emptyObject = _dereq_(154);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Base class helpers for the updating state of a component.
+ */
+function ReactComponent(props, context, updater) {
+ this.props = props;
+ this.context = context;
+ this.refs = emptyObject;
+ // We initialize the default updater but the real one gets injected by the
+ // renderer.
+ this.updater = updater || ReactNoopUpdateQueue;
+}
+
+ReactComponent.prototype.isReactComponent = {};
+
+/**
+ * Sets a subset of the state. Always use this to mutate
+ * state. You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * There is no guarantee that calls to `setState` will run synchronously,
+ * as they may eventually be batched together. You can provide an optional
+ * callback that will be executed when the call to setState is actually
+ * completed.
+ *
+ * When a function is provided to setState, it will be called at some point in
+ * the future (not synchronously). It will be called with the up to date
+ * component arguments (state, props, context). These values can be different
+ * from this.* because your function may be called after receiveProps but before
+ * shouldComponentUpdate, and this new state, props, and context will not yet be
+ * assigned to this.
+ *
+ * @param {object|function} partialState Next partial state or function to
+ * produce next partial state to be merged with current state.
+ * @param {?function} callback Called after state is updated.
+ * @final
+ * @protected
+ */
+ReactComponent.prototype.setState = function (partialState, callback) {
+ !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? "development" !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.') : invariant(false) : undefined;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(partialState != null, 'setState(...): You passed an undefined or null state object; ' + 'instead, use forceUpdate().') : undefined;
+ }
+ this.updater.enqueueSetState(this, partialState);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+};
+
+/**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldComponentUpdate`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {?function} callback Called after update is complete.
+ * @final
+ * @protected
+ */
+ReactComponent.prototype.forceUpdate = function (callback) {
+ this.updater.enqueueForceUpdate(this);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+};
+
+/**
+ * Deprecated APIs. These APIs used to exist on classic React classes but since
+ * we would like to deprecate them, we're not going to move them over to this
+ * modern base class. Instead, we define a getter that warns if it's accessed.
+ */
+if ("development" !== 'production') {
+ var deprecatedAPIs = {
+ getDOMNode: ['getDOMNode', 'Use ReactDOM.findDOMNode(component) instead.'],
+ isMounted: ['isMounted', 'Instead, make sure to clean up subscriptions and pending requests in ' + 'componentWillUnmount to prevent memory leaks.'],
+ replaceProps: ['replaceProps', 'Instead, call render again at the top level.'],
+ replaceState: ['replaceState', 'Refactor your code to use setState instead (see ' + 'https://github.com/facebook/react/issues/3236).'],
+ setProps: ['setProps', 'Instead, call render again at the top level.']
+ };
+ var defineDeprecationWarning = function (methodName, info) {
+ if (canDefineProperty) {
+ Object.defineProperty(ReactComponent.prototype, methodName, {
+ get: function () {
+ "development" !== 'production' ? warning(false, '%s(...) is deprecated in plain JavaScript React classes. %s', info[0], info[1]) : undefined;
+ return undefined;
+ }
+ });
+ }
+ };
+ for (var fnName in deprecatedAPIs) {
+ if (deprecatedAPIs.hasOwnProperty(fnName)) {
+ defineDeprecationWarning(fnName, deprecatedAPIs[fnName]);
+ }
+ }
+}
+
+module.exports = ReactComponent;
+},{"117":117,"154":154,"161":161,"173":173,"76":76}],35:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponentBrowserEnvironment
+ */
+
+'use strict';
+
+var ReactDOMIDOperations = _dereq_(45);
+var ReactMount = _dereq_(72);
+
+/**
+ * Abstracts away all functionality of the reconciler that requires knowledge of
+ * the browser context. TODO: These callers should be refactored to avoid the
+ * need for this injection.
+ */
+var ReactComponentBrowserEnvironment = {
+
+ processChildrenUpdates: ReactDOMIDOperations.dangerouslyProcessChildrenUpdates,
+
+ replaceNodeWithMarkupByID: ReactDOMIDOperations.dangerouslyReplaceNodeWithMarkupByID,
+
+ /**
+ * If a particular environment requires that some resources be cleaned up,
+ * specify this in the injected Mixin. In the DOM, we would likely want to
+ * purge any cached node ID lookups.
+ *
+ * @private
+ */
+ unmountIDFromEnvironment: function (rootNodeID) {
+ ReactMount.purgeID(rootNodeID);
+ }
+
+};
+
+module.exports = ReactComponentBrowserEnvironment;
+},{"45":45,"72":72}],36:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponentEnvironment
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+var injected = false;
+
+var ReactComponentEnvironment = {
+
+ /**
+ * Optionally injectable environment dependent cleanup hook. (server vs.
+ * browser etc). Example: A browser system caches DOM nodes based on component
+ * ID and must remove that cache entry when this instance is unmounted.
+ */
+ unmountIDFromEnvironment: null,
+
+ /**
+ * Optionally injectable hook for swapping out mount images in the middle of
+ * the tree.
+ */
+ replaceNodeWithMarkupByID: null,
+
+ /**
+ * Optionally injectable hook for processing a queue of child updates. Will
+ * later move into MultiChildComponents.
+ */
+ processChildrenUpdates: null,
+
+ injection: {
+ injectEnvironment: function (environment) {
+ !!injected ? "development" !== 'production' ? invariant(false, 'ReactCompositeComponent: injectEnvironment() can only be called once.') : invariant(false) : undefined;
+ ReactComponentEnvironment.unmountIDFromEnvironment = environment.unmountIDFromEnvironment;
+ ReactComponentEnvironment.replaceNodeWithMarkupByID = environment.replaceNodeWithMarkupByID;
+ ReactComponentEnvironment.processChildrenUpdates = environment.processChildrenUpdates;
+ injected = true;
+ }
+ }
+
+};
+
+module.exports = ReactComponentEnvironment;
+},{"161":161}],37:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponentWithPureRenderMixin
+ */
+
+'use strict';
+
+var shallowCompare = _dereq_(140);
+
+/**
+ * If your React component's render function is "pure", e.g. it will render the
+ * same result given the same props and state, provide this Mixin for a
+ * considerable performance boost.
+ *
+ * Most React components have pure render functions.
+ *
+ * Example:
+ *
+ * var ReactComponentWithPureRenderMixin =
+ * require('ReactComponentWithPureRenderMixin');
+ * React.createClass({
+ * mixins: [ReactComponentWithPureRenderMixin],
+ *
+ * render: function() {
+ * return <div className={this.props.className}>foo</div>;
+ * }
+ * });
+ *
+ * Note: This only checks shallow equality for props and state. If these contain
+ * complex data structures this mixin may have false-negatives for deeper
+ * differences. Only mixin to components which have simple props and state, or
+ * use `forceUpdate()` when you know deep data structures have changed.
+ */
+var ReactComponentWithPureRenderMixin = {
+ shouldComponentUpdate: function (nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
+};
+
+module.exports = ReactComponentWithPureRenderMixin;
+},{"140":140}],38:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactCompositeComponent
+ */
+
+'use strict';
+
+var ReactComponentEnvironment = _dereq_(36);
+var ReactCurrentOwner = _dereq_(39);
+var ReactElement = _dereq_(57);
+var ReactInstanceMap = _dereq_(68);
+var ReactPerf = _dereq_(78);
+var ReactPropTypeLocations = _dereq_(81);
+var ReactPropTypeLocationNames = _dereq_(80);
+var ReactReconciler = _dereq_(84);
+var ReactUpdateQueue = _dereq_(95);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var invariant = _dereq_(161);
+var shouldUpdateReactComponent = _dereq_(141);
+var warning = _dereq_(173);
+
+function getDeclarationErrorAddendum(component) {
+ var owner = component._currentElement._owner || null;
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+function StatelessComponent(Component) {}
+StatelessComponent.prototype.render = function () {
+ var Component = ReactInstanceMap.get(this)._currentElement.type;
+ return Component(this.props, this.context, this.updater);
+};
+
+/**
+ * ------------------ The Life-Cycle of a Composite Component ------------------
+ *
+ * - constructor: Initialization of state. The instance is now retained.
+ * - componentWillMount
+ * - render
+ * - [children's constructors]
+ * - [children's componentWillMount and render]
+ * - [children's componentDidMount]
+ * - componentDidMount
+ *
+ * Update Phases:
+ * - componentWillReceiveProps (only called if parent updated)
+ * - shouldComponentUpdate
+ * - componentWillUpdate
+ * - render
+ * - [children's constructors or receive props phases]
+ * - componentDidUpdate
+ *
+ * - componentWillUnmount
+ * - [children's componentWillUnmount]
+ * - [children destroyed]
+ * - (destroyed): The instance is now blank, released by React and ready for GC.
+ *
+ * -----------------------------------------------------------------------------
+ */
+
+/**
+ * An incrementing ID assigned to each component when it is mounted. This is
+ * used to enforce the order in which `ReactUpdates` updates dirty components.
+ *
+ * @private
+ */
+var nextMountID = 1;
+
+/**
+ * @lends {ReactCompositeComponent.prototype}
+ */
+var ReactCompositeComponentMixin = {
+
+ /**
+ * Base constructor for all composite component.
+ *
+ * @param {ReactElement} element
+ * @final
+ * @internal
+ */
+ construct: function (element) {
+ this._currentElement = element;
+ this._rootNodeID = null;
+ this._instance = null;
+
+ // See ReactUpdateQueue
+ this._pendingElement = null;
+ this._pendingStateQueue = null;
+ this._pendingReplaceState = false;
+ this._pendingForceUpdate = false;
+
+ this._renderedComponent = null;
+
+ this._context = null;
+ this._mountOrder = 0;
+ this._topLevelWrapper = null;
+
+ // See ReactUpdates and ReactUpdateQueue.
+ this._pendingCallbacks = null;
+ },
+
+ /**
+ * Initializes the component, renders markup, and registers event listeners.
+ *
+ * @param {string} rootID DOM ID of the root node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @return {?string} Rendered markup to be inserted into the DOM.
+ * @final
+ * @internal
+ */
+ mountComponent: function (rootID, transaction, context) {
+ this._context = context;
+ this._mountOrder = nextMountID++;
+ this._rootNodeID = rootID;
+
+ var publicProps = this._processProps(this._currentElement.props);
+ var publicContext = this._processContext(context);
+
+ var Component = this._currentElement.type;
+
+ // Initialize the public class
+ var inst;
+ var renderedElement;
+
+ // This is a way to detect if Component is a stateless arrow function
+ // component, which is not newable. It might not be 100% reliable but is
+ // something we can do until we start detecting that Component extends
+ // React.Component. We already assume that typeof Component === 'function'.
+ var canInstantiate = ('prototype' in Component);
+
+ if (canInstantiate) {
+ if ("development" !== 'production') {
+ ReactCurrentOwner.current = this;
+ try {
+ inst = new Component(publicProps, publicContext, ReactUpdateQueue);
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ } else {
+ inst = new Component(publicProps, publicContext, ReactUpdateQueue);
+ }
+ }
+
+ if (!canInstantiate || inst === null || inst === false || ReactElement.isValidElement(inst)) {
+ renderedElement = inst;
+ inst = new StatelessComponent(Component);
+ }
+
+ if ("development" !== 'production') {
+ // This will throw later in _renderValidatedComponent, but add an early
+ // warning now to help debugging
+ if (inst.render == null) {
+ "development" !== 'production' ? warning(false, '%s(...): No `render` method found on the returned component ' + 'instance: you may have forgotten to define `render`, returned ' + 'null/false from a stateless component, or tried to render an ' + 'element whose type is a function that isn\'t a React component.', Component.displayName || Component.name || 'Component') : undefined;
+ } else {
+ // We support ES6 inheriting from React.Component, the module pattern,
+ // and stateless components, but not ES6 classes that don't extend
+ "development" !== 'production' ? warning(Component.prototype && Component.prototype.isReactComponent || !canInstantiate || !(inst instanceof Component), '%s(...): React component classes must extend React.Component.', Component.displayName || Component.name || 'Component') : undefined;
+ }
+ }
+
+ // These should be set up in the constructor, but as a convenience for
+ // simpler class abstractions, we set them up after the fact.
+ inst.props = publicProps;
+ inst.context = publicContext;
+ inst.refs = emptyObject;
+ inst.updater = ReactUpdateQueue;
+
+ this._instance = inst;
+
+ // Store a reference from the instance back to the internal representation
+ ReactInstanceMap.set(inst, this);
+
+ if ("development" !== 'production') {
+ // Since plain JS classes are defined without any special initialization
+ // logic, we can not catch common errors early. Therefore, we have to
+ // catch them here, at initialization time, instead.
+ "development" !== 'production' ? warning(!inst.getInitialState || inst.getInitialState.isReactClassApproved, 'getInitialState was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Did you mean to define a state property instead?', this.getName() || 'a component') : undefined;
+ "development" !== 'production' ? warning(!inst.getDefaultProps || inst.getDefaultProps.isReactClassApproved, 'getDefaultProps was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Use a static property to define defaultProps instead.', this.getName() || 'a component') : undefined;
+ "development" !== 'production' ? warning(!inst.propTypes, 'propTypes was defined as an instance property on %s. Use a static ' + 'property to define propTypes instead.', this.getName() || 'a component') : undefined;
+ "development" !== 'production' ? warning(!inst.contextTypes, 'contextTypes was defined as an instance property on %s. Use a ' + 'static property to define contextTypes instead.', this.getName() || 'a component') : undefined;
+ "development" !== 'production' ? warning(typeof inst.componentShouldUpdate !== 'function', '%s has a method called ' + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 'The name is phrased as a question because the function is ' + 'expected to return a value.', this.getName() || 'A component') : undefined;
+ "development" !== 'production' ? warning(typeof inst.componentDidUnmount !== 'function', '%s has a method called ' + 'componentDidUnmount(). But there is no such lifecycle method. ' + 'Did you mean componentWillUnmount()?', this.getName() || 'A component') : undefined;
+ "development" !== 'production' ? warning(typeof inst.componentWillRecieveProps !== 'function', '%s has a method called ' + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', this.getName() || 'A component') : undefined;
+ }
+
+ var initialState = inst.state;
+ if (initialState === undefined) {
+ inst.state = initialState = null;
+ }
+ !(typeof initialState === 'object' && !Array.isArray(initialState)) ? "development" !== 'production' ? invariant(false, '%s.state: must be set to an object or null', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined;
+
+ this._pendingStateQueue = null;
+ this._pendingReplaceState = false;
+ this._pendingForceUpdate = false;
+
+ if (inst.componentWillMount) {
+ inst.componentWillMount();
+ // When mounting, calls to `setState` by `componentWillMount` will set
+ // `this._pendingStateQueue` without triggering a re-render.
+ if (this._pendingStateQueue) {
+ inst.state = this._processPendingState(inst.props, inst.context);
+ }
+ }
+
+ // If not a stateless component, we now render
+ if (renderedElement === undefined) {
+ renderedElement = this._renderValidatedComponent();
+ }
+
+ this._renderedComponent = this._instantiateReactComponent(renderedElement);
+
+ var markup = ReactReconciler.mountComponent(this._renderedComponent, rootID, transaction, this._processChildContext(context));
+ if (inst.componentDidMount) {
+ transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
+ }
+
+ return markup;
+ },
+
+ /**
+ * Releases any resources allocated by `mountComponent`.
+ *
+ * @final
+ * @internal
+ */
+ unmountComponent: function () {
+ var inst = this._instance;
+
+ if (inst.componentWillUnmount) {
+ inst.componentWillUnmount();
+ }
+
+ ReactReconciler.unmountComponent(this._renderedComponent);
+ this._renderedComponent = null;
+ this._instance = null;
+
+ // Reset pending fields
+ // Even if this component is scheduled for another update in ReactUpdates,
+ // it would still be ignored because these fields are reset.
+ this._pendingStateQueue = null;
+ this._pendingReplaceState = false;
+ this._pendingForceUpdate = false;
+ this._pendingCallbacks = null;
+ this._pendingElement = null;
+
+ // These fields do not really need to be reset since this object is no
+ // longer accessible.
+ this._context = null;
+ this._rootNodeID = null;
+ this._topLevelWrapper = null;
+
+ // Delete the reference from the instance to this internal representation
+ // which allow the internals to be properly cleaned up even if the user
+ // leaks a reference to the public instance.
+ ReactInstanceMap.remove(inst);
+
+ // Some existing components rely on inst.props even after they've been
+ // destroyed (in event handlers).
+ // TODO: inst.props = null;
+ // TODO: inst.state = null;
+ // TODO: inst.context = null;
+ },
+
+ /**
+ * Filters the context object to only contain keys specified in
+ * `contextTypes`
+ *
+ * @param {object} context
+ * @return {?object}
+ * @private
+ */
+ _maskContext: function (context) {
+ var maskedContext = null;
+ var Component = this._currentElement.type;
+ var contextTypes = Component.contextTypes;
+ if (!contextTypes) {
+ return emptyObject;
+ }
+ maskedContext = {};
+ for (var contextName in contextTypes) {
+ maskedContext[contextName] = context[contextName];
+ }
+ return maskedContext;
+ },
+
+ /**
+ * Filters the context object to only contain keys specified in
+ * `contextTypes`, and asserts that they are valid.
+ *
+ * @param {object} context
+ * @return {?object}
+ * @private
+ */
+ _processContext: function (context) {
+ var maskedContext = this._maskContext(context);
+ if ("development" !== 'production') {
+ var Component = this._currentElement.type;
+ if (Component.contextTypes) {
+ this._checkPropTypes(Component.contextTypes, maskedContext, ReactPropTypeLocations.context);
+ }
+ }
+ return maskedContext;
+ },
+
+ /**
+ * @param {object} currentContext
+ * @return {object}
+ * @private
+ */
+ _processChildContext: function (currentContext) {
+ var Component = this._currentElement.type;
+ var inst = this._instance;
+ var childContext = inst.getChildContext && inst.getChildContext();
+ if (childContext) {
+ !(typeof Component.childContextTypes === 'object') ? "development" !== 'production' ? invariant(false, '%s.getChildContext(): childContextTypes must be defined in order to ' + 'use getChildContext().', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined;
+ if ("development" !== 'production') {
+ this._checkPropTypes(Component.childContextTypes, childContext, ReactPropTypeLocations.childContext);
+ }
+ for (var name in childContext) {
+ !(name in Component.childContextTypes) ? "development" !== 'production' ? invariant(false, '%s.getChildContext(): key "%s" is not defined in childContextTypes.', this.getName() || 'ReactCompositeComponent', name) : invariant(false) : undefined;
+ }
+ return assign({}, currentContext, childContext);
+ }
+ return currentContext;
+ },
+
+ /**
+ * Processes props by setting default values for unspecified props and
+ * asserting that the props are valid. Does not mutate its argument; returns
+ * a new props object with defaults merged in.
+ *
+ * @param {object} newProps
+ * @return {object}
+ * @private
+ */
+ _processProps: function (newProps) {
+ if ("development" !== 'production') {
+ var Component = this._currentElement.type;
+ if (Component.propTypes) {
+ this._checkPropTypes(Component.propTypes, newProps, ReactPropTypeLocations.prop);
+ }
+ }
+ return newProps;
+ },
+
+ /**
+ * Assert that the props are valid
+ *
+ * @param {object} propTypes Map of prop name to a ReactPropType
+ * @param {object} props
+ * @param {string} location e.g. "prop", "context", "child context"
+ * @private
+ */
+ _checkPropTypes: function (propTypes, props, location) {
+ // TODO: Stop validating prop types here and only use the element
+ // validation.
+ var componentName = this.getName();
+ for (var propName in propTypes) {
+ if (propTypes.hasOwnProperty(propName)) {
+ var error;
+ try {
+ // This is intentionally an invariant that gets caught. It's the same
+ // behavior as without this statement except with a better message.
+ !(typeof propTypes[propName] === 'function') ? "development" !== 'production' ? invariant(false, '%s: %s type `%s` is invalid; it must be a function, usually ' + 'from React.PropTypes.', componentName || 'React class', ReactPropTypeLocationNames[location], propName) : invariant(false) : undefined;
+ error = propTypes[propName](props, propName, componentName, location);
+ } catch (ex) {
+ error = ex;
+ }
+ if (error instanceof Error) {
+ // We may want to extend this logic for similar errors in
+ // top-level render calls, so I'm abstracting it away into
+ // a function to minimize refactoring in the future
+ var addendum = getDeclarationErrorAddendum(this);
+
+ if (location === ReactPropTypeLocations.prop) {
+ // Preface gives us something to blacklist in warning module
+ "development" !== 'production' ? warning(false, 'Failed Composite propType: %s%s', error.message, addendum) : undefined;
+ } else {
+ "development" !== 'production' ? warning(false, 'Failed Context Types: %s%s', error.message, addendum) : undefined;
+ }
+ }
+ }
+ }
+ },
+
+ receiveComponent: function (nextElement, transaction, nextContext) {
+ var prevElement = this._currentElement;
+ var prevContext = this._context;
+
+ this._pendingElement = null;
+
+ this.updateComponent(transaction, prevElement, nextElement, prevContext, nextContext);
+ },
+
+ /**
+ * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate`
+ * is set, update the component.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ performUpdateIfNecessary: function (transaction) {
+ if (this._pendingElement != null) {
+ ReactReconciler.receiveComponent(this, this._pendingElement || this._currentElement, transaction, this._context);
+ }
+
+ if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
+ this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
+ }
+ },
+
+ /**
+ * Perform an update to a mounted component. The componentWillReceiveProps and
+ * shouldComponentUpdate methods are called, then (assuming the update isn't
+ * skipped) the remaining update lifecycle methods are called and the DOM
+ * representation is updated.
+ *
+ * By default, this implements React's rendering and reconciliation algorithm.
+ * Sophisticated clients may wish to override this.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @param {ReactElement} prevParentElement
+ * @param {ReactElement} nextParentElement
+ * @internal
+ * @overridable
+ */
+ updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
+ var inst = this._instance;
+
+ var nextContext = this._context === nextUnmaskedContext ? inst.context : this._processContext(nextUnmaskedContext);
+ var nextProps;
+
+ // Distinguish between a props update versus a simple state update
+ if (prevParentElement === nextParentElement) {
+ // Skip checking prop types again -- we don't read inst.props to avoid
+ // warning for DOM component props in this upgrade
+ nextProps = nextParentElement.props;
+ } else {
+ nextProps = this._processProps(nextParentElement.props);
+ // An update here will schedule an update but immediately set
+ // _pendingStateQueue which will ensure that any state updates gets
+ // immediately reconciled instead of waiting for the next batch.
+
+ if (inst.componentWillReceiveProps) {
+ inst.componentWillReceiveProps(nextProps, nextContext);
+ }
+ }
+
+ var nextState = this._processPendingState(nextProps, nextContext);
+
+ var shouldUpdate = this._pendingForceUpdate || !inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext);
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(typeof shouldUpdate !== 'undefined', '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', this.getName() || 'ReactCompositeComponent') : undefined;
+ }
+
+ if (shouldUpdate) {
+ this._pendingForceUpdate = false;
+ // Will set `this.props`, `this.state` and `this.context`.
+ this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext);
+ } else {
+ // If it's determined that a component should not update, we still want
+ // to set props and state but we shortcut the rest of the update.
+ this._currentElement = nextParentElement;
+ this._context = nextUnmaskedContext;
+ inst.props = nextProps;
+ inst.state = nextState;
+ inst.context = nextContext;
+ }
+ },
+
+ _processPendingState: function (props, context) {
+ var inst = this._instance;
+ var queue = this._pendingStateQueue;
+ var replace = this._pendingReplaceState;
+ this._pendingReplaceState = false;
+ this._pendingStateQueue = null;
+
+ if (!queue) {
+ return inst.state;
+ }
+
+ if (replace && queue.length === 1) {
+ return queue[0];
+ }
+
+ var nextState = assign({}, replace ? queue[0] : inst.state);
+ for (var i = replace ? 1 : 0; i < queue.length; i++) {
+ var partial = queue[i];
+ assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
+ }
+
+ return nextState;
+ },
+
+ /**
+ * Merges new props and state, notifies delegate methods of update and
+ * performs update.
+ *
+ * @param {ReactElement} nextElement Next element
+ * @param {object} nextProps Next public object to set as properties.
+ * @param {?object} nextState Next object to set as state.
+ * @param {?object} nextContext Next public object to set as context.
+ * @param {ReactReconcileTransaction} transaction
+ * @param {?object} unmaskedContext
+ * @private
+ */
+ _performComponentUpdate: function (nextElement, nextProps, nextState, nextContext, transaction, unmaskedContext) {
+ var inst = this._instance;
+
+ var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
+ var prevProps;
+ var prevState;
+ var prevContext;
+ if (hasComponentDidUpdate) {
+ prevProps = inst.props;
+ prevState = inst.state;
+ prevContext = inst.context;
+ }
+
+ if (inst.componentWillUpdate) {
+ inst.componentWillUpdate(nextProps, nextState, nextContext);
+ }
+
+ this._currentElement = nextElement;
+ this._context = unmaskedContext;
+ inst.props = nextProps;
+ inst.state = nextState;
+ inst.context = nextContext;
+
+ this._updateRenderedComponent(transaction, unmaskedContext);
+
+ if (hasComponentDidUpdate) {
+ transaction.getReactMountReady().enqueue(inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), inst);
+ }
+ },
+
+ /**
+ * Call the component's `render` method and update the DOM accordingly.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ _updateRenderedComponent: function (transaction, context) {
+ var prevComponentInstance = this._renderedComponent;
+ var prevRenderedElement = prevComponentInstance._currentElement;
+ var nextRenderedElement = this._renderValidatedComponent();
+ if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
+ ReactReconciler.receiveComponent(prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context));
+ } else {
+ // These two IDs are actually the same! But nothing should rely on that.
+ var thisID = this._rootNodeID;
+ var prevComponentID = prevComponentInstance._rootNodeID;
+ ReactReconciler.unmountComponent(prevComponentInstance);
+
+ this._renderedComponent = this._instantiateReactComponent(nextRenderedElement);
+ var nextMarkup = ReactReconciler.mountComponent(this._renderedComponent, thisID, transaction, this._processChildContext(context));
+ this._replaceNodeWithMarkupByID(prevComponentID, nextMarkup);
+ }
+ },
+
+ /**
+ * @protected
+ */
+ _replaceNodeWithMarkupByID: function (prevComponentID, nextMarkup) {
+ ReactComponentEnvironment.replaceNodeWithMarkupByID(prevComponentID, nextMarkup);
+ },
+
+ /**
+ * @protected
+ */
+ _renderValidatedComponentWithoutOwnerOrContext: function () {
+ var inst = this._instance;
+ var renderedComponent = inst.render();
+ if ("development" !== 'production') {
+ // We allow auto-mocks to proceed as if they're returning null.
+ if (typeof renderedComponent === 'undefined' && inst.render._isMockFunction) {
+ // This is probably bad practice. Consider warning here and
+ // deprecating this convenience.
+ renderedComponent = null;
+ }
+ }
+
+ return renderedComponent;
+ },
+
+ /**
+ * @private
+ */
+ _renderValidatedComponent: function () {
+ var renderedComponent;
+ ReactCurrentOwner.current = this;
+ try {
+ renderedComponent = this._renderValidatedComponentWithoutOwnerOrContext();
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ !(
+ // TODO: An `isValidNode` function would probably be more appropriate
+ renderedComponent === null || renderedComponent === false || ReactElement.isValidElement(renderedComponent)) ? "development" !== 'production' ? invariant(false, '%s.render(): A valid ReactComponent must be returned. You may have ' + 'returned undefined, an array or some other invalid object.', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined;
+ return renderedComponent;
+ },
+
+ /**
+ * Lazily allocates the refs object and stores `component` as `ref`.
+ *
+ * @param {string} ref Reference name.
+ * @param {component} component Component to store as `ref`.
+ * @final
+ * @private
+ */
+ attachRef: function (ref, component) {
+ var inst = this.getPublicInstance();
+ !(inst != null) ? "development" !== 'production' ? invariant(false, 'Stateless function components cannot have refs.') : invariant(false) : undefined;
+ var publicComponentInstance = component.getPublicInstance();
+ if ("development" !== 'production') {
+ var componentName = component && component.getName ? component.getName() : 'a component';
+ "development" !== 'production' ? warning(publicComponentInstance != null, 'Stateless function components cannot be given refs ' + '(See ref "%s" in %s created by %s). ' + 'Attempts to access this ref will fail.', ref, componentName, this.getName()) : undefined;
+ }
+ var refs = inst.refs === emptyObject ? inst.refs = {} : inst.refs;
+ refs[ref] = publicComponentInstance;
+ },
+
+ /**
+ * Detaches a reference name.
+ *
+ * @param {string} ref Name to dereference.
+ * @final
+ * @private
+ */
+ detachRef: function (ref) {
+ var refs = this.getPublicInstance().refs;
+ delete refs[ref];
+ },
+
+ /**
+ * Get a text description of the component that can be used to identify it
+ * in error messages.
+ * @return {string} The name or null.
+ * @internal
+ */
+ getName: function () {
+ var type = this._currentElement.type;
+ var constructor = this._instance && this._instance.constructor;
+ return type.displayName || constructor && constructor.displayName || type.name || constructor && constructor.name || null;
+ },
+
+ /**
+ * Get the publicly accessible representation of this component - i.e. what
+ * is exposed by refs and returned by render. Can be null for stateless
+ * components.
+ *
+ * @return {ReactComponent} the public component instance.
+ * @internal
+ */
+ getPublicInstance: function () {
+ var inst = this._instance;
+ if (inst instanceof StatelessComponent) {
+ return null;
+ }
+ return inst;
+ },
+
+ // Stub
+ _instantiateReactComponent: null
+
+};
+
+ReactPerf.measureMethods(ReactCompositeComponentMixin, 'ReactCompositeComponent', {
+ mountComponent: 'mountComponent',
+ updateComponent: 'updateComponent',
+ _renderValidatedComponent: '_renderValidatedComponent'
+});
+
+var ReactCompositeComponent = {
+
+ Mixin: ReactCompositeComponentMixin
+
+};
+
+module.exports = ReactCompositeComponent;
+},{"141":141,"154":154,"161":161,"173":173,"24":24,"36":36,"39":39,"57":57,"68":68,"78":78,"80":80,"81":81,"84":84,"95":95}],39:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactCurrentOwner
+ */
+
+'use strict';
+
+/**
+ * Keeps track of the current owner.
+ *
+ * The current owner is the component who should own any components that are
+ * currently being constructed.
+ */
+var ReactCurrentOwner = {
+
+ /**
+ * @internal
+ * @type {ReactComponent}
+ */
+ current: null
+
+};
+
+module.exports = ReactCurrentOwner;
+},{}],40:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOM
+ */
+
+/* globals __REACT_DEVTOOLS_GLOBAL_HOOK__*/
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactDOMTextComponent = _dereq_(51);
+var ReactDefaultInjection = _dereq_(54);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMount = _dereq_(72);
+var ReactPerf = _dereq_(78);
+var ReactReconciler = _dereq_(84);
+var ReactUpdates = _dereq_(96);
+var ReactVersion = _dereq_(97);
+
+var findDOMNode = _dereq_(122);
+var renderSubtreeIntoContainer = _dereq_(137);
+var warning = _dereq_(173);
+
+ReactDefaultInjection.inject();
+
+var render = ReactPerf.measure('React', 'render', ReactMount.render);
+
+var React = {
+ findDOMNode: findDOMNode,
+ render: render,
+ unmountComponentAtNode: ReactMount.unmountComponentAtNode,
+ version: ReactVersion,
+
+ /* eslint-disable camelcase */
+ unstable_batchedUpdates: ReactUpdates.batchedUpdates,
+ unstable_renderSubtreeIntoContainer: renderSubtreeIntoContainer
+};
+
+// Inject the runtime into a devtools global hook regardless of browser.
+// Allows for debugging when the hook is injected on the page.
+/* eslint-enable camelcase */
+if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') {
+ __REACT_DEVTOOLS_GLOBAL_HOOK__.inject({
+ CurrentOwner: ReactCurrentOwner,
+ InstanceHandles: ReactInstanceHandles,
+ Mount: ReactMount,
+ Reconciler: ReactReconciler,
+ TextComponent: ReactDOMTextComponent
+ });
+}
+
+if ("development" !== 'production') {
+ var ExecutionEnvironment = _dereq_(147);
+ if (ExecutionEnvironment.canUseDOM && window.top === window.self) {
+
+ // First check if devtools is not installed
+ if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
+ // If we're in Chrome or Firefox, provide a download link if not installed.
+ if (navigator.userAgent.indexOf('Chrome') > -1 && navigator.userAgent.indexOf('Edge') === -1 || navigator.userAgent.indexOf('Firefox') > -1) {
+ console.debug('Download the React DevTools for a better development experience: ' + 'https://fb.me/react-devtools');
+ }
+ }
+
+ // If we're in IE8, check to see if we are in compatibility mode and provide
+ // information on preventing compatibility mode
+ var ieCompatibilityMode = document.documentMode && document.documentMode < 8;
+
+ "development" !== 'production' ? warning(!ieCompatibilityMode, 'Internet Explorer is running in compatibility mode; please add the ' + 'following tag to your HTML to prevent this from happening: ' + '<meta http-equiv="X-UA-Compatible" content="IE=edge" />') : undefined;
+
+ var expectedFeatures = [
+ // shims
+ Array.isArray, Array.prototype.every, Array.prototype.forEach, Array.prototype.indexOf, Array.prototype.map, Date.now, Function.prototype.bind, Object.keys, String.prototype.split, String.prototype.trim,
+
+ // shams
+ Object.create, Object.freeze];
+
+ for (var i = 0; i < expectedFeatures.length; i++) {
+ if (!expectedFeatures[i]) {
+ console.error('One or more ES5 shim/shams expected by React are not available: ' + 'https://fb.me/react-warning-polyfills');
+ break;
+ }
+ }
+ }
+}
+
+module.exports = React;
+},{"122":122,"137":137,"147":147,"173":173,"39":39,"51":51,"54":54,"67":67,"72":72,"78":78,"84":84,"96":96,"97":97}],41:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMButton
+ */
+
+'use strict';
+
+var mouseListenerNames = {
+ onClick: true,
+ onDoubleClick: true,
+ onMouseDown: true,
+ onMouseMove: true,
+ onMouseUp: true,
+
+ onClickCapture: true,
+ onDoubleClickCapture: true,
+ onMouseDownCapture: true,
+ onMouseMoveCapture: true,
+ onMouseUpCapture: true
+};
+
+/**
+ * Implements a <button> native component that does not receive mouse events
+ * when `disabled` is set.
+ */
+var ReactDOMButton = {
+ getNativeProps: function (inst, props, context) {
+ if (!props.disabled) {
+ return props;
+ }
+
+ // Copy the props, except the mouse listeners
+ var nativeProps = {};
+ for (var key in props) {
+ if (props.hasOwnProperty(key) && !mouseListenerNames[key]) {
+ nativeProps[key] = props[key];
+ }
+ }
+
+ return nativeProps;
+ }
+};
+
+module.exports = ReactDOMButton;
+},{}],42:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMComponent
+ * @typechecks static-only
+ */
+
+/* global hasOwnProperty:true */
+
+'use strict';
+
+var AutoFocusUtils = _dereq_(2);
+var CSSPropertyOperations = _dereq_(5);
+var DOMProperty = _dereq_(10);
+var DOMPropertyOperations = _dereq_(11);
+var EventConstants = _dereq_(15);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactComponentBrowserEnvironment = _dereq_(35);
+var ReactDOMButton = _dereq_(41);
+var ReactDOMInput = _dereq_(46);
+var ReactDOMOption = _dereq_(47);
+var ReactDOMSelect = _dereq_(48);
+var ReactDOMTextarea = _dereq_(52);
+var ReactMount = _dereq_(72);
+var ReactMultiChild = _dereq_(73);
+var ReactPerf = _dereq_(78);
+var ReactUpdateQueue = _dereq_(95);
+
+var assign = _dereq_(24);
+var canDefineProperty = _dereq_(117);
+var escapeTextContentForBrowser = _dereq_(121);
+var invariant = _dereq_(161);
+var isEventSupported = _dereq_(133);
+var keyOf = _dereq_(166);
+var setInnerHTML = _dereq_(138);
+var setTextContent = _dereq_(139);
+var shallowEqual = _dereq_(171);
+var validateDOMNesting = _dereq_(144);
+var warning = _dereq_(173);
+
+var deleteListener = ReactBrowserEventEmitter.deleteListener;
+var listenTo = ReactBrowserEventEmitter.listenTo;
+var registrationNameModules = ReactBrowserEventEmitter.registrationNameModules;
+
+// For quickly matching children type, to test if can be treated as content.
+var CONTENT_TYPES = { 'string': true, 'number': true };
+
+var CHILDREN = keyOf({ children: null });
+var STYLE = keyOf({ style: null });
+var HTML = keyOf({ __html: null });
+
+var ELEMENT_NODE_TYPE = 1;
+
+function getDeclarationErrorAddendum(internalInstance) {
+ if (internalInstance) {
+ var owner = internalInstance._currentElement._owner || null;
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' This DOM node was rendered by `' + name + '`.';
+ }
+ }
+ }
+ return '';
+}
+
+var legacyPropsDescriptor;
+if ("development" !== 'production') {
+ legacyPropsDescriptor = {
+ props: {
+ enumerable: false,
+ get: function () {
+ var component = this._reactInternalComponent;
+ "development" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .props of a DOM node; instead, ' + 'recreate the props as `render` did originally or read the DOM ' + 'properties/attributes directly from this node (e.g., ' + 'this.refs.box.className).%s', getDeclarationErrorAddendum(component)) : undefined;
+ return component._currentElement.props;
+ }
+ }
+ };
+}
+
+function legacyGetDOMNode() {
+ if ("development" !== 'production') {
+ var component = this._reactInternalComponent;
+ "development" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .getDOMNode() of a DOM node; ' + 'instead, use the node directly.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ return this;
+}
+
+function legacyIsMounted() {
+ var component = this._reactInternalComponent;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .isMounted() of a DOM node.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ return !!component;
+}
+
+function legacySetStateEtc() {
+ if ("development" !== 'production') {
+ var component = this._reactInternalComponent;
+ "development" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .setState(), .replaceState(), or ' + '.forceUpdate() of a DOM node. This is a no-op.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+}
+
+function legacySetProps(partialProps, callback) {
+ var component = this._reactInternalComponent;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .setProps() of a DOM node. ' + 'Instead, call ReactDOM.render again at the top level.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ if (!component) {
+ return;
+ }
+ ReactUpdateQueue.enqueueSetPropsInternal(component, partialProps);
+ if (callback) {
+ ReactUpdateQueue.enqueueCallbackInternal(component, callback);
+ }
+}
+
+function legacyReplaceProps(partialProps, callback) {
+ var component = this._reactInternalComponent;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .replaceProps() of a DOM node. ' + 'Instead, call ReactDOM.render again at the top level.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ if (!component) {
+ return;
+ }
+ ReactUpdateQueue.enqueueReplacePropsInternal(component, partialProps);
+ if (callback) {
+ ReactUpdateQueue.enqueueCallbackInternal(component, callback);
+ }
+}
+
+function friendlyStringify(obj) {
+ if (typeof obj === 'object') {
+ if (Array.isArray(obj)) {
+ return '[' + obj.map(friendlyStringify).join(', ') + ']';
+ } else {
+ var pairs = [];
+ for (var key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ var keyEscaped = /^[a-z$_][\w$_]*$/i.test(key) ? key : JSON.stringify(key);
+ pairs.push(keyEscaped + ': ' + friendlyStringify(obj[key]));
+ }
+ }
+ return '{' + pairs.join(', ') + '}';
+ }
+ } else if (typeof obj === 'string') {
+ return JSON.stringify(obj);
+ } else if (typeof obj === 'function') {
+ return '[function object]';
+ }
+ // Differs from JSON.stringify in that undefined becauses undefined and that
+ // inf and nan don't become null
+ return String(obj);
+}
+
+var styleMutationWarning = {};
+
+function checkAndWarnForMutatedStyle(style1, style2, component) {
+ if (style1 == null || style2 == null) {
+ return;
+ }
+ if (shallowEqual(style1, style2)) {
+ return;
+ }
+
+ var componentName = component._tag;
+ var owner = component._currentElement._owner;
+ var ownerName;
+ if (owner) {
+ ownerName = owner.getName();
+ }
+
+ var hash = ownerName + '|' + componentName;
+
+ if (styleMutationWarning.hasOwnProperty(hash)) {
+ return;
+ }
+
+ styleMutationWarning[hash] = true;
+
+ "development" !== 'production' ? warning(false, '`%s` was passed a style object that has previously been mutated. ' + 'Mutating `style` is deprecated. Consider cloning it beforehand. Check ' + 'the `render` %s. Previous style: %s. Mutated style: %s.', componentName, owner ? 'of `' + ownerName + '`' : 'using <' + componentName + '>', friendlyStringify(style1), friendlyStringify(style2)) : undefined;
+}
+
+/**
+ * @param {object} component
+ * @param {?object} props
+ */
+function assertValidProps(component, props) {
+ if (!props) {
+ return;
+ }
+ // Note the use of `==` which checks for null or undefined.
+ if ("development" !== 'production') {
+ if (voidElementTags[component._tag]) {
+ "development" !== 'production' ? warning(props.children == null && props.dangerouslySetInnerHTML == null, '%s is a void element tag and must not have `children` or ' + 'use `props.dangerouslySetInnerHTML`.%s', component._tag, component._currentElement._owner ? ' Check the render method of ' + component._currentElement._owner.getName() + '.' : '') : undefined;
+ }
+ }
+ if (props.dangerouslySetInnerHTML != null) {
+ !(props.children == null) ? "development" !== 'production' ? invariant(false, 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.') : invariant(false) : undefined;
+ !(typeof props.dangerouslySetInnerHTML === 'object' && HTML in props.dangerouslySetInnerHTML) ? "development" !== 'production' ? invariant(false, '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + 'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' + 'for more information.') : invariant(false) : undefined;
+ }
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(props.innerHTML == null, 'Directly setting property `innerHTML` is not permitted. ' + 'For more information, lookup documentation on `dangerouslySetInnerHTML`.') : undefined;
+ "development" !== 'production' ? warning(!props.contentEditable || props.children == null, 'A component is `contentEditable` and contains `children` managed by ' + 'React. It is now your responsibility to guarantee that none of ' + 'those nodes are unexpectedly modified or duplicated. This is ' + 'probably not intentional.') : undefined;
+ }
+ !(props.style == null || typeof props.style === 'object') ? "development" !== 'production' ? invariant(false, 'The `style` prop expects a mapping from style properties to values, ' + 'not a string. For example, style={{marginRight: spacing + \'em\'}} when ' + 'using JSX.%s', getDeclarationErrorAddendum(component)) : invariant(false) : undefined;
+}
+
+function enqueuePutListener(id, registrationName, listener, transaction) {
+ if ("development" !== 'production') {
+ // IE8 has no API for event capturing and the `onScroll` event doesn't
+ // bubble.
+ "development" !== 'production' ? warning(registrationName !== 'onScroll' || isEventSupported('scroll', true), 'This browser doesn\'t support the `onScroll` event') : undefined;
+ }
+ var container = ReactMount.findReactContainerForID(id);
+ if (container) {
+ var doc = container.nodeType === ELEMENT_NODE_TYPE ? container.ownerDocument : container;
+ listenTo(registrationName, doc);
+ }
+ transaction.getReactMountReady().enqueue(putListener, {
+ id: id,
+ registrationName: registrationName,
+ listener: listener
+ });
+}
+
+function putListener() {
+ var listenerToPut = this;
+ ReactBrowserEventEmitter.putListener(listenerToPut.id, listenerToPut.registrationName, listenerToPut.listener);
+}
+
+// There are so many media events, it makes sense to just
+// maintain a list rather than create a `trapBubbledEvent` for each
+var mediaEvents = {
+ topAbort: 'abort',
+ topCanPlay: 'canplay',
+ topCanPlayThrough: 'canplaythrough',
+ topDurationChange: 'durationchange',
+ topEmptied: 'emptied',
+ topEncrypted: 'encrypted',
+ topEnded: 'ended',
+ topError: 'error',
+ topLoadedData: 'loadeddata',
+ topLoadedMetadata: 'loadedmetadata',
+ topLoadStart: 'loadstart',
+ topPause: 'pause',
+ topPlay: 'play',
+ topPlaying: 'playing',
+ topProgress: 'progress',
+ topRateChange: 'ratechange',
+ topSeeked: 'seeked',
+ topSeeking: 'seeking',
+ topStalled: 'stalled',
+ topSuspend: 'suspend',
+ topTimeUpdate: 'timeupdate',
+ topVolumeChange: 'volumechange',
+ topWaiting: 'waiting'
+};
+
+function trapBubbledEventsLocal() {
+ var inst = this;
+ // If a component renders to null or if another component fatals and causes
+ // the state of the tree to be corrupted, `node` here can be null.
+ !inst._rootNodeID ? "development" !== 'production' ? invariant(false, 'Must be mounted to trap events') : invariant(false) : undefined;
+ var node = ReactMount.getNode(inst._rootNodeID);
+ !node ? "development" !== 'production' ? invariant(false, 'trapBubbledEvent(...): Requires node to be rendered.') : invariant(false) : undefined;
+
+ switch (inst._tag) {
+ case 'iframe':
+ inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topLoad, 'load', node)];
+ break;
+ case 'video':
+ case 'audio':
+
+ inst._wrapperState.listeners = [];
+ // create listener for each media event
+ for (var event in mediaEvents) {
+ if (mediaEvents.hasOwnProperty(event)) {
+ inst._wrapperState.listeners.push(ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes[event], mediaEvents[event], node));
+ }
+ }
+
+ break;
+ case 'img':
+ inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topError, 'error', node), ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topLoad, 'load', node)];
+ break;
+ case 'form':
+ inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topReset, 'reset', node), ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topSubmit, 'submit', node)];
+ break;
+ }
+}
+
+function mountReadyInputWrapper() {
+ ReactDOMInput.mountReadyWrapper(this);
+}
+
+function postUpdateSelectWrapper() {
+ ReactDOMSelect.postUpdateWrapper(this);
+}
+
+// For HTML, certain tags should omit their close tag. We keep a whitelist for
+// those special cased tags.
+
+var omittedCloseTags = {
+ 'area': true,
+ 'base': true,
+ 'br': true,
+ 'col': true,
+ 'embed': true,
+ 'hr': true,
+ 'img': true,
+ 'input': true,
+ 'keygen': true,
+ 'link': true,
+ 'meta': true,
+ 'param': true,
+ 'source': true,
+ 'track': true,
+ 'wbr': true
+};
+
+// NOTE: menuitem's close tag should be omitted, but that causes problems.
+var newlineEatingTags = {
+ 'listing': true,
+ 'pre': true,
+ 'textarea': true
+};
+
+// For HTML, certain tags cannot have children. This has the same purpose as
+// `omittedCloseTags` except that `menuitem` should still have its closing tag.
+
+var voidElementTags = assign({
+ 'menuitem': true
+}, omittedCloseTags);
+
+// We accept any tag to be rendered but since this gets injected into arbitrary
+// HTML, we want to make sure that it's a safe tag.
+// http://www.w3.org/TR/REC-xml/#NT-Name
+
+var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset
+var validatedTagCache = {};
+var hasOwnProperty = ({}).hasOwnProperty;
+
+function validateDangerousTag(tag) {
+ if (!hasOwnProperty.call(validatedTagCache, tag)) {
+ !VALID_TAG_REGEX.test(tag) ? "development" !== 'production' ? invariant(false, 'Invalid tag: %s', tag) : invariant(false) : undefined;
+ validatedTagCache[tag] = true;
+ }
+}
+
+function processChildContextDev(context, inst) {
+ // Pass down our tag name to child components for validation purposes
+ context = assign({}, context);
+ var info = context[validateDOMNesting.ancestorInfoContextKey];
+ context[validateDOMNesting.ancestorInfoContextKey] = validateDOMNesting.updatedAncestorInfo(info, inst._tag, inst);
+ return context;
+}
+
+function isCustomComponent(tagName, props) {
+ return tagName.indexOf('-') >= 0 || props.is != null;
+}
+
+/**
+ * Creates a new React class that is idempotent and capable of containing other
+ * React components. It accepts event listeners and DOM properties that are
+ * valid according to `DOMProperty`.
+ *
+ * - Event listeners: `onClick`, `onMouseDown`, etc.
+ * - DOM properties: `className`, `name`, `title`, etc.
+ *
+ * The `style` property functions differently from the DOM API. It accepts an
+ * object mapping of style properties to values.
+ *
+ * @constructor ReactDOMComponent
+ * @extends ReactMultiChild
+ */
+function ReactDOMComponent(tag) {
+ validateDangerousTag(tag);
+ this._tag = tag.toLowerCase();
+ this._renderedChildren = null;
+ this._previousStyle = null;
+ this._previousStyleCopy = null;
+ this._rootNodeID = null;
+ this._wrapperState = null;
+ this._topLevelWrapper = null;
+ this._nodeWithLegacyProperties = null;
+ if ("development" !== 'production') {
+ this._unprocessedContextDev = null;
+ this._processedContextDev = null;
+ }
+}
+
+ReactDOMComponent.displayName = 'ReactDOMComponent';
+
+ReactDOMComponent.Mixin = {
+
+ construct: function (element) {
+ this._currentElement = element;
+ },
+
+ /**
+ * Generates root tag markup then recurses. This method has side effects and
+ * is not idempotent.
+ *
+ * @internal
+ * @param {string} rootID The root DOM ID for this node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} context
+ * @return {string} The computed markup.
+ */
+ mountComponent: function (rootID, transaction, context) {
+ this._rootNodeID = rootID;
+
+ var props = this._currentElement.props;
+
+ switch (this._tag) {
+ case 'iframe':
+ case 'img':
+ case 'form':
+ case 'video':
+ case 'audio':
+ this._wrapperState = {
+ listeners: null
+ };
+ transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
+ break;
+ case 'button':
+ props = ReactDOMButton.getNativeProps(this, props, context);
+ break;
+ case 'input':
+ ReactDOMInput.mountWrapper(this, props, context);
+ props = ReactDOMInput.getNativeProps(this, props, context);
+ break;
+ case 'option':
+ ReactDOMOption.mountWrapper(this, props, context);
+ props = ReactDOMOption.getNativeProps(this, props, context);
+ break;
+ case 'select':
+ ReactDOMSelect.mountWrapper(this, props, context);
+ props = ReactDOMSelect.getNativeProps(this, props, context);
+ context = ReactDOMSelect.processChildContext(this, props, context);
+ break;
+ case 'textarea':
+ ReactDOMTextarea.mountWrapper(this, props, context);
+ props = ReactDOMTextarea.getNativeProps(this, props, context);
+ break;
+ }
+
+ assertValidProps(this, props);
+ if ("development" !== 'production') {
+ if (context[validateDOMNesting.ancestorInfoContextKey]) {
+ validateDOMNesting(this._tag, this, context[validateDOMNesting.ancestorInfoContextKey]);
+ }
+ }
+
+ if ("development" !== 'production') {
+ this._unprocessedContextDev = context;
+ this._processedContextDev = processChildContextDev(context, this);
+ context = this._processedContextDev;
+ }
+
+ var mountImage;
+ if (transaction.useCreateElement) {
+ var ownerDocument = context[ReactMount.ownerDocumentContextKey];
+ var el = ownerDocument.createElementNS('http://www.w3.org/1999/xhtml', this._currentElement.type);
+ DOMPropertyOperations.setAttributeForID(el, this._rootNodeID);
+ // Populate node cache
+ ReactMount.getID(el);
+ this._updateDOMProperties({}, props, transaction, el);
+ this._createInitialChildren(transaction, props, context, el);
+ mountImage = el;
+ } else {
+ var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
+ var tagContent = this._createContentMarkup(transaction, props, context);
+ if (!tagContent && omittedCloseTags[this._tag]) {
+ mountImage = tagOpen + '/>';
+ } else {
+ mountImage = tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
+ }
+ }
+
+ switch (this._tag) {
+ case 'input':
+ transaction.getReactMountReady().enqueue(mountReadyInputWrapper, this);
+ // falls through
+ case 'button':
+ case 'select':
+ case 'textarea':
+ if (props.autoFocus) {
+ transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
+ }
+ break;
+ }
+
+ return mountImage;
+ },
+
+ /**
+ * Creates markup for the open tag and all attributes.
+ *
+ * This method has side effects because events get registered.
+ *
+ * Iterating over object properties is faster than iterating over arrays.
+ * @see http://jsperf.com/obj-vs-arr-iteration
+ *
+ * @private
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} props
+ * @return {string} Markup of opening tag.
+ */
+ _createOpenTagMarkupAndPutListeners: function (transaction, props) {
+ var ret = '<' + this._currentElement.type;
+
+ for (var propKey in props) {
+ if (!props.hasOwnProperty(propKey)) {
+ continue;
+ }
+ var propValue = props[propKey];
+ if (propValue == null) {
+ continue;
+ }
+ if (registrationNameModules.hasOwnProperty(propKey)) {
+ if (propValue) {
+ enqueuePutListener(this._rootNodeID, propKey, propValue, transaction);
+ }
+ } else {
+ if (propKey === STYLE) {
+ if (propValue) {
+ if ("development" !== 'production') {
+ // See `_updateDOMProperties`. style block
+ this._previousStyle = propValue;
+ }
+ propValue = this._previousStyleCopy = assign({}, props.style);
+ }
+ propValue = CSSPropertyOperations.createMarkupForStyles(propValue);
+ }
+ var markup = null;
+ if (this._tag != null && isCustomComponent(this._tag, props)) {
+ if (propKey !== CHILDREN) {
+ markup = DOMPropertyOperations.createMarkupForCustomAttribute(propKey, propValue);
+ }
+ } else {
+ markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
+ }
+ if (markup) {
+ ret += ' ' + markup;
+ }
+ }
+ }
+
+ // For static pages, no need to put React ID and checksum. Saves lots of
+ // bytes.
+ if (transaction.renderToStaticMarkup) {
+ return ret;
+ }
+
+ var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID);
+ return ret + ' ' + markupForID;
+ },
+
+ /**
+ * Creates markup for the content between the tags.
+ *
+ * @private
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} props
+ * @param {object} context
+ * @return {string} Content markup.
+ */
+ _createContentMarkup: function (transaction, props, context) {
+ var ret = '';
+
+ // Intentional use of != to avoid catching zero/false.
+ var innerHTML = props.dangerouslySetInnerHTML;
+ if (innerHTML != null) {
+ if (innerHTML.__html != null) {
+ ret = innerHTML.__html;
+ }
+ } else {
+ var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
+ var childrenToUse = contentToUse != null ? null : props.children;
+ if (contentToUse != null) {
+ // TODO: Validate that text is allowed as a child of this node
+ ret = escapeTextContentForBrowser(contentToUse);
+ } else if (childrenToUse != null) {
+ var mountImages = this.mountChildren(childrenToUse, transaction, context);
+ ret = mountImages.join('');
+ }
+ }
+ if (newlineEatingTags[this._tag] && ret.charAt(0) === '\n') {
+ // text/html ignores the first character in these tags if it's a newline
+ // Prefer to break application/xml over text/html (for now) by adding
+ // a newline specifically to get eaten by the parser. (Alternately for
+ // textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
+ // \r is normalized out by HTMLTextAreaElement#value.)
+ // See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
+ // See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
+ // See: <http://www.w3.org/TR/html5/syntax.html#newlines>
+ // See: Parsing of "textarea" "listing" and "pre" elements
+ // from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
+ return '\n' + ret;
+ } else {
+ return ret;
+ }
+ },
+
+ _createInitialChildren: function (transaction, props, context, el) {
+ // Intentional use of != to avoid catching zero/false.
+ var innerHTML = props.dangerouslySetInnerHTML;
+ if (innerHTML != null) {
+ if (innerHTML.__html != null) {
+ setInnerHTML(el, innerHTML.__html);
+ }
+ } else {
+ var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
+ var childrenToUse = contentToUse != null ? null : props.children;
+ if (contentToUse != null) {
+ // TODO: Validate that text is allowed as a child of this node
+ setTextContent(el, contentToUse);
+ } else if (childrenToUse != null) {
+ var mountImages = this.mountChildren(childrenToUse, transaction, context);
+ for (var i = 0; i < mountImages.length; i++) {
+ el.appendChild(mountImages[i]);
+ }
+ }
+ }
+ },
+
+ /**
+ * Receives a next element and updates the component.
+ *
+ * @internal
+ * @param {ReactElement} nextElement
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} context
+ */
+ receiveComponent: function (nextElement, transaction, context) {
+ var prevElement = this._currentElement;
+ this._currentElement = nextElement;
+ this.updateComponent(transaction, prevElement, nextElement, context);
+ },
+
+ /**
+ * Updates a native DOM component after it has already been allocated and
+ * attached to the DOM. Reconciles the root DOM node, then recurses.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @param {ReactElement} prevElement
+ * @param {ReactElement} nextElement
+ * @internal
+ * @overridable
+ */
+ updateComponent: function (transaction, prevElement, nextElement, context) {
+ var lastProps = prevElement.props;
+ var nextProps = this._currentElement.props;
+
+ switch (this._tag) {
+ case 'button':
+ lastProps = ReactDOMButton.getNativeProps(this, lastProps);
+ nextProps = ReactDOMButton.getNativeProps(this, nextProps);
+ break;
+ case 'input':
+ ReactDOMInput.updateWrapper(this);
+ lastProps = ReactDOMInput.getNativeProps(this, lastProps);
+ nextProps = ReactDOMInput.getNativeProps(this, nextProps);
+ break;
+ case 'option':
+ lastProps = ReactDOMOption.getNativeProps(this, lastProps);
+ nextProps = ReactDOMOption.getNativeProps(this, nextProps);
+ break;
+ case 'select':
+ lastProps = ReactDOMSelect.getNativeProps(this, lastProps);
+ nextProps = ReactDOMSelect.getNativeProps(this, nextProps);
+ break;
+ case 'textarea':
+ ReactDOMTextarea.updateWrapper(this);
+ lastProps = ReactDOMTextarea.getNativeProps(this, lastProps);
+ nextProps = ReactDOMTextarea.getNativeProps(this, nextProps);
+ break;
+ }
+
+ if ("development" !== 'production') {
+ // If the context is reference-equal to the old one, pass down the same
+ // processed object so the update bailout in ReactReconciler behaves
+ // correctly (and identically in dev and prod). See #5005.
+ if (this._unprocessedContextDev !== context) {
+ this._unprocessedContextDev = context;
+ this._processedContextDev = processChildContextDev(context, this);
+ }
+ context = this._processedContextDev;
+ }
+
+ assertValidProps(this, nextProps);
+ this._updateDOMProperties(lastProps, nextProps, transaction, null);
+ this._updateDOMChildren(lastProps, nextProps, transaction, context);
+
+ if (!canDefineProperty && this._nodeWithLegacyProperties) {
+ this._nodeWithLegacyProperties.props = nextProps;
+ }
+
+ if (this._tag === 'select') {
+ // <select> value update needs to occur after <option> children
+ // reconciliation
+ transaction.getReactMountReady().enqueue(postUpdateSelectWrapper, this);
+ }
+ },
+
+ /**
+ * Reconciles the properties by detecting differences in property values and
+ * updating the DOM as necessary. This function is probably the single most
+ * critical path for performance optimization.
+ *
+ * TODO: Benchmark whether checking for changed values in memory actually
+ * improves performance (especially statically positioned elements).
+ * TODO: Benchmark the effects of putting this at the top since 99% of props
+ * do not change for a given reconciliation.
+ * TODO: Benchmark areas that can be improved with caching.
+ *
+ * @private
+ * @param {object} lastProps
+ * @param {object} nextProps
+ * @param {ReactReconcileTransaction} transaction
+ * @param {?DOMElement} node
+ */
+ _updateDOMProperties: function (lastProps, nextProps, transaction, node) {
+ var propKey;
+ var styleName;
+ var styleUpdates;
+ for (propKey in lastProps) {
+ if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) {
+ continue;
+ }
+ if (propKey === STYLE) {
+ var lastStyle = this._previousStyleCopy;
+ for (styleName in lastStyle) {
+ if (lastStyle.hasOwnProperty(styleName)) {
+ styleUpdates = styleUpdates || {};
+ styleUpdates[styleName] = '';
+ }
+ }
+ this._previousStyleCopy = null;
+ } else if (registrationNameModules.hasOwnProperty(propKey)) {
+ if (lastProps[propKey]) {
+ // Only call deleteListener if there was a listener previously or
+ // else willDeleteListener gets called when there wasn't actually a
+ // listener (e.g., onClick={null})
+ deleteListener(this._rootNodeID, propKey);
+ }
+ } else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ DOMPropertyOperations.deleteValueForProperty(node, propKey);
+ }
+ }
+ for (propKey in nextProps) {
+ var nextProp = nextProps[propKey];
+ var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps[propKey];
+ if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
+ continue;
+ }
+ if (propKey === STYLE) {
+ if (nextProp) {
+ if ("development" !== 'production') {
+ checkAndWarnForMutatedStyle(this._previousStyleCopy, this._previousStyle, this);
+ this._previousStyle = nextProp;
+ }
+ nextProp = this._previousStyleCopy = assign({}, nextProp);
+ } else {
+ this._previousStyleCopy = null;
+ }
+ if (lastProp) {
+ // Unset styles on `lastProp` but not on `nextProp`.
+ for (styleName in lastProp) {
+ if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) {
+ styleUpdates = styleUpdates || {};
+ styleUpdates[styleName] = '';
+ }
+ }
+ // Update styles that changed since `lastProp`.
+ for (styleName in nextProp) {
+ if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
+ styleUpdates = styleUpdates || {};
+ styleUpdates[styleName] = nextProp[styleName];
+ }
+ }
+ } else {
+ // Relies on `updateStylesByID` not mutating `styleUpdates`.
+ styleUpdates = nextProp;
+ }
+ } else if (registrationNameModules.hasOwnProperty(propKey)) {
+ if (nextProp) {
+ enqueuePutListener(this._rootNodeID, propKey, nextProp, transaction);
+ } else if (lastProp) {
+ deleteListener(this._rootNodeID, propKey);
+ }
+ } else if (isCustomComponent(this._tag, nextProps)) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ if (propKey === CHILDREN) {
+ nextProp = null;
+ }
+ DOMPropertyOperations.setValueForAttribute(node, propKey, nextProp);
+ } else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ // If we're updating to null or undefined, we should remove the property
+ // from the DOM node instead of inadvertantly setting to a string. This
+ // brings us in line with the same behavior we have on initial render.
+ if (nextProp != null) {
+ DOMPropertyOperations.setValueForProperty(node, propKey, nextProp);
+ } else {
+ DOMPropertyOperations.deleteValueForProperty(node, propKey);
+ }
+ }
+ }
+ if (styleUpdates) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ CSSPropertyOperations.setValueForStyles(node, styleUpdates);
+ }
+ },
+
+ /**
+ * Reconciles the children with the various properties that affect the
+ * children content.
+ *
+ * @param {object} lastProps
+ * @param {object} nextProps
+ * @param {ReactReconcileTransaction} transaction
+ * @param {object} context
+ */
+ _updateDOMChildren: function (lastProps, nextProps, transaction, context) {
+ var lastContent = CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
+ var nextContent = CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
+
+ var lastHtml = lastProps.dangerouslySetInnerHTML && lastProps.dangerouslySetInnerHTML.__html;
+ var nextHtml = nextProps.dangerouslySetInnerHTML && nextProps.dangerouslySetInnerHTML.__html;
+
+ // Note the use of `!=` which checks for null or undefined.
+ var lastChildren = lastContent != null ? null : lastProps.children;
+ var nextChildren = nextContent != null ? null : nextProps.children;
+
+ // If we're switching from children to content/html or vice versa, remove
+ // the old content
+ var lastHasContentOrHtml = lastContent != null || lastHtml != null;
+ var nextHasContentOrHtml = nextContent != null || nextHtml != null;
+ if (lastChildren != null && nextChildren == null) {
+ this.updateChildren(null, transaction, context);
+ } else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
+ this.updateTextContent('');
+ }
+
+ if (nextContent != null) {
+ if (lastContent !== nextContent) {
+ this.updateTextContent('' + nextContent);
+ }
+ } else if (nextHtml != null) {
+ if (lastHtml !== nextHtml) {
+ this.updateMarkup('' + nextHtml);
+ }
+ } else if (nextChildren != null) {
+ this.updateChildren(nextChildren, transaction, context);
+ }
+ },
+
+ /**
+ * Destroys all event registrations for this instance. Does not remove from
+ * the DOM. That must be done by the parent.
+ *
+ * @internal
+ */
+ unmountComponent: function () {
+ switch (this._tag) {
+ case 'iframe':
+ case 'img':
+ case 'form':
+ case 'video':
+ case 'audio':
+ var listeners = this._wrapperState.listeners;
+ if (listeners) {
+ for (var i = 0; i < listeners.length; i++) {
+ listeners[i].remove();
+ }
+ }
+ break;
+ case 'input':
+ ReactDOMInput.unmountWrapper(this);
+ break;
+ case 'html':
+ case 'head':
+ case 'body':
+ /**
+ * Components like <html> <head> and <body> can't be removed or added
+ * easily in a cross-browser way, however it's valuable to be able to
+ * take advantage of React's reconciliation for styling and <title>
+ * management. So we just document it and throw in dangerous cases.
+ */
+ !false ? "development" !== 'production' ? invariant(false, '<%s> tried to unmount. Because of cross-browser quirks it is ' + 'impossible to unmount some top-level components (eg <html>, ' + '<head>, and <body>) reliably and efficiently. To fix this, have a ' + 'single top-level component that never unmounts render these ' + 'elements.', this._tag) : invariant(false) : undefined;
+ break;
+ }
+
+ this.unmountChildren();
+ ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID);
+ ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
+ this._rootNodeID = null;
+ this._wrapperState = null;
+ if (this._nodeWithLegacyProperties) {
+ var node = this._nodeWithLegacyProperties;
+ node._reactInternalComponent = null;
+ this._nodeWithLegacyProperties = null;
+ }
+ },
+
+ getPublicInstance: function () {
+ if (!this._nodeWithLegacyProperties) {
+ var node = ReactMount.getNode(this._rootNodeID);
+
+ node._reactInternalComponent = this;
+ node.getDOMNode = legacyGetDOMNode;
+ node.isMounted = legacyIsMounted;
+ node.setState = legacySetStateEtc;
+ node.replaceState = legacySetStateEtc;
+ node.forceUpdate = legacySetStateEtc;
+ node.setProps = legacySetProps;
+ node.replaceProps = legacyReplaceProps;
+
+ if ("development" !== 'production') {
+ if (canDefineProperty) {
+ Object.defineProperties(node, legacyPropsDescriptor);
+ } else {
+ // updateComponent will update this property on subsequent renders
+ node.props = this._currentElement.props;
+ }
+ } else {
+ // updateComponent will update this property on subsequent renders
+ node.props = this._currentElement.props;
+ }
+
+ this._nodeWithLegacyProperties = node;
+ }
+ return this._nodeWithLegacyProperties;
+ }
+
+};
+
+ReactPerf.measureMethods(ReactDOMComponent, 'ReactDOMComponent', {
+ mountComponent: 'mountComponent',
+ updateComponent: 'updateComponent'
+});
+
+assign(ReactDOMComponent.prototype, ReactDOMComponent.Mixin, ReactMultiChild.Mixin);
+
+module.exports = ReactDOMComponent;
+},{"10":10,"11":11,"117":117,"121":121,"133":133,"138":138,"139":139,"144":144,"15":15,"161":161,"166":166,"171":171,"173":173,"2":2,"24":24,"28":28,"35":35,"41":41,"46":46,"47":47,"48":48,"5":5,"52":52,"72":72,"73":73,"78":78,"95":95}],43:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMFactories
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactElementValidator = _dereq_(58);
+
+var mapObject = _dereq_(167);
+
+/**
+ * Create a factory that creates HTML tag elements.
+ *
+ * @param {string} tag Tag name (e.g. `div`).
+ * @private
+ */
+function createDOMFactory(tag) {
+ if ("development" !== 'production') {
+ return ReactElementValidator.createFactory(tag);
+ }
+ return ReactElement.createFactory(tag);
+}
+
+/**
+ * Creates a mapping from supported HTML tags to `ReactDOMComponent` classes.
+ * This is also accessible via `React.DOM`.
+ *
+ * @public
+ */
+var ReactDOMFactories = mapObject({
+ a: 'a',
+ abbr: 'abbr',
+ address: 'address',
+ area: 'area',
+ article: 'article',
+ aside: 'aside',
+ audio: 'audio',
+ b: 'b',
+ base: 'base',
+ bdi: 'bdi',
+ bdo: 'bdo',
+ big: 'big',
+ blockquote: 'blockquote',
+ body: 'body',
+ br: 'br',
+ button: 'button',
+ canvas: 'canvas',
+ caption: 'caption',
+ cite: 'cite',
+ code: 'code',
+ col: 'col',
+ colgroup: 'colgroup',
+ data: 'data',
+ datalist: 'datalist',
+ dd: 'dd',
+ del: 'del',
+ details: 'details',
+ dfn: 'dfn',
+ dialog: 'dialog',
+ div: 'div',
+ dl: 'dl',
+ dt: 'dt',
+ em: 'em',
+ embed: 'embed',
+ fieldset: 'fieldset',
+ figcaption: 'figcaption',
+ figure: 'figure',
+ footer: 'footer',
+ form: 'form',
+ h1: 'h1',
+ h2: 'h2',
+ h3: 'h3',
+ h4: 'h4',
+ h5: 'h5',
+ h6: 'h6',
+ head: 'head',
+ header: 'header',
+ hgroup: 'hgroup',
+ hr: 'hr',
+ html: 'html',
+ i: 'i',
+ iframe: 'iframe',
+ img: 'img',
+ input: 'input',
+ ins: 'ins',
+ kbd: 'kbd',
+ keygen: 'keygen',
+ label: 'label',
+ legend: 'legend',
+ li: 'li',
+ link: 'link',
+ main: 'main',
+ map: 'map',
+ mark: 'mark',
+ menu: 'menu',
+ menuitem: 'menuitem',
+ meta: 'meta',
+ meter: 'meter',
+ nav: 'nav',
+ noscript: 'noscript',
+ object: 'object',
+ ol: 'ol',
+ optgroup: 'optgroup',
+ option: 'option',
+ output: 'output',
+ p: 'p',
+ param: 'param',
+ picture: 'picture',
+ pre: 'pre',
+ progress: 'progress',
+ q: 'q',
+ rp: 'rp',
+ rt: 'rt',
+ ruby: 'ruby',
+ s: 's',
+ samp: 'samp',
+ script: 'script',
+ section: 'section',
+ select: 'select',
+ small: 'small',
+ source: 'source',
+ span: 'span',
+ strong: 'strong',
+ style: 'style',
+ sub: 'sub',
+ summary: 'summary',
+ sup: 'sup',
+ table: 'table',
+ tbody: 'tbody',
+ td: 'td',
+ textarea: 'textarea',
+ tfoot: 'tfoot',
+ th: 'th',
+ thead: 'thead',
+ time: 'time',
+ title: 'title',
+ tr: 'tr',
+ track: 'track',
+ u: 'u',
+ ul: 'ul',
+ 'var': 'var',
+ video: 'video',
+ wbr: 'wbr',
+
+ // SVG
+ circle: 'circle',
+ clipPath: 'clipPath',
+ defs: 'defs',
+ ellipse: 'ellipse',
+ g: 'g',
+ image: 'image',
+ line: 'line',
+ linearGradient: 'linearGradient',
+ mask: 'mask',
+ path: 'path',
+ pattern: 'pattern',
+ polygon: 'polygon',
+ polyline: 'polyline',
+ radialGradient: 'radialGradient',
+ rect: 'rect',
+ stop: 'stop',
+ svg: 'svg',
+ text: 'text',
+ tspan: 'tspan'
+
+}, createDOMFactory);
+
+module.exports = ReactDOMFactories;
+},{"167":167,"57":57,"58":58}],44:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMFeatureFlags
+ */
+
+'use strict';
+
+var ReactDOMFeatureFlags = {
+ useCreateElement: false
+};
+
+module.exports = ReactDOMFeatureFlags;
+},{}],45:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMIDOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMChildrenOperations = _dereq_(9);
+var DOMPropertyOperations = _dereq_(11);
+var ReactMount = _dereq_(72);
+var ReactPerf = _dereq_(78);
+
+var invariant = _dereq_(161);
+
+/**
+ * Errors for properties that should not be updated with `updatePropertyByID()`.
+ *
+ * @type {object}
+ * @private
+ */
+var INVALID_PROPERTY_ERRORS = {
+ dangerouslySetInnerHTML: '`dangerouslySetInnerHTML` must be set using `updateInnerHTMLByID()`.',
+ style: '`style` must be set using `updateStylesByID()`.'
+};
+
+/**
+ * Operations used to process updates to DOM nodes.
+ */
+var ReactDOMIDOperations = {
+
+ /**
+ * Updates a DOM node with new property values. This should only be used to
+ * update DOM properties in `DOMProperty`.
+ *
+ * @param {string} id ID of the node to update.
+ * @param {string} name A valid property name, see `DOMProperty`.
+ * @param {*} value New value of the property.
+ * @internal
+ */
+ updatePropertyByID: function (id, name, value) {
+ var node = ReactMount.getNode(id);
+ !!INVALID_PROPERTY_ERRORS.hasOwnProperty(name) ? "development" !== 'production' ? invariant(false, 'updatePropertyByID(...): %s', INVALID_PROPERTY_ERRORS[name]) : invariant(false) : undefined;
+
+ // If we're updating to null or undefined, we should remove the property
+ // from the DOM node instead of inadvertantly setting to a string. This
+ // brings us in line with the same behavior we have on initial render.
+ if (value != null) {
+ DOMPropertyOperations.setValueForProperty(node, name, value);
+ } else {
+ DOMPropertyOperations.deleteValueForProperty(node, name);
+ }
+ },
+
+ /**
+ * Replaces a DOM node that exists in the document with markup.
+ *
+ * @param {string} id ID of child to be replaced.
+ * @param {string} markup Dangerous markup to inject in place of child.
+ * @internal
+ * @see {Danger.dangerouslyReplaceNodeWithMarkup}
+ */
+ dangerouslyReplaceNodeWithMarkupByID: function (id, markup) {
+ var node = ReactMount.getNode(id);
+ DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup);
+ },
+
+ /**
+ * Updates a component's children by processing a series of updates.
+ *
+ * @param {array<object>} updates List of update configurations.
+ * @param {array<string>} markup List of markup strings.
+ * @internal
+ */
+ dangerouslyProcessChildrenUpdates: function (updates, markup) {
+ for (var i = 0; i < updates.length; i++) {
+ updates[i].parentNode = ReactMount.getNode(updates[i].parentID);
+ }
+ DOMChildrenOperations.processUpdates(updates, markup);
+ }
+};
+
+ReactPerf.measureMethods(ReactDOMIDOperations, 'ReactDOMIDOperations', {
+ dangerouslyReplaceNodeWithMarkupByID: 'dangerouslyReplaceNodeWithMarkupByID',
+ dangerouslyProcessChildrenUpdates: 'dangerouslyProcessChildrenUpdates'
+});
+
+module.exports = ReactDOMIDOperations;
+},{"11":11,"161":161,"72":72,"78":78,"9":9}],46:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMInput
+ */
+
+'use strict';
+
+var ReactDOMIDOperations = _dereq_(45);
+var LinkedValueUtils = _dereq_(23);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+var instancesByReactID = {};
+
+function forceUpdateIfMounted() {
+ if (this._rootNodeID) {
+ // DOM component is still mounted; update
+ ReactDOMInput.updateWrapper(this);
+ }
+}
+
+/**
+ * Implements an <input> native component that allows setting these optional
+ * props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
+ *
+ * If `checked` or `value` are not supplied (or null/undefined), user actions
+ * that affect the checked state or value will trigger updates to the element.
+ *
+ * If they are supplied (and not null/undefined), the rendered element will not
+ * trigger updates to the element. Instead, the props must change in order for
+ * the rendered element to be updated.
+ *
+ * The rendered element will be initialized as unchecked (or `defaultChecked`)
+ * with an empty value (or `defaultValue`).
+ *
+ * @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
+ */
+var ReactDOMInput = {
+ getNativeProps: function (inst, props, context) {
+ var value = LinkedValueUtils.getValue(props);
+ var checked = LinkedValueUtils.getChecked(props);
+
+ var nativeProps = assign({}, props, {
+ defaultChecked: undefined,
+ defaultValue: undefined,
+ value: value != null ? value : inst._wrapperState.initialValue,
+ checked: checked != null ? checked : inst._wrapperState.initialChecked,
+ onChange: inst._wrapperState.onChange
+ });
+
+ return nativeProps;
+ },
+
+ mountWrapper: function (inst, props) {
+ if ("development" !== 'production') {
+ LinkedValueUtils.checkPropTypes('input', props, inst._currentElement._owner);
+ }
+
+ var defaultValue = props.defaultValue;
+ inst._wrapperState = {
+ initialChecked: props.defaultChecked || false,
+ initialValue: defaultValue != null ? defaultValue : null,
+ onChange: _handleChange.bind(inst)
+ };
+ },
+
+ mountReadyWrapper: function (inst) {
+ // Can't be in mountWrapper or else server rendering leaks.
+ instancesByReactID[inst._rootNodeID] = inst;
+ },
+
+ unmountWrapper: function (inst) {
+ delete instancesByReactID[inst._rootNodeID];
+ },
+
+ updateWrapper: function (inst) {
+ var props = inst._currentElement.props;
+
+ // TODO: Shouldn't this be getChecked(props)?
+ var checked = props.checked;
+ if (checked != null) {
+ ReactDOMIDOperations.updatePropertyByID(inst._rootNodeID, 'checked', checked || false);
+ }
+
+ var value = LinkedValueUtils.getValue(props);
+ if (value != null) {
+ // Cast `value` to a string to ensure the value is set correctly. While
+ // browsers typically do this as necessary, jsdom doesn't.
+ ReactDOMIDOperations.updatePropertyByID(inst._rootNodeID, 'value', '' + value);
+ }
+ }
+};
+
+function _handleChange(event) {
+ var props = this._currentElement.props;
+
+ var returnValue = LinkedValueUtils.executeOnChange(props, event);
+
+ // Here we use asap to wait until all updates have propagated, which
+ // is important when using controlled components within layers:
+ // https://github.com/facebook/react/issues/1698
+ ReactUpdates.asap(forceUpdateIfMounted, this);
+
+ var name = props.name;
+ if (props.type === 'radio' && name != null) {
+ var rootNode = ReactMount.getNode(this._rootNodeID);
+ var queryRoot = rootNode;
+
+ while (queryRoot.parentNode) {
+ queryRoot = queryRoot.parentNode;
+ }
+
+ // If `rootNode.form` was non-null, then we could try `form.elements`,
+ // but that sometimes behaves strangely in IE8. We could also try using
+ // `form.getElementsByName`, but that will only return direct children
+ // and won't include inputs that use the HTML5 `form=` attribute. Since
+ // the input might not even be in a form, let's just use the global
+ // `querySelectorAll` to ensure we don't miss anything.
+ var group = queryRoot.querySelectorAll('input[name=' + JSON.stringify('' + name) + '][type="radio"]');
+
+ for (var i = 0; i < group.length; i++) {
+ var otherNode = group[i];
+ if (otherNode === rootNode || otherNode.form !== rootNode.form) {
+ continue;
+ }
+ // This will throw if radio buttons rendered by different copies of React
+ // and the same name are rendered into the same form (same as #1939).
+ // That's probably okay; we don't support it just as we don't support
+ // mixing React with non-React.
+ var otherID = ReactMount.getID(otherNode);
+ !otherID ? "development" !== 'production' ? invariant(false, 'ReactDOMInput: Mixing React and non-React radio inputs with the ' + 'same `name` is not supported.') : invariant(false) : undefined;
+ var otherInstance = instancesByReactID[otherID];
+ !otherInstance ? "development" !== 'production' ? invariant(false, 'ReactDOMInput: Unknown radio button ID %s.', otherID) : invariant(false) : undefined;
+ // If this is a controlled radio button group, forcing the input that
+ // was previously checked to update will cause it to be come re-checked
+ // as appropriate.
+ ReactUpdates.asap(forceUpdateIfMounted, otherInstance);
+ }
+ }
+
+ return returnValue;
+}
+
+module.exports = ReactDOMInput;
+},{"161":161,"23":23,"24":24,"45":45,"72":72,"96":96}],47:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMOption
+ */
+
+'use strict';
+
+var ReactChildren = _dereq_(32);
+var ReactDOMSelect = _dereq_(48);
+
+var assign = _dereq_(24);
+var warning = _dereq_(173);
+
+var valueContextKey = ReactDOMSelect.valueContextKey;
+
+/**
+ * Implements an <option> native component that warns when `selected` is set.
+ */
+var ReactDOMOption = {
+ mountWrapper: function (inst, props, context) {
+ // TODO (yungsters): Remove support for `selected` in <option>.
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(props.selected == null, 'Use the `defaultValue` or `value` props on <select> instead of ' + 'setting `selected` on <option>.') : undefined;
+ }
+
+ // Look up whether this option is 'selected' via context
+ var selectValue = context[valueContextKey];
+
+ // If context key is null (e.g., no specified value or after initial mount)
+ // or missing (e.g., for <datalist>), we don't change props.selected
+ var selected = null;
+ if (selectValue != null) {
+ selected = false;
+ if (Array.isArray(selectValue)) {
+ // multiple
+ for (var i = 0; i < selectValue.length; i++) {
+ if ('' + selectValue[i] === '' + props.value) {
+ selected = true;
+ break;
+ }
+ }
+ } else {
+ selected = '' + selectValue === '' + props.value;
+ }
+ }
+
+ inst._wrapperState = { selected: selected };
+ },
+
+ getNativeProps: function (inst, props, context) {
+ var nativeProps = assign({ selected: undefined, children: undefined }, props);
+
+ // Read state only from initial mount because <select> updates value
+ // manually; we need the initial state only for server rendering
+ if (inst._wrapperState.selected != null) {
+ nativeProps.selected = inst._wrapperState.selected;
+ }
+
+ var content = '';
+
+ // Flatten children and warn if they aren't strings or numbers;
+ // invalid types are ignored.
+ ReactChildren.forEach(props.children, function (child) {
+ if (child == null) {
+ return;
+ }
+ if (typeof child === 'string' || typeof child === 'number') {
+ content += child;
+ } else {
+ "development" !== 'production' ? warning(false, 'Only strings and numbers are supported as <option> children.') : undefined;
+ }
+ });
+
+ nativeProps.children = content;
+ return nativeProps;
+ }
+
+};
+
+module.exports = ReactDOMOption;
+},{"173":173,"24":24,"32":32,"48":48}],48:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMSelect
+ */
+
+'use strict';
+
+var LinkedValueUtils = _dereq_(23);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var warning = _dereq_(173);
+
+var valueContextKey = '__ReactDOMSelect_value$' + Math.random().toString(36).slice(2);
+
+function updateOptionsIfPendingUpdateAndMounted() {
+ if (this._rootNodeID && this._wrapperState.pendingUpdate) {
+ this._wrapperState.pendingUpdate = false;
+
+ var props = this._currentElement.props;
+ var value = LinkedValueUtils.getValue(props);
+
+ if (value != null) {
+ updateOptions(this, Boolean(props.multiple), value);
+ }
+ }
+}
+
+function getDeclarationErrorAddendum(owner) {
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+var valuePropNames = ['value', 'defaultValue'];
+
+/**
+ * Validation function for `value` and `defaultValue`.
+ * @private
+ */
+function checkSelectPropTypes(inst, props) {
+ var owner = inst._currentElement._owner;
+ LinkedValueUtils.checkPropTypes('select', props, owner);
+
+ for (var i = 0; i < valuePropNames.length; i++) {
+ var propName = valuePropNames[i];
+ if (props[propName] == null) {
+ continue;
+ }
+ if (props.multiple) {
+ "development" !== 'production' ? warning(Array.isArray(props[propName]), 'The `%s` prop supplied to <select> must be an array if ' + '`multiple` is true.%s', propName, getDeclarationErrorAddendum(owner)) : undefined;
+ } else {
+ "development" !== 'production' ? warning(!Array.isArray(props[propName]), 'The `%s` prop supplied to <select> must be a scalar ' + 'value if `multiple` is false.%s', propName, getDeclarationErrorAddendum(owner)) : undefined;
+ }
+ }
+}
+
+/**
+ * @param {ReactDOMComponent} inst
+ * @param {boolean} multiple
+ * @param {*} propValue A stringable (with `multiple`, a list of stringables).
+ * @private
+ */
+function updateOptions(inst, multiple, propValue) {
+ var selectedValue, i;
+ var options = ReactMount.getNode(inst._rootNodeID).options;
+
+ if (multiple) {
+ selectedValue = {};
+ for (i = 0; i < propValue.length; i++) {
+ selectedValue['' + propValue[i]] = true;
+ }
+ for (i = 0; i < options.length; i++) {
+ var selected = selectedValue.hasOwnProperty(options[i].value);
+ if (options[i].selected !== selected) {
+ options[i].selected = selected;
+ }
+ }
+ } else {
+ // Do not set `select.value` as exact behavior isn't consistent across all
+ // browsers for all cases.
+ selectedValue = '' + propValue;
+ for (i = 0; i < options.length; i++) {
+ if (options[i].value === selectedValue) {
+ options[i].selected = true;
+ return;
+ }
+ }
+ if (options.length) {
+ options[0].selected = true;
+ }
+ }
+}
+
+/**
+ * Implements a <select> native component that allows optionally setting the
+ * props `value` and `defaultValue`. If `multiple` is false, the prop must be a
+ * stringable. If `multiple` is true, the prop must be an array of stringables.
+ *
+ * If `value` is not supplied (or null/undefined), user actions that change the
+ * selected option will trigger updates to the rendered options.
+ *
+ * If it is supplied (and not null/undefined), the rendered options will not
+ * update in response to user actions. Instead, the `value` prop must change in
+ * order for the rendered options to update.
+ *
+ * If `defaultValue` is provided, any options with the supplied values will be
+ * selected.
+ */
+var ReactDOMSelect = {
+ valueContextKey: valueContextKey,
+
+ getNativeProps: function (inst, props, context) {
+ return assign({}, props, {
+ onChange: inst._wrapperState.onChange,
+ value: undefined
+ });
+ },
+
+ mountWrapper: function (inst, props) {
+ if ("development" !== 'production') {
+ checkSelectPropTypes(inst, props);
+ }
+
+ var value = LinkedValueUtils.getValue(props);
+ inst._wrapperState = {
+ pendingUpdate: false,
+ initialValue: value != null ? value : props.defaultValue,
+ onChange: _handleChange.bind(inst),
+ wasMultiple: Boolean(props.multiple)
+ };
+ },
+
+ processChildContext: function (inst, props, context) {
+ // Pass down initial value so initial generated markup has correct
+ // `selected` attributes
+ var childContext = assign({}, context);
+ childContext[valueContextKey] = inst._wrapperState.initialValue;
+ return childContext;
+ },
+
+ postUpdateWrapper: function (inst) {
+ var props = inst._currentElement.props;
+
+ // After the initial mount, we control selected-ness manually so don't pass
+ // the context value down
+ inst._wrapperState.initialValue = undefined;
+
+ var wasMultiple = inst._wrapperState.wasMultiple;
+ inst._wrapperState.wasMultiple = Boolean(props.multiple);
+
+ var value = LinkedValueUtils.getValue(props);
+ if (value != null) {
+ inst._wrapperState.pendingUpdate = false;
+ updateOptions(inst, Boolean(props.multiple), value);
+ } else if (wasMultiple !== Boolean(props.multiple)) {
+ // For simplicity, reapply `defaultValue` if `multiple` is toggled.
+ if (props.defaultValue != null) {
+ updateOptions(inst, Boolean(props.multiple), props.defaultValue);
+ } else {
+ // Revert the select back to its default unselected state.
+ updateOptions(inst, Boolean(props.multiple), props.multiple ? [] : '');
+ }
+ }
+ }
+};
+
+function _handleChange(event) {
+ var props = this._currentElement.props;
+ var returnValue = LinkedValueUtils.executeOnChange(props, event);
+
+ this._wrapperState.pendingUpdate = true;
+ ReactUpdates.asap(updateOptionsIfPendingUpdateAndMounted, this);
+ return returnValue;
+}
+
+module.exports = ReactDOMSelect;
+},{"173":173,"23":23,"24":24,"72":72,"96":96}],49:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMSelection
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var getNodeForCharacterOffset = _dereq_(130);
+var getTextContentAccessor = _dereq_(131);
+
+/**
+ * While `isCollapsed` is available on the Selection object and `collapsed`
+ * is available on the Range object, IE11 sometimes gets them wrong.
+ * If the anchor/focus nodes and offsets are the same, the range is collapsed.
+ */
+function isCollapsed(anchorNode, anchorOffset, focusNode, focusOffset) {
+ return anchorNode === focusNode && anchorOffset === focusOffset;
+}
+
+/**
+ * Get the appropriate anchor and focus node/offset pairs for IE.
+ *
+ * The catch here is that IE's selection API doesn't provide information
+ * about whether the selection is forward or backward, so we have to
+ * behave as though it's always forward.
+ *
+ * IE text differs from modern selection in that it behaves as though
+ * block elements end with a new line. This means character offsets will
+ * differ between the two APIs.
+ *
+ * @param {DOMElement} node
+ * @return {object}
+ */
+function getIEOffsets(node) {
+ var selection = document.selection;
+ var selectedRange = selection.createRange();
+ var selectedLength = selectedRange.text.length;
+
+ // Duplicate selection so we can move range without breaking user selection.
+ var fromStart = selectedRange.duplicate();
+ fromStart.moveToElementText(node);
+ fromStart.setEndPoint('EndToStart', selectedRange);
+
+ var startOffset = fromStart.text.length;
+ var endOffset = startOffset + selectedLength;
+
+ return {
+ start: startOffset,
+ end: endOffset
+ };
+}
+
+/**
+ * @param {DOMElement} node
+ * @return {?object}
+ */
+function getModernOffsets(node) {
+ var selection = window.getSelection && window.getSelection();
+
+ if (!selection || selection.rangeCount === 0) {
+ return null;
+ }
+
+ var anchorNode = selection.anchorNode;
+ var anchorOffset = selection.anchorOffset;
+ var focusNode = selection.focusNode;
+ var focusOffset = selection.focusOffset;
+
+ var currentRange = selection.getRangeAt(0);
+
+ // In Firefox, range.startContainer and range.endContainer can be "anonymous
+ // divs", e.g. the up/down buttons on an <input type="number">. Anonymous
+ // divs do not seem to expose properties, triggering a "Permission denied
+ // error" if any of its properties are accessed. The only seemingly possible
+ // way to avoid erroring is to access a property that typically works for
+ // non-anonymous divs and catch any error that may otherwise arise. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=208427
+ try {
+ /* eslint-disable no-unused-expressions */
+ currentRange.startContainer.nodeType;
+ currentRange.endContainer.nodeType;
+ /* eslint-enable no-unused-expressions */
+ } catch (e) {
+ return null;
+ }
+
+ // If the node and offset values are the same, the selection is collapsed.
+ // `Selection.isCollapsed` is available natively, but IE sometimes gets
+ // this value wrong.
+ var isSelectionCollapsed = isCollapsed(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
+
+ var rangeLength = isSelectionCollapsed ? 0 : currentRange.toString().length;
+
+ var tempRange = currentRange.cloneRange();
+ tempRange.selectNodeContents(node);
+ tempRange.setEnd(currentRange.startContainer, currentRange.startOffset);
+
+ var isTempRangeCollapsed = isCollapsed(tempRange.startContainer, tempRange.startOffset, tempRange.endContainer, tempRange.endOffset);
+
+ var start = isTempRangeCollapsed ? 0 : tempRange.toString().length;
+ var end = start + rangeLength;
+
+ // Detect whether the selection is backward.
+ var detectionRange = document.createRange();
+ detectionRange.setStart(anchorNode, anchorOffset);
+ detectionRange.setEnd(focusNode, focusOffset);
+ var isBackward = detectionRange.collapsed;
+
+ return {
+ start: isBackward ? end : start,
+ end: isBackward ? start : end
+ };
+}
+
+/**
+ * @param {DOMElement|DOMTextNode} node
+ * @param {object} offsets
+ */
+function setIEOffsets(node, offsets) {
+ var range = document.selection.createRange().duplicate();
+ var start, end;
+
+ if (typeof offsets.end === 'undefined') {
+ start = offsets.start;
+ end = start;
+ } else if (offsets.start > offsets.end) {
+ start = offsets.end;
+ end = offsets.start;
+ } else {
+ start = offsets.start;
+ end = offsets.end;
+ }
+
+ range.moveToElementText(node);
+ range.moveStart('character', start);
+ range.setEndPoint('EndToStart', range);
+ range.moveEnd('character', end - start);
+ range.select();
+}
+
+/**
+ * In modern non-IE browsers, we can support both forward and backward
+ * selections.
+ *
+ * Note: IE10+ supports the Selection object, but it does not support
+ * the `extend` method, which means that even in modern IE, it's not possible
+ * to programatically create a backward selection. Thus, for all IE
+ * versions, we use the old IE API to create our selections.
+ *
+ * @param {DOMElement|DOMTextNode} node
+ * @param {object} offsets
+ */
+function setModernOffsets(node, offsets) {
+ if (!window.getSelection) {
+ return;
+ }
+
+ var selection = window.getSelection();
+ var length = node[getTextContentAccessor()].length;
+ var start = Math.min(offsets.start, length);
+ var end = typeof offsets.end === 'undefined' ? start : Math.min(offsets.end, length);
+
+ // IE 11 uses modern selection, but doesn't support the extend method.
+ // Flip backward selections, so we can set with a single range.
+ if (!selection.extend && start > end) {
+ var temp = end;
+ end = start;
+ start = temp;
+ }
+
+ var startMarker = getNodeForCharacterOffset(node, start);
+ var endMarker = getNodeForCharacterOffset(node, end);
+
+ if (startMarker && endMarker) {
+ var range = document.createRange();
+ range.setStart(startMarker.node, startMarker.offset);
+ selection.removeAllRanges();
+
+ if (start > end) {
+ selection.addRange(range);
+ selection.extend(endMarker.node, endMarker.offset);
+ } else {
+ range.setEnd(endMarker.node, endMarker.offset);
+ selection.addRange(range);
+ }
+ }
+}
+
+var useIEOffsets = ExecutionEnvironment.canUseDOM && 'selection' in document && !('getSelection' in window);
+
+var ReactDOMSelection = {
+ /**
+ * @param {DOMElement} node
+ */
+ getOffsets: useIEOffsets ? getIEOffsets : getModernOffsets,
+
+ /**
+ * @param {DOMElement|DOMTextNode} node
+ * @param {object} offsets
+ */
+ setOffsets: useIEOffsets ? setIEOffsets : setModernOffsets
+};
+
+module.exports = ReactDOMSelection;
+},{"130":130,"131":131,"147":147}],50:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMServer
+ */
+
+'use strict';
+
+var ReactDefaultInjection = _dereq_(54);
+var ReactServerRendering = _dereq_(88);
+var ReactVersion = _dereq_(97);
+
+ReactDefaultInjection.inject();
+
+var ReactDOMServer = {
+ renderToString: ReactServerRendering.renderToString,
+ renderToStaticMarkup: ReactServerRendering.renderToStaticMarkup,
+ version: ReactVersion
+};
+
+module.exports = ReactDOMServer;
+},{"54":54,"88":88,"97":97}],51:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMTextComponent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMChildrenOperations = _dereq_(9);
+var DOMPropertyOperations = _dereq_(11);
+var ReactComponentBrowserEnvironment = _dereq_(35);
+var ReactMount = _dereq_(72);
+
+var assign = _dereq_(24);
+var escapeTextContentForBrowser = _dereq_(121);
+var setTextContent = _dereq_(139);
+var validateDOMNesting = _dereq_(144);
+
+/**
+ * Text nodes violate a couple assumptions that React makes about components:
+ *
+ * - When mounting text into the DOM, adjacent text nodes are merged.
+ * - Text nodes cannot be assigned a React root ID.
+ *
+ * This component is used to wrap strings in elements so that they can undergo
+ * the same reconciliation that is applied to elements.
+ *
+ * TODO: Investigate representing React components in the DOM with text nodes.
+ *
+ * @class ReactDOMTextComponent
+ * @extends ReactComponent
+ * @internal
+ */
+var ReactDOMTextComponent = function (props) {
+ // This constructor and its argument is currently used by mocks.
+};
+
+assign(ReactDOMTextComponent.prototype, {
+
+ /**
+ * @param {ReactText} text
+ * @internal
+ */
+ construct: function (text) {
+ // TODO: This is really a ReactText (ReactNode), not a ReactElement
+ this._currentElement = text;
+ this._stringText = '' + text;
+
+ // Properties
+ this._rootNodeID = null;
+ this._mountIndex = 0;
+ },
+
+ /**
+ * Creates the markup for this text node. This node is not intended to have
+ * any features besides containing text content.
+ *
+ * @param {string} rootID DOM ID of the root node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @return {string} Markup for this text node.
+ * @internal
+ */
+ mountComponent: function (rootID, transaction, context) {
+ if ("development" !== 'production') {
+ if (context[validateDOMNesting.ancestorInfoContextKey]) {
+ validateDOMNesting('span', null, context[validateDOMNesting.ancestorInfoContextKey]);
+ }
+ }
+
+ this._rootNodeID = rootID;
+ if (transaction.useCreateElement) {
+ var ownerDocument = context[ReactMount.ownerDocumentContextKey];
+ var el = ownerDocument.createElementNS('http://www.w3.org/1999/xhtml', 'span');
+ DOMPropertyOperations.setAttributeForID(el, rootID);
+ // Populate node cache
+ ReactMount.getID(el);
+ setTextContent(el, this._stringText);
+ return el;
+ } else {
+ var escapedText = escapeTextContentForBrowser(this._stringText);
+
+ if (transaction.renderToStaticMarkup) {
+ // Normally we'd wrap this in a `span` for the reasons stated above, but
+ // since this is a situation where React won't take over (static pages),
+ // we can simply return the text as it is.
+ return escapedText;
+ }
+
+ return '<span ' + DOMPropertyOperations.createMarkupForID(rootID) + '>' + escapedText + '</span>';
+ }
+ },
+
+ /**
+ * Updates this component by updating the text content.
+ *
+ * @param {ReactText} nextText The next text content
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ receiveComponent: function (nextText, transaction) {
+ if (nextText !== this._currentElement) {
+ this._currentElement = nextText;
+ var nextStringText = '' + nextText;
+ if (nextStringText !== this._stringText) {
+ // TODO: Save this as pending props and use performUpdateIfNecessary
+ // and/or updateComponent to do the actual update for consistency with
+ // other component types?
+ this._stringText = nextStringText;
+ var node = ReactMount.getNode(this._rootNodeID);
+ DOMChildrenOperations.updateTextContent(node, nextStringText);
+ }
+ }
+ },
+
+ unmountComponent: function () {
+ ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
+ }
+
+});
+
+module.exports = ReactDOMTextComponent;
+},{"11":11,"121":121,"139":139,"144":144,"24":24,"35":35,"72":72,"9":9}],52:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMTextarea
+ */
+
+'use strict';
+
+var LinkedValueUtils = _dereq_(23);
+var ReactDOMIDOperations = _dereq_(45);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+function forceUpdateIfMounted() {
+ if (this._rootNodeID) {
+ // DOM component is still mounted; update
+ ReactDOMTextarea.updateWrapper(this);
+ }
+}
+
+/**
+ * Implements a <textarea> native component that allows setting `value`, and
+ * `defaultValue`. This differs from the traditional DOM API because value is
+ * usually set as PCDATA children.
+ *
+ * If `value` is not supplied (or null/undefined), user actions that affect the
+ * value will trigger updates to the element.
+ *
+ * If `value` is supplied (and not null/undefined), the rendered element will
+ * not trigger updates to the element. Instead, the `value` prop must change in
+ * order for the rendered element to be updated.
+ *
+ * The rendered element will be initialized with an empty value, the prop
+ * `defaultValue` if specified, or the children content (deprecated).
+ */
+var ReactDOMTextarea = {
+ getNativeProps: function (inst, props, context) {
+ !(props.dangerouslySetInnerHTML == null) ? "development" !== 'production' ? invariant(false, '`dangerouslySetInnerHTML` does not make sense on <textarea>.') : invariant(false) : undefined;
+
+ // Always set children to the same thing. In IE9, the selection range will
+ // get reset if `textContent` is mutated.
+ var nativeProps = assign({}, props, {
+ defaultValue: undefined,
+ value: undefined,
+ children: inst._wrapperState.initialValue,
+ onChange: inst._wrapperState.onChange
+ });
+
+ return nativeProps;
+ },
+
+ mountWrapper: function (inst, props) {
+ if ("development" !== 'production') {
+ LinkedValueUtils.checkPropTypes('textarea', props, inst._currentElement._owner);
+ }
+
+ var defaultValue = props.defaultValue;
+ // TODO (yungsters): Remove support for children content in <textarea>.
+ var children = props.children;
+ if (children != null) {
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(false, 'Use the `defaultValue` or `value` props instead of setting ' + 'children on <textarea>.') : undefined;
+ }
+ !(defaultValue == null) ? "development" !== 'production' ? invariant(false, 'If you supply `defaultValue` on a <textarea>, do not pass children.') : invariant(false) : undefined;
+ if (Array.isArray(children)) {
+ !(children.length <= 1) ? "development" !== 'production' ? invariant(false, '<textarea> can only have at most one child.') : invariant(false) : undefined;
+ children = children[0];
+ }
+
+ defaultValue = '' + children;
+ }
+ if (defaultValue == null) {
+ defaultValue = '';
+ }
+ var value = LinkedValueUtils.getValue(props);
+
+ inst._wrapperState = {
+ // We save the initial value so that `ReactDOMComponent` doesn't update
+ // `textContent` (unnecessary since we update value).
+ // The initial value can be a boolean or object so that's why it's
+ // forced to be a string.
+ initialValue: '' + (value != null ? value : defaultValue),
+ onChange: _handleChange.bind(inst)
+ };
+ },
+
+ updateWrapper: function (inst) {
+ var props = inst._currentElement.props;
+ var value = LinkedValueUtils.getValue(props);
+ if (value != null) {
+ // Cast `value` to a string to ensure the value is set correctly. While
+ // browsers typically do this as necessary, jsdom doesn't.
+ ReactDOMIDOperations.updatePropertyByID(inst._rootNodeID, 'value', '' + value);
+ }
+ }
+};
+
+function _handleChange(event) {
+ var props = this._currentElement.props;
+ var returnValue = LinkedValueUtils.executeOnChange(props, event);
+ ReactUpdates.asap(forceUpdateIfMounted, this);
+ return returnValue;
+}
+
+module.exports = ReactDOMTextarea;
+},{"161":161,"173":173,"23":23,"24":24,"45":45,"96":96}],53:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultBatchingStrategy
+ */
+
+'use strict';
+
+var ReactUpdates = _dereq_(96);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+
+var RESET_BATCHED_UPDATES = {
+ initialize: emptyFunction,
+ close: function () {
+ ReactDefaultBatchingStrategy.isBatchingUpdates = false;
+ }
+};
+
+var FLUSH_BATCHED_UPDATES = {
+ initialize: emptyFunction,
+ close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
+};
+
+var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
+
+function ReactDefaultBatchingStrategyTransaction() {
+ this.reinitializeTransaction();
+}
+
+assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction.Mixin, {
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ }
+});
+
+var transaction = new ReactDefaultBatchingStrategyTransaction();
+
+var ReactDefaultBatchingStrategy = {
+ isBatchingUpdates: false,
+
+ /**
+ * Call the provided function in a context within which calls to `setState`
+ * and friends are batched such that components aren't updated unnecessarily.
+ */
+ batchedUpdates: function (callback, a, b, c, d, e) {
+ var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
+
+ ReactDefaultBatchingStrategy.isBatchingUpdates = true;
+
+ // The code is written this way to avoid extra allocations
+ if (alreadyBatchingUpdates) {
+ callback(a, b, c, d, e);
+ } else {
+ transaction.perform(callback, null, a, b, c, d, e);
+ }
+ }
+};
+
+module.exports = ReactDefaultBatchingStrategy;
+},{"113":113,"153":153,"24":24,"96":96}],54:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultInjection
+ */
+
+'use strict';
+
+var BeforeInputEventPlugin = _dereq_(3);
+var ChangeEventPlugin = _dereq_(7);
+var ClientReactRootIndex = _dereq_(8);
+var DefaultEventPluginOrder = _dereq_(13);
+var EnterLeaveEventPlugin = _dereq_(14);
+var ExecutionEnvironment = _dereq_(147);
+var HTMLDOMPropertyConfig = _dereq_(21);
+var ReactBrowserComponentMixin = _dereq_(27);
+var ReactComponentBrowserEnvironment = _dereq_(35);
+var ReactDefaultBatchingStrategy = _dereq_(53);
+var ReactDOMComponent = _dereq_(42);
+var ReactDOMTextComponent = _dereq_(51);
+var ReactEventListener = _dereq_(63);
+var ReactInjection = _dereq_(65);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMount = _dereq_(72);
+var ReactReconcileTransaction = _dereq_(83);
+var SelectEventPlugin = _dereq_(99);
+var ServerReactRootIndex = _dereq_(100);
+var SimpleEventPlugin = _dereq_(101);
+var SVGDOMPropertyConfig = _dereq_(98);
+
+var alreadyInjected = false;
+
+function inject() {
+ if (alreadyInjected) {
+ // TODO: This is currently true because these injections are shared between
+ // the client and the server package. They should be built independently
+ // and not share any injection state. Then this problem will be solved.
+ return;
+ }
+ alreadyInjected = true;
+
+ ReactInjection.EventEmitter.injectReactEventListener(ReactEventListener);
+
+ /**
+ * Inject modules for resolving DOM hierarchy and plugin ordering.
+ */
+ ReactInjection.EventPluginHub.injectEventPluginOrder(DefaultEventPluginOrder);
+ ReactInjection.EventPluginHub.injectInstanceHandle(ReactInstanceHandles);
+ ReactInjection.EventPluginHub.injectMount(ReactMount);
+
+ /**
+ * Some important event plugins included by default (without having to require
+ * them).
+ */
+ ReactInjection.EventPluginHub.injectEventPluginsByName({
+ SimpleEventPlugin: SimpleEventPlugin,
+ EnterLeaveEventPlugin: EnterLeaveEventPlugin,
+ ChangeEventPlugin: ChangeEventPlugin,
+ SelectEventPlugin: SelectEventPlugin,
+ BeforeInputEventPlugin: BeforeInputEventPlugin
+ });
+
+ ReactInjection.NativeComponent.injectGenericComponentClass(ReactDOMComponent);
+
+ ReactInjection.NativeComponent.injectTextComponentClass(ReactDOMTextComponent);
+
+ ReactInjection.Class.injectMixin(ReactBrowserComponentMixin);
+
+ ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig);
+ ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig);
+
+ ReactInjection.EmptyComponent.injectEmptyComponent('noscript');
+
+ ReactInjection.Updates.injectReconcileTransaction(ReactReconcileTransaction);
+ ReactInjection.Updates.injectBatchingStrategy(ReactDefaultBatchingStrategy);
+
+ ReactInjection.RootIndex.injectCreateReactRootIndex(ExecutionEnvironment.canUseDOM ? ClientReactRootIndex.createReactRootIndex : ServerReactRootIndex.createReactRootIndex);
+
+ ReactInjection.Component.injectEnvironment(ReactComponentBrowserEnvironment);
+
+ if ("development" !== 'production') {
+ var url = ExecutionEnvironment.canUseDOM && window.location.href || '';
+ if (/[?&]react_perf\b/.test(url)) {
+ var ReactDefaultPerf = _dereq_(55);
+ ReactDefaultPerf.start();
+ }
+ }
+}
+
+module.exports = {
+ inject: inject
+};
+},{"100":100,"101":101,"13":13,"14":14,"147":147,"21":21,"27":27,"3":3,"35":35,"42":42,"51":51,"53":53,"55":55,"63":63,"65":65,"67":67,"7":7,"72":72,"8":8,"83":83,"98":98,"99":99}],55:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultPerf
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ReactDefaultPerfAnalysis = _dereq_(56);
+var ReactMount = _dereq_(72);
+var ReactPerf = _dereq_(78);
+
+var performanceNow = _dereq_(170);
+
+function roundFloat(val) {
+ return Math.floor(val * 100) / 100;
+}
+
+function addValue(obj, key, val) {
+ obj[key] = (obj[key] || 0) + val;
+}
+
+var ReactDefaultPerf = {
+ _allMeasurements: [], // last item in the list is the current one
+ _mountStack: [0],
+ _injected: false,
+
+ start: function () {
+ if (!ReactDefaultPerf._injected) {
+ ReactPerf.injection.injectMeasure(ReactDefaultPerf.measure);
+ }
+
+ ReactDefaultPerf._allMeasurements.length = 0;
+ ReactPerf.enableMeasure = true;
+ },
+
+ stop: function () {
+ ReactPerf.enableMeasure = false;
+ },
+
+ getLastMeasurements: function () {
+ return ReactDefaultPerf._allMeasurements;
+ },
+
+ printExclusive: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ var summary = ReactDefaultPerfAnalysis.getExclusiveSummary(measurements);
+ console.table(summary.map(function (item) {
+ return {
+ 'Component class name': item.componentName,
+ 'Total inclusive time (ms)': roundFloat(item.inclusive),
+ 'Exclusive mount time (ms)': roundFloat(item.exclusive),
+ 'Exclusive render time (ms)': roundFloat(item.render),
+ 'Mount time per instance (ms)': roundFloat(item.exclusive / item.count),
+ 'Render time per instance (ms)': roundFloat(item.render / item.count),
+ 'Instances': item.count
+ };
+ }));
+ // TODO: ReactDefaultPerfAnalysis.getTotalTime() does not return the correct
+ // number.
+ },
+
+ printInclusive: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(measurements);
+ console.table(summary.map(function (item) {
+ return {
+ 'Owner > component': item.componentName,
+ 'Inclusive time (ms)': roundFloat(item.time),
+ 'Instances': item.count
+ };
+ }));
+ console.log('Total time:', ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms');
+ },
+
+ getMeasurementsSummaryMap: function (measurements) {
+ var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(measurements, true);
+ return summary.map(function (item) {
+ return {
+ 'Owner > component': item.componentName,
+ 'Wasted time (ms)': item.time,
+ 'Instances': item.count
+ };
+ });
+ },
+
+ printWasted: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ console.table(ReactDefaultPerf.getMeasurementsSummaryMap(measurements));
+ console.log('Total time:', ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms');
+ },
+
+ printDOM: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ var summary = ReactDefaultPerfAnalysis.getDOMSummary(measurements);
+ console.table(summary.map(function (item) {
+ var result = {};
+ result[DOMProperty.ID_ATTRIBUTE_NAME] = item.id;
+ result.type = item.type;
+ result.args = JSON.stringify(item.args);
+ return result;
+ }));
+ console.log('Total time:', ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms');
+ },
+
+ _recordWrite: function (id, fnName, totalTime, args) {
+ // TODO: totalTime isn't that useful since it doesn't count paints/reflows
+ var writes = ReactDefaultPerf._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1].writes;
+ writes[id] = writes[id] || [];
+ writes[id].push({
+ type: fnName,
+ time: totalTime,
+ args: args
+ });
+ },
+
+ measure: function (moduleName, fnName, func) {
+ return function () {
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ var totalTime;
+ var rv;
+ var start;
+
+ if (fnName === '_renderNewRootComponent' || fnName === 'flushBatchedUpdates') {
+ // A "measurement" is a set of metrics recorded for each flush. We want
+ // to group the metrics for a given flush together so we can look at the
+ // components that rendered and the DOM operations that actually
+ // happened to determine the amount of "wasted work" performed.
+ ReactDefaultPerf._allMeasurements.push({
+ exclusive: {},
+ inclusive: {},
+ render: {},
+ counts: {},
+ writes: {},
+ displayNames: {},
+ totalTime: 0,
+ created: {}
+ });
+ start = performanceNow();
+ rv = func.apply(this, args);
+ ReactDefaultPerf._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1].totalTime = performanceNow() - start;
+ return rv;
+ } else if (fnName === '_mountImageIntoNode' || moduleName === 'ReactBrowserEventEmitter' || moduleName === 'ReactDOMIDOperations' || moduleName === 'CSSPropertyOperations' || moduleName === 'DOMChildrenOperations' || moduleName === 'DOMPropertyOperations') {
+ start = performanceNow();
+ rv = func.apply(this, args);
+ totalTime = performanceNow() - start;
+
+ if (fnName === '_mountImageIntoNode') {
+ var mountID = ReactMount.getID(args[1]);
+ ReactDefaultPerf._recordWrite(mountID, fnName, totalTime, args[0]);
+ } else if (fnName === 'dangerouslyProcessChildrenUpdates') {
+ // special format
+ args[0].forEach(function (update) {
+ var writeArgs = {};
+ if (update.fromIndex !== null) {
+ writeArgs.fromIndex = update.fromIndex;
+ }
+ if (update.toIndex !== null) {
+ writeArgs.toIndex = update.toIndex;
+ }
+ if (update.textContent !== null) {
+ writeArgs.textContent = update.textContent;
+ }
+ if (update.markupIndex !== null) {
+ writeArgs.markup = args[1][update.markupIndex];
+ }
+ ReactDefaultPerf._recordWrite(update.parentID, update.type, totalTime, writeArgs);
+ });
+ } else {
+ // basic format
+ var id = args[0];
+ if (typeof id === 'object') {
+ id = ReactMount.getID(args[0]);
+ }
+ ReactDefaultPerf._recordWrite(id, fnName, totalTime, Array.prototype.slice.call(args, 1));
+ }
+ return rv;
+ } else if (moduleName === 'ReactCompositeComponent' && (fnName === 'mountComponent' || fnName === 'updateComponent' || // TODO: receiveComponent()?
+ fnName === '_renderValidatedComponent')) {
+
+ if (this._currentElement.type === ReactMount.TopLevelWrapper) {
+ return func.apply(this, args);
+ }
+
+ var rootNodeID = fnName === 'mountComponent' ? args[0] : this._rootNodeID;
+ var isRender = fnName === '_renderValidatedComponent';
+ var isMount = fnName === 'mountComponent';
+
+ var mountStack = ReactDefaultPerf._mountStack;
+ var entry = ReactDefaultPerf._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1];
+
+ if (isRender) {
+ addValue(entry.counts, rootNodeID, 1);
+ } else if (isMount) {
+ entry.created[rootNodeID] = true;
+ mountStack.push(0);
+ }
+
+ start = performanceNow();
+ rv = func.apply(this, args);
+ totalTime = performanceNow() - start;
+
+ if (isRender) {
+ addValue(entry.render, rootNodeID, totalTime);
+ } else if (isMount) {
+ var subMountTime = mountStack.pop();
+ mountStack[mountStack.length - 1] += totalTime;
+ addValue(entry.exclusive, rootNodeID, totalTime - subMountTime);
+ addValue(entry.inclusive, rootNodeID, totalTime);
+ } else {
+ addValue(entry.inclusive, rootNodeID, totalTime);
+ }
+
+ entry.displayNames[rootNodeID] = {
+ current: this.getName(),
+ owner: this._currentElement._owner ? this._currentElement._owner.getName() : '<root>'
+ };
+
+ return rv;
+ } else {
+ return func.apply(this, args);
+ }
+ };
+ }
+};
+
+module.exports = ReactDefaultPerf;
+},{"10":10,"170":170,"56":56,"72":72,"78":78}],56:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultPerfAnalysis
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+
+// Don't try to save users less than 1.2ms (a number I made up)
+var DONT_CARE_THRESHOLD = 1.2;
+var DOM_OPERATION_TYPES = {
+ '_mountImageIntoNode': 'set innerHTML',
+ INSERT_MARKUP: 'set innerHTML',
+ MOVE_EXISTING: 'move',
+ REMOVE_NODE: 'remove',
+ SET_MARKUP: 'set innerHTML',
+ TEXT_CONTENT: 'set textContent',
+ 'setValueForProperty': 'update attribute',
+ 'setValueForAttribute': 'update attribute',
+ 'deleteValueForProperty': 'remove attribute',
+ 'setValueForStyles': 'update styles',
+ 'replaceNodeWithMarkup': 'replace',
+ 'updateTextContent': 'set textContent'
+};
+
+function getTotalTime(measurements) {
+ // TODO: return number of DOM ops? could be misleading.
+ // TODO: measure dropped frames after reconcile?
+ // TODO: log total time of each reconcile and the top-level component
+ // class that triggered it.
+ var totalTime = 0;
+ for (var i = 0; i < measurements.length; i++) {
+ var measurement = measurements[i];
+ totalTime += measurement.totalTime;
+ }
+ return totalTime;
+}
+
+function getDOMSummary(measurements) {
+ var items = [];
+ measurements.forEach(function (measurement) {
+ Object.keys(measurement.writes).forEach(function (id) {
+ measurement.writes[id].forEach(function (write) {
+ items.push({
+ id: id,
+ type: DOM_OPERATION_TYPES[write.type] || write.type,
+ args: write.args
+ });
+ });
+ });
+ });
+ return items;
+}
+
+function getExclusiveSummary(measurements) {
+ var candidates = {};
+ var displayName;
+
+ for (var i = 0; i < measurements.length; i++) {
+ var measurement = measurements[i];
+ var allIDs = assign({}, measurement.exclusive, measurement.inclusive);
+
+ for (var id in allIDs) {
+ displayName = measurement.displayNames[id].current;
+
+ candidates[displayName] = candidates[displayName] || {
+ componentName: displayName,
+ inclusive: 0,
+ exclusive: 0,
+ render: 0,
+ count: 0
+ };
+ if (measurement.render[id]) {
+ candidates[displayName].render += measurement.render[id];
+ }
+ if (measurement.exclusive[id]) {
+ candidates[displayName].exclusive += measurement.exclusive[id];
+ }
+ if (measurement.inclusive[id]) {
+ candidates[displayName].inclusive += measurement.inclusive[id];
+ }
+ if (measurement.counts[id]) {
+ candidates[displayName].count += measurement.counts[id];
+ }
+ }
+ }
+
+ // Now make a sorted array with the results.
+ var arr = [];
+ for (displayName in candidates) {
+ if (candidates[displayName].exclusive >= DONT_CARE_THRESHOLD) {
+ arr.push(candidates[displayName]);
+ }
+ }
+
+ arr.sort(function (a, b) {
+ return b.exclusive - a.exclusive;
+ });
+
+ return arr;
+}
+
+function getInclusiveSummary(measurements, onlyClean) {
+ var candidates = {};
+ var inclusiveKey;
+
+ for (var i = 0; i < measurements.length; i++) {
+ var measurement = measurements[i];
+ var allIDs = assign({}, measurement.exclusive, measurement.inclusive);
+ var cleanComponents;
+
+ if (onlyClean) {
+ cleanComponents = getUnchangedComponents(measurement);
+ }
+
+ for (var id in allIDs) {
+ if (onlyClean && !cleanComponents[id]) {
+ continue;
+ }
+
+ var displayName = measurement.displayNames[id];
+
+ // Inclusive time is not useful for many components without knowing where
+ // they are instantiated. So we aggregate inclusive time with both the
+ // owner and current displayName as the key.
+ inclusiveKey = displayName.owner + ' > ' + displayName.current;
+
+ candidates[inclusiveKey] = candidates[inclusiveKey] || {
+ componentName: inclusiveKey,
+ time: 0,
+ count: 0
+ };
+
+ if (measurement.inclusive[id]) {
+ candidates[inclusiveKey].time += measurement.inclusive[id];
+ }
+ if (measurement.counts[id]) {
+ candidates[inclusiveKey].count += measurement.counts[id];
+ }
+ }
+ }
+
+ // Now make a sorted array with the results.
+ var arr = [];
+ for (inclusiveKey in candidates) {
+ if (candidates[inclusiveKey].time >= DONT_CARE_THRESHOLD) {
+ arr.push(candidates[inclusiveKey]);
+ }
+ }
+
+ arr.sort(function (a, b) {
+ return b.time - a.time;
+ });
+
+ return arr;
+}
+
+function getUnchangedComponents(measurement) {
+ // For a given reconcile, look at which components did not actually
+ // render anything to the DOM and return a mapping of their ID to
+ // the amount of time it took to render the entire subtree.
+ var cleanComponents = {};
+ var dirtyLeafIDs = Object.keys(measurement.writes);
+ var allIDs = assign({}, measurement.exclusive, measurement.inclusive);
+
+ for (var id in allIDs) {
+ var isDirty = false;
+ // For each component that rendered, see if a component that triggered
+ // a DOM op is in its subtree.
+ for (var i = 0; i < dirtyLeafIDs.length; i++) {
+ if (dirtyLeafIDs[i].indexOf(id) === 0) {
+ isDirty = true;
+ break;
+ }
+ }
+ // check if component newly created
+ if (measurement.created[id]) {
+ isDirty = true;
+ }
+ if (!isDirty && measurement.counts[id] > 0) {
+ cleanComponents[id] = true;
+ }
+ }
+ return cleanComponents;
+}
+
+var ReactDefaultPerfAnalysis = {
+ getExclusiveSummary: getExclusiveSummary,
+ getInclusiveSummary: getInclusiveSummary,
+ getDOMSummary: getDOMSummary,
+ getTotalTime: getTotalTime
+};
+
+module.exports = ReactDefaultPerfAnalysis;
+},{"24":24}],57:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactElement
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+
+var assign = _dereq_(24);
+var canDefineProperty = _dereq_(117);
+
+// The Symbol used to tag the ReactElement type. If there is no native Symbol
+// nor polyfill, then a plain number is used for performance.
+var REACT_ELEMENT_TYPE = typeof Symbol === 'function' && Symbol['for'] && Symbol['for']('react.element') || 0xeac7;
+
+var RESERVED_PROPS = {
+ key: true,
+ ref: true,
+ __self: true,
+ __source: true
+};
+
+/**
+ * Base constructor for all React elements. This is only used to make this
+ * work with a dynamic instanceof check. Nothing should live on this prototype.
+ *
+ * @param {*} type
+ * @param {*} key
+ * @param {string|object} ref
+ * @param {*} self A *temporary* helper to detect places where `this` is
+ * different from the `owner` when React.createElement is called, so that we
+ * can warn. We want to get rid of owner and replace string `ref`s with arrow
+ * functions, and as long as `this` and owner are the same, there will be no
+ * change in behavior.
+ * @param {*} source An annotation object (added by a transpiler or otherwise)
+ * indicating filename, line number, and/or other information.
+ * @param {*} owner
+ * @param {*} props
+ * @internal
+ */
+var ReactElement = function (type, key, ref, self, source, owner, props) {
+ var element = {
+ // This tag allow us to uniquely identify this as a React Element
+ $$typeof: REACT_ELEMENT_TYPE,
+
+ // Built-in properties that belong on the element
+ type: type,
+ key: key,
+ ref: ref,
+ props: props,
+
+ // Record the component responsible for creating this element.
+ _owner: owner
+ };
+
+ if ("development" !== 'production') {
+ // The validation flag is currently mutative. We put it on
+ // an external backing store so that we can freeze the whole object.
+ // This can be replaced with a WeakMap once they are implemented in
+ // commonly used development environments.
+ element._store = {};
+
+ // To make comparing ReactElements easier for testing purposes, we make
+ // the validation flag non-enumerable (where possible, which should
+ // include every environment we run tests in), so the test framework
+ // ignores it.
+ if (canDefineProperty) {
+ Object.defineProperty(element._store, 'validated', {
+ configurable: false,
+ enumerable: false,
+ writable: true,
+ value: false
+ });
+ // self and source are DEV only properties.
+ Object.defineProperty(element, '_self', {
+ configurable: false,
+ enumerable: false,
+ writable: false,
+ value: self
+ });
+ // Two elements created in two different places should be considered
+ // equal for testing purposes and therefore we hide it from enumeration.
+ Object.defineProperty(element, '_source', {
+ configurable: false,
+ enumerable: false,
+ writable: false,
+ value: source
+ });
+ } else {
+ element._store.validated = false;
+ element._self = self;
+ element._source = source;
+ }
+ Object.freeze(element.props);
+ Object.freeze(element);
+ }
+
+ return element;
+};
+
+ReactElement.createElement = function (type, config, children) {
+ var propName;
+
+ // Reserved names are extracted
+ var props = {};
+
+ var key = null;
+ var ref = null;
+ var self = null;
+ var source = null;
+
+ if (config != null) {
+ ref = config.ref === undefined ? null : config.ref;
+ key = config.key === undefined ? null : '' + config.key;
+ self = config.__self === undefined ? null : config.__self;
+ source = config.__source === undefined ? null : config.__source;
+ // Remaining properties are added to a new props object
+ for (propName in config) {
+ if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
+ props[propName] = config[propName];
+ }
+ }
+ }
+
+ // Children can be more than one argument, and those are transferred onto
+ // the newly allocated props object.
+ var childrenLength = arguments.length - 2;
+ if (childrenLength === 1) {
+ props.children = children;
+ } else if (childrenLength > 1) {
+ var childArray = Array(childrenLength);
+ for (var i = 0; i < childrenLength; i++) {
+ childArray[i] = arguments[i + 2];
+ }
+ props.children = childArray;
+ }
+
+ // Resolve default props
+ if (type && type.defaultProps) {
+ var defaultProps = type.defaultProps;
+ for (propName in defaultProps) {
+ if (typeof props[propName] === 'undefined') {
+ props[propName] = defaultProps[propName];
+ }
+ }
+ }
+
+ return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
+};
+
+ReactElement.createFactory = function (type) {
+ var factory = ReactElement.createElement.bind(null, type);
+ // Expose the type on the factory and the prototype so that it can be
+ // easily accessed on elements. E.g. `<Foo />.type === Foo`.
+ // This should not be named `constructor` since this may not be the function
+ // that created the element, and it may not even be a constructor.
+ // Legacy hook TODO: Warn if this is accessed
+ factory.type = type;
+ return factory;
+};
+
+ReactElement.cloneAndReplaceKey = function (oldElement, newKey) {
+ var newElement = ReactElement(oldElement.type, newKey, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, oldElement.props);
+
+ return newElement;
+};
+
+ReactElement.cloneAndReplaceProps = function (oldElement, newProps) {
+ var newElement = ReactElement(oldElement.type, oldElement.key, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, newProps);
+
+ if ("development" !== 'production') {
+ // If the key on the original is valid, then the clone is valid
+ newElement._store.validated = oldElement._store.validated;
+ }
+
+ return newElement;
+};
+
+ReactElement.cloneElement = function (element, config, children) {
+ var propName;
+
+ // Original props are copied
+ var props = assign({}, element.props);
+
+ // Reserved names are extracted
+ var key = element.key;
+ var ref = element.ref;
+ // Self is preserved since the owner is preserved.
+ var self = element._self;
+ // Source is preserved since cloneElement is unlikely to be targeted by a
+ // transpiler, and the original source is probably a better indicator of the
+ // true owner.
+ var source = element._source;
+
+ // Owner will be preserved, unless ref is overridden
+ var owner = element._owner;
+
+ if (config != null) {
+ if (config.ref !== undefined) {
+ // Silently steal the ref from the parent.
+ ref = config.ref;
+ owner = ReactCurrentOwner.current;
+ }
+ if (config.key !== undefined) {
+ key = '' + config.key;
+ }
+ // Remaining properties override existing props
+ for (propName in config) {
+ if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
+ props[propName] = config[propName];
+ }
+ }
+ }
+
+ // Children can be more than one argument, and those are transferred onto
+ // the newly allocated props object.
+ var childrenLength = arguments.length - 2;
+ if (childrenLength === 1) {
+ props.children = children;
+ } else if (childrenLength > 1) {
+ var childArray = Array(childrenLength);
+ for (var i = 0; i < childrenLength; i++) {
+ childArray[i] = arguments[i + 2];
+ }
+ props.children = childArray;
+ }
+
+ return ReactElement(element.type, key, ref, self, source, owner, props);
+};
+
+/**
+ * @param {?object} object
+ * @return {boolean} True if `object` is a valid component.
+ * @final
+ */
+ReactElement.isValidElement = function (object) {
+ return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
+};
+
+module.exports = ReactElement;
+},{"117":117,"24":24,"39":39}],58:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactElementValidator
+ */
+
+/**
+ * ReactElementValidator provides a wrapper around a element factory
+ * which validates the props passed to the element. This is intended to be
+ * used only in DEV and could be replaced by a static type checker for languages
+ * that support it.
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactPropTypeLocations = _dereq_(81);
+var ReactPropTypeLocationNames = _dereq_(80);
+var ReactCurrentOwner = _dereq_(39);
+
+var canDefineProperty = _dereq_(117);
+var getIteratorFn = _dereq_(129);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+function getDeclarationErrorAddendum() {
+ if (ReactCurrentOwner.current) {
+ var name = ReactCurrentOwner.current.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+/**
+ * Warn if there's no key explicitly set on dynamic arrays of children or
+ * object keys are not valid. This allows us to keep track of children between
+ * updates.
+ */
+var ownerHasKeyUseWarning = {};
+
+var loggedTypeFailures = {};
+
+/**
+ * Warn if the element doesn't have an explicit key assigned to it.
+ * This element is in an array. The array could grow and shrink or be
+ * reordered. All children that haven't already been validated are required to
+ * have a "key" property assigned to it.
+ *
+ * @internal
+ * @param {ReactElement} element Element that requires a key.
+ * @param {*} parentType element's parent's type.
+ */
+function validateExplicitKey(element, parentType) {
+ if (!element._store || element._store.validated || element.key != null) {
+ return;
+ }
+ element._store.validated = true;
+
+ var addenda = getAddendaForKeyUse('uniqueKey', element, parentType);
+ if (addenda === null) {
+ // we already showed the warning
+ return;
+ }
+ "development" !== 'production' ? warning(false, 'Each child in an array or iterator should have a unique "key" prop.' + '%s%s%s', addenda.parentOrOwner || '', addenda.childOwner || '', addenda.url || '') : undefined;
+}
+
+/**
+ * Shared warning and monitoring code for the key warnings.
+ *
+ * @internal
+ * @param {string} messageType A key used for de-duping warnings.
+ * @param {ReactElement} element Component that requires a key.
+ * @param {*} parentType element's parent's type.
+ * @returns {?object} A set of addenda to use in the warning message, or null
+ * if the warning has already been shown before (and shouldn't be shown again).
+ */
+function getAddendaForKeyUse(messageType, element, parentType) {
+ var addendum = getDeclarationErrorAddendum();
+ if (!addendum) {
+ var parentName = typeof parentType === 'string' ? parentType : parentType.displayName || parentType.name;
+ if (parentName) {
+ addendum = ' Check the top-level render call using <' + parentName + '>.';
+ }
+ }
+
+ var memoizer = ownerHasKeyUseWarning[messageType] || (ownerHasKeyUseWarning[messageType] = {});
+ if (memoizer[addendum]) {
+ return null;
+ }
+ memoizer[addendum] = true;
+
+ var addenda = {
+ parentOrOwner: addendum,
+ url: ' See https://fb.me/react-warning-keys for more information.',
+ childOwner: null
+ };
+
+ // Usually the current owner is the offender, but if it accepts children as a
+ // property, it may be the creator of the child that's responsible for
+ // assigning it a key.
+ if (element && element._owner && element._owner !== ReactCurrentOwner.current) {
+ // Give the component that originally created this child.
+ addenda.childOwner = ' It was passed a child from ' + element._owner.getName() + '.';
+ }
+
+ return addenda;
+}
+
+/**
+ * Ensure that every element either is passed in a static location, in an
+ * array with an explicit keys property defined, or in an object literal
+ * with valid key property.
+ *
+ * @internal
+ * @param {ReactNode} node Statically passed child of any type.
+ * @param {*} parentType node's parent's type.
+ */
+function validateChildKeys(node, parentType) {
+ if (typeof node !== 'object') {
+ return;
+ }
+ if (Array.isArray(node)) {
+ for (var i = 0; i < node.length; i++) {
+ var child = node[i];
+ if (ReactElement.isValidElement(child)) {
+ validateExplicitKey(child, parentType);
+ }
+ }
+ } else if (ReactElement.isValidElement(node)) {
+ // This element was passed in a valid location.
+ if (node._store) {
+ node._store.validated = true;
+ }
+ } else if (node) {
+ var iteratorFn = getIteratorFn(node);
+ // Entry iterators provide implicit keys.
+ if (iteratorFn) {
+ if (iteratorFn !== node.entries) {
+ var iterator = iteratorFn.call(node);
+ var step;
+ while (!(step = iterator.next()).done) {
+ if (ReactElement.isValidElement(step.value)) {
+ validateExplicitKey(step.value, parentType);
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Assert that the props are valid
+ *
+ * @param {string} componentName Name of the component for error messages.
+ * @param {object} propTypes Map of prop name to a ReactPropType
+ * @param {object} props
+ * @param {string} location e.g. "prop", "context", "child context"
+ * @private
+ */
+function checkPropTypes(componentName, propTypes, props, location) {
+ for (var propName in propTypes) {
+ if (propTypes.hasOwnProperty(propName)) {
+ var error;
+ // Prop type validation may throw. In case they do, we don't want to
+ // fail the render phase where it didn't fail before. So we log it.
+ // After these have been cleaned up, we'll let them throw.
+ try {
+ // This is intentionally an invariant that gets caught. It's the same
+ // behavior as without this statement except with a better message.
+ !(typeof propTypes[propName] === 'function') ? "development" !== 'production' ? invariant(false, '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', componentName || 'React class', ReactPropTypeLocationNames[location], propName) : invariant(false) : undefined;
+ error = propTypes[propName](props, propName, componentName, location);
+ } catch (ex) {
+ error = ex;
+ }
+ "development" !== 'production' ? warning(!error || error instanceof Error, '%s: type specification of %s `%s` is invalid; the type checker ' + 'function must return `null` or an `Error` but returned a %s. ' + 'You may have forgotten to pass an argument to the type checker ' + 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + 'shape all require an argument).', componentName || 'React class', ReactPropTypeLocationNames[location], propName, typeof error) : undefined;
+ if (error instanceof Error && !(error.message in loggedTypeFailures)) {
+ // Only monitor this failure once because there tends to be a lot of the
+ // same error.
+ loggedTypeFailures[error.message] = true;
+
+ var addendum = getDeclarationErrorAddendum();
+ "development" !== 'production' ? warning(false, 'Failed propType: %s%s', error.message, addendum) : undefined;
+ }
+ }
+ }
+}
+
+/**
+ * Given an element, validate that its props follow the propTypes definition,
+ * provided by the type.
+ *
+ * @param {ReactElement} element
+ */
+function validatePropTypes(element) {
+ var componentClass = element.type;
+ if (typeof componentClass !== 'function') {
+ return;
+ }
+ var name = componentClass.displayName || componentClass.name;
+ if (componentClass.propTypes) {
+ checkPropTypes(name, componentClass.propTypes, element.props, ReactPropTypeLocations.prop);
+ }
+ if (typeof componentClass.getDefaultProps === 'function') {
+ "development" !== 'production' ? warning(componentClass.getDefaultProps.isReactClassApproved, 'getDefaultProps is only used on classic React.createClass ' + 'definitions. Use a static property named `defaultProps` instead.') : undefined;
+ }
+}
+
+var ReactElementValidator = {
+
+ createElement: function (type, props, children) {
+ var validType = typeof type === 'string' || typeof type === 'function';
+ // We warn in this case but don't throw. We expect the element creation to
+ // succeed and there will likely be errors in render.
+ "development" !== 'production' ? warning(validType, 'React.createElement: type should not be null, undefined, boolean, or ' + 'number. It should be a string (for DOM elements) or a ReactClass ' + '(for composite components).%s', getDeclarationErrorAddendum()) : undefined;
+
+ var element = ReactElement.createElement.apply(this, arguments);
+
+ // The result can be nullish if a mock or a custom function is used.
+ // TODO: Drop this when these are no longer allowed as the type argument.
+ if (element == null) {
+ return element;
+ }
+
+ // Skip key warning if the type isn't valid since our key validation logic
+ // doesn't expect a non-string/function type and can throw confusing errors.
+ // We don't want exception behavior to differ between dev and prod.
+ // (Rendering will throw with a helpful message and as soon as the type is
+ // fixed, the key warnings will appear.)
+ if (validType) {
+ for (var i = 2; i < arguments.length; i++) {
+ validateChildKeys(arguments[i], type);
+ }
+ }
+
+ validatePropTypes(element);
+
+ return element;
+ },
+
+ createFactory: function (type) {
+ var validatedFactory = ReactElementValidator.createElement.bind(null, type);
+ // Legacy hook TODO: Warn if this is accessed
+ validatedFactory.type = type;
+
+ if ("development" !== 'production') {
+ if (canDefineProperty) {
+ Object.defineProperty(validatedFactory, 'type', {
+ enumerable: false,
+ get: function () {
+ "development" !== 'production' ? warning(false, 'Factory.type is deprecated. Access the class directly ' + 'before passing it to createFactory.') : undefined;
+ Object.defineProperty(this, 'type', {
+ value: type
+ });
+ return type;
+ }
+ });
+ }
+ }
+
+ return validatedFactory;
+ },
+
+ cloneElement: function (element, props, children) {
+ var newElement = ReactElement.cloneElement.apply(this, arguments);
+ for (var i = 2; i < arguments.length; i++) {
+ validateChildKeys(arguments[i], newElement.type);
+ }
+ validatePropTypes(newElement);
+ return newElement;
+ }
+
+};
+
+module.exports = ReactElementValidator;
+},{"117":117,"129":129,"161":161,"173":173,"39":39,"57":57,"80":80,"81":81}],59:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEmptyComponent
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactEmptyComponentRegistry = _dereq_(60);
+var ReactReconciler = _dereq_(84);
+
+var assign = _dereq_(24);
+
+var placeholderElement;
+
+var ReactEmptyComponentInjection = {
+ injectEmptyComponent: function (component) {
+ placeholderElement = ReactElement.createElement(component);
+ }
+};
+
+var ReactEmptyComponent = function (instantiate) {
+ this._currentElement = null;
+ this._rootNodeID = null;
+ this._renderedComponent = instantiate(placeholderElement);
+};
+assign(ReactEmptyComponent.prototype, {
+ construct: function (element) {},
+ mountComponent: function (rootID, transaction, context) {
+ ReactEmptyComponentRegistry.registerNullComponentID(rootID);
+ this._rootNodeID = rootID;
+ return ReactReconciler.mountComponent(this._renderedComponent, rootID, transaction, context);
+ },
+ receiveComponent: function () {},
+ unmountComponent: function (rootID, transaction, context) {
+ ReactReconciler.unmountComponent(this._renderedComponent);
+ ReactEmptyComponentRegistry.deregisterNullComponentID(this._rootNodeID);
+ this._rootNodeID = null;
+ this._renderedComponent = null;
+ }
+});
+
+ReactEmptyComponent.injection = ReactEmptyComponentInjection;
+
+module.exports = ReactEmptyComponent;
+},{"24":24,"57":57,"60":60,"84":84}],60:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEmptyComponentRegistry
+ */
+
+'use strict';
+
+// This registry keeps track of the React IDs of the components that rendered to
+// `null` (in reality a placeholder such as `noscript`)
+var nullComponentIDsRegistry = {};
+
+/**
+ * @param {string} id Component's `_rootNodeID`.
+ * @return {boolean} True if the component is rendered to null.
+ */
+function isNullComponentID(id) {
+ return !!nullComponentIDsRegistry[id];
+}
+
+/**
+ * Mark the component as having rendered to null.
+ * @param {string} id Component's `_rootNodeID`.
+ */
+function registerNullComponentID(id) {
+ nullComponentIDsRegistry[id] = true;
+}
+
+/**
+ * Unmark the component as having rendered to null: it renders to something now.
+ * @param {string} id Component's `_rootNodeID`.
+ */
+function deregisterNullComponentID(id) {
+ delete nullComponentIDsRegistry[id];
+}
+
+var ReactEmptyComponentRegistry = {
+ isNullComponentID: isNullComponentID,
+ registerNullComponentID: registerNullComponentID,
+ deregisterNullComponentID: deregisterNullComponentID
+};
+
+module.exports = ReactEmptyComponentRegistry;
+},{}],61:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactErrorUtils
+ * @typechecks
+ */
+
+'use strict';
+
+var caughtError = null;
+
+/**
+ * Call a function while guarding against errors that happens within it.
+ *
+ * @param {?String} name of the guard to use for logging or debugging
+ * @param {Function} func The function to invoke
+ * @param {*} a First argument
+ * @param {*} b Second argument
+ */
+function invokeGuardedCallback(name, func, a, b) {
+ try {
+ return func(a, b);
+ } catch (x) {
+ if (caughtError === null) {
+ caughtError = x;
+ }
+ return undefined;
+ }
+}
+
+var ReactErrorUtils = {
+ invokeGuardedCallback: invokeGuardedCallback,
+
+ /**
+ * Invoked by ReactTestUtils.Simulate so that any errors thrown by the event
+ * handler are sure to be rethrown by rethrowCaughtError.
+ */
+ invokeGuardedCallbackWithCatch: invokeGuardedCallback,
+
+ /**
+ * During execution of guarded functions we will capture the first error which
+ * we will rethrow to be handled by the top level error handler.
+ */
+ rethrowCaughtError: function () {
+ if (caughtError) {
+ var error = caughtError;
+ caughtError = null;
+ throw error;
+ }
+ }
+};
+
+if ("development" !== 'production') {
+ /**
+ * To help development we can get better devtools integration by simulating a
+ * real browser event.
+ */
+ if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function') {
+ var fakeNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'react');
+ ReactErrorUtils.invokeGuardedCallback = function (name, func, a, b) {
+ var boundFunc = func.bind(null, a, b);
+ var evtType = 'react-' + name;
+ fakeNode.addEventListener(evtType, boundFunc, false);
+ var evt = document.createEvent('Event');
+ evt.initEvent(evtType, false, false);
+ fakeNode.dispatchEvent(evt);
+ fakeNode.removeEventListener(evtType, boundFunc, false);
+ };
+ }
+}
+
+module.exports = ReactErrorUtils;
+},{}],62:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEventEmitterMixin
+ */
+
+'use strict';
+
+var EventPluginHub = _dereq_(16);
+
+function runEventQueueInBatch(events) {
+ EventPluginHub.enqueueEvents(events);
+ EventPluginHub.processEventQueue(false);
+}
+
+var ReactEventEmitterMixin = {
+
+ /**
+ * Streams a fired top-level event to `EventPluginHub` where plugins have the
+ * opportunity to create `ReactEvent`s to be dispatched.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {object} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native environment event.
+ */
+ handleTopLevel: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var events = EventPluginHub.extractEvents(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget);
+ runEventQueueInBatch(events);
+ }
+};
+
+module.exports = ReactEventEmitterMixin;
+},{"16":16}],63:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEventListener
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventListener = _dereq_(146);
+var ExecutionEnvironment = _dereq_(147);
+var PooledClass = _dereq_(25);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var getEventTarget = _dereq_(128);
+var getUnboundedScrollPosition = _dereq_(158);
+
+var DOCUMENT_FRAGMENT_NODE_TYPE = 11;
+
+/**
+ * Finds the parent React component of `node`.
+ *
+ * @param {*} node
+ * @return {?DOMEventTarget} Parent container, or `null` if the specified node
+ * is not nested.
+ */
+function findParent(node) {
+ // TODO: It may be a good idea to cache this to prevent unnecessary DOM
+ // traversal, but caching is difficult to do correctly without using a
+ // mutation observer to listen for all DOM changes.
+ var nodeID = ReactMount.getID(node);
+ var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID);
+ var container = ReactMount.findReactContainerForID(rootID);
+ var parent = ReactMount.getFirstReactDOM(container);
+ return parent;
+}
+
+// Used to store ancestor hierarchy in top level callback
+function TopLevelCallbackBookKeeping(topLevelType, nativeEvent) {
+ this.topLevelType = topLevelType;
+ this.nativeEvent = nativeEvent;
+ this.ancestors = [];
+}
+assign(TopLevelCallbackBookKeeping.prototype, {
+ destructor: function () {
+ this.topLevelType = null;
+ this.nativeEvent = null;
+ this.ancestors.length = 0;
+ }
+});
+PooledClass.addPoolingTo(TopLevelCallbackBookKeeping, PooledClass.twoArgumentPooler);
+
+function handleTopLevelImpl(bookKeeping) {
+ // TODO: Re-enable event.path handling
+ //
+ // if (bookKeeping.nativeEvent.path && bookKeeping.nativeEvent.path.length > 1) {
+ // // New browsers have a path attribute on native events
+ // handleTopLevelWithPath(bookKeeping);
+ // } else {
+ // // Legacy browsers don't have a path attribute on native events
+ // handleTopLevelWithoutPath(bookKeeping);
+ // }
+
+ void handleTopLevelWithPath; // temporarily unused
+ handleTopLevelWithoutPath(bookKeeping);
+}
+
+// Legacy browsers don't have a path attribute on native events
+function handleTopLevelWithoutPath(bookKeeping) {
+ var topLevelTarget = ReactMount.getFirstReactDOM(getEventTarget(bookKeeping.nativeEvent)) || window;
+
+ // Loop through the hierarchy, in case there's any nested components.
+ // It's important that we build the array of ancestors before calling any
+ // event handlers, because event handlers can modify the DOM, leading to
+ // inconsistencies with ReactMount's node cache. See #1105.
+ var ancestor = topLevelTarget;
+ while (ancestor) {
+ bookKeeping.ancestors.push(ancestor);
+ ancestor = findParent(ancestor);
+ }
+
+ for (var i = 0; i < bookKeeping.ancestors.length; i++) {
+ topLevelTarget = bookKeeping.ancestors[i];
+ var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';
+ ReactEventListener._handleTopLevel(bookKeeping.topLevelType, topLevelTarget, topLevelTargetID, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
+ }
+}
+
+// New browsers have a path attribute on native events
+function handleTopLevelWithPath(bookKeeping) {
+ var path = bookKeeping.nativeEvent.path;
+ var currentNativeTarget = path[0];
+ var eventsFired = 0;
+ for (var i = 0; i < path.length; i++) {
+ var currentPathElement = path[i];
+ if (currentPathElement.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE) {
+ currentNativeTarget = path[i + 1];
+ }
+ // TODO: slow
+ var reactParent = ReactMount.getFirstReactDOM(currentPathElement);
+ if (reactParent === currentPathElement) {
+ var currentPathElementID = ReactMount.getID(currentPathElement);
+ var newRootID = ReactInstanceHandles.getReactRootIDFromNodeID(currentPathElementID);
+ bookKeeping.ancestors.push(currentPathElement);
+
+ var topLevelTargetID = ReactMount.getID(currentPathElement) || '';
+ eventsFired++;
+ ReactEventListener._handleTopLevel(bookKeeping.topLevelType, currentPathElement, topLevelTargetID, bookKeeping.nativeEvent, currentNativeTarget);
+
+ // Jump to the root of this React render tree
+ while (currentPathElementID !== newRootID) {
+ i++;
+ currentPathElement = path[i];
+ currentPathElementID = ReactMount.getID(currentPathElement);
+ }
+ }
+ }
+ if (eventsFired === 0) {
+ ReactEventListener._handleTopLevel(bookKeeping.topLevelType, window, '', bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
+ }
+}
+
+function scrollValueMonitor(cb) {
+ var scrollPosition = getUnboundedScrollPosition(window);
+ cb(scrollPosition);
+}
+
+var ReactEventListener = {
+ _enabled: true,
+ _handleTopLevel: null,
+
+ WINDOW_HANDLE: ExecutionEnvironment.canUseDOM ? window : null,
+
+ setHandleTopLevel: function (handleTopLevel) {
+ ReactEventListener._handleTopLevel = handleTopLevel;
+ },
+
+ setEnabled: function (enabled) {
+ ReactEventListener._enabled = !!enabled;
+ },
+
+ isEnabled: function () {
+ return ReactEventListener._enabled;
+ },
+
+ /**
+ * Traps top-level events by using event bubbling.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {string} handlerBaseName Event name (e.g. "click").
+ * @param {object} handle Element on which to attach listener.
+ * @return {?object} An object with a remove function which will forcefully
+ * remove the listener.
+ * @internal
+ */
+ trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
+ var element = handle;
+ if (!element) {
+ return null;
+ }
+ return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
+ },
+
+ /**
+ * Traps a top-level event by using event capturing.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {string} handlerBaseName Event name (e.g. "click").
+ * @param {object} handle Element on which to attach listener.
+ * @return {?object} An object with a remove function which will forcefully
+ * remove the listener.
+ * @internal
+ */
+ trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
+ var element = handle;
+ if (!element) {
+ return null;
+ }
+ return EventListener.capture(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
+ },
+
+ monitorScrollValue: function (refresh) {
+ var callback = scrollValueMonitor.bind(null, refresh);
+ EventListener.listen(window, 'scroll', callback);
+ },
+
+ dispatchEvent: function (topLevelType, nativeEvent) {
+ if (!ReactEventListener._enabled) {
+ return;
+ }
+
+ var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
+ try {
+ // Event queue being processed in the same cycle allows
+ // `preventDefault`.
+ ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
+ } finally {
+ TopLevelCallbackBookKeeping.release(bookKeeping);
+ }
+ }
+};
+
+module.exports = ReactEventListener;
+},{"128":128,"146":146,"147":147,"158":158,"24":24,"25":25,"67":67,"72":72,"96":96}],64:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactFragment
+ */
+
+'use strict';
+
+var ReactChildren = _dereq_(32);
+var ReactElement = _dereq_(57);
+
+var emptyFunction = _dereq_(153);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * We used to allow keyed objects to serve as a collection of ReactElements,
+ * or nested sets. This allowed us a way to explicitly key a set a fragment of
+ * components. This is now being replaced with an opaque data structure.
+ * The upgrade path is to call React.addons.createFragment({ key: value }) to
+ * create a keyed fragment. The resulting data structure is an array.
+ */
+
+var numericPropertyRegex = /^\d+$/;
+
+var warnedAboutNumeric = false;
+
+var ReactFragment = {
+ // Wrap a keyed object in an opaque proxy that warns you if you access any
+ // of its properties.
+ create: function (object) {
+ if (typeof object !== 'object' || !object || Array.isArray(object)) {
+ "development" !== 'production' ? warning(false, 'React.addons.createFragment only accepts a single object. Got: %s', object) : undefined;
+ return object;
+ }
+ if (ReactElement.isValidElement(object)) {
+ "development" !== 'production' ? warning(false, 'React.addons.createFragment does not accept a ReactElement ' + 'without a wrapper object.') : undefined;
+ return object;
+ }
+
+ !(object.nodeType !== 1) ? "development" !== 'production' ? invariant(false, 'React.addons.createFragment(...): Encountered an invalid child; DOM ' + 'elements are not valid children of React components.') : invariant(false) : undefined;
+
+ var result = [];
+
+ for (var key in object) {
+ if ("development" !== 'production') {
+ if (!warnedAboutNumeric && numericPropertyRegex.test(key)) {
+ "development" !== 'production' ? warning(false, 'React.addons.createFragment(...): Child objects should have ' + 'non-numeric keys so ordering is preserved.') : undefined;
+ warnedAboutNumeric = true;
+ }
+ }
+ ReactChildren.mapIntoWithKeyPrefixInternal(object[key], result, key, emptyFunction.thatReturnsArgument);
+ }
+
+ return result;
+ }
+};
+
+module.exports = ReactFragment;
+},{"153":153,"161":161,"173":173,"32":32,"57":57}],65:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInjection
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var EventPluginHub = _dereq_(16);
+var ReactComponentEnvironment = _dereq_(36);
+var ReactClass = _dereq_(33);
+var ReactEmptyComponent = _dereq_(59);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactNativeComponent = _dereq_(75);
+var ReactPerf = _dereq_(78);
+var ReactRootIndex = _dereq_(86);
+var ReactUpdates = _dereq_(96);
+
+var ReactInjection = {
+ Component: ReactComponentEnvironment.injection,
+ Class: ReactClass.injection,
+ DOMProperty: DOMProperty.injection,
+ EmptyComponent: ReactEmptyComponent.injection,
+ EventPluginHub: EventPluginHub.injection,
+ EventEmitter: ReactBrowserEventEmitter.injection,
+ NativeComponent: ReactNativeComponent.injection,
+ Perf: ReactPerf.injection,
+ RootIndex: ReactRootIndex.injection,
+ Updates: ReactUpdates.injection
+};
+
+module.exports = ReactInjection;
+},{"10":10,"16":16,"28":28,"33":33,"36":36,"59":59,"75":75,"78":78,"86":86,"96":96}],66:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInputSelection
+ */
+
+'use strict';
+
+var ReactDOMSelection = _dereq_(49);
+
+var containsNode = _dereq_(150);
+var focusNode = _dereq_(155);
+var getActiveElement = _dereq_(156);
+
+function isInDocument(node) {
+ return containsNode(document.documentElement, node);
+}
+
+/**
+ * @ReactInputSelection: React input selection module. Based on Selection.js,
+ * but modified to be suitable for react and has a couple of bug fixes (doesn't
+ * assume buttons have range selections allowed).
+ * Input selection module for React.
+ */
+var ReactInputSelection = {
+
+ hasSelectionCapabilities: function (elem) {
+ var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
+ return nodeName && (nodeName === 'input' && elem.type === 'text' || nodeName === 'textarea' || elem.contentEditable === 'true');
+ },
+
+ getSelectionInformation: function () {
+ var focusedElem = getActiveElement();
+ return {
+ focusedElem: focusedElem,
+ selectionRange: ReactInputSelection.hasSelectionCapabilities(focusedElem) ? ReactInputSelection.getSelection(focusedElem) : null
+ };
+ },
+
+ /**
+ * @restoreSelection: If any selection information was potentially lost,
+ * restore it. This is useful when performing operations that could remove dom
+ * nodes and place them back in, resulting in focus being lost.
+ */
+ restoreSelection: function (priorSelectionInformation) {
+ var curFocusedElem = getActiveElement();
+ var priorFocusedElem = priorSelectionInformation.focusedElem;
+ var priorSelectionRange = priorSelectionInformation.selectionRange;
+ if (curFocusedElem !== priorFocusedElem && isInDocument(priorFocusedElem)) {
+ if (ReactInputSelection.hasSelectionCapabilities(priorFocusedElem)) {
+ ReactInputSelection.setSelection(priorFocusedElem, priorSelectionRange);
+ }
+ focusNode(priorFocusedElem);
+ }
+ },
+
+ /**
+ * @getSelection: Gets the selection bounds of a focused textarea, input or
+ * contentEditable node.
+ * -@input: Look up selection bounds of this input
+ * -@return {start: selectionStart, end: selectionEnd}
+ */
+ getSelection: function (input) {
+ var selection;
+
+ if ('selectionStart' in input) {
+ // Modern browser with input or textarea.
+ selection = {
+ start: input.selectionStart,
+ end: input.selectionEnd
+ };
+ } else if (document.selection && (input.nodeName && input.nodeName.toLowerCase() === 'input')) {
+ // IE8 input.
+ var range = document.selection.createRange();
+ // There can only be one selection per document in IE, so it must
+ // be in our element.
+ if (range.parentElement() === input) {
+ selection = {
+ start: -range.moveStart('character', -input.value.length),
+ end: -range.moveEnd('character', -input.value.length)
+ };
+ }
+ } else {
+ // Content editable or old IE textarea.
+ selection = ReactDOMSelection.getOffsets(input);
+ }
+
+ return selection || { start: 0, end: 0 };
+ },
+
+ /**
+ * @setSelection: Sets the selection bounds of a textarea or input and focuses
+ * the input.
+ * -@input Set selection bounds of this input or textarea
+ * -@offsets Object of same form that is returned from get*
+ */
+ setSelection: function (input, offsets) {
+ var start = offsets.start;
+ var end = offsets.end;
+ if (typeof end === 'undefined') {
+ end = start;
+ }
+
+ if ('selectionStart' in input) {
+ input.selectionStart = start;
+ input.selectionEnd = Math.min(end, input.value.length);
+ } else if (document.selection && (input.nodeName && input.nodeName.toLowerCase() === 'input')) {
+ var range = input.createTextRange();
+ range.collapse(true);
+ range.moveStart('character', start);
+ range.moveEnd('character', end - start);
+ range.select();
+ } else {
+ ReactDOMSelection.setOffsets(input, offsets);
+ }
+ }
+};
+
+module.exports = ReactInputSelection;
+},{"150":150,"155":155,"156":156,"49":49}],67:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInstanceHandles
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactRootIndex = _dereq_(86);
+
+var invariant = _dereq_(161);
+
+var SEPARATOR = '.';
+var SEPARATOR_LENGTH = SEPARATOR.length;
+
+/**
+ * Maximum depth of traversals before we consider the possibility of a bad ID.
+ */
+var MAX_TREE_DEPTH = 10000;
+
+/**
+ * Creates a DOM ID prefix to use when mounting React components.
+ *
+ * @param {number} index A unique integer
+ * @return {string} React root ID.
+ * @internal
+ */
+function getReactRootIDString(index) {
+ return SEPARATOR + index.toString(36);
+}
+
+/**
+ * Checks if a character in the supplied ID is a separator or the end.
+ *
+ * @param {string} id A React DOM ID.
+ * @param {number} index Index of the character to check.
+ * @return {boolean} True if the character is a separator or end of the ID.
+ * @private
+ */
+function isBoundary(id, index) {
+ return id.charAt(index) === SEPARATOR || index === id.length;
+}
+
+/**
+ * Checks if the supplied string is a valid React DOM ID.
+ *
+ * @param {string} id A React DOM ID, maybe.
+ * @return {boolean} True if the string is a valid React DOM ID.
+ * @private
+ */
+function isValidID(id) {
+ return id === '' || id.charAt(0) === SEPARATOR && id.charAt(id.length - 1) !== SEPARATOR;
+}
+
+/**
+ * Checks if the first ID is an ancestor of or equal to the second ID.
+ *
+ * @param {string} ancestorID
+ * @param {string} descendantID
+ * @return {boolean} True if `ancestorID` is an ancestor of `descendantID`.
+ * @internal
+ */
+function isAncestorIDOf(ancestorID, descendantID) {
+ return descendantID.indexOf(ancestorID) === 0 && isBoundary(descendantID, ancestorID.length);
+}
+
+/**
+ * Gets the parent ID of the supplied React DOM ID, `id`.
+ *
+ * @param {string} id ID of a component.
+ * @return {string} ID of the parent, or an empty string.
+ * @private
+ */
+function getParentID(id) {
+ return id ? id.substr(0, id.lastIndexOf(SEPARATOR)) : '';
+}
+
+/**
+ * Gets the next DOM ID on the tree path from the supplied `ancestorID` to the
+ * supplied `destinationID`. If they are equal, the ID is returned.
+ *
+ * @param {string} ancestorID ID of an ancestor node of `destinationID`.
+ * @param {string} destinationID ID of the destination node.
+ * @return {string} Next ID on the path from `ancestorID` to `destinationID`.
+ * @private
+ */
+function getNextDescendantID(ancestorID, destinationID) {
+ !(isValidID(ancestorID) && isValidID(destinationID)) ? "development" !== 'production' ? invariant(false, 'getNextDescendantID(%s, %s): Received an invalid React DOM ID.', ancestorID, destinationID) : invariant(false) : undefined;
+ !isAncestorIDOf(ancestorID, destinationID) ? "development" !== 'production' ? invariant(false, 'getNextDescendantID(...): React has made an invalid assumption about ' + 'the DOM hierarchy. Expected `%s` to be an ancestor of `%s`.', ancestorID, destinationID) : invariant(false) : undefined;
+ if (ancestorID === destinationID) {
+ return ancestorID;
+ }
+ // Skip over the ancestor and the immediate separator. Traverse until we hit
+ // another separator or we reach the end of `destinationID`.
+ var start = ancestorID.length + SEPARATOR_LENGTH;
+ var i;
+ for (i = start; i < destinationID.length; i++) {
+ if (isBoundary(destinationID, i)) {
+ break;
+ }
+ }
+ return destinationID.substr(0, i);
+}
+
+/**
+ * Gets the nearest common ancestor ID of two IDs.
+ *
+ * Using this ID scheme, the nearest common ancestor ID is the longest common
+ * prefix of the two IDs that immediately preceded a "marker" in both strings.
+ *
+ * @param {string} oneID
+ * @param {string} twoID
+ * @return {string} Nearest common ancestor ID, or the empty string if none.
+ * @private
+ */
+function getFirstCommonAncestorID(oneID, twoID) {
+ var minLength = Math.min(oneID.length, twoID.length);
+ if (minLength === 0) {
+ return '';
+ }
+ var lastCommonMarkerIndex = 0;
+ // Use `<=` to traverse until the "EOL" of the shorter string.
+ for (var i = 0; i <= minLength; i++) {
+ if (isBoundary(oneID, i) && isBoundary(twoID, i)) {
+ lastCommonMarkerIndex = i;
+ } else if (oneID.charAt(i) !== twoID.charAt(i)) {
+ break;
+ }
+ }
+ var longestCommonID = oneID.substr(0, lastCommonMarkerIndex);
+ !isValidID(longestCommonID) ? "development" !== 'production' ? invariant(false, 'getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s', oneID, twoID, longestCommonID) : invariant(false) : undefined;
+ return longestCommonID;
+}
+
+/**
+ * Traverses the parent path between two IDs (either up or down). The IDs must
+ * not be the same, and there must exist a parent path between them. If the
+ * callback returns `false`, traversal is stopped.
+ *
+ * @param {?string} start ID at which to start traversal.
+ * @param {?string} stop ID at which to end traversal.
+ * @param {function} cb Callback to invoke each ID with.
+ * @param {*} arg Argument to invoke the callback with.
+ * @param {?boolean} skipFirst Whether or not to skip the first node.
+ * @param {?boolean} skipLast Whether or not to skip the last node.
+ * @private
+ */
+function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
+ start = start || '';
+ stop = stop || '';
+ !(start !== stop) ? "development" !== 'production' ? invariant(false, 'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.', start) : invariant(false) : undefined;
+ var traverseUp = isAncestorIDOf(stop, start);
+ !(traverseUp || isAncestorIDOf(start, stop)) ? "development" !== 'production' ? invariant(false, 'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do ' + 'not have a parent path.', start, stop) : invariant(false) : undefined;
+ // Traverse from `start` to `stop` one depth at a time.
+ var depth = 0;
+ var traverse = traverseUp ? getParentID : getNextDescendantID;
+ for (var id = start;; /* until break */id = traverse(id, stop)) {
+ var ret;
+ if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) {
+ ret = cb(id, traverseUp, arg);
+ }
+ if (ret === false || id === stop) {
+ // Only break //after// visiting `stop`.
+ break;
+ }
+ !(depth++ < MAX_TREE_DEPTH) ? "development" !== 'production' ? invariant(false, 'traverseParentPath(%s, %s, ...): Detected an infinite loop while ' + 'traversing the React DOM ID tree. This may be due to malformed IDs: %s', start, stop, id) : invariant(false) : undefined;
+ }
+}
+
+/**
+ * Manages the IDs assigned to DOM representations of React components. This
+ * uses a specific scheme in order to traverse the DOM efficiently (e.g. in
+ * order to simulate events).
+ *
+ * @internal
+ */
+var ReactInstanceHandles = {
+
+ /**
+ * Constructs a React root ID
+ * @return {string} A React root ID.
+ */
+ createReactRootID: function () {
+ return getReactRootIDString(ReactRootIndex.createReactRootIndex());
+ },
+
+ /**
+ * Constructs a React ID by joining a root ID with a name.
+ *
+ * @param {string} rootID Root ID of a parent component.
+ * @param {string} name A component's name (as flattened children).
+ * @return {string} A React ID.
+ * @internal
+ */
+ createReactID: function (rootID, name) {
+ return rootID + name;
+ },
+
+ /**
+ * Gets the DOM ID of the React component that is the root of the tree that
+ * contains the React component with the supplied DOM ID.
+ *
+ * @param {string} id DOM ID of a React component.
+ * @return {?string} DOM ID of the React component that is the root.
+ * @internal
+ */
+ getReactRootIDFromNodeID: function (id) {
+ if (id && id.charAt(0) === SEPARATOR && id.length > 1) {
+ var index = id.indexOf(SEPARATOR, 1);
+ return index > -1 ? id.substr(0, index) : id;
+ }
+ return null;
+ },
+
+ /**
+ * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that
+ * should would receive a `mouseEnter` or `mouseLeave` event.
+ *
+ * NOTE: Does not invoke the callback on the nearest common ancestor because
+ * nothing "entered" or "left" that element.
+ *
+ * @param {string} leaveID ID being left.
+ * @param {string} enterID ID being entered.
+ * @param {function} cb Callback to invoke on each entered/left ID.
+ * @param {*} upArg Argument to invoke the callback with on left IDs.
+ * @param {*} downArg Argument to invoke the callback with on entered IDs.
+ * @internal
+ */
+ traverseEnterLeave: function (leaveID, enterID, cb, upArg, downArg) {
+ var ancestorID = getFirstCommonAncestorID(leaveID, enterID);
+ if (ancestorID !== leaveID) {
+ traverseParentPath(leaveID, ancestorID, cb, upArg, false, true);
+ }
+ if (ancestorID !== enterID) {
+ traverseParentPath(ancestorID, enterID, cb, downArg, true, false);
+ }
+ },
+
+ /**
+ * Simulates the traversal of a two-phase, capture/bubble event dispatch.
+ *
+ * NOTE: This traversal happens on IDs without touching the DOM.
+ *
+ * @param {string} targetID ID of the target node.
+ * @param {function} cb Callback to invoke.
+ * @param {*} arg Argument to invoke the callback with.
+ * @internal
+ */
+ traverseTwoPhase: function (targetID, cb, arg) {
+ if (targetID) {
+ traverseParentPath('', targetID, cb, arg, true, false);
+ traverseParentPath(targetID, '', cb, arg, false, true);
+ }
+ },
+
+ /**
+ * Same as `traverseTwoPhase` but skips the `targetID`.
+ */
+ traverseTwoPhaseSkipTarget: function (targetID, cb, arg) {
+ if (targetID) {
+ traverseParentPath('', targetID, cb, arg, true, true);
+ traverseParentPath(targetID, '', cb, arg, true, true);
+ }
+ },
+
+ /**
+ * Traverse a node ID, calling the supplied `cb` for each ancestor ID. For
+ * example, passing `.0.$row-0.1` would result in `cb` getting called
+ * with `.0`, `.0.$row-0`, and `.0.$row-0.1`.
+ *
+ * NOTE: This traversal happens on IDs without touching the DOM.
+ *
+ * @param {string} targetID ID of the target node.
+ * @param {function} cb Callback to invoke.
+ * @param {*} arg Argument to invoke the callback with.
+ * @internal
+ */
+ traverseAncestors: function (targetID, cb, arg) {
+ traverseParentPath('', targetID, cb, arg, true, false);
+ },
+
+ getFirstCommonAncestorID: getFirstCommonAncestorID,
+
+ /**
+ * Exposed for unit testing.
+ * @private
+ */
+ _getNextDescendantID: getNextDescendantID,
+
+ isAncestorIDOf: isAncestorIDOf,
+
+ SEPARATOR: SEPARATOR
+
+};
+
+module.exports = ReactInstanceHandles;
+},{"161":161,"86":86}],68:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInstanceMap
+ */
+
+'use strict';
+
+/**
+ * `ReactInstanceMap` maintains a mapping from a public facing stateful
+ * instance (key) and the internal representation (value). This allows public
+ * methods to accept the user facing instance as an argument and map them back
+ * to internal methods.
+ */
+
+// TODO: Replace this with ES6: var ReactInstanceMap = new Map();
+var ReactInstanceMap = {
+
+ /**
+ * This API should be called `delete` but we'd have to make sure to always
+ * transform these to strings for IE support. When this transform is fully
+ * supported we can rename it.
+ */
+ remove: function (key) {
+ key._reactInternalInstance = undefined;
+ },
+
+ get: function (key) {
+ return key._reactInternalInstance;
+ },
+
+ has: function (key) {
+ return key._reactInternalInstance !== undefined;
+ },
+
+ set: function (key, value) {
+ key._reactInternalInstance = value;
+ }
+
+};
+
+module.exports = ReactInstanceMap;
+},{}],69:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactIsomorphic
+ */
+
+'use strict';
+
+var ReactChildren = _dereq_(32);
+var ReactComponent = _dereq_(34);
+var ReactClass = _dereq_(33);
+var ReactDOMFactories = _dereq_(43);
+var ReactElement = _dereq_(57);
+var ReactElementValidator = _dereq_(58);
+var ReactPropTypes = _dereq_(82);
+var ReactVersion = _dereq_(97);
+
+var assign = _dereq_(24);
+var onlyChild = _dereq_(135);
+
+var createElement = ReactElement.createElement;
+var createFactory = ReactElement.createFactory;
+var cloneElement = ReactElement.cloneElement;
+
+if ("development" !== 'production') {
+ createElement = ReactElementValidator.createElement;
+ createFactory = ReactElementValidator.createFactory;
+ cloneElement = ReactElementValidator.cloneElement;
+}
+
+var React = {
+
+ // Modern
+
+ Children: {
+ map: ReactChildren.map,
+ forEach: ReactChildren.forEach,
+ count: ReactChildren.count,
+ toArray: ReactChildren.toArray,
+ only: onlyChild
+ },
+
+ Component: ReactComponent,
+
+ createElement: createElement,
+ cloneElement: cloneElement,
+ isValidElement: ReactElement.isValidElement,
+
+ // Classic
+
+ PropTypes: ReactPropTypes,
+ createClass: ReactClass.createClass,
+ createFactory: createFactory,
+ createMixin: function (mixin) {
+ // Currently a noop. Will be used to validate and trace mixins.
+ return mixin;
+ },
+
+ // This looks DOM specific but these are actually isomorphic helpers
+ // since they are just generating DOM strings.
+ DOM: ReactDOMFactories,
+
+ version: ReactVersion,
+
+ // Hook for JSX spread, don't use this for anything else.
+ __spread: assign
+};
+
+module.exports = React;
+},{"135":135,"24":24,"32":32,"33":33,"34":34,"43":43,"57":57,"58":58,"82":82,"97":97}],70:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactLink
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * ReactLink encapsulates a common pattern in which a component wants to modify
+ * a prop received from its parent. ReactLink allows the parent to pass down a
+ * value coupled with a callback that, when invoked, expresses an intent to
+ * modify that value. For example:
+ *
+ * React.createClass({
+ * getInitialState: function() {
+ * return {value: ''};
+ * },
+ * render: function() {
+ * var valueLink = new ReactLink(this.state.value, this._handleValueChange);
+ * return <input valueLink={valueLink} />;
+ * },
+ * _handleValueChange: function(newValue) {
+ * this.setState({value: newValue});
+ * }
+ * });
+ *
+ * We have provided some sugary mixins to make the creation and
+ * consumption of ReactLink easier; see LinkedValueUtils and LinkedStateMixin.
+ */
+
+var React = _dereq_(26);
+
+/**
+ * @param {*} value current value of the link
+ * @param {function} requestChange callback to request a change
+ */
+function ReactLink(value, requestChange) {
+ this.value = value;
+ this.requestChange = requestChange;
+}
+
+/**
+ * Creates a PropType that enforces the ReactLink API and optionally checks the
+ * type of the value being passed inside the link. Example:
+ *
+ * MyComponent.propTypes = {
+ * tabIndexLink: ReactLink.PropTypes.link(React.PropTypes.number)
+ * }
+ */
+function createLinkTypeChecker(linkType) {
+ var shapes = {
+ value: typeof linkType === 'undefined' ? React.PropTypes.any.isRequired : linkType.isRequired,
+ requestChange: React.PropTypes.func.isRequired
+ };
+ return React.PropTypes.shape(shapes);
+}
+
+ReactLink.PropTypes = {
+ link: createLinkTypeChecker
+};
+
+module.exports = ReactLink;
+},{"26":26}],71:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMarkupChecksum
+ */
+
+'use strict';
+
+var adler32 = _dereq_(116);
+
+var TAG_END = /\/?>/;
+
+var ReactMarkupChecksum = {
+ CHECKSUM_ATTR_NAME: 'data-react-checksum',
+
+ /**
+ * @param {string} markup Markup string
+ * @return {string} Markup string with checksum attribute attached
+ */
+ addChecksumToMarkup: function (markup) {
+ var checksum = adler32(markup);
+
+ // Add checksum (handle both parent tags and self-closing tags)
+ return markup.replace(TAG_END, ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="' + checksum + '"$&');
+ },
+
+ /**
+ * @param {string} markup to use
+ * @param {DOMElement} element root React element
+ * @returns {boolean} whether or not the markup is the same
+ */
+ canReuseMarkup: function (markup, element) {
+ var existingChecksum = element.getAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
+ existingChecksum = existingChecksum && parseInt(existingChecksum, 10);
+ var markupChecksum = adler32(markup);
+ return markupChecksum === existingChecksum;
+ }
+};
+
+module.exports = ReactMarkupChecksum;
+},{"116":116}],72:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMount
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactCurrentOwner = _dereq_(39);
+var ReactDOMFeatureFlags = _dereq_(44);
+var ReactElement = _dereq_(57);
+var ReactEmptyComponentRegistry = _dereq_(60);
+var ReactInstanceHandles = _dereq_(67);
+var ReactInstanceMap = _dereq_(68);
+var ReactMarkupChecksum = _dereq_(71);
+var ReactPerf = _dereq_(78);
+var ReactReconciler = _dereq_(84);
+var ReactUpdateQueue = _dereq_(95);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var containsNode = _dereq_(150);
+var instantiateReactComponent = _dereq_(132);
+var invariant = _dereq_(161);
+var setInnerHTML = _dereq_(138);
+var shouldUpdateReactComponent = _dereq_(141);
+var validateDOMNesting = _dereq_(144);
+var warning = _dereq_(173);
+
+var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME;
+var nodeCache = {};
+
+var ELEMENT_NODE_TYPE = 1;
+var DOC_NODE_TYPE = 9;
+var DOCUMENT_FRAGMENT_NODE_TYPE = 11;
+
+var ownerDocumentContextKey = '__ReactMount_ownerDocument$' + Math.random().toString(36).slice(2);
+
+/** Mapping from reactRootID to React component instance. */
+var instancesByReactRootID = {};
+
+/** Mapping from reactRootID to `container` nodes. */
+var containersByReactRootID = {};
+
+if ("development" !== 'production') {
+ /** __DEV__-only mapping from reactRootID to root elements. */
+ var rootElementsByReactRootID = {};
+}
+
+// Used to store breadth-first search state in findComponentRoot.
+var findComponentRootReusableArray = [];
+
+/**
+ * Finds the index of the first character
+ * that's not common between the two given strings.
+ *
+ * @return {number} the index of the character where the strings diverge
+ */
+function firstDifferenceIndex(string1, string2) {
+ var minLen = Math.min(string1.length, string2.length);
+ for (var i = 0; i < minLen; i++) {
+ if (string1.charAt(i) !== string2.charAt(i)) {
+ return i;
+ }
+ }
+ return string1.length === string2.length ? -1 : minLen;
+}
+
+/**
+ * @param {DOMElement|DOMDocument} container DOM element that may contain
+ * a React component
+ * @return {?*} DOM element that may have the reactRoot ID, or null.
+ */
+function getReactRootElementInContainer(container) {
+ if (!container) {
+ return null;
+ }
+
+ if (container.nodeType === DOC_NODE_TYPE) {
+ return container.documentElement;
+ } else {
+ return container.firstChild;
+ }
+}
+
+/**
+ * @param {DOMElement} container DOM element that may contain a React component.
+ * @return {?string} A "reactRoot" ID, if a React component is rendered.
+ */
+function getReactRootID(container) {
+ var rootElement = getReactRootElementInContainer(container);
+ return rootElement && ReactMount.getID(rootElement);
+}
+
+/**
+ * Accessing node[ATTR_NAME] or calling getAttribute(ATTR_NAME) on a form
+ * element can return its control whose name or ID equals ATTR_NAME. All
+ * DOM nodes support `getAttributeNode` but this can also get called on
+ * other objects so just return '' if we're given something other than a
+ * DOM node (such as window).
+ *
+ * @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node.
+ * @return {string} ID of the supplied `domNode`.
+ */
+function getID(node) {
+ var id = internalGetID(node);
+ if (id) {
+ if (nodeCache.hasOwnProperty(id)) {
+ var cached = nodeCache[id];
+ if (cached !== node) {
+ !!isValid(cached, id) ? "development" !== 'production' ? invariant(false, 'ReactMount: Two valid but unequal nodes with the same `%s`: %s', ATTR_NAME, id) : invariant(false) : undefined;
+
+ nodeCache[id] = node;
+ }
+ } else {
+ nodeCache[id] = node;
+ }
+ }
+
+ return id;
+}
+
+function internalGetID(node) {
+ // If node is something like a window, document, or text node, none of
+ // which support attributes or a .getAttribute method, gracefully return
+ // the empty string, as if the attribute were missing.
+ return node && node.getAttribute && node.getAttribute(ATTR_NAME) || '';
+}
+
+/**
+ * Sets the React-specific ID of the given node.
+ *
+ * @param {DOMElement} node The DOM node whose ID will be set.
+ * @param {string} id The value of the ID attribute.
+ */
+function setID(node, id) {
+ var oldID = internalGetID(node);
+ if (oldID !== id) {
+ delete nodeCache[oldID];
+ }
+ node.setAttribute(ATTR_NAME, id);
+ nodeCache[id] = node;
+}
+
+/**
+ * Finds the node with the supplied React-generated DOM ID.
+ *
+ * @param {string} id A React-generated DOM ID.
+ * @return {DOMElement} DOM node with the suppled `id`.
+ * @internal
+ */
+function getNode(id) {
+ if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {
+ nodeCache[id] = ReactMount.findReactNodeByID(id);
+ }
+ return nodeCache[id];
+}
+
+/**
+ * Finds the node with the supplied public React instance.
+ *
+ * @param {*} instance A public React instance.
+ * @return {?DOMElement} DOM node with the suppled `id`.
+ * @internal
+ */
+function getNodeFromInstance(instance) {
+ var id = ReactInstanceMap.get(instance)._rootNodeID;
+ if (ReactEmptyComponentRegistry.isNullComponentID(id)) {
+ return null;
+ }
+ if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {
+ nodeCache[id] = ReactMount.findReactNodeByID(id);
+ }
+ return nodeCache[id];
+}
+
+/**
+ * A node is "valid" if it is contained by a currently mounted container.
+ *
+ * This means that the node does not have to be contained by a document in
+ * order to be considered valid.
+ *
+ * @param {?DOMElement} node The candidate DOM node.
+ * @param {string} id The expected ID of the node.
+ * @return {boolean} Whether the node is contained by a mounted container.
+ */
+function isValid(node, id) {
+ if (node) {
+ !(internalGetID(node) === id) ? "development" !== 'production' ? invariant(false, 'ReactMount: Unexpected modification of `%s`', ATTR_NAME) : invariant(false) : undefined;
+
+ var container = ReactMount.findReactContainerForID(id);
+ if (container && containsNode(container, node)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Causes the cache to forget about one React-specific ID.
+ *
+ * @param {string} id The ID to forget.
+ */
+function purgeID(id) {
+ delete nodeCache[id];
+}
+
+var deepestNodeSoFar = null;
+function findDeepestCachedAncestorImpl(ancestorID) {
+ var ancestor = nodeCache[ancestorID];
+ if (ancestor && isValid(ancestor, ancestorID)) {
+ deepestNodeSoFar = ancestor;
+ } else {
+ // This node isn't populated in the cache, so presumably none of its
+ // descendants are. Break out of the loop.
+ return false;
+ }
+}
+
+/**
+ * Return the deepest cached node whose ID is a prefix of `targetID`.
+ */
+function findDeepestCachedAncestor(targetID) {
+ deepestNodeSoFar = null;
+ ReactInstanceHandles.traverseAncestors(targetID, findDeepestCachedAncestorImpl);
+
+ var foundNode = deepestNodeSoFar;
+ deepestNodeSoFar = null;
+ return foundNode;
+}
+
+/**
+ * Mounts this component and inserts it into the DOM.
+ *
+ * @param {ReactComponent} componentInstance The instance to mount.
+ * @param {string} rootID DOM ID of the root node.
+ * @param {DOMElement} container DOM element to mount into.
+ * @param {ReactReconcileTransaction} transaction
+ * @param {boolean} shouldReuseMarkup If true, do not insert markup
+ */
+function mountComponentIntoNode(componentInstance, rootID, container, transaction, shouldReuseMarkup, context) {
+ if (ReactDOMFeatureFlags.useCreateElement) {
+ context = assign({}, context);
+ if (container.nodeType === DOC_NODE_TYPE) {
+ context[ownerDocumentContextKey] = container;
+ } else {
+ context[ownerDocumentContextKey] = container.ownerDocument;
+ }
+ }
+ if ("development" !== 'production') {
+ if (context === emptyObject) {
+ context = {};
+ }
+ var tag = container.nodeName.toLowerCase();
+ context[validateDOMNesting.ancestorInfoContextKey] = validateDOMNesting.updatedAncestorInfo(null, tag, null);
+ }
+ var markup = ReactReconciler.mountComponent(componentInstance, rootID, transaction, context);
+ componentInstance._renderedComponent._topLevelWrapper = componentInstance;
+ ReactMount._mountImageIntoNode(markup, container, shouldReuseMarkup, transaction);
+}
+
+/**
+ * Batched mount.
+ *
+ * @param {ReactComponent} componentInstance The instance to mount.
+ * @param {string} rootID DOM ID of the root node.
+ * @param {DOMElement} container DOM element to mount into.
+ * @param {boolean} shouldReuseMarkup If true, do not insert markup
+ */
+function batchedMountComponentIntoNode(componentInstance, rootID, container, shouldReuseMarkup, context) {
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
+ /* forceHTML */shouldReuseMarkup);
+ transaction.perform(mountComponentIntoNode, null, componentInstance, rootID, container, transaction, shouldReuseMarkup, context);
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
+}
+
+/**
+ * Unmounts a component and removes it from the DOM.
+ *
+ * @param {ReactComponent} instance React component instance.
+ * @param {DOMElement} container DOM element to unmount from.
+ * @final
+ * @internal
+ * @see {ReactMount.unmountComponentAtNode}
+ */
+function unmountComponentFromNode(instance, container) {
+ ReactReconciler.unmountComponent(instance);
+
+ if (container.nodeType === DOC_NODE_TYPE) {
+ container = container.documentElement;
+ }
+
+ // http://jsperf.com/emptying-a-node
+ while (container.lastChild) {
+ container.removeChild(container.lastChild);
+ }
+}
+
+/**
+ * True if the supplied DOM node has a direct React-rendered child that is
+ * not a React root element. Useful for warning in `render`,
+ * `unmountComponentAtNode`, etc.
+ *
+ * @param {?DOMElement} node The candidate DOM node.
+ * @return {boolean} True if the DOM element contains a direct child that was
+ * rendered by React but is not a root element.
+ * @internal
+ */
+function hasNonRootReactChild(node) {
+ var reactRootID = getReactRootID(node);
+ return reactRootID ? reactRootID !== ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID) : false;
+}
+
+/**
+ * Returns the first (deepest) ancestor of a node which is rendered by this copy
+ * of React.
+ */
+function findFirstReactDOMImpl(node) {
+ // This node might be from another React instance, so we make sure not to
+ // examine the node cache here
+ for (; node && node.parentNode !== node; node = node.parentNode) {
+ if (node.nodeType !== 1) {
+ // Not a DOMElement, therefore not a React component
+ continue;
+ }
+ var nodeID = internalGetID(node);
+ if (!nodeID) {
+ continue;
+ }
+ var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID);
+
+ // If containersByReactRootID contains the container we find by crawling up
+ // the tree, we know that this instance of React rendered the node.
+ // nb. isValid's strategy (with containsNode) does not work because render
+ // trees may be nested and we don't want a false positive in that case.
+ var current = node;
+ var lastID;
+ do {
+ lastID = internalGetID(current);
+ current = current.parentNode;
+ if (current == null) {
+ // The passed-in node has been detached from the container it was
+ // originally rendered into.
+ return null;
+ }
+ } while (lastID !== reactRootID);
+
+ if (current === containersByReactRootID[reactRootID]) {
+ return node;
+ }
+ }
+ return null;
+}
+
+/**
+ * Temporary (?) hack so that we can store all top-level pending updates on
+ * composites instead of having to worry about different types of components
+ * here.
+ */
+var TopLevelWrapper = function () {};
+TopLevelWrapper.prototype.isReactComponent = {};
+if ("development" !== 'production') {
+ TopLevelWrapper.displayName = 'TopLevelWrapper';
+}
+TopLevelWrapper.prototype.render = function () {
+ // this.props is actually a ReactElement
+ return this.props;
+};
+
+/**
+ * Mounting is the process of initializing a React component by creating its
+ * representative DOM elements and inserting them into a supplied `container`.
+ * Any prior content inside `container` is destroyed in the process.
+ *
+ * ReactMount.render(
+ * component,
+ * document.getElementById('container')
+ * );
+ *
+ * <div id="container"> <-- Supplied `container`.
+ * <div data-reactid=".3"> <-- Rendered reactRoot of React
+ * // ... component.
+ * </div>
+ * </div>
+ *
+ * Inside of `container`, the first element rendered is the "reactRoot".
+ */
+var ReactMount = {
+
+ TopLevelWrapper: TopLevelWrapper,
+
+ /** Exposed for debugging purposes **/
+ _instancesByReactRootID: instancesByReactRootID,
+
+ /**
+ * This is a hook provided to support rendering React components while
+ * ensuring that the apparent scroll position of its `container` does not
+ * change.
+ *
+ * @param {DOMElement} container The `container` being rendered into.
+ * @param {function} renderCallback This must be called once to do the render.
+ */
+ scrollMonitor: function (container, renderCallback) {
+ renderCallback();
+ },
+
+ /**
+ * Take a component that's already mounted into the DOM and replace its props
+ * @param {ReactComponent} prevComponent component instance already in the DOM
+ * @param {ReactElement} nextElement component instance to render
+ * @param {DOMElement} container container to render into
+ * @param {?function} callback function triggered on completion
+ */
+ _updateRootComponent: function (prevComponent, nextElement, container, callback) {
+ ReactMount.scrollMonitor(container, function () {
+ ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement);
+ if (callback) {
+ ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback);
+ }
+ });
+
+ if ("development" !== 'production') {
+ // Record the root element in case it later gets transplanted.
+ rootElementsByReactRootID[getReactRootID(container)] = getReactRootElementInContainer(container);
+ }
+
+ return prevComponent;
+ },
+
+ /**
+ * Register a component into the instance map and starts scroll value
+ * monitoring
+ * @param {ReactComponent} nextComponent component instance to render
+ * @param {DOMElement} container container to render into
+ * @return {string} reactRoot ID prefix
+ */
+ _registerComponent: function (nextComponent, container) {
+ !(container && (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE || container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)) ? "development" !== 'production' ? invariant(false, '_registerComponent(...): Target container is not a DOM element.') : invariant(false) : undefined;
+
+ ReactBrowserEventEmitter.ensureScrollValueMonitoring();
+
+ var reactRootID = ReactMount.registerContainer(container);
+ instancesByReactRootID[reactRootID] = nextComponent;
+ return reactRootID;
+ },
+
+ /**
+ * Render a new component into the DOM.
+ * @param {ReactElement} nextElement element to render
+ * @param {DOMElement} container container to render into
+ * @param {boolean} shouldReuseMarkup if we should skip the markup insertion
+ * @return {ReactComponent} nextComponent
+ */
+ _renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
+ // Various parts of our code (such as ReactCompositeComponent's
+ // _renderValidatedComponent) assume that calls to render aren't nested;
+ // verify that that's the case.
+ "development" !== 'production' ? warning(ReactCurrentOwner.current == null, '_renderNewRootComponent(): Render methods should be a pure function ' + 'of props and state; triggering nested component updates from ' + 'render is not allowed. If necessary, trigger nested updates in ' + 'componentDidUpdate. Check the render method of %s.', ReactCurrentOwner.current && ReactCurrentOwner.current.getName() || 'ReactCompositeComponent') : undefined;
+
+ var componentInstance = instantiateReactComponent(nextElement, null);
+ var reactRootID = ReactMount._registerComponent(componentInstance, container);
+
+ // The initial render is synchronous but any updates that happen during
+ // rendering, in componentWillMount or componentDidMount, will be batched
+ // according to the current batching strategy.
+
+ ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, reactRootID, container, shouldReuseMarkup, context);
+
+ if ("development" !== 'production') {
+ // Record the root element in case it later gets transplanted.
+ rootElementsByReactRootID[reactRootID] = getReactRootElementInContainer(container);
+ }
+
+ return componentInstance;
+ },
+
+ /**
+ * Renders a React component into the DOM in the supplied `container`.
+ *
+ * If the React component was previously rendered into `container`, this will
+ * perform an update on it and only mutate the DOM as necessary to reflect the
+ * latest React component.
+ *
+ * @param {ReactComponent} parentComponent The conceptual parent of this render tree.
+ * @param {ReactElement} nextElement Component element to render.
+ * @param {DOMElement} container DOM element to render into.
+ * @param {?function} callback function triggered on completion
+ * @return {ReactComponent} Component instance rendered in `container`.
+ */
+ renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
+ !(parentComponent != null && parentComponent._reactInternalInstance != null) ? "development" !== 'production' ? invariant(false, 'parentComponent must be a valid React Component') : invariant(false) : undefined;
+ return ReactMount._renderSubtreeIntoContainer(parentComponent, nextElement, container, callback);
+ },
+
+ _renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
+ !ReactElement.isValidElement(nextElement) ? "development" !== 'production' ? invariant(false, 'ReactDOM.render(): Invalid component element.%s', typeof nextElement === 'string' ? ' Instead of passing an element string, make sure to instantiate ' + 'it by passing it to React.createElement.' : typeof nextElement === 'function' ? ' Instead of passing a component class, make sure to instantiate ' + 'it by passing it to React.createElement.' :
+ // Check if it quacks like an element
+ nextElement != null && nextElement.props !== undefined ? ' This may be caused by unintentionally loading two independent ' + 'copies of React.' : '') : invariant(false) : undefined;
+
+ "development" !== 'production' ? warning(!container || !container.tagName || container.tagName.toUpperCase() !== 'BODY', 'render(): Rendering components directly into document.body is ' + 'discouraged, since its children are often manipulated by third-party ' + 'scripts and browser extensions. This may lead to subtle ' + 'reconciliation issues. Try rendering into a container element created ' + 'for your app.') : undefined;
+
+ var nextWrappedElement = new ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement);
+
+ var prevComponent = instancesByReactRootID[getReactRootID(container)];
+
+ if (prevComponent) {
+ var prevWrappedElement = prevComponent._currentElement;
+ var prevElement = prevWrappedElement.props;
+ if (shouldUpdateReactComponent(prevElement, nextElement)) {
+ var publicInst = prevComponent._renderedComponent.getPublicInstance();
+ var updatedCallback = callback && function () {
+ callback.call(publicInst);
+ };
+ ReactMount._updateRootComponent(prevComponent, nextWrappedElement, container, updatedCallback);
+ return publicInst;
+ } else {
+ ReactMount.unmountComponentAtNode(container);
+ }
+ }
+
+ var reactRootElement = getReactRootElementInContainer(container);
+ var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
+ var containerHasNonRootReactChild = hasNonRootReactChild(container);
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(!containerHasNonRootReactChild, 'render(...): Replacing React-rendered children with a new root ' + 'component. If you intended to update the children of this node, ' + 'you should instead have the existing children update their state ' + 'and render the new components instead of calling ReactDOM.render.') : undefined;
+
+ if (!containerHasReactMarkup || reactRootElement.nextSibling) {
+ var rootElementSibling = reactRootElement;
+ while (rootElementSibling) {
+ if (internalGetID(rootElementSibling)) {
+ "development" !== 'production' ? warning(false, 'render(): Target node has markup rendered by React, but there ' + 'are unrelated nodes as well. This is most commonly caused by ' + 'white-space inserted around server-rendered markup.') : undefined;
+ break;
+ }
+ rootElementSibling = rootElementSibling.nextSibling;
+ }
+ }
+ }
+
+ var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
+ var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, parentComponent != null ? parentComponent._reactInternalInstance._processChildContext(parentComponent._reactInternalInstance._context) : emptyObject)._renderedComponent.getPublicInstance();
+ if (callback) {
+ callback.call(component);
+ }
+ return component;
+ },
+
+ /**
+ * Renders a React component into the DOM in the supplied `container`.
+ *
+ * If the React component was previously rendered into `container`, this will
+ * perform an update on it and only mutate the DOM as necessary to reflect the
+ * latest React component.
+ *
+ * @param {ReactElement} nextElement Component element to render.
+ * @param {DOMElement} container DOM element to render into.
+ * @param {?function} callback function triggered on completion
+ * @return {ReactComponent} Component instance rendered in `container`.
+ */
+ render: function (nextElement, container, callback) {
+ return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
+ },
+
+ /**
+ * Registers a container node into which React components will be rendered.
+ * This also creates the "reactRoot" ID that will be assigned to the element
+ * rendered within.
+ *
+ * @param {DOMElement} container DOM element to register as a container.
+ * @return {string} The "reactRoot" ID of elements rendered within.
+ */
+ registerContainer: function (container) {
+ var reactRootID = getReactRootID(container);
+ if (reactRootID) {
+ // If one exists, make sure it is a valid "reactRoot" ID.
+ reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID);
+ }
+ if (!reactRootID) {
+ // No valid "reactRoot" ID found, create one.
+ reactRootID = ReactInstanceHandles.createReactRootID();
+ }
+ containersByReactRootID[reactRootID] = container;
+ return reactRootID;
+ },
+
+ /**
+ * Unmounts and destroys the React component rendered in the `container`.
+ *
+ * @param {DOMElement} container DOM element containing a React component.
+ * @return {boolean} True if a component was found in and unmounted from
+ * `container`
+ */
+ unmountComponentAtNode: function (container) {
+ // Various parts of our code (such as ReactCompositeComponent's
+ // _renderValidatedComponent) assume that calls to render aren't nested;
+ // verify that that's the case. (Strictly speaking, unmounting won't cause a
+ // render but we still don't expect to be in a render call here.)
+ "development" !== 'production' ? warning(ReactCurrentOwner.current == null, 'unmountComponentAtNode(): Render methods should be a pure function ' + 'of props and state; triggering nested component updates from render ' + 'is not allowed. If necessary, trigger nested updates in ' + 'componentDidUpdate. Check the render method of %s.', ReactCurrentOwner.current && ReactCurrentOwner.current.getName() || 'ReactCompositeComponent') : undefined;
+
+ !(container && (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE || container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)) ? "development" !== 'production' ? invariant(false, 'unmountComponentAtNode(...): Target container is not a DOM element.') : invariant(false) : undefined;
+
+ var reactRootID = getReactRootID(container);
+ var component = instancesByReactRootID[reactRootID];
+ if (!component) {
+ // Check if the node being unmounted was rendered by React, but isn't a
+ // root node.
+ var containerHasNonRootReactChild = hasNonRootReactChild(container);
+
+ // Check if the container itself is a React root node.
+ var containerID = internalGetID(container);
+ var isContainerReactRoot = containerID && containerID === ReactInstanceHandles.getReactRootIDFromNodeID(containerID);
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(!containerHasNonRootReactChild, 'unmountComponentAtNode(): The node you\'re attempting to unmount ' + 'was rendered by React and is not a top-level container. %s', isContainerReactRoot ? 'You may have accidentally passed in a React root node instead ' + 'of its container.' : 'Instead, have the parent component update its state and ' + 'rerender in order to remove this component.') : undefined;
+ }
+
+ return false;
+ }
+ ReactUpdates.batchedUpdates(unmountComponentFromNode, component, container);
+ delete instancesByReactRootID[reactRootID];
+ delete containersByReactRootID[reactRootID];
+ if ("development" !== 'production') {
+ delete rootElementsByReactRootID[reactRootID];
+ }
+ return true;
+ },
+
+ /**
+ * Finds the container DOM element that contains React component to which the
+ * supplied DOM `id` belongs.
+ *
+ * @param {string} id The ID of an element rendered by a React component.
+ * @return {?DOMElement} DOM element that contains the `id`.
+ */
+ findReactContainerForID: function (id) {
+ var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(id);
+ var container = containersByReactRootID[reactRootID];
+
+ if ("development" !== 'production') {
+ var rootElement = rootElementsByReactRootID[reactRootID];
+ if (rootElement && rootElement.parentNode !== container) {
+ "development" !== 'production' ? warning(
+ // Call internalGetID here because getID calls isValid which calls
+ // findReactContainerForID (this function).
+ internalGetID(rootElement) === reactRootID, 'ReactMount: Root element ID differed from reactRootID.') : undefined;
+ var containerChild = container.firstChild;
+ if (containerChild && reactRootID === internalGetID(containerChild)) {
+ // If the container has a new child with the same ID as the old
+ // root element, then rootElementsByReactRootID[reactRootID] is
+ // just stale and needs to be updated. The case that deserves a
+ // warning is when the container is empty.
+ rootElementsByReactRootID[reactRootID] = containerChild;
+ } else {
+ "development" !== 'production' ? warning(false, 'ReactMount: Root element has been removed from its original ' + 'container. New container: %s', rootElement.parentNode) : undefined;
+ }
+ }
+ }
+
+ return container;
+ },
+
+ /**
+ * Finds an element rendered by React with the supplied ID.
+ *
+ * @param {string} id ID of a DOM node in the React component.
+ * @return {DOMElement} Root DOM node of the React component.
+ */
+ findReactNodeByID: function (id) {
+ var reactRoot = ReactMount.findReactContainerForID(id);
+ return ReactMount.findComponentRoot(reactRoot, id);
+ },
+
+ /**
+ * Traverses up the ancestors of the supplied node to find a node that is a
+ * DOM representation of a React component rendered by this copy of React.
+ *
+ * @param {*} node
+ * @return {?DOMEventTarget}
+ * @internal
+ */
+ getFirstReactDOM: function (node) {
+ return findFirstReactDOMImpl(node);
+ },
+
+ /**
+ * Finds a node with the supplied `targetID` inside of the supplied
+ * `ancestorNode`. Exploits the ID naming scheme to perform the search
+ * quickly.
+ *
+ * @param {DOMEventTarget} ancestorNode Search from this root.
+ * @pararm {string} targetID ID of the DOM representation of the component.
+ * @return {DOMEventTarget} DOM node with the supplied `targetID`.
+ * @internal
+ */
+ findComponentRoot: function (ancestorNode, targetID) {
+ var firstChildren = findComponentRootReusableArray;
+ var childIndex = 0;
+
+ var deepestAncestor = findDeepestCachedAncestor(targetID) || ancestorNode;
+
+ if ("development" !== 'production') {
+ // This will throw on the next line; give an early warning
+ "development" !== 'production' ? warning(deepestAncestor != null, 'React can\'t find the root component node for data-reactid value ' + '`%s`. If you\'re seeing this message, it probably means that ' + 'you\'ve loaded two copies of React on the page. At this time, only ' + 'a single copy of React can be loaded at a time.', targetID) : undefined;
+ }
+
+ firstChildren[0] = deepestAncestor.firstChild;
+ firstChildren.length = 1;
+
+ while (childIndex < firstChildren.length) {
+ var child = firstChildren[childIndex++];
+ var targetChild;
+
+ while (child) {
+ var childID = ReactMount.getID(child);
+ if (childID) {
+ // Even if we find the node we're looking for, we finish looping
+ // through its siblings to ensure they're cached so that we don't have
+ // to revisit this node again. Otherwise, we make n^2 calls to getID
+ // when visiting the many children of a single node in order.
+
+ if (targetID === childID) {
+ targetChild = child;
+ } else if (ReactInstanceHandles.isAncestorIDOf(childID, targetID)) {
+ // If we find a child whose ID is an ancestor of the given ID,
+ // then we can be sure that we only want to search the subtree
+ // rooted at this child, so we can throw out the rest of the
+ // search state.
+ firstChildren.length = childIndex = 0;
+ firstChildren.push(child.firstChild);
+ }
+ } else {
+ // If this child had no ID, then there's a chance that it was
+ // injected automatically by the browser, as when a `<table>`
+ // element sprouts an extra `<tbody>` child as a side effect of
+ // `.innerHTML` parsing. Optimistically continue down this
+ // branch, but not before examining the other siblings.
+ firstChildren.push(child.firstChild);
+ }
+
+ child = child.nextSibling;
+ }
+
+ if (targetChild) {
+ // Emptying firstChildren/findComponentRootReusableArray is
+ // not necessary for correctness, but it helps the GC reclaim
+ // any nodes that were left at the end of the search.
+ firstChildren.length = 0;
+
+ return targetChild;
+ }
+ }
+
+ firstChildren.length = 0;
+
+ !false ? "development" !== 'production' ? invariant(false, 'findComponentRoot(..., %s): Unable to find element. This probably ' + 'means the DOM was unexpectedly mutated (e.g., by the browser), ' + 'usually due to forgetting a <tbody> when using tables, nesting tags ' + 'like <form>, <p>, or <a>, or using non-SVG elements in an <svg> ' + 'parent. ' + 'Try inspecting the child nodes of the element with React ID `%s`.', targetID, ReactMount.getID(ancestorNode)) : invariant(false) : undefined;
+ },
+
+ _mountImageIntoNode: function (markup, container, shouldReuseMarkup, transaction) {
+ !(container && (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE || container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)) ? "development" !== 'production' ? invariant(false, 'mountComponentIntoNode(...): Target container is not valid.') : invariant(false) : undefined;
+
+ if (shouldReuseMarkup) {
+ var rootElement = getReactRootElementInContainer(container);
+ if (ReactMarkupChecksum.canReuseMarkup(markup, rootElement)) {
+ return;
+ } else {
+ var checksum = rootElement.getAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
+ rootElement.removeAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
+
+ var rootMarkup = rootElement.outerHTML;
+ rootElement.setAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME, checksum);
+
+ var normalizedMarkup = markup;
+ if ("development" !== 'production') {
+ // because rootMarkup is retrieved from the DOM, various normalizations
+ // will have occurred which will not be present in `markup`. Here,
+ // insert markup into a <div> or <iframe> depending on the container
+ // type to perform the same normalizations before comparing.
+ var normalizer;
+ if (container.nodeType === ELEMENT_NODE_TYPE) {
+ normalizer = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ normalizer.innerHTML = markup;
+ normalizedMarkup = normalizer.innerHTML;
+ } else {
+ normalizer = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe');
+ document.body.appendChild(normalizer);
+ normalizer.contentDocument.write(markup);
+ normalizedMarkup = normalizer.contentDocument.documentElement.outerHTML;
+ document.body.removeChild(normalizer);
+ }
+ }
+
+ var diffIndex = firstDifferenceIndex(normalizedMarkup, rootMarkup);
+ var difference = ' (client) ' + normalizedMarkup.substring(diffIndex - 20, diffIndex + 20) + '\n (server) ' + rootMarkup.substring(diffIndex - 20, diffIndex + 20);
+
+ !(container.nodeType !== DOC_NODE_TYPE) ? "development" !== 'production' ? invariant(false, 'You\'re trying to render a component to the document using ' + 'server rendering but the checksum was invalid. This usually ' + 'means you rendered a different component type or props on ' + 'the client from the one on the server, or your render() ' + 'methods are impure. React cannot handle this case due to ' + 'cross-browser quirks by rendering at the document root. You ' + 'should look for environment dependent code in your components ' + 'and ensure the props are the same client and server side:\n%s', difference) : invariant(false) : undefined;
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(false, 'React attempted to reuse markup in a container but the ' + 'checksum was invalid. This generally means that you are ' + 'using server rendering and the markup generated on the ' + 'server was not what the client was expecting. React injected ' + 'new markup to compensate which works but you have lost many ' + 'of the benefits of server rendering. Instead, figure out ' + 'why the markup being generated is different on the client ' + 'or server:\n%s', difference) : undefined;
+ }
+ }
+ }
+
+ !(container.nodeType !== DOC_NODE_TYPE) ? "development" !== 'production' ? invariant(false, 'You\'re trying to render a component to the document but ' + 'you didn\'t use server rendering. We can\'t do this ' + 'without using server rendering due to cross-browser quirks. ' + 'See ReactDOMServer.renderToString() for server rendering.') : invariant(false) : undefined;
+
+ if (transaction.useCreateElement) {
+ while (container.lastChild) {
+ container.removeChild(container.lastChild);
+ }
+ container.appendChild(markup);
+ } else {
+ setInnerHTML(container, markup);
+ }
+ },
+
+ ownerDocumentContextKey: ownerDocumentContextKey,
+
+ /**
+ * React ID utilities.
+ */
+
+ getReactRootID: getReactRootID,
+
+ getID: getID,
+
+ setID: setID,
+
+ getNode: getNode,
+
+ getNodeFromInstance: getNodeFromInstance,
+
+ isValid: isValid,
+
+ purgeID: purgeID
+};
+
+ReactPerf.measureMethods(ReactMount, 'ReactMount', {
+ _renderNewRootComponent: '_renderNewRootComponent',
+ _mountImageIntoNode: '_mountImageIntoNode'
+});
+
+module.exports = ReactMount;
+},{"10":10,"132":132,"138":138,"141":141,"144":144,"150":150,"154":154,"161":161,"173":173,"24":24,"28":28,"39":39,"44":44,"57":57,"60":60,"67":67,"68":68,"71":71,"78":78,"84":84,"95":95,"96":96}],73:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMultiChild
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactComponentEnvironment = _dereq_(36);
+var ReactMultiChildUpdateTypes = _dereq_(74);
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactReconciler = _dereq_(84);
+var ReactChildReconciler = _dereq_(31);
+
+var flattenChildren = _dereq_(123);
+
+/**
+ * Updating children of a component may trigger recursive updates. The depth is
+ * used to batch recursive updates to render markup more efficiently.
+ *
+ * @type {number}
+ * @private
+ */
+var updateDepth = 0;
+
+/**
+ * Queue of update configuration objects.
+ *
+ * Each object has a `type` property that is in `ReactMultiChildUpdateTypes`.
+ *
+ * @type {array<object>}
+ * @private
+ */
+var updateQueue = [];
+
+/**
+ * Queue of markup to be rendered.
+ *
+ * @type {array<string>}
+ * @private
+ */
+var markupQueue = [];
+
+/**
+ * Enqueues markup to be rendered and inserted at a supplied index.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {string} markup Markup that renders into an element.
+ * @param {number} toIndex Destination index.
+ * @private
+ */
+function enqueueInsertMarkup(parentID, markup, toIndex) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
+ markupIndex: markupQueue.push(markup) - 1,
+ content: null,
+ fromIndex: null,
+ toIndex: toIndex
+ });
+}
+
+/**
+ * Enqueues moving an existing element to another index.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {number} fromIndex Source index of the existing element.
+ * @param {number} toIndex Destination index of the element.
+ * @private
+ */
+function enqueueMove(parentID, fromIndex, toIndex) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
+ markupIndex: null,
+ content: null,
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ });
+}
+
+/**
+ * Enqueues removing an element at an index.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {number} fromIndex Index of the element to remove.
+ * @private
+ */
+function enqueueRemove(parentID, fromIndex) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.REMOVE_NODE,
+ markupIndex: null,
+ content: null,
+ fromIndex: fromIndex,
+ toIndex: null
+ });
+}
+
+/**
+ * Enqueues setting the markup of a node.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {string} markup Markup that renders into an element.
+ * @private
+ */
+function enqueueSetMarkup(parentID, markup) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.SET_MARKUP,
+ markupIndex: null,
+ content: markup,
+ fromIndex: null,
+ toIndex: null
+ });
+}
+
+/**
+ * Enqueues setting the text content.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {string} textContent Text content to set.
+ * @private
+ */
+function enqueueTextContent(parentID, textContent) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.TEXT_CONTENT,
+ markupIndex: null,
+ content: textContent,
+ fromIndex: null,
+ toIndex: null
+ });
+}
+
+/**
+ * Processes any enqueued updates.
+ *
+ * @private
+ */
+function processQueue() {
+ if (updateQueue.length) {
+ ReactComponentEnvironment.processChildrenUpdates(updateQueue, markupQueue);
+ clearQueue();
+ }
+}
+
+/**
+ * Clears any enqueued updates.
+ *
+ * @private
+ */
+function clearQueue() {
+ updateQueue.length = 0;
+ markupQueue.length = 0;
+}
+
+/**
+ * ReactMultiChild are capable of reconciling multiple children.
+ *
+ * @class ReactMultiChild
+ * @internal
+ */
+var ReactMultiChild = {
+
+ /**
+ * Provides common functionality for components that must reconcile multiple
+ * children. This is used by `ReactDOMComponent` to mount, update, and
+ * unmount child components.
+ *
+ * @lends {ReactMultiChild.prototype}
+ */
+ Mixin: {
+
+ _reconcilerInstantiateChildren: function (nestedChildren, transaction, context) {
+ if ("development" !== 'production') {
+ if (this._currentElement) {
+ try {
+ ReactCurrentOwner.current = this._currentElement._owner;
+ return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ }
+ }
+ return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);
+ },
+
+ _reconcilerUpdateChildren: function (prevChildren, nextNestedChildrenElements, transaction, context) {
+ var nextChildren;
+ if ("development" !== 'production') {
+ if (this._currentElement) {
+ try {
+ ReactCurrentOwner.current = this._currentElement._owner;
+ nextChildren = flattenChildren(nextNestedChildrenElements);
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ return ReactChildReconciler.updateChildren(prevChildren, nextChildren, transaction, context);
+ }
+ }
+ nextChildren = flattenChildren(nextNestedChildrenElements);
+ return ReactChildReconciler.updateChildren(prevChildren, nextChildren, transaction, context);
+ },
+
+ /**
+ * Generates a "mount image" for each of the supplied children. In the case
+ * of `ReactDOMComponent`, a mount image is a string of markup.
+ *
+ * @param {?object} nestedChildren Nested child maps.
+ * @return {array} An array of mounted representations.
+ * @internal
+ */
+ mountChildren: function (nestedChildren, transaction, context) {
+ var children = this._reconcilerInstantiateChildren(nestedChildren, transaction, context);
+ this._renderedChildren = children;
+ var mountImages = [];
+ var index = 0;
+ for (var name in children) {
+ if (children.hasOwnProperty(name)) {
+ var child = children[name];
+ // Inlined for performance, see `ReactInstanceHandles.createReactID`.
+ var rootID = this._rootNodeID + name;
+ var mountImage = ReactReconciler.mountComponent(child, rootID, transaction, context);
+ child._mountIndex = index++;
+ mountImages.push(mountImage);
+ }
+ }
+ return mountImages;
+ },
+
+ /**
+ * Replaces any rendered children with a text content string.
+ *
+ * @param {string} nextContent String of content.
+ * @internal
+ */
+ updateTextContent: function (nextContent) {
+ updateDepth++;
+ var errorThrown = true;
+ try {
+ var prevChildren = this._renderedChildren;
+ // Remove any rendered children.
+ ReactChildReconciler.unmountChildren(prevChildren);
+ // TODO: The setTextContent operation should be enough
+ for (var name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name)) {
+ this._unmountChild(prevChildren[name]);
+ }
+ }
+ // Set new text content.
+ this.setTextContent(nextContent);
+ errorThrown = false;
+ } finally {
+ updateDepth--;
+ if (!updateDepth) {
+ if (errorThrown) {
+ clearQueue();
+ } else {
+ processQueue();
+ }
+ }
+ }
+ },
+
+ /**
+ * Replaces any rendered children with a markup string.
+ *
+ * @param {string} nextMarkup String of markup.
+ * @internal
+ */
+ updateMarkup: function (nextMarkup) {
+ updateDepth++;
+ var errorThrown = true;
+ try {
+ var prevChildren = this._renderedChildren;
+ // Remove any rendered children.
+ ReactChildReconciler.unmountChildren(prevChildren);
+ for (var name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name)) {
+ this._unmountChildByName(prevChildren[name], name);
+ }
+ }
+ this.setMarkup(nextMarkup);
+ errorThrown = false;
+ } finally {
+ updateDepth--;
+ if (!updateDepth) {
+ if (errorThrown) {
+ clearQueue();
+ } else {
+ processQueue();
+ }
+ }
+ }
+ },
+
+ /**
+ * Updates the rendered children with new children.
+ *
+ * @param {?object} nextNestedChildrenElements Nested child element maps.
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ updateChildren: function (nextNestedChildrenElements, transaction, context) {
+ updateDepth++;
+ var errorThrown = true;
+ try {
+ this._updateChildren(nextNestedChildrenElements, transaction, context);
+ errorThrown = false;
+ } finally {
+ updateDepth--;
+ if (!updateDepth) {
+ if (errorThrown) {
+ clearQueue();
+ } else {
+ processQueue();
+ }
+ }
+ }
+ },
+
+ /**
+ * Improve performance by isolating this hot code path from the try/catch
+ * block in `updateChildren`.
+ *
+ * @param {?object} nextNestedChildrenElements Nested child element maps.
+ * @param {ReactReconcileTransaction} transaction
+ * @final
+ * @protected
+ */
+ _updateChildren: function (nextNestedChildrenElements, transaction, context) {
+ var prevChildren = this._renderedChildren;
+ var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, transaction, context);
+ this._renderedChildren = nextChildren;
+ if (!nextChildren && !prevChildren) {
+ return;
+ }
+ var name;
+ // `nextIndex` will increment for each child in `nextChildren`, but
+ // `lastIndex` will be the last index visited in `prevChildren`.
+ var lastIndex = 0;
+ var nextIndex = 0;
+ for (name in nextChildren) {
+ if (!nextChildren.hasOwnProperty(name)) {
+ continue;
+ }
+ var prevChild = prevChildren && prevChildren[name];
+ var nextChild = nextChildren[name];
+ if (prevChild === nextChild) {
+ this.moveChild(prevChild, nextIndex, lastIndex);
+ lastIndex = Math.max(prevChild._mountIndex, lastIndex);
+ prevChild._mountIndex = nextIndex;
+ } else {
+ if (prevChild) {
+ // Update `lastIndex` before `_mountIndex` gets unset by unmounting.
+ lastIndex = Math.max(prevChild._mountIndex, lastIndex);
+ this._unmountChild(prevChild);
+ }
+ // The child must be instantiated before it's mounted.
+ this._mountChildByNameAtIndex(nextChild, name, nextIndex, transaction, context);
+ }
+ nextIndex++;
+ }
+ // Remove children that are no longer present.
+ for (name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
+ this._unmountChild(prevChildren[name]);
+ }
+ }
+ },
+
+ /**
+ * Unmounts all rendered children. This should be used to clean up children
+ * when this component is unmounted.
+ *
+ * @internal
+ */
+ unmountChildren: function () {
+ var renderedChildren = this._renderedChildren;
+ ReactChildReconciler.unmountChildren(renderedChildren);
+ this._renderedChildren = null;
+ },
+
+ /**
+ * Moves a child component to the supplied index.
+ *
+ * @param {ReactComponent} child Component to move.
+ * @param {number} toIndex Destination index of the element.
+ * @param {number} lastIndex Last index visited of the siblings of `child`.
+ * @protected
+ */
+ moveChild: function (child, toIndex, lastIndex) {
+ // If the index of `child` is less than `lastIndex`, then it needs to
+ // be moved. Otherwise, we do not need to move it because a child will be
+ // inserted or moved before `child`.
+ if (child._mountIndex < lastIndex) {
+ enqueueMove(this._rootNodeID, child._mountIndex, toIndex);
+ }
+ },
+
+ /**
+ * Creates a child component.
+ *
+ * @param {ReactComponent} child Component to create.
+ * @param {string} mountImage Markup to insert.
+ * @protected
+ */
+ createChild: function (child, mountImage) {
+ enqueueInsertMarkup(this._rootNodeID, mountImage, child._mountIndex);
+ },
+
+ /**
+ * Removes a child component.
+ *
+ * @param {ReactComponent} child Child to remove.
+ * @protected
+ */
+ removeChild: function (child) {
+ enqueueRemove(this._rootNodeID, child._mountIndex);
+ },
+
+ /**
+ * Sets this text content string.
+ *
+ * @param {string} textContent Text content to set.
+ * @protected
+ */
+ setTextContent: function (textContent) {
+ enqueueTextContent(this._rootNodeID, textContent);
+ },
+
+ /**
+ * Sets this markup string.
+ *
+ * @param {string} markup Markup to set.
+ * @protected
+ */
+ setMarkup: function (markup) {
+ enqueueSetMarkup(this._rootNodeID, markup);
+ },
+
+ /**
+ * Mounts a child with the supplied name.
+ *
+ * NOTE: This is part of `updateChildren` and is here for readability.
+ *
+ * @param {ReactComponent} child Component to mount.
+ * @param {string} name Name of the child.
+ * @param {number} index Index at which to insert the child.
+ * @param {ReactReconcileTransaction} transaction
+ * @private
+ */
+ _mountChildByNameAtIndex: function (child, name, index, transaction, context) {
+ // Inlined for performance, see `ReactInstanceHandles.createReactID`.
+ var rootID = this._rootNodeID + name;
+ var mountImage = ReactReconciler.mountComponent(child, rootID, transaction, context);
+ child._mountIndex = index;
+ this.createChild(child, mountImage);
+ },
+
+ /**
+ * Unmounts a rendered child.
+ *
+ * NOTE: This is part of `updateChildren` and is here for readability.
+ *
+ * @param {ReactComponent} child Component to unmount.
+ * @private
+ */
+ _unmountChild: function (child) {
+ this.removeChild(child);
+ child._mountIndex = null;
+ }
+
+ }
+
+};
+
+module.exports = ReactMultiChild;
+},{"123":123,"31":31,"36":36,"39":39,"74":74,"84":84}],74:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMultiChildUpdateTypes
+ */
+
+'use strict';
+
+var keyMirror = _dereq_(165);
+
+/**
+ * When a component's children are updated, a series of update configuration
+ * objects are created in order to batch and serialize the required changes.
+ *
+ * Enumerates all the possible types of update configurations.
+ *
+ * @internal
+ */
+var ReactMultiChildUpdateTypes = keyMirror({
+ INSERT_MARKUP: null,
+ MOVE_EXISTING: null,
+ REMOVE_NODE: null,
+ SET_MARKUP: null,
+ TEXT_CONTENT: null
+});
+
+module.exports = ReactMultiChildUpdateTypes;
+},{"165":165}],75:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactNativeComponent
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+var autoGenerateWrapperClass = null;
+var genericComponentClass = null;
+// This registry keeps track of wrapper classes around native tags.
+var tagToComponentClass = {};
+var textComponentClass = null;
+
+var ReactNativeComponentInjection = {
+ // This accepts a class that receives the tag string. This is a catch all
+ // that can render any kind of tag.
+ injectGenericComponentClass: function (componentClass) {
+ genericComponentClass = componentClass;
+ },
+ // This accepts a text component class that takes the text string to be
+ // rendered as props.
+ injectTextComponentClass: function (componentClass) {
+ textComponentClass = componentClass;
+ },
+ // This accepts a keyed object with classes as values. Each key represents a
+ // tag. That particular tag will use this class instead of the generic one.
+ injectComponentClasses: function (componentClasses) {
+ assign(tagToComponentClass, componentClasses);
+ }
+};
+
+/**
+ * Get a composite component wrapper class for a specific tag.
+ *
+ * @param {ReactElement} element The tag for which to get the class.
+ * @return {function} The React class constructor function.
+ */
+function getComponentClassForElement(element) {
+ if (typeof element.type === 'function') {
+ return element.type;
+ }
+ var tag = element.type;
+ var componentClass = tagToComponentClass[tag];
+ if (componentClass == null) {
+ tagToComponentClass[tag] = componentClass = autoGenerateWrapperClass(tag);
+ }
+ return componentClass;
+}
+
+/**
+ * Get a native internal component class for a specific tag.
+ *
+ * @param {ReactElement} element The element to create.
+ * @return {function} The internal class constructor function.
+ */
+function createInternalComponent(element) {
+ !genericComponentClass ? "development" !== 'production' ? invariant(false, 'There is no registered component for the tag %s', element.type) : invariant(false) : undefined;
+ return new genericComponentClass(element.type, element.props);
+}
+
+/**
+ * @param {ReactText} text
+ * @return {ReactComponent}
+ */
+function createInstanceForText(text) {
+ return new textComponentClass(text);
+}
+
+/**
+ * @param {ReactComponent} component
+ * @return {boolean}
+ */
+function isTextComponent(component) {
+ return component instanceof textComponentClass;
+}
+
+var ReactNativeComponent = {
+ getComponentClassForElement: getComponentClassForElement,
+ createInternalComponent: createInternalComponent,
+ createInstanceForText: createInstanceForText,
+ isTextComponent: isTextComponent,
+ injection: ReactNativeComponentInjection
+};
+
+module.exports = ReactNativeComponent;
+},{"161":161,"24":24}],76:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactNoopUpdateQueue
+ */
+
+'use strict';
+
+var warning = _dereq_(173);
+
+function warnTDZ(publicInstance, callerName) {
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(false, '%s(...): Can only update a mounted or mounting component. ' + 'This usually means you called %s() on an unmounted component. ' + 'This is a no-op. Please check the code for the %s component.', callerName, callerName, publicInstance.constructor && publicInstance.constructor.displayName || '') : undefined;
+ }
+}
+
+/**
+ * This is the abstract API for an update queue.
+ */
+var ReactNoopUpdateQueue = {
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @param {ReactClass} publicInstance The instance we want to test.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function (publicInstance) {
+ return false;
+ },
+
+ /**
+ * Enqueue a callback that will be executed after all the pending updates
+ * have processed.
+ *
+ * @param {ReactClass} publicInstance The instance to use as `this` context.
+ * @param {?function} callback Called after state is updated.
+ * @internal
+ */
+ enqueueCallback: function (publicInstance, callback) {},
+
+ /**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldComponentUpdate`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @internal
+ */
+ enqueueForceUpdate: function (publicInstance) {
+ warnTDZ(publicInstance, 'forceUpdate');
+ },
+
+ /**
+ * Replaces all of the state. Always use this or `setState` to mutate state.
+ * You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} completeState Next state.
+ * @internal
+ */
+ enqueueReplaceState: function (publicInstance, completeState) {
+ warnTDZ(publicInstance, 'replaceState');
+ },
+
+ /**
+ * Sets a subset of the state. This only exists because _pendingState is
+ * internal. This provides a merging strategy that is not available to deep
+ * properties which is confusing. TODO: Expose pendingState or don't use it
+ * during the merge.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialState Next partial state to be merged with state.
+ * @internal
+ */
+ enqueueSetState: function (publicInstance, partialState) {
+ warnTDZ(publicInstance, 'setState');
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialProps Subset of the next props.
+ * @internal
+ */
+ enqueueSetProps: function (publicInstance, partialProps) {
+ warnTDZ(publicInstance, 'setProps');
+ },
+
+ /**
+ * Replaces all of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} props New props.
+ * @internal
+ */
+ enqueueReplaceProps: function (publicInstance, props) {
+ warnTDZ(publicInstance, 'replaceProps');
+ }
+
+};
+
+module.exports = ReactNoopUpdateQueue;
+},{"173":173}],77:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactOwner
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * ReactOwners are capable of storing references to owned components.
+ *
+ * All components are capable of //being// referenced by owner components, but
+ * only ReactOwner components are capable of //referencing// owned components.
+ * The named reference is known as a "ref".
+ *
+ * Refs are available when mounted and updated during reconciliation.
+ *
+ * var MyComponent = React.createClass({
+ * render: function() {
+ * return (
+ * <div onClick={this.handleClick}>
+ * <CustomComponent ref="custom" />
+ * </div>
+ * );
+ * },
+ * handleClick: function() {
+ * this.refs.custom.handleClick();
+ * },
+ * componentDidMount: function() {
+ * this.refs.custom.initialize();
+ * }
+ * });
+ *
+ * Refs should rarely be used. When refs are used, they should only be done to
+ * control data that is not handled by React's data flow.
+ *
+ * @class ReactOwner
+ */
+var ReactOwner = {
+
+ /**
+ * @param {?object} object
+ * @return {boolean} True if `object` is a valid owner.
+ * @final
+ */
+ isValidOwner: function (object) {
+ return !!(object && typeof object.attachRef === 'function' && typeof object.detachRef === 'function');
+ },
+
+ /**
+ * Adds a component by ref to an owner component.
+ *
+ * @param {ReactComponent} component Component to reference.
+ * @param {string} ref Name by which to refer to the component.
+ * @param {ReactOwner} owner Component on which to record the ref.
+ * @final
+ * @internal
+ */
+ addComponentAsRefTo: function (component, ref, owner) {
+ !ReactOwner.isValidOwner(owner) ? "development" !== 'production' ? invariant(false, 'addComponentAsRefTo(...): Only a ReactOwner can have refs. You might ' + 'be adding a ref to a component that was not created inside a component\'s ' + '`render` method, or you have multiple copies of React loaded ' + '(details: https://fb.me/react-refs-must-have-owner).') : invariant(false) : undefined;
+ owner.attachRef(ref, component);
+ },
+
+ /**
+ * Removes a component by ref from an owner component.
+ *
+ * @param {ReactComponent} component Component to dereference.
+ * @param {string} ref Name of the ref to remove.
+ * @param {ReactOwner} owner Component on which the ref is recorded.
+ * @final
+ * @internal
+ */
+ removeComponentAsRefFrom: function (component, ref, owner) {
+ !ReactOwner.isValidOwner(owner) ? "development" !== 'production' ? invariant(false, 'removeComponentAsRefFrom(...): Only a ReactOwner can have refs. You might ' + 'be removing a ref to a component that was not created inside a component\'s ' + '`render` method, or you have multiple copies of React loaded ' + '(details: https://fb.me/react-refs-must-have-owner).') : invariant(false) : undefined;
+ // Check that `component` is still the current ref because we do not want to
+ // detach the ref if another component stole it.
+ if (owner.getPublicInstance().refs[ref] === component.getPublicInstance()) {
+ owner.detachRef(ref);
+ }
+ }
+
+};
+
+module.exports = ReactOwner;
+},{"161":161}],78:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPerf
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * ReactPerf is a general AOP system designed to measure performance. This
+ * module only has the hooks: see ReactDefaultPerf for the analysis tool.
+ */
+var ReactPerf = {
+ /**
+ * Boolean to enable/disable measurement. Set to false by default to prevent
+ * accidental logging and perf loss.
+ */
+ enableMeasure: false,
+
+ /**
+ * Holds onto the measure function in use. By default, don't measure
+ * anything, but we'll override this if we inject a measure function.
+ */
+ storedMeasure: _noMeasure,
+
+ /**
+ * @param {object} object
+ * @param {string} objectName
+ * @param {object<string>} methodNames
+ */
+ measureMethods: function (object, objectName, methodNames) {
+ if ("development" !== 'production') {
+ for (var key in methodNames) {
+ if (!methodNames.hasOwnProperty(key)) {
+ continue;
+ }
+ object[key] = ReactPerf.measure(objectName, methodNames[key], object[key]);
+ }
+ }
+ },
+
+ /**
+ * Use this to wrap methods you want to measure. Zero overhead in production.
+ *
+ * @param {string} objName
+ * @param {string} fnName
+ * @param {function} func
+ * @return {function}
+ */
+ measure: function (objName, fnName, func) {
+ if ("development" !== 'production') {
+ var measuredFunc = null;
+ var wrapper = function () {
+ if (ReactPerf.enableMeasure) {
+ if (!measuredFunc) {
+ measuredFunc = ReactPerf.storedMeasure(objName, fnName, func);
+ }
+ return measuredFunc.apply(this, arguments);
+ }
+ return func.apply(this, arguments);
+ };
+ wrapper.displayName = objName + '_' + fnName;
+ return wrapper;
+ }
+ return func;
+ },
+
+ injection: {
+ /**
+ * @param {function} measure
+ */
+ injectMeasure: function (measure) {
+ ReactPerf.storedMeasure = measure;
+ }
+ }
+};
+
+/**
+ * Simply passes through the measured function, without measuring it.
+ *
+ * @param {string} objName
+ * @param {string} fnName
+ * @param {function} func
+ * @return {function}
+ */
+function _noMeasure(objName, fnName, func) {
+ return func;
+}
+
+module.exports = ReactPerf;
+},{}],79:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTransferer
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+var joinClasses = _dereq_(164);
+
+/**
+ * Creates a transfer strategy that will merge prop values using the supplied
+ * `mergeStrategy`. If a prop was previously unset, this just sets it.
+ *
+ * @param {function} mergeStrategy
+ * @return {function}
+ */
+function createTransferStrategy(mergeStrategy) {
+ return function (props, key, value) {
+ if (!props.hasOwnProperty(key)) {
+ props[key] = value;
+ } else {
+ props[key] = mergeStrategy(props[key], value);
+ }
+ };
+}
+
+var transferStrategyMerge = createTransferStrategy(function (a, b) {
+ // `merge` overrides the first object's (`props[key]` above) keys using the
+ // second object's (`value`) keys. An object's style's existing `propA` would
+ // get overridden. Flip the order here.
+ return assign({}, b, a);
+});
+
+/**
+ * Transfer strategies dictate how props are transferred by `transferPropsTo`.
+ * NOTE: if you add any more exceptions to this list you should be sure to
+ * update `cloneWithProps()` accordingly.
+ */
+var TransferStrategies = {
+ /**
+ * Never transfer `children`.
+ */
+ children: emptyFunction,
+ /**
+ * Transfer the `className` prop by merging them.
+ */
+ className: createTransferStrategy(joinClasses),
+ /**
+ * Transfer the `style` prop (which is an object) by merging them.
+ */
+ style: transferStrategyMerge
+};
+
+/**
+ * Mutates the first argument by transferring the properties from the second
+ * argument.
+ *
+ * @param {object} props
+ * @param {object} newProps
+ * @return {object}
+ */
+function transferInto(props, newProps) {
+ for (var thisKey in newProps) {
+ if (!newProps.hasOwnProperty(thisKey)) {
+ continue;
+ }
+
+ var transferStrategy = TransferStrategies[thisKey];
+
+ if (transferStrategy && TransferStrategies.hasOwnProperty(thisKey)) {
+ transferStrategy(props, thisKey, newProps[thisKey]);
+ } else if (!props.hasOwnProperty(thisKey)) {
+ props[thisKey] = newProps[thisKey];
+ }
+ }
+ return props;
+}
+
+/**
+ * ReactPropTransferer are capable of transferring props to another component
+ * using a `transferPropsTo` method.
+ *
+ * @class ReactPropTransferer
+ */
+var ReactPropTransferer = {
+
+ /**
+ * Merge two props objects using TransferStrategies.
+ *
+ * @param {object} oldProps original props (they take precedence)
+ * @param {object} newProps new props to merge in
+ * @return {object} a new object containing both sets of props merged.
+ */
+ mergeProps: function (oldProps, newProps) {
+ return transferInto(assign({}, oldProps), newProps);
+ }
+
+};
+
+module.exports = ReactPropTransferer;
+},{"153":153,"164":164,"24":24}],80:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTypeLocationNames
+ */
+
+'use strict';
+
+var ReactPropTypeLocationNames = {};
+
+if ("development" !== 'production') {
+ ReactPropTypeLocationNames = {
+ prop: 'prop',
+ context: 'context',
+ childContext: 'child context'
+ };
+}
+
+module.exports = ReactPropTypeLocationNames;
+},{}],81:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTypeLocations
+ */
+
+'use strict';
+
+var keyMirror = _dereq_(165);
+
+var ReactPropTypeLocations = keyMirror({
+ prop: null,
+ context: null,
+ childContext: null
+});
+
+module.exports = ReactPropTypeLocations;
+},{"165":165}],82:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTypes
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactPropTypeLocationNames = _dereq_(80);
+
+var emptyFunction = _dereq_(153);
+var getIteratorFn = _dereq_(129);
+
+/**
+ * Collection of methods that allow declaration and validation of props that are
+ * supplied to React components. Example usage:
+ *
+ * var Props = require('ReactPropTypes');
+ * var MyArticle = React.createClass({
+ * propTypes: {
+ * // An optional string prop named "description".
+ * description: Props.string,
+ *
+ * // A required enum prop named "category".
+ * category: Props.oneOf(['News','Photos']).isRequired,
+ *
+ * // A prop named "dialog" that requires an instance of Dialog.
+ * dialog: Props.instanceOf(Dialog).isRequired
+ * },
+ * render: function() { ... }
+ * });
+ *
+ * A more formal specification of how these methods are used:
+ *
+ * type := array|bool|func|object|number|string|oneOf([...])|instanceOf(...)
+ * decl := ReactPropTypes.{type}(.isRequired)?
+ *
+ * Each and every declaration produces a function with the same signature. This
+ * allows the creation of custom validation functions. For example:
+ *
+ * var MyLink = React.createClass({
+ * propTypes: {
+ * // An optional string or URI prop named "href".
+ * href: function(props, propName, componentName) {
+ * var propValue = props[propName];
+ * if (propValue != null && typeof propValue !== 'string' &&
+ * !(propValue instanceof URI)) {
+ * return new Error(
+ * 'Expected a string or an URI for ' + propName + ' in ' +
+ * componentName
+ * );
+ * }
+ * }
+ * },
+ * render: function() {...}
+ * });
+ *
+ * @internal
+ */
+
+var ANONYMOUS = '<<anonymous>>';
+
+var ReactPropTypes = {
+ array: createPrimitiveTypeChecker('array'),
+ bool: createPrimitiveTypeChecker('boolean'),
+ func: createPrimitiveTypeChecker('function'),
+ number: createPrimitiveTypeChecker('number'),
+ object: createPrimitiveTypeChecker('object'),
+ string: createPrimitiveTypeChecker('string'),
+
+ any: createAnyTypeChecker(),
+ arrayOf: createArrayOfTypeChecker,
+ element: createElementTypeChecker(),
+ instanceOf: createInstanceTypeChecker,
+ node: createNodeChecker(),
+ objectOf: createObjectOfTypeChecker,
+ oneOf: createEnumTypeChecker,
+ oneOfType: createUnionTypeChecker,
+ shape: createShapeTypeChecker
+};
+
+function createChainableTypeChecker(validate) {
+ function checkType(isRequired, props, propName, componentName, location, propFullName) {
+ componentName = componentName || ANONYMOUS;
+ propFullName = propFullName || propName;
+ if (props[propName] == null) {
+ var locationName = ReactPropTypeLocationNames[location];
+ if (isRequired) {
+ return new Error('Required ' + locationName + ' `' + propFullName + '` was not specified in ' + ('`' + componentName + '`.'));
+ }
+ return null;
+ } else {
+ return validate(props, propName, componentName, location, propFullName);
+ }
+ }
+
+ var chainedCheckType = checkType.bind(null, false);
+ chainedCheckType.isRequired = checkType.bind(null, true);
+
+ return chainedCheckType;
+}
+
+function createPrimitiveTypeChecker(expectedType) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== expectedType) {
+ var locationName = ReactPropTypeLocationNames[location];
+ // `propValue` being instance of, say, date/regexp, pass the 'object'
+ // check, but we can offer a more precise error message here rather than
+ // 'of type `object`'.
+ var preciseType = getPreciseType(propValue);
+
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + preciseType + '` supplied to `' + componentName + '`, expected ') + ('`' + expectedType + '`.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createAnyTypeChecker() {
+ return createChainableTypeChecker(emptyFunction.thatReturns(null));
+}
+
+function createArrayOfTypeChecker(typeChecker) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ if (!Array.isArray(propValue)) {
+ var locationName = ReactPropTypeLocationNames[location];
+ var propType = getPropType(propValue);
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an array.'));
+ }
+ for (var i = 0; i < propValue.length; i++) {
+ var error = typeChecker(propValue, i, componentName, location, propFullName + '[' + i + ']');
+ if (error instanceof Error) {
+ return error;
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createElementTypeChecker() {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!ReactElement.isValidElement(props[propName])) {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a single ReactElement.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createInstanceTypeChecker(expectedClass) {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!(props[propName] instanceof expectedClass)) {
+ var locationName = ReactPropTypeLocationNames[location];
+ var expectedClassName = expectedClass.name || ANONYMOUS;
+ var actualClassName = getClassName(props[propName]);
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + actualClassName + '` supplied to `' + componentName + '`, expected ') + ('instance of `' + expectedClassName + '`.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createEnumTypeChecker(expectedValues) {
+ if (!Array.isArray(expectedValues)) {
+ return createChainableTypeChecker(function () {
+ return new Error('Invalid argument supplied to oneOf, expected an instance of array.');
+ });
+ }
+
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ for (var i = 0; i < expectedValues.length; i++) {
+ if (propValue === expectedValues[i]) {
+ return null;
+ }
+ }
+
+ var locationName = ReactPropTypeLocationNames[location];
+ var valuesString = JSON.stringify(expectedValues);
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of value `' + propValue + '` ' + ('supplied to `' + componentName + '`, expected one of ' + valuesString + '.'));
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createObjectOfTypeChecker(typeChecker) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== 'object') {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an object.'));
+ }
+ for (var key in propValue) {
+ if (propValue.hasOwnProperty(key)) {
+ var error = typeChecker(propValue, key, componentName, location, propFullName + '.' + key);
+ if (error instanceof Error) {
+ return error;
+ }
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createUnionTypeChecker(arrayOfTypeCheckers) {
+ if (!Array.isArray(arrayOfTypeCheckers)) {
+ return createChainableTypeChecker(function () {
+ return new Error('Invalid argument supplied to oneOfType, expected an instance of array.');
+ });
+ }
+
+ function validate(props, propName, componentName, location, propFullName) {
+ for (var i = 0; i < arrayOfTypeCheckers.length; i++) {
+ var checker = arrayOfTypeCheckers[i];
+ if (checker(props, propName, componentName, location, propFullName) == null) {
+ return null;
+ }
+ }
+
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`.'));
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createNodeChecker() {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!isNode(props[propName])) {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a ReactNode.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createShapeTypeChecker(shapeTypes) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== 'object') {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.'));
+ }
+ for (var key in shapeTypes) {
+ var checker = shapeTypes[key];
+ if (!checker) {
+ continue;
+ }
+ var error = checker(propValue, key, componentName, location, propFullName + '.' + key);
+ if (error) {
+ return error;
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function isNode(propValue) {
+ switch (typeof propValue) {
+ case 'number':
+ case 'string':
+ case 'undefined':
+ return true;
+ case 'boolean':
+ return !propValue;
+ case 'object':
+ if (Array.isArray(propValue)) {
+ return propValue.every(isNode);
+ }
+ if (propValue === null || ReactElement.isValidElement(propValue)) {
+ return true;
+ }
+
+ var iteratorFn = getIteratorFn(propValue);
+ if (iteratorFn) {
+ var iterator = iteratorFn.call(propValue);
+ var step;
+ if (iteratorFn !== propValue.entries) {
+ while (!(step = iterator.next()).done) {
+ if (!isNode(step.value)) {
+ return false;
+ }
+ }
+ } else {
+ // Iterator will provide entry [k,v] tuples rather than values.
+ while (!(step = iterator.next()).done) {
+ var entry = step.value;
+ if (entry) {
+ if (!isNode(entry[1])) {
+ return false;
+ }
+ }
+ }
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Equivalent of `typeof` but with special handling for array and regexp.
+function getPropType(propValue) {
+ var propType = typeof propValue;
+ if (Array.isArray(propValue)) {
+ return 'array';
+ }
+ if (propValue instanceof RegExp) {
+ // Old webkits (at least until Android 4.0) return 'function' rather than
+ // 'object' for typeof a RegExp. We'll normalize this here so that /bla/
+ // passes PropTypes.object.
+ return 'object';
+ }
+ return propType;
+}
+
+// This handles more types than `getPropType`. Only used for error messages.
+// See `createPrimitiveTypeChecker`.
+function getPreciseType(propValue) {
+ var propType = getPropType(propValue);
+ if (propType === 'object') {
+ if (propValue instanceof Date) {
+ return 'date';
+ } else if (propValue instanceof RegExp) {
+ return 'regexp';
+ }
+ }
+ return propType;
+}
+
+// Returns class name of the object, if any.
+function getClassName(propValue) {
+ if (!propValue.constructor || !propValue.constructor.name) {
+ return '<<anonymous>>';
+ }
+ return propValue.constructor.name;
+}
+
+module.exports = ReactPropTypes;
+},{"129":129,"153":153,"57":57,"80":80}],83:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactReconcileTransaction
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var CallbackQueue = _dereq_(6);
+var PooledClass = _dereq_(25);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactDOMFeatureFlags = _dereq_(44);
+var ReactInputSelection = _dereq_(66);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+
+/**
+ * Ensures that, when possible, the selection range (currently selected text
+ * input) is not disturbed by performing the transaction.
+ */
+var SELECTION_RESTORATION = {
+ /**
+ * @return {Selection} Selection information.
+ */
+ initialize: ReactInputSelection.getSelectionInformation,
+ /**
+ * @param {Selection} sel Selection information returned from `initialize`.
+ */
+ close: ReactInputSelection.restoreSelection
+};
+
+/**
+ * Suppresses events (blur/focus) that could be inadvertently dispatched due to
+ * high level DOM manipulations (like temporarily removing a text input from the
+ * DOM).
+ */
+var EVENT_SUPPRESSION = {
+ /**
+ * @return {boolean} The enabled status of `ReactBrowserEventEmitter` before
+ * the reconciliation.
+ */
+ initialize: function () {
+ var currentlyEnabled = ReactBrowserEventEmitter.isEnabled();
+ ReactBrowserEventEmitter.setEnabled(false);
+ return currentlyEnabled;
+ },
+
+ /**
+ * @param {boolean} previouslyEnabled Enabled status of
+ * `ReactBrowserEventEmitter` before the reconciliation occurred. `close`
+ * restores the previous value.
+ */
+ close: function (previouslyEnabled) {
+ ReactBrowserEventEmitter.setEnabled(previouslyEnabled);
+ }
+};
+
+/**
+ * Provides a queue for collecting `componentDidMount` and
+ * `componentDidUpdate` callbacks during the the transaction.
+ */
+var ON_DOM_READY_QUEUEING = {
+ /**
+ * Initializes the internal `onDOMReady` queue.
+ */
+ initialize: function () {
+ this.reactMountReady.reset();
+ },
+
+ /**
+ * After DOM is flushed, invoke all registered `onDOMReady` callbacks.
+ */
+ close: function () {
+ this.reactMountReady.notifyAll();
+ }
+};
+
+/**
+ * Executed within the scope of the `Transaction` instance. Consider these as
+ * being member methods, but with an implied ordering while being isolated from
+ * each other.
+ */
+var TRANSACTION_WRAPPERS = [SELECTION_RESTORATION, EVENT_SUPPRESSION, ON_DOM_READY_QUEUEING];
+
+/**
+ * Currently:
+ * - The order that these are listed in the transaction is critical:
+ * - Suppresses events.
+ * - Restores selection range.
+ *
+ * Future:
+ * - Restore document/overflow scroll positions that were unintentionally
+ * modified via DOM insertions above the top viewport boundary.
+ * - Implement/integrate with customized constraint based layout system and keep
+ * track of which dimensions must be remeasured.
+ *
+ * @class ReactReconcileTransaction
+ */
+function ReactReconcileTransaction(forceHTML) {
+ this.reinitializeTransaction();
+ // Only server-side rendering really needs this option (see
+ // `ReactServerRendering`), but server-side uses
+ // `ReactServerRenderingTransaction` instead. This option is here so that it's
+ // accessible and defaults to false when `ReactDOMComponent` and
+ // `ReactTextComponent` checks it in `mountComponent`.`
+ this.renderToStaticMarkup = false;
+ this.reactMountReady = CallbackQueue.getPooled(null);
+ this.useCreateElement = !forceHTML && ReactDOMFeatureFlags.useCreateElement;
+}
+
+var Mixin = {
+ /**
+ * @see Transaction
+ * @abstract
+ * @final
+ * @return {array<object>} List of operation wrap procedures.
+ * TODO: convert to array<TransactionWrapper>
+ */
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ },
+
+ /**
+ * @return {object} The queue to collect `onDOMReady` callbacks with.
+ */
+ getReactMountReady: function () {
+ return this.reactMountReady;
+ },
+
+ /**
+ * `PooledClass` looks for this, and will invoke this before allowing this
+ * instance to be reused.
+ */
+ destructor: function () {
+ CallbackQueue.release(this.reactMountReady);
+ this.reactMountReady = null;
+ }
+};
+
+assign(ReactReconcileTransaction.prototype, Transaction.Mixin, Mixin);
+
+PooledClass.addPoolingTo(ReactReconcileTransaction);
+
+module.exports = ReactReconcileTransaction;
+},{"113":113,"24":24,"25":25,"28":28,"44":44,"6":6,"66":66}],84:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactReconciler
+ */
+
+'use strict';
+
+var ReactRef = _dereq_(85);
+
+/**
+ * Helper to call ReactRef.attachRefs with this composite component, split out
+ * to avoid allocations in the transaction mount-ready queue.
+ */
+function attachRefs() {
+ ReactRef.attachRefs(this, this._currentElement);
+}
+
+var ReactReconciler = {
+
+ /**
+ * Initializes the component, renders markup, and registers event listeners.
+ *
+ * @param {ReactComponent} internalInstance
+ * @param {string} rootID DOM ID of the root node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @return {?string} Rendered markup to be inserted into the DOM.
+ * @final
+ * @internal
+ */
+ mountComponent: function (internalInstance, rootID, transaction, context) {
+ var markup = internalInstance.mountComponent(rootID, transaction, context);
+ if (internalInstance._currentElement && internalInstance._currentElement.ref != null) {
+ transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
+ }
+ return markup;
+ },
+
+ /**
+ * Releases any resources allocated by `mountComponent`.
+ *
+ * @final
+ * @internal
+ */
+ unmountComponent: function (internalInstance) {
+ ReactRef.detachRefs(internalInstance, internalInstance._currentElement);
+ internalInstance.unmountComponent();
+ },
+
+ /**
+ * Update a component using a new element.
+ *
+ * @param {ReactComponent} internalInstance
+ * @param {ReactElement} nextElement
+ * @param {ReactReconcileTransaction} transaction
+ * @param {object} context
+ * @internal
+ */
+ receiveComponent: function (internalInstance, nextElement, transaction, context) {
+ var prevElement = internalInstance._currentElement;
+
+ if (nextElement === prevElement && context === internalInstance._context) {
+ // Since elements are immutable after the owner is rendered,
+ // we can do a cheap identity compare here to determine if this is a
+ // superfluous reconcile. It's possible for state to be mutable but such
+ // change should trigger an update of the owner which would recreate
+ // the element. We explicitly check for the existence of an owner since
+ // it's possible for an element created outside a composite to be
+ // deeply mutated and reused.
+
+ // TODO: Bailing out early is just a perf optimization right?
+ // TODO: Removing the return statement should affect correctness?
+ return;
+ }
+
+ var refsChanged = ReactRef.shouldUpdateRefs(prevElement, nextElement);
+
+ if (refsChanged) {
+ ReactRef.detachRefs(internalInstance, prevElement);
+ }
+
+ internalInstance.receiveComponent(nextElement, transaction, context);
+
+ if (refsChanged && internalInstance._currentElement && internalInstance._currentElement.ref != null) {
+ transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
+ }
+ },
+
+ /**
+ * Flush any dirty changes in a component.
+ *
+ * @param {ReactComponent} internalInstance
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ performUpdateIfNecessary: function (internalInstance, transaction) {
+ internalInstance.performUpdateIfNecessary(transaction);
+ }
+
+};
+
+module.exports = ReactReconciler;
+},{"85":85}],85:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactRef
+ */
+
+'use strict';
+
+var ReactOwner = _dereq_(77);
+
+var ReactRef = {};
+
+function attachRef(ref, component, owner) {
+ if (typeof ref === 'function') {
+ ref(component.getPublicInstance());
+ } else {
+ // Legacy ref
+ ReactOwner.addComponentAsRefTo(component, ref, owner);
+ }
+}
+
+function detachRef(ref, component, owner) {
+ if (typeof ref === 'function') {
+ ref(null);
+ } else {
+ // Legacy ref
+ ReactOwner.removeComponentAsRefFrom(component, ref, owner);
+ }
+}
+
+ReactRef.attachRefs = function (instance, element) {
+ if (element === null || element === false) {
+ return;
+ }
+ var ref = element.ref;
+ if (ref != null) {
+ attachRef(ref, instance, element._owner);
+ }
+};
+
+ReactRef.shouldUpdateRefs = function (prevElement, nextElement) {
+ // If either the owner or a `ref` has changed, make sure the newest owner
+ // has stored a reference to `this`, and the previous owner (if different)
+ // has forgotten the reference to `this`. We use the element instead
+ // of the public this.props because the post processing cannot determine
+ // a ref. The ref conceptually lives on the element.
+
+ // TODO: Should this even be possible? The owner cannot change because
+ // it's forbidden by shouldUpdateReactComponent. The ref can change
+ // if you swap the keys of but not the refs. Reconsider where this check
+ // is made. It probably belongs where the key checking and
+ // instantiateReactComponent is done.
+
+ var prevEmpty = prevElement === null || prevElement === false;
+ var nextEmpty = nextElement === null || nextElement === false;
+
+ return(
+ // This has a few false positives w/r/t empty components.
+ prevEmpty || nextEmpty || nextElement._owner !== prevElement._owner || nextElement.ref !== prevElement.ref
+ );
+};
+
+ReactRef.detachRefs = function (instance, element) {
+ if (element === null || element === false) {
+ return;
+ }
+ var ref = element.ref;
+ if (ref != null) {
+ detachRef(ref, instance, element._owner);
+ }
+};
+
+module.exports = ReactRef;
+},{"77":77}],86:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactRootIndex
+ * @typechecks
+ */
+
+'use strict';
+
+var ReactRootIndexInjection = {
+ /**
+ * @param {function} _createReactRootIndex
+ */
+ injectCreateReactRootIndex: function (_createReactRootIndex) {
+ ReactRootIndex.createReactRootIndex = _createReactRootIndex;
+ }
+};
+
+var ReactRootIndex = {
+ createReactRootIndex: null,
+ injection: ReactRootIndexInjection
+};
+
+module.exports = ReactRootIndex;
+},{}],87:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactServerBatchingStrategy
+ * @typechecks
+ */
+
+'use strict';
+
+var ReactServerBatchingStrategy = {
+ isBatchingUpdates: false,
+ batchedUpdates: function (callback) {
+ // Don't do anything here. During the server rendering we don't want to
+ // schedule any updates. We will simply ignore them.
+ }
+};
+
+module.exports = ReactServerBatchingStrategy;
+},{}],88:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks static-only
+ * @providesModule ReactServerRendering
+ */
+'use strict';
+
+var ReactDefaultBatchingStrategy = _dereq_(53);
+var ReactElement = _dereq_(57);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMarkupChecksum = _dereq_(71);
+var ReactServerBatchingStrategy = _dereq_(87);
+var ReactServerRenderingTransaction = _dereq_(89);
+var ReactUpdates = _dereq_(96);
+
+var emptyObject = _dereq_(154);
+var instantiateReactComponent = _dereq_(132);
+var invariant = _dereq_(161);
+
+/**
+ * @param {ReactElement} element
+ * @return {string} the HTML markup
+ */
+function renderToString(element) {
+ !ReactElement.isValidElement(element) ? "development" !== 'production' ? invariant(false, 'renderToString(): You must pass a valid ReactElement.') : invariant(false) : undefined;
+
+ var transaction;
+ try {
+ ReactUpdates.injection.injectBatchingStrategy(ReactServerBatchingStrategy);
+
+ var id = ReactInstanceHandles.createReactRootID();
+ transaction = ReactServerRenderingTransaction.getPooled(false);
+
+ return transaction.perform(function () {
+ var componentInstance = instantiateReactComponent(element, null);
+ var markup = componentInstance.mountComponent(id, transaction, emptyObject);
+ return ReactMarkupChecksum.addChecksumToMarkup(markup);
+ }, null);
+ } finally {
+ ReactServerRenderingTransaction.release(transaction);
+ // Revert to the DOM batching strategy since these two renderers
+ // currently share these stateful modules.
+ ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
+ }
+}
+
+/**
+ * @param {ReactElement} element
+ * @return {string} the HTML markup, without the extra React ID and checksum
+ * (for generating static pages)
+ */
+function renderToStaticMarkup(element) {
+ !ReactElement.isValidElement(element) ? "development" !== 'production' ? invariant(false, 'renderToStaticMarkup(): You must pass a valid ReactElement.') : invariant(false) : undefined;
+
+ var transaction;
+ try {
+ ReactUpdates.injection.injectBatchingStrategy(ReactServerBatchingStrategy);
+
+ var id = ReactInstanceHandles.createReactRootID();
+ transaction = ReactServerRenderingTransaction.getPooled(true);
+
+ return transaction.perform(function () {
+ var componentInstance = instantiateReactComponent(element, null);
+ return componentInstance.mountComponent(id, transaction, emptyObject);
+ }, null);
+ } finally {
+ ReactServerRenderingTransaction.release(transaction);
+ // Revert to the DOM batching strategy since these two renderers
+ // currently share these stateful modules.
+ ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
+ }
+}
+
+module.exports = {
+ renderToString: renderToString,
+ renderToStaticMarkup: renderToStaticMarkup
+};
+},{"132":132,"154":154,"161":161,"53":53,"57":57,"67":67,"71":71,"87":87,"89":89,"96":96}],89:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactServerRenderingTransaction
+ * @typechecks
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+var CallbackQueue = _dereq_(6);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+
+/**
+ * Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks
+ * during the performing of the transaction.
+ */
+var ON_DOM_READY_QUEUEING = {
+ /**
+ * Initializes the internal `onDOMReady` queue.
+ */
+ initialize: function () {
+ this.reactMountReady.reset();
+ },
+
+ close: emptyFunction
+};
+
+/**
+ * Executed within the scope of the `Transaction` instance. Consider these as
+ * being member methods, but with an implied ordering while being isolated from
+ * each other.
+ */
+var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING];
+
+/**
+ * @class ReactServerRenderingTransaction
+ * @param {boolean} renderToStaticMarkup
+ */
+function ReactServerRenderingTransaction(renderToStaticMarkup) {
+ this.reinitializeTransaction();
+ this.renderToStaticMarkup = renderToStaticMarkup;
+ this.reactMountReady = CallbackQueue.getPooled(null);
+ this.useCreateElement = false;
+}
+
+var Mixin = {
+ /**
+ * @see Transaction
+ * @abstract
+ * @final
+ * @return {array} Empty list of operation wrap procedures.
+ */
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ },
+
+ /**
+ * @return {object} The queue to collect `onDOMReady` callbacks with.
+ */
+ getReactMountReady: function () {
+ return this.reactMountReady;
+ },
+
+ /**
+ * `PooledClass` looks for this, and will invoke this before allowing this
+ * instance to be reused.
+ */
+ destructor: function () {
+ CallbackQueue.release(this.reactMountReady);
+ this.reactMountReady = null;
+ }
+};
+
+assign(ReactServerRenderingTransaction.prototype, Transaction.Mixin, Mixin);
+
+PooledClass.addPoolingTo(ReactServerRenderingTransaction);
+
+module.exports = ReactServerRenderingTransaction;
+},{"113":113,"153":153,"24":24,"25":25,"6":6}],90:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactStateSetters
+ */
+
+'use strict';
+
+var ReactStateSetters = {
+ /**
+ * Returns a function that calls the provided function, and uses the result
+ * of that to set the component's state.
+ *
+ * @param {ReactCompositeComponent} component
+ * @param {function} funcReturningState Returned callback uses this to
+ * determine how to update state.
+ * @return {function} callback that when invoked uses funcReturningState to
+ * determined the object literal to setState.
+ */
+ createStateSetter: function (component, funcReturningState) {
+ return function (a, b, c, d, e, f) {
+ var partialState = funcReturningState.call(component, a, b, c, d, e, f);
+ if (partialState) {
+ component.setState(partialState);
+ }
+ };
+ },
+
+ /**
+ * Returns a single-argument callback that can be used to update a single
+ * key in the component's state.
+ *
+ * Note: this is memoized function, which makes it inexpensive to call.
+ *
+ * @param {ReactCompositeComponent} component
+ * @param {string} key The key in the state that you should update.
+ * @return {function} callback of 1 argument which calls setState() with
+ * the provided keyName and callback argument.
+ */
+ createStateKeySetter: function (component, key) {
+ // Memoize the setters.
+ var cache = component.__keySetters || (component.__keySetters = {});
+ return cache[key] || (cache[key] = createStateKeySetter(component, key));
+ }
+};
+
+function createStateKeySetter(component, key) {
+ // Partial state is allocated outside of the function closure so it can be
+ // reused with every call, avoiding memory allocation when this function
+ // is called.
+ var partialState = {};
+ return function stateKeySetter(value) {
+ partialState[key] = value;
+ component.setState(partialState);
+ };
+}
+
+ReactStateSetters.Mixin = {
+ /**
+ * Returns a function that calls the provided function, and uses the result
+ * of that to set the component's state.
+ *
+ * For example, these statements are equivalent:
+ *
+ * this.setState({x: 1});
+ * this.createStateSetter(function(xValue) {
+ * return {x: xValue};
+ * })(1);
+ *
+ * @param {function} funcReturningState Returned callback uses this to
+ * determine how to update state.
+ * @return {function} callback that when invoked uses funcReturningState to
+ * determined the object literal to setState.
+ */
+ createStateSetter: function (funcReturningState) {
+ return ReactStateSetters.createStateSetter(this, funcReturningState);
+ },
+
+ /**
+ * Returns a single-argument callback that can be used to update a single
+ * key in the component's state.
+ *
+ * For example, these statements are equivalent:
+ *
+ * this.setState({x: 1});
+ * this.createStateKeySetter('x')(1);
+ *
+ * Note: this is memoized function, which makes it inexpensive to call.
+ *
+ * @param {string} key The key in the state that you should update.
+ * @return {function} callback of 1 argument which calls setState() with
+ * the provided keyName and callback argument.
+ */
+ createStateKeySetter: function (key) {
+ return ReactStateSetters.createStateKeySetter(this, key);
+ }
+};
+
+module.exports = ReactStateSetters;
+},{}],91:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactTestUtils
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+var EventPropagators = _dereq_(19);
+var React = _dereq_(26);
+var ReactDOM = _dereq_(40);
+var ReactElement = _dereq_(57);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactCompositeComponent = _dereq_(38);
+var ReactInstanceHandles = _dereq_(67);
+var ReactInstanceMap = _dereq_(68);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+var SyntheticEvent = _dereq_(105);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var findDOMNode = _dereq_(122);
+var invariant = _dereq_(161);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+function Event(suffix) {}
+
+/**
+ * @class ReactTestUtils
+ */
+
+function findAllInRenderedTreeInternal(inst, test) {
+ if (!inst || !inst.getPublicInstance) {
+ return [];
+ }
+ var publicInst = inst.getPublicInstance();
+ var ret = test(publicInst) ? [publicInst] : [];
+ var currentElement = inst._currentElement;
+ if (ReactTestUtils.isDOMComponent(publicInst)) {
+ var renderedChildren = inst._renderedChildren;
+ var key;
+ for (key in renderedChildren) {
+ if (!renderedChildren.hasOwnProperty(key)) {
+ continue;
+ }
+ ret = ret.concat(findAllInRenderedTreeInternal(renderedChildren[key], test));
+ }
+ } else if (ReactElement.isValidElement(currentElement) && typeof currentElement.type === 'function') {
+ ret = ret.concat(findAllInRenderedTreeInternal(inst._renderedComponent, test));
+ }
+ return ret;
+}
+
+/**
+ * Todo: Support the entire DOM.scry query syntax. For now, these simple
+ * utilities will suffice for testing purposes.
+ * @lends ReactTestUtils
+ */
+var ReactTestUtils = {
+ renderIntoDocument: function (instance) {
+ var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ // None of our tests actually require attaching the container to the
+ // DOM, and doing so creates a mess that we rely on test isolation to
+ // clean up, so we're going to stop honoring the name of this method
+ // (and probably rename it eventually) if no problems arise.
+ // document.documentElement.appendChild(div);
+ return ReactDOM.render(instance, div);
+ },
+
+ isElement: function (element) {
+ return ReactElement.isValidElement(element);
+ },
+
+ isElementOfType: function (inst, convenienceConstructor) {
+ return ReactElement.isValidElement(inst) && inst.type === convenienceConstructor;
+ },
+
+ isDOMComponent: function (inst) {
+ return !!(inst && inst.nodeType === 1 && inst.tagName);
+ },
+
+ isDOMComponentElement: function (inst) {
+ return !!(inst && ReactElement.isValidElement(inst) && !!inst.tagName);
+ },
+
+ isCompositeComponent: function (inst) {
+ if (ReactTestUtils.isDOMComponent(inst)) {
+ // Accessing inst.setState warns; just return false as that'll be what
+ // this returns when we have DOM nodes as refs directly
+ return false;
+ }
+ return inst != null && typeof inst.render === 'function' && typeof inst.setState === 'function';
+ },
+
+ isCompositeComponentWithType: function (inst, type) {
+ if (!ReactTestUtils.isCompositeComponent(inst)) {
+ return false;
+ }
+ var internalInstance = ReactInstanceMap.get(inst);
+ var constructor = internalInstance._currentElement.type;
+
+ return constructor === type;
+ },
+
+ isCompositeComponentElement: function (inst) {
+ if (!ReactElement.isValidElement(inst)) {
+ return false;
+ }
+ // We check the prototype of the type that will get mounted, not the
+ // instance itself. This is a future proof way of duck typing.
+ var prototype = inst.type.prototype;
+ return typeof prototype.render === 'function' && typeof prototype.setState === 'function';
+ },
+
+ isCompositeComponentElementWithType: function (inst, type) {
+ var internalInstance = ReactInstanceMap.get(inst);
+ var constructor = internalInstance._currentElement.type;
+
+ return !!(ReactTestUtils.isCompositeComponentElement(inst) && constructor === type);
+ },
+
+ getRenderedChildOfCompositeComponent: function (inst) {
+ if (!ReactTestUtils.isCompositeComponent(inst)) {
+ return null;
+ }
+ var internalInstance = ReactInstanceMap.get(inst);
+ return internalInstance._renderedComponent.getPublicInstance();
+ },
+
+ findAllInRenderedTree: function (inst, test) {
+ if (!inst) {
+ return [];
+ }
+ !ReactTestUtils.isCompositeComponent(inst) ? "development" !== 'production' ? invariant(false, 'findAllInRenderedTree(...): instance must be a composite component') : invariant(false) : undefined;
+ return findAllInRenderedTreeInternal(ReactInstanceMap.get(inst), test);
+ },
+
+ /**
+ * Finds all instance of components in the rendered tree that are DOM
+ * components with the class name matching `className`.
+ * @return {array} an array of all the matches.
+ */
+ scryRenderedDOMComponentsWithClass: function (root, classNames) {
+ if (!Array.isArray(classNames)) {
+ classNames = classNames.split(/\s+/);
+ }
+ return ReactTestUtils.findAllInRenderedTree(root, function (inst) {
+ if (ReactTestUtils.isDOMComponent(inst)) {
+ var className = inst.className;
+ if (typeof className !== 'string') {
+ // SVG, probably.
+ className = inst.getAttribute('class') || '';
+ }
+ var classList = className.split(/\s+/);
+ return classNames.every(function (name) {
+ return classList.indexOf(name) !== -1;
+ });
+ }
+ return false;
+ });
+ },
+
+ /**
+ * Like scryRenderedDOMComponentsWithClass but expects there to be one result,
+ * and returns that one result, or throws exception if there is any other
+ * number of matches besides one.
+ * @return {!ReactDOMComponent} The one match.
+ */
+ findRenderedDOMComponentWithClass: function (root, className) {
+ var all = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, className);
+ if (all.length !== 1) {
+ throw new Error('Did not find exactly one match ' + '(found: ' + all.length + ') for class:' + className);
+ }
+ return all[0];
+ },
+
+ /**
+ * Finds all instance of components in the rendered tree that are DOM
+ * components with the tag name matching `tagName`.
+ * @return {array} an array of all the matches.
+ */
+ scryRenderedDOMComponentsWithTag: function (root, tagName) {
+ return ReactTestUtils.findAllInRenderedTree(root, function (inst) {
+ return ReactTestUtils.isDOMComponent(inst) && inst.tagName.toUpperCase() === tagName.toUpperCase();
+ });
+ },
+
+ /**
+ * Like scryRenderedDOMComponentsWithTag but expects there to be one result,
+ * and returns that one result, or throws exception if there is any other
+ * number of matches besides one.
+ * @return {!ReactDOMComponent} The one match.
+ */
+ findRenderedDOMComponentWithTag: function (root, tagName) {
+ var all = ReactTestUtils.scryRenderedDOMComponentsWithTag(root, tagName);
+ if (all.length !== 1) {
+ throw new Error('Did not find exactly one match for tag:' + tagName);
+ }
+ return all[0];
+ },
+
+ /**
+ * Finds all instances of components with type equal to `componentType`.
+ * @return {array} an array of all the matches.
+ */
+ scryRenderedComponentsWithType: function (root, componentType) {
+ return ReactTestUtils.findAllInRenderedTree(root, function (inst) {
+ return ReactTestUtils.isCompositeComponentWithType(inst, componentType);
+ });
+ },
+
+ /**
+ * Same as `scryRenderedComponentsWithType` but expects there to be one result
+ * and returns that one result, or throws exception if there is any other
+ * number of matches besides one.
+ * @return {!ReactComponent} The one match.
+ */
+ findRenderedComponentWithType: function (root, componentType) {
+ var all = ReactTestUtils.scryRenderedComponentsWithType(root, componentType);
+ if (all.length !== 1) {
+ throw new Error('Did not find exactly one match for componentType:' + componentType + ' (found ' + all.length + ')');
+ }
+ return all[0];
+ },
+
+ /**
+ * Pass a mocked component module to this method to augment it with
+ * useful methods that allow it to be used as a dummy React component.
+ * Instead of rendering as usual, the component will become a simple
+ * <div> containing any provided children.
+ *
+ * @param {object} module the mock function object exported from a
+ * module that defines the component to be mocked
+ * @param {?string} mockTagName optional dummy root tag name to return
+ * from render method (overrides
+ * module.mockTagName if provided)
+ * @return {object} the ReactTestUtils object (for chaining)
+ */
+ mockComponent: function (module, mockTagName) {
+ mockTagName = mockTagName || module.mockTagName || 'div';
+
+ module.prototype.render.mockImplementation(function () {
+ return React.createElement(mockTagName, null, this.props.children);
+ });
+
+ return this;
+ },
+
+ /**
+ * Simulates a top level event being dispatched from a raw event that occurred
+ * on an `Element` node.
+ * @param {Object} topLevelType A type from `EventConstants.topLevelTypes`
+ * @param {!Element} node The dom to simulate an event occurring on.
+ * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent.
+ */
+ simulateNativeEventOnNode: function (topLevelType, node, fakeNativeEvent) {
+ fakeNativeEvent.target = node;
+ ReactBrowserEventEmitter.ReactEventListener.dispatchEvent(topLevelType, fakeNativeEvent);
+ },
+
+ /**
+ * Simulates a top level event being dispatched from a raw event that occurred
+ * on the `ReactDOMComponent` `comp`.
+ * @param {Object} topLevelType A type from `EventConstants.topLevelTypes`.
+ * @param {!ReactDOMComponent} comp
+ * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent.
+ */
+ simulateNativeEventOnDOMComponent: function (topLevelType, comp, fakeNativeEvent) {
+ ReactTestUtils.simulateNativeEventOnNode(topLevelType, findDOMNode(comp), fakeNativeEvent);
+ },
+
+ nativeTouchData: function (x, y) {
+ return {
+ touches: [{ pageX: x, pageY: y }]
+ };
+ },
+
+ createRenderer: function () {
+ return new ReactShallowRenderer();
+ },
+
+ Simulate: null,
+ SimulateNative: {}
+};
+
+/**
+ * @class ReactShallowRenderer
+ */
+var ReactShallowRenderer = function () {
+ this._instance = null;
+};
+
+ReactShallowRenderer.prototype.getRenderOutput = function () {
+ return this._instance && this._instance._renderedComponent && this._instance._renderedComponent._renderedOutput || null;
+};
+
+var NoopInternalComponent = function (element) {
+ this._renderedOutput = element;
+ this._currentElement = element;
+};
+
+NoopInternalComponent.prototype = {
+
+ mountComponent: function () {},
+
+ receiveComponent: function (element) {
+ this._renderedOutput = element;
+ this._currentElement = element;
+ },
+
+ unmountComponent: function () {},
+
+ getPublicInstance: function () {
+ return null;
+ }
+};
+
+var ShallowComponentWrapper = function () {};
+assign(ShallowComponentWrapper.prototype, ReactCompositeComponent.Mixin, {
+ _instantiateReactComponent: function (element) {
+ return new NoopInternalComponent(element);
+ },
+ _replaceNodeWithMarkupByID: function () {},
+ _renderValidatedComponent: ReactCompositeComponent.Mixin._renderValidatedComponentWithoutOwnerOrContext
+});
+
+ReactShallowRenderer.prototype.render = function (element, context) {
+ !ReactElement.isValidElement(element) ? "development" !== 'production' ? invariant(false, 'ReactShallowRenderer render(): Invalid component element.%s', typeof element === 'function' ? ' Instead of passing a component class, make sure to instantiate ' + 'it by passing it to React.createElement.' : '') : invariant(false) : undefined;
+ !(typeof element.type !== 'string') ? "development" !== 'production' ? invariant(false, 'ReactShallowRenderer render(): Shallow rendering works only with custom ' + 'components, not primitives (%s). Instead of calling `.render(el)` and ' + 'inspecting the rendered output, look at `el.props` directly instead.', element.type) : invariant(false) : undefined;
+
+ if (!context) {
+ context = emptyObject;
+ }
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(false);
+ this._render(element, transaction, context);
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
+};
+
+ReactShallowRenderer.prototype.unmount = function () {
+ if (this._instance) {
+ this._instance.unmountComponent();
+ }
+};
+
+ReactShallowRenderer.prototype._render = function (element, transaction, context) {
+ if (this._instance) {
+ this._instance.receiveComponent(element, transaction, context);
+ } else {
+ var rootID = ReactInstanceHandles.createReactRootID();
+ var instance = new ShallowComponentWrapper(element.type);
+ instance.construct(element);
+
+ instance.mountComponent(rootID, transaction, context);
+
+ this._instance = instance;
+ }
+};
+
+/**
+ * Exports:
+ *
+ * - `ReactTestUtils.Simulate.click(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.Simulate.mouseMove(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.Simulate.change(Element/ReactDOMComponent)`
+ * - ... (All keys from event plugin `eventTypes` objects)
+ */
+function makeSimulator(eventType) {
+ return function (domComponentOrNode, eventData) {
+ var node;
+ if (ReactTestUtils.isDOMComponent(domComponentOrNode)) {
+ node = findDOMNode(domComponentOrNode);
+ } else if (domComponentOrNode.tagName) {
+ node = domComponentOrNode;
+ }
+
+ var dispatchConfig = ReactBrowserEventEmitter.eventNameDispatchConfigs[eventType];
+
+ var fakeNativeEvent = new Event();
+ fakeNativeEvent.target = node;
+ // We don't use SyntheticEvent.getPooled in order to not have to worry about
+ // properly destroying any properties assigned from `eventData` upon release
+ var event = new SyntheticEvent(dispatchConfig, ReactMount.getID(node), fakeNativeEvent, node);
+ assign(event, eventData);
+
+ if (dispatchConfig.phasedRegistrationNames) {
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ } else {
+ EventPropagators.accumulateDirectDispatches(event);
+ }
+
+ ReactUpdates.batchedUpdates(function () {
+ EventPluginHub.enqueueEvents(event);
+ EventPluginHub.processEventQueue(true);
+ });
+ };
+}
+
+function buildSimulators() {
+ ReactTestUtils.Simulate = {};
+
+ var eventType;
+ for (eventType in ReactBrowserEventEmitter.eventNameDispatchConfigs) {
+ /**
+ * @param {!Element|ReactDOMComponent} domComponentOrNode
+ * @param {?object} eventData Fake event data to use in SyntheticEvent.
+ */
+ ReactTestUtils.Simulate[eventType] = makeSimulator(eventType);
+ }
+}
+
+// Rebuild ReactTestUtils.Simulate whenever event plugins are injected
+var oldInjectEventPluginOrder = EventPluginHub.injection.injectEventPluginOrder;
+EventPluginHub.injection.injectEventPluginOrder = function () {
+ oldInjectEventPluginOrder.apply(this, arguments);
+ buildSimulators();
+};
+var oldInjectEventPlugins = EventPluginHub.injection.injectEventPluginsByName;
+EventPluginHub.injection.injectEventPluginsByName = function () {
+ oldInjectEventPlugins.apply(this, arguments);
+ buildSimulators();
+};
+
+buildSimulators();
+
+/**
+ * Exports:
+ *
+ * - `ReactTestUtils.SimulateNative.click(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.SimulateNative.mouseMove(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.SimulateNative.mouseIn/ReactDOMComponent)`
+ * - `ReactTestUtils.SimulateNative.mouseOut(Element/ReactDOMComponent)`
+ * - ... (All keys from `EventConstants.topLevelTypes`)
+ *
+ * Note: Top level event types are a subset of the entire set of handler types
+ * (which include a broader set of "synthetic" events). For example, onDragDone
+ * is a synthetic event. Except when testing an event plugin or React's event
+ * handling code specifically, you probably want to use ReactTestUtils.Simulate
+ * to dispatch synthetic events.
+ */
+
+function makeNativeSimulator(eventType) {
+ return function (domComponentOrNode, nativeEventData) {
+ var fakeNativeEvent = new Event(eventType);
+ assign(fakeNativeEvent, nativeEventData);
+ if (ReactTestUtils.isDOMComponent(domComponentOrNode)) {
+ ReactTestUtils.simulateNativeEventOnDOMComponent(eventType, domComponentOrNode, fakeNativeEvent);
+ } else if (domComponentOrNode.tagName) {
+ // Will allow on actual dom nodes.
+ ReactTestUtils.simulateNativeEventOnNode(eventType, domComponentOrNode, fakeNativeEvent);
+ }
+ };
+}
+
+Object.keys(topLevelTypes).forEach(function (eventType) {
+ // Event type is stored as 'topClick' - we transform that to 'click'
+ var convenienceName = eventType.indexOf('top') === 0 ? eventType.charAt(3).toLowerCase() + eventType.substr(4) : eventType;
+ /**
+ * @param {!Element|ReactDOMComponent} domComponentOrNode
+ * @param {?Event} nativeEventData Fake native event to use in SyntheticEvent.
+ */
+ ReactTestUtils.SimulateNative[convenienceName] = makeNativeSimulator(eventType);
+});
+
+module.exports = ReactTestUtils;
+},{"105":105,"122":122,"15":15,"154":154,"16":16,"161":161,"19":19,"24":24,"26":26,"28":28,"38":38,"40":40,"57":57,"67":67,"68":68,"72":72,"96":96}],92:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks static-only
+ * @providesModule ReactTransitionChildMapping
+ */
+
+'use strict';
+
+var flattenChildren = _dereq_(123);
+
+var ReactTransitionChildMapping = {
+ /**
+ * Given `this.props.children`, return an object mapping key to child. Just
+ * simple syntactic sugar around flattenChildren().
+ *
+ * @param {*} children `this.props.children`
+ * @return {object} Mapping of key to child
+ */
+ getChildMapping: function (children) {
+ if (!children) {
+ return children;
+ }
+ return flattenChildren(children);
+ },
+
+ /**
+ * When you're adding or removing children some may be added or removed in the
+ * same render pass. We want to show *both* since we want to simultaneously
+ * animate elements in and out. This function takes a previous set of keys
+ * and a new set of keys and merges them with its best guess of the correct
+ * ordering. In the future we may expose some of the utilities in
+ * ReactMultiChild to make this easy, but for now React itself does not
+ * directly have this concept of the union of prevChildren and nextChildren
+ * so we implement it here.
+ *
+ * @param {object} prev prev children as returned from
+ * `ReactTransitionChildMapping.getChildMapping()`.
+ * @param {object} next next children as returned from
+ * `ReactTransitionChildMapping.getChildMapping()`.
+ * @return {object} a key set that contains all keys in `prev` and all keys
+ * in `next` in a reasonable order.
+ */
+ mergeChildMappings: function (prev, next) {
+ prev = prev || {};
+ next = next || {};
+
+ function getValueForKey(key) {
+ if (next.hasOwnProperty(key)) {
+ return next[key];
+ } else {
+ return prev[key];
+ }
+ }
+
+ // For each key of `next`, the list of keys to insert before that key in
+ // the combined list
+ var nextKeysPending = {};
+
+ var pendingKeys = [];
+ for (var prevKey in prev) {
+ if (next.hasOwnProperty(prevKey)) {
+ if (pendingKeys.length) {
+ nextKeysPending[prevKey] = pendingKeys;
+ pendingKeys = [];
+ }
+ } else {
+ pendingKeys.push(prevKey);
+ }
+ }
+
+ var i;
+ var childMapping = {};
+ for (var nextKey in next) {
+ if (nextKeysPending.hasOwnProperty(nextKey)) {
+ for (i = 0; i < nextKeysPending[nextKey].length; i++) {
+ var pendingNextKey = nextKeysPending[nextKey][i];
+ childMapping[nextKeysPending[nextKey][i]] = getValueForKey(pendingNextKey);
+ }
+ }
+ childMapping[nextKey] = getValueForKey(nextKey);
+ }
+
+ // Finally, add the keys which didn't appear before any key in `next`
+ for (i = 0; i < pendingKeys.length; i++) {
+ childMapping[pendingKeys[i]] = getValueForKey(pendingKeys[i]);
+ }
+
+ return childMapping;
+ }
+};
+
+module.exports = ReactTransitionChildMapping;
+},{"123":123}],93:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactTransitionEvents
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+/**
+ * EVENT_NAME_MAP is used to determine which event fired when a
+ * transition/animation ends, based on the style property used to
+ * define that event.
+ */
+var EVENT_NAME_MAP = {
+ transitionend: {
+ 'transition': 'transitionend',
+ 'WebkitTransition': 'webkitTransitionEnd',
+ 'MozTransition': 'mozTransitionEnd',
+ 'OTransition': 'oTransitionEnd',
+ 'msTransition': 'MSTransitionEnd'
+ },
+
+ animationend: {
+ 'animation': 'animationend',
+ 'WebkitAnimation': 'webkitAnimationEnd',
+ 'MozAnimation': 'mozAnimationEnd',
+ 'OAnimation': 'oAnimationEnd',
+ 'msAnimation': 'MSAnimationEnd'
+ }
+};
+
+var endEvents = [];
+
+function detectEvents() {
+ var testEl = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var style = testEl.style;
+
+ // On some platforms, in particular some releases of Android 4.x,
+ // the un-prefixed "animation" and "transition" properties are defined on the
+ // style object but the events that fire will still be prefixed, so we need
+ // to check if the un-prefixed events are useable, and if not remove them
+ // from the map
+ if (!('AnimationEvent' in window)) {
+ delete EVENT_NAME_MAP.animationend.animation;
+ }
+
+ if (!('TransitionEvent' in window)) {
+ delete EVENT_NAME_MAP.transitionend.transition;
+ }
+
+ for (var baseEventName in EVENT_NAME_MAP) {
+ var baseEvents = EVENT_NAME_MAP[baseEventName];
+ for (var styleName in baseEvents) {
+ if (styleName in style) {
+ endEvents.push(baseEvents[styleName]);
+ break;
+ }
+ }
+ }
+}
+
+if (ExecutionEnvironment.canUseDOM) {
+ detectEvents();
+}
+
+// We use the raw {add|remove}EventListener() call because EventListener
+// does not know how to remove event listeners and we really should
+// clean up. Also, these events are not triggered in older browsers
+// so we should be A-OK here.
+
+function addEventListener(node, eventName, eventListener) {
+ node.addEventListener(eventName, eventListener, false);
+}
+
+function removeEventListener(node, eventName, eventListener) {
+ node.removeEventListener(eventName, eventListener, false);
+}
+
+var ReactTransitionEvents = {
+ addEndEventListener: function (node, eventListener) {
+ if (endEvents.length === 0) {
+ // If CSS transitions are not supported, trigger an "end animation"
+ // event immediately.
+ window.setTimeout(eventListener, 0);
+ return;
+ }
+ endEvents.forEach(function (endEvent) {
+ addEventListener(node, endEvent, eventListener);
+ });
+ },
+
+ removeEndEventListener: function (node, eventListener) {
+ if (endEvents.length === 0) {
+ return;
+ }
+ endEvents.forEach(function (endEvent) {
+ removeEventListener(node, endEvent, eventListener);
+ });
+ }
+};
+
+module.exports = ReactTransitionEvents;
+},{"147":147}],94:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactTransitionGroup
+ */
+
+'use strict';
+
+var React = _dereq_(26);
+var ReactTransitionChildMapping = _dereq_(92);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+
+var ReactTransitionGroup = React.createClass({
+ displayName: 'ReactTransitionGroup',
+
+ propTypes: {
+ component: React.PropTypes.any,
+ childFactory: React.PropTypes.func
+ },
+
+ getDefaultProps: function () {
+ return {
+ component: 'span',
+ childFactory: emptyFunction.thatReturnsArgument
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ children: ReactTransitionChildMapping.getChildMapping(this.props.children)
+ };
+ },
+
+ componentWillMount: function () {
+ this.currentlyTransitioningKeys = {};
+ this.keysToEnter = [];
+ this.keysToLeave = [];
+ },
+
+ componentDidMount: function () {
+ var initialChildMapping = this.state.children;
+ for (var key in initialChildMapping) {
+ if (initialChildMapping[key]) {
+ this.performAppear(key);
+ }
+ }
+ },
+
+ componentWillReceiveProps: function (nextProps) {
+ var nextChildMapping = ReactTransitionChildMapping.getChildMapping(nextProps.children);
+ var prevChildMapping = this.state.children;
+
+ this.setState({
+ children: ReactTransitionChildMapping.mergeChildMappings(prevChildMapping, nextChildMapping)
+ });
+
+ var key;
+
+ for (key in nextChildMapping) {
+ var hasPrev = prevChildMapping && prevChildMapping.hasOwnProperty(key);
+ if (nextChildMapping[key] && !hasPrev && !this.currentlyTransitioningKeys[key]) {
+ this.keysToEnter.push(key);
+ }
+ }
+
+ for (key in prevChildMapping) {
+ var hasNext = nextChildMapping && nextChildMapping.hasOwnProperty(key);
+ if (prevChildMapping[key] && !hasNext && !this.currentlyTransitioningKeys[key]) {
+ this.keysToLeave.push(key);
+ }
+ }
+
+ // If we want to someday check for reordering, we could do it here.
+ },
+
+ componentDidUpdate: function () {
+ var keysToEnter = this.keysToEnter;
+ this.keysToEnter = [];
+ keysToEnter.forEach(this.performEnter);
+
+ var keysToLeave = this.keysToLeave;
+ this.keysToLeave = [];
+ keysToLeave.forEach(this.performLeave);
+ },
+
+ performAppear: function (key) {
+ this.currentlyTransitioningKeys[key] = true;
+
+ var component = this.refs[key];
+
+ if (component.componentWillAppear) {
+ component.componentWillAppear(this._handleDoneAppearing.bind(this, key));
+ } else {
+ this._handleDoneAppearing(key);
+ }
+ },
+
+ _handleDoneAppearing: function (key) {
+ var component = this.refs[key];
+ if (component.componentDidAppear) {
+ component.componentDidAppear();
+ }
+
+ delete this.currentlyTransitioningKeys[key];
+
+ var currentChildMapping = ReactTransitionChildMapping.getChildMapping(this.props.children);
+
+ if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {
+ // This was removed before it had fully appeared. Remove it.
+ this.performLeave(key);
+ }
+ },
+
+ performEnter: function (key) {
+ this.currentlyTransitioningKeys[key] = true;
+
+ var component = this.refs[key];
+
+ if (component.componentWillEnter) {
+ component.componentWillEnter(this._handleDoneEntering.bind(this, key));
+ } else {
+ this._handleDoneEntering(key);
+ }
+ },
+
+ _handleDoneEntering: function (key) {
+ var component = this.refs[key];
+ if (component.componentDidEnter) {
+ component.componentDidEnter();
+ }
+
+ delete this.currentlyTransitioningKeys[key];
+
+ var currentChildMapping = ReactTransitionChildMapping.getChildMapping(this.props.children);
+
+ if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {
+ // This was removed before it had fully entered. Remove it.
+ this.performLeave(key);
+ }
+ },
+
+ performLeave: function (key) {
+ this.currentlyTransitioningKeys[key] = true;
+
+ var component = this.refs[key];
+ if (component.componentWillLeave) {
+ component.componentWillLeave(this._handleDoneLeaving.bind(this, key));
+ } else {
+ // Note that this is somewhat dangerous b/c it calls setState()
+ // again, effectively mutating the component before all the work
+ // is done.
+ this._handleDoneLeaving(key);
+ }
+ },
+
+ _handleDoneLeaving: function (key) {
+ var component = this.refs[key];
+
+ if (component.componentDidLeave) {
+ component.componentDidLeave();
+ }
+
+ delete this.currentlyTransitioningKeys[key];
+
+ var currentChildMapping = ReactTransitionChildMapping.getChildMapping(this.props.children);
+
+ if (currentChildMapping && currentChildMapping.hasOwnProperty(key)) {
+ // This entered again before it fully left. Add it again.
+ this.performEnter(key);
+ } else {
+ this.setState(function (state) {
+ var newChildren = assign({}, state.children);
+ delete newChildren[key];
+ return { children: newChildren };
+ });
+ }
+ },
+
+ render: function () {
+ // TODO: we could get rid of the need for the wrapper node
+ // by cloning a single child
+ var childrenToRender = [];
+ for (var key in this.state.children) {
+ var child = this.state.children[key];
+ if (child) {
+ // You may need to apply reactive updates to a child as it is leaving.
+ // The normal React way to do it won't work since the child will have
+ // already been removed. In case you need this behavior you can provide
+ // a childFactory function to wrap every child, even the ones that are
+ // leaving.
+ childrenToRender.push(React.cloneElement(this.props.childFactory(child), { ref: key, key: key }));
+ }
+ }
+ return React.createElement(this.props.component, this.props, childrenToRender);
+ }
+});
+
+module.exports = ReactTransitionGroup;
+},{"153":153,"24":24,"26":26,"92":92}],95:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactUpdateQueue
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactElement = _dereq_(57);
+var ReactInstanceMap = _dereq_(68);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+function enqueueUpdate(internalInstance) {
+ ReactUpdates.enqueueUpdate(internalInstance);
+}
+
+function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
+ var internalInstance = ReactInstanceMap.get(publicInstance);
+ if (!internalInstance) {
+ if ("development" !== 'production') {
+ // Only warn when we have a callerName. Otherwise we should be silent.
+ // We're probably calling from enqueueCallback. We don't want to warn
+ // there because we already warned for the corresponding lifecycle method.
+ "development" !== 'production' ? warning(!callerName, '%s(...): Can only update a mounted or mounting component. ' + 'This usually means you called %s() on an unmounted component. ' + 'This is a no-op. Please check the code for the %s component.', callerName, callerName, publicInstance.constructor.displayName) : undefined;
+ }
+ return null;
+ }
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(ReactCurrentOwner.current == null, '%s(...): Cannot update during an existing state transition ' + '(such as within `render`). Render methods should be a pure function ' + 'of props and state.', callerName) : undefined;
+ }
+
+ return internalInstance;
+}
+
+/**
+ * ReactUpdateQueue allows for state updates to be scheduled into a later
+ * reconciliation step.
+ */
+var ReactUpdateQueue = {
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @param {ReactClass} publicInstance The instance we want to test.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function (publicInstance) {
+ if ("development" !== 'production') {
+ var owner = ReactCurrentOwner.current;
+ if (owner !== null) {
+ "development" !== 'production' ? warning(owner._warnedAboutRefsInRender, '%s is accessing isMounted inside its render() function. ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + 'componentDidUpdate instead.', owner.getName() || 'A component') : undefined;
+ owner._warnedAboutRefsInRender = true;
+ }
+ }
+ var internalInstance = ReactInstanceMap.get(publicInstance);
+ if (internalInstance) {
+ // During componentWillMount and render this will still be null but after
+ // that will always render to something. At least for now. So we can use
+ // this hack.
+ return !!internalInstance._renderedComponent;
+ } else {
+ return false;
+ }
+ },
+
+ /**
+ * Enqueue a callback that will be executed after all the pending updates
+ * have processed.
+ *
+ * @param {ReactClass} publicInstance The instance to use as `this` context.
+ * @param {?function} callback Called after state is updated.
+ * @internal
+ */
+ enqueueCallback: function (publicInstance, callback) {
+ !(typeof callback === 'function') ? "development" !== 'production' ? invariant(false, 'enqueueCallback(...): You called `setProps`, `replaceProps`, ' + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + 'isn\'t callable.') : invariant(false) : undefined;
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
+
+ // Previously we would throw an error if we didn't have an internal
+ // instance. Since we want to make it a no-op instead, we mirror the same
+ // behavior we have in other enqueue* methods.
+ // We also need to ignore callbacks in componentWillMount. See
+ // enqueueUpdates.
+ if (!internalInstance) {
+ return null;
+ }
+
+ if (internalInstance._pendingCallbacks) {
+ internalInstance._pendingCallbacks.push(callback);
+ } else {
+ internalInstance._pendingCallbacks = [callback];
+ }
+ // TODO: The callback here is ignored when setState is called from
+ // componentWillMount. Either fix it or disallow doing so completely in
+ // favor of getInitialState. Alternatively, we can disallow
+ // componentWillMount during server-side rendering.
+ enqueueUpdate(internalInstance);
+ },
+
+ enqueueCallbackInternal: function (internalInstance, callback) {
+ !(typeof callback === 'function') ? "development" !== 'production' ? invariant(false, 'enqueueCallback(...): You called `setProps`, `replaceProps`, ' + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + 'isn\'t callable.') : invariant(false) : undefined;
+ if (internalInstance._pendingCallbacks) {
+ internalInstance._pendingCallbacks.push(callback);
+ } else {
+ internalInstance._pendingCallbacks = [callback];
+ }
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldComponentUpdate`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @internal
+ */
+ enqueueForceUpdate: function (publicInstance) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'forceUpdate');
+
+ if (!internalInstance) {
+ return;
+ }
+
+ internalInstance._pendingForceUpdate = true;
+
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Replaces all of the state. Always use this or `setState` to mutate state.
+ * You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} completeState Next state.
+ * @internal
+ */
+ enqueueReplaceState: function (publicInstance, completeState) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'replaceState');
+
+ if (!internalInstance) {
+ return;
+ }
+
+ internalInstance._pendingStateQueue = [completeState];
+ internalInstance._pendingReplaceState = true;
+
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Sets a subset of the state. This only exists because _pendingState is
+ * internal. This provides a merging strategy that is not available to deep
+ * properties which is confusing. TODO: Expose pendingState or don't use it
+ * during the merge.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialState Next partial state to be merged with state.
+ * @internal
+ */
+ enqueueSetState: function (publicInstance, partialState) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
+
+ if (!internalInstance) {
+ return;
+ }
+
+ var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
+ queue.push(partialState);
+
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialProps Subset of the next props.
+ * @internal
+ */
+ enqueueSetProps: function (publicInstance, partialProps) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setProps');
+ if (!internalInstance) {
+ return;
+ }
+ ReactUpdateQueue.enqueueSetPropsInternal(internalInstance, partialProps);
+ },
+
+ enqueueSetPropsInternal: function (internalInstance, partialProps) {
+ var topLevelWrapper = internalInstance._topLevelWrapper;
+ !topLevelWrapper ? "development" !== 'production' ? invariant(false, 'setProps(...): You called `setProps` on a ' + 'component with a parent. This is an anti-pattern since props will ' + 'get reactively updated when rendered. Instead, change the owner\'s ' + '`render` method to pass the correct value as props to the component ' + 'where it is created.') : invariant(false) : undefined;
+
+ // Merge with the pending element if it exists, otherwise with existing
+ // element props.
+ var wrapElement = topLevelWrapper._pendingElement || topLevelWrapper._currentElement;
+ var element = wrapElement.props;
+ var props = assign({}, element.props, partialProps);
+ topLevelWrapper._pendingElement = ReactElement.cloneAndReplaceProps(wrapElement, ReactElement.cloneAndReplaceProps(element, props));
+
+ enqueueUpdate(topLevelWrapper);
+ },
+
+ /**
+ * Replaces all of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} props New props.
+ * @internal
+ */
+ enqueueReplaceProps: function (publicInstance, props) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'replaceProps');
+ if (!internalInstance) {
+ return;
+ }
+ ReactUpdateQueue.enqueueReplacePropsInternal(internalInstance, props);
+ },
+
+ enqueueReplacePropsInternal: function (internalInstance, props) {
+ var topLevelWrapper = internalInstance._topLevelWrapper;
+ !topLevelWrapper ? "development" !== 'production' ? invariant(false, 'replaceProps(...): You called `replaceProps` on a ' + 'component with a parent. This is an anti-pattern since props will ' + 'get reactively updated when rendered. Instead, change the owner\'s ' + '`render` method to pass the correct value as props to the component ' + 'where it is created.') : invariant(false) : undefined;
+
+ // Merge with the pending element if it exists, otherwise with existing
+ // element props.
+ var wrapElement = topLevelWrapper._pendingElement || topLevelWrapper._currentElement;
+ var element = wrapElement.props;
+ topLevelWrapper._pendingElement = ReactElement.cloneAndReplaceProps(wrapElement, ReactElement.cloneAndReplaceProps(element, props));
+
+ enqueueUpdate(topLevelWrapper);
+ },
+
+ enqueueElementInternal: function (internalInstance, newElement) {
+ internalInstance._pendingElement = newElement;
+ enqueueUpdate(internalInstance);
+ }
+
+};
+
+module.exports = ReactUpdateQueue;
+},{"161":161,"173":173,"24":24,"39":39,"57":57,"68":68,"96":96}],96:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactUpdates
+ */
+
+'use strict';
+
+var CallbackQueue = _dereq_(6);
+var PooledClass = _dereq_(25);
+var ReactPerf = _dereq_(78);
+var ReactReconciler = _dereq_(84);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+var dirtyComponents = [];
+var asapCallbackQueue = CallbackQueue.getPooled();
+var asapEnqueued = false;
+
+var batchingStrategy = null;
+
+function ensureInjected() {
+ !(ReactUpdates.ReactReconcileTransaction && batchingStrategy) ? "development" !== 'production' ? invariant(false, 'ReactUpdates: must inject a reconcile transaction class and batching ' + 'strategy') : invariant(false) : undefined;
+}
+
+var NESTED_UPDATES = {
+ initialize: function () {
+ this.dirtyComponentsLength = dirtyComponents.length;
+ },
+ close: function () {
+ if (this.dirtyComponentsLength !== dirtyComponents.length) {
+ // Additional updates were enqueued by componentDidUpdate handlers or
+ // similar; before our own UPDATE_QUEUEING wrapper closes, we want to run
+ // these new updates so that if A's componentDidUpdate calls setState on
+ // B, B will update before the callback A's updater provided when calling
+ // setState.
+ dirtyComponents.splice(0, this.dirtyComponentsLength);
+ flushBatchedUpdates();
+ } else {
+ dirtyComponents.length = 0;
+ }
+ }
+};
+
+var UPDATE_QUEUEING = {
+ initialize: function () {
+ this.callbackQueue.reset();
+ },
+ close: function () {
+ this.callbackQueue.notifyAll();
+ }
+};
+
+var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];
+
+function ReactUpdatesFlushTransaction() {
+ this.reinitializeTransaction();
+ this.dirtyComponentsLength = null;
+ this.callbackQueue = CallbackQueue.getPooled();
+ this.reconcileTransaction = ReactUpdates.ReactReconcileTransaction.getPooled( /* forceHTML */false);
+}
+
+assign(ReactUpdatesFlushTransaction.prototype, Transaction.Mixin, {
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ },
+
+ destructor: function () {
+ this.dirtyComponentsLength = null;
+ CallbackQueue.release(this.callbackQueue);
+ this.callbackQueue = null;
+ ReactUpdates.ReactReconcileTransaction.release(this.reconcileTransaction);
+ this.reconcileTransaction = null;
+ },
+
+ perform: function (method, scope, a) {
+ // Essentially calls `this.reconcileTransaction.perform(method, scope, a)`
+ // with this transaction's wrappers around it.
+ return Transaction.Mixin.perform.call(this, this.reconcileTransaction.perform, this.reconcileTransaction, method, scope, a);
+ }
+});
+
+PooledClass.addPoolingTo(ReactUpdatesFlushTransaction);
+
+function batchedUpdates(callback, a, b, c, d, e) {
+ ensureInjected();
+ batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
+}
+
+/**
+ * Array comparator for ReactComponents by mount ordering.
+ *
+ * @param {ReactComponent} c1 first component you're comparing
+ * @param {ReactComponent} c2 second component you're comparing
+ * @return {number} Return value usable by Array.prototype.sort().
+ */
+function mountOrderComparator(c1, c2) {
+ return c1._mountOrder - c2._mountOrder;
+}
+
+function runBatchedUpdates(transaction) {
+ var len = transaction.dirtyComponentsLength;
+ !(len === dirtyComponents.length) ? "development" !== 'production' ? invariant(false, 'Expected flush transaction\'s stored dirty-components length (%s) to ' + 'match dirty-components array length (%s).', len, dirtyComponents.length) : invariant(false) : undefined;
+
+ // Since reconciling a component higher in the owner hierarchy usually (not
+ // always -- see shouldComponentUpdate()) will reconcile children, reconcile
+ // them before their children by sorting the array.
+ dirtyComponents.sort(mountOrderComparator);
+
+ for (var i = 0; i < len; i++) {
+ // If a component is unmounted before pending changes apply, it will still
+ // be here, but we assume that it has cleared its _pendingCallbacks and
+ // that performUpdateIfNecessary is a noop.
+ var component = dirtyComponents[i];
+
+ // If performUpdateIfNecessary happens to enqueue any new updates, we
+ // shouldn't execute the callbacks until the next render happens, so
+ // stash the callbacks first
+ var callbacks = component._pendingCallbacks;
+ component._pendingCallbacks = null;
+
+ ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);
+
+ if (callbacks) {
+ for (var j = 0; j < callbacks.length; j++) {
+ transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
+ }
+ }
+ }
+}
+
+var flushBatchedUpdates = function () {
+ // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
+ // array and perform any updates enqueued by mount-ready handlers (i.e.,
+ // componentDidUpdate) but we need to check here too in order to catch
+ // updates enqueued by setState callbacks and asap calls.
+ while (dirtyComponents.length || asapEnqueued) {
+ if (dirtyComponents.length) {
+ var transaction = ReactUpdatesFlushTransaction.getPooled();
+ transaction.perform(runBatchedUpdates, null, transaction);
+ ReactUpdatesFlushTransaction.release(transaction);
+ }
+
+ if (asapEnqueued) {
+ asapEnqueued = false;
+ var queue = asapCallbackQueue;
+ asapCallbackQueue = CallbackQueue.getPooled();
+ queue.notifyAll();
+ CallbackQueue.release(queue);
+ }
+ }
+};
+flushBatchedUpdates = ReactPerf.measure('ReactUpdates', 'flushBatchedUpdates', flushBatchedUpdates);
+
+/**
+ * Mark a component as needing a rerender, adding an optional callback to a
+ * list of functions which will be executed once the rerender occurs.
+ */
+function enqueueUpdate(component) {
+ ensureInjected();
+
+ // Various parts of our code (such as ReactCompositeComponent's
+ // _renderValidatedComponent) assume that calls to render aren't nested;
+ // verify that that's the case. (This is called by each top-level update
+ // function, like setProps, setState, forceUpdate, etc.; creation and
+ // destruction of top-level components is guarded in ReactMount.)
+
+ if (!batchingStrategy.isBatchingUpdates) {
+ batchingStrategy.batchedUpdates(enqueueUpdate, component);
+ return;
+ }
+
+ dirtyComponents.push(component);
+}
+
+/**
+ * Enqueue a callback to be run at the end of the current batching cycle. Throws
+ * if no updates are currently being performed.
+ */
+function asap(callback, context) {
+ !batchingStrategy.isBatchingUpdates ? "development" !== 'production' ? invariant(false, 'ReactUpdates.asap: Can\'t enqueue an asap callback in a context where' + 'updates are not being batched.') : invariant(false) : undefined;
+ asapCallbackQueue.enqueue(callback, context);
+ asapEnqueued = true;
+}
+
+var ReactUpdatesInjection = {
+ injectReconcileTransaction: function (ReconcileTransaction) {
+ !ReconcileTransaction ? "development" !== 'production' ? invariant(false, 'ReactUpdates: must provide a reconcile transaction class') : invariant(false) : undefined;
+ ReactUpdates.ReactReconcileTransaction = ReconcileTransaction;
+ },
+
+ injectBatchingStrategy: function (_batchingStrategy) {
+ !_batchingStrategy ? "development" !== 'production' ? invariant(false, 'ReactUpdates: must provide a batching strategy') : invariant(false) : undefined;
+ !(typeof _batchingStrategy.batchedUpdates === 'function') ? "development" !== 'production' ? invariant(false, 'ReactUpdates: must provide a batchedUpdates() function') : invariant(false) : undefined;
+ !(typeof _batchingStrategy.isBatchingUpdates === 'boolean') ? "development" !== 'production' ? invariant(false, 'ReactUpdates: must provide an isBatchingUpdates boolean attribute') : invariant(false) : undefined;
+ batchingStrategy = _batchingStrategy;
+ }
+};
+
+var ReactUpdates = {
+ /**
+ * React references `ReactReconcileTransaction` using this property in order
+ * to allow dependency injection.
+ *
+ * @internal
+ */
+ ReactReconcileTransaction: null,
+
+ batchedUpdates: batchedUpdates,
+ enqueueUpdate: enqueueUpdate,
+ flushBatchedUpdates: flushBatchedUpdates,
+ injection: ReactUpdatesInjection,
+ asap: asap
+};
+
+module.exports = ReactUpdates;
+},{"113":113,"161":161,"24":24,"25":25,"6":6,"78":78,"84":84}],97:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactVersion
+ */
+
+'use strict';
+
+module.exports = '0.14.6';
+},{}],98:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SVGDOMPropertyConfig
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+
+var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
+
+var NS = {
+ xlink: 'http://www.w3.org/1999/xlink',
+ xml: 'http://www.w3.org/XML/1998/namespace'
+};
+
+var SVGDOMPropertyConfig = {
+ Properties: {
+ clipPath: MUST_USE_ATTRIBUTE,
+ cx: MUST_USE_ATTRIBUTE,
+ cy: MUST_USE_ATTRIBUTE,
+ d: MUST_USE_ATTRIBUTE,
+ dx: MUST_USE_ATTRIBUTE,
+ dy: MUST_USE_ATTRIBUTE,
+ fill: MUST_USE_ATTRIBUTE,
+ fillOpacity: MUST_USE_ATTRIBUTE,
+ fontFamily: MUST_USE_ATTRIBUTE,
+ fontSize: MUST_USE_ATTRIBUTE,
+ fx: MUST_USE_ATTRIBUTE,
+ fy: MUST_USE_ATTRIBUTE,
+ gradientTransform: MUST_USE_ATTRIBUTE,
+ gradientUnits: MUST_USE_ATTRIBUTE,
+ markerEnd: MUST_USE_ATTRIBUTE,
+ markerMid: MUST_USE_ATTRIBUTE,
+ markerStart: MUST_USE_ATTRIBUTE,
+ offset: MUST_USE_ATTRIBUTE,
+ opacity: MUST_USE_ATTRIBUTE,
+ patternContentUnits: MUST_USE_ATTRIBUTE,
+ patternUnits: MUST_USE_ATTRIBUTE,
+ points: MUST_USE_ATTRIBUTE,
+ preserveAspectRatio: MUST_USE_ATTRIBUTE,
+ r: MUST_USE_ATTRIBUTE,
+ rx: MUST_USE_ATTRIBUTE,
+ ry: MUST_USE_ATTRIBUTE,
+ spreadMethod: MUST_USE_ATTRIBUTE,
+ stopColor: MUST_USE_ATTRIBUTE,
+ stopOpacity: MUST_USE_ATTRIBUTE,
+ stroke: MUST_USE_ATTRIBUTE,
+ strokeDasharray: MUST_USE_ATTRIBUTE,
+ strokeLinecap: MUST_USE_ATTRIBUTE,
+ strokeOpacity: MUST_USE_ATTRIBUTE,
+ strokeWidth: MUST_USE_ATTRIBUTE,
+ textAnchor: MUST_USE_ATTRIBUTE,
+ transform: MUST_USE_ATTRIBUTE,
+ version: MUST_USE_ATTRIBUTE,
+ viewBox: MUST_USE_ATTRIBUTE,
+ x1: MUST_USE_ATTRIBUTE,
+ x2: MUST_USE_ATTRIBUTE,
+ x: MUST_USE_ATTRIBUTE,
+ xlinkActuate: MUST_USE_ATTRIBUTE,
+ xlinkArcrole: MUST_USE_ATTRIBUTE,
+ xlinkHref: MUST_USE_ATTRIBUTE,
+ xlinkRole: MUST_USE_ATTRIBUTE,
+ xlinkShow: MUST_USE_ATTRIBUTE,
+ xlinkTitle: MUST_USE_ATTRIBUTE,
+ xlinkType: MUST_USE_ATTRIBUTE,
+ xmlBase: MUST_USE_ATTRIBUTE,
+ xmlLang: MUST_USE_ATTRIBUTE,
+ xmlSpace: MUST_USE_ATTRIBUTE,
+ y1: MUST_USE_ATTRIBUTE,
+ y2: MUST_USE_ATTRIBUTE,
+ y: MUST_USE_ATTRIBUTE
+ },
+ DOMAttributeNamespaces: {
+ xlinkActuate: NS.xlink,
+ xlinkArcrole: NS.xlink,
+ xlinkHref: NS.xlink,
+ xlinkRole: NS.xlink,
+ xlinkShow: NS.xlink,
+ xlinkTitle: NS.xlink,
+ xlinkType: NS.xlink,
+ xmlBase: NS.xml,
+ xmlLang: NS.xml,
+ xmlSpace: NS.xml
+ },
+ DOMAttributeNames: {
+ clipPath: 'clip-path',
+ fillOpacity: 'fill-opacity',
+ fontFamily: 'font-family',
+ fontSize: 'font-size',
+ gradientTransform: 'gradientTransform',
+ gradientUnits: 'gradientUnits',
+ markerEnd: 'marker-end',
+ markerMid: 'marker-mid',
+ markerStart: 'marker-start',
+ patternContentUnits: 'patternContentUnits',
+ patternUnits: 'patternUnits',
+ preserveAspectRatio: 'preserveAspectRatio',
+ spreadMethod: 'spreadMethod',
+ stopColor: 'stop-color',
+ stopOpacity: 'stop-opacity',
+ strokeDasharray: 'stroke-dasharray',
+ strokeLinecap: 'stroke-linecap',
+ strokeOpacity: 'stroke-opacity',
+ strokeWidth: 'stroke-width',
+ textAnchor: 'text-anchor',
+ viewBox: 'viewBox',
+ xlinkActuate: 'xlink:actuate',
+ xlinkArcrole: 'xlink:arcrole',
+ xlinkHref: 'xlink:href',
+ xlinkRole: 'xlink:role',
+ xlinkShow: 'xlink:show',
+ xlinkTitle: 'xlink:title',
+ xlinkType: 'xlink:type',
+ xmlBase: 'xml:base',
+ xmlLang: 'xml:lang',
+ xmlSpace: 'xml:space'
+ }
+};
+
+module.exports = SVGDOMPropertyConfig;
+},{"10":10}],99:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SelectEventPlugin
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPropagators = _dereq_(19);
+var ExecutionEnvironment = _dereq_(147);
+var ReactInputSelection = _dereq_(66);
+var SyntheticEvent = _dereq_(105);
+
+var getActiveElement = _dereq_(156);
+var isTextInputElement = _dereq_(134);
+var keyOf = _dereq_(166);
+var shallowEqual = _dereq_(171);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+var skipSelectionChangeEvent = ExecutionEnvironment.canUseDOM && 'documentMode' in document && document.documentMode <= 11;
+
+var eventTypes = {
+ select: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSelect: null }),
+ captured: keyOf({ onSelectCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topContextMenu, topLevelTypes.topFocus, topLevelTypes.topKeyDown, topLevelTypes.topMouseDown, topLevelTypes.topMouseUp, topLevelTypes.topSelectionChange]
+ }
+};
+
+var activeElement = null;
+var activeElementID = null;
+var lastSelection = null;
+var mouseDown = false;
+
+// Track whether a listener exists for this plugin. If none exist, we do
+// not extract events.
+var hasListener = false;
+var ON_SELECT_KEY = keyOf({ onSelect: null });
+
+/**
+ * Get an object which is a unique representation of the current selection.
+ *
+ * The return value will not be consistent across nodes or browsers, but
+ * two identical selections on the same node will return identical objects.
+ *
+ * @param {DOMElement} node
+ * @return {object}
+ */
+function getSelection(node) {
+ if ('selectionStart' in node && ReactInputSelection.hasSelectionCapabilities(node)) {
+ return {
+ start: node.selectionStart,
+ end: node.selectionEnd
+ };
+ } else if (window.getSelection) {
+ var selection = window.getSelection();
+ return {
+ anchorNode: selection.anchorNode,
+ anchorOffset: selection.anchorOffset,
+ focusNode: selection.focusNode,
+ focusOffset: selection.focusOffset
+ };
+ } else if (document.selection) {
+ var range = document.selection.createRange();
+ return {
+ parentElement: range.parentElement(),
+ text: range.text,
+ top: range.boundingTop,
+ left: range.boundingLeft
+ };
+ }
+}
+
+/**
+ * Poll selection to see whether it's changed.
+ *
+ * @param {object} nativeEvent
+ * @return {?SyntheticEvent}
+ */
+function constructSelectEvent(nativeEvent, nativeEventTarget) {
+ // Ensure we have the right element, and that the user is not dragging a
+ // selection (this matches native `select` event behavior). In HTML5, select
+ // fires only on input and textarea thus if there's no focused element we
+ // won't dispatch.
+ if (mouseDown || activeElement == null || activeElement !== getActiveElement()) {
+ return null;
+ }
+
+ // Only fire when selection has actually changed.
+ var currentSelection = getSelection(activeElement);
+ if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {
+ lastSelection = currentSelection;
+
+ var syntheticEvent = SyntheticEvent.getPooled(eventTypes.select, activeElementID, nativeEvent, nativeEventTarget);
+
+ syntheticEvent.type = 'select';
+ syntheticEvent.target = activeElement;
+
+ EventPropagators.accumulateTwoPhaseDispatches(syntheticEvent);
+
+ return syntheticEvent;
+ }
+
+ return null;
+}
+
+/**
+ * This plugin creates an `onSelect` event that normalizes select events
+ * across form elements.
+ *
+ * Supported elements are:
+ * - input (see `isTextInputElement`)
+ * - textarea
+ * - contentEditable
+ *
+ * This differs from native browser implementations in the following ways:
+ * - Fires on contentEditable fields as well as inputs.
+ * - Fires for collapsed selection.
+ * - Fires after user input.
+ */
+var SelectEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ if (!hasListener) {
+ return null;
+ }
+
+ switch (topLevelType) {
+ // Track the input node that has focus.
+ case topLevelTypes.topFocus:
+ if (isTextInputElement(topLevelTarget) || topLevelTarget.contentEditable === 'true') {
+ activeElement = topLevelTarget;
+ activeElementID = topLevelTargetID;
+ lastSelection = null;
+ }
+ break;
+ case topLevelTypes.topBlur:
+ activeElement = null;
+ activeElementID = null;
+ lastSelection = null;
+ break;
+
+ // Don't fire the event while the user is dragging. This matches the
+ // semantics of the native select event.
+ case topLevelTypes.topMouseDown:
+ mouseDown = true;
+ break;
+ case topLevelTypes.topContextMenu:
+ case topLevelTypes.topMouseUp:
+ mouseDown = false;
+ return constructSelectEvent(nativeEvent, nativeEventTarget);
+
+ // Chrome and IE fire non-standard event when selection is changed (and
+ // sometimes when it hasn't). IE's event fires out of order with respect
+ // to key and input events on deletion, so we discard it.
+ //
+ // Firefox doesn't support selectionchange, so check selection status
+ // after each key entry. The selection changes after keydown and before
+ // keyup, but we check on keydown as well in the case of holding down a
+ // key, when multiple keydown events are fired but only one keyup is.
+ // This is also our approach for IE handling, for the reason above.
+ case topLevelTypes.topSelectionChange:
+ if (skipSelectionChangeEvent) {
+ break;
+ }
+ // falls through
+ case topLevelTypes.topKeyDown:
+ case topLevelTypes.topKeyUp:
+ return constructSelectEvent(nativeEvent, nativeEventTarget);
+ }
+
+ return null;
+ },
+
+ didPutListener: function (id, registrationName, listener) {
+ if (registrationName === ON_SELECT_KEY) {
+ hasListener = true;
+ }
+ }
+};
+
+module.exports = SelectEventPlugin;
+},{"105":105,"134":134,"147":147,"15":15,"156":156,"166":166,"171":171,"19":19,"66":66}],100:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ServerReactRootIndex
+ * @typechecks
+ */
+
+'use strict';
+
+/**
+ * Size of the reactRoot ID space. We generate random numbers for React root
+ * IDs and if there's a collision the events and DOM update system will
+ * get confused. In the future we need a way to generate GUIDs but for
+ * now this will work on a smaller scale.
+ */
+var GLOBAL_MOUNT_POINT_MAX = Math.pow(2, 53);
+
+var ServerReactRootIndex = {
+ createReactRootIndex: function () {
+ return Math.ceil(Math.random() * GLOBAL_MOUNT_POINT_MAX);
+ }
+};
+
+module.exports = ServerReactRootIndex;
+},{}],101:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SimpleEventPlugin
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventListener = _dereq_(146);
+var EventPropagators = _dereq_(19);
+var ReactMount = _dereq_(72);
+var SyntheticClipboardEvent = _dereq_(102);
+var SyntheticEvent = _dereq_(105);
+var SyntheticFocusEvent = _dereq_(106);
+var SyntheticKeyboardEvent = _dereq_(108);
+var SyntheticMouseEvent = _dereq_(109);
+var SyntheticDragEvent = _dereq_(104);
+var SyntheticTouchEvent = _dereq_(110);
+var SyntheticUIEvent = _dereq_(111);
+var SyntheticWheelEvent = _dereq_(112);
+
+var emptyFunction = _dereq_(153);
+var getEventCharCode = _dereq_(125);
+var invariant = _dereq_(161);
+var keyOf = _dereq_(166);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+var eventTypes = {
+ abort: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onAbort: true }),
+ captured: keyOf({ onAbortCapture: true })
+ }
+ },
+ blur: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onBlur: true }),
+ captured: keyOf({ onBlurCapture: true })
+ }
+ },
+ canPlay: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCanPlay: true }),
+ captured: keyOf({ onCanPlayCapture: true })
+ }
+ },
+ canPlayThrough: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCanPlayThrough: true }),
+ captured: keyOf({ onCanPlayThroughCapture: true })
+ }
+ },
+ click: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onClick: true }),
+ captured: keyOf({ onClickCapture: true })
+ }
+ },
+ contextMenu: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onContextMenu: true }),
+ captured: keyOf({ onContextMenuCapture: true })
+ }
+ },
+ copy: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCopy: true }),
+ captured: keyOf({ onCopyCapture: true })
+ }
+ },
+ cut: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCut: true }),
+ captured: keyOf({ onCutCapture: true })
+ }
+ },
+ doubleClick: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDoubleClick: true }),
+ captured: keyOf({ onDoubleClickCapture: true })
+ }
+ },
+ drag: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDrag: true }),
+ captured: keyOf({ onDragCapture: true })
+ }
+ },
+ dragEnd: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragEnd: true }),
+ captured: keyOf({ onDragEndCapture: true })
+ }
+ },
+ dragEnter: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragEnter: true }),
+ captured: keyOf({ onDragEnterCapture: true })
+ }
+ },
+ dragExit: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragExit: true }),
+ captured: keyOf({ onDragExitCapture: true })
+ }
+ },
+ dragLeave: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragLeave: true }),
+ captured: keyOf({ onDragLeaveCapture: true })
+ }
+ },
+ dragOver: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragOver: true }),
+ captured: keyOf({ onDragOverCapture: true })
+ }
+ },
+ dragStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragStart: true }),
+ captured: keyOf({ onDragStartCapture: true })
+ }
+ },
+ drop: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDrop: true }),
+ captured: keyOf({ onDropCapture: true })
+ }
+ },
+ durationChange: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDurationChange: true }),
+ captured: keyOf({ onDurationChangeCapture: true })
+ }
+ },
+ emptied: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onEmptied: true }),
+ captured: keyOf({ onEmptiedCapture: true })
+ }
+ },
+ encrypted: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onEncrypted: true }),
+ captured: keyOf({ onEncryptedCapture: true })
+ }
+ },
+ ended: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onEnded: true }),
+ captured: keyOf({ onEndedCapture: true })
+ }
+ },
+ error: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onError: true }),
+ captured: keyOf({ onErrorCapture: true })
+ }
+ },
+ focus: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onFocus: true }),
+ captured: keyOf({ onFocusCapture: true })
+ }
+ },
+ input: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onInput: true }),
+ captured: keyOf({ onInputCapture: true })
+ }
+ },
+ keyDown: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onKeyDown: true }),
+ captured: keyOf({ onKeyDownCapture: true })
+ }
+ },
+ keyPress: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onKeyPress: true }),
+ captured: keyOf({ onKeyPressCapture: true })
+ }
+ },
+ keyUp: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onKeyUp: true }),
+ captured: keyOf({ onKeyUpCapture: true })
+ }
+ },
+ load: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoad: true }),
+ captured: keyOf({ onLoadCapture: true })
+ }
+ },
+ loadedData: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoadedData: true }),
+ captured: keyOf({ onLoadedDataCapture: true })
+ }
+ },
+ loadedMetadata: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoadedMetadata: true }),
+ captured: keyOf({ onLoadedMetadataCapture: true })
+ }
+ },
+ loadStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoadStart: true }),
+ captured: keyOf({ onLoadStartCapture: true })
+ }
+ },
+ // Note: We do not allow listening to mouseOver events. Instead, use the
+ // onMouseEnter/onMouseLeave created by `EnterLeaveEventPlugin`.
+ mouseDown: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseDown: true }),
+ captured: keyOf({ onMouseDownCapture: true })
+ }
+ },
+ mouseMove: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseMove: true }),
+ captured: keyOf({ onMouseMoveCapture: true })
+ }
+ },
+ mouseOut: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseOut: true }),
+ captured: keyOf({ onMouseOutCapture: true })
+ }
+ },
+ mouseOver: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseOver: true }),
+ captured: keyOf({ onMouseOverCapture: true })
+ }
+ },
+ mouseUp: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseUp: true }),
+ captured: keyOf({ onMouseUpCapture: true })
+ }
+ },
+ paste: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPaste: true }),
+ captured: keyOf({ onPasteCapture: true })
+ }
+ },
+ pause: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPause: true }),
+ captured: keyOf({ onPauseCapture: true })
+ }
+ },
+ play: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPlay: true }),
+ captured: keyOf({ onPlayCapture: true })
+ }
+ },
+ playing: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPlaying: true }),
+ captured: keyOf({ onPlayingCapture: true })
+ }
+ },
+ progress: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onProgress: true }),
+ captured: keyOf({ onProgressCapture: true })
+ }
+ },
+ rateChange: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onRateChange: true }),
+ captured: keyOf({ onRateChangeCapture: true })
+ }
+ },
+ reset: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onReset: true }),
+ captured: keyOf({ onResetCapture: true })
+ }
+ },
+ scroll: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onScroll: true }),
+ captured: keyOf({ onScrollCapture: true })
+ }
+ },
+ seeked: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSeeked: true }),
+ captured: keyOf({ onSeekedCapture: true })
+ }
+ },
+ seeking: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSeeking: true }),
+ captured: keyOf({ onSeekingCapture: true })
+ }
+ },
+ stalled: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onStalled: true }),
+ captured: keyOf({ onStalledCapture: true })
+ }
+ },
+ submit: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSubmit: true }),
+ captured: keyOf({ onSubmitCapture: true })
+ }
+ },
+ suspend: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSuspend: true }),
+ captured: keyOf({ onSuspendCapture: true })
+ }
+ },
+ timeUpdate: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTimeUpdate: true }),
+ captured: keyOf({ onTimeUpdateCapture: true })
+ }
+ },
+ touchCancel: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchCancel: true }),
+ captured: keyOf({ onTouchCancelCapture: true })
+ }
+ },
+ touchEnd: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchEnd: true }),
+ captured: keyOf({ onTouchEndCapture: true })
+ }
+ },
+ touchMove: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchMove: true }),
+ captured: keyOf({ onTouchMoveCapture: true })
+ }
+ },
+ touchStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchStart: true }),
+ captured: keyOf({ onTouchStartCapture: true })
+ }
+ },
+ volumeChange: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onVolumeChange: true }),
+ captured: keyOf({ onVolumeChangeCapture: true })
+ }
+ },
+ waiting: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onWaiting: true }),
+ captured: keyOf({ onWaitingCapture: true })
+ }
+ },
+ wheel: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onWheel: true }),
+ captured: keyOf({ onWheelCapture: true })
+ }
+ }
+};
+
+var topLevelEventsToDispatchConfig = {
+ topAbort: eventTypes.abort,
+ topBlur: eventTypes.blur,
+ topCanPlay: eventTypes.canPlay,
+ topCanPlayThrough: eventTypes.canPlayThrough,
+ topClick: eventTypes.click,
+ topContextMenu: eventTypes.contextMenu,
+ topCopy: eventTypes.copy,
+ topCut: eventTypes.cut,
+ topDoubleClick: eventTypes.doubleClick,
+ topDrag: eventTypes.drag,
+ topDragEnd: eventTypes.dragEnd,
+ topDragEnter: eventTypes.dragEnter,
+ topDragExit: eventTypes.dragExit,
+ topDragLeave: eventTypes.dragLeave,
+ topDragOver: eventTypes.dragOver,
+ topDragStart: eventTypes.dragStart,
+ topDrop: eventTypes.drop,
+ topDurationChange: eventTypes.durationChange,
+ topEmptied: eventTypes.emptied,
+ topEncrypted: eventTypes.encrypted,
+ topEnded: eventTypes.ended,
+ topError: eventTypes.error,
+ topFocus: eventTypes.focus,
+ topInput: eventTypes.input,
+ topKeyDown: eventTypes.keyDown,
+ topKeyPress: eventTypes.keyPress,
+ topKeyUp: eventTypes.keyUp,
+ topLoad: eventTypes.load,
+ topLoadedData: eventTypes.loadedData,
+ topLoadedMetadata: eventTypes.loadedMetadata,
+ topLoadStart: eventTypes.loadStart,
+ topMouseDown: eventTypes.mouseDown,
+ topMouseMove: eventTypes.mouseMove,
+ topMouseOut: eventTypes.mouseOut,
+ topMouseOver: eventTypes.mouseOver,
+ topMouseUp: eventTypes.mouseUp,
+ topPaste: eventTypes.paste,
+ topPause: eventTypes.pause,
+ topPlay: eventTypes.play,
+ topPlaying: eventTypes.playing,
+ topProgress: eventTypes.progress,
+ topRateChange: eventTypes.rateChange,
+ topReset: eventTypes.reset,
+ topScroll: eventTypes.scroll,
+ topSeeked: eventTypes.seeked,
+ topSeeking: eventTypes.seeking,
+ topStalled: eventTypes.stalled,
+ topSubmit: eventTypes.submit,
+ topSuspend: eventTypes.suspend,
+ topTimeUpdate: eventTypes.timeUpdate,
+ topTouchCancel: eventTypes.touchCancel,
+ topTouchEnd: eventTypes.touchEnd,
+ topTouchMove: eventTypes.touchMove,
+ topTouchStart: eventTypes.touchStart,
+ topVolumeChange: eventTypes.volumeChange,
+ topWaiting: eventTypes.waiting,
+ topWheel: eventTypes.wheel
+};
+
+for (var type in topLevelEventsToDispatchConfig) {
+ topLevelEventsToDispatchConfig[type].dependencies = [type];
+}
+
+var ON_CLICK_KEY = keyOf({ onClick: null });
+var onClickListeners = {};
+
+var SimpleEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
+ if (!dispatchConfig) {
+ return null;
+ }
+ var EventConstructor;
+ switch (topLevelType) {
+ case topLevelTypes.topAbort:
+ case topLevelTypes.topCanPlay:
+ case topLevelTypes.topCanPlayThrough:
+ case topLevelTypes.topDurationChange:
+ case topLevelTypes.topEmptied:
+ case topLevelTypes.topEncrypted:
+ case topLevelTypes.topEnded:
+ case topLevelTypes.topError:
+ case topLevelTypes.topInput:
+ case topLevelTypes.topLoad:
+ case topLevelTypes.topLoadedData:
+ case topLevelTypes.topLoadedMetadata:
+ case topLevelTypes.topLoadStart:
+ case topLevelTypes.topPause:
+ case topLevelTypes.topPlay:
+ case topLevelTypes.topPlaying:
+ case topLevelTypes.topProgress:
+ case topLevelTypes.topRateChange:
+ case topLevelTypes.topReset:
+ case topLevelTypes.topSeeked:
+ case topLevelTypes.topSeeking:
+ case topLevelTypes.topStalled:
+ case topLevelTypes.topSubmit:
+ case topLevelTypes.topSuspend:
+ case topLevelTypes.topTimeUpdate:
+ case topLevelTypes.topVolumeChange:
+ case topLevelTypes.topWaiting:
+ // HTML Events
+ // @see http://www.w3.org/TR/html5/index.html#events-0
+ EventConstructor = SyntheticEvent;
+ break;
+ case topLevelTypes.topKeyPress:
+ // FireFox creates a keypress event for function keys too. This removes
+ // the unwanted keypress events. Enter is however both printable and
+ // non-printable. One would expect Tab to be as well (but it isn't).
+ if (getEventCharCode(nativeEvent) === 0) {
+ return null;
+ }
+ /* falls through */
+ case topLevelTypes.topKeyDown:
+ case topLevelTypes.topKeyUp:
+ EventConstructor = SyntheticKeyboardEvent;
+ break;
+ case topLevelTypes.topBlur:
+ case topLevelTypes.topFocus:
+ EventConstructor = SyntheticFocusEvent;
+ break;
+ case topLevelTypes.topClick:
+ // Firefox creates a click event on right mouse clicks. This removes the
+ // unwanted click events.
+ if (nativeEvent.button === 2) {
+ return null;
+ }
+ /* falls through */
+ case topLevelTypes.topContextMenu:
+ case topLevelTypes.topDoubleClick:
+ case topLevelTypes.topMouseDown:
+ case topLevelTypes.topMouseMove:
+ case topLevelTypes.topMouseOut:
+ case topLevelTypes.topMouseOver:
+ case topLevelTypes.topMouseUp:
+ EventConstructor = SyntheticMouseEvent;
+ break;
+ case topLevelTypes.topDrag:
+ case topLevelTypes.topDragEnd:
+ case topLevelTypes.topDragEnter:
+ case topLevelTypes.topDragExit:
+ case topLevelTypes.topDragLeave:
+ case topLevelTypes.topDragOver:
+ case topLevelTypes.topDragStart:
+ case topLevelTypes.topDrop:
+ EventConstructor = SyntheticDragEvent;
+ break;
+ case topLevelTypes.topTouchCancel:
+ case topLevelTypes.topTouchEnd:
+ case topLevelTypes.topTouchMove:
+ case topLevelTypes.topTouchStart:
+ EventConstructor = SyntheticTouchEvent;
+ break;
+ case topLevelTypes.topScroll:
+ EventConstructor = SyntheticUIEvent;
+ break;
+ case topLevelTypes.topWheel:
+ EventConstructor = SyntheticWheelEvent;
+ break;
+ case topLevelTypes.topCopy:
+ case topLevelTypes.topCut:
+ case topLevelTypes.topPaste:
+ EventConstructor = SyntheticClipboardEvent;
+ break;
+ }
+ !EventConstructor ? "development" !== 'production' ? invariant(false, 'SimpleEventPlugin: Unhandled event type, `%s`.', topLevelType) : invariant(false) : undefined;
+ var event = EventConstructor.getPooled(dispatchConfig, topLevelTargetID, nativeEvent, nativeEventTarget);
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+ },
+
+ didPutListener: function (id, registrationName, listener) {
+ // Mobile Safari does not fire properly bubble click events on
+ // non-interactive elements, which means delegated click listeners do not
+ // fire. The workaround for this bug involves attaching an empty click
+ // listener on the target node.
+ if (registrationName === ON_CLICK_KEY) {
+ var node = ReactMount.getNode(id);
+ if (!onClickListeners[id]) {
+ onClickListeners[id] = EventListener.listen(node, 'click', emptyFunction);
+ }
+ }
+ },
+
+ willDeleteListener: function (id, registrationName) {
+ if (registrationName === ON_CLICK_KEY) {
+ onClickListeners[id].remove();
+ delete onClickListeners[id];
+ }
+ }
+
+};
+
+module.exports = SimpleEventPlugin;
+},{"102":102,"104":104,"105":105,"106":106,"108":108,"109":109,"110":110,"111":111,"112":112,"125":125,"146":146,"15":15,"153":153,"161":161,"166":166,"19":19,"72":72}],102:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticClipboardEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/clipboard-apis/
+ */
+var ClipboardEventInterface = {
+ clipboardData: function (event) {
+ return 'clipboardData' in event ? event.clipboardData : window.clipboardData;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticClipboardEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticClipboardEvent, ClipboardEventInterface);
+
+module.exports = SyntheticClipboardEvent;
+},{"105":105}],103:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticCompositionEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/#events-compositionevents
+ */
+var CompositionEventInterface = {
+ data: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticCompositionEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticCompositionEvent, CompositionEventInterface);
+
+module.exports = SyntheticCompositionEvent;
+},{"105":105}],104:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticDragEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticMouseEvent = _dereq_(109);
+
+/**
+ * @interface DragEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var DragEventInterface = {
+ dataTransfer: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticDragEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticMouseEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticMouseEvent.augmentClass(SyntheticDragEvent, DragEventInterface);
+
+module.exports = SyntheticDragEvent;
+},{"109":109}],105:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+var warning = _dereq_(173);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var EventInterface = {
+ type: null,
+ // currentTarget is set when dispatching; no use in copying it here
+ currentTarget: emptyFunction.thatReturnsNull,
+ eventPhase: null,
+ bubbles: null,
+ cancelable: null,
+ timeStamp: function (event) {
+ return event.timeStamp || Date.now();
+ },
+ defaultPrevented: null,
+ isTrusted: null
+};
+
+/**
+ * Synthetic events are dispatched by event plugins, typically in response to a
+ * top-level event delegation handler.
+ *
+ * These systems should generally use pooling to reduce the frequency of garbage
+ * collection. The system should check `isPersistent` to determine whether the
+ * event should be released into the pool after being dispatched. Users that
+ * need a persisted event should invoke `persist`.
+ *
+ * Synthetic events (and subclasses) implement the DOM Level 3 Events API by
+ * normalizing browser quirks. Subclasses do not necessarily have to implement a
+ * DOM interface; custom application-specific events can also subclass this.
+ *
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ */
+function SyntheticEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ this.dispatchConfig = dispatchConfig;
+ this.dispatchMarker = dispatchMarker;
+ this.nativeEvent = nativeEvent;
+ this.target = nativeEventTarget;
+ this.currentTarget = nativeEventTarget;
+
+ var Interface = this.constructor.Interface;
+ for (var propName in Interface) {
+ if (!Interface.hasOwnProperty(propName)) {
+ continue;
+ }
+ var normalize = Interface[propName];
+ if (normalize) {
+ this[propName] = normalize(nativeEvent);
+ } else {
+ this[propName] = nativeEvent[propName];
+ }
+ }
+
+ var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;
+ if (defaultPrevented) {
+ this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
+ } else {
+ this.isDefaultPrevented = emptyFunction.thatReturnsFalse;
+ }
+ this.isPropagationStopped = emptyFunction.thatReturnsFalse;
+}
+
+assign(SyntheticEvent.prototype, {
+
+ preventDefault: function () {
+ this.defaultPrevented = true;
+ var event = this.nativeEvent;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(event, 'This synthetic event is reused for performance reasons. If you\'re ' + 'seeing this, you\'re calling `preventDefault` on a ' + 'released/nullified synthetic event. This is a no-op. See ' + 'https://fb.me/react-event-pooling for more information.') : undefined;
+ }
+ if (!event) {
+ return;
+ }
+
+ if (event.preventDefault) {
+ event.preventDefault();
+ } else {
+ event.returnValue = false;
+ }
+ this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
+ },
+
+ stopPropagation: function () {
+ var event = this.nativeEvent;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(event, 'This synthetic event is reused for performance reasons. If you\'re ' + 'seeing this, you\'re calling `stopPropagation` on a ' + 'released/nullified synthetic event. This is a no-op. See ' + 'https://fb.me/react-event-pooling for more information.') : undefined;
+ }
+ if (!event) {
+ return;
+ }
+
+ if (event.stopPropagation) {
+ event.stopPropagation();
+ } else {
+ event.cancelBubble = true;
+ }
+ this.isPropagationStopped = emptyFunction.thatReturnsTrue;
+ },
+
+ /**
+ * We release all dispatched `SyntheticEvent`s after each event loop, adding
+ * them back into the pool. This allows a way to hold onto a reference that
+ * won't be added back into the pool.
+ */
+ persist: function () {
+ this.isPersistent = emptyFunction.thatReturnsTrue;
+ },
+
+ /**
+ * Checks if this event should be released back into the pool.
+ *
+ * @return {boolean} True if this should not be released, false otherwise.
+ */
+ isPersistent: emptyFunction.thatReturnsFalse,
+
+ /**
+ * `PooledClass` looks for `destructor` on each instance it releases.
+ */
+ destructor: function () {
+ var Interface = this.constructor.Interface;
+ for (var propName in Interface) {
+ this[propName] = null;
+ }
+ this.dispatchConfig = null;
+ this.dispatchMarker = null;
+ this.nativeEvent = null;
+ }
+
+});
+
+SyntheticEvent.Interface = EventInterface;
+
+/**
+ * Helper to reduce boilerplate when creating subclasses.
+ *
+ * @param {function} Class
+ * @param {?object} Interface
+ */
+SyntheticEvent.augmentClass = function (Class, Interface) {
+ var Super = this;
+
+ var prototype = Object.create(Super.prototype);
+ assign(prototype, Class.prototype);
+ Class.prototype = prototype;
+ Class.prototype.constructor = Class;
+
+ Class.Interface = assign({}, Super.Interface, Interface);
+ Class.augmentClass = Super.augmentClass;
+
+ PooledClass.addPoolingTo(Class, PooledClass.fourArgumentPooler);
+};
+
+PooledClass.addPoolingTo(SyntheticEvent, PooledClass.fourArgumentPooler);
+
+module.exports = SyntheticEvent;
+},{"153":153,"173":173,"24":24,"25":25}],106:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticFocusEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+
+/**
+ * @interface FocusEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var FocusEventInterface = {
+ relatedTarget: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticFocusEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticFocusEvent, FocusEventInterface);
+
+module.exports = SyntheticFocusEvent;
+},{"111":111}],107:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticInputEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105
+ * /#events-inputevents
+ */
+var InputEventInterface = {
+ data: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticInputEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticInputEvent, InputEventInterface);
+
+module.exports = SyntheticInputEvent;
+},{"105":105}],108:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticKeyboardEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+
+var getEventCharCode = _dereq_(125);
+var getEventKey = _dereq_(126);
+var getEventModifierState = _dereq_(127);
+
+/**
+ * @interface KeyboardEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var KeyboardEventInterface = {
+ key: getEventKey,
+ location: null,
+ ctrlKey: null,
+ shiftKey: null,
+ altKey: null,
+ metaKey: null,
+ repeat: null,
+ locale: null,
+ getModifierState: getEventModifierState,
+ // Legacy Interface
+ charCode: function (event) {
+ // `charCode` is the result of a KeyPress event and represents the value of
+ // the actual printable character.
+
+ // KeyPress is deprecated, but its replacement is not yet final and not
+ // implemented in any major browser. Only KeyPress has charCode.
+ if (event.type === 'keypress') {
+ return getEventCharCode(event);
+ }
+ return 0;
+ },
+ keyCode: function (event) {
+ // `keyCode` is the result of a KeyDown/Up event and represents the value of
+ // physical keyboard key.
+
+ // The actual meaning of the value depends on the users' keyboard layout
+ // which cannot be detected. Assuming that it is a US keyboard layout
+ // provides a surprisingly accurate mapping for US and European users.
+ // Due to this, it is left to the user to implement at this time.
+ if (event.type === 'keydown' || event.type === 'keyup') {
+ return event.keyCode;
+ }
+ return 0;
+ },
+ which: function (event) {
+ // `which` is an alias for either `keyCode` or `charCode` depending on the
+ // type of the event.
+ if (event.type === 'keypress') {
+ return getEventCharCode(event);
+ }
+ if (event.type === 'keydown' || event.type === 'keyup') {
+ return event.keyCode;
+ }
+ return 0;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticKeyboardEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticKeyboardEvent, KeyboardEventInterface);
+
+module.exports = SyntheticKeyboardEvent;
+},{"111":111,"125":125,"126":126,"127":127}],109:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticMouseEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+var ViewportMetrics = _dereq_(114);
+
+var getEventModifierState = _dereq_(127);
+
+/**
+ * @interface MouseEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var MouseEventInterface = {
+ screenX: null,
+ screenY: null,
+ clientX: null,
+ clientY: null,
+ ctrlKey: null,
+ shiftKey: null,
+ altKey: null,
+ metaKey: null,
+ getModifierState: getEventModifierState,
+ button: function (event) {
+ // Webkit, Firefox, IE9+
+ // which: 1 2 3
+ // button: 0 1 2 (standard)
+ var button = event.button;
+ if ('which' in event) {
+ return button;
+ }
+ // IE<9
+ // which: undefined
+ // button: 0 0 0
+ // button: 1 4 2 (onmouseup)
+ return button === 2 ? 2 : button === 4 ? 1 : 0;
+ },
+ buttons: null,
+ relatedTarget: function (event) {
+ return event.relatedTarget || (event.fromElement === event.srcElement ? event.toElement : event.fromElement);
+ },
+ // "Proprietary" Interface.
+ pageX: function (event) {
+ return 'pageX' in event ? event.pageX : event.clientX + ViewportMetrics.currentScrollLeft;
+ },
+ pageY: function (event) {
+ return 'pageY' in event ? event.pageY : event.clientY + ViewportMetrics.currentScrollTop;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticMouseEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticMouseEvent, MouseEventInterface);
+
+module.exports = SyntheticMouseEvent;
+},{"111":111,"114":114,"127":127}],110:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticTouchEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+
+var getEventModifierState = _dereq_(127);
+
+/**
+ * @interface TouchEvent
+ * @see http://www.w3.org/TR/touch-events/
+ */
+var TouchEventInterface = {
+ touches: null,
+ targetTouches: null,
+ changedTouches: null,
+ altKey: null,
+ metaKey: null,
+ ctrlKey: null,
+ shiftKey: null,
+ getModifierState: getEventModifierState
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticTouchEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticTouchEvent, TouchEventInterface);
+
+module.exports = SyntheticTouchEvent;
+},{"111":111,"127":127}],111:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticUIEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+var getEventTarget = _dereq_(128);
+
+/**
+ * @interface UIEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var UIEventInterface = {
+ view: function (event) {
+ if (event.view) {
+ return event.view;
+ }
+
+ var target = getEventTarget(event);
+ if (target != null && target.window === target) {
+ // target is a window object
+ return target;
+ }
+
+ var doc = target.ownerDocument;
+ // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.
+ if (doc) {
+ return doc.defaultView || doc.parentWindow;
+ } else {
+ return window;
+ }
+ },
+ detail: function (event) {
+ return event.detail || 0;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticEvent}
+ */
+function SyntheticUIEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticUIEvent, UIEventInterface);
+
+module.exports = SyntheticUIEvent;
+},{"105":105,"128":128}],112:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticWheelEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticMouseEvent = _dereq_(109);
+
+/**
+ * @interface WheelEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var WheelEventInterface = {
+ deltaX: function (event) {
+ return 'deltaX' in event ? event.deltaX :
+ // Fallback to `wheelDeltaX` for Webkit and normalize (right is positive).
+ 'wheelDeltaX' in event ? -event.wheelDeltaX : 0;
+ },
+ deltaY: function (event) {
+ return 'deltaY' in event ? event.deltaY :
+ // Fallback to `wheelDeltaY` for Webkit and normalize (down is positive).
+ 'wheelDeltaY' in event ? -event.wheelDeltaY :
+ // Fallback to `wheelDelta` for IE<9 and normalize (down is positive).
+ 'wheelDelta' in event ? -event.wheelDelta : 0;
+ },
+ deltaZ: null,
+
+ // Browsers without "deltaMode" is reporting in raw wheel delta where one
+ // notch on the scroll is always +/- 120, roughly equivalent to pixels.
+ // A good approximation of DOM_DELTA_LINE (1) is 5% of viewport size or
+ // ~40 pixels, for DOM_DELTA_SCREEN (2) it is 87.5% of viewport size.
+ deltaMode: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticMouseEvent}
+ */
+function SyntheticWheelEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticMouseEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticMouseEvent.augmentClass(SyntheticWheelEvent, WheelEventInterface);
+
+module.exports = SyntheticWheelEvent;
+},{"109":109}],113:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Transaction
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * `Transaction` creates a black box that is able to wrap any method such that
+ * certain invariants are maintained before and after the method is invoked
+ * (Even if an exception is thrown while invoking the wrapped method). Whoever
+ * instantiates a transaction can provide enforcers of the invariants at
+ * creation time. The `Transaction` class itself will supply one additional
+ * automatic invariant for you - the invariant that any transaction instance
+ * should not be run while it is already being run. You would typically create a
+ * single instance of a `Transaction` for reuse multiple times, that potentially
+ * is used to wrap several different methods. Wrappers are extremely simple -
+ * they only require implementing two methods.
+ *
+ * <pre>
+ * wrappers (injected at creation time)
+ * + +
+ * | |
+ * +-----------------|--------|--------------+
+ * | v | |
+ * | +---------------+ | |
+ * | +--| wrapper1 |---|----+ |
+ * | | +---------------+ v | |
+ * | | +-------------+ | |
+ * | | +----| wrapper2 |--------+ |
+ * | | | +-------------+ | | |
+ * | | | | | |
+ * | v v v v | wrapper
+ * | +---+ +---+ +---------+ +---+ +---+ | invariants
+ * perform(anyMethod) | | | | | | | | | | | | maintained
+ * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
+ * | | | | | | | | | | | |
+ * | | | | | | | | | | | |
+ * | | | | | | | | | | | |
+ * | +---+ +---+ +---------+ +---+ +---+ |
+ * | initialize close |
+ * +-----------------------------------------+
+ * </pre>
+ *
+ * Use cases:
+ * - Preserving the input selection ranges before/after reconciliation.
+ * Restoring selection even in the event of an unexpected error.
+ * - Deactivating events while rearranging the DOM, preventing blurs/focuses,
+ * while guaranteeing that afterwards, the event system is reactivated.
+ * - Flushing a queue of collected DOM mutations to the main UI thread after a
+ * reconciliation takes place in a worker thread.
+ * - Invoking any collected `componentDidUpdate` callbacks after rendering new
+ * content.
+ * - (Future use case): Wrapping particular flushes of the `ReactWorker` queue
+ * to preserve the `scrollTop` (an automatic scroll aware DOM).
+ * - (Future use case): Layout calculations before and after DOM updates.
+ *
+ * Transactional plugin API:
+ * - A module that has an `initialize` method that returns any precomputation.
+ * - and a `close` method that accepts the precomputation. `close` is invoked
+ * when the wrapped process is completed, or has failed.
+ *
+ * @param {Array<TransactionalWrapper>} transactionWrapper Wrapper modules
+ * that implement `initialize` and `close`.
+ * @return {Transaction} Single transaction for reuse in thread.
+ *
+ * @class Transaction
+ */
+var Mixin = {
+ /**
+ * Sets up this instance so that it is prepared for collecting metrics. Does
+ * so such that this setup method may be used on an instance that is already
+ * initialized, in a way that does not consume additional memory upon reuse.
+ * That can be useful if you decide to make your subclass of this mixin a
+ * "PooledClass".
+ */
+ reinitializeTransaction: function () {
+ this.transactionWrappers = this.getTransactionWrappers();
+ if (this.wrapperInitData) {
+ this.wrapperInitData.length = 0;
+ } else {
+ this.wrapperInitData = [];
+ }
+ this._isInTransaction = false;
+ },
+
+ _isInTransaction: false,
+
+ /**
+ * @abstract
+ * @return {Array<TransactionWrapper>} Array of transaction wrappers.
+ */
+ getTransactionWrappers: null,
+
+ isInTransaction: function () {
+ return !!this._isInTransaction;
+ },
+
+ /**
+ * Executes the function within a safety window. Use this for the top level
+ * methods that result in large amounts of computation/mutations that would
+ * need to be safety checked. The optional arguments helps prevent the need
+ * to bind in many cases.
+ *
+ * @param {function} method Member of scope to call.
+ * @param {Object} scope Scope to invoke from.
+ * @param {Object?=} a Argument to pass to the method.
+ * @param {Object?=} b Argument to pass to the method.
+ * @param {Object?=} c Argument to pass to the method.
+ * @param {Object?=} d Argument to pass to the method.
+ * @param {Object?=} e Argument to pass to the method.
+ * @param {Object?=} f Argument to pass to the method.
+ *
+ * @return {*} Return value from `method`.
+ */
+ perform: function (method, scope, a, b, c, d, e, f) {
+ !!this.isInTransaction() ? "development" !== 'production' ? invariant(false, 'Transaction.perform(...): Cannot initialize a transaction when there ' + 'is already an outstanding transaction.') : invariant(false) : undefined;
+ var errorThrown;
+ var ret;
+ try {
+ this._isInTransaction = true;
+ // Catching errors makes debugging more difficult, so we start with
+ // errorThrown set to true before setting it to false after calling
+ // close -- if it's still set to true in the finally block, it means
+ // one of these calls threw.
+ errorThrown = true;
+ this.initializeAll(0);
+ ret = method.call(scope, a, b, c, d, e, f);
+ errorThrown = false;
+ } finally {
+ try {
+ if (errorThrown) {
+ // If `method` throws, prefer to show that stack trace over any thrown
+ // by invoking `closeAll`.
+ try {
+ this.closeAll(0);
+ } catch (err) {}
+ } else {
+ // Since `method` didn't throw, we don't want to silence the exception
+ // here.
+ this.closeAll(0);
+ }
+ } finally {
+ this._isInTransaction = false;
+ }
+ }
+ return ret;
+ },
+
+ initializeAll: function (startIndex) {
+ var transactionWrappers = this.transactionWrappers;
+ for (var i = startIndex; i < transactionWrappers.length; i++) {
+ var wrapper = transactionWrappers[i];
+ try {
+ // Catching errors makes debugging more difficult, so we start with the
+ // OBSERVED_ERROR state before overwriting it with the real return value
+ // of initialize -- if it's still set to OBSERVED_ERROR in the finally
+ // block, it means wrapper.initialize threw.
+ this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
+ this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null;
+ } finally {
+ if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) {
+ // The initializer for wrapper i threw an error; initialize the
+ // remaining wrappers but silence any exceptions from them to ensure
+ // that the first error is the one to bubble up.
+ try {
+ this.initializeAll(i + 1);
+ } catch (err) {}
+ }
+ }
+ }
+ },
+
+ /**
+ * Invokes each of `this.transactionWrappers.close[i]` functions, passing into
+ * them the respective return values of `this.transactionWrappers.init[i]`
+ * (`close`rs that correspond to initializers that failed will not be
+ * invoked).
+ */
+ closeAll: function (startIndex) {
+ !this.isInTransaction() ? "development" !== 'production' ? invariant(false, 'Transaction.closeAll(): Cannot close transaction when none are open.') : invariant(false) : undefined;
+ var transactionWrappers = this.transactionWrappers;
+ for (var i = startIndex; i < transactionWrappers.length; i++) {
+ var wrapper = transactionWrappers[i];
+ var initData = this.wrapperInitData[i];
+ var errorThrown;
+ try {
+ // Catching errors makes debugging more difficult, so we start with
+ // errorThrown set to true before setting it to false after calling
+ // close -- if it's still set to true in the finally block, it means
+ // wrapper.close threw.
+ errorThrown = true;
+ if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) {
+ wrapper.close.call(this, initData);
+ }
+ errorThrown = false;
+ } finally {
+ if (errorThrown) {
+ // The closer for wrapper i threw an error; close the remaining
+ // wrappers but silence any exceptions from them to ensure that the
+ // first error is the one to bubble up.
+ try {
+ this.closeAll(i + 1);
+ } catch (e) {}
+ }
+ }
+ }
+ this.wrapperInitData.length = 0;
+ }
+};
+
+var Transaction = {
+
+ Mixin: Mixin,
+
+ /**
+ * Token to look for to determine if an error occurred.
+ */
+ OBSERVED_ERROR: {}
+
+};
+
+module.exports = Transaction;
+},{"161":161}],114:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ViewportMetrics
+ */
+
+'use strict';
+
+var ViewportMetrics = {
+
+ currentScrollLeft: 0,
+
+ currentScrollTop: 0,
+
+ refreshScrollValues: function (scrollPosition) {
+ ViewportMetrics.currentScrollLeft = scrollPosition.x;
+ ViewportMetrics.currentScrollTop = scrollPosition.y;
+ }
+
+};
+
+module.exports = ViewportMetrics;
+},{}],115:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule accumulateInto
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ *
+ * Accumulates items that must not be null or undefined into the first one. This
+ * is used to conserve memory by avoiding array allocations, and thus sacrifices
+ * API cleanness. Since `current` can be null before being passed in and not
+ * null after this function, make sure to assign it back to `current`:
+ *
+ * `a = accumulateInto(a, b);`
+ *
+ * This API should be sparingly used. Try `accumulate` for something cleaner.
+ *
+ * @return {*|array<*>} An accumulation of items.
+ */
+
+function accumulateInto(current, next) {
+ !(next != null) ? "development" !== 'production' ? invariant(false, 'accumulateInto(...): Accumulated items must not be null or undefined.') : invariant(false) : undefined;
+ if (current == null) {
+ return next;
+ }
+
+ // Both are not empty. Warning: Never call x.concat(y) when you are not
+ // certain that x is an Array (x could be a string with concat method).
+ var currentIsArray = Array.isArray(current);
+ var nextIsArray = Array.isArray(next);
+
+ if (currentIsArray && nextIsArray) {
+ current.push.apply(current, next);
+ return current;
+ }
+
+ if (currentIsArray) {
+ current.push(next);
+ return current;
+ }
+
+ if (nextIsArray) {
+ // A bit too dangerous to mutate `next`.
+ return [current].concat(next);
+ }
+
+ return [current, next];
+}
+
+module.exports = accumulateInto;
+},{"161":161}],116:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule adler32
+ */
+
+'use strict';
+
+var MOD = 65521;
+
+// adler32 is not cryptographically strong, and is only used to sanity check that
+// markup generated on the server matches the markup generated on the client.
+// This implementation (a modified version of the SheetJS version) has been optimized
+// for our use case, at the expense of conforming to the adler32 specification
+// for non-ascii inputs.
+function adler32(data) {
+ var a = 1;
+ var b = 0;
+ var i = 0;
+ var l = data.length;
+ var m = l & ~0x3;
+ while (i < m) {
+ for (; i < Math.min(i + 4096, m); i += 4) {
+ b += (a += data.charCodeAt(i)) + (a += data.charCodeAt(i + 1)) + (a += data.charCodeAt(i + 2)) + (a += data.charCodeAt(i + 3));
+ }
+ a %= MOD;
+ b %= MOD;
+ }
+ for (; i < l; i++) {
+ b += a += data.charCodeAt(i);
+ }
+ a %= MOD;
+ b %= MOD;
+ return a | b << 16;
+}
+
+module.exports = adler32;
+},{}],117:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule canDefineProperty
+ */
+
+'use strict';
+
+var canDefineProperty = false;
+if ("development" !== 'production') {
+ try {
+ Object.defineProperty({}, 'x', { get: function () {} });
+ canDefineProperty = true;
+ } catch (x) {
+ // IE will fail on defineProperty
+ }
+}
+
+module.exports = canDefineProperty;
+},{}],118:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks static-only
+ * @providesModule cloneWithProps
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactPropTransferer = _dereq_(79);
+
+var keyOf = _dereq_(166);
+var warning = _dereq_(173);
+
+var CHILDREN_PROP = keyOf({ children: null });
+
+var didDeprecatedWarn = false;
+
+/**
+ * Sometimes you want to change the props of a child passed to you. Usually
+ * this is to add a CSS class.
+ *
+ * @param {ReactElement} child child element you'd like to clone
+ * @param {object} props props you'd like to modify. className and style will be
+ * merged automatically.
+ * @return {ReactElement} a clone of child with props merged in.
+ * @deprecated
+ */
+function cloneWithProps(child, props) {
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(didDeprecatedWarn, 'cloneWithProps(...) is deprecated. ' + 'Please use React.cloneElement instead.') : undefined;
+ didDeprecatedWarn = true;
+ "development" !== 'production' ? warning(!child.ref, 'You are calling cloneWithProps() on a child with a ref. This is ' + 'dangerous because you\'re creating a new child which will not be ' + 'added as a ref to its parent.') : undefined;
+ }
+
+ var newProps = ReactPropTransferer.mergeProps(props, child.props);
+
+ // Use `child.props.children` if it is provided.
+ if (!newProps.hasOwnProperty(CHILDREN_PROP) && child.props.hasOwnProperty(CHILDREN_PROP)) {
+ newProps.children = child.props.children;
+ }
+
+ // The current API doesn't retain _owner, which is why this
+ // doesn't use ReactElement.cloneAndReplaceProps.
+ return ReactElement.createElement(child.type, newProps);
+}
+
+module.exports = cloneWithProps;
+},{"166":166,"173":173,"57":57,"79":79}],119:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule dangerousStyleValue
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var CSSProperty = _dereq_(4);
+
+var isUnitlessNumber = CSSProperty.isUnitlessNumber;
+
+/**
+ * Convert a value into the proper css writable value. The style name `name`
+ * should be logical (no hyphens), as specified
+ * in `CSSProperty.isUnitlessNumber`.
+ *
+ * @param {string} name CSS property name such as `topMargin`.
+ * @param {*} value CSS property value such as `10px`.
+ * @return {string} Normalized style value with dimensions applied.
+ */
+function dangerousStyleValue(name, value) {
+ // Note that we've removed escapeTextForBrowser() calls here since the
+ // whole string will be escaped when the attribute is injected into
+ // the markup. If you provide unsafe user data here they can inject
+ // arbitrary CSS which may be problematic (I couldn't repro this):
+ // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
+ // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/
+ // This is not an XSS hole but instead a potential CSS injection issue
+ // which has lead to a greater discussion about how we're going to
+ // trust URLs moving forward. See #2115901
+
+ var isEmpty = value == null || typeof value === 'boolean' || value === '';
+ if (isEmpty) {
+ return '';
+ }
+
+ var isNonNumeric = isNaN(value);
+ if (isNonNumeric || value === 0 || isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name]) {
+ return '' + value; // cast to string
+ }
+
+ if (typeof value === 'string') {
+ value = value.trim();
+ }
+ return value + 'px';
+}
+
+module.exports = dangerousStyleValue;
+},{"4":4}],120:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule deprecated
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var warning = _dereq_(173);
+
+/**
+ * This will log a single deprecation notice per function and forward the call
+ * on to the new API.
+ *
+ * @param {string} fnName The name of the function
+ * @param {string} newModule The module that fn will exist in
+ * @param {string} newPackage The module that fn will exist in
+ * @param {*} ctx The context this forwarded call should run in
+ * @param {function} fn The function to forward on to
+ * @return {function} The function that will warn once and then call fn
+ */
+function deprecated(fnName, newModule, newPackage, ctx, fn) {
+ var warned = false;
+ if ("development" !== 'production') {
+ var newFn = function () {
+ "development" !== 'production' ? warning(warned,
+ // Require examples in this string must be split to prevent React's
+ // build tools from mistaking them for real requires.
+ // Otherwise the build tools will attempt to build a '%s' module.
+ 'React.%s is deprecated. Please use %s.%s from require' + '(\'%s\') ' + 'instead.', fnName, newModule, fnName, newPackage) : undefined;
+ warned = true;
+ return fn.apply(ctx, arguments);
+ };
+ // We need to make sure all properties of the original fn are copied over.
+ // In particular, this is needed to support PropTypes
+ return assign(newFn, fn);
+ }
+
+ return fn;
+}
+
+module.exports = deprecated;
+},{"173":173,"24":24}],121:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule escapeTextContentForBrowser
+ */
+
+'use strict';
+
+var ESCAPE_LOOKUP = {
+ '&': '&amp;',
+ '>': '&gt;',
+ '<': '&lt;',
+ '"': '&quot;',
+ '\'': '&#x27;'
+};
+
+var ESCAPE_REGEX = /[&><"']/g;
+
+function escaper(match) {
+ return ESCAPE_LOOKUP[match];
+}
+
+/**
+ * Escapes text to prevent scripting attacks.
+ *
+ * @param {*} text Text value to escape.
+ * @return {string} An escaped string.
+ */
+function escapeTextContentForBrowser(text) {
+ return ('' + text).replace(ESCAPE_REGEX, escaper);
+}
+
+module.exports = escapeTextContentForBrowser;
+},{}],122:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule findDOMNode
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactInstanceMap = _dereq_(68);
+var ReactMount = _dereq_(72);
+
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Returns the DOM node rendered by this element.
+ *
+ * @param {ReactComponent|DOMElement} componentOrElement
+ * @return {?DOMElement} The root node of this element.
+ */
+function findDOMNode(componentOrElement) {
+ if ("development" !== 'production') {
+ var owner = ReactCurrentOwner.current;
+ if (owner !== null) {
+ "development" !== 'production' ? warning(owner._warnedAboutRefsInRender, '%s is accessing getDOMNode or findDOMNode inside its render(). ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + 'componentDidUpdate instead.', owner.getName() || 'A component') : undefined;
+ owner._warnedAboutRefsInRender = true;
+ }
+ }
+ if (componentOrElement == null) {
+ return null;
+ }
+ if (componentOrElement.nodeType === 1) {
+ return componentOrElement;
+ }
+ if (ReactInstanceMap.has(componentOrElement)) {
+ return ReactMount.getNodeFromInstance(componentOrElement);
+ }
+ !(componentOrElement.render == null || typeof componentOrElement.render !== 'function') ? "development" !== 'production' ? invariant(false, 'findDOMNode was called on an unmounted component.') : invariant(false) : undefined;
+ !false ? "development" !== 'production' ? invariant(false, 'Element appears to be neither ReactComponent nor DOMNode (keys: %s)', Object.keys(componentOrElement)) : invariant(false) : undefined;
+}
+
+module.exports = findDOMNode;
+},{"161":161,"173":173,"39":39,"68":68,"72":72}],123:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule flattenChildren
+ */
+
+'use strict';
+
+var traverseAllChildren = _dereq_(142);
+var warning = _dereq_(173);
+
+/**
+ * @param {function} traverseContext Context passed through traversal.
+ * @param {?ReactComponent} child React child component.
+ * @param {!string} name String name of key path to child.
+ */
+function flattenSingleChildIntoContext(traverseContext, child, name) {
+ // We found a component instance.
+ var result = traverseContext;
+ var keyUnique = result[name] === undefined;
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(keyUnique, 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.', name) : undefined;
+ }
+ if (keyUnique && child != null) {
+ result[name] = child;
+ }
+}
+
+/**
+ * Flattens children that are typically specified as `props.children`. Any null
+ * children will not be included in the resulting object.
+ * @return {!object} flattened children keyed by name.
+ */
+function flattenChildren(children) {
+ if (children == null) {
+ return children;
+ }
+ var result = {};
+ traverseAllChildren(children, flattenSingleChildIntoContext, result);
+ return result;
+}
+
+module.exports = flattenChildren;
+},{"142":142,"173":173}],124:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule forEachAccumulated
+ */
+
+'use strict';
+
+/**
+ * @param {array} arr an "accumulation" of items which is either an Array or
+ * a single item. Useful when paired with the `accumulate` module. This is a
+ * simple utility that allows us to reason about a collection of items, but
+ * handling the case when there is exactly one item (and we do not need to
+ * allocate an array).
+ */
+var forEachAccumulated = function (arr, cb, scope) {
+ if (Array.isArray(arr)) {
+ arr.forEach(cb, scope);
+ } else if (arr) {
+ cb.call(scope, arr);
+ }
+};
+
+module.exports = forEachAccumulated;
+},{}],125:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventCharCode
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * `charCode` represents the actual "character code" and is safe to use with
+ * `String.fromCharCode`. As such, only keys that correspond to printable
+ * characters produce a valid `charCode`, the only exception to this is Enter.
+ * The Tab-key is considered non-printable and does not have a `charCode`,
+ * presumably because it does not produce a tab-character in browsers.
+ *
+ * @param {object} nativeEvent Native browser event.
+ * @return {number} Normalized `charCode` property.
+ */
+function getEventCharCode(nativeEvent) {
+ var charCode;
+ var keyCode = nativeEvent.keyCode;
+
+ if ('charCode' in nativeEvent) {
+ charCode = nativeEvent.charCode;
+
+ // FF does not set `charCode` for the Enter-key, check against `keyCode`.
+ if (charCode === 0 && keyCode === 13) {
+ charCode = 13;
+ }
+ } else {
+ // IE8 does not implement `charCode`, but `keyCode` has the correct value.
+ charCode = keyCode;
+ }
+
+ // Some non-printable keys are reported in `charCode`/`keyCode`, discard them.
+ // Must not discard the (non-)printable Enter-key.
+ if (charCode >= 32 || charCode === 13) {
+ return charCode;
+ }
+
+ return 0;
+}
+
+module.exports = getEventCharCode;
+},{}],126:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventKey
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var getEventCharCode = _dereq_(125);
+
+/**
+ * Normalization of deprecated HTML5 `key` values
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names
+ */
+var normalizeKey = {
+ 'Esc': 'Escape',
+ 'Spacebar': ' ',
+ 'Left': 'ArrowLeft',
+ 'Up': 'ArrowUp',
+ 'Right': 'ArrowRight',
+ 'Down': 'ArrowDown',
+ 'Del': 'Delete',
+ 'Win': 'OS',
+ 'Menu': 'ContextMenu',
+ 'Apps': 'ContextMenu',
+ 'Scroll': 'ScrollLock',
+ 'MozPrintableKey': 'Unidentified'
+};
+
+/**
+ * Translation from legacy `keyCode` to HTML5 `key`
+ * Only special keys supported, all others depend on keyboard layout or browser
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names
+ */
+var translateToKey = {
+ 8: 'Backspace',
+ 9: 'Tab',
+ 12: 'Clear',
+ 13: 'Enter',
+ 16: 'Shift',
+ 17: 'Control',
+ 18: 'Alt',
+ 19: 'Pause',
+ 20: 'CapsLock',
+ 27: 'Escape',
+ 32: ' ',
+ 33: 'PageUp',
+ 34: 'PageDown',
+ 35: 'End',
+ 36: 'Home',
+ 37: 'ArrowLeft',
+ 38: 'ArrowUp',
+ 39: 'ArrowRight',
+ 40: 'ArrowDown',
+ 45: 'Insert',
+ 46: 'Delete',
+ 112: 'F1', 113: 'F2', 114: 'F3', 115: 'F4', 116: 'F5', 117: 'F6',
+ 118: 'F7', 119: 'F8', 120: 'F9', 121: 'F10', 122: 'F11', 123: 'F12',
+ 144: 'NumLock',
+ 145: 'ScrollLock',
+ 224: 'Meta'
+};
+
+/**
+ * @param {object} nativeEvent Native browser event.
+ * @return {string} Normalized `key` property.
+ */
+function getEventKey(nativeEvent) {
+ if (nativeEvent.key) {
+ // Normalize inconsistent values reported by browsers due to
+ // implementations of a working draft specification.
+
+ // FireFox implements `key` but returns `MozPrintableKey` for all
+ // printable characters (normalized to `Unidentified`), ignore it.
+ var key = normalizeKey[nativeEvent.key] || nativeEvent.key;
+ if (key !== 'Unidentified') {
+ return key;
+ }
+ }
+
+ // Browser does not implement `key`, polyfill as much of it as we can.
+ if (nativeEvent.type === 'keypress') {
+ var charCode = getEventCharCode(nativeEvent);
+
+ // The enter-key is technically both printable and non-printable and can
+ // thus be captured by `keypress`, no other non-printable key should.
+ return charCode === 13 ? 'Enter' : String.fromCharCode(charCode);
+ }
+ if (nativeEvent.type === 'keydown' || nativeEvent.type === 'keyup') {
+ // While user keyboard layout determines the actual meaning of each
+ // `keyCode` value, almost all function keys have a universal value.
+ return translateToKey[nativeEvent.keyCode] || 'Unidentified';
+ }
+ return '';
+}
+
+module.exports = getEventKey;
+},{"125":125}],127:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventModifierState
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Translation from modifier key to the associated property in the event.
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/#keys-Modifiers
+ */
+
+var modifierKeyToProp = {
+ 'Alt': 'altKey',
+ 'Control': 'ctrlKey',
+ 'Meta': 'metaKey',
+ 'Shift': 'shiftKey'
+};
+
+// IE8 does not implement getModifierState so we simply map it to the only
+// modifier keys exposed by the event itself, does not support Lock-keys.
+// Currently, all major browsers except Chrome seems to support Lock-keys.
+function modifierStateGetter(keyArg) {
+ var syntheticEvent = this;
+ var nativeEvent = syntheticEvent.nativeEvent;
+ if (nativeEvent.getModifierState) {
+ return nativeEvent.getModifierState(keyArg);
+ }
+ var keyProp = modifierKeyToProp[keyArg];
+ return keyProp ? !!nativeEvent[keyProp] : false;
+}
+
+function getEventModifierState(nativeEvent) {
+ return modifierStateGetter;
+}
+
+module.exports = getEventModifierState;
+},{}],128:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventTarget
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Gets the target node from a native browser event by accounting for
+ * inconsistencies in browser DOM APIs.
+ *
+ * @param {object} nativeEvent Native browser event.
+ * @return {DOMEventTarget} Target node.
+ */
+function getEventTarget(nativeEvent) {
+ var target = nativeEvent.target || nativeEvent.srcElement || window;
+ // Safari may fire events on text nodes (Node.TEXT_NODE is 3).
+ // @see http://www.quirksmode.org/js/events_properties.html
+ return target.nodeType === 3 ? target.parentNode : target;
+}
+
+module.exports = getEventTarget;
+},{}],129:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getIteratorFn
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/* global Symbol */
+var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
+var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec.
+
+/**
+ * Returns the iterator method function contained on the iterable object.
+ *
+ * Be sure to invoke the function with the iterable as context:
+ *
+ * var iteratorFn = getIteratorFn(myIterable);
+ * if (iteratorFn) {
+ * var iterator = iteratorFn.call(myIterable);
+ * ...
+ * }
+ *
+ * @param {?object} maybeIterable
+ * @return {?function}
+ */
+function getIteratorFn(maybeIterable) {
+ var iteratorFn = maybeIterable && (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]);
+ if (typeof iteratorFn === 'function') {
+ return iteratorFn;
+ }
+}
+
+module.exports = getIteratorFn;
+},{}],130:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getNodeForCharacterOffset
+ */
+
+'use strict';
+
+/**
+ * Given any node return the first leaf node without children.
+ *
+ * @param {DOMElement|DOMTextNode} node
+ * @return {DOMElement|DOMTextNode}
+ */
+function getLeafNode(node) {
+ while (node && node.firstChild) {
+ node = node.firstChild;
+ }
+ return node;
+}
+
+/**
+ * Get the next sibling within a container. This will walk up the
+ * DOM if a node's siblings have been exhausted.
+ *
+ * @param {DOMElement|DOMTextNode} node
+ * @return {?DOMElement|DOMTextNode}
+ */
+function getSiblingNode(node) {
+ while (node) {
+ if (node.nextSibling) {
+ return node.nextSibling;
+ }
+ node = node.parentNode;
+ }
+}
+
+/**
+ * Get object describing the nodes which contain characters at offset.
+ *
+ * @param {DOMElement|DOMTextNode} root
+ * @param {number} offset
+ * @return {?object}
+ */
+function getNodeForCharacterOffset(root, offset) {
+ var node = getLeafNode(root);
+ var nodeStart = 0;
+ var nodeEnd = 0;
+
+ while (node) {
+ if (node.nodeType === 3) {
+ nodeEnd = nodeStart + node.textContent.length;
+
+ if (nodeStart <= offset && nodeEnd >= offset) {
+ return {
+ node: node,
+ offset: offset - nodeStart
+ };
+ }
+
+ nodeStart = nodeEnd;
+ }
+
+ node = getLeafNode(getSiblingNode(node));
+ }
+}
+
+module.exports = getNodeForCharacterOffset;
+},{}],131:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getTextContentAccessor
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var contentKey = null;
+
+/**
+ * Gets the key used to access text content on a DOM node.
+ *
+ * @return {?string} Key used to access text content.
+ * @internal
+ */
+function getTextContentAccessor() {
+ if (!contentKey && ExecutionEnvironment.canUseDOM) {
+ // Prefer textContent to innerText because many browsers support both but
+ // SVG <text> elements don't support innerText even when <div> does.
+ contentKey = 'textContent' in document.documentElement ? 'textContent' : 'innerText';
+ }
+ return contentKey;
+}
+
+module.exports = getTextContentAccessor;
+},{"147":147}],132:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule instantiateReactComponent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactCompositeComponent = _dereq_(38);
+var ReactEmptyComponent = _dereq_(59);
+var ReactNativeComponent = _dereq_(75);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+// To avoid a cyclic dependency, we create the final class in this module
+var ReactCompositeComponentWrapper = function () {};
+assign(ReactCompositeComponentWrapper.prototype, ReactCompositeComponent.Mixin, {
+ _instantiateReactComponent: instantiateReactComponent
+});
+
+function getDeclarationErrorAddendum(owner) {
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+/**
+ * Check if the type reference is a known internal type. I.e. not a user
+ * provided composite type.
+ *
+ * @param {function} type
+ * @return {boolean} Returns true if this is a valid internal type.
+ */
+function isInternalComponentType(type) {
+ return typeof type === 'function' && typeof type.prototype !== 'undefined' && typeof type.prototype.mountComponent === 'function' && typeof type.prototype.receiveComponent === 'function';
+}
+
+/**
+ * Given a ReactNode, create an instance that will actually be mounted.
+ *
+ * @param {ReactNode} node
+ * @return {object} A new instance of the element's constructor.
+ * @protected
+ */
+function instantiateReactComponent(node) {
+ var instance;
+
+ if (node === null || node === false) {
+ instance = new ReactEmptyComponent(instantiateReactComponent);
+ } else if (typeof node === 'object') {
+ var element = node;
+ !(element && (typeof element.type === 'function' || typeof element.type === 'string')) ? "development" !== 'production' ? invariant(false, 'Element type is invalid: expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: %s.%s', element.type == null ? element.type : typeof element.type, getDeclarationErrorAddendum(element._owner)) : invariant(false) : undefined;
+
+ // Special case string values
+ if (typeof element.type === 'string') {
+ instance = ReactNativeComponent.createInternalComponent(element);
+ } else if (isInternalComponentType(element.type)) {
+ // This is temporarily available for custom components that are not string
+ // representations. I.e. ART. Once those are updated to use the string
+ // representation, we can drop this code path.
+ instance = new element.type(element);
+ } else {
+ instance = new ReactCompositeComponentWrapper();
+ }
+ } else if (typeof node === 'string' || typeof node === 'number') {
+ instance = ReactNativeComponent.createInstanceForText(node);
+ } else {
+ !false ? "development" !== 'production' ? invariant(false, 'Encountered invalid React node of type %s', typeof node) : invariant(false) : undefined;
+ }
+
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(typeof instance.construct === 'function' && typeof instance.mountComponent === 'function' && typeof instance.receiveComponent === 'function' && typeof instance.unmountComponent === 'function', 'Only React Components can be mounted.') : undefined;
+ }
+
+ // Sets up the instance. This can probably just move into the constructor now.
+ instance.construct(node);
+
+ // These two fields are used by the DOM and ART diffing algorithms
+ // respectively. Instead of using expandos on components, we should be
+ // storing the state needed by the diffing algorithms elsewhere.
+ instance._mountIndex = 0;
+ instance._mountImage = null;
+
+ if ("development" !== 'production') {
+ instance._isOwnerNecessary = false;
+ instance._warnedAboutRefsInRender = false;
+ }
+
+ // Internal instances should fully constructed at this point, so they should
+ // not get any new fields added to them at this point.
+ if ("development" !== 'production') {
+ if (Object.preventExtensions) {
+ Object.preventExtensions(instance);
+ }
+ }
+
+ return instance;
+}
+
+module.exports = instantiateReactComponent;
+},{"161":161,"173":173,"24":24,"38":38,"59":59,"75":75}],133:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isEventSupported
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var useHasFeature;
+if (ExecutionEnvironment.canUseDOM) {
+ useHasFeature = document.implementation && document.implementation.hasFeature &&
+ // always returns true in newer browsers as per the standard.
+ // @see http://dom.spec.whatwg.org/#dom-domimplementation-hasfeature
+ document.implementation.hasFeature('', '') !== true;
+}
+
+/**
+ * Checks if an event is supported in the current execution environment.
+ *
+ * NOTE: This will not work correctly for non-generic events such as `change`,
+ * `reset`, `load`, `error`, and `select`.
+ *
+ * Borrows from Modernizr.
+ *
+ * @param {string} eventNameSuffix Event name, e.g. "click".
+ * @param {?boolean} capture Check if the capture phase is supported.
+ * @return {boolean} True if the event is supported.
+ * @internal
+ * @license Modernizr 3.0.0pre (Custom Build) | MIT
+ */
+function isEventSupported(eventNameSuffix, capture) {
+ if (!ExecutionEnvironment.canUseDOM || capture && !('addEventListener' in document)) {
+ return false;
+ }
+
+ var eventName = 'on' + eventNameSuffix;
+ var isSupported = (eventName in document);
+
+ if (!isSupported) {
+ var element = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ element.setAttribute(eventName, 'return;');
+ isSupported = typeof element[eventName] === 'function';
+ }
+
+ if (!isSupported && useHasFeature && eventNameSuffix === 'wheel') {
+ // This is the only way to test support for the `wheel` event in IE9+.
+ isSupported = document.implementation.hasFeature('Events.wheel', '3.0');
+ }
+
+ return isSupported;
+}
+
+module.exports = isEventSupported;
+},{"147":147}],134:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isTextInputElement
+ */
+
+'use strict';
+
+/**
+ * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary
+ */
+var supportedInputTypes = {
+ 'color': true,
+ 'date': true,
+ 'datetime': true,
+ 'datetime-local': true,
+ 'email': true,
+ 'month': true,
+ 'number': true,
+ 'password': true,
+ 'range': true,
+ 'search': true,
+ 'tel': true,
+ 'text': true,
+ 'time': true,
+ 'url': true,
+ 'week': true
+};
+
+function isTextInputElement(elem) {
+ var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
+ return nodeName && (nodeName === 'input' && supportedInputTypes[elem.type] || nodeName === 'textarea');
+}
+
+module.exports = isTextInputElement;
+},{}],135:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule onlyChild
+ */
+'use strict';
+
+var ReactElement = _dereq_(57);
+
+var invariant = _dereq_(161);
+
+/**
+ * Returns the first child in a collection of children and verifies that there
+ * is only one child in the collection. The current implementation of this
+ * function assumes that a single child gets passed without a wrapper, but the
+ * purpose of this helper function is to abstract away the particular structure
+ * of children.
+ *
+ * @param {?object} children Child collection structure.
+ * @return {ReactComponent} The first and only `ReactComponent` contained in the
+ * structure.
+ */
+function onlyChild(children) {
+ !ReactElement.isValidElement(children) ? "development" !== 'production' ? invariant(false, 'onlyChild must be passed a children with exactly one child.') : invariant(false) : undefined;
+ return children;
+}
+
+module.exports = onlyChild;
+},{"161":161,"57":57}],136:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule quoteAttributeValueForBrowser
+ */
+
+'use strict';
+
+var escapeTextContentForBrowser = _dereq_(121);
+
+/**
+ * Escapes attribute value to prevent scripting attacks.
+ *
+ * @param {*} value Value to escape.
+ * @return {string} An escaped string.
+ */
+function quoteAttributeValueForBrowser(value) {
+ return '"' + escapeTextContentForBrowser(value) + '"';
+}
+
+module.exports = quoteAttributeValueForBrowser;
+},{"121":121}],137:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+* @providesModule renderSubtreeIntoContainer
+*/
+
+'use strict';
+
+var ReactMount = _dereq_(72);
+
+module.exports = ReactMount.renderSubtreeIntoContainer;
+},{"72":72}],138:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule setInnerHTML
+ */
+
+/* globals MSApp */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var WHITESPACE_TEST = /^[ \r\n\t\f]/;
+var NONVISIBLE_TEST = /<(!--|link|noscript|meta|script|style)[ \r\n\t\f\/>]/;
+
+/**
+ * Set the innerHTML property of a node, ensuring that whitespace is preserved
+ * even in IE8.
+ *
+ * @param {DOMElement} node
+ * @param {string} html
+ * @internal
+ */
+var setInnerHTML = function (node, html) {
+ node.innerHTML = html;
+};
+
+// Win8 apps: Allow all html to be inserted
+if (typeof MSApp !== 'undefined' && MSApp.execUnsafeLocalFunction) {
+ setInnerHTML = function (node, html) {
+ MSApp.execUnsafeLocalFunction(function () {
+ node.innerHTML = html;
+ });
+ };
+}
+
+if (ExecutionEnvironment.canUseDOM) {
+ // IE8: When updating a just created node with innerHTML only leading
+ // whitespace is removed. When updating an existing node with innerHTML
+ // whitespace in root TextNodes is also collapsed.
+ // @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html
+
+ // Feature detection; only IE8 is known to behave improperly like this.
+ var testElement = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ testElement.innerHTML = ' ';
+ if (testElement.innerHTML === '') {
+ setInnerHTML = function (node, html) {
+ // Magic theory: IE8 supposedly differentiates between added and updated
+ // nodes when processing innerHTML, innerHTML on updated nodes suffers
+ // from worse whitespace behavior. Re-adding a node like this triggers
+ // the initial and more favorable whitespace behavior.
+ // TODO: What to do on a detached node?
+ if (node.parentNode) {
+ node.parentNode.replaceChild(node, node);
+ }
+
+ // We also implement a workaround for non-visible tags disappearing into
+ // thin air on IE8, this only happens if there is no visible text
+ // in-front of the non-visible tags. Piggyback on the whitespace fix
+ // and simply check if any non-visible tags appear in the source.
+ if (WHITESPACE_TEST.test(html) || html[0] === '<' && NONVISIBLE_TEST.test(html)) {
+ // Recover leading whitespace by temporarily prepending any character.
+ // \uFEFF has the potential advantage of being zero-width/invisible.
+ // UglifyJS drops U+FEFF chars when parsing, so use String.fromCharCode
+ // in hopes that this is preserved even if "\uFEFF" is transformed to
+ // the actual Unicode character (by Babel, for example).
+ // https://github.com/mishoo/UglifyJS2/blob/v2.4.20/lib/parse.js#L216
+ node.innerHTML = String.fromCharCode(0xFEFF) + html;
+
+ // deleteData leaves an empty `TextNode` which offsets the index of all
+ // children. Definitely want to avoid this.
+ var textNode = node.firstChild;
+ if (textNode.data.length === 1) {
+ node.removeChild(textNode);
+ } else {
+ textNode.deleteData(0, 1);
+ }
+ } else {
+ node.innerHTML = html;
+ }
+ };
+ }
+}
+
+module.exports = setInnerHTML;
+},{"147":147}],139:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule setTextContent
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+var escapeTextContentForBrowser = _dereq_(121);
+var setInnerHTML = _dereq_(138);
+
+/**
+ * Set the textContent property of a node, ensuring that whitespace is preserved
+ * even in IE8. innerText is a poor substitute for textContent and, among many
+ * issues, inserts <br> instead of the literal newline chars. innerHTML behaves
+ * as it should.
+ *
+ * @param {DOMElement} node
+ * @param {string} text
+ * @internal
+ */
+var setTextContent = function (node, text) {
+ node.textContent = text;
+};
+
+if (ExecutionEnvironment.canUseDOM) {
+ if (!('textContent' in document.documentElement)) {
+ setTextContent = function (node, text) {
+ setInnerHTML(node, escapeTextContentForBrowser(text));
+ };
+ }
+}
+
+module.exports = setTextContent;
+},{"121":121,"138":138,"147":147}],140:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+* @providesModule shallowCompare
+*/
+
+'use strict';
+
+var shallowEqual = _dereq_(171);
+
+/**
+ * Does a shallow comparison for props and state.
+ * See ReactComponentWithPureRenderMixin
+ */
+function shallowCompare(instance, nextProps, nextState) {
+ return !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState);
+}
+
+module.exports = shallowCompare;
+},{"171":171}],141:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule shouldUpdateReactComponent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Given a `prevElement` and `nextElement`, determines if the existing
+ * instance should be updated as opposed to being destroyed or replaced by a new
+ * instance. Both arguments are elements. This ensures that this logic can
+ * operate on stateless trees without any backing instance.
+ *
+ * @param {?object} prevElement
+ * @param {?object} nextElement
+ * @return {boolean} True if the existing instance should be updated.
+ * @protected
+ */
+function shouldUpdateReactComponent(prevElement, nextElement) {
+ var prevEmpty = prevElement === null || prevElement === false;
+ var nextEmpty = nextElement === null || nextElement === false;
+ if (prevEmpty || nextEmpty) {
+ return prevEmpty === nextEmpty;
+ }
+
+ var prevType = typeof prevElement;
+ var nextType = typeof nextElement;
+ if (prevType === 'string' || prevType === 'number') {
+ return nextType === 'string' || nextType === 'number';
+ } else {
+ return nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key;
+ }
+ return false;
+}
+
+module.exports = shouldUpdateReactComponent;
+},{}],142:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule traverseAllChildren
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactElement = _dereq_(57);
+var ReactInstanceHandles = _dereq_(67);
+
+var getIteratorFn = _dereq_(129);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+var SEPARATOR = ReactInstanceHandles.SEPARATOR;
+var SUBSEPARATOR = ':';
+
+/**
+ * TODO: Test that a single child and an array with one item have the same key
+ * pattern.
+ */
+
+var userProvidedKeyEscaperLookup = {
+ '=': '=0',
+ '.': '=1',
+ ':': '=2'
+};
+
+var userProvidedKeyEscapeRegex = /[=.:]/g;
+
+var didWarnAboutMaps = false;
+
+function userProvidedKeyEscaper(match) {
+ return userProvidedKeyEscaperLookup[match];
+}
+
+/**
+ * Generate a key string that identifies a component within a set.
+ *
+ * @param {*} component A component that could contain a manual key.
+ * @param {number} index Index that is used if a manual key is not provided.
+ * @return {string}
+ */
+function getComponentKey(component, index) {
+ if (component && component.key != null) {
+ // Explicit key
+ return wrapUserProvidedKey(component.key);
+ }
+ // Implicit key determined by the index in the set
+ return index.toString(36);
+}
+
+/**
+ * Escape a component key so that it is safe to use in a reactid.
+ *
+ * @param {*} text Component key to be escaped.
+ * @return {string} An escaped string.
+ */
+function escapeUserProvidedKey(text) {
+ return ('' + text).replace(userProvidedKeyEscapeRegex, userProvidedKeyEscaper);
+}
+
+/**
+ * Wrap a `key` value explicitly provided by the user to distinguish it from
+ * implicitly-generated keys generated by a component's index in its parent.
+ *
+ * @param {string} key Value of a user-provided `key` attribute
+ * @return {string}
+ */
+function wrapUserProvidedKey(key) {
+ return '$' + escapeUserProvidedKey(key);
+}
+
+/**
+ * @param {?*} children Children tree container.
+ * @param {!string} nameSoFar Name of the key path so far.
+ * @param {!function} callback Callback to invoke with each child found.
+ * @param {?*} traverseContext Used to pass information throughout the traversal
+ * process.
+ * @return {!number} The number of children in this subtree.
+ */
+function traverseAllChildrenImpl(children, nameSoFar, callback, traverseContext) {
+ var type = typeof children;
+
+ if (type === 'undefined' || type === 'boolean') {
+ // All of the above are perceived as null.
+ children = null;
+ }
+
+ if (children === null || type === 'string' || type === 'number' || ReactElement.isValidElement(children)) {
+ callback(traverseContext, children,
+ // If it's the only child, treat the name as if it was wrapped in an array
+ // so that it's consistent if the number of children grows.
+ nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar);
+ return 1;
+ }
+
+ var child;
+ var nextName;
+ var subtreeCount = 0; // Count of children found in the current subtree.
+ var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
+
+ if (Array.isArray(children)) {
+ for (var i = 0; i < children.length; i++) {
+ child = children[i];
+ nextName = nextNamePrefix + getComponentKey(child, i);
+ subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
+ }
+ } else {
+ var iteratorFn = getIteratorFn(children);
+ if (iteratorFn) {
+ var iterator = iteratorFn.call(children);
+ var step;
+ if (iteratorFn !== children.entries) {
+ var ii = 0;
+ while (!(step = iterator.next()).done) {
+ child = step.value;
+ nextName = nextNamePrefix + getComponentKey(child, ii++);
+ subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
+ }
+ } else {
+ if ("development" !== 'production') {
+ "development" !== 'production' ? warning(didWarnAboutMaps, 'Using Maps as children is not yet fully supported. It is an ' + 'experimental feature that might be removed. Convert it to a ' + 'sequence / iterable of keyed ReactElements instead.') : undefined;
+ didWarnAboutMaps = true;
+ }
+ // Iterator will provide entry [k,v] tuples rather than values.
+ while (!(step = iterator.next()).done) {
+ var entry = step.value;
+ if (entry) {
+ child = entry[1];
+ nextName = nextNamePrefix + wrapUserProvidedKey(entry[0]) + SUBSEPARATOR + getComponentKey(child, 0);
+ subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
+ }
+ }
+ }
+ } else if (type === 'object') {
+ var addendum = '';
+ if ("development" !== 'production') {
+ addendum = ' If you meant to render a collection of children, use an array ' + 'instead or wrap the object using createFragment(object) from the ' + 'React add-ons.';
+ if (children._isReactElement) {
+ addendum = ' It looks like you\'re using an element created by a different ' + 'version of React. Make sure to use only one copy of React.';
+ }
+ if (ReactCurrentOwner.current) {
+ var name = ReactCurrentOwner.current.getName();
+ if (name) {
+ addendum += ' Check the render method of `' + name + '`.';
+ }
+ }
+ }
+ var childrenString = String(children);
+ !false ? "development" !== 'production' ? invariant(false, 'Objects are not valid as a React child (found: %s).%s', childrenString === '[object Object]' ? 'object with keys {' + Object.keys(children).join(', ') + '}' : childrenString, addendum) : invariant(false) : undefined;
+ }
+ }
+
+ return subtreeCount;
+}
+
+/**
+ * Traverses children that are typically specified as `props.children`, but
+ * might also be specified through attributes:
+ *
+ * - `traverseAllChildren(this.props.children, ...)`
+ * - `traverseAllChildren(this.props.leftPanelChildren, ...)`
+ *
+ * The `traverseContext` is an optional argument that is passed through the
+ * entire traversal. It can be used to store accumulations or anything else that
+ * the callback might find relevant.
+ *
+ * @param {?*} children Children tree object.
+ * @param {!function} callback To invoke upon traversing each child.
+ * @param {?*} traverseContext Context for traversal.
+ * @return {!number} The number of children in this subtree.
+ */
+function traverseAllChildren(children, callback, traverseContext) {
+ if (children == null) {
+ return 0;
+ }
+
+ return traverseAllChildrenImpl(children, '', callback, traverseContext);
+}
+
+module.exports = traverseAllChildren;
+},{"129":129,"161":161,"173":173,"39":39,"57":57,"67":67}],143:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule update
+ */
+
+/* global hasOwnProperty:true */
+
+'use strict';
+
+var assign = _dereq_(24);
+var keyOf = _dereq_(166);
+var invariant = _dereq_(161);
+var hasOwnProperty = ({}).hasOwnProperty;
+
+function shallowCopy(x) {
+ if (Array.isArray(x)) {
+ return x.concat();
+ } else if (x && typeof x === 'object') {
+ return assign(new x.constructor(), x);
+ } else {
+ return x;
+ }
+}
+
+var COMMAND_PUSH = keyOf({ $push: null });
+var COMMAND_UNSHIFT = keyOf({ $unshift: null });
+var COMMAND_SPLICE = keyOf({ $splice: null });
+var COMMAND_SET = keyOf({ $set: null });
+var COMMAND_MERGE = keyOf({ $merge: null });
+var COMMAND_APPLY = keyOf({ $apply: null });
+
+var ALL_COMMANDS_LIST = [COMMAND_PUSH, COMMAND_UNSHIFT, COMMAND_SPLICE, COMMAND_SET, COMMAND_MERGE, COMMAND_APPLY];
+
+var ALL_COMMANDS_SET = {};
+
+ALL_COMMANDS_LIST.forEach(function (command) {
+ ALL_COMMANDS_SET[command] = true;
+});
+
+function invariantArrayCase(value, spec, command) {
+ !Array.isArray(value) ? "development" !== 'production' ? invariant(false, 'update(): expected target of %s to be an array; got %s.', command, value) : invariant(false) : undefined;
+ var specValue = spec[command];
+ !Array.isArray(specValue) ? "development" !== 'production' ? invariant(false, 'update(): expected spec of %s to be an array; got %s. ' + 'Did you forget to wrap your parameter in an array?', command, specValue) : invariant(false) : undefined;
+}
+
+function update(value, spec) {
+ !(typeof spec === 'object') ? "development" !== 'production' ? invariant(false, 'update(): You provided a key path to update() that did not contain one ' + 'of %s. Did you forget to include {%s: ...}?', ALL_COMMANDS_LIST.join(', '), COMMAND_SET) : invariant(false) : undefined;
+
+ if (hasOwnProperty.call(spec, COMMAND_SET)) {
+ !(Object.keys(spec).length === 1) ? "development" !== 'production' ? invariant(false, 'Cannot have more than one key in an object with %s', COMMAND_SET) : invariant(false) : undefined;
+
+ return spec[COMMAND_SET];
+ }
+
+ var nextValue = shallowCopy(value);
+
+ if (hasOwnProperty.call(spec, COMMAND_MERGE)) {
+ var mergeObj = spec[COMMAND_MERGE];
+ !(mergeObj && typeof mergeObj === 'object') ? "development" !== 'production' ? invariant(false, 'update(): %s expects a spec of type \'object\'; got %s', COMMAND_MERGE, mergeObj) : invariant(false) : undefined;
+ !(nextValue && typeof nextValue === 'object') ? "development" !== 'production' ? invariant(false, 'update(): %s expects a target of type \'object\'; got %s', COMMAND_MERGE, nextValue) : invariant(false) : undefined;
+ assign(nextValue, spec[COMMAND_MERGE]);
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_PUSH)) {
+ invariantArrayCase(value, spec, COMMAND_PUSH);
+ spec[COMMAND_PUSH].forEach(function (item) {
+ nextValue.push(item);
+ });
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_UNSHIFT)) {
+ invariantArrayCase(value, spec, COMMAND_UNSHIFT);
+ spec[COMMAND_UNSHIFT].forEach(function (item) {
+ nextValue.unshift(item);
+ });
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_SPLICE)) {
+ !Array.isArray(value) ? "development" !== 'production' ? invariant(false, 'Expected %s target to be an array; got %s', COMMAND_SPLICE, value) : invariant(false) : undefined;
+ !Array.isArray(spec[COMMAND_SPLICE]) ? "development" !== 'production' ? invariant(false, 'update(): expected spec of %s to be an array of arrays; got %s. ' + 'Did you forget to wrap your parameters in an array?', COMMAND_SPLICE, spec[COMMAND_SPLICE]) : invariant(false) : undefined;
+ spec[COMMAND_SPLICE].forEach(function (args) {
+ !Array.isArray(args) ? "development" !== 'production' ? invariant(false, 'update(): expected spec of %s to be an array of arrays; got %s. ' + 'Did you forget to wrap your parameters in an array?', COMMAND_SPLICE, spec[COMMAND_SPLICE]) : invariant(false) : undefined;
+ nextValue.splice.apply(nextValue, args);
+ });
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_APPLY)) {
+ !(typeof spec[COMMAND_APPLY] === 'function') ? "development" !== 'production' ? invariant(false, 'update(): expected spec of %s to be a function; got %s.', COMMAND_APPLY, spec[COMMAND_APPLY]) : invariant(false) : undefined;
+ nextValue = spec[COMMAND_APPLY](nextValue);
+ }
+
+ for (var k in spec) {
+ if (!(ALL_COMMANDS_SET.hasOwnProperty(k) && ALL_COMMANDS_SET[k])) {
+ nextValue[k] = update(value[k], spec[k]);
+ }
+ }
+
+ return nextValue;
+}
+
+module.exports = update;
+},{"161":161,"166":166,"24":24}],144:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule validateDOMNesting
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+var warning = _dereq_(173);
+
+var validateDOMNesting = emptyFunction;
+
+if ("development" !== 'production') {
+ // This validation code was written based on the HTML5 parsing spec:
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
+ //
+ // Note: this does not catch all invalid nesting, nor does it try to (as it's
+ // not clear what practical benefit doing so provides); instead, we warn only
+ // for cases where the parser will give a parse tree differing from what React
+ // intended. For example, <b><div></div></b> is invalid but we don't warn
+ // because it still parses correctly; we do warn for other cases like nested
+ // <p> tags where the beginning of the second element implicitly closes the
+ // first, causing a confusing mess.
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#special
+ var specialTags = ['address', 'applet', 'area', 'article', 'aside', 'base', 'basefont', 'bgsound', 'blockquote', 'body', 'br', 'button', 'caption', 'center', 'col', 'colgroup', 'dd', 'details', 'dir', 'div', 'dl', 'dt', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'iframe', 'img', 'input', 'isindex', 'li', 'link', 'listing', 'main', 'marquee', 'menu', 'menuitem', 'meta', 'nav', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'p', 'param', 'plaintext', 'pre', 'script', 'section', 'select', 'source', 'style', 'summary', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'track', 'ul', 'wbr', 'xmp'];
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
+ var inScopeTags = ['applet', 'caption', 'html', 'table', 'td', 'th', 'marquee', 'object', 'template',
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point
+ // TODO: Distinguish by namespace here -- for <title>, including it here
+ // errs on the side of fewer warnings
+ 'foreignObject', 'desc', 'title'];
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope
+ var buttonScopeTags = inScopeTags.concat(['button']);
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
+ var impliedEndTags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
+
+ var emptyAncestorInfo = {
+ parentTag: null,
+
+ formTag: null,
+ aTagInScope: null,
+ buttonTagInScope: null,
+ nobrTagInScope: null,
+ pTagInButtonScope: null,
+
+ listItemTagAutoclosing: null,
+ dlItemTagAutoclosing: null
+ };
+
+ var updatedAncestorInfo = function (oldInfo, tag, instance) {
+ var ancestorInfo = assign({}, oldInfo || emptyAncestorInfo);
+ var info = { tag: tag, instance: instance };
+
+ if (inScopeTags.indexOf(tag) !== -1) {
+ ancestorInfo.aTagInScope = null;
+ ancestorInfo.buttonTagInScope = null;
+ ancestorInfo.nobrTagInScope = null;
+ }
+ if (buttonScopeTags.indexOf(tag) !== -1) {
+ ancestorInfo.pTagInButtonScope = null;
+ }
+
+ // See rules for 'li', 'dd', 'dt' start tags in
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
+ if (specialTags.indexOf(tag) !== -1 && tag !== 'address' && tag !== 'div' && tag !== 'p') {
+ ancestorInfo.listItemTagAutoclosing = null;
+ ancestorInfo.dlItemTagAutoclosing = null;
+ }
+
+ ancestorInfo.parentTag = info;
+
+ if (tag === 'form') {
+ ancestorInfo.formTag = info;
+ }
+ if (tag === 'a') {
+ ancestorInfo.aTagInScope = info;
+ }
+ if (tag === 'button') {
+ ancestorInfo.buttonTagInScope = info;
+ }
+ if (tag === 'nobr') {
+ ancestorInfo.nobrTagInScope = info;
+ }
+ if (tag === 'p') {
+ ancestorInfo.pTagInButtonScope = info;
+ }
+ if (tag === 'li') {
+ ancestorInfo.listItemTagAutoclosing = info;
+ }
+ if (tag === 'dd' || tag === 'dt') {
+ ancestorInfo.dlItemTagAutoclosing = info;
+ }
+
+ return ancestorInfo;
+ };
+
+ /**
+ * Returns whether
+ */
+ var isTagValidWithParent = function (tag, parentTag) {
+ // First, let's check if we're in an unusual parsing mode...
+ switch (parentTag) {
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
+ case 'select':
+ return tag === 'option' || tag === 'optgroup' || tag === '#text';
+ case 'optgroup':
+ return tag === 'option' || tag === '#text';
+ // Strictly speaking, seeing an <option> doesn't mean we're in a <select>
+ // but
+ case 'option':
+ return tag === '#text';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
+ // No special behavior since these rules fall back to "in body" mode for
+ // all except special table nodes which cause bad parsing behavior anyway.
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
+ case 'tr':
+ return tag === 'th' || tag === 'td' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
+ case 'tbody':
+ case 'thead':
+ case 'tfoot':
+ return tag === 'tr' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
+ case 'colgroup':
+ return tag === 'col' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
+ case 'table':
+ return tag === 'caption' || tag === 'colgroup' || tag === 'tbody' || tag === 'tfoot' || tag === 'thead' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
+ case 'head':
+ return tag === 'base' || tag === 'basefont' || tag === 'bgsound' || tag === 'link' || tag === 'meta' || tag === 'title' || tag === 'noscript' || tag === 'noframes' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
+ case 'html':
+ return tag === 'head' || tag === 'body';
+ }
+
+ // Probably in the "in body" parsing mode, so we outlaw only tag combos
+ // where the parsing rules cause implicit opens or closes to be added.
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
+ switch (tag) {
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ return parentTag !== 'h1' && parentTag !== 'h2' && parentTag !== 'h3' && parentTag !== 'h4' && parentTag !== 'h5' && parentTag !== 'h6';
+
+ case 'rp':
+ case 'rt':
+ return impliedEndTags.indexOf(parentTag) === -1;
+
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ case 'frame':
+ case 'head':
+ case 'tbody':
+ case 'td':
+ case 'tfoot':
+ case 'th':
+ case 'thead':
+ case 'tr':
+ // These tags are only valid with a few parents that have special child
+ // parsing rules -- if we're down here, then none of those matched and
+ // so we allow it only if we don't know what the parent is, as all other
+ // cases are invalid.
+ return parentTag == null;
+ }
+
+ return true;
+ };
+
+ /**
+ * Returns whether
+ */
+ var findInvalidAncestorForTag = function (tag, ancestorInfo) {
+ switch (tag) {
+ case 'address':
+ case 'article':
+ case 'aside':
+ case 'blockquote':
+ case 'center':
+ case 'details':
+ case 'dialog':
+ case 'dir':
+ case 'div':
+ case 'dl':
+ case 'fieldset':
+ case 'figcaption':
+ case 'figure':
+ case 'footer':
+ case 'header':
+ case 'hgroup':
+ case 'main':
+ case 'menu':
+ case 'nav':
+ case 'ol':
+ case 'p':
+ case 'section':
+ case 'summary':
+ case 'ul':
+
+ case 'pre':
+ case 'listing':
+
+ case 'table':
+
+ case 'hr':
+
+ case 'xmp':
+
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ return ancestorInfo.pTagInButtonScope;
+
+ case 'form':
+ return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;
+
+ case 'li':
+ return ancestorInfo.listItemTagAutoclosing;
+
+ case 'dd':
+ case 'dt':
+ return ancestorInfo.dlItemTagAutoclosing;
+
+ case 'button':
+ return ancestorInfo.buttonTagInScope;
+
+ case 'a':
+ // Spec says something about storing a list of markers, but it sounds
+ // equivalent to this check.
+ return ancestorInfo.aTagInScope;
+
+ case 'nobr':
+ return ancestorInfo.nobrTagInScope;
+ }
+
+ return null;
+ };
+
+ /**
+ * Given a ReactCompositeComponent instance, return a list of its recursive
+ * owners, starting at the root and ending with the instance itself.
+ */
+ var findOwnerStack = function (instance) {
+ if (!instance) {
+ return [];
+ }
+
+ var stack = [];
+ /*eslint-disable space-after-keywords */
+ do {
+ /*eslint-enable space-after-keywords */
+ stack.push(instance);
+ } while (instance = instance._currentElement._owner);
+ stack.reverse();
+ return stack;
+ };
+
+ var didWarn = {};
+
+ validateDOMNesting = function (childTag, childInstance, ancestorInfo) {
+ ancestorInfo = ancestorInfo || emptyAncestorInfo;
+ var parentInfo = ancestorInfo.parentTag;
+ var parentTag = parentInfo && parentInfo.tag;
+
+ var invalidParent = isTagValidWithParent(childTag, parentTag) ? null : parentInfo;
+ var invalidAncestor = invalidParent ? null : findInvalidAncestorForTag(childTag, ancestorInfo);
+ var problematic = invalidParent || invalidAncestor;
+
+ if (problematic) {
+ var ancestorTag = problematic.tag;
+ var ancestorInstance = problematic.instance;
+
+ var childOwner = childInstance && childInstance._currentElement._owner;
+ var ancestorOwner = ancestorInstance && ancestorInstance._currentElement._owner;
+
+ var childOwners = findOwnerStack(childOwner);
+ var ancestorOwners = findOwnerStack(ancestorOwner);
+
+ var minStackLen = Math.min(childOwners.length, ancestorOwners.length);
+ var i;
+
+ var deepestCommon = -1;
+ for (i = 0; i < minStackLen; i++) {
+ if (childOwners[i] === ancestorOwners[i]) {
+ deepestCommon = i;
+ } else {
+ break;
+ }
+ }
+
+ var UNKNOWN = '(unknown)';
+ var childOwnerNames = childOwners.slice(deepestCommon + 1).map(function (inst) {
+ return inst.getName() || UNKNOWN;
+ });
+ var ancestorOwnerNames = ancestorOwners.slice(deepestCommon + 1).map(function (inst) {
+ return inst.getName() || UNKNOWN;
+ });
+ var ownerInfo = [].concat(
+ // If the parent and child instances have a common owner ancestor, start
+ // with that -- otherwise we just start with the parent's owners.
+ deepestCommon !== -1 ? childOwners[deepestCommon].getName() || UNKNOWN : [], ancestorOwnerNames, ancestorTag,
+ // If we're warning about an invalid (non-parent) ancestry, add '...'
+ invalidAncestor ? ['...'] : [], childOwnerNames, childTag).join(' > ');
+
+ var warnKey = !!invalidParent + '|' + childTag + '|' + ancestorTag + '|' + ownerInfo;
+ if (didWarn[warnKey]) {
+ return;
+ }
+ didWarn[warnKey] = true;
+
+ if (invalidParent) {
+ var info = '';
+ if (ancestorTag === 'table' && childTag === 'tr') {
+ info += ' Add a <tbody> to your code to match the DOM tree generated by ' + 'the browser.';
+ }
+ "development" !== 'production' ? warning(false, 'validateDOMNesting(...): <%s> cannot appear as a child of <%s>. ' + 'See %s.%s', childTag, ancestorTag, ownerInfo, info) : undefined;
+ } else {
+ "development" !== 'production' ? warning(false, 'validateDOMNesting(...): <%s> cannot appear as a descendant of ' + '<%s>. See %s.', childTag, ancestorTag, ownerInfo) : undefined;
+ }
+ }
+ };
+
+ validateDOMNesting.ancestorInfoContextKey = '__validateDOMNesting_ancestorInfo$' + Math.random().toString(36).slice(2);
+
+ validateDOMNesting.updatedAncestorInfo = updatedAncestorInfo;
+
+ // For testing
+ validateDOMNesting.isTagValidInContext = function (tag, ancestorInfo) {
+ ancestorInfo = ancestorInfo || emptyAncestorInfo;
+ var parentInfo = ancestorInfo.parentTag;
+ var parentTag = parentInfo && parentInfo.tag;
+ return isTagValidWithParent(tag, parentTag) && !findInvalidAncestorForTag(tag, ancestorInfo);
+ };
+}
+
+module.exports = validateDOMNesting;
+},{"153":153,"173":173,"24":24}],145:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSCore
+ * @typechecks
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * The CSSCore module specifies the API (and implements most of the methods)
+ * that should be used when dealing with the display of elements (via their
+ * CSS classes and visibility on screen. It is an API focused on mutating the
+ * display and not reading it as no logical state should be encoded in the
+ * display of elements.
+ */
+
+var CSSCore = {
+
+ /**
+ * Adds the class passed in to the element if it doesn't already have it.
+ *
+ * @param {DOMElement} element the element to set the class on
+ * @param {string} className the CSS className
+ * @return {DOMElement} the element passed in
+ */
+ addClass: function (element, className) {
+ !!/\s/.test(className) ? "development" !== 'production' ? invariant(false, 'CSSCore.addClass takes only a single class name. "%s" contains ' + 'multiple classes.', className) : invariant(false) : undefined;
+
+ if (className) {
+ if (element.classList) {
+ element.classList.add(className);
+ } else if (!CSSCore.hasClass(element, className)) {
+ element.className = element.className + ' ' + className;
+ }
+ }
+ return element;
+ },
+
+ /**
+ * Removes the class passed in from the element
+ *
+ * @param {DOMElement} element the element to set the class on
+ * @param {string} className the CSS className
+ * @return {DOMElement} the element passed in
+ */
+ removeClass: function (element, className) {
+ !!/\s/.test(className) ? "development" !== 'production' ? invariant(false, 'CSSCore.removeClass takes only a single class name. "%s" contains ' + 'multiple classes.', className) : invariant(false) : undefined;
+
+ if (className) {
+ if (element.classList) {
+ element.classList.remove(className);
+ } else if (CSSCore.hasClass(element, className)) {
+ element.className = element.className.replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), '$1').replace(/\s+/g, ' ') // multiple spaces to one
+ .replace(/^\s*|\s*$/g, ''); // trim the ends
+ }
+ }
+ return element;
+ },
+
+ /**
+ * Helper to add or remove a class from an element based on a condition.
+ *
+ * @param {DOMElement} element the element to set the class on
+ * @param {string} className the CSS className
+ * @param {*} bool condition to whether to add or remove the class
+ * @return {DOMElement} the element passed in
+ */
+ conditionClass: function (element, className, bool) {
+ return (bool ? CSSCore.addClass : CSSCore.removeClass)(element, className);
+ },
+
+ /**
+ * Tests whether the element has the class specified.
+ *
+ * @param {DOMNode|DOMWindow} element the element to set the class on
+ * @param {string} className the CSS className
+ * @return {boolean} true if the element has the class, false if not
+ */
+ hasClass: function (element, className) {
+ !!/\s/.test(className) ? "development" !== 'production' ? invariant(false, 'CSS.hasClass takes only a single class name.') : invariant(false) : undefined;
+ if (element.classList) {
+ return !!className && element.classList.contains(className);
+ }
+ return (' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1;
+ }
+
+};
+
+module.exports = CSSCore;
+},{"161":161}],146:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @providesModule EventListener
+ * @typechecks
+ */
+
+'use strict';
+
+var emptyFunction = _dereq_(153);
+
+/**
+ * Upstream version of event listener. Does not take into account specific
+ * nature of platform.
+ */
+var EventListener = {
+ /**
+ * Listen to DOM events during the bubble phase.
+ *
+ * @param {DOMEventTarget} target DOM element to register listener on.
+ * @param {string} eventType Event type, e.g. 'click' or 'mouseover'.
+ * @param {function} callback Callback function.
+ * @return {object} Object with a `remove` method.
+ */
+ listen: function (target, eventType, callback) {
+ if (target.addEventListener) {
+ target.addEventListener(eventType, callback, false);
+ return {
+ remove: function () {
+ target.removeEventListener(eventType, callback, false);
+ }
+ };
+ } else if (target.attachEvent) {
+ target.attachEvent('on' + eventType, callback);
+ return {
+ remove: function () {
+ target.detachEvent('on' + eventType, callback);
+ }
+ };
+ }
+ },
+
+ /**
+ * Listen to DOM events during the capture phase.
+ *
+ * @param {DOMEventTarget} target DOM element to register listener on.
+ * @param {string} eventType Event type, e.g. 'click' or 'mouseover'.
+ * @param {function} callback Callback function.
+ * @return {object} Object with a `remove` method.
+ */
+ capture: function (target, eventType, callback) {
+ if (target.addEventListener) {
+ target.addEventListener(eventType, callback, true);
+ return {
+ remove: function () {
+ target.removeEventListener(eventType, callback, true);
+ }
+ };
+ } else {
+ if ("development" !== 'production') {
+ console.error('Attempted to listen to events during the capture phase on a ' + 'browser that does not support the capture phase. Your application ' + 'will not receive some events.');
+ }
+ return {
+ remove: emptyFunction
+ };
+ }
+ },
+
+ registerDefault: function () {}
+};
+
+module.exports = EventListener;
+},{"153":153}],147:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ExecutionEnvironment
+ */
+
+'use strict';
+
+var canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
+
+/**
+ * Simple, lightweight module assisting with the detection and context of
+ * Worker. Helps avoid circular dependencies and allows code to reason about
+ * whether or not they are in a Worker, even if they never include the main
+ * `ReactWorker` dependency.
+ */
+var ExecutionEnvironment = {
+
+ canUseDOM: canUseDOM,
+
+ canUseWorkers: typeof Worker !== 'undefined',
+
+ canUseEventListeners: canUseDOM && !!(window.addEventListener || window.attachEvent),
+
+ canUseViewport: canUseDOM && !!window.screen,
+
+ isInWorker: !canUseDOM // For now, this is true - might change in the future.
+
+};
+
+module.exports = ExecutionEnvironment;
+},{}],148:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule camelize
+ * @typechecks
+ */
+
+"use strict";
+
+var _hyphenPattern = /-(.)/g;
+
+/**
+ * Camelcases a hyphenated string, for example:
+ *
+ * > camelize('background-color')
+ * < "backgroundColor"
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function camelize(string) {
+ return string.replace(_hyphenPattern, function (_, character) {
+ return character.toUpperCase();
+ });
+}
+
+module.exports = camelize;
+},{}],149:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule camelizeStyleName
+ * @typechecks
+ */
+
+'use strict';
+
+var camelize = _dereq_(148);
+
+var msPattern = /^-ms-/;
+
+/**
+ * Camelcases a hyphenated CSS property name, for example:
+ *
+ * > camelizeStyleName('background-color')
+ * < "backgroundColor"
+ * > camelizeStyleName('-moz-transition')
+ * < "MozTransition"
+ * > camelizeStyleName('-ms-transition')
+ * < "msTransition"
+ *
+ * As Andi Smith suggests
+ * (http://www.andismith.com/blog/2012/02/modernizr-prefixed/), an `-ms` prefix
+ * is converted to lowercase `ms`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function camelizeStyleName(string) {
+ return camelize(string.replace(msPattern, 'ms-'));
+}
+
+module.exports = camelizeStyleName;
+},{"148":148}],150:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule containsNode
+ * @typechecks
+ */
+
+'use strict';
+
+var isTextNode = _dereq_(163);
+
+/*eslint-disable no-bitwise */
+
+/**
+ * Checks if a given DOM node contains or is another DOM node.
+ *
+ * @param {?DOMNode} outerNode Outer DOM node.
+ * @param {?DOMNode} innerNode Inner DOM node.
+ * @return {boolean} True if `outerNode` contains or is `innerNode`.
+ */
+function containsNode(_x, _x2) {
+ var _again = true;
+
+ _function: while (_again) {
+ var outerNode = _x,
+ innerNode = _x2;
+ _again = false;
+
+ if (!outerNode || !innerNode) {
+ return false;
+ } else if (outerNode === innerNode) {
+ return true;
+ } else if (isTextNode(outerNode)) {
+ return false;
+ } else if (isTextNode(innerNode)) {
+ _x = outerNode;
+ _x2 = innerNode.parentNode;
+ _again = true;
+ continue _function;
+ } else if (outerNode.contains) {
+ return outerNode.contains(innerNode);
+ } else if (outerNode.compareDocumentPosition) {
+ return !!(outerNode.compareDocumentPosition(innerNode) & 16);
+ } else {
+ return false;
+ }
+ }
+}
+
+module.exports = containsNode;
+},{"163":163}],151:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule createArrayFromMixed
+ * @typechecks
+ */
+
+'use strict';
+
+var toArray = _dereq_(172);
+
+/**
+ * Perform a heuristic test to determine if an object is "array-like".
+ *
+ * A monk asked Joshu, a Zen master, "Has a dog Buddha nature?"
+ * Joshu replied: "Mu."
+ *
+ * This function determines if its argument has "array nature": it returns
+ * true if the argument is an actual array, an `arguments' object, or an
+ * HTMLCollection (e.g. node.childNodes or node.getElementsByTagName()).
+ *
+ * It will return false for other array-like objects like Filelist.
+ *
+ * @param {*} obj
+ * @return {boolean}
+ */
+function hasArrayNature(obj) {
+ return(
+ // not null/false
+ !!obj && (
+ // arrays are objects, NodeLists are functions in Safari
+ typeof obj == 'object' || typeof obj == 'function') &&
+ // quacks like an array
+ 'length' in obj &&
+ // not window
+ !('setInterval' in obj) &&
+ // no DOM node should be considered an array-like
+ // a 'select' element has 'length' and 'item' properties on IE8
+ typeof obj.nodeType != 'number' && (
+ // a real array
+ Array.isArray(obj) ||
+ // arguments
+ 'callee' in obj ||
+ // HTMLCollection/NodeList
+ 'item' in obj)
+ );
+}
+
+/**
+ * Ensure that the argument is an array by wrapping it in an array if it is not.
+ * Creates a copy of the argument if it is already an array.
+ *
+ * This is mostly useful idiomatically:
+ *
+ * var createArrayFromMixed = require('createArrayFromMixed');
+ *
+ * function takesOneOrMoreThings(things) {
+ * things = createArrayFromMixed(things);
+ * ...
+ * }
+ *
+ * This allows you to treat `things' as an array, but accept scalars in the API.
+ *
+ * If you need to convert an array-like object, like `arguments`, into an array
+ * use toArray instead.
+ *
+ * @param {*} obj
+ * @return {array}
+ */
+function createArrayFromMixed(obj) {
+ if (!hasArrayNature(obj)) {
+ return [obj];
+ } else if (Array.isArray(obj)) {
+ return obj.slice();
+ } else {
+ return toArray(obj);
+ }
+}
+
+module.exports = createArrayFromMixed;
+},{"172":172}],152:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule createNodesFromMarkup
+ * @typechecks
+ */
+
+/*eslint-disable fb-www/unsafe-html*/
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var createArrayFromMixed = _dereq_(151);
+var getMarkupWrap = _dereq_(157);
+var invariant = _dereq_(161);
+
+/**
+ * Dummy container used to render all markup.
+ */
+var dummyNode = ExecutionEnvironment.canUseDOM ? document.createElementNS('http://www.w3.org/1999/xhtml', 'div') : null;
+
+/**
+ * Pattern used by `getNodeName`.
+ */
+var nodeNamePattern = /^\s*<(\w+)/;
+
+/**
+ * Extracts the `nodeName` of the first element in a string of markup.
+ *
+ * @param {string} markup String of markup.
+ * @return {?string} Node name of the supplied markup.
+ */
+function getNodeName(markup) {
+ var nodeNameMatch = markup.match(nodeNamePattern);
+ return nodeNameMatch && nodeNameMatch[1].toLowerCase();
+}
+
+/**
+ * Creates an array containing the nodes rendered from the supplied markup. The
+ * optionally supplied `handleScript` function will be invoked once for each
+ * <script> element that is rendered. If no `handleScript` function is supplied,
+ * an exception is thrown if any <script> elements are rendered.
+ *
+ * @param {string} markup A string of valid HTML markup.
+ * @param {?function} handleScript Invoked once for each rendered <script>.
+ * @return {array<DOMElement|DOMTextNode>} An array of rendered nodes.
+ */
+function createNodesFromMarkup(markup, handleScript) {
+ var node = dummyNode;
+ !!!dummyNode ? "development" !== 'production' ? invariant(false, 'createNodesFromMarkup dummy not initialized') : invariant(false) : undefined;
+ var nodeName = getNodeName(markup);
+
+ var wrap = nodeName && getMarkupWrap(nodeName);
+ if (wrap) {
+ node.innerHTML = wrap[1] + markup + wrap[2];
+
+ var wrapDepth = wrap[0];
+ while (wrapDepth--) {
+ node = node.lastChild;
+ }
+ } else {
+ node.innerHTML = markup;
+ }
+
+ var scripts = node.getElementsByTagName('script');
+ if (scripts.length) {
+ !handleScript ? "development" !== 'production' ? invariant(false, 'createNodesFromMarkup(...): Unexpected <script> element rendered.') : invariant(false) : undefined;
+ createArrayFromMixed(scripts).forEach(handleScript);
+ }
+
+ var nodes = createArrayFromMixed(node.childNodes);
+ while (node.lastChild) {
+ node.removeChild(node.lastChild);
+ }
+ return nodes;
+}
+
+module.exports = createNodesFromMarkup;
+
+},{"147":147,"151":151,"157":157,"161":161}],153:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule emptyFunction
+ */
+
+"use strict";
+
+function makeEmptyFunction(arg) {
+ return function () {
+ return arg;
+ };
+}
+
+/**
+ * This function accepts and discards inputs; it has no side effects. This is
+ * primarily useful idiomatically for overridable function endpoints which
+ * always need to be callable, since JS lacks a null-call idiom ala Cocoa.
+ */
+function emptyFunction() {}
+
+emptyFunction.thatReturns = makeEmptyFunction;
+emptyFunction.thatReturnsFalse = makeEmptyFunction(false);
+emptyFunction.thatReturnsTrue = makeEmptyFunction(true);
+emptyFunction.thatReturnsNull = makeEmptyFunction(null);
+emptyFunction.thatReturnsThis = function () {
+ return this;
+};
+emptyFunction.thatReturnsArgument = function (arg) {
+ return arg;
+};
+
+module.exports = emptyFunction;
+},{}],154:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule emptyObject
+ */
+
+'use strict';
+
+var emptyObject = {};
+
+if ("development" !== 'production') {
+ Object.freeze(emptyObject);
+}
+
+module.exports = emptyObject;
+},{}],155:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule focusNode
+ */
+
+'use strict';
+
+/**
+ * @param {DOMElement} node input/textarea to focus
+ */
+function focusNode(node) {
+ // IE8 can throw "Can't move focus to the control because it is invisible,
+ // not enabled, or of a type that does not accept the focus." for all kinds of
+ // reasons that are too expensive and fragile to test.
+ try {
+ node.focus();
+ } catch (e) {}
+}
+
+module.exports = focusNode;
+},{}],156:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getActiveElement
+ * @typechecks
+ */
+
+/* eslint-disable fb-www/typeof-undefined */
+
+/**
+ * Same as document.activeElement but wraps in a try-catch block. In IE it is
+ * not safe to call document.activeElement if there is nothing focused.
+ *
+ * The activeElement will be null only if the document or document body is not
+ * yet defined.
+ */
+'use strict';
+
+function getActiveElement() /*?DOMElement*/{
+ if (typeof document === 'undefined') {
+ return null;
+ }
+ try {
+ return document.activeElement || document.body;
+ } catch (e) {
+ return document.body;
+ }
+}
+
+module.exports = getActiveElement;
+},{}],157:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getMarkupWrap
+ */
+
+/*eslint-disable fb-www/unsafe-html */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var invariant = _dereq_(161);
+
+/**
+ * Dummy container used to detect which wraps are necessary.
+ */
+var dummyNode = ExecutionEnvironment.canUseDOM ? document.createElementNS('http://www.w3.org/1999/xhtml', 'div') : null;
+
+/**
+ * Some browsers cannot use `innerHTML` to render certain elements standalone,
+ * so we wrap them, render the wrapped nodes, then extract the desired node.
+ *
+ * In IE8, certain elements cannot render alone, so wrap all elements ('*').
+ */
+
+var shouldWrap = {};
+
+var selectWrap = [1, '<select multiple="true">', '</select>'];
+var tableWrap = [1, '<table>', '</table>'];
+var trWrap = [3, '<table><tbody><tr>', '</tr></tbody></table>'];
+
+var svgWrap = [1, '<svg xmlns="http://www.w3.org/2000/svg">', '</svg>'];
+
+var markupWrap = {
+ '*': [1, '?<div>', '</div>'],
+
+ 'area': [1, '<map>', '</map>'],
+ 'col': [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
+ 'legend': [1, '<fieldset>', '</fieldset>'],
+ 'param': [1, '<object>', '</object>'],
+ 'tr': [2, '<table><tbody>', '</tbody></table>'],
+
+ 'optgroup': selectWrap,
+ 'option': selectWrap,
+
+ 'caption': tableWrap,
+ 'colgroup': tableWrap,
+ 'tbody': tableWrap,
+ 'tfoot': tableWrap,
+ 'thead': tableWrap,
+
+ 'td': trWrap,
+ 'th': trWrap
+};
+
+// Initialize the SVG elements since we know they'll always need to be wrapped
+// consistently. If they are created inside a <div> they will be initialized in
+// the wrong namespace (and will not display).
+var svgElements = ['circle', 'clipPath', 'defs', 'ellipse', 'g', 'image', 'line', 'linearGradient', 'mask', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', 'rect', 'stop', 'text', 'tspan'];
+svgElements.forEach(function (nodeName) {
+ markupWrap[nodeName] = svgWrap;
+ shouldWrap[nodeName] = true;
+});
+
+/**
+ * Gets the markup wrap configuration for the supplied `nodeName`.
+ *
+ * NOTE: This lazily detects which wraps are necessary for the current browser.
+ *
+ * @param {string} nodeName Lowercase `nodeName`.
+ * @return {?array} Markup wrap configuration, if applicable.
+ */
+function getMarkupWrap(nodeName) {
+ !!!dummyNode ? "development" !== 'production' ? invariant(false, 'Markup wrapping node not initialized') : invariant(false) : undefined;
+ if (!markupWrap.hasOwnProperty(nodeName)) {
+ nodeName = '*';
+ }
+ if (!shouldWrap.hasOwnProperty(nodeName)) {
+ if (nodeName === '*') {
+ dummyNode.innerHTML = '<link />';
+ } else {
+ dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';
+ }
+ shouldWrap[nodeName] = !dummyNode.firstChild;
+ }
+ return shouldWrap[nodeName] ? markupWrap[nodeName] : null;
+}
+
+module.exports = getMarkupWrap;
+},{"147":147,"161":161}],158:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getUnboundedScrollPosition
+ * @typechecks
+ */
+
+'use strict';
+
+/**
+ * Gets the scroll position of the supplied element or window.
+ *
+ * The return values are unbounded, unlike `getScrollPosition`. This means they
+ * may be negative or exceed the element boundaries (which is possible using
+ * inertial scrolling).
+ *
+ * @param {DOMWindow|DOMElement} scrollable
+ * @return {object} Map with `x` and `y` keys.
+ */
+function getUnboundedScrollPosition(scrollable) {
+ if (scrollable === window) {
+ return {
+ x: window.pageXOffset || document.documentElement.scrollLeft,
+ y: window.pageYOffset || document.documentElement.scrollTop
+ };
+ }
+ return {
+ x: scrollable.scrollLeft,
+ y: scrollable.scrollTop
+ };
+}
+
+module.exports = getUnboundedScrollPosition;
+},{}],159:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule hyphenate
+ * @typechecks
+ */
+
+'use strict';
+
+var _uppercasePattern = /([A-Z])/g;
+
+/**
+ * Hyphenates a camelcased string, for example:
+ *
+ * > hyphenate('backgroundColor')
+ * < "background-color"
+ *
+ * For CSS style names, use `hyphenateStyleName` instead which works properly
+ * with all vendor prefixes, including `ms`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function hyphenate(string) {
+ return string.replace(_uppercasePattern, '-$1').toLowerCase();
+}
+
+module.exports = hyphenate;
+},{}],160:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule hyphenateStyleName
+ * @typechecks
+ */
+
+'use strict';
+
+var hyphenate = _dereq_(159);
+
+var msPattern = /^ms-/;
+
+/**
+ * Hyphenates a camelcased CSS property name, for example:
+ *
+ * > hyphenateStyleName('backgroundColor')
+ * < "background-color"
+ * > hyphenateStyleName('MozTransition')
+ * < "-moz-transition"
+ * > hyphenateStyleName('msTransition')
+ * < "-ms-transition"
+ *
+ * As Modernizr suggests (http://modernizr.com/docs/#prefixed), an `ms` prefix
+ * is converted to `-ms-`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function hyphenateStyleName(string) {
+ return hyphenate(string).replace(msPattern, '-ms-');
+}
+
+module.exports = hyphenateStyleName;
+},{"159":159}],161:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule invariant
+ */
+
+'use strict';
+
+/**
+ * Use invariant() to assert state which your program assumes to be true.
+ *
+ * Provide sprintf-style format (only %s is supported) and arguments
+ * to provide information about what broke and what you were
+ * expecting.
+ *
+ * The invariant message will be stripped in production, but the invariant
+ * will remain to ensure logic does not differ in production.
+ */
+
+function invariant(condition, format, a, b, c, d, e, f) {
+ if ("development" !== 'production') {
+ if (format === undefined) {
+ throw new Error('invariant requires an error message argument');
+ }
+ }
+
+ if (!condition) {
+ var error;
+ if (format === undefined) {
+ error = new Error('Minified exception occurred; use the non-minified dev environment ' + 'for the full error message and additional helpful warnings.');
+ } else {
+ var args = [a, b, c, d, e, f];
+ var argIndex = 0;
+ error = new Error(format.replace(/%s/g, function () {
+ return args[argIndex++];
+ }));
+ error.name = 'Invariant Violation';
+ }
+
+ error.framesToPop = 1; // we don't care about invariant's own frame
+ throw error;
+ }
+}
+
+module.exports = invariant;
+},{}],162:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isNode
+ * @typechecks
+ */
+
+/**
+ * @param {*} object The object to check.
+ * @return {boolean} Whether or not the object is a DOM node.
+ */
+'use strict';
+
+function isNode(object) {
+ return !!(object && (typeof Node === 'function' ? object instanceof Node : typeof object === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string'));
+}
+
+module.exports = isNode;
+},{}],163:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isTextNode
+ * @typechecks
+ */
+
+'use strict';
+
+var isNode = _dereq_(162);
+
+/**
+ * @param {*} object The object to check.
+ * @return {boolean} Whether or not the object is a DOM text node.
+ */
+function isTextNode(object) {
+ return isNode(object) && object.nodeType == 3;
+}
+
+module.exports = isTextNode;
+},{"162":162}],164:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule joinClasses
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Combines multiple className strings into one.
+ * http://jsperf.com/joinclasses-args-vs-array
+ *
+ * @param {...?string} className
+ * @return {string}
+ */
+function joinClasses(className /*, ... */) {
+ if (!className) {
+ className = '';
+ }
+ var nextClass;
+ var argLength = arguments.length;
+ if (argLength > 1) {
+ for (var ii = 1; ii < argLength; ii++) {
+ nextClass = arguments[ii];
+ if (nextClass) {
+ className = (className ? className + ' ' : '') + nextClass;
+ }
+ }
+ }
+ return className;
+}
+
+module.exports = joinClasses;
+},{}],165:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyMirror
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Constructs an enumeration with keys equal to their value.
+ *
+ * For example:
+ *
+ * var COLORS = keyMirror({blue: null, red: null});
+ * var myColor = COLORS.blue;
+ * var isColorValid = !!COLORS[myColor];
+ *
+ * The last line could not be performed if the values of the generated enum were
+ * not equal to their keys.
+ *
+ * Input: {key1: val1, key2: val2}
+ * Output: {key1: key1, key2: key2}
+ *
+ * @param {object} obj
+ * @return {object}
+ */
+var keyMirror = function (obj) {
+ var ret = {};
+ var key;
+ !(obj instanceof Object && !Array.isArray(obj)) ? "development" !== 'production' ? invariant(false, 'keyMirror(...): Argument must be an object.') : invariant(false) : undefined;
+ for (key in obj) {
+ if (!obj.hasOwnProperty(key)) {
+ continue;
+ }
+ ret[key] = key;
+ }
+ return ret;
+};
+
+module.exports = keyMirror;
+},{"161":161}],166:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyOf
+ */
+
+/**
+ * Allows extraction of a minified key. Let's the build system minify keys
+ * without losing the ability to dynamically use key strings as values
+ * themselves. Pass in an object with a single key/val pair and it will return
+ * you the string key of that single record. Suppose you want to grab the
+ * value for a key 'className' inside of an object. Key/val minification may
+ * have aliased that key to be 'xa12'. keyOf({className: null}) will return
+ * 'xa12' in that case. Resolve keys you want to use once at startup time, then
+ * reuse those resolutions.
+ */
+"use strict";
+
+var keyOf = function (oneKeyObj) {
+ var key;
+ for (key in oneKeyObj) {
+ if (!oneKeyObj.hasOwnProperty(key)) {
+ continue;
+ }
+ return key;
+ }
+ return null;
+};
+
+module.exports = keyOf;
+},{}],167:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule mapObject
+ */
+
+'use strict';
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+/**
+ * Executes the provided `callback` once for each enumerable own property in the
+ * object and constructs a new object from the results. The `callback` is
+ * invoked with three arguments:
+ *
+ * - the property value
+ * - the property name
+ * - the object being traversed
+ *
+ * Properties that are added after the call to `mapObject` will not be visited
+ * by `callback`. If the values of existing properties are changed, the value
+ * passed to `callback` will be the value at the time `mapObject` visits them.
+ * Properties that are deleted before being visited are not visited.
+ *
+ * @grep function objectMap()
+ * @grep function objMap()
+ *
+ * @param {?object} object
+ * @param {function} callback
+ * @param {*} context
+ * @return {?object}
+ */
+function mapObject(object, callback, context) {
+ if (!object) {
+ return null;
+ }
+ var result = {};
+ for (var name in object) {
+ if (hasOwnProperty.call(object, name)) {
+ result[name] = callback.call(context, object[name], name, object);
+ }
+ }
+ return result;
+}
+
+module.exports = mapObject;
+},{}],168:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule memoizeStringOnly
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Memoizes the return value of a function that accepts one string argument.
+ *
+ * @param {function} callback
+ * @return {function}
+ */
+function memoizeStringOnly(callback) {
+ var cache = {};
+ return function (string) {
+ if (!cache.hasOwnProperty(string)) {
+ cache[string] = callback.call(this, string);
+ }
+ return cache[string];
+ };
+}
+
+module.exports = memoizeStringOnly;
+},{}],169:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule performance
+ * @typechecks
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var performance;
+
+if (ExecutionEnvironment.canUseDOM) {
+ performance = window.performance || window.msPerformance || window.webkitPerformance;
+}
+
+module.exports = performance || {};
+},{"147":147}],170:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule performanceNow
+ * @typechecks
+ */
+
+'use strict';
+
+var performance = _dereq_(169);
+
+var performanceNow;
+
+/**
+ * Detect if we can use `window.performance.now()` and gracefully fallback to
+ * `Date.now()` if it doesn't exist. We need to support Firefox < 15 for now
+ * because of Facebook's testing infrastructure.
+ */
+if (performance.now) {
+ performanceNow = function () {
+ return performance.now();
+ };
+} else {
+ performanceNow = function () {
+ return Date.now();
+ };
+}
+
+module.exports = performanceNow;
+},{"169":169}],171:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule shallowEqual
+ * @typechecks
+ *
+ */
+
+'use strict';
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+/**
+ * Performs equality by iterating through keys on an object and returning false
+ * when any key has values which are not strictly equal between the arguments.
+ * Returns true when the values of all keys are strictly equal.
+ */
+function shallowEqual(objA, objB) {
+ if (objA === objB) {
+ return true;
+ }
+
+ if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
+ return false;
+ }
+
+ var keysA = Object.keys(objA);
+ var keysB = Object.keys(objB);
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ // Test for A's keys different from B.
+ var bHasOwnProperty = hasOwnProperty.bind(objB);
+ for (var i = 0; i < keysA.length; i++) {
+ if (!bHasOwnProperty(keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+module.exports = shallowEqual;
+},{}],172:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule toArray
+ * @typechecks
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Convert array-like objects to arrays.
+ *
+ * This API assumes the caller knows the contents of the data type. For less
+ * well defined inputs use createArrayFromMixed.
+ *
+ * @param {object|function|filelist} obj
+ * @return {array}
+ */
+function toArray(obj) {
+ var length = obj.length;
+
+ // Some browse builtin objects can report typeof 'function' (e.g. NodeList in
+ // old versions of Safari).
+ !(!Array.isArray(obj) && (typeof obj === 'object' || typeof obj === 'function')) ? "development" !== 'production' ? invariant(false, 'toArray: Array-like object expected') : invariant(false) : undefined;
+
+ !(typeof length === 'number') ? "development" !== 'production' ? invariant(false, 'toArray: Object needs a length property') : invariant(false) : undefined;
+
+ !(length === 0 || length - 1 in obj) ? "development" !== 'production' ? invariant(false, 'toArray: Object should have keys for indices') : invariant(false) : undefined;
+
+ // Old IE doesn't give collections access to hasOwnProperty. Assume inputs
+ // without method will throw during the slice call and skip straight to the
+ // fallback.
+ if (obj.hasOwnProperty) {
+ try {
+ return Array.prototype.slice.call(obj);
+ } catch (e) {
+ // IE < 9 does not support Array#slice on collections objects
+ }
+ }
+
+ // Fall back to copying key by key. This assumes all keys have a value,
+ // so will not preserve sparsely populated inputs.
+ var ret = Array(length);
+ for (var ii = 0; ii < length; ii++) {
+ ret[ii] = obj[ii];
+ }
+ return ret;
+}
+
+module.exports = toArray;
+},{"161":161}],173:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule warning
+ */
+
+'use strict';
+
+var emptyFunction = _dereq_(153);
+
+/**
+ * Similar to invariant but only logs a warning if the condition is not met.
+ * This can be used to log issues in development environments in critical
+ * paths. Removing the logging code for production environments will keep the
+ * same logic and follow the same code paths.
+ */
+
+var warning = emptyFunction;
+
+if ("development" !== 'production') {
+ warning = function (condition, format) {
+ for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
+ args[_key - 2] = arguments[_key];
+ }
+
+ if (format === undefined) {
+ throw new Error('`warning(condition, format, ...args)` requires a warning ' + 'message argument');
+ }
+
+ if (format.indexOf('Failed Composite propType: ') === 0) {
+ return; // Ignore CompositeComponent proptype check.
+ }
+
+ if (!condition) {
+ var argIndex = 0;
+ var message = 'Warning: ' + format.replace(/%s/g, function () {
+ return args[argIndex++];
+ });
+ if (typeof console !== 'undefined') {
+ console.error(message);
+ }
+ try {
+ // --- Welcome to debugging React ---
+ // This error was thrown as a convenience so that you can use this stack
+ // to find the callsite that caused this warning to fire.
+ throw new Error(message);
+ } catch (x) {}
+ }
+ };
+}
+
+module.exports = warning;
+},{"153":153}]},{},[1])(1)
+});
diff --git a/devtools/client/shared/vendor/react-dom.js b/devtools/client/shared/vendor/react-dom.js
new file mode 100644
index 000000000..f7c77bd90
--- /dev/null
+++ b/devtools/client/shared/vendor/react-dom.js
@@ -0,0 +1,42 @@
+/**
+ * ReactDOM v0.14.1
+ *
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ */
+// Based off https://github.com/ForbesLindesay/umd/blob/master/template.js
+;(function(f) {
+ // CommonJS
+ if (typeof exports === "object" && typeof module !== "undefined") {
+ module.exports = f(require('devtools/client/shared/vendor/react'));
+
+ // RequireJS
+ } else if (typeof define === "function" && define.amd) {
+ define(['devtools/client/shared/vendor/react'], f);
+
+ // <script>
+ } else {
+ var g
+ if (typeof window !== "undefined") {
+ g = window;
+ } else if (typeof global !== "undefined") {
+ g = global;
+ } else if (typeof self !== "undefined") {
+ g = self;
+ } else {
+ // works providing we're not in "use strict";
+ // needed for Java 8 Nashorn
+ // see https://github.com/facebook/react/issues/3037
+ g = this;
+ }
+ g.ReactDOM = f(g.React);
+ }
+
+})(function(React) {
+ return React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
+});
diff --git a/devtools/client/shared/vendor/react-proxy.js b/devtools/client/shared/vendor/react-proxy.js
new file mode 100644
index 000000000..95346a026
--- /dev/null
+++ b/devtools/client/shared/vendor/react-proxy.js
@@ -0,0 +1,1909 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define(factory);
+ else if(typeof exports === 'object')
+ exports["ReactProxy"] = factory();
+ else
+ root["ReactProxy"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+/******/
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ Object.defineProperty(exports, '__esModule', {
+ value: true
+ });
+
+ function _interopRequire(obj) { return obj && obj.__esModule ? obj['default'] : obj; }
+
+ var _createClassProxy = __webpack_require__(12);
+
+ exports.createProxy = _interopRequire(_createClassProxy);
+
+ var _reactDeepForceUpdate = __webpack_require__(39);
+
+ exports.getForceUpdate = _interopRequire(_reactDeepForceUpdate);
+
+/***/ },
+/* 1 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`.
+ * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an object, else `false`.
+ * @example
+ *
+ * _.isObject({});
+ * // => true
+ *
+ * _.isObject([1, 2, 3]);
+ * // => true
+ *
+ * _.isObject(1);
+ * // => false
+ */
+ function isObject(value) {
+ // Avoid a V8 JIT bug in Chrome 19-20.
+ // See https://code.google.com/p/v8/issues/detail?id=2291 for more details.
+ var type = typeof value;
+ return !!value && (type == 'object' || type == 'function');
+ }
+
+ module.exports = isObject;
+
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getLength = __webpack_require__(30),
+ isLength = __webpack_require__(5);
+
+ /**
+ * Checks if `value` is array-like.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is array-like, else `false`.
+ */
+ function isArrayLike(value) {
+ return value != null && isLength(getLength(value));
+ }
+
+ module.exports = isArrayLike;
+
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Checks if `value` is object-like.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
+ */
+ function isObjectLike(value) {
+ return !!value && typeof value == 'object';
+ }
+
+ module.exports = isObjectLike;
+
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isNative = __webpack_require__(35);
+
+ /**
+ * Gets the native function at `key` of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {string} key The key of the method to get.
+ * @returns {*} Returns the function if it's native, else `undefined`.
+ */
+ function getNative(object, key) {
+ var value = object == null ? undefined : object[key];
+ return isNative(value) ? value : undefined;
+ }
+
+ module.exports = getNative;
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Used as the [maximum length](http://ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer)
+ * of an array-like value.
+ */
+ var MAX_SAFE_INTEGER = 9007199254740991;
+
+ /**
+ * Checks if `value` is a valid array-like length.
+ *
+ * **Note:** This function is based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength).
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
+ */
+ function isLength(value) {
+ return typeof value == 'number' && value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
+ }
+
+ module.exports = isLength;
+
+
+/***/ },
+/* 6 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /** Used to detect unsigned integer values. */
+ var reIsUint = /^\d+$/;
+
+ /**
+ * Used as the [maximum length](http://ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer)
+ * of an array-like value.
+ */
+ var MAX_SAFE_INTEGER = 9007199254740991;
+
+ /**
+ * Checks if `value` is a valid array-like index.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
+ * @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
+ */
+ function isIndex(value, length) {
+ value = (typeof value == 'number' || reIsUint.test(value)) ? +value : -1;
+ length = length == null ? MAX_SAFE_INTEGER : length;
+ return value > -1 && value % 1 == 0 && value < length;
+ }
+
+ module.exports = isIndex;
+
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isArrayLike = __webpack_require__(2),
+ isObjectLike = __webpack_require__(3);
+
+ /** Used for native method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /** Native method references. */
+ var propertyIsEnumerable = objectProto.propertyIsEnumerable;
+
+ /**
+ * Checks if `value` is classified as an `arguments` object.
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+ * @example
+ *
+ * _.isArguments(function() { return arguments; }());
+ * // => true
+ *
+ * _.isArguments([1, 2, 3]);
+ * // => false
+ */
+ function isArguments(value) {
+ return isObjectLike(value) && isArrayLike(value) &&
+ hasOwnProperty.call(value, 'callee') && !propertyIsEnumerable.call(value, 'callee');
+ }
+
+ module.exports = isArguments;
+
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getNative = __webpack_require__(4),
+ isLength = __webpack_require__(5),
+ isObjectLike = __webpack_require__(3);
+
+ /** `Object#toString` result references. */
+ var arrayTag = '[object Array]';
+
+ /** Used for native method references. */
+ var objectProto = Object.prototype;
+
+ /**
+ * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objToString = objectProto.toString;
+
+ /* Native method references for those with the same name as other `lodash` methods. */
+ var nativeIsArray = getNative(Array, 'isArray');
+
+ /**
+ * Checks if `value` is classified as an `Array` object.
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+ * @example
+ *
+ * _.isArray([1, 2, 3]);
+ * // => true
+ *
+ * _.isArray(function() { return arguments; }());
+ * // => false
+ */
+ var isArray = nativeIsArray || function(value) {
+ return isObjectLike(value) && isLength(value.length) && objToString.call(value) == arrayTag;
+ };
+
+ module.exports = isArray;
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /** Used as the `TypeError` message for "Functions" methods. */
+ var FUNC_ERROR_TEXT = 'Expected a function';
+
+ /* Native method references for those with the same name as other `lodash` methods. */
+ var nativeMax = Math.max;
+
+ /**
+ * Creates a function that invokes `func` with the `this` binding of the
+ * created function and arguments from `start` and beyond provided as an array.
+ *
+ * **Note:** This method is based on the [rest parameter](https://developer.mozilla.org/Web/JavaScript/Reference/Functions/rest_parameters).
+ *
+ * @static
+ * @memberOf _
+ * @category Function
+ * @param {Function} func The function to apply a rest parameter to.
+ * @param {number} [start=func.length-1] The start position of the rest parameter.
+ * @returns {Function} Returns the new function.
+ * @example
+ *
+ * var say = _.restParam(function(what, names) {
+ * return what + ' ' + _.initial(names).join(', ') +
+ * (_.size(names) > 1 ? ', & ' : '') + _.last(names);
+ * });
+ *
+ * say('hello', 'fred', 'barney', 'pebbles');
+ * // => 'hello fred, barney, & pebbles'
+ */
+ function restParam(func, start) {
+ if (typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT);
+ }
+ start = nativeMax(start === undefined ? (func.length - 1) : (+start || 0), 0);
+ return function() {
+ var args = arguments,
+ index = -1,
+ length = nativeMax(args.length - start, 0),
+ rest = Array(length);
+
+ while (++index < length) {
+ rest[index] = args[start + index];
+ }
+ switch (start) {
+ case 0: return func.call(this, rest);
+ case 1: return func.call(this, args[0], rest);
+ case 2: return func.call(this, args[0], args[1], rest);
+ }
+ var otherArgs = Array(start + 1);
+ index = -1;
+ while (++index < start) {
+ otherArgs[index] = args[index];
+ }
+ otherArgs[start] = rest;
+ return func.apply(this, otherArgs);
+ };
+ }
+
+ module.exports = restParam;
+
+
+/***/ },
+/* 10 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getNative = __webpack_require__(4),
+ isArrayLike = __webpack_require__(2),
+ isObject = __webpack_require__(1),
+ shimKeys = __webpack_require__(33);
+
+ /* Native method references for those with the same name as other `lodash` methods. */
+ var nativeKeys = getNative(Object, 'keys');
+
+ /**
+ * Creates an array of the own enumerable property names of `object`.
+ *
+ * **Note:** Non-object values are coerced to objects. See the
+ * [ES spec](http://ecma-international.org/ecma-262/6.0/#sec-object.keys)
+ * for more details.
+ *
+ * @static
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.keys(new Foo);
+ * // => ['a', 'b'] (iteration order is not guaranteed)
+ *
+ * _.keys('hi');
+ * // => ['0', '1']
+ */
+ var keys = !nativeKeys ? shimKeys : function(object) {
+ var Ctor = object == null ? undefined : object.constructor;
+ if ((typeof Ctor == 'function' && Ctor.prototype === object) ||
+ (typeof object != 'function' && isArrayLike(object))) {
+ return shimKeys(object);
+ }
+ return isObject(object) ? nativeKeys(object) : [];
+ };
+
+ module.exports = keys;
+
+
+/***/ },
+/* 11 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of React source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * Original:
+ * https://github.com/facebook/react/blob/6508b1ad273a6f371e8d90ae676e5390199461b4/src/isomorphic/classic/class/ReactClass.js#L650-L713
+ */
+
+ 'use strict';
+
+ Object.defineProperty(exports, '__esModule', {
+ value: true
+ });
+ exports['default'] = bindAutoBindMethods;
+ function bindAutoBindMethod(component, method) {
+ var boundMethod = method.bind(component);
+
+ boundMethod.__reactBoundContext = component;
+ boundMethod.__reactBoundMethod = method;
+ boundMethod.__reactBoundArguments = null;
+
+ var componentName = component.constructor.displayName,
+ _bind = boundMethod.bind;
+
+ boundMethod.bind = function (newThis) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ if (newThis !== component && newThis !== null) {
+ console.warn('bind(): React component methods may only be bound to the ' + 'component instance. See ' + componentName);
+ } else if (!args.length) {
+ console.warn('bind(): You are binding a component method to the component. ' + 'React does this for you automatically in a high-performance ' + 'way, so you can safely remove this call. See ' + componentName);
+ return boundMethod;
+ }
+
+ var reboundMethod = _bind.apply(boundMethod, arguments);
+ reboundMethod.__reactBoundContext = component;
+ reboundMethod.__reactBoundMethod = method;
+ reboundMethod.__reactBoundArguments = args;
+
+ return reboundMethod;
+ };
+
+ return boundMethod;
+ }
+
+ function bindAutoBindMethods(component) {
+ for (var autoBindKey in component.__reactAutoBindMap) {
+ if (!component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
+ return;
+ }
+
+ // Tweak: skip methods that are already bound.
+ // This is to preserve method reference in case it is used
+ // as a subscription handler that needs to be detached later.
+ if (component.hasOwnProperty(autoBindKey) && component[autoBindKey].__reactBoundContext === component) {
+ continue;
+ }
+
+ var method = component.__reactAutoBindMap[autoBindKey];
+ component[autoBindKey] = bindAutoBindMethod(component, method);
+ }
+ }
+
+ ;
+ module.exports = exports['default'];
+
+/***/ },
+/* 12 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ Object.defineProperty(exports, '__esModule', {
+ value: true
+ });
+
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+ exports['default'] = proxyClass;
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ var _createPrototypeProxy = __webpack_require__(13);
+
+ var _createPrototypeProxy2 = _interopRequireDefault(_createPrototypeProxy);
+
+ var _bindAutoBindMethods = __webpack_require__(11);
+
+ var _bindAutoBindMethods2 = _interopRequireDefault(_bindAutoBindMethods);
+
+ var _deleteUnknownAutoBindMethods = __webpack_require__(14);
+
+ var _deleteUnknownAutoBindMethods2 = _interopRequireDefault(_deleteUnknownAutoBindMethods);
+
+ var RESERVED_STATICS = ['length', 'name', 'arguments', 'caller', 'prototype', 'toString'];
+
+ function isEqualDescriptor(a, b) {
+ if (!a && !b) {
+ return true;
+ }
+ if (!a || !b) {
+ return false;
+ }
+ for (var key in a) {
+ if (a[key] !== b[key]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function proxyClass(InitialClass) {
+ // Prevent double wrapping.
+ // Given a proxy class, return the existing proxy managing it.
+ if (Object.prototype.hasOwnProperty.call(InitialClass, '__reactPatchProxy')) {
+ return InitialClass.__reactPatchProxy;
+ }
+
+ var prototypeProxy = (0, _createPrototypeProxy2['default'])();
+ var CurrentClass = undefined;
+
+ var staticDescriptors = {};
+ function wasStaticModifiedByUser(key) {
+ // Compare the descriptor with the one we previously set ourselves.
+ var currentDescriptor = Object.getOwnPropertyDescriptor(ProxyClass, key);
+ return !isEqualDescriptor(staticDescriptors[key], currentDescriptor);
+ }
+
+ var ProxyClass = undefined;
+ try {
+ // Create a proxy constructor with matching name
+ ProxyClass = new Function('getCurrentClass', 'return function ' + (InitialClass.name || 'ProxyClass') + '() {\n return getCurrentClass().apply(this, arguments);\n }')(function () {
+ return CurrentClass;
+ });
+ } catch (err) {
+ // Some environments may forbid dynamic evaluation
+ ProxyClass = function () {
+ return CurrentClass.apply(this, arguments);
+ };
+ }
+
+ // Point proxy constructor to the proxy prototype
+ ProxyClass.prototype = prototypeProxy.get();
+
+ // Proxy toString() to the current constructor
+ ProxyClass.toString = function toString() {
+ return CurrentClass.toString();
+ };
+
+ function update(_x) {
+ var _again = true;
+
+ _function: while (_again) {
+ var NextClass = _x;
+ mountedInstances = undefined;
+ _again = false;
+
+ if (typeof NextClass !== 'function') {
+ throw new Error('Expected a constructor.');
+ }
+
+ // Prevent proxy cycles
+ if (Object.prototype.hasOwnProperty.call(NextClass, '__reactPatchProxy')) {
+ _x = NextClass.__reactPatchProxy.__getCurrent();
+ _again = true;
+ continue _function;
+ }
+
+ // Save the next constructor so we call it
+ CurrentClass = NextClass;
+
+ // Update the prototype proxy with new methods
+ var mountedInstances = prototypeProxy.update(NextClass.prototype);
+
+ // Set up the constructor property so accessing the statics work
+ ProxyClass.prototype.constructor = ProxyClass;
+
+ // Set up the same prototype for inherited statics
+ ProxyClass.__proto__ = NextClass.__proto__;
+
+ // Copy static methods and properties
+ Object.getOwnPropertyNames(NextClass).forEach(function (key) {
+ if (RESERVED_STATICS.indexOf(key) > -1) {
+ return;
+ }
+
+ var staticDescriptor = _extends({}, Object.getOwnPropertyDescriptor(NextClass, key), {
+ configurable: true
+ });
+
+ // Copy static unless user has redefined it at runtime
+ if (!wasStaticModifiedByUser(key)) {
+ Object.defineProperty(ProxyClass, key, staticDescriptor);
+ staticDescriptors[key] = staticDescriptor;
+ }
+ });
+
+ // Remove old static methods and properties
+ Object.getOwnPropertyNames(ProxyClass).forEach(function (key) {
+ if (RESERVED_STATICS.indexOf(key) > -1) {
+ return;
+ }
+
+ // Skip statics that exist on the next class
+ if (NextClass.hasOwnProperty(key)) {
+ return;
+ }
+
+ // Skip non-configurable statics
+ var descriptor = Object.getOwnPropertyDescriptor(ProxyClass, key);
+ if (descriptor && !descriptor.configurable) {
+ return;
+ }
+
+ // Delete static unless user has redefined it at runtime
+ if (!wasStaticModifiedByUser(key)) {
+ delete ProxyClass[key];
+ delete staticDescriptors[key];
+ }
+ });
+
+ // Try to infer displayName
+ ProxyClass.displayName = NextClass.displayName || NextClass.name;
+
+ // We might have added new methods that need to be auto-bound
+ mountedInstances.forEach(_bindAutoBindMethods2['default']);
+ mountedInstances.forEach(_deleteUnknownAutoBindMethods2['default']);
+
+ // Let the user take care of redrawing
+ return mountedInstances;
+ }
+ };
+
+ function get() {
+ return ProxyClass;
+ }
+
+ function getCurrent() {
+ return CurrentClass;
+ }
+
+ update(InitialClass);
+
+ var proxy = { get: get, update: update };
+
+ Object.defineProperty(proxy, '__getCurrent', {
+ configurable: false,
+ writable: false,
+ enumerable: false,
+ value: getCurrent
+ });
+
+ Object.defineProperty(ProxyClass, '__reactPatchProxy', {
+ configurable: false,
+ writable: false,
+ enumerable: false,
+ value: proxy
+ });
+
+ return proxy;
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 13 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ Object.defineProperty(exports, '__esModule', {
+ value: true
+ });
+ exports['default'] = createPrototypeProxy;
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ var _lodashObjectAssign = __webpack_require__(36);
+
+ var _lodashObjectAssign2 = _interopRequireDefault(_lodashObjectAssign);
+
+ var _lodashArrayDifference = __webpack_require__(15);
+
+ var _lodashArrayDifference2 = _interopRequireDefault(_lodashArrayDifference);
+
+ function createPrototypeProxy() {
+ var proxy = {};
+ var current = null;
+ var mountedInstances = [];
+
+ /**
+ * Creates a proxied toString() method pointing to the current version's toString().
+ */
+ function proxyToString(name) {
+ // Wrap to always call the current version
+ return function toString() {
+ if (typeof current[name] === 'function') {
+ return current[name].toString();
+ } else {
+ return '<method was deleted>';
+ }
+ };
+ }
+
+ /**
+ * Creates a proxied method that calls the current version, whenever available.
+ */
+ function proxyMethod(name) {
+ // Wrap to always call the current version
+ var proxiedMethod = function proxiedMethod() {
+ if (typeof current[name] === 'function') {
+ return current[name].apply(this, arguments);
+ }
+ };
+
+ // Copy properties of the original function, if any
+ (0, _lodashObjectAssign2['default'])(proxiedMethod, current[name]);
+ proxiedMethod.toString = proxyToString(name);
+
+ return proxiedMethod;
+ }
+
+ /**
+ * Augments the original componentDidMount with instance tracking.
+ */
+ function proxiedComponentDidMount() {
+ mountedInstances.push(this);
+ if (typeof current.componentDidMount === 'function') {
+ return current.componentDidMount.apply(this, arguments);
+ }
+ }
+ proxiedComponentDidMount.toString = proxyToString('componentDidMount');
+
+ /**
+ * Augments the original componentWillUnmount with instance tracking.
+ */
+ function proxiedComponentWillUnmount() {
+ var index = mountedInstances.indexOf(this);
+ // Unless we're in a weird environment without componentDidMount
+ if (index !== -1) {
+ mountedInstances.splice(index, 1);
+ }
+ if (typeof current.componentWillUnmount === 'function') {
+ return current.componentWillUnmount.apply(this, arguments);
+ }
+ }
+ proxiedComponentWillUnmount.toString = proxyToString('componentWillUnmount');
+
+ /**
+ * Defines a property on the proxy.
+ */
+ function defineProxyProperty(name, descriptor) {
+ Object.defineProperty(proxy, name, descriptor);
+ }
+
+ /**
+ * Defines a property, attempting to keep the original descriptor configuration.
+ */
+ function defineProxyPropertyWithValue(name, value) {
+ var _ref = Object.getOwnPropertyDescriptor(current, name) || {};
+
+ var _ref$enumerable = _ref.enumerable;
+ var enumerable = _ref$enumerable === undefined ? false : _ref$enumerable;
+ var _ref$writable = _ref.writable;
+ var writable = _ref$writable === undefined ? true : _ref$writable;
+
+ defineProxyProperty(name, {
+ configurable: true,
+ enumerable: enumerable,
+ writable: writable,
+ value: value
+ });
+ }
+
+ /**
+ * Creates an auto-bind map mimicking the original map, but directed at proxy.
+ */
+ function createAutoBindMap() {
+ if (!current.__reactAutoBindMap) {
+ return;
+ }
+
+ var __reactAutoBindMap = {};
+ for (var _name in current.__reactAutoBindMap) {
+ if (current.__reactAutoBindMap.hasOwnProperty(_name)) {
+ __reactAutoBindMap[_name] = proxy[_name];
+ }
+ }
+
+ return __reactAutoBindMap;
+ }
+
+ /**
+ * Applies the updated prototype.
+ */
+ function update(next) {
+ // Save current source of truth
+ current = next;
+
+ // Find changed property names
+ var currentNames = Object.getOwnPropertyNames(current);
+ var previousName = Object.getOwnPropertyNames(proxy);
+ var addedNames = (0, _lodashArrayDifference2['default'])(currentNames, previousName);
+ var removedNames = (0, _lodashArrayDifference2['default'])(previousName, currentNames);
+
+ // Remove properties and methods that are no longer there
+ removedNames.forEach(function (name) {
+ delete proxy[name];
+ });
+
+ // Copy every descriptor
+ currentNames.forEach(function (name) {
+ var descriptor = Object.getOwnPropertyDescriptor(current, name);
+ if (typeof descriptor.value === 'function') {
+ // Functions require additional wrapping so they can be bound later
+ defineProxyPropertyWithValue(name, proxyMethod(name));
+ } else {
+ // Other values can be copied directly
+ defineProxyProperty(name, descriptor);
+ }
+ });
+
+ // Track mounting and unmounting
+ defineProxyPropertyWithValue('componentDidMount', proxiedComponentDidMount);
+ defineProxyPropertyWithValue('componentWillUnmount', proxiedComponentWillUnmount);
+ defineProxyPropertyWithValue('__reactAutoBindMap', createAutoBindMap());
+
+ // Set up the prototype chain
+ proxy.__proto__ = next;
+
+ return mountedInstances;
+ }
+
+ /**
+ * Returns the up-to-date proxy prototype.
+ */
+ function get() {
+ return proxy;
+ }
+
+ return {
+ update: update,
+ get: get
+ };
+ }
+
+ ;
+ module.exports = exports['default'];
+
+/***/ },
+/* 14 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ Object.defineProperty(exports, '__esModule', {
+ value: true
+ });
+ exports['default'] = deleteUnknownAutoBindMethods;
+ function shouldDeleteClassicInstanceMethod(component, name) {
+ if (component.__reactAutoBindMap.hasOwnProperty(name)) {
+ // It's a known autobound function, keep it
+ return false;
+ }
+
+ if (component[name].__reactBoundArguments !== null) {
+ // It's a function bound to specific args, keep it
+ return false;
+ }
+
+ // It's a cached bound method for a function
+ // that was deleted by user, so we delete it from component.
+ return true;
+ }
+
+ function shouldDeleteModernInstanceMethod(component, name) {
+ var prototype = component.constructor.prototype;
+
+ var prototypeDescriptor = Object.getOwnPropertyDescriptor(prototype, name);
+
+ if (!prototypeDescriptor || !prototypeDescriptor.get) {
+ // This is definitely not an autobinding getter
+ return false;
+ }
+
+ if (prototypeDescriptor.get().length !== component[name].length) {
+ // The length doesn't match, bail out
+ return false;
+ }
+
+ // This seems like a method bound using an autobinding getter on the prototype
+ // Hopefully we won't run into too many false positives.
+ return true;
+ }
+
+ function shouldDeleteInstanceMethod(component, name) {
+ var descriptor = Object.getOwnPropertyDescriptor(component, name);
+ if (typeof descriptor.value !== 'function') {
+ // Not a function, or something fancy: bail out
+ return;
+ }
+
+ if (component.__reactAutoBindMap) {
+ // Classic
+ return shouldDeleteClassicInstanceMethod(component, name);
+ } else {
+ // Modern
+ return shouldDeleteModernInstanceMethod(component, name);
+ }
+ }
+
+ /**
+ * Deletes autobound methods from the instance.
+ *
+ * For classic React classes, we only delete the methods that no longer exist in map.
+ * This means the user actually deleted them in code.
+ *
+ * For modern classes, we delete methods that exist on prototype with the same length,
+ * and which have getters on prototype, but are normal values on the instance.
+ * This is usually an indication that an autobinding decorator is being used,
+ * and the getter will re-generate the memoized handler on next access.
+ */
+
+ function deleteUnknownAutoBindMethods(component) {
+ var names = Object.getOwnPropertyNames(component);
+
+ names.forEach(function (name) {
+ if (shouldDeleteInstanceMethod(component, name)) {
+ delete component[name];
+ }
+ });
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 15 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseDifference = __webpack_require__(21),
+ baseFlatten = __webpack_require__(22),
+ isArrayLike = __webpack_require__(2),
+ isObjectLike = __webpack_require__(3),
+ restParam = __webpack_require__(9);
+
+ /**
+ * Creates an array of unique `array` values not included in the other
+ * provided arrays using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
+ * for equality comparisons.
+ *
+ * @static
+ * @memberOf _
+ * @category Array
+ * @param {Array} array The array to inspect.
+ * @param {...Array} [values] The arrays of values to exclude.
+ * @returns {Array} Returns the new array of filtered values.
+ * @example
+ *
+ * _.difference([1, 2, 3], [4, 2]);
+ * // => [1, 3]
+ */
+ var difference = restParam(function(array, values) {
+ return (isObjectLike(array) && isArrayLike(array))
+ ? baseDifference(array, baseFlatten(values, false, true))
+ : [];
+ });
+
+ module.exports = difference;
+
+
+/***/ },
+/* 16 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {var cachePush = __webpack_require__(27),
+ getNative = __webpack_require__(4);
+
+ /** Native method references. */
+ var Set = getNative(global, 'Set');
+
+ /* Native method references for those with the same name as other `lodash` methods. */
+ var nativeCreate = getNative(Object, 'create');
+
+ /**
+ *
+ * Creates a cache object to store unique values.
+ *
+ * @private
+ * @param {Array} [values] The values to cache.
+ */
+ function SetCache(values) {
+ var length = values ? values.length : 0;
+
+ this.data = { 'hash': nativeCreate(null), 'set': new Set };
+ while (length--) {
+ this.push(values[length]);
+ }
+ }
+
+ // Add functions to the `Set` cache.
+ SetCache.prototype.push = cachePush;
+
+ module.exports = SetCache;
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ },
+/* 17 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Appends the elements of `values` to `array`.
+ *
+ * @private
+ * @param {Array} array The array to modify.
+ * @param {Array} values The values to append.
+ * @returns {Array} Returns `array`.
+ */
+ function arrayPush(array, values) {
+ var index = -1,
+ length = values.length,
+ offset = array.length;
+
+ while (++index < length) {
+ array[offset + index] = values[index];
+ }
+ return array;
+ }
+
+ module.exports = arrayPush;
+
+
+/***/ },
+/* 18 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var keys = __webpack_require__(10);
+
+ /**
+ * A specialized version of `_.assign` for customizing assigned values without
+ * support for argument juggling, multiple sources, and `this` binding `customizer`
+ * functions.
+ *
+ * @private
+ * @param {Object} object The destination object.
+ * @param {Object} source The source object.
+ * @param {Function} customizer The function to customize assigned values.
+ * @returns {Object} Returns `object`.
+ */
+ function assignWith(object, source, customizer) {
+ var index = -1,
+ props = keys(source),
+ length = props.length;
+
+ while (++index < length) {
+ var key = props[index],
+ value = object[key],
+ result = customizer(value, source[key], key, object, source);
+
+ if ((result === result ? (result !== value) : (value === value)) ||
+ (value === undefined && !(key in object))) {
+ object[key] = result;
+ }
+ }
+ return object;
+ }
+
+ module.exports = assignWith;
+
+
+/***/ },
+/* 19 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseCopy = __webpack_require__(20),
+ keys = __webpack_require__(10);
+
+ /**
+ * The base implementation of `_.assign` without support for argument juggling,
+ * multiple sources, and `customizer` functions.
+ *
+ * @private
+ * @param {Object} object The destination object.
+ * @param {Object} source The source object.
+ * @returns {Object} Returns `object`.
+ */
+ function baseAssign(object, source) {
+ return source == null
+ ? object
+ : baseCopy(source, keys(source), object);
+ }
+
+ module.exports = baseAssign;
+
+
+/***/ },
+/* 20 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Copies properties of `source` to `object`.
+ *
+ * @private
+ * @param {Object} source The object to copy properties from.
+ * @param {Array} props The property names to copy.
+ * @param {Object} [object={}] The object to copy properties to.
+ * @returns {Object} Returns `object`.
+ */
+ function baseCopy(source, props, object) {
+ object || (object = {});
+
+ var index = -1,
+ length = props.length;
+
+ while (++index < length) {
+ var key = props[index];
+ object[key] = source[key];
+ }
+ return object;
+ }
+
+ module.exports = baseCopy;
+
+
+/***/ },
+/* 21 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseIndexOf = __webpack_require__(23),
+ cacheIndexOf = __webpack_require__(26),
+ createCache = __webpack_require__(29);
+
+ /** Used as the size to enable large array optimizations. */
+ var LARGE_ARRAY_SIZE = 200;
+
+ /**
+ * The base implementation of `_.difference` which accepts a single array
+ * of values to exclude.
+ *
+ * @private
+ * @param {Array} array The array to inspect.
+ * @param {Array} values The values to exclude.
+ * @returns {Array} Returns the new array of filtered values.
+ */
+ function baseDifference(array, values) {
+ var length = array ? array.length : 0,
+ result = [];
+
+ if (!length) {
+ return result;
+ }
+ var index = -1,
+ indexOf = baseIndexOf,
+ isCommon = true,
+ cache = (isCommon && values.length >= LARGE_ARRAY_SIZE) ? createCache(values) : null,
+ valuesLength = values.length;
+
+ if (cache) {
+ indexOf = cacheIndexOf;
+ isCommon = false;
+ values = cache;
+ }
+ outer:
+ while (++index < length) {
+ var value = array[index];
+
+ if (isCommon && value === value) {
+ var valuesIndex = valuesLength;
+ while (valuesIndex--) {
+ if (values[valuesIndex] === value) {
+ continue outer;
+ }
+ }
+ result.push(value);
+ }
+ else if (indexOf(values, value, 0) < 0) {
+ result.push(value);
+ }
+ }
+ return result;
+ }
+
+ module.exports = baseDifference;
+
+
+/***/ },
+/* 22 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var arrayPush = __webpack_require__(17),
+ isArguments = __webpack_require__(7),
+ isArray = __webpack_require__(8),
+ isArrayLike = __webpack_require__(2),
+ isObjectLike = __webpack_require__(3);
+
+ /**
+ * The base implementation of `_.flatten` with added support for restricting
+ * flattening and specifying the start index.
+ *
+ * @private
+ * @param {Array} array The array to flatten.
+ * @param {boolean} [isDeep] Specify a deep flatten.
+ * @param {boolean} [isStrict] Restrict flattening to arrays-like objects.
+ * @param {Array} [result=[]] The initial result value.
+ * @returns {Array} Returns the new flattened array.
+ */
+ function baseFlatten(array, isDeep, isStrict, result) {
+ result || (result = []);
+
+ var index = -1,
+ length = array.length;
+
+ while (++index < length) {
+ var value = array[index];
+ if (isObjectLike(value) && isArrayLike(value) &&
+ (isStrict || isArray(value) || isArguments(value))) {
+ if (isDeep) {
+ // Recursively flatten arrays (susceptible to call stack limits).
+ baseFlatten(value, isDeep, isStrict, result);
+ } else {
+ arrayPush(result, value);
+ }
+ } else if (!isStrict) {
+ result[result.length] = value;
+ }
+ }
+ return result;
+ }
+
+ module.exports = baseFlatten;
+
+
+/***/ },
+/* 23 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var indexOfNaN = __webpack_require__(31);
+
+ /**
+ * The base implementation of `_.indexOf` without support for binary searches.
+ *
+ * @private
+ * @param {Array} array The array to search.
+ * @param {*} value The value to search for.
+ * @param {number} fromIndex The index to search from.
+ * @returns {number} Returns the index of the matched value, else `-1`.
+ */
+ function baseIndexOf(array, value, fromIndex) {
+ if (value !== value) {
+ return indexOfNaN(array, fromIndex);
+ }
+ var index = fromIndex - 1,
+ length = array.length;
+
+ while (++index < length) {
+ if (array[index] === value) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ module.exports = baseIndexOf;
+
+
+/***/ },
+/* 24 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * The base implementation of `_.property` without support for deep paths.
+ *
+ * @private
+ * @param {string} key The key of the property to get.
+ * @returns {Function} Returns the new function.
+ */
+ function baseProperty(key) {
+ return function(object) {
+ return object == null ? undefined : object[key];
+ };
+ }
+
+ module.exports = baseProperty;
+
+
+/***/ },
+/* 25 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var identity = __webpack_require__(38);
+
+ /**
+ * A specialized version of `baseCallback` which only supports `this` binding
+ * and specifying the number of arguments to provide to `func`.
+ *
+ * @private
+ * @param {Function} func The function to bind.
+ * @param {*} thisArg The `this` binding of `func`.
+ * @param {number} [argCount] The number of arguments to provide to `func`.
+ * @returns {Function} Returns the callback.
+ */
+ function bindCallback(func, thisArg, argCount) {
+ if (typeof func != 'function') {
+ return identity;
+ }
+ if (thisArg === undefined) {
+ return func;
+ }
+ switch (argCount) {
+ case 1: return function(value) {
+ return func.call(thisArg, value);
+ };
+ case 3: return function(value, index, collection) {
+ return func.call(thisArg, value, index, collection);
+ };
+ case 4: return function(accumulator, value, index, collection) {
+ return func.call(thisArg, accumulator, value, index, collection);
+ };
+ case 5: return function(value, other, key, object, source) {
+ return func.call(thisArg, value, other, key, object, source);
+ };
+ }
+ return function() {
+ return func.apply(thisArg, arguments);
+ };
+ }
+
+ module.exports = bindCallback;
+
+
+/***/ },
+/* 26 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isObject = __webpack_require__(1);
+
+ /**
+ * Checks if `value` is in `cache` mimicking the return signature of
+ * `_.indexOf` by returning `0` if the value is found, else `-1`.
+ *
+ * @private
+ * @param {Object} cache The cache to search.
+ * @param {*} value The value to search for.
+ * @returns {number} Returns `0` if `value` is found, else `-1`.
+ */
+ function cacheIndexOf(cache, value) {
+ var data = cache.data,
+ result = (typeof value == 'string' || isObject(value)) ? data.set.has(value) : data.hash[value];
+
+ return result ? 0 : -1;
+ }
+
+ module.exports = cacheIndexOf;
+
+
+/***/ },
+/* 27 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isObject = __webpack_require__(1);
+
+ /**
+ * Adds `value` to the cache.
+ *
+ * @private
+ * @name push
+ * @memberOf SetCache
+ * @param {*} value The value to cache.
+ */
+ function cachePush(value) {
+ var data = this.data;
+ if (typeof value == 'string' || isObject(value)) {
+ data.set.add(value);
+ } else {
+ data.hash[value] = true;
+ }
+ }
+
+ module.exports = cachePush;
+
+
+/***/ },
+/* 28 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var bindCallback = __webpack_require__(25),
+ isIterateeCall = __webpack_require__(32),
+ restParam = __webpack_require__(9);
+
+ /**
+ * Creates a `_.assign`, `_.defaults`, or `_.merge` function.
+ *
+ * @private
+ * @param {Function} assigner The function to assign values.
+ * @returns {Function} Returns the new assigner function.
+ */
+ function createAssigner(assigner) {
+ return restParam(function(object, sources) {
+ var index = -1,
+ length = object == null ? 0 : sources.length,
+ customizer = length > 2 ? sources[length - 2] : undefined,
+ guard = length > 2 ? sources[2] : undefined,
+ thisArg = length > 1 ? sources[length - 1] : undefined;
+
+ if (typeof customizer == 'function') {
+ customizer = bindCallback(customizer, thisArg, 5);
+ length -= 2;
+ } else {
+ customizer = typeof thisArg == 'function' ? thisArg : undefined;
+ length -= (customizer ? 1 : 0);
+ }
+ if (guard && isIterateeCall(sources[0], sources[1], guard)) {
+ customizer = length < 3 ? undefined : customizer;
+ length = 1;
+ }
+ while (++index < length) {
+ var source = sources[index];
+ if (source) {
+ assigner(object, source, customizer);
+ }
+ }
+ return object;
+ });
+ }
+
+ module.exports = createAssigner;
+
+
+/***/ },
+/* 29 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {var SetCache = __webpack_require__(16),
+ getNative = __webpack_require__(4);
+
+ /** Native method references. */
+ var Set = getNative(global, 'Set');
+
+ /* Native method references for those with the same name as other `lodash` methods. */
+ var nativeCreate = getNative(Object, 'create');
+
+ /**
+ * Creates a `Set` cache object to optimize linear searches of large arrays.
+ *
+ * @private
+ * @param {Array} [values] The values to cache.
+ * @returns {null|Object} Returns the new cache object if `Set` is supported, else `null`.
+ */
+ function createCache(values) {
+ return (nativeCreate && Set) ? new SetCache(values) : null;
+ }
+
+ module.exports = createCache;
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ },
+/* 30 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var baseProperty = __webpack_require__(24);
+
+ /**
+ * Gets the "length" property value of `object`.
+ *
+ * **Note:** This function is used to avoid a [JIT bug](https://bugs.webkit.org/show_bug.cgi?id=142792)
+ * that affects Safari on at least iOS 8.1-8.3 ARM64.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {*} Returns the "length" value.
+ */
+ var getLength = baseProperty('length');
+
+ module.exports = getLength;
+
+
+/***/ },
+/* 31 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Gets the index at which the first occurrence of `NaN` is found in `array`.
+ *
+ * @private
+ * @param {Array} array The array to search.
+ * @param {number} fromIndex The index to search from.
+ * @param {boolean} [fromRight] Specify iterating from right to left.
+ * @returns {number} Returns the index of the matched `NaN`, else `-1`.
+ */
+ function indexOfNaN(array, fromIndex, fromRight) {
+ var length = array.length,
+ index = fromIndex + (fromRight ? 0 : -1);
+
+ while ((fromRight ? index-- : ++index < length)) {
+ var other = array[index];
+ if (other !== other) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ module.exports = indexOfNaN;
+
+
+/***/ },
+/* 32 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isArrayLike = __webpack_require__(2),
+ isIndex = __webpack_require__(6),
+ isObject = __webpack_require__(1);
+
+ /**
+ * Checks if the provided arguments are from an iteratee call.
+ *
+ * @private
+ * @param {*} value The potential iteratee value argument.
+ * @param {*} index The potential iteratee index or key argument.
+ * @param {*} object The potential iteratee object argument.
+ * @returns {boolean} Returns `true` if the arguments are from an iteratee call, else `false`.
+ */
+ function isIterateeCall(value, index, object) {
+ if (!isObject(object)) {
+ return false;
+ }
+ var type = typeof index;
+ if (type == 'number'
+ ? (isArrayLike(object) && isIndex(index, object.length))
+ : (type == 'string' && index in object)) {
+ var other = object[index];
+ return value === value ? (value === other) : (other !== other);
+ }
+ return false;
+ }
+
+ module.exports = isIterateeCall;
+
+
+/***/ },
+/* 33 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isArguments = __webpack_require__(7),
+ isArray = __webpack_require__(8),
+ isIndex = __webpack_require__(6),
+ isLength = __webpack_require__(5),
+ keysIn = __webpack_require__(37);
+
+ /** Used for native method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /**
+ * A fallback implementation of `Object.keys` which creates an array of the
+ * own enumerable property names of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ */
+ function shimKeys(object) {
+ var props = keysIn(object),
+ propsLength = props.length,
+ length = propsLength && object.length;
+
+ var allowIndexes = !!length && isLength(length) &&
+ (isArray(object) || isArguments(object));
+
+ var index = -1,
+ result = [];
+
+ while (++index < propsLength) {
+ var key = props[index];
+ if ((allowIndexes && isIndex(key, length)) || hasOwnProperty.call(object, key)) {
+ result.push(key);
+ }
+ }
+ return result;
+ }
+
+ module.exports = shimKeys;
+
+
+/***/ },
+/* 34 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isObject = __webpack_require__(1);
+
+ /** `Object#toString` result references. */
+ var funcTag = '[object Function]';
+
+ /** Used for native method references. */
+ var objectProto = Object.prototype;
+
+ /**
+ * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objToString = objectProto.toString;
+
+ /**
+ * Checks if `value` is classified as a `Function` object.
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+ * @example
+ *
+ * _.isFunction(_);
+ * // => true
+ *
+ * _.isFunction(/abc/);
+ * // => false
+ */
+ function isFunction(value) {
+ // The use of `Object#toString` avoids issues with the `typeof` operator
+ // in older versions of Chrome and Safari which return 'function' for regexes
+ // and Safari 8 which returns 'object' for typed array constructors.
+ return isObject(value) && objToString.call(value) == funcTag;
+ }
+
+ module.exports = isFunction;
+
+
+/***/ },
+/* 35 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isFunction = __webpack_require__(34),
+ isObjectLike = __webpack_require__(3);
+
+ /** Used to detect host constructors (Safari > 5). */
+ var reIsHostCtor = /^\[object .+?Constructor\]$/;
+
+ /** Used for native method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to resolve the decompiled source of functions. */
+ var fnToString = Function.prototype.toString;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /** Used to detect if a method is native. */
+ var reIsNative = RegExp('^' +
+ fnToString.call(hasOwnProperty).replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
+ .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
+ );
+
+ /**
+ * Checks if `value` is a native function.
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a native function, else `false`.
+ * @example
+ *
+ * _.isNative(Array.prototype.push);
+ * // => true
+ *
+ * _.isNative(_);
+ * // => false
+ */
+ function isNative(value) {
+ if (value == null) {
+ return false;
+ }
+ if (isFunction(value)) {
+ return reIsNative.test(fnToString.call(value));
+ }
+ return isObjectLike(value) && reIsHostCtor.test(value);
+ }
+
+ module.exports = isNative;
+
+
+/***/ },
+/* 36 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var assignWith = __webpack_require__(18),
+ baseAssign = __webpack_require__(19),
+ createAssigner = __webpack_require__(28);
+
+ /**
+ * Assigns own enumerable properties of source object(s) to the destination
+ * object. Subsequent sources overwrite property assignments of previous sources.
+ * If `customizer` is provided it's invoked to produce the assigned values.
+ * The `customizer` is bound to `thisArg` and invoked with five arguments:
+ * (objectValue, sourceValue, key, object, source).
+ *
+ * **Note:** This method mutates `object` and is based on
+ * [`Object.assign`](http://ecma-international.org/ecma-262/6.0/#sec-object.assign).
+ *
+ * @static
+ * @memberOf _
+ * @alias extend
+ * @category Object
+ * @param {Object} object The destination object.
+ * @param {...Object} [sources] The source objects.
+ * @param {Function} [customizer] The function to customize assigned values.
+ * @param {*} [thisArg] The `this` binding of `customizer`.
+ * @returns {Object} Returns `object`.
+ * @example
+ *
+ * _.assign({ 'user': 'barney' }, { 'age': 40 }, { 'user': 'fred' });
+ * // => { 'user': 'fred', 'age': 40 }
+ *
+ * // using a customizer callback
+ * var defaults = _.partialRight(_.assign, function(value, other) {
+ * return _.isUndefined(value) ? other : value;
+ * });
+ *
+ * defaults({ 'user': 'barney' }, { 'age': 36 }, { 'user': 'fred' });
+ * // => { 'user': 'barney', 'age': 36 }
+ */
+ var assign = createAssigner(function(object, source, customizer) {
+ return customizer
+ ? assignWith(object, source, customizer)
+ : baseAssign(object, source);
+ });
+
+ module.exports = assign;
+
+
+/***/ },
+/* 37 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isArguments = __webpack_require__(7),
+ isArray = __webpack_require__(8),
+ isIndex = __webpack_require__(6),
+ isLength = __webpack_require__(5),
+ isObject = __webpack_require__(1);
+
+ /** Used for native method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /**
+ * Creates an array of the own and inherited enumerable property names of `object`.
+ *
+ * **Note:** Non-object values are coerced to objects.
+ *
+ * @static
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.keysIn(new Foo);
+ * // => ['a', 'b', 'c'] (iteration order is not guaranteed)
+ */
+ function keysIn(object) {
+ if (object == null) {
+ return [];
+ }
+ if (!isObject(object)) {
+ object = Object(object);
+ }
+ var length = object.length;
+ length = (length && isLength(length) &&
+ (isArray(object) || isArguments(object)) && length) || 0;
+
+ var Ctor = object.constructor,
+ index = -1,
+ isProto = typeof Ctor == 'function' && Ctor.prototype === object,
+ result = Array(length),
+ skipIndexes = length > 0;
+
+ while (++index < length) {
+ result[index] = (index + '');
+ }
+ for (var key in object) {
+ if (!(skipIndexes && isIndex(key, length)) &&
+ !(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) {
+ result.push(key);
+ }
+ }
+ return result;
+ }
+
+ module.exports = keysIn;
+
+
+/***/ },
+/* 38 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * This method returns the first argument provided to it.
+ *
+ * @static
+ * @memberOf _
+ * @category Utility
+ * @param {*} value Any value.
+ * @returns {*} Returns `value`.
+ * @example
+ *
+ * var object = { 'user': 'fred' };
+ *
+ * _.identity(object) === object;
+ * // => true
+ */
+ function identity(value) {
+ return value;
+ }
+
+ module.exports = identity;
+
+
+/***/ },
+/* 39 */
+/***/ function(module, exports, __webpack_require__) {
+
+ "use strict";
+
+ exports.__esModule = true;
+ exports["default"] = getForceUpdate;
+ function traverseRenderedChildren(internalInstance, callback, argument) {
+ callback(internalInstance, argument);
+
+ if (internalInstance._renderedComponent) {
+ traverseRenderedChildren(internalInstance._renderedComponent, callback, argument);
+ } else {
+ for (var key in internalInstance._renderedChildren) {
+ if (internalInstance._renderedChildren.hasOwnProperty(key)) {
+ traverseRenderedChildren(internalInstance._renderedChildren[key], callback, argument);
+ }
+ }
+ }
+ }
+
+ function setPendingForceUpdate(internalInstance) {
+ if (internalInstance._pendingForceUpdate === false) {
+ internalInstance._pendingForceUpdate = true;
+ }
+ }
+
+ function forceUpdateIfPending(internalInstance, React) {
+ if (internalInstance._pendingForceUpdate === true) {
+ var publicInstance = internalInstance._instance;
+ React.Component.prototype.forceUpdate.call(publicInstance);
+ }
+ }
+
+ function getForceUpdate(React) {
+ return function (instance) {
+ var internalInstance = instance._reactInternalInstance;
+ traverseRenderedChildren(internalInstance, setPendingForceUpdate);
+ traverseRenderedChildren(internalInstance, forceUpdateIfPending, React);
+ };
+ }
+
+ module.exports = exports["default"];
+
+/***/ }
+/******/ ])
+});
diff --git a/devtools/client/shared/vendor/react-redux.js b/devtools/client/shared/vendor/react-redux.js
new file mode 100644
index 000000000..562f4916f
--- /dev/null
+++ b/devtools/client/shared/vendor/react-redux.js
@@ -0,0 +1,724 @@
+var REACT_PATH = "devtools/client/shared/vendor/react";
+var REDUX_PATH = "devtools/client/shared/vendor/redux";
+
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory(require(REACT_PATH), require(REDUX_PATH));
+ else if(typeof define === 'function' && define.amd)
+ define(["react", "redux"], factory);
+ else if(typeof exports === 'object')
+ exports["ReactRedux"] = factory(require("react"), require("redux"));
+ else
+ root["ReactRedux"] = factory(root["React"], root["Redux"]);
+})(this, function(__WEBPACK_EXTERNAL_MODULE_10__, __WEBPACK_EXTERNAL_MODULE_11__) {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+
+
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ var _react = __webpack_require__(10);
+
+ var _react2 = _interopRequireDefault(_react);
+
+ var _componentsCreateAll = __webpack_require__(2);
+
+ var _componentsCreateAll2 = _interopRequireDefault(_componentsCreateAll);
+
+ var _createAll = _componentsCreateAll2['default'](_react2['default']);
+
+ var Provider = _createAll.Provider;
+ var connect = _createAll.connect;
+ exports.Provider = Provider;
+ exports.connect = connect;
+
+/***/ },
+/* 1 */
+/***/ function(module, exports) {
+
+ "use strict";
+
+ exports.__esModule = true;
+ exports["default"] = createStoreShape;
+
+ function createStoreShape(PropTypes) {
+ return PropTypes.shape({
+ subscribe: PropTypes.func.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ getState: PropTypes.func.isRequired
+ });
+ }
+
+ module.exports = exports["default"];
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports['default'] = createAll;
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ var _createProvider = __webpack_require__(4);
+
+ var _createProvider2 = _interopRequireDefault(_createProvider);
+
+ var _createConnect = __webpack_require__(3);
+
+ var _createConnect2 = _interopRequireDefault(_createConnect);
+
+ function createAll(React) {
+ var Provider = _createProvider2['default'](React);
+ var connect = _createConnect2['default'](React);
+
+ return { Provider: Provider, connect: connect };
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+ exports['default'] = createConnect;
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+ function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+ var _utilsCreateStoreShape = __webpack_require__(1);
+
+ var _utilsCreateStoreShape2 = _interopRequireDefault(_utilsCreateStoreShape);
+
+ var _utilsShallowEqual = __webpack_require__(6);
+
+ var _utilsShallowEqual2 = _interopRequireDefault(_utilsShallowEqual);
+
+ var _utilsIsPlainObject = __webpack_require__(5);
+
+ var _utilsIsPlainObject2 = _interopRequireDefault(_utilsIsPlainObject);
+
+ var _utilsWrapActionCreators = __webpack_require__(7);
+
+ var _utilsWrapActionCreators2 = _interopRequireDefault(_utilsWrapActionCreators);
+
+ var _hoistNonReactStatics = __webpack_require__(8);
+
+ var _hoistNonReactStatics2 = _interopRequireDefault(_hoistNonReactStatics);
+
+ var _invariant = __webpack_require__(9);
+
+ var _invariant2 = _interopRequireDefault(_invariant);
+
+ var defaultMapStateToProps = function defaultMapStateToProps() {
+ return {};
+ };
+ var defaultMapDispatchToProps = function defaultMapDispatchToProps(dispatch) {
+ return { dispatch: dispatch };
+ };
+ var defaultMergeProps = function defaultMergeProps(stateProps, dispatchProps, parentProps) {
+ return _extends({}, parentProps, stateProps, dispatchProps);
+ };
+
+ function getDisplayName(Component) {
+ return Component.displayName || Component.name || 'Component';
+ }
+
+ // Helps track hot reloading.
+ var nextVersion = 0;
+
+ function createConnect(React) {
+ var Component = React.Component;
+ var PropTypes = React.PropTypes;
+
+ var storeShape = _utilsCreateStoreShape2['default'](PropTypes);
+
+ return function connect(mapStateToProps, mapDispatchToProps, mergeProps) {
+ var options = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3];
+
+ var shouldSubscribe = Boolean(mapStateToProps);
+ var finalMapStateToProps = mapStateToProps || defaultMapStateToProps;
+ var finalMapDispatchToProps = _utilsIsPlainObject2['default'](mapDispatchToProps) ? _utilsWrapActionCreators2['default'](mapDispatchToProps) : mapDispatchToProps || defaultMapDispatchToProps;
+ var finalMergeProps = mergeProps || defaultMergeProps;
+ var shouldUpdateStateProps = finalMapStateToProps.length > 1;
+ var shouldUpdateDispatchProps = finalMapDispatchToProps.length > 1;
+ var _options$pure = options.pure;
+ var pure = _options$pure === undefined ? true : _options$pure;
+
+ // Helps track hot reloading.
+ var version = nextVersion++;
+
+ function computeStateProps(store, props) {
+ var state = store.getState();
+ var stateProps = shouldUpdateStateProps ? finalMapStateToProps(state, props) : finalMapStateToProps(state);
+
+ _invariant2['default'](_utilsIsPlainObject2['default'](stateProps), '`mapStateToProps` must return an object. Instead received %s.', stateProps);
+ return stateProps;
+ }
+
+ function computeDispatchProps(store, props) {
+ var dispatch = store.dispatch;
+
+ var dispatchProps = shouldUpdateDispatchProps ? finalMapDispatchToProps(dispatch, props) : finalMapDispatchToProps(dispatch);
+
+ _invariant2['default'](_utilsIsPlainObject2['default'](dispatchProps), '`mapDispatchToProps` must return an object. Instead received %s.', dispatchProps);
+ return dispatchProps;
+ }
+
+ function _computeNextState(stateProps, dispatchProps, parentProps) {
+ var mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps);
+ _invariant2['default'](_utilsIsPlainObject2['default'](mergedProps), '`mergeProps` must return an object. Instead received %s.', mergedProps);
+ return mergedProps;
+ }
+
+ return function wrapWithConnect(WrappedComponent) {
+ var Connect = (function (_Component) {
+ _inherits(Connect, _Component);
+
+ Connect.prototype.shouldComponentUpdate = function shouldComponentUpdate(nextProps, nextState) {
+ if (!pure) {
+ this.updateStateProps(nextProps);
+ this.updateDispatchProps(nextProps);
+ this.updateState(nextProps);
+ return true;
+ }
+
+ var storeChanged = nextState.storeState !== this.state.storeState;
+ var propsChanged = !_utilsShallowEqual2['default'](nextProps, this.props);
+ var mapStateProducedChange = false;
+ var dispatchPropsChanged = false;
+
+ if (storeChanged || propsChanged && shouldUpdateStateProps) {
+ mapStateProducedChange = this.updateStateProps(nextProps);
+ }
+
+ if (propsChanged && shouldUpdateDispatchProps) {
+ dispatchPropsChanged = this.updateDispatchProps(nextProps);
+ }
+
+ if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
+ this.updateState(nextProps);
+ return true;
+ }
+
+ return false;
+ };
+
+ function Connect(props, context) {
+ _classCallCheck(this, Connect);
+
+ _Component.call(this, props, context);
+ this.version = version;
+ this.store = props.store || context.store;
+
+ _invariant2['default'](this.store, 'Could not find "store" in either the context or ' + ('props of "' + this.constructor.displayName + '". ') + 'Either wrap the root component in a <Provider>, ' + ('or explicitly pass "store" as a prop to "' + this.constructor.displayName + '".'));
+
+ this.stateProps = computeStateProps(this.store, props);
+ this.dispatchProps = computeDispatchProps(this.store, props);
+ this.state = { storeState: null };
+ this.updateState();
+ }
+
+ Connect.prototype.computeNextState = function computeNextState() {
+ var props = arguments.length <= 0 || arguments[0] === undefined ? this.props : arguments[0];
+
+ return _computeNextState(this.stateProps, this.dispatchProps, props);
+ };
+
+ Connect.prototype.updateStateProps = function updateStateProps() {
+ var props = arguments.length <= 0 || arguments[0] === undefined ? this.props : arguments[0];
+
+ var nextStateProps = computeStateProps(this.store, props);
+ if (_utilsShallowEqual2['default'](nextStateProps, this.stateProps)) {
+ return false;
+ }
+
+ this.stateProps = nextStateProps;
+ return true;
+ };
+
+ Connect.prototype.updateDispatchProps = function updateDispatchProps() {
+ var props = arguments.length <= 0 || arguments[0] === undefined ? this.props : arguments[0];
+
+ var nextDispatchProps = computeDispatchProps(this.store, props);
+ if (_utilsShallowEqual2['default'](nextDispatchProps, this.dispatchProps)) {
+ return false;
+ }
+
+ this.dispatchProps = nextDispatchProps;
+ return true;
+ };
+
+ Connect.prototype.updateState = function updateState() {
+ var props = arguments.length <= 0 || arguments[0] === undefined ? this.props : arguments[0];
+
+ this.nextState = this.computeNextState(props);
+ };
+
+ Connect.prototype.isSubscribed = function isSubscribed() {
+ return typeof this.unsubscribe === 'function';
+ };
+
+ Connect.prototype.trySubscribe = function trySubscribe() {
+ if (shouldSubscribe && !this.unsubscribe) {
+ this.unsubscribe = this.store.subscribe(this.handleChange.bind(this));
+ this.handleChange();
+ }
+ };
+
+ Connect.prototype.tryUnsubscribe = function tryUnsubscribe() {
+ if (this.unsubscribe) {
+ this.unsubscribe();
+ this.unsubscribe = null;
+ }
+ };
+
+ Connect.prototype.componentDidMount = function componentDidMount() {
+ this.trySubscribe();
+ };
+
+ Connect.prototype.componentWillUnmount = function componentWillUnmount() {
+ this.tryUnsubscribe();
+ };
+
+ Connect.prototype.handleChange = function handleChange() {
+ if (!this.unsubscribe) {
+ return;
+ }
+
+ this.setState({
+ storeState: this.store.getState()
+ });
+ };
+
+ Connect.prototype.getWrappedInstance = function getWrappedInstance() {
+ return this.refs.wrappedInstance;
+ };
+
+ Connect.prototype.render = function render() {
+ return React.createElement(WrappedComponent, _extends({ ref: 'wrappedInstance'
+ }, this.nextState));
+ };
+
+ return Connect;
+ })(Component);
+
+ Connect.displayName = 'Connect(' + getDisplayName(WrappedComponent) + ')';
+ Connect.WrappedComponent = WrappedComponent;
+ Connect.contextTypes = {
+ store: storeShape
+ };
+ Connect.propTypes = {
+ store: storeShape
+ };
+
+ if (true) {
+ Connect.prototype.componentWillUpdate = function componentWillUpdate() {
+ if (this.version === version) {
+ return;
+ }
+
+ // We are hot reloading!
+ this.version = version;
+
+ // Update the state and bindings.
+ this.trySubscribe();
+ this.updateStateProps();
+ this.updateDispatchProps();
+ this.updateState();
+ };
+ }
+
+ return _hoistNonReactStatics2['default'](Connect, WrappedComponent);
+ };
+ };
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports['default'] = createProvider;
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+ function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+ var _utilsCreateStoreShape = __webpack_require__(1);
+
+ var _utilsCreateStoreShape2 = _interopRequireDefault(_utilsCreateStoreShape);
+
+ function isUsingOwnerContext(React) {
+ var version = React.version;
+
+ if (typeof version !== 'string') {
+ return true;
+ }
+
+ var sections = version.split('.');
+ var major = parseInt(sections[0], 10);
+ var minor = parseInt(sections[1], 10);
+
+ return major === 0 && minor === 13;
+ }
+
+ function createProvider(React) {
+ var Component = React.Component;
+ var PropTypes = React.PropTypes;
+ var Children = React.Children;
+
+ var storeShape = _utilsCreateStoreShape2['default'](PropTypes);
+ var requireFunctionChild = isUsingOwnerContext(React);
+
+ var didWarnAboutChild = false;
+ function warnAboutFunctionChild() {
+ if (didWarnAboutChild || requireFunctionChild) {
+ return;
+ }
+
+ didWarnAboutChild = true;
+ console.error( // eslint-disable-line no-console
+ 'With React 0.14 and later versions, you no longer need to ' + 'wrap <Provider> child into a function.');
+ }
+ function warnAboutElementChild() {
+ if (didWarnAboutChild || !requireFunctionChild) {
+ return;
+ }
+
+ didWarnAboutChild = true;
+ console.error( // eslint-disable-line no-console
+ 'With React 0.13, you need to ' + 'wrap <Provider> child into a function. ' + 'This restriction will be removed with React 0.14.');
+ }
+
+ var didWarnAboutReceivingStore = false;
+ function warnAboutReceivingStore() {
+ if (didWarnAboutReceivingStore) {
+ return;
+ }
+
+ didWarnAboutReceivingStore = true;
+ console.error( // eslint-disable-line no-console
+ '<Provider> does not support changing `store` on the fly. ' + 'It is most likely that you see this error because you updated to ' + 'Redux 2.x and React Redux 2.x which no longer hot reload reducers ' + 'automatically. See https://github.com/rackt/react-redux/releases/' + 'tag/v2.0.0 for the migration instructions.');
+ }
+
+ var Provider = (function (_Component) {
+ _inherits(Provider, _Component);
+
+ Provider.prototype.getChildContext = function getChildContext() {
+ return { store: this.store };
+ };
+
+ function Provider(props, context) {
+ _classCallCheck(this, Provider);
+
+ _Component.call(this, props, context);
+ this.store = props.store;
+ }
+
+ Provider.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
+ var store = this.store;
+ var nextStore = nextProps.store;
+
+ if (store !== nextStore) {
+ warnAboutReceivingStore();
+ }
+ };
+
+ Provider.prototype.render = function render() {
+ var children = this.props.children;
+
+ if (typeof children === 'function') {
+ warnAboutFunctionChild();
+ children = children();
+ } else {
+ warnAboutElementChild();
+ }
+
+ return Children.only(children);
+ };
+
+ return Provider;
+ })(Component);
+
+ Provider.childContextTypes = {
+ store: storeShape.isRequired
+ };
+ Provider.propTypes = {
+ store: storeShape.isRequired,
+ children: (requireFunctionChild ? PropTypes.func : PropTypes.element).isRequired
+ };
+
+ return Provider;
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 5 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports['default'] = isPlainObject;
+ var fnToString = function fnToString(fn) {
+ return Function.prototype.toString.call(fn);
+ };
+
+ /**
+ * @param {any} obj The object to inspect.
+ * @returns {boolean} True if the argument appears to be a plain object.
+ */
+
+ function isPlainObject(obj) {
+ if (!obj || typeof obj !== 'object') {
+ return false;
+ }
+
+ var proto = typeof obj.constructor === 'function' ? Object.getPrototypeOf(obj) : Object.prototype;
+
+ if (proto === null) {
+ return true;
+ }
+
+ var constructor = proto.constructor;
+
+ return typeof constructor === 'function' && constructor instanceof constructor && fnToString(constructor) === fnToString(Object);
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 6 */
+/***/ function(module, exports) {
+
+ "use strict";
+
+ exports.__esModule = true;
+ exports["default"] = shallowEqual;
+
+ function shallowEqual(objA, objB) {
+ if (objA === objB) {
+ return true;
+ }
+
+ var keysA = Object.keys(objA);
+ var keysB = Object.keys(objB);
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ // Test for A's keys different from B.
+ var hasOwn = Object.prototype.hasOwnProperty;
+ for (var i = 0; i < keysA.length; i++) {
+ if (!hasOwn.call(objB, keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ module.exports = exports["default"];
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports['default'] = wrapActionCreators;
+
+ var _redux = __webpack_require__(11);
+
+ function wrapActionCreators(actionCreators) {
+ return function (dispatch) {
+ return _redux.bindActionCreators(actionCreators, dispatch);
+ };
+ }
+
+ module.exports = exports['default'];
+
+/***/ },
+/* 8 */
+/***/ function(module, exports) {
+
+ /**
+ * Copyright 2015, Yahoo! Inc.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+ 'use strict';
+
+ var REACT_STATICS = {
+ childContextTypes: true,
+ contextTypes: true,
+ defaultProps: true,
+ displayName: true,
+ getDefaultProps: true,
+ mixins: true,
+ propTypes: true,
+ type: true
+ };
+
+ var KNOWN_STATICS = {
+ name: true,
+ length: true,
+ prototype: true,
+ caller: true,
+ arguments: true,
+ arity: true
+ };
+
+ module.exports = function hoistNonReactStatics(targetComponent, sourceComponent) {
+ var keys = Object.getOwnPropertyNames(sourceComponent);
+ for (var i=0; i<keys.length; ++i) {
+ if (!REACT_STATICS[keys[i]] && !KNOWN_STATICS[keys[i]]) {
+ targetComponent[keys[i]] = sourceComponent[keys[i]];
+ }
+ }
+
+ return targetComponent;
+ };
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule invariant
+ */
+
+ 'use strict';
+
+ /**
+ * Use invariant() to assert state which your program assumes to be true.
+ *
+ * Provide sprintf-style format (only %s is supported) and arguments
+ * to provide information about what broke and what you were
+ * expecting.
+ *
+ * The invariant message will be stripped in production, but the invariant
+ * will remain to ensure logic does not differ in production.
+ */
+
+ var invariant = function(condition, format, a, b, c, d, e, f) {
+ if (true) {
+ if (format === undefined) {
+ throw new Error('invariant requires an error message argument');
+ }
+ }
+
+ if (!condition) {
+ var error;
+ if (format === undefined) {
+ error = new Error(
+ 'Minified exception occurred; use the non-minified dev environment ' +
+ 'for the full error message and additional helpful warnings.'
+ );
+ } else {
+ var args = [a, b, c, d, e, f];
+ var argIndex = 0;
+ error = new Error(
+ 'Invariant Violation: ' +
+ format.replace(/%s/g, function() { return args[argIndex++]; })
+ );
+ }
+
+ error.framesToPop = 1; // we don't care about invariant's own frame
+ throw error;
+ }
+ };
+
+ module.exports = invariant;
+
+
+/***/ },
+/* 10 */
+/***/ function(module, exports) {
+
+ module.exports = __WEBPACK_EXTERNAL_MODULE_10__;
+
+/***/ },
+/* 11 */
+/***/ function(module, exports) {
+
+ module.exports = __WEBPACK_EXTERNAL_MODULE_11__;
+
+/***/ }
+/******/ ])
+});
+;
diff --git a/devtools/client/shared/vendor/react-virtualized.js b/devtools/client/shared/vendor/react-virtualized.js
new file mode 100644
index 000000000..dab201ac6
--- /dev/null
+++ b/devtools/client/shared/vendor/react-virtualized.js
@@ -0,0 +1,4296 @@
+var REACT_PATH = "devtools/client/shared/vendor/react";
+var REACT_DOM_PATH = "devtools/client/shared/vendor/react-dom";
+var REACT_SHALLOW_COMPARE = "devtools/client/shared/vendor/react-addons-shallow-compare";
+
+!function(root, factory) {
+ let React = require(REACT_PATH);
+ let shallowCompare = require(REACT_SHALLOW_COMPARE);
+ let ReactDOM = require(REACT_DOM_PATH);
+ module.exports = factory(React, shallowCompare, ReactDOM);
+}(this, function(__WEBPACK_EXTERNAL_MODULE_89__, __WEBPACK_EXTERNAL_MODULE_90__, __WEBPACK_EXTERNAL_MODULE_96__) {
+ /******/
+ return function(modules) {
+ /******/
+ /******/
+ // The require function
+ /******/
+ function __webpack_require__(moduleId) {
+ /******/
+ /******/
+ // Check if module is in cache
+ /******/
+ if (installedModules[moduleId]) /******/
+ return installedModules[moduleId].exports;
+ /******/
+ /******/
+ // Create a new module (and put it into the cache)
+ /******/
+ var module = installedModules[moduleId] = {
+ /******/
+ exports: {},
+ /******/
+ id: moduleId,
+ /******/
+ loaded: !1
+ };
+ /******/
+ /******/
+ // Return the exports of the module
+ /******/
+ /******/
+ /******/
+ // Execute the module function
+ /******/
+ /******/
+ /******/
+ // Flag the module as loaded
+ /******/
+ return modules[moduleId].call(module.exports, module, module.exports, __webpack_require__),
+ module.loaded = !0, module.exports;
+ }
+ // webpackBootstrap
+ /******/
+ // The module cache
+ /******/
+ var installedModules = {};
+ /******/
+ /******/
+ // Load entry module and return exports
+ /******/
+ /******/
+ /******/
+ /******/
+ // expose the modules object (__webpack_modules__)
+ /******/
+ /******/
+ /******/
+ // expose the module cache
+ /******/
+ /******/
+ /******/
+ // __webpack_public_path__
+ /******/
+ return __webpack_require__.m = modules, __webpack_require__.c = installedModules,
+ __webpack_require__.p = "", __webpack_require__(0);
+ }([ /* 0 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _ArrowKeyStepper = __webpack_require__(1);
+ Object.defineProperty(exports, "ArrowKeyStepper", {
+ enumerable: !0,
+ get: function() {
+ return _ArrowKeyStepper.ArrowKeyStepper;
+ }
+ });
+ var _AutoSizer = __webpack_require__(91);
+ Object.defineProperty(exports, "AutoSizer", {
+ enumerable: !0,
+ get: function() {
+ return _AutoSizer.AutoSizer;
+ }
+ });
+ var _CellMeasurer = __webpack_require__(94);
+ Object.defineProperty(exports, "CellMeasurer", {
+ enumerable: !0,
+ get: function() {
+ return _CellMeasurer.CellMeasurer;
+ }
+ }), Object.defineProperty(exports, "defaultCellMeasurerCellSizeCache", {
+ enumerable: !0,
+ get: function() {
+ return _CellMeasurer.defaultCellSizeCache;
+ }
+ }), Object.defineProperty(exports, "uniformSizeCellMeasurerCellSizeCache", {
+ enumerable: !0,
+ get: function() {
+ return _CellMeasurer.defaultCellSizeCache;
+ }
+ });
+ var _Collection = __webpack_require__(98);
+ Object.defineProperty(exports, "Collection", {
+ enumerable: !0,
+ get: function() {
+ return _Collection.Collection;
+ }
+ });
+ var _ColumnSizer = __webpack_require__(118);
+ Object.defineProperty(exports, "ColumnSizer", {
+ enumerable: !0,
+ get: function() {
+ return _ColumnSizer.ColumnSizer;
+ }
+ });
+ var _Table = __webpack_require__(128);
+ Object.defineProperty(exports, "defaultTableCellDataGetter", {
+ enumerable: !0,
+ get: function() {
+ return _Table.defaultCellDataGetter;
+ }
+ }), Object.defineProperty(exports, "defaultTableCellRenderer", {
+ enumerable: !0,
+ get: function() {
+ return _Table.defaultCellRenderer;
+ }
+ }), Object.defineProperty(exports, "defaultTableHeaderRenderer", {
+ enumerable: !0,
+ get: function() {
+ return _Table.defaultHeaderRenderer;
+ }
+ }), Object.defineProperty(exports, "defaultTableRowRenderer", {
+ enumerable: !0,
+ get: function() {
+ return _Table.defaultRowRenderer;
+ }
+ }), Object.defineProperty(exports, "Table", {
+ enumerable: !0,
+ get: function() {
+ return _Table.Table;
+ }
+ }), Object.defineProperty(exports, "Column", {
+ enumerable: !0,
+ get: function() {
+ return _Table.Column;
+ }
+ }), Object.defineProperty(exports, "SortDirection", {
+ enumerable: !0,
+ get: function() {
+ return _Table.SortDirection;
+ }
+ }), Object.defineProperty(exports, "SortIndicator", {
+ enumerable: !0,
+ get: function() {
+ return _Table.SortIndicator;
+ }
+ });
+ var _Grid = __webpack_require__(120);
+ Object.defineProperty(exports, "defaultCellRangeRenderer", {
+ enumerable: !0,
+ get: function() {
+ return _Grid.defaultCellRangeRenderer;
+ }
+ }), Object.defineProperty(exports, "Grid", {
+ enumerable: !0,
+ get: function() {
+ return _Grid.Grid;
+ }
+ });
+ var _InfiniteLoader = __webpack_require__(137);
+ Object.defineProperty(exports, "InfiniteLoader", {
+ enumerable: !0,
+ get: function() {
+ return _InfiniteLoader.InfiniteLoader;
+ }
+ });
+ var _ScrollSync = __webpack_require__(139);
+ Object.defineProperty(exports, "ScrollSync", {
+ enumerable: !0,
+ get: function() {
+ return _ScrollSync.ScrollSync;
+ }
+ });
+ var _List = __webpack_require__(141);
+ Object.defineProperty(exports, "List", {
+ enumerable: !0,
+ get: function() {
+ return _List.List;
+ }
+ });
+ var _WindowScroller = __webpack_require__(143);
+ Object.defineProperty(exports, "WindowScroller", {
+ enumerable: !0,
+ get: function() {
+ return _WindowScroller.WindowScroller;
+ }
+ });
+ }, /* 1 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.ArrowKeyStepper = exports.default = void 0;
+ var _ArrowKeyStepper2 = __webpack_require__(2), _ArrowKeyStepper3 = _interopRequireDefault(_ArrowKeyStepper2);
+ exports.default = _ArrowKeyStepper3.default, exports.ArrowKeyStepper = _ArrowKeyStepper3.default;
+ }, /* 2 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), ArrowKeyStepper = function(_Component) {
+ function ArrowKeyStepper(props, context) {
+ (0, _classCallCheck3.default)(this, ArrowKeyStepper);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (ArrowKeyStepper.__proto__ || (0,
+ _getPrototypeOf2.default)(ArrowKeyStepper)).call(this, props, context));
+ return _this.state = {
+ scrollToColumn: 0,
+ scrollToRow: 0
+ }, _this._columnStartIndex = 0, _this._columnStopIndex = 0, _this._rowStartIndex = 0,
+ _this._rowStopIndex = 0, _this._onKeyDown = _this._onKeyDown.bind(_this), _this._onSectionRendered = _this._onSectionRendered.bind(_this),
+ _this;
+ }
+ return (0, _inherits3.default)(ArrowKeyStepper, _Component), (0, _createClass3.default)(ArrowKeyStepper, [ {
+ key: "render",
+ value: function() {
+ var _props = this.props, className = _props.className, children = _props.children, _state = this.state, scrollToColumn = _state.scrollToColumn, scrollToRow = _state.scrollToRow;
+ return _react2.default.createElement("div", {
+ className: className,
+ onKeyDown: this._onKeyDown
+ }, children({
+ onSectionRendered: this._onSectionRendered,
+ scrollToColumn: scrollToColumn,
+ scrollToRow: scrollToRow
+ }));
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_onKeyDown",
+ value: function(event) {
+ var _props2 = this.props, columnCount = _props2.columnCount, rowCount = _props2.rowCount;
+ switch (event.key) {
+ case "ArrowDown":
+ event.preventDefault(), this.setState({
+ scrollToRow: Math.min(this._rowStopIndex + 1, rowCount - 1)
+ });
+ break;
+
+ case "ArrowLeft":
+ event.preventDefault(), this.setState({
+ scrollToColumn: Math.max(this._columnStartIndex - 1, 0)
+ });
+ break;
+
+ case "ArrowRight":
+ event.preventDefault(), this.setState({
+ scrollToColumn: Math.min(this._columnStopIndex + 1, columnCount - 1)
+ });
+ break;
+
+ case "ArrowUp":
+ event.preventDefault(), this.setState({
+ scrollToRow: Math.max(this._rowStartIndex - 1, 0)
+ });
+ }
+ }
+ }, {
+ key: "_onSectionRendered",
+ value: function(_ref) {
+ var columnStartIndex = _ref.columnStartIndex, columnStopIndex = _ref.columnStopIndex, rowStartIndex = _ref.rowStartIndex, rowStopIndex = _ref.rowStopIndex;
+ this._columnStartIndex = columnStartIndex, this._columnStopIndex = columnStopIndex,
+ this._rowStartIndex = rowStartIndex, this._rowStopIndex = rowStopIndex;
+ }
+ } ]), ArrowKeyStepper;
+ }(_react.Component);
+ exports.default = ArrowKeyStepper;
+ }, /* 3 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(4),
+ __esModule: !0
+ };
+ }, /* 4 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(5), module.exports = __webpack_require__(16).Object.getPrototypeOf;
+ }, /* 5 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.2.9 Object.getPrototypeOf(O)
+ var toObject = __webpack_require__(6), $getPrototypeOf = __webpack_require__(8);
+ __webpack_require__(14)("getPrototypeOf", function() {
+ return function(it) {
+ return $getPrototypeOf(toObject(it));
+ };
+ });
+ }, /* 6 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 7.1.13 ToObject(argument)
+ var defined = __webpack_require__(7);
+ module.exports = function(it) {
+ return Object(defined(it));
+ };
+ }, /* 7 */
+ /***/
+ function(module, exports) {
+ // 7.2.1 RequireObjectCoercible(argument)
+ module.exports = function(it) {
+ if (void 0 == it) throw TypeError("Can't call method on " + it);
+ return it;
+ };
+ }, /* 8 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.2.9 / 15.2.3.2 Object.getPrototypeOf(O)
+ var has = __webpack_require__(9), toObject = __webpack_require__(6), IE_PROTO = __webpack_require__(10)("IE_PROTO"), ObjectProto = Object.prototype;
+ module.exports = Object.getPrototypeOf || function(O) {
+ return O = toObject(O), has(O, IE_PROTO) ? O[IE_PROTO] : "function" == typeof O.constructor && O instanceof O.constructor ? O.constructor.prototype : O instanceof Object ? ObjectProto : null;
+ };
+ }, /* 9 */
+ /***/
+ function(module, exports) {
+ var hasOwnProperty = {}.hasOwnProperty;
+ module.exports = function(it, key) {
+ return hasOwnProperty.call(it, key);
+ };
+ }, /* 10 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var shared = __webpack_require__(11)("keys"), uid = __webpack_require__(13);
+ module.exports = function(key) {
+ return shared[key] || (shared[key] = uid(key));
+ };
+ }, /* 11 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var global = __webpack_require__(12), SHARED = "__core-js_shared__", store = global[SHARED] || (global[SHARED] = {});
+ module.exports = function(key) {
+ return store[key] || (store[key] = {});
+ };
+ }, /* 12 */
+ /***/
+ function(module, exports) {
+ // https://github.com/zloirock/core-js/issues/86#issuecomment-115759028
+ var global = module.exports = "undefined" != typeof window && window.Math == Math ? window : "undefined" != typeof self && self.Math == Math ? self : Function("return this")();
+ "number" == typeof __g && (__g = global);
+ }, /* 13 */
+ /***/
+ function(module, exports) {
+ var id = 0, px = Math.random();
+ module.exports = function(key) {
+ return "Symbol(".concat(void 0 === key ? "" : key, ")_", (++id + px).toString(36));
+ };
+ }, /* 14 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // most Object methods by ES6 should accept primitives
+ var $export = __webpack_require__(15), core = __webpack_require__(16), fails = __webpack_require__(25);
+ module.exports = function(KEY, exec) {
+ var fn = (core.Object || {})[KEY] || Object[KEY], exp = {};
+ exp[KEY] = exec(fn), $export($export.S + $export.F * fails(function() {
+ fn(1);
+ }), "Object", exp);
+ };
+ }, /* 15 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var global = __webpack_require__(12), core = __webpack_require__(16), ctx = __webpack_require__(17), hide = __webpack_require__(19), PROTOTYPE = "prototype", $export = function(type, name, source) {
+ var key, own, out, IS_FORCED = type & $export.F, IS_GLOBAL = type & $export.G, IS_STATIC = type & $export.S, IS_PROTO = type & $export.P, IS_BIND = type & $export.B, IS_WRAP = type & $export.W, exports = IS_GLOBAL ? core : core[name] || (core[name] = {}), expProto = exports[PROTOTYPE], target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {})[PROTOTYPE];
+ IS_GLOBAL && (source = name);
+ for (key in source) // contains in native
+ own = !IS_FORCED && target && void 0 !== target[key], own && key in exports || (// export native or passed
+ out = own ? target[key] : source[key], // prevent global pollution for namespaces
+ exports[key] = IS_GLOBAL && "function" != typeof target[key] ? source[key] : IS_BIND && own ? ctx(out, global) : IS_WRAP && target[key] == out ? function(C) {
+ var F = function(a, b, c) {
+ if (this instanceof C) {
+ switch (arguments.length) {
+ case 0:
+ return new C();
+
+ case 1:
+ return new C(a);
+
+ case 2:
+ return new C(a, b);
+ }
+ return new C(a, b, c);
+ }
+ return C.apply(this, arguments);
+ };
+ return F[PROTOTYPE] = C[PROTOTYPE], F;
+ }(out) : IS_PROTO && "function" == typeof out ? ctx(Function.call, out) : out, // export proto methods to core.%CONSTRUCTOR%.methods.%NAME%
+ IS_PROTO && ((exports.virtual || (exports.virtual = {}))[key] = out, // export proto methods to core.%CONSTRUCTOR%.prototype.%NAME%
+ type & $export.R && expProto && !expProto[key] && hide(expProto, key, out)));
+ };
+ // type bitmap
+ $export.F = 1, // forced
+ $export.G = 2, // global
+ $export.S = 4, // static
+ $export.P = 8, // proto
+ $export.B = 16, // bind
+ $export.W = 32, // wrap
+ $export.U = 64, // safe
+ $export.R = 128, // real proto method for `library`
+ module.exports = $export;
+ }, /* 16 */
+ /***/
+ function(module, exports) {
+ var core = module.exports = {
+ version: "2.4.0"
+ };
+ "number" == typeof __e && (__e = core);
+ }, /* 17 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // optional / simple context binding
+ var aFunction = __webpack_require__(18);
+ module.exports = function(fn, that, length) {
+ if (aFunction(fn), void 0 === that) return fn;
+ switch (length) {
+ case 1:
+ return function(a) {
+ return fn.call(that, a);
+ };
+
+ case 2:
+ return function(a, b) {
+ return fn.call(that, a, b);
+ };
+
+ case 3:
+ return function(a, b, c) {
+ return fn.call(that, a, b, c);
+ };
+ }
+ return function() {
+ return fn.apply(that, arguments);
+ };
+ };
+ }, /* 18 */
+ /***/
+ function(module, exports) {
+ module.exports = function(it) {
+ if ("function" != typeof it) throw TypeError(it + " is not a function!");
+ return it;
+ };
+ }, /* 19 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var dP = __webpack_require__(20), createDesc = __webpack_require__(28);
+ module.exports = __webpack_require__(24) ? function(object, key, value) {
+ return dP.f(object, key, createDesc(1, value));
+ } : function(object, key, value) {
+ return object[key] = value, object;
+ };
+ }, /* 20 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var anObject = __webpack_require__(21), IE8_DOM_DEFINE = __webpack_require__(23), toPrimitive = __webpack_require__(27), dP = Object.defineProperty;
+ exports.f = __webpack_require__(24) ? Object.defineProperty : function(O, P, Attributes) {
+ if (anObject(O), P = toPrimitive(P, !0), anObject(Attributes), IE8_DOM_DEFINE) try {
+ return dP(O, P, Attributes);
+ } catch (e) {}
+ if ("get" in Attributes || "set" in Attributes) throw TypeError("Accessors not supported!");
+ return "value" in Attributes && (O[P] = Attributes.value), O;
+ };
+ }, /* 21 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var isObject = __webpack_require__(22);
+ module.exports = function(it) {
+ if (!isObject(it)) throw TypeError(it + " is not an object!");
+ return it;
+ };
+ }, /* 22 */
+ /***/
+ function(module, exports) {
+ module.exports = function(it) {
+ return "object" == typeof it ? null !== it : "function" == typeof it;
+ };
+ }, /* 23 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = !__webpack_require__(24) && !__webpack_require__(25)(function() {
+ return 7 != Object.defineProperty(__webpack_require__(26)("div"), "a", {
+ get: function() {
+ return 7;
+ }
+ }).a;
+ });
+ }, /* 24 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // Thank's IE8 for his funny defineProperty
+ module.exports = !__webpack_require__(25)(function() {
+ return 7 != Object.defineProperty({}, "a", {
+ get: function() {
+ return 7;
+ }
+ }).a;
+ });
+ }, /* 25 */
+ /***/
+ function(module, exports) {
+ module.exports = function(exec) {
+ try {
+ return !!exec();
+ } catch (e) {
+ return !0;
+ }
+ };
+ }, /* 26 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var isObject = __webpack_require__(22), document = __webpack_require__(12).document, is = isObject(document) && isObject(document.createElement);
+ module.exports = function(it) {
+ return is ? document.createElementNS("http://www.w3.org/1999/xhtml",it) : {};
+ };
+ }, /* 27 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 7.1.1 ToPrimitive(input [, PreferredType])
+ var isObject = __webpack_require__(22);
+ // instead of the ES6 spec version, we didn't implement @@toPrimitive case
+ // and the second argument - flag - preferred type is a string
+ module.exports = function(it, S) {
+ if (!isObject(it)) return it;
+ var fn, val;
+ if (S && "function" == typeof (fn = it.toString) && !isObject(val = fn.call(it))) return val;
+ if ("function" == typeof (fn = it.valueOf) && !isObject(val = fn.call(it))) return val;
+ if (!S && "function" == typeof (fn = it.toString) && !isObject(val = fn.call(it))) return val;
+ throw TypeError("Can't convert object to primitive value");
+ };
+ }, /* 28 */
+ /***/
+ function(module, exports) {
+ module.exports = function(bitmap, value) {
+ return {
+ enumerable: !(1 & bitmap),
+ configurable: !(2 & bitmap),
+ writable: !(4 & bitmap),
+ value: value
+ };
+ };
+ }, /* 29 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ exports.__esModule = !0, exports.default = function(instance, Constructor) {
+ if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function");
+ };
+ }, /* 30 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ exports.__esModule = !0;
+ var _defineProperty = __webpack_require__(31), _defineProperty2 = _interopRequireDefault(_defineProperty);
+ exports.default = function() {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || !1, descriptor.configurable = !0,
+ "value" in descriptor && (descriptor.writable = !0), (0, _defineProperty2.default)(target, descriptor.key, descriptor);
+ }
+ }
+ return function(Constructor, protoProps, staticProps) {
+ return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps),
+ Constructor;
+ };
+ }();
+ }, /* 31 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(32),
+ __esModule: !0
+ };
+ }, /* 32 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(33);
+ var $Object = __webpack_require__(16).Object;
+ module.exports = function(it, key, desc) {
+ return $Object.defineProperty(it, key, desc);
+ };
+ }, /* 33 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var $export = __webpack_require__(15);
+ // 19.1.2.4 / 15.2.3.6 Object.defineProperty(O, P, Attributes)
+ $export($export.S + $export.F * !__webpack_require__(24), "Object", {
+ defineProperty: __webpack_require__(20).f
+ });
+ }, /* 34 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ exports.__esModule = !0;
+ var _typeof2 = __webpack_require__(35), _typeof3 = _interopRequireDefault(_typeof2);
+ exports.default = function(self, call) {
+ if (!self) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+ return !call || "object" !== ("undefined" == typeof call ? "undefined" : (0, _typeof3.default)(call)) && "function" != typeof call ? self : call;
+ };
+ }, /* 35 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ exports.__esModule = !0;
+ var _iterator = __webpack_require__(36), _iterator2 = _interopRequireDefault(_iterator), _symbol = __webpack_require__(65), _symbol2 = _interopRequireDefault(_symbol), _typeof = "function" == typeof _symbol2.default && "symbol" == typeof _iterator2.default ? function(obj) {
+ return typeof obj;
+ } : function(obj) {
+ return obj && "function" == typeof _symbol2.default && obj.constructor === _symbol2.default && obj !== _symbol2.default.prototype ? "symbol" : typeof obj;
+ };
+ exports.default = "function" == typeof _symbol2.default && "symbol" === _typeof(_iterator2.default) ? function(obj) {
+ return "undefined" == typeof obj ? "undefined" : _typeof(obj);
+ } : function(obj) {
+ return obj && "function" == typeof _symbol2.default && obj.constructor === _symbol2.default && obj !== _symbol2.default.prototype ? "symbol" : "undefined" == typeof obj ? "undefined" : _typeof(obj);
+ };
+ }, /* 36 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(37),
+ __esModule: !0
+ };
+ }, /* 37 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(38), __webpack_require__(60), module.exports = __webpack_require__(64).f("iterator");
+ }, /* 38 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ var $at = __webpack_require__(39)(!0);
+ // 21.1.3.27 String.prototype[@@iterator]()
+ __webpack_require__(41)(String, "String", function(iterated) {
+ this._t = String(iterated), // target
+ this._i = 0;
+ }, function() {
+ var point, O = this._t, index = this._i;
+ return index >= O.length ? {
+ value: void 0,
+ done: !0
+ } : (point = $at(O, index), this._i += point.length, {
+ value: point,
+ done: !1
+ });
+ });
+ }, /* 39 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var toInteger = __webpack_require__(40), defined = __webpack_require__(7);
+ // true -> String#at
+ // false -> String#codePointAt
+ module.exports = function(TO_STRING) {
+ return function(that, pos) {
+ var a, b, s = String(defined(that)), i = toInteger(pos), l = s.length;
+ return i < 0 || i >= l ? TO_STRING ? "" : void 0 : (a = s.charCodeAt(i), a < 55296 || a > 56319 || i + 1 === l || (b = s.charCodeAt(i + 1)) < 56320 || b > 57343 ? TO_STRING ? s.charAt(i) : a : TO_STRING ? s.slice(i, i + 2) : (a - 55296 << 10) + (b - 56320) + 65536);
+ };
+ };
+ }, /* 40 */
+ /***/
+ function(module, exports) {
+ // 7.1.4 ToInteger
+ var ceil = Math.ceil, floor = Math.floor;
+ module.exports = function(it) {
+ return isNaN(it = +it) ? 0 : (it > 0 ? floor : ceil)(it);
+ };
+ }, /* 41 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ var LIBRARY = __webpack_require__(42), $export = __webpack_require__(15), redefine = __webpack_require__(43), hide = __webpack_require__(19), has = __webpack_require__(9), Iterators = __webpack_require__(44), $iterCreate = __webpack_require__(45), setToStringTag = __webpack_require__(58), getPrototypeOf = __webpack_require__(8), ITERATOR = __webpack_require__(59)("iterator"), BUGGY = !([].keys && "next" in [].keys()), FF_ITERATOR = "@@iterator", KEYS = "keys", VALUES = "values", returnThis = function() {
+ return this;
+ };
+ module.exports = function(Base, NAME, Constructor, next, DEFAULT, IS_SET, FORCED) {
+ $iterCreate(Constructor, NAME, next);
+ var methods, key, IteratorPrototype, getMethod = function(kind) {
+ if (!BUGGY && kind in proto) return proto[kind];
+ switch (kind) {
+ case KEYS:
+ return function() {
+ return new Constructor(this, kind);
+ };
+
+ case VALUES:
+ return function() {
+ return new Constructor(this, kind);
+ };
+ }
+ return function() {
+ return new Constructor(this, kind);
+ };
+ }, TAG = NAME + " Iterator", DEF_VALUES = DEFAULT == VALUES, VALUES_BUG = !1, proto = Base.prototype, $native = proto[ITERATOR] || proto[FF_ITERATOR] || DEFAULT && proto[DEFAULT], $default = $native || getMethod(DEFAULT), $entries = DEFAULT ? DEF_VALUES ? getMethod("entries") : $default : void 0, $anyNative = "Array" == NAME ? proto.entries || $native : $native;
+ if (// Fix native
+ $anyNative && (IteratorPrototype = getPrototypeOf($anyNative.call(new Base())),
+ IteratorPrototype !== Object.prototype && (// Set @@toStringTag to native iterators
+ setToStringTag(IteratorPrototype, TAG, !0), // fix for some old engines
+ LIBRARY || has(IteratorPrototype, ITERATOR) || hide(IteratorPrototype, ITERATOR, returnThis))),
+ // fix Array#{values, @@iterator}.name in V8 / FF
+ DEF_VALUES && $native && $native.name !== VALUES && (VALUES_BUG = !0, $default = function() {
+ return $native.call(this);
+ }), // Define iterator
+ LIBRARY && !FORCED || !BUGGY && !VALUES_BUG && proto[ITERATOR] || hide(proto, ITERATOR, $default),
+ // Plug for library
+ Iterators[NAME] = $default, Iterators[TAG] = returnThis, DEFAULT) if (methods = {
+ values: DEF_VALUES ? $default : getMethod(VALUES),
+ keys: IS_SET ? $default : getMethod(KEYS),
+ entries: $entries
+ }, FORCED) for (key in methods) key in proto || redefine(proto, key, methods[key]); else $export($export.P + $export.F * (BUGGY || VALUES_BUG), NAME, methods);
+ return methods;
+ };
+ }, /* 42 */
+ /***/
+ function(module, exports) {
+ module.exports = !0;
+ }, /* 43 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = __webpack_require__(19);
+ }, /* 44 */
+ /***/
+ function(module, exports) {
+ module.exports = {};
+ }, /* 45 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ var create = __webpack_require__(46), descriptor = __webpack_require__(28), setToStringTag = __webpack_require__(58), IteratorPrototype = {};
+ // 25.1.2.1.1 %IteratorPrototype%[@@iterator]()
+ __webpack_require__(19)(IteratorPrototype, __webpack_require__(59)("iterator"), function() {
+ return this;
+ }), module.exports = function(Constructor, NAME, next) {
+ Constructor.prototype = create(IteratorPrototype, {
+ next: descriptor(1, next)
+ }), setToStringTag(Constructor, NAME + " Iterator");
+ };
+ }, /* 46 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.2.2 / 15.2.3.5 Object.create(O [, Properties])
+ var anObject = __webpack_require__(21), dPs = __webpack_require__(47), enumBugKeys = __webpack_require__(56), IE_PROTO = __webpack_require__(10)("IE_PROTO"), Empty = function() {}, PROTOTYPE = "prototype", createDict = function() {
+ // Thrash, waste and sodomy: IE GC bug
+ var iframeDocument, iframe = __webpack_require__(26)("iframe"), i = enumBugKeys.length, lt = "<", gt = ">";
+ for (iframe.style.display = "none", __webpack_require__(57).appendChild(iframe),
+ iframe.src = "javascript:", // eslint-disable-line no-script-url
+ // createDict = iframe.contentWindow.Object;
+ // html.removeChild(iframe);
+ iframeDocument = iframe.contentWindow.document, iframeDocument.open(), iframeDocument.write(lt + "script" + gt + "document.F=Object" + lt + "/script" + gt),
+ iframeDocument.close(), createDict = iframeDocument.F; i--; ) delete createDict[PROTOTYPE][enumBugKeys[i]];
+ return createDict();
+ };
+ module.exports = Object.create || function(O, Properties) {
+ var result;
+ // add "__proto__" for Object.getPrototypeOf polyfill
+ return null !== O ? (Empty[PROTOTYPE] = anObject(O), result = new Empty(), Empty[PROTOTYPE] = null,
+ result[IE_PROTO] = O) : result = createDict(), void 0 === Properties ? result : dPs(result, Properties);
+ };
+ }, /* 47 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var dP = __webpack_require__(20), anObject = __webpack_require__(21), getKeys = __webpack_require__(48);
+ module.exports = __webpack_require__(24) ? Object.defineProperties : function(O, Properties) {
+ anObject(O);
+ for (var P, keys = getKeys(Properties), length = keys.length, i = 0; length > i; ) dP.f(O, P = keys[i++], Properties[P]);
+ return O;
+ };
+ }, /* 48 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.2.14 / 15.2.3.14 Object.keys(O)
+ var $keys = __webpack_require__(49), enumBugKeys = __webpack_require__(56);
+ module.exports = Object.keys || function(O) {
+ return $keys(O, enumBugKeys);
+ };
+ }, /* 49 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var has = __webpack_require__(9), toIObject = __webpack_require__(50), arrayIndexOf = __webpack_require__(53)(!1), IE_PROTO = __webpack_require__(10)("IE_PROTO");
+ module.exports = function(object, names) {
+ var key, O = toIObject(object), i = 0, result = [];
+ for (key in O) key != IE_PROTO && has(O, key) && result.push(key);
+ // Don't enum bug & hidden keys
+ for (;names.length > i; ) has(O, key = names[i++]) && (~arrayIndexOf(result, key) || result.push(key));
+ return result;
+ };
+ }, /* 50 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // to indexed object, toObject with fallback for non-array-like ES3 strings
+ var IObject = __webpack_require__(51), defined = __webpack_require__(7);
+ module.exports = function(it) {
+ return IObject(defined(it));
+ };
+ }, /* 51 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // fallback for non-array-like ES3 and non-enumerable old V8 strings
+ var cof = __webpack_require__(52);
+ module.exports = Object("z").propertyIsEnumerable(0) ? Object : function(it) {
+ return "String" == cof(it) ? it.split("") : Object(it);
+ };
+ }, /* 52 */
+ /***/
+ function(module, exports) {
+ var toString = {}.toString;
+ module.exports = function(it) {
+ return toString.call(it).slice(8, -1);
+ };
+ }, /* 53 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // false -> Array#indexOf
+ // true -> Array#includes
+ var toIObject = __webpack_require__(50), toLength = __webpack_require__(54), toIndex = __webpack_require__(55);
+ module.exports = function(IS_INCLUDES) {
+ return function($this, el, fromIndex) {
+ var value, O = toIObject($this), length = toLength(O.length), index = toIndex(fromIndex, length);
+ // Array#includes uses SameValueZero equality algorithm
+ if (IS_INCLUDES && el != el) {
+ for (;length > index; ) if (value = O[index++], value != value) return !0;
+ } else for (;length > index; index++) if ((IS_INCLUDES || index in O) && O[index] === el) return IS_INCLUDES || index || 0;
+ return !IS_INCLUDES && -1;
+ };
+ };
+ }, /* 54 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 7.1.15 ToLength
+ var toInteger = __webpack_require__(40), min = Math.min;
+ module.exports = function(it) {
+ return it > 0 ? min(toInteger(it), 9007199254740991) : 0;
+ };
+ }, /* 55 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var toInteger = __webpack_require__(40), max = Math.max, min = Math.min;
+ module.exports = function(index, length) {
+ return index = toInteger(index), index < 0 ? max(index + length, 0) : min(index, length);
+ };
+ }, /* 56 */
+ /***/
+ function(module, exports) {
+ // IE 8- don't enum bug keys
+ module.exports = "constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",");
+ }, /* 57 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = __webpack_require__(12).document && document.documentElement;
+ }, /* 58 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var def = __webpack_require__(20).f, has = __webpack_require__(9), TAG = __webpack_require__(59)("toStringTag");
+ module.exports = function(it, tag, stat) {
+ it && !has(it = stat ? it : it.prototype, TAG) && def(it, TAG, {
+ configurable: !0,
+ value: tag
+ });
+ };
+ }, /* 59 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var store = __webpack_require__(11)("wks"), uid = __webpack_require__(13), Symbol = __webpack_require__(12).Symbol, USE_SYMBOL = "function" == typeof Symbol, $exports = module.exports = function(name) {
+ return store[name] || (store[name] = USE_SYMBOL && Symbol[name] || (USE_SYMBOL ? Symbol : uid)("Symbol." + name));
+ };
+ $exports.store = store;
+ }, /* 60 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(61);
+ for (var global = __webpack_require__(12), hide = __webpack_require__(19), Iterators = __webpack_require__(44), TO_STRING_TAG = __webpack_require__(59)("toStringTag"), collections = [ "NodeList", "DOMTokenList", "MediaList", "StyleSheetList", "CSSRuleList" ], i = 0; i < 5; i++) {
+ var NAME = collections[i], Collection = global[NAME], proto = Collection && Collection.prototype;
+ proto && !proto[TO_STRING_TAG] && hide(proto, TO_STRING_TAG, NAME), Iterators[NAME] = Iterators.Array;
+ }
+ }, /* 61 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ var addToUnscopables = __webpack_require__(62), step = __webpack_require__(63), Iterators = __webpack_require__(44), toIObject = __webpack_require__(50);
+ // 22.1.3.4 Array.prototype.entries()
+ // 22.1.3.13 Array.prototype.keys()
+ // 22.1.3.29 Array.prototype.values()
+ // 22.1.3.30 Array.prototype[@@iterator]()
+ module.exports = __webpack_require__(41)(Array, "Array", function(iterated, kind) {
+ this._t = toIObject(iterated), // target
+ this._i = 0, // next index
+ this._k = kind;
+ }, function() {
+ var O = this._t, kind = this._k, index = this._i++;
+ return !O || index >= O.length ? (this._t = void 0, step(1)) : "keys" == kind ? step(0, index) : "values" == kind ? step(0, O[index]) : step(0, [ index, O[index] ]);
+ }, "values"), // argumentsList[@@iterator] is %ArrayProto_values% (9.4.4.6, 9.4.4.7)
+ Iterators.Arguments = Iterators.Array, addToUnscopables("keys"), addToUnscopables("values"),
+ addToUnscopables("entries");
+ }, /* 62 */
+ /***/
+ function(module, exports) {
+ module.exports = function() {};
+ }, /* 63 */
+ /***/
+ function(module, exports) {
+ module.exports = function(done, value) {
+ return {
+ value: value,
+ done: !!done
+ };
+ };
+ }, /* 64 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ exports.f = __webpack_require__(59);
+ }, /* 65 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(66),
+ __esModule: !0
+ };
+ }, /* 66 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(67), __webpack_require__(78), __webpack_require__(79), __webpack_require__(80),
+ module.exports = __webpack_require__(16).Symbol;
+ }, /* 67 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ // ECMAScript 6 symbols shim
+ var global = __webpack_require__(12), has = __webpack_require__(9), DESCRIPTORS = __webpack_require__(24), $export = __webpack_require__(15), redefine = __webpack_require__(43), META = __webpack_require__(68).KEY, $fails = __webpack_require__(25), shared = __webpack_require__(11), setToStringTag = __webpack_require__(58), uid = __webpack_require__(13), wks = __webpack_require__(59), wksExt = __webpack_require__(64), wksDefine = __webpack_require__(69), keyOf = __webpack_require__(70), enumKeys = __webpack_require__(71), isArray = __webpack_require__(74), anObject = __webpack_require__(21), toIObject = __webpack_require__(50), toPrimitive = __webpack_require__(27), createDesc = __webpack_require__(28), _create = __webpack_require__(46), gOPNExt = __webpack_require__(75), $GOPD = __webpack_require__(77), $DP = __webpack_require__(20), $keys = __webpack_require__(48), gOPD = $GOPD.f, dP = $DP.f, gOPN = gOPNExt.f, $Symbol = global.Symbol, $JSON = global.JSON, _stringify = $JSON && $JSON.stringify, PROTOTYPE = "prototype", HIDDEN = wks("_hidden"), TO_PRIMITIVE = wks("toPrimitive"), isEnum = {}.propertyIsEnumerable, SymbolRegistry = shared("symbol-registry"), AllSymbols = shared("symbols"), OPSymbols = shared("op-symbols"), ObjectProto = Object[PROTOTYPE], USE_NATIVE = "function" == typeof $Symbol, QObject = global.QObject, setter = !QObject || !QObject[PROTOTYPE] || !QObject[PROTOTYPE].findChild, setSymbolDesc = DESCRIPTORS && $fails(function() {
+ return 7 != _create(dP({}, "a", {
+ get: function() {
+ return dP(this, "a", {
+ value: 7
+ }).a;
+ }
+ })).a;
+ }) ? function(it, key, D) {
+ var protoDesc = gOPD(ObjectProto, key);
+ protoDesc && delete ObjectProto[key], dP(it, key, D), protoDesc && it !== ObjectProto && dP(ObjectProto, key, protoDesc);
+ } : dP, wrap = function(tag) {
+ var sym = AllSymbols[tag] = _create($Symbol[PROTOTYPE]);
+ return sym._k = tag, sym;
+ }, isSymbol = USE_NATIVE && "symbol" == typeof $Symbol.iterator ? function(it) {
+ return "symbol" == typeof it;
+ } : function(it) {
+ return it instanceof $Symbol;
+ }, $defineProperty = function(it, key, D) {
+ return it === ObjectProto && $defineProperty(OPSymbols, key, D), anObject(it), key = toPrimitive(key, !0),
+ anObject(D), has(AllSymbols, key) ? (D.enumerable ? (has(it, HIDDEN) && it[HIDDEN][key] && (it[HIDDEN][key] = !1),
+ D = _create(D, {
+ enumerable: createDesc(0, !1)
+ })) : (has(it, HIDDEN) || dP(it, HIDDEN, createDesc(1, {})), it[HIDDEN][key] = !0),
+ setSymbolDesc(it, key, D)) : dP(it, key, D);
+ }, $defineProperties = function(it, P) {
+ anObject(it);
+ for (var key, keys = enumKeys(P = toIObject(P)), i = 0, l = keys.length; l > i; ) $defineProperty(it, key = keys[i++], P[key]);
+ return it;
+ }, $create = function(it, P) {
+ return void 0 === P ? _create(it) : $defineProperties(_create(it), P);
+ }, $propertyIsEnumerable = function(key) {
+ var E = isEnum.call(this, key = toPrimitive(key, !0));
+ return !(this === ObjectProto && has(AllSymbols, key) && !has(OPSymbols, key)) && (!(E || !has(this, key) || !has(AllSymbols, key) || has(this, HIDDEN) && this[HIDDEN][key]) || E);
+ }, $getOwnPropertyDescriptor = function(it, key) {
+ if (it = toIObject(it), key = toPrimitive(key, !0), it !== ObjectProto || !has(AllSymbols, key) || has(OPSymbols, key)) {
+ var D = gOPD(it, key);
+ return !D || !has(AllSymbols, key) || has(it, HIDDEN) && it[HIDDEN][key] || (D.enumerable = !0),
+ D;
+ }
+ }, $getOwnPropertyNames = function(it) {
+ for (var key, names = gOPN(toIObject(it)), result = [], i = 0; names.length > i; ) has(AllSymbols, key = names[i++]) || key == HIDDEN || key == META || result.push(key);
+ return result;
+ }, $getOwnPropertySymbols = function(it) {
+ for (var key, IS_OP = it === ObjectProto, names = gOPN(IS_OP ? OPSymbols : toIObject(it)), result = [], i = 0; names.length > i; ) !has(AllSymbols, key = names[i++]) || IS_OP && !has(ObjectProto, key) || result.push(AllSymbols[key]);
+ return result;
+ };
+ // 19.4.1.1 Symbol([description])
+ USE_NATIVE || ($Symbol = function() {
+ if (this instanceof $Symbol) throw TypeError("Symbol is not a constructor!");
+ var tag = uid(arguments.length > 0 ? arguments[0] : void 0), $set = function(value) {
+ this === ObjectProto && $set.call(OPSymbols, value), has(this, HIDDEN) && has(this[HIDDEN], tag) && (this[HIDDEN][tag] = !1),
+ setSymbolDesc(this, tag, createDesc(1, value));
+ };
+ return DESCRIPTORS && setter && setSymbolDesc(ObjectProto, tag, {
+ configurable: !0,
+ set: $set
+ }), wrap(tag);
+ }, redefine($Symbol[PROTOTYPE], "toString", function() {
+ return this._k;
+ }), $GOPD.f = $getOwnPropertyDescriptor, $DP.f = $defineProperty, __webpack_require__(76).f = gOPNExt.f = $getOwnPropertyNames,
+ __webpack_require__(73).f = $propertyIsEnumerable, __webpack_require__(72).f = $getOwnPropertySymbols,
+ DESCRIPTORS && !__webpack_require__(42) && redefine(ObjectProto, "propertyIsEnumerable", $propertyIsEnumerable, !0),
+ wksExt.f = function(name) {
+ return wrap(wks(name));
+ }), $export($export.G + $export.W + $export.F * !USE_NATIVE, {
+ Symbol: $Symbol
+ });
+ for (var symbols = "hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","), i = 0; symbols.length > i; ) wks(symbols[i++]);
+ for (var symbols = $keys(wks.store), i = 0; symbols.length > i; ) wksDefine(symbols[i++]);
+ $export($export.S + $export.F * !USE_NATIVE, "Symbol", {
+ // 19.4.2.1 Symbol.for(key)
+ for: function(key) {
+ return has(SymbolRegistry, key += "") ? SymbolRegistry[key] : SymbolRegistry[key] = $Symbol(key);
+ },
+ // 19.4.2.5 Symbol.keyFor(sym)
+ keyFor: function(key) {
+ if (isSymbol(key)) return keyOf(SymbolRegistry, key);
+ throw TypeError(key + " is not a symbol!");
+ },
+ useSetter: function() {
+ setter = !0;
+ },
+ useSimple: function() {
+ setter = !1;
+ }
+ }), $export($export.S + $export.F * !USE_NATIVE, "Object", {
+ // 19.1.2.2 Object.create(O [, Properties])
+ create: $create,
+ // 19.1.2.4 Object.defineProperty(O, P, Attributes)
+ defineProperty: $defineProperty,
+ // 19.1.2.3 Object.defineProperties(O, Properties)
+ defineProperties: $defineProperties,
+ // 19.1.2.6 Object.getOwnPropertyDescriptor(O, P)
+ getOwnPropertyDescriptor: $getOwnPropertyDescriptor,
+ // 19.1.2.7 Object.getOwnPropertyNames(O)
+ getOwnPropertyNames: $getOwnPropertyNames,
+ // 19.1.2.8 Object.getOwnPropertySymbols(O)
+ getOwnPropertySymbols: $getOwnPropertySymbols
+ }), // 24.3.2 JSON.stringify(value [, replacer [, space]])
+ $JSON && $export($export.S + $export.F * (!USE_NATIVE || $fails(function() {
+ var S = $Symbol();
+ // MS Edge converts symbol values to JSON as {}
+ // WebKit converts symbol values to JSON as null
+ // V8 throws on boxed symbols
+ return "[null]" != _stringify([ S ]) || "{}" != _stringify({
+ a: S
+ }) || "{}" != _stringify(Object(S));
+ })), "JSON", {
+ stringify: function(it) {
+ if (void 0 !== it && !isSymbol(it)) {
+ for (// IE8 returns string on undefined
+ var replacer, $replacer, args = [ it ], i = 1; arguments.length > i; ) args.push(arguments[i++]);
+ return replacer = args[1], "function" == typeof replacer && ($replacer = replacer),
+ !$replacer && isArray(replacer) || (replacer = function(key, value) {
+ if ($replacer && (value = $replacer.call(this, key, value)), !isSymbol(value)) return value;
+ }), args[1] = replacer, _stringify.apply($JSON, args);
+ }
+ }
+ }), // 19.4.3.4 Symbol.prototype[@@toPrimitive](hint)
+ $Symbol[PROTOTYPE][TO_PRIMITIVE] || __webpack_require__(19)($Symbol[PROTOTYPE], TO_PRIMITIVE, $Symbol[PROTOTYPE].valueOf),
+ // 19.4.3.5 Symbol.prototype[@@toStringTag]
+ setToStringTag($Symbol, "Symbol"), // 20.2.1.9 Math[@@toStringTag]
+ setToStringTag(Math, "Math", !0), // 24.3.3 JSON[@@toStringTag]
+ setToStringTag(global.JSON, "JSON", !0);
+ }, /* 68 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var META = __webpack_require__(13)("meta"), isObject = __webpack_require__(22), has = __webpack_require__(9), setDesc = __webpack_require__(20).f, id = 0, isExtensible = Object.isExtensible || function() {
+ return !0;
+ }, FREEZE = !__webpack_require__(25)(function() {
+ return isExtensible(Object.preventExtensions({}));
+ }), setMeta = function(it) {
+ setDesc(it, META, {
+ value: {
+ i: "O" + ++id,
+ // object ID
+ w: {}
+ }
+ });
+ }, fastKey = function(it, create) {
+ // return primitive with prefix
+ if (!isObject(it)) return "symbol" == typeof it ? it : ("string" == typeof it ? "S" : "P") + it;
+ if (!has(it, META)) {
+ // can't set metadata to uncaught frozen object
+ if (!isExtensible(it)) return "F";
+ // not necessary to add metadata
+ if (!create) return "E";
+ // add missing metadata
+ setMeta(it);
+ }
+ return it[META].i;
+ }, getWeak = function(it, create) {
+ if (!has(it, META)) {
+ // can't set metadata to uncaught frozen object
+ if (!isExtensible(it)) return !0;
+ // not necessary to add metadata
+ if (!create) return !1;
+ // add missing metadata
+ setMeta(it);
+ }
+ return it[META].w;
+ }, onFreeze = function(it) {
+ return FREEZE && meta.NEED && isExtensible(it) && !has(it, META) && setMeta(it),
+ it;
+ }, meta = module.exports = {
+ KEY: META,
+ NEED: !1,
+ fastKey: fastKey,
+ getWeak: getWeak,
+ onFreeze: onFreeze
+ };
+ }, /* 69 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var global = __webpack_require__(12), core = __webpack_require__(16), LIBRARY = __webpack_require__(42), wksExt = __webpack_require__(64), defineProperty = __webpack_require__(20).f;
+ module.exports = function(name) {
+ var $Symbol = core.Symbol || (core.Symbol = LIBRARY ? {} : global.Symbol || {});
+ "_" == name.charAt(0) || name in $Symbol || defineProperty($Symbol, name, {
+ value: wksExt.f(name)
+ });
+ };
+ }, /* 70 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var getKeys = __webpack_require__(48), toIObject = __webpack_require__(50);
+ module.exports = function(object, el) {
+ for (var key, O = toIObject(object), keys = getKeys(O), length = keys.length, index = 0; length > index; ) if (O[key = keys[index++]] === el) return key;
+ };
+ }, /* 71 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // all enumerable object keys, includes symbols
+ var getKeys = __webpack_require__(48), gOPS = __webpack_require__(72), pIE = __webpack_require__(73);
+ module.exports = function(it) {
+ var result = getKeys(it), getSymbols = gOPS.f;
+ if (getSymbols) for (var key, symbols = getSymbols(it), isEnum = pIE.f, i = 0; symbols.length > i; ) isEnum.call(it, key = symbols[i++]) && result.push(key);
+ return result;
+ };
+ }, /* 72 */
+ /***/
+ function(module, exports) {
+ exports.f = Object.getOwnPropertySymbols;
+ }, /* 73 */
+ /***/
+ function(module, exports) {
+ exports.f = {}.propertyIsEnumerable;
+ }, /* 74 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 7.2.2 IsArray(argument)
+ var cof = __webpack_require__(52);
+ module.exports = Array.isArray || function(arg) {
+ return "Array" == cof(arg);
+ };
+ }, /* 75 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // fallback for IE11 buggy Object.getOwnPropertyNames with iframe and window
+ var toIObject = __webpack_require__(50), gOPN = __webpack_require__(76).f, toString = {}.toString, windowNames = "object" == typeof window && window && Object.getOwnPropertyNames ? Object.getOwnPropertyNames(window) : [], getWindowNames = function(it) {
+ try {
+ return gOPN(it);
+ } catch (e) {
+ return windowNames.slice();
+ }
+ };
+ module.exports.f = function(it) {
+ return windowNames && "[object Window]" == toString.call(it) ? getWindowNames(it) : gOPN(toIObject(it));
+ };
+ }, /* 76 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.2.7 / 15.2.3.4 Object.getOwnPropertyNames(O)
+ var $keys = __webpack_require__(49), hiddenKeys = __webpack_require__(56).concat("length", "prototype");
+ exports.f = Object.getOwnPropertyNames || function(O) {
+ return $keys(O, hiddenKeys);
+ };
+ }, /* 77 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var pIE = __webpack_require__(73), createDesc = __webpack_require__(28), toIObject = __webpack_require__(50), toPrimitive = __webpack_require__(27), has = __webpack_require__(9), IE8_DOM_DEFINE = __webpack_require__(23), gOPD = Object.getOwnPropertyDescriptor;
+ exports.f = __webpack_require__(24) ? gOPD : function(O, P) {
+ if (O = toIObject(O), P = toPrimitive(P, !0), IE8_DOM_DEFINE) try {
+ return gOPD(O, P);
+ } catch (e) {}
+ if (has(O, P)) return createDesc(!pIE.f.call(O, P), O[P]);
+ };
+ }, /* 78 */
+ /***/
+ function(module, exports) {}, /* 79 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(69)("asyncIterator");
+ }, /* 80 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(69)("observable");
+ }, /* 81 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ exports.__esModule = !0;
+ var _setPrototypeOf = __webpack_require__(82), _setPrototypeOf2 = _interopRequireDefault(_setPrototypeOf), _create = __webpack_require__(86), _create2 = _interopRequireDefault(_create), _typeof2 = __webpack_require__(35), _typeof3 = _interopRequireDefault(_typeof2);
+ exports.default = function(subClass, superClass) {
+ if ("function" != typeof superClass && null !== superClass) throw new TypeError("Super expression must either be null or a function, not " + ("undefined" == typeof superClass ? "undefined" : (0,
+ _typeof3.default)(superClass)));
+ subClass.prototype = (0, _create2.default)(superClass && superClass.prototype, {
+ constructor: {
+ value: subClass,
+ enumerable: !1,
+ writable: !0,
+ configurable: !0
+ }
+ }), superClass && (_setPrototypeOf2.default ? (0, _setPrototypeOf2.default)(subClass, superClass) : subClass.__proto__ = superClass);
+ };
+ }, /* 82 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(83),
+ __esModule: !0
+ };
+ }, /* 83 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(84), module.exports = __webpack_require__(16).Object.setPrototypeOf;
+ }, /* 84 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.3.19 Object.setPrototypeOf(O, proto)
+ var $export = __webpack_require__(15);
+ $export($export.S, "Object", {
+ setPrototypeOf: __webpack_require__(85).set
+ });
+ }, /* 85 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // Works with __proto__ only. Old v8 can't work with null proto objects.
+ /* eslint-disable no-proto */
+ var isObject = __webpack_require__(22), anObject = __webpack_require__(21), check = function(O, proto) {
+ if (anObject(O), !isObject(proto) && null !== proto) throw TypeError(proto + ": can't set as prototype!");
+ };
+ module.exports = {
+ set: Object.setPrototypeOf || ("__proto__" in {} ? // eslint-disable-line
+ function(test, buggy, set) {
+ try {
+ set = __webpack_require__(17)(Function.call, __webpack_require__(77).f(Object.prototype, "__proto__").set, 2),
+ set(test, []), buggy = !(test instanceof Array);
+ } catch (e) {
+ buggy = !0;
+ }
+ return function(O, proto) {
+ return check(O, proto), buggy ? O.__proto__ = proto : set(O, proto), O;
+ };
+ }({}, !1) : void 0),
+ check: check
+ };
+ }, /* 86 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(87),
+ __esModule: !0
+ };
+ }, /* 87 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(88);
+ var $Object = __webpack_require__(16).Object;
+ module.exports = function(P, D) {
+ return $Object.create(P, D);
+ };
+ }, /* 88 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var $export = __webpack_require__(15);
+ // 19.1.2.2 / 15.2.3.5 Object.create(O [, Properties])
+ $export($export.S, "Object", {
+ create: __webpack_require__(46)
+ });
+ }, /* 89 */
+ /***/
+ function(module, exports) {
+ module.exports = __WEBPACK_EXTERNAL_MODULE_89__;
+ }, /* 90 */
+ /***/
+ function(module, exports) {
+ module.exports = __WEBPACK_EXTERNAL_MODULE_90__;
+ }, /* 91 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.AutoSizer = exports.default = void 0;
+ var _AutoSizer2 = __webpack_require__(92), _AutoSizer3 = _interopRequireDefault(_AutoSizer2);
+ exports.default = _AutoSizer3.default, exports.AutoSizer = _AutoSizer3.default;
+ }, /* 92 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _detectElementResize = __webpack_require__(93), _detectElementResize2 = _interopRequireDefault(_detectElementResize), AutoSizer = function(_Component) {
+ function AutoSizer(props) {
+ (0, _classCallCheck3.default)(this, AutoSizer);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (AutoSizer.__proto__ || (0,
+ _getPrototypeOf2.default)(AutoSizer)).call(this, props));
+ return _this.state = {
+ height: 0,
+ width: 0
+ }, _this._onResize = _this._onResize.bind(_this), _this._setRef = _this._setRef.bind(_this),
+ _this;
+ }
+ return (0, _inherits3.default)(AutoSizer, _Component), (0, _createClass3.default)(AutoSizer, [ {
+ key: "componentDidMount",
+ value: function() {
+ this._parentNode = this._autoSizer.parentNode, this._detectElementResize = (0, _detectElementResize2.default)(),
+ this._detectElementResize.addResizeListener(this._parentNode, this._onResize), this._onResize();
+ }
+ }, {
+ key: "componentWillUnmount",
+ value: function() {
+ this._detectElementResize && this._detectElementResize.removeResizeListener(this._parentNode, this._onResize);
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _props = this.props, children = _props.children, disableHeight = _props.disableHeight, disableWidth = _props.disableWidth, _state = this.state, height = _state.height, width = _state.width, outerStyle = {
+ overflow: "visible"
+ };
+ return disableHeight || (outerStyle.height = 0), disableWidth || (outerStyle.width = 0),
+ _react2.default.createElement("div", {
+ ref: this._setRef,
+ style: outerStyle
+ }, children({
+ height: height,
+ width: width
+ }));
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_onResize",
+ value: function() {
+ var onResize = this.props.onResize, boundingRect = this._parentNode.getBoundingClientRect(), height = boundingRect.height || 0, width = boundingRect.width || 0, style = window.getComputedStyle(this._parentNode), paddingLeft = parseInt(style.paddingLeft, 10) || 0, paddingRight = parseInt(style.paddingRight, 10) || 0, paddingTop = parseInt(style.paddingTop, 10) || 0, paddingBottom = parseInt(style.paddingBottom, 10) || 0;
+ this.setState({
+ height: height - paddingTop - paddingBottom,
+ width: width - paddingLeft - paddingRight
+ }), onResize({
+ height: height,
+ width: width
+ });
+ }
+ }, {
+ key: "_setRef",
+ value: function(autoSizer) {
+ this._autoSizer = autoSizer;
+ }
+ } ]), AutoSizer;
+ }(_react.Component);
+ AutoSizer.defaultProps = {
+ onResize: function() {}
+ }, exports.default = AutoSizer;
+ }, /* 93 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function createDetectElementResize() {
+ var _window;
+ _window = "undefined" != typeof window ? window : "undefined" != typeof self ? self : this;
+ var attachEvent = "undefined" != typeof document && document.attachEvent, stylesCreated = !1;
+ if (!attachEvent) {
+ var requestFrame = function() {
+ var raf = _window.requestAnimationFrame || _window.mozRequestAnimationFrame || _window.webkitRequestAnimationFrame || function(fn) {
+ return _window.setTimeout(fn, 20);
+ };
+ return function(fn) {
+ return raf(fn);
+ };
+ }(), cancelFrame = function() {
+ var cancel = _window.cancelAnimationFrame || _window.mozCancelAnimationFrame || _window.webkitCancelAnimationFrame || _window.clearTimeout;
+ return function(id) {
+ return cancel(id);
+ };
+ }(), resetTriggers = function(element) {
+ var triggers = element.__resizeTriggers__, expand = triggers.firstElementChild, contract = triggers.lastElementChild, expandChild = expand.firstElementChild;
+ contract.scrollLeft = contract.scrollWidth, contract.scrollTop = contract.scrollHeight,
+ expandChild.style.width = expand.offsetWidth + 1 + "px", expandChild.style.height = expand.offsetHeight + 1 + "px",
+ expand.scrollLeft = expand.scrollWidth, expand.scrollTop = expand.scrollHeight;
+ }, checkTriggers = function(element) {
+ return element.offsetWidth != element.__resizeLast__.width || element.offsetHeight != element.__resizeLast__.height;
+ }, scrollListener = function(e) {
+ if (!(e.target.className.indexOf("contract-trigger") < 0 && e.target.className.indexOf("expand-trigger") < 0)) {
+ var element = this;
+ resetTriggers(this), this.__resizeRAF__ && cancelFrame(this.__resizeRAF__), this.__resizeRAF__ = requestFrame(function() {
+ checkTriggers(element) && (element.__resizeLast__.width = element.offsetWidth, element.__resizeLast__.height = element.offsetHeight,
+ element.__resizeListeners__.forEach(function(fn) {
+ fn.call(element, e);
+ }));
+ });
+ }
+ }, animation = !1, animationstring = "animation", keyframeprefix = "", animationstartevent = "animationstart", domPrefixes = "Webkit Moz O ms".split(" "), startEvents = "webkitAnimationStart animationstart oAnimationStart MSAnimationStart".split(" "), pfx = "", elm = document.createElementNS("http://www.w3.org/1999/xhtml","fakeelement");
+ if (void 0 !== elm.style.animationName && (animation = !0), animation === !1) for (var i = 0; i < domPrefixes.length; i++) if (void 0 !== elm.style[domPrefixes[i] + "AnimationName"]) {
+ pfx = domPrefixes[i], animationstring = pfx + "Animation", keyframeprefix = "-" + pfx.toLowerCase() + "-",
+ animationstartevent = startEvents[i], animation = !0;
+ break;
+ }
+ var animationName = "resizeanim", animationKeyframes = "@" + keyframeprefix + "keyframes " + animationName + " { from { opacity: 0; } to { opacity: 0; } } ", animationStyle = keyframeprefix + "animation: 1ms " + animationName + "; ";
+ }
+ var createStyles = function() {
+ if (!stylesCreated) {
+ var css = (animationKeyframes ? animationKeyframes : "") + ".resize-triggers { " + (animationStyle ? animationStyle : "") + 'visibility: hidden; opacity: 0; } .resize-triggers, .resize-triggers > div, .contract-trigger:before { content: " "; display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; z-index: -1; } .resize-triggers > div { background: #eee; overflow: auto; } .contract-trigger:before { width: 200%; height: 200%; }', head = document.firstElementChild || document.getElementsByTagName("head")[0], style = document.createElementNS("http://www.w3.org/1999/xhtml","style");
+ style.type = "text/css", style.styleSheet ? style.styleSheet.cssText = css : style.appendChild(document.createTextNode(css)),
+ head.appendChild(style), stylesCreated = !0;
+ }
+ }, addResizeListener = function(element, fn) {
+ attachEvent ? element.attachEvent("onresize", fn) : (element.__resizeTriggers__ || ("static" == _window.getComputedStyle(element).position && (element.style.position = "relative"),
+ createStyles(), element.__resizeLast__ = {}, element.__resizeListeners__ = [], (element.__resizeTriggers__ = document.createElementNS("http://www.w3.org/1999/xhtml","div")).className = "resize-triggers",
+ element.__resizeTriggers__.innerHTML = '<div class="expand-trigger"><div></div></div><div class="contract-trigger"></div>',
+ element.appendChild(element.__resizeTriggers__), resetTriggers(element), element.addEventListener("scroll", scrollListener, !0),
+ animationstartevent && (element.__resizeTriggers__.__animationListener__ = function(e) {
+ e.animationName == animationName && resetTriggers(element);
+ }, element.__resizeTriggers__.addEventListener(animationstartevent, element.__resizeTriggers__.__animationListener__))),
+ element.__resizeListeners__.push(fn));
+ }, removeResizeListener = function(element, fn) {
+ attachEvent ? element.detachEvent("onresize", fn) : (element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1),
+ element.__resizeListeners__.length || (element.removeEventListener("scroll", scrollListener, !0),
+ element.__resizeTriggers__.__animationListener__ && (element.__resizeTriggers__.removeEventListener(animationstartevent, element.__resizeTriggers__.__animationListener__),
+ element.__resizeTriggers__.__animationListener__ = null), element.__resizeTriggers__ = !element.removeChild(element.__resizeTriggers__)));
+ };
+ return {
+ addResizeListener: addResizeListener,
+ removeResizeListener: removeResizeListener
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = createDetectElementResize;
+ }, /* 94 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.defaultCellSizeCache = exports.CellMeasurer = exports.default = void 0;
+ var _CellMeasurer2 = __webpack_require__(95), _CellMeasurer3 = _interopRequireDefault(_CellMeasurer2), _defaultCellSizeCache2 = __webpack_require__(97), _defaultCellSizeCache3 = _interopRequireDefault(_defaultCellSizeCache2);
+ exports.default = _CellMeasurer3.default, exports.CellMeasurer = _CellMeasurer3.default,
+ exports.defaultCellSizeCache = _defaultCellSizeCache3.default;
+ }, /* 95 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _reactAddonsShallowCompare = (_interopRequireDefault(_react),
+ __webpack_require__(90)), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _reactDom = __webpack_require__(96), _reactDom2 = _interopRequireDefault(_reactDom), _defaultCellSizeCache = __webpack_require__(97), _defaultCellSizeCache2 = _interopRequireDefault(_defaultCellSizeCache), CellMeasurer = function(_Component) {
+ function CellMeasurer(props, state) {
+ (0, _classCallCheck3.default)(this, CellMeasurer);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (CellMeasurer.__proto__ || (0,
+ _getPrototypeOf2.default)(CellMeasurer)).call(this, props, state));
+ return _this._cellSizeCache = props.cellSizeCache || new _defaultCellSizeCache2.default(),
+ _this.getColumnWidth = _this.getColumnWidth.bind(_this), _this.getRowHeight = _this.getRowHeight.bind(_this),
+ _this.resetMeasurements = _this.resetMeasurements.bind(_this), _this.resetMeasurementForColumn = _this.resetMeasurementForColumn.bind(_this),
+ _this.resetMeasurementForRow = _this.resetMeasurementForRow.bind(_this), _this;
+ }
+ return (0, _inherits3.default)(CellMeasurer, _Component), (0, _createClass3.default)(CellMeasurer, [ {
+ key: "getColumnWidth",
+ value: function(_ref) {
+ var index = _ref.index;
+ if (this._cellSizeCache.hasColumnWidth(index)) return this._cellSizeCache.getColumnWidth(index);
+ for (var rowCount = this.props.rowCount, maxWidth = 0, rowIndex = 0; rowIndex < rowCount; rowIndex++) {
+ var _measureCell2 = this._measureCell({
+ clientWidth: !0,
+ columnIndex: index,
+ rowIndex: rowIndex
+ }), width = _measureCell2.width;
+ maxWidth = Math.max(maxWidth, width);
+ }
+ return this._cellSizeCache.setColumnWidth(index, maxWidth), maxWidth;
+ }
+ }, {
+ key: "getRowHeight",
+ value: function(_ref2) {
+ var index = _ref2.index;
+ if (this._cellSizeCache.hasRowHeight(index)) return this._cellSizeCache.getRowHeight(index);
+ for (var columnCount = this.props.columnCount, maxHeight = 0, columnIndex = 0; columnIndex < columnCount; columnIndex++) {
+ var _measureCell3 = this._measureCell({
+ clientHeight: !0,
+ columnIndex: columnIndex,
+ rowIndex: index
+ }), height = _measureCell3.height;
+ maxHeight = Math.max(maxHeight, height);
+ }
+ return this._cellSizeCache.setRowHeight(index, maxHeight), maxHeight;
+ }
+ }, {
+ key: "resetMeasurementForColumn",
+ value: function(columnIndex) {
+ this._cellSizeCache.clearColumnWidth(columnIndex);
+ }
+ }, {
+ key: "resetMeasurementForRow",
+ value: function(rowIndex) {
+ this._cellSizeCache.clearRowHeight(rowIndex);
+ }
+ }, {
+ key: "resetMeasurements",
+ value: function() {
+ this._cellSizeCache.clearAllColumnWidths(), this._cellSizeCache.clearAllRowHeights();
+ }
+ }, {
+ key: "componentDidMount",
+ value: function() {
+ this._renderAndMount();
+ }
+ }, {
+ key: "componentWillReceiveProps",
+ value: function(nextProps) {
+ var cellSizeCache = this.props.cellSizeCache;
+ cellSizeCache !== nextProps.cellSizeCache && (this._cellSizeCache = nextProps.cellSizeCache),
+ this._updateDivDimensions(nextProps);
+ }
+ }, {
+ key: "componentWillUnmount",
+ value: function() {
+ this._unmountContainer();
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var children = this.props.children;
+ return children({
+ getColumnWidth: this.getColumnWidth,
+ getRowHeight: this.getRowHeight,
+ resetMeasurements: this.resetMeasurements,
+ resetMeasurementForColumn: this.resetMeasurementForColumn,
+ resetMeasurementForRow: this.resetMeasurementForRow
+ });
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_getContainerNode",
+ value: function(props) {
+ var container = props.container;
+ return container ? _reactDom2.default.findDOMNode("function" == typeof container ? container() : container) : document.firstElementChild;
+ }
+ }, {
+ key: "_measureCell",
+ value: function(_ref3) {
+ var _ref3$clientHeight = _ref3.clientHeight, clientHeight = void 0 !== _ref3$clientHeight && _ref3$clientHeight, _ref3$clientWidth = _ref3.clientWidth, clientWidth = void 0 === _ref3$clientWidth || _ref3$clientWidth, columnIndex = _ref3.columnIndex, rowIndex = _ref3.rowIndex, cellRenderer = this.props.cellRenderer, rendered = cellRenderer({
+ columnIndex: columnIndex,
+ rowIndex: rowIndex
+ });
+ this._renderAndMount(), _reactDom2.default.unstable_renderSubtreeIntoContainer(this, rendered, this._div);
+ var measurements = {
+ height: clientHeight && this._div.clientHeight,
+ width: clientWidth && this._div.clientWidth
+ };
+ return _reactDom2.default.unmountComponentAtNode(this._div), measurements;
+ }
+ }, {
+ key: "_renderAndMount",
+ value: function() {
+ this._div || (this._div = document.createElementNS("http://www.w3.org/1999/xhtml","div"), this._div.style.display = "inline-block",
+ this._div.style.position = "absolute", this._div.style.visibility = "hidden", this._div.style.zIndex = -1,
+ this._updateDivDimensions(this.props), this._containerNode = this._getContainerNode(this.props),
+ this._containerNode.appendChild(this._div));
+ }
+ }, {
+ key: "_unmountContainer",
+ value: function() {
+ this._div && (this._containerNode.removeChild(this._div), this._div = null), this._containerNode = null;
+ }
+ }, {
+ key: "_updateDivDimensions",
+ value: function(props) {
+ var height = props.height, width = props.width;
+ height && height !== this._divHeight && (this._divHeight = height, this._div.style.height = height + "px"),
+ width && width !== this._divWidth && (this._divWidth = width, this._div.style.width = width + "px");
+ }
+ } ]), CellMeasurer;
+ }(_react.Component);
+ exports.default = CellMeasurer;
+ }, /* 96 */
+ /***/
+ function(module, exports) {
+ module.exports = __WEBPACK_EXTERNAL_MODULE_96__;
+ }, /* 97 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), CellSizeCache = function() {
+ function CellSizeCache() {
+ var _ref = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, _ref$uniformRowHeight = _ref.uniformRowHeight, uniformRowHeight = void 0 !== _ref$uniformRowHeight && _ref$uniformRowHeight, _ref$uniformColumnWid = _ref.uniformColumnWidth, uniformColumnWidth = void 0 !== _ref$uniformColumnWid && _ref$uniformColumnWid;
+ (0, _classCallCheck3.default)(this, CellSizeCache), this._uniformRowHeight = uniformRowHeight,
+ this._uniformColumnWidth = uniformColumnWidth, this._cachedColumnWidths = {}, this._cachedRowHeights = {};
+ }
+ return (0, _createClass3.default)(CellSizeCache, [ {
+ key: "clearAllColumnWidths",
+ value: function() {
+ this._cachedColumnWidth = void 0, this._cachedColumnWidths = {};
+ }
+ }, {
+ key: "clearAllRowHeights",
+ value: function() {
+ this._cachedRowHeight = void 0, this._cachedRowHeights = {};
+ }
+ }, {
+ key: "clearColumnWidth",
+ value: function(index) {
+ this._cachedColumnWidth = void 0, delete this._cachedColumnWidths[index];
+ }
+ }, {
+ key: "clearRowHeight",
+ value: function(index) {
+ this._cachedRowHeight = void 0, delete this._cachedRowHeights[index];
+ }
+ }, {
+ key: "getColumnWidth",
+ value: function(index) {
+ return this._uniformColumnWidth ? this._cachedColumnWidth : this._cachedColumnWidths[index];
+ }
+ }, {
+ key: "getRowHeight",
+ value: function(index) {
+ return this._uniformRowHeight ? this._cachedRowHeight : this._cachedRowHeights[index];
+ }
+ }, {
+ key: "hasColumnWidth",
+ value: function(index) {
+ return this._uniformColumnWidth ? !!this._cachedColumnWidth : !!this._cachedColumnWidths[index];
+ }
+ }, {
+ key: "hasRowHeight",
+ value: function(index) {
+ return this._uniformRowHeight ? !!this._cachedRowHeight : !!this._cachedRowHeights[index];
+ }
+ }, {
+ key: "setColumnWidth",
+ value: function(index, width) {
+ this._cachedColumnWidth = width, this._cachedColumnWidths[index] = width;
+ }
+ }, {
+ key: "setRowHeight",
+ value: function(index, height) {
+ this._cachedRowHeight = height, this._cachedRowHeights[index] = height;
+ }
+ } ]), CellSizeCache;
+ }();
+ exports.default = CellSizeCache;
+ }, /* 98 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.Collection = exports.default = void 0;
+ var _Collection2 = __webpack_require__(99), _Collection3 = _interopRequireDefault(_Collection2);
+ exports.default = _Collection3.default, exports.Collection = _Collection3.default;
+ }, /* 99 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function defaultCellGroupRenderer(_ref5) {
+ var cellCache = _ref5.cellCache, cellRenderer = _ref5.cellRenderer, cellSizeAndPositionGetter = _ref5.cellSizeAndPositionGetter, indices = _ref5.indices, isScrolling = _ref5.isScrolling;
+ return indices.map(function(index) {
+ var cellMetadata = cellSizeAndPositionGetter({
+ index: index
+ }), cellRendererProps = {
+ index: index,
+ isScrolling: isScrolling,
+ key: index,
+ style: {
+ height: cellMetadata.height,
+ left: cellMetadata.x,
+ position: "absolute",
+ top: cellMetadata.y,
+ width: cellMetadata.width
+ }
+ };
+ return isScrolling ? (index in cellCache || (cellCache[index] = cellRenderer(cellRendererProps)),
+ cellCache[index]) : cellRenderer(cellRendererProps);
+ }).filter(function(renderedCell) {
+ return !!renderedCell;
+ });
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _extends2 = __webpack_require__(100), _extends3 = _interopRequireDefault(_extends2), _objectWithoutProperties2 = __webpack_require__(105), _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2), _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _CollectionView = __webpack_require__(106), _CollectionView2 = _interopRequireDefault(_CollectionView), _calculateSizeAndPositionData2 = __webpack_require__(114), _calculateSizeAndPositionData3 = _interopRequireDefault(_calculateSizeAndPositionData2), _getUpdatedOffsetForIndex = __webpack_require__(117), _getUpdatedOffsetForIndex2 = _interopRequireDefault(_getUpdatedOffsetForIndex), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), Collection = function(_Component) {
+ function Collection(props, context) {
+ (0, _classCallCheck3.default)(this, Collection);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (Collection.__proto__ || (0,
+ _getPrototypeOf2.default)(Collection)).call(this, props, context));
+ return _this._cellMetadata = [], _this._lastRenderedCellIndices = [], _this._cellCache = [],
+ _this._isScrollingChange = _this._isScrollingChange.bind(_this), _this;
+ }
+ return (0, _inherits3.default)(Collection, _Component), (0, _createClass3.default)(Collection, [ {
+ key: "recomputeCellSizesAndPositions",
+ value: function() {
+ this._cellCache = [], this._collectionView.recomputeCellSizesAndPositions();
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _this2 = this, props = (0, _objectWithoutProperties3.default)(this.props, []);
+ return _react2.default.createElement(_CollectionView2.default, (0, _extends3.default)({
+ cellLayoutManager: this,
+ isScrollingChange: this._isScrollingChange,
+ ref: function(_ref) {
+ _this2._collectionView = _ref;
+ }
+ }, props));
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "calculateSizeAndPositionData",
+ value: function() {
+ var _props = this.props, cellCount = _props.cellCount, cellSizeAndPositionGetter = _props.cellSizeAndPositionGetter, sectionSize = _props.sectionSize, data = (0,
+ _calculateSizeAndPositionData3.default)({
+ cellCount: cellCount,
+ cellSizeAndPositionGetter: cellSizeAndPositionGetter,
+ sectionSize: sectionSize
+ });
+ this._cellMetadata = data.cellMetadata, this._sectionManager = data.sectionManager,
+ this._height = data.height, this._width = data.width;
+ }
+ }, {
+ key: "getLastRenderedIndices",
+ value: function() {
+ return this._lastRenderedCellIndices;
+ }
+ }, {
+ key: "getScrollPositionForCell",
+ value: function(_ref2) {
+ var align = _ref2.align, cellIndex = _ref2.cellIndex, height = _ref2.height, scrollLeft = _ref2.scrollLeft, scrollTop = _ref2.scrollTop, width = _ref2.width, cellCount = this.props.cellCount;
+ if (cellIndex >= 0 && cellIndex < cellCount) {
+ var cellMetadata = this._cellMetadata[cellIndex];
+ scrollLeft = (0, _getUpdatedOffsetForIndex2.default)({
+ align: align,
+ cellOffset: cellMetadata.x,
+ cellSize: cellMetadata.width,
+ containerSize: width,
+ currentOffset: scrollLeft,
+ targetIndex: cellIndex
+ }), scrollTop = (0, _getUpdatedOffsetForIndex2.default)({
+ align: align,
+ cellOffset: cellMetadata.y,
+ cellSize: cellMetadata.height,
+ containerSize: height,
+ currentOffset: scrollTop,
+ targetIndex: cellIndex
+ });
+ }
+ return {
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop
+ };
+ }
+ }, {
+ key: "getTotalSize",
+ value: function() {
+ return {
+ height: this._height,
+ width: this._width
+ };
+ }
+ }, {
+ key: "cellRenderers",
+ value: function(_ref3) {
+ var _this3 = this, height = _ref3.height, isScrolling = _ref3.isScrolling, width = _ref3.width, x = _ref3.x, y = _ref3.y, _props2 = this.props, cellGroupRenderer = _props2.cellGroupRenderer, cellRenderer = _props2.cellRenderer;
+ return this._lastRenderedCellIndices = this._sectionManager.getCellIndices({
+ height: height,
+ width: width,
+ x: x,
+ y: y
+ }), cellGroupRenderer({
+ cellCache: this._cellCache,
+ cellRenderer: cellRenderer,
+ cellSizeAndPositionGetter: function(_ref4) {
+ var index = _ref4.index;
+ return _this3._sectionManager.getCellMetadata({
+ index: index
+ });
+ },
+ indices: this._lastRenderedCellIndices,
+ isScrolling: isScrolling
+ });
+ }
+ }, {
+ key: "_isScrollingChange",
+ value: function(isScrolling) {
+ isScrolling || (this._cellCache = []);
+ }
+ } ]), Collection;
+ }(_react.Component);
+ Collection.defaultProps = {
+ "aria-label": "grid",
+ cellGroupRenderer: defaultCellGroupRenderer
+ }, exports.default = Collection;
+ }, /* 100 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ exports.__esModule = !0;
+ var _assign = __webpack_require__(101), _assign2 = _interopRequireDefault(_assign);
+ exports.default = _assign2.default || function(target) {
+ for (var i = 1; i < arguments.length; i++) {
+ var source = arguments[i];
+ for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]);
+ }
+ return target;
+ };
+ }, /* 101 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(102),
+ __esModule: !0
+ };
+ }, /* 102 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(103), module.exports = __webpack_require__(16).Object.assign;
+ }, /* 103 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.3.1 Object.assign(target, source)
+ var $export = __webpack_require__(15);
+ $export($export.S + $export.F, "Object", {
+ assign: __webpack_require__(104)
+ });
+ }, /* 104 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ // 19.1.2.1 Object.assign(target, source, ...)
+ var getKeys = __webpack_require__(48), gOPS = __webpack_require__(72), pIE = __webpack_require__(73), toObject = __webpack_require__(6), IObject = __webpack_require__(51), $assign = Object.assign;
+ // should work with symbols and should have deterministic property order (V8 bug)
+ module.exports = !$assign || __webpack_require__(25)(function() {
+ var A = {}, B = {}, S = Symbol(), K = "abcdefghijklmnopqrst";
+ return A[S] = 7, K.split("").forEach(function(k) {
+ B[k] = k;
+ }), 7 != $assign({}, A)[S] || Object.keys($assign({}, B)).join("") != K;
+ }) ? function(target, source) {
+ for (// eslint-disable-line no-unused-vars
+ var T = toObject(target), aLen = arguments.length, index = 1, getSymbols = gOPS.f, isEnum = pIE.f; aLen > index; ) for (var key, S = IObject(arguments[index++]), keys = getSymbols ? getKeys(S).concat(getSymbols(S)) : getKeys(S), length = keys.length, j = 0; length > j; ) isEnum.call(S, key = keys[j++]) && (T[key] = S[key]);
+ return T;
+ } : $assign;
+ }, /* 105 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ exports.__esModule = !0, exports.default = function(obj, keys) {
+ var target = {};
+ for (var i in obj) keys.indexOf(i) >= 0 || Object.prototype.hasOwnProperty.call(obj, i) && (target[i] = obj[i]);
+ return target;
+ };
+ }, /* 106 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _extends2 = __webpack_require__(100), _extends3 = _interopRequireDefault(_extends2), _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _classnames = __webpack_require__(107), _classnames2 = _interopRequireDefault(_classnames), _createCallbackMemoizer = __webpack_require__(108), _createCallbackMemoizer2 = _interopRequireDefault(_createCallbackMemoizer), _scrollbarSize = __webpack_require__(112), _scrollbarSize2 = _interopRequireDefault(_scrollbarSize), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), IS_SCROLLING_TIMEOUT = 150, SCROLL_POSITION_CHANGE_REASONS = {
+ OBSERVED: "observed",
+ REQUESTED: "requested"
+ }, CollectionView = function(_Component) {
+ function CollectionView(props, context) {
+ (0, _classCallCheck3.default)(this, CollectionView);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (CollectionView.__proto__ || (0,
+ _getPrototypeOf2.default)(CollectionView)).call(this, props, context));
+ return _this.state = {
+ calculateSizeAndPositionDataOnNextUpdate: !1,
+ isScrolling: !1,
+ scrollLeft: 0,
+ scrollTop: 0
+ }, _this._onSectionRenderedMemoizer = (0, _createCallbackMemoizer2.default)(), _this._onScrollMemoizer = (0,
+ _createCallbackMemoizer2.default)(!1), _this._invokeOnSectionRenderedHelper = _this._invokeOnSectionRenderedHelper.bind(_this),
+ _this._onScroll = _this._onScroll.bind(_this), _this._updateScrollPositionForScrollToCell = _this._updateScrollPositionForScrollToCell.bind(_this),
+ _this;
+ }
+ return (0, _inherits3.default)(CollectionView, _Component), (0, _createClass3.default)(CollectionView, [ {
+ key: "recomputeCellSizesAndPositions",
+ value: function() {
+ this.setState({
+ calculateSizeAndPositionDataOnNextUpdate: !0
+ });
+ }
+ }, {
+ key: "componentDidMount",
+ value: function() {
+ var _props = this.props, cellLayoutManager = _props.cellLayoutManager, scrollLeft = _props.scrollLeft, scrollToCell = _props.scrollToCell, scrollTop = _props.scrollTop;
+ this._scrollbarSizeMeasured || (this._scrollbarSize = (0, _scrollbarSize2.default)(),
+ this._scrollbarSizeMeasured = !0, this.setState({})), scrollToCell >= 0 ? this._updateScrollPositionForScrollToCell() : (scrollLeft >= 0 || scrollTop >= 0) && this._setScrollPosition({
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop
+ }), this._invokeOnSectionRenderedHelper();
+ var _cellLayoutManager$ge = cellLayoutManager.getTotalSize(), totalHeight = _cellLayoutManager$ge.height, totalWidth = _cellLayoutManager$ge.width;
+ this._invokeOnScrollMemoizer({
+ scrollLeft: scrollLeft || 0,
+ scrollTop: scrollTop || 0,
+ totalHeight: totalHeight,
+ totalWidth: totalWidth
+ });
+ }
+ }, {
+ key: "componentDidUpdate",
+ value: function(prevProps, prevState) {
+ var _props2 = this.props, height = _props2.height, scrollToAlignment = _props2.scrollToAlignment, scrollToCell = _props2.scrollToCell, width = _props2.width, _state = this.state, scrollLeft = _state.scrollLeft, scrollPositionChangeReason = _state.scrollPositionChangeReason, scrollTop = _state.scrollTop;
+ scrollPositionChangeReason === SCROLL_POSITION_CHANGE_REASONS.REQUESTED && (scrollLeft >= 0 && scrollLeft !== prevState.scrollLeft && scrollLeft !== this._scrollingContainer.scrollLeft && (this._scrollingContainer.scrollLeft = scrollLeft),
+ scrollTop >= 0 && scrollTop !== prevState.scrollTop && scrollTop !== this._scrollingContainer.scrollTop && (this._scrollingContainer.scrollTop = scrollTop)),
+ height === prevProps.height && scrollToAlignment === prevProps.scrollToAlignment && scrollToCell === prevProps.scrollToCell && width === prevProps.width || this._updateScrollPositionForScrollToCell(),
+ this._invokeOnSectionRenderedHelper();
+ }
+ }, {
+ key: "componentWillMount",
+ value: function() {
+ var cellLayoutManager = this.props.cellLayoutManager;
+ cellLayoutManager.calculateSizeAndPositionData(), this._scrollbarSize = (0, _scrollbarSize2.default)(),
+ void 0 === this._scrollbarSize ? (this._scrollbarSizeMeasured = !1, this._scrollbarSize = 0) : this._scrollbarSizeMeasured = !0;
+ }
+ }, {
+ key: "componentWillUnmount",
+ value: function() {
+ this._disablePointerEventsTimeoutId && clearTimeout(this._disablePointerEventsTimeoutId);
+ }
+ }, {
+ key: "componentWillUpdate",
+ value: function(nextProps, nextState) {
+ 0 !== nextProps.cellCount || 0 === nextState.scrollLeft && 0 === nextState.scrollTop ? nextProps.scrollLeft === this.props.scrollLeft && nextProps.scrollTop === this.props.scrollTop || this._setScrollPosition({
+ scrollLeft: nextProps.scrollLeft,
+ scrollTop: nextProps.scrollTop
+ }) : this._setScrollPosition({
+ scrollLeft: 0,
+ scrollTop: 0
+ }), (nextProps.cellCount !== this.props.cellCount || nextProps.cellLayoutManager !== this.props.cellLayoutManager || nextState.calculateSizeAndPositionDataOnNextUpdate) && nextProps.cellLayoutManager.calculateSizeAndPositionData(),
+ nextState.calculateSizeAndPositionDataOnNextUpdate && this.setState({
+ calculateSizeAndPositionDataOnNextUpdate: !1
+ });
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _this2 = this, _props3 = this.props, autoHeight = _props3.autoHeight, cellCount = _props3.cellCount, cellLayoutManager = _props3.cellLayoutManager, className = _props3.className, height = _props3.height, horizontalOverscanSize = _props3.horizontalOverscanSize, id = _props3.id, noContentRenderer = _props3.noContentRenderer, style = _props3.style, verticalOverscanSize = _props3.verticalOverscanSize, width = _props3.width, _state2 = this.state, isScrolling = _state2.isScrolling, scrollLeft = _state2.scrollLeft, scrollTop = _state2.scrollTop, _cellLayoutManager$ge2 = cellLayoutManager.getTotalSize(), totalHeight = _cellLayoutManager$ge2.height, totalWidth = _cellLayoutManager$ge2.width, left = Math.max(0, scrollLeft - horizontalOverscanSize), top = Math.max(0, scrollTop - verticalOverscanSize), right = Math.min(totalWidth, scrollLeft + width + horizontalOverscanSize), bottom = Math.min(totalHeight, scrollTop + height + verticalOverscanSize), childrenToDisplay = height > 0 && width > 0 ? cellLayoutManager.cellRenderers({
+ height: bottom - top,
+ isScrolling: isScrolling,
+ width: right - left,
+ x: left,
+ y: top
+ }) : [], collectionStyle = {
+ boxSizing: "border-box",
+ height: autoHeight ? "auto" : height,
+ overflow: "auto",
+ position: "relative",
+ WebkitOverflowScrolling: "touch",
+ width: width,
+ willChange: "transform"
+ }, verticalScrollBarSize = totalHeight > height ? this._scrollbarSize : 0, horizontalScrollBarSize = totalWidth > width ? this._scrollbarSize : 0;
+ return totalWidth + verticalScrollBarSize <= width && (collectionStyle.overflowX = "hidden"),
+ totalHeight + horizontalScrollBarSize <= height && (collectionStyle.overflowY = "hidden"),
+ _react2.default.createElement("div", {
+ ref: function(_ref) {
+ _this2._scrollingContainer = _ref;
+ },
+ "aria-label": this.props["aria-label"],
+ className: (0, _classnames2.default)("ReactVirtualized__Collection", className),
+ id: id,
+ onScroll: this._onScroll,
+ role: "grid",
+ style: (0, _extends3.default)({}, collectionStyle, style),
+ tabIndex: 0
+ }, cellCount > 0 && _react2.default.createElement("div", {
+ className: "ReactVirtualized__Collection__innerScrollContainer",
+ style: {
+ height: totalHeight,
+ maxHeight: totalHeight,
+ maxWidth: totalWidth,
+ overflow: "hidden",
+ pointerEvents: isScrolling ? "none" : "",
+ width: totalWidth
+ }
+ }, childrenToDisplay), 0 === cellCount && noContentRenderer());
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_enablePointerEventsAfterDelay",
+ value: function() {
+ var _this3 = this;
+ this._disablePointerEventsTimeoutId && clearTimeout(this._disablePointerEventsTimeoutId),
+ this._disablePointerEventsTimeoutId = setTimeout(function() {
+ var isScrollingChange = _this3.props.isScrollingChange;
+ isScrollingChange(!1), _this3._disablePointerEventsTimeoutId = null, _this3.setState({
+ isScrolling: !1
+ });
+ }, IS_SCROLLING_TIMEOUT);
+ }
+ }, {
+ key: "_invokeOnSectionRenderedHelper",
+ value: function() {
+ var _props4 = this.props, cellLayoutManager = _props4.cellLayoutManager, onSectionRendered = _props4.onSectionRendered;
+ this._onSectionRenderedMemoizer({
+ callback: onSectionRendered,
+ indices: {
+ indices: cellLayoutManager.getLastRenderedIndices()
+ }
+ });
+ }
+ }, {
+ key: "_invokeOnScrollMemoizer",
+ value: function(_ref2) {
+ var _this4 = this, scrollLeft = _ref2.scrollLeft, scrollTop = _ref2.scrollTop, totalHeight = _ref2.totalHeight, totalWidth = _ref2.totalWidth;
+ this._onScrollMemoizer({
+ callback: function(_ref3) {
+ var scrollLeft = _ref3.scrollLeft, scrollTop = _ref3.scrollTop, _props5 = _this4.props, height = _props5.height, onScroll = _props5.onScroll, width = _props5.width;
+ onScroll({
+ clientHeight: height,
+ clientWidth: width,
+ scrollHeight: totalHeight,
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ scrollWidth: totalWidth
+ });
+ },
+ indices: {
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop
+ }
+ });
+ }
+ }, {
+ key: "_setScrollPosition",
+ value: function(_ref4) {
+ var scrollLeft = _ref4.scrollLeft, scrollTop = _ref4.scrollTop, newState = {
+ scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.REQUESTED
+ };
+ scrollLeft >= 0 && (newState.scrollLeft = scrollLeft), scrollTop >= 0 && (newState.scrollTop = scrollTop),
+ (scrollLeft >= 0 && scrollLeft !== this.state.scrollLeft || scrollTop >= 0 && scrollTop !== this.state.scrollTop) && this.setState(newState);
+ }
+ }, {
+ key: "_updateScrollPositionForScrollToCell",
+ value: function() {
+ var _props6 = this.props, cellLayoutManager = _props6.cellLayoutManager, height = _props6.height, scrollToAlignment = _props6.scrollToAlignment, scrollToCell = _props6.scrollToCell, width = _props6.width, _state3 = this.state, scrollLeft = _state3.scrollLeft, scrollTop = _state3.scrollTop;
+ if (scrollToCell >= 0) {
+ var scrollPosition = cellLayoutManager.getScrollPositionForCell({
+ align: scrollToAlignment,
+ cellIndex: scrollToCell,
+ height: height,
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ width: width
+ });
+ scrollPosition.scrollLeft === scrollLeft && scrollPosition.scrollTop === scrollTop || this._setScrollPosition(scrollPosition);
+ }
+ }
+ }, {
+ key: "_onScroll",
+ value: function(event) {
+ if (event.target === this._scrollingContainer) {
+ this._enablePointerEventsAfterDelay();
+ var _props7 = this.props, cellLayoutManager = _props7.cellLayoutManager, height = _props7.height, isScrollingChange = _props7.isScrollingChange, width = _props7.width, scrollbarSize = this._scrollbarSize, _cellLayoutManager$ge3 = cellLayoutManager.getTotalSize(), totalHeight = _cellLayoutManager$ge3.height, totalWidth = _cellLayoutManager$ge3.width, scrollLeft = Math.max(0, Math.min(totalWidth - width + scrollbarSize, event.target.scrollLeft)), scrollTop = Math.max(0, Math.min(totalHeight - height + scrollbarSize, event.target.scrollTop));
+ if (this.state.scrollLeft !== scrollLeft || this.state.scrollTop !== scrollTop) {
+ var scrollPositionChangeReason = event.cancelable ? SCROLL_POSITION_CHANGE_REASONS.OBSERVED : SCROLL_POSITION_CHANGE_REASONS.REQUESTED;
+ this.state.isScrolling || isScrollingChange(!0), this.setState({
+ isScrolling: !0,
+ scrollLeft: scrollLeft,
+ scrollPositionChangeReason: scrollPositionChangeReason,
+ scrollTop: scrollTop
+ });
+ }
+ this._invokeOnScrollMemoizer({
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ totalWidth: totalWidth,
+ totalHeight: totalHeight
+ });
+ }
+ }
+ } ]), CollectionView;
+ }(_react.Component);
+ CollectionView.defaultProps = {
+ "aria-label": "grid",
+ horizontalOverscanSize: 0,
+ noContentRenderer: function() {
+ return null;
+ },
+ onScroll: function() {
+ return null;
+ },
+ onSectionRendered: function() {
+ return null;
+ },
+ scrollToAlignment: "auto",
+ style: {},
+ verticalOverscanSize: 0
+ }, exports.default = CollectionView;
+ }, /* 107 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;
+ /*!
+ Copyright (c) 2016 Jed Watson.
+ Licensed under the MIT License (MIT), see
+ http://jedwatson.github.io/classnames
+ */
+ /* global define */
+ !function() {
+ "use strict";
+ function classNames() {
+ for (var classes = [], i = 0; i < arguments.length; i++) {
+ var arg = arguments[i];
+ if (arg) {
+ var argType = typeof arg;
+ if ("string" === argType || "number" === argType) classes.push(arg); else if (Array.isArray(arg)) classes.push(classNames.apply(null, arg)); else if ("object" === argType) for (var key in arg) hasOwn.call(arg, key) && arg[key] && classes.push(key);
+ }
+ }
+ return classes.join(" ");
+ }
+ var hasOwn = {}.hasOwnProperty;
+ "undefined" != typeof module && module.exports ? module.exports = classNames : (__WEBPACK_AMD_DEFINE_ARRAY__ = [],
+ __WEBPACK_AMD_DEFINE_RESULT__ = function() {
+ return classNames;
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), // register as 'classnames', consistent with npm package name
+ !(void 0 !== __WEBPACK_AMD_DEFINE_RESULT__ && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)));
+ }();
+ }, /* 108 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function createCallbackMemoizer() {
+ var requireAllKeys = !(arguments.length > 0 && void 0 !== arguments[0]) || arguments[0], cachedIndices = {};
+ return function(_ref) {
+ var callback = _ref.callback, indices = _ref.indices, keys = (0, _keys2.default)(indices), allInitialized = !requireAllKeys || keys.every(function(key) {
+ var value = indices[key];
+ return Array.isArray(value) ? value.length > 0 : value >= 0;
+ }), indexChanged = keys.length !== (0, _keys2.default)(cachedIndices).length || keys.some(function(key) {
+ var cachedValue = cachedIndices[key], value = indices[key];
+ return Array.isArray(value) ? cachedValue.join(",") !== value.join(",") : cachedValue !== value;
+ });
+ cachedIndices = indices, allInitialized && indexChanged && callback(indices);
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _keys = __webpack_require__(109), _keys2 = _interopRequireDefault(_keys);
+ exports.default = createCallbackMemoizer;
+ }, /* 109 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ module.exports = {
+ default: __webpack_require__(110),
+ __esModule: !0
+ };
+ }, /* 110 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ __webpack_require__(111), module.exports = __webpack_require__(16).Object.keys;
+ }, /* 111 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ // 19.1.2.14 Object.keys(O)
+ var toObject = __webpack_require__(6), $keys = __webpack_require__(48);
+ __webpack_require__(14)("keys", function() {
+ return function(it) {
+ return $keys(toObject(it));
+ };
+ });
+ }, /* 112 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ var size, canUseDOM = __webpack_require__(113);
+ module.exports = function(recalc) {
+ if ((!size || recalc) && canUseDOM) {
+ var scrollDiv = document.createElementNS("http://www.w3.org/1999/xhtml","div");
+ scrollDiv.style.position = "absolute", scrollDiv.style.top = "-9999px", scrollDiv.style.width = "50px",
+ scrollDiv.style.height = "50px", scrollDiv.style.overflow = "scroll", document.firstElementChild.appendChild(scrollDiv),
+ size = scrollDiv.offsetWidth - scrollDiv.clientWidth, document.firstElementChild.removeChild(scrollDiv);
+ }
+ return size;
+ };
+ }, /* 113 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ module.exports = !("undefined" == typeof window || !window.document || !window.document.createElement);
+ }, /* 114 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function calculateSizeAndPositionData(_ref) {
+ for (var cellCount = _ref.cellCount, cellSizeAndPositionGetter = _ref.cellSizeAndPositionGetter, sectionSize = _ref.sectionSize, cellMetadata = [], sectionManager = new _SectionManager2.default(sectionSize), height = 0, width = 0, index = 0; index < cellCount; index++) {
+ var cellMetadatum = cellSizeAndPositionGetter({
+ index: index
+ });
+ if (null == cellMetadatum.height || isNaN(cellMetadatum.height) || null == cellMetadatum.width || isNaN(cellMetadatum.width) || null == cellMetadatum.x || isNaN(cellMetadatum.x) || null == cellMetadatum.y || isNaN(cellMetadatum.y)) throw Error("Invalid metadata returned for cell " + index + ":\n x:" + cellMetadatum.x + ", y:" + cellMetadatum.y + ", width:" + cellMetadatum.width + ", height:" + cellMetadatum.height);
+ height = Math.max(height, cellMetadatum.y + cellMetadatum.height), width = Math.max(width, cellMetadatum.x + cellMetadatum.width),
+ cellMetadata[index] = cellMetadatum, sectionManager.registerCell({
+ cellMetadatum: cellMetadatum,
+ index: index
+ });
+ }
+ return {
+ cellMetadata: cellMetadata,
+ height: height,
+ sectionManager: sectionManager,
+ width: width
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = calculateSizeAndPositionData;
+ var _SectionManager = __webpack_require__(115), _SectionManager2 = _interopRequireDefault(_SectionManager);
+ }, /* 115 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _keys = __webpack_require__(109), _keys2 = _interopRequireDefault(_keys), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _Section = __webpack_require__(116), _Section2 = _interopRequireDefault(_Section), SECTION_SIZE = 100, SectionManager = function() {
+ function SectionManager() {
+ var sectionSize = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : SECTION_SIZE;
+ (0, _classCallCheck3.default)(this, SectionManager), this._sectionSize = sectionSize,
+ this._cellMetadata = [], this._sections = {};
+ }
+ return (0, _createClass3.default)(SectionManager, [ {
+ key: "getCellIndices",
+ value: function(_ref) {
+ var height = _ref.height, width = _ref.width, x = _ref.x, y = _ref.y, indices = {};
+ return this.getSections({
+ height: height,
+ width: width,
+ x: x,
+ y: y
+ }).forEach(function(section) {
+ return section.getCellIndices().forEach(function(index) {
+ indices[index] = index;
+ });
+ }), (0, _keys2.default)(indices).map(function(index) {
+ return indices[index];
+ });
+ }
+ }, {
+ key: "getCellMetadata",
+ value: function(_ref2) {
+ var index = _ref2.index;
+ return this._cellMetadata[index];
+ }
+ }, {
+ key: "getSections",
+ value: function(_ref3) {
+ for (var height = _ref3.height, width = _ref3.width, x = _ref3.x, y = _ref3.y, sectionXStart = Math.floor(x / this._sectionSize), sectionXStop = Math.floor((x + width - 1) / this._sectionSize), sectionYStart = Math.floor(y / this._sectionSize), sectionYStop = Math.floor((y + height - 1) / this._sectionSize), sections = [], sectionX = sectionXStart; sectionX <= sectionXStop; sectionX++) for (var sectionY = sectionYStart; sectionY <= sectionYStop; sectionY++) {
+ var key = sectionX + "." + sectionY;
+ this._sections[key] || (this._sections[key] = new _Section2.default({
+ height: this._sectionSize,
+ width: this._sectionSize,
+ x: sectionX * this._sectionSize,
+ y: sectionY * this._sectionSize
+ })), sections.push(this._sections[key]);
+ }
+ return sections;
+ }
+ }, {
+ key: "getTotalSectionCount",
+ value: function() {
+ return (0, _keys2.default)(this._sections).length;
+ }
+ }, {
+ key: "toString",
+ value: function() {
+ var _this = this;
+ return (0, _keys2.default)(this._sections).map(function(index) {
+ return _this._sections[index].toString();
+ });
+ }
+ }, {
+ key: "registerCell",
+ value: function(_ref4) {
+ var cellMetadatum = _ref4.cellMetadatum, index = _ref4.index;
+ this._cellMetadata[index] = cellMetadatum, this.getSections(cellMetadatum).forEach(function(section) {
+ return section.addCellIndex({
+ index: index
+ });
+ });
+ }
+ } ]), SectionManager;
+ }();
+ exports.default = SectionManager;
+ }, /* 116 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), Section = function() {
+ function Section(_ref) {
+ var height = _ref.height, width = _ref.width, x = _ref.x, y = _ref.y;
+ (0, _classCallCheck3.default)(this, Section), this.height = height, this.width = width,
+ this.x = x, this.y = y, this._indexMap = {}, this._indices = [];
+ }
+ return (0, _createClass3.default)(Section, [ {
+ key: "addCellIndex",
+ value: function(_ref2) {
+ var index = _ref2.index;
+ this._indexMap[index] || (this._indexMap[index] = !0, this._indices.push(index));
+ }
+ }, {
+ key: "getCellIndices",
+ value: function() {
+ return this._indices;
+ }
+ }, {
+ key: "toString",
+ value: function() {
+ return this.x + "," + this.y + " " + this.width + "x" + this.height;
+ }
+ } ]), Section;
+ }();
+ exports.default = Section;
+ }, /* 117 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function getUpdatedOffsetForIndex(_ref) {
+ var _ref$align = _ref.align, align = void 0 === _ref$align ? "auto" : _ref$align, cellOffset = _ref.cellOffset, cellSize = _ref.cellSize, containerSize = _ref.containerSize, currentOffset = _ref.currentOffset, maxOffset = cellOffset, minOffset = maxOffset - containerSize + cellSize;
+ switch (align) {
+ case "start":
+ return maxOffset;
+
+ case "end":
+ return minOffset;
+
+ case "center":
+ return maxOffset - (containerSize - cellSize) / 2;
+
+ default:
+ return Math.max(minOffset, Math.min(maxOffset, currentOffset));
+ }
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = getUpdatedOffsetForIndex;
+ }, /* 118 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.ColumnSizer = exports.default = void 0;
+ var _ColumnSizer2 = __webpack_require__(119), _ColumnSizer3 = _interopRequireDefault(_ColumnSizer2);
+ exports.default = _ColumnSizer3.default, exports.ColumnSizer = _ColumnSizer3.default;
+ }, /* 119 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _Grid = __webpack_require__(120), _Grid2 = _interopRequireDefault(_Grid), ColumnSizer = function(_Component) {
+ function ColumnSizer(props, context) {
+ (0, _classCallCheck3.default)(this, ColumnSizer);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (ColumnSizer.__proto__ || (0,
+ _getPrototypeOf2.default)(ColumnSizer)).call(this, props, context));
+ return _this._registerChild = _this._registerChild.bind(_this), _this;
+ }
+ return (0, _inherits3.default)(ColumnSizer, _Component), (0, _createClass3.default)(ColumnSizer, [ {
+ key: "componentDidUpdate",
+ value: function(prevProps, prevState) {
+ var _props = this.props, columnMaxWidth = _props.columnMaxWidth, columnMinWidth = _props.columnMinWidth, columnCount = _props.columnCount, width = _props.width;
+ columnMaxWidth === prevProps.columnMaxWidth && columnMinWidth === prevProps.columnMinWidth && columnCount === prevProps.columnCount && width === prevProps.width || this._registeredChild && this._registeredChild.recomputeGridSize();
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _props2 = this.props, children = _props2.children, columnMaxWidth = _props2.columnMaxWidth, columnMinWidth = _props2.columnMinWidth, columnCount = _props2.columnCount, width = _props2.width, safeColumnMinWidth = columnMinWidth || 1, safeColumnMaxWidth = columnMaxWidth ? Math.min(columnMaxWidth, width) : width, columnWidth = width / columnCount;
+ columnWidth = Math.max(safeColumnMinWidth, columnWidth), columnWidth = Math.min(safeColumnMaxWidth, columnWidth),
+ columnWidth = Math.floor(columnWidth);
+ var adjustedWidth = Math.min(width, columnWidth * columnCount);
+ return children({
+ adjustedWidth: adjustedWidth,
+ getColumnWidth: function() {
+ return columnWidth;
+ },
+ registerChild: this._registerChild
+ });
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_registerChild",
+ value: function(child) {
+ if (null !== child && !(child instanceof _Grid2.default)) throw Error("Unexpected child type registered; only Grid children are supported.");
+ this._registeredChild = child, this._registeredChild && this._registeredChild.recomputeGridSize();
+ }
+ } ]), ColumnSizer;
+ }(_react.Component);
+ exports.default = ColumnSizer;
+ }, /* 120 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.defaultCellRangeRenderer = exports.Grid = exports.default = void 0;
+ var _Grid2 = __webpack_require__(121), _Grid3 = _interopRequireDefault(_Grid2), _defaultCellRangeRenderer2 = __webpack_require__(127), _defaultCellRangeRenderer3 = _interopRequireDefault(_defaultCellRangeRenderer2);
+ exports.default = _Grid3.default, exports.Grid = _Grid3.default, exports.defaultCellRangeRenderer = _defaultCellRangeRenderer3.default;
+ }, /* 121 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.DEFAULT_SCROLLING_RESET_TIME_INTERVAL = void 0;
+ var _extends2 = __webpack_require__(100), _extends3 = _interopRequireDefault(_extends2), _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _classnames = __webpack_require__(107), _classnames2 = _interopRequireDefault(_classnames), _calculateSizeAndPositionDataAndUpdateScrollOffset = __webpack_require__(122), _calculateSizeAndPositionDataAndUpdateScrollOffset2 = _interopRequireDefault(_calculateSizeAndPositionDataAndUpdateScrollOffset), _ScalingCellSizeAndPositionManager = __webpack_require__(123), _ScalingCellSizeAndPositionManager2 = _interopRequireDefault(_ScalingCellSizeAndPositionManager), _createCallbackMemoizer = __webpack_require__(108), _createCallbackMemoizer2 = _interopRequireDefault(_createCallbackMemoizer), _getOverscanIndices = __webpack_require__(125), _getOverscanIndices2 = _interopRequireDefault(_getOverscanIndices), _scrollbarSize = __webpack_require__(112), _scrollbarSize2 = _interopRequireDefault(_scrollbarSize), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _updateScrollIndexHelper = __webpack_require__(126), _updateScrollIndexHelper2 = _interopRequireDefault(_updateScrollIndexHelper), _defaultCellRangeRenderer = __webpack_require__(127), _defaultCellRangeRenderer2 = _interopRequireDefault(_defaultCellRangeRenderer), DEFAULT_SCROLLING_RESET_TIME_INTERVAL = exports.DEFAULT_SCROLLING_RESET_TIME_INTERVAL = 150, SCROLL_POSITION_CHANGE_REASONS = {
+ OBSERVED: "observed",
+ REQUESTED: "requested"
+ }, Grid = function(_Component) {
+ function Grid(props, context) {
+ (0, _classCallCheck3.default)(this, Grid);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (Grid.__proto__ || (0,
+ _getPrototypeOf2.default)(Grid)).call(this, props, context));
+ return _this.state = {
+ isScrolling: !1,
+ scrollDirectionHorizontal: _getOverscanIndices.SCROLL_DIRECTION_FIXED,
+ scrollDirectionVertical: _getOverscanIndices.SCROLL_DIRECTION_FIXED,
+ scrollLeft: 0,
+ scrollTop: 0
+ }, _this._onGridRenderedMemoizer = (0, _createCallbackMemoizer2.default)(), _this._onScrollMemoizer = (0,
+ _createCallbackMemoizer2.default)(!1), _this._enablePointerEventsAfterDelayCallback = _this._enablePointerEventsAfterDelayCallback.bind(_this),
+ _this._invokeOnGridRenderedHelper = _this._invokeOnGridRenderedHelper.bind(_this),
+ _this._onScroll = _this._onScroll.bind(_this), _this._updateScrollLeftForScrollToColumn = _this._updateScrollLeftForScrollToColumn.bind(_this),
+ _this._updateScrollTopForScrollToRow = _this._updateScrollTopForScrollToRow.bind(_this),
+ _this._columnWidthGetter = _this._wrapSizeGetter(props.columnWidth), _this._rowHeightGetter = _this._wrapSizeGetter(props.rowHeight),
+ _this._columnSizeAndPositionManager = new _ScalingCellSizeAndPositionManager2.default({
+ cellCount: props.columnCount,
+ cellSizeGetter: function(index) {
+ return _this._columnWidthGetter(index);
+ },
+ estimatedCellSize: _this._getEstimatedColumnSize(props)
+ }), _this._rowSizeAndPositionManager = new _ScalingCellSizeAndPositionManager2.default({
+ cellCount: props.rowCount,
+ cellSizeGetter: function(index) {
+ return _this._rowHeightGetter(index);
+ },
+ estimatedCellSize: _this._getEstimatedRowSize(props)
+ }), _this._cellCache = {}, _this;
+ }
+ return (0, _inherits3.default)(Grid, _Component), (0, _createClass3.default)(Grid, [ {
+ key: "measureAllCells",
+ value: function() {
+ var _props = this.props, columnCount = _props.columnCount, rowCount = _props.rowCount;
+ this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnCount - 1), this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowCount - 1);
+ }
+ }, {
+ key: "recomputeGridSize",
+ value: function() {
+ var _ref = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, _ref$columnIndex = _ref.columnIndex, columnIndex = void 0 === _ref$columnIndex ? 0 : _ref$columnIndex, _ref$rowIndex = _ref.rowIndex, rowIndex = void 0 === _ref$rowIndex ? 0 : _ref$rowIndex;
+ this._columnSizeAndPositionManager.resetCell(columnIndex), this._rowSizeAndPositionManager.resetCell(rowIndex),
+ this._cellCache = {}, this.forceUpdate();
+ }
+ }, {
+ key: "componentDidMount",
+ value: function() {
+ var _props2 = this.props, scrollLeft = _props2.scrollLeft, scrollToColumn = _props2.scrollToColumn, scrollTop = _props2.scrollTop, scrollToRow = _props2.scrollToRow;
+ this._scrollbarSizeMeasured || (this._scrollbarSize = (0, _scrollbarSize2.default)(),
+ this._scrollbarSizeMeasured = !0, this.setState({})), (scrollLeft >= 0 || scrollTop >= 0) && this._setScrollPosition({
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop
+ }), (scrollToColumn >= 0 || scrollToRow >= 0) && (this._updateScrollLeftForScrollToColumn(),
+ this._updateScrollTopForScrollToRow()), this._invokeOnGridRenderedHelper(), this._invokeOnScrollMemoizer({
+ scrollLeft: scrollLeft || 0,
+ scrollTop: scrollTop || 0,
+ totalColumnsWidth: this._columnSizeAndPositionManager.getTotalSize(),
+ totalRowsHeight: this._rowSizeAndPositionManager.getTotalSize()
+ });
+ }
+ }, {
+ key: "componentDidUpdate",
+ value: function(prevProps, prevState) {
+ var _this2 = this, _props3 = this.props, autoHeight = _props3.autoHeight, columnCount = _props3.columnCount, height = _props3.height, rowCount = _props3.rowCount, scrollToAlignment = _props3.scrollToAlignment, scrollToColumn = _props3.scrollToColumn, scrollToRow = _props3.scrollToRow, width = _props3.width, _state = this.state, scrollLeft = _state.scrollLeft, scrollPositionChangeReason = _state.scrollPositionChangeReason, scrollTop = _state.scrollTop, columnOrRowCountJustIncreasedFromZero = columnCount > 0 && 0 === prevProps.columnCount || rowCount > 0 && 0 === prevProps.rowCount;
+ if (scrollPositionChangeReason === SCROLL_POSITION_CHANGE_REASONS.REQUESTED && (scrollLeft >= 0 && (scrollLeft !== prevState.scrollLeft && scrollLeft !== this._scrollingContainer.scrollLeft || columnOrRowCountJustIncreasedFromZero) && (this._scrollingContainer.scrollLeft = scrollLeft),
+ !autoHeight && scrollTop >= 0 && (scrollTop !== prevState.scrollTop && scrollTop !== this._scrollingContainer.scrollTop || columnOrRowCountJustIncreasedFromZero) && (this._scrollingContainer.scrollTop = scrollTop)),
+ (0, _updateScrollIndexHelper2.default)({
+ cellSizeAndPositionManager: this._columnSizeAndPositionManager,
+ previousCellsCount: prevProps.columnCount,
+ previousCellSize: prevProps.columnWidth,
+ previousScrollToAlignment: prevProps.scrollToAlignment,
+ previousScrollToIndex: prevProps.scrollToColumn,
+ previousSize: prevProps.width,
+ scrollOffset: scrollLeft,
+ scrollToAlignment: scrollToAlignment,
+ scrollToIndex: scrollToColumn,
+ size: width,
+ updateScrollIndexCallback: function(scrollToColumn) {
+ return _this2._updateScrollLeftForScrollToColumn((0, _extends3.default)({}, _this2.props, {
+ scrollToColumn: scrollToColumn
+ }));
+ }
+ }), (0, _updateScrollIndexHelper2.default)({
+ cellSizeAndPositionManager: this._rowSizeAndPositionManager,
+ previousCellsCount: prevProps.rowCount,
+ previousCellSize: prevProps.rowHeight,
+ previousScrollToAlignment: prevProps.scrollToAlignment,
+ previousScrollToIndex: prevProps.scrollToRow,
+ previousSize: prevProps.height,
+ scrollOffset: scrollTop,
+ scrollToAlignment: scrollToAlignment,
+ scrollToIndex: scrollToRow,
+ size: height,
+ updateScrollIndexCallback: function(scrollToRow) {
+ return _this2._updateScrollTopForScrollToRow((0, _extends3.default)({}, _this2.props, {
+ scrollToRow: scrollToRow
+ }));
+ }
+ }), this._invokeOnGridRenderedHelper(), scrollLeft !== prevState.scrollLeft || scrollTop !== prevState.scrollTop) {
+ var totalRowsHeight = this._rowSizeAndPositionManager.getTotalSize(), totalColumnsWidth = this._columnSizeAndPositionManager.getTotalSize();
+ this._invokeOnScrollMemoizer({
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ totalColumnsWidth: totalColumnsWidth,
+ totalRowsHeight: totalRowsHeight
+ });
+ }
+ }
+ }, {
+ key: "componentWillMount",
+ value: function() {
+ this._scrollbarSize = (0, _scrollbarSize2.default)(), void 0 === this._scrollbarSize ? (this._scrollbarSizeMeasured = !1,
+ this._scrollbarSize = 0) : this._scrollbarSizeMeasured = !0, this._calculateChildrenToRender();
+ }
+ }, {
+ key: "componentWillUnmount",
+ value: function() {
+ this._disablePointerEventsTimeoutId && clearTimeout(this._disablePointerEventsTimeoutId);
+ }
+ }, {
+ key: "componentWillUpdate",
+ value: function(nextProps, nextState) {
+ var _this3 = this;
+ 0 === nextProps.columnCount && 0 !== nextState.scrollLeft || 0 === nextProps.rowCount && 0 !== nextState.scrollTop ? this._setScrollPosition({
+ scrollLeft: 0,
+ scrollTop: 0
+ }) : nextProps.scrollLeft === this.props.scrollLeft && nextProps.scrollTop === this.props.scrollTop || this._setScrollPosition({
+ scrollLeft: nextProps.scrollLeft,
+ scrollTop: nextProps.scrollTop
+ }), this._columnWidthGetter = this._wrapSizeGetter(nextProps.columnWidth), this._rowHeightGetter = this._wrapSizeGetter(nextProps.rowHeight),
+ this._columnSizeAndPositionManager.configure({
+ cellCount: nextProps.columnCount,
+ estimatedCellSize: this._getEstimatedColumnSize(nextProps)
+ }), this._rowSizeAndPositionManager.configure({
+ cellCount: nextProps.rowCount,
+ estimatedCellSize: this._getEstimatedRowSize(nextProps)
+ }), (0, _calculateSizeAndPositionDataAndUpdateScrollOffset2.default)({
+ cellCount: this.props.columnCount,
+ cellSize: this.props.columnWidth,
+ computeMetadataCallback: function() {
+ return _this3._columnSizeAndPositionManager.resetCell(0);
+ },
+ computeMetadataCallbackProps: nextProps,
+ nextCellsCount: nextProps.columnCount,
+ nextCellSize: nextProps.columnWidth,
+ nextScrollToIndex: nextProps.scrollToColumn,
+ scrollToIndex: this.props.scrollToColumn,
+ updateScrollOffsetForScrollToIndex: function() {
+ return _this3._updateScrollLeftForScrollToColumn(nextProps, nextState);
+ }
+ }), (0, _calculateSizeAndPositionDataAndUpdateScrollOffset2.default)({
+ cellCount: this.props.rowCount,
+ cellSize: this.props.rowHeight,
+ computeMetadataCallback: function() {
+ return _this3._rowSizeAndPositionManager.resetCell(0);
+ },
+ computeMetadataCallbackProps: nextProps,
+ nextCellsCount: nextProps.rowCount,
+ nextCellSize: nextProps.rowHeight,
+ nextScrollToIndex: nextProps.scrollToRow,
+ scrollToIndex: this.props.scrollToRow,
+ updateScrollOffsetForScrollToIndex: function() {
+ return _this3._updateScrollTopForScrollToRow(nextProps, nextState);
+ }
+ }), this._calculateChildrenToRender(nextProps, nextState);
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _this4 = this, _props4 = this.props, autoContainerWidth = _props4.autoContainerWidth, autoHeight = _props4.autoHeight, className = _props4.className, containerStyle = _props4.containerStyle, height = _props4.height, id = _props4.id, noContentRenderer = _props4.noContentRenderer, style = _props4.style, tabIndex = _props4.tabIndex, width = _props4.width, isScrolling = this.state.isScrolling, gridStyle = {
+ boxSizing: "border-box",
+ height: autoHeight ? "auto" : height,
+ position: "relative",
+ width: width,
+ WebkitOverflowScrolling: "touch",
+ willChange: "transform"
+ }, totalColumnsWidth = this._columnSizeAndPositionManager.getTotalSize(), totalRowsHeight = this._rowSizeAndPositionManager.getTotalSize(), verticalScrollBarSize = totalRowsHeight > height ? this._scrollbarSize : 0, horizontalScrollBarSize = totalColumnsWidth > width ? this._scrollbarSize : 0;
+ gridStyle.overflowX = totalColumnsWidth + verticalScrollBarSize <= width ? "hidden" : "auto",
+ gridStyle.overflowY = totalRowsHeight + horizontalScrollBarSize <= height ? "hidden" : "auto";
+ var childrenToDisplay = this._childrenToDisplay, showNoContentRenderer = 0 === childrenToDisplay.length && height > 0 && width > 0;
+ return _react2.default.createElement("div", {
+ ref: function(_ref2) {
+ _this4._scrollingContainer = _ref2;
+ },
+ "aria-label": this.props["aria-label"],
+ className: (0, _classnames2.default)("ReactVirtualized__Grid", className),
+ id: id,
+ onScroll: this._onScroll,
+ role: "grid",
+ style: (0, _extends3.default)({}, gridStyle, style),
+ tabIndex: tabIndex
+ }, childrenToDisplay.length > 0 && _react2.default.createElement("div", {
+ className: "ReactVirtualized__Grid__innerScrollContainer",
+ style: (0, _extends3.default)({
+ width: autoContainerWidth ? "auto" : totalColumnsWidth,
+ height: totalRowsHeight,
+ maxWidth: totalColumnsWidth,
+ maxHeight: totalRowsHeight,
+ overflow: "hidden",
+ pointerEvents: isScrolling ? "none" : ""
+ }, containerStyle)
+ }, childrenToDisplay), showNoContentRenderer && noContentRenderer());
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_calculateChildrenToRender",
+ value: function() {
+ var props = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : this.props, state = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : this.state, cellRenderer = props.cellRenderer, cellRangeRenderer = props.cellRangeRenderer, columnCount = props.columnCount, height = props.height, overscanColumnCount = props.overscanColumnCount, overscanRowCount = props.overscanRowCount, rowCount = props.rowCount, width = props.width, isScrolling = state.isScrolling, scrollDirectionHorizontal = state.scrollDirectionHorizontal, scrollDirectionVertical = state.scrollDirectionVertical, scrollLeft = state.scrollLeft, scrollTop = state.scrollTop;
+ if (this._childrenToDisplay = [], height > 0 && width > 0) {
+ var visibleColumnIndices = this._columnSizeAndPositionManager.getVisibleCellRange({
+ containerSize: width,
+ offset: scrollLeft
+ }), visibleRowIndices = this._rowSizeAndPositionManager.getVisibleCellRange({
+ containerSize: height,
+ offset: scrollTop
+ }), horizontalOffsetAdjustment = this._columnSizeAndPositionManager.getOffsetAdjustment({
+ containerSize: width,
+ offset: scrollLeft
+ }), verticalOffsetAdjustment = this._rowSizeAndPositionManager.getOffsetAdjustment({
+ containerSize: height,
+ offset: scrollTop
+ });
+ this._renderedColumnStartIndex = visibleColumnIndices.start, this._renderedColumnStopIndex = visibleColumnIndices.stop,
+ this._renderedRowStartIndex = visibleRowIndices.start, this._renderedRowStopIndex = visibleRowIndices.stop;
+ var overscanColumnIndices = (0, _getOverscanIndices2.default)({
+ cellCount: columnCount,
+ overscanCellsCount: overscanColumnCount,
+ scrollDirection: scrollDirectionHorizontal,
+ startIndex: this._renderedColumnStartIndex,
+ stopIndex: this._renderedColumnStopIndex
+ }), overscanRowIndices = (0, _getOverscanIndices2.default)({
+ cellCount: rowCount,
+ overscanCellsCount: overscanRowCount,
+ scrollDirection: scrollDirectionVertical,
+ startIndex: this._renderedRowStartIndex,
+ stopIndex: this._renderedRowStopIndex
+ });
+ this._columnStartIndex = overscanColumnIndices.overscanStartIndex, this._columnStopIndex = overscanColumnIndices.overscanStopIndex,
+ this._rowStartIndex = overscanRowIndices.overscanStartIndex, this._rowStopIndex = overscanRowIndices.overscanStopIndex,
+ this._childrenToDisplay = cellRangeRenderer({
+ cellCache: this._cellCache,
+ cellRenderer: cellRenderer,
+ columnSizeAndPositionManager: this._columnSizeAndPositionManager,
+ columnStartIndex: this._columnStartIndex,
+ columnStopIndex: this._columnStopIndex,
+ horizontalOffsetAdjustment: horizontalOffsetAdjustment,
+ isScrolling: isScrolling,
+ rowSizeAndPositionManager: this._rowSizeAndPositionManager,
+ rowStartIndex: this._rowStartIndex,
+ rowStopIndex: this._rowStopIndex,
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ verticalOffsetAdjustment: verticalOffsetAdjustment,
+ visibleColumnIndices: visibleColumnIndices,
+ visibleRowIndices: visibleRowIndices
+ });
+ }
+ }
+ }, {
+ key: "_enablePointerEventsAfterDelay",
+ value: function() {
+ var scrollingResetTimeInterval = this.props.scrollingResetTimeInterval;
+ this._disablePointerEventsTimeoutId && clearTimeout(this._disablePointerEventsTimeoutId),
+ this._disablePointerEventsTimeoutId = setTimeout(this._enablePointerEventsAfterDelayCallback, scrollingResetTimeInterval);
+ }
+ }, {
+ key: "_enablePointerEventsAfterDelayCallback",
+ value: function() {
+ this._disablePointerEventsTimeoutId = null, this._cellCache = {}, this.setState({
+ isScrolling: !1,
+ scrollDirectionHorizontal: _getOverscanIndices.SCROLL_DIRECTION_FIXED,
+ scrollDirectionVertical: _getOverscanIndices.SCROLL_DIRECTION_FIXED
+ });
+ }
+ }, {
+ key: "_getEstimatedColumnSize",
+ value: function(props) {
+ return "number" == typeof props.columnWidth ? props.columnWidth : props.estimatedColumnSize;
+ }
+ }, {
+ key: "_getEstimatedRowSize",
+ value: function(props) {
+ return "number" == typeof props.rowHeight ? props.rowHeight : props.estimatedRowSize;
+ }
+ }, {
+ key: "_invokeOnGridRenderedHelper",
+ value: function() {
+ var onSectionRendered = this.props.onSectionRendered;
+ this._onGridRenderedMemoizer({
+ callback: onSectionRendered,
+ indices: {
+ columnOverscanStartIndex: this._columnStartIndex,
+ columnOverscanStopIndex: this._columnStopIndex,
+ columnStartIndex: this._renderedColumnStartIndex,
+ columnStopIndex: this._renderedColumnStopIndex,
+ rowOverscanStartIndex: this._rowStartIndex,
+ rowOverscanStopIndex: this._rowStopIndex,
+ rowStartIndex: this._renderedRowStartIndex,
+ rowStopIndex: this._renderedRowStopIndex
+ }
+ });
+ }
+ }, {
+ key: "_invokeOnScrollMemoizer",
+ value: function(_ref3) {
+ var _this5 = this, scrollLeft = _ref3.scrollLeft, scrollTop = _ref3.scrollTop, totalColumnsWidth = _ref3.totalColumnsWidth, totalRowsHeight = _ref3.totalRowsHeight;
+ this._onScrollMemoizer({
+ callback: function(_ref4) {
+ var scrollLeft = _ref4.scrollLeft, scrollTop = _ref4.scrollTop, _props5 = _this5.props, height = _props5.height, onScroll = _props5.onScroll, width = _props5.width;
+ onScroll({
+ clientHeight: height,
+ clientWidth: width,
+ scrollHeight: totalRowsHeight,
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ scrollWidth: totalColumnsWidth
+ });
+ },
+ indices: {
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop
+ }
+ });
+ }
+ }, {
+ key: "_setScrollPosition",
+ value: function(_ref5) {
+ var scrollLeft = _ref5.scrollLeft, scrollTop = _ref5.scrollTop, newState = {
+ scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.REQUESTED
+ };
+ scrollLeft >= 0 && (newState.scrollLeft = scrollLeft), scrollTop >= 0 && (newState.scrollTop = scrollTop),
+ (scrollLeft >= 0 && scrollLeft !== this.state.scrollLeft || scrollTop >= 0 && scrollTop !== this.state.scrollTop) && this.setState(newState);
+ }
+ }, {
+ key: "_wrapPropertyGetter",
+ value: function(value) {
+ return value instanceof Function ? value : function() {
+ return value;
+ };
+ }
+ }, {
+ key: "_wrapSizeGetter",
+ value: function(size) {
+ return this._wrapPropertyGetter(size);
+ }
+ }, {
+ key: "_updateScrollLeftForScrollToColumn",
+ value: function() {
+ var props = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : this.props, state = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : this.state, columnCount = props.columnCount, scrollToAlignment = props.scrollToAlignment, scrollToColumn = props.scrollToColumn, width = props.width, scrollLeft = state.scrollLeft;
+ if (scrollToColumn >= 0 && columnCount > 0) {
+ var targetIndex = Math.max(0, Math.min(columnCount - 1, scrollToColumn)), calculatedScrollLeft = this._columnSizeAndPositionManager.getUpdatedOffsetForIndex({
+ align: scrollToAlignment,
+ containerSize: width,
+ currentOffset: scrollLeft,
+ targetIndex: targetIndex
+ });
+ scrollLeft !== calculatedScrollLeft && this._setScrollPosition({
+ scrollLeft: calculatedScrollLeft
+ });
+ }
+ }
+ }, {
+ key: "_updateScrollTopForScrollToRow",
+ value: function() {
+ var props = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : this.props, state = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : this.state, height = props.height, rowCount = props.rowCount, scrollToAlignment = props.scrollToAlignment, scrollToRow = props.scrollToRow, scrollTop = state.scrollTop;
+ if (scrollToRow >= 0 && rowCount > 0) {
+ var targetIndex = Math.max(0, Math.min(rowCount - 1, scrollToRow)), calculatedScrollTop = this._rowSizeAndPositionManager.getUpdatedOffsetForIndex({
+ align: scrollToAlignment,
+ containerSize: height,
+ currentOffset: scrollTop,
+ targetIndex: targetIndex
+ });
+ scrollTop !== calculatedScrollTop && this._setScrollPosition({
+ scrollTop: calculatedScrollTop
+ });
+ }
+ }
+ }, {
+ key: "_onScroll",
+ value: function(event) {
+ if (event.target === this._scrollingContainer) {
+ this._enablePointerEventsAfterDelay();
+ var _props6 = this.props, height = _props6.height, width = _props6.width, scrollbarSize = this._scrollbarSize, totalRowsHeight = this._rowSizeAndPositionManager.getTotalSize(), totalColumnsWidth = this._columnSizeAndPositionManager.getTotalSize(), scrollLeft = Math.min(Math.max(0, totalColumnsWidth - width + scrollbarSize), event.target.scrollLeft), scrollTop = Math.min(Math.max(0, totalRowsHeight - height + scrollbarSize), event.target.scrollTop);
+ if (this.state.scrollLeft !== scrollLeft || this.state.scrollTop !== scrollTop) {
+ var scrollDirectionVertical = scrollTop > this.state.scrollTop ? _getOverscanIndices.SCROLL_DIRECTION_FORWARD : _getOverscanIndices.SCROLL_DIRECTION_BACKWARD, scrollDirectionHorizontal = scrollLeft > this.state.scrollLeft ? _getOverscanIndices.SCROLL_DIRECTION_FORWARD : _getOverscanIndices.SCROLL_DIRECTION_BACKWARD;
+ this.setState({
+ isScrolling: !0,
+ scrollDirectionHorizontal: scrollDirectionHorizontal,
+ scrollDirectionVertical: scrollDirectionVertical,
+ scrollLeft: scrollLeft,
+ scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.OBSERVED,
+ scrollTop: scrollTop
+ });
+ }
+ this._invokeOnScrollMemoizer({
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ totalColumnsWidth: totalColumnsWidth,
+ totalRowsHeight: totalRowsHeight
+ });
+ }
+ }
+ } ]), Grid;
+ }(_react.Component);
+ Grid.defaultProps = {
+ "aria-label": "grid",
+ cellRangeRenderer: _defaultCellRangeRenderer2.default,
+ estimatedColumnSize: 100,
+ estimatedRowSize: 30,
+ noContentRenderer: function() {
+ return null;
+ },
+ onScroll: function() {
+ return null;
+ },
+ onSectionRendered: function() {
+ return null;
+ },
+ overscanColumnCount: 0,
+ overscanRowCount: 10,
+ scrollingResetTimeInterval: DEFAULT_SCROLLING_RESET_TIME_INTERVAL,
+ scrollToAlignment: "auto",
+ style: {},
+ tabIndex: 0
+ }, exports.default = Grid;
+ }, /* 122 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function calculateSizeAndPositionDataAndUpdateScrollOffset(_ref) {
+ var cellCount = _ref.cellCount, cellSize = _ref.cellSize, computeMetadataCallback = _ref.computeMetadataCallback, computeMetadataCallbackProps = _ref.computeMetadataCallbackProps, nextCellsCount = _ref.nextCellsCount, nextCellSize = _ref.nextCellSize, nextScrollToIndex = _ref.nextScrollToIndex, scrollToIndex = _ref.scrollToIndex, updateScrollOffsetForScrollToIndex = _ref.updateScrollOffsetForScrollToIndex;
+ cellCount === nextCellsCount && ("number" != typeof cellSize && "number" != typeof nextCellSize || cellSize === nextCellSize) || (computeMetadataCallback(computeMetadataCallbackProps),
+ scrollToIndex >= 0 && scrollToIndex === nextScrollToIndex && updateScrollOffsetForScrollToIndex());
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = calculateSizeAndPositionDataAndUpdateScrollOffset;
+ }, /* 123 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.DEFAULT_MAX_SCROLL_SIZE = void 0;
+ var _objectWithoutProperties2 = __webpack_require__(105), _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _CellSizeAndPositionManager = __webpack_require__(124), _CellSizeAndPositionManager2 = _interopRequireDefault(_CellSizeAndPositionManager), DEFAULT_MAX_SCROLL_SIZE = exports.DEFAULT_MAX_SCROLL_SIZE = 15e5, ScalingCellSizeAndPositionManager = function() {
+ function ScalingCellSizeAndPositionManager(_ref) {
+ var _ref$maxScrollSize = _ref.maxScrollSize, maxScrollSize = void 0 === _ref$maxScrollSize ? DEFAULT_MAX_SCROLL_SIZE : _ref$maxScrollSize, params = (0,
+ _objectWithoutProperties3.default)(_ref, [ "maxScrollSize" ]);
+ (0, _classCallCheck3.default)(this, ScalingCellSizeAndPositionManager), this._cellSizeAndPositionManager = new _CellSizeAndPositionManager2.default(params),
+ this._maxScrollSize = maxScrollSize;
+ }
+ return (0, _createClass3.default)(ScalingCellSizeAndPositionManager, [ {
+ key: "configure",
+ value: function(params) {
+ this._cellSizeAndPositionManager.configure(params);
+ }
+ }, {
+ key: "getCellCount",
+ value: function() {
+ return this._cellSizeAndPositionManager.getCellCount();
+ }
+ }, {
+ key: "getEstimatedCellSize",
+ value: function() {
+ return this._cellSizeAndPositionManager.getEstimatedCellSize();
+ }
+ }, {
+ key: "getLastMeasuredIndex",
+ value: function() {
+ return this._cellSizeAndPositionManager.getLastMeasuredIndex();
+ }
+ }, {
+ key: "getOffsetAdjustment",
+ value: function(_ref2) {
+ var containerSize = _ref2.containerSize, offset = _ref2.offset, totalSize = this._cellSizeAndPositionManager.getTotalSize(), safeTotalSize = this.getTotalSize(), offsetPercentage = this._getOffsetPercentage({
+ containerSize: containerSize,
+ offset: offset,
+ totalSize: safeTotalSize
+ });
+ return Math.round(offsetPercentage * (safeTotalSize - totalSize));
+ }
+ }, {
+ key: "getSizeAndPositionOfCell",
+ value: function(index) {
+ return this._cellSizeAndPositionManager.getSizeAndPositionOfCell(index);
+ }
+ }, {
+ key: "getSizeAndPositionOfLastMeasuredCell",
+ value: function() {
+ return this._cellSizeAndPositionManager.getSizeAndPositionOfLastMeasuredCell();
+ }
+ }, {
+ key: "getTotalSize",
+ value: function() {
+ return Math.min(this._maxScrollSize, this._cellSizeAndPositionManager.getTotalSize());
+ }
+ }, {
+ key: "getUpdatedOffsetForIndex",
+ value: function(_ref3) {
+ var _ref3$align = _ref3.align, align = void 0 === _ref3$align ? "auto" : _ref3$align, containerSize = _ref3.containerSize, currentOffset = _ref3.currentOffset, targetIndex = _ref3.targetIndex, totalSize = _ref3.totalSize;
+ currentOffset = this._safeOffsetToOffset({
+ containerSize: containerSize,
+ offset: currentOffset
+ });
+ var offset = this._cellSizeAndPositionManager.getUpdatedOffsetForIndex({
+ align: align,
+ containerSize: containerSize,
+ currentOffset: currentOffset,
+ targetIndex: targetIndex,
+ totalSize: totalSize
+ });
+ return this._offsetToSafeOffset({
+ containerSize: containerSize,
+ offset: offset
+ });
+ }
+ }, {
+ key: "getVisibleCellRange",
+ value: function(_ref4) {
+ var containerSize = _ref4.containerSize, offset = _ref4.offset;
+ return offset = this._safeOffsetToOffset({
+ containerSize: containerSize,
+ offset: offset
+ }), this._cellSizeAndPositionManager.getVisibleCellRange({
+ containerSize: containerSize,
+ offset: offset
+ });
+ }
+ }, {
+ key: "resetCell",
+ value: function(index) {
+ this._cellSizeAndPositionManager.resetCell(index);
+ }
+ }, {
+ key: "_getOffsetPercentage",
+ value: function(_ref5) {
+ var containerSize = _ref5.containerSize, offset = _ref5.offset, totalSize = _ref5.totalSize;
+ return totalSize <= containerSize ? 0 : offset / (totalSize - containerSize);
+ }
+ }, {
+ key: "_offsetToSafeOffset",
+ value: function(_ref6) {
+ var containerSize = _ref6.containerSize, offset = _ref6.offset, totalSize = this._cellSizeAndPositionManager.getTotalSize(), safeTotalSize = this.getTotalSize();
+ if (totalSize === safeTotalSize) return offset;
+ var offsetPercentage = this._getOffsetPercentage({
+ containerSize: containerSize,
+ offset: offset,
+ totalSize: totalSize
+ });
+ return Math.round(offsetPercentage * (safeTotalSize - containerSize));
+ }
+ }, {
+ key: "_safeOffsetToOffset",
+ value: function(_ref7) {
+ var containerSize = _ref7.containerSize, offset = _ref7.offset, totalSize = this._cellSizeAndPositionManager.getTotalSize(), safeTotalSize = this.getTotalSize();
+ if (totalSize === safeTotalSize) return offset;
+ var offsetPercentage = this._getOffsetPercentage({
+ containerSize: containerSize,
+ offset: offset,
+ totalSize: safeTotalSize
+ });
+ return Math.round(offsetPercentage * (totalSize - containerSize));
+ }
+ } ]), ScalingCellSizeAndPositionManager;
+ }();
+ exports.default = ScalingCellSizeAndPositionManager;
+ }, /* 124 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), CellSizeAndPositionManager = function() {
+ function CellSizeAndPositionManager(_ref) {
+ var cellCount = _ref.cellCount, cellSizeGetter = _ref.cellSizeGetter, estimatedCellSize = _ref.estimatedCellSize;
+ (0, _classCallCheck3.default)(this, CellSizeAndPositionManager), this._cellSizeGetter = cellSizeGetter,
+ this._cellCount = cellCount, this._estimatedCellSize = estimatedCellSize, this._cellSizeAndPositionData = {},
+ this._lastMeasuredIndex = -1;
+ }
+ return (0, _createClass3.default)(CellSizeAndPositionManager, [ {
+ key: "configure",
+ value: function(_ref2) {
+ var cellCount = _ref2.cellCount, estimatedCellSize = _ref2.estimatedCellSize;
+ this._cellCount = cellCount, this._estimatedCellSize = estimatedCellSize;
+ }
+ }, {
+ key: "getCellCount",
+ value: function() {
+ return this._cellCount;
+ }
+ }, {
+ key: "getEstimatedCellSize",
+ value: function() {
+ return this._estimatedCellSize;
+ }
+ }, {
+ key: "getLastMeasuredIndex",
+ value: function() {
+ return this._lastMeasuredIndex;
+ }
+ }, {
+ key: "getSizeAndPositionOfCell",
+ value: function(index) {
+ if (index < 0 || index >= this._cellCount) throw Error("Requested index " + index + " is outside of range 0.." + this._cellCount);
+ if (index > this._lastMeasuredIndex) {
+ for (var lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell(), _offset = lastMeasuredCellSizeAndPosition.offset + lastMeasuredCellSizeAndPosition.size, i = this._lastMeasuredIndex + 1; i <= index; i++) {
+ var _size = this._cellSizeGetter({
+ index: i
+ });
+ if (null == _size || isNaN(_size)) throw Error("Invalid size returned for cell " + i + " of value " + _size);
+ this._cellSizeAndPositionData[i] = {
+ offset: _offset,
+ size: _size
+ }, _offset += _size;
+ }
+ this._lastMeasuredIndex = index;
+ }
+ return this._cellSizeAndPositionData[index];
+ }
+ }, {
+ key: "getSizeAndPositionOfLastMeasuredCell",
+ value: function() {
+ return this._lastMeasuredIndex >= 0 ? this._cellSizeAndPositionData[this._lastMeasuredIndex] : {
+ offset: 0,
+ size: 0
+ };
+ }
+ }, {
+ key: "getTotalSize",
+ value: function() {
+ var lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell();
+ return lastMeasuredCellSizeAndPosition.offset + lastMeasuredCellSizeAndPosition.size + (this._cellCount - this._lastMeasuredIndex - 1) * this._estimatedCellSize;
+ }
+ }, {
+ key: "getUpdatedOffsetForIndex",
+ value: function(_ref3) {
+ var _ref3$align = _ref3.align, align = void 0 === _ref3$align ? "auto" : _ref3$align, containerSize = _ref3.containerSize, currentOffset = _ref3.currentOffset, targetIndex = _ref3.targetIndex;
+ if (containerSize <= 0) return 0;
+ var datum = this.getSizeAndPositionOfCell(targetIndex), maxOffset = datum.offset, minOffset = maxOffset - containerSize + datum.size, idealOffset = void 0;
+ switch (align) {
+ case "start":
+ idealOffset = maxOffset;
+ break;
+
+ case "end":
+ idealOffset = minOffset;
+ break;
+
+ case "center":
+ idealOffset = maxOffset - (containerSize - datum.size) / 2;
+ break;
+
+ default:
+ idealOffset = Math.max(minOffset, Math.min(maxOffset, currentOffset));
+ }
+ var totalSize = this.getTotalSize();
+ return Math.max(0, Math.min(totalSize - containerSize, idealOffset));
+ }
+ }, {
+ key: "getVisibleCellRange",
+ value: function(_ref4) {
+ var containerSize = _ref4.containerSize, offset = _ref4.offset, totalSize = this.getTotalSize();
+ if (0 === totalSize) return {};
+ var maxOffset = offset + containerSize, start = this._findNearestCell(offset), datum = this.getSizeAndPositionOfCell(start);
+ offset = datum.offset + datum.size;
+ for (var stop = start; offset < maxOffset && stop < this._cellCount - 1; ) stop++,
+ offset += this.getSizeAndPositionOfCell(stop).size;
+ return {
+ start: start,
+ stop: stop
+ };
+ }
+ }, {
+ key: "resetCell",
+ value: function(index) {
+ this._lastMeasuredIndex = Math.min(this._lastMeasuredIndex, index - 1);
+ }
+ }, {
+ key: "_binarySearch",
+ value: function(_ref5) {
+ for (var high = _ref5.high, low = _ref5.low, offset = _ref5.offset, middle = void 0, currentOffset = void 0; low <= high; ) {
+ if (middle = low + Math.floor((high - low) / 2), currentOffset = this.getSizeAndPositionOfCell(middle).offset,
+ currentOffset === offset) return middle;
+ currentOffset < offset ? low = middle + 1 : currentOffset > offset && (high = middle - 1);
+ }
+ if (low > 0) return low - 1;
+ }
+ }, {
+ key: "_exponentialSearch",
+ value: function(_ref6) {
+ for (var index = _ref6.index, offset = _ref6.offset, interval = 1; index < this._cellCount && this.getSizeAndPositionOfCell(index).offset < offset; ) index += interval,
+ interval *= 2;
+ return this._binarySearch({
+ high: Math.min(index, this._cellCount - 1),
+ low: Math.floor(index / 2),
+ offset: offset
+ });
+ }
+ }, {
+ key: "_findNearestCell",
+ value: function(offset) {
+ if (isNaN(offset)) throw Error("Invalid offset " + offset + " specified");
+ offset = Math.max(0, offset);
+ var lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell(), lastMeasuredIndex = Math.max(0, this._lastMeasuredIndex);
+ return lastMeasuredCellSizeAndPosition.offset >= offset ? this._binarySearch({
+ high: lastMeasuredIndex,
+ low: 0,
+ offset: offset
+ }) : this._exponentialSearch({
+ index: lastMeasuredIndex,
+ offset: offset
+ });
+ }
+ } ]), CellSizeAndPositionManager;
+ }();
+ exports.default = CellSizeAndPositionManager;
+ }, /* 125 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function getOverscanIndices(_ref) {
+ var cellCount = _ref.cellCount, overscanCellsCount = _ref.overscanCellsCount, scrollDirection = _ref.scrollDirection, startIndex = _ref.startIndex, stopIndex = _ref.stopIndex, overscanStartIndex = void 0, overscanStopIndex = void 0;
+ return scrollDirection === SCROLL_DIRECTION_FORWARD ? (overscanStartIndex = startIndex,
+ overscanStopIndex = stopIndex + 2 * overscanCellsCount) : scrollDirection === SCROLL_DIRECTION_BACKWARD ? (overscanStartIndex = startIndex - 2 * overscanCellsCount,
+ overscanStopIndex = stopIndex) : (overscanStartIndex = startIndex - overscanCellsCount,
+ overscanStopIndex = stopIndex + overscanCellsCount), {
+ overscanStartIndex: Math.max(0, overscanStartIndex),
+ overscanStopIndex: Math.min(cellCount - 1, overscanStopIndex)
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = getOverscanIndices;
+ var SCROLL_DIRECTION_BACKWARD = exports.SCROLL_DIRECTION_BACKWARD = -1, SCROLL_DIRECTION_FORWARD = (exports.SCROLL_DIRECTION_FIXED = 0,
+ exports.SCROLL_DIRECTION_FORWARD = 1);
+ }, /* 126 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function updateScrollIndexHelper(_ref) {
+ var cellSize = _ref.cellSize, cellSizeAndPositionManager = _ref.cellSizeAndPositionManager, previousCellsCount = _ref.previousCellsCount, previousCellSize = _ref.previousCellSize, previousScrollToAlignment = _ref.previousScrollToAlignment, previousScrollToIndex = _ref.previousScrollToIndex, previousSize = _ref.previousSize, scrollOffset = _ref.scrollOffset, scrollToAlignment = _ref.scrollToAlignment, scrollToIndex = _ref.scrollToIndex, size = _ref.size, updateScrollIndexCallback = _ref.updateScrollIndexCallback, cellCount = cellSizeAndPositionManager.getCellCount(), hasScrollToIndex = scrollToIndex >= 0 && scrollToIndex < cellCount, sizeHasChanged = size !== previousSize || !previousCellSize || "number" == typeof cellSize && cellSize !== previousCellSize;
+ hasScrollToIndex && (sizeHasChanged || scrollToAlignment !== previousScrollToAlignment || scrollToIndex !== previousScrollToIndex) ? updateScrollIndexCallback(scrollToIndex) : !hasScrollToIndex && cellCount > 0 && (size < previousSize || cellCount < previousCellsCount) && scrollOffset > cellSizeAndPositionManager.getTotalSize() - size && updateScrollIndexCallback(cellCount - 1);
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = updateScrollIndexHelper;
+ }, /* 127 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function defaultCellRangeRenderer(_ref) {
+ for (var cellCache = _ref.cellCache, cellRenderer = _ref.cellRenderer, columnSizeAndPositionManager = _ref.columnSizeAndPositionManager, columnStartIndex = _ref.columnStartIndex, columnStopIndex = _ref.columnStopIndex, horizontalOffsetAdjustment = _ref.horizontalOffsetAdjustment, isScrolling = _ref.isScrolling, rowSizeAndPositionManager = _ref.rowSizeAndPositionManager, rowStartIndex = _ref.rowStartIndex, rowStopIndex = _ref.rowStopIndex, verticalOffsetAdjustment = (_ref.scrollLeft,
+ _ref.scrollTop, _ref.verticalOffsetAdjustment), visibleColumnIndices = _ref.visibleColumnIndices, visibleRowIndices = _ref.visibleRowIndices, renderedCells = [], rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) for (var rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex), columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
+ var columnDatum = columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex), isVisible = columnIndex >= visibleColumnIndices.start && columnIndex <= visibleColumnIndices.stop && rowIndex >= visibleRowIndices.start && rowIndex <= visibleRowIndices.stop, key = rowIndex + "-" + columnIndex, style = {
+ height: rowDatum.size,
+ left: columnDatum.offset + horizontalOffsetAdjustment,
+ position: "absolute",
+ top: rowDatum.offset + verticalOffsetAdjustment,
+ width: columnDatum.size
+ }, cellRendererParams = {
+ columnIndex: columnIndex,
+ isScrolling: isScrolling,
+ isVisible: isVisible,
+ key: key,
+ rowIndex: rowIndex,
+ style: style
+ }, renderedCell = void 0;
+ !isScrolling || horizontalOffsetAdjustment || verticalOffsetAdjustment ? renderedCell = cellRenderer(cellRendererParams) : (cellCache[key] || (cellCache[key] = cellRenderer(cellRendererParams)),
+ renderedCell = cellCache[key]), null != renderedCell && renderedCell !== !1 && renderedCells.push(renderedCell);
+ }
+ return renderedCells;
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = defaultCellRangeRenderer;
+ }, /* 128 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.SortIndicator = exports.SortDirection = exports.Column = exports.Table = exports.defaultRowRenderer = exports.defaultHeaderRenderer = exports.defaultCellRenderer = exports.defaultCellDataGetter = exports.default = void 0;
+ var _Table2 = __webpack_require__(129), _Table3 = _interopRequireDefault(_Table2), _defaultCellDataGetter2 = __webpack_require__(135), _defaultCellDataGetter3 = _interopRequireDefault(_defaultCellDataGetter2), _defaultCellRenderer2 = __webpack_require__(134), _defaultCellRenderer3 = _interopRequireDefault(_defaultCellRenderer2), _defaultHeaderRenderer2 = __webpack_require__(131), _defaultHeaderRenderer3 = _interopRequireDefault(_defaultHeaderRenderer2), _defaultRowRenderer2 = __webpack_require__(136), _defaultRowRenderer3 = _interopRequireDefault(_defaultRowRenderer2), _Column2 = __webpack_require__(130), _Column3 = _interopRequireDefault(_Column2), _SortDirection2 = __webpack_require__(133), _SortDirection3 = _interopRequireDefault(_SortDirection2), _SortIndicator2 = __webpack_require__(132), _SortIndicator3 = _interopRequireDefault(_SortIndicator2);
+ exports.default = _Table3.default, exports.defaultCellDataGetter = _defaultCellDataGetter3.default,
+ exports.defaultCellRenderer = _defaultCellRenderer3.default, exports.defaultHeaderRenderer = _defaultHeaderRenderer3.default,
+ exports.defaultRowRenderer = _defaultRowRenderer3.default, exports.Table = _Table3.default,
+ exports.Column = _Column3.default, exports.SortDirection = _SortDirection3.default,
+ exports.SortIndicator = _SortIndicator3.default;
+ }, /* 129 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _extends2 = __webpack_require__(100), _extends3 = _interopRequireDefault(_extends2), _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _classnames = __webpack_require__(107), _classnames2 = _interopRequireDefault(_classnames), _Column = __webpack_require__(130), _react = (_interopRequireDefault(_Column),
+ __webpack_require__(89)), _react2 = _interopRequireDefault(_react), _reactDom = __webpack_require__(96), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _Grid = __webpack_require__(120), _Grid2 = _interopRequireDefault(_Grid), _defaultRowRenderer = __webpack_require__(136), _defaultRowRenderer2 = _interopRequireDefault(_defaultRowRenderer), _SortDirection = __webpack_require__(133), _SortDirection2 = _interopRequireDefault(_SortDirection), Table = function(_Component) {
+ function Table(props) {
+ (0, _classCallCheck3.default)(this, Table);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (Table.__proto__ || (0,
+ _getPrototypeOf2.default)(Table)).call(this, props));
+ return _this.state = {
+ scrollbarWidth: 0
+ }, _this._createColumn = _this._createColumn.bind(_this), _this._createRow = _this._createRow.bind(_this),
+ _this._onScroll = _this._onScroll.bind(_this), _this._onSectionRendered = _this._onSectionRendered.bind(_this),
+ _this;
+ }
+ return (0, _inherits3.default)(Table, _Component), (0, _createClass3.default)(Table, [ {
+ key: "forceUpdateGrid",
+ value: function() {
+ this.Grid.forceUpdate();
+ }
+ }, {
+ key: "measureAllRows",
+ value: function() {
+ this.Grid.measureAllCells();
+ }
+ }, {
+ key: "recomputeRowHeights",
+ value: function() {
+ var index = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0;
+ this.Grid.recomputeGridSize({
+ rowIndex: index
+ }), this.forceUpdateGrid();
+ }
+ }, {
+ key: "componentDidMount",
+ value: function() {
+ this._setScrollbarWidth();
+ }
+ }, {
+ key: "componentDidUpdate",
+ value: function() {
+ this._setScrollbarWidth();
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _this2 = this, _props = this.props, children = _props.children, className = _props.className, disableHeader = _props.disableHeader, gridClassName = _props.gridClassName, gridStyle = _props.gridStyle, headerHeight = _props.headerHeight, height = _props.height, id = _props.id, noRowsRenderer = _props.noRowsRenderer, rowClassName = _props.rowClassName, rowStyle = _props.rowStyle, scrollToIndex = _props.scrollToIndex, style = _props.style, width = _props.width, scrollbarWidth = this.state.scrollbarWidth, availableRowsHeight = height - headerHeight, rowClass = rowClassName instanceof Function ? rowClassName({
+ index: -1
+ }) : rowClassName, rowStyleObject = rowStyle instanceof Function ? rowStyle({
+ index: -1
+ }) : rowStyle;
+ return this._cachedColumnStyles = [], _react2.default.Children.toArray(children).forEach(function(column, index) {
+ var flexStyles = _this2._getFlexStyleForColumn(column, column.props.style);
+ _this2._cachedColumnStyles[index] = (0, _extends3.default)({}, flexStyles, {
+ overflow: "hidden"
+ });
+ }), _react2.default.createElement("div", {
+ className: (0, _classnames2.default)("ReactVirtualized__Table", className),
+ id: id,
+ style: style
+ }, !disableHeader && _react2.default.createElement("div", {
+ className: (0, _classnames2.default)("ReactVirtualized__Table__headerRow", rowClass),
+ style: (0, _extends3.default)({}, rowStyleObject, {
+ height: headerHeight,
+ overflow: "hidden",
+ paddingRight: scrollbarWidth,
+ width: width
+ })
+ }, this._getRenderedHeaderRow()), _react2.default.createElement(_Grid2.default, (0,
+ _extends3.default)({}, this.props, {
+ autoContainerWidth: !0,
+ className: (0, _classnames2.default)("ReactVirtualized__Table__Grid", gridClassName),
+ cellRenderer: this._createRow,
+ columnWidth: width,
+ columnCount: 1,
+ height: availableRowsHeight,
+ id: void 0,
+ noContentRenderer: noRowsRenderer,
+ onScroll: this._onScroll,
+ onSectionRendered: this._onSectionRendered,
+ ref: function(_ref) {
+ _this2.Grid = _ref;
+ },
+ scrollbarWidth: scrollbarWidth,
+ scrollToRow: scrollToIndex,
+ style: (0, _extends3.default)({}, gridStyle, {
+ overflowX: "hidden"
+ })
+ })));
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_createColumn",
+ value: function(_ref2) {
+ var column = _ref2.column, columnIndex = _ref2.columnIndex, isScrolling = _ref2.isScrolling, rowData = _ref2.rowData, rowIndex = _ref2.rowIndex, _column$props = column.props, cellDataGetter = _column$props.cellDataGetter, cellRenderer = _column$props.cellRenderer, className = _column$props.className, columnData = _column$props.columnData, dataKey = _column$props.dataKey, cellData = cellDataGetter({
+ columnData: columnData,
+ dataKey: dataKey,
+ rowData: rowData
+ }), renderedCell = cellRenderer({
+ cellData: cellData,
+ columnData: columnData,
+ dataKey: dataKey,
+ isScrolling: isScrolling,
+ rowData: rowData,
+ rowIndex: rowIndex
+ }), style = this._cachedColumnStyles[columnIndex], title = "string" == typeof renderedCell ? renderedCell : null;
+ return _react2.default.createElement("div", {
+ key: "Row" + rowIndex + "-Col" + columnIndex,
+ className: (0, _classnames2.default)("ReactVirtualized__Table__rowColumn", className),
+ style: style,
+ title: title
+ }, renderedCell);
+ }
+ }, {
+ key: "_createHeader",
+ value: function(_ref3) {
+ var column = _ref3.column, index = _ref3.index, _props2 = this.props, headerClassName = _props2.headerClassName, headerStyle = _props2.headerStyle, onHeaderClick = _props2.onHeaderClick, sort = _props2.sort, sortBy = _props2.sortBy, sortDirection = _props2.sortDirection, _column$props2 = column.props, dataKey = _column$props2.dataKey, disableSort = _column$props2.disableSort, headerRenderer = _column$props2.headerRenderer, label = _column$props2.label, columnData = _column$props2.columnData, sortEnabled = !disableSort && sort, classNames = (0,
+ _classnames2.default)("ReactVirtualized__Table__headerColumn", headerClassName, column.props.headerClassName, {
+ ReactVirtualized__Table__sortableHeaderColumn: sortEnabled
+ }), style = this._getFlexStyleForColumn(column, headerStyle), renderedHeader = headerRenderer({
+ columnData: columnData,
+ dataKey: dataKey,
+ disableSort: disableSort,
+ label: label,
+ sortBy: sortBy,
+ sortDirection: sortDirection
+ }), a11yProps = {};
+ return (sortEnabled || onHeaderClick) && !function() {
+ var newSortDirection = sortBy !== dataKey || sortDirection === _SortDirection2.default.DESC ? _SortDirection2.default.ASC : _SortDirection2.default.DESC, onClick = function() {
+ sortEnabled && sort({
+ sortBy: dataKey,
+ sortDirection: newSortDirection
+ }), onHeaderClick && onHeaderClick({
+ columnData: columnData,
+ dataKey: dataKey
+ });
+ }, onKeyDown = function(event) {
+ "Enter" !== event.key && " " !== event.key || onClick();
+ };
+ a11yProps["aria-label"] = column.props["aria-label"] || label || dataKey, a11yProps.role = "rowheader",
+ a11yProps.tabIndex = 0, a11yProps.onClick = onClick, a11yProps.onKeyDown = onKeyDown;
+ }(), _react2.default.createElement("div", (0, _extends3.default)({}, a11yProps, {
+ key: "Header-Col" + index,
+ className: classNames,
+ style: style
+ }), renderedHeader);
+ }
+ }, {
+ key: "_createRow",
+ value: function(_ref4) {
+ var _this3 = this, index = _ref4.rowIndex, isScrolling = _ref4.isScrolling, key = _ref4.key, style = _ref4.style, _props3 = this.props, children = _props3.children, onRowClick = _props3.onRowClick, onRowDoubleClick = _props3.onRowDoubleClick, onRowMouseOver = _props3.onRowMouseOver, onRowMouseOut = _props3.onRowMouseOut, rowClassName = _props3.rowClassName, rowGetter = _props3.rowGetter, rowRenderer = _props3.rowRenderer, rowStyle = _props3.rowStyle, scrollbarWidth = this.state.scrollbarWidth, rowClass = rowClassName instanceof Function ? rowClassName({
+ index: index
+ }) : rowClassName, rowStyleObject = rowStyle instanceof Function ? rowStyle({
+ index: index
+ }) : rowStyle, rowData = rowGetter({
+ index: index
+ }), columns = _react2.default.Children.toArray(children).map(function(column, columnIndex) {
+ return _this3._createColumn({
+ column: column,
+ columnIndex: columnIndex,
+ isScrolling: isScrolling,
+ rowData: rowData,
+ rowIndex: index,
+ scrollbarWidth: scrollbarWidth
+ });
+ }), className = (0, _classnames2.default)("ReactVirtualized__Table__row", rowClass), flattenedStyle = (0,
+ _extends3.default)({}, style, rowStyleObject, {
+ height: this._getRowHeight(index),
+ overflow: "hidden",
+ paddingRight: scrollbarWidth
+ });
+ return rowRenderer({
+ className: className,
+ columns: columns,
+ index: index,
+ isScrolling: isScrolling,
+ key: key,
+ onRowClick: onRowClick,
+ onRowDoubleClick: onRowDoubleClick,
+ onRowMouseOver: onRowMouseOver,
+ onRowMouseOut: onRowMouseOut,
+ rowData: rowData,
+ style: flattenedStyle
+ });
+ }
+ }, {
+ key: "_getFlexStyleForColumn",
+ value: function(column) {
+ var customStyle = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, flexValue = column.props.flexGrow + " " + column.props.flexShrink + " " + column.props.width + "px", style = (0,
+ _extends3.default)({}, customStyle, {
+ flex: flexValue,
+ msFlex: flexValue,
+ WebkitFlex: flexValue
+ });
+ return column.props.maxWidth && (style.maxWidth = column.props.maxWidth), column.props.minWidth && (style.minWidth = column.props.minWidth),
+ style;
+ }
+ }, {
+ key: "_getRenderedHeaderRow",
+ value: function() {
+ var _this4 = this, _props4 = this.props, children = _props4.children, disableHeader = _props4.disableHeader, items = disableHeader ? [] : _react2.default.Children.toArray(children);
+ return items.map(function(column, index) {
+ return _this4._createHeader({
+ column: column,
+ index: index
+ });
+ });
+ }
+ }, {
+ key: "_getRowHeight",
+ value: function(rowIndex) {
+ var rowHeight = this.props.rowHeight;
+ return rowHeight instanceof Function ? rowHeight({
+ index: rowIndex
+ }) : rowHeight;
+ }
+ }, {
+ key: "_onScroll",
+ value: function(_ref5) {
+ var clientHeight = _ref5.clientHeight, scrollHeight = _ref5.scrollHeight, scrollTop = _ref5.scrollTop, onScroll = this.props.onScroll;
+ onScroll({
+ clientHeight: clientHeight,
+ scrollHeight: scrollHeight,
+ scrollTop: scrollTop
+ });
+ }
+ }, {
+ key: "_onSectionRendered",
+ value: function(_ref6) {
+ var rowOverscanStartIndex = _ref6.rowOverscanStartIndex, rowOverscanStopIndex = _ref6.rowOverscanStopIndex, rowStartIndex = _ref6.rowStartIndex, rowStopIndex = _ref6.rowStopIndex, onRowsRendered = this.props.onRowsRendered;
+ onRowsRendered({
+ overscanStartIndex: rowOverscanStartIndex,
+ overscanStopIndex: rowOverscanStopIndex,
+ startIndex: rowStartIndex,
+ stopIndex: rowStopIndex
+ });
+ }
+ }, {
+ key: "_setScrollbarWidth",
+ value: function() {
+ var Grid = (0, _reactDom.findDOMNode)(this.Grid), clientWidth = Grid.clientWidth || 0, offsetWidth = Grid.offsetWidth || 0, scrollbarWidth = offsetWidth - clientWidth;
+ this.setState({
+ scrollbarWidth: scrollbarWidth
+ });
+ }
+ } ]), Table;
+ }(_react.Component);
+ Table.defaultProps = {
+ disableHeader: !1,
+ estimatedRowSize: 30,
+ headerHeight: 0,
+ headerStyle: {},
+ noRowsRenderer: function() {
+ return null;
+ },
+ onRowsRendered: function() {
+ return null;
+ },
+ onScroll: function() {
+ return null;
+ },
+ overscanRowCount: 10,
+ rowRenderer: _defaultRowRenderer2.default,
+ rowStyle: {},
+ scrollToAlignment: "auto",
+ style: {}
+ }, exports.default = Table;
+ }, /* 130 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _defaultHeaderRenderer = __webpack_require__(131), _defaultHeaderRenderer2 = _interopRequireDefault(_defaultHeaderRenderer), _defaultCellRenderer = __webpack_require__(134), _defaultCellRenderer2 = _interopRequireDefault(_defaultCellRenderer), _defaultCellDataGetter = __webpack_require__(135), _defaultCellDataGetter2 = _interopRequireDefault(_defaultCellDataGetter), Column = function(_Component) {
+ function Column() {
+ return (0, _classCallCheck3.default)(this, Column), (0, _possibleConstructorReturn3.default)(this, (Column.__proto__ || (0,
+ _getPrototypeOf2.default)(Column)).apply(this, arguments));
+ }
+ return (0, _inherits3.default)(Column, _Component), Column;
+ }(_react.Component);
+ Column.defaultProps = {
+ cellDataGetter: _defaultCellDataGetter2.default,
+ cellRenderer: _defaultCellRenderer2.default,
+ flexGrow: 0,
+ flexShrink: 1,
+ headerRenderer: _defaultHeaderRenderer2.default,
+ style: {}
+ }, exports.default = Column;
+ }, /* 131 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function defaultHeaderRenderer(_ref) {
+ var dataKey = (_ref.columnData, _ref.dataKey), label = (_ref.disableSort, _ref.label), sortBy = _ref.sortBy, sortDirection = _ref.sortDirection, showSortIndicator = sortBy === dataKey, children = [ _react2.default.createElement("span", {
+ className: "ReactVirtualized__Table__headerTruncatedText",
+ key: "label",
+ title: label
+ }, label) ];
+ return showSortIndicator && children.push(_react2.default.createElement(_SortIndicator2.default, {
+ key: "SortIndicator",
+ sortDirection: sortDirection
+ })), children;
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = defaultHeaderRenderer;
+ var _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _SortIndicator = __webpack_require__(132), _SortIndicator2 = _interopRequireDefault(_SortIndicator);
+ }, /* 132 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function SortIndicator(_ref) {
+ var sortDirection = _ref.sortDirection, classNames = (0, _classnames2.default)("ReactVirtualized__Table__sortableHeaderIcon", {
+ "ReactVirtualized__Table__sortableHeaderIcon--ASC": sortDirection === _SortDirection2.default.ASC,
+ "ReactVirtualized__Table__sortableHeaderIcon--DESC": sortDirection === _SortDirection2.default.DESC
+ });
+ return _react2.default.createElement("svg", {
+ className: classNames,
+ width: 18,
+ height: 18,
+ viewBox: "0 0 24 24"
+ }, sortDirection === _SortDirection2.default.ASC ? _react2.default.createElement("path", {
+ d: "M7 14l5-5 5 5z"
+ }) : _react2.default.createElement("path", {
+ d: "M7 10l5 5 5-5z"
+ }), _react2.default.createElement("path", {
+ d: "M0 0h24v24H0z",
+ fill: "none"
+ }));
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = SortIndicator;
+ var _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _classnames = __webpack_require__(107), _classnames2 = _interopRequireDefault(_classnames), _SortDirection = __webpack_require__(133), _SortDirection2 = _interopRequireDefault(_SortDirection);
+ }, /* 133 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var SortDirection = {
+ ASC: "ASC",
+ DESC: "DESC"
+ };
+ exports.default = SortDirection;
+ }, /* 134 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function defaultCellRenderer(_ref) {
+ var cellData = _ref.cellData;
+ _ref.cellDataKey, _ref.columnData, _ref.rowData, _ref.rowIndex;
+ return null == cellData ? "" : String(cellData);
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = defaultCellRenderer;
+ }, /* 135 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function defaultCellDataGetter(_ref) {
+ var dataKey = (_ref.columnData, _ref.dataKey), rowData = _ref.rowData;
+ return rowData.get instanceof Function ? rowData.get(dataKey) : rowData[dataKey];
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.default = defaultCellDataGetter;
+ }, /* 136 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function defaultRowRenderer(_ref) {
+ var className = _ref.className, columns = _ref.columns, index = _ref.index, key = (_ref.isScrolling,
+ _ref.key), onRowClick = _ref.onRowClick, onRowDoubleClick = _ref.onRowDoubleClick, onRowMouseOver = _ref.onRowMouseOver, onRowMouseOut = _ref.onRowMouseOut, style = (_ref.rowData,
+ _ref.style), a11yProps = {};
+ return (onRowClick || onRowDoubleClick || onRowMouseOver || onRowMouseOut) && (a11yProps["aria-label"] = "row",
+ a11yProps.role = "row", a11yProps.tabIndex = 0, onRowClick && (a11yProps.onClick = function() {
+ return onRowClick({
+ index: index
+ });
+ }), onRowDoubleClick && (a11yProps.onDoubleClick = function() {
+ return onRowDoubleClick({
+ index: index
+ });
+ }), onRowMouseOut && (a11yProps.onMouseOut = function() {
+ return onRowMouseOut({
+ index: index
+ });
+ }), onRowMouseOver && (a11yProps.onMouseOver = function() {
+ return onRowMouseOver({
+ index: index
+ });
+ })), _react2.default.createElement("div", (0, _extends3.default)({}, a11yProps, {
+ className: className,
+ key: key,
+ style: style
+ }), columns);
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _extends2 = __webpack_require__(100), _extends3 = _interopRequireDefault(_extends2);
+ exports.default = defaultRowRenderer;
+ var _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react);
+ }, /* 137 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.InfiniteLoader = exports.default = void 0;
+ var _InfiniteLoader2 = __webpack_require__(138), _InfiniteLoader3 = _interopRequireDefault(_InfiniteLoader2);
+ exports.default = _InfiniteLoader3.default, exports.InfiniteLoader = _InfiniteLoader3.default;
+ }, /* 138 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ function isRangeVisible(_ref2) {
+ var lastRenderedStartIndex = _ref2.lastRenderedStartIndex, lastRenderedStopIndex = _ref2.lastRenderedStopIndex, startIndex = _ref2.startIndex, stopIndex = _ref2.stopIndex;
+ return !(startIndex > lastRenderedStopIndex || stopIndex < lastRenderedStartIndex);
+ }
+ function scanForUnloadedRanges(_ref3) {
+ for (var isRowLoaded = _ref3.isRowLoaded, minimumBatchSize = _ref3.minimumBatchSize, rowCount = _ref3.rowCount, startIndex = _ref3.startIndex, stopIndex = _ref3.stopIndex, unloadedRanges = [], rangeStartIndex = null, rangeStopIndex = null, index = startIndex; index <= stopIndex; index++) {
+ var loaded = isRowLoaded({
+ index: index
+ });
+ loaded ? null !== rangeStopIndex && (unloadedRanges.push({
+ startIndex: rangeStartIndex,
+ stopIndex: rangeStopIndex
+ }), rangeStartIndex = rangeStopIndex = null) : (rangeStopIndex = index, null === rangeStartIndex && (rangeStartIndex = index));
+ }
+ if (null !== rangeStopIndex) {
+ for (var potentialStopIndex = Math.min(Math.max(rangeStopIndex, rangeStartIndex + minimumBatchSize - 1), rowCount - 1), _index = rangeStopIndex + 1; _index <= potentialStopIndex && !isRowLoaded({
+ index: _index
+ }); _index++) rangeStopIndex = _index;
+ unloadedRanges.push({
+ startIndex: rangeStartIndex,
+ stopIndex: rangeStopIndex
+ });
+ }
+ if (unloadedRanges.length) for (var firstUnloadedRange = unloadedRanges[0]; firstUnloadedRange.stopIndex - firstUnloadedRange.startIndex + 1 < minimumBatchSize && firstUnloadedRange.startIndex > 0; ) {
+ var _index2 = firstUnloadedRange.startIndex - 1;
+ if (isRowLoaded({
+ index: _index2
+ })) break;
+ firstUnloadedRange.startIndex = _index2;
+ }
+ return unloadedRanges;
+ }
+ function forceUpdateReactVirtualizedComponent(component) {
+ "function" == typeof component.forceUpdateGrid ? component.forceUpdateGrid() : component.forceUpdate();
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2);
+ exports.isRangeVisible = isRangeVisible, exports.scanForUnloadedRanges = scanForUnloadedRanges,
+ exports.forceUpdateReactVirtualizedComponent = forceUpdateReactVirtualizedComponent;
+ var _react = __webpack_require__(89), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _createCallbackMemoizer = __webpack_require__(108), _createCallbackMemoizer2 = _interopRequireDefault(_createCallbackMemoizer), InfiniteLoader = function(_Component) {
+ function InfiniteLoader(props, context) {
+ (0, _classCallCheck3.default)(this, InfiniteLoader);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (InfiniteLoader.__proto__ || (0,
+ _getPrototypeOf2.default)(InfiniteLoader)).call(this, props, context));
+ return _this._loadMoreRowsMemoizer = (0, _createCallbackMemoizer2.default)(), _this._onRowsRendered = _this._onRowsRendered.bind(_this),
+ _this._registerChild = _this._registerChild.bind(_this), _this;
+ }
+ return (0, _inherits3.default)(InfiniteLoader, _Component), (0, _createClass3.default)(InfiniteLoader, [ {
+ key: "render",
+ value: function() {
+ var children = this.props.children;
+ return children({
+ onRowsRendered: this._onRowsRendered,
+ registerChild: this._registerChild
+ });
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_loadUnloadedRanges",
+ value: function(unloadedRanges) {
+ var _this2 = this, loadMoreRows = this.props.loadMoreRows;
+ unloadedRanges.forEach(function(unloadedRange) {
+ var promise = loadMoreRows(unloadedRange);
+ promise && promise.then(function() {
+ isRangeVisible({
+ lastRenderedStartIndex: _this2._lastRenderedStartIndex,
+ lastRenderedStopIndex: _this2._lastRenderedStopIndex,
+ startIndex: unloadedRange.startIndex,
+ stopIndex: unloadedRange.stopIndex
+ }) && _this2._registeredChild && forceUpdateReactVirtualizedComponent(_this2._registeredChild);
+ });
+ });
+ }
+ }, {
+ key: "_onRowsRendered",
+ value: function(_ref) {
+ var _this3 = this, startIndex = _ref.startIndex, stopIndex = _ref.stopIndex, _props = this.props, isRowLoaded = _props.isRowLoaded, minimumBatchSize = _props.minimumBatchSize, rowCount = _props.rowCount, threshold = _props.threshold;
+ this._lastRenderedStartIndex = startIndex, this._lastRenderedStopIndex = stopIndex;
+ var unloadedRanges = scanForUnloadedRanges({
+ isRowLoaded: isRowLoaded,
+ minimumBatchSize: minimumBatchSize,
+ rowCount: rowCount,
+ startIndex: Math.max(0, startIndex - threshold),
+ stopIndex: Math.min(rowCount - 1, stopIndex + threshold)
+ }), squashedUnloadedRanges = unloadedRanges.reduce(function(reduced, unloadedRange) {
+ return reduced.concat([ unloadedRange.startIndex, unloadedRange.stopIndex ]);
+ }, []);
+ this._loadMoreRowsMemoizer({
+ callback: function() {
+ _this3._loadUnloadedRanges(unloadedRanges);
+ },
+ indices: {
+ squashedUnloadedRanges: squashedUnloadedRanges
+ }
+ });
+ }
+ }, {
+ key: "_registerChild",
+ value: function(registeredChild) {
+ this._registeredChild = registeredChild;
+ }
+ } ]), InfiniteLoader;
+ }(_react.Component);
+ InfiniteLoader.defaultProps = {
+ minimumBatchSize: 10,
+ rowCount: 0,
+ threshold: 15
+ }, exports.default = InfiniteLoader;
+ }, /* 139 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.ScrollSync = exports.default = void 0;
+ var _ScrollSync2 = __webpack_require__(140), _ScrollSync3 = _interopRequireDefault(_ScrollSync2);
+ exports.default = _ScrollSync3.default, exports.ScrollSync = _ScrollSync3.default;
+ }, /* 140 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), ScrollSync = function(_Component) {
+ function ScrollSync(props, context) {
+ (0, _classCallCheck3.default)(this, ScrollSync);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (ScrollSync.__proto__ || (0,
+ _getPrototypeOf2.default)(ScrollSync)).call(this, props, context));
+ return _this.state = {
+ clientHeight: 0,
+ clientWidth: 0,
+ scrollHeight: 0,
+ scrollLeft: 0,
+ scrollTop: 0,
+ scrollWidth: 0
+ }, _this._onScroll = _this._onScroll.bind(_this), _this;
+ }
+ return (0, _inherits3.default)(ScrollSync, _Component), (0, _createClass3.default)(ScrollSync, [ {
+ key: "render",
+ value: function() {
+ var children = this.props.children, _state = this.state, clientHeight = _state.clientHeight, clientWidth = _state.clientWidth, scrollHeight = _state.scrollHeight, scrollLeft = _state.scrollLeft, scrollTop = _state.scrollTop, scrollWidth = _state.scrollWidth;
+ return children({
+ clientHeight: clientHeight,
+ clientWidth: clientWidth,
+ onScroll: this._onScroll,
+ scrollHeight: scrollHeight,
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ scrollWidth: scrollWidth
+ });
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_onScroll",
+ value: function(_ref) {
+ var clientHeight = _ref.clientHeight, clientWidth = _ref.clientWidth, scrollHeight = _ref.scrollHeight, scrollLeft = _ref.scrollLeft, scrollTop = _ref.scrollTop, scrollWidth = _ref.scrollWidth;
+ this.setState({
+ clientHeight: clientHeight,
+ clientWidth: clientWidth,
+ scrollHeight: scrollHeight,
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ scrollWidth: scrollWidth
+ });
+ }
+ } ]), ScrollSync;
+ }(_react.Component);
+ exports.default = ScrollSync;
+ }, /* 141 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.List = exports.default = void 0;
+ var _List2 = __webpack_require__(142), _List3 = _interopRequireDefault(_List2);
+ exports.default = _List3.default, exports.List = _List3.default;
+ }, /* 142 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _objectWithoutProperties2 = __webpack_require__(105), _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2), _extends2 = __webpack_require__(100), _extends3 = _interopRequireDefault(_extends2), _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _Grid = __webpack_require__(120), _Grid2 = _interopRequireDefault(_Grid), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _classnames = __webpack_require__(107), _classnames2 = _interopRequireDefault(_classnames), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), List = function(_Component) {
+ function List(props, context) {
+ (0, _classCallCheck3.default)(this, List);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (List.__proto__ || (0,
+ _getPrototypeOf2.default)(List)).call(this, props, context));
+ return _this._cellRenderer = _this._cellRenderer.bind(_this), _this._onScroll = _this._onScroll.bind(_this),
+ _this._onSectionRendered = _this._onSectionRendered.bind(_this), _this;
+ }
+ return (0, _inherits3.default)(List, _Component), (0, _createClass3.default)(List, [ {
+ key: "forceUpdateGrid",
+ value: function() {
+ this.Grid.forceUpdate();
+ }
+ }, {
+ key: "measureAllRows",
+ value: function() {
+ this.Grid.measureAllCells();
+ }
+ }, {
+ key: "recomputeRowHeights",
+ value: function() {
+ var index = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0;
+ this.Grid.recomputeGridSize({
+ rowIndex: index
+ }), this.forceUpdateGrid();
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var _this2 = this, _props = this.props, className = _props.className, noRowsRenderer = _props.noRowsRenderer, scrollToIndex = _props.scrollToIndex, width = _props.width, classNames = (0,
+ _classnames2.default)("ReactVirtualized__List", className);
+ return _react2.default.createElement(_Grid2.default, (0, _extends3.default)({}, this.props, {
+ autoContainerWidth: !0,
+ cellRenderer: this._cellRenderer,
+ className: classNames,
+ columnWidth: width,
+ columnCount: 1,
+ noContentRenderer: noRowsRenderer,
+ onScroll: this._onScroll,
+ onSectionRendered: this._onSectionRendered,
+ ref: function(_ref) {
+ _this2.Grid = _ref;
+ },
+ scrollToRow: scrollToIndex
+ }));
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_cellRenderer",
+ value: function(_ref2) {
+ var rowIndex = _ref2.rowIndex, style = _ref2.style, rest = (0, _objectWithoutProperties3.default)(_ref2, [ "rowIndex", "style" ]), rowRenderer = this.props.rowRenderer;
+ return style.width = "100%", rowRenderer((0, _extends3.default)({
+ index: rowIndex,
+ style: style
+ }, rest));
+ }
+ }, {
+ key: "_onScroll",
+ value: function(_ref3) {
+ var clientHeight = _ref3.clientHeight, scrollHeight = _ref3.scrollHeight, scrollTop = _ref3.scrollTop, onScroll = this.props.onScroll;
+ onScroll({
+ clientHeight: clientHeight,
+ scrollHeight: scrollHeight,
+ scrollTop: scrollTop
+ });
+ }
+ }, {
+ key: "_onSectionRendered",
+ value: function(_ref4) {
+ var rowOverscanStartIndex = _ref4.rowOverscanStartIndex, rowOverscanStopIndex = _ref4.rowOverscanStopIndex, rowStartIndex = _ref4.rowStartIndex, rowStopIndex = _ref4.rowStopIndex, onRowsRendered = this.props.onRowsRendered;
+ onRowsRendered({
+ overscanStartIndex: rowOverscanStartIndex,
+ overscanStopIndex: rowOverscanStopIndex,
+ startIndex: rowStartIndex,
+ stopIndex: rowStopIndex
+ });
+ }
+ } ]), List;
+ }(_react.Component);
+ List.defaultProps = {
+ estimatedRowSize: 30,
+ noRowsRenderer: function() {
+ return null;
+ },
+ onRowsRendered: function() {
+ return null;
+ },
+ onScroll: function() {
+ return null;
+ },
+ overscanRowCount: 10,
+ scrollToAlignment: "auto",
+ style: {}
+ }, exports.default = List;
+ }, /* 143 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.IS_SCROLLING_TIMEOUT = exports.WindowScroller = exports.default = void 0;
+ var _onScroll = __webpack_require__(144);
+ Object.defineProperty(exports, "IS_SCROLLING_TIMEOUT", {
+ enumerable: !0,
+ get: function() {
+ return _onScroll.IS_SCROLLING_TIMEOUT;
+ }
+ });
+ var _WindowScroller2 = __webpack_require__(145), _WindowScroller3 = _interopRequireDefault(_WindowScroller2);
+ exports.default = _WindowScroller3.default, exports.WindowScroller = _WindowScroller3.default;
+ }, /* 144 */
+ /***/
+ function(module, exports) {
+ "use strict";
+ function enablePointerEventsIfDisabled() {
+ disablePointerEventsTimeoutId && (disablePointerEventsTimeoutId = null, document.firstElementChild.style.pointerEvents = originalBodyPointerEvents,
+ originalBodyPointerEvents = null);
+ }
+ function enablePointerEventsAfterDelayCallback() {
+ enablePointerEventsIfDisabled(), mountedInstances.forEach(function(component) {
+ return component._enablePointerEventsAfterDelayCallback();
+ });
+ }
+ function enablePointerEventsAfterDelay() {
+ disablePointerEventsTimeoutId && clearTimeout(disablePointerEventsTimeoutId), disablePointerEventsTimeoutId = setTimeout(enablePointerEventsAfterDelayCallback, IS_SCROLLING_TIMEOUT);
+ }
+ function onScrollWindow(event) {
+ null == originalBodyPointerEvents && (originalBodyPointerEvents = document.firstElementChild.style.pointerEvents,
+ document.firstElementChild.style.pointerEvents = "none", enablePointerEventsAfterDelay()), mountedInstances.forEach(function(component) {
+ return component._onScrollWindow(event);
+ });
+ }
+ function registerScrollListener(component) {
+ mountedInstances.length || window.addEventListener("scroll", onScrollWindow), mountedInstances.push(component);
+ }
+ function unregisterScrollListener(component) {
+ mountedInstances = mountedInstances.filter(function(c) {
+ return c !== component;
+ }), mountedInstances.length || (window.removeEventListener("scroll", onScrollWindow),
+ disablePointerEventsTimeoutId && (clearTimeout(disablePointerEventsTimeoutId), enablePointerEventsIfDisabled()));
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ }), exports.registerScrollListener = registerScrollListener, exports.unregisterScrollListener = unregisterScrollListener;
+ var mountedInstances = [], originalBodyPointerEvents = null, disablePointerEventsTimeoutId = null, IS_SCROLLING_TIMEOUT = exports.IS_SCROLLING_TIMEOUT = 150;
+ }, /* 145 */
+ /***/
+ function(module, exports, __webpack_require__) {
+ "use strict";
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+ Object.defineProperty(exports, "__esModule", {
+ value: !0
+ });
+ var _getPrototypeOf = __webpack_require__(3), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(29), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(30), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(34), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(81), _inherits3 = _interopRequireDefault(_inherits2), _react = __webpack_require__(89), _react2 = _interopRequireDefault(_react), _reactDom = __webpack_require__(96), _reactDom2 = _interopRequireDefault(_reactDom), _reactAddonsShallowCompare = __webpack_require__(90), _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare), _onScroll = __webpack_require__(144), WindowScroller = function(_Component) {
+ function WindowScroller(props) {
+ (0, _classCallCheck3.default)(this, WindowScroller);
+ var _this = (0, _possibleConstructorReturn3.default)(this, (WindowScroller.__proto__ || (0,
+ _getPrototypeOf2.default)(WindowScroller)).call(this, props)), height = "undefined" != typeof window ? window.innerHeight : 0;
+ return _this.state = {
+ isScrolling: !1,
+ height: height,
+ scrollTop: 0
+ }, _this._onScrollWindow = _this._onScrollWindow.bind(_this), _this._onResizeWindow = _this._onResizeWindow.bind(_this),
+ _this._enablePointerEventsAfterDelayCallback = _this._enablePointerEventsAfterDelayCallback.bind(_this),
+ _this;
+ }
+ return (0, _inherits3.default)(WindowScroller, _Component), (0, _createClass3.default)(WindowScroller, [ {
+ key: "componentDidMount",
+ value: function() {
+ var height = this.state.height;
+ this._positionFromTop = _reactDom2.default.findDOMNode(this).getBoundingClientRect().top - document.documentElement.getBoundingClientRect().top,
+ height !== window.innerHeight && this.setState({
+ height: window.innerHeight
+ }), (0, _onScroll.registerScrollListener)(this), window.addEventListener("resize", this._onResizeWindow, !1);
+ }
+ }, {
+ key: "componentWillUnmount",
+ value: function() {
+ (0, _onScroll.unregisterScrollListener)(this), window.removeEventListener("resize", this._onResizeWindow, !1);
+ }
+ }, {
+ key: "render",
+ value: function() {
+ var children = this.props.children, _state = this.state, isScrolling = _state.isScrolling, scrollTop = _state.scrollTop, height = _state.height;
+ return _react2.default.createElement("div", null, children({
+ height: height,
+ isScrolling: isScrolling,
+ scrollTop: scrollTop
+ }));
+ }
+ }, {
+ key: "shouldComponentUpdate",
+ value: function(nextProps, nextState) {
+ return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState);
+ }
+ }, {
+ key: "_enablePointerEventsAfterDelayCallback",
+ value: function() {
+ this.setState({
+ isScrolling: !1
+ });
+ }
+ }, {
+ key: "_onResizeWindow",
+ value: function(event) {
+ var onResize = this.props.onResize, height = window.innerHeight || 0;
+ this.setState({
+ height: height
+ }), onResize({
+ height: height
+ });
+ }
+ }, {
+ key: "_onScrollWindow",
+ value: function(event) {
+ var onScroll = this.props.onScroll, scrollY = "scrollY" in window ? window.scrollY : document.documentElement.scrollTop, scrollTop = Math.max(0, scrollY - this._positionFromTop);
+ this.setState({
+ isScrolling: !0,
+ scrollTop: scrollTop
+ }), onScroll({
+ scrollTop: scrollTop
+ });
+ }
+ } ]), WindowScroller;
+ }(_react.Component);
+ WindowScroller.defaultProps = {
+ onResize: function() {},
+ onScroll: function() {}
+ }, exports.default = WindowScroller;
+ } ]);
+});
+//# sourceMappingURL=react-virtualized.js.map \ No newline at end of file
diff --git a/devtools/client/shared/vendor/react.js b/devtools/client/shared/vendor/react.js
new file mode 100644
index 000000000..828dc5f13
--- /dev/null
+++ b/devtools/client/shared/vendor/react.js
@@ -0,0 +1,20763 @@
+ /**
+ * React (with addons) v0.14.6
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.React = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactWithAddons
+ */
+
+/**
+ * This module exists purely in the open source project, and is meant as a way
+ * to create a separate standalone build of React. This build has "addons", or
+ * functionality we've built and think might be useful but doesn't have a good
+ * place to live inside React core.
+ */
+
+'use strict';
+
+var LinkedStateMixin = _dereq_(22);
+var React = _dereq_(26);
+var ReactComponentWithPureRenderMixin = _dereq_(37);
+var ReactCSSTransitionGroup = _dereq_(29);
+var ReactFragment = _dereq_(64);
+var ReactTransitionGroup = _dereq_(94);
+var ReactUpdates = _dereq_(96);
+
+var cloneWithProps = _dereq_(118);
+var shallowCompare = _dereq_(140);
+var update = _dereq_(143);
+var warning = _dereq_(173);
+
+var warnedAboutBatchedUpdates = false;
+
+React.addons = {
+ CSSTransitionGroup: ReactCSSTransitionGroup,
+ LinkedStateMixin: LinkedStateMixin,
+ PureRenderMixin: ReactComponentWithPureRenderMixin,
+ TransitionGroup: ReactTransitionGroup,
+
+ batchedUpdates: function () {
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(warnedAboutBatchedUpdates, 'React.addons.batchedUpdates is deprecated. Use ' + 'ReactDOM.unstable_batchedUpdates instead.') : undefined;
+ warnedAboutBatchedUpdates = true;
+ }
+ return ReactUpdates.batchedUpdates.apply(this, arguments);
+ },
+ cloneWithProps: cloneWithProps,
+ createFragment: ReactFragment.create,
+ shallowCompare: shallowCompare,
+ update: update
+};
+
+React.addons.TestUtils = _dereq_(91);
+
+if ("production" !== 'production') {
+ React.addons.Perf = _dereq_(55);
+}
+
+module.exports = React;
+},{"118":118,"140":140,"143":143,"173":173,"22":22,"26":26,"29":29,"37":37,"55":55,"64":64,"91":91,"94":94,"96":96}],2:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule AutoFocusUtils
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactMount = _dereq_(72);
+
+var findDOMNode = _dereq_(122);
+var focusNode = _dereq_(155);
+
+var Mixin = {
+ componentDidMount: function () {
+ if (this.props.autoFocus) {
+ focusNode(findDOMNode(this));
+ }
+ }
+};
+
+var AutoFocusUtils = {
+ Mixin: Mixin,
+
+ focusDOMComponent: function () {
+ focusNode(ReactMount.getNode(this._rootNodeID));
+ }
+};
+
+module.exports = AutoFocusUtils;
+},{"122":122,"155":155,"72":72}],3:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015 Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule BeforeInputEventPlugin
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPropagators = _dereq_(19);
+var ExecutionEnvironment = _dereq_(147);
+var FallbackCompositionState = _dereq_(20);
+var SyntheticCompositionEvent = _dereq_(103);
+var SyntheticInputEvent = _dereq_(107);
+
+var keyOf = _dereq_(166);
+
+var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
+var START_KEYCODE = 229;
+
+var canUseCompositionEvent = ExecutionEnvironment.canUseDOM && 'CompositionEvent' in window;
+
+var documentMode = null;
+if (ExecutionEnvironment.canUseDOM && 'documentMode' in document) {
+ documentMode = document.documentMode;
+}
+
+// Webkit offers a very useful `textInput` event that can be used to
+// directly represent `beforeInput`. The IE `textinput` event is not as
+// useful, so we don't use it.
+var canUseTextInputEvent = ExecutionEnvironment.canUseDOM && 'TextEvent' in window && !documentMode && !isPresto();
+
+// In IE9+, we have access to composition events, but the data supplied
+// by the native compositionend event may be incorrect. Japanese ideographic
+// spaces, for instance (\u3000) are not recorded correctly.
+var useFallbackCompositionData = ExecutionEnvironment.canUseDOM && (!canUseCompositionEvent || documentMode && documentMode > 8 && documentMode <= 11);
+
+/**
+ * Opera <= 12 includes TextEvent in window, but does not fire
+ * text input events. Rely on keypress instead.
+ */
+function isPresto() {
+ var opera = window.opera;
+ return typeof opera === 'object' && typeof opera.version === 'function' && parseInt(opera.version(), 10) <= 12;
+}
+
+var SPACEBAR_CODE = 32;
+var SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+// Events and their corresponding property names.
+var eventTypes = {
+ beforeInput: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onBeforeInput: null }),
+ captured: keyOf({ onBeforeInputCapture: null })
+ },
+ dependencies: [topLevelTypes.topCompositionEnd, topLevelTypes.topKeyPress, topLevelTypes.topTextInput, topLevelTypes.topPaste]
+ },
+ compositionEnd: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCompositionEnd: null }),
+ captured: keyOf({ onCompositionEndCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionEnd, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown]
+ },
+ compositionStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCompositionStart: null }),
+ captured: keyOf({ onCompositionStartCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionStart, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown]
+ },
+ compositionUpdate: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCompositionUpdate: null }),
+ captured: keyOf({ onCompositionUpdateCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionUpdate, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown]
+ }
+};
+
+// Track whether we've ever handled a keypress on the space key.
+var hasSpaceKeypress = false;
+
+/**
+ * Return whether a native keypress event is assumed to be a command.
+ * This is required because Firefox fires `keypress` events for key commands
+ * (cut, copy, select-all, etc.) even though no character is inserted.
+ */
+function isKeypressCommand(nativeEvent) {
+ return (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&
+ // ctrlKey && altKey is equivalent to AltGr, and is not a command.
+ !(nativeEvent.ctrlKey && nativeEvent.altKey);
+}
+
+/**
+ * Translate native top level events into event types.
+ *
+ * @param {string} topLevelType
+ * @return {object}
+ */
+function getCompositionEventType(topLevelType) {
+ switch (topLevelType) {
+ case topLevelTypes.topCompositionStart:
+ return eventTypes.compositionStart;
+ case topLevelTypes.topCompositionEnd:
+ return eventTypes.compositionEnd;
+ case topLevelTypes.topCompositionUpdate:
+ return eventTypes.compositionUpdate;
+ }
+}
+
+/**
+ * Does our fallback best-guess model think this event signifies that
+ * composition has begun?
+ *
+ * @param {string} topLevelType
+ * @param {object} nativeEvent
+ * @return {boolean}
+ */
+function isFallbackCompositionStart(topLevelType, nativeEvent) {
+ return topLevelType === topLevelTypes.topKeyDown && nativeEvent.keyCode === START_KEYCODE;
+}
+
+/**
+ * Does our fallback mode think that this event is the end of composition?
+ *
+ * @param {string} topLevelType
+ * @param {object} nativeEvent
+ * @return {boolean}
+ */
+function isFallbackCompositionEnd(topLevelType, nativeEvent) {
+ switch (topLevelType) {
+ case topLevelTypes.topKeyUp:
+ // Command keys insert or clear IME input.
+ return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
+ case topLevelTypes.topKeyDown:
+ // Expect IME keyCode on each keydown. If we get any other
+ // code we must have exited earlier.
+ return nativeEvent.keyCode !== START_KEYCODE;
+ case topLevelTypes.topKeyPress:
+ case topLevelTypes.topMouseDown:
+ case topLevelTypes.topBlur:
+ // Events are not possible without cancelling IME.
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * Google Input Tools provides composition data via a CustomEvent,
+ * with the `data` property populated in the `detail` object. If this
+ * is available on the event object, use it. If not, this is a plain
+ * composition event and we have nothing special to extract.
+ *
+ * @param {object} nativeEvent
+ * @return {?string}
+ */
+function getDataFromCustomEvent(nativeEvent) {
+ var detail = nativeEvent.detail;
+ if (typeof detail === 'object' && 'data' in detail) {
+ return detail.data;
+ }
+ return null;
+}
+
+// Track the current IME composition fallback object, if any.
+var currentComposition = null;
+
+/**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?object} A SyntheticCompositionEvent.
+ */
+function extractCompositionEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var eventType;
+ var fallbackData;
+
+ if (canUseCompositionEvent) {
+ eventType = getCompositionEventType(topLevelType);
+ } else if (!currentComposition) {
+ if (isFallbackCompositionStart(topLevelType, nativeEvent)) {
+ eventType = eventTypes.compositionStart;
+ }
+ } else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) {
+ eventType = eventTypes.compositionEnd;
+ }
+
+ if (!eventType) {
+ return null;
+ }
+
+ if (useFallbackCompositionData) {
+ // The current composition is stored statically and must not be
+ // overwritten while composition continues.
+ if (!currentComposition && eventType === eventTypes.compositionStart) {
+ currentComposition = FallbackCompositionState.getPooled(topLevelTarget);
+ } else if (eventType === eventTypes.compositionEnd) {
+ if (currentComposition) {
+ fallbackData = currentComposition.getData();
+ }
+ }
+ }
+
+ var event = SyntheticCompositionEvent.getPooled(eventType, topLevelTargetID, nativeEvent, nativeEventTarget);
+
+ if (fallbackData) {
+ // Inject data generated from fallback path into the synthetic event.
+ // This matches the property of native CompositionEventInterface.
+ event.data = fallbackData;
+ } else {
+ var customData = getDataFromCustomEvent(nativeEvent);
+ if (customData !== null) {
+ event.data = customData;
+ }
+ }
+
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+}
+
+/**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?string} The string corresponding to this `beforeInput` event.
+ */
+function getNativeBeforeInputChars(topLevelType, nativeEvent) {
+ switch (topLevelType) {
+ case topLevelTypes.topCompositionEnd:
+ return getDataFromCustomEvent(nativeEvent);
+ case topLevelTypes.topKeyPress:
+ /**
+ * If native `textInput` events are available, our goal is to make
+ * use of them. However, there is a special case: the spacebar key.
+ * In Webkit, preventing default on a spacebar `textInput` event
+ * cancels character insertion, but it *also* causes the browser
+ * to fall back to its default spacebar behavior of scrolling the
+ * page.
+ *
+ * Tracking at:
+ * https://code.google.com/p/chromium/issues/detail?id=355103
+ *
+ * To avoid this issue, use the keypress event as if no `textInput`
+ * event is available.
+ */
+ var which = nativeEvent.which;
+ if (which !== SPACEBAR_CODE) {
+ return null;
+ }
+
+ hasSpaceKeypress = true;
+ return SPACEBAR_CHAR;
+
+ case topLevelTypes.topTextInput:
+ // Record the characters to be added to the DOM.
+ var chars = nativeEvent.data;
+
+ // If it's a spacebar character, assume that we have already handled
+ // it at the keypress level and bail immediately. Android Chrome
+ // doesn't give us keycodes, so we need to blacklist it.
+ if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {
+ return null;
+ }
+
+ return chars;
+
+ default:
+ // For other native event types, do nothing.
+ return null;
+ }
+}
+
+/**
+ * For browsers that do not provide the `textInput` event, extract the
+ * appropriate string to use for SyntheticInputEvent.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?string} The fallback string for this `beforeInput` event.
+ */
+function getFallbackBeforeInputChars(topLevelType, nativeEvent) {
+ // If we are currently composing (IME) and using a fallback to do so,
+ // try to extract the composed characters from the fallback object.
+ if (currentComposition) {
+ if (topLevelType === topLevelTypes.topCompositionEnd || isFallbackCompositionEnd(topLevelType, nativeEvent)) {
+ var chars = currentComposition.getData();
+ FallbackCompositionState.release(currentComposition);
+ currentComposition = null;
+ return chars;
+ }
+ return null;
+ }
+
+ switch (topLevelType) {
+ case topLevelTypes.topPaste:
+ // If a paste event occurs after a keypress, throw out the input
+ // chars. Paste events should not lead to BeforeInput events.
+ return null;
+ case topLevelTypes.topKeyPress:
+ /**
+ * As of v27, Firefox may fire keypress events even when no character
+ * will be inserted. A few possibilities:
+ *
+ * - `which` is `0`. Arrow keys, Esc key, etc.
+ *
+ * - `which` is the pressed key code, but no char is available.
+ * Ex: 'AltGr + d` in Polish. There is no modified character for
+ * this key combination and no character is inserted into the
+ * document, but FF fires the keypress for char code `100` anyway.
+ * No `input` event will occur.
+ *
+ * - `which` is the pressed key code, but a command combination is
+ * being used. Ex: `Cmd+C`. No character is inserted, and no
+ * `input` event will occur.
+ */
+ if (nativeEvent.which && !isKeypressCommand(nativeEvent)) {
+ return String.fromCharCode(nativeEvent.which);
+ }
+ return null;
+ case topLevelTypes.topCompositionEnd:
+ return useFallbackCompositionData ? null : nativeEvent.data;
+ default:
+ return null;
+ }
+}
+
+/**
+ * Extract a SyntheticInputEvent for `beforeInput`, based on either native
+ * `textInput` or fallback behavior.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {?object} A SyntheticInputEvent.
+ */
+function extractBeforeInputEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var chars;
+
+ if (canUseTextInputEvent) {
+ chars = getNativeBeforeInputChars(topLevelType, nativeEvent);
+ } else {
+ chars = getFallbackBeforeInputChars(topLevelType, nativeEvent);
+ }
+
+ // If no characters are being inserted, no BeforeInput event should
+ // be fired.
+ if (!chars) {
+ return null;
+ }
+
+ var event = SyntheticInputEvent.getPooled(eventTypes.beforeInput, topLevelTargetID, nativeEvent, nativeEventTarget);
+
+ event.data = chars;
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+}
+
+/**
+ * Create an `onBeforeInput` event to match
+ * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.
+ *
+ * This event plugin is based on the native `textInput` event
+ * available in Chrome, Safari, Opera, and IE. This event fires after
+ * `onKeyPress` and `onCompositionEnd`, but before `onInput`.
+ *
+ * `beforeInput` is spec'd but not implemented in any browsers, and
+ * the `input` event does not provide any useful information about what has
+ * actually been added, contrary to the spec. Thus, `textInput` is the best
+ * available event to identify the characters that have actually been inserted
+ * into the target node.
+ *
+ * This plugin is also responsible for emitting `composition` events, thus
+ * allowing us to share composition fallback code for both `beforeInput` and
+ * `composition` event types.
+ */
+var BeforeInputEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ return [extractCompositionEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget), extractBeforeInputEvent(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget)];
+ }
+};
+
+module.exports = BeforeInputEventPlugin;
+},{"103":103,"107":107,"147":147,"15":15,"166":166,"19":19,"20":20}],4:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSProperty
+ */
+
+'use strict';
+
+/**
+ * CSS properties which accept numbers but are not in units of "px".
+ */
+var isUnitlessNumber = {
+ animationIterationCount: true,
+ boxFlex: true,
+ boxFlexGroup: true,
+ boxOrdinalGroup: true,
+ columnCount: true,
+ flex: true,
+ flexGrow: true,
+ flexPositive: true,
+ flexShrink: true,
+ flexNegative: true,
+ flexOrder: true,
+ fontWeight: true,
+ lineClamp: true,
+ lineHeight: true,
+ opacity: true,
+ order: true,
+ orphans: true,
+ tabSize: true,
+ widows: true,
+ zIndex: true,
+ zoom: true,
+
+ // SVG-related properties
+ fillOpacity: true,
+ stopOpacity: true,
+ strokeDashoffset: true,
+ strokeOpacity: true,
+ strokeWidth: true
+};
+
+/**
+ * @param {string} prefix vendor-specific prefix, eg: Webkit
+ * @param {string} key style name, eg: transitionDuration
+ * @return {string} style name prefixed with `prefix`, properly camelCased, eg:
+ * WebkitTransitionDuration
+ */
+function prefixKey(prefix, key) {
+ return prefix + key.charAt(0).toUpperCase() + key.substring(1);
+}
+
+/**
+ * Support style names that may come passed in prefixed by adding permutations
+ * of vendor prefixes.
+ */
+var prefixes = ['Webkit', 'ms', 'Moz', 'O'];
+
+// Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an
+// infinite loop, because it iterates over the newly added props too.
+Object.keys(isUnitlessNumber).forEach(function (prop) {
+ prefixes.forEach(function (prefix) {
+ isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop];
+ });
+});
+
+/**
+ * Most style properties can be unset by doing .style[prop] = '' but IE8
+ * doesn't like doing that with shorthand properties so for the properties that
+ * IE8 breaks on, which are listed here, we instead unset each of the
+ * individual properties. See http://bugs.jquery.com/ticket/12385.
+ * The 4-value 'clock' properties like margin, padding, border-width seem to
+ * behave without any problems. Curiously, list-style works too without any
+ * special prodding.
+ */
+var shorthandPropertyExpansions = {
+ background: {
+ backgroundAttachment: true,
+ backgroundColor: true,
+ backgroundImage: true,
+ backgroundPositionX: true,
+ backgroundPositionY: true,
+ backgroundRepeat: true
+ },
+ backgroundPosition: {
+ backgroundPositionX: true,
+ backgroundPositionY: true
+ },
+ border: {
+ borderWidth: true,
+ borderStyle: true,
+ borderColor: true
+ },
+ borderBottom: {
+ borderBottomWidth: true,
+ borderBottomStyle: true,
+ borderBottomColor: true
+ },
+ borderLeft: {
+ borderLeftWidth: true,
+ borderLeftStyle: true,
+ borderLeftColor: true
+ },
+ borderRight: {
+ borderRightWidth: true,
+ borderRightStyle: true,
+ borderRightColor: true
+ },
+ borderTop: {
+ borderTopWidth: true,
+ borderTopStyle: true,
+ borderTopColor: true
+ },
+ font: {
+ fontStyle: true,
+ fontVariant: true,
+ fontWeight: true,
+ fontSize: true,
+ lineHeight: true,
+ fontFamily: true
+ },
+ outline: {
+ outlineWidth: true,
+ outlineStyle: true,
+ outlineColor: true
+ }
+};
+
+var CSSProperty = {
+ isUnitlessNumber: isUnitlessNumber,
+ shorthandPropertyExpansions: shorthandPropertyExpansions
+};
+
+module.exports = CSSProperty;
+},{}],5:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSPropertyOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var CSSProperty = _dereq_(4);
+var ExecutionEnvironment = _dereq_(147);
+var ReactPerf = _dereq_(78);
+
+var camelizeStyleName = _dereq_(149);
+var dangerousStyleValue = _dereq_(119);
+var hyphenateStyleName = _dereq_(160);
+var memoizeStringOnly = _dereq_(168);
+var warning = _dereq_(173);
+
+var processStyleName = memoizeStringOnly(function (styleName) {
+ return hyphenateStyleName(styleName);
+});
+
+var hasShorthandPropertyBug = false;
+var styleFloatAccessor = 'cssFloat';
+if (ExecutionEnvironment.canUseDOM) {
+ var tempStyle = document.createElementNS('http://www.w3.org/1999/xhtml', 'div').style;
+ try {
+ // IE8 throws "Invalid argument." if resetting shorthand style properties.
+ tempStyle.font = '';
+ } catch (e) {
+ hasShorthandPropertyBug = true;
+ }
+ // IE8 only supports accessing cssFloat (standard) as styleFloat
+ if (document.documentElement.style.cssFloat === undefined) {
+ styleFloatAccessor = 'styleFloat';
+ }
+}
+
+if ("production" !== 'production') {
+ // 'msTransform' is correct, but the other prefixes should be capitalized
+ var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/;
+
+ // style values shouldn't contain a semicolon
+ var badStyleValueWithSemicolonPattern = /;\s*$/;
+
+ var warnedStyleNames = {};
+ var warnedStyleValues = {};
+
+ var warnHyphenatedStyleName = function (name) {
+ if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
+ return;
+ }
+
+ warnedStyleNames[name] = true;
+ "production" !== 'production' ? warning(false, 'Unsupported style property %s. Did you mean %s?', name, camelizeStyleName(name)) : undefined;
+ };
+
+ var warnBadVendoredStyleName = function (name) {
+ if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
+ return;
+ }
+
+ warnedStyleNames[name] = true;
+ "production" !== 'production' ? warning(false, 'Unsupported vendor-prefixed style property %s. Did you mean %s?', name, name.charAt(0).toUpperCase() + name.slice(1)) : undefined;
+ };
+
+ var warnStyleValueWithSemicolon = function (name, value) {
+ if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) {
+ return;
+ }
+
+ warnedStyleValues[value] = true;
+ "production" !== 'production' ? warning(false, 'Style property values shouldn\'t contain a semicolon. ' + 'Try "%s: %s" instead.', name, value.replace(badStyleValueWithSemicolonPattern, '')) : undefined;
+ };
+
+ /**
+ * @param {string} name
+ * @param {*} value
+ */
+ var warnValidStyle = function (name, value) {
+ if (name.indexOf('-') > -1) {
+ warnHyphenatedStyleName(name);
+ } else if (badVendoredStyleNamePattern.test(name)) {
+ warnBadVendoredStyleName(name);
+ } else if (badStyleValueWithSemicolonPattern.test(value)) {
+ warnStyleValueWithSemicolon(name, value);
+ }
+ };
+}
+
+/**
+ * Operations for dealing with CSS properties.
+ */
+var CSSPropertyOperations = {
+
+ /**
+ * Serializes a mapping of style properties for use as inline styles:
+ *
+ * > createMarkupForStyles({width: '200px', height: 0})
+ * "width:200px;height:0;"
+ *
+ * Undefined values are ignored so that declarative programming is easier.
+ * The result should be HTML-escaped before insertion into the DOM.
+ *
+ * @param {object} styles
+ * @return {?string}
+ */
+ createMarkupForStyles: function (styles) {
+ var serialized = '';
+ for (var styleName in styles) {
+ if (!styles.hasOwnProperty(styleName)) {
+ continue;
+ }
+ var styleValue = styles[styleName];
+ if ("production" !== 'production') {
+ warnValidStyle(styleName, styleValue);
+ }
+ if (styleValue != null) {
+ serialized += processStyleName(styleName) + ':';
+ serialized += dangerousStyleValue(styleName, styleValue) + ';';
+ }
+ }
+ return serialized || null;
+ },
+
+ /**
+ * Sets the value for multiple styles on a node. If a value is specified as
+ * '' (empty string), the corresponding style property will be unset.
+ *
+ * @param {DOMElement} node
+ * @param {object} styles
+ */
+ setValueForStyles: function (node, styles) {
+ var style = node.style;
+ for (var styleName in styles) {
+ if (!styles.hasOwnProperty(styleName)) {
+ continue;
+ }
+ if ("production" !== 'production') {
+ warnValidStyle(styleName, styles[styleName]);
+ }
+ var styleValue = dangerousStyleValue(styleName, styles[styleName]);
+ if (styleName === 'float') {
+ styleName = styleFloatAccessor;
+ }
+ if (styleValue) {
+ style[styleName] = styleValue;
+ } else {
+ var expansion = hasShorthandPropertyBug && CSSProperty.shorthandPropertyExpansions[styleName];
+ if (expansion) {
+ // Shorthand property that IE8 won't like unsetting, so unset each
+ // component to placate it
+ for (var individualStyleName in expansion) {
+ style[individualStyleName] = '';
+ }
+ } else {
+ style[styleName] = '';
+ }
+ }
+ }
+ }
+
+};
+
+ReactPerf.measureMethods(CSSPropertyOperations, 'CSSPropertyOperations', {
+ setValueForStyles: 'setValueForStyles'
+});
+
+module.exports = CSSPropertyOperations;
+},{"119":119,"147":147,"149":149,"160":160,"168":168,"173":173,"4":4,"78":78}],6:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CallbackQueue
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+/**
+ * A specialized pseudo-event module to help keep track of components waiting to
+ * be notified when their DOM representations are available for use.
+ *
+ * This implements `PooledClass`, so you should never need to instantiate this.
+ * Instead, use `CallbackQueue.getPooled()`.
+ *
+ * @class ReactMountReady
+ * @implements PooledClass
+ * @internal
+ */
+function CallbackQueue() {
+ this._callbacks = null;
+ this._contexts = null;
+}
+
+assign(CallbackQueue.prototype, {
+
+ /**
+ * Enqueues a callback to be invoked when `notifyAll` is invoked.
+ *
+ * @param {function} callback Invoked when `notifyAll` is invoked.
+ * @param {?object} context Context to call `callback` with.
+ * @internal
+ */
+ enqueue: function (callback, context) {
+ this._callbacks = this._callbacks || [];
+ this._contexts = this._contexts || [];
+ this._callbacks.push(callback);
+ this._contexts.push(context);
+ },
+
+ /**
+ * Invokes all enqueued callbacks and clears the queue. This is invoked after
+ * the DOM representation of a component has been created or updated.
+ *
+ * @internal
+ */
+ notifyAll: function () {
+ var callbacks = this._callbacks;
+ var contexts = this._contexts;
+ if (callbacks) {
+ !(callbacks.length === contexts.length) ? "production" !== 'production' ? invariant(false, 'Mismatched list of contexts in callback queue') : invariant(false) : undefined;
+ this._callbacks = null;
+ this._contexts = null;
+ for (var i = 0; i < callbacks.length; i++) {
+ callbacks[i].call(contexts[i]);
+ }
+ callbacks.length = 0;
+ contexts.length = 0;
+ }
+ },
+
+ /**
+ * Resets the internal queue.
+ *
+ * @internal
+ */
+ reset: function () {
+ this._callbacks = null;
+ this._contexts = null;
+ },
+
+ /**
+ * `PooledClass` looks for this.
+ */
+ destructor: function () {
+ this.reset();
+ }
+
+});
+
+PooledClass.addPoolingTo(CallbackQueue);
+
+module.exports = CallbackQueue;
+},{"161":161,"24":24,"25":25}],7:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ChangeEventPlugin
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+var EventPropagators = _dereq_(19);
+var ExecutionEnvironment = _dereq_(147);
+var ReactUpdates = _dereq_(96);
+var SyntheticEvent = _dereq_(105);
+
+var getEventTarget = _dereq_(128);
+var isEventSupported = _dereq_(133);
+var isTextInputElement = _dereq_(134);
+var keyOf = _dereq_(166);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+var eventTypes = {
+ change: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onChange: null }),
+ captured: keyOf({ onChangeCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topChange, topLevelTypes.topClick, topLevelTypes.topFocus, topLevelTypes.topInput, topLevelTypes.topKeyDown, topLevelTypes.topKeyUp, topLevelTypes.topSelectionChange]
+ }
+};
+
+/**
+ * For IE shims
+ */
+var activeElement = null;
+var activeElementID = null;
+var activeElementValue = null;
+var activeElementValueProp = null;
+
+/**
+ * SECTION: handle `change` event
+ */
+function shouldUseChangeEvent(elem) {
+ var nodeName = elem.nodeName && elem.nodeName.toLowerCase();
+ return nodeName === 'select' || nodeName === 'input' && elem.type === 'file';
+}
+
+var doesChangeEventBubble = false;
+if (ExecutionEnvironment.canUseDOM) {
+ // See `handleChange` comment below
+ doesChangeEventBubble = isEventSupported('change') && (!('documentMode' in document) || document.documentMode > 8);
+}
+
+function manualDispatchChangeEvent(nativeEvent) {
+ var event = SyntheticEvent.getPooled(eventTypes.change, activeElementID, nativeEvent, getEventTarget(nativeEvent));
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+
+ // If change and propertychange bubbled, we'd just bind to it like all the
+ // other events and have it go through ReactBrowserEventEmitter. Since it
+ // doesn't, we manually listen for the events and so we have to enqueue and
+ // process the abstract event manually.
+ //
+ // Batching is necessary here in order to ensure that all event handlers run
+ // before the next rerender (including event handlers attached to ancestor
+ // elements instead of directly on the input). Without this, controlled
+ // components don't work properly in conjunction with event bubbling because
+ // the component is rerendered and the value reverted before all the event
+ // handlers can run. See https://github.com/facebook/react/issues/708.
+ ReactUpdates.batchedUpdates(runEventInBatch, event);
+}
+
+function runEventInBatch(event) {
+ EventPluginHub.enqueueEvents(event);
+ EventPluginHub.processEventQueue(false);
+}
+
+function startWatchingForChangeEventIE8(target, targetID) {
+ activeElement = target;
+ activeElementID = targetID;
+ activeElement.attachEvent('onchange', manualDispatchChangeEvent);
+}
+
+function stopWatchingForChangeEventIE8() {
+ if (!activeElement) {
+ return;
+ }
+ activeElement.detachEvent('onchange', manualDispatchChangeEvent);
+ activeElement = null;
+ activeElementID = null;
+}
+
+function getTargetIDForChangeEvent(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topChange) {
+ return topLevelTargetID;
+ }
+}
+function handleEventsForChangeEventIE8(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topFocus) {
+ // stopWatching() should be a noop here but we call it just in case we
+ // missed a blur event somehow.
+ stopWatchingForChangeEventIE8();
+ startWatchingForChangeEventIE8(topLevelTarget, topLevelTargetID);
+ } else if (topLevelType === topLevelTypes.topBlur) {
+ stopWatchingForChangeEventIE8();
+ }
+}
+
+/**
+ * SECTION: handle `input` event
+ */
+var isInputEventSupported = false;
+if (ExecutionEnvironment.canUseDOM) {
+ // IE9 claims to support the input event but fails to trigger it when
+ // deleting text, so we ignore its input events
+ isInputEventSupported = isEventSupported('input') && (!('documentMode' in document) || document.documentMode > 9);
+}
+
+/**
+ * (For old IE.) Replacement getter/setter for the `value` property that gets
+ * set on the active element.
+ */
+var newValueProp = {
+ get: function () {
+ return activeElementValueProp.get.call(this);
+ },
+ set: function (val) {
+ // Cast to a string so we can do equality checks.
+ activeElementValue = '' + val;
+ activeElementValueProp.set.call(this, val);
+ }
+};
+
+/**
+ * (For old IE.) Starts tracking propertychange events on the passed-in element
+ * and override the value property so that we can distinguish user events from
+ * value changes in JS.
+ */
+function startWatchingForValueChange(target, targetID) {
+ activeElement = target;
+ activeElementID = targetID;
+ activeElementValue = target.value;
+ activeElementValueProp = Object.getOwnPropertyDescriptor(target.constructor.prototype, 'value');
+
+ // Not guarded in a canDefineProperty check: IE8 supports defineProperty only
+ // on DOM elements
+ Object.defineProperty(activeElement, 'value', newValueProp);
+ activeElement.attachEvent('onpropertychange', handlePropertyChange);
+}
+
+/**
+ * (For old IE.) Removes the event listeners from the currently-tracked element,
+ * if any exists.
+ */
+function stopWatchingForValueChange() {
+ if (!activeElement) {
+ return;
+ }
+
+ // delete restores the original property definition
+ delete activeElement.value;
+ activeElement.detachEvent('onpropertychange', handlePropertyChange);
+
+ activeElement = null;
+ activeElementID = null;
+ activeElementValue = null;
+ activeElementValueProp = null;
+}
+
+/**
+ * (For old IE.) Handles a propertychange event, sending a `change` event if
+ * the value of the active element has changed.
+ */
+function handlePropertyChange(nativeEvent) {
+ if (nativeEvent.propertyName !== 'value') {
+ return;
+ }
+ var value = nativeEvent.srcElement.value;
+ if (value === activeElementValue) {
+ return;
+ }
+ activeElementValue = value;
+
+ manualDispatchChangeEvent(nativeEvent);
+}
+
+/**
+ * If a `change` event should be fired, returns the target's ID.
+ */
+function getTargetIDForInputEvent(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topInput) {
+ // In modern browsers (i.e., not IE8 or IE9), the input event is exactly
+ // what we want so fall through here and trigger an abstract event
+ return topLevelTargetID;
+ }
+}
+
+// For IE8 and IE9.
+function handleEventsForInputEventIE(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topFocus) {
+ // In IE8, we can capture almost all .value changes by adding a
+ // propertychange handler and looking for events with propertyName
+ // equal to 'value'
+ // In IE9, propertychange fires for most input events but is buggy and
+ // doesn't fire when text is deleted, but conveniently, selectionchange
+ // appears to fire in all of the remaining cases so we catch those and
+ // forward the event if the value has changed
+ // In either case, we don't want to call the event handler if the value
+ // is changed from JS so we redefine a setter for `.value` that updates
+ // our activeElementValue variable, allowing us to ignore those changes
+ //
+ // stopWatching() should be a noop here but we call it just in case we
+ // missed a blur event somehow.
+ stopWatchingForValueChange();
+ startWatchingForValueChange(topLevelTarget, topLevelTargetID);
+ } else if (topLevelType === topLevelTypes.topBlur) {
+ stopWatchingForValueChange();
+ }
+}
+
+// For IE8 and IE9.
+function getTargetIDForInputEventIE(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topSelectionChange || topLevelType === topLevelTypes.topKeyUp || topLevelType === topLevelTypes.topKeyDown) {
+ // On the selectionchange event, the target is just document which isn't
+ // helpful for us so just check activeElement instead.
+ //
+ // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
+ // propertychange on the first input event after setting `value` from a
+ // script and fires only keydown, keypress, keyup. Catching keyup usually
+ // gets it and catching keydown lets us fire an event for the first
+ // keystroke if user does a key repeat (it'll be a little delayed: right
+ // before the second keystroke). Other input methods (e.g., paste) seem to
+ // fire selectionchange normally.
+ if (activeElement && activeElement.value !== activeElementValue) {
+ activeElementValue = activeElement.value;
+ return activeElementID;
+ }
+ }
+}
+
+/**
+ * SECTION: handle `click` event
+ */
+function shouldUseClickEvent(elem) {
+ // Use the `click` event to detect changes to checkbox and radio inputs.
+ // This approach works across all browsers, whereas `change` does not fire
+ // until `blur` in IE8.
+ return elem.nodeName && elem.nodeName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio');
+}
+
+function getTargetIDForClickEvent(topLevelType, topLevelTarget, topLevelTargetID) {
+ if (topLevelType === topLevelTypes.topClick) {
+ return topLevelTargetID;
+ }
+}
+
+/**
+ * This plugin creates an `onChange` event that normalizes change events
+ * across form elements. This event fires at a time when it's possible to
+ * change the element's value without seeing a flicker.
+ *
+ * Supported elements are:
+ * - input (see `isTextInputElement`)
+ * - textarea
+ * - select
+ */
+var ChangeEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+
+ var getTargetIDFunc, handleEventFunc;
+ if (shouldUseChangeEvent(topLevelTarget)) {
+ if (doesChangeEventBubble) {
+ getTargetIDFunc = getTargetIDForChangeEvent;
+ } else {
+ handleEventFunc = handleEventsForChangeEventIE8;
+ }
+ } else if (isTextInputElement(topLevelTarget)) {
+ if (isInputEventSupported) {
+ getTargetIDFunc = getTargetIDForInputEvent;
+ } else {
+ getTargetIDFunc = getTargetIDForInputEventIE;
+ handleEventFunc = handleEventsForInputEventIE;
+ }
+ } else if (shouldUseClickEvent(topLevelTarget)) {
+ getTargetIDFunc = getTargetIDForClickEvent;
+ }
+
+ if (getTargetIDFunc) {
+ var targetID = getTargetIDFunc(topLevelType, topLevelTarget, topLevelTargetID);
+ if (targetID) {
+ var event = SyntheticEvent.getPooled(eventTypes.change, targetID, nativeEvent, nativeEventTarget);
+ event.type = 'change';
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+ }
+ }
+
+ if (handleEventFunc) {
+ handleEventFunc(topLevelType, topLevelTarget, topLevelTargetID);
+ }
+ }
+
+};
+
+module.exports = ChangeEventPlugin;
+},{"105":105,"128":128,"133":133,"134":134,"147":147,"15":15,"16":16,"166":166,"19":19,"96":96}],8:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ClientReactRootIndex
+ * @typechecks
+ */
+
+'use strict';
+
+var nextReactRootIndex = 0;
+
+var ClientReactRootIndex = {
+ createReactRootIndex: function () {
+ return nextReactRootIndex++;
+ }
+};
+
+module.exports = ClientReactRootIndex;
+},{}],9:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMChildrenOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var Danger = _dereq_(12);
+var ReactMultiChildUpdateTypes = _dereq_(74);
+var ReactPerf = _dereq_(78);
+
+var setInnerHTML = _dereq_(138);
+var setTextContent = _dereq_(139);
+var invariant = _dereq_(161);
+
+/**
+ * Inserts `childNode` as a child of `parentNode` at the `index`.
+ *
+ * @param {DOMElement} parentNode Parent node in which to insert.
+ * @param {DOMElement} childNode Child node to insert.
+ * @param {number} index Index at which to insert the child.
+ * @internal
+ */
+function insertChildAt(parentNode, childNode, index) {
+ // By exploiting arrays returning `undefined` for an undefined index, we can
+ // rely exclusively on `insertBefore(node, null)` instead of also using
+ // `appendChild(node)`. However, using `undefined` is not allowed by all
+ // browsers so we must replace it with `null`.
+
+ // fix render order error in safari
+ // IE8 will throw error when index out of list size.
+ var beforeChild = index >= parentNode.childNodes.length ? null : parentNode.childNodes.item(index);
+
+ parentNode.insertBefore(childNode, beforeChild);
+}
+
+/**
+ * Operations for updating with DOM children.
+ */
+var DOMChildrenOperations = {
+
+ dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup,
+
+ updateTextContent: setTextContent,
+
+ /**
+ * Updates a component's children by processing a series of updates. The
+ * update configurations are each expected to have a `parentNode` property.
+ *
+ * @param {array<object>} updates List of update configurations.
+ * @param {array<string>} markupList List of markup strings.
+ * @internal
+ */
+ processUpdates: function (updates, markupList) {
+ var update;
+ // Mapping from parent IDs to initial child orderings.
+ var initialChildren = null;
+ // List of children that will be moved or removed.
+ var updatedChildren = null;
+
+ for (var i = 0; i < updates.length; i++) {
+ update = updates[i];
+ if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING || update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) {
+ var updatedIndex = update.fromIndex;
+ var updatedChild = update.parentNode.childNodes[updatedIndex];
+ var parentID = update.parentID;
+
+ !updatedChild ? "production" !== 'production' ? invariant(false, 'processUpdates(): Unable to find child %s of element. This ' + 'probably means the DOM was unexpectedly mutated (e.g., by the ' + 'browser), usually due to forgetting a <tbody> when using tables, ' + 'nesting tags like <form>, <p>, or <a>, or using non-SVG elements ' + 'in an <svg> parent. Try inspecting the child nodes of the element ' + 'with React ID `%s`.', updatedIndex, parentID) : invariant(false) : undefined;
+
+ initialChildren = initialChildren || {};
+ initialChildren[parentID] = initialChildren[parentID] || [];
+ initialChildren[parentID][updatedIndex] = updatedChild;
+
+ updatedChildren = updatedChildren || [];
+ updatedChildren.push(updatedChild);
+ }
+ }
+
+ var renderedMarkup;
+ // markupList is either a list of markup or just a list of elements
+ if (markupList.length && typeof markupList[0] === 'string') {
+ renderedMarkup = Danger.dangerouslyRenderMarkup(markupList);
+ } else {
+ renderedMarkup = markupList;
+ }
+
+ // Remove updated children first so that `toIndex` is consistent.
+ if (updatedChildren) {
+ for (var j = 0; j < updatedChildren.length; j++) {
+ updatedChildren[j].parentNode.removeChild(updatedChildren[j]);
+ }
+ }
+
+ for (var k = 0; k < updates.length; k++) {
+ update = updates[k];
+ switch (update.type) {
+ case ReactMultiChildUpdateTypes.INSERT_MARKUP:
+ insertChildAt(update.parentNode, renderedMarkup[update.markupIndex], update.toIndex);
+ break;
+ case ReactMultiChildUpdateTypes.MOVE_EXISTING:
+ insertChildAt(update.parentNode, initialChildren[update.parentID][update.fromIndex], update.toIndex);
+ break;
+ case ReactMultiChildUpdateTypes.SET_MARKUP:
+ setInnerHTML(update.parentNode, update.content);
+ break;
+ case ReactMultiChildUpdateTypes.TEXT_CONTENT:
+ setTextContent(update.parentNode, update.content);
+ break;
+ case ReactMultiChildUpdateTypes.REMOVE_NODE:
+ // Already removed by the for-loop above.
+ break;
+ }
+ }
+ }
+
+};
+
+ReactPerf.measureMethods(DOMChildrenOperations, 'DOMChildrenOperations', {
+ updateTextContent: 'updateTextContent'
+});
+
+module.exports = DOMChildrenOperations;
+},{"12":12,"138":138,"139":139,"161":161,"74":74,"78":78}],10:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMProperty
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+function checkMask(value, bitmask) {
+ return (value & bitmask) === bitmask;
+}
+
+var DOMPropertyInjection = {
+ /**
+ * Mapping from normalized, camelcased property names to a configuration that
+ * specifies how the associated DOM property should be accessed or rendered.
+ */
+ MUST_USE_ATTRIBUTE: 0x1,
+ MUST_USE_PROPERTY: 0x2,
+ HAS_SIDE_EFFECTS: 0x4,
+ HAS_BOOLEAN_VALUE: 0x8,
+ HAS_NUMERIC_VALUE: 0x10,
+ HAS_POSITIVE_NUMERIC_VALUE: 0x20 | 0x10,
+ HAS_OVERLOADED_BOOLEAN_VALUE: 0x40,
+
+ /**
+ * Inject some specialized knowledge about the DOM. This takes a config object
+ * with the following properties:
+ *
+ * isCustomAttribute: function that given an attribute name will return true
+ * if it can be inserted into the DOM verbatim. Useful for data-* or aria-*
+ * attributes where it's impossible to enumerate all of the possible
+ * attribute names,
+ *
+ * Properties: object mapping DOM property name to one of the
+ * DOMPropertyInjection constants or null. If your attribute isn't in here,
+ * it won't get written to the DOM.
+ *
+ * DOMAttributeNames: object mapping React attribute name to the DOM
+ * attribute name. Attribute names not specified use the **lowercase**
+ * normalized name.
+ *
+ * DOMAttributeNamespaces: object mapping React attribute name to the DOM
+ * attribute namespace URL. (Attribute names not specified use no namespace.)
+ *
+ * DOMPropertyNames: similar to DOMAttributeNames but for DOM properties.
+ * Property names not specified use the normalized name.
+ *
+ * DOMMutationMethods: Properties that require special mutation methods. If
+ * `value` is undefined, the mutation method should unset the property.
+ *
+ * @param {object} domPropertyConfig the config as described above.
+ */
+ injectDOMPropertyConfig: function (domPropertyConfig) {
+ var Injection = DOMPropertyInjection;
+ var Properties = domPropertyConfig.Properties || {};
+ var DOMAttributeNamespaces = domPropertyConfig.DOMAttributeNamespaces || {};
+ var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {};
+ var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {};
+ var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {};
+
+ if (domPropertyConfig.isCustomAttribute) {
+ DOMProperty._isCustomAttributeFunctions.push(domPropertyConfig.isCustomAttribute);
+ }
+
+ for (var propName in Properties) {
+ !!DOMProperty.properties.hasOwnProperty(propName) ? "production" !== 'production' ? invariant(false, 'injectDOMPropertyConfig(...): You\'re trying to inject DOM property ' + '\'%s\' which has already been injected. You may be accidentally ' + 'injecting the same DOM property config twice, or you may be ' + 'injecting two configs that have conflicting property names.', propName) : invariant(false) : undefined;
+
+ var lowerCased = propName.toLowerCase();
+ var propConfig = Properties[propName];
+
+ var propertyInfo = {
+ attributeName: lowerCased,
+ attributeNamespace: null,
+ propertyName: propName,
+ mutationMethod: null,
+
+ mustUseAttribute: checkMask(propConfig, Injection.MUST_USE_ATTRIBUTE),
+ mustUseProperty: checkMask(propConfig, Injection.MUST_USE_PROPERTY),
+ hasSideEffects: checkMask(propConfig, Injection.HAS_SIDE_EFFECTS),
+ hasBooleanValue: checkMask(propConfig, Injection.HAS_BOOLEAN_VALUE),
+ hasNumericValue: checkMask(propConfig, Injection.HAS_NUMERIC_VALUE),
+ hasPositiveNumericValue: checkMask(propConfig, Injection.HAS_POSITIVE_NUMERIC_VALUE),
+ hasOverloadedBooleanValue: checkMask(propConfig, Injection.HAS_OVERLOADED_BOOLEAN_VALUE)
+ };
+
+ !(!propertyInfo.mustUseAttribute || !propertyInfo.mustUseProperty) ? "production" !== 'production' ? invariant(false, 'DOMProperty: Cannot require using both attribute and property: %s', propName) : invariant(false) : undefined;
+ !(propertyInfo.mustUseProperty || !propertyInfo.hasSideEffects) ? "production" !== 'production' ? invariant(false, 'DOMProperty: Properties that have side effects must use property: %s', propName) : invariant(false) : undefined;
+ !(propertyInfo.hasBooleanValue + propertyInfo.hasNumericValue + propertyInfo.hasOverloadedBooleanValue <= 1) ? "production" !== 'production' ? invariant(false, 'DOMProperty: Value can be one of boolean, overloaded boolean, or ' + 'numeric value, but not a combination: %s', propName) : invariant(false) : undefined;
+
+ if ("production" !== 'production') {
+ DOMProperty.getPossibleStandardName[lowerCased] = propName;
+ }
+
+ if (DOMAttributeNames.hasOwnProperty(propName)) {
+ var attributeName = DOMAttributeNames[propName];
+ propertyInfo.attributeName = attributeName;
+ if ("production" !== 'production') {
+ DOMProperty.getPossibleStandardName[attributeName] = propName;
+ }
+ }
+
+ if (DOMAttributeNamespaces.hasOwnProperty(propName)) {
+ propertyInfo.attributeNamespace = DOMAttributeNamespaces[propName];
+ }
+
+ if (DOMPropertyNames.hasOwnProperty(propName)) {
+ propertyInfo.propertyName = DOMPropertyNames[propName];
+ }
+
+ if (DOMMutationMethods.hasOwnProperty(propName)) {
+ propertyInfo.mutationMethod = DOMMutationMethods[propName];
+ }
+
+ DOMProperty.properties[propName] = propertyInfo;
+ }
+ }
+};
+var defaultValueCache = {};
+
+/**
+ * DOMProperty exports lookup objects that can be used like functions:
+ *
+ * > DOMProperty.isValid['id']
+ * true
+ * > DOMProperty.isValid['foobar']
+ * undefined
+ *
+ * Although this may be confusing, it performs better in general.
+ *
+ * @see http://jsperf.com/key-exists
+ * @see http://jsperf.com/key-missing
+ */
+var DOMProperty = {
+
+ ID_ATTRIBUTE_NAME: 'data-reactid',
+
+ /**
+ * Map from property "standard name" to an object with info about how to set
+ * the property in the DOM. Each object contains:
+ *
+ * attributeName:
+ * Used when rendering markup or with `*Attribute()`.
+ * attributeNamespace
+ * propertyName:
+ * Used on DOM node instances. (This includes properties that mutate due to
+ * external factors.)
+ * mutationMethod:
+ * If non-null, used instead of the property or `setAttribute()` after
+ * initial render.
+ * mustUseAttribute:
+ * Whether the property must be accessed and mutated using `*Attribute()`.
+ * (This includes anything that fails `<propName> in <element>`.)
+ * mustUseProperty:
+ * Whether the property must be accessed and mutated as an object property.
+ * hasSideEffects:
+ * Whether or not setting a value causes side effects such as triggering
+ * resources to be loaded or text selection changes. If true, we read from
+ * the DOM before updating to ensure that the value is only set if it has
+ * changed.
+ * hasBooleanValue:
+ * Whether the property should be removed when set to a falsey value.
+ * hasNumericValue:
+ * Whether the property must be numeric or parse as a numeric and should be
+ * removed when set to a falsey value.
+ * hasPositiveNumericValue:
+ * Whether the property must be positive numeric or parse as a positive
+ * numeric and should be removed when set to a falsey value.
+ * hasOverloadedBooleanValue:
+ * Whether the property can be used as a flag as well as with a value.
+ * Removed when strictly equal to false; present without a value when
+ * strictly equal to true; present with a value otherwise.
+ */
+ properties: {},
+
+ /**
+ * Mapping from lowercase property names to the properly cased version, used
+ * to warn in the case of missing properties. Available only in __DEV__.
+ * @type {Object}
+ */
+ getPossibleStandardName: "production" !== 'production' ? {} : null,
+
+ /**
+ * All of the isCustomAttribute() functions that have been injected.
+ */
+ _isCustomAttributeFunctions: [],
+
+ /**
+ * Checks whether a property name is a custom attribute.
+ * @method
+ */
+ isCustomAttribute: function (attributeName) {
+ for (var i = 0; i < DOMProperty._isCustomAttributeFunctions.length; i++) {
+ var isCustomAttributeFn = DOMProperty._isCustomAttributeFunctions[i];
+ if (isCustomAttributeFn(attributeName)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Returns the default property value for a DOM property (i.e., not an
+ * attribute). Most default values are '' or false, but not all. Worse yet,
+ * some (in particular, `type`) vary depending on the type of element.
+ *
+ * TODO: Is it better to grab all the possible properties when creating an
+ * element to avoid having to create the same element twice?
+ */
+ getDefaultValueForProperty: function (nodeName, prop) {
+ var nodeDefaults = defaultValueCache[nodeName];
+ var testElement;
+ if (!nodeDefaults) {
+ defaultValueCache[nodeName] = nodeDefaults = {};
+ }
+ if (!(prop in nodeDefaults)) {
+ testElement = document.createElementNS('http://www.w3.org/1999/xhtml', nodeName);
+ nodeDefaults[prop] = testElement[prop];
+ }
+ return nodeDefaults[prop];
+ },
+
+ injection: DOMPropertyInjection
+};
+
+module.exports = DOMProperty;
+},{"161":161}],11:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMPropertyOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ReactPerf = _dereq_(78);
+
+var quoteAttributeValueForBrowser = _dereq_(136);
+var warning = _dereq_(173);
+
+// Simplified subset
+var VALID_ATTRIBUTE_NAME_REGEX = /^[a-zA-Z_][\w\.\-]*$/;
+var illegalAttributeNameCache = {};
+var validatedAttributeNameCache = {};
+
+function isAttributeNameSafe(attributeName) {
+ if (validatedAttributeNameCache.hasOwnProperty(attributeName)) {
+ return true;
+ }
+ if (illegalAttributeNameCache.hasOwnProperty(attributeName)) {
+ return false;
+ }
+ if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) {
+ validatedAttributeNameCache[attributeName] = true;
+ return true;
+ }
+ illegalAttributeNameCache[attributeName] = true;
+ "production" !== 'production' ? warning(false, 'Invalid attribute name: `%s`', attributeName) : undefined;
+ return false;
+}
+
+function shouldIgnoreValue(propertyInfo, value) {
+ return value == null || propertyInfo.hasBooleanValue && !value || propertyInfo.hasNumericValue && isNaN(value) || propertyInfo.hasPositiveNumericValue && value < 1 || propertyInfo.hasOverloadedBooleanValue && value === false;
+}
+
+if ("production" !== 'production') {
+ var reactProps = {
+ children: true,
+ dangerouslySetInnerHTML: true,
+ key: true,
+ ref: true
+ };
+ var warnedProperties = {};
+
+ var warnUnknownProperty = function (name) {
+ if (reactProps.hasOwnProperty(name) && reactProps[name] || warnedProperties.hasOwnProperty(name) && warnedProperties[name]) {
+ return;
+ }
+
+ warnedProperties[name] = true;
+ var lowerCasedName = name.toLowerCase();
+
+ // data-* attributes should be lowercase; suggest the lowercase version
+ var standardName = DOMProperty.isCustomAttribute(lowerCasedName) ? lowerCasedName : DOMProperty.getPossibleStandardName.hasOwnProperty(lowerCasedName) ? DOMProperty.getPossibleStandardName[lowerCasedName] : null;
+
+ // For now, only warn when we have a suggested correction. This prevents
+ // logging too much when using transferPropsTo.
+ "production" !== 'production' ? warning(standardName == null, 'Unknown DOM property %s. Did you mean %s?', name, standardName) : undefined;
+ };
+}
+
+/**
+ * Operations for dealing with DOM properties.
+ */
+var DOMPropertyOperations = {
+
+ /**
+ * Creates markup for the ID property.
+ *
+ * @param {string} id Unescaped ID.
+ * @return {string} Markup string.
+ */
+ createMarkupForID: function (id) {
+ return DOMProperty.ID_ATTRIBUTE_NAME + '=' + quoteAttributeValueForBrowser(id);
+ },
+
+ setAttributeForID: function (node, id) {
+ node.setAttribute(DOMProperty.ID_ATTRIBUTE_NAME, id);
+ },
+
+ /**
+ * Creates markup for a property.
+ *
+ * @param {string} name
+ * @param {*} value
+ * @return {?string} Markup string, or null if the property was invalid.
+ */
+ createMarkupForProperty: function (name, value) {
+ var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null;
+ if (propertyInfo) {
+ if (shouldIgnoreValue(propertyInfo, value)) {
+ return '';
+ }
+ var attributeName = propertyInfo.attributeName;
+ if (propertyInfo.hasBooleanValue || propertyInfo.hasOverloadedBooleanValue && value === true) {
+ return attributeName + '=""';
+ }
+ return attributeName + '=' + quoteAttributeValueForBrowser(value);
+ } else if (DOMProperty.isCustomAttribute(name)) {
+ if (value == null) {
+ return '';
+ }
+ return name + '=' + quoteAttributeValueForBrowser(value);
+ } else if ("production" !== 'production') {
+ warnUnknownProperty(name);
+ }
+ return null;
+ },
+
+ /**
+ * Creates markup for a custom property.
+ *
+ * @param {string} name
+ * @param {*} value
+ * @return {string} Markup string, or empty string if the property was invalid.
+ */
+ createMarkupForCustomAttribute: function (name, value) {
+ if (!isAttributeNameSafe(name) || value == null) {
+ return '';
+ }
+ return name + '=' + quoteAttributeValueForBrowser(value);
+ },
+
+ /**
+ * Sets the value for a property on a node.
+ *
+ * @param {DOMElement} node
+ * @param {string} name
+ * @param {*} value
+ */
+ setValueForProperty: function (node, name, value) {
+ var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null;
+ if (propertyInfo) {
+ var mutationMethod = propertyInfo.mutationMethod;
+ if (mutationMethod) {
+ mutationMethod(node, value);
+ } else if (shouldIgnoreValue(propertyInfo, value)) {
+ this.deleteValueForProperty(node, name);
+ } else if (propertyInfo.mustUseAttribute) {
+ var attributeName = propertyInfo.attributeName;
+ var namespace = propertyInfo.attributeNamespace;
+ // `setAttribute` with objects becomes only `[object]` in IE8/9,
+ // ('' + value) makes it output the correct toString()-value.
+ if (namespace) {
+ node.setAttributeNS(namespace, attributeName, '' + value);
+ } else if (propertyInfo.hasBooleanValue || propertyInfo.hasOverloadedBooleanValue && value === true) {
+ node.setAttribute(attributeName, '');
+ } else {
+ node.setAttribute(attributeName, '' + value);
+ }
+ } else {
+ var propName = propertyInfo.propertyName;
+ // Must explicitly cast values for HAS_SIDE_EFFECTS-properties to the
+ // property type before comparing; only `value` does and is string.
+ if (!propertyInfo.hasSideEffects || '' + node[propName] !== '' + value) {
+ // Contrary to `setAttribute`, object properties are properly
+ // `toString`ed by IE8/9.
+ node[propName] = value;
+ }
+ }
+ } else if (DOMProperty.isCustomAttribute(name)) {
+ DOMPropertyOperations.setValueForAttribute(node, name, value);
+ } else if ("production" !== 'production') {
+ warnUnknownProperty(name);
+ }
+ },
+
+ setValueForAttribute: function (node, name, value) {
+ if (!isAttributeNameSafe(name)) {
+ return;
+ }
+ if (value == null) {
+ node.removeAttribute(name);
+ } else {
+ node.setAttribute(name, '' + value);
+ }
+ },
+
+ /**
+ * Deletes the value for a property on a node.
+ *
+ * @param {DOMElement} node
+ * @param {string} name
+ */
+ deleteValueForProperty: function (node, name) {
+ var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null;
+ if (propertyInfo) {
+ var mutationMethod = propertyInfo.mutationMethod;
+ if (mutationMethod) {
+ mutationMethod(node, undefined);
+ } else if (propertyInfo.mustUseAttribute) {
+ node.removeAttribute(propertyInfo.attributeName);
+ } else {
+ var propName = propertyInfo.propertyName;
+ var defaultValue = DOMProperty.getDefaultValueForProperty(node.nodeName, propName);
+ if (!propertyInfo.hasSideEffects || '' + node[propName] !== defaultValue) {
+ node[propName] = defaultValue;
+ }
+ }
+ } else if (DOMProperty.isCustomAttribute(name)) {
+ node.removeAttribute(name);
+ } else if ("production" !== 'production') {
+ warnUnknownProperty(name);
+ }
+ }
+
+};
+
+ReactPerf.measureMethods(DOMPropertyOperations, 'DOMPropertyOperations', {
+ setValueForProperty: 'setValueForProperty',
+ setValueForAttribute: 'setValueForAttribute',
+ deleteValueForProperty: 'deleteValueForProperty'
+});
+
+module.exports = DOMPropertyOperations;
+},{"10":10,"136":136,"173":173,"78":78}],12:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Danger
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var createNodesFromMarkup = _dereq_(152);
+var emptyFunction = _dereq_(153);
+var getMarkupWrap = _dereq_(157);
+var invariant = _dereq_(161);
+
+var OPEN_TAG_NAME_EXP = /^(<[^ \/>]+)/;
+var RESULT_INDEX_ATTR = 'data-danger-index';
+
+/**
+ * Extracts the `nodeName` from a string of markup.
+ *
+ * NOTE: Extracting the `nodeName` does not require a regular expression match
+ * because we make assumptions about React-generated markup (i.e. there are no
+ * spaces surrounding the opening tag and there is at least one attribute).
+ *
+ * @param {string} markup String of markup.
+ * @return {string} Node name of the supplied markup.
+ * @see http://jsperf.com/extract-nodename
+ */
+function getNodeName(markup) {
+ return markup.substring(1, markup.indexOf(' '));
+}
+
+var Danger = {
+
+ /**
+ * Renders markup into an array of nodes. The markup is expected to render
+ * into a list of root nodes. Also, the length of `resultList` and
+ * `markupList` should be the same.
+ *
+ * @param {array<string>} markupList List of markup strings to render.
+ * @return {array<DOMElement>} List of rendered nodes.
+ * @internal
+ */
+ dangerouslyRenderMarkup: function (markupList) {
+ !ExecutionEnvironment.canUseDOM ? "production" !== 'production' ? invariant(false, 'dangerouslyRenderMarkup(...): Cannot render markup in a worker ' + 'thread. Make sure `window` and `document` are available globally ' + 'before requiring React when unit testing or use ' + 'ReactDOMServer.renderToString for server rendering.') : invariant(false) : undefined;
+ var nodeName;
+ var markupByNodeName = {};
+ // Group markup by `nodeName` if a wrap is necessary, else by '*'.
+ for (var i = 0; i < markupList.length; i++) {
+ !markupList[i] ? "production" !== 'production' ? invariant(false, 'dangerouslyRenderMarkup(...): Missing markup.') : invariant(false) : undefined;
+ nodeName = getNodeName(markupList[i]);
+ nodeName = getMarkupWrap(nodeName) ? nodeName : '*';
+ markupByNodeName[nodeName] = markupByNodeName[nodeName] || [];
+ markupByNodeName[nodeName][i] = markupList[i];
+ }
+ var resultList = [];
+ var resultListAssignmentCount = 0;
+ for (nodeName in markupByNodeName) {
+ if (!markupByNodeName.hasOwnProperty(nodeName)) {
+ continue;
+ }
+ var markupListByNodeName = markupByNodeName[nodeName];
+
+ // This for-in loop skips the holes of the sparse array. The order of
+ // iteration should follow the order of assignment, which happens to match
+ // numerical index order, but we don't rely on that.
+ var resultIndex;
+ for (resultIndex in markupListByNodeName) {
+ if (markupListByNodeName.hasOwnProperty(resultIndex)) {
+ var markup = markupListByNodeName[resultIndex];
+
+ // Push the requested markup with an additional RESULT_INDEX_ATTR
+ // attribute. If the markup does not start with a < character, it
+ // will be discarded below (with an appropriate console.error).
+ markupListByNodeName[resultIndex] = markup.replace(OPEN_TAG_NAME_EXP,
+ // This index will be parsed back out below.
+ '$1 ' + RESULT_INDEX_ATTR + '="' + resultIndex + '" ');
+ }
+ }
+
+ // Render each group of markup with similar wrapping `nodeName`.
+ var renderNodes = createNodesFromMarkup(markupListByNodeName.join(''), emptyFunction // Do nothing special with <script> tags.
+ );
+
+ for (var j = 0; j < renderNodes.length; ++j) {
+ var renderNode = renderNodes[j];
+ if (renderNode.hasAttribute && renderNode.hasAttribute(RESULT_INDEX_ATTR)) {
+
+ resultIndex = +renderNode.getAttribute(RESULT_INDEX_ATTR);
+ renderNode.removeAttribute(RESULT_INDEX_ATTR);
+
+ !!resultList.hasOwnProperty(resultIndex) ? "production" !== 'production' ? invariant(false, 'Danger: Assigning to an already-occupied result index.') : invariant(false) : undefined;
+
+ resultList[resultIndex] = renderNode;
+
+ // This should match resultList.length and markupList.length when
+ // we're done.
+ resultListAssignmentCount += 1;
+ } else if ("production" !== 'production') {
+ console.error('Danger: Discarding unexpected node:', renderNode);
+ }
+ }
+ }
+
+ // Although resultList was populated out of order, it should now be a dense
+ // array.
+ !(resultListAssignmentCount === resultList.length) ? "production" !== 'production' ? invariant(false, 'Danger: Did not assign to every index of resultList.') : invariant(false) : undefined;
+
+ !(resultList.length === markupList.length) ? "production" !== 'production' ? invariant(false, 'Danger: Expected markup to render %s nodes, but rendered %s.', markupList.length, resultList.length) : invariant(false) : undefined;
+
+ return resultList;
+ },
+
+ /**
+ * Replaces a node with a string of markup at its current position within its
+ * parent. The markup must render into a single root node.
+ *
+ * @param {DOMElement} oldChild Child node to replace.
+ * @param {string} markup Markup to render in place of the child node.
+ * @internal
+ */
+ dangerouslyReplaceNodeWithMarkup: function (oldChild, markup) {
+ !ExecutionEnvironment.canUseDOM ? "production" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Cannot render markup in a ' + 'worker thread. Make sure `window` and `document` are available ' + 'globally before requiring React when unit testing or use ' + 'ReactDOMServer.renderToString() for server rendering.') : invariant(false) : undefined;
+ !markup ? "production" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.') : invariant(false) : undefined;
+ !(oldChild.tagName.toLowerCase() !== 'html') ? "production" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Cannot replace markup of the ' + '<html> node. This is because browser quirks make this unreliable ' + 'and/or slow. If you want to render to the root you must use ' + 'server rendering. See ReactDOMServer.renderToString().') : invariant(false) : undefined;
+
+ var newChild;
+ if (typeof markup === 'string') {
+ newChild = createNodesFromMarkup(markup, emptyFunction)[0];
+ } else {
+ newChild = markup;
+ }
+ oldChild.parentNode.replaceChild(newChild, oldChild);
+ }
+
+};
+
+module.exports = Danger;
+},{"147":147,"152":152,"153":153,"157":157,"161":161}],13:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DefaultEventPluginOrder
+ */
+
+'use strict';
+
+var keyOf = _dereq_(166);
+
+/**
+ * Module that is injectable into `EventPluginHub`, that specifies a
+ * deterministic ordering of `EventPlugin`s. A convenient way to reason about
+ * plugins, without having to package every one of them. This is better than
+ * having plugins be ordered in the same order that they are injected because
+ * that ordering would be influenced by the packaging order.
+ * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that
+ * preventing default on events is convenient in `SimpleEventPlugin` handlers.
+ */
+var DefaultEventPluginOrder = [keyOf({ ResponderEventPlugin: null }), keyOf({ SimpleEventPlugin: null }), keyOf({ TapEventPlugin: null }), keyOf({ EnterLeaveEventPlugin: null }), keyOf({ ChangeEventPlugin: null }), keyOf({ SelectEventPlugin: null }), keyOf({ BeforeInputEventPlugin: null })];
+
+module.exports = DefaultEventPluginOrder;
+},{"166":166}],14:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EnterLeaveEventPlugin
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPropagators = _dereq_(19);
+var SyntheticMouseEvent = _dereq_(109);
+
+var ReactMount = _dereq_(72);
+var keyOf = _dereq_(166);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+var getFirstReactDOM = ReactMount.getFirstReactDOM;
+
+var eventTypes = {
+ mouseEnter: {
+ registrationName: keyOf({ onMouseEnter: null }),
+ dependencies: [topLevelTypes.topMouseOut, topLevelTypes.topMouseOver]
+ },
+ mouseLeave: {
+ registrationName: keyOf({ onMouseLeave: null }),
+ dependencies: [topLevelTypes.topMouseOut, topLevelTypes.topMouseOver]
+ }
+};
+
+var extractedEvents = [null, null];
+
+var EnterLeaveEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * For almost every interaction we care about, there will be both a top-level
+ * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that
+ * we do not extract duplicate events. However, moving the mouse into the
+ * browser from outside will not fire a `mouseout` event. In this case, we use
+ * the `mouseover` top-level event.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ if (topLevelType === topLevelTypes.topMouseOver && (nativeEvent.relatedTarget || nativeEvent.fromElement)) {
+ return null;
+ }
+ if (topLevelType !== topLevelTypes.topMouseOut && topLevelType !== topLevelTypes.topMouseOver) {
+ // Must not be a mouse in or mouse out - ignoring.
+ return null;
+ }
+
+ var win;
+ if (topLevelTarget.window === topLevelTarget) {
+ // `topLevelTarget` is probably a window object.
+ win = topLevelTarget;
+ } else {
+ // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.
+ var doc = topLevelTarget.ownerDocument;
+ if (doc) {
+ win = doc.defaultView || doc.parentWindow;
+ } else {
+ win = window;
+ }
+ }
+
+ var from;
+ var to;
+ var fromID = '';
+ var toID = '';
+ if (topLevelType === topLevelTypes.topMouseOut) {
+ from = topLevelTarget;
+ fromID = topLevelTargetID;
+ to = getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement);
+ if (to) {
+ toID = ReactMount.getID(to);
+ } else {
+ to = win;
+ }
+ to = to || win;
+ } else {
+ from = win;
+ to = topLevelTarget;
+ toID = topLevelTargetID;
+ }
+
+ if (from === to) {
+ // Nothing pertains to our managed components.
+ return null;
+ }
+
+ var leave = SyntheticMouseEvent.getPooled(eventTypes.mouseLeave, fromID, nativeEvent, nativeEventTarget);
+ leave.type = 'mouseleave';
+ leave.target = from;
+ leave.relatedTarget = to;
+
+ var enter = SyntheticMouseEvent.getPooled(eventTypes.mouseEnter, toID, nativeEvent, nativeEventTarget);
+ enter.type = 'mouseenter';
+ enter.target = to;
+ enter.relatedTarget = from;
+
+ EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID);
+
+ extractedEvents[0] = leave;
+ extractedEvents[1] = enter;
+
+ return extractedEvents;
+ }
+
+};
+
+module.exports = EnterLeaveEventPlugin;
+},{"109":109,"15":15,"166":166,"19":19,"72":72}],15:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventConstants
+ */
+
+'use strict';
+
+var keyMirror = _dereq_(165);
+
+var PropagationPhases = keyMirror({ bubbled: null, captured: null });
+
+/**
+ * Types of raw signals from the browser caught at the top level.
+ */
+var topLevelTypes = keyMirror({
+ topAbort: null,
+ topBlur: null,
+ topCanPlay: null,
+ topCanPlayThrough: null,
+ topChange: null,
+ topClick: null,
+ topCompositionEnd: null,
+ topCompositionStart: null,
+ topCompositionUpdate: null,
+ topContextMenu: null,
+ topCopy: null,
+ topCut: null,
+ topDoubleClick: null,
+ topDrag: null,
+ topDragEnd: null,
+ topDragEnter: null,
+ topDragExit: null,
+ topDragLeave: null,
+ topDragOver: null,
+ topDragStart: null,
+ topDrop: null,
+ topDurationChange: null,
+ topEmptied: null,
+ topEncrypted: null,
+ topEnded: null,
+ topError: null,
+ topFocus: null,
+ topInput: null,
+ topKeyDown: null,
+ topKeyPress: null,
+ topKeyUp: null,
+ topLoad: null,
+ topLoadedData: null,
+ topLoadedMetadata: null,
+ topLoadStart: null,
+ topMouseDown: null,
+ topMouseMove: null,
+ topMouseOut: null,
+ topMouseOver: null,
+ topMouseUp: null,
+ topPaste: null,
+ topPause: null,
+ topPlay: null,
+ topPlaying: null,
+ topProgress: null,
+ topRateChange: null,
+ topReset: null,
+ topScroll: null,
+ topSeeked: null,
+ topSeeking: null,
+ topSelectionChange: null,
+ topStalled: null,
+ topSubmit: null,
+ topSuspend: null,
+ topTextInput: null,
+ topTimeUpdate: null,
+ topTouchCancel: null,
+ topTouchEnd: null,
+ topTouchMove: null,
+ topTouchStart: null,
+ topVolumeChange: null,
+ topWaiting: null,
+ topWheel: null
+});
+
+var EventConstants = {
+ topLevelTypes: topLevelTypes,
+ PropagationPhases: PropagationPhases
+};
+
+module.exports = EventConstants;
+},{"165":165}],16:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPluginHub
+ */
+
+'use strict';
+
+var EventPluginRegistry = _dereq_(17);
+var EventPluginUtils = _dereq_(18);
+var ReactErrorUtils = _dereq_(61);
+
+var accumulateInto = _dereq_(115);
+var forEachAccumulated = _dereq_(124);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Internal store for event listeners
+ */
+var listenerBank = {};
+
+/**
+ * Internal queue of events that have accumulated their dispatches and are
+ * waiting to have their dispatches executed.
+ */
+var eventQueue = null;
+
+/**
+ * Dispatches an event and releases it back into the pool, unless persistent.
+ *
+ * @param {?object} event Synthetic event to be dispatched.
+ * @param {boolean} simulated If the event is simulated (changes exn behavior)
+ * @private
+ */
+var executeDispatchesAndRelease = function (event, simulated) {
+ if (event) {
+ EventPluginUtils.executeDispatchesInOrder(event, simulated);
+
+ if (!event.isPersistent()) {
+ event.constructor.release(event);
+ }
+ }
+};
+var executeDispatchesAndReleaseSimulated = function (e) {
+ return executeDispatchesAndRelease(e, true);
+};
+var executeDispatchesAndReleaseTopLevel = function (e) {
+ return executeDispatchesAndRelease(e, false);
+};
+
+/**
+ * - `InstanceHandle`: [required] Module that performs logical traversals of DOM
+ * hierarchy given ids of the logical DOM elements involved.
+ */
+var InstanceHandle = null;
+
+function validateInstanceHandle() {
+ var valid = InstanceHandle && InstanceHandle.traverseTwoPhase && InstanceHandle.traverseEnterLeave;
+ "production" !== 'production' ? warning(valid, 'InstanceHandle not injected before use!') : undefined;
+}
+
+/**
+ * This is a unified interface for event plugins to be installed and configured.
+ *
+ * Event plugins can implement the following properties:
+ *
+ * `extractEvents` {function(string, DOMEventTarget, string, object): *}
+ * Required. When a top-level event is fired, this method is expected to
+ * extract synthetic events that will in turn be queued and dispatched.
+ *
+ * `eventTypes` {object}
+ * Optional, plugins that fire events must publish a mapping of registration
+ * names that are used to register listeners. Values of this mapping must
+ * be objects that contain `registrationName` or `phasedRegistrationNames`.
+ *
+ * `executeDispatch` {function(object, function, string)}
+ * Optional, allows plugins to override how an event gets dispatched. By
+ * default, the listener is simply invoked.
+ *
+ * Each plugin that is injected into `EventsPluginHub` is immediately operable.
+ *
+ * @public
+ */
+var EventPluginHub = {
+
+ /**
+ * Methods for injecting dependencies.
+ */
+ injection: {
+
+ /**
+ * @param {object} InjectedMount
+ * @public
+ */
+ injectMount: EventPluginUtils.injection.injectMount,
+
+ /**
+ * @param {object} InjectedInstanceHandle
+ * @public
+ */
+ injectInstanceHandle: function (InjectedInstanceHandle) {
+ InstanceHandle = InjectedInstanceHandle;
+ if ("production" !== 'production') {
+ validateInstanceHandle();
+ }
+ },
+
+ getInstanceHandle: function () {
+ if ("production" !== 'production') {
+ validateInstanceHandle();
+ }
+ return InstanceHandle;
+ },
+
+ /**
+ * @param {array} InjectedEventPluginOrder
+ * @public
+ */
+ injectEventPluginOrder: EventPluginRegistry.injectEventPluginOrder,
+
+ /**
+ * @param {object} injectedNamesToPlugins Map from names to plugin modules.
+ */
+ injectEventPluginsByName: EventPluginRegistry.injectEventPluginsByName
+
+ },
+
+ eventNameDispatchConfigs: EventPluginRegistry.eventNameDispatchConfigs,
+
+ registrationNameModules: EventPluginRegistry.registrationNameModules,
+
+ /**
+ * Stores `listener` at `listenerBank[registrationName][id]`. Is idempotent.
+ *
+ * @param {string} id ID of the DOM element.
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ * @param {?function} listener The callback to store.
+ */
+ putListener: function (id, registrationName, listener) {
+ !(typeof listener === 'function') ? "production" !== 'production' ? invariant(false, 'Expected %s listener to be a function, instead got type %s', registrationName, typeof listener) : invariant(false) : undefined;
+
+ var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
+ bankForRegistrationName[id] = listener;
+
+ var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
+ if (PluginModule && PluginModule.didPutListener) {
+ PluginModule.didPutListener(id, registrationName, listener);
+ }
+ },
+
+ /**
+ * @param {string} id ID of the DOM element.
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ * @return {?function} The stored callback.
+ */
+ getListener: function (id, registrationName) {
+ var bankForRegistrationName = listenerBank[registrationName];
+ return bankForRegistrationName && bankForRegistrationName[id];
+ },
+
+ /**
+ * Deletes a listener from the registration bank.
+ *
+ * @param {string} id ID of the DOM element.
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ */
+ deleteListener: function (id, registrationName) {
+ var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
+ if (PluginModule && PluginModule.willDeleteListener) {
+ PluginModule.willDeleteListener(id, registrationName);
+ }
+
+ var bankForRegistrationName = listenerBank[registrationName];
+ // TODO: This should never be null -- when is it?
+ if (bankForRegistrationName) {
+ delete bankForRegistrationName[id];
+ }
+ },
+
+ /**
+ * Deletes all listeners for the DOM element with the supplied ID.
+ *
+ * @param {string} id ID of the DOM element.
+ */
+ deleteAllListeners: function (id) {
+ for (var registrationName in listenerBank) {
+ if (!listenerBank[registrationName][id]) {
+ continue;
+ }
+
+ var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
+ if (PluginModule && PluginModule.willDeleteListener) {
+ PluginModule.willDeleteListener(id, registrationName);
+ }
+
+ delete listenerBank[registrationName][id];
+ }
+ },
+
+ /**
+ * Allows registered plugins an opportunity to extract events from top-level
+ * native browser events.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @internal
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var events;
+ var plugins = EventPluginRegistry.plugins;
+ for (var i = 0; i < plugins.length; i++) {
+ // Not every plugin in the ordering may be loaded at runtime.
+ var possiblePlugin = plugins[i];
+ if (possiblePlugin) {
+ var extractedEvents = possiblePlugin.extractEvents(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget);
+ if (extractedEvents) {
+ events = accumulateInto(events, extractedEvents);
+ }
+ }
+ }
+ return events;
+ },
+
+ /**
+ * Enqueues a synthetic event that should be dispatched when
+ * `processEventQueue` is invoked.
+ *
+ * @param {*} events An accumulation of synthetic events.
+ * @internal
+ */
+ enqueueEvents: function (events) {
+ if (events) {
+ eventQueue = accumulateInto(eventQueue, events);
+ }
+ },
+
+ /**
+ * Dispatches all synthetic events on the event queue.
+ *
+ * @internal
+ */
+ processEventQueue: function (simulated) {
+ // Set `eventQueue` to null before processing it so that we can tell if more
+ // events get enqueued while processing.
+ var processingEventQueue = eventQueue;
+ eventQueue = null;
+ if (simulated) {
+ forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
+ } else {
+ forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
+ }
+ !!eventQueue ? "production" !== 'production' ? invariant(false, 'processEventQueue(): Additional events were enqueued while processing ' + 'an event queue. Support for this has not yet been implemented.') : invariant(false) : undefined;
+ // This would be a good time to rethrow if any of the event handlers threw.
+ ReactErrorUtils.rethrowCaughtError();
+ },
+
+ /**
+ * These are needed for tests only. Do not use!
+ */
+ __purge: function () {
+ listenerBank = {};
+ },
+
+ __getListenerBank: function () {
+ return listenerBank;
+ }
+
+};
+
+module.exports = EventPluginHub;
+},{"115":115,"124":124,"161":161,"17":17,"173":173,"18":18,"61":61}],17:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPluginRegistry
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Injectable ordering of event plugins.
+ */
+var EventPluginOrder = null;
+
+/**
+ * Injectable mapping from names to event plugin modules.
+ */
+var namesToPlugins = {};
+
+/**
+ * Recomputes the plugin list using the injected plugins and plugin ordering.
+ *
+ * @private
+ */
+function recomputePluginOrdering() {
+ if (!EventPluginOrder) {
+ // Wait until an `EventPluginOrder` is injected.
+ return;
+ }
+ for (var pluginName in namesToPlugins) {
+ var PluginModule = namesToPlugins[pluginName];
+ var pluginIndex = EventPluginOrder.indexOf(pluginName);
+ !(pluginIndex > -1) ? "production" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject event plugins that do not exist in ' + 'the plugin ordering, `%s`.', pluginName) : invariant(false) : undefined;
+ if (EventPluginRegistry.plugins[pluginIndex]) {
+ continue;
+ }
+ !PluginModule.extractEvents ? "production" !== 'production' ? invariant(false, 'EventPluginRegistry: Event plugins must implement an `extractEvents` ' + 'method, but `%s` does not.', pluginName) : invariant(false) : undefined;
+ EventPluginRegistry.plugins[pluginIndex] = PluginModule;
+ var publishedEvents = PluginModule.eventTypes;
+ for (var eventName in publishedEvents) {
+ !publishEventForPlugin(publishedEvents[eventName], PluginModule, eventName) ? "production" !== 'production' ? invariant(false, 'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.', eventName, pluginName) : invariant(false) : undefined;
+ }
+ }
+}
+
+/**
+ * Publishes an event so that it can be dispatched by the supplied plugin.
+ *
+ * @param {object} dispatchConfig Dispatch configuration for the event.
+ * @param {object} PluginModule Plugin publishing the event.
+ * @return {boolean} True if the event was successfully published.
+ * @private
+ */
+function publishEventForPlugin(dispatchConfig, PluginModule, eventName) {
+ !!EventPluginRegistry.eventNameDispatchConfigs.hasOwnProperty(eventName) ? "production" !== 'production' ? invariant(false, 'EventPluginHub: More than one plugin attempted to publish the same ' + 'event name, `%s`.', eventName) : invariant(false) : undefined;
+ EventPluginRegistry.eventNameDispatchConfigs[eventName] = dispatchConfig;
+
+ var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames;
+ if (phasedRegistrationNames) {
+ for (var phaseName in phasedRegistrationNames) {
+ if (phasedRegistrationNames.hasOwnProperty(phaseName)) {
+ var phasedRegistrationName = phasedRegistrationNames[phaseName];
+ publishRegistrationName(phasedRegistrationName, PluginModule, eventName);
+ }
+ }
+ return true;
+ } else if (dispatchConfig.registrationName) {
+ publishRegistrationName(dispatchConfig.registrationName, PluginModule, eventName);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Publishes a registration name that is used to identify dispatched events and
+ * can be used with `EventPluginHub.putListener` to register listeners.
+ *
+ * @param {string} registrationName Registration name to add.
+ * @param {object} PluginModule Plugin publishing the event.
+ * @private
+ */
+function publishRegistrationName(registrationName, PluginModule, eventName) {
+ !!EventPluginRegistry.registrationNameModules[registrationName] ? "production" !== 'production' ? invariant(false, 'EventPluginHub: More than one plugin attempted to publish the same ' + 'registration name, `%s`.', registrationName) : invariant(false) : undefined;
+ EventPluginRegistry.registrationNameModules[registrationName] = PluginModule;
+ EventPluginRegistry.registrationNameDependencies[registrationName] = PluginModule.eventTypes[eventName].dependencies;
+}
+
+/**
+ * Registers plugins so that they can extract and dispatch events.
+ *
+ * @see {EventPluginHub}
+ */
+var EventPluginRegistry = {
+
+ /**
+ * Ordered list of injected plugins.
+ */
+ plugins: [],
+
+ /**
+ * Mapping from event name to dispatch config
+ */
+ eventNameDispatchConfigs: {},
+
+ /**
+ * Mapping from registration name to plugin module
+ */
+ registrationNameModules: {},
+
+ /**
+ * Mapping from registration name to event name
+ */
+ registrationNameDependencies: {},
+
+ /**
+ * Injects an ordering of plugins (by plugin name). This allows the ordering
+ * to be decoupled from injection of the actual plugins so that ordering is
+ * always deterministic regardless of packaging, on-the-fly injection, etc.
+ *
+ * @param {array} InjectedEventPluginOrder
+ * @internal
+ * @see {EventPluginHub.injection.injectEventPluginOrder}
+ */
+ injectEventPluginOrder: function (InjectedEventPluginOrder) {
+ !!EventPluginOrder ? "production" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject event plugin ordering more than ' + 'once. You are likely trying to load more than one copy of React.') : invariant(false) : undefined;
+ // Clone the ordering so it cannot be dynamically mutated.
+ EventPluginOrder = Array.prototype.slice.call(InjectedEventPluginOrder);
+ recomputePluginOrdering();
+ },
+
+ /**
+ * Injects plugins to be used by `EventPluginHub`. The plugin names must be
+ * in the ordering injected by `injectEventPluginOrder`.
+ *
+ * Plugins can be injected as part of page initialization or on-the-fly.
+ *
+ * @param {object} injectedNamesToPlugins Map from names to plugin modules.
+ * @internal
+ * @see {EventPluginHub.injection.injectEventPluginsByName}
+ */
+ injectEventPluginsByName: function (injectedNamesToPlugins) {
+ var isOrderingDirty = false;
+ for (var pluginName in injectedNamesToPlugins) {
+ if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {
+ continue;
+ }
+ var PluginModule = injectedNamesToPlugins[pluginName];
+ if (!namesToPlugins.hasOwnProperty(pluginName) || namesToPlugins[pluginName] !== PluginModule) {
+ !!namesToPlugins[pluginName] ? "production" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject two different event plugins ' + 'using the same name, `%s`.', pluginName) : invariant(false) : undefined;
+ namesToPlugins[pluginName] = PluginModule;
+ isOrderingDirty = true;
+ }
+ }
+ if (isOrderingDirty) {
+ recomputePluginOrdering();
+ }
+ },
+
+ /**
+ * Looks up the plugin for the supplied event.
+ *
+ * @param {object} event A synthetic event.
+ * @return {?object} The plugin that created the supplied event.
+ * @internal
+ */
+ getPluginModuleForEvent: function (event) {
+ var dispatchConfig = event.dispatchConfig;
+ if (dispatchConfig.registrationName) {
+ return EventPluginRegistry.registrationNameModules[dispatchConfig.registrationName] || null;
+ }
+ for (var phase in dispatchConfig.phasedRegistrationNames) {
+ if (!dispatchConfig.phasedRegistrationNames.hasOwnProperty(phase)) {
+ continue;
+ }
+ var PluginModule = EventPluginRegistry.registrationNameModules[dispatchConfig.phasedRegistrationNames[phase]];
+ if (PluginModule) {
+ return PluginModule;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Exposed for unit testing.
+ * @private
+ */
+ _resetEventPlugins: function () {
+ EventPluginOrder = null;
+ for (var pluginName in namesToPlugins) {
+ if (namesToPlugins.hasOwnProperty(pluginName)) {
+ delete namesToPlugins[pluginName];
+ }
+ }
+ EventPluginRegistry.plugins.length = 0;
+
+ var eventNameDispatchConfigs = EventPluginRegistry.eventNameDispatchConfigs;
+ for (var eventName in eventNameDispatchConfigs) {
+ if (eventNameDispatchConfigs.hasOwnProperty(eventName)) {
+ delete eventNameDispatchConfigs[eventName];
+ }
+ }
+
+ var registrationNameModules = EventPluginRegistry.registrationNameModules;
+ for (var registrationName in registrationNameModules) {
+ if (registrationNameModules.hasOwnProperty(registrationName)) {
+ delete registrationNameModules[registrationName];
+ }
+ }
+ }
+
+};
+
+module.exports = EventPluginRegistry;
+},{"161":161}],18:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPluginUtils
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var ReactErrorUtils = _dereq_(61);
+
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Injected dependencies:
+ */
+
+/**
+ * - `Mount`: [required] Module that can convert between React dom IDs and
+ * actual node references.
+ */
+var injection = {
+ Mount: null,
+ injectMount: function (InjectedMount) {
+ injection.Mount = InjectedMount;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(InjectedMount && InjectedMount.getNode && InjectedMount.getID, 'EventPluginUtils.injection.injectMount(...): Injected Mount ' + 'module is missing getNode or getID.') : undefined;
+ }
+ }
+};
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+function isEndish(topLevelType) {
+ return topLevelType === topLevelTypes.topMouseUp || topLevelType === topLevelTypes.topTouchEnd || topLevelType === topLevelTypes.topTouchCancel;
+}
+
+function isMoveish(topLevelType) {
+ return topLevelType === topLevelTypes.topMouseMove || topLevelType === topLevelTypes.topTouchMove;
+}
+function isStartish(topLevelType) {
+ return topLevelType === topLevelTypes.topMouseDown || topLevelType === topLevelTypes.topTouchStart;
+}
+
+var validateEventDispatches;
+if ("production" !== 'production') {
+ validateEventDispatches = function (event) {
+ var dispatchListeners = event._dispatchListeners;
+ var dispatchIDs = event._dispatchIDs;
+
+ var listenersIsArr = Array.isArray(dispatchListeners);
+ var idsIsArr = Array.isArray(dispatchIDs);
+ var IDsLen = idsIsArr ? dispatchIDs.length : dispatchIDs ? 1 : 0;
+ var listenersLen = listenersIsArr ? dispatchListeners.length : dispatchListeners ? 1 : 0;
+
+ "production" !== 'production' ? warning(idsIsArr === listenersIsArr && IDsLen === listenersLen, 'EventPluginUtils: Invalid `event`.') : undefined;
+ };
+}
+
+/**
+ * Dispatch the event to the listener.
+ * @param {SyntheticEvent} event SyntheticEvent to handle
+ * @param {boolean} simulated If the event is simulated (changes exn behavior)
+ * @param {function} listener Application-level callback
+ * @param {string} domID DOM id to pass to the callback.
+ */
+function executeDispatch(event, simulated, listener, domID) {
+ var type = event.type || 'unknown-event';
+ event.currentTarget = injection.Mount.getNode(domID);
+ if (simulated) {
+ ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event, domID);
+ } else {
+ ReactErrorUtils.invokeGuardedCallback(type, listener, event, domID);
+ }
+ event.currentTarget = null;
+}
+
+/**
+ * Standard/simple iteration through an event's collected dispatches.
+ */
+function executeDispatchesInOrder(event, simulated) {
+ var dispatchListeners = event._dispatchListeners;
+ var dispatchIDs = event._dispatchIDs;
+ if ("production" !== 'production') {
+ validateEventDispatches(event);
+ }
+ if (Array.isArray(dispatchListeners)) {
+ for (var i = 0; i < dispatchListeners.length; i++) {
+ if (event.isPropagationStopped()) {
+ break;
+ }
+ // Listeners and IDs are two parallel arrays that are always in sync.
+ executeDispatch(event, simulated, dispatchListeners[i], dispatchIDs[i]);
+ }
+ } else if (dispatchListeners) {
+ executeDispatch(event, simulated, dispatchListeners, dispatchIDs);
+ }
+ event._dispatchListeners = null;
+ event._dispatchIDs = null;
+}
+
+/**
+ * Standard/simple iteration through an event's collected dispatches, but stops
+ * at the first dispatch execution returning true, and returns that id.
+ *
+ * @return {?string} id of the first dispatch execution who's listener returns
+ * true, or null if no listener returned true.
+ */
+function executeDispatchesInOrderStopAtTrueImpl(event) {
+ var dispatchListeners = event._dispatchListeners;
+ var dispatchIDs = event._dispatchIDs;
+ if ("production" !== 'production') {
+ validateEventDispatches(event);
+ }
+ if (Array.isArray(dispatchListeners)) {
+ for (var i = 0; i < dispatchListeners.length; i++) {
+ if (event.isPropagationStopped()) {
+ break;
+ }
+ // Listeners and IDs are two parallel arrays that are always in sync.
+ if (dispatchListeners[i](event, dispatchIDs[i])) {
+ return dispatchIDs[i];
+ }
+ }
+ } else if (dispatchListeners) {
+ if (dispatchListeners(event, dispatchIDs)) {
+ return dispatchIDs;
+ }
+ }
+ return null;
+}
+
+/**
+ * @see executeDispatchesInOrderStopAtTrueImpl
+ */
+function executeDispatchesInOrderStopAtTrue(event) {
+ var ret = executeDispatchesInOrderStopAtTrueImpl(event);
+ event._dispatchIDs = null;
+ event._dispatchListeners = null;
+ return ret;
+}
+
+/**
+ * Execution of a "direct" dispatch - there must be at most one dispatch
+ * accumulated on the event or it is considered an error. It doesn't really make
+ * sense for an event with multiple dispatches (bubbled) to keep track of the
+ * return values at each dispatch execution, but it does tend to make sense when
+ * dealing with "direct" dispatches.
+ *
+ * @return {*} The return value of executing the single dispatch.
+ */
+function executeDirectDispatch(event) {
+ if ("production" !== 'production') {
+ validateEventDispatches(event);
+ }
+ var dispatchListener = event._dispatchListeners;
+ var dispatchID = event._dispatchIDs;
+ !!Array.isArray(dispatchListener) ? "production" !== 'production' ? invariant(false, 'executeDirectDispatch(...): Invalid `event`.') : invariant(false) : undefined;
+ var res = dispatchListener ? dispatchListener(event, dispatchID) : null;
+ event._dispatchListeners = null;
+ event._dispatchIDs = null;
+ return res;
+}
+
+/**
+ * @param {SyntheticEvent} event
+ * @return {boolean} True iff number of dispatches accumulated is greater than 0.
+ */
+function hasDispatches(event) {
+ return !!event._dispatchListeners;
+}
+
+/**
+ * General utilities that are useful in creating custom Event Plugins.
+ */
+var EventPluginUtils = {
+ isEndish: isEndish,
+ isMoveish: isMoveish,
+ isStartish: isStartish,
+
+ executeDirectDispatch: executeDirectDispatch,
+ executeDispatchesInOrder: executeDispatchesInOrder,
+ executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue,
+ hasDispatches: hasDispatches,
+
+ getNode: function (id) {
+ return injection.Mount.getNode(id);
+ },
+ getID: function (node) {
+ return injection.Mount.getID(node);
+ },
+
+ injection: injection
+};
+
+module.exports = EventPluginUtils;
+},{"15":15,"161":161,"173":173,"61":61}],19:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EventPropagators
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+
+var warning = _dereq_(173);
+
+var accumulateInto = _dereq_(115);
+var forEachAccumulated = _dereq_(124);
+
+var PropagationPhases = EventConstants.PropagationPhases;
+var getListener = EventPluginHub.getListener;
+
+/**
+ * Some event types have a notion of different registration names for different
+ * "phases" of propagation. This finds listeners by a given phase.
+ */
+function listenerAtPhase(id, event, propagationPhase) {
+ var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
+ return getListener(id, registrationName);
+}
+
+/**
+ * Tags a `SyntheticEvent` with dispatched listeners. Creating this function
+ * here, allows us to not have to bind or create functions for each event.
+ * Mutating the event's members allows us to not have to create a wrapping
+ * "dispatch" object that pairs the event with the listener.
+ */
+function accumulateDirectionalDispatches(domID, upwards, event) {
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(domID, 'Dispatching id must not be null') : undefined;
+ }
+ var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;
+ var listener = listenerAtPhase(domID, event, phase);
+ if (listener) {
+ event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
+ event._dispatchIDs = accumulateInto(event._dispatchIDs, domID);
+ }
+}
+
+/**
+ * Collect dispatches (must be entirely collected before dispatching - see unit
+ * tests). Lazily allocate the array to conserve memory. We must loop through
+ * each event and perform the traversal for each one. We cannot perform a
+ * single traversal for the entire collection of events because each event may
+ * have a different target.
+ */
+function accumulateTwoPhaseDispatchesSingle(event) {
+ if (event && event.dispatchConfig.phasedRegistrationNames) {
+ EventPluginHub.injection.getInstanceHandle().traverseTwoPhase(event.dispatchMarker, accumulateDirectionalDispatches, event);
+ }
+}
+
+/**
+ * Same as `accumulateTwoPhaseDispatchesSingle`, but skips over the targetID.
+ */
+function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
+ if (event && event.dispatchConfig.phasedRegistrationNames) {
+ EventPluginHub.injection.getInstanceHandle().traverseTwoPhaseSkipTarget(event.dispatchMarker, accumulateDirectionalDispatches, event);
+ }
+}
+
+/**
+ * Accumulates without regard to direction, does not look for phased
+ * registration names. Same as `accumulateDirectDispatchesSingle` but without
+ * requiring that the `dispatchMarker` be the same as the dispatched ID.
+ */
+function accumulateDispatches(id, ignoredDirection, event) {
+ if (event && event.dispatchConfig.registrationName) {
+ var registrationName = event.dispatchConfig.registrationName;
+ var listener = getListener(id, registrationName);
+ if (listener) {
+ event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
+ event._dispatchIDs = accumulateInto(event._dispatchIDs, id);
+ }
+ }
+}
+
+/**
+ * Accumulates dispatches on an `SyntheticEvent`, but only for the
+ * `dispatchMarker`.
+ * @param {SyntheticEvent} event
+ */
+function accumulateDirectDispatchesSingle(event) {
+ if (event && event.dispatchConfig.registrationName) {
+ accumulateDispatches(event.dispatchMarker, null, event);
+ }
+}
+
+function accumulateTwoPhaseDispatches(events) {
+ forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
+}
+
+function accumulateTwoPhaseDispatchesSkipTarget(events) {
+ forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget);
+}
+
+function accumulateEnterLeaveDispatches(leave, enter, fromID, toID) {
+ EventPluginHub.injection.getInstanceHandle().traverseEnterLeave(fromID, toID, accumulateDispatches, leave, enter);
+}
+
+function accumulateDirectDispatches(events) {
+ forEachAccumulated(events, accumulateDirectDispatchesSingle);
+}
+
+/**
+ * A small set of propagation patterns, each of which will accept a small amount
+ * of information, and generate a set of "dispatch ready event objects" - which
+ * are sets of events that have already been annotated with a set of dispatched
+ * listener functions/ids. The API is designed this way to discourage these
+ * propagation strategies from actually executing the dispatches, since we
+ * always want to collect the entire set of dispatches before executing event a
+ * single one.
+ *
+ * @constructor EventPropagators
+ */
+var EventPropagators = {
+ accumulateTwoPhaseDispatches: accumulateTwoPhaseDispatches,
+ accumulateTwoPhaseDispatchesSkipTarget: accumulateTwoPhaseDispatchesSkipTarget,
+ accumulateDirectDispatches: accumulateDirectDispatches,
+ accumulateEnterLeaveDispatches: accumulateEnterLeaveDispatches
+};
+
+module.exports = EventPropagators;
+},{"115":115,"124":124,"15":15,"16":16,"173":173}],20:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule FallbackCompositionState
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+
+var assign = _dereq_(24);
+var getTextContentAccessor = _dereq_(131);
+
+/**
+ * This helper class stores information about text content of a target node,
+ * allowing comparison of content before and after a given event.
+ *
+ * Identify the node where selection currently begins, then observe
+ * both its text content and its current position in the DOM. Since the
+ * browser may natively replace the target node during composition, we can
+ * use its position to find its replacement.
+ *
+ * @param {DOMEventTarget} root
+ */
+function FallbackCompositionState(root) {
+ this._root = root;
+ this._startText = this.getText();
+ this._fallbackText = null;
+}
+
+assign(FallbackCompositionState.prototype, {
+ destructor: function () {
+ this._root = null;
+ this._startText = null;
+ this._fallbackText = null;
+ },
+
+ /**
+ * Get current text of input.
+ *
+ * @return {string}
+ */
+ getText: function () {
+ if ('value' in this._root) {
+ return this._root.value;
+ }
+ return this._root[getTextContentAccessor()];
+ },
+
+ /**
+ * Determine the differing substring between the initially stored
+ * text content and the current content.
+ *
+ * @return {string}
+ */
+ getData: function () {
+ if (this._fallbackText) {
+ return this._fallbackText;
+ }
+
+ var start;
+ var startValue = this._startText;
+ var startLength = startValue.length;
+ var end;
+ var endValue = this.getText();
+ var endLength = endValue.length;
+
+ for (start = 0; start < startLength; start++) {
+ if (startValue[start] !== endValue[start]) {
+ break;
+ }
+ }
+
+ var minEnd = startLength - start;
+ for (end = 1; end <= minEnd; end++) {
+ if (startValue[startLength - end] !== endValue[endLength - end]) {
+ break;
+ }
+ }
+
+ var sliceTail = end > 1 ? 1 - end : undefined;
+ this._fallbackText = endValue.slice(start, sliceTail);
+ return this._fallbackText;
+ }
+});
+
+PooledClass.addPoolingTo(FallbackCompositionState);
+
+module.exports = FallbackCompositionState;
+},{"131":131,"24":24,"25":25}],21:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule HTMLDOMPropertyConfig
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ExecutionEnvironment = _dereq_(147);
+
+var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
+var MUST_USE_PROPERTY = DOMProperty.injection.MUST_USE_PROPERTY;
+var HAS_BOOLEAN_VALUE = DOMProperty.injection.HAS_BOOLEAN_VALUE;
+var HAS_SIDE_EFFECTS = DOMProperty.injection.HAS_SIDE_EFFECTS;
+var HAS_NUMERIC_VALUE = DOMProperty.injection.HAS_NUMERIC_VALUE;
+var HAS_POSITIVE_NUMERIC_VALUE = DOMProperty.injection.HAS_POSITIVE_NUMERIC_VALUE;
+var HAS_OVERLOADED_BOOLEAN_VALUE = DOMProperty.injection.HAS_OVERLOADED_BOOLEAN_VALUE;
+
+var hasSVG;
+if (ExecutionEnvironment.canUseDOM) {
+ var implementation = document.implementation;
+ hasSVG = implementation && implementation.hasFeature && implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1');
+}
+
+var HTMLDOMPropertyConfig = {
+ isCustomAttribute: RegExp.prototype.test.bind(/^(data|aria)-[a-z_][a-z\d_.\-]*$/),
+ Properties: {
+ /**
+ * Standard Properties
+ */
+ accept: null,
+ acceptCharset: null,
+ accessKey: null,
+ action: null,
+ allowFullScreen: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ allowTransparency: MUST_USE_ATTRIBUTE,
+ alt: null,
+ async: HAS_BOOLEAN_VALUE,
+ autoComplete: null,
+ // autoFocus is polyfilled/normalized by AutoFocusUtils
+ // autoFocus: HAS_BOOLEAN_VALUE,
+ autoPlay: HAS_BOOLEAN_VALUE,
+ capture: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ cellPadding: null,
+ cellSpacing: null,
+ charSet: MUST_USE_ATTRIBUTE,
+ challenge: MUST_USE_ATTRIBUTE,
+ checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ classID: MUST_USE_ATTRIBUTE,
+ // To set className on SVG elements, it's necessary to use .setAttribute;
+ // this works on HTML elements too in all browsers except IE8. Conveniently,
+ // IE8 doesn't support SVG and so we can simply use the attribute in
+ // browsers that support SVG and the property in browsers that don't,
+ // regardless of whether the element is HTML or SVG.
+ className: hasSVG ? MUST_USE_ATTRIBUTE : MUST_USE_PROPERTY,
+ cols: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
+ colSpan: null,
+ content: null,
+ contentEditable: null,
+ contextMenu: MUST_USE_ATTRIBUTE,
+ controls: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ coords: null,
+ crossOrigin: null,
+ data: null, // For `<object />` acts as `src`.
+ dateTime: MUST_USE_ATTRIBUTE,
+ 'default': HAS_BOOLEAN_VALUE,
+ defer: HAS_BOOLEAN_VALUE,
+ dir: null,
+ disabled: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ download: HAS_OVERLOADED_BOOLEAN_VALUE,
+ draggable: null,
+ encType: null,
+ form: MUST_USE_ATTRIBUTE,
+ formAction: MUST_USE_ATTRIBUTE,
+ formEncType: MUST_USE_ATTRIBUTE,
+ formMethod: MUST_USE_ATTRIBUTE,
+ formNoValidate: HAS_BOOLEAN_VALUE,
+ formTarget: MUST_USE_ATTRIBUTE,
+ frameBorder: MUST_USE_ATTRIBUTE,
+ headers: null,
+ height: MUST_USE_ATTRIBUTE,
+ hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ high: null,
+ href: null,
+ hrefLang: null,
+ htmlFor: null,
+ httpEquiv: null,
+ icon: null,
+ id: MUST_USE_PROPERTY,
+ inputMode: MUST_USE_ATTRIBUTE,
+ integrity: null,
+ is: MUST_USE_ATTRIBUTE,
+ keyParams: MUST_USE_ATTRIBUTE,
+ keyType: MUST_USE_ATTRIBUTE,
+ kind: null,
+ label: null,
+ lang: null,
+ list: MUST_USE_ATTRIBUTE,
+ loop: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ low: null,
+ manifest: MUST_USE_ATTRIBUTE,
+ marginHeight: null,
+ marginWidth: null,
+ max: null,
+ maxLength: MUST_USE_ATTRIBUTE,
+ media: MUST_USE_ATTRIBUTE,
+ mediaGroup: null,
+ method: null,
+ min: null,
+ minLength: MUST_USE_ATTRIBUTE,
+ multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ name: null,
+ nonce: MUST_USE_ATTRIBUTE,
+ noValidate: HAS_BOOLEAN_VALUE,
+ open: HAS_BOOLEAN_VALUE,
+ optimum: null,
+ pattern: null,
+ placeholder: null,
+ poster: null,
+ preload: null,
+ radioGroup: null,
+ readOnly: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ rel: null,
+ required: HAS_BOOLEAN_VALUE,
+ reversed: HAS_BOOLEAN_VALUE,
+ role: MUST_USE_ATTRIBUTE,
+ rows: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
+ rowSpan: null,
+ sandbox: null,
+ scope: null,
+ scoped: HAS_BOOLEAN_VALUE,
+ scrolling: null,
+ seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
+ shape: null,
+ size: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
+ sizes: MUST_USE_ATTRIBUTE,
+ span: HAS_POSITIVE_NUMERIC_VALUE,
+ spellCheck: null,
+ src: null,
+ srcDoc: MUST_USE_PROPERTY,
+ srcLang: null,
+ srcSet: MUST_USE_ATTRIBUTE,
+ start: HAS_NUMERIC_VALUE,
+ step: null,
+ style: null,
+ summary: null,
+ tabIndex: null,
+ target: null,
+ title: null,
+ type: null,
+ useMap: null,
+ value: MUST_USE_PROPERTY | HAS_SIDE_EFFECTS,
+ width: MUST_USE_ATTRIBUTE,
+ wmode: MUST_USE_ATTRIBUTE,
+ wrap: null,
+
+ /**
+ * RDFa Properties
+ */
+ about: MUST_USE_ATTRIBUTE,
+ datatype: MUST_USE_ATTRIBUTE,
+ inlist: MUST_USE_ATTRIBUTE,
+ prefix: MUST_USE_ATTRIBUTE,
+ // property is also supported for OpenGraph in meta tags.
+ property: MUST_USE_ATTRIBUTE,
+ resource: MUST_USE_ATTRIBUTE,
+ 'typeof': MUST_USE_ATTRIBUTE,
+ vocab: MUST_USE_ATTRIBUTE,
+
+ /**
+ * Non-standard Properties
+ */
+ // autoCapitalize and autoCorrect are supported in Mobile Safari for
+ // keyboard hints.
+ autoCapitalize: MUST_USE_ATTRIBUTE,
+ autoCorrect: MUST_USE_ATTRIBUTE,
+ // autoSave allows WebKit/Blink to persist values of input fields on page reloads
+ autoSave: null,
+ // color is for Safari mask-icon link
+ color: null,
+ // itemProp, itemScope, itemType are for
+ // Microdata support. See http://schema.org/docs/gs.html
+ itemProp: MUST_USE_ATTRIBUTE,
+ itemScope: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
+ itemType: MUST_USE_ATTRIBUTE,
+ // itemID and itemRef are for Microdata support as well but
+ // only specified in the the WHATWG spec document. See
+ // https://html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api
+ itemID: MUST_USE_ATTRIBUTE,
+ itemRef: MUST_USE_ATTRIBUTE,
+ // results show looking glass icon and recent searches on input
+ // search fields in WebKit/Blink
+ results: null,
+ // IE-only attribute that specifies security restrictions on an iframe
+ // as an alternative to the sandbox attribute on IE<10
+ security: MUST_USE_ATTRIBUTE,
+ // IE-only attribute that controls focus behavior
+ unselectable: MUST_USE_ATTRIBUTE
+ },
+ DOMAttributeNames: {
+ acceptCharset: 'accept-charset',
+ className: 'class',
+ htmlFor: 'for',
+ httpEquiv: 'http-equiv'
+ },
+ DOMPropertyNames: {
+ autoComplete: 'autocomplete',
+ autoFocus: 'autofocus',
+ autoPlay: 'autoplay',
+ autoSave: 'autosave',
+ // `encoding` is equivalent to `enctype`, IE8 lacks an `enctype` setter.
+ // http://www.w3.org/TR/html5/forms.html#dom-fs-encoding
+ encType: 'encoding',
+ hrefLang: 'hreflang',
+ radioGroup: 'radiogroup',
+ spellCheck: 'spellcheck',
+ srcDoc: 'srcdoc',
+ srcSet: 'srcset'
+ }
+};
+
+module.exports = HTMLDOMPropertyConfig;
+},{"10":10,"147":147}],22:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule LinkedStateMixin
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactLink = _dereq_(70);
+var ReactStateSetters = _dereq_(90);
+
+/**
+ * A simple mixin around ReactLink.forState().
+ */
+var LinkedStateMixin = {
+ /**
+ * Create a ReactLink that's linked to part of this component's state. The
+ * ReactLink will have the current value of this.state[key] and will call
+ * setState() when a change is requested.
+ *
+ * @param {string} key state key to update. Note: you may want to use keyOf()
+ * if you're using Google Closure Compiler advanced mode.
+ * @return {ReactLink} ReactLink instance linking to the state.
+ */
+ linkState: function (key) {
+ return new ReactLink(this.state[key], ReactStateSetters.createStateKeySetter(this, key));
+ }
+};
+
+module.exports = LinkedStateMixin;
+},{"70":70,"90":90}],23:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule LinkedValueUtils
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactPropTypes = _dereq_(82);
+var ReactPropTypeLocations = _dereq_(81);
+
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+var hasReadOnlyValue = {
+ 'button': true,
+ 'checkbox': true,
+ 'image': true,
+ 'hidden': true,
+ 'radio': true,
+ 'reset': true,
+ 'submit': true
+};
+
+function _assertSingleLink(inputProps) {
+ !(inputProps.checkedLink == null || inputProps.valueLink == null) ? "production" !== 'production' ? invariant(false, 'Cannot provide a checkedLink and a valueLink. If you want to use ' + 'checkedLink, you probably don\'t want to use valueLink and vice versa.') : invariant(false) : undefined;
+}
+function _assertValueLink(inputProps) {
+ _assertSingleLink(inputProps);
+ !(inputProps.value == null && inputProps.onChange == null) ? "production" !== 'production' ? invariant(false, 'Cannot provide a valueLink and a value or onChange event. If you want ' + 'to use value or onChange, you probably don\'t want to use valueLink.') : invariant(false) : undefined;
+}
+
+function _assertCheckedLink(inputProps) {
+ _assertSingleLink(inputProps);
+ !(inputProps.checked == null && inputProps.onChange == null) ? "production" !== 'production' ? invariant(false, 'Cannot provide a checkedLink and a checked property or onChange event. ' + 'If you want to use checked or onChange, you probably don\'t want to ' + 'use checkedLink') : invariant(false) : undefined;
+}
+
+var propTypes = {
+ value: function (props, propName, componentName) {
+ if (!props[propName] || hasReadOnlyValue[props.type] || props.onChange || props.readOnly || props.disabled) {
+ return null;
+ }
+ return new Error('You provided a `value` prop to a form field without an ' + '`onChange` handler. This will render a read-only field. If ' + 'the field should be mutable use `defaultValue`. Otherwise, ' + 'set either `onChange` or `readOnly`.');
+ },
+ checked: function (props, propName, componentName) {
+ if (!props[propName] || props.onChange || props.readOnly || props.disabled) {
+ return null;
+ }
+ return new Error('You provided a `checked` prop to a form field without an ' + '`onChange` handler. This will render a read-only field. If ' + 'the field should be mutable use `defaultChecked`. Otherwise, ' + 'set either `onChange` or `readOnly`.');
+ },
+ onChange: ReactPropTypes.func
+};
+
+var loggedTypeFailures = {};
+function getDeclarationErrorAddendum(owner) {
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+/**
+ * Provide a linked `value` attribute for controlled forms. You should not use
+ * this outside of the ReactDOM controlled form components.
+ */
+var LinkedValueUtils = {
+ checkPropTypes: function (tagName, props, owner) {
+ for (var propName in propTypes) {
+ if (propTypes.hasOwnProperty(propName)) {
+ var error = propTypes[propName](props, propName, tagName, ReactPropTypeLocations.prop);
+ }
+ if (error instanceof Error && !(error.message in loggedTypeFailures)) {
+ // Only monitor this failure once because there tends to be a lot of the
+ // same error.
+ loggedTypeFailures[error.message] = true;
+
+ var addendum = getDeclarationErrorAddendum(owner);
+ "production" !== 'production' ? warning(false, 'Failed form propType: %s%s', error.message, addendum) : undefined;
+ }
+ }
+ },
+
+ /**
+ * @param {object} inputProps Props for form component
+ * @return {*} current value of the input either from value prop or link.
+ */
+ getValue: function (inputProps) {
+ if (inputProps.valueLink) {
+ _assertValueLink(inputProps);
+ return inputProps.valueLink.value;
+ }
+ return inputProps.value;
+ },
+
+ /**
+ * @param {object} inputProps Props for form component
+ * @return {*} current checked status of the input either from checked prop
+ * or link.
+ */
+ getChecked: function (inputProps) {
+ if (inputProps.checkedLink) {
+ _assertCheckedLink(inputProps);
+ return inputProps.checkedLink.value;
+ }
+ return inputProps.checked;
+ },
+
+ /**
+ * @param {object} inputProps Props for form component
+ * @param {SyntheticEvent} event change event to handle
+ */
+ executeOnChange: function (inputProps, event) {
+ if (inputProps.valueLink) {
+ _assertValueLink(inputProps);
+ return inputProps.valueLink.requestChange(event.target.value);
+ } else if (inputProps.checkedLink) {
+ _assertCheckedLink(inputProps);
+ return inputProps.checkedLink.requestChange(event.target.checked);
+ } else if (inputProps.onChange) {
+ return inputProps.onChange.call(undefined, event);
+ }
+ }
+};
+
+module.exports = LinkedValueUtils;
+},{"161":161,"173":173,"81":81,"82":82}],24:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Object.assign
+ */
+
+// https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.assign
+
+'use strict';
+
+function assign(target, sources) {
+ if (target == null) {
+ throw new TypeError('Object.assign target cannot be null or undefined');
+ }
+
+ var to = Object(target);
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+ for (var nextIndex = 1; nextIndex < arguments.length; nextIndex++) {
+ var nextSource = arguments[nextIndex];
+ if (nextSource == null) {
+ continue;
+ }
+
+ var from = Object(nextSource);
+
+ // We don't currently support accessors nor proxies. Therefore this
+ // copy cannot throw. If we ever supported this then we must handle
+ // exceptions and side-effects. We don't support symbols so they won't
+ // be transferred.
+
+ for (var key in from) {
+ if (hasOwnProperty.call(from, key)) {
+ to[key] = from[key];
+ }
+ }
+ }
+
+ return to;
+}
+
+module.exports = assign;
+},{}],25:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule PooledClass
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Static poolers. Several custom versions for each potential number of
+ * arguments. A completely generic pooler is easy to implement, but would
+ * require accessing the `arguments` object. In each of these, `this` refers to
+ * the Class itself, not an instance. If any others are needed, simply add them
+ * here, or in their own files.
+ */
+var oneArgumentPooler = function (copyFieldsFrom) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, copyFieldsFrom);
+ return instance;
+ } else {
+ return new Klass(copyFieldsFrom);
+ }
+};
+
+var twoArgumentPooler = function (a1, a2) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2);
+ return instance;
+ } else {
+ return new Klass(a1, a2);
+ }
+};
+
+var threeArgumentPooler = function (a1, a2, a3) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2, a3);
+ return instance;
+ } else {
+ return new Klass(a1, a2, a3);
+ }
+};
+
+var fourArgumentPooler = function (a1, a2, a3, a4) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2, a3, a4);
+ return instance;
+ } else {
+ return new Klass(a1, a2, a3, a4);
+ }
+};
+
+var fiveArgumentPooler = function (a1, a2, a3, a4, a5) {
+ var Klass = this;
+ if (Klass.instancePool.length) {
+ var instance = Klass.instancePool.pop();
+ Klass.call(instance, a1, a2, a3, a4, a5);
+ return instance;
+ } else {
+ return new Klass(a1, a2, a3, a4, a5);
+ }
+};
+
+var standardReleaser = function (instance) {
+ var Klass = this;
+ !(instance instanceof Klass) ? "production" !== 'production' ? invariant(false, 'Trying to release an instance into a pool of a different type.') : invariant(false) : undefined;
+ instance.destructor();
+ if (Klass.instancePool.length < Klass.poolSize) {
+ Klass.instancePool.push(instance);
+ }
+};
+
+var DEFAULT_POOL_SIZE = 10;
+var DEFAULT_POOLER = oneArgumentPooler;
+
+/**
+ * Augments `CopyConstructor` to be a poolable class, augmenting only the class
+ * itself (statically) not adding any prototypical fields. Any CopyConstructor
+ * you give this may have a `poolSize` property, and will look for a
+ * prototypical `destructor` on instances (optional).
+ *
+ * @param {Function} CopyConstructor Constructor that can be used to reset.
+ * @param {Function} pooler Customizable pooler.
+ */
+var addPoolingTo = function (CopyConstructor, pooler) {
+ var NewKlass = CopyConstructor;
+ NewKlass.instancePool = [];
+ NewKlass.getPooled = pooler || DEFAULT_POOLER;
+ if (!NewKlass.poolSize) {
+ NewKlass.poolSize = DEFAULT_POOL_SIZE;
+ }
+ NewKlass.release = standardReleaser;
+ return NewKlass;
+};
+
+var PooledClass = {
+ addPoolingTo: addPoolingTo,
+ oneArgumentPooler: oneArgumentPooler,
+ twoArgumentPooler: twoArgumentPooler,
+ threeArgumentPooler: threeArgumentPooler,
+ fourArgumentPooler: fourArgumentPooler,
+ fiveArgumentPooler: fiveArgumentPooler
+};
+
+module.exports = PooledClass;
+},{"161":161}],26:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule React
+ */
+
+'use strict';
+
+var ReactDOM = _dereq_(40);
+var ReactDOMServer = _dereq_(50);
+var ReactIsomorphic = _dereq_(69);
+
+var assign = _dereq_(24);
+var deprecated = _dereq_(120);
+
+// `version` will be added here by ReactIsomorphic.
+var React = {};
+
+assign(React, ReactIsomorphic);
+
+assign(React, {
+ // ReactDOM
+ findDOMNode: deprecated('findDOMNode', 'ReactDOM', 'react-dom', ReactDOM, ReactDOM.findDOMNode),
+ render: deprecated('render', 'ReactDOM', 'react-dom', ReactDOM, ReactDOM.render),
+ unmountComponentAtNode: deprecated('unmountComponentAtNode', 'ReactDOM', 'react-dom', ReactDOM, ReactDOM.unmountComponentAtNode),
+
+ // ReactDOMServer
+ renderToString: deprecated('renderToString', 'ReactDOMServer', 'react-dom/server', ReactDOMServer, ReactDOMServer.renderToString),
+ renderToStaticMarkup: deprecated('renderToStaticMarkup', 'ReactDOMServer', 'react-dom/server', ReactDOMServer, ReactDOMServer.renderToStaticMarkup)
+});
+
+React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactDOM;
+React.__SECRET_DOM_SERVER_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactDOMServer;
+
+module.exports = React;
+},{"120":120,"24":24,"40":40,"50":50,"69":69}],27:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactBrowserComponentMixin
+ */
+
+'use strict';
+
+var ReactInstanceMap = _dereq_(68);
+
+var findDOMNode = _dereq_(122);
+var warning = _dereq_(173);
+
+var didWarnKey = '_getDOMNodeDidWarn';
+
+var ReactBrowserComponentMixin = {
+ /**
+ * Returns the DOM node rendered by this component.
+ *
+ * @return {DOMElement} The root node of this component.
+ * @final
+ * @protected
+ */
+ getDOMNode: function () {
+ "production" !== 'production' ? warning(this.constructor[didWarnKey], '%s.getDOMNode(...) is deprecated. Please use ' + 'ReactDOM.findDOMNode(instance) instead.', ReactInstanceMap.get(this).getName() || this.tagName || 'Unknown') : undefined;
+ this.constructor[didWarnKey] = true;
+ return findDOMNode(this);
+ }
+};
+
+module.exports = ReactBrowserComponentMixin;
+},{"122":122,"173":173,"68":68}],28:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactBrowserEventEmitter
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+var EventPluginRegistry = _dereq_(17);
+var ReactEventEmitterMixin = _dereq_(62);
+var ReactPerf = _dereq_(78);
+var ViewportMetrics = _dereq_(114);
+
+var assign = _dereq_(24);
+var isEventSupported = _dereq_(133);
+
+/**
+ * Summary of `ReactBrowserEventEmitter` event handling:
+ *
+ * - Top-level delegation is used to trap most native browser events. This
+ * may only occur in the main thread and is the responsibility of
+ * ReactEventListener, which is injected and can therefore support pluggable
+ * event sources. This is the only work that occurs in the main thread.
+ *
+ * - We normalize and de-duplicate events to account for browser quirks. This
+ * may be done in the worker thread.
+ *
+ * - Forward these native events (with the associated top-level type used to
+ * trap it) to `EventPluginHub`, which in turn will ask plugins if they want
+ * to extract any synthetic events.
+ *
+ * - The `EventPluginHub` will then process each event by annotating them with
+ * "dispatches", a sequence of listeners and IDs that care about that event.
+ *
+ * - The `EventPluginHub` then dispatches the events.
+ *
+ * Overview of React and the event system:
+ *
+ * +------------+ .
+ * | DOM | .
+ * +------------+ .
+ * | .
+ * v .
+ * +------------+ .
+ * | ReactEvent | .
+ * | Listener | .
+ * +------------+ . +-----------+
+ * | . +--------+|SimpleEvent|
+ * | . | |Plugin |
+ * +-----|------+ . v +-----------+
+ * | | | . +--------------+ +------------+
+ * | +-----------.--->|EventPluginHub| | Event |
+ * | | . | | +-----------+ | Propagators|
+ * | ReactEvent | . | | |TapEvent | |------------|
+ * | Emitter | . | |<---+|Plugin | |other plugin|
+ * | | . | | +-----------+ | utilities |
+ * | +-----------.--->| | +------------+
+ * | | | . +--------------+
+ * +-----|------+ . ^ +-----------+
+ * | . | |Enter/Leave|
+ * + . +-------+|Plugin |
+ * +-------------+ . +-----------+
+ * | application | .
+ * |-------------| .
+ * | | .
+ * | | .
+ * +-------------+ .
+ * .
+ * React Core . General Purpose Event Plugin System
+ */
+
+var alreadyListeningTo = {};
+var isMonitoringScrollValue = false;
+var reactTopListenersCounter = 0;
+
+// For events like 'submit' which don't consistently bubble (which we trap at a
+// lower node than `document`), binding at `document` would cause duplicate
+// events so we don't include them here
+var topEventMapping = {
+ topAbort: 'abort',
+ topBlur: 'blur',
+ topCanPlay: 'canplay',
+ topCanPlayThrough: 'canplaythrough',
+ topChange: 'change',
+ topClick: 'click',
+ topCompositionEnd: 'compositionend',
+ topCompositionStart: 'compositionstart',
+ topCompositionUpdate: 'compositionupdate',
+ topContextMenu: 'contextmenu',
+ topCopy: 'copy',
+ topCut: 'cut',
+ topDoubleClick: 'dblclick',
+ topDrag: 'drag',
+ topDragEnd: 'dragend',
+ topDragEnter: 'dragenter',
+ topDragExit: 'dragexit',
+ topDragLeave: 'dragleave',
+ topDragOver: 'dragover',
+ topDragStart: 'dragstart',
+ topDrop: 'drop',
+ topDurationChange: 'durationchange',
+ topEmptied: 'emptied',
+ topEncrypted: 'encrypted',
+ topEnded: 'ended',
+ topError: 'error',
+ topFocus: 'focus',
+ topInput: 'input',
+ topKeyDown: 'keydown',
+ topKeyPress: 'keypress',
+ topKeyUp: 'keyup',
+ topLoadedData: 'loadeddata',
+ topLoadedMetadata: 'loadedmetadata',
+ topLoadStart: 'loadstart',
+ topMouseDown: 'mousedown',
+ topMouseMove: 'mousemove',
+ topMouseOut: 'mouseout',
+ topMouseOver: 'mouseover',
+ topMouseUp: 'mouseup',
+ topPaste: 'paste',
+ topPause: 'pause',
+ topPlay: 'play',
+ topPlaying: 'playing',
+ topProgress: 'progress',
+ topRateChange: 'ratechange',
+ topScroll: 'scroll',
+ topSeeked: 'seeked',
+ topSeeking: 'seeking',
+ topSelectionChange: 'selectionchange',
+ topStalled: 'stalled',
+ topSuspend: 'suspend',
+ topTextInput: 'textInput',
+ topTimeUpdate: 'timeupdate',
+ topTouchCancel: 'touchcancel',
+ topTouchEnd: 'touchend',
+ topTouchMove: 'touchmove',
+ topTouchStart: 'touchstart',
+ topVolumeChange: 'volumechange',
+ topWaiting: 'waiting',
+ topWheel: 'wheel'
+};
+
+/**
+ * To ensure no conflicts with other potential React instances on the page
+ */
+var topListenersIDKey = '_reactListenersID' + String(Math.random()).slice(2);
+
+function getListeningForDocument(mountAt) {
+ // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`
+ // directly.
+ if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) {
+ mountAt[topListenersIDKey] = reactTopListenersCounter++;
+ alreadyListeningTo[mountAt[topListenersIDKey]] = {};
+ }
+ return alreadyListeningTo[mountAt[topListenersIDKey]];
+}
+
+/**
+ * `ReactBrowserEventEmitter` is used to attach top-level event listeners. For
+ * example:
+ *
+ * ReactBrowserEventEmitter.putListener('myID', 'onClick', myFunction);
+ *
+ * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
+ *
+ * @internal
+ */
+var ReactBrowserEventEmitter = assign({}, ReactEventEmitterMixin, {
+
+ /**
+ * Injectable event backend
+ */
+ ReactEventListener: null,
+
+ injection: {
+ /**
+ * @param {object} ReactEventListener
+ */
+ injectReactEventListener: function (ReactEventListener) {
+ ReactEventListener.setHandleTopLevel(ReactBrowserEventEmitter.handleTopLevel);
+ ReactBrowserEventEmitter.ReactEventListener = ReactEventListener;
+ }
+ },
+
+ /**
+ * Sets whether or not any created callbacks should be enabled.
+ *
+ * @param {boolean} enabled True if callbacks should be enabled.
+ */
+ setEnabled: function (enabled) {
+ if (ReactBrowserEventEmitter.ReactEventListener) {
+ ReactBrowserEventEmitter.ReactEventListener.setEnabled(enabled);
+ }
+ },
+
+ /**
+ * @return {boolean} True if callbacks are enabled.
+ */
+ isEnabled: function () {
+ return !!(ReactBrowserEventEmitter.ReactEventListener && ReactBrowserEventEmitter.ReactEventListener.isEnabled());
+ },
+
+ /**
+ * We listen for bubbled touch events on the document object.
+ *
+ * Firefox v8.01 (and possibly others) exhibited strange behavior when
+ * mounting `onmousemove` events at some node that was not the document
+ * element. The symptoms were that if your mouse is not moving over something
+ * contained within that mount point (for example on the background) the
+ * top-level listeners for `onmousemove` won't be called. However, if you
+ * register the `mousemove` on the document object, then it will of course
+ * catch all `mousemove`s. This along with iOS quirks, justifies restricting
+ * top-level listeners to the document object only, at least for these
+ * movement types of events and possibly all events.
+ *
+ * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
+ *
+ * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
+ * they bubble to document.
+ *
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
+ * @param {object} contentDocumentHandle Document which owns the container
+ */
+ listenTo: function (registrationName, contentDocumentHandle) {
+ var mountAt = contentDocumentHandle;
+ var isListening = getListeningForDocument(mountAt);
+ var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];
+
+ var topLevelTypes = EventConstants.topLevelTypes;
+ for (var i = 0; i < dependencies.length; i++) {
+ var dependency = dependencies[i];
+ if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
+ if (dependency === topLevelTypes.topWheel) {
+ if (isEventSupported('wheel')) {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt);
+ } else if (isEventSupported('mousewheel')) {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt);
+ } else {
+ // Firefox needs to capture a different mouse scroll event.
+ // @see http://www.quirksmode.org/dom/events/tests/scroll.html
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt);
+ }
+ } else if (dependency === topLevelTypes.topScroll) {
+
+ if (isEventSupported('scroll', true)) {
+ ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt);
+ } else {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topScroll, 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE);
+ }
+ } else if (dependency === topLevelTypes.topFocus || dependency === topLevelTypes.topBlur) {
+
+ if (isEventSupported('focus', true)) {
+ ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt);
+ ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt);
+ } else if (isEventSupported('focusin')) {
+ // IE has `focusin` and `focusout` events which bubble.
+ // @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt);
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt);
+ }
+
+ // to make sure blur and focus event listeners are only attached once
+ isListening[topLevelTypes.topBlur] = true;
+ isListening[topLevelTypes.topFocus] = true;
+ } else if (topEventMapping.hasOwnProperty(dependency)) {
+ ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
+ }
+
+ isListening[dependency] = true;
+ }
+ }
+ },
+
+ trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
+ return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelType, handlerBaseName, handle);
+ },
+
+ trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
+ return ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelType, handlerBaseName, handle);
+ },
+
+ /**
+ * Listens to window scroll and resize events. We cache scroll values so that
+ * application code can access them without triggering reflows.
+ *
+ * NOTE: Scroll events do not bubble.
+ *
+ * @see http://www.quirksmode.org/dom/events/scroll.html
+ */
+ ensureScrollValueMonitoring: function () {
+ if (!isMonitoringScrollValue) {
+ var refresh = ViewportMetrics.refreshScrollValues;
+ ReactBrowserEventEmitter.ReactEventListener.monitorScrollValue(refresh);
+ isMonitoringScrollValue = true;
+ }
+ },
+
+ eventNameDispatchConfigs: EventPluginHub.eventNameDispatchConfigs,
+
+ registrationNameModules: EventPluginHub.registrationNameModules,
+
+ putListener: EventPluginHub.putListener,
+
+ getListener: EventPluginHub.getListener,
+
+ deleteListener: EventPluginHub.deleteListener,
+
+ deleteAllListeners: EventPluginHub.deleteAllListeners
+
+});
+
+ReactPerf.measureMethods(ReactBrowserEventEmitter, 'ReactBrowserEventEmitter', {
+ putListener: 'putListener',
+ deleteListener: 'deleteListener'
+});
+
+module.exports = ReactBrowserEventEmitter;
+},{"114":114,"133":133,"15":15,"16":16,"17":17,"24":24,"62":62,"78":78}],29:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks
+ * @providesModule ReactCSSTransitionGroup
+ */
+
+'use strict';
+
+var React = _dereq_(26);
+
+var assign = _dereq_(24);
+
+var ReactTransitionGroup = _dereq_(94);
+var ReactCSSTransitionGroupChild = _dereq_(30);
+
+function createTransitionTimeoutPropValidator(transitionType) {
+ var timeoutPropName = 'transition' + transitionType + 'Timeout';
+ var enabledPropName = 'transition' + transitionType;
+
+ return function (props) {
+ // If the transition is enabled
+ if (props[enabledPropName]) {
+ // If no timeout duration is provided
+ if (props[timeoutPropName] == null) {
+ return new Error(timeoutPropName + ' wasn\'t supplied to ReactCSSTransitionGroup: ' + 'this can cause unreliable animations and won\'t be supported in ' + 'a future version of React. See ' + 'https://fb.me/react-animation-transition-group-timeout for more ' + 'information.');
+
+ // If the duration isn't a number
+ } else if (typeof props[timeoutPropName] !== 'number') {
+ return new Error(timeoutPropName + ' must be a number (in milliseconds)');
+ }
+ }
+ };
+}
+
+var ReactCSSTransitionGroup = React.createClass({
+ displayName: 'ReactCSSTransitionGroup',
+
+ propTypes: {
+ transitionName: ReactCSSTransitionGroupChild.propTypes.name,
+
+ transitionAppear: React.PropTypes.bool,
+ transitionEnter: React.PropTypes.bool,
+ transitionLeave: React.PropTypes.bool,
+ transitionAppearTimeout: createTransitionTimeoutPropValidator('Appear'),
+ transitionEnterTimeout: createTransitionTimeoutPropValidator('Enter'),
+ transitionLeaveTimeout: createTransitionTimeoutPropValidator('Leave')
+ },
+
+ getDefaultProps: function () {
+ return {
+ transitionAppear: false,
+ transitionEnter: true,
+ transitionLeave: true
+ };
+ },
+
+ _wrapChild: function (child) {
+ // We need to provide this childFactory so that
+ // ReactCSSTransitionGroupChild can receive updates to name, enter, and
+ // leave while it is leaving.
+ return React.createElement(ReactCSSTransitionGroupChild, {
+ name: this.props.transitionName,
+ appear: this.props.transitionAppear,
+ enter: this.props.transitionEnter,
+ leave: this.props.transitionLeave,
+ appearTimeout: this.props.transitionAppearTimeout,
+ enterTimeout: this.props.transitionEnterTimeout,
+ leaveTimeout: this.props.transitionLeaveTimeout
+ }, child);
+ },
+
+ render: function () {
+ return React.createElement(ReactTransitionGroup, assign({}, this.props, { childFactory: this._wrapChild }));
+ }
+});
+
+module.exports = ReactCSSTransitionGroup;
+},{"24":24,"26":26,"30":30,"94":94}],30:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks
+ * @providesModule ReactCSSTransitionGroupChild
+ */
+
+'use strict';
+
+var React = _dereq_(26);
+var ReactDOM = _dereq_(40);
+
+var CSSCore = _dereq_(145);
+var ReactTransitionEvents = _dereq_(93);
+
+var onlyChild = _dereq_(135);
+
+// We don't remove the element from the DOM until we receive an animationend or
+// transitionend event. If the user screws up and forgets to add an animation
+// their node will be stuck in the DOM forever, so we detect if an animation
+// does not start and if it doesn't, we just call the end listener immediately.
+var TICK = 17;
+
+var ReactCSSTransitionGroupChild = React.createClass({
+ displayName: 'ReactCSSTransitionGroupChild',
+
+ propTypes: {
+ name: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.shape({
+ enter: React.PropTypes.string,
+ leave: React.PropTypes.string,
+ active: React.PropTypes.string
+ }), React.PropTypes.shape({
+ enter: React.PropTypes.string,
+ enterActive: React.PropTypes.string,
+ leave: React.PropTypes.string,
+ leaveActive: React.PropTypes.string,
+ appear: React.PropTypes.string,
+ appearActive: React.PropTypes.string
+ })]).isRequired,
+
+ // Once we require timeouts to be specified, we can remove the
+ // boolean flags (appear etc.) and just accept a number
+ // or a bool for the timeout flags (appearTimeout etc.)
+ appear: React.PropTypes.bool,
+ enter: React.PropTypes.bool,
+ leave: React.PropTypes.bool,
+ appearTimeout: React.PropTypes.number,
+ enterTimeout: React.PropTypes.number,
+ leaveTimeout: React.PropTypes.number
+ },
+
+ transition: function (animationType, finishCallback, userSpecifiedDelay) {
+ var node = ReactDOM.findDOMNode(this);
+
+ if (!node) {
+ if (finishCallback) {
+ finishCallback();
+ }
+ return;
+ }
+
+ var className = this.props.name[animationType] || this.props.name + '-' + animationType;
+ var activeClassName = this.props.name[animationType + 'Active'] || className + '-active';
+ var timeout = null;
+
+ var endListener = function (e) {
+ if (e && e.target !== node) {
+ return;
+ }
+
+ clearTimeout(timeout);
+
+ CSSCore.removeClass(node, className);
+ CSSCore.removeClass(node, activeClassName);
+
+ ReactTransitionEvents.removeEndEventListener(node, endListener);
+
+ // Usually this optional callback is used for informing an owner of
+ // a leave animation and telling it to remove the child.
+ if (finishCallback) {
+ finishCallback();
+ }
+ };
+
+ CSSCore.addClass(node, className);
+
+ // Need to do this to actually trigger a transition.
+ this.queueClass(activeClassName);
+
+ // If the user specified a timeout delay.
+ if (userSpecifiedDelay) {
+ // Clean-up the animation after the specified delay
+ timeout = setTimeout(endListener, userSpecifiedDelay);
+ this.transitionTimeouts.push(timeout);
+ } else {
+ // DEPRECATED: this listener will be removed in a future version of react
+ ReactTransitionEvents.addEndEventListener(node, endListener);
+ }
+ },
+
+ queueClass: function (className) {
+ this.classNameQueue.push(className);
+
+ if (!this.timeout) {
+ this.timeout = setTimeout(this.flushClassNameQueue, TICK);
+ }
+ },
+
+ flushClassNameQueue: function () {
+ if (this.isMounted()) {
+ this.classNameQueue.forEach(CSSCore.addClass.bind(CSSCore, ReactDOM.findDOMNode(this)));
+ }
+ this.classNameQueue.length = 0;
+ this.timeout = null;
+ },
+
+ componentWillMount: function () {
+ this.classNameQueue = [];
+ this.transitionTimeouts = [];
+ },
+
+ componentWillUnmount: function () {
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+ this.transitionTimeouts.forEach(function (timeout) {
+ clearTimeout(timeout);
+ });
+ },
+
+ componentWillAppear: function (done) {
+ if (this.props.appear) {
+ this.transition('appear', done, this.props.appearTimeout);
+ } else {
+ done();
+ }
+ },
+
+ componentWillEnter: function (done) {
+ if (this.props.enter) {
+ this.transition('enter', done, this.props.enterTimeout);
+ } else {
+ done();
+ }
+ },
+
+ componentWillLeave: function (done) {
+ if (this.props.leave) {
+ this.transition('leave', done, this.props.leaveTimeout);
+ } else {
+ done();
+ }
+ },
+
+ render: function () {
+ return onlyChild(this.props.children);
+ }
+});
+
+module.exports = ReactCSSTransitionGroupChild;
+},{"135":135,"145":145,"26":26,"40":40,"93":93}],31:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactChildReconciler
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactReconciler = _dereq_(84);
+
+var instantiateReactComponent = _dereq_(132);
+var shouldUpdateReactComponent = _dereq_(141);
+var traverseAllChildren = _dereq_(142);
+var warning = _dereq_(173);
+
+function instantiateChild(childInstances, child, name) {
+ // We found a component instance.
+ var keyUnique = childInstances[name] === undefined;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(keyUnique, 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.', name) : undefined;
+ }
+ if (child != null && keyUnique) {
+ childInstances[name] = instantiateReactComponent(child, null);
+ }
+}
+
+/**
+ * ReactChildReconciler provides helpers for initializing or updating a set of
+ * children. Its output is suitable for passing it onto ReactMultiChild which
+ * does diffed reordering and insertion.
+ */
+var ReactChildReconciler = {
+ /**
+ * Generates a "mount image" for each of the supplied children. In the case
+ * of `ReactDOMComponent`, a mount image is a string of markup.
+ *
+ * @param {?object} nestedChildNodes Nested child maps.
+ * @return {?object} A set of child instances.
+ * @internal
+ */
+ instantiateChildren: function (nestedChildNodes, transaction, context) {
+ if (nestedChildNodes == null) {
+ return null;
+ }
+ var childInstances = {};
+ traverseAllChildren(nestedChildNodes, instantiateChild, childInstances);
+ return childInstances;
+ },
+
+ /**
+ * Updates the rendered children and returns a new set of children.
+ *
+ * @param {?object} prevChildren Previously initialized set of children.
+ * @param {?object} nextChildren Flat child element maps.
+ * @param {ReactReconcileTransaction} transaction
+ * @param {object} context
+ * @return {?object} A new set of child instances.
+ * @internal
+ */
+ updateChildren: function (prevChildren, nextChildren, transaction, context) {
+ // We currently don't have a way to track moves here but if we use iterators
+ // instead of for..in we can zip the iterators and check if an item has
+ // moved.
+ // TODO: If nothing has changed, return the prevChildren object so that we
+ // can quickly bailout if nothing has changed.
+ if (!nextChildren && !prevChildren) {
+ return null;
+ }
+ var name;
+ for (name in nextChildren) {
+ if (!nextChildren.hasOwnProperty(name)) {
+ continue;
+ }
+ var prevChild = prevChildren && prevChildren[name];
+ var prevElement = prevChild && prevChild._currentElement;
+ var nextElement = nextChildren[name];
+ if (prevChild != null && shouldUpdateReactComponent(prevElement, nextElement)) {
+ ReactReconciler.receiveComponent(prevChild, nextElement, transaction, context);
+ nextChildren[name] = prevChild;
+ } else {
+ if (prevChild) {
+ ReactReconciler.unmountComponent(prevChild, name);
+ }
+ // The child must be instantiated before it's mounted.
+ var nextChildInstance = instantiateReactComponent(nextElement, null);
+ nextChildren[name] = nextChildInstance;
+ }
+ }
+ // Unmount children that are no longer present.
+ for (name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
+ ReactReconciler.unmountComponent(prevChildren[name]);
+ }
+ }
+ return nextChildren;
+ },
+
+ /**
+ * Unmounts all rendered children. This should be used to clean up children
+ * when this component is unmounted.
+ *
+ * @param {?object} renderedChildren Previously initialized set of children.
+ * @internal
+ */
+ unmountChildren: function (renderedChildren) {
+ for (var name in renderedChildren) {
+ if (renderedChildren.hasOwnProperty(name)) {
+ var renderedChild = renderedChildren[name];
+ ReactReconciler.unmountComponent(renderedChild);
+ }
+ }
+ }
+
+};
+
+module.exports = ReactChildReconciler;
+},{"132":132,"141":141,"142":142,"173":173,"84":84}],32:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactChildren
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+var ReactElement = _dereq_(57);
+
+var emptyFunction = _dereq_(153);
+var traverseAllChildren = _dereq_(142);
+
+var twoArgumentPooler = PooledClass.twoArgumentPooler;
+var fourArgumentPooler = PooledClass.fourArgumentPooler;
+
+var userProvidedKeyEscapeRegex = /\/(?!\/)/g;
+function escapeUserProvidedKey(text) {
+ return ('' + text).replace(userProvidedKeyEscapeRegex, '//');
+}
+
+/**
+ * PooledClass representing the bookkeeping associated with performing a child
+ * traversal. Allows avoiding binding callbacks.
+ *
+ * @constructor ForEachBookKeeping
+ * @param {!function} forEachFunction Function to perform traversal with.
+ * @param {?*} forEachContext Context to perform context with.
+ */
+function ForEachBookKeeping(forEachFunction, forEachContext) {
+ this.func = forEachFunction;
+ this.context = forEachContext;
+ this.count = 0;
+}
+ForEachBookKeeping.prototype.destructor = function () {
+ this.func = null;
+ this.context = null;
+ this.count = 0;
+};
+PooledClass.addPoolingTo(ForEachBookKeeping, twoArgumentPooler);
+
+function forEachSingleChild(bookKeeping, child, name) {
+ var func = bookKeeping.func;
+ var context = bookKeeping.context;
+
+ func.call(context, child, bookKeeping.count++);
+}
+
+/**
+ * Iterates through children that are typically specified as `props.children`.
+ *
+ * The provided forEachFunc(child, index) will be called for each
+ * leaf child.
+ *
+ * @param {?*} children Children tree container.
+ * @param {function(*, int)} forEachFunc
+ * @param {*} forEachContext Context for forEachContext.
+ */
+function forEachChildren(children, forEachFunc, forEachContext) {
+ if (children == null) {
+ return children;
+ }
+ var traverseContext = ForEachBookKeeping.getPooled(forEachFunc, forEachContext);
+ traverseAllChildren(children, forEachSingleChild, traverseContext);
+ ForEachBookKeeping.release(traverseContext);
+}
+
+/**
+ * PooledClass representing the bookkeeping associated with performing a child
+ * mapping. Allows avoiding binding callbacks.
+ *
+ * @constructor MapBookKeeping
+ * @param {!*} mapResult Object containing the ordered map of results.
+ * @param {!function} mapFunction Function to perform mapping with.
+ * @param {?*} mapContext Context to perform mapping with.
+ */
+function MapBookKeeping(mapResult, keyPrefix, mapFunction, mapContext) {
+ this.result = mapResult;
+ this.keyPrefix = keyPrefix;
+ this.func = mapFunction;
+ this.context = mapContext;
+ this.count = 0;
+}
+MapBookKeeping.prototype.destructor = function () {
+ this.result = null;
+ this.keyPrefix = null;
+ this.func = null;
+ this.context = null;
+ this.count = 0;
+};
+PooledClass.addPoolingTo(MapBookKeeping, fourArgumentPooler);
+
+function mapSingleChildIntoContext(bookKeeping, child, childKey) {
+ var result = bookKeeping.result;
+ var keyPrefix = bookKeeping.keyPrefix;
+ var func = bookKeeping.func;
+ var context = bookKeeping.context;
+
+ var mappedChild = func.call(context, child, bookKeeping.count++);
+ if (Array.isArray(mappedChild)) {
+ mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, emptyFunction.thatReturnsArgument);
+ } else if (mappedChild != null) {
+ if (ReactElement.isValidElement(mappedChild)) {
+ mappedChild = ReactElement.cloneAndReplaceKey(mappedChild,
+ // Keep both the (mapped) and old keys if they differ, just as
+ // traverseAllChildren used to do for objects as children
+ keyPrefix + (mappedChild !== child ? escapeUserProvidedKey(mappedChild.key || '') + '/' : '') + childKey);
+ }
+ result.push(mappedChild);
+ }
+}
+
+function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
+ var escapedPrefix = '';
+ if (prefix != null) {
+ escapedPrefix = escapeUserProvidedKey(prefix) + '/';
+ }
+ var traverseContext = MapBookKeeping.getPooled(array, escapedPrefix, func, context);
+ traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
+ MapBookKeeping.release(traverseContext);
+}
+
+/**
+ * Maps children that are typically specified as `props.children`.
+ *
+ * The provided mapFunction(child, key, index) will be called for each
+ * leaf child.
+ *
+ * @param {?*} children Children tree container.
+ * @param {function(*, int)} func The map function.
+ * @param {*} context Context for mapFunction.
+ * @return {object} Object containing the ordered map of results.
+ */
+function mapChildren(children, func, context) {
+ if (children == null) {
+ return children;
+ }
+ var result = [];
+ mapIntoWithKeyPrefixInternal(children, result, null, func, context);
+ return result;
+}
+
+function forEachSingleChildDummy(traverseContext, child, name) {
+ return null;
+}
+
+/**
+ * Count the number of children that are typically specified as
+ * `props.children`.
+ *
+ * @param {?*} children Children tree container.
+ * @return {number} The number of children.
+ */
+function countChildren(children, context) {
+ return traverseAllChildren(children, forEachSingleChildDummy, null);
+}
+
+/**
+ * Flatten a children object (typically specified as `props.children`) and
+ * return an array with appropriately re-keyed children.
+ */
+function toArray(children) {
+ var result = [];
+ mapIntoWithKeyPrefixInternal(children, result, null, emptyFunction.thatReturnsArgument);
+ return result;
+}
+
+var ReactChildren = {
+ forEach: forEachChildren,
+ map: mapChildren,
+ mapIntoWithKeyPrefixInternal: mapIntoWithKeyPrefixInternal,
+ count: countChildren,
+ toArray: toArray
+};
+
+module.exports = ReactChildren;
+},{"142":142,"153":153,"25":25,"57":57}],33:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactClass
+ */
+
+'use strict';
+
+var ReactComponent = _dereq_(34);
+var ReactElement = _dereq_(57);
+var ReactPropTypeLocations = _dereq_(81);
+var ReactPropTypeLocationNames = _dereq_(80);
+var ReactNoopUpdateQueue = _dereq_(76);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var invariant = _dereq_(161);
+var keyMirror = _dereq_(165);
+var keyOf = _dereq_(166);
+var warning = _dereq_(173);
+
+var MIXINS_KEY = keyOf({ mixins: null });
+
+/**
+ * Policies that describe methods in `ReactClassInterface`.
+ */
+var SpecPolicy = keyMirror({
+ /**
+ * These methods may be defined only once by the class specification or mixin.
+ */
+ DEFINE_ONCE: null,
+ /**
+ * These methods may be defined by both the class specification and mixins.
+ * Subsequent definitions will be chained. These methods must return void.
+ */
+ DEFINE_MANY: null,
+ /**
+ * These methods are overriding the base class.
+ */
+ OVERRIDE_BASE: null,
+ /**
+ * These methods are similar to DEFINE_MANY, except we assume they return
+ * objects. We try to merge the keys of the return values of all the mixed in
+ * functions. If there is a key conflict we throw.
+ */
+ DEFINE_MANY_MERGED: null
+});
+
+var injectedMixins = [];
+
+var warnedSetProps = false;
+function warnSetProps() {
+ if (!warnedSetProps) {
+ warnedSetProps = true;
+ "production" !== 'production' ? warning(false, 'setProps(...) and replaceProps(...) are deprecated. ' + 'Instead, call render again at the top level.') : undefined;
+ }
+}
+
+/**
+ * Composite components are higher-level components that compose other composite
+ * or native components.
+ *
+ * To create a new type of `ReactClass`, pass a specification of
+ * your new class to `React.createClass`. The only requirement of your class
+ * specification is that you implement a `render` method.
+ *
+ * var MyComponent = React.createClass({
+ * render: function() {
+ * return <div>Hello World</div>;
+ * }
+ * });
+ *
+ * The class specification supports a specific protocol of methods that have
+ * special meaning (e.g. `render`). See `ReactClassInterface` for
+ * more the comprehensive protocol. Any other properties and methods in the
+ * class specification will be available on the prototype.
+ *
+ * @interface ReactClassInterface
+ * @internal
+ */
+var ReactClassInterface = {
+
+ /**
+ * An array of Mixin objects to include when defining your component.
+ *
+ * @type {array}
+ * @optional
+ */
+ mixins: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * An object containing properties and methods that should be defined on
+ * the component's constructor instead of its prototype (static methods).
+ *
+ * @type {object}
+ * @optional
+ */
+ statics: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Definition of prop types for this component.
+ *
+ * @type {object}
+ * @optional
+ */
+ propTypes: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Definition of context types for this component.
+ *
+ * @type {object}
+ * @optional
+ */
+ contextTypes: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Definition of context types this component sets for its children.
+ *
+ * @type {object}
+ * @optional
+ */
+ childContextTypes: SpecPolicy.DEFINE_MANY,
+
+ // ==== Definition methods ====
+
+ /**
+ * Invoked when the component is mounted. Values in the mapping will be set on
+ * `this.props` if that prop is not specified (i.e. using an `in` check).
+ *
+ * This method is invoked before `getInitialState` and therefore cannot rely
+ * on `this.state` or use `this.setState`.
+ *
+ * @return {object}
+ * @optional
+ */
+ getDefaultProps: SpecPolicy.DEFINE_MANY_MERGED,
+
+ /**
+ * Invoked once before the component is mounted. The return value will be used
+ * as the initial value of `this.state`.
+ *
+ * getInitialState: function() {
+ * return {
+ * isOn: false,
+ * fooBaz: new BazFoo()
+ * }
+ * }
+ *
+ * @return {object}
+ * @optional
+ */
+ getInitialState: SpecPolicy.DEFINE_MANY_MERGED,
+
+ /**
+ * @return {object}
+ * @optional
+ */
+ getChildContext: SpecPolicy.DEFINE_MANY_MERGED,
+
+ /**
+ * Uses props from `this.props` and state from `this.state` to render the
+ * structure of the component.
+ *
+ * No guarantees are made about when or how often this method is invoked, so
+ * it must not have side effects.
+ *
+ * render: function() {
+ * var name = this.props.name;
+ * return <div>Hello, {name}!</div>;
+ * }
+ *
+ * @return {ReactComponent}
+ * @nosideeffects
+ * @required
+ */
+ render: SpecPolicy.DEFINE_ONCE,
+
+ // ==== Delegate methods ====
+
+ /**
+ * Invoked when the component is initially created and about to be mounted.
+ * This may have side effects, but any external subscriptions or data created
+ * by this method must be cleaned up in `componentWillUnmount`.
+ *
+ * @optional
+ */
+ componentWillMount: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked when the component has been mounted and has a DOM representation.
+ * However, there is no guarantee that the DOM node is in the document.
+ *
+ * Use this as an opportunity to operate on the DOM when the component has
+ * been mounted (initialized and rendered) for the first time.
+ *
+ * @param {DOMElement} rootNode DOM element representing the component.
+ * @optional
+ */
+ componentDidMount: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked before the component receives new props.
+ *
+ * Use this as an opportunity to react to a prop transition by updating the
+ * state using `this.setState`. Current props are accessed via `this.props`.
+ *
+ * componentWillReceiveProps: function(nextProps, nextContext) {
+ * this.setState({
+ * likesIncreasing: nextProps.likeCount > this.props.likeCount
+ * });
+ * }
+ *
+ * NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop
+ * transition may cause a state change, but the opposite is not true. If you
+ * need it, you are probably looking for `componentWillUpdate`.
+ *
+ * @param {object} nextProps
+ * @optional
+ */
+ componentWillReceiveProps: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked while deciding if the component should be updated as a result of
+ * receiving new props, state and/or context.
+ *
+ * Use this as an opportunity to `return false` when you're certain that the
+ * transition to the new props/state/context will not require a component
+ * update.
+ *
+ * shouldComponentUpdate: function(nextProps, nextState, nextContext) {
+ * return !equal(nextProps, this.props) ||
+ * !equal(nextState, this.state) ||
+ * !equal(nextContext, this.context);
+ * }
+ *
+ * @param {object} nextProps
+ * @param {?object} nextState
+ * @param {?object} nextContext
+ * @return {boolean} True if the component should update.
+ * @optional
+ */
+ shouldComponentUpdate: SpecPolicy.DEFINE_ONCE,
+
+ /**
+ * Invoked when the component is about to update due to a transition from
+ * `this.props`, `this.state` and `this.context` to `nextProps`, `nextState`
+ * and `nextContext`.
+ *
+ * Use this as an opportunity to perform preparation before an update occurs.
+ *
+ * NOTE: You **cannot** use `this.setState()` in this method.
+ *
+ * @param {object} nextProps
+ * @param {?object} nextState
+ * @param {?object} nextContext
+ * @param {ReactReconcileTransaction} transaction
+ * @optional
+ */
+ componentWillUpdate: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked when the component's DOM representation has been updated.
+ *
+ * Use this as an opportunity to operate on the DOM when the component has
+ * been updated.
+ *
+ * @param {object} prevProps
+ * @param {?object} prevState
+ * @param {?object} prevContext
+ * @param {DOMElement} rootNode DOM element representing the component.
+ * @optional
+ */
+ componentDidUpdate: SpecPolicy.DEFINE_MANY,
+
+ /**
+ * Invoked when the component is about to be removed from its parent and have
+ * its DOM representation destroyed.
+ *
+ * Use this as an opportunity to deallocate any external resources.
+ *
+ * NOTE: There is no `componentDidUnmount` since your component will have been
+ * destroyed by that point.
+ *
+ * @optional
+ */
+ componentWillUnmount: SpecPolicy.DEFINE_MANY,
+
+ // ==== Advanced methods ====
+
+ /**
+ * Updates the component's currently mounted DOM representation.
+ *
+ * By default, this implements React's rendering and reconciliation algorithm.
+ * Sophisticated clients may wish to override this.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ * @overridable
+ */
+ updateComponent: SpecPolicy.OVERRIDE_BASE
+
+};
+
+/**
+ * Mapping from class specification keys to special processing functions.
+ *
+ * Although these are declared like instance properties in the specification
+ * when defining classes using `React.createClass`, they are actually static
+ * and are accessible on the constructor instead of the prototype. Despite
+ * being static, they must be defined outside of the "statics" key under
+ * which all other static methods are defined.
+ */
+var RESERVED_SPEC_KEYS = {
+ displayName: function (Constructor, displayName) {
+ Constructor.displayName = displayName;
+ },
+ mixins: function (Constructor, mixins) {
+ if (mixins) {
+ for (var i = 0; i < mixins.length; i++) {
+ mixSpecIntoComponent(Constructor, mixins[i]);
+ }
+ }
+ },
+ childContextTypes: function (Constructor, childContextTypes) {
+ if ("production" !== 'production') {
+ validateTypeDef(Constructor, childContextTypes, ReactPropTypeLocations.childContext);
+ }
+ Constructor.childContextTypes = assign({}, Constructor.childContextTypes, childContextTypes);
+ },
+ contextTypes: function (Constructor, contextTypes) {
+ if ("production" !== 'production') {
+ validateTypeDef(Constructor, contextTypes, ReactPropTypeLocations.context);
+ }
+ Constructor.contextTypes = assign({}, Constructor.contextTypes, contextTypes);
+ },
+ /**
+ * Special case getDefaultProps which should move into statics but requires
+ * automatic merging.
+ */
+ getDefaultProps: function (Constructor, getDefaultProps) {
+ if (Constructor.getDefaultProps) {
+ Constructor.getDefaultProps = createMergedResultFunction(Constructor.getDefaultProps, getDefaultProps);
+ } else {
+ Constructor.getDefaultProps = getDefaultProps;
+ }
+ },
+ propTypes: function (Constructor, propTypes) {
+ if ("production" !== 'production') {
+ validateTypeDef(Constructor, propTypes, ReactPropTypeLocations.prop);
+ }
+ Constructor.propTypes = assign({}, Constructor.propTypes, propTypes);
+ },
+ statics: function (Constructor, statics) {
+ mixStaticSpecIntoComponent(Constructor, statics);
+ },
+ autobind: function () {} };
+
+// noop
+function validateTypeDef(Constructor, typeDef, location) {
+ for (var propName in typeDef) {
+ if (typeDef.hasOwnProperty(propName)) {
+ // use a warning instead of an invariant so components
+ // don't show up in prod but not in __DEV__
+ "production" !== 'production' ? warning(typeof typeDef[propName] === 'function', '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', Constructor.displayName || 'ReactClass', ReactPropTypeLocationNames[location], propName) : undefined;
+ }
+ }
+}
+
+function validateMethodOverride(proto, name) {
+ var specPolicy = ReactClassInterface.hasOwnProperty(name) ? ReactClassInterface[name] : null;
+
+ // Disallow overriding of base class methods unless explicitly allowed.
+ if (ReactClassMixin.hasOwnProperty(name)) {
+ !(specPolicy === SpecPolicy.OVERRIDE_BASE) ? "production" !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to override ' + '`%s` from your class specification. Ensure that your method names ' + 'do not overlap with React methods.', name) : invariant(false) : undefined;
+ }
+
+ // Disallow defining methods more than once unless explicitly allowed.
+ if (proto.hasOwnProperty(name)) {
+ !(specPolicy === SpecPolicy.DEFINE_MANY || specPolicy === SpecPolicy.DEFINE_MANY_MERGED) ? "production" !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to define ' + '`%s` on your component more than once. This conflict may be due ' + 'to a mixin.', name) : invariant(false) : undefined;
+ }
+}
+
+/**
+ * Mixin helper which handles policy validation and reserved
+ * specification keys when building React classses.
+ */
+function mixSpecIntoComponent(Constructor, spec) {
+ if (!spec) {
+ return;
+ }
+
+ !(typeof spec !== 'function') ? "production" !== 'production' ? invariant(false, 'ReactClass: You\'re attempting to ' + 'use a component class as a mixin. Instead, just use a regular object.') : invariant(false) : undefined;
+ !!ReactElement.isValidElement(spec) ? "production" !== 'production' ? invariant(false, 'ReactClass: You\'re attempting to ' + 'use a component as a mixin. Instead, just use a regular object.') : invariant(false) : undefined;
+
+ var proto = Constructor.prototype;
+
+ // By handling mixins before any other properties, we ensure the same
+ // chaining order is applied to methods with DEFINE_MANY policy, whether
+ // mixins are listed before or after these methods in the spec.
+ if (spec.hasOwnProperty(MIXINS_KEY)) {
+ RESERVED_SPEC_KEYS.mixins(Constructor, spec.mixins);
+ }
+
+ for (var name in spec) {
+ if (!spec.hasOwnProperty(name)) {
+ continue;
+ }
+
+ if (name === MIXINS_KEY) {
+ // We have already handled mixins in a special case above.
+ continue;
+ }
+
+ var property = spec[name];
+ validateMethodOverride(proto, name);
+
+ if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) {
+ RESERVED_SPEC_KEYS[name](Constructor, property);
+ } else {
+ // Setup methods on prototype:
+ // The following member methods should not be automatically bound:
+ // 1. Expected ReactClass methods (in the "interface").
+ // 2. Overridden methods (that were mixed in).
+ var isReactClassMethod = ReactClassInterface.hasOwnProperty(name);
+ var isAlreadyDefined = proto.hasOwnProperty(name);
+ var isFunction = typeof property === 'function';
+ var shouldAutoBind = isFunction && !isReactClassMethod && !isAlreadyDefined && spec.autobind !== false;
+
+ if (shouldAutoBind) {
+ if (!proto.__reactAutoBindMap) {
+ proto.__reactAutoBindMap = {};
+ }
+ proto.__reactAutoBindMap[name] = property;
+ proto[name] = property;
+ } else {
+ if (isAlreadyDefined) {
+ var specPolicy = ReactClassInterface[name];
+
+ // These cases should already be caught by validateMethodOverride.
+ !(isReactClassMethod && (specPolicy === SpecPolicy.DEFINE_MANY_MERGED || specPolicy === SpecPolicy.DEFINE_MANY)) ? "production" !== 'production' ? invariant(false, 'ReactClass: Unexpected spec policy %s for key %s ' + 'when mixing in component specs.', specPolicy, name) : invariant(false) : undefined;
+
+ // For methods which are defined more than once, call the existing
+ // methods before calling the new property, merging if appropriate.
+ if (specPolicy === SpecPolicy.DEFINE_MANY_MERGED) {
+ proto[name] = createMergedResultFunction(proto[name], property);
+ } else if (specPolicy === SpecPolicy.DEFINE_MANY) {
+ proto[name] = createChainedFunction(proto[name], property);
+ }
+ } else {
+ proto[name] = property;
+ if ("production" !== 'production') {
+ // Add verbose displayName to the function, which helps when looking
+ // at profiling tools.
+ if (typeof property === 'function' && spec.displayName) {
+ proto[name].displayName = spec.displayName + '_' + name;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+function mixStaticSpecIntoComponent(Constructor, statics) {
+ if (!statics) {
+ return;
+ }
+ for (var name in statics) {
+ var property = statics[name];
+ if (!statics.hasOwnProperty(name)) {
+ continue;
+ }
+
+ var isReserved = (name in RESERVED_SPEC_KEYS);
+ !!isReserved ? "production" !== 'production' ? invariant(false, 'ReactClass: You are attempting to define a reserved ' + 'property, `%s`, that shouldn\'t be on the "statics" key. Define it ' + 'as an instance property instead; it will still be accessible on the ' + 'constructor.', name) : invariant(false) : undefined;
+
+ var isInherited = (name in Constructor);
+ !!isInherited ? "production" !== 'production' ? invariant(false, 'ReactClass: You are attempting to define ' + '`%s` on your component more than once. This conflict may be ' + 'due to a mixin.', name) : invariant(false) : undefined;
+ Constructor[name] = property;
+ }
+}
+
+/**
+ * Merge two objects, but throw if both contain the same key.
+ *
+ * @param {object} one The first object, which is mutated.
+ * @param {object} two The second object
+ * @return {object} one after it has been mutated to contain everything in two.
+ */
+function mergeIntoWithNoDuplicateKeys(one, two) {
+ !(one && two && typeof one === 'object' && typeof two === 'object') ? "production" !== 'production' ? invariant(false, 'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.') : invariant(false) : undefined;
+
+ for (var key in two) {
+ if (two.hasOwnProperty(key)) {
+ !(one[key] === undefined) ? "production" !== 'production' ? invariant(false, 'mergeIntoWithNoDuplicateKeys(): ' + 'Tried to merge two objects with the same key: `%s`. This conflict ' + 'may be due to a mixin; in particular, this may be caused by two ' + 'getInitialState() or getDefaultProps() methods returning objects ' + 'with clashing keys.', key) : invariant(false) : undefined;
+ one[key] = two[key];
+ }
+ }
+ return one;
+}
+
+/**
+ * Creates a function that invokes two functions and merges their return values.
+ *
+ * @param {function} one Function to invoke first.
+ * @param {function} two Function to invoke second.
+ * @return {function} Function that invokes the two argument functions.
+ * @private
+ */
+function createMergedResultFunction(one, two) {
+ return function mergedResult() {
+ var a = one.apply(this, arguments);
+ var b = two.apply(this, arguments);
+ if (a == null) {
+ return b;
+ } else if (b == null) {
+ return a;
+ }
+ var c = {};
+ mergeIntoWithNoDuplicateKeys(c, a);
+ mergeIntoWithNoDuplicateKeys(c, b);
+ return c;
+ };
+}
+
+/**
+ * Creates a function that invokes two functions and ignores their return vales.
+ *
+ * @param {function} one Function to invoke first.
+ * @param {function} two Function to invoke second.
+ * @return {function} Function that invokes the two argument functions.
+ * @private
+ */
+function createChainedFunction(one, two) {
+ return function chainedFunction() {
+ one.apply(this, arguments);
+ two.apply(this, arguments);
+ };
+}
+
+/**
+ * Binds a method to the component.
+ *
+ * @param {object} component Component whose method is going to be bound.
+ * @param {function} method Method to be bound.
+ * @return {function} The bound method.
+ */
+function bindAutoBindMethod(component, method) {
+ var boundMethod = method.bind(component);
+ if ("production" !== 'production') {
+ boundMethod.__reactBoundContext = component;
+ boundMethod.__reactBoundMethod = method;
+ boundMethod.__reactBoundArguments = null;
+ var componentName = component.constructor.displayName;
+ var _bind = boundMethod.bind;
+ /* eslint-disable block-scoped-var, no-undef */
+ boundMethod.bind = function (newThis) {
+ for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ args[_key - 1] = arguments[_key];
+ }
+
+ // User is trying to bind() an autobound method; we effectively will
+ // ignore the value of "this" that the user is trying to use, so
+ // let's warn.
+ if (newThis !== component && newThis !== null) {
+ "production" !== 'production' ? warning(false, 'bind(): React component methods may only be bound to the ' + 'component instance. See %s', componentName) : undefined;
+ } else if (!args.length) {
+ "production" !== 'production' ? warning(false, 'bind(): You are binding a component method to the component. ' + 'React does this for you automatically in a high-performance ' + 'way, so you can safely remove this call. See %s', componentName) : undefined;
+ return boundMethod;
+ }
+ var reboundMethod = _bind.apply(boundMethod, arguments);
+ reboundMethod.__reactBoundContext = component;
+ reboundMethod.__reactBoundMethod = method;
+ reboundMethod.__reactBoundArguments = args;
+ return reboundMethod;
+ /* eslint-enable */
+ };
+ }
+ return boundMethod;
+}
+
+/**
+ * Binds all auto-bound methods in a component.
+ *
+ * @param {object} component Component whose method is going to be bound.
+ */
+function bindAutoBindMethods(component) {
+ for (var autoBindKey in component.__reactAutoBindMap) {
+ if (component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
+ var method = component.__reactAutoBindMap[autoBindKey];
+ component[autoBindKey] = bindAutoBindMethod(component, method);
+ }
+ }
+}
+
+/**
+ * Add more to the ReactClass base class. These are all legacy features and
+ * therefore not already part of the modern ReactComponent.
+ */
+var ReactClassMixin = {
+
+ /**
+ * TODO: This will be deprecated because state should always keep a consistent
+ * type signature and the only use case for this, is to avoid that.
+ */
+ replaceState: function (newState, callback) {
+ this.updater.enqueueReplaceState(this, newState);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+ },
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function () {
+ return this.updater.isMounted(this);
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {object} partialProps Subset of the next props.
+ * @param {?function} callback Called after props are updated.
+ * @final
+ * @public
+ * @deprecated
+ */
+ setProps: function (partialProps, callback) {
+ if ("production" !== 'production') {
+ warnSetProps();
+ }
+ this.updater.enqueueSetProps(this, partialProps);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+ },
+
+ /**
+ * Replace all the props.
+ *
+ * @param {object} newProps Subset of the next props.
+ * @param {?function} callback Called after props are updated.
+ * @final
+ * @public
+ * @deprecated
+ */
+ replaceProps: function (newProps, callback) {
+ if ("production" !== 'production') {
+ warnSetProps();
+ }
+ this.updater.enqueueReplaceProps(this, newProps);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+ }
+};
+
+var ReactClassComponent = function () {};
+assign(ReactClassComponent.prototype, ReactComponent.prototype, ReactClassMixin);
+
+/**
+ * Module for creating composite components.
+ *
+ * @class ReactClass
+ */
+var ReactClass = {
+
+ /**
+ * Creates a composite component class given a class specification.
+ *
+ * @param {object} spec Class specification (which must define `render`).
+ * @return {function} Component constructor function.
+ * @public
+ */
+ createClass: function (spec) {
+ var Constructor = function (props, context, updater) {
+ // This constructor is overridden by mocks. The argument is used
+ // by mocks to assert on what gets mounted.
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(this instanceof Constructor, 'Something is calling a React component directly. Use a factory or ' + 'JSX instead. See: https://fb.me/react-legacyfactory') : undefined;
+ }
+
+ // Wire up auto-binding
+ if (this.__reactAutoBindMap) {
+ bindAutoBindMethods(this);
+ }
+
+ this.props = props;
+ this.context = context;
+ this.refs = emptyObject;
+ this.updater = updater || ReactNoopUpdateQueue;
+
+ this.state = null;
+
+ // ReactClasses doesn't have constructors. Instead, they use the
+ // getInitialState and componentWillMount methods for initialization.
+
+ var initialState = this.getInitialState ? this.getInitialState() : null;
+ if ("production" !== 'production') {
+ // We allow auto-mocks to proceed as if they're returning null.
+ if (typeof initialState === 'undefined' && this.getInitialState._isMockFunction) {
+ // This is probably bad practice. Consider warning here and
+ // deprecating this convenience.
+ initialState = null;
+ }
+ }
+ !(typeof initialState === 'object' && !Array.isArray(initialState)) ? "production" !== 'production' ? invariant(false, '%s.getInitialState(): must return an object or null', Constructor.displayName || 'ReactCompositeComponent') : invariant(false) : undefined;
+
+ this.state = initialState;
+ };
+ Constructor.prototype = new ReactClassComponent();
+ Constructor.prototype.constructor = Constructor;
+
+ injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor));
+
+ mixSpecIntoComponent(Constructor, spec);
+
+ // Initialize the defaultProps property after all mixins have been merged.
+ if (Constructor.getDefaultProps) {
+ Constructor.defaultProps = Constructor.getDefaultProps();
+ }
+
+ if ("production" !== 'production') {
+ // This is a tag to indicate that the use of these method names is ok,
+ // since it's used with createClass. If it's not, then it's likely a
+ // mistake so we'll warn you to use the static property, property
+ // initializer or constructor respectively.
+ if (Constructor.getDefaultProps) {
+ Constructor.getDefaultProps.isReactClassApproved = {};
+ }
+ if (Constructor.prototype.getInitialState) {
+ Constructor.prototype.getInitialState.isReactClassApproved = {};
+ }
+ }
+
+ !Constructor.prototype.render ? "production" !== 'production' ? invariant(false, 'createClass(...): Class specification must implement a `render` method.') : invariant(false) : undefined;
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(!Constructor.prototype.componentShouldUpdate, '%s has a method called ' + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 'The name is phrased as a question because the function is ' + 'expected to return a value.', spec.displayName || 'A component') : undefined;
+ "production" !== 'production' ? warning(!Constructor.prototype.componentWillRecieveProps, '%s has a method called ' + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', spec.displayName || 'A component') : undefined;
+ }
+
+ // Reduce time spent doing lookups by setting these on the prototype.
+ for (var methodName in ReactClassInterface) {
+ if (!Constructor.prototype[methodName]) {
+ Constructor.prototype[methodName] = null;
+ }
+ }
+
+ return Constructor;
+ },
+
+ injection: {
+ injectMixin: function (mixin) {
+ injectedMixins.push(mixin);
+ }
+ }
+
+};
+
+module.exports = ReactClass;
+},{"154":154,"161":161,"165":165,"166":166,"173":173,"24":24,"34":34,"57":57,"76":76,"80":80,"81":81}],34:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponent
+ */
+
+'use strict';
+
+var ReactNoopUpdateQueue = _dereq_(76);
+
+var canDefineProperty = _dereq_(117);
+var emptyObject = _dereq_(154);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Base class helpers for the updating state of a component.
+ */
+function ReactComponent(props, context, updater) {
+ this.props = props;
+ this.context = context;
+ this.refs = emptyObject;
+ // We initialize the default updater but the real one gets injected by the
+ // renderer.
+ this.updater = updater || ReactNoopUpdateQueue;
+}
+
+ReactComponent.prototype.isReactComponent = {};
+
+/**
+ * Sets a subset of the state. Always use this to mutate
+ * state. You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * There is no guarantee that calls to `setState` will run synchronously,
+ * as they may eventually be batched together. You can provide an optional
+ * callback that will be executed when the call to setState is actually
+ * completed.
+ *
+ * When a function is provided to setState, it will be called at some point in
+ * the future (not synchronously). It will be called with the up to date
+ * component arguments (state, props, context). These values can be different
+ * from this.* because your function may be called after receiveProps but before
+ * shouldComponentUpdate, and this new state, props, and context will not yet be
+ * assigned to this.
+ *
+ * @param {object|function} partialState Next partial state or function to
+ * produce next partial state to be merged with current state.
+ * @param {?function} callback Called after state is updated.
+ * @final
+ * @protected
+ */
+ReactComponent.prototype.setState = function (partialState, callback) {
+ !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? "production" !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.') : invariant(false) : undefined;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(partialState != null, 'setState(...): You passed an undefined or null state object; ' + 'instead, use forceUpdate().') : undefined;
+ }
+ this.updater.enqueueSetState(this, partialState);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+};
+
+/**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldComponentUpdate`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {?function} callback Called after update is complete.
+ * @final
+ * @protected
+ */
+ReactComponent.prototype.forceUpdate = function (callback) {
+ this.updater.enqueueForceUpdate(this);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback);
+ }
+};
+
+/**
+ * Deprecated APIs. These APIs used to exist on classic React classes but since
+ * we would like to deprecate them, we're not going to move them over to this
+ * modern base class. Instead, we define a getter that warns if it's accessed.
+ */
+if ("production" !== 'production') {
+ var deprecatedAPIs = {
+ getDOMNode: ['getDOMNode', 'Use ReactDOM.findDOMNode(component) instead.'],
+ isMounted: ['isMounted', 'Instead, make sure to clean up subscriptions and pending requests in ' + 'componentWillUnmount to prevent memory leaks.'],
+ replaceProps: ['replaceProps', 'Instead, call render again at the top level.'],
+ replaceState: ['replaceState', 'Refactor your code to use setState instead (see ' + 'https://github.com/facebook/react/issues/3236).'],
+ setProps: ['setProps', 'Instead, call render again at the top level.']
+ };
+ var defineDeprecationWarning = function (methodName, info) {
+ if (canDefineProperty) {
+ Object.defineProperty(ReactComponent.prototype, methodName, {
+ get: function () {
+ "production" !== 'production' ? warning(false, '%s(...) is deprecated in plain JavaScript React classes. %s', info[0], info[1]) : undefined;
+ return undefined;
+ }
+ });
+ }
+ };
+ for (var fnName in deprecatedAPIs) {
+ if (deprecatedAPIs.hasOwnProperty(fnName)) {
+ defineDeprecationWarning(fnName, deprecatedAPIs[fnName]);
+ }
+ }
+}
+
+module.exports = ReactComponent;
+},{"117":117,"154":154,"161":161,"173":173,"76":76}],35:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponentBrowserEnvironment
+ */
+
+'use strict';
+
+var ReactDOMIDOperations = _dereq_(45);
+var ReactMount = _dereq_(72);
+
+/**
+ * Abstracts away all functionality of the reconciler that requires knowledge of
+ * the browser context. TODO: These callers should be refactored to avoid the
+ * need for this injection.
+ */
+var ReactComponentBrowserEnvironment = {
+
+ processChildrenUpdates: ReactDOMIDOperations.dangerouslyProcessChildrenUpdates,
+
+ replaceNodeWithMarkupByID: ReactDOMIDOperations.dangerouslyReplaceNodeWithMarkupByID,
+
+ /**
+ * If a particular environment requires that some resources be cleaned up,
+ * specify this in the injected Mixin. In the DOM, we would likely want to
+ * purge any cached node ID lookups.
+ *
+ * @private
+ */
+ unmountIDFromEnvironment: function (rootNodeID) {
+ ReactMount.purgeID(rootNodeID);
+ }
+
+};
+
+module.exports = ReactComponentBrowserEnvironment;
+},{"45":45,"72":72}],36:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponentEnvironment
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+var injected = false;
+
+var ReactComponentEnvironment = {
+
+ /**
+ * Optionally injectable environment dependent cleanup hook. (server vs.
+ * browser etc). Example: A browser system caches DOM nodes based on component
+ * ID and must remove that cache entry when this instance is unmounted.
+ */
+ unmountIDFromEnvironment: null,
+
+ /**
+ * Optionally injectable hook for swapping out mount images in the middle of
+ * the tree.
+ */
+ replaceNodeWithMarkupByID: null,
+
+ /**
+ * Optionally injectable hook for processing a queue of child updates. Will
+ * later move into MultiChildComponents.
+ */
+ processChildrenUpdates: null,
+
+ injection: {
+ injectEnvironment: function (environment) {
+ !!injected ? "production" !== 'production' ? invariant(false, 'ReactCompositeComponent: injectEnvironment() can only be called once.') : invariant(false) : undefined;
+ ReactComponentEnvironment.unmountIDFromEnvironment = environment.unmountIDFromEnvironment;
+ ReactComponentEnvironment.replaceNodeWithMarkupByID = environment.replaceNodeWithMarkupByID;
+ ReactComponentEnvironment.processChildrenUpdates = environment.processChildrenUpdates;
+ injected = true;
+ }
+ }
+
+};
+
+module.exports = ReactComponentEnvironment;
+},{"161":161}],37:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactComponentWithPureRenderMixin
+ */
+
+'use strict';
+
+var shallowCompare = _dereq_(140);
+
+/**
+ * If your React component's render function is "pure", e.g. it will render the
+ * same result given the same props and state, provide this Mixin for a
+ * considerable performance boost.
+ *
+ * Most React components have pure render functions.
+ *
+ * Example:
+ *
+ * var ReactComponentWithPureRenderMixin =
+ * require('ReactComponentWithPureRenderMixin');
+ * React.createClass({
+ * mixins: [ReactComponentWithPureRenderMixin],
+ *
+ * render: function() {
+ * return <div className={this.props.className}>foo</div>;
+ * }
+ * });
+ *
+ * Note: This only checks shallow equality for props and state. If these contain
+ * complex data structures this mixin may have false-negatives for deeper
+ * differences. Only mixin to components which have simple props and state, or
+ * use `forceUpdate()` when you know deep data structures have changed.
+ */
+var ReactComponentWithPureRenderMixin = {
+ shouldComponentUpdate: function (nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
+};
+
+module.exports = ReactComponentWithPureRenderMixin;
+},{"140":140}],38:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactCompositeComponent
+ */
+
+'use strict';
+
+var ReactComponentEnvironment = _dereq_(36);
+var ReactCurrentOwner = _dereq_(39);
+var ReactElement = _dereq_(57);
+var ReactInstanceMap = _dereq_(68);
+var ReactPerf = _dereq_(78);
+var ReactPropTypeLocations = _dereq_(81);
+var ReactPropTypeLocationNames = _dereq_(80);
+var ReactReconciler = _dereq_(84);
+var ReactUpdateQueue = _dereq_(95);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var invariant = _dereq_(161);
+var shouldUpdateReactComponent = _dereq_(141);
+var warning = _dereq_(173);
+
+function getDeclarationErrorAddendum(component) {
+ var owner = component._currentElement._owner || null;
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+function StatelessComponent(Component) {}
+StatelessComponent.prototype.render = function () {
+ var Component = ReactInstanceMap.get(this)._currentElement.type;
+ return Component(this.props, this.context, this.updater);
+};
+
+/**
+ * ------------------ The Life-Cycle of a Composite Component ------------------
+ *
+ * - constructor: Initialization of state. The instance is now retained.
+ * - componentWillMount
+ * - render
+ * - [children's constructors]
+ * - [children's componentWillMount and render]
+ * - [children's componentDidMount]
+ * - componentDidMount
+ *
+ * Update Phases:
+ * - componentWillReceiveProps (only called if parent updated)
+ * - shouldComponentUpdate
+ * - componentWillUpdate
+ * - render
+ * - [children's constructors or receive props phases]
+ * - componentDidUpdate
+ *
+ * - componentWillUnmount
+ * - [children's componentWillUnmount]
+ * - [children destroyed]
+ * - (destroyed): The instance is now blank, released by React and ready for GC.
+ *
+ * -----------------------------------------------------------------------------
+ */
+
+/**
+ * An incrementing ID assigned to each component when it is mounted. This is
+ * used to enforce the order in which `ReactUpdates` updates dirty components.
+ *
+ * @private
+ */
+var nextMountID = 1;
+
+/**
+ * @lends {ReactCompositeComponent.prototype}
+ */
+var ReactCompositeComponentMixin = {
+
+ /**
+ * Base constructor for all composite component.
+ *
+ * @param {ReactElement} element
+ * @final
+ * @internal
+ */
+ construct: function (element) {
+ this._currentElement = element;
+ this._rootNodeID = null;
+ this._instance = null;
+
+ // See ReactUpdateQueue
+ this._pendingElement = null;
+ this._pendingStateQueue = null;
+ this._pendingReplaceState = false;
+ this._pendingForceUpdate = false;
+
+ this._renderedComponent = null;
+
+ this._context = null;
+ this._mountOrder = 0;
+ this._topLevelWrapper = null;
+
+ // See ReactUpdates and ReactUpdateQueue.
+ this._pendingCallbacks = null;
+ },
+
+ /**
+ * Initializes the component, renders markup, and registers event listeners.
+ *
+ * @param {string} rootID DOM ID of the root node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @return {?string} Rendered markup to be inserted into the DOM.
+ * @final
+ * @internal
+ */
+ mountComponent: function (rootID, transaction, context) {
+ this._context = context;
+ this._mountOrder = nextMountID++;
+ this._rootNodeID = rootID;
+
+ var publicProps = this._processProps(this._currentElement.props);
+ var publicContext = this._processContext(context);
+
+ var Component = this._currentElement.type;
+
+ // Initialize the public class
+ var inst;
+ var renderedElement;
+
+ // This is a way to detect if Component is a stateless arrow function
+ // component, which is not newable. It might not be 100% reliable but is
+ // something we can do until we start detecting that Component extends
+ // React.Component. We already assume that typeof Component === 'function'.
+ var canInstantiate = ('prototype' in Component);
+
+ if (canInstantiate) {
+ if ("production" !== 'production') {
+ ReactCurrentOwner.current = this;
+ try {
+ inst = new Component(publicProps, publicContext, ReactUpdateQueue);
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ } else {
+ inst = new Component(publicProps, publicContext, ReactUpdateQueue);
+ }
+ }
+
+ if (!canInstantiate || inst === null || inst === false || ReactElement.isValidElement(inst)) {
+ renderedElement = inst;
+ inst = new StatelessComponent(Component);
+ }
+
+ if ("production" !== 'production') {
+ // This will throw later in _renderValidatedComponent, but add an early
+ // warning now to help debugging
+ if (inst.render == null) {
+ "production" !== 'production' ? warning(false, '%s(...): No `render` method found on the returned component ' + 'instance: you may have forgotten to define `render`, returned ' + 'null/false from a stateless component, or tried to render an ' + 'element whose type is a function that isn\'t a React component.', Component.displayName || Component.name || 'Component') : undefined;
+ } else {
+ // We support ES6 inheriting from React.Component, the module pattern,
+ // and stateless components, but not ES6 classes that don't extend
+ "production" !== 'production' ? warning(Component.prototype && Component.prototype.isReactComponent || !canInstantiate || !(inst instanceof Component), '%s(...): React component classes must extend React.Component.', Component.displayName || Component.name || 'Component') : undefined;
+ }
+ }
+
+ // These should be set up in the constructor, but as a convenience for
+ // simpler class abstractions, we set them up after the fact.
+ inst.props = publicProps;
+ inst.context = publicContext;
+ inst.refs = emptyObject;
+ inst.updater = ReactUpdateQueue;
+
+ this._instance = inst;
+
+ // Store a reference from the instance back to the internal representation
+ ReactInstanceMap.set(inst, this);
+
+ if ("production" !== 'production') {
+ // Since plain JS classes are defined without any special initialization
+ // logic, we can not catch common errors early. Therefore, we have to
+ // catch them here, at initialization time, instead.
+ "production" !== 'production' ? warning(!inst.getInitialState || inst.getInitialState.isReactClassApproved, 'getInitialState was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Did you mean to define a state property instead?', this.getName() || 'a component') : undefined;
+ "production" !== 'production' ? warning(!inst.getDefaultProps || inst.getDefaultProps.isReactClassApproved, 'getDefaultProps was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Use a static property to define defaultProps instead.', this.getName() || 'a component') : undefined;
+ "production" !== 'production' ? warning(!inst.propTypes, 'propTypes was defined as an instance property on %s. Use a static ' + 'property to define propTypes instead.', this.getName() || 'a component') : undefined;
+ "production" !== 'production' ? warning(!inst.contextTypes, 'contextTypes was defined as an instance property on %s. Use a ' + 'static property to define contextTypes instead.', this.getName() || 'a component') : undefined;
+ "production" !== 'production' ? warning(typeof inst.componentShouldUpdate !== 'function', '%s has a method called ' + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 'The name is phrased as a question because the function is ' + 'expected to return a value.', this.getName() || 'A component') : undefined;
+ "production" !== 'production' ? warning(typeof inst.componentDidUnmount !== 'function', '%s has a method called ' + 'componentDidUnmount(). But there is no such lifecycle method. ' + 'Did you mean componentWillUnmount()?', this.getName() || 'A component') : undefined;
+ "production" !== 'production' ? warning(typeof inst.componentWillRecieveProps !== 'function', '%s has a method called ' + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', this.getName() || 'A component') : undefined;
+ }
+
+ var initialState = inst.state;
+ if (initialState === undefined) {
+ inst.state = initialState = null;
+ }
+ !(typeof initialState === 'object' && !Array.isArray(initialState)) ? "production" !== 'production' ? invariant(false, '%s.state: must be set to an object or null', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined;
+
+ this._pendingStateQueue = null;
+ this._pendingReplaceState = false;
+ this._pendingForceUpdate = false;
+
+ if (inst.componentWillMount) {
+ inst.componentWillMount();
+ // When mounting, calls to `setState` by `componentWillMount` will set
+ // `this._pendingStateQueue` without triggering a re-render.
+ if (this._pendingStateQueue) {
+ inst.state = this._processPendingState(inst.props, inst.context);
+ }
+ }
+
+ // If not a stateless component, we now render
+ if (renderedElement === undefined) {
+ renderedElement = this._renderValidatedComponent();
+ }
+
+ this._renderedComponent = this._instantiateReactComponent(renderedElement);
+
+ var markup = ReactReconciler.mountComponent(this._renderedComponent, rootID, transaction, this._processChildContext(context));
+ if (inst.componentDidMount) {
+ transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
+ }
+
+ return markup;
+ },
+
+ /**
+ * Releases any resources allocated by `mountComponent`.
+ *
+ * @final
+ * @internal
+ */
+ unmountComponent: function () {
+ var inst = this._instance;
+
+ if (inst.componentWillUnmount) {
+ inst.componentWillUnmount();
+ }
+
+ ReactReconciler.unmountComponent(this._renderedComponent);
+ this._renderedComponent = null;
+ this._instance = null;
+
+ // Reset pending fields
+ // Even if this component is scheduled for another update in ReactUpdates,
+ // it would still be ignored because these fields are reset.
+ this._pendingStateQueue = null;
+ this._pendingReplaceState = false;
+ this._pendingForceUpdate = false;
+ this._pendingCallbacks = null;
+ this._pendingElement = null;
+
+ // These fields do not really need to be reset since this object is no
+ // longer accessible.
+ this._context = null;
+ this._rootNodeID = null;
+ this._topLevelWrapper = null;
+
+ // Delete the reference from the instance to this internal representation
+ // which allow the internals to be properly cleaned up even if the user
+ // leaks a reference to the public instance.
+ ReactInstanceMap.remove(inst);
+
+ // Some existing components rely on inst.props even after they've been
+ // destroyed (in event handlers).
+ // TODO: inst.props = null;
+ // TODO: inst.state = null;
+ // TODO: inst.context = null;
+ },
+
+ /**
+ * Filters the context object to only contain keys specified in
+ * `contextTypes`
+ *
+ * @param {object} context
+ * @return {?object}
+ * @private
+ */
+ _maskContext: function (context) {
+ var maskedContext = null;
+ var Component = this._currentElement.type;
+ var contextTypes = Component.contextTypes;
+ if (!contextTypes) {
+ return emptyObject;
+ }
+ maskedContext = {};
+ for (var contextName in contextTypes) {
+ maskedContext[contextName] = context[contextName];
+ }
+ return maskedContext;
+ },
+
+ /**
+ * Filters the context object to only contain keys specified in
+ * `contextTypes`, and asserts that they are valid.
+ *
+ * @param {object} context
+ * @return {?object}
+ * @private
+ */
+ _processContext: function (context) {
+ var maskedContext = this._maskContext(context);
+ if ("production" !== 'production') {
+ var Component = this._currentElement.type;
+ if (Component.contextTypes) {
+ this._checkPropTypes(Component.contextTypes, maskedContext, ReactPropTypeLocations.context);
+ }
+ }
+ return maskedContext;
+ },
+
+ /**
+ * @param {object} currentContext
+ * @return {object}
+ * @private
+ */
+ _processChildContext: function (currentContext) {
+ var Component = this._currentElement.type;
+ var inst = this._instance;
+ var childContext = inst.getChildContext && inst.getChildContext();
+ if (childContext) {
+ !(typeof Component.childContextTypes === 'object') ? "production" !== 'production' ? invariant(false, '%s.getChildContext(): childContextTypes must be defined in order to ' + 'use getChildContext().', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined;
+ if ("production" !== 'production') {
+ this._checkPropTypes(Component.childContextTypes, childContext, ReactPropTypeLocations.childContext);
+ }
+ for (var name in childContext) {
+ !(name in Component.childContextTypes) ? "production" !== 'production' ? invariant(false, '%s.getChildContext(): key "%s" is not defined in childContextTypes.', this.getName() || 'ReactCompositeComponent', name) : invariant(false) : undefined;
+ }
+ return assign({}, currentContext, childContext);
+ }
+ return currentContext;
+ },
+
+ /**
+ * Processes props by setting default values for unspecified props and
+ * asserting that the props are valid. Does not mutate its argument; returns
+ * a new props object with defaults merged in.
+ *
+ * @param {object} newProps
+ * @return {object}
+ * @private
+ */
+ _processProps: function (newProps) {
+ if ("production" !== 'production') {
+ var Component = this._currentElement.type;
+ if (Component.propTypes) {
+ this._checkPropTypes(Component.propTypes, newProps, ReactPropTypeLocations.prop);
+ }
+ }
+ return newProps;
+ },
+
+ /**
+ * Assert that the props are valid
+ *
+ * @param {object} propTypes Map of prop name to a ReactPropType
+ * @param {object} props
+ * @param {string} location e.g. "prop", "context", "child context"
+ * @private
+ */
+ _checkPropTypes: function (propTypes, props, location) {
+ // TODO: Stop validating prop types here and only use the element
+ // validation.
+ var componentName = this.getName();
+ for (var propName in propTypes) {
+ if (propTypes.hasOwnProperty(propName)) {
+ var error;
+ try {
+ // This is intentionally an invariant that gets caught. It's the same
+ // behavior as without this statement except with a better message.
+ !(typeof propTypes[propName] === 'function') ? "production" !== 'production' ? invariant(false, '%s: %s type `%s` is invalid; it must be a function, usually ' + 'from React.PropTypes.', componentName || 'React class', ReactPropTypeLocationNames[location], propName) : invariant(false) : undefined;
+ error = propTypes[propName](props, propName, componentName, location);
+ } catch (ex) {
+ error = ex;
+ }
+ if (error instanceof Error) {
+ // We may want to extend this logic for similar errors in
+ // top-level render calls, so I'm abstracting it away into
+ // a function to minimize refactoring in the future
+ var addendum = getDeclarationErrorAddendum(this);
+
+ if (location === ReactPropTypeLocations.prop) {
+ // Preface gives us something to blacklist in warning module
+ "production" !== 'production' ? warning(false, 'Failed Composite propType: %s%s', error.message, addendum) : undefined;
+ } else {
+ "production" !== 'production' ? warning(false, 'Failed Context Types: %s%s', error.message, addendum) : undefined;
+ }
+ }
+ }
+ }
+ },
+
+ receiveComponent: function (nextElement, transaction, nextContext) {
+ var prevElement = this._currentElement;
+ var prevContext = this._context;
+
+ this._pendingElement = null;
+
+ this.updateComponent(transaction, prevElement, nextElement, prevContext, nextContext);
+ },
+
+ /**
+ * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate`
+ * is set, update the component.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ performUpdateIfNecessary: function (transaction) {
+ if (this._pendingElement != null) {
+ ReactReconciler.receiveComponent(this, this._pendingElement || this._currentElement, transaction, this._context);
+ }
+
+ if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
+ this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
+ }
+ },
+
+ /**
+ * Perform an update to a mounted component. The componentWillReceiveProps and
+ * shouldComponentUpdate methods are called, then (assuming the update isn't
+ * skipped) the remaining update lifecycle methods are called and the DOM
+ * representation is updated.
+ *
+ * By default, this implements React's rendering and reconciliation algorithm.
+ * Sophisticated clients may wish to override this.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @param {ReactElement} prevParentElement
+ * @param {ReactElement} nextParentElement
+ * @internal
+ * @overridable
+ */
+ updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
+ var inst = this._instance;
+
+ var nextContext = this._context === nextUnmaskedContext ? inst.context : this._processContext(nextUnmaskedContext);
+ var nextProps;
+
+ // Distinguish between a props update versus a simple state update
+ if (prevParentElement === nextParentElement) {
+ // Skip checking prop types again -- we don't read inst.props to avoid
+ // warning for DOM component props in this upgrade
+ nextProps = nextParentElement.props;
+ } else {
+ nextProps = this._processProps(nextParentElement.props);
+ // An update here will schedule an update but immediately set
+ // _pendingStateQueue which will ensure that any state updates gets
+ // immediately reconciled instead of waiting for the next batch.
+
+ if (inst.componentWillReceiveProps) {
+ inst.componentWillReceiveProps(nextProps, nextContext);
+ }
+ }
+
+ var nextState = this._processPendingState(nextProps, nextContext);
+
+ var shouldUpdate = this._pendingForceUpdate || !inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext);
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(typeof shouldUpdate !== 'undefined', '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', this.getName() || 'ReactCompositeComponent') : undefined;
+ }
+
+ if (shouldUpdate) {
+ this._pendingForceUpdate = false;
+ // Will set `this.props`, `this.state` and `this.context`.
+ this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext);
+ } else {
+ // If it's determined that a component should not update, we still want
+ // to set props and state but we shortcut the rest of the update.
+ this._currentElement = nextParentElement;
+ this._context = nextUnmaskedContext;
+ inst.props = nextProps;
+ inst.state = nextState;
+ inst.context = nextContext;
+ }
+ },
+
+ _processPendingState: function (props, context) {
+ var inst = this._instance;
+ var queue = this._pendingStateQueue;
+ var replace = this._pendingReplaceState;
+ this._pendingReplaceState = false;
+ this._pendingStateQueue = null;
+
+ if (!queue) {
+ return inst.state;
+ }
+
+ if (replace && queue.length === 1) {
+ return queue[0];
+ }
+
+ var nextState = assign({}, replace ? queue[0] : inst.state);
+ for (var i = replace ? 1 : 0; i < queue.length; i++) {
+ var partial = queue[i];
+ assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
+ }
+
+ return nextState;
+ },
+
+ /**
+ * Merges new props and state, notifies delegate methods of update and
+ * performs update.
+ *
+ * @param {ReactElement} nextElement Next element
+ * @param {object} nextProps Next public object to set as properties.
+ * @param {?object} nextState Next object to set as state.
+ * @param {?object} nextContext Next public object to set as context.
+ * @param {ReactReconcileTransaction} transaction
+ * @param {?object} unmaskedContext
+ * @private
+ */
+ _performComponentUpdate: function (nextElement, nextProps, nextState, nextContext, transaction, unmaskedContext) {
+ var inst = this._instance;
+
+ var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
+ var prevProps;
+ var prevState;
+ var prevContext;
+ if (hasComponentDidUpdate) {
+ prevProps = inst.props;
+ prevState = inst.state;
+ prevContext = inst.context;
+ }
+
+ if (inst.componentWillUpdate) {
+ inst.componentWillUpdate(nextProps, nextState, nextContext);
+ }
+
+ this._currentElement = nextElement;
+ this._context = unmaskedContext;
+ inst.props = nextProps;
+ inst.state = nextState;
+ inst.context = nextContext;
+
+ this._updateRenderedComponent(transaction, unmaskedContext);
+
+ if (hasComponentDidUpdate) {
+ transaction.getReactMountReady().enqueue(inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), inst);
+ }
+ },
+
+ /**
+ * Call the component's `render` method and update the DOM accordingly.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ _updateRenderedComponent: function (transaction, context) {
+ var prevComponentInstance = this._renderedComponent;
+ var prevRenderedElement = prevComponentInstance._currentElement;
+ var nextRenderedElement = this._renderValidatedComponent();
+ if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
+ ReactReconciler.receiveComponent(prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context));
+ } else {
+ // These two IDs are actually the same! But nothing should rely on that.
+ var thisID = this._rootNodeID;
+ var prevComponentID = prevComponentInstance._rootNodeID;
+ ReactReconciler.unmountComponent(prevComponentInstance);
+
+ this._renderedComponent = this._instantiateReactComponent(nextRenderedElement);
+ var nextMarkup = ReactReconciler.mountComponent(this._renderedComponent, thisID, transaction, this._processChildContext(context));
+ this._replaceNodeWithMarkupByID(prevComponentID, nextMarkup);
+ }
+ },
+
+ /**
+ * @protected
+ */
+ _replaceNodeWithMarkupByID: function (prevComponentID, nextMarkup) {
+ ReactComponentEnvironment.replaceNodeWithMarkupByID(prevComponentID, nextMarkup);
+ },
+
+ /**
+ * @protected
+ */
+ _renderValidatedComponentWithoutOwnerOrContext: function () {
+ var inst = this._instance;
+ var renderedComponent = inst.render();
+ if ("production" !== 'production') {
+ // We allow auto-mocks to proceed as if they're returning null.
+ if (typeof renderedComponent === 'undefined' && inst.render._isMockFunction) {
+ // This is probably bad practice. Consider warning here and
+ // deprecating this convenience.
+ renderedComponent = null;
+ }
+ }
+
+ return renderedComponent;
+ },
+
+ /**
+ * @private
+ */
+ _renderValidatedComponent: function () {
+ var renderedComponent;
+ ReactCurrentOwner.current = this;
+ try {
+ renderedComponent = this._renderValidatedComponentWithoutOwnerOrContext();
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ !(
+ // TODO: An `isValidNode` function would probably be more appropriate
+ renderedComponent === null || renderedComponent === false || ReactElement.isValidElement(renderedComponent)) ? "production" !== 'production' ? invariant(false, '%s.render(): A valid ReactComponent must be returned. You may have ' + 'returned undefined, an array or some other invalid object.', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined;
+ return renderedComponent;
+ },
+
+ /**
+ * Lazily allocates the refs object and stores `component` as `ref`.
+ *
+ * @param {string} ref Reference name.
+ * @param {component} component Component to store as `ref`.
+ * @final
+ * @private
+ */
+ attachRef: function (ref, component) {
+ var inst = this.getPublicInstance();
+ !(inst != null) ? "production" !== 'production' ? invariant(false, 'Stateless function components cannot have refs.') : invariant(false) : undefined;
+ var publicComponentInstance = component.getPublicInstance();
+ if ("production" !== 'production') {
+ var componentName = component && component.getName ? component.getName() : 'a component';
+ "production" !== 'production' ? warning(publicComponentInstance != null, 'Stateless function components cannot be given refs ' + '(See ref "%s" in %s created by %s). ' + 'Attempts to access this ref will fail.', ref, componentName, this.getName()) : undefined;
+ }
+ var refs = inst.refs === emptyObject ? inst.refs = {} : inst.refs;
+ refs[ref] = publicComponentInstance;
+ },
+
+ /**
+ * Detaches a reference name.
+ *
+ * @param {string} ref Name to dereference.
+ * @final
+ * @private
+ */
+ detachRef: function (ref) {
+ var refs = this.getPublicInstance().refs;
+ delete refs[ref];
+ },
+
+ /**
+ * Get a text description of the component that can be used to identify it
+ * in error messages.
+ * @return {string} The name or null.
+ * @internal
+ */
+ getName: function () {
+ var type = this._currentElement.type;
+ var constructor = this._instance && this._instance.constructor;
+ return type.displayName || constructor && constructor.displayName || type.name || constructor && constructor.name || null;
+ },
+
+ /**
+ * Get the publicly accessible representation of this component - i.e. what
+ * is exposed by refs and returned by render. Can be null for stateless
+ * components.
+ *
+ * @return {ReactComponent} the public component instance.
+ * @internal
+ */
+ getPublicInstance: function () {
+ var inst = this._instance;
+ if (inst instanceof StatelessComponent) {
+ return null;
+ }
+ return inst;
+ },
+
+ // Stub
+ _instantiateReactComponent: null
+
+};
+
+ReactPerf.measureMethods(ReactCompositeComponentMixin, 'ReactCompositeComponent', {
+ mountComponent: 'mountComponent',
+ updateComponent: 'updateComponent',
+ _renderValidatedComponent: '_renderValidatedComponent'
+});
+
+var ReactCompositeComponent = {
+
+ Mixin: ReactCompositeComponentMixin
+
+};
+
+module.exports = ReactCompositeComponent;
+},{"141":141,"154":154,"161":161,"173":173,"24":24,"36":36,"39":39,"57":57,"68":68,"78":78,"80":80,"81":81,"84":84,"95":95}],39:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactCurrentOwner
+ */
+
+'use strict';
+
+/**
+ * Keeps track of the current owner.
+ *
+ * The current owner is the component who should own any components that are
+ * currently being constructed.
+ */
+var ReactCurrentOwner = {
+
+ /**
+ * @internal
+ * @type {ReactComponent}
+ */
+ current: null
+
+};
+
+module.exports = ReactCurrentOwner;
+},{}],40:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOM
+ */
+
+/* globals __REACT_DEVTOOLS_GLOBAL_HOOK__*/
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactDOMTextComponent = _dereq_(51);
+var ReactDefaultInjection = _dereq_(54);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMount = _dereq_(72);
+var ReactPerf = _dereq_(78);
+var ReactReconciler = _dereq_(84);
+var ReactUpdates = _dereq_(96);
+var ReactVersion = _dereq_(97);
+
+var findDOMNode = _dereq_(122);
+var renderSubtreeIntoContainer = _dereq_(137);
+var warning = _dereq_(173);
+
+ReactDefaultInjection.inject();
+
+var render = ReactPerf.measure('React', 'render', ReactMount.render);
+
+var React = {
+ findDOMNode: findDOMNode,
+ render: render,
+ unmountComponentAtNode: ReactMount.unmountComponentAtNode,
+ version: ReactVersion,
+
+ /* eslint-disable camelcase */
+ unstable_batchedUpdates: ReactUpdates.batchedUpdates,
+ unstable_renderSubtreeIntoContainer: renderSubtreeIntoContainer
+};
+
+// Inject the runtime into a devtools global hook regardless of browser.
+// Allows for debugging when the hook is injected on the page.
+/* eslint-enable camelcase */
+if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') {
+ __REACT_DEVTOOLS_GLOBAL_HOOK__.inject({
+ CurrentOwner: ReactCurrentOwner,
+ InstanceHandles: ReactInstanceHandles,
+ Mount: ReactMount,
+ Reconciler: ReactReconciler,
+ TextComponent: ReactDOMTextComponent
+ });
+}
+
+if ("production" !== 'production') {
+ var ExecutionEnvironment = _dereq_(147);
+ if (ExecutionEnvironment.canUseDOM && window.top === window.self) {
+
+ // First check if devtools is not installed
+ if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
+ // If we're in Chrome or Firefox, provide a download link if not installed.
+ if (navigator.userAgent.indexOf('Chrome') > -1 && navigator.userAgent.indexOf('Edge') === -1 || navigator.userAgent.indexOf('Firefox') > -1) {
+ console.debug('Download the React DevTools for a better development experience: ' + 'https://fb.me/react-devtools');
+ }
+ }
+
+ // If we're in IE8, check to see if we are in compatibility mode and provide
+ // information on preventing compatibility mode
+ var ieCompatibilityMode = document.documentMode && document.documentMode < 8;
+
+ "production" !== 'production' ? warning(!ieCompatibilityMode, 'Internet Explorer is running in compatibility mode; please add the ' + 'following tag to your HTML to prevent this from happening: ' + '<meta http-equiv="X-UA-Compatible" content="IE=edge" />') : undefined;
+
+ var expectedFeatures = [
+ // shims
+ Array.isArray, Array.prototype.every, Array.prototype.forEach, Array.prototype.indexOf, Array.prototype.map, Date.now, Function.prototype.bind, Object.keys, String.prototype.split, String.prototype.trim,
+
+ // shams
+ Object.create, Object.freeze];
+
+ for (var i = 0; i < expectedFeatures.length; i++) {
+ if (!expectedFeatures[i]) {
+ console.error('One or more ES5 shim/shams expected by React are not available: ' + 'https://fb.me/react-warning-polyfills');
+ break;
+ }
+ }
+ }
+}
+
+module.exports = React;
+},{"122":122,"137":137,"147":147,"173":173,"39":39,"51":51,"54":54,"67":67,"72":72,"78":78,"84":84,"96":96,"97":97}],41:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMButton
+ */
+
+'use strict';
+
+var mouseListenerNames = {
+ onClick: true,
+ onDoubleClick: true,
+ onMouseDown: true,
+ onMouseMove: true,
+ onMouseUp: true,
+
+ onClickCapture: true,
+ onDoubleClickCapture: true,
+ onMouseDownCapture: true,
+ onMouseMoveCapture: true,
+ onMouseUpCapture: true
+};
+
+/**
+ * Implements a <button> native component that does not receive mouse events
+ * when `disabled` is set.
+ */
+var ReactDOMButton = {
+ getNativeProps: function (inst, props, context) {
+ if (!props.disabled) {
+ return props;
+ }
+
+ // Copy the props, except the mouse listeners
+ var nativeProps = {};
+ for (var key in props) {
+ if (props.hasOwnProperty(key) && !mouseListenerNames[key]) {
+ nativeProps[key] = props[key];
+ }
+ }
+
+ return nativeProps;
+ }
+};
+
+module.exports = ReactDOMButton;
+},{}],42:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMComponent
+ * @typechecks static-only
+ */
+
+/* global hasOwnProperty:true */
+
+'use strict';
+
+var AutoFocusUtils = _dereq_(2);
+var CSSPropertyOperations = _dereq_(5);
+var DOMProperty = _dereq_(10);
+var DOMPropertyOperations = _dereq_(11);
+var EventConstants = _dereq_(15);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactComponentBrowserEnvironment = _dereq_(35);
+var ReactDOMButton = _dereq_(41);
+var ReactDOMInput = _dereq_(46);
+var ReactDOMOption = _dereq_(47);
+var ReactDOMSelect = _dereq_(48);
+var ReactDOMTextarea = _dereq_(52);
+var ReactMount = _dereq_(72);
+var ReactMultiChild = _dereq_(73);
+var ReactPerf = _dereq_(78);
+var ReactUpdateQueue = _dereq_(95);
+
+var assign = _dereq_(24);
+var canDefineProperty = _dereq_(117);
+var escapeTextContentForBrowser = _dereq_(121);
+var invariant = _dereq_(161);
+var isEventSupported = _dereq_(133);
+var keyOf = _dereq_(166);
+var setInnerHTML = _dereq_(138);
+var setTextContent = _dereq_(139);
+var shallowEqual = _dereq_(171);
+var validateDOMNesting = _dereq_(144);
+var warning = _dereq_(173);
+
+var deleteListener = ReactBrowserEventEmitter.deleteListener;
+var listenTo = ReactBrowserEventEmitter.listenTo;
+var registrationNameModules = ReactBrowserEventEmitter.registrationNameModules;
+
+// For quickly matching children type, to test if can be treated as content.
+var CONTENT_TYPES = { 'string': true, 'number': true };
+
+var CHILDREN = keyOf({ children: null });
+var STYLE = keyOf({ style: null });
+var HTML = keyOf({ __html: null });
+
+var ELEMENT_NODE_TYPE = 1;
+
+function getDeclarationErrorAddendum(internalInstance) {
+ if (internalInstance) {
+ var owner = internalInstance._currentElement._owner || null;
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' This DOM node was rendered by `' + name + '`.';
+ }
+ }
+ }
+ return '';
+}
+
+var legacyPropsDescriptor;
+if ("production" !== 'production') {
+ legacyPropsDescriptor = {
+ props: {
+ enumerable: false,
+ get: function () {
+ var component = this._reactInternalComponent;
+ "production" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .props of a DOM node; instead, ' + 'recreate the props as `render` did originally or read the DOM ' + 'properties/attributes directly from this node (e.g., ' + 'this.refs.box.className).%s', getDeclarationErrorAddendum(component)) : undefined;
+ return component._currentElement.props;
+ }
+ }
+ };
+}
+
+function legacyGetDOMNode() {
+ if ("production" !== 'production') {
+ var component = this._reactInternalComponent;
+ "production" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .getDOMNode() of a DOM node; ' + 'instead, use the node directly.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ return this;
+}
+
+function legacyIsMounted() {
+ var component = this._reactInternalComponent;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .isMounted() of a DOM node.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ return !!component;
+}
+
+function legacySetStateEtc() {
+ if ("production" !== 'production') {
+ var component = this._reactInternalComponent;
+ "production" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .setState(), .replaceState(), or ' + '.forceUpdate() of a DOM node. This is a no-op.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+}
+
+function legacySetProps(partialProps, callback) {
+ var component = this._reactInternalComponent;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .setProps() of a DOM node. ' + 'Instead, call ReactDOM.render again at the top level.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ if (!component) {
+ return;
+ }
+ ReactUpdateQueue.enqueueSetPropsInternal(component, partialProps);
+ if (callback) {
+ ReactUpdateQueue.enqueueCallbackInternal(component, callback);
+ }
+}
+
+function legacyReplaceProps(partialProps, callback) {
+ var component = this._reactInternalComponent;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(false, 'ReactDOMComponent: Do not access .replaceProps() of a DOM node. ' + 'Instead, call ReactDOM.render again at the top level.%s', getDeclarationErrorAddendum(component)) : undefined;
+ }
+ if (!component) {
+ return;
+ }
+ ReactUpdateQueue.enqueueReplacePropsInternal(component, partialProps);
+ if (callback) {
+ ReactUpdateQueue.enqueueCallbackInternal(component, callback);
+ }
+}
+
+function friendlyStringify(obj) {
+ if (typeof obj === 'object') {
+ if (Array.isArray(obj)) {
+ return '[' + obj.map(friendlyStringify).join(', ') + ']';
+ } else {
+ var pairs = [];
+ for (var key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ var keyEscaped = /^[a-z$_][\w$_]*$/i.test(key) ? key : JSON.stringify(key);
+ pairs.push(keyEscaped + ': ' + friendlyStringify(obj[key]));
+ }
+ }
+ return '{' + pairs.join(', ') + '}';
+ }
+ } else if (typeof obj === 'string') {
+ return JSON.stringify(obj);
+ } else if (typeof obj === 'function') {
+ return '[function object]';
+ }
+ // Differs from JSON.stringify in that undefined becauses undefined and that
+ // inf and nan don't become null
+ return String(obj);
+}
+
+var styleMutationWarning = {};
+
+function checkAndWarnForMutatedStyle(style1, style2, component) {
+ if (style1 == null || style2 == null) {
+ return;
+ }
+ if (shallowEqual(style1, style2)) {
+ return;
+ }
+
+ var componentName = component._tag;
+ var owner = component._currentElement._owner;
+ var ownerName;
+ if (owner) {
+ ownerName = owner.getName();
+ }
+
+ var hash = ownerName + '|' + componentName;
+
+ if (styleMutationWarning.hasOwnProperty(hash)) {
+ return;
+ }
+
+ styleMutationWarning[hash] = true;
+
+ "production" !== 'production' ? warning(false, '`%s` was passed a style object that has previously been mutated. ' + 'Mutating `style` is deprecated. Consider cloning it beforehand. Check ' + 'the `render` %s. Previous style: %s. Mutated style: %s.', componentName, owner ? 'of `' + ownerName + '`' : 'using <' + componentName + '>', friendlyStringify(style1), friendlyStringify(style2)) : undefined;
+}
+
+/**
+ * @param {object} component
+ * @param {?object} props
+ */
+function assertValidProps(component, props) {
+ if (!props) {
+ return;
+ }
+ // Note the use of `==` which checks for null or undefined.
+ if ("production" !== 'production') {
+ if (voidElementTags[component._tag]) {
+ "production" !== 'production' ? warning(props.children == null && props.dangerouslySetInnerHTML == null, '%s is a void element tag and must not have `children` or ' + 'use `props.dangerouslySetInnerHTML`.%s', component._tag, component._currentElement._owner ? ' Check the render method of ' + component._currentElement._owner.getName() + '.' : '') : undefined;
+ }
+ }
+ if (props.dangerouslySetInnerHTML != null) {
+ !(props.children == null) ? "production" !== 'production' ? invariant(false, 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.') : invariant(false) : undefined;
+ !(typeof props.dangerouslySetInnerHTML === 'object' && HTML in props.dangerouslySetInnerHTML) ? "production" !== 'production' ? invariant(false, '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + 'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' + 'for more information.') : invariant(false) : undefined;
+ }
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(props.innerHTML == null, 'Directly setting property `innerHTML` is not permitted. ' + 'For more information, lookup documentation on `dangerouslySetInnerHTML`.') : undefined;
+ "production" !== 'production' ? warning(!props.contentEditable || props.children == null, 'A component is `contentEditable` and contains `children` managed by ' + 'React. It is now your responsibility to guarantee that none of ' + 'those nodes are unexpectedly modified or duplicated. This is ' + 'probably not intentional.') : undefined;
+ }
+ !(props.style == null || typeof props.style === 'object') ? "production" !== 'production' ? invariant(false, 'The `style` prop expects a mapping from style properties to values, ' + 'not a string. For example, style={{marginRight: spacing + \'em\'}} when ' + 'using JSX.%s', getDeclarationErrorAddendum(component)) : invariant(false) : undefined;
+}
+
+function enqueuePutListener(id, registrationName, listener, transaction) {
+ if ("production" !== 'production') {
+ // IE8 has no API for event capturing and the `onScroll` event doesn't
+ // bubble.
+ "production" !== 'production' ? warning(registrationName !== 'onScroll' || isEventSupported('scroll', true), 'This browser doesn\'t support the `onScroll` event') : undefined;
+ }
+ var container = ReactMount.findReactContainerForID(id);
+ if (container) {
+ var doc = container.nodeType === ELEMENT_NODE_TYPE ? container.ownerDocument : container;
+ listenTo(registrationName, doc);
+ }
+ transaction.getReactMountReady().enqueue(putListener, {
+ id: id,
+ registrationName: registrationName,
+ listener: listener
+ });
+}
+
+function putListener() {
+ var listenerToPut = this;
+ ReactBrowserEventEmitter.putListener(listenerToPut.id, listenerToPut.registrationName, listenerToPut.listener);
+}
+
+// There are so many media events, it makes sense to just
+// maintain a list rather than create a `trapBubbledEvent` for each
+var mediaEvents = {
+ topAbort: 'abort',
+ topCanPlay: 'canplay',
+ topCanPlayThrough: 'canplaythrough',
+ topDurationChange: 'durationchange',
+ topEmptied: 'emptied',
+ topEncrypted: 'encrypted',
+ topEnded: 'ended',
+ topError: 'error',
+ topLoadedData: 'loadeddata',
+ topLoadedMetadata: 'loadedmetadata',
+ topLoadStart: 'loadstart',
+ topPause: 'pause',
+ topPlay: 'play',
+ topPlaying: 'playing',
+ topProgress: 'progress',
+ topRateChange: 'ratechange',
+ topSeeked: 'seeked',
+ topSeeking: 'seeking',
+ topStalled: 'stalled',
+ topSuspend: 'suspend',
+ topTimeUpdate: 'timeupdate',
+ topVolumeChange: 'volumechange',
+ topWaiting: 'waiting'
+};
+
+function trapBubbledEventsLocal() {
+ var inst = this;
+ // If a component renders to null or if another component fatals and causes
+ // the state of the tree to be corrupted, `node` here can be null.
+ !inst._rootNodeID ? "production" !== 'production' ? invariant(false, 'Must be mounted to trap events') : invariant(false) : undefined;
+ var node = ReactMount.getNode(inst._rootNodeID);
+ !node ? "production" !== 'production' ? invariant(false, 'trapBubbledEvent(...): Requires node to be rendered.') : invariant(false) : undefined;
+
+ switch (inst._tag) {
+ case 'iframe':
+ inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topLoad, 'load', node)];
+ break;
+ case 'video':
+ case 'audio':
+
+ inst._wrapperState.listeners = [];
+ // create listener for each media event
+ for (var event in mediaEvents) {
+ if (mediaEvents.hasOwnProperty(event)) {
+ inst._wrapperState.listeners.push(ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes[event], mediaEvents[event], node));
+ }
+ }
+
+ break;
+ case 'img':
+ inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topError, 'error', node), ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topLoad, 'load', node)];
+ break;
+ case 'form':
+ inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topReset, 'reset', node), ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topSubmit, 'submit', node)];
+ break;
+ }
+}
+
+function mountReadyInputWrapper() {
+ ReactDOMInput.mountReadyWrapper(this);
+}
+
+function postUpdateSelectWrapper() {
+ ReactDOMSelect.postUpdateWrapper(this);
+}
+
+// For HTML, certain tags should omit their close tag. We keep a whitelist for
+// those special cased tags.
+
+var omittedCloseTags = {
+ 'area': true,
+ 'base': true,
+ 'br': true,
+ 'col': true,
+ 'embed': true,
+ 'hr': true,
+ 'img': true,
+ 'input': true,
+ 'keygen': true,
+ 'link': true,
+ 'meta': true,
+ 'param': true,
+ 'source': true,
+ 'track': true,
+ 'wbr': true
+};
+
+// NOTE: menuitem's close tag should be omitted, but that causes problems.
+var newlineEatingTags = {
+ 'listing': true,
+ 'pre': true,
+ 'textarea': true
+};
+
+// For HTML, certain tags cannot have children. This has the same purpose as
+// `omittedCloseTags` except that `menuitem` should still have its closing tag.
+
+var voidElementTags = assign({
+ 'menuitem': true
+}, omittedCloseTags);
+
+// We accept any tag to be rendered but since this gets injected into arbitrary
+// HTML, we want to make sure that it's a safe tag.
+// http://www.w3.org/TR/REC-xml/#NT-Name
+
+var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset
+var validatedTagCache = {};
+var hasOwnProperty = ({}).hasOwnProperty;
+
+function validateDangerousTag(tag) {
+ if (!hasOwnProperty.call(validatedTagCache, tag)) {
+ !VALID_TAG_REGEX.test(tag) ? "production" !== 'production' ? invariant(false, 'Invalid tag: %s', tag) : invariant(false) : undefined;
+ validatedTagCache[tag] = true;
+ }
+}
+
+function processChildContextDev(context, inst) {
+ // Pass down our tag name to child components for validation purposes
+ context = assign({}, context);
+ var info = context[validateDOMNesting.ancestorInfoContextKey];
+ context[validateDOMNesting.ancestorInfoContextKey] = validateDOMNesting.updatedAncestorInfo(info, inst._tag, inst);
+ return context;
+}
+
+function isCustomComponent(tagName, props) {
+ return tagName.indexOf('-') >= 0 || props.is != null;
+}
+
+/**
+ * Creates a new React class that is idempotent and capable of containing other
+ * React components. It accepts event listeners and DOM properties that are
+ * valid according to `DOMProperty`.
+ *
+ * - Event listeners: `onClick`, `onMouseDown`, etc.
+ * - DOM properties: `className`, `name`, `title`, etc.
+ *
+ * The `style` property functions differently from the DOM API. It accepts an
+ * object mapping of style properties to values.
+ *
+ * @constructor ReactDOMComponent
+ * @extends ReactMultiChild
+ */
+function ReactDOMComponent(tag) {
+ validateDangerousTag(tag);
+ this._tag = tag.toLowerCase();
+ this._renderedChildren = null;
+ this._previousStyle = null;
+ this._previousStyleCopy = null;
+ this._rootNodeID = null;
+ this._wrapperState = null;
+ this._topLevelWrapper = null;
+ this._nodeWithLegacyProperties = null;
+ if ("production" !== 'production') {
+ this._unprocessedContextDev = null;
+ this._processedContextDev = null;
+ }
+}
+
+ReactDOMComponent.displayName = 'ReactDOMComponent';
+
+ReactDOMComponent.Mixin = {
+
+ construct: function (element) {
+ this._currentElement = element;
+ },
+
+ /**
+ * Generates root tag markup then recurses. This method has side effects and
+ * is not idempotent.
+ *
+ * @internal
+ * @param {string} rootID The root DOM ID for this node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} context
+ * @return {string} The computed markup.
+ */
+ mountComponent: function (rootID, transaction, context) {
+ this._rootNodeID = rootID;
+
+ var props = this._currentElement.props;
+
+ switch (this._tag) {
+ case 'iframe':
+ case 'img':
+ case 'form':
+ case 'video':
+ case 'audio':
+ this._wrapperState = {
+ listeners: null
+ };
+ transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
+ break;
+ case 'button':
+ props = ReactDOMButton.getNativeProps(this, props, context);
+ break;
+ case 'input':
+ ReactDOMInput.mountWrapper(this, props, context);
+ props = ReactDOMInput.getNativeProps(this, props, context);
+ break;
+ case 'option':
+ ReactDOMOption.mountWrapper(this, props, context);
+ props = ReactDOMOption.getNativeProps(this, props, context);
+ break;
+ case 'select':
+ ReactDOMSelect.mountWrapper(this, props, context);
+ props = ReactDOMSelect.getNativeProps(this, props, context);
+ context = ReactDOMSelect.processChildContext(this, props, context);
+ break;
+ case 'textarea':
+ ReactDOMTextarea.mountWrapper(this, props, context);
+ props = ReactDOMTextarea.getNativeProps(this, props, context);
+ break;
+ }
+
+ assertValidProps(this, props);
+ if ("production" !== 'production') {
+ if (context[validateDOMNesting.ancestorInfoContextKey]) {
+ validateDOMNesting(this._tag, this, context[validateDOMNesting.ancestorInfoContextKey]);
+ }
+ }
+
+ if ("production" !== 'production') {
+ this._unprocessedContextDev = context;
+ this._processedContextDev = processChildContextDev(context, this);
+ context = this._processedContextDev;
+ }
+
+ var mountImage;
+ if (transaction.useCreateElement) {
+ var ownerDocument = context[ReactMount.ownerDocumentContextKey];
+ var el = ownerDocument.createElementNS('http://www.w3.org/1999/xhtml', this._currentElement.type);
+ DOMPropertyOperations.setAttributeForID(el, this._rootNodeID);
+ // Populate node cache
+ ReactMount.getID(el);
+ this._updateDOMProperties({}, props, transaction, el);
+ this._createInitialChildren(transaction, props, context, el);
+ mountImage = el;
+ } else {
+ var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
+ var tagContent = this._createContentMarkup(transaction, props, context);
+ if (!tagContent && omittedCloseTags[this._tag]) {
+ mountImage = tagOpen + '/>';
+ } else {
+ mountImage = tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
+ }
+ }
+
+ switch (this._tag) {
+ case 'input':
+ transaction.getReactMountReady().enqueue(mountReadyInputWrapper, this);
+ // falls through
+ case 'button':
+ case 'select':
+ case 'textarea':
+ if (props.autoFocus) {
+ transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
+ }
+ break;
+ }
+
+ return mountImage;
+ },
+
+ /**
+ * Creates markup for the open tag and all attributes.
+ *
+ * This method has side effects because events get registered.
+ *
+ * Iterating over object properties is faster than iterating over arrays.
+ * @see http://jsperf.com/obj-vs-arr-iteration
+ *
+ * @private
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} props
+ * @return {string} Markup of opening tag.
+ */
+ _createOpenTagMarkupAndPutListeners: function (transaction, props) {
+ var ret = '<' + this._currentElement.type;
+
+ for (var propKey in props) {
+ if (!props.hasOwnProperty(propKey)) {
+ continue;
+ }
+ var propValue = props[propKey];
+ if (propValue == null) {
+ continue;
+ }
+ if (registrationNameModules.hasOwnProperty(propKey)) {
+ if (propValue) {
+ enqueuePutListener(this._rootNodeID, propKey, propValue, transaction);
+ }
+ } else {
+ if (propKey === STYLE) {
+ if (propValue) {
+ if ("production" !== 'production') {
+ // See `_updateDOMProperties`. style block
+ this._previousStyle = propValue;
+ }
+ propValue = this._previousStyleCopy = assign({}, props.style);
+ }
+ propValue = CSSPropertyOperations.createMarkupForStyles(propValue);
+ }
+ var markup = null;
+ if (this._tag != null && isCustomComponent(this._tag, props)) {
+ if (propKey !== CHILDREN) {
+ markup = DOMPropertyOperations.createMarkupForCustomAttribute(propKey, propValue);
+ }
+ } else {
+ markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
+ }
+ if (markup) {
+ ret += ' ' + markup;
+ }
+ }
+ }
+
+ // For static pages, no need to put React ID and checksum. Saves lots of
+ // bytes.
+ if (transaction.renderToStaticMarkup) {
+ return ret;
+ }
+
+ var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID);
+ return ret + ' ' + markupForID;
+ },
+
+ /**
+ * Creates markup for the content between the tags.
+ *
+ * @private
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} props
+ * @param {object} context
+ * @return {string} Content markup.
+ */
+ _createContentMarkup: function (transaction, props, context) {
+ var ret = '';
+
+ // Intentional use of != to avoid catching zero/false.
+ var innerHTML = props.dangerouslySetInnerHTML;
+ if (innerHTML != null) {
+ if (innerHTML.__html != null) {
+ ret = innerHTML.__html;
+ }
+ } else {
+ var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
+ var childrenToUse = contentToUse != null ? null : props.children;
+ if (contentToUse != null) {
+ // TODO: Validate that text is allowed as a child of this node
+ ret = escapeTextContentForBrowser(contentToUse);
+ } else if (childrenToUse != null) {
+ var mountImages = this.mountChildren(childrenToUse, transaction, context);
+ ret = mountImages.join('');
+ }
+ }
+ if (newlineEatingTags[this._tag] && ret.charAt(0) === '\n') {
+ // text/html ignores the first character in these tags if it's a newline
+ // Prefer to break application/xml over text/html (for now) by adding
+ // a newline specifically to get eaten by the parser. (Alternately for
+ // textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
+ // \r is normalized out by HTMLTextAreaElement#value.)
+ // See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
+ // See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
+ // See: <http://www.w3.org/TR/html5/syntax.html#newlines>
+ // See: Parsing of "textarea" "listing" and "pre" elements
+ // from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
+ return '\n' + ret;
+ } else {
+ return ret;
+ }
+ },
+
+ _createInitialChildren: function (transaction, props, context, el) {
+ // Intentional use of != to avoid catching zero/false.
+ var innerHTML = props.dangerouslySetInnerHTML;
+ if (innerHTML != null) {
+ if (innerHTML.__html != null) {
+ setInnerHTML(el, innerHTML.__html);
+ }
+ } else {
+ var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
+ var childrenToUse = contentToUse != null ? null : props.children;
+ if (contentToUse != null) {
+ // TODO: Validate that text is allowed as a child of this node
+ setTextContent(el, contentToUse);
+ } else if (childrenToUse != null) {
+ var mountImages = this.mountChildren(childrenToUse, transaction, context);
+ for (var i = 0; i < mountImages.length; i++) {
+ el.appendChild(mountImages[i]);
+ }
+ }
+ }
+ },
+
+ /**
+ * Receives a next element and updates the component.
+ *
+ * @internal
+ * @param {ReactElement} nextElement
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @param {object} context
+ */
+ receiveComponent: function (nextElement, transaction, context) {
+ var prevElement = this._currentElement;
+ this._currentElement = nextElement;
+ this.updateComponent(transaction, prevElement, nextElement, context);
+ },
+
+ /**
+ * Updates a native DOM component after it has already been allocated and
+ * attached to the DOM. Reconciles the root DOM node, then recurses.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @param {ReactElement} prevElement
+ * @param {ReactElement} nextElement
+ * @internal
+ * @overridable
+ */
+ updateComponent: function (transaction, prevElement, nextElement, context) {
+ var lastProps = prevElement.props;
+ var nextProps = this._currentElement.props;
+
+ switch (this._tag) {
+ case 'button':
+ lastProps = ReactDOMButton.getNativeProps(this, lastProps);
+ nextProps = ReactDOMButton.getNativeProps(this, nextProps);
+ break;
+ case 'input':
+ ReactDOMInput.updateWrapper(this);
+ lastProps = ReactDOMInput.getNativeProps(this, lastProps);
+ nextProps = ReactDOMInput.getNativeProps(this, nextProps);
+ break;
+ case 'option':
+ lastProps = ReactDOMOption.getNativeProps(this, lastProps);
+ nextProps = ReactDOMOption.getNativeProps(this, nextProps);
+ break;
+ case 'select':
+ lastProps = ReactDOMSelect.getNativeProps(this, lastProps);
+ nextProps = ReactDOMSelect.getNativeProps(this, nextProps);
+ break;
+ case 'textarea':
+ ReactDOMTextarea.updateWrapper(this);
+ lastProps = ReactDOMTextarea.getNativeProps(this, lastProps);
+ nextProps = ReactDOMTextarea.getNativeProps(this, nextProps);
+ break;
+ }
+
+ if ("production" !== 'production') {
+ // If the context is reference-equal to the old one, pass down the same
+ // processed object so the update bailout in ReactReconciler behaves
+ // correctly (and identically in dev and prod). See #5005.
+ if (this._unprocessedContextDev !== context) {
+ this._unprocessedContextDev = context;
+ this._processedContextDev = processChildContextDev(context, this);
+ }
+ context = this._processedContextDev;
+ }
+
+ assertValidProps(this, nextProps);
+ this._updateDOMProperties(lastProps, nextProps, transaction, null);
+ this._updateDOMChildren(lastProps, nextProps, transaction, context);
+
+ if (!canDefineProperty && this._nodeWithLegacyProperties) {
+ this._nodeWithLegacyProperties.props = nextProps;
+ }
+
+ if (this._tag === 'select') {
+ // <select> value update needs to occur after <option> children
+ // reconciliation
+ transaction.getReactMountReady().enqueue(postUpdateSelectWrapper, this);
+ }
+ },
+
+ /**
+ * Reconciles the properties by detecting differences in property values and
+ * updating the DOM as necessary. This function is probably the single most
+ * critical path for performance optimization.
+ *
+ * TODO: Benchmark whether checking for changed values in memory actually
+ * improves performance (especially statically positioned elements).
+ * TODO: Benchmark the effects of putting this at the top since 99% of props
+ * do not change for a given reconciliation.
+ * TODO: Benchmark areas that can be improved with caching.
+ *
+ * @private
+ * @param {object} lastProps
+ * @param {object} nextProps
+ * @param {ReactReconcileTransaction} transaction
+ * @param {?DOMElement} node
+ */
+ _updateDOMProperties: function (lastProps, nextProps, transaction, node) {
+ var propKey;
+ var styleName;
+ var styleUpdates;
+ for (propKey in lastProps) {
+ if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) {
+ continue;
+ }
+ if (propKey === STYLE) {
+ var lastStyle = this._previousStyleCopy;
+ for (styleName in lastStyle) {
+ if (lastStyle.hasOwnProperty(styleName)) {
+ styleUpdates = styleUpdates || {};
+ styleUpdates[styleName] = '';
+ }
+ }
+ this._previousStyleCopy = null;
+ } else if (registrationNameModules.hasOwnProperty(propKey)) {
+ if (lastProps[propKey]) {
+ // Only call deleteListener if there was a listener previously or
+ // else willDeleteListener gets called when there wasn't actually a
+ // listener (e.g., onClick={null})
+ deleteListener(this._rootNodeID, propKey);
+ }
+ } else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ DOMPropertyOperations.deleteValueForProperty(node, propKey);
+ }
+ }
+ for (propKey in nextProps) {
+ var nextProp = nextProps[propKey];
+ var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps[propKey];
+ if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
+ continue;
+ }
+ if (propKey === STYLE) {
+ if (nextProp) {
+ if ("production" !== 'production') {
+ checkAndWarnForMutatedStyle(this._previousStyleCopy, this._previousStyle, this);
+ this._previousStyle = nextProp;
+ }
+ nextProp = this._previousStyleCopy = assign({}, nextProp);
+ } else {
+ this._previousStyleCopy = null;
+ }
+ if (lastProp) {
+ // Unset styles on `lastProp` but not on `nextProp`.
+ for (styleName in lastProp) {
+ if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) {
+ styleUpdates = styleUpdates || {};
+ styleUpdates[styleName] = '';
+ }
+ }
+ // Update styles that changed since `lastProp`.
+ for (styleName in nextProp) {
+ if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
+ styleUpdates = styleUpdates || {};
+ styleUpdates[styleName] = nextProp[styleName];
+ }
+ }
+ } else {
+ // Relies on `updateStylesByID` not mutating `styleUpdates`.
+ styleUpdates = nextProp;
+ }
+ } else if (registrationNameModules.hasOwnProperty(propKey)) {
+ if (nextProp) {
+ enqueuePutListener(this._rootNodeID, propKey, nextProp, transaction);
+ } else if (lastProp) {
+ deleteListener(this._rootNodeID, propKey);
+ }
+ } else if (isCustomComponent(this._tag, nextProps)) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ if (propKey === CHILDREN) {
+ nextProp = null;
+ }
+ DOMPropertyOperations.setValueForAttribute(node, propKey, nextProp);
+ } else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ // If we're updating to null or undefined, we should remove the property
+ // from the DOM node instead of inadvertantly setting to a string. This
+ // brings us in line with the same behavior we have on initial render.
+ if (nextProp != null) {
+ DOMPropertyOperations.setValueForProperty(node, propKey, nextProp);
+ } else {
+ DOMPropertyOperations.deleteValueForProperty(node, propKey);
+ }
+ }
+ }
+ if (styleUpdates) {
+ if (!node) {
+ node = ReactMount.getNode(this._rootNodeID);
+ }
+ CSSPropertyOperations.setValueForStyles(node, styleUpdates);
+ }
+ },
+
+ /**
+ * Reconciles the children with the various properties that affect the
+ * children content.
+ *
+ * @param {object} lastProps
+ * @param {object} nextProps
+ * @param {ReactReconcileTransaction} transaction
+ * @param {object} context
+ */
+ _updateDOMChildren: function (lastProps, nextProps, transaction, context) {
+ var lastContent = CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
+ var nextContent = CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
+
+ var lastHtml = lastProps.dangerouslySetInnerHTML && lastProps.dangerouslySetInnerHTML.__html;
+ var nextHtml = nextProps.dangerouslySetInnerHTML && nextProps.dangerouslySetInnerHTML.__html;
+
+ // Note the use of `!=` which checks for null or undefined.
+ var lastChildren = lastContent != null ? null : lastProps.children;
+ var nextChildren = nextContent != null ? null : nextProps.children;
+
+ // If we're switching from children to content/html or vice versa, remove
+ // the old content
+ var lastHasContentOrHtml = lastContent != null || lastHtml != null;
+ var nextHasContentOrHtml = nextContent != null || nextHtml != null;
+ if (lastChildren != null && nextChildren == null) {
+ this.updateChildren(null, transaction, context);
+ } else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
+ this.updateTextContent('');
+ }
+
+ if (nextContent != null) {
+ if (lastContent !== nextContent) {
+ this.updateTextContent('' + nextContent);
+ }
+ } else if (nextHtml != null) {
+ if (lastHtml !== nextHtml) {
+ this.updateMarkup('' + nextHtml);
+ }
+ } else if (nextChildren != null) {
+ this.updateChildren(nextChildren, transaction, context);
+ }
+ },
+
+ /**
+ * Destroys all event registrations for this instance. Does not remove from
+ * the DOM. That must be done by the parent.
+ *
+ * @internal
+ */
+ unmountComponent: function () {
+ switch (this._tag) {
+ case 'iframe':
+ case 'img':
+ case 'form':
+ case 'video':
+ case 'audio':
+ var listeners = this._wrapperState.listeners;
+ if (listeners) {
+ for (var i = 0; i < listeners.length; i++) {
+ listeners[i].remove();
+ }
+ }
+ break;
+ case 'input':
+ ReactDOMInput.unmountWrapper(this);
+ break;
+ case 'html':
+ case 'head':
+ case 'body':
+ /**
+ * Components like <html> <head> and <body> can't be removed or added
+ * easily in a cross-browser way, however it's valuable to be able to
+ * take advantage of React's reconciliation for styling and <title>
+ * management. So we just document it and throw in dangerous cases.
+ */
+ !false ? "production" !== 'production' ? invariant(false, '<%s> tried to unmount. Because of cross-browser quirks it is ' + 'impossible to unmount some top-level components (eg <html>, ' + '<head>, and <body>) reliably and efficiently. To fix this, have a ' + 'single top-level component that never unmounts render these ' + 'elements.', this._tag) : invariant(false) : undefined;
+ break;
+ }
+
+ this.unmountChildren();
+ ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID);
+ ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
+ this._rootNodeID = null;
+ this._wrapperState = null;
+ if (this._nodeWithLegacyProperties) {
+ var node = this._nodeWithLegacyProperties;
+ node._reactInternalComponent = null;
+ this._nodeWithLegacyProperties = null;
+ }
+ },
+
+ getPublicInstance: function () {
+ if (!this._nodeWithLegacyProperties) {
+ var node = ReactMount.getNode(this._rootNodeID);
+
+ node._reactInternalComponent = this;
+ node.getDOMNode = legacyGetDOMNode;
+ node.isMounted = legacyIsMounted;
+ node.setState = legacySetStateEtc;
+ node.replaceState = legacySetStateEtc;
+ node.forceUpdate = legacySetStateEtc;
+ node.setProps = legacySetProps;
+ node.replaceProps = legacyReplaceProps;
+
+ if ("production" !== 'production') {
+ if (canDefineProperty) {
+ Object.defineProperties(node, legacyPropsDescriptor);
+ } else {
+ // updateComponent will update this property on subsequent renders
+ node.props = this._currentElement.props;
+ }
+ } else {
+ // updateComponent will update this property on subsequent renders
+ node.props = this._currentElement.props;
+ }
+
+ this._nodeWithLegacyProperties = node;
+ }
+ return this._nodeWithLegacyProperties;
+ }
+
+};
+
+ReactPerf.measureMethods(ReactDOMComponent, 'ReactDOMComponent', {
+ mountComponent: 'mountComponent',
+ updateComponent: 'updateComponent'
+});
+
+assign(ReactDOMComponent.prototype, ReactDOMComponent.Mixin, ReactMultiChild.Mixin);
+
+module.exports = ReactDOMComponent;
+},{"10":10,"11":11,"117":117,"121":121,"133":133,"138":138,"139":139,"144":144,"15":15,"161":161,"166":166,"171":171,"173":173,"2":2,"24":24,"28":28,"35":35,"41":41,"46":46,"47":47,"48":48,"5":5,"52":52,"72":72,"73":73,"78":78,"95":95}],43:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMFactories
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactElementValidator = _dereq_(58);
+
+var mapObject = _dereq_(167);
+
+/**
+ * Create a factory that creates HTML tag elements.
+ *
+ * @param {string} tag Tag name (e.g. `div`).
+ * @private
+ */
+function createDOMFactory(tag) {
+ if ("production" !== 'production') {
+ return ReactElementValidator.createFactory(tag);
+ }
+ return ReactElement.createFactory(tag);
+}
+
+/**
+ * Creates a mapping from supported HTML tags to `ReactDOMComponent` classes.
+ * This is also accessible via `React.DOM`.
+ *
+ * @public
+ */
+var ReactDOMFactories = mapObject({
+ a: 'a',
+ abbr: 'abbr',
+ address: 'address',
+ area: 'area',
+ article: 'article',
+ aside: 'aside',
+ audio: 'audio',
+ b: 'b',
+ base: 'base',
+ bdi: 'bdi',
+ bdo: 'bdo',
+ big: 'big',
+ blockquote: 'blockquote',
+ body: 'body',
+ br: 'br',
+ button: 'button',
+ canvas: 'canvas',
+ caption: 'caption',
+ cite: 'cite',
+ code: 'code',
+ col: 'col',
+ colgroup: 'colgroup',
+ data: 'data',
+ datalist: 'datalist',
+ dd: 'dd',
+ del: 'del',
+ details: 'details',
+ dfn: 'dfn',
+ dialog: 'dialog',
+ div: 'div',
+ dl: 'dl',
+ dt: 'dt',
+ em: 'em',
+ embed: 'embed',
+ fieldset: 'fieldset',
+ figcaption: 'figcaption',
+ figure: 'figure',
+ footer: 'footer',
+ form: 'form',
+ h1: 'h1',
+ h2: 'h2',
+ h3: 'h3',
+ h4: 'h4',
+ h5: 'h5',
+ h6: 'h6',
+ head: 'head',
+ header: 'header',
+ hgroup: 'hgroup',
+ hr: 'hr',
+ html: 'html',
+ i: 'i',
+ iframe: 'iframe',
+ img: 'img',
+ input: 'input',
+ ins: 'ins',
+ kbd: 'kbd',
+ keygen: 'keygen',
+ label: 'label',
+ legend: 'legend',
+ li: 'li',
+ link: 'link',
+ main: 'main',
+ map: 'map',
+ mark: 'mark',
+ menu: 'menu',
+ menuitem: 'menuitem',
+ meta: 'meta',
+ meter: 'meter',
+ nav: 'nav',
+ noscript: 'noscript',
+ object: 'object',
+ ol: 'ol',
+ optgroup: 'optgroup',
+ option: 'option',
+ output: 'output',
+ p: 'p',
+ param: 'param',
+ picture: 'picture',
+ pre: 'pre',
+ progress: 'progress',
+ q: 'q',
+ rp: 'rp',
+ rt: 'rt',
+ ruby: 'ruby',
+ s: 's',
+ samp: 'samp',
+ script: 'script',
+ section: 'section',
+ select: 'select',
+ small: 'small',
+ source: 'source',
+ span: 'span',
+ strong: 'strong',
+ style: 'style',
+ sub: 'sub',
+ summary: 'summary',
+ sup: 'sup',
+ table: 'table',
+ tbody: 'tbody',
+ td: 'td',
+ textarea: 'textarea',
+ tfoot: 'tfoot',
+ th: 'th',
+ thead: 'thead',
+ time: 'time',
+ title: 'title',
+ tr: 'tr',
+ track: 'track',
+ u: 'u',
+ ul: 'ul',
+ 'var': 'var',
+ video: 'video',
+ wbr: 'wbr',
+
+ // SVG
+ circle: 'circle',
+ clipPath: 'clipPath',
+ defs: 'defs',
+ ellipse: 'ellipse',
+ g: 'g',
+ image: 'image',
+ line: 'line',
+ linearGradient: 'linearGradient',
+ mask: 'mask',
+ path: 'path',
+ pattern: 'pattern',
+ polygon: 'polygon',
+ polyline: 'polyline',
+ radialGradient: 'radialGradient',
+ rect: 'rect',
+ stop: 'stop',
+ svg: 'svg',
+ text: 'text',
+ tspan: 'tspan'
+
+}, createDOMFactory);
+
+module.exports = ReactDOMFactories;
+},{"167":167,"57":57,"58":58}],44:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMFeatureFlags
+ */
+
+'use strict';
+
+var ReactDOMFeatureFlags = {
+ useCreateElement: false
+};
+
+module.exports = ReactDOMFeatureFlags;
+},{}],45:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMIDOperations
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMChildrenOperations = _dereq_(9);
+var DOMPropertyOperations = _dereq_(11);
+var ReactMount = _dereq_(72);
+var ReactPerf = _dereq_(78);
+
+var invariant = _dereq_(161);
+
+/**
+ * Errors for properties that should not be updated with `updatePropertyByID()`.
+ *
+ * @type {object}
+ * @private
+ */
+var INVALID_PROPERTY_ERRORS = {
+ dangerouslySetInnerHTML: '`dangerouslySetInnerHTML` must be set using `updateInnerHTMLByID()`.',
+ style: '`style` must be set using `updateStylesByID()`.'
+};
+
+/**
+ * Operations used to process updates to DOM nodes.
+ */
+var ReactDOMIDOperations = {
+
+ /**
+ * Updates a DOM node with new property values. This should only be used to
+ * update DOM properties in `DOMProperty`.
+ *
+ * @param {string} id ID of the node to update.
+ * @param {string} name A valid property name, see `DOMProperty`.
+ * @param {*} value New value of the property.
+ * @internal
+ */
+ updatePropertyByID: function (id, name, value) {
+ var node = ReactMount.getNode(id);
+ !!INVALID_PROPERTY_ERRORS.hasOwnProperty(name) ? "production" !== 'production' ? invariant(false, 'updatePropertyByID(...): %s', INVALID_PROPERTY_ERRORS[name]) : invariant(false) : undefined;
+
+ // If we're updating to null or undefined, we should remove the property
+ // from the DOM node instead of inadvertantly setting to a string. This
+ // brings us in line with the same behavior we have on initial render.
+ if (value != null) {
+ DOMPropertyOperations.setValueForProperty(node, name, value);
+ } else {
+ DOMPropertyOperations.deleteValueForProperty(node, name);
+ }
+ },
+
+ /**
+ * Replaces a DOM node that exists in the document with markup.
+ *
+ * @param {string} id ID of child to be replaced.
+ * @param {string} markup Dangerous markup to inject in place of child.
+ * @internal
+ * @see {Danger.dangerouslyReplaceNodeWithMarkup}
+ */
+ dangerouslyReplaceNodeWithMarkupByID: function (id, markup) {
+ var node = ReactMount.getNode(id);
+ DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup);
+ },
+
+ /**
+ * Updates a component's children by processing a series of updates.
+ *
+ * @param {array<object>} updates List of update configurations.
+ * @param {array<string>} markup List of markup strings.
+ * @internal
+ */
+ dangerouslyProcessChildrenUpdates: function (updates, markup) {
+ for (var i = 0; i < updates.length; i++) {
+ updates[i].parentNode = ReactMount.getNode(updates[i].parentID);
+ }
+ DOMChildrenOperations.processUpdates(updates, markup);
+ }
+};
+
+ReactPerf.measureMethods(ReactDOMIDOperations, 'ReactDOMIDOperations', {
+ dangerouslyReplaceNodeWithMarkupByID: 'dangerouslyReplaceNodeWithMarkupByID',
+ dangerouslyProcessChildrenUpdates: 'dangerouslyProcessChildrenUpdates'
+});
+
+module.exports = ReactDOMIDOperations;
+},{"11":11,"161":161,"72":72,"78":78,"9":9}],46:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMInput
+ */
+
+'use strict';
+
+var ReactDOMIDOperations = _dereq_(45);
+var LinkedValueUtils = _dereq_(23);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+var instancesByReactID = {};
+
+function forceUpdateIfMounted() {
+ if (this._rootNodeID) {
+ // DOM component is still mounted; update
+ ReactDOMInput.updateWrapper(this);
+ }
+}
+
+/**
+ * Implements an <input> native component that allows setting these optional
+ * props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
+ *
+ * If `checked` or `value` are not supplied (or null/undefined), user actions
+ * that affect the checked state or value will trigger updates to the element.
+ *
+ * If they are supplied (and not null/undefined), the rendered element will not
+ * trigger updates to the element. Instead, the props must change in order for
+ * the rendered element to be updated.
+ *
+ * The rendered element will be initialized as unchecked (or `defaultChecked`)
+ * with an empty value (or `defaultValue`).
+ *
+ * @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
+ */
+var ReactDOMInput = {
+ getNativeProps: function (inst, props, context) {
+ var value = LinkedValueUtils.getValue(props);
+ var checked = LinkedValueUtils.getChecked(props);
+
+ var nativeProps = assign({}, props, {
+ defaultChecked: undefined,
+ defaultValue: undefined,
+ value: value != null ? value : inst._wrapperState.initialValue,
+ checked: checked != null ? checked : inst._wrapperState.initialChecked,
+ onChange: inst._wrapperState.onChange
+ });
+
+ return nativeProps;
+ },
+
+ mountWrapper: function (inst, props) {
+ if ("production" !== 'production') {
+ LinkedValueUtils.checkPropTypes('input', props, inst._currentElement._owner);
+ }
+
+ var defaultValue = props.defaultValue;
+ inst._wrapperState = {
+ initialChecked: props.defaultChecked || false,
+ initialValue: defaultValue != null ? defaultValue : null,
+ onChange: _handleChange.bind(inst)
+ };
+ },
+
+ mountReadyWrapper: function (inst) {
+ // Can't be in mountWrapper or else server rendering leaks.
+ instancesByReactID[inst._rootNodeID] = inst;
+ },
+
+ unmountWrapper: function (inst) {
+ delete instancesByReactID[inst._rootNodeID];
+ },
+
+ updateWrapper: function (inst) {
+ var props = inst._currentElement.props;
+
+ // TODO: Shouldn't this be getChecked(props)?
+ var checked = props.checked;
+ if (checked != null) {
+ ReactDOMIDOperations.updatePropertyByID(inst._rootNodeID, 'checked', checked || false);
+ }
+
+ var value = LinkedValueUtils.getValue(props);
+ if (value != null) {
+ // Cast `value` to a string to ensure the value is set correctly. While
+ // browsers typically do this as necessary, jsdom doesn't.
+ ReactDOMIDOperations.updatePropertyByID(inst._rootNodeID, 'value', '' + value);
+ }
+ }
+};
+
+function _handleChange(event) {
+ var props = this._currentElement.props;
+
+ var returnValue = LinkedValueUtils.executeOnChange(props, event);
+
+ // Here we use asap to wait until all updates have propagated, which
+ // is important when using controlled components within layers:
+ // https://github.com/facebook/react/issues/1698
+ ReactUpdates.asap(forceUpdateIfMounted, this);
+
+ var name = props.name;
+ if (props.type === 'radio' && name != null) {
+ var rootNode = ReactMount.getNode(this._rootNodeID);
+ var queryRoot = rootNode;
+
+ while (queryRoot.parentNode) {
+ queryRoot = queryRoot.parentNode;
+ }
+
+ // If `rootNode.form` was non-null, then we could try `form.elements`,
+ // but that sometimes behaves strangely in IE8. We could also try using
+ // `form.getElementsByName`, but that will only return direct children
+ // and won't include inputs that use the HTML5 `form=` attribute. Since
+ // the input might not even be in a form, let's just use the global
+ // `querySelectorAll` to ensure we don't miss anything.
+ var group = queryRoot.querySelectorAll('input[name=' + JSON.stringify('' + name) + '][type="radio"]');
+
+ for (var i = 0; i < group.length; i++) {
+ var otherNode = group[i];
+ if (otherNode === rootNode || otherNode.form !== rootNode.form) {
+ continue;
+ }
+ // This will throw if radio buttons rendered by different copies of React
+ // and the same name are rendered into the same form (same as #1939).
+ // That's probably okay; we don't support it just as we don't support
+ // mixing React with non-React.
+ var otherID = ReactMount.getID(otherNode);
+ !otherID ? "production" !== 'production' ? invariant(false, 'ReactDOMInput: Mixing React and non-React radio inputs with the ' + 'same `name` is not supported.') : invariant(false) : undefined;
+ var otherInstance = instancesByReactID[otherID];
+ !otherInstance ? "production" !== 'production' ? invariant(false, 'ReactDOMInput: Unknown radio button ID %s.', otherID) : invariant(false) : undefined;
+ // If this is a controlled radio button group, forcing the input that
+ // was previously checked to update will cause it to be come re-checked
+ // as appropriate.
+ ReactUpdates.asap(forceUpdateIfMounted, otherInstance);
+ }
+ }
+
+ return returnValue;
+}
+
+module.exports = ReactDOMInput;
+},{"161":161,"23":23,"24":24,"45":45,"72":72,"96":96}],47:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMOption
+ */
+
+'use strict';
+
+var ReactChildren = _dereq_(32);
+var ReactDOMSelect = _dereq_(48);
+
+var assign = _dereq_(24);
+var warning = _dereq_(173);
+
+var valueContextKey = ReactDOMSelect.valueContextKey;
+
+/**
+ * Implements an <option> native component that warns when `selected` is set.
+ */
+var ReactDOMOption = {
+ mountWrapper: function (inst, props, context) {
+ // TODO (yungsters): Remove support for `selected` in <option>.
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(props.selected == null, 'Use the `defaultValue` or `value` props on <select> instead of ' + 'setting `selected` on <option>.') : undefined;
+ }
+
+ // Look up whether this option is 'selected' via context
+ var selectValue = context[valueContextKey];
+
+ // If context key is null (e.g., no specified value or after initial mount)
+ // or missing (e.g., for <datalist>), we don't change props.selected
+ var selected = null;
+ if (selectValue != null) {
+ selected = false;
+ if (Array.isArray(selectValue)) {
+ // multiple
+ for (var i = 0; i < selectValue.length; i++) {
+ if ('' + selectValue[i] === '' + props.value) {
+ selected = true;
+ break;
+ }
+ }
+ } else {
+ selected = '' + selectValue === '' + props.value;
+ }
+ }
+
+ inst._wrapperState = { selected: selected };
+ },
+
+ getNativeProps: function (inst, props, context) {
+ var nativeProps = assign({ selected: undefined, children: undefined }, props);
+
+ // Read state only from initial mount because <select> updates value
+ // manually; we need the initial state only for server rendering
+ if (inst._wrapperState.selected != null) {
+ nativeProps.selected = inst._wrapperState.selected;
+ }
+
+ var content = '';
+
+ // Flatten children and warn if they aren't strings or numbers;
+ // invalid types are ignored.
+ ReactChildren.forEach(props.children, function (child) {
+ if (child == null) {
+ return;
+ }
+ if (typeof child === 'string' || typeof child === 'number') {
+ content += child;
+ } else {
+ "production" !== 'production' ? warning(false, 'Only strings and numbers are supported as <option> children.') : undefined;
+ }
+ });
+
+ nativeProps.children = content;
+ return nativeProps;
+ }
+
+};
+
+module.exports = ReactDOMOption;
+},{"173":173,"24":24,"32":32,"48":48}],48:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMSelect
+ */
+
+'use strict';
+
+var LinkedValueUtils = _dereq_(23);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var warning = _dereq_(173);
+
+var valueContextKey = '__ReactDOMSelect_value$' + Math.random().toString(36).slice(2);
+
+function updateOptionsIfPendingUpdateAndMounted() {
+ if (this._rootNodeID && this._wrapperState.pendingUpdate) {
+ this._wrapperState.pendingUpdate = false;
+
+ var props = this._currentElement.props;
+ var value = LinkedValueUtils.getValue(props);
+
+ if (value != null) {
+ updateOptions(this, Boolean(props.multiple), value);
+ }
+ }
+}
+
+function getDeclarationErrorAddendum(owner) {
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+var valuePropNames = ['value', 'defaultValue'];
+
+/**
+ * Validation function for `value` and `defaultValue`.
+ * @private
+ */
+function checkSelectPropTypes(inst, props) {
+ var owner = inst._currentElement._owner;
+ LinkedValueUtils.checkPropTypes('select', props, owner);
+
+ for (var i = 0; i < valuePropNames.length; i++) {
+ var propName = valuePropNames[i];
+ if (props[propName] == null) {
+ continue;
+ }
+ if (props.multiple) {
+ "production" !== 'production' ? warning(Array.isArray(props[propName]), 'The `%s` prop supplied to <select> must be an array if ' + '`multiple` is true.%s', propName, getDeclarationErrorAddendum(owner)) : undefined;
+ } else {
+ "production" !== 'production' ? warning(!Array.isArray(props[propName]), 'The `%s` prop supplied to <select> must be a scalar ' + 'value if `multiple` is false.%s', propName, getDeclarationErrorAddendum(owner)) : undefined;
+ }
+ }
+}
+
+/**
+ * @param {ReactDOMComponent} inst
+ * @param {boolean} multiple
+ * @param {*} propValue A stringable (with `multiple`, a list of stringables).
+ * @private
+ */
+function updateOptions(inst, multiple, propValue) {
+ var selectedValue, i;
+ var options = ReactMount.getNode(inst._rootNodeID).options;
+
+ if (multiple) {
+ selectedValue = {};
+ for (i = 0; i < propValue.length; i++) {
+ selectedValue['' + propValue[i]] = true;
+ }
+ for (i = 0; i < options.length; i++) {
+ var selected = selectedValue.hasOwnProperty(options[i].value);
+ if (options[i].selected !== selected) {
+ options[i].selected = selected;
+ }
+ }
+ } else {
+ // Do not set `select.value` as exact behavior isn't consistent across all
+ // browsers for all cases.
+ selectedValue = '' + propValue;
+ for (i = 0; i < options.length; i++) {
+ if (options[i].value === selectedValue) {
+ options[i].selected = true;
+ return;
+ }
+ }
+ if (options.length) {
+ options[0].selected = true;
+ }
+ }
+}
+
+/**
+ * Implements a <select> native component that allows optionally setting the
+ * props `value` and `defaultValue`. If `multiple` is false, the prop must be a
+ * stringable. If `multiple` is true, the prop must be an array of stringables.
+ *
+ * If `value` is not supplied (or null/undefined), user actions that change the
+ * selected option will trigger updates to the rendered options.
+ *
+ * If it is supplied (and not null/undefined), the rendered options will not
+ * update in response to user actions. Instead, the `value` prop must change in
+ * order for the rendered options to update.
+ *
+ * If `defaultValue` is provided, any options with the supplied values will be
+ * selected.
+ */
+var ReactDOMSelect = {
+ valueContextKey: valueContextKey,
+
+ getNativeProps: function (inst, props, context) {
+ return assign({}, props, {
+ onChange: inst._wrapperState.onChange,
+ value: undefined
+ });
+ },
+
+ mountWrapper: function (inst, props) {
+ if ("production" !== 'production') {
+ checkSelectPropTypes(inst, props);
+ }
+
+ var value = LinkedValueUtils.getValue(props);
+ inst._wrapperState = {
+ pendingUpdate: false,
+ initialValue: value != null ? value : props.defaultValue,
+ onChange: _handleChange.bind(inst),
+ wasMultiple: Boolean(props.multiple)
+ };
+ },
+
+ processChildContext: function (inst, props, context) {
+ // Pass down initial value so initial generated markup has correct
+ // `selected` attributes
+ var childContext = assign({}, context);
+ childContext[valueContextKey] = inst._wrapperState.initialValue;
+ return childContext;
+ },
+
+ postUpdateWrapper: function (inst) {
+ var props = inst._currentElement.props;
+
+ // After the initial mount, we control selected-ness manually so don't pass
+ // the context value down
+ inst._wrapperState.initialValue = undefined;
+
+ var wasMultiple = inst._wrapperState.wasMultiple;
+ inst._wrapperState.wasMultiple = Boolean(props.multiple);
+
+ var value = LinkedValueUtils.getValue(props);
+ if (value != null) {
+ inst._wrapperState.pendingUpdate = false;
+ updateOptions(inst, Boolean(props.multiple), value);
+ } else if (wasMultiple !== Boolean(props.multiple)) {
+ // For simplicity, reapply `defaultValue` if `multiple` is toggled.
+ if (props.defaultValue != null) {
+ updateOptions(inst, Boolean(props.multiple), props.defaultValue);
+ } else {
+ // Revert the select back to its default unselected state.
+ updateOptions(inst, Boolean(props.multiple), props.multiple ? [] : '');
+ }
+ }
+ }
+};
+
+function _handleChange(event) {
+ var props = this._currentElement.props;
+ var returnValue = LinkedValueUtils.executeOnChange(props, event);
+
+ this._wrapperState.pendingUpdate = true;
+ ReactUpdates.asap(updateOptionsIfPendingUpdateAndMounted, this);
+ return returnValue;
+}
+
+module.exports = ReactDOMSelect;
+},{"173":173,"23":23,"24":24,"72":72,"96":96}],49:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMSelection
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var getNodeForCharacterOffset = _dereq_(130);
+var getTextContentAccessor = _dereq_(131);
+
+/**
+ * While `isCollapsed` is available on the Selection object and `collapsed`
+ * is available on the Range object, IE11 sometimes gets them wrong.
+ * If the anchor/focus nodes and offsets are the same, the range is collapsed.
+ */
+function isCollapsed(anchorNode, anchorOffset, focusNode, focusOffset) {
+ return anchorNode === focusNode && anchorOffset === focusOffset;
+}
+
+/**
+ * Get the appropriate anchor and focus node/offset pairs for IE.
+ *
+ * The catch here is that IE's selection API doesn't provide information
+ * about whether the selection is forward or backward, so we have to
+ * behave as though it's always forward.
+ *
+ * IE text differs from modern selection in that it behaves as though
+ * block elements end with a new line. This means character offsets will
+ * differ between the two APIs.
+ *
+ * @param {DOMElement} node
+ * @return {object}
+ */
+function getIEOffsets(node) {
+ var selection = document.selection;
+ var selectedRange = selection.createRange();
+ var selectedLength = selectedRange.text.length;
+
+ // Duplicate selection so we can move range without breaking user selection.
+ var fromStart = selectedRange.duplicate();
+ fromStart.moveToElementText(node);
+ fromStart.setEndPoint('EndToStart', selectedRange);
+
+ var startOffset = fromStart.text.length;
+ var endOffset = startOffset + selectedLength;
+
+ return {
+ start: startOffset,
+ end: endOffset
+ };
+}
+
+/**
+ * @param {DOMElement} node
+ * @return {?object}
+ */
+function getModernOffsets(node) {
+ var selection = window.getSelection && window.getSelection();
+
+ if (!selection || selection.rangeCount === 0) {
+ return null;
+ }
+
+ var anchorNode = selection.anchorNode;
+ var anchorOffset = selection.anchorOffset;
+ var focusNode = selection.focusNode;
+ var focusOffset = selection.focusOffset;
+
+ var currentRange = selection.getRangeAt(0);
+
+ // In Firefox, range.startContainer and range.endContainer can be "anonymous
+ // divs", e.g. the up/down buttons on an <input type="number">. Anonymous
+ // divs do not seem to expose properties, triggering a "Permission denied
+ // error" if any of its properties are accessed. The only seemingly possible
+ // way to avoid erroring is to access a property that typically works for
+ // non-anonymous divs and catch any error that may otherwise arise. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=208427
+ try {
+ /* eslint-disable no-unused-expressions */
+ currentRange.startContainer.nodeType;
+ currentRange.endContainer.nodeType;
+ /* eslint-enable no-unused-expressions */
+ } catch (e) {
+ return null;
+ }
+
+ // If the node and offset values are the same, the selection is collapsed.
+ // `Selection.isCollapsed` is available natively, but IE sometimes gets
+ // this value wrong.
+ var isSelectionCollapsed = isCollapsed(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
+
+ var rangeLength = isSelectionCollapsed ? 0 : currentRange.toString().length;
+
+ var tempRange = currentRange.cloneRange();
+ tempRange.selectNodeContents(node);
+ tempRange.setEnd(currentRange.startContainer, currentRange.startOffset);
+
+ var isTempRangeCollapsed = isCollapsed(tempRange.startContainer, tempRange.startOffset, tempRange.endContainer, tempRange.endOffset);
+
+ var start = isTempRangeCollapsed ? 0 : tempRange.toString().length;
+ var end = start + rangeLength;
+
+ // Detect whether the selection is backward.
+ var detectionRange = document.createRange();
+ detectionRange.setStart(anchorNode, anchorOffset);
+ detectionRange.setEnd(focusNode, focusOffset);
+ var isBackward = detectionRange.collapsed;
+
+ return {
+ start: isBackward ? end : start,
+ end: isBackward ? start : end
+ };
+}
+
+/**
+ * @param {DOMElement|DOMTextNode} node
+ * @param {object} offsets
+ */
+function setIEOffsets(node, offsets) {
+ var range = document.selection.createRange().duplicate();
+ var start, end;
+
+ if (typeof offsets.end === 'undefined') {
+ start = offsets.start;
+ end = start;
+ } else if (offsets.start > offsets.end) {
+ start = offsets.end;
+ end = offsets.start;
+ } else {
+ start = offsets.start;
+ end = offsets.end;
+ }
+
+ range.moveToElementText(node);
+ range.moveStart('character', start);
+ range.setEndPoint('EndToStart', range);
+ range.moveEnd('character', end - start);
+ range.select();
+}
+
+/**
+ * In modern non-IE browsers, we can support both forward and backward
+ * selections.
+ *
+ * Note: IE10+ supports the Selection object, but it does not support
+ * the `extend` method, which means that even in modern IE, it's not possible
+ * to programatically create a backward selection. Thus, for all IE
+ * versions, we use the old IE API to create our selections.
+ *
+ * @param {DOMElement|DOMTextNode} node
+ * @param {object} offsets
+ */
+function setModernOffsets(node, offsets) {
+ if (!window.getSelection) {
+ return;
+ }
+
+ var selection = window.getSelection();
+ var length = node[getTextContentAccessor()].length;
+ var start = Math.min(offsets.start, length);
+ var end = typeof offsets.end === 'undefined' ? start : Math.min(offsets.end, length);
+
+ // IE 11 uses modern selection, but doesn't support the extend method.
+ // Flip backward selections, so we can set with a single range.
+ if (!selection.extend && start > end) {
+ var temp = end;
+ end = start;
+ start = temp;
+ }
+
+ var startMarker = getNodeForCharacterOffset(node, start);
+ var endMarker = getNodeForCharacterOffset(node, end);
+
+ if (startMarker && endMarker) {
+ var range = document.createRange();
+ range.setStart(startMarker.node, startMarker.offset);
+ selection.removeAllRanges();
+
+ if (start > end) {
+ selection.addRange(range);
+ selection.extend(endMarker.node, endMarker.offset);
+ } else {
+ range.setEnd(endMarker.node, endMarker.offset);
+ selection.addRange(range);
+ }
+ }
+}
+
+var useIEOffsets = ExecutionEnvironment.canUseDOM && 'selection' in document && !('getSelection' in window);
+
+var ReactDOMSelection = {
+ /**
+ * @param {DOMElement} node
+ */
+ getOffsets: useIEOffsets ? getIEOffsets : getModernOffsets,
+
+ /**
+ * @param {DOMElement|DOMTextNode} node
+ * @param {object} offsets
+ */
+ setOffsets: useIEOffsets ? setIEOffsets : setModernOffsets
+};
+
+module.exports = ReactDOMSelection;
+},{"130":130,"131":131,"147":147}],50:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMServer
+ */
+
+'use strict';
+
+var ReactDefaultInjection = _dereq_(54);
+var ReactServerRendering = _dereq_(88);
+var ReactVersion = _dereq_(97);
+
+ReactDefaultInjection.inject();
+
+var ReactDOMServer = {
+ renderToString: ReactServerRendering.renderToString,
+ renderToStaticMarkup: ReactServerRendering.renderToStaticMarkup,
+ version: ReactVersion
+};
+
+module.exports = ReactDOMServer;
+},{"54":54,"88":88,"97":97}],51:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMTextComponent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMChildrenOperations = _dereq_(9);
+var DOMPropertyOperations = _dereq_(11);
+var ReactComponentBrowserEnvironment = _dereq_(35);
+var ReactMount = _dereq_(72);
+
+var assign = _dereq_(24);
+var escapeTextContentForBrowser = _dereq_(121);
+var setTextContent = _dereq_(139);
+var validateDOMNesting = _dereq_(144);
+
+/**
+ * Text nodes violate a couple assumptions that React makes about components:
+ *
+ * - When mounting text into the DOM, adjacent text nodes are merged.
+ * - Text nodes cannot be assigned a React root ID.
+ *
+ * This component is used to wrap strings in elements so that they can undergo
+ * the same reconciliation that is applied to elements.
+ *
+ * TODO: Investigate representing React components in the DOM with text nodes.
+ *
+ * @class ReactDOMTextComponent
+ * @extends ReactComponent
+ * @internal
+ */
+var ReactDOMTextComponent = function (props) {
+ // This constructor and its argument is currently used by mocks.
+};
+
+assign(ReactDOMTextComponent.prototype, {
+
+ /**
+ * @param {ReactText} text
+ * @internal
+ */
+ construct: function (text) {
+ // TODO: This is really a ReactText (ReactNode), not a ReactElement
+ this._currentElement = text;
+ this._stringText = '' + text;
+
+ // Properties
+ this._rootNodeID = null;
+ this._mountIndex = 0;
+ },
+
+ /**
+ * Creates the markup for this text node. This node is not intended to have
+ * any features besides containing text content.
+ *
+ * @param {string} rootID DOM ID of the root node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @return {string} Markup for this text node.
+ * @internal
+ */
+ mountComponent: function (rootID, transaction, context) {
+ if ("production" !== 'production') {
+ if (context[validateDOMNesting.ancestorInfoContextKey]) {
+ validateDOMNesting('span', null, context[validateDOMNesting.ancestorInfoContextKey]);
+ }
+ }
+
+ this._rootNodeID = rootID;
+ if (transaction.useCreateElement) {
+ var ownerDocument = context[ReactMount.ownerDocumentContextKey];
+ var el = ownerDocument.createElementNS('http://www.w3.org/1999/xhtml', 'span');
+ DOMPropertyOperations.setAttributeForID(el, rootID);
+ // Populate node cache
+ ReactMount.getID(el);
+ setTextContent(el, this._stringText);
+ return el;
+ } else {
+ var escapedText = escapeTextContentForBrowser(this._stringText);
+
+ if (transaction.renderToStaticMarkup) {
+ // Normally we'd wrap this in a `span` for the reasons stated above, but
+ // since this is a situation where React won't take over (static pages),
+ // we can simply return the text as it is.
+ return escapedText;
+ }
+
+ return '<span ' + DOMPropertyOperations.createMarkupForID(rootID) + '>' + escapedText + '</span>';
+ }
+ },
+
+ /**
+ * Updates this component by updating the text content.
+ *
+ * @param {ReactText} nextText The next text content
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ receiveComponent: function (nextText, transaction) {
+ if (nextText !== this._currentElement) {
+ this._currentElement = nextText;
+ var nextStringText = '' + nextText;
+ if (nextStringText !== this._stringText) {
+ // TODO: Save this as pending props and use performUpdateIfNecessary
+ // and/or updateComponent to do the actual update for consistency with
+ // other component types?
+ this._stringText = nextStringText;
+ var node = ReactMount.getNode(this._rootNodeID);
+ DOMChildrenOperations.updateTextContent(node, nextStringText);
+ }
+ }
+ },
+
+ unmountComponent: function () {
+ ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
+ }
+
+});
+
+module.exports = ReactDOMTextComponent;
+},{"11":11,"121":121,"139":139,"144":144,"24":24,"35":35,"72":72,"9":9}],52:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDOMTextarea
+ */
+
+'use strict';
+
+var LinkedValueUtils = _dereq_(23);
+var ReactDOMIDOperations = _dereq_(45);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+function forceUpdateIfMounted() {
+ if (this._rootNodeID) {
+ // DOM component is still mounted; update
+ ReactDOMTextarea.updateWrapper(this);
+ }
+}
+
+/**
+ * Implements a <textarea> native component that allows setting `value`, and
+ * `defaultValue`. This differs from the traditional DOM API because value is
+ * usually set as PCDATA children.
+ *
+ * If `value` is not supplied (or null/undefined), user actions that affect the
+ * value will trigger updates to the element.
+ *
+ * If `value` is supplied (and not null/undefined), the rendered element will
+ * not trigger updates to the element. Instead, the `value` prop must change in
+ * order for the rendered element to be updated.
+ *
+ * The rendered element will be initialized with an empty value, the prop
+ * `defaultValue` if specified, or the children content (deprecated).
+ */
+var ReactDOMTextarea = {
+ getNativeProps: function (inst, props, context) {
+ !(props.dangerouslySetInnerHTML == null) ? "production" !== 'production' ? invariant(false, '`dangerouslySetInnerHTML` does not make sense on <textarea>.') : invariant(false) : undefined;
+
+ // Always set children to the same thing. In IE9, the selection range will
+ // get reset if `textContent` is mutated.
+ var nativeProps = assign({}, props, {
+ defaultValue: undefined,
+ value: undefined,
+ children: inst._wrapperState.initialValue,
+ onChange: inst._wrapperState.onChange
+ });
+
+ return nativeProps;
+ },
+
+ mountWrapper: function (inst, props) {
+ if ("production" !== 'production') {
+ LinkedValueUtils.checkPropTypes('textarea', props, inst._currentElement._owner);
+ }
+
+ var defaultValue = props.defaultValue;
+ // TODO (yungsters): Remove support for children content in <textarea>.
+ var children = props.children;
+ if (children != null) {
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(false, 'Use the `defaultValue` or `value` props instead of setting ' + 'children on <textarea>.') : undefined;
+ }
+ !(defaultValue == null) ? "production" !== 'production' ? invariant(false, 'If you supply `defaultValue` on a <textarea>, do not pass children.') : invariant(false) : undefined;
+ if (Array.isArray(children)) {
+ !(children.length <= 1) ? "production" !== 'production' ? invariant(false, '<textarea> can only have at most one child.') : invariant(false) : undefined;
+ children = children[0];
+ }
+
+ defaultValue = '' + children;
+ }
+ if (defaultValue == null) {
+ defaultValue = '';
+ }
+ var value = LinkedValueUtils.getValue(props);
+
+ inst._wrapperState = {
+ // We save the initial value so that `ReactDOMComponent` doesn't update
+ // `textContent` (unnecessary since we update value).
+ // The initial value can be a boolean or object so that's why it's
+ // forced to be a string.
+ initialValue: '' + (value != null ? value : defaultValue),
+ onChange: _handleChange.bind(inst)
+ };
+ },
+
+ updateWrapper: function (inst) {
+ var props = inst._currentElement.props;
+ var value = LinkedValueUtils.getValue(props);
+ if (value != null) {
+ // Cast `value` to a string to ensure the value is set correctly. While
+ // browsers typically do this as necessary, jsdom doesn't.
+ ReactDOMIDOperations.updatePropertyByID(inst._rootNodeID, 'value', '' + value);
+ }
+ }
+};
+
+function _handleChange(event) {
+ var props = this._currentElement.props;
+ var returnValue = LinkedValueUtils.executeOnChange(props, event);
+ ReactUpdates.asap(forceUpdateIfMounted, this);
+ return returnValue;
+}
+
+module.exports = ReactDOMTextarea;
+},{"161":161,"173":173,"23":23,"24":24,"45":45,"96":96}],53:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultBatchingStrategy
+ */
+
+'use strict';
+
+var ReactUpdates = _dereq_(96);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+
+var RESET_BATCHED_UPDATES = {
+ initialize: emptyFunction,
+ close: function () {
+ ReactDefaultBatchingStrategy.isBatchingUpdates = false;
+ }
+};
+
+var FLUSH_BATCHED_UPDATES = {
+ initialize: emptyFunction,
+ close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
+};
+
+var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
+
+function ReactDefaultBatchingStrategyTransaction() {
+ this.reinitializeTransaction();
+}
+
+assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction.Mixin, {
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ }
+});
+
+var transaction = new ReactDefaultBatchingStrategyTransaction();
+
+var ReactDefaultBatchingStrategy = {
+ isBatchingUpdates: false,
+
+ /**
+ * Call the provided function in a context within which calls to `setState`
+ * and friends are batched such that components aren't updated unnecessarily.
+ */
+ batchedUpdates: function (callback, a, b, c, d, e) {
+ var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
+
+ ReactDefaultBatchingStrategy.isBatchingUpdates = true;
+
+ // The code is written this way to avoid extra allocations
+ if (alreadyBatchingUpdates) {
+ callback(a, b, c, d, e);
+ } else {
+ transaction.perform(callback, null, a, b, c, d, e);
+ }
+ }
+};
+
+module.exports = ReactDefaultBatchingStrategy;
+},{"113":113,"153":153,"24":24,"96":96}],54:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultInjection
+ */
+
+'use strict';
+
+var BeforeInputEventPlugin = _dereq_(3);
+var ChangeEventPlugin = _dereq_(7);
+var ClientReactRootIndex = _dereq_(8);
+var DefaultEventPluginOrder = _dereq_(13);
+var EnterLeaveEventPlugin = _dereq_(14);
+var ExecutionEnvironment = _dereq_(147);
+var HTMLDOMPropertyConfig = _dereq_(21);
+var ReactBrowserComponentMixin = _dereq_(27);
+var ReactComponentBrowserEnvironment = _dereq_(35);
+var ReactDefaultBatchingStrategy = _dereq_(53);
+var ReactDOMComponent = _dereq_(42);
+var ReactDOMTextComponent = _dereq_(51);
+var ReactEventListener = _dereq_(63);
+var ReactInjection = _dereq_(65);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMount = _dereq_(72);
+var ReactReconcileTransaction = _dereq_(83);
+var SelectEventPlugin = _dereq_(99);
+var ServerReactRootIndex = _dereq_(100);
+var SimpleEventPlugin = _dereq_(101);
+var SVGDOMPropertyConfig = _dereq_(98);
+
+var alreadyInjected = false;
+
+function inject() {
+ if (alreadyInjected) {
+ // TODO: This is currently true because these injections are shared between
+ // the client and the server package. They should be built independently
+ // and not share any injection state. Then this problem will be solved.
+ return;
+ }
+ alreadyInjected = true;
+
+ ReactInjection.EventEmitter.injectReactEventListener(ReactEventListener);
+
+ /**
+ * Inject modules for resolving DOM hierarchy and plugin ordering.
+ */
+ ReactInjection.EventPluginHub.injectEventPluginOrder(DefaultEventPluginOrder);
+ ReactInjection.EventPluginHub.injectInstanceHandle(ReactInstanceHandles);
+ ReactInjection.EventPluginHub.injectMount(ReactMount);
+
+ /**
+ * Some important event plugins included by default (without having to require
+ * them).
+ */
+ ReactInjection.EventPluginHub.injectEventPluginsByName({
+ SimpleEventPlugin: SimpleEventPlugin,
+ EnterLeaveEventPlugin: EnterLeaveEventPlugin,
+ ChangeEventPlugin: ChangeEventPlugin,
+ SelectEventPlugin: SelectEventPlugin,
+ BeforeInputEventPlugin: BeforeInputEventPlugin
+ });
+
+ ReactInjection.NativeComponent.injectGenericComponentClass(ReactDOMComponent);
+
+ ReactInjection.NativeComponent.injectTextComponentClass(ReactDOMTextComponent);
+
+ ReactInjection.Class.injectMixin(ReactBrowserComponentMixin);
+
+ ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig);
+ ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig);
+
+ ReactInjection.EmptyComponent.injectEmptyComponent('noscript');
+
+ ReactInjection.Updates.injectReconcileTransaction(ReactReconcileTransaction);
+ ReactInjection.Updates.injectBatchingStrategy(ReactDefaultBatchingStrategy);
+
+ ReactInjection.RootIndex.injectCreateReactRootIndex(ExecutionEnvironment.canUseDOM ? ClientReactRootIndex.createReactRootIndex : ServerReactRootIndex.createReactRootIndex);
+
+ ReactInjection.Component.injectEnvironment(ReactComponentBrowserEnvironment);
+
+ if ("production" !== 'production') {
+ var url = ExecutionEnvironment.canUseDOM && window.location.href || '';
+ if (/[?&]react_perf\b/.test(url)) {
+ var ReactDefaultPerf = _dereq_(55);
+ ReactDefaultPerf.start();
+ }
+ }
+}
+
+module.exports = {
+ inject: inject
+};
+},{"100":100,"101":101,"13":13,"14":14,"147":147,"21":21,"27":27,"3":3,"35":35,"42":42,"51":51,"53":53,"55":55,"63":63,"65":65,"67":67,"7":7,"72":72,"8":8,"83":83,"98":98,"99":99}],55:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultPerf
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ReactDefaultPerfAnalysis = _dereq_(56);
+var ReactMount = _dereq_(72);
+var ReactPerf = _dereq_(78);
+
+var performanceNow = _dereq_(170);
+
+function roundFloat(val) {
+ return Math.floor(val * 100) / 100;
+}
+
+function addValue(obj, key, val) {
+ obj[key] = (obj[key] || 0) + val;
+}
+
+var ReactDefaultPerf = {
+ _allMeasurements: [], // last item in the list is the current one
+ _mountStack: [0],
+ _injected: false,
+
+ start: function () {
+ if (!ReactDefaultPerf._injected) {
+ ReactPerf.injection.injectMeasure(ReactDefaultPerf.measure);
+ }
+
+ ReactDefaultPerf._allMeasurements.length = 0;
+ ReactPerf.enableMeasure = true;
+ },
+
+ stop: function () {
+ ReactPerf.enableMeasure = false;
+ },
+
+ getLastMeasurements: function () {
+ return ReactDefaultPerf._allMeasurements;
+ },
+
+ printExclusive: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ var summary = ReactDefaultPerfAnalysis.getExclusiveSummary(measurements);
+ console.table(summary.map(function (item) {
+ return {
+ 'Component class name': item.componentName,
+ 'Total inclusive time (ms)': roundFloat(item.inclusive),
+ 'Exclusive mount time (ms)': roundFloat(item.exclusive),
+ 'Exclusive render time (ms)': roundFloat(item.render),
+ 'Mount time per instance (ms)': roundFloat(item.exclusive / item.count),
+ 'Render time per instance (ms)': roundFloat(item.render / item.count),
+ 'Instances': item.count
+ };
+ }));
+ // TODO: ReactDefaultPerfAnalysis.getTotalTime() does not return the correct
+ // number.
+ },
+
+ printInclusive: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(measurements);
+ console.table(summary.map(function (item) {
+ return {
+ 'Owner > component': item.componentName,
+ 'Inclusive time (ms)': roundFloat(item.time),
+ 'Instances': item.count
+ };
+ }));
+ console.log('Total time:', ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms');
+ },
+
+ getMeasurementsSummaryMap: function (measurements) {
+ var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(measurements, true);
+ return summary.map(function (item) {
+ return {
+ 'Owner > component': item.componentName,
+ 'Wasted time (ms)': item.time,
+ 'Instances': item.count
+ };
+ });
+ },
+
+ printWasted: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ console.table(ReactDefaultPerf.getMeasurementsSummaryMap(measurements));
+ console.log('Total time:', ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms');
+ },
+
+ printDOM: function (measurements) {
+ measurements = measurements || ReactDefaultPerf._allMeasurements;
+ var summary = ReactDefaultPerfAnalysis.getDOMSummary(measurements);
+ console.table(summary.map(function (item) {
+ var result = {};
+ result[DOMProperty.ID_ATTRIBUTE_NAME] = item.id;
+ result.type = item.type;
+ result.args = JSON.stringify(item.args);
+ return result;
+ }));
+ console.log('Total time:', ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms');
+ },
+
+ _recordWrite: function (id, fnName, totalTime, args) {
+ // TODO: totalTime isn't that useful since it doesn't count paints/reflows
+ var writes = ReactDefaultPerf._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1].writes;
+ writes[id] = writes[id] || [];
+ writes[id].push({
+ type: fnName,
+ time: totalTime,
+ args: args
+ });
+ },
+
+ measure: function (moduleName, fnName, func) {
+ return function () {
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ var totalTime;
+ var rv;
+ var start;
+
+ if (fnName === '_renderNewRootComponent' || fnName === 'flushBatchedUpdates') {
+ // A "measurement" is a set of metrics recorded for each flush. We want
+ // to group the metrics for a given flush together so we can look at the
+ // components that rendered and the DOM operations that actually
+ // happened to determine the amount of "wasted work" performed.
+ ReactDefaultPerf._allMeasurements.push({
+ exclusive: {},
+ inclusive: {},
+ render: {},
+ counts: {},
+ writes: {},
+ displayNames: {},
+ totalTime: 0,
+ created: {}
+ });
+ start = performanceNow();
+ rv = func.apply(this, args);
+ ReactDefaultPerf._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1].totalTime = performanceNow() - start;
+ return rv;
+ } else if (fnName === '_mountImageIntoNode' || moduleName === 'ReactBrowserEventEmitter' || moduleName === 'ReactDOMIDOperations' || moduleName === 'CSSPropertyOperations' || moduleName === 'DOMChildrenOperations' || moduleName === 'DOMPropertyOperations') {
+ start = performanceNow();
+ rv = func.apply(this, args);
+ totalTime = performanceNow() - start;
+
+ if (fnName === '_mountImageIntoNode') {
+ var mountID = ReactMount.getID(args[1]);
+ ReactDefaultPerf._recordWrite(mountID, fnName, totalTime, args[0]);
+ } else if (fnName === 'dangerouslyProcessChildrenUpdates') {
+ // special format
+ args[0].forEach(function (update) {
+ var writeArgs = {};
+ if (update.fromIndex !== null) {
+ writeArgs.fromIndex = update.fromIndex;
+ }
+ if (update.toIndex !== null) {
+ writeArgs.toIndex = update.toIndex;
+ }
+ if (update.textContent !== null) {
+ writeArgs.textContent = update.textContent;
+ }
+ if (update.markupIndex !== null) {
+ writeArgs.markup = args[1][update.markupIndex];
+ }
+ ReactDefaultPerf._recordWrite(update.parentID, update.type, totalTime, writeArgs);
+ });
+ } else {
+ // basic format
+ var id = args[0];
+ if (typeof id === 'object') {
+ id = ReactMount.getID(args[0]);
+ }
+ ReactDefaultPerf._recordWrite(id, fnName, totalTime, Array.prototype.slice.call(args, 1));
+ }
+ return rv;
+ } else if (moduleName === 'ReactCompositeComponent' && (fnName === 'mountComponent' || fnName === 'updateComponent' || // TODO: receiveComponent()?
+ fnName === '_renderValidatedComponent')) {
+
+ if (this._currentElement.type === ReactMount.TopLevelWrapper) {
+ return func.apply(this, args);
+ }
+
+ var rootNodeID = fnName === 'mountComponent' ? args[0] : this._rootNodeID;
+ var isRender = fnName === '_renderValidatedComponent';
+ var isMount = fnName === 'mountComponent';
+
+ var mountStack = ReactDefaultPerf._mountStack;
+ var entry = ReactDefaultPerf._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1];
+
+ if (isRender) {
+ addValue(entry.counts, rootNodeID, 1);
+ } else if (isMount) {
+ entry.created[rootNodeID] = true;
+ mountStack.push(0);
+ }
+
+ start = performanceNow();
+ rv = func.apply(this, args);
+ totalTime = performanceNow() - start;
+
+ if (isRender) {
+ addValue(entry.render, rootNodeID, totalTime);
+ } else if (isMount) {
+ var subMountTime = mountStack.pop();
+ mountStack[mountStack.length - 1] += totalTime;
+ addValue(entry.exclusive, rootNodeID, totalTime - subMountTime);
+ addValue(entry.inclusive, rootNodeID, totalTime);
+ } else {
+ addValue(entry.inclusive, rootNodeID, totalTime);
+ }
+
+ entry.displayNames[rootNodeID] = {
+ current: this.getName(),
+ owner: this._currentElement._owner ? this._currentElement._owner.getName() : '<root>'
+ };
+
+ return rv;
+ } else {
+ return func.apply(this, args);
+ }
+ };
+ }
+};
+
+module.exports = ReactDefaultPerf;
+},{"10":10,"170":170,"56":56,"72":72,"78":78}],56:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactDefaultPerfAnalysis
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+
+// Don't try to save users less than 1.2ms (a number I made up)
+var DONT_CARE_THRESHOLD = 1.2;
+var DOM_OPERATION_TYPES = {
+ '_mountImageIntoNode': 'set innerHTML',
+ INSERT_MARKUP: 'set innerHTML',
+ MOVE_EXISTING: 'move',
+ REMOVE_NODE: 'remove',
+ SET_MARKUP: 'set innerHTML',
+ TEXT_CONTENT: 'set textContent',
+ 'setValueForProperty': 'update attribute',
+ 'setValueForAttribute': 'update attribute',
+ 'deleteValueForProperty': 'remove attribute',
+ 'setValueForStyles': 'update styles',
+ 'replaceNodeWithMarkup': 'replace',
+ 'updateTextContent': 'set textContent'
+};
+
+function getTotalTime(measurements) {
+ // TODO: return number of DOM ops? could be misleading.
+ // TODO: measure dropped frames after reconcile?
+ // TODO: log total time of each reconcile and the top-level component
+ // class that triggered it.
+ var totalTime = 0;
+ for (var i = 0; i < measurements.length; i++) {
+ var measurement = measurements[i];
+ totalTime += measurement.totalTime;
+ }
+ return totalTime;
+}
+
+function getDOMSummary(measurements) {
+ var items = [];
+ measurements.forEach(function (measurement) {
+ Object.keys(measurement.writes).forEach(function (id) {
+ measurement.writes[id].forEach(function (write) {
+ items.push({
+ id: id,
+ type: DOM_OPERATION_TYPES[write.type] || write.type,
+ args: write.args
+ });
+ });
+ });
+ });
+ return items;
+}
+
+function getExclusiveSummary(measurements) {
+ var candidates = {};
+ var displayName;
+
+ for (var i = 0; i < measurements.length; i++) {
+ var measurement = measurements[i];
+ var allIDs = assign({}, measurement.exclusive, measurement.inclusive);
+
+ for (var id in allIDs) {
+ displayName = measurement.displayNames[id].current;
+
+ candidates[displayName] = candidates[displayName] || {
+ componentName: displayName,
+ inclusive: 0,
+ exclusive: 0,
+ render: 0,
+ count: 0
+ };
+ if (measurement.render[id]) {
+ candidates[displayName].render += measurement.render[id];
+ }
+ if (measurement.exclusive[id]) {
+ candidates[displayName].exclusive += measurement.exclusive[id];
+ }
+ if (measurement.inclusive[id]) {
+ candidates[displayName].inclusive += measurement.inclusive[id];
+ }
+ if (measurement.counts[id]) {
+ candidates[displayName].count += measurement.counts[id];
+ }
+ }
+ }
+
+ // Now make a sorted array with the results.
+ var arr = [];
+ for (displayName in candidates) {
+ if (candidates[displayName].exclusive >= DONT_CARE_THRESHOLD) {
+ arr.push(candidates[displayName]);
+ }
+ }
+
+ arr.sort(function (a, b) {
+ return b.exclusive - a.exclusive;
+ });
+
+ return arr;
+}
+
+function getInclusiveSummary(measurements, onlyClean) {
+ var candidates = {};
+ var inclusiveKey;
+
+ for (var i = 0; i < measurements.length; i++) {
+ var measurement = measurements[i];
+ var allIDs = assign({}, measurement.exclusive, measurement.inclusive);
+ var cleanComponents;
+
+ if (onlyClean) {
+ cleanComponents = getUnchangedComponents(measurement);
+ }
+
+ for (var id in allIDs) {
+ if (onlyClean && !cleanComponents[id]) {
+ continue;
+ }
+
+ var displayName = measurement.displayNames[id];
+
+ // Inclusive time is not useful for many components without knowing where
+ // they are instantiated. So we aggregate inclusive time with both the
+ // owner and current displayName as the key.
+ inclusiveKey = displayName.owner + ' > ' + displayName.current;
+
+ candidates[inclusiveKey] = candidates[inclusiveKey] || {
+ componentName: inclusiveKey,
+ time: 0,
+ count: 0
+ };
+
+ if (measurement.inclusive[id]) {
+ candidates[inclusiveKey].time += measurement.inclusive[id];
+ }
+ if (measurement.counts[id]) {
+ candidates[inclusiveKey].count += measurement.counts[id];
+ }
+ }
+ }
+
+ // Now make a sorted array with the results.
+ var arr = [];
+ for (inclusiveKey in candidates) {
+ if (candidates[inclusiveKey].time >= DONT_CARE_THRESHOLD) {
+ arr.push(candidates[inclusiveKey]);
+ }
+ }
+
+ arr.sort(function (a, b) {
+ return b.time - a.time;
+ });
+
+ return arr;
+}
+
+function getUnchangedComponents(measurement) {
+ // For a given reconcile, look at which components did not actually
+ // render anything to the DOM and return a mapping of their ID to
+ // the amount of time it took to render the entire subtree.
+ var cleanComponents = {};
+ var dirtyLeafIDs = Object.keys(measurement.writes);
+ var allIDs = assign({}, measurement.exclusive, measurement.inclusive);
+
+ for (var id in allIDs) {
+ var isDirty = false;
+ // For each component that rendered, see if a component that triggered
+ // a DOM op is in its subtree.
+ for (var i = 0; i < dirtyLeafIDs.length; i++) {
+ if (dirtyLeafIDs[i].indexOf(id) === 0) {
+ isDirty = true;
+ break;
+ }
+ }
+ // check if component newly created
+ if (measurement.created[id]) {
+ isDirty = true;
+ }
+ if (!isDirty && measurement.counts[id] > 0) {
+ cleanComponents[id] = true;
+ }
+ }
+ return cleanComponents;
+}
+
+var ReactDefaultPerfAnalysis = {
+ getExclusiveSummary: getExclusiveSummary,
+ getInclusiveSummary: getInclusiveSummary,
+ getDOMSummary: getDOMSummary,
+ getTotalTime: getTotalTime
+};
+
+module.exports = ReactDefaultPerfAnalysis;
+},{"24":24}],57:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactElement
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+
+var assign = _dereq_(24);
+var canDefineProperty = _dereq_(117);
+
+// The Symbol used to tag the ReactElement type. If there is no native Symbol
+// nor polyfill, then a plain number is used for performance.
+var REACT_ELEMENT_TYPE = typeof Symbol === 'function' && Symbol['for'] && Symbol['for']('react.element') || 0xeac7;
+
+var RESERVED_PROPS = {
+ key: true,
+ ref: true,
+ __self: true,
+ __source: true
+};
+
+/**
+ * Base constructor for all React elements. This is only used to make this
+ * work with a dynamic instanceof check. Nothing should live on this prototype.
+ *
+ * @param {*} type
+ * @param {*} key
+ * @param {string|object} ref
+ * @param {*} self A *temporary* helper to detect places where `this` is
+ * different from the `owner` when React.createElement is called, so that we
+ * can warn. We want to get rid of owner and replace string `ref`s with arrow
+ * functions, and as long as `this` and owner are the same, there will be no
+ * change in behavior.
+ * @param {*} source An annotation object (added by a transpiler or otherwise)
+ * indicating filename, line number, and/or other information.
+ * @param {*} owner
+ * @param {*} props
+ * @internal
+ */
+var ReactElement = function (type, key, ref, self, source, owner, props) {
+ var element = {
+ // This tag allow us to uniquely identify this as a React Element
+ $$typeof: REACT_ELEMENT_TYPE,
+
+ // Built-in properties that belong on the element
+ type: type,
+ key: key,
+ ref: ref,
+ props: props,
+
+ // Record the component responsible for creating this element.
+ _owner: owner
+ };
+
+ if ("production" !== 'production') {
+ // The validation flag is currently mutative. We put it on
+ // an external backing store so that we can freeze the whole object.
+ // This can be replaced with a WeakMap once they are implemented in
+ // commonly used development environments.
+ element._store = {};
+
+ // To make comparing ReactElements easier for testing purposes, we make
+ // the validation flag non-enumerable (where possible, which should
+ // include every environment we run tests in), so the test framework
+ // ignores it.
+ if (canDefineProperty) {
+ Object.defineProperty(element._store, 'validated', {
+ configurable: false,
+ enumerable: false,
+ writable: true,
+ value: false
+ });
+ // self and source are DEV only properties.
+ Object.defineProperty(element, '_self', {
+ configurable: false,
+ enumerable: false,
+ writable: false,
+ value: self
+ });
+ // Two elements created in two different places should be considered
+ // equal for testing purposes and therefore we hide it from enumeration.
+ Object.defineProperty(element, '_source', {
+ configurable: false,
+ enumerable: false,
+ writable: false,
+ value: source
+ });
+ } else {
+ element._store.validated = false;
+ element._self = self;
+ element._source = source;
+ }
+ Object.freeze(element.props);
+ Object.freeze(element);
+ }
+
+ return element;
+};
+
+ReactElement.createElement = function (type, config, children) {
+ var propName;
+
+ // Reserved names are extracted
+ var props = {};
+
+ var key = null;
+ var ref = null;
+ var self = null;
+ var source = null;
+
+ if (config != null) {
+ ref = config.ref === undefined ? null : config.ref;
+ key = config.key === undefined ? null : '' + config.key;
+ self = config.__self === undefined ? null : config.__self;
+ source = config.__source === undefined ? null : config.__source;
+ // Remaining properties are added to a new props object
+ for (propName in config) {
+ if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
+ props[propName] = config[propName];
+ }
+ }
+ }
+
+ // Children can be more than one argument, and those are transferred onto
+ // the newly allocated props object.
+ var childrenLength = arguments.length - 2;
+ if (childrenLength === 1) {
+ props.children = children;
+ } else if (childrenLength > 1) {
+ var childArray = Array(childrenLength);
+ for (var i = 0; i < childrenLength; i++) {
+ childArray[i] = arguments[i + 2];
+ }
+ props.children = childArray;
+ }
+
+ // Resolve default props
+ if (type && type.defaultProps) {
+ var defaultProps = type.defaultProps;
+ for (propName in defaultProps) {
+ if (typeof props[propName] === 'undefined') {
+ props[propName] = defaultProps[propName];
+ }
+ }
+ }
+
+ return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
+};
+
+ReactElement.createFactory = function (type) {
+ var factory = ReactElement.createElement.bind(null, type);
+ // Expose the type on the factory and the prototype so that it can be
+ // easily accessed on elements. E.g. `<Foo />.type === Foo`.
+ // This should not be named `constructor` since this may not be the function
+ // that created the element, and it may not even be a constructor.
+ // Legacy hook TODO: Warn if this is accessed
+ factory.type = type;
+ return factory;
+};
+
+ReactElement.cloneAndReplaceKey = function (oldElement, newKey) {
+ var newElement = ReactElement(oldElement.type, newKey, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, oldElement.props);
+
+ return newElement;
+};
+
+ReactElement.cloneAndReplaceProps = function (oldElement, newProps) {
+ var newElement = ReactElement(oldElement.type, oldElement.key, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, newProps);
+
+ if ("production" !== 'production') {
+ // If the key on the original is valid, then the clone is valid
+ newElement._store.validated = oldElement._store.validated;
+ }
+
+ return newElement;
+};
+
+ReactElement.cloneElement = function (element, config, children) {
+ var propName;
+
+ // Original props are copied
+ var props = assign({}, element.props);
+
+ // Reserved names are extracted
+ var key = element.key;
+ var ref = element.ref;
+ // Self is preserved since the owner is preserved.
+ var self = element._self;
+ // Source is preserved since cloneElement is unlikely to be targeted by a
+ // transpiler, and the original source is probably a better indicator of the
+ // true owner.
+ var source = element._source;
+
+ // Owner will be preserved, unless ref is overridden
+ var owner = element._owner;
+
+ if (config != null) {
+ if (config.ref !== undefined) {
+ // Silently steal the ref from the parent.
+ ref = config.ref;
+ owner = ReactCurrentOwner.current;
+ }
+ if (config.key !== undefined) {
+ key = '' + config.key;
+ }
+ // Remaining properties override existing props
+ for (propName in config) {
+ if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
+ props[propName] = config[propName];
+ }
+ }
+ }
+
+ // Children can be more than one argument, and those are transferred onto
+ // the newly allocated props object.
+ var childrenLength = arguments.length - 2;
+ if (childrenLength === 1) {
+ props.children = children;
+ } else if (childrenLength > 1) {
+ var childArray = Array(childrenLength);
+ for (var i = 0; i < childrenLength; i++) {
+ childArray[i] = arguments[i + 2];
+ }
+ props.children = childArray;
+ }
+
+ return ReactElement(element.type, key, ref, self, source, owner, props);
+};
+
+/**
+ * @param {?object} object
+ * @return {boolean} True if `object` is a valid component.
+ * @final
+ */
+ReactElement.isValidElement = function (object) {
+ return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
+};
+
+module.exports = ReactElement;
+},{"117":117,"24":24,"39":39}],58:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactElementValidator
+ */
+
+/**
+ * ReactElementValidator provides a wrapper around a element factory
+ * which validates the props passed to the element. This is intended to be
+ * used only in DEV and could be replaced by a static type checker for languages
+ * that support it.
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactPropTypeLocations = _dereq_(81);
+var ReactPropTypeLocationNames = _dereq_(80);
+var ReactCurrentOwner = _dereq_(39);
+
+var canDefineProperty = _dereq_(117);
+var getIteratorFn = _dereq_(129);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+function getDeclarationErrorAddendum() {
+ if (ReactCurrentOwner.current) {
+ var name = ReactCurrentOwner.current.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+/**
+ * Warn if there's no key explicitly set on dynamic arrays of children or
+ * object keys are not valid. This allows us to keep track of children between
+ * updates.
+ */
+var ownerHasKeyUseWarning = {};
+
+var loggedTypeFailures = {};
+
+/**
+ * Warn if the element doesn't have an explicit key assigned to it.
+ * This element is in an array. The array could grow and shrink or be
+ * reordered. All children that haven't already been validated are required to
+ * have a "key" property assigned to it.
+ *
+ * @internal
+ * @param {ReactElement} element Element that requires a key.
+ * @param {*} parentType element's parent's type.
+ */
+function validateExplicitKey(element, parentType) {
+ if (!element._store || element._store.validated || element.key != null) {
+ return;
+ }
+ element._store.validated = true;
+
+ var addenda = getAddendaForKeyUse('uniqueKey', element, parentType);
+ if (addenda === null) {
+ // we already showed the warning
+ return;
+ }
+ "production" !== 'production' ? warning(false, 'Each child in an array or iterator should have a unique "key" prop.' + '%s%s%s', addenda.parentOrOwner || '', addenda.childOwner || '', addenda.url || '') : undefined;
+}
+
+/**
+ * Shared warning and monitoring code for the key warnings.
+ *
+ * @internal
+ * @param {string} messageType A key used for de-duping warnings.
+ * @param {ReactElement} element Component that requires a key.
+ * @param {*} parentType element's parent's type.
+ * @returns {?object} A set of addenda to use in the warning message, or null
+ * if the warning has already been shown before (and shouldn't be shown again).
+ */
+function getAddendaForKeyUse(messageType, element, parentType) {
+ var addendum = getDeclarationErrorAddendum();
+ if (!addendum) {
+ var parentName = typeof parentType === 'string' ? parentType : parentType.displayName || parentType.name;
+ if (parentName) {
+ addendum = ' Check the top-level render call using <' + parentName + '>.';
+ }
+ }
+
+ var memoizer = ownerHasKeyUseWarning[messageType] || (ownerHasKeyUseWarning[messageType] = {});
+ if (memoizer[addendum]) {
+ return null;
+ }
+ memoizer[addendum] = true;
+
+ var addenda = {
+ parentOrOwner: addendum,
+ url: ' See https://fb.me/react-warning-keys for more information.',
+ childOwner: null
+ };
+
+ // Usually the current owner is the offender, but if it accepts children as a
+ // property, it may be the creator of the child that's responsible for
+ // assigning it a key.
+ if (element && element._owner && element._owner !== ReactCurrentOwner.current) {
+ // Give the component that originally created this child.
+ addenda.childOwner = ' It was passed a child from ' + element._owner.getName() + '.';
+ }
+
+ return addenda;
+}
+
+/**
+ * Ensure that every element either is passed in a static location, in an
+ * array with an explicit keys property defined, or in an object literal
+ * with valid key property.
+ *
+ * @internal
+ * @param {ReactNode} node Statically passed child of any type.
+ * @param {*} parentType node's parent's type.
+ */
+function validateChildKeys(node, parentType) {
+ if (typeof node !== 'object') {
+ return;
+ }
+ if (Array.isArray(node)) {
+ for (var i = 0; i < node.length; i++) {
+ var child = node[i];
+ if (ReactElement.isValidElement(child)) {
+ validateExplicitKey(child, parentType);
+ }
+ }
+ } else if (ReactElement.isValidElement(node)) {
+ // This element was passed in a valid location.
+ if (node._store) {
+ node._store.validated = true;
+ }
+ } else if (node) {
+ var iteratorFn = getIteratorFn(node);
+ // Entry iterators provide implicit keys.
+ if (iteratorFn) {
+ if (iteratorFn !== node.entries) {
+ var iterator = iteratorFn.call(node);
+ var step;
+ while (!(step = iterator.next()).done) {
+ if (ReactElement.isValidElement(step.value)) {
+ validateExplicitKey(step.value, parentType);
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Assert that the props are valid
+ *
+ * @param {string} componentName Name of the component for error messages.
+ * @param {object} propTypes Map of prop name to a ReactPropType
+ * @param {object} props
+ * @param {string} location e.g. "prop", "context", "child context"
+ * @private
+ */
+function checkPropTypes(componentName, propTypes, props, location) {
+ for (var propName in propTypes) {
+ if (propTypes.hasOwnProperty(propName)) {
+ var error;
+ // Prop type validation may throw. In case they do, we don't want to
+ // fail the render phase where it didn't fail before. So we log it.
+ // After these have been cleaned up, we'll let them throw.
+ try {
+ // This is intentionally an invariant that gets caught. It's the same
+ // behavior as without this statement except with a better message.
+ !(typeof propTypes[propName] === 'function') ? "production" !== 'production' ? invariant(false, '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', componentName || 'React class', ReactPropTypeLocationNames[location], propName) : invariant(false) : undefined;
+ error = propTypes[propName](props, propName, componentName, location);
+ } catch (ex) {
+ error = ex;
+ }
+ "production" !== 'production' ? warning(!error || error instanceof Error, '%s: type specification of %s `%s` is invalid; the type checker ' + 'function must return `null` or an `Error` but returned a %s. ' + 'You may have forgotten to pass an argument to the type checker ' + 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + 'shape all require an argument).', componentName || 'React class', ReactPropTypeLocationNames[location], propName, typeof error) : undefined;
+ if (error instanceof Error && !(error.message in loggedTypeFailures)) {
+ // Only monitor this failure once because there tends to be a lot of the
+ // same error.
+ loggedTypeFailures[error.message] = true;
+
+ var addendum = getDeclarationErrorAddendum();
+ "production" !== 'production' ? warning(false, 'Failed propType: %s%s', error.message, addendum) : undefined;
+ }
+ }
+ }
+}
+
+/**
+ * Given an element, validate that its props follow the propTypes definition,
+ * provided by the type.
+ *
+ * @param {ReactElement} element
+ */
+function validatePropTypes(element) {
+ var componentClass = element.type;
+ if (typeof componentClass !== 'function') {
+ return;
+ }
+ var name = componentClass.displayName || componentClass.name;
+ if (componentClass.propTypes) {
+ checkPropTypes(name, componentClass.propTypes, element.props, ReactPropTypeLocations.prop);
+ }
+ if (typeof componentClass.getDefaultProps === 'function') {
+ "production" !== 'production' ? warning(componentClass.getDefaultProps.isReactClassApproved, 'getDefaultProps is only used on classic React.createClass ' + 'definitions. Use a static property named `defaultProps` instead.') : undefined;
+ }
+}
+
+var ReactElementValidator = {
+
+ createElement: function (type, props, children) {
+ var validType = typeof type === 'string' || typeof type === 'function';
+ // We warn in this case but don't throw. We expect the element creation to
+ // succeed and there will likely be errors in render.
+ "production" !== 'production' ? warning(validType, 'React.createElement: type should not be null, undefined, boolean, or ' + 'number. It should be a string (for DOM elements) or a ReactClass ' + '(for composite components).%s', getDeclarationErrorAddendum()) : undefined;
+
+ var element = ReactElement.createElement.apply(this, arguments);
+
+ // The result can be nullish if a mock or a custom function is used.
+ // TODO: Drop this when these are no longer allowed as the type argument.
+ if (element == null) {
+ return element;
+ }
+
+ // Skip key warning if the type isn't valid since our key validation logic
+ // doesn't expect a non-string/function type and can throw confusing errors.
+ // We don't want exception behavior to differ between dev and prod.
+ // (Rendering will throw with a helpful message and as soon as the type is
+ // fixed, the key warnings will appear.)
+ if (validType) {
+ for (var i = 2; i < arguments.length; i++) {
+ validateChildKeys(arguments[i], type);
+ }
+ }
+
+ validatePropTypes(element);
+
+ return element;
+ },
+
+ createFactory: function (type) {
+ var validatedFactory = ReactElementValidator.createElement.bind(null, type);
+ // Legacy hook TODO: Warn if this is accessed
+ validatedFactory.type = type;
+
+ if ("production" !== 'production') {
+ if (canDefineProperty) {
+ Object.defineProperty(validatedFactory, 'type', {
+ enumerable: false,
+ get: function () {
+ "production" !== 'production' ? warning(false, 'Factory.type is deprecated. Access the class directly ' + 'before passing it to createFactory.') : undefined;
+ Object.defineProperty(this, 'type', {
+ value: type
+ });
+ return type;
+ }
+ });
+ }
+ }
+
+ return validatedFactory;
+ },
+
+ cloneElement: function (element, props, children) {
+ var newElement = ReactElement.cloneElement.apply(this, arguments);
+ for (var i = 2; i < arguments.length; i++) {
+ validateChildKeys(arguments[i], newElement.type);
+ }
+ validatePropTypes(newElement);
+ return newElement;
+ }
+
+};
+
+module.exports = ReactElementValidator;
+},{"117":117,"129":129,"161":161,"173":173,"39":39,"57":57,"80":80,"81":81}],59:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEmptyComponent
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactEmptyComponentRegistry = _dereq_(60);
+var ReactReconciler = _dereq_(84);
+
+var assign = _dereq_(24);
+
+var placeholderElement;
+
+var ReactEmptyComponentInjection = {
+ injectEmptyComponent: function (component) {
+ placeholderElement = ReactElement.createElement(component);
+ }
+};
+
+var ReactEmptyComponent = function (instantiate) {
+ this._currentElement = null;
+ this._rootNodeID = null;
+ this._renderedComponent = instantiate(placeholderElement);
+};
+assign(ReactEmptyComponent.prototype, {
+ construct: function (element) {},
+ mountComponent: function (rootID, transaction, context) {
+ ReactEmptyComponentRegistry.registerNullComponentID(rootID);
+ this._rootNodeID = rootID;
+ return ReactReconciler.mountComponent(this._renderedComponent, rootID, transaction, context);
+ },
+ receiveComponent: function () {},
+ unmountComponent: function (rootID, transaction, context) {
+ ReactReconciler.unmountComponent(this._renderedComponent);
+ ReactEmptyComponentRegistry.deregisterNullComponentID(this._rootNodeID);
+ this._rootNodeID = null;
+ this._renderedComponent = null;
+ }
+});
+
+ReactEmptyComponent.injection = ReactEmptyComponentInjection;
+
+module.exports = ReactEmptyComponent;
+},{"24":24,"57":57,"60":60,"84":84}],60:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEmptyComponentRegistry
+ */
+
+'use strict';
+
+// This registry keeps track of the React IDs of the components that rendered to
+// `null` (in reality a placeholder such as `noscript`)
+var nullComponentIDsRegistry = {};
+
+/**
+ * @param {string} id Component's `_rootNodeID`.
+ * @return {boolean} True if the component is rendered to null.
+ */
+function isNullComponentID(id) {
+ return !!nullComponentIDsRegistry[id];
+}
+
+/**
+ * Mark the component as having rendered to null.
+ * @param {string} id Component's `_rootNodeID`.
+ */
+function registerNullComponentID(id) {
+ nullComponentIDsRegistry[id] = true;
+}
+
+/**
+ * Unmark the component as having rendered to null: it renders to something now.
+ * @param {string} id Component's `_rootNodeID`.
+ */
+function deregisterNullComponentID(id) {
+ delete nullComponentIDsRegistry[id];
+}
+
+var ReactEmptyComponentRegistry = {
+ isNullComponentID: isNullComponentID,
+ registerNullComponentID: registerNullComponentID,
+ deregisterNullComponentID: deregisterNullComponentID
+};
+
+module.exports = ReactEmptyComponentRegistry;
+},{}],61:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactErrorUtils
+ * @typechecks
+ */
+
+'use strict';
+
+var caughtError = null;
+
+/**
+ * Call a function while guarding against errors that happens within it.
+ *
+ * @param {?String} name of the guard to use for logging or debugging
+ * @param {Function} func The function to invoke
+ * @param {*} a First argument
+ * @param {*} b Second argument
+ */
+function invokeGuardedCallback(name, func, a, b) {
+ try {
+ return func(a, b);
+ } catch (x) {
+ if (caughtError === null) {
+ caughtError = x;
+ }
+ return undefined;
+ }
+}
+
+var ReactErrorUtils = {
+ invokeGuardedCallback: invokeGuardedCallback,
+
+ /**
+ * Invoked by ReactTestUtils.Simulate so that any errors thrown by the event
+ * handler are sure to be rethrown by rethrowCaughtError.
+ */
+ invokeGuardedCallbackWithCatch: invokeGuardedCallback,
+
+ /**
+ * During execution of guarded functions we will capture the first error which
+ * we will rethrow to be handled by the top level error handler.
+ */
+ rethrowCaughtError: function () {
+ if (caughtError) {
+ var error = caughtError;
+ caughtError = null;
+ throw error;
+ }
+ }
+};
+
+if ("production" !== 'production') {
+ /**
+ * To help development we can get better devtools integration by simulating a
+ * real browser event.
+ */
+ if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function') {
+ var fakeNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'react');
+ ReactErrorUtils.invokeGuardedCallback = function (name, func, a, b) {
+ var boundFunc = func.bind(null, a, b);
+ var evtType = 'react-' + name;
+ fakeNode.addEventListener(evtType, boundFunc, false);
+ var evt = document.createEvent('Event');
+ evt.initEvent(evtType, false, false);
+ fakeNode.dispatchEvent(evt);
+ fakeNode.removeEventListener(evtType, boundFunc, false);
+ };
+ }
+}
+
+module.exports = ReactErrorUtils;
+},{}],62:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEventEmitterMixin
+ */
+
+'use strict';
+
+var EventPluginHub = _dereq_(16);
+
+function runEventQueueInBatch(events) {
+ EventPluginHub.enqueueEvents(events);
+ EventPluginHub.processEventQueue(false);
+}
+
+var ReactEventEmitterMixin = {
+
+ /**
+ * Streams a fired top-level event to `EventPluginHub` where plugins have the
+ * opportunity to create `ReactEvent`s to be dispatched.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {object} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native environment event.
+ */
+ handleTopLevel: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var events = EventPluginHub.extractEvents(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget);
+ runEventQueueInBatch(events);
+ }
+};
+
+module.exports = ReactEventEmitterMixin;
+},{"16":16}],63:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactEventListener
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var EventListener = _dereq_(146);
+var ExecutionEnvironment = _dereq_(147);
+var PooledClass = _dereq_(25);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var getEventTarget = _dereq_(128);
+var getUnboundedScrollPosition = _dereq_(158);
+
+var DOCUMENT_FRAGMENT_NODE_TYPE = 11;
+
+/**
+ * Finds the parent React component of `node`.
+ *
+ * @param {*} node
+ * @return {?DOMEventTarget} Parent container, or `null` if the specified node
+ * is not nested.
+ */
+function findParent(node) {
+ // TODO: It may be a good idea to cache this to prevent unnecessary DOM
+ // traversal, but caching is difficult to do correctly without using a
+ // mutation observer to listen for all DOM changes.
+ var nodeID = ReactMount.getID(node);
+ var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID);
+ var container = ReactMount.findReactContainerForID(rootID);
+ var parent = ReactMount.getFirstReactDOM(container);
+ return parent;
+}
+
+// Used to store ancestor hierarchy in top level callback
+function TopLevelCallbackBookKeeping(topLevelType, nativeEvent) {
+ this.topLevelType = topLevelType;
+ this.nativeEvent = nativeEvent;
+ this.ancestors = [];
+}
+assign(TopLevelCallbackBookKeeping.prototype, {
+ destructor: function () {
+ this.topLevelType = null;
+ this.nativeEvent = null;
+ this.ancestors.length = 0;
+ }
+});
+PooledClass.addPoolingTo(TopLevelCallbackBookKeeping, PooledClass.twoArgumentPooler);
+
+function handleTopLevelImpl(bookKeeping) {
+ // TODO: Re-enable event.path handling
+ //
+ // if (bookKeeping.nativeEvent.path && bookKeeping.nativeEvent.path.length > 1) {
+ // // New browsers have a path attribute on native events
+ // handleTopLevelWithPath(bookKeeping);
+ // } else {
+ // // Legacy browsers don't have a path attribute on native events
+ // handleTopLevelWithoutPath(bookKeeping);
+ // }
+
+ void handleTopLevelWithPath; // temporarily unused
+ handleTopLevelWithoutPath(bookKeeping);
+}
+
+// Legacy browsers don't have a path attribute on native events
+function handleTopLevelWithoutPath(bookKeeping) {
+ var topLevelTarget = ReactMount.getFirstReactDOM(getEventTarget(bookKeeping.nativeEvent)) || window;
+
+ // Loop through the hierarchy, in case there's any nested components.
+ // It's important that we build the array of ancestors before calling any
+ // event handlers, because event handlers can modify the DOM, leading to
+ // inconsistencies with ReactMount's node cache. See #1105.
+ var ancestor = topLevelTarget;
+ while (ancestor) {
+ bookKeeping.ancestors.push(ancestor);
+ ancestor = findParent(ancestor);
+ }
+
+ for (var i = 0; i < bookKeeping.ancestors.length; i++) {
+ topLevelTarget = bookKeeping.ancestors[i];
+ var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';
+ ReactEventListener._handleTopLevel(bookKeeping.topLevelType, topLevelTarget, topLevelTargetID, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
+ }
+}
+
+// New browsers have a path attribute on native events
+function handleTopLevelWithPath(bookKeeping) {
+ var path = bookKeeping.nativeEvent.path;
+ var currentNativeTarget = path[0];
+ var eventsFired = 0;
+ for (var i = 0; i < path.length; i++) {
+ var currentPathElement = path[i];
+ if (currentPathElement.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE) {
+ currentNativeTarget = path[i + 1];
+ }
+ // TODO: slow
+ var reactParent = ReactMount.getFirstReactDOM(currentPathElement);
+ if (reactParent === currentPathElement) {
+ var currentPathElementID = ReactMount.getID(currentPathElement);
+ var newRootID = ReactInstanceHandles.getReactRootIDFromNodeID(currentPathElementID);
+ bookKeeping.ancestors.push(currentPathElement);
+
+ var topLevelTargetID = ReactMount.getID(currentPathElement) || '';
+ eventsFired++;
+ ReactEventListener._handleTopLevel(bookKeeping.topLevelType, currentPathElement, topLevelTargetID, bookKeeping.nativeEvent, currentNativeTarget);
+
+ // Jump to the root of this React render tree
+ while (currentPathElementID !== newRootID) {
+ i++;
+ currentPathElement = path[i];
+ currentPathElementID = ReactMount.getID(currentPathElement);
+ }
+ }
+ }
+ if (eventsFired === 0) {
+ ReactEventListener._handleTopLevel(bookKeeping.topLevelType, window, '', bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
+ }
+}
+
+function scrollValueMonitor(cb) {
+ var scrollPosition = getUnboundedScrollPosition(window);
+ cb(scrollPosition);
+}
+
+var ReactEventListener = {
+ _enabled: true,
+ _handleTopLevel: null,
+
+ WINDOW_HANDLE: ExecutionEnvironment.canUseDOM ? window : null,
+
+ setHandleTopLevel: function (handleTopLevel) {
+ ReactEventListener._handleTopLevel = handleTopLevel;
+ },
+
+ setEnabled: function (enabled) {
+ ReactEventListener._enabled = !!enabled;
+ },
+
+ isEnabled: function () {
+ return ReactEventListener._enabled;
+ },
+
+ /**
+ * Traps top-level events by using event bubbling.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {string} handlerBaseName Event name (e.g. "click").
+ * @param {object} handle Element on which to attach listener.
+ * @return {?object} An object with a remove function which will forcefully
+ * remove the listener.
+ * @internal
+ */
+ trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
+ var element = handle;
+ if (!element) {
+ return null;
+ }
+ return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
+ },
+
+ /**
+ * Traps a top-level event by using event capturing.
+ *
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {string} handlerBaseName Event name (e.g. "click").
+ * @param {object} handle Element on which to attach listener.
+ * @return {?object} An object with a remove function which will forcefully
+ * remove the listener.
+ * @internal
+ */
+ trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
+ var element = handle;
+ if (!element) {
+ return null;
+ }
+ return EventListener.capture(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
+ },
+
+ monitorScrollValue: function (refresh) {
+ var callback = scrollValueMonitor.bind(null, refresh);
+ EventListener.listen(window, 'scroll', callback);
+ },
+
+ dispatchEvent: function (topLevelType, nativeEvent) {
+ if (!ReactEventListener._enabled) {
+ return;
+ }
+
+ var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
+ try {
+ // Event queue being processed in the same cycle allows
+ // `preventDefault`.
+ ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
+ } finally {
+ TopLevelCallbackBookKeeping.release(bookKeeping);
+ }
+ }
+};
+
+module.exports = ReactEventListener;
+},{"128":128,"146":146,"147":147,"158":158,"24":24,"25":25,"67":67,"72":72,"96":96}],64:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactFragment
+ */
+
+'use strict';
+
+var ReactChildren = _dereq_(32);
+var ReactElement = _dereq_(57);
+
+var emptyFunction = _dereq_(153);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * We used to allow keyed objects to serve as a collection of ReactElements,
+ * or nested sets. This allowed us a way to explicitly key a set a fragment of
+ * components. This is now being replaced with an opaque data structure.
+ * The upgrade path is to call React.addons.createFragment({ key: value }) to
+ * create a keyed fragment. The resulting data structure is an array.
+ */
+
+var numericPropertyRegex = /^\d+$/;
+
+var warnedAboutNumeric = false;
+
+var ReactFragment = {
+ // Wrap a keyed object in an opaque proxy that warns you if you access any
+ // of its properties.
+ create: function (object) {
+ if (typeof object !== 'object' || !object || Array.isArray(object)) {
+ "production" !== 'production' ? warning(false, 'React.addons.createFragment only accepts a single object. Got: %s', object) : undefined;
+ return object;
+ }
+ if (ReactElement.isValidElement(object)) {
+ "production" !== 'production' ? warning(false, 'React.addons.createFragment does not accept a ReactElement ' + 'without a wrapper object.') : undefined;
+ return object;
+ }
+
+ !(object.nodeType !== 1) ? "production" !== 'production' ? invariant(false, 'React.addons.createFragment(...): Encountered an invalid child; DOM ' + 'elements are not valid children of React components.') : invariant(false) : undefined;
+
+ var result = [];
+
+ for (var key in object) {
+ if ("production" !== 'production') {
+ if (!warnedAboutNumeric && numericPropertyRegex.test(key)) {
+ "production" !== 'production' ? warning(false, 'React.addons.createFragment(...): Child objects should have ' + 'non-numeric keys so ordering is preserved.') : undefined;
+ warnedAboutNumeric = true;
+ }
+ }
+ ReactChildren.mapIntoWithKeyPrefixInternal(object[key], result, key, emptyFunction.thatReturnsArgument);
+ }
+
+ return result;
+ }
+};
+
+module.exports = ReactFragment;
+},{"153":153,"161":161,"173":173,"32":32,"57":57}],65:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInjection
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var EventPluginHub = _dereq_(16);
+var ReactComponentEnvironment = _dereq_(36);
+var ReactClass = _dereq_(33);
+var ReactEmptyComponent = _dereq_(59);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactNativeComponent = _dereq_(75);
+var ReactPerf = _dereq_(78);
+var ReactRootIndex = _dereq_(86);
+var ReactUpdates = _dereq_(96);
+
+var ReactInjection = {
+ Component: ReactComponentEnvironment.injection,
+ Class: ReactClass.injection,
+ DOMProperty: DOMProperty.injection,
+ EmptyComponent: ReactEmptyComponent.injection,
+ EventPluginHub: EventPluginHub.injection,
+ EventEmitter: ReactBrowserEventEmitter.injection,
+ NativeComponent: ReactNativeComponent.injection,
+ Perf: ReactPerf.injection,
+ RootIndex: ReactRootIndex.injection,
+ Updates: ReactUpdates.injection
+};
+
+module.exports = ReactInjection;
+},{"10":10,"16":16,"28":28,"33":33,"36":36,"59":59,"75":75,"78":78,"86":86,"96":96}],66:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInputSelection
+ */
+
+'use strict';
+
+var ReactDOMSelection = _dereq_(49);
+
+var containsNode = _dereq_(150);
+var focusNode = _dereq_(155);
+var getActiveElement = _dereq_(156);
+
+function isInDocument(node) {
+ return containsNode(document.documentElement, node);
+}
+
+/**
+ * @ReactInputSelection: React input selection module. Based on Selection.js,
+ * but modified to be suitable for react and has a couple of bug fixes (doesn't
+ * assume buttons have range selections allowed).
+ * Input selection module for React.
+ */
+var ReactInputSelection = {
+
+ hasSelectionCapabilities: function (elem) {
+ var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
+ return nodeName && (nodeName === 'input' && elem.type === 'text' || nodeName === 'textarea' || elem.contentEditable === 'true');
+ },
+
+ getSelectionInformation: function () {
+ var focusedElem = getActiveElement();
+ return {
+ focusedElem: focusedElem,
+ selectionRange: ReactInputSelection.hasSelectionCapabilities(focusedElem) ? ReactInputSelection.getSelection(focusedElem) : null
+ };
+ },
+
+ /**
+ * @restoreSelection: If any selection information was potentially lost,
+ * restore it. This is useful when performing operations that could remove dom
+ * nodes and place them back in, resulting in focus being lost.
+ */
+ restoreSelection: function (priorSelectionInformation) {
+ var curFocusedElem = getActiveElement();
+ var priorFocusedElem = priorSelectionInformation.focusedElem;
+ var priorSelectionRange = priorSelectionInformation.selectionRange;
+ if (curFocusedElem !== priorFocusedElem && isInDocument(priorFocusedElem)) {
+ if (ReactInputSelection.hasSelectionCapabilities(priorFocusedElem)) {
+ ReactInputSelection.setSelection(priorFocusedElem, priorSelectionRange);
+ }
+ focusNode(priorFocusedElem);
+ }
+ },
+
+ /**
+ * @getSelection: Gets the selection bounds of a focused textarea, input or
+ * contentEditable node.
+ * -@input: Look up selection bounds of this input
+ * -@return {start: selectionStart, end: selectionEnd}
+ */
+ getSelection: function (input) {
+ var selection;
+
+ if ('selectionStart' in input) {
+ // Modern browser with input or textarea.
+ selection = {
+ start: input.selectionStart,
+ end: input.selectionEnd
+ };
+ } else if (document.selection && (input.nodeName && input.nodeName.toLowerCase() === 'input')) {
+ // IE8 input.
+ var range = document.selection.createRange();
+ // There can only be one selection per document in IE, so it must
+ // be in our element.
+ if (range.parentElement() === input) {
+ selection = {
+ start: -range.moveStart('character', -input.value.length),
+ end: -range.moveEnd('character', -input.value.length)
+ };
+ }
+ } else {
+ // Content editable or old IE textarea.
+ selection = ReactDOMSelection.getOffsets(input);
+ }
+
+ return selection || { start: 0, end: 0 };
+ },
+
+ /**
+ * @setSelection: Sets the selection bounds of a textarea or input and focuses
+ * the input.
+ * -@input Set selection bounds of this input or textarea
+ * -@offsets Object of same form that is returned from get*
+ */
+ setSelection: function (input, offsets) {
+ var start = offsets.start;
+ var end = offsets.end;
+ if (typeof end === 'undefined') {
+ end = start;
+ }
+
+ if ('selectionStart' in input) {
+ input.selectionStart = start;
+ input.selectionEnd = Math.min(end, input.value.length);
+ } else if (document.selection && (input.nodeName && input.nodeName.toLowerCase() === 'input')) {
+ var range = input.createTextRange();
+ range.collapse(true);
+ range.moveStart('character', start);
+ range.moveEnd('character', end - start);
+ range.select();
+ } else {
+ ReactDOMSelection.setOffsets(input, offsets);
+ }
+ }
+};
+
+module.exports = ReactInputSelection;
+},{"150":150,"155":155,"156":156,"49":49}],67:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInstanceHandles
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactRootIndex = _dereq_(86);
+
+var invariant = _dereq_(161);
+
+var SEPARATOR = '.';
+var SEPARATOR_LENGTH = SEPARATOR.length;
+
+/**
+ * Maximum depth of traversals before we consider the possibility of a bad ID.
+ */
+var MAX_TREE_DEPTH = 10000;
+
+/**
+ * Creates a DOM ID prefix to use when mounting React components.
+ *
+ * @param {number} index A unique integer
+ * @return {string} React root ID.
+ * @internal
+ */
+function getReactRootIDString(index) {
+ return SEPARATOR + index.toString(36);
+}
+
+/**
+ * Checks if a character in the supplied ID is a separator or the end.
+ *
+ * @param {string} id A React DOM ID.
+ * @param {number} index Index of the character to check.
+ * @return {boolean} True if the character is a separator or end of the ID.
+ * @private
+ */
+function isBoundary(id, index) {
+ return id.charAt(index) === SEPARATOR || index === id.length;
+}
+
+/**
+ * Checks if the supplied string is a valid React DOM ID.
+ *
+ * @param {string} id A React DOM ID, maybe.
+ * @return {boolean} True if the string is a valid React DOM ID.
+ * @private
+ */
+function isValidID(id) {
+ return id === '' || id.charAt(0) === SEPARATOR && id.charAt(id.length - 1) !== SEPARATOR;
+}
+
+/**
+ * Checks if the first ID is an ancestor of or equal to the second ID.
+ *
+ * @param {string} ancestorID
+ * @param {string} descendantID
+ * @return {boolean} True if `ancestorID` is an ancestor of `descendantID`.
+ * @internal
+ */
+function isAncestorIDOf(ancestorID, descendantID) {
+ return descendantID.indexOf(ancestorID) === 0 && isBoundary(descendantID, ancestorID.length);
+}
+
+/**
+ * Gets the parent ID of the supplied React DOM ID, `id`.
+ *
+ * @param {string} id ID of a component.
+ * @return {string} ID of the parent, or an empty string.
+ * @private
+ */
+function getParentID(id) {
+ return id ? id.substr(0, id.lastIndexOf(SEPARATOR)) : '';
+}
+
+/**
+ * Gets the next DOM ID on the tree path from the supplied `ancestorID` to the
+ * supplied `destinationID`. If they are equal, the ID is returned.
+ *
+ * @param {string} ancestorID ID of an ancestor node of `destinationID`.
+ * @param {string} destinationID ID of the destination node.
+ * @return {string} Next ID on the path from `ancestorID` to `destinationID`.
+ * @private
+ */
+function getNextDescendantID(ancestorID, destinationID) {
+ !(isValidID(ancestorID) && isValidID(destinationID)) ? "production" !== 'production' ? invariant(false, 'getNextDescendantID(%s, %s): Received an invalid React DOM ID.', ancestorID, destinationID) : invariant(false) : undefined;
+ !isAncestorIDOf(ancestorID, destinationID) ? "production" !== 'production' ? invariant(false, 'getNextDescendantID(...): React has made an invalid assumption about ' + 'the DOM hierarchy. Expected `%s` to be an ancestor of `%s`.', ancestorID, destinationID) : invariant(false) : undefined;
+ if (ancestorID === destinationID) {
+ return ancestorID;
+ }
+ // Skip over the ancestor and the immediate separator. Traverse until we hit
+ // another separator or we reach the end of `destinationID`.
+ var start = ancestorID.length + SEPARATOR_LENGTH;
+ var i;
+ for (i = start; i < destinationID.length; i++) {
+ if (isBoundary(destinationID, i)) {
+ break;
+ }
+ }
+ return destinationID.substr(0, i);
+}
+
+/**
+ * Gets the nearest common ancestor ID of two IDs.
+ *
+ * Using this ID scheme, the nearest common ancestor ID is the longest common
+ * prefix of the two IDs that immediately preceded a "marker" in both strings.
+ *
+ * @param {string} oneID
+ * @param {string} twoID
+ * @return {string} Nearest common ancestor ID, or the empty string if none.
+ * @private
+ */
+function getFirstCommonAncestorID(oneID, twoID) {
+ var minLength = Math.min(oneID.length, twoID.length);
+ if (minLength === 0) {
+ return '';
+ }
+ var lastCommonMarkerIndex = 0;
+ // Use `<=` to traverse until the "EOL" of the shorter string.
+ for (var i = 0; i <= minLength; i++) {
+ if (isBoundary(oneID, i) && isBoundary(twoID, i)) {
+ lastCommonMarkerIndex = i;
+ } else if (oneID.charAt(i) !== twoID.charAt(i)) {
+ break;
+ }
+ }
+ var longestCommonID = oneID.substr(0, lastCommonMarkerIndex);
+ !isValidID(longestCommonID) ? "production" !== 'production' ? invariant(false, 'getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s', oneID, twoID, longestCommonID) : invariant(false) : undefined;
+ return longestCommonID;
+}
+
+/**
+ * Traverses the parent path between two IDs (either up or down). The IDs must
+ * not be the same, and there must exist a parent path between them. If the
+ * callback returns `false`, traversal is stopped.
+ *
+ * @param {?string} start ID at which to start traversal.
+ * @param {?string} stop ID at which to end traversal.
+ * @param {function} cb Callback to invoke each ID with.
+ * @param {*} arg Argument to invoke the callback with.
+ * @param {?boolean} skipFirst Whether or not to skip the first node.
+ * @param {?boolean} skipLast Whether or not to skip the last node.
+ * @private
+ */
+function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
+ start = start || '';
+ stop = stop || '';
+ !(start !== stop) ? "production" !== 'production' ? invariant(false, 'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.', start) : invariant(false) : undefined;
+ var traverseUp = isAncestorIDOf(stop, start);
+ !(traverseUp || isAncestorIDOf(start, stop)) ? "production" !== 'production' ? invariant(false, 'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do ' + 'not have a parent path.', start, stop) : invariant(false) : undefined;
+ // Traverse from `start` to `stop` one depth at a time.
+ var depth = 0;
+ var traverse = traverseUp ? getParentID : getNextDescendantID;
+ for (var id = start;; /* until break */id = traverse(id, stop)) {
+ var ret;
+ if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) {
+ ret = cb(id, traverseUp, arg);
+ }
+ if (ret === false || id === stop) {
+ // Only break //after// visiting `stop`.
+ break;
+ }
+ !(depth++ < MAX_TREE_DEPTH) ? "production" !== 'production' ? invariant(false, 'traverseParentPath(%s, %s, ...): Detected an infinite loop while ' + 'traversing the React DOM ID tree. This may be due to malformed IDs: %s', start, stop, id) : invariant(false) : undefined;
+ }
+}
+
+/**
+ * Manages the IDs assigned to DOM representations of React components. This
+ * uses a specific scheme in order to traverse the DOM efficiently (e.g. in
+ * order to simulate events).
+ *
+ * @internal
+ */
+var ReactInstanceHandles = {
+
+ /**
+ * Constructs a React root ID
+ * @return {string} A React root ID.
+ */
+ createReactRootID: function () {
+ return getReactRootIDString(ReactRootIndex.createReactRootIndex());
+ },
+
+ /**
+ * Constructs a React ID by joining a root ID with a name.
+ *
+ * @param {string} rootID Root ID of a parent component.
+ * @param {string} name A component's name (as flattened children).
+ * @return {string} A React ID.
+ * @internal
+ */
+ createReactID: function (rootID, name) {
+ return rootID + name;
+ },
+
+ /**
+ * Gets the DOM ID of the React component that is the root of the tree that
+ * contains the React component with the supplied DOM ID.
+ *
+ * @param {string} id DOM ID of a React component.
+ * @return {?string} DOM ID of the React component that is the root.
+ * @internal
+ */
+ getReactRootIDFromNodeID: function (id) {
+ if (id && id.charAt(0) === SEPARATOR && id.length > 1) {
+ var index = id.indexOf(SEPARATOR, 1);
+ return index > -1 ? id.substr(0, index) : id;
+ }
+ return null;
+ },
+
+ /**
+ * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that
+ * should would receive a `mouseEnter` or `mouseLeave` event.
+ *
+ * NOTE: Does not invoke the callback on the nearest common ancestor because
+ * nothing "entered" or "left" that element.
+ *
+ * @param {string} leaveID ID being left.
+ * @param {string} enterID ID being entered.
+ * @param {function} cb Callback to invoke on each entered/left ID.
+ * @param {*} upArg Argument to invoke the callback with on left IDs.
+ * @param {*} downArg Argument to invoke the callback with on entered IDs.
+ * @internal
+ */
+ traverseEnterLeave: function (leaveID, enterID, cb, upArg, downArg) {
+ var ancestorID = getFirstCommonAncestorID(leaveID, enterID);
+ if (ancestorID !== leaveID) {
+ traverseParentPath(leaveID, ancestorID, cb, upArg, false, true);
+ }
+ if (ancestorID !== enterID) {
+ traverseParentPath(ancestorID, enterID, cb, downArg, true, false);
+ }
+ },
+
+ /**
+ * Simulates the traversal of a two-phase, capture/bubble event dispatch.
+ *
+ * NOTE: This traversal happens on IDs without touching the DOM.
+ *
+ * @param {string} targetID ID of the target node.
+ * @param {function} cb Callback to invoke.
+ * @param {*} arg Argument to invoke the callback with.
+ * @internal
+ */
+ traverseTwoPhase: function (targetID, cb, arg) {
+ if (targetID) {
+ traverseParentPath('', targetID, cb, arg, true, false);
+ traverseParentPath(targetID, '', cb, arg, false, true);
+ }
+ },
+
+ /**
+ * Same as `traverseTwoPhase` but skips the `targetID`.
+ */
+ traverseTwoPhaseSkipTarget: function (targetID, cb, arg) {
+ if (targetID) {
+ traverseParentPath('', targetID, cb, arg, true, true);
+ traverseParentPath(targetID, '', cb, arg, true, true);
+ }
+ },
+
+ /**
+ * Traverse a node ID, calling the supplied `cb` for each ancestor ID. For
+ * example, passing `.0.$row-0.1` would result in `cb` getting called
+ * with `.0`, `.0.$row-0`, and `.0.$row-0.1`.
+ *
+ * NOTE: This traversal happens on IDs without touching the DOM.
+ *
+ * @param {string} targetID ID of the target node.
+ * @param {function} cb Callback to invoke.
+ * @param {*} arg Argument to invoke the callback with.
+ * @internal
+ */
+ traverseAncestors: function (targetID, cb, arg) {
+ traverseParentPath('', targetID, cb, arg, true, false);
+ },
+
+ getFirstCommonAncestorID: getFirstCommonAncestorID,
+
+ /**
+ * Exposed for unit testing.
+ * @private
+ */
+ _getNextDescendantID: getNextDescendantID,
+
+ isAncestorIDOf: isAncestorIDOf,
+
+ SEPARATOR: SEPARATOR
+
+};
+
+module.exports = ReactInstanceHandles;
+},{"161":161,"86":86}],68:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInstanceMap
+ */
+
+'use strict';
+
+/**
+ * `ReactInstanceMap` maintains a mapping from a public facing stateful
+ * instance (key) and the internal representation (value). This allows public
+ * methods to accept the user facing instance as an argument and map them back
+ * to internal methods.
+ */
+
+// TODO: Replace this with ES6: var ReactInstanceMap = new Map();
+var ReactInstanceMap = {
+
+ /**
+ * This API should be called `delete` but we'd have to make sure to always
+ * transform these to strings for IE support. When this transform is fully
+ * supported we can rename it.
+ */
+ remove: function (key) {
+ key._reactInternalInstance = undefined;
+ },
+
+ get: function (key) {
+ return key._reactInternalInstance;
+ },
+
+ has: function (key) {
+ return key._reactInternalInstance !== undefined;
+ },
+
+ set: function (key, value) {
+ key._reactInternalInstance = value;
+ }
+
+};
+
+module.exports = ReactInstanceMap;
+},{}],69:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactIsomorphic
+ */
+
+'use strict';
+
+var ReactChildren = _dereq_(32);
+var ReactComponent = _dereq_(34);
+var ReactClass = _dereq_(33);
+var ReactDOMFactories = _dereq_(43);
+var ReactElement = _dereq_(57);
+var ReactElementValidator = _dereq_(58);
+var ReactPropTypes = _dereq_(82);
+var ReactVersion = _dereq_(97);
+
+var assign = _dereq_(24);
+var onlyChild = _dereq_(135);
+
+var createElement = ReactElement.createElement;
+var createFactory = ReactElement.createFactory;
+var cloneElement = ReactElement.cloneElement;
+
+if ("production" !== 'production') {
+ createElement = ReactElementValidator.createElement;
+ createFactory = ReactElementValidator.createFactory;
+ cloneElement = ReactElementValidator.cloneElement;
+}
+
+var React = {
+
+ // Modern
+
+ Children: {
+ map: ReactChildren.map,
+ forEach: ReactChildren.forEach,
+ count: ReactChildren.count,
+ toArray: ReactChildren.toArray,
+ only: onlyChild
+ },
+
+ Component: ReactComponent,
+
+ createElement: createElement,
+ cloneElement: cloneElement,
+ isValidElement: ReactElement.isValidElement,
+
+ // Classic
+
+ PropTypes: ReactPropTypes,
+ createClass: ReactClass.createClass,
+ createFactory: createFactory,
+ createMixin: function (mixin) {
+ // Currently a noop. Will be used to validate and trace mixins.
+ return mixin;
+ },
+
+ // This looks DOM specific but these are actually isomorphic helpers
+ // since they are just generating DOM strings.
+ DOM: ReactDOMFactories,
+
+ version: ReactVersion,
+
+ // Hook for JSX spread, don't use this for anything else.
+ __spread: assign
+};
+
+module.exports = React;
+},{"135":135,"24":24,"32":32,"33":33,"34":34,"43":43,"57":57,"58":58,"82":82,"97":97}],70:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactLink
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * ReactLink encapsulates a common pattern in which a component wants to modify
+ * a prop received from its parent. ReactLink allows the parent to pass down a
+ * value coupled with a callback that, when invoked, expresses an intent to
+ * modify that value. For example:
+ *
+ * React.createClass({
+ * getInitialState: function() {
+ * return {value: ''};
+ * },
+ * render: function() {
+ * var valueLink = new ReactLink(this.state.value, this._handleValueChange);
+ * return <input valueLink={valueLink} />;
+ * },
+ * _handleValueChange: function(newValue) {
+ * this.setState({value: newValue});
+ * }
+ * });
+ *
+ * We have provided some sugary mixins to make the creation and
+ * consumption of ReactLink easier; see LinkedValueUtils and LinkedStateMixin.
+ */
+
+var React = _dereq_(26);
+
+/**
+ * @param {*} value current value of the link
+ * @param {function} requestChange callback to request a change
+ */
+function ReactLink(value, requestChange) {
+ this.value = value;
+ this.requestChange = requestChange;
+}
+
+/**
+ * Creates a PropType that enforces the ReactLink API and optionally checks the
+ * type of the value being passed inside the link. Example:
+ *
+ * MyComponent.propTypes = {
+ * tabIndexLink: ReactLink.PropTypes.link(React.PropTypes.number)
+ * }
+ */
+function createLinkTypeChecker(linkType) {
+ var shapes = {
+ value: typeof linkType === 'undefined' ? React.PropTypes.any.isRequired : linkType.isRequired,
+ requestChange: React.PropTypes.func.isRequired
+ };
+ return React.PropTypes.shape(shapes);
+}
+
+ReactLink.PropTypes = {
+ link: createLinkTypeChecker
+};
+
+module.exports = ReactLink;
+},{"26":26}],71:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMarkupChecksum
+ */
+
+'use strict';
+
+var adler32 = _dereq_(116);
+
+var TAG_END = /\/?>/;
+
+var ReactMarkupChecksum = {
+ CHECKSUM_ATTR_NAME: 'data-react-checksum',
+
+ /**
+ * @param {string} markup Markup string
+ * @return {string} Markup string with checksum attribute attached
+ */
+ addChecksumToMarkup: function (markup) {
+ var checksum = adler32(markup);
+
+ // Add checksum (handle both parent tags and self-closing tags)
+ return markup.replace(TAG_END, ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="' + checksum + '"$&');
+ },
+
+ /**
+ * @param {string} markup to use
+ * @param {DOMElement} element root React element
+ * @returns {boolean} whether or not the markup is the same
+ */
+ canReuseMarkup: function (markup, element) {
+ var existingChecksum = element.getAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
+ existingChecksum = existingChecksum && parseInt(existingChecksum, 10);
+ var markupChecksum = adler32(markup);
+ return markupChecksum === existingChecksum;
+ }
+};
+
+module.exports = ReactMarkupChecksum;
+},{"116":116}],72:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMount
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactCurrentOwner = _dereq_(39);
+var ReactDOMFeatureFlags = _dereq_(44);
+var ReactElement = _dereq_(57);
+var ReactEmptyComponentRegistry = _dereq_(60);
+var ReactInstanceHandles = _dereq_(67);
+var ReactInstanceMap = _dereq_(68);
+var ReactMarkupChecksum = _dereq_(71);
+var ReactPerf = _dereq_(78);
+var ReactReconciler = _dereq_(84);
+var ReactUpdateQueue = _dereq_(95);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var containsNode = _dereq_(150);
+var instantiateReactComponent = _dereq_(132);
+var invariant = _dereq_(161);
+var setInnerHTML = _dereq_(138);
+var shouldUpdateReactComponent = _dereq_(141);
+var validateDOMNesting = _dereq_(144);
+var warning = _dereq_(173);
+
+var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME;
+var nodeCache = {};
+
+var ELEMENT_NODE_TYPE = 1;
+var DOC_NODE_TYPE = 9;
+var DOCUMENT_FRAGMENT_NODE_TYPE = 11;
+
+var ownerDocumentContextKey = '__ReactMount_ownerDocument$' + Math.random().toString(36).slice(2);
+
+/** Mapping from reactRootID to React component instance. */
+var instancesByReactRootID = {};
+
+/** Mapping from reactRootID to `container` nodes. */
+var containersByReactRootID = {};
+
+if ("production" !== 'production') {
+ /** __DEV__-only mapping from reactRootID to root elements. */
+ var rootElementsByReactRootID = {};
+}
+
+// Used to store breadth-first search state in findComponentRoot.
+var findComponentRootReusableArray = [];
+
+/**
+ * Finds the index of the first character
+ * that's not common between the two given strings.
+ *
+ * @return {number} the index of the character where the strings diverge
+ */
+function firstDifferenceIndex(string1, string2) {
+ var minLen = Math.min(string1.length, string2.length);
+ for (var i = 0; i < minLen; i++) {
+ if (string1.charAt(i) !== string2.charAt(i)) {
+ return i;
+ }
+ }
+ return string1.length === string2.length ? -1 : minLen;
+}
+
+/**
+ * @param {DOMElement|DOMDocument} container DOM element that may contain
+ * a React component
+ * @return {?*} DOM element that may have the reactRoot ID, or null.
+ */
+function getReactRootElementInContainer(container) {
+ if (!container) {
+ return null;
+ }
+
+ if (container.nodeType === DOC_NODE_TYPE) {
+ return container.documentElement;
+ } else {
+ return container.firstChild;
+ }
+}
+
+/**
+ * @param {DOMElement} container DOM element that may contain a React component.
+ * @return {?string} A "reactRoot" ID, if a React component is rendered.
+ */
+function getReactRootID(container) {
+ var rootElement = getReactRootElementInContainer(container);
+ return rootElement && ReactMount.getID(rootElement);
+}
+
+/**
+ * Accessing node[ATTR_NAME] or calling getAttribute(ATTR_NAME) on a form
+ * element can return its control whose name or ID equals ATTR_NAME. All
+ * DOM nodes support `getAttributeNode` but this can also get called on
+ * other objects so just return '' if we're given something other than a
+ * DOM node (such as window).
+ *
+ * @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node.
+ * @return {string} ID of the supplied `domNode`.
+ */
+function getID(node) {
+ var id = internalGetID(node);
+ if (id) {
+ if (nodeCache.hasOwnProperty(id)) {
+ var cached = nodeCache[id];
+ if (cached !== node) {
+ !!isValid(cached, id) ? "production" !== 'production' ? invariant(false, 'ReactMount: Two valid but unequal nodes with the same `%s`: %s', ATTR_NAME, id) : invariant(false) : undefined;
+
+ nodeCache[id] = node;
+ }
+ } else {
+ nodeCache[id] = node;
+ }
+ }
+
+ return id;
+}
+
+function internalGetID(node) {
+ // If node is something like a window, document, or text node, none of
+ // which support attributes or a .getAttribute method, gracefully return
+ // the empty string, as if the attribute were missing.
+ return node && node.getAttribute && node.getAttribute(ATTR_NAME) || '';
+}
+
+/**
+ * Sets the React-specific ID of the given node.
+ *
+ * @param {DOMElement} node The DOM node whose ID will be set.
+ * @param {string} id The value of the ID attribute.
+ */
+function setID(node, id) {
+ var oldID = internalGetID(node);
+ if (oldID !== id) {
+ delete nodeCache[oldID];
+ }
+ node.setAttribute(ATTR_NAME, id);
+ nodeCache[id] = node;
+}
+
+/**
+ * Finds the node with the supplied React-generated DOM ID.
+ *
+ * @param {string} id A React-generated DOM ID.
+ * @return {DOMElement} DOM node with the suppled `id`.
+ * @internal
+ */
+function getNode(id) {
+ if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {
+ nodeCache[id] = ReactMount.findReactNodeByID(id);
+ }
+ return nodeCache[id];
+}
+
+/**
+ * Finds the node with the supplied public React instance.
+ *
+ * @param {*} instance A public React instance.
+ * @return {?DOMElement} DOM node with the suppled `id`.
+ * @internal
+ */
+function getNodeFromInstance(instance) {
+ var id = ReactInstanceMap.get(instance)._rootNodeID;
+ if (ReactEmptyComponentRegistry.isNullComponentID(id)) {
+ return null;
+ }
+ if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {
+ nodeCache[id] = ReactMount.findReactNodeByID(id);
+ }
+ return nodeCache[id];
+}
+
+/**
+ * A node is "valid" if it is contained by a currently mounted container.
+ *
+ * This means that the node does not have to be contained by a document in
+ * order to be considered valid.
+ *
+ * @param {?DOMElement} node The candidate DOM node.
+ * @param {string} id The expected ID of the node.
+ * @return {boolean} Whether the node is contained by a mounted container.
+ */
+function isValid(node, id) {
+ if (node) {
+ !(internalGetID(node) === id) ? "production" !== 'production' ? invariant(false, 'ReactMount: Unexpected modification of `%s`', ATTR_NAME) : invariant(false) : undefined;
+
+ var container = ReactMount.findReactContainerForID(id);
+ if (container && containsNode(container, node)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Causes the cache to forget about one React-specific ID.
+ *
+ * @param {string} id The ID to forget.
+ */
+function purgeID(id) {
+ delete nodeCache[id];
+}
+
+var deepestNodeSoFar = null;
+function findDeepestCachedAncestorImpl(ancestorID) {
+ var ancestor = nodeCache[ancestorID];
+ if (ancestor && isValid(ancestor, ancestorID)) {
+ deepestNodeSoFar = ancestor;
+ } else {
+ // This node isn't populated in the cache, so presumably none of its
+ // descendants are. Break out of the loop.
+ return false;
+ }
+}
+
+/**
+ * Return the deepest cached node whose ID is a prefix of `targetID`.
+ */
+function findDeepestCachedAncestor(targetID) {
+ deepestNodeSoFar = null;
+ ReactInstanceHandles.traverseAncestors(targetID, findDeepestCachedAncestorImpl);
+
+ var foundNode = deepestNodeSoFar;
+ deepestNodeSoFar = null;
+ return foundNode;
+}
+
+/**
+ * Mounts this component and inserts it into the DOM.
+ *
+ * @param {ReactComponent} componentInstance The instance to mount.
+ * @param {string} rootID DOM ID of the root node.
+ * @param {DOMElement} container DOM element to mount into.
+ * @param {ReactReconcileTransaction} transaction
+ * @param {boolean} shouldReuseMarkup If true, do not insert markup
+ */
+function mountComponentIntoNode(componentInstance, rootID, container, transaction, shouldReuseMarkup, context) {
+ if (ReactDOMFeatureFlags.useCreateElement) {
+ context = assign({}, context);
+ if (container.nodeType === DOC_NODE_TYPE) {
+ context[ownerDocumentContextKey] = container;
+ } else {
+ context[ownerDocumentContextKey] = container.ownerDocument;
+ }
+ }
+ if ("production" !== 'production') {
+ if (context === emptyObject) {
+ context = {};
+ }
+ var tag = container.nodeName.toLowerCase();
+ context[validateDOMNesting.ancestorInfoContextKey] = validateDOMNesting.updatedAncestorInfo(null, tag, null);
+ }
+ var markup = ReactReconciler.mountComponent(componentInstance, rootID, transaction, context);
+ componentInstance._renderedComponent._topLevelWrapper = componentInstance;
+ ReactMount._mountImageIntoNode(markup, container, shouldReuseMarkup, transaction);
+}
+
+/**
+ * Batched mount.
+ *
+ * @param {ReactComponent} componentInstance The instance to mount.
+ * @param {string} rootID DOM ID of the root node.
+ * @param {DOMElement} container DOM element to mount into.
+ * @param {boolean} shouldReuseMarkup If true, do not insert markup
+ */
+function batchedMountComponentIntoNode(componentInstance, rootID, container, shouldReuseMarkup, context) {
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
+ /* forceHTML */shouldReuseMarkup);
+ transaction.perform(mountComponentIntoNode, null, componentInstance, rootID, container, transaction, shouldReuseMarkup, context);
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
+}
+
+/**
+ * Unmounts a component and removes it from the DOM.
+ *
+ * @param {ReactComponent} instance React component instance.
+ * @param {DOMElement} container DOM element to unmount from.
+ * @final
+ * @internal
+ * @see {ReactMount.unmountComponentAtNode}
+ */
+function unmountComponentFromNode(instance, container) {
+ ReactReconciler.unmountComponent(instance);
+
+ if (container.nodeType === DOC_NODE_TYPE) {
+ container = container.documentElement;
+ }
+
+ // http://jsperf.com/emptying-a-node
+ while (container.lastChild) {
+ container.removeChild(container.lastChild);
+ }
+}
+
+/**
+ * True if the supplied DOM node has a direct React-rendered child that is
+ * not a React root element. Useful for warning in `render`,
+ * `unmountComponentAtNode`, etc.
+ *
+ * @param {?DOMElement} node The candidate DOM node.
+ * @return {boolean} True if the DOM element contains a direct child that was
+ * rendered by React but is not a root element.
+ * @internal
+ */
+function hasNonRootReactChild(node) {
+ var reactRootID = getReactRootID(node);
+ return reactRootID ? reactRootID !== ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID) : false;
+}
+
+/**
+ * Returns the first (deepest) ancestor of a node which is rendered by this copy
+ * of React.
+ */
+function findFirstReactDOMImpl(node) {
+ // This node might be from another React instance, so we make sure not to
+ // examine the node cache here
+ for (; node && node.parentNode !== node; node = node.parentNode) {
+ if (node.nodeType !== 1) {
+ // Not a DOMElement, therefore not a React component
+ continue;
+ }
+ var nodeID = internalGetID(node);
+ if (!nodeID) {
+ continue;
+ }
+ var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID);
+
+ // If containersByReactRootID contains the container we find by crawling up
+ // the tree, we know that this instance of React rendered the node.
+ // nb. isValid's strategy (with containsNode) does not work because render
+ // trees may be nested and we don't want a false positive in that case.
+ var current = node;
+ var lastID;
+ do {
+ lastID = internalGetID(current);
+ current = current.parentNode;
+ if (current == null) {
+ // The passed-in node has been detached from the container it was
+ // originally rendered into.
+ return null;
+ }
+ } while (lastID !== reactRootID);
+
+ if (current === containersByReactRootID[reactRootID]) {
+ return node;
+ }
+ }
+ return null;
+}
+
+/**
+ * Temporary (?) hack so that we can store all top-level pending updates on
+ * composites instead of having to worry about different types of components
+ * here.
+ */
+var TopLevelWrapper = function () {};
+TopLevelWrapper.prototype.isReactComponent = {};
+if ("production" !== 'production') {
+ TopLevelWrapper.displayName = 'TopLevelWrapper';
+}
+TopLevelWrapper.prototype.render = function () {
+ // this.props is actually a ReactElement
+ return this.props;
+};
+
+/**
+ * Mounting is the process of initializing a React component by creating its
+ * representative DOM elements and inserting them into a supplied `container`.
+ * Any prior content inside `container` is destroyed in the process.
+ *
+ * ReactMount.render(
+ * component,
+ * document.getElementById('container')
+ * );
+ *
+ * <div id="container"> <-- Supplied `container`.
+ * <div data-reactid=".3"> <-- Rendered reactRoot of React
+ * // ... component.
+ * </div>
+ * </div>
+ *
+ * Inside of `container`, the first element rendered is the "reactRoot".
+ */
+var ReactMount = {
+
+ TopLevelWrapper: TopLevelWrapper,
+
+ /** Exposed for debugging purposes **/
+ _instancesByReactRootID: instancesByReactRootID,
+
+ /**
+ * This is a hook provided to support rendering React components while
+ * ensuring that the apparent scroll position of its `container` does not
+ * change.
+ *
+ * @param {DOMElement} container The `container` being rendered into.
+ * @param {function} renderCallback This must be called once to do the render.
+ */
+ scrollMonitor: function (container, renderCallback) {
+ renderCallback();
+ },
+
+ /**
+ * Take a component that's already mounted into the DOM and replace its props
+ * @param {ReactComponent} prevComponent component instance already in the DOM
+ * @param {ReactElement} nextElement component instance to render
+ * @param {DOMElement} container container to render into
+ * @param {?function} callback function triggered on completion
+ */
+ _updateRootComponent: function (prevComponent, nextElement, container, callback) {
+ ReactMount.scrollMonitor(container, function () {
+ ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement);
+ if (callback) {
+ ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback);
+ }
+ });
+
+ if ("production" !== 'production') {
+ // Record the root element in case it later gets transplanted.
+ rootElementsByReactRootID[getReactRootID(container)] = getReactRootElementInContainer(container);
+ }
+
+ return prevComponent;
+ },
+
+ /**
+ * Register a component into the instance map and starts scroll value
+ * monitoring
+ * @param {ReactComponent} nextComponent component instance to render
+ * @param {DOMElement} container container to render into
+ * @return {string} reactRoot ID prefix
+ */
+ _registerComponent: function (nextComponent, container) {
+ !(container && (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE || container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)) ? "production" !== 'production' ? invariant(false, '_registerComponent(...): Target container is not a DOM element.') : invariant(false) : undefined;
+
+ ReactBrowserEventEmitter.ensureScrollValueMonitoring();
+
+ var reactRootID = ReactMount.registerContainer(container);
+ instancesByReactRootID[reactRootID] = nextComponent;
+ return reactRootID;
+ },
+
+ /**
+ * Render a new component into the DOM.
+ * @param {ReactElement} nextElement element to render
+ * @param {DOMElement} container container to render into
+ * @param {boolean} shouldReuseMarkup if we should skip the markup insertion
+ * @return {ReactComponent} nextComponent
+ */
+ _renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
+ // Various parts of our code (such as ReactCompositeComponent's
+ // _renderValidatedComponent) assume that calls to render aren't nested;
+ // verify that that's the case.
+ "production" !== 'production' ? warning(ReactCurrentOwner.current == null, '_renderNewRootComponent(): Render methods should be a pure function ' + 'of props and state; triggering nested component updates from ' + 'render is not allowed. If necessary, trigger nested updates in ' + 'componentDidUpdate. Check the render method of %s.', ReactCurrentOwner.current && ReactCurrentOwner.current.getName() || 'ReactCompositeComponent') : undefined;
+
+ var componentInstance = instantiateReactComponent(nextElement, null);
+ var reactRootID = ReactMount._registerComponent(componentInstance, container);
+
+ // The initial render is synchronous but any updates that happen during
+ // rendering, in componentWillMount or componentDidMount, will be batched
+ // according to the current batching strategy.
+
+ ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, reactRootID, container, shouldReuseMarkup, context);
+
+ if ("production" !== 'production') {
+ // Record the root element in case it later gets transplanted.
+ rootElementsByReactRootID[reactRootID] = getReactRootElementInContainer(container);
+ }
+
+ return componentInstance;
+ },
+
+ /**
+ * Renders a React component into the DOM in the supplied `container`.
+ *
+ * If the React component was previously rendered into `container`, this will
+ * perform an update on it and only mutate the DOM as necessary to reflect the
+ * latest React component.
+ *
+ * @param {ReactComponent} parentComponent The conceptual parent of this render tree.
+ * @param {ReactElement} nextElement Component element to render.
+ * @param {DOMElement} container DOM element to render into.
+ * @param {?function} callback function triggered on completion
+ * @return {ReactComponent} Component instance rendered in `container`.
+ */
+ renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
+ !(parentComponent != null && parentComponent._reactInternalInstance != null) ? "production" !== 'production' ? invariant(false, 'parentComponent must be a valid React Component') : invariant(false) : undefined;
+ return ReactMount._renderSubtreeIntoContainer(parentComponent, nextElement, container, callback);
+ },
+
+ _renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
+ !ReactElement.isValidElement(nextElement) ? "production" !== 'production' ? invariant(false, 'ReactDOM.render(): Invalid component element.%s', typeof nextElement === 'string' ? ' Instead of passing an element string, make sure to instantiate ' + 'it by passing it to React.createElement.' : typeof nextElement === 'function' ? ' Instead of passing a component class, make sure to instantiate ' + 'it by passing it to React.createElement.' :
+ // Check if it quacks like an element
+ nextElement != null && nextElement.props !== undefined ? ' This may be caused by unintentionally loading two independent ' + 'copies of React.' : '') : invariant(false) : undefined;
+
+ "production" !== 'production' ? warning(!container || !container.tagName || container.tagName.toUpperCase() !== 'BODY', 'render(): Rendering components directly into document.body is ' + 'discouraged, since its children are often manipulated by third-party ' + 'scripts and browser extensions. This may lead to subtle ' + 'reconciliation issues. Try rendering into a container element created ' + 'for your app.') : undefined;
+
+ var nextWrappedElement = new ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement);
+
+ var prevComponent = instancesByReactRootID[getReactRootID(container)];
+
+ if (prevComponent) {
+ var prevWrappedElement = prevComponent._currentElement;
+ var prevElement = prevWrappedElement.props;
+ if (shouldUpdateReactComponent(prevElement, nextElement)) {
+ var publicInst = prevComponent._renderedComponent.getPublicInstance();
+ var updatedCallback = callback && function () {
+ callback.call(publicInst);
+ };
+ ReactMount._updateRootComponent(prevComponent, nextWrappedElement, container, updatedCallback);
+ return publicInst;
+ } else {
+ ReactMount.unmountComponentAtNode(container);
+ }
+ }
+
+ var reactRootElement = getReactRootElementInContainer(container);
+ var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
+ var containerHasNonRootReactChild = hasNonRootReactChild(container);
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(!containerHasNonRootReactChild, 'render(...): Replacing React-rendered children with a new root ' + 'component. If you intended to update the children of this node, ' + 'you should instead have the existing children update their state ' + 'and render the new components instead of calling ReactDOM.render.') : undefined;
+
+ if (!containerHasReactMarkup || reactRootElement.nextSibling) {
+ var rootElementSibling = reactRootElement;
+ while (rootElementSibling) {
+ if (internalGetID(rootElementSibling)) {
+ "production" !== 'production' ? warning(false, 'render(): Target node has markup rendered by React, but there ' + 'are unrelated nodes as well. This is most commonly caused by ' + 'white-space inserted around server-rendered markup.') : undefined;
+ break;
+ }
+ rootElementSibling = rootElementSibling.nextSibling;
+ }
+ }
+ }
+
+ var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
+ var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, parentComponent != null ? parentComponent._reactInternalInstance._processChildContext(parentComponent._reactInternalInstance._context) : emptyObject)._renderedComponent.getPublicInstance();
+ if (callback) {
+ callback.call(component);
+ }
+ return component;
+ },
+
+ /**
+ * Renders a React component into the DOM in the supplied `container`.
+ *
+ * If the React component was previously rendered into `container`, this will
+ * perform an update on it and only mutate the DOM as necessary to reflect the
+ * latest React component.
+ *
+ * @param {ReactElement} nextElement Component element to render.
+ * @param {DOMElement} container DOM element to render into.
+ * @param {?function} callback function triggered on completion
+ * @return {ReactComponent} Component instance rendered in `container`.
+ */
+ render: function (nextElement, container, callback) {
+ return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
+ },
+
+ /**
+ * Registers a container node into which React components will be rendered.
+ * This also creates the "reactRoot" ID that will be assigned to the element
+ * rendered within.
+ *
+ * @param {DOMElement} container DOM element to register as a container.
+ * @return {string} The "reactRoot" ID of elements rendered within.
+ */
+ registerContainer: function (container) {
+ var reactRootID = getReactRootID(container);
+ if (reactRootID) {
+ // If one exists, make sure it is a valid "reactRoot" ID.
+ reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID);
+ }
+ if (!reactRootID) {
+ // No valid "reactRoot" ID found, create one.
+ reactRootID = ReactInstanceHandles.createReactRootID();
+ }
+ containersByReactRootID[reactRootID] = container;
+ return reactRootID;
+ },
+
+ /**
+ * Unmounts and destroys the React component rendered in the `container`.
+ *
+ * @param {DOMElement} container DOM element containing a React component.
+ * @return {boolean} True if a component was found in and unmounted from
+ * `container`
+ */
+ unmountComponentAtNode: function (container) {
+ // Various parts of our code (such as ReactCompositeComponent's
+ // _renderValidatedComponent) assume that calls to render aren't nested;
+ // verify that that's the case. (Strictly speaking, unmounting won't cause a
+ // render but we still don't expect to be in a render call here.)
+ "production" !== 'production' ? warning(ReactCurrentOwner.current == null, 'unmountComponentAtNode(): Render methods should be a pure function ' + 'of props and state; triggering nested component updates from render ' + 'is not allowed. If necessary, trigger nested updates in ' + 'componentDidUpdate. Check the render method of %s.', ReactCurrentOwner.current && ReactCurrentOwner.current.getName() || 'ReactCompositeComponent') : undefined;
+
+ !(container && (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE || container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)) ? "production" !== 'production' ? invariant(false, 'unmountComponentAtNode(...): Target container is not a DOM element.') : invariant(false) : undefined;
+
+ var reactRootID = getReactRootID(container);
+ var component = instancesByReactRootID[reactRootID];
+ if (!component) {
+ // Check if the node being unmounted was rendered by React, but isn't a
+ // root node.
+ var containerHasNonRootReactChild = hasNonRootReactChild(container);
+
+ // Check if the container itself is a React root node.
+ var containerID = internalGetID(container);
+ var isContainerReactRoot = containerID && containerID === ReactInstanceHandles.getReactRootIDFromNodeID(containerID);
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(!containerHasNonRootReactChild, 'unmountComponentAtNode(): The node you\'re attempting to unmount ' + 'was rendered by React and is not a top-level container. %s', isContainerReactRoot ? 'You may have accidentally passed in a React root node instead ' + 'of its container.' : 'Instead, have the parent component update its state and ' + 'rerender in order to remove this component.') : undefined;
+ }
+
+ return false;
+ }
+ ReactUpdates.batchedUpdates(unmountComponentFromNode, component, container);
+ delete instancesByReactRootID[reactRootID];
+ delete containersByReactRootID[reactRootID];
+ if ("production" !== 'production') {
+ delete rootElementsByReactRootID[reactRootID];
+ }
+ return true;
+ },
+
+ /**
+ * Finds the container DOM element that contains React component to which the
+ * supplied DOM `id` belongs.
+ *
+ * @param {string} id The ID of an element rendered by a React component.
+ * @return {?DOMElement} DOM element that contains the `id`.
+ */
+ findReactContainerForID: function (id) {
+ var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(id);
+ var container = containersByReactRootID[reactRootID];
+
+ if ("production" !== 'production') {
+ var rootElement = rootElementsByReactRootID[reactRootID];
+ if (rootElement && rootElement.parentNode !== container) {
+ "production" !== 'production' ? warning(
+ // Call internalGetID here because getID calls isValid which calls
+ // findReactContainerForID (this function).
+ internalGetID(rootElement) === reactRootID, 'ReactMount: Root element ID differed from reactRootID.') : undefined;
+ var containerChild = container.firstChild;
+ if (containerChild && reactRootID === internalGetID(containerChild)) {
+ // If the container has a new child with the same ID as the old
+ // root element, then rootElementsByReactRootID[reactRootID] is
+ // just stale and needs to be updated. The case that deserves a
+ // warning is when the container is empty.
+ rootElementsByReactRootID[reactRootID] = containerChild;
+ } else {
+ "production" !== 'production' ? warning(false, 'ReactMount: Root element has been removed from its original ' + 'container. New container: %s', rootElement.parentNode) : undefined;
+ }
+ }
+ }
+
+ return container;
+ },
+
+ /**
+ * Finds an element rendered by React with the supplied ID.
+ *
+ * @param {string} id ID of a DOM node in the React component.
+ * @return {DOMElement} Root DOM node of the React component.
+ */
+ findReactNodeByID: function (id) {
+ var reactRoot = ReactMount.findReactContainerForID(id);
+ return ReactMount.findComponentRoot(reactRoot, id);
+ },
+
+ /**
+ * Traverses up the ancestors of the supplied node to find a node that is a
+ * DOM representation of a React component rendered by this copy of React.
+ *
+ * @param {*} node
+ * @return {?DOMEventTarget}
+ * @internal
+ */
+ getFirstReactDOM: function (node) {
+ return findFirstReactDOMImpl(node);
+ },
+
+ /**
+ * Finds a node with the supplied `targetID` inside of the supplied
+ * `ancestorNode`. Exploits the ID naming scheme to perform the search
+ * quickly.
+ *
+ * @param {DOMEventTarget} ancestorNode Search from this root.
+ * @pararm {string} targetID ID of the DOM representation of the component.
+ * @return {DOMEventTarget} DOM node with the supplied `targetID`.
+ * @internal
+ */
+ findComponentRoot: function (ancestorNode, targetID) {
+ var firstChildren = findComponentRootReusableArray;
+ var childIndex = 0;
+
+ var deepestAncestor = findDeepestCachedAncestor(targetID) || ancestorNode;
+
+ if ("production" !== 'production') {
+ // This will throw on the next line; give an early warning
+ "production" !== 'production' ? warning(deepestAncestor != null, 'React can\'t find the root component node for data-reactid value ' + '`%s`. If you\'re seeing this message, it probably means that ' + 'you\'ve loaded two copies of React on the page. At this time, only ' + 'a single copy of React can be loaded at a time.', targetID) : undefined;
+ }
+
+ firstChildren[0] = deepestAncestor.firstChild;
+ firstChildren.length = 1;
+
+ while (childIndex < firstChildren.length) {
+ var child = firstChildren[childIndex++];
+ var targetChild;
+
+ while (child) {
+ var childID = ReactMount.getID(child);
+ if (childID) {
+ // Even if we find the node we're looking for, we finish looping
+ // through its siblings to ensure they're cached so that we don't have
+ // to revisit this node again. Otherwise, we make n^2 calls to getID
+ // when visiting the many children of a single node in order.
+
+ if (targetID === childID) {
+ targetChild = child;
+ } else if (ReactInstanceHandles.isAncestorIDOf(childID, targetID)) {
+ // If we find a child whose ID is an ancestor of the given ID,
+ // then we can be sure that we only want to search the subtree
+ // rooted at this child, so we can throw out the rest of the
+ // search state.
+ firstChildren.length = childIndex = 0;
+ firstChildren.push(child.firstChild);
+ }
+ } else {
+ // If this child had no ID, then there's a chance that it was
+ // injected automatically by the browser, as when a `<table>`
+ // element sprouts an extra `<tbody>` child as a side effect of
+ // `.innerHTML` parsing. Optimistically continue down this
+ // branch, but not before examining the other siblings.
+ firstChildren.push(child.firstChild);
+ }
+
+ child = child.nextSibling;
+ }
+
+ if (targetChild) {
+ // Emptying firstChildren/findComponentRootReusableArray is
+ // not necessary for correctness, but it helps the GC reclaim
+ // any nodes that were left at the end of the search.
+ firstChildren.length = 0;
+
+ return targetChild;
+ }
+ }
+
+ firstChildren.length = 0;
+
+ !false ? "production" !== 'production' ? invariant(false, 'findComponentRoot(..., %s): Unable to find element. This probably ' + 'means the DOM was unexpectedly mutated (e.g., by the browser), ' + 'usually due to forgetting a <tbody> when using tables, nesting tags ' + 'like <form>, <p>, or <a>, or using non-SVG elements in an <svg> ' + 'parent. ' + 'Try inspecting the child nodes of the element with React ID `%s`.', targetID, ReactMount.getID(ancestorNode)) : invariant(false) : undefined;
+ },
+
+ _mountImageIntoNode: function (markup, container, shouldReuseMarkup, transaction) {
+ !(container && (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE || container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)) ? "production" !== 'production' ? invariant(false, 'mountComponentIntoNode(...): Target container is not valid.') : invariant(false) : undefined;
+
+ if (shouldReuseMarkup) {
+ var rootElement = getReactRootElementInContainer(container);
+ if (ReactMarkupChecksum.canReuseMarkup(markup, rootElement)) {
+ return;
+ } else {
+ var checksum = rootElement.getAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
+ rootElement.removeAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);
+
+ var rootMarkup = rootElement.outerHTML;
+ rootElement.setAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME, checksum);
+
+ var normalizedMarkup = markup;
+ if ("production" !== 'production') {
+ // because rootMarkup is retrieved from the DOM, various normalizations
+ // will have occurred which will not be present in `markup`. Here,
+ // insert markup into a <div> or <iframe> depending on the container
+ // type to perform the same normalizations before comparing.
+ var normalizer;
+ if (container.nodeType === ELEMENT_NODE_TYPE) {
+ normalizer = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ normalizer.innerHTML = markup;
+ normalizedMarkup = normalizer.innerHTML;
+ } else {
+ normalizer = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe');
+ document.body.appendChild(normalizer);
+ normalizer.contentDocument.write(markup);
+ normalizedMarkup = normalizer.contentDocument.documentElement.outerHTML;
+ document.body.removeChild(normalizer);
+ }
+ }
+
+ var diffIndex = firstDifferenceIndex(normalizedMarkup, rootMarkup);
+ var difference = ' (client) ' + normalizedMarkup.substring(diffIndex - 20, diffIndex + 20) + '\n (server) ' + rootMarkup.substring(diffIndex - 20, diffIndex + 20);
+
+ !(container.nodeType !== DOC_NODE_TYPE) ? "production" !== 'production' ? invariant(false, 'You\'re trying to render a component to the document using ' + 'server rendering but the checksum was invalid. This usually ' + 'means you rendered a different component type or props on ' + 'the client from the one on the server, or your render() ' + 'methods are impure. React cannot handle this case due to ' + 'cross-browser quirks by rendering at the document root. You ' + 'should look for environment dependent code in your components ' + 'and ensure the props are the same client and server side:\n%s', difference) : invariant(false) : undefined;
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(false, 'React attempted to reuse markup in a container but the ' + 'checksum was invalid. This generally means that you are ' + 'using server rendering and the markup generated on the ' + 'server was not what the client was expecting. React injected ' + 'new markup to compensate which works but you have lost many ' + 'of the benefits of server rendering. Instead, figure out ' + 'why the markup being generated is different on the client ' + 'or server:\n%s', difference) : undefined;
+ }
+ }
+ }
+
+ !(container.nodeType !== DOC_NODE_TYPE) ? "production" !== 'production' ? invariant(false, 'You\'re trying to render a component to the document but ' + 'you didn\'t use server rendering. We can\'t do this ' + 'without using server rendering due to cross-browser quirks. ' + 'See ReactDOMServer.renderToString() for server rendering.') : invariant(false) : undefined;
+
+ if (transaction.useCreateElement) {
+ while (container.lastChild) {
+ container.removeChild(container.lastChild);
+ }
+ container.appendChild(markup);
+ } else {
+ setInnerHTML(container, markup);
+ }
+ },
+
+ ownerDocumentContextKey: ownerDocumentContextKey,
+
+ /**
+ * React ID utilities.
+ */
+
+ getReactRootID: getReactRootID,
+
+ getID: getID,
+
+ setID: setID,
+
+ getNode: getNode,
+
+ getNodeFromInstance: getNodeFromInstance,
+
+ isValid: isValid,
+
+ purgeID: purgeID
+};
+
+ReactPerf.measureMethods(ReactMount, 'ReactMount', {
+ _renderNewRootComponent: '_renderNewRootComponent',
+ _mountImageIntoNode: '_mountImageIntoNode'
+});
+
+module.exports = ReactMount;
+},{"10":10,"132":132,"138":138,"141":141,"144":144,"150":150,"154":154,"161":161,"173":173,"24":24,"28":28,"39":39,"44":44,"57":57,"60":60,"67":67,"68":68,"71":71,"78":78,"84":84,"95":95,"96":96}],73:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMultiChild
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactComponentEnvironment = _dereq_(36);
+var ReactMultiChildUpdateTypes = _dereq_(74);
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactReconciler = _dereq_(84);
+var ReactChildReconciler = _dereq_(31);
+
+var flattenChildren = _dereq_(123);
+
+/**
+ * Updating children of a component may trigger recursive updates. The depth is
+ * used to batch recursive updates to render markup more efficiently.
+ *
+ * @type {number}
+ * @private
+ */
+var updateDepth = 0;
+
+/**
+ * Queue of update configuration objects.
+ *
+ * Each object has a `type` property that is in `ReactMultiChildUpdateTypes`.
+ *
+ * @type {array<object>}
+ * @private
+ */
+var updateQueue = [];
+
+/**
+ * Queue of markup to be rendered.
+ *
+ * @type {array<string>}
+ * @private
+ */
+var markupQueue = [];
+
+/**
+ * Enqueues markup to be rendered and inserted at a supplied index.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {string} markup Markup that renders into an element.
+ * @param {number} toIndex Destination index.
+ * @private
+ */
+function enqueueInsertMarkup(parentID, markup, toIndex) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
+ markupIndex: markupQueue.push(markup) - 1,
+ content: null,
+ fromIndex: null,
+ toIndex: toIndex
+ });
+}
+
+/**
+ * Enqueues moving an existing element to another index.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {number} fromIndex Source index of the existing element.
+ * @param {number} toIndex Destination index of the element.
+ * @private
+ */
+function enqueueMove(parentID, fromIndex, toIndex) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
+ markupIndex: null,
+ content: null,
+ fromIndex: fromIndex,
+ toIndex: toIndex
+ });
+}
+
+/**
+ * Enqueues removing an element at an index.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {number} fromIndex Index of the element to remove.
+ * @private
+ */
+function enqueueRemove(parentID, fromIndex) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.REMOVE_NODE,
+ markupIndex: null,
+ content: null,
+ fromIndex: fromIndex,
+ toIndex: null
+ });
+}
+
+/**
+ * Enqueues setting the markup of a node.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {string} markup Markup that renders into an element.
+ * @private
+ */
+function enqueueSetMarkup(parentID, markup) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.SET_MARKUP,
+ markupIndex: null,
+ content: markup,
+ fromIndex: null,
+ toIndex: null
+ });
+}
+
+/**
+ * Enqueues setting the text content.
+ *
+ * @param {string} parentID ID of the parent component.
+ * @param {string} textContent Text content to set.
+ * @private
+ */
+function enqueueTextContent(parentID, textContent) {
+ // NOTE: Null values reduce hidden classes.
+ updateQueue.push({
+ parentID: parentID,
+ parentNode: null,
+ type: ReactMultiChildUpdateTypes.TEXT_CONTENT,
+ markupIndex: null,
+ content: textContent,
+ fromIndex: null,
+ toIndex: null
+ });
+}
+
+/**
+ * Processes any enqueued updates.
+ *
+ * @private
+ */
+function processQueue() {
+ if (updateQueue.length) {
+ ReactComponentEnvironment.processChildrenUpdates(updateQueue, markupQueue);
+ clearQueue();
+ }
+}
+
+/**
+ * Clears any enqueued updates.
+ *
+ * @private
+ */
+function clearQueue() {
+ updateQueue.length = 0;
+ markupQueue.length = 0;
+}
+
+/**
+ * ReactMultiChild are capable of reconciling multiple children.
+ *
+ * @class ReactMultiChild
+ * @internal
+ */
+var ReactMultiChild = {
+
+ /**
+ * Provides common functionality for components that must reconcile multiple
+ * children. This is used by `ReactDOMComponent` to mount, update, and
+ * unmount child components.
+ *
+ * @lends {ReactMultiChild.prototype}
+ */
+ Mixin: {
+
+ _reconcilerInstantiateChildren: function (nestedChildren, transaction, context) {
+ if ("production" !== 'production') {
+ if (this._currentElement) {
+ try {
+ ReactCurrentOwner.current = this._currentElement._owner;
+ return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ }
+ }
+ return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);
+ },
+
+ _reconcilerUpdateChildren: function (prevChildren, nextNestedChildrenElements, transaction, context) {
+ var nextChildren;
+ if ("production" !== 'production') {
+ if (this._currentElement) {
+ try {
+ ReactCurrentOwner.current = this._currentElement._owner;
+ nextChildren = flattenChildren(nextNestedChildrenElements);
+ } finally {
+ ReactCurrentOwner.current = null;
+ }
+ return ReactChildReconciler.updateChildren(prevChildren, nextChildren, transaction, context);
+ }
+ }
+ nextChildren = flattenChildren(nextNestedChildrenElements);
+ return ReactChildReconciler.updateChildren(prevChildren, nextChildren, transaction, context);
+ },
+
+ /**
+ * Generates a "mount image" for each of the supplied children. In the case
+ * of `ReactDOMComponent`, a mount image is a string of markup.
+ *
+ * @param {?object} nestedChildren Nested child maps.
+ * @return {array} An array of mounted representations.
+ * @internal
+ */
+ mountChildren: function (nestedChildren, transaction, context) {
+ var children = this._reconcilerInstantiateChildren(nestedChildren, transaction, context);
+ this._renderedChildren = children;
+ var mountImages = [];
+ var index = 0;
+ for (var name in children) {
+ if (children.hasOwnProperty(name)) {
+ var child = children[name];
+ // Inlined for performance, see `ReactInstanceHandles.createReactID`.
+ var rootID = this._rootNodeID + name;
+ var mountImage = ReactReconciler.mountComponent(child, rootID, transaction, context);
+ child._mountIndex = index++;
+ mountImages.push(mountImage);
+ }
+ }
+ return mountImages;
+ },
+
+ /**
+ * Replaces any rendered children with a text content string.
+ *
+ * @param {string} nextContent String of content.
+ * @internal
+ */
+ updateTextContent: function (nextContent) {
+ updateDepth++;
+ var errorThrown = true;
+ try {
+ var prevChildren = this._renderedChildren;
+ // Remove any rendered children.
+ ReactChildReconciler.unmountChildren(prevChildren);
+ // TODO: The setTextContent operation should be enough
+ for (var name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name)) {
+ this._unmountChild(prevChildren[name]);
+ }
+ }
+ // Set new text content.
+ this.setTextContent(nextContent);
+ errorThrown = false;
+ } finally {
+ updateDepth--;
+ if (!updateDepth) {
+ if (errorThrown) {
+ clearQueue();
+ } else {
+ processQueue();
+ }
+ }
+ }
+ },
+
+ /**
+ * Replaces any rendered children with a markup string.
+ *
+ * @param {string} nextMarkup String of markup.
+ * @internal
+ */
+ updateMarkup: function (nextMarkup) {
+ updateDepth++;
+ var errorThrown = true;
+ try {
+ var prevChildren = this._renderedChildren;
+ // Remove any rendered children.
+ ReactChildReconciler.unmountChildren(prevChildren);
+ for (var name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name)) {
+ this._unmountChildByName(prevChildren[name], name);
+ }
+ }
+ this.setMarkup(nextMarkup);
+ errorThrown = false;
+ } finally {
+ updateDepth--;
+ if (!updateDepth) {
+ if (errorThrown) {
+ clearQueue();
+ } else {
+ processQueue();
+ }
+ }
+ }
+ },
+
+ /**
+ * Updates the rendered children with new children.
+ *
+ * @param {?object} nextNestedChildrenElements Nested child element maps.
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ updateChildren: function (nextNestedChildrenElements, transaction, context) {
+ updateDepth++;
+ var errorThrown = true;
+ try {
+ this._updateChildren(nextNestedChildrenElements, transaction, context);
+ errorThrown = false;
+ } finally {
+ updateDepth--;
+ if (!updateDepth) {
+ if (errorThrown) {
+ clearQueue();
+ } else {
+ processQueue();
+ }
+ }
+ }
+ },
+
+ /**
+ * Improve performance by isolating this hot code path from the try/catch
+ * block in `updateChildren`.
+ *
+ * @param {?object} nextNestedChildrenElements Nested child element maps.
+ * @param {ReactReconcileTransaction} transaction
+ * @final
+ * @protected
+ */
+ _updateChildren: function (nextNestedChildrenElements, transaction, context) {
+ var prevChildren = this._renderedChildren;
+ var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, transaction, context);
+ this._renderedChildren = nextChildren;
+ if (!nextChildren && !prevChildren) {
+ return;
+ }
+ var name;
+ // `nextIndex` will increment for each child in `nextChildren`, but
+ // `lastIndex` will be the last index visited in `prevChildren`.
+ var lastIndex = 0;
+ var nextIndex = 0;
+ for (name in nextChildren) {
+ if (!nextChildren.hasOwnProperty(name)) {
+ continue;
+ }
+ var prevChild = prevChildren && prevChildren[name];
+ var nextChild = nextChildren[name];
+ if (prevChild === nextChild) {
+ this.moveChild(prevChild, nextIndex, lastIndex);
+ lastIndex = Math.max(prevChild._mountIndex, lastIndex);
+ prevChild._mountIndex = nextIndex;
+ } else {
+ if (prevChild) {
+ // Update `lastIndex` before `_mountIndex` gets unset by unmounting.
+ lastIndex = Math.max(prevChild._mountIndex, lastIndex);
+ this._unmountChild(prevChild);
+ }
+ // The child must be instantiated before it's mounted.
+ this._mountChildByNameAtIndex(nextChild, name, nextIndex, transaction, context);
+ }
+ nextIndex++;
+ }
+ // Remove children that are no longer present.
+ for (name in prevChildren) {
+ if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
+ this._unmountChild(prevChildren[name]);
+ }
+ }
+ },
+
+ /**
+ * Unmounts all rendered children. This should be used to clean up children
+ * when this component is unmounted.
+ *
+ * @internal
+ */
+ unmountChildren: function () {
+ var renderedChildren = this._renderedChildren;
+ ReactChildReconciler.unmountChildren(renderedChildren);
+ this._renderedChildren = null;
+ },
+
+ /**
+ * Moves a child component to the supplied index.
+ *
+ * @param {ReactComponent} child Component to move.
+ * @param {number} toIndex Destination index of the element.
+ * @param {number} lastIndex Last index visited of the siblings of `child`.
+ * @protected
+ */
+ moveChild: function (child, toIndex, lastIndex) {
+ // If the index of `child` is less than `lastIndex`, then it needs to
+ // be moved. Otherwise, we do not need to move it because a child will be
+ // inserted or moved before `child`.
+ if (child._mountIndex < lastIndex) {
+ enqueueMove(this._rootNodeID, child._mountIndex, toIndex);
+ }
+ },
+
+ /**
+ * Creates a child component.
+ *
+ * @param {ReactComponent} child Component to create.
+ * @param {string} mountImage Markup to insert.
+ * @protected
+ */
+ createChild: function (child, mountImage) {
+ enqueueInsertMarkup(this._rootNodeID, mountImage, child._mountIndex);
+ },
+
+ /**
+ * Removes a child component.
+ *
+ * @param {ReactComponent} child Child to remove.
+ * @protected
+ */
+ removeChild: function (child) {
+ enqueueRemove(this._rootNodeID, child._mountIndex);
+ },
+
+ /**
+ * Sets this text content string.
+ *
+ * @param {string} textContent Text content to set.
+ * @protected
+ */
+ setTextContent: function (textContent) {
+ enqueueTextContent(this._rootNodeID, textContent);
+ },
+
+ /**
+ * Sets this markup string.
+ *
+ * @param {string} markup Markup to set.
+ * @protected
+ */
+ setMarkup: function (markup) {
+ enqueueSetMarkup(this._rootNodeID, markup);
+ },
+
+ /**
+ * Mounts a child with the supplied name.
+ *
+ * NOTE: This is part of `updateChildren` and is here for readability.
+ *
+ * @param {ReactComponent} child Component to mount.
+ * @param {string} name Name of the child.
+ * @param {number} index Index at which to insert the child.
+ * @param {ReactReconcileTransaction} transaction
+ * @private
+ */
+ _mountChildByNameAtIndex: function (child, name, index, transaction, context) {
+ // Inlined for performance, see `ReactInstanceHandles.createReactID`.
+ var rootID = this._rootNodeID + name;
+ var mountImage = ReactReconciler.mountComponent(child, rootID, transaction, context);
+ child._mountIndex = index;
+ this.createChild(child, mountImage);
+ },
+
+ /**
+ * Unmounts a rendered child.
+ *
+ * NOTE: This is part of `updateChildren` and is here for readability.
+ *
+ * @param {ReactComponent} child Component to unmount.
+ * @private
+ */
+ _unmountChild: function (child) {
+ this.removeChild(child);
+ child._mountIndex = null;
+ }
+
+ }
+
+};
+
+module.exports = ReactMultiChild;
+},{"123":123,"31":31,"36":36,"39":39,"74":74,"84":84}],74:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactMultiChildUpdateTypes
+ */
+
+'use strict';
+
+var keyMirror = _dereq_(165);
+
+/**
+ * When a component's children are updated, a series of update configuration
+ * objects are created in order to batch and serialize the required changes.
+ *
+ * Enumerates all the possible types of update configurations.
+ *
+ * @internal
+ */
+var ReactMultiChildUpdateTypes = keyMirror({
+ INSERT_MARKUP: null,
+ MOVE_EXISTING: null,
+ REMOVE_NODE: null,
+ SET_MARKUP: null,
+ TEXT_CONTENT: null
+});
+
+module.exports = ReactMultiChildUpdateTypes;
+},{"165":165}],75:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactNativeComponent
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+var autoGenerateWrapperClass = null;
+var genericComponentClass = null;
+// This registry keeps track of wrapper classes around native tags.
+var tagToComponentClass = {};
+var textComponentClass = null;
+
+var ReactNativeComponentInjection = {
+ // This accepts a class that receives the tag string. This is a catch all
+ // that can render any kind of tag.
+ injectGenericComponentClass: function (componentClass) {
+ genericComponentClass = componentClass;
+ },
+ // This accepts a text component class that takes the text string to be
+ // rendered as props.
+ injectTextComponentClass: function (componentClass) {
+ textComponentClass = componentClass;
+ },
+ // This accepts a keyed object with classes as values. Each key represents a
+ // tag. That particular tag will use this class instead of the generic one.
+ injectComponentClasses: function (componentClasses) {
+ assign(tagToComponentClass, componentClasses);
+ }
+};
+
+/**
+ * Get a composite component wrapper class for a specific tag.
+ *
+ * @param {ReactElement} element The tag for which to get the class.
+ * @return {function} The React class constructor function.
+ */
+function getComponentClassForElement(element) {
+ if (typeof element.type === 'function') {
+ return element.type;
+ }
+ var tag = element.type;
+ var componentClass = tagToComponentClass[tag];
+ if (componentClass == null) {
+ tagToComponentClass[tag] = componentClass = autoGenerateWrapperClass(tag);
+ }
+ return componentClass;
+}
+
+/**
+ * Get a native internal component class for a specific tag.
+ *
+ * @param {ReactElement} element The element to create.
+ * @return {function} The internal class constructor function.
+ */
+function createInternalComponent(element) {
+ !genericComponentClass ? "production" !== 'production' ? invariant(false, 'There is no registered component for the tag %s', element.type) : invariant(false) : undefined;
+ return new genericComponentClass(element.type, element.props);
+}
+
+/**
+ * @param {ReactText} text
+ * @return {ReactComponent}
+ */
+function createInstanceForText(text) {
+ return new textComponentClass(text);
+}
+
+/**
+ * @param {ReactComponent} component
+ * @return {boolean}
+ */
+function isTextComponent(component) {
+ return component instanceof textComponentClass;
+}
+
+var ReactNativeComponent = {
+ getComponentClassForElement: getComponentClassForElement,
+ createInternalComponent: createInternalComponent,
+ createInstanceForText: createInstanceForText,
+ isTextComponent: isTextComponent,
+ injection: ReactNativeComponentInjection
+};
+
+module.exports = ReactNativeComponent;
+},{"161":161,"24":24}],76:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactNoopUpdateQueue
+ */
+
+'use strict';
+
+var warning = _dereq_(173);
+
+function warnTDZ(publicInstance, callerName) {
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(false, '%s(...): Can only update a mounted or mounting component. ' + 'This usually means you called %s() on an unmounted component. ' + 'This is a no-op. Please check the code for the %s component.', callerName, callerName, publicInstance.constructor && publicInstance.constructor.displayName || '') : undefined;
+ }
+}
+
+/**
+ * This is the abstract API for an update queue.
+ */
+var ReactNoopUpdateQueue = {
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @param {ReactClass} publicInstance The instance we want to test.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function (publicInstance) {
+ return false;
+ },
+
+ /**
+ * Enqueue a callback that will be executed after all the pending updates
+ * have processed.
+ *
+ * @param {ReactClass} publicInstance The instance to use as `this` context.
+ * @param {?function} callback Called after state is updated.
+ * @internal
+ */
+ enqueueCallback: function (publicInstance, callback) {},
+
+ /**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldComponentUpdate`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @internal
+ */
+ enqueueForceUpdate: function (publicInstance) {
+ warnTDZ(publicInstance, 'forceUpdate');
+ },
+
+ /**
+ * Replaces all of the state. Always use this or `setState` to mutate state.
+ * You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} completeState Next state.
+ * @internal
+ */
+ enqueueReplaceState: function (publicInstance, completeState) {
+ warnTDZ(publicInstance, 'replaceState');
+ },
+
+ /**
+ * Sets a subset of the state. This only exists because _pendingState is
+ * internal. This provides a merging strategy that is not available to deep
+ * properties which is confusing. TODO: Expose pendingState or don't use it
+ * during the merge.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialState Next partial state to be merged with state.
+ * @internal
+ */
+ enqueueSetState: function (publicInstance, partialState) {
+ warnTDZ(publicInstance, 'setState');
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialProps Subset of the next props.
+ * @internal
+ */
+ enqueueSetProps: function (publicInstance, partialProps) {
+ warnTDZ(publicInstance, 'setProps');
+ },
+
+ /**
+ * Replaces all of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} props New props.
+ * @internal
+ */
+ enqueueReplaceProps: function (publicInstance, props) {
+ warnTDZ(publicInstance, 'replaceProps');
+ }
+
+};
+
+module.exports = ReactNoopUpdateQueue;
+},{"173":173}],77:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactOwner
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * ReactOwners are capable of storing references to owned components.
+ *
+ * All components are capable of //being// referenced by owner components, but
+ * only ReactOwner components are capable of //referencing// owned components.
+ * The named reference is known as a "ref".
+ *
+ * Refs are available when mounted and updated during reconciliation.
+ *
+ * var MyComponent = React.createClass({
+ * render: function() {
+ * return (
+ * <div onClick={this.handleClick}>
+ * <CustomComponent ref="custom" />
+ * </div>
+ * );
+ * },
+ * handleClick: function() {
+ * this.refs.custom.handleClick();
+ * },
+ * componentDidMount: function() {
+ * this.refs.custom.initialize();
+ * }
+ * });
+ *
+ * Refs should rarely be used. When refs are used, they should only be done to
+ * control data that is not handled by React's data flow.
+ *
+ * @class ReactOwner
+ */
+var ReactOwner = {
+
+ /**
+ * @param {?object} object
+ * @return {boolean} True if `object` is a valid owner.
+ * @final
+ */
+ isValidOwner: function (object) {
+ return !!(object && typeof object.attachRef === 'function' && typeof object.detachRef === 'function');
+ },
+
+ /**
+ * Adds a component by ref to an owner component.
+ *
+ * @param {ReactComponent} component Component to reference.
+ * @param {string} ref Name by which to refer to the component.
+ * @param {ReactOwner} owner Component on which to record the ref.
+ * @final
+ * @internal
+ */
+ addComponentAsRefTo: function (component, ref, owner) {
+ !ReactOwner.isValidOwner(owner) ? "production" !== 'production' ? invariant(false, 'addComponentAsRefTo(...): Only a ReactOwner can have refs. You might ' + 'be adding a ref to a component that was not created inside a component\'s ' + '`render` method, or you have multiple copies of React loaded ' + '(details: https://fb.me/react-refs-must-have-owner).') : invariant(false) : undefined;
+ owner.attachRef(ref, component);
+ },
+
+ /**
+ * Removes a component by ref from an owner component.
+ *
+ * @param {ReactComponent} component Component to dereference.
+ * @param {string} ref Name of the ref to remove.
+ * @param {ReactOwner} owner Component on which the ref is recorded.
+ * @final
+ * @internal
+ */
+ removeComponentAsRefFrom: function (component, ref, owner) {
+ !ReactOwner.isValidOwner(owner) ? "production" !== 'production' ? invariant(false, 'removeComponentAsRefFrom(...): Only a ReactOwner can have refs. You might ' + 'be removing a ref to a component that was not created inside a component\'s ' + '`render` method, or you have multiple copies of React loaded ' + '(details: https://fb.me/react-refs-must-have-owner).') : invariant(false) : undefined;
+ // Check that `component` is still the current ref because we do not want to
+ // detach the ref if another component stole it.
+ if (owner.getPublicInstance().refs[ref] === component.getPublicInstance()) {
+ owner.detachRef(ref);
+ }
+ }
+
+};
+
+module.exports = ReactOwner;
+},{"161":161}],78:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPerf
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * ReactPerf is a general AOP system designed to measure performance. This
+ * module only has the hooks: see ReactDefaultPerf for the analysis tool.
+ */
+var ReactPerf = {
+ /**
+ * Boolean to enable/disable measurement. Set to false by default to prevent
+ * accidental logging and perf loss.
+ */
+ enableMeasure: false,
+
+ /**
+ * Holds onto the measure function in use. By default, don't measure
+ * anything, but we'll override this if we inject a measure function.
+ */
+ storedMeasure: _noMeasure,
+
+ /**
+ * @param {object} object
+ * @param {string} objectName
+ * @param {object<string>} methodNames
+ */
+ measureMethods: function (object, objectName, methodNames) {
+ if ("production" !== 'production') {
+ for (var key in methodNames) {
+ if (!methodNames.hasOwnProperty(key)) {
+ continue;
+ }
+ object[key] = ReactPerf.measure(objectName, methodNames[key], object[key]);
+ }
+ }
+ },
+
+ /**
+ * Use this to wrap methods you want to measure. Zero overhead in production.
+ *
+ * @param {string} objName
+ * @param {string} fnName
+ * @param {function} func
+ * @return {function}
+ */
+ measure: function (objName, fnName, func) {
+ if ("production" !== 'production') {
+ var measuredFunc = null;
+ var wrapper = function () {
+ if (ReactPerf.enableMeasure) {
+ if (!measuredFunc) {
+ measuredFunc = ReactPerf.storedMeasure(objName, fnName, func);
+ }
+ return measuredFunc.apply(this, arguments);
+ }
+ return func.apply(this, arguments);
+ };
+ wrapper.displayName = objName + '_' + fnName;
+ return wrapper;
+ }
+ return func;
+ },
+
+ injection: {
+ /**
+ * @param {function} measure
+ */
+ injectMeasure: function (measure) {
+ ReactPerf.storedMeasure = measure;
+ }
+ }
+};
+
+/**
+ * Simply passes through the measured function, without measuring it.
+ *
+ * @param {string} objName
+ * @param {string} fnName
+ * @param {function} func
+ * @return {function}
+ */
+function _noMeasure(objName, fnName, func) {
+ return func;
+}
+
+module.exports = ReactPerf;
+},{}],79:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTransferer
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+var joinClasses = _dereq_(164);
+
+/**
+ * Creates a transfer strategy that will merge prop values using the supplied
+ * `mergeStrategy`. If a prop was previously unset, this just sets it.
+ *
+ * @param {function} mergeStrategy
+ * @return {function}
+ */
+function createTransferStrategy(mergeStrategy) {
+ return function (props, key, value) {
+ if (!props.hasOwnProperty(key)) {
+ props[key] = value;
+ } else {
+ props[key] = mergeStrategy(props[key], value);
+ }
+ };
+}
+
+var transferStrategyMerge = createTransferStrategy(function (a, b) {
+ // `merge` overrides the first object's (`props[key]` above) keys using the
+ // second object's (`value`) keys. An object's style's existing `propA` would
+ // get overridden. Flip the order here.
+ return assign({}, b, a);
+});
+
+/**
+ * Transfer strategies dictate how props are transferred by `transferPropsTo`.
+ * NOTE: if you add any more exceptions to this list you should be sure to
+ * update `cloneWithProps()` accordingly.
+ */
+var TransferStrategies = {
+ /**
+ * Never transfer `children`.
+ */
+ children: emptyFunction,
+ /**
+ * Transfer the `className` prop by merging them.
+ */
+ className: createTransferStrategy(joinClasses),
+ /**
+ * Transfer the `style` prop (which is an object) by merging them.
+ */
+ style: transferStrategyMerge
+};
+
+/**
+ * Mutates the first argument by transferring the properties from the second
+ * argument.
+ *
+ * @param {object} props
+ * @param {object} newProps
+ * @return {object}
+ */
+function transferInto(props, newProps) {
+ for (var thisKey in newProps) {
+ if (!newProps.hasOwnProperty(thisKey)) {
+ continue;
+ }
+
+ var transferStrategy = TransferStrategies[thisKey];
+
+ if (transferStrategy && TransferStrategies.hasOwnProperty(thisKey)) {
+ transferStrategy(props, thisKey, newProps[thisKey]);
+ } else if (!props.hasOwnProperty(thisKey)) {
+ props[thisKey] = newProps[thisKey];
+ }
+ }
+ return props;
+}
+
+/**
+ * ReactPropTransferer are capable of transferring props to another component
+ * using a `transferPropsTo` method.
+ *
+ * @class ReactPropTransferer
+ */
+var ReactPropTransferer = {
+
+ /**
+ * Merge two props objects using TransferStrategies.
+ *
+ * @param {object} oldProps original props (they take precedence)
+ * @param {object} newProps new props to merge in
+ * @return {object} a new object containing both sets of props merged.
+ */
+ mergeProps: function (oldProps, newProps) {
+ return transferInto(assign({}, oldProps), newProps);
+ }
+
+};
+
+module.exports = ReactPropTransferer;
+},{"153":153,"164":164,"24":24}],80:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTypeLocationNames
+ */
+
+'use strict';
+
+var ReactPropTypeLocationNames = {};
+
+if ("production" !== 'production') {
+ ReactPropTypeLocationNames = {
+ prop: 'prop',
+ context: 'context',
+ childContext: 'child context'
+ };
+}
+
+module.exports = ReactPropTypeLocationNames;
+},{}],81:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTypeLocations
+ */
+
+'use strict';
+
+var keyMirror = _dereq_(165);
+
+var ReactPropTypeLocations = keyMirror({
+ prop: null,
+ context: null,
+ childContext: null
+});
+
+module.exports = ReactPropTypeLocations;
+},{"165":165}],82:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPropTypes
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactPropTypeLocationNames = _dereq_(80);
+
+var emptyFunction = _dereq_(153);
+var getIteratorFn = _dereq_(129);
+
+/**
+ * Collection of methods that allow declaration and validation of props that are
+ * supplied to React components. Example usage:
+ *
+ * var Props = require('ReactPropTypes');
+ * var MyArticle = React.createClass({
+ * propTypes: {
+ * // An optional string prop named "description".
+ * description: Props.string,
+ *
+ * // A required enum prop named "category".
+ * category: Props.oneOf(['News','Photos']).isRequired,
+ *
+ * // A prop named "dialog" that requires an instance of Dialog.
+ * dialog: Props.instanceOf(Dialog).isRequired
+ * },
+ * render: function() { ... }
+ * });
+ *
+ * A more formal specification of how these methods are used:
+ *
+ * type := array|bool|func|object|number|string|oneOf([...])|instanceOf(...)
+ * decl := ReactPropTypes.{type}(.isRequired)?
+ *
+ * Each and every declaration produces a function with the same signature. This
+ * allows the creation of custom validation functions. For example:
+ *
+ * var MyLink = React.createClass({
+ * propTypes: {
+ * // An optional string or URI prop named "href".
+ * href: function(props, propName, componentName) {
+ * var propValue = props[propName];
+ * if (propValue != null && typeof propValue !== 'string' &&
+ * !(propValue instanceof URI)) {
+ * return new Error(
+ * 'Expected a string or an URI for ' + propName + ' in ' +
+ * componentName
+ * );
+ * }
+ * }
+ * },
+ * render: function() {...}
+ * });
+ *
+ * @internal
+ */
+
+var ANONYMOUS = '<<anonymous>>';
+
+var ReactPropTypes = {
+ array: createPrimitiveTypeChecker('array'),
+ bool: createPrimitiveTypeChecker('boolean'),
+ func: createPrimitiveTypeChecker('function'),
+ number: createPrimitiveTypeChecker('number'),
+ object: createPrimitiveTypeChecker('object'),
+ string: createPrimitiveTypeChecker('string'),
+
+ any: createAnyTypeChecker(),
+ arrayOf: createArrayOfTypeChecker,
+ element: createElementTypeChecker(),
+ instanceOf: createInstanceTypeChecker,
+ node: createNodeChecker(),
+ objectOf: createObjectOfTypeChecker,
+ oneOf: createEnumTypeChecker,
+ oneOfType: createUnionTypeChecker,
+ shape: createShapeTypeChecker
+};
+
+function createChainableTypeChecker(validate) {
+ function checkType(isRequired, props, propName, componentName, location, propFullName) {
+ componentName = componentName || ANONYMOUS;
+ propFullName = propFullName || propName;
+ if (props[propName] == null) {
+ var locationName = ReactPropTypeLocationNames[location];
+ if (isRequired) {
+ return new Error('Required ' + locationName + ' `' + propFullName + '` was not specified in ' + ('`' + componentName + '`.'));
+ }
+ return null;
+ } else {
+ return validate(props, propName, componentName, location, propFullName);
+ }
+ }
+
+ var chainedCheckType = checkType.bind(null, false);
+ chainedCheckType.isRequired = checkType.bind(null, true);
+
+ return chainedCheckType;
+}
+
+function createPrimitiveTypeChecker(expectedType) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== expectedType) {
+ var locationName = ReactPropTypeLocationNames[location];
+ // `propValue` being instance of, say, date/regexp, pass the 'object'
+ // check, but we can offer a more precise error message here rather than
+ // 'of type `object`'.
+ var preciseType = getPreciseType(propValue);
+
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + preciseType + '` supplied to `' + componentName + '`, expected ') + ('`' + expectedType + '`.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createAnyTypeChecker() {
+ return createChainableTypeChecker(emptyFunction.thatReturns(null));
+}
+
+function createArrayOfTypeChecker(typeChecker) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ if (!Array.isArray(propValue)) {
+ var locationName = ReactPropTypeLocationNames[location];
+ var propType = getPropType(propValue);
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an array.'));
+ }
+ for (var i = 0; i < propValue.length; i++) {
+ var error = typeChecker(propValue, i, componentName, location, propFullName + '[' + i + ']');
+ if (error instanceof Error) {
+ return error;
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createElementTypeChecker() {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!ReactElement.isValidElement(props[propName])) {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a single ReactElement.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createInstanceTypeChecker(expectedClass) {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!(props[propName] instanceof expectedClass)) {
+ var locationName = ReactPropTypeLocationNames[location];
+ var expectedClassName = expectedClass.name || ANONYMOUS;
+ var actualClassName = getClassName(props[propName]);
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + actualClassName + '` supplied to `' + componentName + '`, expected ') + ('instance of `' + expectedClassName + '`.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createEnumTypeChecker(expectedValues) {
+ if (!Array.isArray(expectedValues)) {
+ return createChainableTypeChecker(function () {
+ return new Error('Invalid argument supplied to oneOf, expected an instance of array.');
+ });
+ }
+
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ for (var i = 0; i < expectedValues.length; i++) {
+ if (propValue === expectedValues[i]) {
+ return null;
+ }
+ }
+
+ var locationName = ReactPropTypeLocationNames[location];
+ var valuesString = JSON.stringify(expectedValues);
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of value `' + propValue + '` ' + ('supplied to `' + componentName + '`, expected one of ' + valuesString + '.'));
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createObjectOfTypeChecker(typeChecker) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== 'object') {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an object.'));
+ }
+ for (var key in propValue) {
+ if (propValue.hasOwnProperty(key)) {
+ var error = typeChecker(propValue, key, componentName, location, propFullName + '.' + key);
+ if (error instanceof Error) {
+ return error;
+ }
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createUnionTypeChecker(arrayOfTypeCheckers) {
+ if (!Array.isArray(arrayOfTypeCheckers)) {
+ return createChainableTypeChecker(function () {
+ return new Error('Invalid argument supplied to oneOfType, expected an instance of array.');
+ });
+ }
+
+ function validate(props, propName, componentName, location, propFullName) {
+ for (var i = 0; i < arrayOfTypeCheckers.length; i++) {
+ var checker = arrayOfTypeCheckers[i];
+ if (checker(props, propName, componentName, location, propFullName) == null) {
+ return null;
+ }
+ }
+
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`.'));
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createNodeChecker() {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!isNode(props[propName])) {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a ReactNode.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function createShapeTypeChecker(shapeTypes) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== 'object') {
+ var locationName = ReactPropTypeLocationNames[location];
+ return new Error('Invalid ' + locationName + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.'));
+ }
+ for (var key in shapeTypes) {
+ var checker = shapeTypes[key];
+ if (!checker) {
+ continue;
+ }
+ var error = checker(propValue, key, componentName, location, propFullName + '.' + key);
+ if (error) {
+ return error;
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+}
+
+function isNode(propValue) {
+ switch (typeof propValue) {
+ case 'number':
+ case 'string':
+ case 'undefined':
+ return true;
+ case 'boolean':
+ return !propValue;
+ case 'object':
+ if (Array.isArray(propValue)) {
+ return propValue.every(isNode);
+ }
+ if (propValue === null || ReactElement.isValidElement(propValue)) {
+ return true;
+ }
+
+ var iteratorFn = getIteratorFn(propValue);
+ if (iteratorFn) {
+ var iterator = iteratorFn.call(propValue);
+ var step;
+ if (iteratorFn !== propValue.entries) {
+ while (!(step = iterator.next()).done) {
+ if (!isNode(step.value)) {
+ return false;
+ }
+ }
+ } else {
+ // Iterator will provide entry [k,v] tuples rather than values.
+ while (!(step = iterator.next()).done) {
+ var entry = step.value;
+ if (entry) {
+ if (!isNode(entry[1])) {
+ return false;
+ }
+ }
+ }
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Equivalent of `typeof` but with special handling for array and regexp.
+function getPropType(propValue) {
+ var propType = typeof propValue;
+ if (Array.isArray(propValue)) {
+ return 'array';
+ }
+ if (propValue instanceof RegExp) {
+ // Old webkits (at least until Android 4.0) return 'function' rather than
+ // 'object' for typeof a RegExp. We'll normalize this here so that /bla/
+ // passes PropTypes.object.
+ return 'object';
+ }
+ return propType;
+}
+
+// This handles more types than `getPropType`. Only used for error messages.
+// See `createPrimitiveTypeChecker`.
+function getPreciseType(propValue) {
+ var propType = getPropType(propValue);
+ if (propType === 'object') {
+ if (propValue instanceof Date) {
+ return 'date';
+ } else if (propValue instanceof RegExp) {
+ return 'regexp';
+ }
+ }
+ return propType;
+}
+
+// Returns class name of the object, if any.
+function getClassName(propValue) {
+ if (!propValue.constructor || !propValue.constructor.name) {
+ return '<<anonymous>>';
+ }
+ return propValue.constructor.name;
+}
+
+module.exports = ReactPropTypes;
+},{"129":129,"153":153,"57":57,"80":80}],83:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactReconcileTransaction
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var CallbackQueue = _dereq_(6);
+var PooledClass = _dereq_(25);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactDOMFeatureFlags = _dereq_(44);
+var ReactInputSelection = _dereq_(66);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+
+/**
+ * Ensures that, when possible, the selection range (currently selected text
+ * input) is not disturbed by performing the transaction.
+ */
+var SELECTION_RESTORATION = {
+ /**
+ * @return {Selection} Selection information.
+ */
+ initialize: ReactInputSelection.getSelectionInformation,
+ /**
+ * @param {Selection} sel Selection information returned from `initialize`.
+ */
+ close: ReactInputSelection.restoreSelection
+};
+
+/**
+ * Suppresses events (blur/focus) that could be inadvertently dispatched due to
+ * high level DOM manipulations (like temporarily removing a text input from the
+ * DOM).
+ */
+var EVENT_SUPPRESSION = {
+ /**
+ * @return {boolean} The enabled status of `ReactBrowserEventEmitter` before
+ * the reconciliation.
+ */
+ initialize: function () {
+ var currentlyEnabled = ReactBrowserEventEmitter.isEnabled();
+ ReactBrowserEventEmitter.setEnabled(false);
+ return currentlyEnabled;
+ },
+
+ /**
+ * @param {boolean} previouslyEnabled Enabled status of
+ * `ReactBrowserEventEmitter` before the reconciliation occurred. `close`
+ * restores the previous value.
+ */
+ close: function (previouslyEnabled) {
+ ReactBrowserEventEmitter.setEnabled(previouslyEnabled);
+ }
+};
+
+/**
+ * Provides a queue for collecting `componentDidMount` and
+ * `componentDidUpdate` callbacks during the the transaction.
+ */
+var ON_DOM_READY_QUEUEING = {
+ /**
+ * Initializes the internal `onDOMReady` queue.
+ */
+ initialize: function () {
+ this.reactMountReady.reset();
+ },
+
+ /**
+ * After DOM is flushed, invoke all registered `onDOMReady` callbacks.
+ */
+ close: function () {
+ this.reactMountReady.notifyAll();
+ }
+};
+
+/**
+ * Executed within the scope of the `Transaction` instance. Consider these as
+ * being member methods, but with an implied ordering while being isolated from
+ * each other.
+ */
+var TRANSACTION_WRAPPERS = [SELECTION_RESTORATION, EVENT_SUPPRESSION, ON_DOM_READY_QUEUEING];
+
+/**
+ * Currently:
+ * - The order that these are listed in the transaction is critical:
+ * - Suppresses events.
+ * - Restores selection range.
+ *
+ * Future:
+ * - Restore document/overflow scroll positions that were unintentionally
+ * modified via DOM insertions above the top viewport boundary.
+ * - Implement/integrate with customized constraint based layout system and keep
+ * track of which dimensions must be remeasured.
+ *
+ * @class ReactReconcileTransaction
+ */
+function ReactReconcileTransaction(forceHTML) {
+ this.reinitializeTransaction();
+ // Only server-side rendering really needs this option (see
+ // `ReactServerRendering`), but server-side uses
+ // `ReactServerRenderingTransaction` instead. This option is here so that it's
+ // accessible and defaults to false when `ReactDOMComponent` and
+ // `ReactTextComponent` checks it in `mountComponent`.`
+ this.renderToStaticMarkup = false;
+ this.reactMountReady = CallbackQueue.getPooled(null);
+ this.useCreateElement = !forceHTML && ReactDOMFeatureFlags.useCreateElement;
+}
+
+var Mixin = {
+ /**
+ * @see Transaction
+ * @abstract
+ * @final
+ * @return {array<object>} List of operation wrap procedures.
+ * TODO: convert to array<TransactionWrapper>
+ */
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ },
+
+ /**
+ * @return {object} The queue to collect `onDOMReady` callbacks with.
+ */
+ getReactMountReady: function () {
+ return this.reactMountReady;
+ },
+
+ /**
+ * `PooledClass` looks for this, and will invoke this before allowing this
+ * instance to be reused.
+ */
+ destructor: function () {
+ CallbackQueue.release(this.reactMountReady);
+ this.reactMountReady = null;
+ }
+};
+
+assign(ReactReconcileTransaction.prototype, Transaction.Mixin, Mixin);
+
+PooledClass.addPoolingTo(ReactReconcileTransaction);
+
+module.exports = ReactReconcileTransaction;
+},{"113":113,"24":24,"25":25,"28":28,"44":44,"6":6,"66":66}],84:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactReconciler
+ */
+
+'use strict';
+
+var ReactRef = _dereq_(85);
+
+/**
+ * Helper to call ReactRef.attachRefs with this composite component, split out
+ * to avoid allocations in the transaction mount-ready queue.
+ */
+function attachRefs() {
+ ReactRef.attachRefs(this, this._currentElement);
+}
+
+var ReactReconciler = {
+
+ /**
+ * Initializes the component, renders markup, and registers event listeners.
+ *
+ * @param {ReactComponent} internalInstance
+ * @param {string} rootID DOM ID of the root node.
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
+ * @return {?string} Rendered markup to be inserted into the DOM.
+ * @final
+ * @internal
+ */
+ mountComponent: function (internalInstance, rootID, transaction, context) {
+ var markup = internalInstance.mountComponent(rootID, transaction, context);
+ if (internalInstance._currentElement && internalInstance._currentElement.ref != null) {
+ transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
+ }
+ return markup;
+ },
+
+ /**
+ * Releases any resources allocated by `mountComponent`.
+ *
+ * @final
+ * @internal
+ */
+ unmountComponent: function (internalInstance) {
+ ReactRef.detachRefs(internalInstance, internalInstance._currentElement);
+ internalInstance.unmountComponent();
+ },
+
+ /**
+ * Update a component using a new element.
+ *
+ * @param {ReactComponent} internalInstance
+ * @param {ReactElement} nextElement
+ * @param {ReactReconcileTransaction} transaction
+ * @param {object} context
+ * @internal
+ */
+ receiveComponent: function (internalInstance, nextElement, transaction, context) {
+ var prevElement = internalInstance._currentElement;
+
+ if (nextElement === prevElement && context === internalInstance._context) {
+ // Since elements are immutable after the owner is rendered,
+ // we can do a cheap identity compare here to determine if this is a
+ // superfluous reconcile. It's possible for state to be mutable but such
+ // change should trigger an update of the owner which would recreate
+ // the element. We explicitly check for the existence of an owner since
+ // it's possible for an element created outside a composite to be
+ // deeply mutated and reused.
+
+ // TODO: Bailing out early is just a perf optimization right?
+ // TODO: Removing the return statement should affect correctness?
+ return;
+ }
+
+ var refsChanged = ReactRef.shouldUpdateRefs(prevElement, nextElement);
+
+ if (refsChanged) {
+ ReactRef.detachRefs(internalInstance, prevElement);
+ }
+
+ internalInstance.receiveComponent(nextElement, transaction, context);
+
+ if (refsChanged && internalInstance._currentElement && internalInstance._currentElement.ref != null) {
+ transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
+ }
+ },
+
+ /**
+ * Flush any dirty changes in a component.
+ *
+ * @param {ReactComponent} internalInstance
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ performUpdateIfNecessary: function (internalInstance, transaction) {
+ internalInstance.performUpdateIfNecessary(transaction);
+ }
+
+};
+
+module.exports = ReactReconciler;
+},{"85":85}],85:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactRef
+ */
+
+'use strict';
+
+var ReactOwner = _dereq_(77);
+
+var ReactRef = {};
+
+function attachRef(ref, component, owner) {
+ if (typeof ref === 'function') {
+ ref(component.getPublicInstance());
+ } else {
+ // Legacy ref
+ ReactOwner.addComponentAsRefTo(component, ref, owner);
+ }
+}
+
+function detachRef(ref, component, owner) {
+ if (typeof ref === 'function') {
+ ref(null);
+ } else {
+ // Legacy ref
+ ReactOwner.removeComponentAsRefFrom(component, ref, owner);
+ }
+}
+
+ReactRef.attachRefs = function (instance, element) {
+ if (element === null || element === false) {
+ return;
+ }
+ var ref = element.ref;
+ if (ref != null) {
+ attachRef(ref, instance, element._owner);
+ }
+};
+
+ReactRef.shouldUpdateRefs = function (prevElement, nextElement) {
+ // If either the owner or a `ref` has changed, make sure the newest owner
+ // has stored a reference to `this`, and the previous owner (if different)
+ // has forgotten the reference to `this`. We use the element instead
+ // of the public this.props because the post processing cannot determine
+ // a ref. The ref conceptually lives on the element.
+
+ // TODO: Should this even be possible? The owner cannot change because
+ // it's forbidden by shouldUpdateReactComponent. The ref can change
+ // if you swap the keys of but not the refs. Reconsider where this check
+ // is made. It probably belongs where the key checking and
+ // instantiateReactComponent is done.
+
+ var prevEmpty = prevElement === null || prevElement === false;
+ var nextEmpty = nextElement === null || nextElement === false;
+
+ return(
+ // This has a few false positives w/r/t empty components.
+ prevEmpty || nextEmpty || nextElement._owner !== prevElement._owner || nextElement.ref !== prevElement.ref
+ );
+};
+
+ReactRef.detachRefs = function (instance, element) {
+ if (element === null || element === false) {
+ return;
+ }
+ var ref = element.ref;
+ if (ref != null) {
+ detachRef(ref, instance, element._owner);
+ }
+};
+
+module.exports = ReactRef;
+},{"77":77}],86:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactRootIndex
+ * @typechecks
+ */
+
+'use strict';
+
+var ReactRootIndexInjection = {
+ /**
+ * @param {function} _createReactRootIndex
+ */
+ injectCreateReactRootIndex: function (_createReactRootIndex) {
+ ReactRootIndex.createReactRootIndex = _createReactRootIndex;
+ }
+};
+
+var ReactRootIndex = {
+ createReactRootIndex: null,
+ injection: ReactRootIndexInjection
+};
+
+module.exports = ReactRootIndex;
+},{}],87:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactServerBatchingStrategy
+ * @typechecks
+ */
+
+'use strict';
+
+var ReactServerBatchingStrategy = {
+ isBatchingUpdates: false,
+ batchedUpdates: function (callback) {
+ // Don't do anything here. During the server rendering we don't want to
+ // schedule any updates. We will simply ignore them.
+ }
+};
+
+module.exports = ReactServerBatchingStrategy;
+},{}],88:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks static-only
+ * @providesModule ReactServerRendering
+ */
+'use strict';
+
+var ReactDefaultBatchingStrategy = _dereq_(53);
+var ReactElement = _dereq_(57);
+var ReactInstanceHandles = _dereq_(67);
+var ReactMarkupChecksum = _dereq_(71);
+var ReactServerBatchingStrategy = _dereq_(87);
+var ReactServerRenderingTransaction = _dereq_(89);
+var ReactUpdates = _dereq_(96);
+
+var emptyObject = _dereq_(154);
+var instantiateReactComponent = _dereq_(132);
+var invariant = _dereq_(161);
+
+/**
+ * @param {ReactElement} element
+ * @return {string} the HTML markup
+ */
+function renderToString(element) {
+ !ReactElement.isValidElement(element) ? "production" !== 'production' ? invariant(false, 'renderToString(): You must pass a valid ReactElement.') : invariant(false) : undefined;
+
+ var transaction;
+ try {
+ ReactUpdates.injection.injectBatchingStrategy(ReactServerBatchingStrategy);
+
+ var id = ReactInstanceHandles.createReactRootID();
+ transaction = ReactServerRenderingTransaction.getPooled(false);
+
+ return transaction.perform(function () {
+ var componentInstance = instantiateReactComponent(element, null);
+ var markup = componentInstance.mountComponent(id, transaction, emptyObject);
+ return ReactMarkupChecksum.addChecksumToMarkup(markup);
+ }, null);
+ } finally {
+ ReactServerRenderingTransaction.release(transaction);
+ // Revert to the DOM batching strategy since these two renderers
+ // currently share these stateful modules.
+ ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
+ }
+}
+
+/**
+ * @param {ReactElement} element
+ * @return {string} the HTML markup, without the extra React ID and checksum
+ * (for generating static pages)
+ */
+function renderToStaticMarkup(element) {
+ !ReactElement.isValidElement(element) ? "production" !== 'production' ? invariant(false, 'renderToStaticMarkup(): You must pass a valid ReactElement.') : invariant(false) : undefined;
+
+ var transaction;
+ try {
+ ReactUpdates.injection.injectBatchingStrategy(ReactServerBatchingStrategy);
+
+ var id = ReactInstanceHandles.createReactRootID();
+ transaction = ReactServerRenderingTransaction.getPooled(true);
+
+ return transaction.perform(function () {
+ var componentInstance = instantiateReactComponent(element, null);
+ return componentInstance.mountComponent(id, transaction, emptyObject);
+ }, null);
+ } finally {
+ ReactServerRenderingTransaction.release(transaction);
+ // Revert to the DOM batching strategy since these two renderers
+ // currently share these stateful modules.
+ ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
+ }
+}
+
+module.exports = {
+ renderToString: renderToString,
+ renderToStaticMarkup: renderToStaticMarkup
+};
+},{"132":132,"154":154,"161":161,"53":53,"57":57,"67":67,"71":71,"87":87,"89":89,"96":96}],89:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactServerRenderingTransaction
+ * @typechecks
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+var CallbackQueue = _dereq_(6);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+
+/**
+ * Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks
+ * during the performing of the transaction.
+ */
+var ON_DOM_READY_QUEUEING = {
+ /**
+ * Initializes the internal `onDOMReady` queue.
+ */
+ initialize: function () {
+ this.reactMountReady.reset();
+ },
+
+ close: emptyFunction
+};
+
+/**
+ * Executed within the scope of the `Transaction` instance. Consider these as
+ * being member methods, but with an implied ordering while being isolated from
+ * each other.
+ */
+var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING];
+
+/**
+ * @class ReactServerRenderingTransaction
+ * @param {boolean} renderToStaticMarkup
+ */
+function ReactServerRenderingTransaction(renderToStaticMarkup) {
+ this.reinitializeTransaction();
+ this.renderToStaticMarkup = renderToStaticMarkup;
+ this.reactMountReady = CallbackQueue.getPooled(null);
+ this.useCreateElement = false;
+}
+
+var Mixin = {
+ /**
+ * @see Transaction
+ * @abstract
+ * @final
+ * @return {array} Empty list of operation wrap procedures.
+ */
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ },
+
+ /**
+ * @return {object} The queue to collect `onDOMReady` callbacks with.
+ */
+ getReactMountReady: function () {
+ return this.reactMountReady;
+ },
+
+ /**
+ * `PooledClass` looks for this, and will invoke this before allowing this
+ * instance to be reused.
+ */
+ destructor: function () {
+ CallbackQueue.release(this.reactMountReady);
+ this.reactMountReady = null;
+ }
+};
+
+assign(ReactServerRenderingTransaction.prototype, Transaction.Mixin, Mixin);
+
+PooledClass.addPoolingTo(ReactServerRenderingTransaction);
+
+module.exports = ReactServerRenderingTransaction;
+},{"113":113,"153":153,"24":24,"25":25,"6":6}],90:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactStateSetters
+ */
+
+'use strict';
+
+var ReactStateSetters = {
+ /**
+ * Returns a function that calls the provided function, and uses the result
+ * of that to set the component's state.
+ *
+ * @param {ReactCompositeComponent} component
+ * @param {function} funcReturningState Returned callback uses this to
+ * determine how to update state.
+ * @return {function} callback that when invoked uses funcReturningState to
+ * determined the object literal to setState.
+ */
+ createStateSetter: function (component, funcReturningState) {
+ return function (a, b, c, d, e, f) {
+ var partialState = funcReturningState.call(component, a, b, c, d, e, f);
+ if (partialState) {
+ component.setState(partialState);
+ }
+ };
+ },
+
+ /**
+ * Returns a single-argument callback that can be used to update a single
+ * key in the component's state.
+ *
+ * Note: this is memoized function, which makes it inexpensive to call.
+ *
+ * @param {ReactCompositeComponent} component
+ * @param {string} key The key in the state that you should update.
+ * @return {function} callback of 1 argument which calls setState() with
+ * the provided keyName and callback argument.
+ */
+ createStateKeySetter: function (component, key) {
+ // Memoize the setters.
+ var cache = component.__keySetters || (component.__keySetters = {});
+ return cache[key] || (cache[key] = createStateKeySetter(component, key));
+ }
+};
+
+function createStateKeySetter(component, key) {
+ // Partial state is allocated outside of the function closure so it can be
+ // reused with every call, avoiding memory allocation when this function
+ // is called.
+ var partialState = {};
+ return function stateKeySetter(value) {
+ partialState[key] = value;
+ component.setState(partialState);
+ };
+}
+
+ReactStateSetters.Mixin = {
+ /**
+ * Returns a function that calls the provided function, and uses the result
+ * of that to set the component's state.
+ *
+ * For example, these statements are equivalent:
+ *
+ * this.setState({x: 1});
+ * this.createStateSetter(function(xValue) {
+ * return {x: xValue};
+ * })(1);
+ *
+ * @param {function} funcReturningState Returned callback uses this to
+ * determine how to update state.
+ * @return {function} callback that when invoked uses funcReturningState to
+ * determined the object literal to setState.
+ */
+ createStateSetter: function (funcReturningState) {
+ return ReactStateSetters.createStateSetter(this, funcReturningState);
+ },
+
+ /**
+ * Returns a single-argument callback that can be used to update a single
+ * key in the component's state.
+ *
+ * For example, these statements are equivalent:
+ *
+ * this.setState({x: 1});
+ * this.createStateKeySetter('x')(1);
+ *
+ * Note: this is memoized function, which makes it inexpensive to call.
+ *
+ * @param {string} key The key in the state that you should update.
+ * @return {function} callback of 1 argument which calls setState() with
+ * the provided keyName and callback argument.
+ */
+ createStateKeySetter: function (key) {
+ return ReactStateSetters.createStateKeySetter(this, key);
+ }
+};
+
+module.exports = ReactStateSetters;
+},{}],91:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactTestUtils
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPluginHub = _dereq_(16);
+var EventPropagators = _dereq_(19);
+var React = _dereq_(26);
+var ReactDOM = _dereq_(40);
+var ReactElement = _dereq_(57);
+var ReactBrowserEventEmitter = _dereq_(28);
+var ReactCompositeComponent = _dereq_(38);
+var ReactInstanceHandles = _dereq_(67);
+var ReactInstanceMap = _dereq_(68);
+var ReactMount = _dereq_(72);
+var ReactUpdates = _dereq_(96);
+var SyntheticEvent = _dereq_(105);
+
+var assign = _dereq_(24);
+var emptyObject = _dereq_(154);
+var findDOMNode = _dereq_(122);
+var invariant = _dereq_(161);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+function Event(suffix) {}
+
+/**
+ * @class ReactTestUtils
+ */
+
+function findAllInRenderedTreeInternal(inst, test) {
+ if (!inst || !inst.getPublicInstance) {
+ return [];
+ }
+ var publicInst = inst.getPublicInstance();
+ var ret = test(publicInst) ? [publicInst] : [];
+ var currentElement = inst._currentElement;
+ if (ReactTestUtils.isDOMComponent(publicInst)) {
+ var renderedChildren = inst._renderedChildren;
+ var key;
+ for (key in renderedChildren) {
+ if (!renderedChildren.hasOwnProperty(key)) {
+ continue;
+ }
+ ret = ret.concat(findAllInRenderedTreeInternal(renderedChildren[key], test));
+ }
+ } else if (ReactElement.isValidElement(currentElement) && typeof currentElement.type === 'function') {
+ ret = ret.concat(findAllInRenderedTreeInternal(inst._renderedComponent, test));
+ }
+ return ret;
+}
+
+/**
+ * Todo: Support the entire DOM.scry query syntax. For now, these simple
+ * utilities will suffice for testing purposes.
+ * @lends ReactTestUtils
+ */
+var ReactTestUtils = {
+ renderIntoDocument: function (instance) {
+ var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ // None of our tests actually require attaching the container to the
+ // DOM, and doing so creates a mess that we rely on test isolation to
+ // clean up, so we're going to stop honoring the name of this method
+ // (and probably rename it eventually) if no problems arise.
+ // document.documentElement.appendChild(div);
+ return ReactDOM.render(instance, div);
+ },
+
+ isElement: function (element) {
+ return ReactElement.isValidElement(element);
+ },
+
+ isElementOfType: function (inst, convenienceConstructor) {
+ return ReactElement.isValidElement(inst) && inst.type === convenienceConstructor;
+ },
+
+ isDOMComponent: function (inst) {
+ return !!(inst && inst.nodeType === 1 && inst.tagName);
+ },
+
+ isDOMComponentElement: function (inst) {
+ return !!(inst && ReactElement.isValidElement(inst) && !!inst.tagName);
+ },
+
+ isCompositeComponent: function (inst) {
+ if (ReactTestUtils.isDOMComponent(inst)) {
+ // Accessing inst.setState warns; just return false as that'll be what
+ // this returns when we have DOM nodes as refs directly
+ return false;
+ }
+ return inst != null && typeof inst.render === 'function' && typeof inst.setState === 'function';
+ },
+
+ isCompositeComponentWithType: function (inst, type) {
+ if (!ReactTestUtils.isCompositeComponent(inst)) {
+ return false;
+ }
+ var internalInstance = ReactInstanceMap.get(inst);
+ var constructor = internalInstance._currentElement.type;
+
+ return constructor === type;
+ },
+
+ isCompositeComponentElement: function (inst) {
+ if (!ReactElement.isValidElement(inst)) {
+ return false;
+ }
+ // We check the prototype of the type that will get mounted, not the
+ // instance itself. This is a future proof way of duck typing.
+ var prototype = inst.type.prototype;
+ return typeof prototype.render === 'function' && typeof prototype.setState === 'function';
+ },
+
+ isCompositeComponentElementWithType: function (inst, type) {
+ var internalInstance = ReactInstanceMap.get(inst);
+ var constructor = internalInstance._currentElement.type;
+
+ return !!(ReactTestUtils.isCompositeComponentElement(inst) && constructor === type);
+ },
+
+ getRenderedChildOfCompositeComponent: function (inst) {
+ if (!ReactTestUtils.isCompositeComponent(inst)) {
+ return null;
+ }
+ var internalInstance = ReactInstanceMap.get(inst);
+ return internalInstance._renderedComponent.getPublicInstance();
+ },
+
+ findAllInRenderedTree: function (inst, test) {
+ if (!inst) {
+ return [];
+ }
+ !ReactTestUtils.isCompositeComponent(inst) ? "production" !== 'production' ? invariant(false, 'findAllInRenderedTree(...): instance must be a composite component') : invariant(false) : undefined;
+ return findAllInRenderedTreeInternal(ReactInstanceMap.get(inst), test);
+ },
+
+ /**
+ * Finds all instance of components in the rendered tree that are DOM
+ * components with the class name matching `className`.
+ * @return {array} an array of all the matches.
+ */
+ scryRenderedDOMComponentsWithClass: function (root, classNames) {
+ if (!Array.isArray(classNames)) {
+ classNames = classNames.split(/\s+/);
+ }
+ return ReactTestUtils.findAllInRenderedTree(root, function (inst) {
+ if (ReactTestUtils.isDOMComponent(inst)) {
+ var className = inst.className;
+ if (typeof className !== 'string') {
+ // SVG, probably.
+ className = inst.getAttribute('class') || '';
+ }
+ var classList = className.split(/\s+/);
+ return classNames.every(function (name) {
+ return classList.indexOf(name) !== -1;
+ });
+ }
+ return false;
+ });
+ },
+
+ /**
+ * Like scryRenderedDOMComponentsWithClass but expects there to be one result,
+ * and returns that one result, or throws exception if there is any other
+ * number of matches besides one.
+ * @return {!ReactDOMComponent} The one match.
+ */
+ findRenderedDOMComponentWithClass: function (root, className) {
+ var all = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, className);
+ if (all.length !== 1) {
+ throw new Error('Did not find exactly one match ' + '(found: ' + all.length + ') for class:' + className);
+ }
+ return all[0];
+ },
+
+ /**
+ * Finds all instance of components in the rendered tree that are DOM
+ * components with the tag name matching `tagName`.
+ * @return {array} an array of all the matches.
+ */
+ scryRenderedDOMComponentsWithTag: function (root, tagName) {
+ return ReactTestUtils.findAllInRenderedTree(root, function (inst) {
+ return ReactTestUtils.isDOMComponent(inst) && inst.tagName.toUpperCase() === tagName.toUpperCase();
+ });
+ },
+
+ /**
+ * Like scryRenderedDOMComponentsWithTag but expects there to be one result,
+ * and returns that one result, or throws exception if there is any other
+ * number of matches besides one.
+ * @return {!ReactDOMComponent} The one match.
+ */
+ findRenderedDOMComponentWithTag: function (root, tagName) {
+ var all = ReactTestUtils.scryRenderedDOMComponentsWithTag(root, tagName);
+ if (all.length !== 1) {
+ throw new Error('Did not find exactly one match for tag:' + tagName);
+ }
+ return all[0];
+ },
+
+ /**
+ * Finds all instances of components with type equal to `componentType`.
+ * @return {array} an array of all the matches.
+ */
+ scryRenderedComponentsWithType: function (root, componentType) {
+ return ReactTestUtils.findAllInRenderedTree(root, function (inst) {
+ return ReactTestUtils.isCompositeComponentWithType(inst, componentType);
+ });
+ },
+
+ /**
+ * Same as `scryRenderedComponentsWithType` but expects there to be one result
+ * and returns that one result, or throws exception if there is any other
+ * number of matches besides one.
+ * @return {!ReactComponent} The one match.
+ */
+ findRenderedComponentWithType: function (root, componentType) {
+ var all = ReactTestUtils.scryRenderedComponentsWithType(root, componentType);
+ if (all.length !== 1) {
+ throw new Error('Did not find exactly one match for componentType:' + componentType + ' (found ' + all.length + ')');
+ }
+ return all[0];
+ },
+
+ /**
+ * Pass a mocked component module to this method to augment it with
+ * useful methods that allow it to be used as a dummy React component.
+ * Instead of rendering as usual, the component will become a simple
+ * <div> containing any provided children.
+ *
+ * @param {object} module the mock function object exported from a
+ * module that defines the component to be mocked
+ * @param {?string} mockTagName optional dummy root tag name to return
+ * from render method (overrides
+ * module.mockTagName if provided)
+ * @return {object} the ReactTestUtils object (for chaining)
+ */
+ mockComponent: function (module, mockTagName) {
+ mockTagName = mockTagName || module.mockTagName || 'div';
+
+ module.prototype.render.mockImplementation(function () {
+ return React.createElement(mockTagName, null, this.props.children);
+ });
+
+ return this;
+ },
+
+ /**
+ * Simulates a top level event being dispatched from a raw event that occurred
+ * on an `Element` node.
+ * @param {Object} topLevelType A type from `EventConstants.topLevelTypes`
+ * @param {!Element} node The dom to simulate an event occurring on.
+ * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent.
+ */
+ simulateNativeEventOnNode: function (topLevelType, node, fakeNativeEvent) {
+ fakeNativeEvent.target = node;
+ ReactBrowserEventEmitter.ReactEventListener.dispatchEvent(topLevelType, fakeNativeEvent);
+ },
+
+ /**
+ * Simulates a top level event being dispatched from a raw event that occurred
+ * on the `ReactDOMComponent` `comp`.
+ * @param {Object} topLevelType A type from `EventConstants.topLevelTypes`.
+ * @param {!ReactDOMComponent} comp
+ * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent.
+ */
+ simulateNativeEventOnDOMComponent: function (topLevelType, comp, fakeNativeEvent) {
+ ReactTestUtils.simulateNativeEventOnNode(topLevelType, findDOMNode(comp), fakeNativeEvent);
+ },
+
+ nativeTouchData: function (x, y) {
+ return {
+ touches: [{ pageX: x, pageY: y }]
+ };
+ },
+
+ createRenderer: function () {
+ return new ReactShallowRenderer();
+ },
+
+ Simulate: null,
+ SimulateNative: {}
+};
+
+/**
+ * @class ReactShallowRenderer
+ */
+var ReactShallowRenderer = function () {
+ this._instance = null;
+};
+
+ReactShallowRenderer.prototype.getRenderOutput = function () {
+ return this._instance && this._instance._renderedComponent && this._instance._renderedComponent._renderedOutput || null;
+};
+
+var NoopInternalComponent = function (element) {
+ this._renderedOutput = element;
+ this._currentElement = element;
+};
+
+NoopInternalComponent.prototype = {
+
+ mountComponent: function () {},
+
+ receiveComponent: function (element) {
+ this._renderedOutput = element;
+ this._currentElement = element;
+ },
+
+ unmountComponent: function () {},
+
+ getPublicInstance: function () {
+ return null;
+ }
+};
+
+var ShallowComponentWrapper = function () {};
+assign(ShallowComponentWrapper.prototype, ReactCompositeComponent.Mixin, {
+ _instantiateReactComponent: function (element) {
+ return new NoopInternalComponent(element);
+ },
+ _replaceNodeWithMarkupByID: function () {},
+ _renderValidatedComponent: ReactCompositeComponent.Mixin._renderValidatedComponentWithoutOwnerOrContext
+});
+
+ReactShallowRenderer.prototype.render = function (element, context) {
+ !ReactElement.isValidElement(element) ? "production" !== 'production' ? invariant(false, 'ReactShallowRenderer render(): Invalid component element.%s', typeof element === 'function' ? ' Instead of passing a component class, make sure to instantiate ' + 'it by passing it to React.createElement.' : '') : invariant(false) : undefined;
+ !(typeof element.type !== 'string') ? "production" !== 'production' ? invariant(false, 'ReactShallowRenderer render(): Shallow rendering works only with custom ' + 'components, not primitives (%s). Instead of calling `.render(el)` and ' + 'inspecting the rendered output, look at `el.props` directly instead.', element.type) : invariant(false) : undefined;
+
+ if (!context) {
+ context = emptyObject;
+ }
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(false);
+ this._render(element, transaction, context);
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
+};
+
+ReactShallowRenderer.prototype.unmount = function () {
+ if (this._instance) {
+ this._instance.unmountComponent();
+ }
+};
+
+ReactShallowRenderer.prototype._render = function (element, transaction, context) {
+ if (this._instance) {
+ this._instance.receiveComponent(element, transaction, context);
+ } else {
+ var rootID = ReactInstanceHandles.createReactRootID();
+ var instance = new ShallowComponentWrapper(element.type);
+ instance.construct(element);
+
+ instance.mountComponent(rootID, transaction, context);
+
+ this._instance = instance;
+ }
+};
+
+/**
+ * Exports:
+ *
+ * - `ReactTestUtils.Simulate.click(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.Simulate.mouseMove(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.Simulate.change(Element/ReactDOMComponent)`
+ * - ... (All keys from event plugin `eventTypes` objects)
+ */
+function makeSimulator(eventType) {
+ return function (domComponentOrNode, eventData) {
+ var node;
+ if (ReactTestUtils.isDOMComponent(domComponentOrNode)) {
+ node = findDOMNode(domComponentOrNode);
+ } else if (domComponentOrNode.tagName) {
+ node = domComponentOrNode;
+ }
+
+ var dispatchConfig = ReactBrowserEventEmitter.eventNameDispatchConfigs[eventType];
+
+ var fakeNativeEvent = new Event();
+ fakeNativeEvent.target = node;
+ // We don't use SyntheticEvent.getPooled in order to not have to worry about
+ // properly destroying any properties assigned from `eventData` upon release
+ var event = new SyntheticEvent(dispatchConfig, ReactMount.getID(node), fakeNativeEvent, node);
+ assign(event, eventData);
+
+ if (dispatchConfig.phasedRegistrationNames) {
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ } else {
+ EventPropagators.accumulateDirectDispatches(event);
+ }
+
+ ReactUpdates.batchedUpdates(function () {
+ EventPluginHub.enqueueEvents(event);
+ EventPluginHub.processEventQueue(true);
+ });
+ };
+}
+
+function buildSimulators() {
+ ReactTestUtils.Simulate = {};
+
+ var eventType;
+ for (eventType in ReactBrowserEventEmitter.eventNameDispatchConfigs) {
+ /**
+ * @param {!Element|ReactDOMComponent} domComponentOrNode
+ * @param {?object} eventData Fake event data to use in SyntheticEvent.
+ */
+ ReactTestUtils.Simulate[eventType] = makeSimulator(eventType);
+ }
+}
+
+// Rebuild ReactTestUtils.Simulate whenever event plugins are injected
+var oldInjectEventPluginOrder = EventPluginHub.injection.injectEventPluginOrder;
+EventPluginHub.injection.injectEventPluginOrder = function () {
+ oldInjectEventPluginOrder.apply(this, arguments);
+ buildSimulators();
+};
+var oldInjectEventPlugins = EventPluginHub.injection.injectEventPluginsByName;
+EventPluginHub.injection.injectEventPluginsByName = function () {
+ oldInjectEventPlugins.apply(this, arguments);
+ buildSimulators();
+};
+
+buildSimulators();
+
+/**
+ * Exports:
+ *
+ * - `ReactTestUtils.SimulateNative.click(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.SimulateNative.mouseMove(Element/ReactDOMComponent)`
+ * - `ReactTestUtils.SimulateNative.mouseIn/ReactDOMComponent)`
+ * - `ReactTestUtils.SimulateNative.mouseOut(Element/ReactDOMComponent)`
+ * - ... (All keys from `EventConstants.topLevelTypes`)
+ *
+ * Note: Top level event types are a subset of the entire set of handler types
+ * (which include a broader set of "synthetic" events). For example, onDragDone
+ * is a synthetic event. Except when testing an event plugin or React's event
+ * handling code specifically, you probably want to use ReactTestUtils.Simulate
+ * to dispatch synthetic events.
+ */
+
+function makeNativeSimulator(eventType) {
+ return function (domComponentOrNode, nativeEventData) {
+ var fakeNativeEvent = new Event(eventType);
+ assign(fakeNativeEvent, nativeEventData);
+ if (ReactTestUtils.isDOMComponent(domComponentOrNode)) {
+ ReactTestUtils.simulateNativeEventOnDOMComponent(eventType, domComponentOrNode, fakeNativeEvent);
+ } else if (domComponentOrNode.tagName) {
+ // Will allow on actual dom nodes.
+ ReactTestUtils.simulateNativeEventOnNode(eventType, domComponentOrNode, fakeNativeEvent);
+ }
+ };
+}
+
+Object.keys(topLevelTypes).forEach(function (eventType) {
+ // Event type is stored as 'topClick' - we transform that to 'click'
+ var convenienceName = eventType.indexOf('top') === 0 ? eventType.charAt(3).toLowerCase() + eventType.substr(4) : eventType;
+ /**
+ * @param {!Element|ReactDOMComponent} domComponentOrNode
+ * @param {?Event} nativeEventData Fake native event to use in SyntheticEvent.
+ */
+ ReactTestUtils.SimulateNative[convenienceName] = makeNativeSimulator(eventType);
+});
+
+module.exports = ReactTestUtils;
+},{"105":105,"122":122,"15":15,"154":154,"16":16,"161":161,"19":19,"24":24,"26":26,"28":28,"38":38,"40":40,"57":57,"67":67,"68":68,"72":72,"96":96}],92:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks static-only
+ * @providesModule ReactTransitionChildMapping
+ */
+
+'use strict';
+
+var flattenChildren = _dereq_(123);
+
+var ReactTransitionChildMapping = {
+ /**
+ * Given `this.props.children`, return an object mapping key to child. Just
+ * simple syntactic sugar around flattenChildren().
+ *
+ * @param {*} children `this.props.children`
+ * @return {object} Mapping of key to child
+ */
+ getChildMapping: function (children) {
+ if (!children) {
+ return children;
+ }
+ return flattenChildren(children);
+ },
+
+ /**
+ * When you're adding or removing children some may be added or removed in the
+ * same render pass. We want to show *both* since we want to simultaneously
+ * animate elements in and out. This function takes a previous set of keys
+ * and a new set of keys and merges them with its best guess of the correct
+ * ordering. In the future we may expose some of the utilities in
+ * ReactMultiChild to make this easy, but for now React itself does not
+ * directly have this concept of the union of prevChildren and nextChildren
+ * so we implement it here.
+ *
+ * @param {object} prev prev children as returned from
+ * `ReactTransitionChildMapping.getChildMapping()`.
+ * @param {object} next next children as returned from
+ * `ReactTransitionChildMapping.getChildMapping()`.
+ * @return {object} a key set that contains all keys in `prev` and all keys
+ * in `next` in a reasonable order.
+ */
+ mergeChildMappings: function (prev, next) {
+ prev = prev || {};
+ next = next || {};
+
+ function getValueForKey(key) {
+ if (next.hasOwnProperty(key)) {
+ return next[key];
+ } else {
+ return prev[key];
+ }
+ }
+
+ // For each key of `next`, the list of keys to insert before that key in
+ // the combined list
+ var nextKeysPending = {};
+
+ var pendingKeys = [];
+ for (var prevKey in prev) {
+ if (next.hasOwnProperty(prevKey)) {
+ if (pendingKeys.length) {
+ nextKeysPending[prevKey] = pendingKeys;
+ pendingKeys = [];
+ }
+ } else {
+ pendingKeys.push(prevKey);
+ }
+ }
+
+ var i;
+ var childMapping = {};
+ for (var nextKey in next) {
+ if (nextKeysPending.hasOwnProperty(nextKey)) {
+ for (i = 0; i < nextKeysPending[nextKey].length; i++) {
+ var pendingNextKey = nextKeysPending[nextKey][i];
+ childMapping[nextKeysPending[nextKey][i]] = getValueForKey(pendingNextKey);
+ }
+ }
+ childMapping[nextKey] = getValueForKey(nextKey);
+ }
+
+ // Finally, add the keys which didn't appear before any key in `next`
+ for (i = 0; i < pendingKeys.length; i++) {
+ childMapping[pendingKeys[i]] = getValueForKey(pendingKeys[i]);
+ }
+
+ return childMapping;
+ }
+};
+
+module.exports = ReactTransitionChildMapping;
+},{"123":123}],93:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactTransitionEvents
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+/**
+ * EVENT_NAME_MAP is used to determine which event fired when a
+ * transition/animation ends, based on the style property used to
+ * define that event.
+ */
+var EVENT_NAME_MAP = {
+ transitionend: {
+ 'transition': 'transitionend',
+ 'WebkitTransition': 'webkitTransitionEnd',
+ 'MozTransition': 'mozTransitionEnd',
+ 'OTransition': 'oTransitionEnd',
+ 'msTransition': 'MSTransitionEnd'
+ },
+
+ animationend: {
+ 'animation': 'animationend',
+ 'WebkitAnimation': 'webkitAnimationEnd',
+ 'MozAnimation': 'mozAnimationEnd',
+ 'OAnimation': 'oAnimationEnd',
+ 'msAnimation': 'MSAnimationEnd'
+ }
+};
+
+var endEvents = [];
+
+function detectEvents() {
+ var testEl = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var style = testEl.style;
+
+ // On some platforms, in particular some releases of Android 4.x,
+ // the un-prefixed "animation" and "transition" properties are defined on the
+ // style object but the events that fire will still be prefixed, so we need
+ // to check if the un-prefixed events are useable, and if not remove them
+ // from the map
+ if (!('AnimationEvent' in window)) {
+ delete EVENT_NAME_MAP.animationend.animation;
+ }
+
+ if (!('TransitionEvent' in window)) {
+ delete EVENT_NAME_MAP.transitionend.transition;
+ }
+
+ for (var baseEventName in EVENT_NAME_MAP) {
+ var baseEvents = EVENT_NAME_MAP[baseEventName];
+ for (var styleName in baseEvents) {
+ if (styleName in style) {
+ endEvents.push(baseEvents[styleName]);
+ break;
+ }
+ }
+ }
+}
+
+if (ExecutionEnvironment.canUseDOM) {
+ detectEvents();
+}
+
+// We use the raw {add|remove}EventListener() call because EventListener
+// does not know how to remove event listeners and we really should
+// clean up. Also, these events are not triggered in older browsers
+// so we should be A-OK here.
+
+function addEventListener(node, eventName, eventListener) {
+ node.addEventListener(eventName, eventListener, false);
+}
+
+function removeEventListener(node, eventName, eventListener) {
+ node.removeEventListener(eventName, eventListener, false);
+}
+
+var ReactTransitionEvents = {
+ addEndEventListener: function (node, eventListener) {
+ if (endEvents.length === 0) {
+ // If CSS transitions are not supported, trigger an "end animation"
+ // event immediately.
+ window.setTimeout(eventListener, 0);
+ return;
+ }
+ endEvents.forEach(function (endEvent) {
+ addEventListener(node, endEvent, eventListener);
+ });
+ },
+
+ removeEndEventListener: function (node, eventListener) {
+ if (endEvents.length === 0) {
+ return;
+ }
+ endEvents.forEach(function (endEvent) {
+ removeEventListener(node, endEvent, eventListener);
+ });
+ }
+};
+
+module.exports = ReactTransitionEvents;
+},{"147":147}],94:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactTransitionGroup
+ */
+
+'use strict';
+
+var React = _dereq_(26);
+var ReactTransitionChildMapping = _dereq_(92);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+
+var ReactTransitionGroup = React.createClass({
+ displayName: 'ReactTransitionGroup',
+
+ propTypes: {
+ component: React.PropTypes.any,
+ childFactory: React.PropTypes.func
+ },
+
+ getDefaultProps: function () {
+ return {
+ component: 'span',
+ childFactory: emptyFunction.thatReturnsArgument
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ children: ReactTransitionChildMapping.getChildMapping(this.props.children)
+ };
+ },
+
+ componentWillMount: function () {
+ this.currentlyTransitioningKeys = {};
+ this.keysToEnter = [];
+ this.keysToLeave = [];
+ },
+
+ componentDidMount: function () {
+ var initialChildMapping = this.state.children;
+ for (var key in initialChildMapping) {
+ if (initialChildMapping[key]) {
+ this.performAppear(key);
+ }
+ }
+ },
+
+ componentWillReceiveProps: function (nextProps) {
+ var nextChildMapping = ReactTransitionChildMapping.getChildMapping(nextProps.children);
+ var prevChildMapping = this.state.children;
+
+ this.setState({
+ children: ReactTransitionChildMapping.mergeChildMappings(prevChildMapping, nextChildMapping)
+ });
+
+ var key;
+
+ for (key in nextChildMapping) {
+ var hasPrev = prevChildMapping && prevChildMapping.hasOwnProperty(key);
+ if (nextChildMapping[key] && !hasPrev && !this.currentlyTransitioningKeys[key]) {
+ this.keysToEnter.push(key);
+ }
+ }
+
+ for (key in prevChildMapping) {
+ var hasNext = nextChildMapping && nextChildMapping.hasOwnProperty(key);
+ if (prevChildMapping[key] && !hasNext && !this.currentlyTransitioningKeys[key]) {
+ this.keysToLeave.push(key);
+ }
+ }
+
+ // If we want to someday check for reordering, we could do it here.
+ },
+
+ componentDidUpdate: function () {
+ var keysToEnter = this.keysToEnter;
+ this.keysToEnter = [];
+ keysToEnter.forEach(this.performEnter);
+
+ var keysToLeave = this.keysToLeave;
+ this.keysToLeave = [];
+ keysToLeave.forEach(this.performLeave);
+ },
+
+ performAppear: function (key) {
+ this.currentlyTransitioningKeys[key] = true;
+
+ var component = this.refs[key];
+
+ if (component.componentWillAppear) {
+ component.componentWillAppear(this._handleDoneAppearing.bind(this, key));
+ } else {
+ this._handleDoneAppearing(key);
+ }
+ },
+
+ _handleDoneAppearing: function (key) {
+ var component = this.refs[key];
+ if (component.componentDidAppear) {
+ component.componentDidAppear();
+ }
+
+ delete this.currentlyTransitioningKeys[key];
+
+ var currentChildMapping = ReactTransitionChildMapping.getChildMapping(this.props.children);
+
+ if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {
+ // This was removed before it had fully appeared. Remove it.
+ this.performLeave(key);
+ }
+ },
+
+ performEnter: function (key) {
+ this.currentlyTransitioningKeys[key] = true;
+
+ var component = this.refs[key];
+
+ if (component.componentWillEnter) {
+ component.componentWillEnter(this._handleDoneEntering.bind(this, key));
+ } else {
+ this._handleDoneEntering(key);
+ }
+ },
+
+ _handleDoneEntering: function (key) {
+ var component = this.refs[key];
+ if (component.componentDidEnter) {
+ component.componentDidEnter();
+ }
+
+ delete this.currentlyTransitioningKeys[key];
+
+ var currentChildMapping = ReactTransitionChildMapping.getChildMapping(this.props.children);
+
+ if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {
+ // This was removed before it had fully entered. Remove it.
+ this.performLeave(key);
+ }
+ },
+
+ performLeave: function (key) {
+ this.currentlyTransitioningKeys[key] = true;
+
+ var component = this.refs[key];
+ if (component.componentWillLeave) {
+ component.componentWillLeave(this._handleDoneLeaving.bind(this, key));
+ } else {
+ // Note that this is somewhat dangerous b/c it calls setState()
+ // again, effectively mutating the component before all the work
+ // is done.
+ this._handleDoneLeaving(key);
+ }
+ },
+
+ _handleDoneLeaving: function (key) {
+ var component = this.refs[key];
+
+ if (component.componentDidLeave) {
+ component.componentDidLeave();
+ }
+
+ delete this.currentlyTransitioningKeys[key];
+
+ var currentChildMapping = ReactTransitionChildMapping.getChildMapping(this.props.children);
+
+ if (currentChildMapping && currentChildMapping.hasOwnProperty(key)) {
+ // This entered again before it fully left. Add it again.
+ this.performEnter(key);
+ } else {
+ this.setState(function (state) {
+ var newChildren = assign({}, state.children);
+ delete newChildren[key];
+ return { children: newChildren };
+ });
+ }
+ },
+
+ render: function () {
+ // TODO: we could get rid of the need for the wrapper node
+ // by cloning a single child
+ var childrenToRender = [];
+ for (var key in this.state.children) {
+ var child = this.state.children[key];
+ if (child) {
+ // You may need to apply reactive updates to a child as it is leaving.
+ // The normal React way to do it won't work since the child will have
+ // already been removed. In case you need this behavior you can provide
+ // a childFactory function to wrap every child, even the ones that are
+ // leaving.
+ childrenToRender.push(React.cloneElement(this.props.childFactory(child), { ref: key, key: key }));
+ }
+ }
+ return React.createElement(this.props.component, this.props, childrenToRender);
+ }
+});
+
+module.exports = ReactTransitionGroup;
+},{"153":153,"24":24,"26":26,"92":92}],95:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactUpdateQueue
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactElement = _dereq_(57);
+var ReactInstanceMap = _dereq_(68);
+var ReactUpdates = _dereq_(96);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+function enqueueUpdate(internalInstance) {
+ ReactUpdates.enqueueUpdate(internalInstance);
+}
+
+function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
+ var internalInstance = ReactInstanceMap.get(publicInstance);
+ if (!internalInstance) {
+ if ("production" !== 'production') {
+ // Only warn when we have a callerName. Otherwise we should be silent.
+ // We're probably calling from enqueueCallback. We don't want to warn
+ // there because we already warned for the corresponding lifecycle method.
+ "production" !== 'production' ? warning(!callerName, '%s(...): Can only update a mounted or mounting component. ' + 'This usually means you called %s() on an unmounted component. ' + 'This is a no-op. Please check the code for the %s component.', callerName, callerName, publicInstance.constructor.displayName) : undefined;
+ }
+ return null;
+ }
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(ReactCurrentOwner.current == null, '%s(...): Cannot update during an existing state transition ' + '(such as within `render`). Render methods should be a pure function ' + 'of props and state.', callerName) : undefined;
+ }
+
+ return internalInstance;
+}
+
+/**
+ * ReactUpdateQueue allows for state updates to be scheduled into a later
+ * reconciliation step.
+ */
+var ReactUpdateQueue = {
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @param {ReactClass} publicInstance The instance we want to test.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function (publicInstance) {
+ if ("production" !== 'production') {
+ var owner = ReactCurrentOwner.current;
+ if (owner !== null) {
+ "production" !== 'production' ? warning(owner._warnedAboutRefsInRender, '%s is accessing isMounted inside its render() function. ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + 'componentDidUpdate instead.', owner.getName() || 'A component') : undefined;
+ owner._warnedAboutRefsInRender = true;
+ }
+ }
+ var internalInstance = ReactInstanceMap.get(publicInstance);
+ if (internalInstance) {
+ // During componentWillMount and render this will still be null but after
+ // that will always render to something. At least for now. So we can use
+ // this hack.
+ return !!internalInstance._renderedComponent;
+ } else {
+ return false;
+ }
+ },
+
+ /**
+ * Enqueue a callback that will be executed after all the pending updates
+ * have processed.
+ *
+ * @param {ReactClass} publicInstance The instance to use as `this` context.
+ * @param {?function} callback Called after state is updated.
+ * @internal
+ */
+ enqueueCallback: function (publicInstance, callback) {
+ !(typeof callback === 'function') ? "production" !== 'production' ? invariant(false, 'enqueueCallback(...): You called `setProps`, `replaceProps`, ' + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + 'isn\'t callable.') : invariant(false) : undefined;
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
+
+ // Previously we would throw an error if we didn't have an internal
+ // instance. Since we want to make it a no-op instead, we mirror the same
+ // behavior we have in other enqueue* methods.
+ // We also need to ignore callbacks in componentWillMount. See
+ // enqueueUpdates.
+ if (!internalInstance) {
+ return null;
+ }
+
+ if (internalInstance._pendingCallbacks) {
+ internalInstance._pendingCallbacks.push(callback);
+ } else {
+ internalInstance._pendingCallbacks = [callback];
+ }
+ // TODO: The callback here is ignored when setState is called from
+ // componentWillMount. Either fix it or disallow doing so completely in
+ // favor of getInitialState. Alternatively, we can disallow
+ // componentWillMount during server-side rendering.
+ enqueueUpdate(internalInstance);
+ },
+
+ enqueueCallbackInternal: function (internalInstance, callback) {
+ !(typeof callback === 'function') ? "production" !== 'production' ? invariant(false, 'enqueueCallback(...): You called `setProps`, `replaceProps`, ' + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + 'isn\'t callable.') : invariant(false) : undefined;
+ if (internalInstance._pendingCallbacks) {
+ internalInstance._pendingCallbacks.push(callback);
+ } else {
+ internalInstance._pendingCallbacks = [callback];
+ }
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldComponentUpdate`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @internal
+ */
+ enqueueForceUpdate: function (publicInstance) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'forceUpdate');
+
+ if (!internalInstance) {
+ return;
+ }
+
+ internalInstance._pendingForceUpdate = true;
+
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Replaces all of the state. Always use this or `setState` to mutate state.
+ * You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} completeState Next state.
+ * @internal
+ */
+ enqueueReplaceState: function (publicInstance, completeState) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'replaceState');
+
+ if (!internalInstance) {
+ return;
+ }
+
+ internalInstance._pendingStateQueue = [completeState];
+ internalInstance._pendingReplaceState = true;
+
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Sets a subset of the state. This only exists because _pendingState is
+ * internal. This provides a merging strategy that is not available to deep
+ * properties which is confusing. TODO: Expose pendingState or don't use it
+ * during the merge.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialState Next partial state to be merged with state.
+ * @internal
+ */
+ enqueueSetState: function (publicInstance, partialState) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
+
+ if (!internalInstance) {
+ return;
+ }
+
+ var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
+ queue.push(partialState);
+
+ enqueueUpdate(internalInstance);
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} partialProps Subset of the next props.
+ * @internal
+ */
+ enqueueSetProps: function (publicInstance, partialProps) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setProps');
+ if (!internalInstance) {
+ return;
+ }
+ ReactUpdateQueue.enqueueSetPropsInternal(internalInstance, partialProps);
+ },
+
+ enqueueSetPropsInternal: function (internalInstance, partialProps) {
+ var topLevelWrapper = internalInstance._topLevelWrapper;
+ !topLevelWrapper ? "production" !== 'production' ? invariant(false, 'setProps(...): You called `setProps` on a ' + 'component with a parent. This is an anti-pattern since props will ' + 'get reactively updated when rendered. Instead, change the owner\'s ' + '`render` method to pass the correct value as props to the component ' + 'where it is created.') : invariant(false) : undefined;
+
+ // Merge with the pending element if it exists, otherwise with existing
+ // element props.
+ var wrapElement = topLevelWrapper._pendingElement || topLevelWrapper._currentElement;
+ var element = wrapElement.props;
+ var props = assign({}, element.props, partialProps);
+ topLevelWrapper._pendingElement = ReactElement.cloneAndReplaceProps(wrapElement, ReactElement.cloneAndReplaceProps(element, props));
+
+ enqueueUpdate(topLevelWrapper);
+ },
+
+ /**
+ * Replaces all of the props.
+ *
+ * @param {ReactClass} publicInstance The instance that should rerender.
+ * @param {object} props New props.
+ * @internal
+ */
+ enqueueReplaceProps: function (publicInstance, props) {
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'replaceProps');
+ if (!internalInstance) {
+ return;
+ }
+ ReactUpdateQueue.enqueueReplacePropsInternal(internalInstance, props);
+ },
+
+ enqueueReplacePropsInternal: function (internalInstance, props) {
+ var topLevelWrapper = internalInstance._topLevelWrapper;
+ !topLevelWrapper ? "production" !== 'production' ? invariant(false, 'replaceProps(...): You called `replaceProps` on a ' + 'component with a parent. This is an anti-pattern since props will ' + 'get reactively updated when rendered. Instead, change the owner\'s ' + '`render` method to pass the correct value as props to the component ' + 'where it is created.') : invariant(false) : undefined;
+
+ // Merge with the pending element if it exists, otherwise with existing
+ // element props.
+ var wrapElement = topLevelWrapper._pendingElement || topLevelWrapper._currentElement;
+ var element = wrapElement.props;
+ topLevelWrapper._pendingElement = ReactElement.cloneAndReplaceProps(wrapElement, ReactElement.cloneAndReplaceProps(element, props));
+
+ enqueueUpdate(topLevelWrapper);
+ },
+
+ enqueueElementInternal: function (internalInstance, newElement) {
+ internalInstance._pendingElement = newElement;
+ enqueueUpdate(internalInstance);
+ }
+
+};
+
+module.exports = ReactUpdateQueue;
+},{"161":161,"173":173,"24":24,"39":39,"57":57,"68":68,"96":96}],96:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactUpdates
+ */
+
+'use strict';
+
+var CallbackQueue = _dereq_(6);
+var PooledClass = _dereq_(25);
+var ReactPerf = _dereq_(78);
+var ReactReconciler = _dereq_(84);
+var Transaction = _dereq_(113);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+
+var dirtyComponents = [];
+var asapCallbackQueue = CallbackQueue.getPooled();
+var asapEnqueued = false;
+
+var batchingStrategy = null;
+
+function ensureInjected() {
+ !(ReactUpdates.ReactReconcileTransaction && batchingStrategy) ? "production" !== 'production' ? invariant(false, 'ReactUpdates: must inject a reconcile transaction class and batching ' + 'strategy') : invariant(false) : undefined;
+}
+
+var NESTED_UPDATES = {
+ initialize: function () {
+ this.dirtyComponentsLength = dirtyComponents.length;
+ },
+ close: function () {
+ if (this.dirtyComponentsLength !== dirtyComponents.length) {
+ // Additional updates were enqueued by componentDidUpdate handlers or
+ // similar; before our own UPDATE_QUEUEING wrapper closes, we want to run
+ // these new updates so that if A's componentDidUpdate calls setState on
+ // B, B will update before the callback A's updater provided when calling
+ // setState.
+ dirtyComponents.splice(0, this.dirtyComponentsLength);
+ flushBatchedUpdates();
+ } else {
+ dirtyComponents.length = 0;
+ }
+ }
+};
+
+var UPDATE_QUEUEING = {
+ initialize: function () {
+ this.callbackQueue.reset();
+ },
+ close: function () {
+ this.callbackQueue.notifyAll();
+ }
+};
+
+var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];
+
+function ReactUpdatesFlushTransaction() {
+ this.reinitializeTransaction();
+ this.dirtyComponentsLength = null;
+ this.callbackQueue = CallbackQueue.getPooled();
+ this.reconcileTransaction = ReactUpdates.ReactReconcileTransaction.getPooled( /* forceHTML */false);
+}
+
+assign(ReactUpdatesFlushTransaction.prototype, Transaction.Mixin, {
+ getTransactionWrappers: function () {
+ return TRANSACTION_WRAPPERS;
+ },
+
+ destructor: function () {
+ this.dirtyComponentsLength = null;
+ CallbackQueue.release(this.callbackQueue);
+ this.callbackQueue = null;
+ ReactUpdates.ReactReconcileTransaction.release(this.reconcileTransaction);
+ this.reconcileTransaction = null;
+ },
+
+ perform: function (method, scope, a) {
+ // Essentially calls `this.reconcileTransaction.perform(method, scope, a)`
+ // with this transaction's wrappers around it.
+ return Transaction.Mixin.perform.call(this, this.reconcileTransaction.perform, this.reconcileTransaction, method, scope, a);
+ }
+});
+
+PooledClass.addPoolingTo(ReactUpdatesFlushTransaction);
+
+function batchedUpdates(callback, a, b, c, d, e) {
+ ensureInjected();
+ batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
+}
+
+/**
+ * Array comparator for ReactComponents by mount ordering.
+ *
+ * @param {ReactComponent} c1 first component you're comparing
+ * @param {ReactComponent} c2 second component you're comparing
+ * @return {number} Return value usable by Array.prototype.sort().
+ */
+function mountOrderComparator(c1, c2) {
+ return c1._mountOrder - c2._mountOrder;
+}
+
+function runBatchedUpdates(transaction) {
+ var len = transaction.dirtyComponentsLength;
+ !(len === dirtyComponents.length) ? "production" !== 'production' ? invariant(false, 'Expected flush transaction\'s stored dirty-components length (%s) to ' + 'match dirty-components array length (%s).', len, dirtyComponents.length) : invariant(false) : undefined;
+
+ // Since reconciling a component higher in the owner hierarchy usually (not
+ // always -- see shouldComponentUpdate()) will reconcile children, reconcile
+ // them before their children by sorting the array.
+ dirtyComponents.sort(mountOrderComparator);
+
+ for (var i = 0; i < len; i++) {
+ // If a component is unmounted before pending changes apply, it will still
+ // be here, but we assume that it has cleared its _pendingCallbacks and
+ // that performUpdateIfNecessary is a noop.
+ var component = dirtyComponents[i];
+
+ // If performUpdateIfNecessary happens to enqueue any new updates, we
+ // shouldn't execute the callbacks until the next render happens, so
+ // stash the callbacks first
+ var callbacks = component._pendingCallbacks;
+ component._pendingCallbacks = null;
+
+ ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);
+
+ if (callbacks) {
+ for (var j = 0; j < callbacks.length; j++) {
+ transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
+ }
+ }
+ }
+}
+
+var flushBatchedUpdates = function () {
+ // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
+ // array and perform any updates enqueued by mount-ready handlers (i.e.,
+ // componentDidUpdate) but we need to check here too in order to catch
+ // updates enqueued by setState callbacks and asap calls.
+ while (dirtyComponents.length || asapEnqueued) {
+ if (dirtyComponents.length) {
+ var transaction = ReactUpdatesFlushTransaction.getPooled();
+ transaction.perform(runBatchedUpdates, null, transaction);
+ ReactUpdatesFlushTransaction.release(transaction);
+ }
+
+ if (asapEnqueued) {
+ asapEnqueued = false;
+ var queue = asapCallbackQueue;
+ asapCallbackQueue = CallbackQueue.getPooled();
+ queue.notifyAll();
+ CallbackQueue.release(queue);
+ }
+ }
+};
+flushBatchedUpdates = ReactPerf.measure('ReactUpdates', 'flushBatchedUpdates', flushBatchedUpdates);
+
+/**
+ * Mark a component as needing a rerender, adding an optional callback to a
+ * list of functions which will be executed once the rerender occurs.
+ */
+function enqueueUpdate(component) {
+ ensureInjected();
+
+ // Various parts of our code (such as ReactCompositeComponent's
+ // _renderValidatedComponent) assume that calls to render aren't nested;
+ // verify that that's the case. (This is called by each top-level update
+ // function, like setProps, setState, forceUpdate, etc.; creation and
+ // destruction of top-level components is guarded in ReactMount.)
+
+ if (!batchingStrategy.isBatchingUpdates) {
+ batchingStrategy.batchedUpdates(enqueueUpdate, component);
+ return;
+ }
+
+ dirtyComponents.push(component);
+}
+
+/**
+ * Enqueue a callback to be run at the end of the current batching cycle. Throws
+ * if no updates are currently being performed.
+ */
+function asap(callback, context) {
+ !batchingStrategy.isBatchingUpdates ? "production" !== 'production' ? invariant(false, 'ReactUpdates.asap: Can\'t enqueue an asap callback in a context where' + 'updates are not being batched.') : invariant(false) : undefined;
+ asapCallbackQueue.enqueue(callback, context);
+ asapEnqueued = true;
+}
+
+var ReactUpdatesInjection = {
+ injectReconcileTransaction: function (ReconcileTransaction) {
+ !ReconcileTransaction ? "production" !== 'production' ? invariant(false, 'ReactUpdates: must provide a reconcile transaction class') : invariant(false) : undefined;
+ ReactUpdates.ReactReconcileTransaction = ReconcileTransaction;
+ },
+
+ injectBatchingStrategy: function (_batchingStrategy) {
+ !_batchingStrategy ? "production" !== 'production' ? invariant(false, 'ReactUpdates: must provide a batching strategy') : invariant(false) : undefined;
+ !(typeof _batchingStrategy.batchedUpdates === 'function') ? "production" !== 'production' ? invariant(false, 'ReactUpdates: must provide a batchedUpdates() function') : invariant(false) : undefined;
+ !(typeof _batchingStrategy.isBatchingUpdates === 'boolean') ? "production" !== 'production' ? invariant(false, 'ReactUpdates: must provide an isBatchingUpdates boolean attribute') : invariant(false) : undefined;
+ batchingStrategy = _batchingStrategy;
+ }
+};
+
+var ReactUpdates = {
+ /**
+ * React references `ReactReconcileTransaction` using this property in order
+ * to allow dependency injection.
+ *
+ * @internal
+ */
+ ReactReconcileTransaction: null,
+
+ batchedUpdates: batchedUpdates,
+ enqueueUpdate: enqueueUpdate,
+ flushBatchedUpdates: flushBatchedUpdates,
+ injection: ReactUpdatesInjection,
+ asap: asap
+};
+
+module.exports = ReactUpdates;
+},{"113":113,"161":161,"24":24,"25":25,"6":6,"78":78,"84":84}],97:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactVersion
+ */
+
+'use strict';
+
+module.exports = '0.14.6';
+},{}],98:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SVGDOMPropertyConfig
+ */
+
+'use strict';
+
+var DOMProperty = _dereq_(10);
+
+var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
+
+var NS = {
+ xlink: 'http://www.w3.org/1999/xlink',
+ xml: 'http://www.w3.org/XML/1998/namespace'
+};
+
+var SVGDOMPropertyConfig = {
+ Properties: {
+ clipPath: MUST_USE_ATTRIBUTE,
+ cx: MUST_USE_ATTRIBUTE,
+ cy: MUST_USE_ATTRIBUTE,
+ d: MUST_USE_ATTRIBUTE,
+ dx: MUST_USE_ATTRIBUTE,
+ dy: MUST_USE_ATTRIBUTE,
+ fill: MUST_USE_ATTRIBUTE,
+ fillOpacity: MUST_USE_ATTRIBUTE,
+ fontFamily: MUST_USE_ATTRIBUTE,
+ fontSize: MUST_USE_ATTRIBUTE,
+ fx: MUST_USE_ATTRIBUTE,
+ fy: MUST_USE_ATTRIBUTE,
+ gradientTransform: MUST_USE_ATTRIBUTE,
+ gradientUnits: MUST_USE_ATTRIBUTE,
+ markerEnd: MUST_USE_ATTRIBUTE,
+ markerMid: MUST_USE_ATTRIBUTE,
+ markerStart: MUST_USE_ATTRIBUTE,
+ offset: MUST_USE_ATTRIBUTE,
+ opacity: MUST_USE_ATTRIBUTE,
+ patternContentUnits: MUST_USE_ATTRIBUTE,
+ patternUnits: MUST_USE_ATTRIBUTE,
+ points: MUST_USE_ATTRIBUTE,
+ preserveAspectRatio: MUST_USE_ATTRIBUTE,
+ r: MUST_USE_ATTRIBUTE,
+ rx: MUST_USE_ATTRIBUTE,
+ ry: MUST_USE_ATTRIBUTE,
+ spreadMethod: MUST_USE_ATTRIBUTE,
+ stopColor: MUST_USE_ATTRIBUTE,
+ stopOpacity: MUST_USE_ATTRIBUTE,
+ stroke: MUST_USE_ATTRIBUTE,
+ strokeDasharray: MUST_USE_ATTRIBUTE,
+ strokeLinecap: MUST_USE_ATTRIBUTE,
+ strokeOpacity: MUST_USE_ATTRIBUTE,
+ strokeWidth: MUST_USE_ATTRIBUTE,
+ textAnchor: MUST_USE_ATTRIBUTE,
+ transform: MUST_USE_ATTRIBUTE,
+ version: MUST_USE_ATTRIBUTE,
+ viewBox: MUST_USE_ATTRIBUTE,
+ x1: MUST_USE_ATTRIBUTE,
+ x2: MUST_USE_ATTRIBUTE,
+ x: MUST_USE_ATTRIBUTE,
+ xlinkActuate: MUST_USE_ATTRIBUTE,
+ xlinkArcrole: MUST_USE_ATTRIBUTE,
+ xlinkHref: MUST_USE_ATTRIBUTE,
+ xlinkRole: MUST_USE_ATTRIBUTE,
+ xlinkShow: MUST_USE_ATTRIBUTE,
+ xlinkTitle: MUST_USE_ATTRIBUTE,
+ xlinkType: MUST_USE_ATTRIBUTE,
+ xmlBase: MUST_USE_ATTRIBUTE,
+ xmlLang: MUST_USE_ATTRIBUTE,
+ xmlSpace: MUST_USE_ATTRIBUTE,
+ y1: MUST_USE_ATTRIBUTE,
+ y2: MUST_USE_ATTRIBUTE,
+ y: MUST_USE_ATTRIBUTE
+ },
+ DOMAttributeNamespaces: {
+ xlinkActuate: NS.xlink,
+ xlinkArcrole: NS.xlink,
+ xlinkHref: NS.xlink,
+ xlinkRole: NS.xlink,
+ xlinkShow: NS.xlink,
+ xlinkTitle: NS.xlink,
+ xlinkType: NS.xlink,
+ xmlBase: NS.xml,
+ xmlLang: NS.xml,
+ xmlSpace: NS.xml
+ },
+ DOMAttributeNames: {
+ clipPath: 'clip-path',
+ fillOpacity: 'fill-opacity',
+ fontFamily: 'font-family',
+ fontSize: 'font-size',
+ gradientTransform: 'gradientTransform',
+ gradientUnits: 'gradientUnits',
+ markerEnd: 'marker-end',
+ markerMid: 'marker-mid',
+ markerStart: 'marker-start',
+ patternContentUnits: 'patternContentUnits',
+ patternUnits: 'patternUnits',
+ preserveAspectRatio: 'preserveAspectRatio',
+ spreadMethod: 'spreadMethod',
+ stopColor: 'stop-color',
+ stopOpacity: 'stop-opacity',
+ strokeDasharray: 'stroke-dasharray',
+ strokeLinecap: 'stroke-linecap',
+ strokeOpacity: 'stroke-opacity',
+ strokeWidth: 'stroke-width',
+ textAnchor: 'text-anchor',
+ viewBox: 'viewBox',
+ xlinkActuate: 'xlink:actuate',
+ xlinkArcrole: 'xlink:arcrole',
+ xlinkHref: 'xlink:href',
+ xlinkRole: 'xlink:role',
+ xlinkShow: 'xlink:show',
+ xlinkTitle: 'xlink:title',
+ xlinkType: 'xlink:type',
+ xmlBase: 'xml:base',
+ xmlLang: 'xml:lang',
+ xmlSpace: 'xml:space'
+ }
+};
+
+module.exports = SVGDOMPropertyConfig;
+},{"10":10}],99:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SelectEventPlugin
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventPropagators = _dereq_(19);
+var ExecutionEnvironment = _dereq_(147);
+var ReactInputSelection = _dereq_(66);
+var SyntheticEvent = _dereq_(105);
+
+var getActiveElement = _dereq_(156);
+var isTextInputElement = _dereq_(134);
+var keyOf = _dereq_(166);
+var shallowEqual = _dereq_(171);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+var skipSelectionChangeEvent = ExecutionEnvironment.canUseDOM && 'documentMode' in document && document.documentMode <= 11;
+
+var eventTypes = {
+ select: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSelect: null }),
+ captured: keyOf({ onSelectCapture: null })
+ },
+ dependencies: [topLevelTypes.topBlur, topLevelTypes.topContextMenu, topLevelTypes.topFocus, topLevelTypes.topKeyDown, topLevelTypes.topMouseDown, topLevelTypes.topMouseUp, topLevelTypes.topSelectionChange]
+ }
+};
+
+var activeElement = null;
+var activeElementID = null;
+var lastSelection = null;
+var mouseDown = false;
+
+// Track whether a listener exists for this plugin. If none exist, we do
+// not extract events.
+var hasListener = false;
+var ON_SELECT_KEY = keyOf({ onSelect: null });
+
+/**
+ * Get an object which is a unique representation of the current selection.
+ *
+ * The return value will not be consistent across nodes or browsers, but
+ * two identical selections on the same node will return identical objects.
+ *
+ * @param {DOMElement} node
+ * @return {object}
+ */
+function getSelection(node) {
+ if ('selectionStart' in node && ReactInputSelection.hasSelectionCapabilities(node)) {
+ return {
+ start: node.selectionStart,
+ end: node.selectionEnd
+ };
+ } else if (window.getSelection) {
+ var selection = window.getSelection();
+ return {
+ anchorNode: selection.anchorNode,
+ anchorOffset: selection.anchorOffset,
+ focusNode: selection.focusNode,
+ focusOffset: selection.focusOffset
+ };
+ } else if (document.selection) {
+ var range = document.selection.createRange();
+ return {
+ parentElement: range.parentElement(),
+ text: range.text,
+ top: range.boundingTop,
+ left: range.boundingLeft
+ };
+ }
+}
+
+/**
+ * Poll selection to see whether it's changed.
+ *
+ * @param {object} nativeEvent
+ * @return {?SyntheticEvent}
+ */
+function constructSelectEvent(nativeEvent, nativeEventTarget) {
+ // Ensure we have the right element, and that the user is not dragging a
+ // selection (this matches native `select` event behavior). In HTML5, select
+ // fires only on input and textarea thus if there's no focused element we
+ // won't dispatch.
+ if (mouseDown || activeElement == null || activeElement !== getActiveElement()) {
+ return null;
+ }
+
+ // Only fire when selection has actually changed.
+ var currentSelection = getSelection(activeElement);
+ if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {
+ lastSelection = currentSelection;
+
+ var syntheticEvent = SyntheticEvent.getPooled(eventTypes.select, activeElementID, nativeEvent, nativeEventTarget);
+
+ syntheticEvent.type = 'select';
+ syntheticEvent.target = activeElement;
+
+ EventPropagators.accumulateTwoPhaseDispatches(syntheticEvent);
+
+ return syntheticEvent;
+ }
+
+ return null;
+}
+
+/**
+ * This plugin creates an `onSelect` event that normalizes select events
+ * across form elements.
+ *
+ * Supported elements are:
+ * - input (see `isTextInputElement`)
+ * - textarea
+ * - contentEditable
+ *
+ * This differs from native browser implementations in the following ways:
+ * - Fires on contentEditable fields as well as inputs.
+ * - Fires for collapsed selection.
+ * - Fires after user input.
+ */
+var SelectEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ if (!hasListener) {
+ return null;
+ }
+
+ switch (topLevelType) {
+ // Track the input node that has focus.
+ case topLevelTypes.topFocus:
+ if (isTextInputElement(topLevelTarget) || topLevelTarget.contentEditable === 'true') {
+ activeElement = topLevelTarget;
+ activeElementID = topLevelTargetID;
+ lastSelection = null;
+ }
+ break;
+ case topLevelTypes.topBlur:
+ activeElement = null;
+ activeElementID = null;
+ lastSelection = null;
+ break;
+
+ // Don't fire the event while the user is dragging. This matches the
+ // semantics of the native select event.
+ case topLevelTypes.topMouseDown:
+ mouseDown = true;
+ break;
+ case topLevelTypes.topContextMenu:
+ case topLevelTypes.topMouseUp:
+ mouseDown = false;
+ return constructSelectEvent(nativeEvent, nativeEventTarget);
+
+ // Chrome and IE fire non-standard event when selection is changed (and
+ // sometimes when it hasn't). IE's event fires out of order with respect
+ // to key and input events on deletion, so we discard it.
+ //
+ // Firefox doesn't support selectionchange, so check selection status
+ // after each key entry. The selection changes after keydown and before
+ // keyup, but we check on keydown as well in the case of holding down a
+ // key, when multiple keydown events are fired but only one keyup is.
+ // This is also our approach for IE handling, for the reason above.
+ case topLevelTypes.topSelectionChange:
+ if (skipSelectionChangeEvent) {
+ break;
+ }
+ // falls through
+ case topLevelTypes.topKeyDown:
+ case topLevelTypes.topKeyUp:
+ return constructSelectEvent(nativeEvent, nativeEventTarget);
+ }
+
+ return null;
+ },
+
+ didPutListener: function (id, registrationName, listener) {
+ if (registrationName === ON_SELECT_KEY) {
+ hasListener = true;
+ }
+ }
+};
+
+module.exports = SelectEventPlugin;
+},{"105":105,"134":134,"147":147,"15":15,"156":156,"166":166,"171":171,"19":19,"66":66}],100:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ServerReactRootIndex
+ * @typechecks
+ */
+
+'use strict';
+
+/**
+ * Size of the reactRoot ID space. We generate random numbers for React root
+ * IDs and if there's a collision the events and DOM update system will
+ * get confused. In the future we need a way to generate GUIDs but for
+ * now this will work on a smaller scale.
+ */
+var GLOBAL_MOUNT_POINT_MAX = Math.pow(2, 53);
+
+var ServerReactRootIndex = {
+ createReactRootIndex: function () {
+ return Math.ceil(Math.random() * GLOBAL_MOUNT_POINT_MAX);
+ }
+};
+
+module.exports = ServerReactRootIndex;
+},{}],101:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SimpleEventPlugin
+ */
+
+'use strict';
+
+var EventConstants = _dereq_(15);
+var EventListener = _dereq_(146);
+var EventPropagators = _dereq_(19);
+var ReactMount = _dereq_(72);
+var SyntheticClipboardEvent = _dereq_(102);
+var SyntheticEvent = _dereq_(105);
+var SyntheticFocusEvent = _dereq_(106);
+var SyntheticKeyboardEvent = _dereq_(108);
+var SyntheticMouseEvent = _dereq_(109);
+var SyntheticDragEvent = _dereq_(104);
+var SyntheticTouchEvent = _dereq_(110);
+var SyntheticUIEvent = _dereq_(111);
+var SyntheticWheelEvent = _dereq_(112);
+
+var emptyFunction = _dereq_(153);
+var getEventCharCode = _dereq_(125);
+var invariant = _dereq_(161);
+var keyOf = _dereq_(166);
+
+var topLevelTypes = EventConstants.topLevelTypes;
+
+var eventTypes = {
+ abort: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onAbort: true }),
+ captured: keyOf({ onAbortCapture: true })
+ }
+ },
+ blur: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onBlur: true }),
+ captured: keyOf({ onBlurCapture: true })
+ }
+ },
+ canPlay: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCanPlay: true }),
+ captured: keyOf({ onCanPlayCapture: true })
+ }
+ },
+ canPlayThrough: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCanPlayThrough: true }),
+ captured: keyOf({ onCanPlayThroughCapture: true })
+ }
+ },
+ click: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onClick: true }),
+ captured: keyOf({ onClickCapture: true })
+ }
+ },
+ contextMenu: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onContextMenu: true }),
+ captured: keyOf({ onContextMenuCapture: true })
+ }
+ },
+ copy: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCopy: true }),
+ captured: keyOf({ onCopyCapture: true })
+ }
+ },
+ cut: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onCut: true }),
+ captured: keyOf({ onCutCapture: true })
+ }
+ },
+ doubleClick: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDoubleClick: true }),
+ captured: keyOf({ onDoubleClickCapture: true })
+ }
+ },
+ drag: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDrag: true }),
+ captured: keyOf({ onDragCapture: true })
+ }
+ },
+ dragEnd: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragEnd: true }),
+ captured: keyOf({ onDragEndCapture: true })
+ }
+ },
+ dragEnter: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragEnter: true }),
+ captured: keyOf({ onDragEnterCapture: true })
+ }
+ },
+ dragExit: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragExit: true }),
+ captured: keyOf({ onDragExitCapture: true })
+ }
+ },
+ dragLeave: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragLeave: true }),
+ captured: keyOf({ onDragLeaveCapture: true })
+ }
+ },
+ dragOver: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragOver: true }),
+ captured: keyOf({ onDragOverCapture: true })
+ }
+ },
+ dragStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDragStart: true }),
+ captured: keyOf({ onDragStartCapture: true })
+ }
+ },
+ drop: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDrop: true }),
+ captured: keyOf({ onDropCapture: true })
+ }
+ },
+ durationChange: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onDurationChange: true }),
+ captured: keyOf({ onDurationChangeCapture: true })
+ }
+ },
+ emptied: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onEmptied: true }),
+ captured: keyOf({ onEmptiedCapture: true })
+ }
+ },
+ encrypted: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onEncrypted: true }),
+ captured: keyOf({ onEncryptedCapture: true })
+ }
+ },
+ ended: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onEnded: true }),
+ captured: keyOf({ onEndedCapture: true })
+ }
+ },
+ error: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onError: true }),
+ captured: keyOf({ onErrorCapture: true })
+ }
+ },
+ focus: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onFocus: true }),
+ captured: keyOf({ onFocusCapture: true })
+ }
+ },
+ input: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onInput: true }),
+ captured: keyOf({ onInputCapture: true })
+ }
+ },
+ keyDown: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onKeyDown: true }),
+ captured: keyOf({ onKeyDownCapture: true })
+ }
+ },
+ keyPress: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onKeyPress: true }),
+ captured: keyOf({ onKeyPressCapture: true })
+ }
+ },
+ keyUp: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onKeyUp: true }),
+ captured: keyOf({ onKeyUpCapture: true })
+ }
+ },
+ load: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoad: true }),
+ captured: keyOf({ onLoadCapture: true })
+ }
+ },
+ loadedData: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoadedData: true }),
+ captured: keyOf({ onLoadedDataCapture: true })
+ }
+ },
+ loadedMetadata: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoadedMetadata: true }),
+ captured: keyOf({ onLoadedMetadataCapture: true })
+ }
+ },
+ loadStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onLoadStart: true }),
+ captured: keyOf({ onLoadStartCapture: true })
+ }
+ },
+ // Note: We do not allow listening to mouseOver events. Instead, use the
+ // onMouseEnter/onMouseLeave created by `EnterLeaveEventPlugin`.
+ mouseDown: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseDown: true }),
+ captured: keyOf({ onMouseDownCapture: true })
+ }
+ },
+ mouseMove: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseMove: true }),
+ captured: keyOf({ onMouseMoveCapture: true })
+ }
+ },
+ mouseOut: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseOut: true }),
+ captured: keyOf({ onMouseOutCapture: true })
+ }
+ },
+ mouseOver: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseOver: true }),
+ captured: keyOf({ onMouseOverCapture: true })
+ }
+ },
+ mouseUp: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onMouseUp: true }),
+ captured: keyOf({ onMouseUpCapture: true })
+ }
+ },
+ paste: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPaste: true }),
+ captured: keyOf({ onPasteCapture: true })
+ }
+ },
+ pause: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPause: true }),
+ captured: keyOf({ onPauseCapture: true })
+ }
+ },
+ play: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPlay: true }),
+ captured: keyOf({ onPlayCapture: true })
+ }
+ },
+ playing: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onPlaying: true }),
+ captured: keyOf({ onPlayingCapture: true })
+ }
+ },
+ progress: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onProgress: true }),
+ captured: keyOf({ onProgressCapture: true })
+ }
+ },
+ rateChange: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onRateChange: true }),
+ captured: keyOf({ onRateChangeCapture: true })
+ }
+ },
+ reset: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onReset: true }),
+ captured: keyOf({ onResetCapture: true })
+ }
+ },
+ scroll: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onScroll: true }),
+ captured: keyOf({ onScrollCapture: true })
+ }
+ },
+ seeked: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSeeked: true }),
+ captured: keyOf({ onSeekedCapture: true })
+ }
+ },
+ seeking: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSeeking: true }),
+ captured: keyOf({ onSeekingCapture: true })
+ }
+ },
+ stalled: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onStalled: true }),
+ captured: keyOf({ onStalledCapture: true })
+ }
+ },
+ submit: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSubmit: true }),
+ captured: keyOf({ onSubmitCapture: true })
+ }
+ },
+ suspend: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onSuspend: true }),
+ captured: keyOf({ onSuspendCapture: true })
+ }
+ },
+ timeUpdate: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTimeUpdate: true }),
+ captured: keyOf({ onTimeUpdateCapture: true })
+ }
+ },
+ touchCancel: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchCancel: true }),
+ captured: keyOf({ onTouchCancelCapture: true })
+ }
+ },
+ touchEnd: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchEnd: true }),
+ captured: keyOf({ onTouchEndCapture: true })
+ }
+ },
+ touchMove: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchMove: true }),
+ captured: keyOf({ onTouchMoveCapture: true })
+ }
+ },
+ touchStart: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onTouchStart: true }),
+ captured: keyOf({ onTouchStartCapture: true })
+ }
+ },
+ volumeChange: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onVolumeChange: true }),
+ captured: keyOf({ onVolumeChangeCapture: true })
+ }
+ },
+ waiting: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onWaiting: true }),
+ captured: keyOf({ onWaitingCapture: true })
+ }
+ },
+ wheel: {
+ phasedRegistrationNames: {
+ bubbled: keyOf({ onWheel: true }),
+ captured: keyOf({ onWheelCapture: true })
+ }
+ }
+};
+
+var topLevelEventsToDispatchConfig = {
+ topAbort: eventTypes.abort,
+ topBlur: eventTypes.blur,
+ topCanPlay: eventTypes.canPlay,
+ topCanPlayThrough: eventTypes.canPlayThrough,
+ topClick: eventTypes.click,
+ topContextMenu: eventTypes.contextMenu,
+ topCopy: eventTypes.copy,
+ topCut: eventTypes.cut,
+ topDoubleClick: eventTypes.doubleClick,
+ topDrag: eventTypes.drag,
+ topDragEnd: eventTypes.dragEnd,
+ topDragEnter: eventTypes.dragEnter,
+ topDragExit: eventTypes.dragExit,
+ topDragLeave: eventTypes.dragLeave,
+ topDragOver: eventTypes.dragOver,
+ topDragStart: eventTypes.dragStart,
+ topDrop: eventTypes.drop,
+ topDurationChange: eventTypes.durationChange,
+ topEmptied: eventTypes.emptied,
+ topEncrypted: eventTypes.encrypted,
+ topEnded: eventTypes.ended,
+ topError: eventTypes.error,
+ topFocus: eventTypes.focus,
+ topInput: eventTypes.input,
+ topKeyDown: eventTypes.keyDown,
+ topKeyPress: eventTypes.keyPress,
+ topKeyUp: eventTypes.keyUp,
+ topLoad: eventTypes.load,
+ topLoadedData: eventTypes.loadedData,
+ topLoadedMetadata: eventTypes.loadedMetadata,
+ topLoadStart: eventTypes.loadStart,
+ topMouseDown: eventTypes.mouseDown,
+ topMouseMove: eventTypes.mouseMove,
+ topMouseOut: eventTypes.mouseOut,
+ topMouseOver: eventTypes.mouseOver,
+ topMouseUp: eventTypes.mouseUp,
+ topPaste: eventTypes.paste,
+ topPause: eventTypes.pause,
+ topPlay: eventTypes.play,
+ topPlaying: eventTypes.playing,
+ topProgress: eventTypes.progress,
+ topRateChange: eventTypes.rateChange,
+ topReset: eventTypes.reset,
+ topScroll: eventTypes.scroll,
+ topSeeked: eventTypes.seeked,
+ topSeeking: eventTypes.seeking,
+ topStalled: eventTypes.stalled,
+ topSubmit: eventTypes.submit,
+ topSuspend: eventTypes.suspend,
+ topTimeUpdate: eventTypes.timeUpdate,
+ topTouchCancel: eventTypes.touchCancel,
+ topTouchEnd: eventTypes.touchEnd,
+ topTouchMove: eventTypes.touchMove,
+ topTouchStart: eventTypes.touchStart,
+ topVolumeChange: eventTypes.volumeChange,
+ topWaiting: eventTypes.waiting,
+ topWheel: eventTypes.wheel
+};
+
+for (var type in topLevelEventsToDispatchConfig) {
+ topLevelEventsToDispatchConfig[type].dependencies = [type];
+}
+
+var ON_CLICK_KEY = keyOf({ onClick: null });
+var onClickListeners = {};
+
+var SimpleEventPlugin = {
+
+ eventTypes: eventTypes,
+
+ /**
+ * @param {string} topLevelType Record from `EventConstants`.
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
+ * @param {object} nativeEvent Native browser event.
+ * @return {*} An accumulation of synthetic events.
+ * @see {EventPluginHub.extractEvents}
+ */
+ extractEvents: function (topLevelType, topLevelTarget, topLevelTargetID, nativeEvent, nativeEventTarget) {
+ var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
+ if (!dispatchConfig) {
+ return null;
+ }
+ var EventConstructor;
+ switch (topLevelType) {
+ case topLevelTypes.topAbort:
+ case topLevelTypes.topCanPlay:
+ case topLevelTypes.topCanPlayThrough:
+ case topLevelTypes.topDurationChange:
+ case topLevelTypes.topEmptied:
+ case topLevelTypes.topEncrypted:
+ case topLevelTypes.topEnded:
+ case topLevelTypes.topError:
+ case topLevelTypes.topInput:
+ case topLevelTypes.topLoad:
+ case topLevelTypes.topLoadedData:
+ case topLevelTypes.topLoadedMetadata:
+ case topLevelTypes.topLoadStart:
+ case topLevelTypes.topPause:
+ case topLevelTypes.topPlay:
+ case topLevelTypes.topPlaying:
+ case topLevelTypes.topProgress:
+ case topLevelTypes.topRateChange:
+ case topLevelTypes.topReset:
+ case topLevelTypes.topSeeked:
+ case topLevelTypes.topSeeking:
+ case topLevelTypes.topStalled:
+ case topLevelTypes.topSubmit:
+ case topLevelTypes.topSuspend:
+ case topLevelTypes.topTimeUpdate:
+ case topLevelTypes.topVolumeChange:
+ case topLevelTypes.topWaiting:
+ // HTML Events
+ // @see http://www.w3.org/TR/html5/index.html#events-0
+ EventConstructor = SyntheticEvent;
+ break;
+ case topLevelTypes.topKeyPress:
+ // FireFox creates a keypress event for function keys too. This removes
+ // the unwanted keypress events. Enter is however both printable and
+ // non-printable. One would expect Tab to be as well (but it isn't).
+ if (getEventCharCode(nativeEvent) === 0) {
+ return null;
+ }
+ /* falls through */
+ case topLevelTypes.topKeyDown:
+ case topLevelTypes.topKeyUp:
+ EventConstructor = SyntheticKeyboardEvent;
+ break;
+ case topLevelTypes.topBlur:
+ case topLevelTypes.topFocus:
+ EventConstructor = SyntheticFocusEvent;
+ break;
+ case topLevelTypes.topClick:
+ // Firefox creates a click event on right mouse clicks. This removes the
+ // unwanted click events.
+ if (nativeEvent.button === 2) {
+ return null;
+ }
+ /* falls through */
+ case topLevelTypes.topContextMenu:
+ case topLevelTypes.topDoubleClick:
+ case topLevelTypes.topMouseDown:
+ case topLevelTypes.topMouseMove:
+ case topLevelTypes.topMouseOut:
+ case topLevelTypes.topMouseOver:
+ case topLevelTypes.topMouseUp:
+ EventConstructor = SyntheticMouseEvent;
+ break;
+ case topLevelTypes.topDrag:
+ case topLevelTypes.topDragEnd:
+ case topLevelTypes.topDragEnter:
+ case topLevelTypes.topDragExit:
+ case topLevelTypes.topDragLeave:
+ case topLevelTypes.topDragOver:
+ case topLevelTypes.topDragStart:
+ case topLevelTypes.topDrop:
+ EventConstructor = SyntheticDragEvent;
+ break;
+ case topLevelTypes.topTouchCancel:
+ case topLevelTypes.topTouchEnd:
+ case topLevelTypes.topTouchMove:
+ case topLevelTypes.topTouchStart:
+ EventConstructor = SyntheticTouchEvent;
+ break;
+ case topLevelTypes.topScroll:
+ EventConstructor = SyntheticUIEvent;
+ break;
+ case topLevelTypes.topWheel:
+ EventConstructor = SyntheticWheelEvent;
+ break;
+ case topLevelTypes.topCopy:
+ case topLevelTypes.topCut:
+ case topLevelTypes.topPaste:
+ EventConstructor = SyntheticClipboardEvent;
+ break;
+ }
+ !EventConstructor ? "production" !== 'production' ? invariant(false, 'SimpleEventPlugin: Unhandled event type, `%s`.', topLevelType) : invariant(false) : undefined;
+ var event = EventConstructor.getPooled(dispatchConfig, topLevelTargetID, nativeEvent, nativeEventTarget);
+ EventPropagators.accumulateTwoPhaseDispatches(event);
+ return event;
+ },
+
+ didPutListener: function (id, registrationName, listener) {
+ // Mobile Safari does not fire properly bubble click events on
+ // non-interactive elements, which means delegated click listeners do not
+ // fire. The workaround for this bug involves attaching an empty click
+ // listener on the target node.
+ if (registrationName === ON_CLICK_KEY) {
+ var node = ReactMount.getNode(id);
+ if (!onClickListeners[id]) {
+ onClickListeners[id] = EventListener.listen(node, 'click', emptyFunction);
+ }
+ }
+ },
+
+ willDeleteListener: function (id, registrationName) {
+ if (registrationName === ON_CLICK_KEY) {
+ onClickListeners[id].remove();
+ delete onClickListeners[id];
+ }
+ }
+
+};
+
+module.exports = SimpleEventPlugin;
+},{"102":102,"104":104,"105":105,"106":106,"108":108,"109":109,"110":110,"111":111,"112":112,"125":125,"146":146,"15":15,"153":153,"161":161,"166":166,"19":19,"72":72}],102:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticClipboardEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/clipboard-apis/
+ */
+var ClipboardEventInterface = {
+ clipboardData: function (event) {
+ return 'clipboardData' in event ? event.clipboardData : window.clipboardData;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticClipboardEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticClipboardEvent, ClipboardEventInterface);
+
+module.exports = SyntheticClipboardEvent;
+},{"105":105}],103:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticCompositionEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/#events-compositionevents
+ */
+var CompositionEventInterface = {
+ data: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticCompositionEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticCompositionEvent, CompositionEventInterface);
+
+module.exports = SyntheticCompositionEvent;
+},{"105":105}],104:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticDragEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticMouseEvent = _dereq_(109);
+
+/**
+ * @interface DragEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var DragEventInterface = {
+ dataTransfer: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticDragEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticMouseEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticMouseEvent.augmentClass(SyntheticDragEvent, DragEventInterface);
+
+module.exports = SyntheticDragEvent;
+},{"109":109}],105:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var PooledClass = _dereq_(25);
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+var warning = _dereq_(173);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var EventInterface = {
+ type: null,
+ // currentTarget is set when dispatching; no use in copying it here
+ currentTarget: emptyFunction.thatReturnsNull,
+ eventPhase: null,
+ bubbles: null,
+ cancelable: null,
+ timeStamp: function (event) {
+ return event.timeStamp || Date.now();
+ },
+ defaultPrevented: null,
+ isTrusted: null
+};
+
+/**
+ * Synthetic events are dispatched by event plugins, typically in response to a
+ * top-level event delegation handler.
+ *
+ * These systems should generally use pooling to reduce the frequency of garbage
+ * collection. The system should check `isPersistent` to determine whether the
+ * event should be released into the pool after being dispatched. Users that
+ * need a persisted event should invoke `persist`.
+ *
+ * Synthetic events (and subclasses) implement the DOM Level 3 Events API by
+ * normalizing browser quirks. Subclasses do not necessarily have to implement a
+ * DOM interface; custom application-specific events can also subclass this.
+ *
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ */
+function SyntheticEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ this.dispatchConfig = dispatchConfig;
+ this.dispatchMarker = dispatchMarker;
+ this.nativeEvent = nativeEvent;
+ this.target = nativeEventTarget;
+ this.currentTarget = nativeEventTarget;
+
+ var Interface = this.constructor.Interface;
+ for (var propName in Interface) {
+ if (!Interface.hasOwnProperty(propName)) {
+ continue;
+ }
+ var normalize = Interface[propName];
+ if (normalize) {
+ this[propName] = normalize(nativeEvent);
+ } else {
+ this[propName] = nativeEvent[propName];
+ }
+ }
+
+ var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;
+ if (defaultPrevented) {
+ this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
+ } else {
+ this.isDefaultPrevented = emptyFunction.thatReturnsFalse;
+ }
+ this.isPropagationStopped = emptyFunction.thatReturnsFalse;
+}
+
+assign(SyntheticEvent.prototype, {
+
+ preventDefault: function () {
+ this.defaultPrevented = true;
+ var event = this.nativeEvent;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(event, 'This synthetic event is reused for performance reasons. If you\'re ' + 'seeing this, you\'re calling `preventDefault` on a ' + 'released/nullified synthetic event. This is a no-op. See ' + 'https://fb.me/react-event-pooling for more information.') : undefined;
+ }
+ if (!event) {
+ return;
+ }
+
+ if (event.preventDefault) {
+ event.preventDefault();
+ } else {
+ event.returnValue = false;
+ }
+ this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
+ },
+
+ stopPropagation: function () {
+ var event = this.nativeEvent;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(event, 'This synthetic event is reused for performance reasons. If you\'re ' + 'seeing this, you\'re calling `stopPropagation` on a ' + 'released/nullified synthetic event. This is a no-op. See ' + 'https://fb.me/react-event-pooling for more information.') : undefined;
+ }
+ if (!event) {
+ return;
+ }
+
+ if (event.stopPropagation) {
+ event.stopPropagation();
+ } else {
+ event.cancelBubble = true;
+ }
+ this.isPropagationStopped = emptyFunction.thatReturnsTrue;
+ },
+
+ /**
+ * We release all dispatched `SyntheticEvent`s after each event loop, adding
+ * them back into the pool. This allows a way to hold onto a reference that
+ * won't be added back into the pool.
+ */
+ persist: function () {
+ this.isPersistent = emptyFunction.thatReturnsTrue;
+ },
+
+ /**
+ * Checks if this event should be released back into the pool.
+ *
+ * @return {boolean} True if this should not be released, false otherwise.
+ */
+ isPersistent: emptyFunction.thatReturnsFalse,
+
+ /**
+ * `PooledClass` looks for `destructor` on each instance it releases.
+ */
+ destructor: function () {
+ var Interface = this.constructor.Interface;
+ for (var propName in Interface) {
+ this[propName] = null;
+ }
+ this.dispatchConfig = null;
+ this.dispatchMarker = null;
+ this.nativeEvent = null;
+ }
+
+});
+
+SyntheticEvent.Interface = EventInterface;
+
+/**
+ * Helper to reduce boilerplate when creating subclasses.
+ *
+ * @param {function} Class
+ * @param {?object} Interface
+ */
+SyntheticEvent.augmentClass = function (Class, Interface) {
+ var Super = this;
+
+ var prototype = Object.create(Super.prototype);
+ assign(prototype, Class.prototype);
+ Class.prototype = prototype;
+ Class.prototype.constructor = Class;
+
+ Class.Interface = assign({}, Super.Interface, Interface);
+ Class.augmentClass = Super.augmentClass;
+
+ PooledClass.addPoolingTo(Class, PooledClass.fourArgumentPooler);
+};
+
+PooledClass.addPoolingTo(SyntheticEvent, PooledClass.fourArgumentPooler);
+
+module.exports = SyntheticEvent;
+},{"153":153,"173":173,"24":24,"25":25}],106:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticFocusEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+
+/**
+ * @interface FocusEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var FocusEventInterface = {
+ relatedTarget: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticFocusEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticFocusEvent, FocusEventInterface);
+
+module.exports = SyntheticFocusEvent;
+},{"111":111}],107:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticInputEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+/**
+ * @interface Event
+ * @see http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105
+ * /#events-inputevents
+ */
+var InputEventInterface = {
+ data: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticInputEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticInputEvent, InputEventInterface);
+
+module.exports = SyntheticInputEvent;
+},{"105":105}],108:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticKeyboardEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+
+var getEventCharCode = _dereq_(125);
+var getEventKey = _dereq_(126);
+var getEventModifierState = _dereq_(127);
+
+/**
+ * @interface KeyboardEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var KeyboardEventInterface = {
+ key: getEventKey,
+ location: null,
+ ctrlKey: null,
+ shiftKey: null,
+ altKey: null,
+ metaKey: null,
+ repeat: null,
+ locale: null,
+ getModifierState: getEventModifierState,
+ // Legacy Interface
+ charCode: function (event) {
+ // `charCode` is the result of a KeyPress event and represents the value of
+ // the actual printable character.
+
+ // KeyPress is deprecated, but its replacement is not yet final and not
+ // implemented in any major browser. Only KeyPress has charCode.
+ if (event.type === 'keypress') {
+ return getEventCharCode(event);
+ }
+ return 0;
+ },
+ keyCode: function (event) {
+ // `keyCode` is the result of a KeyDown/Up event and represents the value of
+ // physical keyboard key.
+
+ // The actual meaning of the value depends on the users' keyboard layout
+ // which cannot be detected. Assuming that it is a US keyboard layout
+ // provides a surprisingly accurate mapping for US and European users.
+ // Due to this, it is left to the user to implement at this time.
+ if (event.type === 'keydown' || event.type === 'keyup') {
+ return event.keyCode;
+ }
+ return 0;
+ },
+ which: function (event) {
+ // `which` is an alias for either `keyCode` or `charCode` depending on the
+ // type of the event.
+ if (event.type === 'keypress') {
+ return getEventCharCode(event);
+ }
+ if (event.type === 'keydown' || event.type === 'keyup') {
+ return event.keyCode;
+ }
+ return 0;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticKeyboardEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticKeyboardEvent, KeyboardEventInterface);
+
+module.exports = SyntheticKeyboardEvent;
+},{"111":111,"125":125,"126":126,"127":127}],109:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticMouseEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+var ViewportMetrics = _dereq_(114);
+
+var getEventModifierState = _dereq_(127);
+
+/**
+ * @interface MouseEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var MouseEventInterface = {
+ screenX: null,
+ screenY: null,
+ clientX: null,
+ clientY: null,
+ ctrlKey: null,
+ shiftKey: null,
+ altKey: null,
+ metaKey: null,
+ getModifierState: getEventModifierState,
+ button: function (event) {
+ // Webkit, Firefox, IE9+
+ // which: 1 2 3
+ // button: 0 1 2 (standard)
+ var button = event.button;
+ if ('which' in event) {
+ return button;
+ }
+ // IE<9
+ // which: undefined
+ // button: 0 0 0
+ // button: 1 4 2 (onmouseup)
+ return button === 2 ? 2 : button === 4 ? 1 : 0;
+ },
+ buttons: null,
+ relatedTarget: function (event) {
+ return event.relatedTarget || (event.fromElement === event.srcElement ? event.toElement : event.fromElement);
+ },
+ // "Proprietary" Interface.
+ pageX: function (event) {
+ return 'pageX' in event ? event.pageX : event.clientX + ViewportMetrics.currentScrollLeft;
+ },
+ pageY: function (event) {
+ return 'pageY' in event ? event.pageY : event.clientY + ViewportMetrics.currentScrollTop;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticMouseEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticMouseEvent, MouseEventInterface);
+
+module.exports = SyntheticMouseEvent;
+},{"111":111,"114":114,"127":127}],110:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticTouchEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticUIEvent = _dereq_(111);
+
+var getEventModifierState = _dereq_(127);
+
+/**
+ * @interface TouchEvent
+ * @see http://www.w3.org/TR/touch-events/
+ */
+var TouchEventInterface = {
+ touches: null,
+ targetTouches: null,
+ changedTouches: null,
+ altKey: null,
+ metaKey: null,
+ ctrlKey: null,
+ shiftKey: null,
+ getModifierState: getEventModifierState
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticUIEvent}
+ */
+function SyntheticTouchEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticUIEvent.augmentClass(SyntheticTouchEvent, TouchEventInterface);
+
+module.exports = SyntheticTouchEvent;
+},{"111":111,"127":127}],111:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticUIEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticEvent = _dereq_(105);
+
+var getEventTarget = _dereq_(128);
+
+/**
+ * @interface UIEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var UIEventInterface = {
+ view: function (event) {
+ if (event.view) {
+ return event.view;
+ }
+
+ var target = getEventTarget(event);
+ if (target != null && target.window === target) {
+ // target is a window object
+ return target;
+ }
+
+ var doc = target.ownerDocument;
+ // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.
+ if (doc) {
+ return doc.defaultView || doc.parentWindow;
+ } else {
+ return window;
+ }
+ },
+ detail: function (event) {
+ return event.detail || 0;
+ }
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticEvent}
+ */
+function SyntheticUIEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticEvent.augmentClass(SyntheticUIEvent, UIEventInterface);
+
+module.exports = SyntheticUIEvent;
+},{"105":105,"128":128}],112:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SyntheticWheelEvent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var SyntheticMouseEvent = _dereq_(109);
+
+/**
+ * @interface WheelEvent
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
+ */
+var WheelEventInterface = {
+ deltaX: function (event) {
+ return 'deltaX' in event ? event.deltaX :
+ // Fallback to `wheelDeltaX` for Webkit and normalize (right is positive).
+ 'wheelDeltaX' in event ? -event.wheelDeltaX : 0;
+ },
+ deltaY: function (event) {
+ return 'deltaY' in event ? event.deltaY :
+ // Fallback to `wheelDeltaY` for Webkit and normalize (down is positive).
+ 'wheelDeltaY' in event ? -event.wheelDeltaY :
+ // Fallback to `wheelDelta` for IE<9 and normalize (down is positive).
+ 'wheelDelta' in event ? -event.wheelDelta : 0;
+ },
+ deltaZ: null,
+
+ // Browsers without "deltaMode" is reporting in raw wheel delta where one
+ // notch on the scroll is always +/- 120, roughly equivalent to pixels.
+ // A good approximation of DOM_DELTA_LINE (1) is 5% of viewport size or
+ // ~40 pixels, for DOM_DELTA_SCREEN (2) it is 87.5% of viewport size.
+ deltaMode: null
+};
+
+/**
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
+ * @param {string} dispatchMarker Marker identifying the event target.
+ * @param {object} nativeEvent Native browser event.
+ * @extends {SyntheticMouseEvent}
+ */
+function SyntheticWheelEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
+ SyntheticMouseEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
+}
+
+SyntheticMouseEvent.augmentClass(SyntheticWheelEvent, WheelEventInterface);
+
+module.exports = SyntheticWheelEvent;
+},{"109":109}],113:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Transaction
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * `Transaction` creates a black box that is able to wrap any method such that
+ * certain invariants are maintained before and after the method is invoked
+ * (Even if an exception is thrown while invoking the wrapped method). Whoever
+ * instantiates a transaction can provide enforcers of the invariants at
+ * creation time. The `Transaction` class itself will supply one additional
+ * automatic invariant for you - the invariant that any transaction instance
+ * should not be run while it is already being run. You would typically create a
+ * single instance of a `Transaction` for reuse multiple times, that potentially
+ * is used to wrap several different methods. Wrappers are extremely simple -
+ * they only require implementing two methods.
+ *
+ * <pre>
+ * wrappers (injected at creation time)
+ * + +
+ * | |
+ * +-----------------|--------|--------------+
+ * | v | |
+ * | +---------------+ | |
+ * | +--| wrapper1 |---|----+ |
+ * | | +---------------+ v | |
+ * | | +-------------+ | |
+ * | | +----| wrapper2 |--------+ |
+ * | | | +-------------+ | | |
+ * | | | | | |
+ * | v v v v | wrapper
+ * | +---+ +---+ +---------+ +---+ +---+ | invariants
+ * perform(anyMethod) | | | | | | | | | | | | maintained
+ * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
+ * | | | | | | | | | | | |
+ * | | | | | | | | | | | |
+ * | | | | | | | | | | | |
+ * | +---+ +---+ +---------+ +---+ +---+ |
+ * | initialize close |
+ * +-----------------------------------------+
+ * </pre>
+ *
+ * Use cases:
+ * - Preserving the input selection ranges before/after reconciliation.
+ * Restoring selection even in the event of an unexpected error.
+ * - Deactivating events while rearranging the DOM, preventing blurs/focuses,
+ * while guaranteeing that afterwards, the event system is reactivated.
+ * - Flushing a queue of collected DOM mutations to the main UI thread after a
+ * reconciliation takes place in a worker thread.
+ * - Invoking any collected `componentDidUpdate` callbacks after rendering new
+ * content.
+ * - (Future use case): Wrapping particular flushes of the `ReactWorker` queue
+ * to preserve the `scrollTop` (an automatic scroll aware DOM).
+ * - (Future use case): Layout calculations before and after DOM updates.
+ *
+ * Transactional plugin API:
+ * - A module that has an `initialize` method that returns any precomputation.
+ * - and a `close` method that accepts the precomputation. `close` is invoked
+ * when the wrapped process is completed, or has failed.
+ *
+ * @param {Array<TransactionalWrapper>} transactionWrapper Wrapper modules
+ * that implement `initialize` and `close`.
+ * @return {Transaction} Single transaction for reuse in thread.
+ *
+ * @class Transaction
+ */
+var Mixin = {
+ /**
+ * Sets up this instance so that it is prepared for collecting metrics. Does
+ * so such that this setup method may be used on an instance that is already
+ * initialized, in a way that does not consume additional memory upon reuse.
+ * That can be useful if you decide to make your subclass of this mixin a
+ * "PooledClass".
+ */
+ reinitializeTransaction: function () {
+ this.transactionWrappers = this.getTransactionWrappers();
+ if (this.wrapperInitData) {
+ this.wrapperInitData.length = 0;
+ } else {
+ this.wrapperInitData = [];
+ }
+ this._isInTransaction = false;
+ },
+
+ _isInTransaction: false,
+
+ /**
+ * @abstract
+ * @return {Array<TransactionWrapper>} Array of transaction wrappers.
+ */
+ getTransactionWrappers: null,
+
+ isInTransaction: function () {
+ return !!this._isInTransaction;
+ },
+
+ /**
+ * Executes the function within a safety window. Use this for the top level
+ * methods that result in large amounts of computation/mutations that would
+ * need to be safety checked. The optional arguments helps prevent the need
+ * to bind in many cases.
+ *
+ * @param {function} method Member of scope to call.
+ * @param {Object} scope Scope to invoke from.
+ * @param {Object?=} a Argument to pass to the method.
+ * @param {Object?=} b Argument to pass to the method.
+ * @param {Object?=} c Argument to pass to the method.
+ * @param {Object?=} d Argument to pass to the method.
+ * @param {Object?=} e Argument to pass to the method.
+ * @param {Object?=} f Argument to pass to the method.
+ *
+ * @return {*} Return value from `method`.
+ */
+ perform: function (method, scope, a, b, c, d, e, f) {
+ !!this.isInTransaction() ? "production" !== 'production' ? invariant(false, 'Transaction.perform(...): Cannot initialize a transaction when there ' + 'is already an outstanding transaction.') : invariant(false) : undefined;
+ var errorThrown;
+ var ret;
+ try {
+ this._isInTransaction = true;
+ // Catching errors makes debugging more difficult, so we start with
+ // errorThrown set to true before setting it to false after calling
+ // close -- if it's still set to true in the finally block, it means
+ // one of these calls threw.
+ errorThrown = true;
+ this.initializeAll(0);
+ ret = method.call(scope, a, b, c, d, e, f);
+ errorThrown = false;
+ } finally {
+ try {
+ if (errorThrown) {
+ // If `method` throws, prefer to show that stack trace over any thrown
+ // by invoking `closeAll`.
+ try {
+ this.closeAll(0);
+ } catch (err) {}
+ } else {
+ // Since `method` didn't throw, we don't want to silence the exception
+ // here.
+ this.closeAll(0);
+ }
+ } finally {
+ this._isInTransaction = false;
+ }
+ }
+ return ret;
+ },
+
+ initializeAll: function (startIndex) {
+ var transactionWrappers = this.transactionWrappers;
+ for (var i = startIndex; i < transactionWrappers.length; i++) {
+ var wrapper = transactionWrappers[i];
+ try {
+ // Catching errors makes debugging more difficult, so we start with the
+ // OBSERVED_ERROR state before overwriting it with the real return value
+ // of initialize -- if it's still set to OBSERVED_ERROR in the finally
+ // block, it means wrapper.initialize threw.
+ this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
+ this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null;
+ } finally {
+ if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) {
+ // The initializer for wrapper i threw an error; initialize the
+ // remaining wrappers but silence any exceptions from them to ensure
+ // that the first error is the one to bubble up.
+ try {
+ this.initializeAll(i + 1);
+ } catch (err) {}
+ }
+ }
+ }
+ },
+
+ /**
+ * Invokes each of `this.transactionWrappers.close[i]` functions, passing into
+ * them the respective return values of `this.transactionWrappers.init[i]`
+ * (`close`rs that correspond to initializers that failed will not be
+ * invoked).
+ */
+ closeAll: function (startIndex) {
+ !this.isInTransaction() ? "production" !== 'production' ? invariant(false, 'Transaction.closeAll(): Cannot close transaction when none are open.') : invariant(false) : undefined;
+ var transactionWrappers = this.transactionWrappers;
+ for (var i = startIndex; i < transactionWrappers.length; i++) {
+ var wrapper = transactionWrappers[i];
+ var initData = this.wrapperInitData[i];
+ var errorThrown;
+ try {
+ // Catching errors makes debugging more difficult, so we start with
+ // errorThrown set to true before setting it to false after calling
+ // close -- if it's still set to true in the finally block, it means
+ // wrapper.close threw.
+ errorThrown = true;
+ if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) {
+ wrapper.close.call(this, initData);
+ }
+ errorThrown = false;
+ } finally {
+ if (errorThrown) {
+ // The closer for wrapper i threw an error; close the remaining
+ // wrappers but silence any exceptions from them to ensure that the
+ // first error is the one to bubble up.
+ try {
+ this.closeAll(i + 1);
+ } catch (e) {}
+ }
+ }
+ }
+ this.wrapperInitData.length = 0;
+ }
+};
+
+var Transaction = {
+
+ Mixin: Mixin,
+
+ /**
+ * Token to look for to determine if an error occurred.
+ */
+ OBSERVED_ERROR: {}
+
+};
+
+module.exports = Transaction;
+},{"161":161}],114:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ViewportMetrics
+ */
+
+'use strict';
+
+var ViewportMetrics = {
+
+ currentScrollLeft: 0,
+
+ currentScrollTop: 0,
+
+ refreshScrollValues: function (scrollPosition) {
+ ViewportMetrics.currentScrollLeft = scrollPosition.x;
+ ViewportMetrics.currentScrollTop = scrollPosition.y;
+ }
+
+};
+
+module.exports = ViewportMetrics;
+},{}],115:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule accumulateInto
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ *
+ * Accumulates items that must not be null or undefined into the first one. This
+ * is used to conserve memory by avoiding array allocations, and thus sacrifices
+ * API cleanness. Since `current` can be null before being passed in and not
+ * null after this function, make sure to assign it back to `current`:
+ *
+ * `a = accumulateInto(a, b);`
+ *
+ * This API should be sparingly used. Try `accumulate` for something cleaner.
+ *
+ * @return {*|array<*>} An accumulation of items.
+ */
+
+function accumulateInto(current, next) {
+ !(next != null) ? "production" !== 'production' ? invariant(false, 'accumulateInto(...): Accumulated items must not be null or undefined.') : invariant(false) : undefined;
+ if (current == null) {
+ return next;
+ }
+
+ // Both are not empty. Warning: Never call x.concat(y) when you are not
+ // certain that x is an Array (x could be a string with concat method).
+ var currentIsArray = Array.isArray(current);
+ var nextIsArray = Array.isArray(next);
+
+ if (currentIsArray && nextIsArray) {
+ current.push.apply(current, next);
+ return current;
+ }
+
+ if (currentIsArray) {
+ current.push(next);
+ return current;
+ }
+
+ if (nextIsArray) {
+ // A bit too dangerous to mutate `next`.
+ return [current].concat(next);
+ }
+
+ return [current, next];
+}
+
+module.exports = accumulateInto;
+},{"161":161}],116:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule adler32
+ */
+
+'use strict';
+
+var MOD = 65521;
+
+// adler32 is not cryptographically strong, and is only used to sanity check that
+// markup generated on the server matches the markup generated on the client.
+// This implementation (a modified version of the SheetJS version) has been optimized
+// for our use case, at the expense of conforming to the adler32 specification
+// for non-ascii inputs.
+function adler32(data) {
+ var a = 1;
+ var b = 0;
+ var i = 0;
+ var l = data.length;
+ var m = l & ~0x3;
+ while (i < m) {
+ for (; i < Math.min(i + 4096, m); i += 4) {
+ b += (a += data.charCodeAt(i)) + (a += data.charCodeAt(i + 1)) + (a += data.charCodeAt(i + 2)) + (a += data.charCodeAt(i + 3));
+ }
+ a %= MOD;
+ b %= MOD;
+ }
+ for (; i < l; i++) {
+ b += a += data.charCodeAt(i);
+ }
+ a %= MOD;
+ b %= MOD;
+ return a | b << 16;
+}
+
+module.exports = adler32;
+},{}],117:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule canDefineProperty
+ */
+
+'use strict';
+
+var canDefineProperty = false;
+if ("production" !== 'production') {
+ try {
+ Object.defineProperty({}, 'x', { get: function () {} });
+ canDefineProperty = true;
+ } catch (x) {
+ // IE will fail on defineProperty
+ }
+}
+
+module.exports = canDefineProperty;
+},{}],118:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @typechecks static-only
+ * @providesModule cloneWithProps
+ */
+
+'use strict';
+
+var ReactElement = _dereq_(57);
+var ReactPropTransferer = _dereq_(79);
+
+var keyOf = _dereq_(166);
+var warning = _dereq_(173);
+
+var CHILDREN_PROP = keyOf({ children: null });
+
+var didDeprecatedWarn = false;
+
+/**
+ * Sometimes you want to change the props of a child passed to you. Usually
+ * this is to add a CSS class.
+ *
+ * @param {ReactElement} child child element you'd like to clone
+ * @param {object} props props you'd like to modify. className and style will be
+ * merged automatically.
+ * @return {ReactElement} a clone of child with props merged in.
+ * @deprecated
+ */
+function cloneWithProps(child, props) {
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(didDeprecatedWarn, 'cloneWithProps(...) is deprecated. ' + 'Please use React.cloneElement instead.') : undefined;
+ didDeprecatedWarn = true;
+ "production" !== 'production' ? warning(!child.ref, 'You are calling cloneWithProps() on a child with a ref. This is ' + 'dangerous because you\'re creating a new child which will not be ' + 'added as a ref to its parent.') : undefined;
+ }
+
+ var newProps = ReactPropTransferer.mergeProps(props, child.props);
+
+ // Use `child.props.children` if it is provided.
+ if (!newProps.hasOwnProperty(CHILDREN_PROP) && child.props.hasOwnProperty(CHILDREN_PROP)) {
+ newProps.children = child.props.children;
+ }
+
+ // The current API doesn't retain _owner, which is why this
+ // doesn't use ReactElement.cloneAndReplaceProps.
+ return ReactElement.createElement(child.type, newProps);
+}
+
+module.exports = cloneWithProps;
+},{"166":166,"173":173,"57":57,"79":79}],119:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule dangerousStyleValue
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var CSSProperty = _dereq_(4);
+
+var isUnitlessNumber = CSSProperty.isUnitlessNumber;
+
+/**
+ * Convert a value into the proper css writable value. The style name `name`
+ * should be logical (no hyphens), as specified
+ * in `CSSProperty.isUnitlessNumber`.
+ *
+ * @param {string} name CSS property name such as `topMargin`.
+ * @param {*} value CSS property value such as `10px`.
+ * @return {string} Normalized style value with dimensions applied.
+ */
+function dangerousStyleValue(name, value) {
+ // Note that we've removed escapeTextForBrowser() calls here since the
+ // whole string will be escaped when the attribute is injected into
+ // the markup. If you provide unsafe user data here they can inject
+ // arbitrary CSS which may be problematic (I couldn't repro this):
+ // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
+ // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/
+ // This is not an XSS hole but instead a potential CSS injection issue
+ // which has lead to a greater discussion about how we're going to
+ // trust URLs moving forward. See #2115901
+
+ var isEmpty = value == null || typeof value === 'boolean' || value === '';
+ if (isEmpty) {
+ return '';
+ }
+
+ var isNonNumeric = isNaN(value);
+ if (isNonNumeric || value === 0 || isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name]) {
+ return '' + value; // cast to string
+ }
+
+ if (typeof value === 'string') {
+ value = value.trim();
+ }
+ return value + 'px';
+}
+
+module.exports = dangerousStyleValue;
+},{"4":4}],120:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule deprecated
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var warning = _dereq_(173);
+
+/**
+ * This will log a single deprecation notice per function and forward the call
+ * on to the new API.
+ *
+ * @param {string} fnName The name of the function
+ * @param {string} newModule The module that fn will exist in
+ * @param {string} newPackage The module that fn will exist in
+ * @param {*} ctx The context this forwarded call should run in
+ * @param {function} fn The function to forward on to
+ * @return {function} The function that will warn once and then call fn
+ */
+function deprecated(fnName, newModule, newPackage, ctx, fn) {
+ var warned = false;
+ if ("production" !== 'production') {
+ var newFn = function () {
+ "production" !== 'production' ? warning(warned,
+ // Require examples in this string must be split to prevent React's
+ // build tools from mistaking them for real requires.
+ // Otherwise the build tools will attempt to build a '%s' module.
+ 'React.%s is deprecated. Please use %s.%s from require' + '(\'%s\') ' + 'instead.', fnName, newModule, fnName, newPackage) : undefined;
+ warned = true;
+ return fn.apply(ctx, arguments);
+ };
+ // We need to make sure all properties of the original fn are copied over.
+ // In particular, this is needed to support PropTypes
+ return assign(newFn, fn);
+ }
+
+ return fn;
+}
+
+module.exports = deprecated;
+},{"173":173,"24":24}],121:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule escapeTextContentForBrowser
+ */
+
+'use strict';
+
+var ESCAPE_LOOKUP = {
+ '&': '&amp;',
+ '>': '&gt;',
+ '<': '&lt;',
+ '"': '&quot;',
+ '\'': '&#x27;'
+};
+
+var ESCAPE_REGEX = /[&><"']/g;
+
+function escaper(match) {
+ return ESCAPE_LOOKUP[match];
+}
+
+/**
+ * Escapes text to prevent scripting attacks.
+ *
+ * @param {*} text Text value to escape.
+ * @return {string} An escaped string.
+ */
+function escapeTextContentForBrowser(text) {
+ return ('' + text).replace(ESCAPE_REGEX, escaper);
+}
+
+module.exports = escapeTextContentForBrowser;
+},{}],122:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule findDOMNode
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactInstanceMap = _dereq_(68);
+var ReactMount = _dereq_(72);
+
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+/**
+ * Returns the DOM node rendered by this element.
+ *
+ * @param {ReactComponent|DOMElement} componentOrElement
+ * @return {?DOMElement} The root node of this element.
+ */
+function findDOMNode(componentOrElement) {
+ if ("production" !== 'production') {
+ var owner = ReactCurrentOwner.current;
+ if (owner !== null) {
+ "production" !== 'production' ? warning(owner._warnedAboutRefsInRender, '%s is accessing getDOMNode or findDOMNode inside its render(). ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + 'componentDidUpdate instead.', owner.getName() || 'A component') : undefined;
+ owner._warnedAboutRefsInRender = true;
+ }
+ }
+ if (componentOrElement == null) {
+ return null;
+ }
+ if (componentOrElement.nodeType === 1) {
+ return componentOrElement;
+ }
+ if (ReactInstanceMap.has(componentOrElement)) {
+ return ReactMount.getNodeFromInstance(componentOrElement);
+ }
+ !(componentOrElement.render == null || typeof componentOrElement.render !== 'function') ? "production" !== 'production' ? invariant(false, 'findDOMNode was called on an unmounted component.') : invariant(false) : undefined;
+ !false ? "production" !== 'production' ? invariant(false, 'Element appears to be neither ReactComponent nor DOMNode (keys: %s)', Object.keys(componentOrElement)) : invariant(false) : undefined;
+}
+
+module.exports = findDOMNode;
+},{"161":161,"173":173,"39":39,"68":68,"72":72}],123:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule flattenChildren
+ */
+
+'use strict';
+
+var traverseAllChildren = _dereq_(142);
+var warning = _dereq_(173);
+
+/**
+ * @param {function} traverseContext Context passed through traversal.
+ * @param {?ReactComponent} child React child component.
+ * @param {!string} name String name of key path to child.
+ */
+function flattenSingleChildIntoContext(traverseContext, child, name) {
+ // We found a component instance.
+ var result = traverseContext;
+ var keyUnique = result[name] === undefined;
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(keyUnique, 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.', name) : undefined;
+ }
+ if (keyUnique && child != null) {
+ result[name] = child;
+ }
+}
+
+/**
+ * Flattens children that are typically specified as `props.children`. Any null
+ * children will not be included in the resulting object.
+ * @return {!object} flattened children keyed by name.
+ */
+function flattenChildren(children) {
+ if (children == null) {
+ return children;
+ }
+ var result = {};
+ traverseAllChildren(children, flattenSingleChildIntoContext, result);
+ return result;
+}
+
+module.exports = flattenChildren;
+},{"142":142,"173":173}],124:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule forEachAccumulated
+ */
+
+'use strict';
+
+/**
+ * @param {array} arr an "accumulation" of items which is either an Array or
+ * a single item. Useful when paired with the `accumulate` module. This is a
+ * simple utility that allows us to reason about a collection of items, but
+ * handling the case when there is exactly one item (and we do not need to
+ * allocate an array).
+ */
+var forEachAccumulated = function (arr, cb, scope) {
+ if (Array.isArray(arr)) {
+ arr.forEach(cb, scope);
+ } else if (arr) {
+ cb.call(scope, arr);
+ }
+};
+
+module.exports = forEachAccumulated;
+},{}],125:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventCharCode
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * `charCode` represents the actual "character code" and is safe to use with
+ * `String.fromCharCode`. As such, only keys that correspond to printable
+ * characters produce a valid `charCode`, the only exception to this is Enter.
+ * The Tab-key is considered non-printable and does not have a `charCode`,
+ * presumably because it does not produce a tab-character in browsers.
+ *
+ * @param {object} nativeEvent Native browser event.
+ * @return {number} Normalized `charCode` property.
+ */
+function getEventCharCode(nativeEvent) {
+ var charCode;
+ var keyCode = nativeEvent.keyCode;
+
+ if ('charCode' in nativeEvent) {
+ charCode = nativeEvent.charCode;
+
+ // FF does not set `charCode` for the Enter-key, check against `keyCode`.
+ if (charCode === 0 && keyCode === 13) {
+ charCode = 13;
+ }
+ } else {
+ // IE8 does not implement `charCode`, but `keyCode` has the correct value.
+ charCode = keyCode;
+ }
+
+ // Some non-printable keys are reported in `charCode`/`keyCode`, discard them.
+ // Must not discard the (non-)printable Enter-key.
+ if (charCode >= 32 || charCode === 13) {
+ return charCode;
+ }
+
+ return 0;
+}
+
+module.exports = getEventCharCode;
+},{}],126:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventKey
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var getEventCharCode = _dereq_(125);
+
+/**
+ * Normalization of deprecated HTML5 `key` values
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names
+ */
+var normalizeKey = {
+ 'Esc': 'Escape',
+ 'Spacebar': ' ',
+ 'Left': 'ArrowLeft',
+ 'Up': 'ArrowUp',
+ 'Right': 'ArrowRight',
+ 'Down': 'ArrowDown',
+ 'Del': 'Delete',
+ 'Win': 'OS',
+ 'Menu': 'ContextMenu',
+ 'Apps': 'ContextMenu',
+ 'Scroll': 'ScrollLock',
+ 'MozPrintableKey': 'Unidentified'
+};
+
+/**
+ * Translation from legacy `keyCode` to HTML5 `key`
+ * Only special keys supported, all others depend on keyboard layout or browser
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names
+ */
+var translateToKey = {
+ 8: 'Backspace',
+ 9: 'Tab',
+ 12: 'Clear',
+ 13: 'Enter',
+ 16: 'Shift',
+ 17: 'Control',
+ 18: 'Alt',
+ 19: 'Pause',
+ 20: 'CapsLock',
+ 27: 'Escape',
+ 32: ' ',
+ 33: 'PageUp',
+ 34: 'PageDown',
+ 35: 'End',
+ 36: 'Home',
+ 37: 'ArrowLeft',
+ 38: 'ArrowUp',
+ 39: 'ArrowRight',
+ 40: 'ArrowDown',
+ 45: 'Insert',
+ 46: 'Delete',
+ 112: 'F1', 113: 'F2', 114: 'F3', 115: 'F4', 116: 'F5', 117: 'F6',
+ 118: 'F7', 119: 'F8', 120: 'F9', 121: 'F10', 122: 'F11', 123: 'F12',
+ 144: 'NumLock',
+ 145: 'ScrollLock',
+ 224: 'Meta'
+};
+
+/**
+ * @param {object} nativeEvent Native browser event.
+ * @return {string} Normalized `key` property.
+ */
+function getEventKey(nativeEvent) {
+ if (nativeEvent.key) {
+ // Normalize inconsistent values reported by browsers due to
+ // implementations of a working draft specification.
+
+ // FireFox implements `key` but returns `MozPrintableKey` for all
+ // printable characters (normalized to `Unidentified`), ignore it.
+ var key = normalizeKey[nativeEvent.key] || nativeEvent.key;
+ if (key !== 'Unidentified') {
+ return key;
+ }
+ }
+
+ // Browser does not implement `key`, polyfill as much of it as we can.
+ if (nativeEvent.type === 'keypress') {
+ var charCode = getEventCharCode(nativeEvent);
+
+ // The enter-key is technically both printable and non-printable and can
+ // thus be captured by `keypress`, no other non-printable key should.
+ return charCode === 13 ? 'Enter' : String.fromCharCode(charCode);
+ }
+ if (nativeEvent.type === 'keydown' || nativeEvent.type === 'keyup') {
+ // While user keyboard layout determines the actual meaning of each
+ // `keyCode` value, almost all function keys have a universal value.
+ return translateToKey[nativeEvent.keyCode] || 'Unidentified';
+ }
+ return '';
+}
+
+module.exports = getEventKey;
+},{"125":125}],127:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventModifierState
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Translation from modifier key to the associated property in the event.
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/#keys-Modifiers
+ */
+
+var modifierKeyToProp = {
+ 'Alt': 'altKey',
+ 'Control': 'ctrlKey',
+ 'Meta': 'metaKey',
+ 'Shift': 'shiftKey'
+};
+
+// IE8 does not implement getModifierState so we simply map it to the only
+// modifier keys exposed by the event itself, does not support Lock-keys.
+// Currently, all major browsers except Chrome seems to support Lock-keys.
+function modifierStateGetter(keyArg) {
+ var syntheticEvent = this;
+ var nativeEvent = syntheticEvent.nativeEvent;
+ if (nativeEvent.getModifierState) {
+ return nativeEvent.getModifierState(keyArg);
+ }
+ var keyProp = modifierKeyToProp[keyArg];
+ return keyProp ? !!nativeEvent[keyProp] : false;
+}
+
+function getEventModifierState(nativeEvent) {
+ return modifierStateGetter;
+}
+
+module.exports = getEventModifierState;
+},{}],128:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEventTarget
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Gets the target node from a native browser event by accounting for
+ * inconsistencies in browser DOM APIs.
+ *
+ * @param {object} nativeEvent Native browser event.
+ * @return {DOMEventTarget} Target node.
+ */
+function getEventTarget(nativeEvent) {
+ var target = nativeEvent.target || nativeEvent.srcElement || window;
+ // Safari may fire events on text nodes (Node.TEXT_NODE is 3).
+ // @see http://www.quirksmode.org/js/events_properties.html
+ return target.nodeType === 3 ? target.parentNode : target;
+}
+
+module.exports = getEventTarget;
+},{}],129:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getIteratorFn
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/* global Symbol */
+var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
+var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec.
+
+/**
+ * Returns the iterator method function contained on the iterable object.
+ *
+ * Be sure to invoke the function with the iterable as context:
+ *
+ * var iteratorFn = getIteratorFn(myIterable);
+ * if (iteratorFn) {
+ * var iterator = iteratorFn.call(myIterable);
+ * ...
+ * }
+ *
+ * @param {?object} maybeIterable
+ * @return {?function}
+ */
+function getIteratorFn(maybeIterable) {
+ var iteratorFn = maybeIterable && (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]);
+ if (typeof iteratorFn === 'function') {
+ return iteratorFn;
+ }
+}
+
+module.exports = getIteratorFn;
+},{}],130:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getNodeForCharacterOffset
+ */
+
+'use strict';
+
+/**
+ * Given any node return the first leaf node without children.
+ *
+ * @param {DOMElement|DOMTextNode} node
+ * @return {DOMElement|DOMTextNode}
+ */
+function getLeafNode(node) {
+ while (node && node.firstChild) {
+ node = node.firstChild;
+ }
+ return node;
+}
+
+/**
+ * Get the next sibling within a container. This will walk up the
+ * DOM if a node's siblings have been exhausted.
+ *
+ * @param {DOMElement|DOMTextNode} node
+ * @return {?DOMElement|DOMTextNode}
+ */
+function getSiblingNode(node) {
+ while (node) {
+ if (node.nextSibling) {
+ return node.nextSibling;
+ }
+ node = node.parentNode;
+ }
+}
+
+/**
+ * Get object describing the nodes which contain characters at offset.
+ *
+ * @param {DOMElement|DOMTextNode} root
+ * @param {number} offset
+ * @return {?object}
+ */
+function getNodeForCharacterOffset(root, offset) {
+ var node = getLeafNode(root);
+ var nodeStart = 0;
+ var nodeEnd = 0;
+
+ while (node) {
+ if (node.nodeType === 3) {
+ nodeEnd = nodeStart + node.textContent.length;
+
+ if (nodeStart <= offset && nodeEnd >= offset) {
+ return {
+ node: node,
+ offset: offset - nodeStart
+ };
+ }
+
+ nodeStart = nodeEnd;
+ }
+
+ node = getLeafNode(getSiblingNode(node));
+ }
+}
+
+module.exports = getNodeForCharacterOffset;
+},{}],131:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getTextContentAccessor
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var contentKey = null;
+
+/**
+ * Gets the key used to access text content on a DOM node.
+ *
+ * @return {?string} Key used to access text content.
+ * @internal
+ */
+function getTextContentAccessor() {
+ if (!contentKey && ExecutionEnvironment.canUseDOM) {
+ // Prefer textContent to innerText because many browsers support both but
+ // SVG <text> elements don't support innerText even when <div> does.
+ contentKey = 'textContent' in document.documentElement ? 'textContent' : 'innerText';
+ }
+ return contentKey;
+}
+
+module.exports = getTextContentAccessor;
+},{"147":147}],132:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule instantiateReactComponent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var ReactCompositeComponent = _dereq_(38);
+var ReactEmptyComponent = _dereq_(59);
+var ReactNativeComponent = _dereq_(75);
+
+var assign = _dereq_(24);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+// To avoid a cyclic dependency, we create the final class in this module
+var ReactCompositeComponentWrapper = function () {};
+assign(ReactCompositeComponentWrapper.prototype, ReactCompositeComponent.Mixin, {
+ _instantiateReactComponent: instantiateReactComponent
+});
+
+function getDeclarationErrorAddendum(owner) {
+ if (owner) {
+ var name = owner.getName();
+ if (name) {
+ return ' Check the render method of `' + name + '`.';
+ }
+ }
+ return '';
+}
+
+/**
+ * Check if the type reference is a known internal type. I.e. not a user
+ * provided composite type.
+ *
+ * @param {function} type
+ * @return {boolean} Returns true if this is a valid internal type.
+ */
+function isInternalComponentType(type) {
+ return typeof type === 'function' && typeof type.prototype !== 'undefined' && typeof type.prototype.mountComponent === 'function' && typeof type.prototype.receiveComponent === 'function';
+}
+
+/**
+ * Given a ReactNode, create an instance that will actually be mounted.
+ *
+ * @param {ReactNode} node
+ * @return {object} A new instance of the element's constructor.
+ * @protected
+ */
+function instantiateReactComponent(node) {
+ var instance;
+
+ if (node === null || node === false) {
+ instance = new ReactEmptyComponent(instantiateReactComponent);
+ } else if (typeof node === 'object') {
+ var element = node;
+ !(element && (typeof element.type === 'function' || typeof element.type === 'string')) ? "production" !== 'production' ? invariant(false, 'Element type is invalid: expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: %s.%s', element.type == null ? element.type : typeof element.type, getDeclarationErrorAddendum(element._owner)) : invariant(false) : undefined;
+
+ // Special case string values
+ if (typeof element.type === 'string') {
+ instance = ReactNativeComponent.createInternalComponent(element);
+ } else if (isInternalComponentType(element.type)) {
+ // This is temporarily available for custom components that are not string
+ // representations. I.e. ART. Once those are updated to use the string
+ // representation, we can drop this code path.
+ instance = new element.type(element);
+ } else {
+ instance = new ReactCompositeComponentWrapper();
+ }
+ } else if (typeof node === 'string' || typeof node === 'number') {
+ instance = ReactNativeComponent.createInstanceForText(node);
+ } else {
+ !false ? "production" !== 'production' ? invariant(false, 'Encountered invalid React node of type %s', typeof node) : invariant(false) : undefined;
+ }
+
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(typeof instance.construct === 'function' && typeof instance.mountComponent === 'function' && typeof instance.receiveComponent === 'function' && typeof instance.unmountComponent === 'function', 'Only React Components can be mounted.') : undefined;
+ }
+
+ // Sets up the instance. This can probably just move into the constructor now.
+ instance.construct(node);
+
+ // These two fields are used by the DOM and ART diffing algorithms
+ // respectively. Instead of using expandos on components, we should be
+ // storing the state needed by the diffing algorithms elsewhere.
+ instance._mountIndex = 0;
+ instance._mountImage = null;
+
+ if ("production" !== 'production') {
+ instance._isOwnerNecessary = false;
+ instance._warnedAboutRefsInRender = false;
+ }
+
+ // Internal instances should fully constructed at this point, so they should
+ // not get any new fields added to them at this point.
+ if ("production" !== 'production') {
+ if (Object.preventExtensions) {
+ Object.preventExtensions(instance);
+ }
+ }
+
+ return instance;
+}
+
+module.exports = instantiateReactComponent;
+},{"161":161,"173":173,"24":24,"38":38,"59":59,"75":75}],133:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isEventSupported
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var useHasFeature;
+if (ExecutionEnvironment.canUseDOM) {
+ useHasFeature = document.implementation && document.implementation.hasFeature &&
+ // always returns true in newer browsers as per the standard.
+ // @see http://dom.spec.whatwg.org/#dom-domimplementation-hasfeature
+ document.implementation.hasFeature('', '') !== true;
+}
+
+/**
+ * Checks if an event is supported in the current execution environment.
+ *
+ * NOTE: This will not work correctly for non-generic events such as `change`,
+ * `reset`, `load`, `error`, and `select`.
+ *
+ * Borrows from Modernizr.
+ *
+ * @param {string} eventNameSuffix Event name, e.g. "click".
+ * @param {?boolean} capture Check if the capture phase is supported.
+ * @return {boolean} True if the event is supported.
+ * @internal
+ * @license Modernizr 3.0.0pre (Custom Build) | MIT
+ */
+function isEventSupported(eventNameSuffix, capture) {
+ if (!ExecutionEnvironment.canUseDOM || capture && !('addEventListener' in document)) {
+ return false;
+ }
+
+ var eventName = 'on' + eventNameSuffix;
+ var isSupported = (eventName in document);
+
+ if (!isSupported) {
+ var element = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ element.setAttribute(eventName, 'return;');
+ isSupported = typeof element[eventName] === 'function';
+ }
+
+ if (!isSupported && useHasFeature && eventNameSuffix === 'wheel') {
+ // This is the only way to test support for the `wheel` event in IE9+.
+ isSupported = document.implementation.hasFeature('Events.wheel', '3.0');
+ }
+
+ return isSupported;
+}
+
+module.exports = isEventSupported;
+},{"147":147}],134:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isTextInputElement
+ */
+
+'use strict';
+
+/**
+ * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary
+ */
+var supportedInputTypes = {
+ 'color': true,
+ 'date': true,
+ 'datetime': true,
+ 'datetime-local': true,
+ 'email': true,
+ 'month': true,
+ 'number': true,
+ 'password': true,
+ 'range': true,
+ 'search': true,
+ 'tel': true,
+ 'text': true,
+ 'time': true,
+ 'url': true,
+ 'week': true
+};
+
+function isTextInputElement(elem) {
+ var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
+ return nodeName && (nodeName === 'input' && supportedInputTypes[elem.type] || nodeName === 'textarea');
+}
+
+module.exports = isTextInputElement;
+},{}],135:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule onlyChild
+ */
+'use strict';
+
+var ReactElement = _dereq_(57);
+
+var invariant = _dereq_(161);
+
+/**
+ * Returns the first child in a collection of children and verifies that there
+ * is only one child in the collection. The current implementation of this
+ * function assumes that a single child gets passed without a wrapper, but the
+ * purpose of this helper function is to abstract away the particular structure
+ * of children.
+ *
+ * @param {?object} children Child collection structure.
+ * @return {ReactComponent} The first and only `ReactComponent` contained in the
+ * structure.
+ */
+function onlyChild(children) {
+ !ReactElement.isValidElement(children) ? "production" !== 'production' ? invariant(false, 'onlyChild must be passed a children with exactly one child.') : invariant(false) : undefined;
+ return children;
+}
+
+module.exports = onlyChild;
+},{"161":161,"57":57}],136:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule quoteAttributeValueForBrowser
+ */
+
+'use strict';
+
+var escapeTextContentForBrowser = _dereq_(121);
+
+/**
+ * Escapes attribute value to prevent scripting attacks.
+ *
+ * @param {*} value Value to escape.
+ * @return {string} An escaped string.
+ */
+function quoteAttributeValueForBrowser(value) {
+ return '"' + escapeTextContentForBrowser(value) + '"';
+}
+
+module.exports = quoteAttributeValueForBrowser;
+},{"121":121}],137:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+* @providesModule renderSubtreeIntoContainer
+*/
+
+'use strict';
+
+var ReactMount = _dereq_(72);
+
+module.exports = ReactMount.renderSubtreeIntoContainer;
+},{"72":72}],138:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule setInnerHTML
+ */
+
+/* globals MSApp */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var WHITESPACE_TEST = /^[ \r\n\t\f]/;
+var NONVISIBLE_TEST = /<(!--|link|noscript|meta|script|style)[ \r\n\t\f\/>]/;
+
+/**
+ * Set the innerHTML property of a node, ensuring that whitespace is preserved
+ * even in IE8.
+ *
+ * @param {DOMElement} node
+ * @param {string} html
+ * @internal
+ */
+var setInnerHTML = function (node, html) {
+ node.innerHTML = html;
+};
+
+// Win8 apps: Allow all html to be inserted
+if (typeof MSApp !== 'undefined' && MSApp.execUnsafeLocalFunction) {
+ setInnerHTML = function (node, html) {
+ MSApp.execUnsafeLocalFunction(function () {
+ node.innerHTML = html;
+ });
+ };
+}
+
+if (ExecutionEnvironment.canUseDOM) {
+ // IE8: When updating a just created node with innerHTML only leading
+ // whitespace is removed. When updating an existing node with innerHTML
+ // whitespace in root TextNodes is also collapsed.
+ // @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html
+
+ // Feature detection; only IE8 is known to behave improperly like this.
+ var testElement = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ testElement.innerHTML = ' ';
+ if (testElement.innerHTML === '') {
+ setInnerHTML = function (node, html) {
+ // Magic theory: IE8 supposedly differentiates between added and updated
+ // nodes when processing innerHTML, innerHTML on updated nodes suffers
+ // from worse whitespace behavior. Re-adding a node like this triggers
+ // the initial and more favorable whitespace behavior.
+ // TODO: What to do on a detached node?
+ if (node.parentNode) {
+ node.parentNode.replaceChild(node, node);
+ }
+
+ // We also implement a workaround for non-visible tags disappearing into
+ // thin air on IE8, this only happens if there is no visible text
+ // in-front of the non-visible tags. Piggyback on the whitespace fix
+ // and simply check if any non-visible tags appear in the source.
+ if (WHITESPACE_TEST.test(html) || html[0] === '<' && NONVISIBLE_TEST.test(html)) {
+ // Recover leading whitespace by temporarily prepending any character.
+ // \uFEFF has the potential advantage of being zero-width/invisible.
+ // UglifyJS drops U+FEFF chars when parsing, so use String.fromCharCode
+ // in hopes that this is preserved even if "\uFEFF" is transformed to
+ // the actual Unicode character (by Babel, for example).
+ // https://github.com/mishoo/UglifyJS2/blob/v2.4.20/lib/parse.js#L216
+ node.innerHTML = String.fromCharCode(0xFEFF) + html;
+
+ // deleteData leaves an empty `TextNode` which offsets the index of all
+ // children. Definitely want to avoid this.
+ var textNode = node.firstChild;
+ if (textNode.data.length === 1) {
+ node.removeChild(textNode);
+ } else {
+ textNode.deleteData(0, 1);
+ }
+ } else {
+ node.innerHTML = html;
+ }
+ };
+ }
+}
+
+module.exports = setInnerHTML;
+},{"147":147}],139:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule setTextContent
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+var escapeTextContentForBrowser = _dereq_(121);
+var setInnerHTML = _dereq_(138);
+
+/**
+ * Set the textContent property of a node, ensuring that whitespace is preserved
+ * even in IE8. innerText is a poor substitute for textContent and, among many
+ * issues, inserts <br> instead of the literal newline chars. innerHTML behaves
+ * as it should.
+ *
+ * @param {DOMElement} node
+ * @param {string} text
+ * @internal
+ */
+var setTextContent = function (node, text) {
+ node.textContent = text;
+};
+
+if (ExecutionEnvironment.canUseDOM) {
+ if (!('textContent' in document.documentElement)) {
+ setTextContent = function (node, text) {
+ setInnerHTML(node, escapeTextContentForBrowser(text));
+ };
+ }
+}
+
+module.exports = setTextContent;
+},{"121":121,"138":138,"147":147}],140:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+* @providesModule shallowCompare
+*/
+
+'use strict';
+
+var shallowEqual = _dereq_(171);
+
+/**
+ * Does a shallow comparison for props and state.
+ * See ReactComponentWithPureRenderMixin
+ */
+function shallowCompare(instance, nextProps, nextState) {
+ return !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState);
+}
+
+module.exports = shallowCompare;
+},{"171":171}],141:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule shouldUpdateReactComponent
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Given a `prevElement` and `nextElement`, determines if the existing
+ * instance should be updated as opposed to being destroyed or replaced by a new
+ * instance. Both arguments are elements. This ensures that this logic can
+ * operate on stateless trees without any backing instance.
+ *
+ * @param {?object} prevElement
+ * @param {?object} nextElement
+ * @return {boolean} True if the existing instance should be updated.
+ * @protected
+ */
+function shouldUpdateReactComponent(prevElement, nextElement) {
+ var prevEmpty = prevElement === null || prevElement === false;
+ var nextEmpty = nextElement === null || nextElement === false;
+ if (prevEmpty || nextEmpty) {
+ return prevEmpty === nextEmpty;
+ }
+
+ var prevType = typeof prevElement;
+ var nextType = typeof nextElement;
+ if (prevType === 'string' || prevType === 'number') {
+ return nextType === 'string' || nextType === 'number';
+ } else {
+ return nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key;
+ }
+ return false;
+}
+
+module.exports = shouldUpdateReactComponent;
+},{}],142:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule traverseAllChildren
+ */
+
+'use strict';
+
+var ReactCurrentOwner = _dereq_(39);
+var ReactElement = _dereq_(57);
+var ReactInstanceHandles = _dereq_(67);
+
+var getIteratorFn = _dereq_(129);
+var invariant = _dereq_(161);
+var warning = _dereq_(173);
+
+var SEPARATOR = ReactInstanceHandles.SEPARATOR;
+var SUBSEPARATOR = ':';
+
+/**
+ * TODO: Test that a single child and an array with one item have the same key
+ * pattern.
+ */
+
+var userProvidedKeyEscaperLookup = {
+ '=': '=0',
+ '.': '=1',
+ ':': '=2'
+};
+
+var userProvidedKeyEscapeRegex = /[=.:]/g;
+
+var didWarnAboutMaps = false;
+
+function userProvidedKeyEscaper(match) {
+ return userProvidedKeyEscaperLookup[match];
+}
+
+/**
+ * Generate a key string that identifies a component within a set.
+ *
+ * @param {*} component A component that could contain a manual key.
+ * @param {number} index Index that is used if a manual key is not provided.
+ * @return {string}
+ */
+function getComponentKey(component, index) {
+ if (component && component.key != null) {
+ // Explicit key
+ return wrapUserProvidedKey(component.key);
+ }
+ // Implicit key determined by the index in the set
+ return index.toString(36);
+}
+
+/**
+ * Escape a component key so that it is safe to use in a reactid.
+ *
+ * @param {*} text Component key to be escaped.
+ * @return {string} An escaped string.
+ */
+function escapeUserProvidedKey(text) {
+ return ('' + text).replace(userProvidedKeyEscapeRegex, userProvidedKeyEscaper);
+}
+
+/**
+ * Wrap a `key` value explicitly provided by the user to distinguish it from
+ * implicitly-generated keys generated by a component's index in its parent.
+ *
+ * @param {string} key Value of a user-provided `key` attribute
+ * @return {string}
+ */
+function wrapUserProvidedKey(key) {
+ return '$' + escapeUserProvidedKey(key);
+}
+
+/**
+ * @param {?*} children Children tree container.
+ * @param {!string} nameSoFar Name of the key path so far.
+ * @param {!function} callback Callback to invoke with each child found.
+ * @param {?*} traverseContext Used to pass information throughout the traversal
+ * process.
+ * @return {!number} The number of children in this subtree.
+ */
+function traverseAllChildrenImpl(children, nameSoFar, callback, traverseContext) {
+ var type = typeof children;
+
+ if (type === 'undefined' || type === 'boolean') {
+ // All of the above are perceived as null.
+ children = null;
+ }
+
+ if (children === null || type === 'string' || type === 'number' || ReactElement.isValidElement(children)) {
+ callback(traverseContext, children,
+ // If it's the only child, treat the name as if it was wrapped in an array
+ // so that it's consistent if the number of children grows.
+ nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar);
+ return 1;
+ }
+
+ var child;
+ var nextName;
+ var subtreeCount = 0; // Count of children found in the current subtree.
+ var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
+
+ if (Array.isArray(children)) {
+ for (var i = 0; i < children.length; i++) {
+ child = children[i];
+ nextName = nextNamePrefix + getComponentKey(child, i);
+ subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
+ }
+ } else {
+ var iteratorFn = getIteratorFn(children);
+ if (iteratorFn) {
+ var iterator = iteratorFn.call(children);
+ var step;
+ if (iteratorFn !== children.entries) {
+ var ii = 0;
+ while (!(step = iterator.next()).done) {
+ child = step.value;
+ nextName = nextNamePrefix + getComponentKey(child, ii++);
+ subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
+ }
+ } else {
+ if ("production" !== 'production') {
+ "production" !== 'production' ? warning(didWarnAboutMaps, 'Using Maps as children is not yet fully supported. It is an ' + 'experimental feature that might be removed. Convert it to a ' + 'sequence / iterable of keyed ReactElements instead.') : undefined;
+ didWarnAboutMaps = true;
+ }
+ // Iterator will provide entry [k,v] tuples rather than values.
+ while (!(step = iterator.next()).done) {
+ var entry = step.value;
+ if (entry) {
+ child = entry[1];
+ nextName = nextNamePrefix + wrapUserProvidedKey(entry[0]) + SUBSEPARATOR + getComponentKey(child, 0);
+ subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
+ }
+ }
+ }
+ } else if (type === 'object') {
+ var addendum = '';
+ if ("production" !== 'production') {
+ addendum = ' If you meant to render a collection of children, use an array ' + 'instead or wrap the object using createFragment(object) from the ' + 'React add-ons.';
+ if (children._isReactElement) {
+ addendum = ' It looks like you\'re using an element created by a different ' + 'version of React. Make sure to use only one copy of React.';
+ }
+ if (ReactCurrentOwner.current) {
+ var name = ReactCurrentOwner.current.getName();
+ if (name) {
+ addendum += ' Check the render method of `' + name + '`.';
+ }
+ }
+ }
+ var childrenString = String(children);
+ !false ? "production" !== 'production' ? invariant(false, 'Objects are not valid as a React child (found: %s).%s', childrenString === '[object Object]' ? 'object with keys {' + Object.keys(children).join(', ') + '}' : childrenString, addendum) : invariant(false) : undefined;
+ }
+ }
+
+ return subtreeCount;
+}
+
+/**
+ * Traverses children that are typically specified as `props.children`, but
+ * might also be specified through attributes:
+ *
+ * - `traverseAllChildren(this.props.children, ...)`
+ * - `traverseAllChildren(this.props.leftPanelChildren, ...)`
+ *
+ * The `traverseContext` is an optional argument that is passed through the
+ * entire traversal. It can be used to store accumulations or anything else that
+ * the callback might find relevant.
+ *
+ * @param {?*} children Children tree object.
+ * @param {!function} callback To invoke upon traversing each child.
+ * @param {?*} traverseContext Context for traversal.
+ * @return {!number} The number of children in this subtree.
+ */
+function traverseAllChildren(children, callback, traverseContext) {
+ if (children == null) {
+ return 0;
+ }
+
+ return traverseAllChildrenImpl(children, '', callback, traverseContext);
+}
+
+module.exports = traverseAllChildren;
+},{"129":129,"161":161,"173":173,"39":39,"57":57,"67":67}],143:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule update
+ */
+
+/* global hasOwnProperty:true */
+
+'use strict';
+
+var assign = _dereq_(24);
+var keyOf = _dereq_(166);
+var invariant = _dereq_(161);
+var hasOwnProperty = ({}).hasOwnProperty;
+
+function shallowCopy(x) {
+ if (Array.isArray(x)) {
+ return x.concat();
+ } else if (x && typeof x === 'object') {
+ return assign(new x.constructor(), x);
+ } else {
+ return x;
+ }
+}
+
+var COMMAND_PUSH = keyOf({ $push: null });
+var COMMAND_UNSHIFT = keyOf({ $unshift: null });
+var COMMAND_SPLICE = keyOf({ $splice: null });
+var COMMAND_SET = keyOf({ $set: null });
+var COMMAND_MERGE = keyOf({ $merge: null });
+var COMMAND_APPLY = keyOf({ $apply: null });
+
+var ALL_COMMANDS_LIST = [COMMAND_PUSH, COMMAND_UNSHIFT, COMMAND_SPLICE, COMMAND_SET, COMMAND_MERGE, COMMAND_APPLY];
+
+var ALL_COMMANDS_SET = {};
+
+ALL_COMMANDS_LIST.forEach(function (command) {
+ ALL_COMMANDS_SET[command] = true;
+});
+
+function invariantArrayCase(value, spec, command) {
+ !Array.isArray(value) ? "production" !== 'production' ? invariant(false, 'update(): expected target of %s to be an array; got %s.', command, value) : invariant(false) : undefined;
+ var specValue = spec[command];
+ !Array.isArray(specValue) ? "production" !== 'production' ? invariant(false, 'update(): expected spec of %s to be an array; got %s. ' + 'Did you forget to wrap your parameter in an array?', command, specValue) : invariant(false) : undefined;
+}
+
+function update(value, spec) {
+ !(typeof spec === 'object') ? "production" !== 'production' ? invariant(false, 'update(): You provided a key path to update() that did not contain one ' + 'of %s. Did you forget to include {%s: ...}?', ALL_COMMANDS_LIST.join(', '), COMMAND_SET) : invariant(false) : undefined;
+
+ if (hasOwnProperty.call(spec, COMMAND_SET)) {
+ !(Object.keys(spec).length === 1) ? "production" !== 'production' ? invariant(false, 'Cannot have more than one key in an object with %s', COMMAND_SET) : invariant(false) : undefined;
+
+ return spec[COMMAND_SET];
+ }
+
+ var nextValue = shallowCopy(value);
+
+ if (hasOwnProperty.call(spec, COMMAND_MERGE)) {
+ var mergeObj = spec[COMMAND_MERGE];
+ !(mergeObj && typeof mergeObj === 'object') ? "production" !== 'production' ? invariant(false, 'update(): %s expects a spec of type \'object\'; got %s', COMMAND_MERGE, mergeObj) : invariant(false) : undefined;
+ !(nextValue && typeof nextValue === 'object') ? "production" !== 'production' ? invariant(false, 'update(): %s expects a target of type \'object\'; got %s', COMMAND_MERGE, nextValue) : invariant(false) : undefined;
+ assign(nextValue, spec[COMMAND_MERGE]);
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_PUSH)) {
+ invariantArrayCase(value, spec, COMMAND_PUSH);
+ spec[COMMAND_PUSH].forEach(function (item) {
+ nextValue.push(item);
+ });
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_UNSHIFT)) {
+ invariantArrayCase(value, spec, COMMAND_UNSHIFT);
+ spec[COMMAND_UNSHIFT].forEach(function (item) {
+ nextValue.unshift(item);
+ });
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_SPLICE)) {
+ !Array.isArray(value) ? "production" !== 'production' ? invariant(false, 'Expected %s target to be an array; got %s', COMMAND_SPLICE, value) : invariant(false) : undefined;
+ !Array.isArray(spec[COMMAND_SPLICE]) ? "production" !== 'production' ? invariant(false, 'update(): expected spec of %s to be an array of arrays; got %s. ' + 'Did you forget to wrap your parameters in an array?', COMMAND_SPLICE, spec[COMMAND_SPLICE]) : invariant(false) : undefined;
+ spec[COMMAND_SPLICE].forEach(function (args) {
+ !Array.isArray(args) ? "production" !== 'production' ? invariant(false, 'update(): expected spec of %s to be an array of arrays; got %s. ' + 'Did you forget to wrap your parameters in an array?', COMMAND_SPLICE, spec[COMMAND_SPLICE]) : invariant(false) : undefined;
+ nextValue.splice.apply(nextValue, args);
+ });
+ }
+
+ if (hasOwnProperty.call(spec, COMMAND_APPLY)) {
+ !(typeof spec[COMMAND_APPLY] === 'function') ? "production" !== 'production' ? invariant(false, 'update(): expected spec of %s to be a function; got %s.', COMMAND_APPLY, spec[COMMAND_APPLY]) : invariant(false) : undefined;
+ nextValue = spec[COMMAND_APPLY](nextValue);
+ }
+
+ for (var k in spec) {
+ if (!(ALL_COMMANDS_SET.hasOwnProperty(k) && ALL_COMMANDS_SET[k])) {
+ nextValue[k] = update(value[k], spec[k]);
+ }
+ }
+
+ return nextValue;
+}
+
+module.exports = update;
+},{"161":161,"166":166,"24":24}],144:[function(_dereq_,module,exports){
+/**
+ * Copyright 2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule validateDOMNesting
+ */
+
+'use strict';
+
+var assign = _dereq_(24);
+var emptyFunction = _dereq_(153);
+var warning = _dereq_(173);
+
+var validateDOMNesting = emptyFunction;
+
+if ("production" !== 'production') {
+ // This validation code was written based on the HTML5 parsing spec:
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
+ //
+ // Note: this does not catch all invalid nesting, nor does it try to (as it's
+ // not clear what practical benefit doing so provides); instead, we warn only
+ // for cases where the parser will give a parse tree differing from what React
+ // intended. For example, <b><div></div></b> is invalid but we don't warn
+ // because it still parses correctly; we do warn for other cases like nested
+ // <p> tags where the beginning of the second element implicitly closes the
+ // first, causing a confusing mess.
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#special
+ var specialTags = ['address', 'applet', 'area', 'article', 'aside', 'base', 'basefont', 'bgsound', 'blockquote', 'body', 'br', 'button', 'caption', 'center', 'col', 'colgroup', 'dd', 'details', 'dir', 'div', 'dl', 'dt', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'iframe', 'img', 'input', 'isindex', 'li', 'link', 'listing', 'main', 'marquee', 'menu', 'menuitem', 'meta', 'nav', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'p', 'param', 'plaintext', 'pre', 'script', 'section', 'select', 'source', 'style', 'summary', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'track', 'ul', 'wbr', 'xmp'];
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
+ var inScopeTags = ['applet', 'caption', 'html', 'table', 'td', 'th', 'marquee', 'object', 'template',
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point
+ // TODO: Distinguish by namespace here -- for <title>, including it here
+ // errs on the side of fewer warnings
+ 'foreignObject', 'desc', 'title'];
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope
+ var buttonScopeTags = inScopeTags.concat(['button']);
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
+ var impliedEndTags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
+
+ var emptyAncestorInfo = {
+ parentTag: null,
+
+ formTag: null,
+ aTagInScope: null,
+ buttonTagInScope: null,
+ nobrTagInScope: null,
+ pTagInButtonScope: null,
+
+ listItemTagAutoclosing: null,
+ dlItemTagAutoclosing: null
+ };
+
+ var updatedAncestorInfo = function (oldInfo, tag, instance) {
+ var ancestorInfo = assign({}, oldInfo || emptyAncestorInfo);
+ var info = { tag: tag, instance: instance };
+
+ if (inScopeTags.indexOf(tag) !== -1) {
+ ancestorInfo.aTagInScope = null;
+ ancestorInfo.buttonTagInScope = null;
+ ancestorInfo.nobrTagInScope = null;
+ }
+ if (buttonScopeTags.indexOf(tag) !== -1) {
+ ancestorInfo.pTagInButtonScope = null;
+ }
+
+ // See rules for 'li', 'dd', 'dt' start tags in
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
+ if (specialTags.indexOf(tag) !== -1 && tag !== 'address' && tag !== 'div' && tag !== 'p') {
+ ancestorInfo.listItemTagAutoclosing = null;
+ ancestorInfo.dlItemTagAutoclosing = null;
+ }
+
+ ancestorInfo.parentTag = info;
+
+ if (tag === 'form') {
+ ancestorInfo.formTag = info;
+ }
+ if (tag === 'a') {
+ ancestorInfo.aTagInScope = info;
+ }
+ if (tag === 'button') {
+ ancestorInfo.buttonTagInScope = info;
+ }
+ if (tag === 'nobr') {
+ ancestorInfo.nobrTagInScope = info;
+ }
+ if (tag === 'p') {
+ ancestorInfo.pTagInButtonScope = info;
+ }
+ if (tag === 'li') {
+ ancestorInfo.listItemTagAutoclosing = info;
+ }
+ if (tag === 'dd' || tag === 'dt') {
+ ancestorInfo.dlItemTagAutoclosing = info;
+ }
+
+ return ancestorInfo;
+ };
+
+ /**
+ * Returns whether
+ */
+ var isTagValidWithParent = function (tag, parentTag) {
+ // First, let's check if we're in an unusual parsing mode...
+ switch (parentTag) {
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
+ case 'select':
+ return tag === 'option' || tag === 'optgroup' || tag === '#text';
+ case 'optgroup':
+ return tag === 'option' || tag === '#text';
+ // Strictly speaking, seeing an <option> doesn't mean we're in a <select>
+ // but
+ case 'option':
+ return tag === '#text';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
+ // No special behavior since these rules fall back to "in body" mode for
+ // all except special table nodes which cause bad parsing behavior anyway.
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
+ case 'tr':
+ return tag === 'th' || tag === 'td' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
+ case 'tbody':
+ case 'thead':
+ case 'tfoot':
+ return tag === 'tr' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
+ case 'colgroup':
+ return tag === 'col' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
+ case 'table':
+ return tag === 'caption' || tag === 'colgroup' || tag === 'tbody' || tag === 'tfoot' || tag === 'thead' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
+ case 'head':
+ return tag === 'base' || tag === 'basefont' || tag === 'bgsound' || tag === 'link' || tag === 'meta' || tag === 'title' || tag === 'noscript' || tag === 'noframes' || tag === 'style' || tag === 'script' || tag === 'template';
+
+ // https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
+ case 'html':
+ return tag === 'head' || tag === 'body';
+ }
+
+ // Probably in the "in body" parsing mode, so we outlaw only tag combos
+ // where the parsing rules cause implicit opens or closes to be added.
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
+ switch (tag) {
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ return parentTag !== 'h1' && parentTag !== 'h2' && parentTag !== 'h3' && parentTag !== 'h4' && parentTag !== 'h5' && parentTag !== 'h6';
+
+ case 'rp':
+ case 'rt':
+ return impliedEndTags.indexOf(parentTag) === -1;
+
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ case 'frame':
+ case 'head':
+ case 'tbody':
+ case 'td':
+ case 'tfoot':
+ case 'th':
+ case 'thead':
+ case 'tr':
+ // These tags are only valid with a few parents that have special child
+ // parsing rules -- if we're down here, then none of those matched and
+ // so we allow it only if we don't know what the parent is, as all other
+ // cases are invalid.
+ return parentTag == null;
+ }
+
+ return true;
+ };
+
+ /**
+ * Returns whether
+ */
+ var findInvalidAncestorForTag = function (tag, ancestorInfo) {
+ switch (tag) {
+ case 'address':
+ case 'article':
+ case 'aside':
+ case 'blockquote':
+ case 'center':
+ case 'details':
+ case 'dialog':
+ case 'dir':
+ case 'div':
+ case 'dl':
+ case 'fieldset':
+ case 'figcaption':
+ case 'figure':
+ case 'footer':
+ case 'header':
+ case 'hgroup':
+ case 'main':
+ case 'menu':
+ case 'nav':
+ case 'ol':
+ case 'p':
+ case 'section':
+ case 'summary':
+ case 'ul':
+
+ case 'pre':
+ case 'listing':
+
+ case 'table':
+
+ case 'hr':
+
+ case 'xmp':
+
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ return ancestorInfo.pTagInButtonScope;
+
+ case 'form':
+ return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;
+
+ case 'li':
+ return ancestorInfo.listItemTagAutoclosing;
+
+ case 'dd':
+ case 'dt':
+ return ancestorInfo.dlItemTagAutoclosing;
+
+ case 'button':
+ return ancestorInfo.buttonTagInScope;
+
+ case 'a':
+ // Spec says something about storing a list of markers, but it sounds
+ // equivalent to this check.
+ return ancestorInfo.aTagInScope;
+
+ case 'nobr':
+ return ancestorInfo.nobrTagInScope;
+ }
+
+ return null;
+ };
+
+ /**
+ * Given a ReactCompositeComponent instance, return a list of its recursive
+ * owners, starting at the root and ending with the instance itself.
+ */
+ var findOwnerStack = function (instance) {
+ if (!instance) {
+ return [];
+ }
+
+ var stack = [];
+ /*eslint-disable space-after-keywords */
+ do {
+ /*eslint-enable space-after-keywords */
+ stack.push(instance);
+ } while (instance = instance._currentElement._owner);
+ stack.reverse();
+ return stack;
+ };
+
+ var didWarn = {};
+
+ validateDOMNesting = function (childTag, childInstance, ancestorInfo) {
+ ancestorInfo = ancestorInfo || emptyAncestorInfo;
+ var parentInfo = ancestorInfo.parentTag;
+ var parentTag = parentInfo && parentInfo.tag;
+
+ var invalidParent = isTagValidWithParent(childTag, parentTag) ? null : parentInfo;
+ var invalidAncestor = invalidParent ? null : findInvalidAncestorForTag(childTag, ancestorInfo);
+ var problematic = invalidParent || invalidAncestor;
+
+ if (problematic) {
+ var ancestorTag = problematic.tag;
+ var ancestorInstance = problematic.instance;
+
+ var childOwner = childInstance && childInstance._currentElement._owner;
+ var ancestorOwner = ancestorInstance && ancestorInstance._currentElement._owner;
+
+ var childOwners = findOwnerStack(childOwner);
+ var ancestorOwners = findOwnerStack(ancestorOwner);
+
+ var minStackLen = Math.min(childOwners.length, ancestorOwners.length);
+ var i;
+
+ var deepestCommon = -1;
+ for (i = 0; i < minStackLen; i++) {
+ if (childOwners[i] === ancestorOwners[i]) {
+ deepestCommon = i;
+ } else {
+ break;
+ }
+ }
+
+ var UNKNOWN = '(unknown)';
+ var childOwnerNames = childOwners.slice(deepestCommon + 1).map(function (inst) {
+ return inst.getName() || UNKNOWN;
+ });
+ var ancestorOwnerNames = ancestorOwners.slice(deepestCommon + 1).map(function (inst) {
+ return inst.getName() || UNKNOWN;
+ });
+ var ownerInfo = [].concat(
+ // If the parent and child instances have a common owner ancestor, start
+ // with that -- otherwise we just start with the parent's owners.
+ deepestCommon !== -1 ? childOwners[deepestCommon].getName() || UNKNOWN : [], ancestorOwnerNames, ancestorTag,
+ // If we're warning about an invalid (non-parent) ancestry, add '...'
+ invalidAncestor ? ['...'] : [], childOwnerNames, childTag).join(' > ');
+
+ var warnKey = !!invalidParent + '|' + childTag + '|' + ancestorTag + '|' + ownerInfo;
+ if (didWarn[warnKey]) {
+ return;
+ }
+ didWarn[warnKey] = true;
+
+ if (invalidParent) {
+ var info = '';
+ if (ancestorTag === 'table' && childTag === 'tr') {
+ info += ' Add a <tbody> to your code to match the DOM tree generated by ' + 'the browser.';
+ }
+ "production" !== 'production' ? warning(false, 'validateDOMNesting(...): <%s> cannot appear as a child of <%s>. ' + 'See %s.%s', childTag, ancestorTag, ownerInfo, info) : undefined;
+ } else {
+ "production" !== 'production' ? warning(false, 'validateDOMNesting(...): <%s> cannot appear as a descendant of ' + '<%s>. See %s.', childTag, ancestorTag, ownerInfo) : undefined;
+ }
+ }
+ };
+
+ validateDOMNesting.ancestorInfoContextKey = '__validateDOMNesting_ancestorInfo$' + Math.random().toString(36).slice(2);
+
+ validateDOMNesting.updatedAncestorInfo = updatedAncestorInfo;
+
+ // For testing
+ validateDOMNesting.isTagValidInContext = function (tag, ancestorInfo) {
+ ancestorInfo = ancestorInfo || emptyAncestorInfo;
+ var parentInfo = ancestorInfo.parentTag;
+ var parentTag = parentInfo && parentInfo.tag;
+ return isTagValidWithParent(tag, parentTag) && !findInvalidAncestorForTag(tag, ancestorInfo);
+ };
+}
+
+module.exports = validateDOMNesting;
+},{"153":153,"173":173,"24":24}],145:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CSSCore
+ * @typechecks
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * The CSSCore module specifies the API (and implements most of the methods)
+ * that should be used when dealing with the display of elements (via their
+ * CSS classes and visibility on screen. It is an API focused on mutating the
+ * display and not reading it as no logical state should be encoded in the
+ * display of elements.
+ */
+
+var CSSCore = {
+
+ /**
+ * Adds the class passed in to the element if it doesn't already have it.
+ *
+ * @param {DOMElement} element the element to set the class on
+ * @param {string} className the CSS className
+ * @return {DOMElement} the element passed in
+ */
+ addClass: function (element, className) {
+ !!/\s/.test(className) ? "production" !== 'production' ? invariant(false, 'CSSCore.addClass takes only a single class name. "%s" contains ' + 'multiple classes.', className) : invariant(false) : undefined;
+
+ if (className) {
+ if (element.classList) {
+ element.classList.add(className);
+ } else if (!CSSCore.hasClass(element, className)) {
+ element.className = element.className + ' ' + className;
+ }
+ }
+ return element;
+ },
+
+ /**
+ * Removes the class passed in from the element
+ *
+ * @param {DOMElement} element the element to set the class on
+ * @param {string} className the CSS className
+ * @return {DOMElement} the element passed in
+ */
+ removeClass: function (element, className) {
+ !!/\s/.test(className) ? "production" !== 'production' ? invariant(false, 'CSSCore.removeClass takes only a single class name. "%s" contains ' + 'multiple classes.', className) : invariant(false) : undefined;
+
+ if (className) {
+ if (element.classList) {
+ element.classList.remove(className);
+ } else if (CSSCore.hasClass(element, className)) {
+ element.className = element.className.replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), '$1').replace(/\s+/g, ' ') // multiple spaces to one
+ .replace(/^\s*|\s*$/g, ''); // trim the ends
+ }
+ }
+ return element;
+ },
+
+ /**
+ * Helper to add or remove a class from an element based on a condition.
+ *
+ * @param {DOMElement} element the element to set the class on
+ * @param {string} className the CSS className
+ * @param {*} bool condition to whether to add or remove the class
+ * @return {DOMElement} the element passed in
+ */
+ conditionClass: function (element, className, bool) {
+ return (bool ? CSSCore.addClass : CSSCore.removeClass)(element, className);
+ },
+
+ /**
+ * Tests whether the element has the class specified.
+ *
+ * @param {DOMNode|DOMWindow} element the element to set the class on
+ * @param {string} className the CSS className
+ * @return {boolean} true if the element has the class, false if not
+ */
+ hasClass: function (element, className) {
+ !!/\s/.test(className) ? "production" !== 'production' ? invariant(false, 'CSS.hasClass takes only a single class name.') : invariant(false) : undefined;
+ if (element.classList) {
+ return !!className && element.classList.contains(className);
+ }
+ return (' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1;
+ }
+
+};
+
+module.exports = CSSCore;
+},{"161":161}],146:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @providesModule EventListener
+ * @typechecks
+ */
+
+'use strict';
+
+var emptyFunction = _dereq_(153);
+
+/**
+ * Upstream version of event listener. Does not take into account specific
+ * nature of platform.
+ */
+var EventListener = {
+ /**
+ * Listen to DOM events during the bubble phase.
+ *
+ * @param {DOMEventTarget} target DOM element to register listener on.
+ * @param {string} eventType Event type, e.g. 'click' or 'mouseover'.
+ * @param {function} callback Callback function.
+ * @return {object} Object with a `remove` method.
+ */
+ listen: function (target, eventType, callback) {
+ if (target.addEventListener) {
+ target.addEventListener(eventType, callback, false);
+ return {
+ remove: function () {
+ target.removeEventListener(eventType, callback, false);
+ }
+ };
+ } else if (target.attachEvent) {
+ target.attachEvent('on' + eventType, callback);
+ return {
+ remove: function () {
+ target.detachEvent('on' + eventType, callback);
+ }
+ };
+ }
+ },
+
+ /**
+ * Listen to DOM events during the capture phase.
+ *
+ * @param {DOMEventTarget} target DOM element to register listener on.
+ * @param {string} eventType Event type, e.g. 'click' or 'mouseover'.
+ * @param {function} callback Callback function.
+ * @return {object} Object with a `remove` method.
+ */
+ capture: function (target, eventType, callback) {
+ if (target.addEventListener) {
+ target.addEventListener(eventType, callback, true);
+ return {
+ remove: function () {
+ target.removeEventListener(eventType, callback, true);
+ }
+ };
+ } else {
+ if ("production" !== 'production') {
+ console.error('Attempted to listen to events during the capture phase on a ' + 'browser that does not support the capture phase. Your application ' + 'will not receive some events.');
+ }
+ return {
+ remove: emptyFunction
+ };
+ }
+ },
+
+ registerDefault: function () {}
+};
+
+module.exports = EventListener;
+},{"153":153}],147:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ExecutionEnvironment
+ */
+
+'use strict';
+
+var canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
+
+/**
+ * Simple, lightweight module assisting with the detection and context of
+ * Worker. Helps avoid circular dependencies and allows code to reason about
+ * whether or not they are in a Worker, even if they never include the main
+ * `ReactWorker` dependency.
+ */
+var ExecutionEnvironment = {
+
+ canUseDOM: canUseDOM,
+
+ canUseWorkers: typeof Worker !== 'undefined',
+
+ canUseEventListeners: canUseDOM && !!(window.addEventListener || window.attachEvent),
+
+ canUseViewport: canUseDOM && !!window.screen,
+
+ isInWorker: !canUseDOM // For now, this is true - might change in the future.
+
+};
+
+module.exports = ExecutionEnvironment;
+},{}],148:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule camelize
+ * @typechecks
+ */
+
+"use strict";
+
+var _hyphenPattern = /-(.)/g;
+
+/**
+ * Camelcases a hyphenated string, for example:
+ *
+ * > camelize('background-color')
+ * < "backgroundColor"
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function camelize(string) {
+ return string.replace(_hyphenPattern, function (_, character) {
+ return character.toUpperCase();
+ });
+}
+
+module.exports = camelize;
+},{}],149:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule camelizeStyleName
+ * @typechecks
+ */
+
+'use strict';
+
+var camelize = _dereq_(148);
+
+var msPattern = /^-ms-/;
+
+/**
+ * Camelcases a hyphenated CSS property name, for example:
+ *
+ * > camelizeStyleName('background-color')
+ * < "backgroundColor"
+ * > camelizeStyleName('-moz-transition')
+ * < "MozTransition"
+ * > camelizeStyleName('-ms-transition')
+ * < "msTransition"
+ *
+ * As Andi Smith suggests
+ * (http://www.andismith.com/blog/2012/02/modernizr-prefixed/), an `-ms` prefix
+ * is converted to lowercase `ms`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function camelizeStyleName(string) {
+ return camelize(string.replace(msPattern, 'ms-'));
+}
+
+module.exports = camelizeStyleName;
+},{"148":148}],150:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule containsNode
+ * @typechecks
+ */
+
+'use strict';
+
+var isTextNode = _dereq_(163);
+
+/*eslint-disable no-bitwise */
+
+/**
+ * Checks if a given DOM node contains or is another DOM node.
+ *
+ * @param {?DOMNode} outerNode Outer DOM node.
+ * @param {?DOMNode} innerNode Inner DOM node.
+ * @return {boolean} True if `outerNode` contains or is `innerNode`.
+ */
+function containsNode(_x, _x2) {
+ var _again = true;
+
+ _function: while (_again) {
+ var outerNode = _x,
+ innerNode = _x2;
+ _again = false;
+
+ if (!outerNode || !innerNode) {
+ return false;
+ } else if (outerNode === innerNode) {
+ return true;
+ } else if (isTextNode(outerNode)) {
+ return false;
+ } else if (isTextNode(innerNode)) {
+ _x = outerNode;
+ _x2 = innerNode.parentNode;
+ _again = true;
+ continue _function;
+ } else if (outerNode.contains) {
+ return outerNode.contains(innerNode);
+ } else if (outerNode.compareDocumentPosition) {
+ return !!(outerNode.compareDocumentPosition(innerNode) & 16);
+ } else {
+ return false;
+ }
+ }
+}
+
+module.exports = containsNode;
+},{"163":163}],151:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule createArrayFromMixed
+ * @typechecks
+ */
+
+'use strict';
+
+var toArray = _dereq_(172);
+
+/**
+ * Perform a heuristic test to determine if an object is "array-like".
+ *
+ * A monk asked Joshu, a Zen master, "Has a dog Buddha nature?"
+ * Joshu replied: "Mu."
+ *
+ * This function determines if its argument has "array nature": it returns
+ * true if the argument is an actual array, an `arguments' object, or an
+ * HTMLCollection (e.g. node.childNodes or node.getElementsByTagName()).
+ *
+ * It will return false for other array-like objects like Filelist.
+ *
+ * @param {*} obj
+ * @return {boolean}
+ */
+function hasArrayNature(obj) {
+ return(
+ // not null/false
+ !!obj && (
+ // arrays are objects, NodeLists are functions in Safari
+ typeof obj == 'object' || typeof obj == 'function') &&
+ // quacks like an array
+ 'length' in obj &&
+ // not window
+ !('setInterval' in obj) &&
+ // no DOM node should be considered an array-like
+ // a 'select' element has 'length' and 'item' properties on IE8
+ typeof obj.nodeType != 'number' && (
+ // a real array
+ Array.isArray(obj) ||
+ // arguments
+ 'callee' in obj ||
+ // HTMLCollection/NodeList
+ 'item' in obj)
+ );
+}
+
+/**
+ * Ensure that the argument is an array by wrapping it in an array if it is not.
+ * Creates a copy of the argument if it is already an array.
+ *
+ * This is mostly useful idiomatically:
+ *
+ * var createArrayFromMixed = require('createArrayFromMixed');
+ *
+ * function takesOneOrMoreThings(things) {
+ * things = createArrayFromMixed(things);
+ * ...
+ * }
+ *
+ * This allows you to treat `things' as an array, but accept scalars in the API.
+ *
+ * If you need to convert an array-like object, like `arguments`, into an array
+ * use toArray instead.
+ *
+ * @param {*} obj
+ * @return {array}
+ */
+function createArrayFromMixed(obj) {
+ if (!hasArrayNature(obj)) {
+ return [obj];
+ } else if (Array.isArray(obj)) {
+ return obj.slice();
+ } else {
+ return toArray(obj);
+ }
+}
+
+module.exports = createArrayFromMixed;
+},{"172":172}],152:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule createNodesFromMarkup
+ * @typechecks
+ */
+
+/*eslint-disable fb-www/unsafe-html*/
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var createArrayFromMixed = _dereq_(151);
+var getMarkupWrap = _dereq_(157);
+var invariant = _dereq_(161);
+
+/**
+ * Dummy container used to render all markup.
+ */
+var dummyNode = ExecutionEnvironment.canUseDOM ? document.createElementNS('http://www.w3.org/1999/xhtml', 'div') : null;
+
+/**
+ * Pattern used by `getNodeName`.
+ */
+var nodeNamePattern = /^\s*<(\w+)/;
+
+/**
+ * Extracts the `nodeName` of the first element in a string of markup.
+ *
+ * @param {string} markup String of markup.
+ * @return {?string} Node name of the supplied markup.
+ */
+function getNodeName(markup) {
+ var nodeNameMatch = markup.match(nodeNamePattern);
+ return nodeNameMatch && nodeNameMatch[1].toLowerCase();
+}
+
+/**
+ * Creates an array containing the nodes rendered from the supplied markup. The
+ * optionally supplied `handleScript` function will be invoked once for each
+ * <script> element that is rendered. If no `handleScript` function is supplied,
+ * an exception is thrown if any <script> elements are rendered.
+ *
+ * @param {string} markup A string of valid HTML markup.
+ * @param {?function} handleScript Invoked once for each rendered <script>.
+ * @return {array<DOMElement|DOMTextNode>} An array of rendered nodes.
+ */
+function createNodesFromMarkup(markup, handleScript) {
+ var node = dummyNode;
+ !!!dummyNode ? "production" !== 'production' ? invariant(false, 'createNodesFromMarkup dummy not initialized') : invariant(false) : undefined;
+ var nodeName = getNodeName(markup);
+
+ var wrap = nodeName && getMarkupWrap(nodeName);
+ if (wrap) {
+ node.innerHTML = wrap[1] + markup + wrap[2];
+
+ var wrapDepth = wrap[0];
+ while (wrapDepth--) {
+ node = node.lastChild;
+ }
+ } else {
+ node.innerHTML = markup;
+ }
+
+ var scripts = node.getElementsByTagName('script');
+ if (scripts.length) {
+ !handleScript ? "production" !== 'production' ? invariant(false, 'createNodesFromMarkup(...): Unexpected <script> element rendered.') : invariant(false) : undefined;
+ createArrayFromMixed(scripts).forEach(handleScript);
+ }
+
+ var nodes = createArrayFromMixed(node.childNodes);
+ while (node.lastChild) {
+ node.removeChild(node.lastChild);
+ }
+ return nodes;
+}
+
+module.exports = createNodesFromMarkup;
+
+},{"147":147,"151":151,"157":157,"161":161}],153:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule emptyFunction
+ */
+
+"use strict";
+
+function makeEmptyFunction(arg) {
+ return function () {
+ return arg;
+ };
+}
+
+/**
+ * This function accepts and discards inputs; it has no side effects. This is
+ * primarily useful idiomatically for overridable function endpoints which
+ * always need to be callable, since JS lacks a null-call idiom ala Cocoa.
+ */
+function emptyFunction() {}
+
+emptyFunction.thatReturns = makeEmptyFunction;
+emptyFunction.thatReturnsFalse = makeEmptyFunction(false);
+emptyFunction.thatReturnsTrue = makeEmptyFunction(true);
+emptyFunction.thatReturnsNull = makeEmptyFunction(null);
+emptyFunction.thatReturnsThis = function () {
+ return this;
+};
+emptyFunction.thatReturnsArgument = function (arg) {
+ return arg;
+};
+
+module.exports = emptyFunction;
+},{}],154:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule emptyObject
+ */
+
+'use strict';
+
+var emptyObject = {};
+
+if ("production" !== 'production') {
+ Object.freeze(emptyObject);
+}
+
+module.exports = emptyObject;
+},{}],155:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule focusNode
+ */
+
+'use strict';
+
+/**
+ * @param {DOMElement} node input/textarea to focus
+ */
+function focusNode(node) {
+ // IE8 can throw "Can't move focus to the control because it is invisible,
+ // not enabled, or of a type that does not accept the focus." for all kinds of
+ // reasons that are too expensive and fragile to test.
+ try {
+ node.focus();
+ } catch (e) {}
+}
+
+module.exports = focusNode;
+},{}],156:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getActiveElement
+ * @typechecks
+ */
+
+/* eslint-disable fb-www/typeof-undefined */
+
+/**
+ * Same as document.activeElement but wraps in a try-catch block. In IE it is
+ * not safe to call document.activeElement if there is nothing focused.
+ *
+ * The activeElement will be null only if the document or document body is not
+ * yet defined.
+ */
+'use strict';
+
+function getActiveElement() /*?DOMElement*/{
+ if (typeof document === 'undefined') {
+ return null;
+ }
+ try {
+ return document.activeElement || document.body;
+ } catch (e) {
+ return document.body;
+ }
+}
+
+module.exports = getActiveElement;
+},{}],157:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getMarkupWrap
+ */
+
+/*eslint-disable fb-www/unsafe-html */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var invariant = _dereq_(161);
+
+/**
+ * Dummy container used to detect which wraps are necessary.
+ */
+var dummyNode = ExecutionEnvironment.canUseDOM ? document.createElementNS('http://www.w3.org/1999/xhtml', 'div') : null;
+
+/**
+ * Some browsers cannot use `innerHTML` to render certain elements standalone,
+ * so we wrap them, render the wrapped nodes, then extract the desired node.
+ *
+ * In IE8, certain elements cannot render alone, so wrap all elements ('*').
+ */
+
+var shouldWrap = {};
+
+var selectWrap = [1, '<select multiple="true">', '</select>'];
+var tableWrap = [1, '<table>', '</table>'];
+var trWrap = [3, '<table><tbody><tr>', '</tr></tbody></table>'];
+
+var svgWrap = [1, '<svg xmlns="http://www.w3.org/2000/svg">', '</svg>'];
+
+var markupWrap = {
+ '*': [1, '?<div>', '</div>'],
+
+ 'area': [1, '<map>', '</map>'],
+ 'col': [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
+ 'legend': [1, '<fieldset>', '</fieldset>'],
+ 'param': [1, '<object>', '</object>'],
+ 'tr': [2, '<table><tbody>', '</tbody></table>'],
+
+ 'optgroup': selectWrap,
+ 'option': selectWrap,
+
+ 'caption': tableWrap,
+ 'colgroup': tableWrap,
+ 'tbody': tableWrap,
+ 'tfoot': tableWrap,
+ 'thead': tableWrap,
+
+ 'td': trWrap,
+ 'th': trWrap
+};
+
+// Initialize the SVG elements since we know they'll always need to be wrapped
+// consistently. If they are created inside a <div> they will be initialized in
+// the wrong namespace (and will not display).
+var svgElements = ['circle', 'clipPath', 'defs', 'ellipse', 'g', 'image', 'line', 'linearGradient', 'mask', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', 'rect', 'stop', 'text', 'tspan'];
+svgElements.forEach(function (nodeName) {
+ markupWrap[nodeName] = svgWrap;
+ shouldWrap[nodeName] = true;
+});
+
+/**
+ * Gets the markup wrap configuration for the supplied `nodeName`.
+ *
+ * NOTE: This lazily detects which wraps are necessary for the current browser.
+ *
+ * @param {string} nodeName Lowercase `nodeName`.
+ * @return {?array} Markup wrap configuration, if applicable.
+ */
+function getMarkupWrap(nodeName) {
+ !!!dummyNode ? "production" !== 'production' ? invariant(false, 'Markup wrapping node not initialized') : invariant(false) : undefined;
+ if (!markupWrap.hasOwnProperty(nodeName)) {
+ nodeName = '*';
+ }
+ if (!shouldWrap.hasOwnProperty(nodeName)) {
+ if (nodeName === '*') {
+ dummyNode.innerHTML = '<link />';
+ } else {
+ dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';
+ }
+ shouldWrap[nodeName] = !dummyNode.firstChild;
+ }
+ return shouldWrap[nodeName] ? markupWrap[nodeName] : null;
+}
+
+module.exports = getMarkupWrap;
+},{"147":147,"161":161}],158:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getUnboundedScrollPosition
+ * @typechecks
+ */
+
+'use strict';
+
+/**
+ * Gets the scroll position of the supplied element or window.
+ *
+ * The return values are unbounded, unlike `getScrollPosition`. This means they
+ * may be negative or exceed the element boundaries (which is possible using
+ * inertial scrolling).
+ *
+ * @param {DOMWindow|DOMElement} scrollable
+ * @return {object} Map with `x` and `y` keys.
+ */
+function getUnboundedScrollPosition(scrollable) {
+ if (scrollable === window) {
+ return {
+ x: window.pageXOffset || document.documentElement.scrollLeft,
+ y: window.pageYOffset || document.documentElement.scrollTop
+ };
+ }
+ return {
+ x: scrollable.scrollLeft,
+ y: scrollable.scrollTop
+ };
+}
+
+module.exports = getUnboundedScrollPosition;
+},{}],159:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule hyphenate
+ * @typechecks
+ */
+
+'use strict';
+
+var _uppercasePattern = /([A-Z])/g;
+
+/**
+ * Hyphenates a camelcased string, for example:
+ *
+ * > hyphenate('backgroundColor')
+ * < "background-color"
+ *
+ * For CSS style names, use `hyphenateStyleName` instead which works properly
+ * with all vendor prefixes, including `ms`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function hyphenate(string) {
+ return string.replace(_uppercasePattern, '-$1').toLowerCase();
+}
+
+module.exports = hyphenate;
+},{}],160:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule hyphenateStyleName
+ * @typechecks
+ */
+
+'use strict';
+
+var hyphenate = _dereq_(159);
+
+var msPattern = /^ms-/;
+
+/**
+ * Hyphenates a camelcased CSS property name, for example:
+ *
+ * > hyphenateStyleName('backgroundColor')
+ * < "background-color"
+ * > hyphenateStyleName('MozTransition')
+ * < "-moz-transition"
+ * > hyphenateStyleName('msTransition')
+ * < "-ms-transition"
+ *
+ * As Modernizr suggests (http://modernizr.com/docs/#prefixed), an `ms` prefix
+ * is converted to `-ms-`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function hyphenateStyleName(string) {
+ return hyphenate(string).replace(msPattern, '-ms-');
+}
+
+module.exports = hyphenateStyleName;
+},{"159":159}],161:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule invariant
+ */
+
+'use strict';
+
+/**
+ * Use invariant() to assert state which your program assumes to be true.
+ *
+ * Provide sprintf-style format (only %s is supported) and arguments
+ * to provide information about what broke and what you were
+ * expecting.
+ *
+ * The invariant message will be stripped in production, but the invariant
+ * will remain to ensure logic does not differ in production.
+ */
+
+function invariant(condition, format, a, b, c, d, e, f) {
+ if ("production" !== 'production') {
+ if (format === undefined) {
+ throw new Error('invariant requires an error message argument');
+ }
+ }
+
+ if (!condition) {
+ var error;
+ if (format === undefined) {
+ error = new Error('Minified exception occurred; use the non-minified dev environment ' + 'for the full error message and additional helpful warnings.');
+ } else {
+ var args = [a, b, c, d, e, f];
+ var argIndex = 0;
+ error = new Error(format.replace(/%s/g, function () {
+ return args[argIndex++];
+ }));
+ error.name = 'Invariant Violation';
+ }
+
+ error.framesToPop = 1; // we don't care about invariant's own frame
+ throw error;
+ }
+}
+
+module.exports = invariant;
+},{}],162:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isNode
+ * @typechecks
+ */
+
+/**
+ * @param {*} object The object to check.
+ * @return {boolean} Whether or not the object is a DOM node.
+ */
+'use strict';
+
+function isNode(object) {
+ return !!(object && (typeof Node === 'function' ? object instanceof Node : typeof object === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string'));
+}
+
+module.exports = isNode;
+},{}],163:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isTextNode
+ * @typechecks
+ */
+
+'use strict';
+
+var isNode = _dereq_(162);
+
+/**
+ * @param {*} object The object to check.
+ * @return {boolean} Whether or not the object is a DOM text node.
+ */
+function isTextNode(object) {
+ return isNode(object) && object.nodeType == 3;
+}
+
+module.exports = isTextNode;
+},{"162":162}],164:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule joinClasses
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Combines multiple className strings into one.
+ * http://jsperf.com/joinclasses-args-vs-array
+ *
+ * @param {...?string} className
+ * @return {string}
+ */
+function joinClasses(className /*, ... */) {
+ if (!className) {
+ className = '';
+ }
+ var nextClass;
+ var argLength = arguments.length;
+ if (argLength > 1) {
+ for (var ii = 1; ii < argLength; ii++) {
+ nextClass = arguments[ii];
+ if (nextClass) {
+ className = (className ? className + ' ' : '') + nextClass;
+ }
+ }
+ }
+ return className;
+}
+
+module.exports = joinClasses;
+},{}],165:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyMirror
+ * @typechecks static-only
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Constructs an enumeration with keys equal to their value.
+ *
+ * For example:
+ *
+ * var COLORS = keyMirror({blue: null, red: null});
+ * var myColor = COLORS.blue;
+ * var isColorValid = !!COLORS[myColor];
+ *
+ * The last line could not be performed if the values of the generated enum were
+ * not equal to their keys.
+ *
+ * Input: {key1: val1, key2: val2}
+ * Output: {key1: key1, key2: key2}
+ *
+ * @param {object} obj
+ * @return {object}
+ */
+var keyMirror = function (obj) {
+ var ret = {};
+ var key;
+ !(obj instanceof Object && !Array.isArray(obj)) ? "production" !== 'production' ? invariant(false, 'keyMirror(...): Argument must be an object.') : invariant(false) : undefined;
+ for (key in obj) {
+ if (!obj.hasOwnProperty(key)) {
+ continue;
+ }
+ ret[key] = key;
+ }
+ return ret;
+};
+
+module.exports = keyMirror;
+},{"161":161}],166:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyOf
+ */
+
+/**
+ * Allows extraction of a minified key. Let's the build system minify keys
+ * without losing the ability to dynamically use key strings as values
+ * themselves. Pass in an object with a single key/val pair and it will return
+ * you the string key of that single record. Suppose you want to grab the
+ * value for a key 'className' inside of an object. Key/val minification may
+ * have aliased that key to be 'xa12'. keyOf({className: null}) will return
+ * 'xa12' in that case. Resolve keys you want to use once at startup time, then
+ * reuse those resolutions.
+ */
+"use strict";
+
+var keyOf = function (oneKeyObj) {
+ var key;
+ for (key in oneKeyObj) {
+ if (!oneKeyObj.hasOwnProperty(key)) {
+ continue;
+ }
+ return key;
+ }
+ return null;
+};
+
+module.exports = keyOf;
+},{}],167:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule mapObject
+ */
+
+'use strict';
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+/**
+ * Executes the provided `callback` once for each enumerable own property in the
+ * object and constructs a new object from the results. The `callback` is
+ * invoked with three arguments:
+ *
+ * - the property value
+ * - the property name
+ * - the object being traversed
+ *
+ * Properties that are added after the call to `mapObject` will not be visited
+ * by `callback`. If the values of existing properties are changed, the value
+ * passed to `callback` will be the value at the time `mapObject` visits them.
+ * Properties that are deleted before being visited are not visited.
+ *
+ * @grep function objectMap()
+ * @grep function objMap()
+ *
+ * @param {?object} object
+ * @param {function} callback
+ * @param {*} context
+ * @return {?object}
+ */
+function mapObject(object, callback, context) {
+ if (!object) {
+ return null;
+ }
+ var result = {};
+ for (var name in object) {
+ if (hasOwnProperty.call(object, name)) {
+ result[name] = callback.call(context, object[name], name, object);
+ }
+ }
+ return result;
+}
+
+module.exports = mapObject;
+},{}],168:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule memoizeStringOnly
+ * @typechecks static-only
+ */
+
+'use strict';
+
+/**
+ * Memoizes the return value of a function that accepts one string argument.
+ *
+ * @param {function} callback
+ * @return {function}
+ */
+function memoizeStringOnly(callback) {
+ var cache = {};
+ return function (string) {
+ if (!cache.hasOwnProperty(string)) {
+ cache[string] = callback.call(this, string);
+ }
+ return cache[string];
+ };
+}
+
+module.exports = memoizeStringOnly;
+},{}],169:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule performance
+ * @typechecks
+ */
+
+'use strict';
+
+var ExecutionEnvironment = _dereq_(147);
+
+var performance;
+
+if (ExecutionEnvironment.canUseDOM) {
+ performance = window.performance || window.msPerformance || window.webkitPerformance;
+}
+
+module.exports = performance || {};
+},{"147":147}],170:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule performanceNow
+ * @typechecks
+ */
+
+'use strict';
+
+var performance = _dereq_(169);
+
+var performanceNow;
+
+/**
+ * Detect if we can use `window.performance.now()` and gracefully fallback to
+ * `Date.now()` if it doesn't exist. We need to support Firefox < 15 for now
+ * because of Facebook's testing infrastructure.
+ */
+if (performance.now) {
+ performanceNow = function () {
+ return performance.now();
+ };
+} else {
+ performanceNow = function () {
+ return Date.now();
+ };
+}
+
+module.exports = performanceNow;
+},{"169":169}],171:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule shallowEqual
+ * @typechecks
+ *
+ */
+
+'use strict';
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+/**
+ * Performs equality by iterating through keys on an object and returning false
+ * when any key has values which are not strictly equal between the arguments.
+ * Returns true when the values of all keys are strictly equal.
+ */
+function shallowEqual(objA, objB) {
+ if (objA === objB) {
+ return true;
+ }
+
+ if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
+ return false;
+ }
+
+ var keysA = Object.keys(objA);
+ var keysB = Object.keys(objB);
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ // Test for A's keys different from B.
+ var bHasOwnProperty = hasOwnProperty.bind(objB);
+ for (var i = 0; i < keysA.length; i++) {
+ if (!bHasOwnProperty(keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+module.exports = shallowEqual;
+},{}],172:[function(_dereq_,module,exports){
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule toArray
+ * @typechecks
+ */
+
+'use strict';
+
+var invariant = _dereq_(161);
+
+/**
+ * Convert array-like objects to arrays.
+ *
+ * This API assumes the caller knows the contents of the data type. For less
+ * well defined inputs use createArrayFromMixed.
+ *
+ * @param {object|function|filelist} obj
+ * @return {array}
+ */
+function toArray(obj) {
+ var length = obj.length;
+
+ // Some browse builtin objects can report typeof 'function' (e.g. NodeList in
+ // old versions of Safari).
+ !(!Array.isArray(obj) && (typeof obj === 'object' || typeof obj === 'function')) ? "production" !== 'production' ? invariant(false, 'toArray: Array-like object expected') : invariant(false) : undefined;
+
+ !(typeof length === 'number') ? "production" !== 'production' ? invariant(false, 'toArray: Object needs a length property') : invariant(false) : undefined;
+
+ !(length === 0 || length - 1 in obj) ? "production" !== 'production' ? invariant(false, 'toArray: Object should have keys for indices') : invariant(false) : undefined;
+
+ // Old IE doesn't give collections access to hasOwnProperty. Assume inputs
+ // without method will throw during the slice call and skip straight to the
+ // fallback.
+ if (obj.hasOwnProperty) {
+ try {
+ return Array.prototype.slice.call(obj);
+ } catch (e) {
+ // IE < 9 does not support Array#slice on collections objects
+ }
+ }
+
+ // Fall back to copying key by key. This assumes all keys have a value,
+ // so will not preserve sparsely populated inputs.
+ var ret = Array(length);
+ for (var ii = 0; ii < length; ii++) {
+ ret[ii] = obj[ii];
+ }
+ return ret;
+}
+
+module.exports = toArray;
+},{"161":161}],173:[function(_dereq_,module,exports){
+/**
+ * Copyright 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule warning
+ */
+
+'use strict';
+
+var emptyFunction = _dereq_(153);
+
+/**
+ * Similar to invariant but only logs a warning if the condition is not met.
+ * This can be used to log issues in development environments in critical
+ * paths. Removing the logging code for production environments will keep the
+ * same logic and follow the same code paths.
+ */
+
+var warning = emptyFunction;
+
+if ("production" !== 'production') {
+ warning = function (condition, format) {
+ for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
+ args[_key - 2] = arguments[_key];
+ }
+
+ if (format === undefined) {
+ throw new Error('`warning(condition, format, ...args)` requires a warning ' + 'message argument');
+ }
+
+ if (format.indexOf('Failed Composite propType: ') === 0) {
+ return; // Ignore CompositeComponent proptype check.
+ }
+
+ if (!condition) {
+ var argIndex = 0;
+ var message = 'Warning: ' + format.replace(/%s/g, function () {
+ return args[argIndex++];
+ });
+ if (typeof console !== 'undefined') {
+ console.error(message);
+ }
+ try {
+ // --- Welcome to debugging React ---
+ // This error was thrown as a convenience so that you can use this stack
+ // to find the callsite that caused this warning to fire.
+ throw new Error(message);
+ } catch (x) {}
+ }
+ };
+}
+
+module.exports = warning;
+},{"153":153}]},{},[1])(1)
+});
diff --git a/devtools/client/shared/vendor/redux.js b/devtools/client/shared/vendor/redux.js
new file mode 100644
index 000000000..fe12ae878
--- /dev/null
+++ b/devtools/client/shared/vendor/redux.js
@@ -0,0 +1,775 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define([], factory);
+ else if(typeof exports === 'object')
+ exports["Redux"] = factory();
+ else
+ root["Redux"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+
+
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports.compose = exports.applyMiddleware = exports.bindActionCreators = exports.combineReducers = exports.createStore = undefined;
+
+ var _createStore = __webpack_require__(2);
+
+ var _createStore2 = _interopRequireDefault(_createStore);
+
+ var _combineReducers = __webpack_require__(7);
+
+ var _combineReducers2 = _interopRequireDefault(_combineReducers);
+
+ var _bindActionCreators = __webpack_require__(6);
+
+ var _bindActionCreators2 = _interopRequireDefault(_bindActionCreators);
+
+ var _applyMiddleware = __webpack_require__(5);
+
+ var _applyMiddleware2 = _interopRequireDefault(_applyMiddleware);
+
+ var _compose = __webpack_require__(1);
+
+ var _compose2 = _interopRequireDefault(_compose);
+
+ var _warning = __webpack_require__(3);
+
+ var _warning2 = _interopRequireDefault(_warning);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ /*
+ * This is a dummy function to check if the function name has been altered by minification.
+ * If the function has been minified and NODE_ENV !== 'production', warn the user.
+ */
+ function isCrushed() {}
+
+ if (("development") !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed') {
+ (0, _warning2["default"])('You are currently using minified code outside of NODE_ENV === \'production\'. ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' + 'to ensure you have the correct code for your production build.');
+ }
+
+ exports.createStore = _createStore2["default"];
+ exports.combineReducers = _combineReducers2["default"];
+ exports.bindActionCreators = _bindActionCreators2["default"];
+ exports.applyMiddleware = _applyMiddleware2["default"];
+ exports.compose = _compose2["default"];
+
+/***/ },
+/* 1 */
+/***/ function(module, exports) {
+
+ "use strict";
+
+ exports.__esModule = true;
+ exports["default"] = compose;
+ /**
+ * Composes single-argument functions from right to left.
+ *
+ * @param {...Function} funcs The functions to compose.
+ * @returns {Function} A function obtained by composing functions from right to
+ * left. For example, compose(f, g, h) is identical to arg => f(g(h(arg))).
+ */
+ function compose() {
+ for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
+ funcs[_key] = arguments[_key];
+ }
+
+ return function () {
+ if (funcs.length === 0) {
+ return arguments.length <= 0 ? undefined : arguments[0];
+ }
+
+ var last = funcs[funcs.length - 1];
+ var rest = funcs.slice(0, -1);
+
+ return rest.reduceRight(function (composed, f) {
+ return f(composed);
+ }, last.apply(undefined, arguments));
+ };
+ }
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports.ActionTypes = undefined;
+ exports["default"] = createStore;
+
+ var _isPlainObject = __webpack_require__(4);
+
+ var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ /**
+ * These are private action types reserved by Redux.
+ * For any unknown actions, you must return the current state.
+ * If the current state is undefined, you must return the initial state.
+ * Do not reference these action types directly in your code.
+ */
+ var ActionTypes = exports.ActionTypes = {
+ INIT: '@@redux/INIT'
+ };
+
+ /**
+ * Creates a Redux store that holds the state tree.
+ * The only way to change the data in the store is to call `dispatch()` on it.
+ *
+ * There should only be a single store in your app. To specify how different
+ * parts of the state tree respond to actions, you may combine several reducers
+ * into a single reducer function by using `combineReducers`.
+ *
+ * @param {Function} reducer A function that returns the next state tree, given
+ * the current state tree and the action to handle.
+ *
+ * @param {any} [initialState] The initial state. You may optionally specify it
+ * to hydrate the state from the server in universal apps, or to restore a
+ * previously serialized user session.
+ * If you use `combineReducers` to produce the root reducer function, this must be
+ * an object with the same shape as `combineReducers` keys.
+ *
+ * @param {Function} enhancer The store enhancer. You may optionally specify it
+ * to enhance the store with third-party capabilities such as middleware,
+ * time travel, persistence, etc. The only store enhancer that ships with Redux
+ * is `applyMiddleware()`.
+ *
+ * @returns {Store} A Redux store that lets you read the state, dispatch actions
+ * and subscribe to changes.
+ */
+ function createStore(reducer, initialState, enhancer) {
+ if (typeof initialState === 'function' && typeof enhancer === 'undefined') {
+ enhancer = initialState;
+ initialState = undefined;
+ }
+
+ if (typeof enhancer !== 'undefined') {
+ if (typeof enhancer !== 'function') {
+ throw new Error('Expected the enhancer to be a function.');
+ }
+
+ return enhancer(createStore)(reducer, initialState);
+ }
+
+ if (typeof reducer !== 'function') {
+ throw new Error('Expected the reducer to be a function.');
+ }
+
+ var currentReducer = reducer;
+ var currentState = initialState;
+ var currentListeners = [];
+ var nextListeners = currentListeners;
+ var isDispatching = false;
+
+ function ensureCanMutateNextListeners() {
+ if (nextListeners === currentListeners) {
+ nextListeners = currentListeners.slice();
+ }
+ }
+
+ /**
+ * Reads the state tree managed by the store.
+ *
+ * @returns {any} The current state tree of your application.
+ */
+ function getState() {
+ return currentState;
+ }
+
+ /**
+ * Adds a change listener. It will be called any time an action is dispatched,
+ * and some part of the state tree may potentially have changed. You may then
+ * call `getState()` to read the current state tree inside the callback.
+ *
+ * You may call `dispatch()` from a change listener, with the following
+ * caveats:
+ *
+ * 1. The subscriptions are snapshotted just before every `dispatch()` call.
+ * If you subscribe or unsubscribe while the listeners are being invoked, this
+ * will not have any effect on the `dispatch()` that is currently in progress.
+ * However, the next `dispatch()` call, whether nested or not, will use a more
+ * recent snapshot of the subscription list.
+ *
+ * 2. The listener should not expect to see all states changes, as the state
+ * might have been updated multiple times during a nested `dispatch()` before
+ * the listener is called. It is, however, guaranteed that all subscribers
+ * registered before the `dispatch()` started will be called with the latest
+ * state by the time it exits.
+ *
+ * @param {Function} listener A callback to be invoked on every dispatch.
+ * @returns {Function} A function to remove this change listener.
+ */
+ function subscribe(listener) {
+ if (typeof listener !== 'function') {
+ throw new Error('Expected listener to be a function.');
+ }
+
+ var isSubscribed = true;
+
+ ensureCanMutateNextListeners();
+ nextListeners.push(listener);
+
+ return function unsubscribe() {
+ if (!isSubscribed) {
+ return;
+ }
+
+ isSubscribed = false;
+
+ ensureCanMutateNextListeners();
+ var index = nextListeners.indexOf(listener);
+ nextListeners.splice(index, 1);
+ };
+ }
+
+ /**
+ * Dispatches an action. It is the only way to trigger a state change.
+ *
+ * The `reducer` function, used to create the store, will be called with the
+ * current state tree and the given `action`. Its return value will
+ * be considered the **next** state of the tree, and the change listeners
+ * will be notified.
+ *
+ * The base implementation only supports plain object actions. If you want to
+ * dispatch a Promise, an Observable, a thunk, or something else, you need to
+ * wrap your store creating function into the corresponding middleware. For
+ * example, see the documentation for the `redux-thunk` package. Even the
+ * middleware will eventually dispatch plain object actions using this method.
+ *
+ * @param {Object} action A plain object representing “what changed”. It is
+ * a good idea to keep actions serializable so you can record and replay user
+ * sessions, or use the time travelling `redux-devtools`. An action must have
+ * a `type` property which may not be `undefined`. It is a good idea to use
+ * string constants for action types.
+ *
+ * @returns {Object} For convenience, the same action object you dispatched.
+ *
+ * Note that, if you use a custom middleware, it may wrap `dispatch()` to
+ * return something else (for example, a Promise you can await).
+ */
+ function dispatch(action) {
+ if (!(0, _isPlainObject2["default"])(action)) {
+ throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');
+ }
+
+ if (typeof action.type === 'undefined') {
+ throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?');
+ }
+
+ if (isDispatching) {
+ throw new Error('Reducers may not dispatch actions.');
+ }
+
+ try {
+ isDispatching = true;
+ currentState = currentReducer(currentState, action);
+ } finally {
+ isDispatching = false;
+ }
+
+ var listeners = currentListeners = nextListeners;
+ for (var i = 0; i < listeners.length; i++) {
+ listeners[i]();
+ }
+
+ return action;
+ }
+
+ /**
+ * Replaces the reducer currently used by the store to calculate the state.
+ *
+ * You might need this if your app implements code splitting and you want to
+ * load some of the reducers dynamically. You might also need this if you
+ * implement a hot reloading mechanism for Redux.
+ *
+ * @param {Function} nextReducer The reducer for the store to use instead.
+ * @returns {void}
+ */
+ function replaceReducer(nextReducer) {
+ if (typeof nextReducer !== 'function') {
+ throw new Error('Expected the nextReducer to be a function.');
+ }
+
+ currentReducer = nextReducer;
+ dispatch({ type: ActionTypes.INIT });
+ }
+
+ // When a store is created, an "INIT" action is dispatched so that every
+ // reducer returns their initial state. This effectively populates
+ // the initial state tree.
+ dispatch({ type: ActionTypes.INIT });
+
+ return {
+ dispatch: dispatch,
+ subscribe: subscribe,
+ getState: getState,
+ replaceReducer: replaceReducer
+ };
+ }
+
+/***/ },
+/* 3 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports["default"] = warning;
+ /**
+ * Prints a warning in the console if it exists.
+ *
+ * @param {String} message The warning message.
+ * @returns {void}
+ */
+ function warning(message) {
+ /* eslint-disable no-console */
+ if (typeof console !== 'undefined' && typeof console.error === 'function') {
+ console.error(message);
+ }
+ /* eslint-enable no-console */
+ try {
+ // This error was thrown as a convenience so that you can use this stack
+ // to find the callsite that caused this warning to fire.
+ throw new Error(message);
+ /* eslint-disable no-empty */
+ } catch (e) {}
+ /* eslint-enable no-empty */
+ }
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var isHostObject = __webpack_require__(8),
+ isObjectLike = __webpack_require__(9);
+
+ /** `Object#toString` result references. */
+ var objectTag = '[object Object]';
+
+ /** Used for built-in method references. */
+ var objectProto = Object.prototype;
+
+ /** Used to resolve the decompiled source of functions. */
+ var funcToString = Function.prototype.toString;
+
+ /** Used to infer the `Object` constructor. */
+ var objectCtorString = funcToString.call(Object);
+
+ /**
+ * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objectToString = objectProto.toString;
+
+ /** Built-in value references. */
+ var getPrototypeOf = Object.getPrototypeOf;
+
+ /**
+ * Checks if `value` is a plain object, that is, an object created by the
+ * `Object` constructor or one with a `[[Prototype]]` of `null`.
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * }
+ *
+ * _.isPlainObject(new Foo);
+ * // => false
+ *
+ * _.isPlainObject([1, 2, 3]);
+ * // => false
+ *
+ * _.isPlainObject({ 'x': 0, 'y': 0 });
+ * // => true
+ *
+ * _.isPlainObject(Object.create(null));
+ * // => true
+ */
+ function isPlainObject(value) {
+ if (!isObjectLike(value) || objectToString.call(value) != objectTag || isHostObject(value)) {
+ return false;
+ }
+ var proto = objectProto;
+ if (typeof value.constructor == 'function') {
+ proto = getPrototypeOf(value);
+ }
+ if (proto === null) {
+ return true;
+ }
+ var Ctor = proto.constructor;
+ return (typeof Ctor == 'function' &&
+ Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString);
+ }
+
+ module.exports = isPlainObject;
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+ exports.__esModule = true;
+ exports["default"] = applyMiddleware;
+
+ var _compose = __webpack_require__(1);
+
+ var _compose2 = _interopRequireDefault(_compose);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ /**
+ * Creates a store enhancer that applies middleware to the dispatch method
+ * of the Redux store. This is handy for a variety of tasks, such as expressing
+ * asynchronous actions in a concise manner, or logging every action payload.
+ *
+ * See `redux-thunk` package as an example of the Redux middleware.
+ *
+ * Because middleware is potentially asynchronous, this should be the first
+ * store enhancer in the composition chain.
+ *
+ * Note that each middleware will be given the `dispatch` and `getState` functions
+ * as named arguments.
+ *
+ * @param {...Function} middlewares The middleware chain to be applied.
+ * @returns {Function} A store enhancer applying the middleware.
+ */
+ function applyMiddleware() {
+ for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
+ middlewares[_key] = arguments[_key];
+ }
+
+ return function (createStore) {
+ return function (reducer, initialState, enhancer) {
+ var store = createStore(reducer, initialState, enhancer);
+ var _dispatch = store.dispatch;
+ var chain = [];
+
+ var middlewareAPI = {
+ getState: store.getState,
+ dispatch: function dispatch(action) {
+ return _dispatch(action);
+ }
+ };
+ chain = middlewares.map(function (middleware) {
+ return middleware(middlewareAPI);
+ });
+ _dispatch = _compose2["default"].apply(undefined, chain)(store.dispatch);
+
+ return _extends({}, store, {
+ dispatch: _dispatch
+ });
+ };
+ };
+ }
+
+/***/ },
+/* 6 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports["default"] = bindActionCreators;
+ function bindActionCreator(actionCreator, dispatch) {
+ return function () {
+ return dispatch(actionCreator.apply(undefined, arguments));
+ };
+ }
+
+ /**
+ * Turns an object whose values are action creators, into an object with the
+ * same keys, but with every function wrapped into a `dispatch` call so they
+ * may be invoked directly. This is just a convenience method, as you can call
+ * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.
+ *
+ * For convenience, you can also pass a single function as the first argument,
+ * and get a function in return.
+ *
+ * @param {Function|Object} actionCreators An object whose values are action
+ * creator functions. One handy way to obtain it is to use ES6 `import * as`
+ * syntax. You may also pass a single function.
+ *
+ * @param {Function} dispatch The `dispatch` function available on your Redux
+ * store.
+ *
+ * @returns {Function|Object} The object mimicking the original object, but with
+ * every action creator wrapped into the `dispatch` call. If you passed a
+ * function as `actionCreators`, the return value will also be a single
+ * function.
+ */
+ function bindActionCreators(actionCreators, dispatch) {
+ if (typeof actionCreators === 'function') {
+ return bindActionCreator(actionCreators, dispatch);
+ }
+
+ if (typeof actionCreators !== 'object' || actionCreators === null) {
+ throw new Error('bindActionCreators expected an object or a function, instead received ' + (actionCreators === null ? 'null' : typeof actionCreators) + '. ' + 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?');
+ }
+
+ var keys = Object.keys(actionCreators);
+ var boundActionCreators = {};
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ var actionCreator = actionCreators[key];
+ if (typeof actionCreator === 'function') {
+ boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
+ }
+ }
+ return boundActionCreators;
+ }
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports["default"] = combineReducers;
+
+ var _createStore = __webpack_require__(2);
+
+ var _isPlainObject = __webpack_require__(4);
+
+ var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+ var _warning = __webpack_require__(3);
+
+ var _warning2 = _interopRequireDefault(_warning);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+ function getUndefinedStateErrorMessage(key, action) {
+ var actionType = action && action.type;
+ var actionName = actionType && '"' + actionType.toString() + '"' || 'an action';
+
+ return 'Reducer "' + key + '" returned undefined handling ' + actionName + '. ' + 'To ignore an action, you must explicitly return the previous state.';
+ }
+
+ function getUnexpectedStateShapeWarningMessage(inputState, reducers, action) {
+ var reducerKeys = Object.keys(reducers);
+ var argumentName = action && action.type === _createStore.ActionTypes.INIT ? 'initialState argument passed to createStore' : 'previous state received by the reducer';
+
+ if (reducerKeys.length === 0) {
+ return 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.';
+ }
+
+ if (!(0, _isPlainObject2["default"])(inputState)) {
+ return 'The ' + argumentName + ' has unexpected type of "' + {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + '". Expected argument to be an object with the following ' + ('keys: "' + reducerKeys.join('", "') + '"');
+ }
+
+ var unexpectedKeys = Object.keys(inputState).filter(function (key) {
+ return !reducers.hasOwnProperty(key);
+ });
+
+ if (unexpectedKeys.length > 0) {
+ return 'Unexpected ' + (unexpectedKeys.length > 1 ? 'keys' : 'key') + ' ' + ('"' + unexpectedKeys.join('", "') + '" found in ' + argumentName + '. ') + 'Expected to find one of the known reducer keys instead: ' + ('"' + reducerKeys.join('", "') + '". Unexpected keys will be ignored.');
+ }
+ }
+
+ function assertReducerSanity(reducers) {
+ Object.keys(reducers).forEach(function (key) {
+ var reducer = reducers[key];
+ var initialState = reducer(undefined, { type: _createStore.ActionTypes.INIT });
+
+ if (typeof initialState === 'undefined') {
+ throw new Error('Reducer "' + key + '" returned undefined during initialization. ' + 'If the state passed to the reducer is undefined, you must ' + 'explicitly return the initial state. The initial state may ' + 'not be undefined.');
+ }
+
+ var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.');
+ if (typeof reducer(undefined, { type: type }) === 'undefined') {
+ throw new Error('Reducer "' + key + '" returned undefined when probed with a random type. ' + ('Don\'t try to handle ' + _createStore.ActionTypes.INIT + ' or other actions in "redux/*" ') + 'namespace. They are considered private. Instead, you must return the ' + 'current state for any unknown actions, unless it is undefined, ' + 'in which case you must return the initial state, regardless of the ' + 'action type. The initial state may not be undefined.');
+ }
+ });
+ }
+
+ /**
+ * Turns an object whose values are different reducer functions, into a single
+ * reducer function. It will call every child reducer, and gather their results
+ * into a single state object, whose keys correspond to the keys of the passed
+ * reducer functions.
+ *
+ * @param {Object} reducers An object whose values correspond to different
+ * reducer functions that need to be combined into one. One handy way to obtain
+ * it is to use ES6 `import * as reducers` syntax. The reducers may never return
+ * undefined for any action. Instead, they should return their initial state
+ * if the state passed to them was undefined, and the current state for any
+ * unrecognized action.
+ *
+ * @returns {Function} A reducer function that invokes every reducer inside the
+ * passed object, and builds a state object with the same shape.
+ */
+ function combineReducers(reducers) {
+ var reducerKeys = Object.keys(reducers);
+ var finalReducers = {};
+ for (var i = 0; i < reducerKeys.length; i++) {
+ var key = reducerKeys[i];
+ if (typeof reducers[key] === 'function') {
+ finalReducers[key] = reducers[key];
+ }
+ }
+ var finalReducerKeys = Object.keys(finalReducers);
+
+ var sanityError;
+ try {
+ assertReducerSanity(finalReducers);
+ } catch (e) {
+ sanityError = e;
+ }
+
+ return function combination() {
+ var state = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+ var action = arguments[1];
+
+ if (sanityError) {
+ throw sanityError;
+ }
+
+ if (true) {
+ var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action);
+ if (warningMessage) {
+ (0, _warning2["default"])(warningMessage);
+ }
+ }
+
+ var hasChanged = false;
+ var nextState = {};
+ for (var i = 0; i < finalReducerKeys.length; i++) {
+ var key = finalReducerKeys[i];
+ var reducer = finalReducers[key];
+ var previousStateForKey = state[key];
+ var nextStateForKey = reducer(previousStateForKey, action);
+ if (typeof nextStateForKey === 'undefined') {
+ var errorMessage = getUndefinedStateErrorMessage(key, action);
+ throw new Error(errorMessage);
+ }
+ nextState[key] = nextStateForKey;
+ hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
+ }
+ return hasChanged ? nextState : state;
+ };
+ }
+
+/***/ },
+/* 8 */
+/***/ function(module, exports) {
+
+ /**
+ * Checks if `value` is a host object in IE < 9.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a host object, else `false`.
+ */
+ function isHostObject(value) {
+ // Many host objects are `Object` objects that can coerce to strings
+ // despite having improperly defined `toString` methods.
+ var result = false;
+ if (value != null && typeof value.toString != 'function') {
+ try {
+ result = !!(value + '');
+ } catch (e) {}
+ }
+ return result;
+ }
+
+ module.exports = isHostObject;
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports) {
+
+ /**
+ * Checks if `value` is object-like. A value is object-like if it's not `null`
+ * and has a `typeof` result of "object".
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
+ * @example
+ *
+ * _.isObjectLike({});
+ * // => true
+ *
+ * _.isObjectLike([1, 2, 3]);
+ * // => true
+ *
+ * _.isObjectLike(_.noop);
+ * // => false
+ *
+ * _.isObjectLike(null);
+ * // => false
+ */
+ function isObjectLike(value) {
+ return !!value && typeof value == 'object';
+ }
+
+ module.exports = isObjectLike;
+
+
+/***/ }
+/******/ ])
+});
+; \ No newline at end of file
diff --git a/devtools/client/shared/vendor/reselect.js b/devtools/client/shared/vendor/reselect.js
new file mode 100644
index 000000000..4d51d342c
--- /dev/null
+++ b/devtools/client/shared/vendor/reselect.js
@@ -0,0 +1,136 @@
+(function (global, factory) {
+ if (typeof define === "function" && define.amd) {
+ define('Reselect', ['exports'], factory);
+ } else if (typeof exports !== "undefined") {
+ factory(exports);
+ } else {
+ var mod = {
+ exports: {}
+ };
+ factory(mod.exports);
+ global.Reselect = mod.exports;
+ }
+})(this, function (exports) {
+ 'use strict';
+
+ exports.__esModule = true;
+ exports.defaultMemoize = defaultMemoize;
+ exports.createSelectorCreator = createSelectorCreator;
+ exports.createStructuredSelector = createStructuredSelector;
+
+ function _toConsumableArray(arr) {
+ if (Array.isArray(arr)) {
+ for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {
+ arr2[i] = arr[i];
+ }
+
+ return arr2;
+ } else {
+ return Array.from(arr);
+ }
+ }
+
+ function defaultEqualityCheck(a, b) {
+ return a === b;
+ }
+
+ function defaultMemoize(func) {
+ var equalityCheck = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultEqualityCheck;
+
+ var lastArgs = null;
+ var lastResult = null;
+ var isEqualToLastArg = function isEqualToLastArg(value, index) {
+ return equalityCheck(value, lastArgs[index]);
+ };
+ return function () {
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ if (lastArgs === null || lastArgs.length !== args.length || !args.every(isEqualToLastArg)) {
+ lastResult = func.apply(undefined, args);
+ }
+ lastArgs = args;
+ return lastResult;
+ };
+ }
+
+ function getDependencies(funcs) {
+ var dependencies = Array.isArray(funcs[0]) ? funcs[0] : funcs;
+
+ if (!dependencies.every(function (dep) {
+ return typeof dep === 'function';
+ })) {
+ var dependencyTypes = dependencies.map(function (dep) {
+ return typeof dep;
+ }).join(', ');
+ throw new Error('Selector creators expect all input-selectors to be functions, ' + ('instead received the following types: [' + dependencyTypes + ']'));
+ }
+
+ return dependencies;
+ }
+
+ function createSelectorCreator(memoize) {
+ for (var _len2 = arguments.length, memoizeOptions = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
+ memoizeOptions[_key2 - 1] = arguments[_key2];
+ }
+
+ return function () {
+ for (var _len3 = arguments.length, funcs = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
+ funcs[_key3] = arguments[_key3];
+ }
+
+ var recomputations = 0;
+ var resultFunc = funcs.pop();
+ var dependencies = getDependencies(funcs);
+
+ var memoizedResultFunc = memoize.apply(undefined, [function () {
+ recomputations++;
+ return resultFunc.apply(undefined, arguments);
+ }].concat(memoizeOptions));
+
+ var selector = function selector(state, props) {
+ for (var _len4 = arguments.length, args = Array(_len4 > 2 ? _len4 - 2 : 0), _key4 = 2; _key4 < _len4; _key4++) {
+ args[_key4 - 2] = arguments[_key4];
+ }
+
+ var params = dependencies.map(function (dependency) {
+ return dependency.apply(undefined, [state, props].concat(args));
+ });
+ return memoizedResultFunc.apply(undefined, _toConsumableArray(params));
+ };
+
+ selector.resultFunc = resultFunc;
+ selector.recomputations = function () {
+ return recomputations;
+ };
+ selector.resetRecomputations = function () {
+ return recomputations = 0;
+ };
+ return selector;
+ };
+ }
+
+ var createSelector = exports.createSelector = createSelectorCreator(defaultMemoize);
+
+ function createStructuredSelector(selectors) {
+ var selectorCreator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : createSelector;
+
+ if (typeof selectors !== 'object') {
+ throw new Error('createStructuredSelector expects first argument to be an object ' + ('where each property is a selector, instead received a ' + typeof selectors));
+ }
+ var objectKeys = Object.keys(selectors);
+ return selectorCreator(objectKeys.map(function (key) {
+ return selectors[key];
+ }), function () {
+ for (var _len5 = arguments.length, values = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
+ values[_key5] = arguments[_key5];
+ }
+
+ return values.reduce(function (composition, value, index) {
+ composition[objectKeys[index]] = value;
+ return composition;
+ }, {});
+ });
+ }
+});
diff --git a/devtools/client/shared/vendor/seamless-immutable.js b/devtools/client/shared/vendor/seamless-immutable.js
new file mode 100644
index 000000000..ef893f384
--- /dev/null
+++ b/devtools/client/shared/vendor/seamless-immutable.js
@@ -0,0 +1,392 @@
+(function(){
+ "use strict";
+
+ function addPropertyTo(target, methodName, value) {
+ Object.defineProperty(target, methodName, {
+ enumerable: false,
+ configurable: false,
+ writable: false,
+ value: value
+ });
+ }
+
+ function banProperty(target, methodName) {
+ addPropertyTo(target, methodName, function() {
+ throw new ImmutableError("The " + methodName +
+ " method cannot be invoked on an Immutable data structure.");
+ });
+ }
+
+ var immutabilityTag = "__immutable_invariants_hold";
+
+ function addImmutabilityTag(target) {
+ addPropertyTo(target, immutabilityTag, true);
+ }
+
+ function isImmutable(target) {
+ if (typeof target === "object") {
+ return target === null || target.hasOwnProperty(immutabilityTag);
+ } else {
+ // In JavaScript, only objects are even potentially mutable.
+ // strings, numbers, null, and undefined are all naturally immutable.
+ return true;
+ }
+ }
+
+ function isMergableObject(target) {
+ return target !== null && typeof target === "object" && !(target instanceof Array) && !(target instanceof Date);
+ }
+
+ var mutatingObjectMethods = [
+ "setPrototypeOf"
+ ];
+
+ var nonMutatingObjectMethods = [
+ "keys"
+ ];
+
+ var mutatingArrayMethods = mutatingObjectMethods.concat([
+ "push", "pop", "sort", "splice", "shift", "unshift", "reverse"
+ ]);
+
+ var nonMutatingArrayMethods = nonMutatingObjectMethods.concat([
+ "map", "filter", "slice", "concat", "reduce", "reduceRight"
+ ]);
+
+ function ImmutableError(message) {
+ var err = new Error(message);
+ err.__proto__ = ImmutableError;
+
+ return err;
+ }
+ ImmutableError.prototype = Error.prototype;
+
+ function makeImmutable(obj, bannedMethods) {
+ // Tag it so we can quickly tell it's immutable later.
+ addImmutabilityTag(obj);
+
+ if ("development" === "development") {
+ // Make all mutating methods throw exceptions.
+ for (var index in bannedMethods) {
+ if (bannedMethods.hasOwnProperty(index)) {
+ banProperty(obj, bannedMethods[index]);
+ }
+ }
+
+ // Freeze it and return it.
+ Object.freeze(obj);
+ }
+
+ return obj;
+ }
+
+ function makeMethodReturnImmutable(obj, methodName) {
+ var currentMethod = obj[methodName];
+
+ addPropertyTo(obj, methodName, function() {
+ return Immutable(currentMethod.apply(obj, arguments));
+ });
+ }
+
+ function makeImmutableArray(array) {
+ // Don't change their implementations, but wrap these functions to make sure
+ // they always return an immutable value.
+ for (var index in nonMutatingArrayMethods) {
+ if (nonMutatingArrayMethods.hasOwnProperty(index)) {
+ var methodName = nonMutatingArrayMethods[index];
+ makeMethodReturnImmutable(array, methodName);
+ }
+ }
+
+ addPropertyTo(array, "flatMap", flatMap);
+ addPropertyTo(array, "asObject", asObject);
+ addPropertyTo(array, "asMutable", asMutableArray);
+
+ for(var i = 0, length = array.length; i < length; i++) {
+ array[i] = Immutable(array[i]);
+ }
+
+ return makeImmutable(array, mutatingArrayMethods);
+ }
+
+ /**
+ * Effectively performs a map() over the elements in the array, using the
+ * provided iterator, except that whenever the iterator returns an array, that
+ * array's elements are added to the final result instead of the array itself.
+ *
+ * @param {function} iterator - The iterator function that will be invoked on each element in the array. It will receive three arguments: the current value, the current index, and the current object.
+ */
+ function flatMap(iterator) {
+ // Calling .flatMap() with no arguments is a no-op. Don't bother cloning.
+ if (arguments.length === 0) {
+ return this;
+ }
+
+ var result = [],
+ length = this.length,
+ index;
+
+ for (index = 0; index < length; index++) {
+ var iteratorResult = iterator(this[index], index, this);
+
+ if (iteratorResult instanceof Array) {
+ // Concatenate Array results into the return value we're building up.
+ result.push.apply(result, iteratorResult);
+ } else {
+ // Handle non-Array results the same way map() does.
+ result.push(iteratorResult);
+ }
+ }
+
+ return makeImmutableArray(result);
+ }
+
+ /**
+ * Returns an Immutable copy of the object without the given keys included.
+ *
+ * @param {array} keysToRemove - A list of strings representing the keys to exclude in the return value. Instead of providing a single array, this method can also be called by passing multiple strings as separate arguments.
+ */
+ function without(keysToRemove) {
+ // Calling .without() with no arguments is a no-op. Don't bother cloning.
+ if (arguments.length === 0) {
+ return this;
+ }
+
+ // If we weren't given an array, use the arguments list.
+ if (!(keysToRemove instanceof Array)) {
+ keysToRemove = Array.prototype.slice.call(arguments);
+ }
+
+ var result = this.instantiateEmptyObject();
+
+ for (var key in this) {
+ if (this.hasOwnProperty(key) && (keysToRemove.indexOf(key) === -1)) {
+ result[key] = this[key];
+ }
+ }
+
+ return makeImmutableObject(result,
+ {instantiateEmptyObject: this.instantiateEmptyObject});
+ }
+
+ function asMutableArray(opts) {
+ var result = [], i, length;
+
+ if(opts && opts.deep) {
+ for(i = 0, length = this.length; i < length; i++) {
+ result.push( asDeepMutable(this[i]) );
+ }
+ } else {
+ for(i = 0, length = this.length; i < length; i++) {
+ result.push(this[i]);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Effectively performs a [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) over the elements in the array, expecting that the iterator function
+ * will return an array of two elements - the first representing a key, the other
+ * a value. Then returns an Immutable Object constructed of those keys and values.
+ *
+ * @param {function} iterator - A function which should return an array of two elements - the first representing the desired key, the other the desired value.
+ */
+ function asObject(iterator) {
+ // If no iterator was provided, assume the identity function
+ // (suggesting this array is already a list of key/value pairs.)
+ if (typeof iterator !== "function") {
+ iterator = function(value) { return value; };
+ }
+
+ var result = {},
+ length = this.length,
+ index;
+
+ for (index = 0; index < length; index++) {
+ var pair = iterator(this[index], index, this),
+ key = pair[0],
+ value = pair[1];
+
+ result[key] = value;
+ }
+
+ return makeImmutableObject(result);
+ }
+
+ function asDeepMutable(obj) {
+ if(!obj || !obj.hasOwnProperty(immutabilityTag) || obj instanceof Date) { return obj; }
+ return obj.asMutable({deep: true});
+ }
+
+ function quickCopy(src, dest) {
+ for (var key in src) {
+ if (src.hasOwnProperty(key)) {
+ dest[key] = src[key];
+ }
+ }
+
+ return dest;
+ }
+
+ /**
+ * Returns an Immutable Object containing the properties and values of both
+ * this object and the provided object, prioritizing the provided object's
+ * values whenever the same key is present in both objects.
+ *
+ * @param {object} other - The other object to merge. Multiple objects can be passed as an array. In such a case, the later an object appears in that list, the higher its priority.
+ * @param {object} config - Optional config object that contains settings. Supported settings are: {deep: true} for deep merge and {merger: mergerFunc} where mergerFunc is a function
+ * that takes a property from both objects. If anything is returned it overrides the normal merge behaviour.
+ */
+ function merge(other, config) {
+ // Calling .merge() with no arguments is a no-op. Don't bother cloning.
+ if (arguments.length === 0) {
+ return this;
+ }
+
+ if (other === null || (typeof other !== "object")) {
+ throw new TypeError("Immutable#merge can only be invoked with objects or arrays, not " + JSON.stringify(other));
+ }
+
+ var anyChanges = false,
+ result = quickCopy(this, this.instantiateEmptyObject()), // A shallow clone of this object.
+ receivedArray = (other instanceof Array),
+ deep = config && config.deep,
+ merger = config && config.merger,
+ key;
+
+ // Use the given key to extract a value from the given object, then place
+ // that value in the result object under the same key. If that resulted
+ // in a change from this object's value at that key, set anyChanges = true.
+ function addToResult(currentObj, otherObj, key) {
+ var immutableValue = Immutable(otherObj[key]);
+ var mergerResult = merger && merger(currentObj[key], immutableValue, config);
+ if (merger && mergerResult && mergerResult === currentObj[key]) return;
+
+ anyChanges = anyChanges ||
+ mergerResult !== undefined ||
+ (!currentObj.hasOwnProperty(key) ||
+ ((immutableValue !== currentObj[key]) &&
+ // Avoid false positives due to (NaN !== NaN) evaluating to true
+ (immutableValue === immutableValue)));
+
+ if (mergerResult) {
+ result[key] = mergerResult;
+ } else if (deep && isMergableObject(currentObj[key]) && isMergableObject(immutableValue)) {
+ result[key] = currentObj[key].merge(immutableValue, config);
+ } else {
+ result[key] = immutableValue;
+ }
+ }
+
+ // Achieve prioritization by overriding previous values that get in the way.
+ if (!receivedArray) {
+ // The most common use case: just merge one object into the existing one.
+ for (key in other) {
+ if (other.hasOwnProperty(key)) {
+ addToResult(this, other, key);
+ }
+ }
+ } else {
+ // We also accept an Array
+ for (var index=0; index < other.length; index++) {
+ var otherFromArray = other[index];
+
+ for (key in otherFromArray) {
+ if (otherFromArray.hasOwnProperty(key)) {
+ addToResult(this, otherFromArray, key);
+ }
+ }
+ }
+ }
+
+ if (anyChanges) {
+ return makeImmutableObject(result,
+ {instantiateEmptyObject: this.instantiateEmptyObject});
+ } else {
+ return this;
+ }
+ }
+
+ function asMutableObject(opts) {
+ var result = this.instantiateEmptyObject(), key;
+
+ if(opts && opts.deep) {
+ for (key in this) {
+ if (this.hasOwnProperty(key)) {
+ result[key] = asDeepMutable(this[key]);
+ }
+ }
+ } else {
+ for (key in this) {
+ if (this.hasOwnProperty(key)) {
+ result[key] = this[key];
+ }
+ }
+ }
+
+ return result;
+ }
+
+ // Creates plain object to be used for cloning
+ function instantiatePlainObject() {
+ return {};
+ }
+
+ // Finalizes an object with immutable methods, freezes it, and returns it.
+ function makeImmutableObject(obj, options) {
+ var instantiateEmptyObject =
+ (options && options.instantiateEmptyObject) ?
+ options.instantiateEmptyObject : instantiatePlainObject;
+
+ addPropertyTo(obj, "merge", merge);
+ addPropertyTo(obj, "without", without);
+ addPropertyTo(obj, "asMutable", asMutableObject);
+ addPropertyTo(obj, "instantiateEmptyObject", instantiateEmptyObject);
+
+ return makeImmutable(obj, mutatingObjectMethods);
+ }
+
+ function Immutable(obj, options) {
+ if (isImmutable(obj)) {
+ return obj;
+ } else if (obj instanceof Array) {
+ return makeImmutableArray(obj.slice());
+ } else if (obj instanceof Date) {
+ return makeImmutable(new Date(obj.getTime()));
+ } else {
+ // Don't freeze the object we were given; make a clone and use that.
+ var prototype = options && options.prototype;
+ var instantiateEmptyObject =
+ (!prototype || prototype === Object.prototype) ?
+ instantiatePlainObject : (function() { return Object.create(prototype); });
+ var clone = instantiateEmptyObject();
+
+ for (var key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ clone[key] = Immutable(obj[key]);
+ }
+ }
+
+ return makeImmutableObject(clone,
+ {instantiateEmptyObject: instantiateEmptyObject});
+ }
+ }
+
+ // Export the library
+ Immutable.isImmutable = isImmutable;
+ Immutable.ImmutableError = ImmutableError;
+
+ Object.freeze(Immutable);
+
+ /* istanbul ignore if */
+ if (typeof module === "object") {
+ module.exports = Immutable;
+ } else if (typeof exports === "object") {
+ exports.Immutable = Immutable;
+ } else if (typeof window === "object") {
+ window.Immutable = Immutable;
+ } else if (typeof global === "object") {
+ global.Immutable = Immutable;
+ }
+})();
diff --git a/devtools/client/shared/view-source.js b/devtools/client/shared/view-source.js
new file mode 100644
index 000000000..6e2623ab4
--- /dev/null
+++ b/devtools/client/shared/view-source.js
@@ -0,0 +1,185 @@
+/* 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";
+
+var { Task } = require("devtools/shared/task");
+
+var Services = require("Services");
+var { gDevTools } = require("devtools/client/framework/devtools");
+var { getSourceText } = require("devtools/client/debugger/content/queries");
+
+/**
+ * Tries to open a Stylesheet file in the Style Editor. If the file is not
+ * found, it is opened in source view instead.
+ * Returns a promise resolving to a boolean indicating whether or not
+ * the source was able to be displayed in the StyleEditor, as the built-in
+ * Firefox View Source is the fallback.
+ *
+ * @param {Toolbox} toolbox
+ * @param {string} sourceURL
+ * @param {number} sourceLine
+ *
+ * @return {Promise<boolean>}
+ */
+exports.viewSourceInStyleEditor = Task.async(function* (toolbox, sourceURL,
+ sourceLine) {
+ let panel = yield toolbox.loadTool("styleeditor");
+
+ try {
+ yield panel.selectStyleSheet(sourceURL, sourceLine);
+ yield toolbox.selectTool("styleeditor");
+ return true;
+ } catch (e) {
+ exports.viewSource(toolbox, sourceURL, sourceLine);
+ return false;
+ }
+});
+
+/**
+ * Tries to open a JavaScript file in the Debugger. If the file is not found,
+ * it is opened in source view instead.
+ * Returns a promise resolving to a boolean indicating whether or not
+ * the source was able to be displayed in the Debugger, as the built-in Firefox
+ * View Source is the fallback.
+ *
+ * @param {Toolbox} toolbox
+ * @param {string} sourceURL
+ * @param {number} sourceLine
+ *
+ * @return {Promise<boolean>}
+ */
+exports.viewSourceInDebugger = Task.async(function* (toolbox, sourceURL, sourceLine) {
+ // If the Debugger was already open, switch to it and try to show the
+ // source immediately. Otherwise, initialize it and wait for the sources
+ // to be added first.
+ let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
+ let dbg = yield toolbox.loadTool("jsdebugger");
+
+ // New debugger frontend
+ if (Services.prefs.getBoolPref("devtools.debugger.new-debugger-frontend")) {
+ yield toolbox.selectTool("jsdebugger");
+ const source = dbg._selectors().getSourceByURL(dbg._getState(), sourceURL);
+ if (source) {
+ dbg._actions().selectSourceURL(sourceURL, { line: sourceLine });
+ return true;
+ }
+
+ exports.viewSource(toolbox, sourceURL, sourceLine);
+ return false;
+ }
+
+ const win = dbg.panelWin;
+
+ // Old debugger frontend
+ if (!debuggerAlreadyOpen) {
+ yield win.DebuggerController.waitForSourcesLoaded();
+ }
+
+ let { DebuggerView } = win;
+ let { Sources } = DebuggerView;
+
+ let item = Sources.getItemForAttachment(a => a.source.url === sourceURL);
+ if (item) {
+ yield toolbox.selectTool("jsdebugger");
+
+ // Determine if the source has already finished loading. There's two cases
+ // in which we need to wait for the source to be shown:
+ // 1) The requested source is not yet selected and will be shown once it is
+ // selected and loaded
+ // 2) The requested source is selected BUT the source text is still loading.
+ const { actor } = item.attachment.source;
+ const state = win.DebuggerController.getState();
+
+ // (1) Is the source selected?
+ const selected = state.sources.selectedSource;
+ const isSelected = selected === actor;
+
+ // (2) Has the source text finished loading?
+ let isLoading = false;
+
+ // Only check if the source is loading when the source is already selected.
+ // If the source is not selected, we will select it below and the already
+ // pending load will be cancelled and this check is useless.
+ if (isSelected) {
+ const sourceTextInfo = getSourceText(state, selected);
+ isLoading = sourceTextInfo && sourceTextInfo.loading;
+ }
+
+ // Select the requested source
+ DebuggerView.setEditorLocation(actor, sourceLine, { noDebug: true });
+
+ // Wait for it to load
+ if (!isSelected || isLoading) {
+ yield win.DebuggerController.waitForSourceShown(sourceURL);
+ }
+ return true;
+ }
+
+ // If not found, still attempt to open in View Source
+ exports.viewSource(toolbox, sourceURL, sourceLine);
+ return false;
+});
+
+/**
+ * Tries to open a JavaScript file in the corresponding Scratchpad.
+ *
+ * @param {string} sourceURL
+ * @param {number} sourceLine
+ *
+ * @return {Promise}
+ */
+exports.viewSourceInScratchpad = Task.async(function* (sourceURL, sourceLine) {
+ // Check for matching top level scratchpad window.
+ let wins = Services.wm.getEnumerator("devtools:scratchpad");
+
+ while (wins.hasMoreElements()) {
+ let win = wins.getNext();
+
+ if (!win.closed && win.Scratchpad.uniqueName === sourceURL) {
+ win.focus();
+ win.Scratchpad.editor.setCursor({ line: sourceLine, ch: 0 });
+ return;
+ }
+ }
+
+ // For scratchpads within toolbox
+ for (let [, toolbox] of gDevTools) {
+ let scratchpadPanel = toolbox.getPanel("scratchpad");
+ if (scratchpadPanel) {
+ let { scratchpad } = scratchpadPanel;
+ if (scratchpad.uniqueName === sourceURL) {
+ toolbox.selectTool("scratchpad");
+ toolbox.raise();
+ scratchpad.editor.focus();
+ scratchpad.editor.setCursor({ line: sourceLine, ch: 0 });
+ return;
+ }
+ }
+ }
+});
+
+/**
+ * Open a link in Firefox's View Source.
+ *
+ * @param {Toolbox} toolbox
+ * @param {string} sourceURL
+ * @param {number} sourceLine
+ *
+ * @return {Promise}
+ */
+exports.viewSource = Task.async(function* (toolbox, sourceURL, sourceLine) {
+ // Attempt to access view source via a browser first, which may display it in
+ // a tab, if enabled.
+ let browserWin = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ if (browserWin && browserWin.BrowserViewSourceOfDocument) {
+ return browserWin.BrowserViewSourceOfDocument({
+ URL: sourceURL,
+ lineNumber: sourceLine
+ });
+ }
+ let utils = toolbox.gViewSourceUtils;
+ utils.viewSource(sourceURL, null, toolbox.doc, sourceLine || 0);
+ return null;
+});
diff --git a/devtools/client/shared/webgl-utils.js b/devtools/client/shared/webgl-utils.js
new file mode 100644
index 000000000..f7618c397
--- /dev/null
+++ b/devtools/client/shared/webgl-utils.js
@@ -0,0 +1,55 @@
+/* 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 { Cc, Ci } = require("chrome");
+const Services = require("Services");
+
+const WEBGL_CONTEXT_NAME = "experimental-webgl";
+
+function isWebGLForceEnabled() {
+ return Services.prefs.getBoolPref("webgl.force-enabled");
+}
+
+function isWebGLSupportedByGFX() {
+ let supported = false;
+
+ try {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ let angle = gfxInfo.FEATURE_WEBGL_ANGLE;
+ let opengl = gfxInfo.FEATURE_WEBGL_OPENGL;
+
+ // if either the Angle or OpenGL renderers are available, WebGL should work
+ supported = gfxInfo.getFeatureStatus(angle) === gfxInfo.FEATURE_STATUS_OK ||
+ gfxInfo.getFeatureStatus(opengl) === gfxInfo.FEATURE_STATUS_OK;
+ } catch (e) {
+ return false;
+ }
+ return supported;
+}
+
+function create3DContext(canvas) {
+ // try to get a valid context from an existing canvas
+ let context = null;
+ try {
+ context = canvas.getContext(WEBGL_CONTEXT_NAME, aFlags);
+ } catch (e) {
+ return null;
+ }
+ return context;
+}
+
+function createCanvas(doc) {
+ return doc.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+}
+
+function isWebGLSupported(doc) {
+ let supported =
+ !isWebGLForceEnabled() &&
+ isWebGLSupportedByGFX() &&
+ create3DContext(createCanvas(doc));
+
+ return supported;
+}
+exports.isWebGLSupported = isWebGLSupported;
diff --git a/devtools/client/shared/widgets/AbstractTreeItem.jsm b/devtools/client/shared/widgets/AbstractTreeItem.jsm
new file mode 100644
index 000000000..541ab6777
--- /dev/null
+++ b/devtools/client/shared/widgets/AbstractTreeItem.jsm
@@ -0,0 +1,661 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { interfaces: Ci, utils: Cu } = Components;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
+const { KeyCodes } = require("devtools/client/shared/keycodes");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource://devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+
+this.EXPORTED_SYMBOLS = ["AbstractTreeItem"];
+
+/**
+ * A very generic and low-level tree view implementation. It is not intended
+ * to be used alone, but as a base class that you can extend to build your
+ * own custom implementation.
+ *
+ * Language:
+ * - An "item" is an instance of an AbstractTreeItem.
+ * - An "element" or "node" is an nsIDOMNode.
+ *
+ * The following events are emitted by this tree, always from the root item,
+ * with the first argument pointing to the affected child item:
+ * - "expand": when an item is expanded in the tree
+ * - "collapse": when an item is collapsed in the tree
+ * - "focus": when an item is selected in the tree
+ *
+ * For example, you can extend this abstract class like this:
+ *
+ * function MyCustomTreeItem(dataSrc, properties) {
+ * AbstractTreeItem.call(this, properties);
+ * this.itemDataSrc = dataSrc;
+ * }
+ *
+ * MyCustomTreeItem.prototype = Heritage.extend(AbstractTreeItem.prototype, {
+ * _displaySelf: function(document, arrowNode) {
+ * let node = document.createElement("hbox");
+ * ...
+ * // Append the provided arrow node wherever you want.
+ * node.appendChild(arrowNode);
+ * ...
+ * // Use `this.itemDataSrc` to customize the tree item and
+ * // `this.level` to calculate the indentation.
+ * node.style.marginInlineStart = (this.level * 10) + "px";
+ * node.appendChild(document.createTextNode(this.itemDataSrc.label));
+ * ...
+ * return node;
+ * },
+ * _populateSelf: function(children) {
+ * ...
+ * // Use `this.itemDataSrc` to get the data source for the child items.
+ * let someChildDataSrc = this.itemDataSrc.children[0];
+ * ...
+ * children.push(new MyCustomTreeItem(someChildDataSrc, {
+ * parent: this,
+ * level: this.level + 1
+ * }));
+ * ...
+ * }
+ * });
+ *
+ * And then you could use it like this:
+ *
+ * let dataSrc = {
+ * label: "root",
+ * children: [{
+ * label: "foo",
+ * children: []
+ * }, {
+ * label: "bar",
+ * children: [{
+ * label: "baz",
+ * children: []
+ * }]
+ * }]
+ * };
+ * let root = new MyCustomTreeItem(dataSrc, { parent: null });
+ * root.attachTo(nsIDOMNode);
+ * root.expand();
+ *
+ * The following tree view will be generated (after expanding all nodes):
+ * ▼ root
+ * ▶ foo
+ * ▼ bar
+ * ▶ baz
+ *
+ * The way the data source is implemented is completely up to you. There's
+ * no assumptions made and you can use it however you like inside the
+ * `_displaySelf` and `populateSelf` methods. If you need to add children to a
+ * node at a later date, you just need to modify the data source:
+ *
+ * dataSrc[...path-to-foo...].children.push({
+ * label: "lazily-added-node"
+ * children: []
+ * });
+ *
+ * The existing tree view will be modified like so (after expanding `foo`):
+ * ▼ root
+ * ▼ foo
+ * ▶ lazily-added-node
+ * ▼ bar
+ * ▶ baz
+ *
+ * Everything else is taken care of automagically!
+ *
+ * @param AbstractTreeItem parent
+ * The parent tree item. Should be null for root items.
+ * @param number level
+ * The indentation level in the tree. The root item is at level 0.
+ */
+function AbstractTreeItem({ parent, level }) {
+ this._rootItem = parent ? parent._rootItem : this;
+ this._parentItem = parent;
+ this._level = level || 0;
+ this._childTreeItems = [];
+
+ // Events are always propagated through the root item. Decorating every
+ // tree item as an event emitter is a very costly operation.
+ if (this == this._rootItem) {
+ EventEmitter.decorate(this);
+ }
+}
+this.AbstractTreeItem = AbstractTreeItem;
+
+AbstractTreeItem.prototype = {
+ _containerNode: null,
+ _targetNode: null,
+ _arrowNode: null,
+ _constructed: false,
+ _populated: false,
+ _expanded: false,
+
+ /**
+ * Optionally, trees may be allowed to automatically expand a few levels deep
+ * to avoid initially displaying a completely collapsed tree.
+ */
+ autoExpandDepth: 0,
+
+ /**
+ * Creates the view for this tree item. Implement this method in the
+ * inheriting classes to create the child node displayed in the tree.
+ * Use `this.level` and the provided `arrowNode` as you see fit.
+ *
+ * @param nsIDOMNode document
+ * @param nsIDOMNode arrowNode
+ * @return nsIDOMNode
+ */
+ _displaySelf: function (document, arrowNode) {
+ throw new Error(
+ "The `_displaySelf` method needs to be implemented by inheriting classes.");
+ },
+
+ /**
+ * Populates this tree item with child items, whenever it's expanded.
+ * Implement this method in the inheriting classes to fill the provided
+ * `children` array with AbstractTreeItem instances, which will then be
+ * magically handled by this tree item.
+ *
+ * @param array:AbstractTreeItem children
+ */
+ _populateSelf: function (children) {
+ throw new Error(
+ "The `_populateSelf` method needs to be implemented by inheriting classes.");
+ },
+
+ /**
+ * Gets the this tree's owner document.
+ * @return Document
+ */
+ get document() {
+ return this._containerNode.ownerDocument;
+ },
+
+ /**
+ * Gets the root item of this tree.
+ * @return AbstractTreeItem
+ */
+ get root() {
+ return this._rootItem;
+ },
+
+ /**
+ * Gets the parent of this tree item.
+ * @return AbstractTreeItem
+ */
+ get parent() {
+ return this._parentItem;
+ },
+
+ /**
+ * Gets the indentation level of this tree item.
+ */
+ get level() {
+ return this._level;
+ },
+
+ /**
+ * Gets the element displaying this tree item.
+ */
+ get target() {
+ return this._targetNode;
+ },
+
+ /**
+ * Gets the element containing all tree items.
+ * @return nsIDOMNode
+ */
+ get container() {
+ return this._containerNode;
+ },
+
+ /**
+ * Returns whether or not this item is populated in the tree.
+ * Collapsed items can still be populated.
+ * @return boolean
+ */
+ get populated() {
+ return this._populated;
+ },
+
+ /**
+ * Returns whether or not this item is expanded in the tree.
+ * Expanded items with no children aren't consudered `populated`.
+ * @return boolean
+ */
+ get expanded() {
+ return this._expanded;
+ },
+
+ /**
+ * Gets the bounds for this tree's container without flushing.
+ * @return object
+ */
+ get bounds() {
+ let win = this.document.defaultView;
+ let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ return utils.getBoundsWithoutFlushing(this._containerNode);
+ },
+
+ /**
+ * Creates and appends this tree item to the specified parent element.
+ *
+ * @param nsIDOMNode containerNode
+ * The parent element for this tree item (and every other tree item).
+ * @param nsIDOMNode fragmentNode [optional]
+ * An optional document fragment temporarily holding this tree item in
+ * the current batch. Defaults to the `containerNode`.
+ * @param nsIDOMNode beforeNode [optional]
+ * An optional child element which should succeed this tree item.
+ */
+ attachTo: function (containerNode, fragmentNode = containerNode, beforeNode = null) {
+ this._containerNode = containerNode;
+ this._constructTargetNode();
+
+ if (beforeNode) {
+ fragmentNode.insertBefore(this._targetNode, beforeNode);
+ } else {
+ fragmentNode.appendChild(this._targetNode);
+ }
+
+ if (this._level < this.autoExpandDepth) {
+ this.expand();
+ }
+ },
+
+ /**
+ * Permanently removes this tree item (and all subsequent children) from the
+ * parent container.
+ */
+ remove: function () {
+ this._targetNode.remove();
+ this._hideChildren();
+ this._childTreeItems.length = 0;
+ },
+
+ /**
+ * Focuses this item in the tree.
+ */
+ focus: function () {
+ this._targetNode.focus();
+ },
+
+ /**
+ * Expands this item in the tree.
+ */
+ expand: function () {
+ if (this._expanded) {
+ return;
+ }
+ this._expanded = true;
+ this._arrowNode.setAttribute("open", "");
+ this._targetNode.setAttribute("expanded", "");
+ this._toggleChildren(true);
+ this._rootItem.emit("expand", this);
+ },
+
+ /**
+ * Collapses this item in the tree.
+ */
+ collapse: function () {
+ if (!this._expanded) {
+ return;
+ }
+ this._expanded = false;
+ this._arrowNode.removeAttribute("open");
+ this._targetNode.removeAttribute("expanded", "");
+ this._toggleChildren(false);
+ this._rootItem.emit("collapse", this);
+ },
+
+ /**
+ * Returns the child item at the specified index.
+ *
+ * @param number index
+ * @return AbstractTreeItem
+ */
+ getChild: function (index = 0) {
+ return this._childTreeItems[index];
+ },
+
+ /**
+ * Calls the provided function on all the descendants of this item.
+ * If this item was never expanded, then no descendents exist yet.
+ * @param function cb
+ */
+ traverse: function (cb) {
+ for (let child of this._childTreeItems) {
+ cb(child);
+ child.bfs();
+ }
+ },
+
+ /**
+ * Calls the provided function on all descendants of this item until
+ * a truthy value is returned by the predicate.
+ * @param function predicate
+ * @return AbstractTreeItem
+ */
+ find: function (predicate) {
+ for (let child of this._childTreeItems) {
+ if (predicate(child) || child.find(predicate)) {
+ return child;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Shows or hides all the children of this item in the tree. If neessary,
+ * populates this item with children.
+ *
+ * @param boolean visible
+ * True if the children should be visible, false otherwise.
+ */
+ _toggleChildren: function (visible) {
+ if (visible) {
+ if (!this._populated) {
+ this._populateSelf(this._childTreeItems);
+ this._populated = this._childTreeItems.length > 0;
+ }
+ this._showChildren();
+ } else {
+ this._hideChildren();
+ }
+ },
+
+ /**
+ * Shows all children of this item in the tree.
+ */
+ _showChildren: function () {
+ // If this is the root item and we're not expanding any child nodes,
+ // it is safe to append everything at once.
+ if (this == this._rootItem && this.autoExpandDepth == 0) {
+ this._appendChildrenBatch();
+ }
+ // Otherwise, append the child items and their descendants successively;
+ // if not, the tree will become garbled and nodes will intertwine,
+ // since all the tree items are sharing a single container node.
+ else {
+ this._appendChildrenSuccessive();
+ }
+ },
+
+ /**
+ * Hides all children of this item in the tree.
+ */
+ _hideChildren: function () {
+ for (let item of this._childTreeItems) {
+ item._targetNode.remove();
+ item._hideChildren();
+ }
+ },
+
+ /**
+ * Appends all children in a single batch.
+ * This only works properly for root nodes when no child nodes will expand.
+ */
+ _appendChildrenBatch: function () {
+ if (this._fragment === undefined) {
+ this._fragment = this.document.createDocumentFragment();
+ }
+
+ let childTreeItems = this._childTreeItems;
+
+ for (let i = 0, len = childTreeItems.length; i < len; i++) {
+ childTreeItems[i].attachTo(this._containerNode, this._fragment);
+ }
+
+ this._containerNode.appendChild(this._fragment);
+ },
+
+ /**
+ * Appends all children successively.
+ */
+ _appendChildrenSuccessive: function () {
+ let childTreeItems = this._childTreeItems;
+ let expandedChildTreeItems = childTreeItems.filter(e => e._expanded);
+ let nextNode = this._getSiblingAtDelta(1);
+
+ for (let i = 0, len = childTreeItems.length; i < len; i++) {
+ childTreeItems[i].attachTo(this._containerNode, undefined, nextNode);
+ }
+ for (let i = 0, len = expandedChildTreeItems.length; i < len; i++) {
+ expandedChildTreeItems[i]._showChildren();
+ }
+ },
+
+ /**
+ * Constructs and stores the target node displaying this tree item.
+ */
+ _constructTargetNode: function () {
+ if (this._constructed) {
+ return;
+ }
+ this._onArrowClick = this._onArrowClick.bind(this);
+ this._onClick = this._onClick.bind(this);
+ this._onDoubleClick = this._onDoubleClick.bind(this);
+ this._onKeyPress = this._onKeyPress.bind(this);
+ this._onFocus = this._onFocus.bind(this);
+ this._onBlur = this._onBlur.bind(this);
+
+ let document = this.document;
+
+ let arrowNode = this._arrowNode = document.createElement("hbox");
+ arrowNode.className = "arrow theme-twisty";
+ arrowNode.addEventListener("mousedown", this._onArrowClick);
+
+ let targetNode = this._targetNode = this._displaySelf(document, arrowNode);
+ targetNode.style.MozUserFocus = "normal";
+
+ targetNode.addEventListener("mousedown", this._onClick);
+ targetNode.addEventListener("dblclick", this._onDoubleClick);
+ targetNode.addEventListener("keypress", this._onKeyPress);
+ targetNode.addEventListener("focus", this._onFocus);
+ targetNode.addEventListener("blur", this._onBlur);
+
+ this._constructed = true;
+ },
+
+ /**
+ * Gets the element displaying an item in the tree at the specified offset
+ * relative to this item.
+ *
+ * @param number delta
+ * The offset from this item to the target item.
+ * @return nsIDOMNode
+ * The element displaying the target item at the specified offset.
+ */
+ _getSiblingAtDelta: function (delta) {
+ let childNodes = this._containerNode.childNodes;
+ let indexOfSelf = Array.indexOf(childNodes, this._targetNode);
+ if (indexOfSelf + delta >= 0) {
+ return childNodes[indexOfSelf + delta];
+ }
+ return undefined;
+ },
+
+ _getNodesPerPageSize: function() {
+ let childNodes = this._containerNode.childNodes;
+ let nodeHeight = this._getHeight(childNodes[childNodes.length - 1]);
+ let containerHeight = this.bounds.height;
+ return Math.ceil(containerHeight / nodeHeight);
+ },
+
+ _getHeight: function(elem) {
+ let win = this.document.defaultView;
+ let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ return utils.getBoundsWithoutFlushing(elem).height;
+ },
+
+ /**
+ * Focuses the first item in this tree.
+ */
+ _focusFirstNode: function () {
+ let childNodes = this._containerNode.childNodes;
+ // The root node of the tree may be hidden in practice, so uses for-loop
+ // here to find the next visible node.
+ for (let i = 0; i < childNodes.length; i++) {
+ // The height will be 0 if an element is invisible.
+ if (this._getHeight(childNodes[i])) {
+ childNodes[i].focus();
+ return;
+ }
+ }
+ },
+
+ /**
+ * Focuses the last item in this tree.
+ */
+ _focusLastNode: function () {
+ let childNodes = this._containerNode.childNodes;
+ childNodes[childNodes.length - 1].focus();
+ },
+
+ /**
+ * Focuses the next item in this tree.
+ */
+ _focusNextNode: function () {
+ let nextElement = this._getSiblingAtDelta(1);
+ if (nextElement) nextElement.focus(); // nsIDOMNode
+ },
+
+ /**
+ * Focuses the previous item in this tree.
+ */
+ _focusPrevNode: function () {
+ let prevElement = this._getSiblingAtDelta(-1);
+ if (prevElement) prevElement.focus(); // nsIDOMNode
+ },
+
+ /**
+ * Focuses the parent item in this tree.
+ *
+ * The parent item is not always the previous item, because any tree item
+ * may have multiple children.
+ */
+ _focusParentNode: function () {
+ let parentItem = this._parentItem;
+ if (parentItem) parentItem.focus(); // AbstractTreeItem
+ },
+
+ /**
+ * Handler for the "click" event on the arrow node of this tree item.
+ */
+ _onArrowClick: function (e) {
+ if (!this._expanded) {
+ this.expand();
+ } else {
+ this.collapse();
+ }
+ },
+
+ /**
+ * Handler for the "click" event on the element displaying this tree item.
+ */
+ _onClick: function (e) {
+ e.stopPropagation();
+ this.focus();
+ },
+
+ /**
+ * Handler for the "dblclick" event on the element displaying this tree item.
+ */
+ _onDoubleClick: function (e) {
+ // Ignore dblclick on the arrow as it has already recived and handled two
+ // click events.
+ if (!e.target.classList.contains("arrow")) {
+ this._onArrowClick(e);
+ }
+ this.focus();
+ },
+
+ /**
+ * Handler for the "keypress" event on the element displaying this tree item.
+ */
+ _onKeyPress: function (e) {
+ // Prevent scrolling when pressing navigation keys.
+ ViewHelpers.preventScrolling(e);
+
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_UP:
+ this._focusPrevNode();
+ return;
+
+ case KeyCodes.DOM_VK_DOWN:
+ this._focusNextNode();
+ return;
+
+ case KeyCodes.DOM_VK_LEFT:
+ if (this._expanded && this._populated) {
+ this.collapse();
+ } else {
+ this._focusParentNode();
+ }
+ return;
+
+ case KeyCodes.DOM_VK_RIGHT:
+ if (!this._expanded) {
+ this.expand();
+ } else {
+ this._focusNextNode();
+ }
+ return;
+
+ case KeyCodes.DOM_VK_PAGE_UP:
+ let pageUpElement =
+ this._getSiblingAtDelta(-this._getNodesPerPageSize());
+ // There's a chance that the root node is hidden. In this case, its
+ // height will be 0.
+ if (pageUpElement && this._getHeight(pageUpElement)) {
+ pageUpElement.focus();
+ } else {
+ this._focusFirstNode();
+ }
+ return;
+
+ case KeyCodes.DOM_VK_PAGE_DOWN:
+ let pageDownElement =
+ this._getSiblingAtDelta(this._getNodesPerPageSize());
+ if (pageDownElement) {
+ pageDownElement.focus();
+ } else {
+ this._focusLastNode();
+ }
+ return;
+
+ case KeyCodes.DOM_VK_HOME:
+ this._focusFirstNode();
+ return;
+
+ case KeyCodes.DOM_VK_END:
+ this._focusLastNode();
+ return;
+ }
+ },
+
+ /**
+ * Handler for the "focus" event on the element displaying this tree item.
+ */
+ _onFocus: function (e) {
+ this._rootItem.emit("focus", this);
+ },
+
+ /**
+ * Handler for the "blur" event on the element displaying this tree item.
+ */
+ _onBlur: function (e) {
+ this._rootItem.emit("blur", this);
+ }
+};
diff --git a/devtools/client/shared/widgets/BarGraphWidget.js b/devtools/client/shared/widgets/BarGraphWidget.js
new file mode 100644
index 000000000..b11c6c021
--- /dev/null
+++ b/devtools/client/shared/widgets/BarGraphWidget.js
@@ -0,0 +1,498 @@
+"use strict";
+
+const { Heritage, setNamedTimeout, clearNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const { AbstractCanvasGraph, CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+// Bar graph constants.
+
+const GRAPH_DAMPEN_VALUES_FACTOR = 0.75;
+
+// The following are in pixels
+const GRAPH_BARS_MARGIN_TOP = 1;
+const GRAPH_BARS_MARGIN_END = 1;
+const GRAPH_MIN_BARS_WIDTH = 5;
+const GRAPH_MIN_BLOCKS_HEIGHT = 1;
+
+const GRAPH_BACKGROUND_GRADIENT_START = "rgba(0,136,204,0.0)";
+const GRAPH_BACKGROUND_GRADIENT_END = "rgba(255,255,255,0.25)";
+
+const GRAPH_CLIPHEAD_LINE_COLOR = "#666";
+const GRAPH_SELECTION_LINE_COLOR = "#555";
+const GRAPH_SELECTION_BACKGROUND_COLOR = "rgba(0,136,204,0.25)";
+const GRAPH_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+const GRAPH_REGION_BACKGROUND_COLOR = "transparent";
+const GRAPH_REGION_STRIPES_COLOR = "rgba(237,38,85,0.2)";
+
+const GRAPH_HIGHLIGHTS_MASK_BACKGROUND = "rgba(255,255,255,0.75)";
+const GRAPH_HIGHLIGHTS_MASK_STRIPES = "rgba(255,255,255,0.5)";
+
+// in ms
+const GRAPH_LEGEND_MOUSEOVER_DEBOUNCE = 50;
+
+/**
+ * A bar graph, plotting tuples of values as rectangles.
+ *
+ * @see AbstractCanvasGraph for emitted events and other options.
+ *
+ * Example usage:
+ * let graph = new BarGraphWidget(node);
+ * graph.format = ...;
+ * graph.once("ready", () => {
+ * graph.setData(src);
+ * });
+ *
+ * The `graph.format` traits are mandatory and will determine how the values
+ * are styled as "blocks" in every "bar":
+ * [
+ * { color: "#f00", label: "Foo" },
+ * { color: "#0f0", label: "Bar" },
+ * ...
+ * { color: "#00f", label: "Baz" }
+ * ]
+ *
+ * Data source format:
+ * [
+ * { delta: x1, values: [y11, y12, ... y1n] },
+ * { delta: x2, values: [y21, y22, ... y2n] },
+ * ...
+ * { delta: xm, values: [ym1, ym2, ... ymn] }
+ * ]
+ * where each item in the array represents a "bar", for which every value
+ * represents a "block" inside that "bar", plotted at the "delta" position.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the graph.
+ */
+this.BarGraphWidget = function (parent, ...args) {
+ AbstractCanvasGraph.apply(this, [parent, "bar-graph", ...args]);
+
+ this.once("ready", () => {
+ this._onLegendMouseOver = this._onLegendMouseOver.bind(this);
+ this._onLegendMouseOut = this._onLegendMouseOut.bind(this);
+ this._onLegendMouseDown = this._onLegendMouseDown.bind(this);
+ this._onLegendMouseUp = this._onLegendMouseUp.bind(this);
+ this._createLegend();
+ });
+};
+
+BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+ clipheadLineColor: GRAPH_CLIPHEAD_LINE_COLOR,
+ selectionLineColor: GRAPH_SELECTION_LINE_COLOR,
+ selectionBackgroundColor: GRAPH_SELECTION_BACKGROUND_COLOR,
+ selectionStripesColor: GRAPH_SELECTION_STRIPES_COLOR,
+ regionBackgroundColor: GRAPH_REGION_BACKGROUND_COLOR,
+ regionStripesColor: GRAPH_REGION_STRIPES_COLOR,
+
+ /**
+ * List of colors used to fill each block inside every bar, also
+ * corresponding to labels displayed in this graph's legend.
+ * @see constructor
+ */
+ format: null,
+
+ /**
+ * Optionally offsets the `delta` in the data source by this scalar.
+ */
+ dataOffsetX: 0,
+
+ /**
+ * Optionally uses this value instead of the last tick in the data source
+ * to compute the horizontal scaling.
+ */
+ dataDuration: 0,
+
+ /**
+ * The scalar used to multiply the graph values to leave some headroom
+ * on the top.
+ */
+ dampenValuesFactor: GRAPH_DAMPEN_VALUES_FACTOR,
+
+ /**
+ * Bars that are too close too each other in the graph will be combined.
+ * This scalar specifies the required minimum width of each bar.
+ */
+ minBarsWidth: GRAPH_MIN_BARS_WIDTH,
+
+ /**
+ * Blocks in a bar that are too thin inside the bar will not be rendered.
+ * This scalar specifies the required minimum height of each block.
+ */
+ minBlocksHeight: GRAPH_MIN_BLOCKS_HEIGHT,
+
+ /**
+ * Renders the graph's background.
+ * @see AbstractCanvasGraph.prototype.buildBackgroundImage
+ */
+ buildBackgroundImage: function () {
+ let { canvas, ctx } = this._getNamedCanvas("bar-graph-background");
+ let width = this._width;
+ let height = this._height;
+
+ let gradient = ctx.createLinearGradient(0, 0, 0, height);
+ gradient.addColorStop(0, GRAPH_BACKGROUND_GRADIENT_START);
+ gradient.addColorStop(1, GRAPH_BACKGROUND_GRADIENT_END);
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, width, height);
+
+ return canvas;
+ },
+
+ /**
+ * Renders the graph's data source.
+ * @see AbstractCanvasGraph.prototype.buildGraphImage
+ */
+ buildGraphImage: function () {
+ if (!this.format || !this.format.length) {
+ throw new Error("The graph format traits are mandatory to style " +
+ "the data source.");
+ }
+ let { canvas, ctx } = this._getNamedCanvas("bar-graph-data");
+ let width = this._width;
+ let height = this._height;
+
+ let totalTypes = this.format.length;
+ let totalTicks = this._data.length;
+ let lastTick = this._data[totalTicks - 1].delta;
+
+ let minBarsWidth = this.minBarsWidth * this._pixelRatio;
+ let minBlocksHeight = this.minBlocksHeight * this._pixelRatio;
+
+ let duration = this.dataDuration || lastTick;
+ let dataScaleX = this.dataScaleX = width / (duration - this.dataOffsetX);
+ let dataScaleY = this.dataScaleY = height / this._calcMaxHeight({
+ data: this._data,
+ dataScaleX: dataScaleX,
+ minBarsWidth: minBarsWidth
+ }) * this.dampenValuesFactor;
+
+ // Draw the graph.
+
+ // Iterate over the blocks, then the bars, to draw all rectangles of
+ // the same color in a single pass. See the @constructor for more
+ // information about the data source, and how a "bar" contains "blocks".
+
+ this._blocksBoundingRects = [];
+ let prevHeight = [];
+ let scaledMarginEnd = GRAPH_BARS_MARGIN_END * this._pixelRatio;
+ let scaledMarginTop = GRAPH_BARS_MARGIN_TOP * this._pixelRatio;
+
+ for (let type = 0; type < totalTypes; type++) {
+ ctx.fillStyle = this.format[type].color || "#000";
+ ctx.beginPath();
+
+ let prevRight = 0;
+ let skippedCount = 0;
+ let skippedHeight = 0;
+
+ for (let tick = 0; tick < totalTicks; tick++) {
+ let delta = this._data[tick].delta;
+ let value = this._data[tick].values[type] || 0;
+ let blockRight = (delta - this.dataOffsetX) * dataScaleX;
+ let blockHeight = value * dataScaleY;
+
+ let blockWidth = blockRight - prevRight;
+ if (blockWidth < minBarsWidth) {
+ skippedCount++;
+ skippedHeight += blockHeight;
+ continue;
+ }
+
+ let averageHeight = (blockHeight + skippedHeight) / (skippedCount + 1);
+ if (averageHeight >= minBlocksHeight) {
+ let bottom = height - ~~prevHeight[tick];
+ ctx.moveTo(prevRight, bottom);
+ ctx.lineTo(prevRight, bottom - averageHeight);
+ ctx.lineTo(blockRight, bottom - averageHeight);
+ ctx.lineTo(blockRight, bottom);
+
+ // Remember this block's type and location.
+ this._blocksBoundingRects.push({
+ type: type,
+ start: prevRight,
+ end: blockRight,
+ top: bottom - averageHeight,
+ bottom: bottom
+ });
+
+ if (prevHeight[tick] === undefined) {
+ prevHeight[tick] = averageHeight + scaledMarginTop;
+ } else {
+ prevHeight[tick] += averageHeight + scaledMarginTop;
+ }
+ }
+
+ prevRight += blockWidth + scaledMarginEnd;
+ skippedHeight = 0;
+ skippedCount = 0;
+ }
+
+ ctx.fill();
+ }
+
+ // The blocks bounding rects isn't guaranteed to be sorted ascending by
+ // block location on the X axis. This should be the case, for better
+ // cache cohesion and a faster `buildMaskImage`.
+ this._blocksBoundingRects.sort((a, b) => a.start > b.start ? 1 : -1);
+
+ // Update the legend.
+
+ while (this._legendNode.hasChildNodes()) {
+ this._legendNode.firstChild.remove();
+ }
+ for (let { color, label } of this.format) {
+ this._createLegendItem(color, label);
+ }
+
+ return canvas;
+ },
+
+ /**
+ * Renders the graph's mask.
+ * Fades in only the parts of the graph that are inside the specified areas.
+ *
+ * @param array highlights
+ * A list of { start, end } values. Optionally, each object
+ * in the list may also specify { top, bottom } pixel values if the
+ * highlighting shouldn't span across the full height of the graph.
+ * @param boolean inPixels
+ * Set this to true if the { start, end } values in the highlights
+ * list are pixel values, and not values from the data source.
+ * @param function unpack [optional]
+ * @see AbstractCanvasGraph.prototype.getMappedSelection
+ */
+ buildMaskImage: function (highlights, inPixels = false,
+ unpack = e => e.delta) {
+ // A null `highlights` array is used to clear the mask. An empty array
+ // will mask the entire graph.
+ if (!highlights) {
+ return null;
+ }
+
+ // Get a render target for the highlights. It will be overlaid on top of
+ // the existing graph, masking the areas that aren't highlighted.
+
+ let { canvas, ctx } = this._getNamedCanvas("graph-highlights");
+ let width = this._width;
+ let height = this._height;
+
+ // Draw the background mask.
+
+ let pattern = AbstractCanvasGraph.getStripePattern({
+ ownerDocument: this._document,
+ backgroundColor: GRAPH_HIGHLIGHTS_MASK_BACKGROUND,
+ stripesColor: GRAPH_HIGHLIGHTS_MASK_STRIPES
+ });
+ ctx.fillStyle = pattern;
+ ctx.fillRect(0, 0, width, height);
+
+ // Clear highlighted areas.
+
+ let totalTicks = this._data.length;
+ let firstTick = unpack(this._data[0]);
+ let lastTick = unpack(this._data[totalTicks - 1]);
+
+ for (let { start, end, top, bottom } of highlights) {
+ if (!inPixels) {
+ start = CanvasGraphUtils.map(start, firstTick, lastTick, 0, width);
+ end = CanvasGraphUtils.map(end, firstTick, lastTick, 0, width);
+ }
+ let firstSnap = findFirst(this._blocksBoundingRects,
+ e => e.start >= start);
+ let lastSnap = findLast(this._blocksBoundingRects,
+ e => e.start >= start && e.end <= end);
+
+ let x1 = firstSnap ? firstSnap.start : start;
+ let x2;
+ if (lastSnap) {
+ x2 = lastSnap.end;
+ } else {
+ x2 = firstSnap ? firstSnap.end : end;
+ }
+
+ let y1 = top || 0;
+ let y2 = bottom || height;
+ ctx.clearRect(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ return canvas;
+ },
+
+ /**
+ * A list storing the bounding rectangle for each drawn block in the graph.
+ * Created whenever `buildGraphImage` is invoked.
+ */
+ _blocksBoundingRects: null,
+
+ /**
+ * Calculates the height of the tallest bar that would eventially be rendered
+ * in this graph.
+ *
+ * Bars that are too close too each other in the graph will be combined.
+ * @see `minBarsWidth`
+ *
+ * @return number
+ * The tallest bar height in this graph.
+ */
+ _calcMaxHeight: function ({ data, dataScaleX, minBarsWidth }) {
+ let maxHeight = 0;
+ let prevRight = 0;
+ let skippedCount = 0;
+ let skippedHeight = 0;
+ let scaledMarginEnd = GRAPH_BARS_MARGIN_END * this._pixelRatio;
+
+ for (let { delta, values } of data) {
+ let barRight = (delta - this.dataOffsetX) * dataScaleX;
+ let barHeight = values.reduce((a, b) => a + b, 0);
+
+ let barWidth = barRight - prevRight;
+ if (barWidth < minBarsWidth) {
+ skippedCount++;
+ skippedHeight += barHeight;
+ continue;
+ }
+
+ let averageHeight = (barHeight + skippedHeight) / (skippedCount + 1);
+ maxHeight = Math.max(averageHeight, maxHeight);
+
+ prevRight += barWidth + scaledMarginEnd;
+ skippedHeight = 0;
+ skippedCount = 0;
+ }
+
+ return maxHeight;
+ },
+
+ /**
+ * Creates the legend container when constructing this graph.
+ */
+ _createLegend: function () {
+ let legendNode = this._legendNode = this._document.createElementNS(HTML_NS,
+ "div");
+ legendNode.className = "bar-graph-widget-legend";
+ this._container.appendChild(legendNode);
+ },
+
+ /**
+ * Creates a legend item when constructing this graph.
+ */
+ _createLegendItem: function (color, label) {
+ let itemNode = this._document.createElementNS(HTML_NS, "div");
+ itemNode.className = "bar-graph-widget-legend-item";
+
+ let colorNode = this._document.createElementNS(HTML_NS, "span");
+ colorNode.setAttribute("view", "color");
+ colorNode.setAttribute("data-index", this._legendNode.childNodes.length);
+ colorNode.style.backgroundColor = color;
+ colorNode.addEventListener("mouseover", this._onLegendMouseOver);
+ colorNode.addEventListener("mouseout", this._onLegendMouseOut);
+ colorNode.addEventListener("mousedown", this._onLegendMouseDown);
+ colorNode.addEventListener("mouseup", this._onLegendMouseUp);
+
+ let labelNode = this._document.createElementNS(HTML_NS, "span");
+ labelNode.setAttribute("view", "label");
+ labelNode.textContent = label;
+
+ itemNode.appendChild(colorNode);
+ itemNode.appendChild(labelNode);
+ this._legendNode.appendChild(itemNode);
+ },
+
+ /**
+ * Invoked whenever a color node in the legend is hovered.
+ */
+ _onLegendMouseOver: function (ev) {
+ setNamedTimeout(
+ "bar-graph-debounce",
+ GRAPH_LEGEND_MOUSEOVER_DEBOUNCE,
+ () => {
+ let type = ev.target.dataset.index;
+ let rects = this._blocksBoundingRects.filter(e => e.type == type);
+
+ this._originalHighlights = this._mask;
+ this._hasCustomHighlights = true;
+ this.setMask(rects, true);
+
+ this.emit("legend-hover", [type, rects]);
+ }
+ );
+ },
+
+ /**
+ * Invoked whenever a color node in the legend is unhovered.
+ */
+ _onLegendMouseOut: function () {
+ clearNamedTimeout("bar-graph-debounce");
+
+ if (this._hasCustomHighlights) {
+ this.setMask(this._originalHighlights);
+ this._hasCustomHighlights = false;
+ this._originalHighlights = null;
+ }
+
+ this.emit("legend-unhover");
+ },
+
+ /**
+ * Invoked whenever a color node in the legend is pressed.
+ */
+ _onLegendMouseDown: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ let type = ev.target.dataset.index;
+ let rects = this._blocksBoundingRects.filter(e => e.type == type);
+ let leftmost = rects[0];
+ let rightmost = rects[rects.length - 1];
+ if (!leftmost || !rightmost) {
+ this.dropSelection();
+ } else {
+ this.setSelection({ start: leftmost.start, end: rightmost.end });
+ }
+
+ this.emit("legend-selection", [leftmost, rightmost]);
+ },
+
+ /**
+ * Invoked whenever a color node in the legend is released.
+ */
+ _onLegendMouseUp: function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+});
+
+/**
+ * Finds the first element in an array that validates a predicate.
+ * @param array
+ * @param function predicate
+ * @return number
+ */
+function findFirst(array, predicate) {
+ for (let i = 0, len = array.length; i < len; i++) {
+ let element = array[i];
+ if (predicate(element)) {
+ return element;
+ }
+ }
+ return null;
+}
+
+/**
+ * Finds the last element in an array that validates a predicate.
+ * @param array
+ * @param function predicate
+ * @return number
+ */
+function findLast(array, predicate) {
+ for (let i = array.length - 1; i >= 0; i--) {
+ let element = array[i];
+ if (predicate(element)) {
+ return element;
+ }
+ }
+ return null;
+}
+
+module.exports = BarGraphWidget;
diff --git a/devtools/client/shared/widgets/BreadcrumbsWidget.jsm b/devtools/client/shared/widgets/BreadcrumbsWidget.jsm
new file mode 100644
index 000000000..900a125b0
--- /dev/null
+++ b/devtools/client/shared/widgets/BreadcrumbsWidget.jsm
@@ -0,0 +1,250 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Cu = Components.utils;
+
+const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { ViewHelpers, setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+this.EXPORTED_SYMBOLS = ["BreadcrumbsWidget"];
+
+/**
+ * A breadcrumb-like list of items.
+ *
+ * Note: this widget should be used in tandem with the WidgetMethods in
+ * view-helpers.js.
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ * @param Object aOptions
+ * - smoothScroll: specifies if smooth scrolling on selection is enabled.
+ */
+this.BreadcrumbsWidget = function BreadcrumbsWidget(aNode, aOptions = {}) {
+ this.document = aNode.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = aNode;
+
+ // Create an internal arrowscrollbox container.
+ this._list = this.document.createElement("arrowscrollbox");
+ this._list.className = "breadcrumbs-widget-container";
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("orient", "horizontal");
+ this._list.setAttribute("clicktoscroll", "true");
+ this._list.setAttribute("smoothscroll", !!aOptions.smoothScroll);
+ this._list.addEventListener("keypress", e => this.emit("keyPress", e), false);
+ this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false);
+ this._parent.appendChild(this._list);
+
+ // By default, hide the arrows. We let the arrowscrollbox show them
+ // in case of overflow.
+ this._list._scrollButtonUp.collapsed = true;
+ this._list._scrollButtonDown.collapsed = true;
+ this._list.addEventListener("underflow", this._onUnderflow.bind(this), false);
+ this._list.addEventListener("overflow", this._onOverflow.bind(this), false);
+
+ // This widget emits events that can be handled in a MenuContainer.
+ EventEmitter.decorate(this);
+
+ // Delegate some of the associated node's methods to satisfy the interface
+ // required by MenuContainer instances.
+ ViewHelpers.delegateWidgetAttributeMethods(this, aNode);
+ ViewHelpers.delegateWidgetEventMethods(this, aNode);
+};
+
+BreadcrumbsWidget.prototype = {
+ /**
+ * Inserts an item in this container at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param nsIDOMNode aContents
+ * The node displayed in the container.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertItemAt: function (aIndex, aContents) {
+ let list = this._list;
+ let breadcrumb = new Breadcrumb(this, aContents);
+ return list.insertBefore(breadcrumb._target, list.childNodes[aIndex]);
+ },
+
+ /**
+ * Returns the child node in this container situated at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ getItemAtIndex: function (aIndex) {
+ return this._list.childNodes[aIndex];
+ },
+
+ /**
+ * Removes the specified child node from this container.
+ *
+ * @param nsIDOMNode aChild
+ * The element associated with the displayed item.
+ */
+ removeChild: function (aChild) {
+ this._list.removeChild(aChild);
+
+ if (this._selectedItem == aChild) {
+ this._selectedItem = null;
+ }
+ },
+
+ /**
+ * Removes all of the child nodes from this container.
+ */
+ removeAllItems: function () {
+ let list = this._list;
+
+ while (list.hasChildNodes()) {
+ list.firstChild.remove();
+ }
+
+ this._selectedItem = null;
+ },
+
+ /**
+ * Gets the currently selected child node in this container.
+ * @return nsIDOMNode
+ */
+ get selectedItem() {
+ return this._selectedItem;
+ },
+
+ /**
+ * Sets the currently selected child node in this container.
+ * @param nsIDOMNode aChild
+ */
+ set selectedItem(aChild) {
+ let childNodes = this._list.childNodes;
+
+ if (!aChild) {
+ this._selectedItem = null;
+ }
+ for (let node of childNodes) {
+ if (node == aChild) {
+ node.setAttribute("checked", "");
+ this._selectedItem = node;
+ } else {
+ node.removeAttribute("checked");
+ }
+ }
+ },
+
+ /**
+ * Returns the value of the named attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ * @return string
+ * The current attribute value.
+ */
+ getAttribute: function (aName) {
+ if (aName == "scrollPosition") return this._list.scrollPosition;
+ if (aName == "scrollWidth") return this._list.scrollWidth;
+ return this._parent.getAttribute(aName);
+ },
+
+ /**
+ * Ensures the specified element is visible.
+ *
+ * @param nsIDOMNode aElement
+ * The element to make visible.
+ */
+ ensureElementIsVisible: function (aElement) {
+ if (!aElement) {
+ return;
+ }
+
+ // Repeated calls to ensureElementIsVisible would interfere with each other
+ // and may sometimes result in incorrect scroll positions.
+ setNamedTimeout("breadcrumb-select", ENSURE_SELECTION_VISIBLE_DELAY, () => {
+ if (this._list.ensureElementIsVisible) {
+ this._list.ensureElementIsVisible(aElement);
+ }
+ });
+ },
+
+ /**
+ * The underflow and overflow listener for the arrowscrollbox container.
+ */
+ _onUnderflow: function ({ target }) {
+ if (target != this._list) {
+ return;
+ }
+ target._scrollButtonUp.collapsed = true;
+ target._scrollButtonDown.collapsed = true;
+ target.removeAttribute("overflows");
+ },
+
+ /**
+ * The underflow and overflow listener for the arrowscrollbox container.
+ */
+ _onOverflow: function ({ target }) {
+ if (target != this._list) {
+ return;
+ }
+ target._scrollButtonUp.collapsed = false;
+ target._scrollButtonDown.collapsed = false;
+ target.setAttribute("overflows", "");
+ },
+
+ window: null,
+ document: null,
+ _parent: null,
+ _list: null,
+ _selectedItem: null
+};
+
+/**
+ * A Breadcrumb constructor for the BreadcrumbsWidget.
+ *
+ * @param BreadcrumbsWidget aWidget
+ * The widget to contain this breadcrumb.
+ * @param nsIDOMNode aContents
+ * The node displayed in the container.
+ */
+function Breadcrumb(aWidget, aContents) {
+ this.document = aWidget.document;
+ this.window = aWidget.window;
+ this.ownerView = aWidget;
+
+ this._target = this.document.createElement("hbox");
+ this._target.className = "breadcrumbs-widget-item";
+ this._target.setAttribute("align", "center");
+ this.contents = aContents;
+}
+
+Breadcrumb.prototype = {
+ /**
+ * Sets the contents displayed in this item's view.
+ *
+ * @param string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ */
+ set contents(aContents) {
+ // If there are already some contents displayed, replace them.
+ if (this._target.hasChildNodes()) {
+ this._target.replaceChild(aContents, this._target.firstChild);
+ return;
+ }
+ // These are the first contents ever displayed.
+ this._target.appendChild(aContents);
+ },
+
+ window: null,
+ document: null,
+ ownerView: null,
+ _target: null
+};
diff --git a/devtools/client/shared/widgets/Chart.jsm b/devtools/client/shared/widgets/Chart.jsm
new file mode 100644
index 000000000..0894a62ca
--- /dev/null
+++ b/devtools/client/shared/widgets/Chart.jsm
@@ -0,0 +1,449 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Cu = Components.utils;
+
+const NET_STRINGS_URI = "devtools/client/locales/netmonitor.properties";
+const SVG_NS = "http://www.w3.org/2000/svg";
+const PI = Math.PI;
+const TAU = PI * 2;
+const EPSILON = 0.0000001;
+const NAMED_SLICE_MIN_ANGLE = TAU / 8;
+const NAMED_SLICE_TEXT_DISTANCE_RATIO = 1.9;
+const HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO = 20;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+this.EXPORTED_SYMBOLS = ["Chart"];
+
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(NET_STRINGS_URI);
+
+/**
+ * A factory for creating charts.
+ * Example usage: let myChart = Chart.Pie(document, { ... });
+ */
+var Chart = {
+ Pie: createPieChart,
+ Table: createTableChart,
+ PieTable: createPieTableChart
+};
+
+/**
+ * A simple pie chart proxy for the underlying view.
+ * Each item in the `slices` property represents a [data, node] pair containing
+ * the data used to create the slice and the nsIDOMNode displaying it.
+ *
+ * @param nsIDOMNode node
+ * The node representing the view for this chart.
+ */
+function PieChart(node) {
+ this.node = node;
+ this.slices = new WeakMap();
+ EventEmitter.decorate(this);
+}
+
+/**
+ * A simple table chart proxy for the underlying view.
+ * Each item in the `rows` property represents a [data, node] pair containing
+ * the data used to create the row and the nsIDOMNode displaying it.
+ *
+ * @param nsIDOMNode node
+ * The node representing the view for this chart.
+ */
+function TableChart(node) {
+ this.node = node;
+ this.rows = new WeakMap();
+ EventEmitter.decorate(this);
+}
+
+/**
+ * A simple pie+table chart proxy for the underlying view.
+ *
+ * @param nsIDOMNode node
+ * The node representing the view for this chart.
+ * @param PieChart pie
+ * The pie chart proxy.
+ * @param TableChart table
+ * The table chart proxy.
+ */
+function PieTableChart(node, pie, table) {
+ this.node = node;
+ this.pie = pie;
+ this.table = table;
+ EventEmitter.decorate(this);
+}
+
+/**
+ * Creates the DOM for a pie+table chart.
+ *
+ * @param nsIDocument document
+ * The document responsible with creating the DOM.
+ * @param object
+ * An object containing all or some of the following properties:
+ * - title: a string displayed as the table chart's (description)/local
+ * - diameter: the diameter of the pie chart, in pixels
+ * - data: an array of items used to display each slice in the pie
+ * and each row in the table;
+ * @see `createPieChart` and `createTableChart` for details.
+ * - strings: @see `createTableChart` for details.
+ * - totals: @see `createTableChart` for details.
+ * - sorted: a flag specifying if the `data` should be sorted
+ * ascending by `size`.
+ * @return PieTableChart
+ * A pie+table chart proxy instance, which emits the following events:
+ * - "mouseover", when the mouse enters a slice or a row
+ * - "mouseout", when the mouse leaves a slice or a row
+ * - "click", when the mouse enters a slice or a row
+ */
+function createPieTableChart(document, { title, diameter, data, strings, totals, sorted }) {
+ if (data && sorted) {
+ data = data.slice().sort((a, b) => +(a.size < b.size));
+ }
+
+ let pie = Chart.Pie(document, {
+ width: diameter,
+ data: data
+ });
+
+ let table = Chart.Table(document, {
+ title: title,
+ data: data,
+ strings: strings,
+ totals: totals
+ });
+
+ let container = document.createElement("hbox");
+ container.className = "pie-table-chart-container";
+ container.appendChild(pie.node);
+ container.appendChild(table.node);
+
+ let proxy = new PieTableChart(container, pie, table);
+
+ pie.on("click", (event, item) => {
+ proxy.emit(event, item);
+ });
+
+ table.on("click", (event, item) => {
+ proxy.emit(event, item);
+ });
+
+ pie.on("mouseover", (event, item) => {
+ proxy.emit(event, item);
+ if (table.rows.has(item)) {
+ table.rows.get(item).setAttribute("focused", "");
+ }
+ });
+
+ pie.on("mouseout", (event, item) => {
+ proxy.emit(event, item);
+ if (table.rows.has(item)) {
+ table.rows.get(item).removeAttribute("focused");
+ }
+ });
+
+ table.on("mouseover", (event, item) => {
+ proxy.emit(event, item);
+ if (pie.slices.has(item)) {
+ pie.slices.get(item).setAttribute("focused", "");
+ }
+ });
+
+ table.on("mouseout", (event, item) => {
+ proxy.emit(event, item);
+ if (pie.slices.has(item)) {
+ pie.slices.get(item).removeAttribute("focused");
+ }
+ });
+
+ return proxy;
+}
+
+/**
+ * Creates the DOM for a pie chart based on the specified properties.
+ *
+ * @param nsIDocument document
+ * The document responsible with creating the DOM.
+ * @param object
+ * An object containing all or some of the following properties:
+ * - data: an array of items used to display each slice; all the items
+ * should be objects containing a `size` and a `label` property.
+ * e.g: [{
+ * size: 1,
+ * label: "foo"
+ * }, {
+ * size: 2,
+ * label: "bar"
+ * }];
+ * - width: the width of the chart, in pixels
+ * - height: optional, the height of the chart, in pixels.
+ * - centerX: optional, the X-axis center of the chart, in pixels.
+ * - centerY: optional, the Y-axis center of the chart, in pixels.
+ * - radius: optional, the radius of the chart, in pixels.
+ * @return PieChart
+ * A pie chart proxy instance, which emits the following events:
+ * - "mouseover", when the mouse enters a slice
+ * - "mouseout", when the mouse leaves a slice
+ * - "click", when the mouse clicks a slice
+ */
+function createPieChart(document, { data, width, height, centerX, centerY, radius }) {
+ height = height || width;
+ centerX = centerX || width / 2;
+ centerY = centerY || height / 2;
+ radius = radius || (width + height) / 4;
+ let isPlaceholder = false;
+
+ // Filter out very small sizes, as they'll just render invisible slices.
+ data = data ? data.filter(e => e.size > EPSILON) : null;
+
+ // If there's no data available, display an empty placeholder.
+ if (!data) {
+ data = loadingPieChartData;
+ isPlaceholder = true;
+ }
+ if (!data.length) {
+ data = emptyPieChartData;
+ isPlaceholder = true;
+ }
+
+ let container = document.createElementNS(SVG_NS, "svg");
+ container.setAttribute("class", "generic-chart-container pie-chart-container");
+ container.setAttribute("pack", "center");
+ container.setAttribute("flex", "1");
+ container.setAttribute("width", width);
+ container.setAttribute("height", height);
+ container.setAttribute("viewBox", "0 0 " + width + " " + height);
+ container.setAttribute("slices", data.length);
+ container.setAttribute("placeholder", isPlaceholder);
+
+ let proxy = new PieChart(container);
+
+ let total = data.reduce((acc, e) => acc + e.size, 0);
+ let angles = data.map(e => e.size / total * (TAU - EPSILON));
+ let largest = data.reduce((a, b) => a.size > b.size ? a : b);
+ let smallest = data.reduce((a, b) => a.size < b.size ? a : b);
+
+ let textDistance = radius / NAMED_SLICE_TEXT_DISTANCE_RATIO;
+ let translateDistance = radius / HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO;
+ let startAngle = TAU;
+ let endAngle = 0;
+ let midAngle = 0;
+ radius -= translateDistance;
+
+ for (let i = data.length - 1; i >= 0; i--) {
+ let sliceInfo = data[i];
+ let sliceAngle = angles[i];
+ if (!sliceInfo.size || sliceAngle < EPSILON) {
+ continue;
+ }
+
+ endAngle = startAngle - sliceAngle;
+ midAngle = (startAngle + endAngle) / 2;
+
+ let x1 = centerX + radius * Math.sin(startAngle);
+ let y1 = centerY - radius * Math.cos(startAngle);
+ let x2 = centerX + radius * Math.sin(endAngle);
+ let y2 = centerY - radius * Math.cos(endAngle);
+ let largeArcFlag = Math.abs(startAngle - endAngle) > PI ? 1 : 0;
+
+ let pathNode = document.createElementNS(SVG_NS, "path");
+ pathNode.setAttribute("class", "pie-chart-slice chart-colored-blob");
+ pathNode.setAttribute("name", sliceInfo.label);
+ pathNode.setAttribute("d",
+ " M " + centerX + "," + centerY +
+ " L " + x2 + "," + y2 +
+ " A " + radius + "," + radius +
+ " 0 " + largeArcFlag +
+ " 1 " + x1 + "," + y1 +
+ " Z");
+
+ if (sliceInfo == largest) {
+ pathNode.setAttribute("largest", "");
+ }
+ if (sliceInfo == smallest) {
+ pathNode.setAttribute("smallest", "");
+ }
+
+ let hoverX = translateDistance * Math.sin(midAngle);
+ let hoverY = -translateDistance * Math.cos(midAngle);
+ let hoverTransform = "transform: translate(" + hoverX + "px, " + hoverY + "px)";
+ pathNode.setAttribute("style", data.length > 1 ? hoverTransform : "");
+
+ proxy.slices.set(sliceInfo, pathNode);
+ delegate(proxy, ["click", "mouseover", "mouseout"], pathNode, sliceInfo);
+ container.appendChild(pathNode);
+
+ if (sliceInfo.label && sliceAngle > NAMED_SLICE_MIN_ANGLE) {
+ let textX = centerX + textDistance * Math.sin(midAngle);
+ let textY = centerY - textDistance * Math.cos(midAngle);
+ let label = document.createElementNS(SVG_NS, "text");
+ label.appendChild(document.createTextNode(sliceInfo.label));
+ label.setAttribute("class", "pie-chart-label");
+ label.setAttribute("style", data.length > 1 ? hoverTransform : "");
+ label.setAttribute("x", data.length > 1 ? textX : centerX);
+ label.setAttribute("y", data.length > 1 ? textY : centerY);
+ container.appendChild(label);
+ }
+
+ startAngle = endAngle;
+ }
+
+ return proxy;
+}
+
+/**
+ * Creates the DOM for a table chart based on the specified properties.
+ *
+ * @param nsIDocument document
+ * The document responsible with creating the DOM.
+ * @param object
+ * An object containing all or some of the following properties:
+ * - title: a string displayed as the chart's (description)/local
+ * - data: an array of items used to display each row; all the items
+ * should be objects representing columns, for which the
+ * properties' values will be displayed in each cell of a row.
+ * e.g: [{
+ * label1: 1,
+ * label2: 3,
+ * label3: "foo"
+ * }, {
+ * label1: 4,
+ * label2: 6,
+ * label3: "bar
+ * }];
+ * - strings: an object specifying for which rows in the `data` array
+ * their cell values should be stringified and localized
+ * based on a predicate function;
+ * e.g: {
+ * label1: value => l10n.getFormatStr("...", value)
+ * }
+ * - totals: an object specifying for which rows in the `data` array
+ * the sum of their cells is to be displayed in the chart;
+ * e.g: {
+ * label1: total => l10n.getFormatStr("...", total), // 5
+ * label2: total => l10n.getFormatStr("...", total), // 9
+ * }
+ * @return TableChart
+ * A table chart proxy instance, which emits the following events:
+ * - "mouseover", when the mouse enters a row
+ * - "mouseout", when the mouse leaves a row
+ * - "click", when the mouse clicks a row
+ */
+function createTableChart(document, { title, data, strings, totals }) {
+ strings = strings || {};
+ totals = totals || {};
+ let isPlaceholder = false;
+
+ // If there's no data available, display an empty placeholder.
+ if (!data) {
+ data = loadingTableChartData;
+ isPlaceholder = true;
+ }
+ if (!data.length) {
+ data = emptyTableChartData;
+ isPlaceholder = true;
+ }
+
+ let container = document.createElement("vbox");
+ container.className = "generic-chart-container table-chart-container";
+ container.setAttribute("pack", "center");
+ container.setAttribute("flex", "1");
+ container.setAttribute("rows", data.length);
+ container.setAttribute("placeholder", isPlaceholder);
+
+ let proxy = new TableChart(container);
+
+ let titleNode = document.createElement("label");
+ titleNode.className = "plain table-chart-title";
+ titleNode.setAttribute("value", title);
+ container.appendChild(titleNode);
+
+ let tableNode = document.createElement("vbox");
+ tableNode.className = "plain table-chart-grid";
+ container.appendChild(tableNode);
+
+ for (let rowInfo of data) {
+ let rowNode = document.createElement("hbox");
+ rowNode.className = "table-chart-row";
+ rowNode.setAttribute("align", "center");
+
+ let boxNode = document.createElement("hbox");
+ boxNode.className = "table-chart-row-box chart-colored-blob";
+ boxNode.setAttribute("name", rowInfo.label);
+ rowNode.appendChild(boxNode);
+
+ for (let [key, value] of Object.entries(rowInfo)) {
+ let index = data.indexOf(rowInfo);
+ let stringified = strings[key] ? strings[key](value, index) : value;
+ let labelNode = document.createElement("label");
+ labelNode.className = "plain table-chart-row-label";
+ labelNode.setAttribute("name", key);
+ labelNode.setAttribute("value", stringified);
+ rowNode.appendChild(labelNode);
+ }
+
+ proxy.rows.set(rowInfo, rowNode);
+ delegate(proxy, ["click", "mouseover", "mouseout"], rowNode, rowInfo);
+ tableNode.appendChild(rowNode);
+ }
+
+ let totalsNode = document.createElement("vbox");
+ totalsNode.className = "table-chart-totals";
+
+ for (let [key, value] of Object.entries(totals)) {
+ let total = data.reduce((acc, e) => acc + e[key], 0);
+ let stringified = totals[key] ? totals[key](total || 0) : total;
+ let labelNode = document.createElement("label");
+ labelNode.className = "plain table-chart-summary-label";
+ labelNode.setAttribute("name", key);
+ labelNode.setAttribute("value", stringified);
+ totalsNode.appendChild(labelNode);
+ }
+
+ container.appendChild(totalsNode);
+
+ return proxy;
+}
+
+XPCOMUtils.defineLazyGetter(this, "loadingPieChartData", () => {
+ return [{ size: 1, label: L10N.getStr("pieChart.loading") }];
+});
+
+XPCOMUtils.defineLazyGetter(this, "emptyPieChartData", () => {
+ return [{ size: 1, label: L10N.getStr("pieChart.unavailable") }];
+});
+
+XPCOMUtils.defineLazyGetter(this, "loadingTableChartData", () => {
+ return [{ size: "", label: L10N.getStr("tableChart.loading") }];
+});
+
+XPCOMUtils.defineLazyGetter(this, "emptyTableChartData", () => {
+ return [{ size: "", label: L10N.getStr("tableChart.unavailable") }];
+});
+
+/**
+ * Delegates DOM events emitted by an nsIDOMNode to an EventEmitter proxy.
+ *
+ * @param EventEmitter emitter
+ * The event emitter proxy instance.
+ * @param array events
+ * An array of events, e.g. ["mouseover", "mouseout"].
+ * @param nsIDOMNode node
+ * The element firing the DOM events.
+ * @param any args
+ * The arguments passed when emitting events through the proxy.
+ */
+function delegate(emitter, events, node, args) {
+ for (let event of events) {
+ node.addEventListener(event, emitter.emit.bind(emitter, event, args));
+ }
+}
diff --git a/devtools/client/shared/widgets/CubicBezierPresets.js b/devtools/client/shared/widgets/CubicBezierPresets.js
new file mode 100644
index 000000000..d2a77a85c
--- /dev/null
+++ b/devtools/client/shared/widgets/CubicBezierPresets.js
@@ -0,0 +1,64 @@
+/**
+ * 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/.
+ */
+
+// Set of preset definitions for use with CubicBezierWidget
+// Credit: http://easings.net
+
+"use strict";
+
+const PREDEFINED = {
+ "ease": [0.25, 0.1, 0.25, 1],
+ "linear": [0, 0, 1, 1],
+ "ease-in": [0.42, 0, 1, 1],
+ "ease-out": [0, 0, 0.58, 1],
+ "ease-in-out": [0.42, 0, 0.58, 1]
+};
+
+const PRESETS = {
+ "ease-in": {
+ "ease-in-linear": [0, 0, 1, 1],
+ "ease-in-ease-in": [0.42, 0, 1, 1],
+ "ease-in-sine": [0.47, 0, 0.74, 0.71],
+ "ease-in-quadratic": [0.55, 0.09, 0.68, 0.53],
+ "ease-in-cubic": [0.55, 0.06, 0.68, 0.19],
+ "ease-in-quartic": [0.9, 0.03, 0.69, 0.22],
+ "ease-in-quintic": [0.76, 0.05, 0.86, 0.06],
+ "ease-in-exponential": [0.95, 0.05, 0.8, 0.04],
+ "ease-in-circular": [0.6, 0.04, 0.98, 0.34],
+ "ease-in-backward": [0.6, -0.28, 0.74, 0.05]
+ },
+ "ease-out": {
+ "ease-out-linear": [0, 0, 1, 1],
+ "ease-out-ease-out": [0, 0, 0.58, 1],
+ "ease-out-sine": [0.39, 0.58, 0.57, 1],
+ "ease-out-quadratic": [0.25, 0.46, 0.45, 0.94],
+ "ease-out-cubic": [0.22, 0.61, 0.36, 1],
+ "ease-out-quartic": [0.17, 0.84, 0.44, 1],
+ "ease-out-quintic": [0.23, 1, 0.32, 1],
+ "ease-out-exponential": [0.19, 1, 0.22, 1],
+ "ease-out-circular": [0.08, 0.82, 0.17, 1],
+ "ease-out-backward": [0.18, 0.89, 0.32, 1.28]
+ },
+ "ease-in-out": {
+ "ease-in-out-linear": [0, 0, 1, 1],
+ "ease-in-out-ease": [0.25, 0.1, 0.25, 1],
+ "ease-in-out-ease-in-out": [0.42, 0, 0.58, 1],
+ "ease-in-out-sine": [0.45, 0.05, 0.55, 0.95],
+ "ease-in-out-quadratic": [0.46, 0.03, 0.52, 0.96],
+ "ease-in-out-cubic": [0.65, 0.05, 0.36, 1],
+ "ease-in-out-quartic": [0.77, 0, 0.18, 1],
+ "ease-in-out-quintic": [0.86, 0, 0.07, 1],
+ "ease-in-out-exponential": [1, 0, 0, 1],
+ "ease-in-out-circular": [0.79, 0.14, 0.15, 0.86],
+ "ease-in-out-backward": [0.68, -0.55, 0.27, 1.55]
+ }
+};
+
+const DEFAULT_PRESET_CATEGORY = Object.keys(PRESETS)[0];
+
+exports.PRESETS = PRESETS;
+exports.PREDEFINED = PREDEFINED;
+exports.DEFAULT_PRESET_CATEGORY = DEFAULT_PRESET_CATEGORY;
diff --git a/devtools/client/shared/widgets/CubicBezierWidget.js b/devtools/client/shared/widgets/CubicBezierWidget.js
new file mode 100644
index 000000000..337282d46
--- /dev/null
+++ b/devtools/client/shared/widgets/CubicBezierWidget.js
@@ -0,0 +1,897 @@
+/**
+ * Copyright (c) 2013 Lea Verou. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+// Based on www.cubic-bezier.com by Lea Verou
+// See https://github.com/LeaVerou/cubic-bezier
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const {
+ PREDEFINED,
+ PRESETS,
+ DEFAULT_PRESET_CATEGORY
+} = require("devtools/client/shared/widgets/CubicBezierPresets");
+const {getCSSLexer} = require("devtools/shared/css/lexer");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * CubicBezier data structure helper
+ * Accepts an array of coordinates and exposes a few useful getters
+ * @param {Array} coordinates i.e. [.42, 0, .58, 1]
+ */
+function CubicBezier(coordinates) {
+ if (!coordinates) {
+ throw new Error("No offsets were defined");
+ }
+
+ this.coordinates = coordinates.map(n => +n);
+
+ for (let i = 4; i--;) {
+ let xy = this.coordinates[i];
+ if (isNaN(xy) || (!(i % 2) && (xy < 0 || xy > 1))) {
+ throw new Error(`Wrong coordinate at ${i}(${xy})`);
+ }
+ }
+
+ this.coordinates.toString = function () {
+ return this.map(n => {
+ return (Math.round(n * 100) / 100 + "").replace(/^0\./, ".");
+ }) + "";
+ };
+}
+
+exports.CubicBezier = CubicBezier;
+
+CubicBezier.prototype = {
+ get P1() {
+ return this.coordinates.slice(0, 2);
+ },
+
+ get P2() {
+ return this.coordinates.slice(2);
+ },
+
+ toString: function () {
+ // Check first if current coords are one of css predefined functions
+ let predefName = Object.keys(PREDEFINED)
+ .find(key => coordsAreEqual(PREDEFINED[key],
+ this.coordinates));
+
+ return predefName || "cubic-bezier(" + this.coordinates + ")";
+ }
+};
+
+/**
+ * Bezier curve canvas plotting class
+ * @param {DOMNode} canvas
+ * @param {CubicBezier} bezier
+ * @param {Array} padding Amount of horizontal,vertical padding around the graph
+ */
+function BezierCanvas(canvas, bezier, padding) {
+ this.canvas = canvas;
+ this.bezier = bezier;
+ this.padding = getPadding(padding);
+
+ // Convert to a cartesian coordinate system with axes from 0 to 1
+ this.ctx = this.canvas.getContext("2d");
+ let p = this.padding;
+
+ this.ctx.scale(canvas.width * (1 - p[1] - p[3]),
+ -canvas.height * (1 - p[0] - p[2]));
+ this.ctx.translate(p[3] / (1 - p[1] - p[3]),
+ -1 - p[0] / (1 - p[0] - p[2]));
+}
+
+exports.BezierCanvas = BezierCanvas;
+
+BezierCanvas.prototype = {
+ /**
+ * Get P1 and P2 current top/left offsets so they can be positioned
+ * @return {Array} Returns an array of 2 {top:String,left:String} objects
+ */
+ get offsets() {
+ let p = this.padding, w = this.canvas.width, h = this.canvas.height;
+
+ return [{
+ left: w * (this.bezier.coordinates[0] * (1 - p[3] - p[1]) - p[3]) + "px",
+ top: h * (1 - this.bezier.coordinates[1] * (1 - p[0] - p[2]) - p[0])
+ + "px"
+ }, {
+ left: w * (this.bezier.coordinates[2] * (1 - p[3] - p[1]) - p[3]) + "px",
+ top: h * (1 - this.bezier.coordinates[3] * (1 - p[0] - p[2]) - p[0])
+ + "px"
+ }];
+ },
+
+ /**
+ * Convert an element's left/top offsets into coordinates
+ */
+ offsetsToCoordinates: function (element) {
+ let p = this.padding, w = this.canvas.width, h = this.canvas.height;
+
+ // Convert padding percentage to actual padding
+ p = p.map((a, i) => a * (i % 2 ? w : h));
+
+ return [
+ (parseFloat(element.style.left) - p[3]) / (w + p[1] + p[3]),
+ (h - parseFloat(element.style.top) - p[2]) / (h - p[0] - p[2])
+ ];
+ },
+
+ /**
+ * Draw the cubic bezier curve for the current coordinates
+ */
+ plot: function (settings = {}) {
+ let xy = this.bezier.coordinates;
+
+ let defaultSettings = {
+ handleColor: "#666",
+ handleThickness: .008,
+ bezierColor: "#4C9ED9",
+ bezierThickness: .015,
+ drawHandles: true
+ };
+
+ for (let setting in settings) {
+ defaultSettings[setting] = settings[setting];
+ }
+
+ // Clear the canvas –making sure to clear the
+ // whole area by resetting the transform first.
+ this.ctx.save();
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0);
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ this.ctx.restore();
+
+ if (defaultSettings.drawHandles) {
+ // Draw control handles
+ this.ctx.beginPath();
+ this.ctx.fillStyle = defaultSettings.handleColor;
+ this.ctx.lineWidth = defaultSettings.handleThickness;
+ this.ctx.strokeStyle = defaultSettings.handleColor;
+
+ this.ctx.moveTo(0, 0);
+ this.ctx.lineTo(xy[0], xy[1]);
+ this.ctx.moveTo(1, 1);
+ this.ctx.lineTo(xy[2], xy[3]);
+
+ this.ctx.stroke();
+ this.ctx.closePath();
+
+ let circle = (ctx, cx, cy, r) => {
+ ctx.beginPath();
+ ctx.arc(cx, cy, r, 0, 2 * Math.PI, !1);
+ ctx.closePath();
+ };
+
+ circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness);
+ this.ctx.fill();
+ circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness);
+ this.ctx.fill();
+ }
+
+ // Draw bezier curve
+ this.ctx.beginPath();
+ this.ctx.lineWidth = defaultSettings.bezierThickness;
+ this.ctx.strokeStyle = defaultSettings.bezierColor;
+ this.ctx.moveTo(0, 0);
+ this.ctx.bezierCurveTo(xy[0], xy[1], xy[2], xy[3], 1, 1);
+ this.ctx.stroke();
+ this.ctx.closePath();
+ }
+};
+
+/**
+ * Cubic-bezier widget. Uses the BezierCanvas class to draw the curve and
+ * adds the control points and user interaction
+ * @param {DOMNode} parent The container where the graph should be created
+ * @param {Array} coordinates Coordinates of the curve to be drawn
+ *
+ * Emits "updated" events whenever the curve is changed. Along with the event is
+ * sent a CubicBezier object
+ */
+function CubicBezierWidget(parent,
+ coordinates = PRESETS["ease-in"]["ease-in-sine"]) {
+ EventEmitter.decorate(this);
+
+ this.parent = parent;
+ let {curve, p1, p2} = this._initMarkup();
+
+ this.curveBoundingBox = curve.getBoundingClientRect();
+ this.curve = curve;
+ this.p1 = p1;
+ this.p2 = p2;
+
+ // Create and plot the bezier curve
+ this.bezierCanvas = new BezierCanvas(this.curve,
+ new CubicBezier(coordinates), [0.30, 0]);
+ this.bezierCanvas.plot();
+
+ // Place the control points
+ let offsets = this.bezierCanvas.offsets;
+ this.p1.style.left = offsets[0].left;
+ this.p1.style.top = offsets[0].top;
+ this.p2.style.left = offsets[1].left;
+ this.p2.style.top = offsets[1].top;
+
+ this._onPointMouseDown = this._onPointMouseDown.bind(this);
+ this._onPointKeyDown = this._onPointKeyDown.bind(this);
+ this._onCurveClick = this._onCurveClick.bind(this);
+ this._onNewCoordinates = this._onNewCoordinates.bind(this);
+
+ // Add preset preview menu
+ this.presets = new CubicBezierPresetWidget(parent);
+
+ // Add the timing function previewer
+ this.timingPreview = new TimingFunctionPreviewWidget(parent);
+
+ this._initEvents();
+}
+
+exports.CubicBezierWidget = CubicBezierWidget;
+
+CubicBezierWidget.prototype = {
+ _initMarkup: function () {
+ let doc = this.parent.ownerDocument;
+
+ let wrap = doc.createElementNS(XHTML_NS, "div");
+ wrap.className = "display-wrap";
+
+ let plane = doc.createElementNS(XHTML_NS, "div");
+ plane.className = "coordinate-plane";
+
+ let p1 = doc.createElementNS(XHTML_NS, "button");
+ p1.className = "control-point";
+ plane.appendChild(p1);
+
+ let p2 = doc.createElementNS(XHTML_NS, "button");
+ p2.className = "control-point";
+ plane.appendChild(p2);
+
+ let curve = doc.createElementNS(XHTML_NS, "canvas");
+ curve.setAttribute("width", 150);
+ curve.setAttribute("height", 370);
+ curve.className = "curve";
+
+ plane.appendChild(curve);
+ wrap.appendChild(plane);
+
+ this.parent.appendChild(wrap);
+
+ return {
+ p1,
+ p2,
+ curve
+ };
+ },
+
+ _removeMarkup: function () {
+ this.parent.querySelector(".display-wrap").remove();
+ },
+
+ _initEvents: function () {
+ this.p1.addEventListener("mousedown", this._onPointMouseDown);
+ this.p2.addEventListener("mousedown", this._onPointMouseDown);
+
+ this.p1.addEventListener("keydown", this._onPointKeyDown);
+ this.p2.addEventListener("keydown", this._onPointKeyDown);
+
+ this.curve.addEventListener("click", this._onCurveClick);
+
+ this.presets.on("new-coordinates", this._onNewCoordinates);
+ },
+
+ _removeEvents: function () {
+ this.p1.removeEventListener("mousedown", this._onPointMouseDown);
+ this.p2.removeEventListener("mousedown", this._onPointMouseDown);
+
+ this.p1.removeEventListener("keydown", this._onPointKeyDown);
+ this.p2.removeEventListener("keydown", this._onPointKeyDown);
+
+ this.curve.removeEventListener("click", this._onCurveClick);
+
+ this.presets.off("new-coordinates", this._onNewCoordinates);
+ },
+
+ _onPointMouseDown: function (event) {
+ // Updating the boundingbox in case it has changed
+ this.curveBoundingBox = this.curve.getBoundingClientRect();
+
+ let point = event.target;
+ let doc = point.ownerDocument;
+ let self = this;
+
+ doc.onmousemove = function drag(e) {
+ let x = e.pageX;
+ let y = e.pageY;
+ let left = self.curveBoundingBox.left;
+ let top = self.curveBoundingBox.top;
+
+ if (x === 0 && y == 0) {
+ return;
+ }
+
+ // Constrain x
+ x = Math.min(Math.max(left, x), left + self.curveBoundingBox.width);
+
+ point.style.left = x - left + "px";
+ point.style.top = y - top + "px";
+
+ self._updateFromPoints();
+ };
+
+ doc.onmouseup = function () {
+ point.focus();
+ doc.onmousemove = doc.onmouseup = null;
+ };
+ },
+
+ _onPointKeyDown: function (event) {
+ let point = event.target;
+ let code = event.keyCode;
+
+ if (code >= 37 && code <= 40) {
+ event.preventDefault();
+
+ // Arrow keys pressed
+ let left = parseInt(point.style.left, 10);
+ let top = parseInt(point.style.top, 10);
+ let offset = 3 * (event.shiftKey ? 10 : 1);
+
+ switch (code) {
+ case 37: point.style.left = left - offset + "px"; break;
+ case 38: point.style.top = top - offset + "px"; break;
+ case 39: point.style.left = left + offset + "px"; break;
+ case 40: point.style.top = top + offset + "px"; break;
+ }
+
+ this._updateFromPoints();
+ }
+ },
+
+ _onCurveClick: function (event) {
+ this.curveBoundingBox = this.curve.getBoundingClientRect();
+
+ let left = this.curveBoundingBox.left;
+ let top = this.curveBoundingBox.top;
+ let x = event.pageX - left;
+ let y = event.pageY - top;
+
+ // Find which point is closer
+ let distP1 = distance(x, y,
+ parseInt(this.p1.style.left, 10), parseInt(this.p1.style.top, 10));
+ let distP2 = distance(x, y,
+ parseInt(this.p2.style.left, 10), parseInt(this.p2.style.top, 10));
+
+ let point = distP1 < distP2 ? this.p1 : this.p2;
+ point.style.left = x + "px";
+ point.style.top = y + "px";
+
+ this._updateFromPoints();
+ },
+
+ _onNewCoordinates: function (event, coordinates) {
+ this.coordinates = coordinates;
+ },
+
+ /**
+ * Get the current point coordinates and redraw the curve to match
+ */
+ _updateFromPoints: function () {
+ // Get the new coordinates from the point's offsets
+ let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1);
+ coordinates = coordinates.concat(
+ this.bezierCanvas.offsetsToCoordinates(this.p2)
+ );
+
+ this.presets.refreshMenu(coordinates);
+ this._redraw(coordinates);
+ },
+
+ /**
+ * Redraw the curve
+ * @param {Array} coordinates The array of control point coordinates
+ */
+ _redraw: function (coordinates) {
+ // Provide a new CubicBezier to the canvas and plot the curve
+ this.bezierCanvas.bezier = new CubicBezier(coordinates);
+ this.bezierCanvas.plot();
+ this.emit("updated", this.bezierCanvas.bezier);
+
+ this.timingPreview.preview(this.bezierCanvas.bezier + "");
+ },
+
+ /**
+ * Set new coordinates for the control points and redraw the curve
+ * @param {Array} coordinates
+ */
+ set coordinates(coordinates) {
+ this._redraw(coordinates);
+
+ // Move the points
+ let offsets = this.bezierCanvas.offsets;
+ this.p1.style.left = offsets[0].left;
+ this.p1.style.top = offsets[0].top;
+ this.p2.style.left = offsets[1].left;
+ this.p2.style.top = offsets[1].top;
+ },
+
+ /**
+ * Set new coordinates for the control point and redraw the curve
+ * @param {String} value A string value. E.g. "linear",
+ * "cubic-bezier(0,0,1,1)"
+ */
+ set cssCubicBezierValue(value) {
+ if (!value) {
+ return;
+ }
+
+ value = value.trim();
+
+ // Try with one of the predefined values
+ let coordinates = parseTimingFunction(value);
+
+ this.presets.refreshMenu(coordinates);
+ this.coordinates = coordinates;
+ },
+
+ destroy: function () {
+ this._removeEvents();
+ this._removeMarkup();
+
+ this.timingPreview.destroy();
+ this.presets.destroy();
+
+ this.curve = this.p1 = this.p2 = null;
+ }
+};
+
+/**
+ * CubicBezierPreset widget.
+ * Builds a menu of presets from CubicBezierPresets
+ * @param {DOMNode} parent The container where the preset panel should be
+ * created
+ *
+ * Emits "new-coordinate" event along with the coordinates
+ * whenever a preset is selected.
+ */
+function CubicBezierPresetWidget(parent) {
+ this.parent = parent;
+
+ let {presetPane, presets, categories} = this._initMarkup();
+ this.presetPane = presetPane;
+ this.presets = presets;
+ this.categories = categories;
+
+ this._activeCategory = null;
+ this._activePresetList = null;
+ this._activePreset = null;
+
+ this._onCategoryClick = this._onCategoryClick.bind(this);
+ this._onPresetClick = this._onPresetClick.bind(this);
+
+ EventEmitter.decorate(this);
+ this._initEvents();
+}
+
+exports.CubicBezierPresetWidget = CubicBezierPresetWidget;
+
+CubicBezierPresetWidget.prototype = {
+ /*
+ * Constructs a list of all preset categories and a list
+ * of presets for each category.
+ *
+ * High level markup:
+ * div .preset-pane
+ * div .preset-categories
+ * div .category
+ * div .category
+ * ...
+ * div .preset-container
+ * div .presetList
+ * div .preset
+ * ...
+ * div .presetList
+ * div .preset
+ * ...
+ */
+ _initMarkup: function () {
+ let doc = this.parent.ownerDocument;
+
+ let presetPane = doc.createElementNS(XHTML_NS, "div");
+ presetPane.className = "preset-pane";
+
+ let categoryList = doc.createElementNS(XHTML_NS, "div");
+ categoryList.id = "preset-categories";
+
+ let presetContainer = doc.createElementNS(XHTML_NS, "div");
+ presetContainer.id = "preset-container";
+
+ Object.keys(PRESETS).forEach(categoryLabel => {
+ let category = this._createCategory(categoryLabel);
+ categoryList.appendChild(category);
+
+ let presetList = this._createPresetList(categoryLabel);
+ presetContainer.appendChild(presetList);
+ });
+
+ presetPane.appendChild(categoryList);
+ presetPane.appendChild(presetContainer);
+
+ this.parent.appendChild(presetPane);
+
+ let allCategories = presetPane.querySelectorAll(".category");
+ let allPresets = presetPane.querySelectorAll(".preset");
+
+ return {
+ presetPane: presetPane,
+ presets: allPresets,
+ categories: allCategories
+ };
+ },
+
+ _createCategory: function (categoryLabel) {
+ let doc = this.parent.ownerDocument;
+
+ let category = doc.createElementNS(XHTML_NS, "div");
+ category.id = categoryLabel;
+ category.classList.add("category");
+
+ let categoryDisplayLabel = this._normalizeCategoryLabel(categoryLabel);
+ category.textContent = categoryDisplayLabel;
+ category.setAttribute("title", categoryDisplayLabel);
+
+ return category;
+ },
+
+ _normalizeCategoryLabel: function (categoryLabel) {
+ return categoryLabel.replace("/-/g", " ");
+ },
+
+ _createPresetList: function (categoryLabel) {
+ let doc = this.parent.ownerDocument;
+
+ let presetList = doc.createElementNS(XHTML_NS, "div");
+ presetList.id = "preset-category-" + categoryLabel;
+ presetList.classList.add("preset-list");
+
+ Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
+ let preset = this._createPreset(categoryLabel, presetLabel);
+ presetList.appendChild(preset);
+ });
+
+ return presetList;
+ },
+
+ _createPreset: function (categoryLabel, presetLabel) {
+ let doc = this.parent.ownerDocument;
+
+ let preset = doc.createElementNS(XHTML_NS, "div");
+ preset.classList.add("preset");
+ preset.id = presetLabel;
+ preset.coordinates = PRESETS[categoryLabel][presetLabel];
+ // Create preset preview
+ let curve = doc.createElementNS(XHTML_NS, "canvas");
+ let bezier = new CubicBezier(preset.coordinates);
+ curve.setAttribute("height", 50);
+ curve.setAttribute("width", 50);
+ preset.bezierCanvas = new BezierCanvas(curve, bezier, [0.15, 0]);
+ preset.bezierCanvas.plot({
+ drawHandles: false,
+ bezierThickness: 0.025
+ });
+ preset.appendChild(curve);
+
+ // Create preset label
+ let presetLabelElem = doc.createElementNS(XHTML_NS, "p");
+ let presetDisplayLabel = this._normalizePresetLabel(categoryLabel,
+ presetLabel);
+ presetLabelElem.textContent = presetDisplayLabel;
+ preset.appendChild(presetLabelElem);
+ preset.setAttribute("title", presetDisplayLabel);
+
+ return preset;
+ },
+
+ _normalizePresetLabel: function (categoryLabel, presetLabel) {
+ return presetLabel.replace(categoryLabel + "-", "").replace("/-/g", " ");
+ },
+
+ _initEvents: function () {
+ for (let category of this.categories) {
+ category.addEventListener("click", this._onCategoryClick);
+ }
+
+ for (let preset of this.presets) {
+ preset.addEventListener("click", this._onPresetClick);
+ }
+ },
+
+ _removeEvents: function () {
+ for (let category of this.categories) {
+ category.removeEventListener("click", this._onCategoryClick);
+ }
+
+ for (let preset of this.presets) {
+ preset.removeEventListener("click", this._onPresetClick);
+ }
+ },
+
+ _onPresetClick: function (event) {
+ this.emit("new-coordinates", event.currentTarget.coordinates);
+ this.activePreset = event.currentTarget;
+ },
+
+ _onCategoryClick: function (event) {
+ this.activeCategory = event.target;
+ },
+
+ _setActivePresetList: function (presetListId) {
+ let presetList = this.presetPane.querySelector("#" + presetListId);
+ swapClassName("active-preset-list", this._activePresetList, presetList);
+ this._activePresetList = presetList;
+ },
+
+ set activeCategory(category) {
+ swapClassName("active-category", this._activeCategory, category);
+ this._activeCategory = category;
+ this._setActivePresetList("preset-category-" + category.id);
+ },
+
+ get activeCategory() {
+ return this._activeCategory;
+ },
+
+ set activePreset(preset) {
+ swapClassName("active-preset", this._activePreset, preset);
+ this._activePreset = preset;
+ },
+
+ get activePreset() {
+ return this._activePreset;
+ },
+
+ /**
+ * Called by CubicBezierWidget onload and when
+ * the curve is modified via the canvas.
+ * Attempts to match the new user setting with an
+ * existing preset.
+ * @param {Array} coordinates new coords [i, j, k, l]
+ */
+ refreshMenu: function (coordinates) {
+ // If we cannot find a matching preset, keep
+ // menu on last known preset category.
+ let category = this._activeCategory;
+
+ // If we cannot find a matching preset
+ // deselect any selected preset.
+ let preset = null;
+
+ // If a category has never been viewed before
+ // show the default category.
+ if (!category) {
+ category = this.parent.querySelector("#" + DEFAULT_PRESET_CATEGORY);
+ }
+
+ // If the new coordinates do match a preset,
+ // set its category and preset button as active.
+ Object.keys(PRESETS).forEach(categoryLabel => {
+ Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
+ if (coordsAreEqual(PRESETS[categoryLabel][presetLabel], coordinates)) {
+ category = this.parent.querySelector("#" + categoryLabel);
+ preset = this.parent.querySelector("#" + presetLabel);
+ }
+ });
+ });
+
+ this.activeCategory = category;
+ this.activePreset = preset;
+ },
+
+ destroy: function () {
+ this._removeEvents();
+ this.parent.querySelector(".preset-pane").remove();
+ }
+};
+
+/**
+ * The TimingFunctionPreviewWidget animates a dot on a scale with a given
+ * timing-function
+ * @param {DOMNode} parent The container where this widget should go
+ */
+function TimingFunctionPreviewWidget(parent) {
+ this.previousValue = null;
+ this.autoRestartAnimation = null;
+
+ this.parent = parent;
+ this._initMarkup();
+}
+
+TimingFunctionPreviewWidget.prototype = {
+ PREVIEW_DURATION: 1000,
+
+ _initMarkup: function () {
+ let doc = this.parent.ownerDocument;
+
+ let container = doc.createElementNS(XHTML_NS, "div");
+ container.className = "timing-function-preview";
+
+ this.dot = doc.createElementNS(XHTML_NS, "div");
+ this.dot.className = "dot";
+ container.appendChild(this.dot);
+
+ let scale = doc.createElementNS(XHTML_NS, "div");
+ scale.className = "scale";
+ container.appendChild(scale);
+
+ this.parent.appendChild(container);
+ },
+
+ destroy: function () {
+ clearTimeout(this.autoRestartAnimation);
+ this.parent.querySelector(".timing-function-preview").remove();
+ this.parent = this.dot = null;
+ },
+
+ /**
+ * Preview a new timing function. The current preview will only be stopped if
+ * the supplied function value is different from the previous one. If the
+ * supplied function is invalid, the preview will stop.
+ * @param {String} value
+ */
+ preview: function (value) {
+ // Don't restart the preview animation if the value is the same
+ if (value === this.previousValue) {
+ return;
+ }
+
+ clearTimeout(this.autoRestartAnimation);
+
+ if (parseTimingFunction(value)) {
+ this.dot.style.animationTimingFunction = value;
+ this.restartAnimation();
+ }
+
+ this.previousValue = value;
+ },
+
+ /**
+ * Re-start the preview animation from the beginning
+ */
+ restartAnimation: function () {
+ // Just toggling the class won't do it unless there's a sync reflow
+ this.dot.animate([
+ { left: "-7px", offset: 0 },
+ { left: "143px", offset: 0.25 },
+ { left: "143px", offset: 0.5 },
+ { left: "-7px", offset: 0.75 },
+ { left: "-7px", offset: 1 }
+ ], {
+ duration: (this.PREVIEW_DURATION * 2),
+ fill: "forwards"
+ });
+
+ // Restart it again after a while
+ this.autoRestartAnimation = setTimeout(this.restartAnimation.bind(this),
+ this.PREVIEW_DURATION * 2);
+ }
+};
+
+// Helpers
+
+function getPadding(padding) {
+ let p = typeof padding === "number" ? [padding] : padding;
+
+ if (p.length === 1) {
+ p[1] = p[0];
+ }
+
+ if (p.length === 2) {
+ p[2] = p[0];
+ }
+
+ if (p.length === 3) {
+ p[3] = p[1];
+ }
+
+ return p;
+}
+
+function distance(x1, y1, x2, y2) {
+ return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
+}
+
+/**
+ * Parse a string to see whether it is a valid timing function.
+ * If it is, return the coordinates as an array.
+ * Otherwise, return undefined.
+ * @param {String} value
+ * @return {Array} of coordinates, or undefined
+ */
+function parseTimingFunction(value) {
+ if (value in PREDEFINED) {
+ return PREDEFINED[value];
+ }
+
+ let tokenStream = getCSSLexer(value);
+ let getNextToken = () => {
+ while (true) {
+ let token = tokenStream.nextToken();
+ if (!token || (token.tokenType !== "whitespace" &&
+ token.tokenType !== "comment")) {
+ return token;
+ }
+ }
+ };
+
+ let token = getNextToken();
+ if (token.tokenType !== "function" || token.text !== "cubic-bezier") {
+ return undefined;
+ }
+
+ let result = [];
+ for (let i = 0; i < 4; ++i) {
+ token = getNextToken();
+ if (!token || token.tokenType !== "number") {
+ return undefined;
+ }
+ result.push(token.number);
+
+ token = getNextToken();
+ if (!token || token.tokenType !== "symbol" ||
+ token.text !== (i == 3 ? ")" : ",")) {
+ return undefined;
+ }
+ }
+
+ return result;
+}
+
+// This is exported for testing.
+exports._parseTimingFunction = parseTimingFunction;
+
+/**
+ * Removes a class from a node and adds it to another.
+ * @param {String} className the class to swap
+ * @param {DOMNode} from the node to remove the class from
+ * @param {DOMNode} to the node to add the class to
+ */
+function swapClassName(className, from, to) {
+ if (from !== null) {
+ from.classList.remove(className);
+ }
+
+ if (to !== null) {
+ to.classList.add(className);
+ }
+}
+
+/**
+ * Compares two arrays of coordinates [i, j, k, l]
+ * @param {Array} c1 first coordinate array to compare
+ * @param {Array} c2 second coordinate array to compare
+ * @return {Boolean}
+ */
+function coordsAreEqual(c1, c2) {
+ return c1.reduce((prev, curr, index) => prev && (curr === c2[index]), true);
+}
diff --git a/devtools/client/shared/widgets/FastListWidget.js b/devtools/client/shared/widgets/FastListWidget.js
new file mode 100644
index 000000000..d005ead51
--- /dev/null
+++ b/devtools/client/shared/widgets/FastListWidget.js
@@ -0,0 +1,249 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
+
+/**
+ * A list menu widget that attempts to be very fast.
+ *
+ * Note: this widget should be used in tandem with the WidgetMethods in
+ * view-helpers.js.
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ */
+const FastListWidget = module.exports = function FastListWidget(node) {
+ this.document = node.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = node;
+ this._fragment = this.document.createDocumentFragment();
+
+ // This is a prototype element that each item added to the list clones.
+ this._templateElement = this.document.createElement("hbox");
+
+ // Create an internal scrollbox container.
+ this._list = this.document.createElement("scrollbox");
+ this._list.className = "fast-list-widget-container theme-body";
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("orient", "vertical");
+ this._list.setAttribute("tabindex", "0");
+ this._list.addEventListener("keypress", e => this.emit("keyPress", e), false);
+ this._list.addEventListener("mousedown", e => this.emit("mousePress", e),
+ false);
+ this._parent.appendChild(this._list);
+
+ this._orderedMenuElementsArray = [];
+ this._itemsByElement = new Map();
+
+ // This widget emits events that can be handled in a MenuContainer.
+ EventEmitter.decorate(this);
+
+ // Delegate some of the associated node's methods to satisfy the interface
+ // required by MenuContainer instances.
+ ViewHelpers.delegateWidgetAttributeMethods(this, node);
+ ViewHelpers.delegateWidgetEventMethods(this, node);
+};
+
+FastListWidget.prototype = {
+ /**
+ * Inserts an item in this container at the specified index, optionally
+ * grouping by name.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param nsIDOMNode aContents
+ * The node to be displayed in the container.
+ * @param Object aAttachment [optional]
+ * Extra data for the user.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertItemAt: function (index, contents, attachment = {}) {
+ let element = this._templateElement.cloneNode();
+ element.appendChild(contents);
+
+ if (index >= 0) {
+ throw new Error("FastListWidget only supports appending items.");
+ }
+
+ this._fragment.appendChild(element);
+ this._orderedMenuElementsArray.push(element);
+ this._itemsByElement.set(element, this);
+
+ return element;
+ },
+
+ /**
+ * This is a non-standard widget implementation method. When appending items,
+ * they are queued in a document fragment. This method appends the document
+ * fragment to the dom.
+ */
+ flush: function () {
+ this._list.appendChild(this._fragment);
+ },
+
+ /**
+ * Removes all of the child nodes from this container.
+ */
+ removeAllItems: function () {
+ let list = this._list;
+
+ while (list.hasChildNodes()) {
+ list.firstChild.remove();
+ }
+
+ this._selectedItem = null;
+
+ this._orderedMenuElementsArray.length = 0;
+ this._itemsByElement.clear();
+ },
+
+ /**
+ * Remove the given item.
+ */
+ removeChild: function (child) {
+ throw new Error("Not yet implemented");
+ },
+
+ /**
+ * Gets the currently selected child node in this container.
+ * @return nsIDOMNode
+ */
+ get selectedItem() {
+ return this._selectedItem;
+ },
+
+ /**
+ * Sets the currently selected child node in this container.
+ * @param nsIDOMNode child
+ */
+ set selectedItem(child) {
+ let menuArray = this._orderedMenuElementsArray;
+
+ if (!child) {
+ this._selectedItem = null;
+ }
+ for (let node of menuArray) {
+ if (node == child) {
+ node.classList.add("selected");
+ this._selectedItem = node;
+ } else {
+ node.classList.remove("selected");
+ }
+ }
+
+ this.ensureElementIsVisible(this.selectedItem);
+ },
+
+ /**
+ * Returns the child node in this container situated at the specified index.
+ *
+ * @param number index
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ getItemAtIndex: function (index) {
+ return this._orderedMenuElementsArray[index];
+ },
+
+ /**
+ * Adds a new attribute or changes an existing attribute on this container.
+ *
+ * @param string name
+ * The name of the attribute.
+ * @param string value
+ * The desired attribute value.
+ */
+ setAttribute: function (name, value) {
+ this._parent.setAttribute(name, value);
+
+ if (name == "emptyText") {
+ this._textWhenEmpty = value;
+ }
+ },
+
+ /**
+ * Removes an attribute on this container.
+ *
+ * @param string name
+ * The name of the attribute.
+ */
+ removeAttribute: function (name) {
+ this._parent.removeAttribute(name);
+
+ if (name == "emptyText") {
+ this._removeEmptyText();
+ }
+ },
+
+ /**
+ * Ensures the specified element is visible.
+ *
+ * @param nsIDOMNode element
+ * The element to make visible.
+ */
+ ensureElementIsVisible: function (element) {
+ if (!element) {
+ return;
+ }
+
+ // Ensure the element is visible but not scrolled horizontally.
+ let boxObject = this._list.boxObject;
+ boxObject.ensureElementIsVisible(element);
+ boxObject.scrollBy(-this._list.clientWidth, 0);
+ },
+
+ /**
+ * Sets the text displayed in this container when empty.
+ * @param string aValue
+ */
+ set _textWhenEmpty(value) {
+ if (this._emptyTextNode) {
+ this._emptyTextNode.setAttribute("value", value);
+ }
+ this._emptyTextValue = value;
+ this._showEmptyText();
+ },
+
+ /**
+ * Creates and appends a label signaling that this container is empty.
+ */
+ _showEmptyText: function () {
+ if (this._emptyTextNode || !this._emptyTextValue) {
+ return;
+ }
+ let label = this.document.createElement("label");
+ label.className = "plain fast-list-widget-empty-text";
+ label.setAttribute("value", this._emptyTextValue);
+
+ this._parent.insertBefore(label, this._list);
+ this._emptyTextNode = label;
+ },
+
+ /**
+ * Removes the label signaling that this container is empty.
+ */
+ _removeEmptyText: function () {
+ if (!this._emptyTextNode) {
+ return;
+ }
+ this._parent.removeChild(this._emptyTextNode);
+ this._emptyTextNode = null;
+ },
+
+ window: null,
+ document: null,
+ _parent: null,
+ _list: null,
+ _selectedItem: null,
+ _orderedMenuElementsArray: null,
+ _itemsByElement: null,
+ _emptyTextNode: null,
+ _emptyTextValue: ""
+};
diff --git a/devtools/client/shared/widgets/FilterWidget.js b/devtools/client/shared/widgets/FilterWidget.js
new file mode 100644
index 000000000..9cdb27a5a
--- /dev/null
+++ b/devtools/client/shared/widgets/FilterWidget.js
@@ -0,0 +1,1073 @@
+/* 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";
+
+/**
+ * This is a CSS Filter Editor widget used
+ * for Rule View's filter swatches
+ */
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Cc, Ci } = require("chrome");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/filterwidget.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const {cssTokenizer} = require("devtools/shared/css/parsing-utils");
+
+const asyncStorage = require("devtools/shared/async-storage");
+
+loader.lazyGetter(this, "DOMUtils", () => {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+const DEFAULT_FILTER_TYPE = "length";
+const UNIT_MAPPING = {
+ percentage: "%",
+ length: "px",
+ angle: "deg",
+ string: ""
+};
+
+const FAST_VALUE_MULTIPLIER = 10;
+const SLOW_VALUE_MULTIPLIER = 0.1;
+const DEFAULT_VALUE_MULTIPLIER = 1;
+
+const LIST_PADDING = 7;
+const LIST_ITEM_HEIGHT = 32;
+
+const filterList = [
+ {
+ "name": "blur",
+ "range": [0, Infinity],
+ "type": "length"
+ },
+ {
+ "name": "brightness",
+ "range": [0, Infinity],
+ "type": "percentage"
+ },
+ {
+ "name": "contrast",
+ "range": [0, Infinity],
+ "type": "percentage"
+ },
+ {
+ "name": "drop-shadow",
+ "placeholder": L10N.getStr("dropShadowPlaceholder"),
+ "type": "string"
+ },
+ {
+ "name": "grayscale",
+ "range": [0, 100],
+ "type": "percentage"
+ },
+ {
+ "name": "hue-rotate",
+ "range": [0, Infinity],
+ "type": "angle"
+ },
+ {
+ "name": "invert",
+ "range": [0, 100],
+ "type": "percentage"
+ },
+ {
+ "name": "opacity",
+ "range": [0, 100],
+ "type": "percentage"
+ },
+ {
+ "name": "saturate",
+ "range": [0, Infinity],
+ "type": "percentage"
+ },
+ {
+ "name": "sepia",
+ "range": [0, 100],
+ "type": "percentage"
+ },
+ {
+ "name": "url",
+ "placeholder": "example.svg#c1",
+ "type": "string"
+ }
+];
+
+// Valid values that shouldn't be parsed for filters.
+const SPECIAL_VALUES = new Set(["none", "unset", "initial", "inherit"]);
+
+/**
+ * A CSS Filter editor widget used to add/remove/modify
+ * filters.
+ *
+ * Normally, it takes a CSS filter value as input, parses it
+ * and creates the required elements / bindings.
+ *
+ * You can, however, use add/remove/update methods manually.
+ * See each method's comments for more details
+ *
+ * @param {nsIDOMNode} el
+ * The widget container.
+ * @param {String} value
+ * CSS filter value
+ * @param {Function} cssIsValid
+ * Test whether css name / value is valid.
+ */
+function CSSFilterEditorWidget(el, value = "", cssIsValid) {
+ this.doc = el.ownerDocument;
+ this.win = this.doc.defaultView;
+ this.el = el;
+ this._cssIsValid = cssIsValid;
+
+ this._addButtonClick = this._addButtonClick.bind(this);
+ this._removeButtonClick = this._removeButtonClick.bind(this);
+ this._mouseMove = this._mouseMove.bind(this);
+ this._mouseUp = this._mouseUp.bind(this);
+ this._mouseDown = this._mouseDown.bind(this);
+ this._keyDown = this._keyDown.bind(this);
+ this._input = this._input.bind(this);
+ this._presetClick = this._presetClick.bind(this);
+ this._savePreset = this._savePreset.bind(this);
+ this._togglePresets = this._togglePresets.bind(this);
+ this._resetFocus = this._resetFocus.bind(this);
+
+ // Passed to asyncStorage, requires binding
+ this.renderPresets = this.renderPresets.bind(this);
+
+ this._initMarkup();
+ this._buildFilterItemMarkup();
+ this._buildPresetItemMarkup();
+ this._addEventListeners();
+
+ EventEmitter.decorate(this);
+
+ this.filters = [];
+ this.setCssValue(value);
+ this.renderPresets();
+}
+
+exports.CSSFilterEditorWidget = CSSFilterEditorWidget;
+
+CSSFilterEditorWidget.prototype = {
+ _initMarkup: function () {
+ let filterListSelectPlaceholder =
+ L10N.getStr("filterListSelectPlaceholder");
+ let addNewFilterButton = L10N.getStr("addNewFilterButton");
+ let presetsToggleButton = L10N.getStr("presetsToggleButton");
+ let newPresetPlaceholder = L10N.getStr("newPresetPlaceholder");
+ let savePresetButton = L10N.getStr("savePresetButton");
+
+ this.el.innerHTML = `
+ <div class="filters-list">
+ <div id="filters"></div>
+ <div class="footer">
+ <select value="">
+ <option value="">${filterListSelectPlaceholder}</option>
+ </select>
+ <button id="add-filter" class="add">${addNewFilterButton}</button>
+ <button id="toggle-presets">${presetsToggleButton}</button>
+ </div>
+ </div>
+
+ <div class="presets-list">
+ <div id="presets"></div>
+ <div class="footer">
+ <input value="" class="devtools-textinput"
+ placeholder="${newPresetPlaceholder}"></input>
+ <button class="add">${savePresetButton}</button>
+ </div>
+ </div>
+ `;
+ this.filtersList = this.el.querySelector("#filters");
+ this.presetsList = this.el.querySelector("#presets");
+ this.togglePresets = this.el.querySelector("#toggle-presets");
+ this.filterSelect = this.el.querySelector("select");
+ this.addPresetButton = this.el.querySelector(".presets-list .add");
+ this.addPresetInput = this.el.querySelector(".presets-list .footer input");
+
+ this.el.querySelector(".presets-list input").value = "";
+
+ this._populateFilterSelect();
+ },
+
+ _destroyMarkup: function () {
+ this._filterItemMarkup.remove();
+ this.el.remove();
+ this.el = this.filtersList = this._filterItemMarkup = null;
+ this.presetsList = this.togglePresets = this.filterSelect = null;
+ this.addPresetButton = null;
+ },
+
+ destroy: function () {
+ this._removeEventListeners();
+ this._destroyMarkup();
+ },
+
+ /**
+ * Creates <option> elements for each filter definition
+ * in filterList
+ */
+ _populateFilterSelect: function () {
+ let select = this.filterSelect;
+ filterList.forEach(filter => {
+ let option = this.doc.createElementNS(XHTML_NS, "option");
+ option.innerHTML = option.value = filter.name;
+ select.appendChild(option);
+ });
+ },
+
+ /**
+ * Creates a template for filter elements which is cloned and used in render
+ */
+ _buildFilterItemMarkup: function () {
+ let base = this.doc.createElementNS(XHTML_NS, "div");
+ base.className = "filter";
+
+ let name = this.doc.createElementNS(XHTML_NS, "div");
+ name.className = "filter-name";
+
+ let value = this.doc.createElementNS(XHTML_NS, "div");
+ value.className = "filter-value";
+
+ let drag = this.doc.createElementNS(XHTML_NS, "i");
+ drag.title = L10N.getStr("dragHandleTooltipText");
+
+ let label = this.doc.createElementNS(XHTML_NS, "label");
+
+ name.appendChild(drag);
+ name.appendChild(label);
+
+ let unitPreview = this.doc.createElementNS(XHTML_NS, "span");
+ let input = this.doc.createElementNS(XHTML_NS, "input");
+ input.classList.add("devtools-textinput");
+
+ value.appendChild(input);
+ value.appendChild(unitPreview);
+
+ let removeButton = this.doc.createElementNS(XHTML_NS, "button");
+ removeButton.className = "remove-button";
+
+ base.appendChild(name);
+ base.appendChild(value);
+ base.appendChild(removeButton);
+
+ this._filterItemMarkup = base;
+ },
+
+ _buildPresetItemMarkup: function () {
+ let base = this.doc.createElementNS(XHTML_NS, "div");
+ base.classList.add("preset");
+
+ let name = this.doc.createElementNS(XHTML_NS, "label");
+ base.appendChild(name);
+
+ let value = this.doc.createElementNS(XHTML_NS, "span");
+ base.appendChild(value);
+
+ let removeButton = this.doc.createElementNS(XHTML_NS, "button");
+ removeButton.classList.add("remove-button");
+
+ base.appendChild(removeButton);
+
+ this._presetItemMarkup = base;
+ },
+
+ _addEventListeners: function () {
+ this.addButton = this.el.querySelector("#add-filter");
+ this.addButton.addEventListener("click", this._addButtonClick);
+ this.filtersList.addEventListener("click", this._removeButtonClick);
+ this.filtersList.addEventListener("mousedown", this._mouseDown);
+ this.filtersList.addEventListener("keydown", this._keyDown);
+ this.el.addEventListener("mousedown", this._resetFocus);
+
+ this.presetsList.addEventListener("click", this._presetClick);
+ this.togglePresets.addEventListener("click", this._togglePresets);
+ this.addPresetButton.addEventListener("click", this._savePreset);
+
+ // These events are event delegators for
+ // drag-drop re-ordering and label-dragging
+ this.win.addEventListener("mousemove", this._mouseMove);
+ this.win.addEventListener("mouseup", this._mouseUp);
+
+ // Used to workaround float-precision problems
+ this.filtersList.addEventListener("input", this._input);
+ },
+
+ _removeEventListeners: function () {
+ this.addButton.removeEventListener("click", this._addButtonClick);
+ this.filtersList.removeEventListener("click", this._removeButtonClick);
+ this.filtersList.removeEventListener("mousedown", this._mouseDown);
+ this.filtersList.removeEventListener("keydown", this._keyDown);
+ this.el.removeEventListener("mousedown", this._resetFocus);
+
+ this.presetsList.removeEventListener("click", this._presetClick);
+ this.togglePresets.removeEventListener("click", this._togglePresets);
+ this.addPresetButton.removeEventListener("click", this._savePreset);
+
+ // These events are used for drag drop re-ordering
+ this.win.removeEventListener("mousemove", this._mouseMove);
+ this.win.removeEventListener("mouseup", this._mouseUp);
+
+ // Used to workaround float-precision problems
+ this.filtersList.removeEventListener("input", this._input);
+ },
+
+ _getFilterElementIndex: function (el) {
+ return [...this.filtersList.children].indexOf(el);
+ },
+
+ _keyDown: function (e) {
+ if (e.target.tagName.toLowerCase() !== "input" ||
+ (e.keyCode !== 40 && e.keyCode !== 38)) {
+ return;
+ }
+ let input = e.target;
+
+ const direction = e.keyCode === 40 ? -1 : 1;
+
+ let multiplier = DEFAULT_VALUE_MULTIPLIER;
+ if (e.altKey) {
+ multiplier = SLOW_VALUE_MULTIPLIER;
+ } else if (e.shiftKey) {
+ multiplier = FAST_VALUE_MULTIPLIER;
+ }
+
+ const filterEl = e.target.closest(".filter");
+ const index = this._getFilterElementIndex(filterEl);
+ const filter = this.filters[index];
+
+ // Filters that have units are number-type filters. For them,
+ // the value can be incremented/decremented simply.
+ // For other types of filters (e.g. drop-shadow) we need to check
+ // if the keypress happened close to a number first.
+ if (filter.unit) {
+ let startValue = parseFloat(e.target.value);
+ let value = startValue + direction * multiplier;
+
+ const [min, max] = this._definition(filter.name).range;
+ if (value < min) {
+ value = min;
+ } else if (value > max) {
+ value = max;
+ }
+
+ input.value = fixFloat(value);
+
+ this.updateValueAt(index, value);
+ } else {
+ let selectionStart = input.selectionStart;
+ let num = getNeighbourNumber(input.value, selectionStart);
+ if (!num) {
+ return;
+ }
+
+ let {start, end, value} = num;
+
+ let split = input.value.split("");
+ let computed = fixFloat(value + direction * multiplier);
+ let dotIndex = computed.indexOf(".0");
+ if (dotIndex > -1) {
+ computed = computed.slice(0, -2);
+
+ selectionStart = selectionStart > start + dotIndex ?
+ start + dotIndex :
+ selectionStart;
+ }
+ split.splice(start, end - start, computed);
+
+ value = split.join("");
+ input.value = value;
+ this.updateValueAt(index, value);
+ input.setSelectionRange(selectionStart, selectionStart);
+ }
+ e.preventDefault();
+ },
+
+ _input: function (e) {
+ let filterEl = e.target.closest(".filter");
+ let index = this._getFilterElementIndex(filterEl);
+ let filter = this.filters[index];
+ let def = this._definition(filter.name);
+
+ if (def.type !== "string") {
+ e.target.value = fixFloat(e.target.value);
+ }
+ this.updateValueAt(index, e.target.value);
+ },
+
+ _mouseDown: function (e) {
+ let filterEl = e.target.closest(".filter");
+
+ // re-ordering drag handle
+ if (e.target.tagName.toLowerCase() === "i") {
+ this.isReorderingFilter = true;
+ filterEl.startingY = e.pageY;
+ filterEl.classList.add("dragging");
+
+ this.el.classList.add("dragging");
+ // label-dragging
+ } else if (e.target.classList.contains("devtools-draglabel")) {
+ let label = e.target;
+ let input = filterEl.querySelector("input");
+ let index = this._getFilterElementIndex(filterEl);
+
+ this._dragging = {
+ index, label, input,
+ startX: e.pageX
+ };
+
+ this.isDraggingLabel = true;
+ }
+ },
+
+ _addButtonClick: function () {
+ const select = this.filterSelect;
+ if (!select.value) {
+ return;
+ }
+
+ const key = select.value;
+ this.add(key, null);
+
+ this.render();
+ },
+
+ _removeButtonClick: function (e) {
+ const isRemoveButton = e.target.classList.contains("remove-button");
+ if (!isRemoveButton) {
+ return;
+ }
+
+ let filterEl = e.target.closest(".filter");
+ let index = this._getFilterElementIndex(filterEl);
+ this.removeAt(index);
+ },
+
+ _mouseMove: function (e) {
+ if (this.isReorderingFilter) {
+ this._dragFilterElement(e);
+ } else if (this.isDraggingLabel) {
+ this._dragLabel(e);
+ }
+ },
+
+ _dragFilterElement: function (e) {
+ const rect = this.filtersList.getBoundingClientRect();
+ let top = e.pageY - LIST_PADDING;
+ let bottom = e.pageY + LIST_PADDING;
+ // don't allow dragging over top/bottom of list
+ if (top < rect.top || bottom > rect.bottom) {
+ return;
+ }
+
+ const filterEl = this.filtersList.querySelector(".dragging");
+
+ const delta = e.pageY - filterEl.startingY;
+ filterEl.style.top = delta + "px";
+
+ // change is the number of _steps_ taken from initial position
+ // i.e. how many elements we have passed
+ let change = delta / LIST_ITEM_HEIGHT;
+ if (change > 0) {
+ change = Math.floor(change);
+ } else if (change < 0) {
+ change = Math.ceil(change);
+ }
+
+ const children = this.filtersList.children;
+ const index = [...children].indexOf(filterEl);
+ const destination = index + change;
+
+ // If we're moving out, or there's no change at all, stop and return
+ if (destination >= children.length || destination < 0 || change === 0) {
+ return;
+ }
+
+ // Re-order filter objects
+ swapArrayIndices(this.filters, index, destination);
+
+ // Re-order the dragging element in markup
+ const target = change > 0 ? children[destination + 1]
+ : children[destination];
+ if (target) {
+ this.filtersList.insertBefore(filterEl, target);
+ } else {
+ this.filtersList.appendChild(filterEl);
+ }
+
+ filterEl.removeAttribute("style");
+
+ const currentPosition = change * LIST_ITEM_HEIGHT;
+ filterEl.startingY = e.pageY + currentPosition - delta;
+ },
+
+ _dragLabel: function (e) {
+ let dragging = this._dragging;
+
+ let input = dragging.input;
+
+ let multiplier = DEFAULT_VALUE_MULTIPLIER;
+
+ if (e.altKey) {
+ multiplier = SLOW_VALUE_MULTIPLIER;
+ } else if (e.shiftKey) {
+ multiplier = FAST_VALUE_MULTIPLIER;
+ }
+
+ dragging.lastX = e.pageX;
+ const delta = e.pageX - dragging.startX;
+ const startValue = parseFloat(input.value);
+ let value = startValue + delta * multiplier;
+
+ const filter = this.filters[dragging.index];
+ const [min, max] = this._definition(filter.name).range;
+ if (value < min) {
+ value = min;
+ } else if (value > max) {
+ value = max;
+ }
+
+ input.value = fixFloat(value);
+
+ dragging.startX = e.pageX;
+
+ this.updateValueAt(dragging.index, value);
+ },
+
+ _mouseUp: function () {
+ // Label-dragging is disabled on mouseup
+ this._dragging = null;
+ this.isDraggingLabel = false;
+
+ // Filter drag/drop needs more cleaning
+ if (!this.isReorderingFilter) {
+ return;
+ }
+ let filterEl = this.filtersList.querySelector(".dragging");
+
+ this.isReorderingFilter = false;
+ filterEl.classList.remove("dragging");
+ this.el.classList.remove("dragging");
+ filterEl.removeAttribute("style");
+
+ this.emit("updated", this.getCssValue());
+ this.render();
+ },
+
+ _presetClick: function (e) {
+ let el = e.target;
+ let preset = el.closest(".preset");
+ if (!preset) {
+ return;
+ }
+
+ let id = +preset.dataset.id;
+
+ this.getPresets().then(presets => {
+ if (el.classList.contains("remove-button")) {
+ // If the click happened on the remove button.
+ presets.splice(id, 1);
+ this.setPresets(presets).then(this.renderPresets,
+ ex => console.error(ex));
+ } else {
+ // Or if the click happened on a preset.
+ let p = presets[id];
+
+ this.setCssValue(p.value);
+ this.addPresetInput.value = p.name;
+ }
+ }, ex => console.error(ex));
+ },
+
+ _togglePresets: function () {
+ this.el.classList.toggle("show-presets");
+ this.emit("render");
+ },
+
+ _savePreset: function (e) {
+ e.preventDefault();
+
+ let name = this.addPresetInput.value;
+ let value = this.getCssValue();
+
+ if (!name || !value || SPECIAL_VALUES.has(value)) {
+ this.emit("preset-save-error");
+ return;
+ }
+
+ this.getPresets().then(presets => {
+ let index = presets.findIndex(preset => preset.name === name);
+
+ if (index > -1) {
+ presets[index].value = value;
+ } else {
+ presets.push({name, value});
+ }
+
+ this.setPresets(presets).then(this.renderPresets,
+ ex => console.error(ex));
+ }, ex => console.error(ex));
+ },
+
+ /**
+ * Workaround needed to reset the focus when using a HTML select inside a XUL panel.
+ * See Bug 1294366.
+ */
+ _resetFocus: function () {
+ this.filterSelect.ownerDocument.defaultView.focus();
+ },
+
+ /**
+ * Clears the list and renders filters, binding required events.
+ * There are some delegated events bound in _addEventListeners method
+ */
+ render: function () {
+ if (!this.filters.length) {
+ this.filtersList.innerHTML = `<p> ${L10N.getStr("emptyFilterList")} <br />
+ ${L10N.getStr("addUsingList")} </p>`;
+ this.emit("render");
+ return;
+ }
+
+ this.filtersList.innerHTML = "";
+
+ let base = this._filterItemMarkup;
+
+ for (let filter of this.filters) {
+ const def = this._definition(filter.name);
+
+ let el = base.cloneNode(true);
+
+ let [name, value] = el.children;
+ let label = name.children[1];
+ let [input, unitPreview] = value.children;
+
+ let min, max;
+ if (def.range) {
+ [min, max] = def.range;
+ }
+
+ label.textContent = filter.name;
+ input.value = filter.value;
+
+ switch (def.type) {
+ case "percentage":
+ case "angle":
+ case "length":
+ input.type = "number";
+ input.min = min;
+ if (max !== Infinity) {
+ input.max = max;
+ }
+ input.step = "0.1";
+ break;
+ case "string":
+ input.type = "text";
+ input.placeholder = def.placeholder;
+ break;
+ }
+
+ // use photoshop-style label-dragging
+ // and show filters' unit next to their <input>
+ if (def.type !== "string") {
+ unitPreview.textContent = filter.unit;
+
+ label.classList.add("devtools-draglabel");
+ label.title = L10N.getStr("labelDragTooltipText");
+ } else {
+ // string-type filters have no unit
+ unitPreview.remove();
+ }
+
+ this.filtersList.appendChild(el);
+ }
+
+ let lastInput =
+ this.filtersList.querySelector(".filter:last-of-type input");
+ if (lastInput) {
+ lastInput.focus();
+ if (lastInput.type === "text") {
+ // move cursor to end of input
+ const end = lastInput.value.length;
+ lastInput.setSelectionRange(end, end);
+ }
+ }
+
+ this.emit("render");
+ },
+
+ renderPresets: function () {
+ this.getPresets().then(presets => {
+ // getPresets is async and the widget may be destroyed in between.
+ if (!this.presetsList) {
+ return;
+ }
+
+ if (!presets || !presets.length) {
+ this.presetsList.innerHTML = `<p>${L10N.getStr("emptyPresetList")}</p>`;
+ this.emit("render");
+ return;
+ }
+ let base = this._presetItemMarkup;
+
+ this.presetsList.innerHTML = "";
+
+ for (let [index, preset] of presets.entries()) {
+ let el = base.cloneNode(true);
+
+ let [label, span] = el.children;
+
+ el.dataset.id = index;
+
+ label.textContent = preset.name;
+ span.textContent = preset.value;
+
+ this.presetsList.appendChild(el);
+ }
+
+ this.emit("render");
+ });
+ },
+
+ /**
+ * returns definition of a filter as defined in filterList
+ *
+ * @param {String} name
+ * filter name (e.g. blur)
+ * @return {Object}
+ * filter's definition
+ */
+ _definition: function (name) {
+ name = name.toLowerCase();
+ return filterList.find(a => a.name === name);
+ },
+
+ /**
+ * Parses the CSS value specified, updating widget's filters
+ *
+ * @param {String} cssValue
+ * css value to be parsed
+ */
+ setCssValue: function (cssValue) {
+ if (!cssValue) {
+ throw new Error("Missing CSS filter value in setCssValue");
+ }
+
+ this.filters = [];
+
+ if (SPECIAL_VALUES.has(cssValue)) {
+ this._specialValue = cssValue;
+ this.emit("updated", this.getCssValue());
+ this.render();
+ return;
+ }
+
+ for (let {name, value, quote} of tokenizeFilterValue(cssValue)) {
+ // If the specified value is invalid, replace it with the
+ // default.
+ if (name !== "url") {
+ if (!this._cssIsValid("filter", name + "(" + value + ")")) {
+ value = null;
+ }
+ }
+
+ this.add(name, value, quote, true);
+ }
+
+ this.emit("updated", this.getCssValue());
+ this.render();
+ },
+
+ /**
+ * Creates a new [name] filter record with value
+ *
+ * @param {String} name
+ * filter name (e.g. blur)
+ * @param {String} value
+ * value of the filter (e.g. 30px, 20%)
+ * If this is |null|, then a default value may be supplied.
+ * @param {String} quote
+ * For a url filter, the quoting style. This can be a
+ * single quote, a double quote, or empty.
+ * @return {Number}
+ * The index of the new filter in the current list of filters
+ * @param {Boolean}
+ * By default, adding a new filter emits an "updated" event, but if
+ * you're calling add in a loop and wait to emit a single event after
+ * the loop yourself, set this parameter to true.
+ */
+ add: function (name, value, quote, noEvent) {
+ const def = this._definition(name);
+ if (!def) {
+ return false;
+ }
+
+ if (value === null) {
+ // UNIT_MAPPING[string] is an empty string (falsy), so
+ // using || doesn't work here
+ const unitLabel = typeof UNIT_MAPPING[def.type] === "undefined" ?
+ UNIT_MAPPING[DEFAULT_FILTER_TYPE] :
+ UNIT_MAPPING[def.type];
+
+ // string-type filters have no default value but a placeholder instead
+ if (!unitLabel) {
+ value = "";
+ } else {
+ value = def.range[0] + unitLabel;
+ }
+
+ if (name === "url") {
+ // Default quote.
+ quote = "\"";
+ }
+ }
+
+ let unit = def.type === "string"
+ ? ""
+ : (/[a-zA-Z%]+/.exec(value) || [])[0];
+
+ if (def.type !== "string") {
+ value = parseFloat(value);
+
+ // You can omit percentage values' and use a value between 0..1
+ if (def.type === "percentage" && !unit) {
+ value = value * 100;
+ unit = "%";
+ }
+
+ const [min, max] = def.range;
+ if (value < min) {
+ value = min;
+ } else if (value > max) {
+ value = max;
+ }
+ }
+
+ const index = this.filters.push({value, unit, name, quote}) - 1;
+ if (!noEvent) {
+ this.emit("updated", this.getCssValue());
+ }
+
+ return index;
+ },
+
+ /**
+ * returns value + unit of the specified filter
+ *
+ * @param {Number} index
+ * filter index
+ * @return {String}
+ * css value of filter
+ */
+ getValueAt: function (index) {
+ let filter = this.filters[index];
+ if (!filter) {
+ return null;
+ }
+
+ // Just return the value+unit for non-url functions.
+ if (filter.name !== "url") {
+ return filter.value + filter.unit;
+ }
+
+ // url values need to be quoted and escaped.
+ if (filter.quote === "'") {
+ return "'" + filter.value.replace(/\'/g, "\\'") + "'";
+ } else if (filter.quote === "\"") {
+ return "\"" + filter.value.replace(/\"/g, "\\\"") + "\"";
+ }
+
+ // Unquoted. This approach might change the original input -- for
+ // example the original might be over-quoted. But, this is
+ // correct and probably good enough.
+ return filter.value.replace(/[\\ \t()"']/g, "\\$&");
+ },
+
+ removeAt: function (index) {
+ if (!this.filters[index]) {
+ return;
+ }
+
+ this.filters.splice(index, 1);
+ this.emit("updated", this.getCssValue());
+ this.render();
+ },
+
+ /**
+ * Generates CSS filter value for filters of the widget
+ *
+ * @return {String}
+ * css value of filters
+ */
+ getCssValue: function () {
+ return this.filters.map((filter, i) => {
+ return `${filter.name}(${this.getValueAt(i)})`;
+ }).join(" ") || this._specialValue || "none";
+ },
+
+ /**
+ * Updates specified filter's value
+ *
+ * @param {Number} index
+ * The index of the filter in the current list of filters
+ * @param {number/string} value
+ * value to set, string for string-typed filters
+ * number for the rest (unit automatically determined)
+ */
+ updateValueAt: function (index, value) {
+ let filter = this.filters[index];
+ if (!filter) {
+ return;
+ }
+
+ const def = this._definition(filter.name);
+
+ if (def.type !== "string") {
+ const [min, max] = def.range;
+ if (value < min) {
+ value = min;
+ } else if (value > max) {
+ value = max;
+ }
+ }
+
+ filter.value = filter.unit ? fixFloat(value, true) : value;
+
+ this.emit("updated", this.getCssValue());
+ },
+
+ getPresets: function () {
+ return asyncStorage.getItem("cssFilterPresets").then(presets => {
+ if (!presets) {
+ return [];
+ }
+
+ return presets;
+ }, e => console.error(e));
+ },
+
+ setPresets: function (presets) {
+ return asyncStorage.setItem("cssFilterPresets", presets)
+ .catch(e => console.error(e));
+ }
+};
+
+// Fixes JavaScript's float precision
+function fixFloat(a, number) {
+ let fixed = parseFloat(a).toFixed(1);
+ return number ? parseFloat(fixed) : fixed;
+}
+
+/**
+ * Used to swap two filters' indexes
+ * after drag/drop re-ordering
+ *
+ * @param {Array} array
+ * the array to swap elements of
+ * @param {Number} a
+ * index of first element
+ * @param {Number} b
+ * index of second element
+ */
+function swapArrayIndices(array, a, b) {
+ array[a] = array.splice(b, 1, array[a])[0];
+}
+
+/**
+ * Tokenizes a CSS Filter value and returns an array of {name, value} pairs.
+ *
+ * @param {String} css CSS Filter value to be parsed
+ * @return {Array} An array of {name, value} pairs
+ */
+function tokenizeFilterValue(css) {
+ let filters = [];
+ let depth = 0;
+
+ if (SPECIAL_VALUES.has(css)) {
+ return filters;
+ }
+
+ let state = "initial";
+ let name;
+ let contents;
+ for (let token of cssTokenizer(css)) {
+ switch (state) {
+ case "initial":
+ if (token.tokenType === "function") {
+ name = token.text;
+ contents = "";
+ state = "function";
+ depth = 1;
+ } else if (token.tokenType === "url" || token.tokenType === "bad_url") {
+ // Extract the quoting style from the url.
+ let originalText = css.substring(token.startOffset, token.endOffset);
+ let [, quote] = /^url\([ \t\r\n\f]*(["']?)/i.exec(originalText);
+
+ filters.push({name: "url", value: token.text.trim(), quote: quote});
+ // Leave state as "initial" because the URL token includes
+ // the trailing close paren.
+ }
+ break;
+
+ case "function":
+ if (token.tokenType === "symbol" && token.text === ")") {
+ --depth;
+ if (depth === 0) {
+ filters.push({name: name, value: contents.trim()});
+ state = "initial";
+ break;
+ }
+ }
+ contents += css.substring(token.startOffset, token.endOffset);
+ if (token.tokenType === "function" ||
+ (token.tokenType === "symbol" && token.text === "(")) {
+ ++depth;
+ }
+ break;
+ }
+ }
+
+ return filters;
+}
+
+/**
+ * Finds neighbour number characters of an index in a string
+ * the numbers may be floats (containing dots)
+ * It's assumed that the value given to this function is a valid number
+ *
+ * @param {String} string
+ * The string containing numbers
+ * @param {Number} index
+ * The index to look for neighbours for
+ * @return {Object}
+ * returns null if no number is found
+ * value: The number found
+ * start: The number's starting index
+ * end: The number's ending index
+ */
+function getNeighbourNumber(string, index) {
+ if (!/\d/.test(string)) {
+ return null;
+ }
+
+ let left = /-?[0-9.]*$/.exec(string.slice(0, index));
+ let right = /-?[0-9.]*/.exec(string.slice(index));
+
+ left = left ? left[0] : "";
+ right = right ? right[0] : "";
+
+ if (!right && !left) {
+ return null;
+ }
+
+ return {
+ value: fixFloat(left + right, true),
+ start: index - left.length,
+ end: index + right.length
+ };
+}
diff --git a/devtools/client/shared/widgets/FlameGraph.js b/devtools/client/shared/widgets/FlameGraph.js
new file mode 100644
index 000000000..e9d25b345
--- /dev/null
+++ b/devtools/client/shared/widgets/FlameGraph.js
@@ -0,0 +1,1462 @@
+/* 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 { Task } = require("devtools/shared/task");
+const { ViewHelpers, setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const { ELLIPSIS } = require("devtools/shared/l10n");
+
+loader.lazyRequireGetter(this, "defer", "devtools/shared/defer");
+loader.lazyRequireGetter(this, "EventEmitter",
+ "devtools/shared/event-emitter");
+
+loader.lazyRequireGetter(this, "getColor",
+ "devtools/client/shared/theme", true);
+
+loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
+ "devtools/client/performance/modules/categories", true);
+loader.lazyRequireGetter(this, "FrameUtils",
+ "devtools/client/performance/modules/logic/frame-utils");
+loader.lazyRequireGetter(this, "demangle",
+ "devtools/client/shared/demangle");
+
+loader.lazyRequireGetter(this, "AbstractCanvasGraph",
+ "devtools/client/shared/widgets/Graphs", true);
+loader.lazyRequireGetter(this, "GraphArea",
+ "devtools/client/shared/widgets/Graphs", true);
+loader.lazyRequireGetter(this, "GraphAreaDragger",
+ "devtools/client/shared/widgets/Graphs", true);
+
+const GRAPH_SRC = "chrome://devtools/content/shared/widgets/graphs-frame.xhtml";
+
+// ms
+const GRAPH_RESIZE_EVENTS_DRAIN = 100;
+
+const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035;
+const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5;
+const GRAPH_KEYBOARD_ZOOM_SENSITIVITY = 20;
+const GRAPH_KEYBOARD_PAN_SENSITIVITY = 20;
+const GRAPH_KEYBOARD_ACCELERATION = 1.05;
+const GRAPH_KEYBOARD_TRANSLATION_MAX = 150;
+
+// ms
+const GRAPH_MIN_SELECTION_WIDTH = 0.001;
+
+// px
+const GRAPH_HORIZONTAL_PAN_THRESHOLD = 10;
+const GRAPH_VERTICAL_PAN_THRESHOLD = 30;
+
+const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
+
+// ms
+const TIMELINE_TICKS_MULTIPLE = 5;
+// px
+const TIMELINE_TICKS_SPACING_MIN = 75;
+
+// px
+const OVERVIEW_HEADER_HEIGHT = 16;
+const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9;
+const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
+// px
+const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6;
+const OVERVIEW_HEADER_TEXT_PADDING_TOP = 5;
+const OVERVIEW_HEADER_TIMELINE_STROKE_COLOR = "rgba(128, 128, 128, 0.5)";
+
+// px
+const FLAME_GRAPH_BLOCK_HEIGHT = 15;
+const FLAME_GRAPH_BLOCK_BORDER = 1;
+const FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 10;
+const FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "message-box, Helvetica Neue," +
+ "Helvetica, sans-serif";
+// px
+const FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP = 0;
+const FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT = 3;
+const FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT = 3;
+
+// Large enough number for a diverse pallette.
+const PALLETTE_SIZE = 20;
+const PALLETTE_HUE_OFFSET = Math.random() * 90;
+const PALLETTE_HUE_RANGE = 270;
+const PALLETTE_SATURATION = 100;
+const PALLETTE_BRIGHTNESS = 55;
+const PALLETTE_OPACITY = 0.35;
+
+const COLOR_PALLETTE = Array.from(Array(PALLETTE_SIZE)).map((_, i) => "hsla" +
+ "(" +
+ ((PALLETTE_HUE_OFFSET + (i / PALLETTE_SIZE * PALLETTE_HUE_RANGE)) | 0 % 360) +
+ "," + PALLETTE_SATURATION + "%" +
+ "," + PALLETTE_BRIGHTNESS + "%" +
+ "," + PALLETTE_OPACITY +
+ ")"
+);
+
+/**
+ * A flamegraph visualization. This implementation is responsable only with
+ * drawing the graph, using a data source consisting of rectangles and
+ * their corresponding widths.
+ *
+ * Example usage:
+ * let graph = new FlameGraph(node);
+ * graph.once("ready", () => {
+ * let data = FlameGraphUtils.createFlameGraphDataFromThread(thread);
+ * let bounds = { startTime, endTime };
+ * graph.setData({ data, bounds });
+ * });
+ *
+ * Data source format:
+ * [
+ * {
+ * color: "string",
+ * blocks: [
+ * {
+ * x: number,
+ * y: number,
+ * width: number,
+ * height: number,
+ * text: "string"
+ * },
+ * ...
+ * ]
+ * },
+ * {
+ * color: "string",
+ * blocks: [...]
+ * },
+ * ...
+ * {
+ * color: "string",
+ * blocks: [...]
+ * }
+ * ]
+ *
+ * Use `FlameGraphUtils` to convert profiler data (or any other data source)
+ * into a drawable format.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the graph.
+ * @param number sharpness [optional]
+ * Defaults to the current device pixel ratio.
+ */
+function FlameGraph(parent, sharpness) {
+ EventEmitter.decorate(this);
+
+ this._parent = parent;
+ this._ready = defer();
+
+ this.setTheme();
+
+ AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => {
+ this._iframe = iframe;
+ this._window = iframe.contentWindow;
+ this._document = iframe.contentDocument;
+ this._pixelRatio = sharpness || this._window.devicePixelRatio;
+
+ let container =
+ this._container = this._document.getElementById("graph-container");
+ container.className = "flame-graph-widget-container graph-widget-container";
+
+ let canvas = this._canvas = this._document.getElementById("graph-canvas");
+ canvas.className = "flame-graph-widget-canvas graph-widget-canvas";
+
+ let bounds = parent.getBoundingClientRect();
+ bounds.width = this.fixedWidth || bounds.width;
+ bounds.height = this.fixedHeight || bounds.height;
+ iframe.setAttribute("width", bounds.width);
+ iframe.setAttribute("height", bounds.height);
+
+ this._width = canvas.width = bounds.width * this._pixelRatio;
+ this._height = canvas.height = bounds.height * this._pixelRatio;
+ this._ctx = canvas.getContext("2d");
+
+ this._bounds = new GraphArea();
+ this._selection = new GraphArea();
+ this._selectionDragger = new GraphAreaDragger();
+ this._verticalOffset = 0;
+ this._verticalOffsetDragger = new GraphAreaDragger(0);
+ this._keyboardZoomAccelerationFactor = 1;
+ this._keyboardPanAccelerationFactor = 1;
+
+ this._userInputStack = 0;
+ this._keysPressed = [];
+
+ // Calculating text widths is necessary to trim the text inside the blocks
+ // while the scaling changes (e.g. via scrolling). This is very expensive,
+ // so maintain a cache of string contents to text widths.
+ this._textWidthsCache = {};
+
+ let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio;
+ let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
+ this._ctx.font = fontSize + "px " + fontFamily;
+ this._averageCharWidth = this._calcAverageCharWidth();
+ this._overflowCharWidth = this._getTextWidth(this.overflowChar);
+
+ this._onAnimationFrame = this._onAnimationFrame.bind(this);
+ this._onKeyDown = this._onKeyDown.bind(this);
+ this._onKeyUp = this._onKeyUp.bind(this);
+ this._onKeyPress = this._onKeyPress.bind(this);
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this._onMouseUp = this._onMouseUp.bind(this);
+ this._onMouseWheel = this._onMouseWheel.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this.refresh = this.refresh.bind(this);
+
+ this._window.addEventListener("keydown", this._onKeyDown);
+ this._window.addEventListener("keyup", this._onKeyUp);
+ this._window.addEventListener("keypress", this._onKeyPress);
+ this._window.addEventListener("mousemove", this._onMouseMove);
+ this._window.addEventListener("mousedown", this._onMouseDown);
+ this._window.addEventListener("mouseup", this._onMouseUp);
+ this._window.addEventListener("MozMousePixelScroll", this._onMouseWheel);
+
+ let ownerWindow = this._parent.ownerDocument.defaultView;
+ ownerWindow.addEventListener("resize", this._onResize);
+
+ this._animationId =
+ this._window.requestAnimationFrame(this._onAnimationFrame);
+
+ this._ready.resolve(this);
+ this.emit("ready", this);
+ });
+}
+
+FlameGraph.prototype = {
+ /**
+ * Read-only width and height of the canvas.
+ * @return number
+ */
+ get width() {
+ return this._width;
+ },
+ get height() {
+ return this._height;
+ },
+
+ /**
+ * Returns a promise resolved once this graph is ready to receive data.
+ */
+ ready: function () {
+ return this._ready.promise;
+ },
+
+ /**
+ * Destroys this graph.
+ */
+ destroy: Task.async(function* () {
+ yield this.ready();
+
+ this._window.removeEventListener("keydown", this._onKeyDown);
+ this._window.removeEventListener("keyup", this._onKeyUp);
+ this._window.removeEventListener("keypress", this._onKeyPress);
+ this._window.removeEventListener("mousemove", this._onMouseMove);
+ this._window.removeEventListener("mousedown", this._onMouseDown);
+ this._window.removeEventListener("mouseup", this._onMouseUp);
+ this._window.removeEventListener("MozMousePixelScroll", this._onMouseWheel);
+
+ let ownerWindow = this._parent.ownerDocument.defaultView;
+ if (ownerWindow) {
+ ownerWindow.removeEventListener("resize", this._onResize);
+ }
+
+ this._window.cancelAnimationFrame(this._animationId);
+ this._iframe.remove();
+
+ this._bounds = null;
+ this._selection = null;
+ this._selectionDragger = null;
+ this._verticalOffset = null;
+ this._verticalOffsetDragger = null;
+ this._keyboardZoomAccelerationFactor = null;
+ this._keyboardPanAccelerationFactor = null;
+ this._textWidthsCache = null;
+
+ this._data = null;
+
+ this.emit("destroyed");
+ }),
+
+ /**
+ * Makes sure the canvas graph is of the specified width or height, and
+ * doesn't flex to fit all the available space.
+ */
+ fixedWidth: null,
+ fixedHeight: null,
+
+ /**
+ * How much preliminar drag is necessary to determine the panning direction.
+ */
+ horizontalPanThreshold: GRAPH_HORIZONTAL_PAN_THRESHOLD,
+ verticalPanThreshold: GRAPH_VERTICAL_PAN_THRESHOLD,
+
+ /**
+ * The units used in the overhead ticks. Could be "ms", for example.
+ * Overwrite this with your own localized format.
+ */
+ timelineTickUnits: "",
+
+ /**
+ * Character used when a block's text is overflowing.
+ * Defaults to an ellipsis.
+ */
+ overflowChar: ELLIPSIS,
+
+ /**
+ * Sets the data source for this graph.
+ *
+ * @param object data
+ * An object containing the following properties:
+ * - data: the data source; see the constructor for more info
+ * - bounds: the minimum/maximum { start, end }, in ms or px
+ * - visible: optional, the shown { start, end }, in ms or px
+ */
+ setData: function ({ data, bounds, visible }) {
+ this._data = data;
+ this.setOuterBounds(bounds);
+ this.setViewRange(visible || bounds);
+ },
+
+ /**
+ * Same as `setData`, but waits for this graph to finish initializing first.
+ *
+ * @param object data
+ * The data source. See the constructor for more information.
+ * @return promise
+ * A promise resolved once the data is set.
+ */
+ setDataWhenReady: Task.async(function* (data) {
+ yield this.ready();
+ this.setData(data);
+ }),
+
+ /**
+ * Gets whether or not this graph has a data source.
+ * @return boolean
+ */
+ hasData: function () {
+ return !!this._data;
+ },
+
+ /**
+ * Sets the maximum selection (i.e. the 'graph bounds').
+ * @param object { start, end }
+ */
+ setOuterBounds: function ({ startTime, endTime }) {
+ this._bounds.start = startTime * this._pixelRatio;
+ this._bounds.end = endTime * this._pixelRatio;
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Sets the selection and vertical offset (i.e. the 'view range').
+ * @return number
+ */
+ setViewRange: function ({ startTime, endTime }, verticalOffset = 0) {
+ this._selection.start = startTime * this._pixelRatio;
+ this._selection.end = endTime * this._pixelRatio;
+ this._verticalOffset = verticalOffset * this._pixelRatio;
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Gets the maximum selection (i.e. the 'graph bounds').
+ * @return number
+ */
+ getOuterBounds: function () {
+ return {
+ startTime: this._bounds.start / this._pixelRatio,
+ endTime: this._bounds.end / this._pixelRatio
+ };
+ },
+
+ /**
+ * Gets the current selection and vertical offset (i.e. the 'view range').
+ * @return number
+ */
+ getViewRange: function () {
+ return {
+ startTime: this._selection.start / this._pixelRatio,
+ endTime: this._selection.end / this._pixelRatio,
+ verticalOffset: this._verticalOffset / this._pixelRatio
+ };
+ },
+
+ /**
+ * Focuses this graph's iframe window.
+ */
+ focus: function () {
+ this._window.focus();
+ },
+
+ /**
+ * Updates this graph to reflect the new dimensions of the parent node.
+ *
+ * @param boolean options.force
+ * Force redraw everything.
+ */
+ refresh: function (options = {}) {
+ let bounds = this._parent.getBoundingClientRect();
+ let newWidth = this.fixedWidth || bounds.width;
+ let newHeight = this.fixedHeight || bounds.height;
+
+ // Prevent redrawing everything if the graph's width & height won't change,
+ // except if force=true.
+ if (!options.force &&
+ this._width == newWidth * this._pixelRatio &&
+ this._height == newHeight * this._pixelRatio) {
+ this.emit("refresh-cancelled");
+ return;
+ }
+
+ bounds.width = newWidth;
+ bounds.height = newHeight;
+ this._iframe.setAttribute("width", bounds.width);
+ this._iframe.setAttribute("height", bounds.height);
+ this._width = this._canvas.width = bounds.width * this._pixelRatio;
+ this._height = this._canvas.height = bounds.height * this._pixelRatio;
+
+ this._shouldRedraw = true;
+ this.emit("refresh");
+ },
+
+ /**
+ * Sets the theme via `theme` to either "light" or "dark",
+ * and updates the internal styling to match. Requires a redraw
+ * to see the effects.
+ */
+ setTheme: function (theme) {
+ theme = theme || "light";
+ this.overviewHeaderBackgroundColor = getColor("body-background", theme);
+ this.overviewHeaderTextColor = getColor("body-color", theme);
+ // Hard to get a color that is readable across both themes for the text
+ // on the flames
+ this.blockTextColor = getColor(theme === "dark" ? "selection-color"
+ : "body-color", theme);
+ },
+
+ /**
+ * The contents of this graph are redrawn only when something changed,
+ * like the data source, or the selection bounds etc. This flag tracks
+ * if the rendering is "dirty" and needs to be refreshed.
+ */
+ _shouldRedraw: false,
+
+ /**
+ * Animation frame callback, invoked on each tick of the refresh driver.
+ */
+ _onAnimationFrame: function () {
+ this._animationId =
+ this._window.requestAnimationFrame(this._onAnimationFrame);
+ this._drawWidget();
+ },
+
+ /**
+ * Redraws the widget when necessary. The actual graph is not refreshed
+ * every time this function is called, only the cliphead, selection etc.
+ */
+ _drawWidget: function () {
+ if (!this._shouldRedraw) {
+ return;
+ }
+
+ // Unlike mouse events which are updated as needed in their own respective
+ // handlers, keyboard events are granular and non-continuous (not even
+ // "keydown", which is fired with a low frequency). Therefore, to maintain
+ // animation smoothness, update anything that's controllable via the
+ // keyboard here, in the animation loop, before any actual drawing.
+ this._keyboardUpdateLoop();
+
+ let ctx = this._ctx;
+ let canvasWidth = this._width;
+ let canvasHeight = this._height;
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+
+ let selection = this._selection;
+ let selectionWidth = selection.end - selection.start;
+ let selectionScale = canvasWidth / selectionWidth;
+ this._drawTicks(selection.start, selectionScale);
+ this._drawPyramid(this._data, this._verticalOffset,
+ selection.start, selectionScale);
+ this._drawHeader(selection.start, selectionScale);
+
+ // If the user isn't doing anything anymore, it's safe to stop drawing.
+ // XXX: This doesn't handle cases where we should still be drawing even
+ // if any input stops (e.g. smooth panning transitions after the user
+ // finishes input). We don't care about that right now.
+ if (this._userInputStack == 0) {
+ this._shouldRedraw = false;
+ return;
+ }
+ if (this._userInputStack < 0) {
+ throw new Error("The user went back in time from a pyramid.");
+ }
+ },
+
+ /**
+ * Performs any necessary changes to the graph's state based on the
+ * user's input on a keyboard.
+ */
+ _keyboardUpdateLoop: function () {
+ const KEY_CODE_UP = 38;
+ const KEY_CODE_DOWN = 40;
+ const KEY_CODE_LEFT = 37;
+ const KEY_CODE_RIGHT = 39;
+ const KEY_CODE_W = 87;
+ const KEY_CODE_A = 65;
+ const KEY_CODE_S = 83;
+ const KEY_CODE_D = 68;
+
+ let canvasWidth = this._width;
+ let pressed = this._keysPressed;
+
+ let selection = this._selection;
+ let selectionWidth = selection.end - selection.start;
+ let selectionScale = canvasWidth / selectionWidth;
+
+ let translation = [0, 0];
+ let isZooming = false;
+ let isPanning = false;
+
+ if (pressed[KEY_CODE_UP] || pressed[KEY_CODE_W]) {
+ translation[0] += GRAPH_KEYBOARD_ZOOM_SENSITIVITY / selectionScale;
+ translation[1] -= GRAPH_KEYBOARD_ZOOM_SENSITIVITY / selectionScale;
+ isZooming = true;
+ }
+ if (pressed[KEY_CODE_DOWN] || pressed[KEY_CODE_S]) {
+ translation[0] -= GRAPH_KEYBOARD_ZOOM_SENSITIVITY / selectionScale;
+ translation[1] += GRAPH_KEYBOARD_ZOOM_SENSITIVITY / selectionScale;
+ isZooming = true;
+ }
+ if (pressed[KEY_CODE_LEFT] || pressed[KEY_CODE_A]) {
+ translation[0] -= GRAPH_KEYBOARD_PAN_SENSITIVITY / selectionScale;
+ translation[1] -= GRAPH_KEYBOARD_PAN_SENSITIVITY / selectionScale;
+ isPanning = true;
+ }
+ if (pressed[KEY_CODE_RIGHT] || pressed[KEY_CODE_D]) {
+ translation[0] += GRAPH_KEYBOARD_PAN_SENSITIVITY / selectionScale;
+ translation[1] += GRAPH_KEYBOARD_PAN_SENSITIVITY / selectionScale;
+ isPanning = true;
+ }
+
+ if (isPanning) {
+ // Accelerate the left/right selection panning continuously
+ // while the pan keys are pressed.
+ this._keyboardPanAccelerationFactor *= GRAPH_KEYBOARD_ACCELERATION;
+ translation[0] *= this._keyboardPanAccelerationFactor;
+ translation[1] *= this._keyboardPanAccelerationFactor;
+ } else {
+ this._keyboardPanAccelerationFactor = 1;
+ }
+
+ if (isZooming) {
+ // Accelerate the in/out selection zooming continuously
+ // while the zoom keys are pressed.
+ this._keyboardZoomAccelerationFactor *= GRAPH_KEYBOARD_ACCELERATION;
+ translation[0] *= this._keyboardZoomAccelerationFactor;
+ translation[1] *= this._keyboardZoomAccelerationFactor;
+ } else {
+ this._keyboardZoomAccelerationFactor = 1;
+ }
+
+ if (translation[0] != 0 || translation[1] != 0) {
+ // Make sure the panning translation speed doesn't end up
+ // being too high.
+ let maxTranslation = GRAPH_KEYBOARD_TRANSLATION_MAX / selectionScale;
+ if (Math.abs(translation[0]) > maxTranslation) {
+ translation[0] = Math.sign(translation[0]) * maxTranslation;
+ }
+ if (Math.abs(translation[1]) > maxTranslation) {
+ translation[1] = Math.sign(translation[1]) * maxTranslation;
+ }
+ this._selection.start += translation[0];
+ this._selection.end += translation[1];
+ this._normalizeSelectionBounds();
+ this.emit("selecting");
+ }
+ },
+
+ /**
+ * Draws the overhead header, with time markers and ticks in this graph.
+ *
+ * @param number dataOffset, dataScale
+ * Offsets and scales the data source by the specified amount.
+ * This is used for scrolling the visualization.
+ */
+ _drawHeader: function (dataOffset, dataScale) {
+ let ctx = this._ctx;
+ let canvasWidth = this._width;
+ let headerHeight = OVERVIEW_HEADER_HEIGHT * this._pixelRatio;
+
+ ctx.fillStyle = this.overviewHeaderBackgroundColor;
+ ctx.fillRect(0, 0, canvasWidth, headerHeight);
+
+ this._drawTicks(dataOffset, dataScale, {
+ from: 0,
+ to: headerHeight,
+ renderText: true
+ });
+ },
+
+ /**
+ * Draws the overhead ticks in this graph in the flame graph area.
+ *
+ * @param number dataOffset, dataScale, from, to, renderText
+ * Offsets and scales the data source by the specified amount.
+ * from and to determine the Y position of how far the stroke
+ * should be drawn.
+ * This is used when scrolling the visualization.
+ */
+ _drawTicks: function (dataOffset, dataScale, options) {
+ let { from, to, renderText } = options || {};
+ let ctx = this._ctx;
+ let canvasWidth = this._width;
+ let canvasHeight = this._height;
+ let scaledOffset = dataOffset * dataScale;
+
+ let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
+ let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
+ let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
+ let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
+ let tickInterval = this._findOptimalTickInterval(dataScale);
+
+ ctx.textBaseline = "top";
+ ctx.font = fontSize + "px " + fontFamily;
+ ctx.fillStyle = this.overviewHeaderTextColor;
+ ctx.strokeStyle = OVERVIEW_HEADER_TIMELINE_STROKE_COLOR;
+ ctx.beginPath();
+
+ for (let x = -scaledOffset % tickInterval; x < canvasWidth;
+ x += tickInterval) {
+ let lineLeft = x;
+ let textLeft = lineLeft + textPaddingLeft;
+ let time = Math.round((x / dataScale + dataOffset) / this._pixelRatio);
+ let label = time + " " + this.timelineTickUnits;
+ if (renderText) {
+ ctx.fillText(label, textLeft, textPaddingTop);
+ }
+ ctx.moveTo(lineLeft, from || 0);
+ ctx.lineTo(lineLeft, to || canvasHeight);
+ }
+
+ ctx.stroke();
+ },
+
+ /**
+ * Draws the blocks and text in this graph.
+ *
+ * @param object dataSource
+ * The data source. See the constructor for more information.
+ * @param number verticalOffset
+ * Offsets the drawing vertically by the specified amount.
+ * @param number dataOffset, dataScale
+ * Offsets and scales the data source by the specified amount.
+ * This is used for scrolling the visualization.
+ */
+ _drawPyramid: function (dataSource, verticalOffset, dataOffset, dataScale) {
+ let ctx = this._ctx;
+
+ let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio;
+ let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
+ let visibleBlocksInfo = this._drawPyramidFill(dataSource, verticalOffset,
+ dataOffset, dataScale);
+
+ ctx.textBaseline = "middle";
+ ctx.font = fontSize + "px " + fontFamily;
+ ctx.fillStyle = this.blockTextColor;
+
+ this._drawPyramidText(visibleBlocksInfo, verticalOffset,
+ dataOffset, dataScale);
+ },
+
+ /**
+ * Fills all block inside this graph's pyramid.
+ * @see FlameGraph.prototype._drawPyramid
+ */
+ _drawPyramidFill: function (dataSource, verticalOffset, dataOffset,
+ dataScale) {
+ let visibleBlocksInfoStore = [];
+ let minVisibleBlockWidth = this._overflowCharWidth;
+
+ for (let { color, blocks } of dataSource) {
+ this._drawBlocksFill(
+ color, blocks, verticalOffset, dataOffset, dataScale,
+ visibleBlocksInfoStore, minVisibleBlockWidth);
+ }
+
+ return visibleBlocksInfoStore;
+ },
+
+ /**
+ * Adds the text for all block inside this graph's pyramid.
+ * @see FlameGraph.prototype._drawPyramid
+ */
+ _drawPyramidText: function (blocksInfo, verticalOffset, dataOffset,
+ dataScale) {
+ for (let { block, rect } of blocksInfo) {
+ this._drawBlockText(block, rect, verticalOffset, dataOffset, dataScale);
+ }
+ },
+
+ /**
+ * Fills a group of blocks sharing the same style.
+ *
+ * @param string color
+ * The color used as the block's background.
+ * @param array blocks
+ * A list of { x, y, width, height } objects visually representing
+ * all the blocks sharing this particular style.
+ * @param number verticalOffset
+ * Offsets the drawing vertically by the specified amount.
+ * @param number dataOffset, dataScale
+ * Offsets and scales the data source by the specified amount.
+ * This is used for scrolling the visualization.
+ * @param array visibleBlocksInfoStore
+ * An array to store all the visible blocks into, along with the
+ * final baked coordinates and dimensions, after drawing them.
+ * The provided array will be populated.
+ * @param number minVisibleBlockWidth
+ * The minimum width of the blocks that will be added into
+ * the `visibleBlocksInfoStore`.
+ */
+ _drawBlocksFill: function (
+ color, blocks, verticalOffset, dataOffset, dataScale,
+ visibleBlocksInfoStore, minVisibleBlockWidth) {
+ let ctx = this._ctx;
+ let canvasWidth = this._width;
+ let canvasHeight = this._height;
+ let scaledOffset = dataOffset * dataScale;
+
+ ctx.fillStyle = color;
+ ctx.beginPath();
+
+ for (let block of blocks) {
+ let { x, y, width, height } = block;
+ let rectLeft = x * this._pixelRatio * dataScale - scaledOffset;
+ let rectTop = (y - verticalOffset + OVERVIEW_HEADER_HEIGHT)
+ * this._pixelRatio;
+ let rectWidth = width * this._pixelRatio * dataScale;
+ let rectHeight = height * this._pixelRatio;
+
+ // Too far respectively right/left/bottom/top
+ if (rectLeft > canvasWidth ||
+ rectLeft < -rectWidth ||
+ rectTop > canvasHeight ||
+ rectTop < -rectHeight) {
+ continue;
+ }
+
+ // Clamp the blocks position to start at 0. Avoid negative X coords,
+ // to properly place the text inside the blocks.
+ if (rectLeft < 0) {
+ rectWidth += rectLeft;
+ rectLeft = 0;
+ }
+
+ // Avoid drawing blocks that are too narrow.
+ if (rectWidth <= FLAME_GRAPH_BLOCK_BORDER ||
+ rectHeight <= FLAME_GRAPH_BLOCK_BORDER) {
+ continue;
+ }
+
+ ctx.rect(
+ rectLeft, rectTop,
+ rectWidth - FLAME_GRAPH_BLOCK_BORDER,
+ rectHeight - FLAME_GRAPH_BLOCK_BORDER);
+
+ // Populate the visible blocks store with this block if the width
+ // is longer than a given threshold.
+ if (rectWidth > minVisibleBlockWidth) {
+ visibleBlocksInfoStore.push({
+ block: block,
+ rect: { rectLeft, rectTop, rectWidth, rectHeight }
+ });
+ }
+ }
+
+ ctx.fill();
+ },
+
+ /**
+ * Adds text for a single block.
+ *
+ * @param object block
+ * A single { x, y, width, height, text } object visually representing
+ * the block containing the text.
+ * @param object rect
+ * A single { rectLeft, rectTop, rectWidth, rectHeight } object
+ * representing the final baked coordinates of the drawn rectangle.
+ * Think of them as screen-space values, vs. object-space values. These
+ * differ from the scalars in `block` when the graph is scaled/panned.
+ * @param number verticalOffset
+ * Offsets the drawing vertically by the specified amount.
+ * @param number dataOffset, dataScale
+ * Offsets and scales the data source by the specified amount.
+ * This is used for scrolling the visualization.
+ */
+ _drawBlockText: function (block, rect, verticalOffset, dataOffset,
+ dataScale) {
+ let ctx = this._ctx;
+
+ let { text } = block;
+ let { rectLeft, rectTop, rectWidth, rectHeight } = rect;
+
+ let paddingTop = FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP * this._pixelRatio;
+ let paddingLeft = FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT * this._pixelRatio;
+ let paddingRight = FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT * this._pixelRatio;
+ let totalHorizontalPadding = paddingLeft + paddingRight;
+
+ // Clamp the blocks position to start at 0. Avoid negative X coords,
+ // to properly place the text inside the blocks.
+ if (rectLeft < 0) {
+ rectWidth += rectLeft;
+ rectLeft = 0;
+ }
+
+ let textLeft = rectLeft + paddingLeft;
+ let textTop = rectTop + rectHeight / 2 + paddingTop;
+ let textAvailableWidth = rectWidth - totalHorizontalPadding;
+
+ // Massage the text to fit inside a given width. This clamps the string
+ // at the end to avoid overflowing.
+ let fittedText = this._getFittedText(text, textAvailableWidth);
+ if (fittedText.length < 1) {
+ return;
+ }
+
+ ctx.fillText(fittedText, textLeft, textTop);
+ },
+
+ /**
+ * Calculating text widths is necessary to trim the text inside the blocks
+ * while the scaling changes (e.g. via scrolling). This is very expensive,
+ * so maintain a cache of string contents to text widths.
+ */
+ _textWidthsCache: null,
+ _overflowCharWidth: null,
+ _averageCharWidth: null,
+
+ /**
+ * Gets the width of the specified text, for the current context state
+ * (font size, family etc.).
+ *
+ * @param string text
+ * The text to analyze.
+ * @return number
+ * The text width.
+ */
+ _getTextWidth: function (text) {
+ let cachedWidth = this._textWidthsCache[text];
+ if (cachedWidth) {
+ return cachedWidth;
+ }
+ let metrics = this._ctx.measureText(text);
+ return (this._textWidthsCache[text] = metrics.width);
+ },
+
+ /**
+ * Gets an approximate width of the specified text. This is much faster
+ * than `_getTextWidth`, but inexact.
+ *
+ * @param string text
+ * The text to analyze.
+ * @return number
+ * The approximate text width.
+ */
+ _getTextWidthApprox: function (text) {
+ return text.length * this._averageCharWidth;
+ },
+
+ /**
+ * Gets the average letter width in the English alphabet, for the current
+ * context state (font size, family etc.). This provides a close enough
+ * value to use in `_getTextWidthApprox`.
+ *
+ * @return number
+ * The average letter width.
+ */
+ _calcAverageCharWidth: function () {
+ let letterWidthsSum = 0;
+ // space
+ let start = 32;
+ // "z"
+ let end = 123;
+
+ for (let i = start; i < end; i++) {
+ let char = String.fromCharCode(i);
+ letterWidthsSum += this._getTextWidth(char);
+ }
+
+ return letterWidthsSum / (end - start);
+ },
+
+ /**
+ * Massage a text to fit inside a given width. This clamps the string
+ * at the end to avoid overflowing.
+ *
+ * @param string text
+ * The text to fit inside the given width.
+ * @param number maxWidth
+ * The available width for the given text.
+ * @return string
+ * The fitted text.
+ */
+ _getFittedText: function (text, maxWidth) {
+ let textWidth = this._getTextWidth(text);
+ if (textWidth < maxWidth) {
+ return text;
+ }
+ if (this._overflowCharWidth > maxWidth) {
+ return "";
+ }
+ for (let i = 1, len = text.length; i <= len; i++) {
+ let trimmedText = text.substring(0, len - i);
+ let trimmedWidth = this._getTextWidthApprox(trimmedText)
+ + this._overflowCharWidth;
+ if (trimmedWidth < maxWidth) {
+ return trimmedText + this.overflowChar;
+ }
+ }
+ return "";
+ },
+
+ /**
+ * Listener for the "keydown" event on the graph's container.
+ */
+ _onKeyDown: function (e) {
+ ViewHelpers.preventScrolling(e);
+
+ const hasModifier = e.ctrlKey || e.shiftKey || e.altKey || e.metaKey;
+
+ if (!hasModifier && !this._keysPressed[e.keyCode]) {
+ this._keysPressed[e.keyCode] = true;
+ this._userInputStack++;
+ this._shouldRedraw = true;
+ }
+ },
+
+ /**
+ * Listener for the "keyup" event on the graph's container.
+ */
+ _onKeyUp: function (e) {
+ ViewHelpers.preventScrolling(e);
+
+ if (this._keysPressed[e.keyCode]) {
+ this._keysPressed[e.keyCode] = false;
+ this._userInputStack--;
+ this._shouldRedraw = true;
+ }
+ },
+
+ /**
+ * Listener for the "keypress" event on the graph's container.
+ */
+ _onKeyPress: function (e) {
+ ViewHelpers.preventScrolling(e);
+ },
+
+ /**
+ * Listener for the "mousemove" event on the graph's container.
+ */
+ _onMouseMove: function (e) {
+ let {mouseX, mouseY} = this._getRelativeEventCoordinates(e);
+
+ let canvasWidth = this._width;
+
+ let selection = this._selection;
+ let selectionWidth = selection.end - selection.start;
+ let selectionScale = canvasWidth / selectionWidth;
+
+ let horizDrag = this._selectionDragger;
+ let vertDrag = this._verticalOffsetDragger;
+
+ // Avoid dragging both horizontally and vertically at the same time,
+ // as this doesn't feel natural. Based on a minimum distance, enable either
+ // one, and remember the drag direction to offset the mouse coords later.
+ if (!this._horizontalDragEnabled && !this._verticalDragEnabled) {
+ let horizDiff = Math.abs(horizDrag.origin - mouseX);
+ if (horizDiff > this.horizontalPanThreshold) {
+ this._horizontalDragDirection = Math.sign(horizDrag.origin - mouseX);
+ this._horizontalDragEnabled = true;
+ }
+ let vertDiff = Math.abs(vertDrag.origin - mouseY);
+ if (vertDiff > this.verticalPanThreshold) {
+ this._verticalDragDirection = Math.sign(vertDrag.origin - mouseY);
+ this._verticalDragEnabled = true;
+ }
+ }
+
+ if (horizDrag.origin != null && this._horizontalDragEnabled) {
+ let relativeX = mouseX + this._horizontalDragDirection *
+ this.horizontalPanThreshold;
+ selection.start = horizDrag.anchor.start +
+ (horizDrag.origin - relativeX) / selectionScale;
+ selection.end = horizDrag.anchor.end +
+ (horizDrag.origin - relativeX) / selectionScale;
+ this._normalizeSelectionBounds();
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ }
+
+ if (vertDrag.origin != null && this._verticalDragEnabled) {
+ let relativeY = mouseY +
+ this._verticalDragDirection * this.verticalPanThreshold;
+ this._verticalOffset = vertDrag.anchor +
+ (vertDrag.origin - relativeY) / this._pixelRatio;
+ this._normalizeVerticalOffset();
+ this._shouldRedraw = true;
+ this.emit("panning-vertically");
+ }
+ },
+
+ /**
+ * Listener for the "mousedown" event on the graph's container.
+ */
+ _onMouseDown: function (e) {
+ let {mouseX, mouseY} = this._getRelativeEventCoordinates(e);
+
+ this._selectionDragger.origin = mouseX;
+ this._selectionDragger.anchor.start = this._selection.start;
+ this._selectionDragger.anchor.end = this._selection.end;
+
+ this._verticalOffsetDragger.origin = mouseY;
+ this._verticalOffsetDragger.anchor = this._verticalOffset;
+
+ this._horizontalDragEnabled = false;
+ this._verticalDragEnabled = false;
+
+ this._canvas.setAttribute("input", "adjusting-view-area");
+ },
+
+ /**
+ * Listener for the "mouseup" event on the graph's container.
+ */
+ _onMouseUp: function () {
+ this._selectionDragger.origin = null;
+ this._verticalOffsetDragger.origin = null;
+ this._horizontalDragEnabled = false;
+ this._horizontalDragDirection = 0;
+ this._verticalDragEnabled = false;
+ this._verticalDragDirection = 0;
+ this._canvas.removeAttribute("input");
+ },
+
+ /**
+ * Listener for the "wheel" event on the graph's container.
+ */
+ _onMouseWheel: function (e) {
+ let {mouseX} = this._getRelativeEventCoordinates(e);
+
+ let canvasWidth = this._width;
+
+ let selection = this._selection;
+ let selectionWidth = selection.end - selection.start;
+ let selectionScale = canvasWidth / selectionWidth;
+
+ switch (e.axis) {
+ case e.VERTICAL_AXIS: {
+ let distFromStart = mouseX;
+ let distFromEnd = canvasWidth - mouseX;
+ let vector = e.detail * GRAPH_WHEEL_ZOOM_SENSITIVITY / selectionScale;
+ selection.start -= distFromStart * vector;
+ selection.end += distFromEnd * vector;
+ break;
+ }
+ case e.HORIZONTAL_AXIS: {
+ let vector = e.detail * GRAPH_WHEEL_SCROLL_SENSITIVITY / selectionScale;
+ selection.start += vector;
+ selection.end += vector;
+ break;
+ }
+ }
+
+ this._normalizeSelectionBounds();
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ },
+
+ /**
+ * Makes sure the start and end points of the current selection
+ * are withing the graph's visible bounds, and that they form a selection
+ * wider than the allowed minimum width.
+ */
+ _normalizeSelectionBounds: function () {
+ let boundsStart = this._bounds.start;
+ let boundsEnd = this._bounds.end;
+ let selectionStart = this._selection.start;
+ let selectionEnd = this._selection.end;
+
+ if (selectionStart < boundsStart) {
+ selectionStart = boundsStart;
+ }
+ if (selectionEnd < boundsStart) {
+ selectionStart = boundsStart;
+ selectionEnd = GRAPH_MIN_SELECTION_WIDTH;
+ }
+ if (selectionEnd > boundsEnd) {
+ selectionEnd = boundsEnd;
+ }
+ if (selectionStart > boundsEnd) {
+ selectionEnd = boundsEnd;
+ selectionStart = boundsEnd - GRAPH_MIN_SELECTION_WIDTH;
+ }
+ if (selectionEnd - selectionStart < GRAPH_MIN_SELECTION_WIDTH) {
+ let midPoint = (selectionStart + selectionEnd) / 2;
+ selectionStart = midPoint - GRAPH_MIN_SELECTION_WIDTH / 2;
+ selectionEnd = midPoint + GRAPH_MIN_SELECTION_WIDTH / 2;
+ }
+
+ this._selection.start = selectionStart;
+ this._selection.end = selectionEnd;
+ },
+
+ /**
+ * Makes sure that the current vertical offset is within the allowed
+ * panning range.
+ */
+ _normalizeVerticalOffset: function () {
+ this._verticalOffset = Math.max(this._verticalOffset, 0);
+ },
+
+ /**
+ *
+ * Finds the optimal tick interval between time markers in this graph.
+ *
+ * @param number dataScale
+ * @return number
+ */
+ _findOptimalTickInterval: function (dataScale) {
+ let timingStep = TIMELINE_TICKS_MULTIPLE;
+ let spacingMin = TIMELINE_TICKS_SPACING_MIN * this._pixelRatio;
+ let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
+ let numIters = 0;
+
+ if (dataScale > spacingMin) {
+ return dataScale;
+ }
+
+ while (true) {
+ let scaledStep = dataScale * timingStep;
+ if (++numIters > maxIters) {
+ return scaledStep;
+ }
+ if (scaledStep < spacingMin) {
+ timingStep <<= 1;
+ continue;
+ }
+ return scaledStep;
+ }
+ },
+
+ /**
+ * Gets the offset of this graph's container relative to the owner window.
+ *
+ * @return object
+ * The { left, top } offset.
+ */
+ _getContainerOffset: function () {
+ let node = this._canvas;
+ let x = 0;
+ let y = 0;
+
+ while ((node = node.offsetParent)) {
+ x += node.offsetLeft;
+ y += node.offsetTop;
+ }
+
+ return { left: x, top: y };
+ },
+
+ /**
+ * Given a MouseEvent, make it relative to this._canvas.
+ * @return object {mouseX,mouseY}
+ */
+ _getRelativeEventCoordinates: function (e) {
+ // For ease of testing, testX and testY can be passed in as the event
+ // object.
+ if ("testX" in e && "testY" in e) {
+ return {
+ mouseX: e.testX * this._pixelRatio,
+ mouseY: e.testY * this._pixelRatio
+ };
+ }
+
+ let offset = this._getContainerOffset();
+ let mouseX = (e.clientX - offset.left) * this._pixelRatio;
+ let mouseY = (e.clientY - offset.top) * this._pixelRatio;
+
+ return {mouseX, mouseY};
+ },
+
+ /**
+ * Listener for the "resize" event on the graph's parent node.
+ */
+ _onResize: function () {
+ if (this.hasData()) {
+ setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
+ }
+ }
+};
+
+/**
+ * A collection of utility functions converting various data sources
+ * into a format drawable by the FlameGraph.
+ */
+var FlameGraphUtils = {
+ _cache: new WeakMap(),
+
+ /**
+ * Create data suitable for use with FlameGraph from a profile's samples.
+ * Iterate the profile's samples and keep a moving window of stack traces.
+ *
+ * @param object thread
+ * The raw thread object received from the backend.
+ * @param object options
+ * Additional supported options,
+ * - boolean contentOnly [optional]
+ * - boolean invertTree [optional]
+ * - boolean flattenRecursion [optional]
+ * - string showIdleBlocks [optional]
+ * @return object
+ * Data source usable by FlameGraph.
+ */
+ createFlameGraphDataFromThread: function (thread, options = {}, out = []) {
+ let cached = this._cache.get(thread);
+ if (cached) {
+ return cached;
+ }
+
+ // 1. Create a map of colors to arrays, representing buckets of
+ // blocks inside the flame graph pyramid sharing the same style.
+
+ let buckets = Array.from({ length: PALLETTE_SIZE }, () => []);
+
+ // 2. Populate the buckets by iterating over every frame in every sample.
+
+ let { samples, stackTable, frameTable, stringTable } = thread;
+
+ const SAMPLE_STACK_SLOT = samples.schema.stack;
+ const SAMPLE_TIME_SLOT = samples.schema.time;
+
+ const STACK_PREFIX_SLOT = stackTable.schema.prefix;
+ const STACK_FRAME_SLOT = stackTable.schema.frame;
+
+ const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame;
+
+ let inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
+ let labelCache = Object.create(null);
+
+ let samplesData = samples.data;
+ let stacksData = stackTable.data;
+
+ let flattenRecursion = options.flattenRecursion;
+
+ // Reused objects.
+ let mutableFrameKeyOptions = {
+ contentOnly: options.contentOnly,
+ isRoot: false,
+ isLeaf: false,
+ isMetaCategoryOut: false
+ };
+
+ // Take the timestamp of the first sample as prevTime. 0 is incorrect due
+ // to circular buffer wraparound. If wraparound happens, then the first
+ // sample will have an incorrect, large duration.
+ let prevTime = samplesData.length > 0 ? samplesData[0][SAMPLE_TIME_SLOT]
+ : 0;
+ let prevFrames = [];
+ let sampleFrames = [];
+ let sampleFrameKeys = [];
+
+ for (let i = 1; i < samplesData.length; i++) {
+ let sample = samplesData[i];
+ let time = sample[SAMPLE_TIME_SLOT];
+
+ let stackIndex = sample[SAMPLE_STACK_SLOT];
+ let prevFrameKey;
+
+ let stackDepth = 0;
+
+ // Inflate the stack and keep a moving window of call stacks.
+ //
+ // For reference, see the similar block comment in
+ // ThreadNode.prototype._buildInverted.
+ //
+ // In a similar fashion to _buildInverted, frames are inflated on the
+ // fly while stackwalking the stackTable trie. The exact same frame key
+ // is computed in both _buildInverted and here.
+ //
+ // Unlike _buildInverted, which builds a call tree directly, the flame
+ // graph inflates the stack into an array, as it maintains a moving
+ // window of stacks over time.
+ //
+ // Like _buildInverted, the various filtering functions are also inlined
+ // into stack inflation loop.
+ while (stackIndex !== null) {
+ let stackEntry = stacksData[stackIndex];
+ let frameIndex = stackEntry[STACK_FRAME_SLOT];
+
+ // Fetch the stack prefix (i.e. older frames) index.
+ stackIndex = stackEntry[STACK_PREFIX_SLOT];
+
+ // Inflate the frame.
+ let inflatedFrame = getOrAddInflatedFrame(inflatedFrameCache,
+ frameIndex, frameTable,
+ stringTable);
+
+ mutableFrameKeyOptions.isRoot = stackIndex === null;
+ mutableFrameKeyOptions.isLeaf = stackDepth === 0;
+ let frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
+
+ // If not skipping the frame, add it to the current level. The (root)
+ // node isn't useful for flame graphs.
+ if (frameKey !== "" && frameKey !== "(root)") {
+ // If the frame is a meta category, use the category label.
+ if (mutableFrameKeyOptions.isMetaCategoryOut) {
+ frameKey = CATEGORY_MAPPINGS[frameKey].label;
+ }
+
+ sampleFrames[stackDepth] = inflatedFrame;
+ sampleFrameKeys[stackDepth] = frameKey;
+
+ // If we shouldn't flatten the current frame into the previous one,
+ // increment the stack depth.
+ if (!flattenRecursion || frameKey !== prevFrameKey) {
+ stackDepth++;
+ }
+
+ prevFrameKey = frameKey;
+ }
+ }
+
+ // Uninvert frames in place if needed.
+ if (!options.invertTree) {
+ sampleFrames.length = stackDepth;
+ sampleFrames.reverse();
+ sampleFrameKeys.length = stackDepth;
+ sampleFrameKeys.reverse();
+ }
+
+ // If no frames are available, add a pseudo "idle" block in between.
+ let isIdleFrame = false;
+ if (options.showIdleBlocks && stackDepth === 0) {
+ sampleFrames[0] = null;
+ sampleFrameKeys[0] = options.showIdleBlocks;
+ stackDepth = 1;
+ isIdleFrame = true;
+ }
+
+ // Put each frame in a bucket.
+ for (let frameIndex = 0; frameIndex < stackDepth; frameIndex++) {
+ let key = sampleFrameKeys[frameIndex];
+ let prevFrame = prevFrames[frameIndex];
+
+ // Frames at the same location and the same depth will be reused.
+ // If there is a block already created, change its width.
+ if (prevFrame && prevFrame.frameKey === key) {
+ prevFrame.width = (time - prevFrame.startTime);
+ } else {
+ // Otherwise, create a new block for this frame at this depth,
+ // using a simple location based salt for picking a color.
+ let hash = this._getStringHash(key);
+ let bucket = buckets[hash % PALLETTE_SIZE];
+
+ let label;
+ if (isIdleFrame) {
+ label = key;
+ } else {
+ label = labelCache[key];
+ if (!label) {
+ label = labelCache[key] =
+ this._formatLabel(key, sampleFrames[frameIndex]);
+ }
+ }
+
+ bucket.push(prevFrames[frameIndex] = {
+ startTime: prevTime,
+ frameKey: key,
+ x: prevTime,
+ y: frameIndex * FLAME_GRAPH_BLOCK_HEIGHT,
+ width: time - prevTime,
+ height: FLAME_GRAPH_BLOCK_HEIGHT,
+ text: label
+ });
+ }
+ }
+
+ // Previous frames at stack depths greater than the current sample's
+ // maximum need to be nullified. It's nonsensical to reuse them.
+ prevFrames.length = stackDepth;
+ prevTime = time;
+ }
+
+ // 3. Convert the buckets into a data source usable by the FlameGraph.
+ // This is a simple conversion from a Map to an Array.
+
+ for (let i = 0; i < buckets.length; i++) {
+ out.push({ color: COLOR_PALLETTE[i], blocks: buckets[i] });
+ }
+
+ this._cache.set(thread, out);
+ return out;
+ },
+
+ /**
+ * Clears the cached flame graph data created for the given source.
+ * @param any source
+ */
+ removeFromCache: function (source) {
+ this._cache.delete(source);
+ },
+
+ /**
+ * Very dumb hashing of a string. Used to pick colors from a pallette.
+ *
+ * @param string input
+ * @return number
+ */
+ _getStringHash: function (input) {
+ const STRING_HASH_PRIME1 = 7;
+ const STRING_HASH_PRIME2 = 31;
+
+ let hash = STRING_HASH_PRIME1;
+
+ for (let i = 0, len = input.length; i < len; i++) {
+ hash *= STRING_HASH_PRIME2;
+ hash += input.charCodeAt(i);
+
+ if (hash > Number.MAX_SAFE_INTEGER / STRING_HASH_PRIME2) {
+ return hash;
+ }
+ }
+
+ return hash;
+ },
+
+ /**
+ * Takes a frame key and a frame, and returns a string that should be
+ * displayed in its flame block.
+ *
+ * @param string key
+ * @param object frame
+ * @return string
+ */
+ _formatLabel: function (key, frame) {
+ let { functionName, fileName, line } =
+ FrameUtils.parseLocation(key, frame.line);
+ let label = FrameUtils.shouldDemangle(functionName) ? demangle(functionName)
+ : functionName;
+
+ if (fileName) {
+ label += ` (${fileName}${line != null ? (":" + line) : ""})`;
+ }
+
+ return label;
+ }
+};
+
+exports.FlameGraph = FlameGraph;
+exports.FlameGraphUtils = FlameGraphUtils;
+exports.PALLETTE_SIZE = PALLETTE_SIZE;
+exports.FLAME_GRAPH_BLOCK_HEIGHT = FLAME_GRAPH_BLOCK_HEIGHT;
+exports.FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE;
+exports.FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
diff --git a/devtools/client/shared/widgets/Graphs.js b/devtools/client/shared/widgets/Graphs.js
new file mode 100644
index 000000000..485da2b1b
--- /dev/null
+++ b/devtools/client/shared/widgets/Graphs.js
@@ -0,0 +1,1424 @@
+/* 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 { Task } = require("devtools/shared/task");
+const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const { getCurrentZoom } = require("devtools/shared/layout/utils");
+
+loader.lazyRequireGetter(this, "defer", "devtools/shared/defer");
+loader.lazyRequireGetter(this, "EventEmitter",
+ "devtools/shared/event-emitter");
+
+loader.lazyImporter(this, "DevToolsWorker",
+ "resource://devtools/shared/worker/worker.js");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const GRAPH_SRC = "chrome://devtools/content/shared/widgets/graphs-frame.xhtml";
+const WORKER_URL =
+ "resource://devtools/client/shared/widgets/GraphsWorker.js";
+
+// Generic constants.
+
+// ms
+const GRAPH_RESIZE_EVENTS_DRAIN = 100;
+const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00075;
+const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.1;
+// px
+const GRAPH_WHEEL_MIN_SELECTION_WIDTH = 10;
+
+// px
+const GRAPH_SELECTION_BOUNDARY_HOVER_LINE_WIDTH = 4;
+const GRAPH_SELECTION_BOUNDARY_HOVER_THRESHOLD = 10;
+const GRAPH_MAX_SELECTION_LEFT_PADDING = 1;
+const GRAPH_MAX_SELECTION_RIGHT_PADDING = 1;
+
+// px
+const GRAPH_REGION_LINE_WIDTH = 1;
+const GRAPH_REGION_LINE_COLOR = "rgba(237,38,85,0.8)";
+
+// px
+const GRAPH_STRIPE_PATTERN_WIDTH = 16;
+const GRAPH_STRIPE_PATTERN_HEIGHT = 16;
+const GRAPH_STRIPE_PATTERN_LINE_WIDTH = 2;
+const GRAPH_STRIPE_PATTERN_LINE_SPACING = 4;
+
+/**
+ * Small data primitives for all graphs.
+ */
+this.GraphCursor = function () {
+ this.x = null;
+ this.y = null;
+};
+
+this.GraphArea = function () {
+ this.start = null;
+ this.end = null;
+};
+
+this.GraphAreaDragger = function (anchor = new GraphArea()) {
+ this.origin = null;
+ this.anchor = anchor;
+};
+
+this.GraphAreaResizer = function () {
+ this.margin = null;
+};
+
+/**
+ * Base class for all graphs using a canvas to render the data source. Handles
+ * frame creation, data source, selection bounds, cursor position, etc.
+ *
+ * Language:
+ * - The "data" represents the values used when building the graph.
+ * Its specific format is defined by the inheriting classes.
+ *
+ * - A "cursor" is the cliphead position across the X axis of the graph.
+ *
+ * - A "selection" is defined by a "start" and an "end" value and
+ * represents the selected bounds in the graph.
+ *
+ * - A "region" is a highlighted area in the graph, also defined by a
+ * "start" and an "end" value, but distinct from the "selection". It is
+ * simply used to highlight important regions in the data.
+ *
+ * Instances of this class are EventEmitters with the following events:
+ * - "ready": when the container iframe and canvas are created.
+ * - "selecting": when the selection is set or changed.
+ * - "deselecting": when the selection is dropped.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the graph.
+ * @param string name
+ * The graph type, used for setting the correct class names.
+ * Currently supported: "line-graph" only.
+ * @param number sharpness [optional]
+ * Defaults to the current device pixel ratio.
+ */
+this.AbstractCanvasGraph = function (parent, name, sharpness) {
+ EventEmitter.decorate(this);
+
+ this._parent = parent;
+ this._ready = defer();
+
+ this._uid = "canvas-graph-" + Date.now();
+ this._renderTargets = new Map();
+
+ AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => {
+ this._iframe = iframe;
+ this._window = iframe.contentWindow;
+ this._topWindow = this._window.top;
+ this._document = iframe.contentDocument;
+ this._pixelRatio = sharpness || this._window.devicePixelRatio;
+
+ let container =
+ this._container = this._document.getElementById("graph-container");
+ container.className = name + "-widget-container graph-widget-container";
+
+ let canvas = this._canvas = this._document.getElementById("graph-canvas");
+ canvas.className = name + "-widget-canvas graph-widget-canvas";
+
+ let bounds = parent.getBoundingClientRect();
+ bounds.width = this.fixedWidth || bounds.width;
+ bounds.height = this.fixedHeight || bounds.height;
+ iframe.setAttribute("width", bounds.width);
+ iframe.setAttribute("height", bounds.height);
+
+ this._width = canvas.width = bounds.width * this._pixelRatio;
+ this._height = canvas.height = bounds.height * this._pixelRatio;
+ this._ctx = canvas.getContext("2d");
+ this._ctx.imageSmoothingEnabled = false;
+
+ this._cursor = new GraphCursor();
+ this._selection = new GraphArea();
+ this._selectionDragger = new GraphAreaDragger();
+ this._selectionResizer = new GraphAreaResizer();
+ this._isMouseActive = false;
+
+ this._onAnimationFrame = this._onAnimationFrame.bind(this);
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this._onMouseUp = this._onMouseUp.bind(this);
+ this._onMouseWheel = this._onMouseWheel.bind(this);
+ this._onMouseOut = this._onMouseOut.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this.refresh = this.refresh.bind(this);
+
+ this._window.addEventListener("mousemove", this._onMouseMove);
+ this._window.addEventListener("mousedown", this._onMouseDown);
+ this._window.addEventListener("MozMousePixelScroll", this._onMouseWheel);
+ this._window.addEventListener("mouseout", this._onMouseOut);
+
+ let ownerWindow = this._parent.ownerDocument.defaultView;
+ ownerWindow.addEventListener("resize", this._onResize);
+
+ this._animationId =
+ this._window.requestAnimationFrame(this._onAnimationFrame);
+
+ this._ready.resolve(this);
+ this.emit("ready", this);
+ });
+};
+
+AbstractCanvasGraph.prototype = {
+ /**
+ * Read-only width and height of the canvas.
+ * @return number
+ */
+ get width() {
+ return this._width;
+ },
+ get height() {
+ return this._height;
+ },
+
+ /**
+ * Return true if the mouse is actively messing with the selection, false
+ * otherwise.
+ */
+ get isMouseActive() {
+ return this._isMouseActive;
+ },
+
+ /**
+ * Returns a promise resolved once this graph is ready to receive data.
+ */
+ ready: function () {
+ return this._ready.promise;
+ },
+
+ /**
+ * Destroys this graph.
+ */
+ destroy: Task.async(function* () {
+ yield this.ready();
+
+ this._topWindow.removeEventListener("mousemove", this._onMouseMove);
+ this._topWindow.removeEventListener("mouseup", this._onMouseUp);
+ this._window.removeEventListener("mousemove", this._onMouseMove);
+ this._window.removeEventListener("mousedown", this._onMouseDown);
+ this._window.removeEventListener("MozMousePixelScroll", this._onMouseWheel);
+ this._window.removeEventListener("mouseout", this._onMouseOut);
+
+ let ownerWindow = this._parent.ownerDocument.defaultView;
+ if (ownerWindow) {
+ ownerWindow.removeEventListener("resize", this._onResize);
+ }
+
+ this._window.cancelAnimationFrame(this._animationId);
+ this._iframe.remove();
+
+ this._cursor = null;
+ this._selection = null;
+ this._selectionDragger = null;
+ this._selectionResizer = null;
+
+ this._data = null;
+ this._mask = null;
+ this._maskArgs = null;
+ this._regions = null;
+
+ this._cachedBackgroundImage = null;
+ this._cachedGraphImage = null;
+ this._cachedMaskImage = null;
+ this._renderTargets.clear();
+ gCachedStripePattern.clear();
+
+ this.emit("destroyed");
+ }),
+
+ /**
+ * Rendering options. Subclasses should override these.
+ */
+ clipheadLineWidth: 1,
+ clipheadLineColor: "transparent",
+ selectionLineWidth: 1,
+ selectionLineColor: "transparent",
+ selectionBackgroundColor: "transparent",
+ selectionStripesColor: "transparent",
+ regionBackgroundColor: "transparent",
+ regionStripesColor: "transparent",
+
+ /**
+ * Makes sure the canvas graph is of the specified width or height, and
+ * doesn't flex to fit all the available space.
+ */
+ fixedWidth: null,
+ fixedHeight: null,
+
+ /**
+ * Optionally builds and caches a background image for this graph.
+ * Inheriting classes may override this method.
+ */
+ buildBackgroundImage: function () {
+ return null;
+ },
+
+ /**
+ * Builds and caches a graph image, based on the data source supplied
+ * in `setData`. The graph image is not rebuilt on each frame, but
+ * only when the data source changes.
+ */
+ buildGraphImage: function () {
+ let error = "This method needs to be implemented by inheriting classes.";
+ throw new Error(error);
+ },
+
+ /**
+ * Optionally builds and caches a mask image for this graph, composited
+ * over the data image created via `buildGraphImage`. Inheriting classes
+ * may override this method.
+ */
+ buildMaskImage: function () {
+ return null;
+ },
+
+ /**
+ * When setting the data source, the coordinates and values may be
+ * stretched or squeezed on the X/Y axis, to fit into the available space.
+ */
+ dataScaleX: 1,
+ dataScaleY: 1,
+
+ /**
+ * Sets the data source for this graph.
+ *
+ * @param object data
+ * The data source. The actual format is specified by subclasses.
+ */
+ setData: function (data) {
+ this._data = data;
+ this._cachedBackgroundImage = this.buildBackgroundImage();
+ this._cachedGraphImage = this.buildGraphImage();
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Same as `setData`, but waits for this graph to finish initializing first.
+ *
+ * @param object data
+ * The data source. The actual format is specified by subclasses.
+ * @return promise
+ * A promise resolved once the data is set.
+ */
+ setDataWhenReady: Task.async(function* (data) {
+ yield this.ready();
+ this.setData(data);
+ }),
+
+ /**
+ * Adds a mask to this graph.
+ *
+ * @param any mask, options
+ * See `buildMaskImage` in inheriting classes for the required args.
+ */
+ setMask: function (mask, ...options) {
+ this._mask = mask;
+ this._maskArgs = [mask, ...options];
+ this._cachedMaskImage = this.buildMaskImage.apply(this, this._maskArgs);
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Adds regions to this graph.
+ *
+ * See the "Language" section in the constructor documentation
+ * for details about what "regions" represent.
+ *
+ * @param array regions
+ * A list of { start, end } values.
+ */
+ setRegions: function (regions) {
+ if (!this._cachedGraphImage) {
+ throw new Error("Can't highlight regions on a graph with " +
+ "no data displayed.");
+ }
+ if (this._regions) {
+ throw new Error("Regions were already highlighted on the graph.");
+ }
+ this._regions = regions.map(e => ({
+ start: e.start * this.dataScaleX,
+ end: e.end * this.dataScaleX
+ }));
+ this._bakeRegions(this._regions, this._cachedGraphImage);
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Gets whether or not this graph has a data source.
+ * @return boolean
+ */
+ hasData: function () {
+ return !!this._data;
+ },
+
+ /**
+ * Gets whether or not this graph has any mask applied.
+ * @return boolean
+ */
+ hasMask: function () {
+ return !!this._mask;
+ },
+
+ /**
+ * Gets whether or not this graph has any regions.
+ * @return boolean
+ */
+ hasRegions: function () {
+ return !!this._regions;
+ },
+
+ /**
+ * Sets the selection bounds.
+ * Use `dropSelection` to remove the selection.
+ *
+ * If the bounds aren't different, no "selection" event is emitted.
+ *
+ * See the "Language" section in the constructor documentation
+ * for details about what a "selection" represents.
+ *
+ * @param object selection
+ * The selection's { start, end } values.
+ */
+ setSelection: function (selection) {
+ if (!selection || selection.start == null || selection.end == null) {
+ throw new Error("Invalid selection coordinates");
+ }
+ if (!this.isSelectionDifferent(selection)) {
+ return;
+ }
+ this._selection.start = selection.start;
+ this._selection.end = selection.end;
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ },
+
+ /**
+ * Gets the selection bounds.
+ * If there's no selection, the bounds have null values.
+ *
+ * @return object
+ * The selection's { start, end } values.
+ */
+ getSelection: function () {
+ if (this.hasSelection()) {
+ return { start: this._selection.start, end: this._selection.end };
+ }
+ if (this.hasSelectionInProgress()) {
+ return { start: this._selection.start, end: this._cursor.x };
+ }
+ return { start: null, end: null };
+ },
+
+ /**
+ * Sets the selection bounds, scaled to correlate with the data source ranges,
+ * such that a [0, max width] selection maps to [first value, last value].
+ *
+ * @param object selection
+ * The selection's { start, end } values.
+ * @param object { mapStart, mapEnd } mapping [optional]
+ * Invoked when retrieving the numbers in the data source representing
+ * the first and last values, on the X axis.
+ */
+ setMappedSelection: function (selection, mapping = {}) {
+ if (!this.hasData()) {
+ throw new Error("A data source is necessary for retrieving " +
+ "a mapped selection.");
+ }
+ if (!selection || selection.start == null || selection.end == null) {
+ throw new Error("Invalid selection coordinates");
+ }
+
+ let { mapStart, mapEnd } = mapping;
+ let startTime = (mapStart || (e => e.delta))(this._data[0]);
+ let endTime = (mapEnd || (e => e.delta))(this._data[this._data.length - 1]);
+
+ // The selection's start and end values are not guaranteed to be ascending.
+ // Also make sure that the selection bounds fit inside the data bounds.
+ let min = Math.max(Math.min(selection.start, selection.end), startTime);
+ let max = Math.min(Math.max(selection.start, selection.end), endTime);
+ min = map(min, startTime, endTime, 0, this._width);
+ max = map(max, startTime, endTime, 0, this._width);
+
+ this.setSelection({ start: min, end: max });
+ },
+
+ /**
+ * Gets the selection bounds, scaled to correlate with the data source ranges,
+ * such that a [0, max width] selection maps to [first value, last value].
+ *
+ * @param object { mapStart, mapEnd } mapping [optional]
+ * Invoked when retrieving the numbers in the data source representing
+ * the first and last values, on the X axis.
+ * @return object
+ * The mapped selection's { min, max } values.
+ */
+ getMappedSelection: function (mapping = {}) {
+ if (!this.hasData()) {
+ throw new Error("A data source is necessary for retrieving a " +
+ "mapped selection.");
+ }
+ if (!this.hasSelection() && !this.hasSelectionInProgress()) {
+ return { min: null, max: null };
+ }
+
+ let { mapStart, mapEnd } = mapping;
+ let startTime = (mapStart || (e => e.delta))(this._data[0]);
+ let endTime = (mapEnd || (e => e.delta))(this._data[this._data.length - 1]);
+
+ // The selection's start and end values are not guaranteed to be ascending.
+ // This can happen, for example, when click & dragging from right to left.
+ // Also make sure that the selection bounds fit inside the canvas bounds.
+ let selection = this.getSelection();
+ let min = Math.max(Math.min(selection.start, selection.end), 0);
+ let max = Math.min(Math.max(selection.start, selection.end), this._width);
+ min = map(min, 0, this._width, startTime, endTime);
+ max = map(max, 0, this._width, startTime, endTime);
+
+ return { min: min, max: max };
+ },
+
+ /**
+ * Removes the selection.
+ */
+ dropSelection: function () {
+ if (!this.hasSelection() && !this.hasSelectionInProgress()) {
+ return;
+ }
+ this._selection.start = null;
+ this._selection.end = null;
+ this._shouldRedraw = true;
+ this.emit("deselecting");
+ },
+
+ /**
+ * Gets whether or not this graph has a selection.
+ * @return boolean
+ */
+ hasSelection: function () {
+ return this._selection &&
+ this._selection.start != null && this._selection.end != null;
+ },
+
+ /**
+ * Gets whether or not a selection is currently being made, for example
+ * via a click+drag operation.
+ * @return boolean
+ */
+ hasSelectionInProgress: function () {
+ return this._selection &&
+ this._selection.start != null && this._selection.end == null;
+ },
+
+ /**
+ * Specifies whether or not mouse selection is allowed.
+ * @type boolean
+ */
+ selectionEnabled: true,
+
+ /**
+ * Sets the selection bounds.
+ * Use `dropCursor` to hide the cursor.
+ *
+ * @param object cursor
+ * The cursor's { x, y } position.
+ */
+ setCursor: function (cursor) {
+ if (!cursor || cursor.x == null || cursor.y == null) {
+ throw new Error("Invalid cursor coordinates");
+ }
+ if (!this.isCursorDifferent(cursor)) {
+ return;
+ }
+ this._cursor.x = cursor.x;
+ this._cursor.y = cursor.y;
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Gets the cursor position.
+ * If there's no cursor, the position has null values.
+ *
+ * @return object
+ * The cursor's { x, y } values.
+ */
+ getCursor: function () {
+ return { x: this._cursor.x, y: this._cursor.y };
+ },
+
+ /**
+ * Hides the cursor.
+ */
+ dropCursor: function () {
+ if (!this.hasCursor()) {
+ return;
+ }
+ this._cursor.x = null;
+ this._cursor.y = null;
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Gets whether or not this graph has a visible cursor.
+ * @return boolean
+ */
+ hasCursor: function () {
+ return this._cursor && this._cursor.x != null;
+ },
+
+ /**
+ * Specifies if this graph's selection is different from another one.
+ *
+ * @param object other
+ * The other graph's selection, as { start, end } values.
+ */
+ isSelectionDifferent: function (other) {
+ if (!other) {
+ return true;
+ }
+ let current = this.getSelection();
+ return current.start != other.start || current.end != other.end;
+ },
+
+ /**
+ * Specifies if this graph's cursor is different from another one.
+ *
+ * @param object other
+ * The other graph's position, as { x, y } values.
+ */
+ isCursorDifferent: function (other) {
+ if (!other) {
+ return true;
+ }
+ let current = this.getCursor();
+ return current.x != other.x || current.y != other.y;
+ },
+
+ /**
+ * Gets the width of the current selection.
+ * If no selection is available, 0 is returned.
+ *
+ * @return number
+ * The selection width.
+ */
+ getSelectionWidth: function () {
+ let selection = this.getSelection();
+ return Math.abs(selection.start - selection.end);
+ },
+
+ /**
+ * Gets the currently hovered region, if any.
+ * If no region is currently hovered, null is returned.
+ *
+ * @return object
+ * The hovered region, as { start, end } values.
+ */
+ getHoveredRegion: function () {
+ if (!this.hasRegions() || !this.hasCursor()) {
+ return null;
+ }
+ let { x } = this._cursor;
+ return this._regions.find(({ start, end }) =>
+ (start < end && start < x && end > x) ||
+ (start > end && end < x && start > x));
+ },
+
+ /**
+ * Updates this graph to reflect the new dimensions of the parent node.
+ *
+ * @param boolean options.force
+ * Force redrawing everything
+ */
+ refresh: function (options = {}) {
+ let bounds = this._parent.getBoundingClientRect();
+ let newWidth = this.fixedWidth || bounds.width;
+ let newHeight = this.fixedHeight || bounds.height;
+
+ // Prevent redrawing everything if the graph's width & height won't change,
+ // except if force=true.
+ if (!options.force &&
+ this._width == newWidth * this._pixelRatio &&
+ this._height == newHeight * this._pixelRatio) {
+ this.emit("refresh-cancelled");
+ return;
+ }
+
+ // Handle a changed size by mapping the old selection to the new width
+ if (this._width && newWidth && this.hasSelection()) {
+ let ratio = this._width / (newWidth * this._pixelRatio);
+ this._selection.start = Math.round(this._selection.start / ratio);
+ this._selection.end = Math.round(this._selection.end / ratio);
+ }
+
+ bounds.width = newWidth;
+ bounds.height = newHeight;
+ this._iframe.setAttribute("width", bounds.width);
+ this._iframe.setAttribute("height", bounds.height);
+ this._width = this._canvas.width = bounds.width * this._pixelRatio;
+ this._height = this._canvas.height = bounds.height * this._pixelRatio;
+
+ if (this.hasData()) {
+ this._cachedBackgroundImage = this.buildBackgroundImage();
+ this._cachedGraphImage = this.buildGraphImage();
+ }
+ if (this.hasMask()) {
+ this._cachedMaskImage = this.buildMaskImage.apply(this, this._maskArgs);
+ }
+ if (this.hasRegions()) {
+ this._bakeRegions(this._regions, this._cachedGraphImage);
+ }
+
+ this._shouldRedraw = true;
+ this.emit("refresh");
+ },
+
+ /**
+ * Gets a canvas with the specified name, for this graph.
+ *
+ * If it doesn't exist yet, it will be created, otherwise the cached instance
+ * will be cleared and returned.
+ *
+ * @param string name
+ * The canvas name.
+ * @param number width, height [optional]
+ * A custom width and height for the canvas. Defaults to this graph's
+ * container canvas width and height.
+ */
+ _getNamedCanvas: function (name, width = this._width, height = this._height) {
+ let cachedRenderTarget = this._renderTargets.get(name);
+ if (cachedRenderTarget) {
+ let { canvas, ctx } = cachedRenderTarget;
+ canvas.width = width;
+ canvas.height = height;
+ ctx.clearRect(0, 0, width, height);
+ return cachedRenderTarget;
+ }
+
+ let canvas = this._document.createElementNS(HTML_NS, "canvas");
+ let ctx = canvas.getContext("2d");
+ canvas.width = width;
+ canvas.height = height;
+
+ let renderTarget = { canvas: canvas, ctx: ctx };
+ this._renderTargets.set(name, renderTarget);
+ return renderTarget;
+ },
+
+ /**
+ * The contents of this graph are redrawn only when something changed,
+ * like the data source, or the selection bounds etc. This flag tracks
+ * if the rendering is "dirty" and needs to be refreshed.
+ */
+ _shouldRedraw: false,
+
+ /**
+ * Animation frame callback, invoked on each tick of the refresh driver.
+ */
+ _onAnimationFrame: function () {
+ this._animationId =
+ this._window.requestAnimationFrame(this._onAnimationFrame);
+ this._drawWidget();
+ },
+
+ /**
+ * Redraws the widget when necessary. The actual graph is not refreshed
+ * every time this function is called, only the cliphead, selection etc.
+ */
+ _drawWidget: function () {
+ if (!this._shouldRedraw) {
+ return;
+ }
+ let ctx = this._ctx;
+ ctx.clearRect(0, 0, this._width, this._height);
+
+ if (this._cachedGraphImage) {
+ ctx.drawImage(this._cachedGraphImage, 0, 0, this._width, this._height);
+ }
+ if (this._cachedMaskImage) {
+ ctx.globalCompositeOperation = "destination-out";
+ ctx.drawImage(this._cachedMaskImage, 0, 0, this._width, this._height);
+ }
+ if (this._cachedBackgroundImage) {
+ ctx.globalCompositeOperation = "destination-over";
+ ctx.drawImage(this._cachedBackgroundImage, 0, 0,
+ this._width, this._height);
+ }
+
+ // Revert to the original global composition operation.
+ if (this._cachedMaskImage || this._cachedBackgroundImage) {
+ ctx.globalCompositeOperation = "source-over";
+ }
+
+ if (this.hasCursor()) {
+ this._drawCliphead();
+ }
+ if (this.hasSelection() || this.hasSelectionInProgress()) {
+ this._drawSelection();
+ }
+
+ this._shouldRedraw = false;
+ },
+
+ /**
+ * Draws the cliphead, if available and necessary.
+ */
+ _drawCliphead: function () {
+ if (this._isHoveringSelectionContentsOrBoundaries() ||
+ this._isHoveringRegion()) {
+ return;
+ }
+
+ let ctx = this._ctx;
+ ctx.lineWidth = this.clipheadLineWidth;
+ ctx.strokeStyle = this.clipheadLineColor;
+ ctx.beginPath();
+ ctx.moveTo(this._cursor.x, 0);
+ ctx.lineTo(this._cursor.x, this._height);
+ ctx.stroke();
+ },
+
+ /**
+ * Draws the selection, if available and necessary.
+ */
+ _drawSelection: function () {
+ let { start, end } = this.getSelection();
+ let input = this._canvas.getAttribute("input");
+
+ let ctx = this._ctx;
+ ctx.strokeStyle = this.selectionLineColor;
+
+ // Fill selection.
+
+ let pattern = AbstractCanvasGraph.getStripePattern({
+ ownerDocument: this._document,
+ backgroundColor: this.selectionBackgroundColor,
+ stripesColor: this.selectionStripesColor
+ });
+ ctx.fillStyle = pattern;
+ let rectStart = Math.min(this._width, Math.max(0, start));
+ let rectEnd = Math.min(this._width, Math.max(0, end));
+ ctx.fillRect(rectStart, 0, rectEnd - rectStart, this._height);
+
+ // Draw left boundary.
+
+ if (input == "hovering-selection-start-boundary") {
+ ctx.lineWidth = GRAPH_SELECTION_BOUNDARY_HOVER_LINE_WIDTH;
+ } else {
+ ctx.lineWidth = this.clipheadLineWidth;
+ }
+ ctx.beginPath();
+ ctx.moveTo(start, 0);
+ ctx.lineTo(start, this._height);
+ ctx.stroke();
+
+ // Draw right boundary.
+
+ if (input == "hovering-selection-end-boundary") {
+ ctx.lineWidth = GRAPH_SELECTION_BOUNDARY_HOVER_LINE_WIDTH;
+ } else {
+ ctx.lineWidth = this.clipheadLineWidth;
+ }
+ ctx.beginPath();
+ ctx.moveTo(end, this._height);
+ ctx.lineTo(end, 0);
+ ctx.stroke();
+ },
+
+ /**
+ * Draws regions into the cached graph image, created via `buildGraphImage`.
+ * Called when new regions are set.
+ */
+ _bakeRegions: function (regions, destination) {
+ let ctx = destination.getContext("2d");
+
+ let pattern = AbstractCanvasGraph.getStripePattern({
+ ownerDocument: this._document,
+ backgroundColor: this.regionBackgroundColor,
+ stripesColor: this.regionStripesColor
+ });
+ ctx.fillStyle = pattern;
+ ctx.strokeStyle = GRAPH_REGION_LINE_COLOR;
+ ctx.lineWidth = GRAPH_REGION_LINE_WIDTH;
+
+ let y = -GRAPH_REGION_LINE_WIDTH;
+ let height = this._height + GRAPH_REGION_LINE_WIDTH;
+
+ for (let { start, end } of regions) {
+ let x = start;
+ let width = end - start;
+ ctx.fillRect(x, y, width, height);
+ ctx.strokeRect(x, y, width, height);
+ }
+ },
+
+ /**
+ * Checks whether the start handle of the selection is hovered.
+ * @return boolean
+ */
+ _isHoveringStartBoundary: function () {
+ if (!this.hasSelection() || !this.hasCursor()) {
+ return false;
+ }
+ let { x } = this._cursor;
+ let { start } = this._selection;
+ let threshold = GRAPH_SELECTION_BOUNDARY_HOVER_THRESHOLD * this._pixelRatio;
+ return Math.abs(start - x) < threshold;
+ },
+
+ /**
+ * Checks whether the end handle of the selection is hovered.
+ * @return boolean
+ */
+ _isHoveringEndBoundary: function () {
+ if (!this.hasSelection() || !this.hasCursor()) {
+ return false;
+ }
+ let { x } = this._cursor;
+ let { end } = this._selection;
+ let threshold = GRAPH_SELECTION_BOUNDARY_HOVER_THRESHOLD * this._pixelRatio;
+ return Math.abs(end - x) < threshold;
+ },
+
+ /**
+ * Checks whether the selection is hovered.
+ * @return boolean
+ */
+ _isHoveringSelectionContents: function () {
+ if (!this.hasSelection() || !this.hasCursor()) {
+ return false;
+ }
+ let { x } = this._cursor;
+ let { start, end } = this._selection;
+ return (start < end && start < x && end > x) ||
+ (start > end && end < x && start > x);
+ },
+
+ /**
+ * Checks whether the selection or its handles are hovered.
+ * @return boolean
+ */
+ _isHoveringSelectionContentsOrBoundaries: function () {
+ return this._isHoveringSelectionContents() ||
+ this._isHoveringStartBoundary() ||
+ this._isHoveringEndBoundary();
+ },
+
+ /**
+ * Checks whether a region is hovered.
+ * @return boolean
+ */
+ _isHoveringRegion: function () {
+ return !!this.getHoveredRegion();
+ },
+
+ /**
+ * Given a MouseEvent, make it relative to this._canvas.
+ * @return object {mouseX,mouseY}
+ */
+ _getRelativeEventCoordinates: function (e) {
+ // For ease of testing, testX and testY can be passed in as the event
+ // object. If so, just return this.
+ if ("testX" in e && "testY" in e) {
+ return {
+ mouseX: e.testX * this._pixelRatio,
+ mouseY: e.testY * this._pixelRatio
+ };
+ }
+
+ // This method is concerned with converting mouse event coordinates from
+ // "screen space" to "local space" (in other words, relative to this
+ // canvas's position, thus (0,0) would correspond to the upper left corner).
+ // We can't simply use `clientX` and `clientY` because the given MouseEvent
+ // object may be generated from events coming from other DOM nodes.
+ // Therefore, we need to get a bounding box relative to the top document and
+ // do some simple math to convert screen coords into local coords.
+ // However, `getBoxQuads` may be a very costly operation depending on the
+ // complexity of the "outside world" DOM, so cache the results until we
+ // suspect they might change (e.g. on a resize).
+ // It'd sure be nice if we could use `getBoundsWithoutFlushing`, but it's
+ // not taking the document zoom factor into consideration consistently.
+ if (!this._boundingBox || this._maybeDirtyBoundingBox) {
+ let topDocument = this._topWindow.document;
+ let boxQuad = this._canvas.getBoxQuads({ relativeTo: topDocument })[0];
+ this._boundingBox = boxQuad;
+ this._maybeDirtyBoundingBox = false;
+ }
+
+ let bb = this._boundingBox;
+ let x = (e.screenX - this._topWindow.screenX) - bb.p1.x;
+ let y = (e.screenY - this._topWindow.screenY) - bb.p1.y;
+
+ // Don't allow the event coordinates to be bigger than the canvas
+ // or less than 0.
+ let maxX = bb.p2.x - bb.p1.x;
+ let maxY = bb.p3.y - bb.p1.y;
+ let mouseX = Math.max(0, Math.min(x, maxX)) * this._pixelRatio;
+ let mouseY = Math.max(0, Math.min(y, maxY)) * this._pixelRatio;
+
+ // The coordinates need to be modified with the current zoom level
+ // to prevent them from being wrong.
+ let zoom = getCurrentZoom(this._canvas);
+ mouseX /= zoom;
+ mouseY /= zoom;
+
+ return {mouseX, mouseY};
+ },
+
+ /**
+ * Listener for the "mousemove" event on the graph's container.
+ */
+ _onMouseMove: function (e) {
+ let resizer = this._selectionResizer;
+ let dragger = this._selectionDragger;
+
+ // Need to stop propagation here, since this function can be bound
+ // to both this._window and this._topWindow. It's only attached to
+ // this._topWindow during a drag event. Null check here since tests
+ // don't pass this method into the event object.
+ if (e.stopPropagation && this._isMouseActive) {
+ e.stopPropagation();
+ }
+
+ // If a mouseup happened outside the window and the current operation
+ // is causing the selection to change, then end it.
+ if (e.buttons == 0 && (this.hasSelectionInProgress() ||
+ resizer.margin != null ||
+ dragger.origin != null)) {
+ this._onMouseUp();
+ return;
+ }
+
+ let {mouseX, mouseY} = this._getRelativeEventCoordinates(e);
+ this._cursor.x = mouseX;
+ this._cursor.y = mouseY;
+
+ if (resizer.margin != null) {
+ this._selection[resizer.margin] = mouseX;
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ return;
+ }
+
+ if (dragger.origin != null) {
+ this._selection.start = dragger.anchor.start - dragger.origin + mouseX;
+ this._selection.end = dragger.anchor.end - dragger.origin + mouseX;
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ return;
+ }
+
+ if (this.hasSelectionInProgress()) {
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ return;
+ }
+
+ if (this.hasSelection()) {
+ if (this._isHoveringStartBoundary()) {
+ this._canvas.setAttribute("input", "hovering-selection-start-boundary");
+ this._shouldRedraw = true;
+ return;
+ }
+ if (this._isHoveringEndBoundary()) {
+ this._canvas.setAttribute("input", "hovering-selection-end-boundary");
+ this._shouldRedraw = true;
+ return;
+ }
+ if (this._isHoveringSelectionContents()) {
+ this._canvas.setAttribute("input", "hovering-selection-contents");
+ this._shouldRedraw = true;
+ return;
+ }
+ }
+
+ let region = this.getHoveredRegion();
+ if (region) {
+ this._canvas.setAttribute("input", "hovering-region");
+ } else {
+ this._canvas.setAttribute("input", "hovering-background");
+ }
+
+ this._shouldRedraw = true;
+ },
+
+ /**
+ * Listener for the "mousedown" event on the graph's container.
+ */
+ _onMouseDown: function (e) {
+ this._isMouseActive = true;
+ let {mouseX} = this._getRelativeEventCoordinates(e);
+
+ switch (this._canvas.getAttribute("input")) {
+ case "hovering-background":
+ case "hovering-region":
+ if (!this.selectionEnabled) {
+ break;
+ }
+ this._selection.start = mouseX;
+ this._selection.end = null;
+ this.emit("selecting");
+ break;
+
+ case "hovering-selection-start-boundary":
+ this._selectionResizer.margin = "start";
+ break;
+
+ case "hovering-selection-end-boundary":
+ this._selectionResizer.margin = "end";
+ break;
+
+ case "hovering-selection-contents":
+ this._selectionDragger.origin = mouseX;
+ this._selectionDragger.anchor.start = this._selection.start;
+ this._selectionDragger.anchor.end = this._selection.end;
+ this._canvas.setAttribute("input", "dragging-selection-contents");
+ break;
+ }
+
+ // During a drag, bind to the top level window so that mouse movement
+ // outside of this frame will still work.
+ this._topWindow.addEventListener("mousemove", this._onMouseMove);
+ this._topWindow.addEventListener("mouseup", this._onMouseUp);
+
+ this._shouldRedraw = true;
+ this.emit("mousedown");
+ },
+
+ /**
+ * Listener for the "mouseup" event on the graph's container.
+ */
+ _onMouseUp: function () {
+ this._isMouseActive = false;
+ switch (this._canvas.getAttribute("input")) {
+ case "hovering-background":
+ case "hovering-region":
+ if (!this.selectionEnabled) {
+ break;
+ }
+ if (this.getSelectionWidth() < 1) {
+ let region = this.getHoveredRegion();
+ if (region) {
+ this._selection.start = region.start;
+ this._selection.end = region.end;
+ this.emit("selecting");
+ } else {
+ this._selection.start = null;
+ this._selection.end = null;
+ this.emit("deselecting");
+ }
+ } else {
+ this._selection.end = this._cursor.x;
+ this.emit("selecting");
+ }
+ break;
+
+ case "hovering-selection-start-boundary":
+ case "hovering-selection-end-boundary":
+ this._selectionResizer.margin = null;
+ break;
+
+ case "dragging-selection-contents":
+ this._selectionDragger.origin = null;
+ this._canvas.setAttribute("input", "hovering-selection-contents");
+ break;
+ }
+
+ // No longer dragging, no need to bind to the top level window.
+ this._topWindow.removeEventListener("mousemove", this._onMouseMove);
+ this._topWindow.removeEventListener("mouseup", this._onMouseUp);
+
+ this._shouldRedraw = true;
+ this.emit("mouseup");
+ },
+
+ /**
+ * Listener for the "wheel" event on the graph's container.
+ */
+ _onMouseWheel: function (e) {
+ if (!this.hasSelection()) {
+ return;
+ }
+
+ let {mouseX} = this._getRelativeEventCoordinates(e);
+ let focusX = mouseX;
+
+ let selection = this._selection;
+ let vector = 0;
+
+ // If the selection is hovered, "zoom" towards or away the cursor,
+ // by shrinking or growing the selection.
+ if (this._isHoveringSelectionContentsOrBoundaries()) {
+ let distStart = selection.start - focusX;
+ let distEnd = selection.end - focusX;
+ vector = e.detail * GRAPH_WHEEL_ZOOM_SENSITIVITY;
+ selection.start = selection.start + distStart * vector;
+ selection.end = selection.end + distEnd * vector;
+ } else {
+ // Otherwise, simply pan the selection towards the left or right.
+ let direction = 0;
+ if (focusX > selection.end) {
+ direction = Math.sign(focusX - selection.end);
+ } else if (focusX < selection.start) {
+ direction = Math.sign(focusX - selection.start);
+ }
+ vector = direction * e.detail * GRAPH_WHEEL_SCROLL_SENSITIVITY;
+ selection.start -= vector;
+ selection.end -= vector;
+ }
+
+ // Make sure the selection bounds are still comfortably inside the
+ // graph's bounds when zooming out, to keep the margin handles accessible.
+
+ let minStart = GRAPH_MAX_SELECTION_LEFT_PADDING;
+ let maxEnd = this._width - GRAPH_MAX_SELECTION_RIGHT_PADDING;
+ if (selection.start < minStart) {
+ selection.start = minStart;
+ }
+ if (selection.start > maxEnd) {
+ selection.start = maxEnd;
+ }
+ if (selection.end < minStart) {
+ selection.end = minStart;
+ }
+ if (selection.end > maxEnd) {
+ selection.end = maxEnd;
+ }
+
+ // Make sure the selection doesn't get too narrow when zooming in.
+
+ let thickness = Math.abs(selection.start - selection.end);
+ if (thickness < GRAPH_WHEEL_MIN_SELECTION_WIDTH) {
+ let midPoint = (selection.start + selection.end) / 2;
+ selection.start = midPoint - GRAPH_WHEEL_MIN_SELECTION_WIDTH / 2;
+ selection.end = midPoint + GRAPH_WHEEL_MIN_SELECTION_WIDTH / 2;
+ }
+
+ this._shouldRedraw = true;
+ this.emit("selecting");
+ this.emit("scroll");
+ },
+
+ /**
+ * Listener for the "mouseout" event on the graph's container.
+ * Clear any active cursors if a drag isn't happening.
+ */
+ _onMouseOut: function (e) {
+ if (!this._isMouseActive) {
+ this._cursor.x = null;
+ this._cursor.y = null;
+ this._canvas.removeAttribute("input");
+ this._shouldRedraw = true;
+ }
+ },
+
+ /**
+ * Listener for the "resize" event on the graph's parent node.
+ */
+ _onResize: function () {
+ if (this.hasData()) {
+ // The assumption is that resize events may change the outside world
+ // layout in a way that affects this graph's bounding box location
+ // relative to the top window's document. Graphs aren't currently
+ // (or ever) expected to move around on their own.
+ this._maybeDirtyBoundingBox = true;
+ setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
+ }
+ }
+};
+
+// Helper functions.
+
+/**
+ * Creates an iframe element with the provided source URL, appends it to
+ * the specified node and invokes the callback once the content is loaded.
+ *
+ * @param string url
+ * The desired source URL for the iframe.
+ * @param nsIDOMNode parent
+ * The desired parent node for the iframe.
+ * @param function callback
+ * Invoked once the content is loaded, with the iframe as an argument.
+ */
+AbstractCanvasGraph.createIframe = function (url, parent, callback) {
+ let iframe = parent.ownerDocument.createElementNS(HTML_NS, "iframe");
+
+ iframe.addEventListener("DOMContentLoaded", function onLoad() {
+ iframe.removeEventListener("DOMContentLoaded", onLoad);
+ callback(iframe);
+ });
+
+ // Setting 100% width on the frame and flex on the parent allows the graph
+ // to properly shrink when the window is resized to be smaller.
+ iframe.setAttribute("frameborder", "0");
+ iframe.style.width = "100%";
+ iframe.style.minWidth = "50px";
+ iframe.src = url;
+
+ parent.style.display = "flex";
+ parent.appendChild(iframe);
+};
+
+/**
+ * Gets a striped pattern used as a background in selections and regions.
+ *
+ * @param object data
+ * The following properties are required:
+ * - ownerDocument: the nsIDocumentElement owning the canvas
+ * - backgroundColor: a string representing the fill style
+ * - stripesColor: a string representing the stroke style
+ * @return nsIDOMCanvasPattern
+ * The custom striped pattern.
+ */
+AbstractCanvasGraph.getStripePattern = function (data) {
+ let { ownerDocument, backgroundColor, stripesColor } = data;
+ let id = [backgroundColor, stripesColor].join(",");
+
+ if (gCachedStripePattern.has(id)) {
+ return gCachedStripePattern.get(id);
+ }
+
+ let canvas = ownerDocument.createElementNS(HTML_NS, "canvas");
+ let ctx = canvas.getContext("2d");
+ let width = canvas.width = GRAPH_STRIPE_PATTERN_WIDTH;
+ let height = canvas.height = GRAPH_STRIPE_PATTERN_HEIGHT;
+
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(0, 0, width, height);
+
+ let pixelRatio = ownerDocument.defaultView.devicePixelRatio;
+ let scaledLineWidth = GRAPH_STRIPE_PATTERN_LINE_WIDTH * pixelRatio;
+ let scaledLineSpacing = GRAPH_STRIPE_PATTERN_LINE_SPACING * pixelRatio;
+
+ ctx.strokeStyle = stripesColor;
+ ctx.lineWidth = scaledLineWidth;
+ ctx.lineCap = "square";
+ ctx.beginPath();
+
+ for (let i = -height; i <= height; i += scaledLineSpacing) {
+ ctx.moveTo(width, i);
+ ctx.lineTo(0, i + height);
+ }
+
+ ctx.stroke();
+
+ let pattern = ctx.createPattern(canvas, "repeat");
+ gCachedStripePattern.set(id, pattern);
+ return pattern;
+};
+
+/**
+ * Cache used by `AbstractCanvasGraph.getStripePattern`.
+ */
+const gCachedStripePattern = new Map();
+
+/**
+ * Utility functions for graph canvases.
+ */
+this.CanvasGraphUtils = {
+ _graphUtilsWorker: null,
+ _graphUtilsTaskId: 0,
+
+ /**
+ * Merges the animation loop of two graphs.
+ */
+ linkAnimation: Task.async(function* (graph1, graph2) {
+ if (!graph1 || !graph2) {
+ return;
+ }
+ yield graph1.ready();
+ yield graph2.ready();
+
+ let window = graph1._window;
+ window.cancelAnimationFrame(graph1._animationId);
+ window.cancelAnimationFrame(graph2._animationId);
+
+ let loop = () => {
+ window.requestAnimationFrame(loop);
+ graph1._drawWidget();
+ graph2._drawWidget();
+ };
+
+ window.requestAnimationFrame(loop);
+ }),
+
+ /**
+ * Makes sure selections in one graph are reflected in another.
+ */
+ linkSelection: function (graph1, graph2) {
+ if (!graph1 || !graph2) {
+ return;
+ }
+
+ if (graph1.hasSelection()) {
+ graph2.setSelection(graph1.getSelection());
+ } else {
+ graph2.dropSelection();
+ }
+
+ graph1.on("selecting", () => {
+ graph2.setSelection(graph1.getSelection());
+ });
+ graph2.on("selecting", () => {
+ graph1.setSelection(graph2.getSelection());
+ });
+ graph1.on("deselecting", () => {
+ graph2.dropSelection();
+ });
+ graph2.on("deselecting", () => {
+ graph1.dropSelection();
+ });
+ },
+
+ /**
+ * Performs the given task in a chrome worker, assuming it exists.
+ *
+ * @param string task
+ * The task name. Currently supported: "plotTimestampsGraph".
+ * @param any data
+ * Extra arguments to pass to the worker.
+ * @return object
+ * A promise that is resolved once the worker finishes the task.
+ */
+ _performTaskInWorker: function (task, data) {
+ let worker = this._graphUtilsWorker || new DevToolsWorker(WORKER_URL);
+ return worker.performTask(task, data);
+ }
+};
+
+/**
+ * Maps a value from one range to another.
+ * @param number value, istart, istop, ostart, ostop
+ * @return number
+ */
+function map(value, istart, istop, ostart, ostop) {
+ let ratio = istop - istart;
+ if (ratio == 0) {
+ return value;
+ }
+ return ostart + (ostop - ostart) * ((value - istart) / ratio);
+}
+
+/**
+ * Constrains a value to a range.
+ * @param number value, min, max
+ * @return number
+ */
+function clamp(value, min, max) {
+ if (value < min) {
+ return min;
+ }
+ if (value > max) {
+ return max;
+ }
+ return value;
+}
+
+exports.GraphCursor = GraphCursor;
+exports.GraphArea = GraphArea;
+exports.GraphAreaDragger = GraphAreaDragger;
+exports.GraphAreaResizer = GraphAreaResizer;
+exports.AbstractCanvasGraph = AbstractCanvasGraph;
+exports.CanvasGraphUtils = CanvasGraphUtils;
+exports.CanvasGraphUtils.map = map;
+exports.CanvasGraphUtils.clamp = clamp;
diff --git a/devtools/client/shared/widgets/GraphsWorker.js b/devtools/client/shared/widgets/GraphsWorker.js
new file mode 100644
index 000000000..1e12f1d11
--- /dev/null
+++ b/devtools/client/shared/widgets/GraphsWorker.js
@@ -0,0 +1,103 @@
+/* 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";
+
+/* eslint-env worker */
+
+/**
+ * Import `createTask` to communicate with `devtools/shared/worker`.
+ */
+importScripts("resource://gre/modules/workers/require.js");
+const { createTask } = require("resource://devtools/shared/worker/helper.js");
+
+/**
+ * @see LineGraphWidget.prototype.setDataFromTimestamps in Graphs.js
+ * @param number id
+ * @param array timestamps
+ * @param number interval
+ * @param number duration
+ */
+createTask(self, "plotTimestampsGraph", function ({ timestamps,
+ interval, duration }) {
+ let plottedData = plotTimestamps(timestamps, interval);
+ let plottedMinMaxSum = getMinMaxAvg(plottedData, timestamps, duration);
+
+ return { plottedData, plottedMinMaxSum };
+});
+
+/**
+ * Gets the min, max and average of the values in an array.
+ * @param array source
+ * @param array timestamps
+ * @param number duration
+ * @return object
+ */
+function getMinMaxAvg(source, timestamps, duration) {
+ let totalFrames = timestamps.length;
+ let maxValue = Number.MIN_SAFE_INTEGER;
+ let minValue = Number.MAX_SAFE_INTEGER;
+ // Calculate the average by counting how many frames occurred
+ // in the duration of the recording, rather than average the frame points
+ // we have, as that weights higher FPS, as there'll be more timestamps for
+ // those values
+ let avgValue = totalFrames / (duration / 1000);
+
+ for (let { value } of source) {
+ maxValue = Math.max(value, maxValue);
+ minValue = Math.min(value, minValue);
+ }
+
+ return { minValue, maxValue, avgValue };
+}
+
+/**
+ * Takes a list of numbers and plots them on a line graph representing
+ * the rate of occurences in a specified interval.
+ *
+ * @param array timestamps
+ * A list of numbers representing time, ordered ascending. For example,
+ * this can be the raw data received from the framerate actor, which
+ * represents the elapsed time on each refresh driver tick.
+ * @param number interval
+ * The maximum amount of time to wait between calculations.
+ * @param number clamp
+ * The maximum allowed value.
+ * @return array
+ * A collection of { delta, value } objects representing the
+ * plotted value at every delta time.
+ */
+function plotTimestamps(timestamps, interval = 100, clamp = 60) {
+ let timeline = [];
+ let totalTicks = timestamps.length;
+
+ // If the refresh driver didn't get a chance to tick before the
+ // recording was stopped, assume rate was 0.
+ if (totalTicks == 0) {
+ timeline.push({ delta: 0, value: 0 });
+ timeline.push({ delta: interval, value: 0 });
+ return timeline;
+ }
+
+ let frameCount = 0;
+ let prevTime = +timestamps[0];
+
+ for (let i = 1; i < totalTicks; i++) {
+ let currTime = +timestamps[i];
+ frameCount++;
+
+ let elapsedTime = currTime - prevTime;
+ if (elapsedTime < interval) {
+ continue;
+ }
+
+ let rate = Math.min(1000 / (elapsedTime / frameCount), clamp);
+ timeline.push({ delta: prevTime, value: rate });
+ timeline.push({ delta: currTime, value: rate });
+
+ frameCount = 0;
+ prevTime = currTime;
+ }
+
+ return timeline;
+}
diff --git a/devtools/client/shared/widgets/LineGraphWidget.js b/devtools/client/shared/widgets/LineGraphWidget.js
new file mode 100644
index 000000000..12ca425ad
--- /dev/null
+++ b/devtools/client/shared/widgets/LineGraphWidget.js
@@ -0,0 +1,402 @@
+"use strict";
+
+const { Task } = require("devtools/shared/task");
+const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const { AbstractCanvasGraph, CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const L10N = new LocalizationHelper("devtools/client/locales/graphs.properties");
+
+// Line graph constants.
+
+const GRAPH_DAMPEN_VALUES_FACTOR = 0.85;
+// px
+const GRAPH_TOOLTIP_SAFE_BOUNDS = 8;
+const GRAPH_MIN_MAX_TOOLTIP_DISTANCE = 14;
+
+const GRAPH_BACKGROUND_COLOR = "#0088cc";
+// px
+const GRAPH_STROKE_WIDTH = 1;
+const GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
+// px
+const GRAPH_HELPER_LINES_DASH = [5];
+const GRAPH_HELPER_LINES_WIDTH = 1;
+const GRAPH_MAXIMUM_LINE_COLOR = "rgba(255,255,255,0.4)";
+const GRAPH_AVERAGE_LINE_COLOR = "rgba(255,255,255,0.7)";
+const GRAPH_MINIMUM_LINE_COLOR = "rgba(255,255,255,0.9)";
+const GRAPH_BACKGROUND_GRADIENT_START = "rgba(255,255,255,0.25)";
+const GRAPH_BACKGROUND_GRADIENT_END = "rgba(255,255,255,0.0)";
+
+const GRAPH_CLIPHEAD_LINE_COLOR = "#fff";
+const GRAPH_SELECTION_LINE_COLOR = "#fff";
+const GRAPH_SELECTION_BACKGROUND_COLOR = "rgba(44,187,15,0.25)";
+const GRAPH_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+const GRAPH_REGION_BACKGROUND_COLOR = "transparent";
+const GRAPH_REGION_STRIPES_COLOR = "rgba(237,38,85,0.2)";
+
+/**
+ * A basic line graph, plotting values on a curve and adding helper lines
+ * and tooltips for maximum, average and minimum values.
+ *
+ * @see AbstractCanvasGraph for emitted events and other options.
+ *
+ * Example usage:
+ * let graph = new LineGraphWidget(node, "units");
+ * graph.once("ready", () => {
+ * graph.setData(src);
+ * });
+ *
+ * Data source format:
+ * [
+ * { delta: x1, value: y1 },
+ * { delta: x2, value: y2 },
+ * ...
+ * { delta: xn, value: yn }
+ * ]
+ * where each item in the array represents a point in the graph.
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the graph.
+ * @param object options [optional]
+ * `metric`: The metric displayed in the graph, e.g. "fps" or "bananas".
+ * `min`: Boolean whether to show the min tooltip/gutter/line (default: true)
+ * `max`: Boolean whether to show the max tooltip/gutter/line (default: true)
+ * `avg`: Boolean whether to show the avg tooltip/gutter/line (default: true)
+ */
+this.LineGraphWidget = function (parent, options = {}, ...args) {
+ let { metric, min, max, avg } = options;
+
+ this._showMin = min !== false;
+ this._showMax = max !== false;
+ this._showAvg = avg !== false;
+
+ AbstractCanvasGraph.apply(this, [parent, "line-graph", ...args]);
+
+ this.once("ready", () => {
+ // Create all gutters and tooltips incase the showing of min/max/avg
+ // are changed later
+ this._gutter = this._createGutter();
+ this._maxGutterLine = this._createGutterLine("maximum");
+ this._maxTooltip = this._createTooltip(
+ "maximum", "start", L10N.getStr("graphs.label.maximum"), metric
+ );
+ this._minGutterLine = this._createGutterLine("minimum");
+ this._minTooltip = this._createTooltip(
+ "minimum", "start", L10N.getStr("graphs.label.minimum"), metric
+ );
+ this._avgGutterLine = this._createGutterLine("average");
+ this._avgTooltip = this._createTooltip(
+ "average", "end", L10N.getStr("graphs.label.average"), metric
+ );
+ });
+};
+
+LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+ backgroundColor: GRAPH_BACKGROUND_COLOR,
+ backgroundGradientStart: GRAPH_BACKGROUND_GRADIENT_START,
+ backgroundGradientEnd: GRAPH_BACKGROUND_GRADIENT_END,
+ strokeColor: GRAPH_STROKE_COLOR,
+ strokeWidth: GRAPH_STROKE_WIDTH,
+ maximumLineColor: GRAPH_MAXIMUM_LINE_COLOR,
+ averageLineColor: GRAPH_AVERAGE_LINE_COLOR,
+ minimumLineColor: GRAPH_MINIMUM_LINE_COLOR,
+ clipheadLineColor: GRAPH_CLIPHEAD_LINE_COLOR,
+ selectionLineColor: GRAPH_SELECTION_LINE_COLOR,
+ selectionBackgroundColor: GRAPH_SELECTION_BACKGROUND_COLOR,
+ selectionStripesColor: GRAPH_SELECTION_STRIPES_COLOR,
+ regionBackgroundColor: GRAPH_REGION_BACKGROUND_COLOR,
+ regionStripesColor: GRAPH_REGION_STRIPES_COLOR,
+
+ /**
+ * Optionally offsets the `delta` in the data source by this scalar.
+ */
+ dataOffsetX: 0,
+
+ /**
+ * Optionally uses this value instead of the last tick in the data source
+ * to compute the horizontal scaling.
+ */
+ dataDuration: 0,
+
+ /**
+ * The scalar used to multiply the graph values to leave some headroom.
+ */
+ dampenValuesFactor: GRAPH_DAMPEN_VALUES_FACTOR,
+
+ /**
+ * Specifies if min/max/avg tooltips have arrow handlers on their sides.
+ */
+ withTooltipArrows: true,
+
+ /**
+ * Specifies if min/max/avg tooltips are positioned based on the actual
+ * values, or just placed next to the graph corners.
+ */
+ withFixedTooltipPositions: false,
+
+ /**
+ * Takes a list of numbers and plots them on a line graph representing
+ * the rate of occurences in a specified interval. Useful for drawing
+ * framerate, for example, from a sequence of timestamps.
+ *
+ * @param array timestamps
+ * A list of numbers representing time, ordered ascending. For example,
+ * this can be the raw data received from the framerate actor, which
+ * represents the elapsed time on each refresh driver tick.
+ * @param number interval
+ * The maximum amount of time to wait between calculations.
+ * @param number duration
+ * The duration of the recording in milliseconds.
+ */
+ setDataFromTimestamps: Task.async(function* (timestamps, interval, duration) {
+ let {
+ plottedData,
+ plottedMinMaxSum
+ } = yield CanvasGraphUtils._performTaskInWorker("plotTimestampsGraph", {
+ timestamps, interval, duration
+ });
+
+ this._tempMinMaxSum = plottedMinMaxSum;
+ this.setData(plottedData);
+ }),
+
+ /**
+ * Renders the graph's data source.
+ * @see AbstractCanvasGraph.prototype.buildGraphImage
+ */
+ buildGraphImage: function () {
+ let { canvas, ctx } = this._getNamedCanvas("line-graph-data");
+ let width = this._width;
+ let height = this._height;
+
+ let totalTicks = this._data.length;
+ let firstTick = totalTicks ? this._data[0].delta : 0;
+ let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0;
+ let maxValue = Number.MIN_SAFE_INTEGER;
+ let minValue = Number.MAX_SAFE_INTEGER;
+ let avgValue = 0;
+
+ if (this._tempMinMaxSum) {
+ maxValue = this._tempMinMaxSum.maxValue;
+ minValue = this._tempMinMaxSum.minValue;
+ avgValue = this._tempMinMaxSum.avgValue;
+ } else {
+ let sumValues = 0;
+ for (let { value } of this._data) {
+ maxValue = Math.max(value, maxValue);
+ minValue = Math.min(value, minValue);
+ sumValues += value;
+ }
+ avgValue = sumValues / totalTicks;
+ }
+
+ let duration = this.dataDuration || lastTick;
+ let dataScaleX = this.dataScaleX = width / (duration - this.dataOffsetX);
+ let dataScaleY =
+ this.dataScaleY = height / maxValue * this.dampenValuesFactor;
+
+ // Draw the background.
+
+ ctx.fillStyle = this.backgroundColor;
+ ctx.fillRect(0, 0, width, height);
+
+ // Draw the graph.
+
+ let gradient = ctx.createLinearGradient(0, height / 2, 0, height);
+ gradient.addColorStop(0, this.backgroundGradientStart);
+ gradient.addColorStop(1, this.backgroundGradientEnd);
+ ctx.fillStyle = gradient;
+ ctx.strokeStyle = this.strokeColor;
+ ctx.lineWidth = this.strokeWidth * this._pixelRatio;
+ ctx.beginPath();
+
+ for (let { delta, value } of this._data) {
+ let currX = (delta - this.dataOffsetX) * dataScaleX;
+ let currY = height - value * dataScaleY;
+
+ if (delta == firstTick) {
+ ctx.moveTo(-GRAPH_STROKE_WIDTH, height);
+ ctx.lineTo(-GRAPH_STROKE_WIDTH, currY);
+ }
+
+ ctx.lineTo(currX, currY);
+
+ if (delta == lastTick) {
+ ctx.lineTo(width + GRAPH_STROKE_WIDTH, currY);
+ ctx.lineTo(width + GRAPH_STROKE_WIDTH, height);
+ }
+ }
+
+ ctx.fill();
+ ctx.stroke();
+
+ this._drawOverlays(ctx, minValue, maxValue, avgValue, dataScaleY);
+
+ return canvas;
+ },
+
+ /**
+ * Draws the min, max and average horizontal lines, along with their
+ * repsective tooltips.
+ *
+ * @param CanvasRenderingContext2D ctx
+ * @param number minValue
+ * @param number maxValue
+ * @param number avgValue
+ * @param number dataScaleY
+ */
+ _drawOverlays: function (ctx, minValue, maxValue, avgValue, dataScaleY) {
+ let width = this._width;
+ let height = this._height;
+ let totalTicks = this._data.length;
+
+ // Draw the maximum value horizontal line.
+ if (this._showMax) {
+ ctx.strokeStyle = this.maximumLineColor;
+ ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
+ ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
+ ctx.beginPath();
+ let maximumY = height - maxValue * dataScaleY;
+ ctx.moveTo(0, maximumY);
+ ctx.lineTo(width, maximumY);
+ ctx.stroke();
+ }
+
+ // Draw the average value horizontal line.
+ if (this._showAvg) {
+ ctx.strokeStyle = this.averageLineColor;
+ ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
+ ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
+ ctx.beginPath();
+ let averageY = height - avgValue * dataScaleY;
+ ctx.moveTo(0, averageY);
+ ctx.lineTo(width, averageY);
+ ctx.stroke();
+ }
+
+ // Draw the minimum value horizontal line.
+ if (this._showMin) {
+ ctx.strokeStyle = this.minimumLineColor;
+ ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
+ ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
+ ctx.beginPath();
+ let minimumY = height - minValue * dataScaleY;
+ ctx.moveTo(0, minimumY);
+ ctx.lineTo(width, minimumY);
+ ctx.stroke();
+ }
+
+ // Update the tooltips text and gutter lines.
+
+ this._maxTooltip.querySelector("[text=value]").textContent =
+ L10N.numberWithDecimals(maxValue, 2);
+ this._avgTooltip.querySelector("[text=value]").textContent =
+ L10N.numberWithDecimals(avgValue, 2);
+ this._minTooltip.querySelector("[text=value]").textContent =
+ L10N.numberWithDecimals(minValue, 2);
+
+ let bottom = height / this._pixelRatio;
+ let maxPosY = CanvasGraphUtils.map(maxValue * this.dampenValuesFactor, 0,
+ maxValue, bottom, 0);
+ let avgPosY = CanvasGraphUtils.map(avgValue * this.dampenValuesFactor, 0,
+ maxValue, bottom, 0);
+ let minPosY = CanvasGraphUtils.map(minValue * this.dampenValuesFactor, 0,
+ maxValue, bottom, 0);
+
+ let safeTop = GRAPH_TOOLTIP_SAFE_BOUNDS;
+ let safeBottom = bottom - GRAPH_TOOLTIP_SAFE_BOUNDS;
+
+ let maxTooltipTop = (this.withFixedTooltipPositions
+ ? safeTop : CanvasGraphUtils.clamp(maxPosY, safeTop, safeBottom));
+ let avgTooltipTop = (this.withFixedTooltipPositions
+ ? safeTop : CanvasGraphUtils.clamp(avgPosY, safeTop, safeBottom));
+ let minTooltipTop = (this.withFixedTooltipPositions
+ ? safeBottom : CanvasGraphUtils.clamp(minPosY, safeTop, safeBottom));
+
+ this._maxTooltip.style.top = maxTooltipTop + "px";
+ this._avgTooltip.style.top = avgTooltipTop + "px";
+ this._minTooltip.style.top = minTooltipTop + "px";
+
+ this._maxGutterLine.style.top = maxPosY + "px";
+ this._avgGutterLine.style.top = avgPosY + "px";
+ this._minGutterLine.style.top = minPosY + "px";
+
+ this._maxTooltip.setAttribute("with-arrows", this.withTooltipArrows);
+ this._avgTooltip.setAttribute("with-arrows", this.withTooltipArrows);
+ this._minTooltip.setAttribute("with-arrows", this.withTooltipArrows);
+
+ let distanceMinMax = Math.abs(maxTooltipTop - minTooltipTop);
+ this._maxTooltip.hidden = this._showMax === false
+ || !totalTicks
+ || distanceMinMax < GRAPH_MIN_MAX_TOOLTIP_DISTANCE;
+ this._avgTooltip.hidden = this._showAvg === false || !totalTicks;
+ this._minTooltip.hidden = this._showMin === false || !totalTicks;
+ this._gutter.hidden = (this._showMin === false &&
+ this._showAvg === false &&
+ this._showMax === false) || !totalTicks;
+
+ this._maxGutterLine.hidden = this._showMax === false;
+ this._avgGutterLine.hidden = this._showAvg === false;
+ this._minGutterLine.hidden = this._showMin === false;
+ },
+
+ /**
+ * Creates the gutter node when constructing this graph.
+ * @return nsIDOMNode
+ */
+ _createGutter: function () {
+ let gutter = this._document.createElementNS(HTML_NS, "div");
+ gutter.className = "line-graph-widget-gutter";
+ gutter.setAttribute("hidden", true);
+ this._container.appendChild(gutter);
+
+ return gutter;
+ },
+
+ /**
+ * Creates the gutter line nodes when constructing this graph.
+ * @return nsIDOMNode
+ */
+ _createGutterLine: function (type) {
+ let line = this._document.createElementNS(HTML_NS, "div");
+ line.className = "line-graph-widget-gutter-line";
+ line.setAttribute("type", type);
+ this._gutter.appendChild(line);
+
+ return line;
+ },
+
+ /**
+ * Creates the tooltip nodes when constructing this graph.
+ * @return nsIDOMNode
+ */
+ _createTooltip: function (type, arrow, info, metric) {
+ let tooltip = this._document.createElementNS(HTML_NS, "div");
+ tooltip.className = "line-graph-widget-tooltip";
+ tooltip.setAttribute("type", type);
+ tooltip.setAttribute("arrow", arrow);
+ tooltip.setAttribute("hidden", true);
+
+ let infoNode = this._document.createElementNS(HTML_NS, "span");
+ infoNode.textContent = info;
+ infoNode.setAttribute("text", "info");
+
+ let valueNode = this._document.createElementNS(HTML_NS, "span");
+ valueNode.textContent = 0;
+ valueNode.setAttribute("text", "value");
+
+ let metricNode = this._document.createElementNS(HTML_NS, "span");
+ metricNode.textContent = metric;
+ metricNode.setAttribute("text", "metric");
+
+ tooltip.appendChild(infoNode);
+ tooltip.appendChild(valueNode);
+ tooltip.appendChild(metricNode);
+ this._container.appendChild(tooltip);
+
+ return tooltip;
+ }
+});
+
+module.exports = LineGraphWidget;
diff --git a/devtools/client/shared/widgets/MdnDocsWidget.js b/devtools/client/shared/widgets/MdnDocsWidget.js
new file mode 100644
index 000000000..6a26b05c8
--- /dev/null
+++ b/devtools/client/shared/widgets/MdnDocsWidget.js
@@ -0,0 +1,510 @@
+/* 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/. */
+
+/**
+ * This file contains functions to retrieve docs content from
+ * MDN (developer.mozilla.org) for particular items, and to display
+ * the content in a tooltip.
+ *
+ * At the moment it only supports fetching content for CSS properties,
+ * but it might support other types of content in the future
+ * (Web APIs, for example).
+ *
+ * It's split into two parts:
+ *
+ * - functions like getCssDocs that just fetch content from MDN,
+ * without any constraints on what to do with the content. If you
+ * want to embed the content in some custom way, use this.
+ *
+ * - the MdnDocsWidget class, that manages and updates a tooltip
+ * document whose content is taken from MDN. If you want to embed
+ * the content in a tooltip, use this in conjunction with Tooltip.js.
+ */
+
+"use strict";
+
+const Services = require("Services");
+const defer = require("devtools/shared/defer");
+const {getCSSLexer} = require("devtools/shared/css/lexer");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {gDevTools} = require("devtools/client/framework/devtools");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+// Parameters for the XHR request
+// see https://developer.mozilla.org/en-US/docs/MDN/Kuma/API#Document_parameters
+const XHR_PARAMS = "?raw&macros";
+// URL for the XHR request
+var XHR_CSS_URL = "https://developer.mozilla.org/en-US/docs/Web/CSS/";
+
+// Parameters for the link to MDN in the tooltip, so
+// so we know which MDN visits come from this feature
+const PAGE_LINK_PARAMS =
+ "?utm_source=mozilla&utm_medium=firefox-inspector&utm_campaign=default";
+// URL for the page link omits locale, so a locale-specific page will be loaded
+var PAGE_LINK_URL = "https://developer.mozilla.org/docs/Web/CSS/";
+exports.PAGE_LINK_URL = PAGE_LINK_URL;
+
+const PROPERTY_NAME_COLOR = "theme-fg-color5";
+const PROPERTY_VALUE_COLOR = "theme-fg-color1";
+const COMMENT_COLOR = "theme-comment";
+
+/**
+ * Turns a string containing a series of CSS declarations into
+ * a series of DOM nodes, with classes applied to provide syntax
+ * highlighting.
+ *
+ * It uses the CSS tokenizer to generate a stream of CSS tokens.
+ * https://dxr.mozilla.org/mozilla-central/source/dom/webidl/CSSLexer.webidl
+ * lists all the token types.
+ *
+ * - "whitespace", "comment", and "symbol" tokens are appended as TEXT nodes,
+ * and will inherit the default style for text.
+ *
+ * - "ident" tokens that we think are property names are considered to be
+ * a property name, and are appended as SPAN nodes with a distinct color class.
+ *
+ * - "ident" nodes which we do not think are property names, and nodes
+ * of all other types ("number", "url", "percentage", ...) are considered
+ * to be part of a property value, and are appended as SPAN nodes with
+ * a different color class.
+ *
+ * @param {Document} doc
+ * Used to create nodes.
+ *
+ * @param {String} syntaxText
+ * The CSS input. This is assumed to consist of a series of
+ * CSS declarations, with trailing semicolons.
+ *
+ * @param {DOM node} syntaxSection
+ * This is the parent for the output nodes. Generated nodes
+ * are appended to this as children.
+ */
+function appendSyntaxHighlightedCSS(cssText, parentElement) {
+ let doc = parentElement.ownerDocument;
+ let identClass = PROPERTY_NAME_COLOR;
+ let lexer = getCSSLexer(cssText);
+
+ /**
+ * Create a SPAN node with the given text content and class.
+ */
+ function createStyledNode(textContent, className) {
+ let newNode = doc.createElementNS(XHTML_NS, "span");
+ newNode.classList.add(className);
+ newNode.textContent = textContent;
+ return newNode;
+ }
+
+ /**
+ * If the symbol is ":", we will expect the next
+ * "ident" token to be part of a property value.
+ *
+ * If the symbol is ";", we will expect the next
+ * "ident" token to be a property name.
+ */
+ function updateIdentClass(tokenText) {
+ if (tokenText === ":") {
+ identClass = PROPERTY_VALUE_COLOR;
+ } else if (tokenText === ";") {
+ identClass = PROPERTY_NAME_COLOR;
+ }
+ }
+
+ /**
+ * Create the appropriate node for this token type.
+ *
+ * If this token is a symbol, also update our expectations
+ * for what the next "ident" token represents.
+ */
+ function tokenToNode(token, tokenText) {
+ switch (token.tokenType) {
+ case "ident":
+ return createStyledNode(tokenText, identClass);
+ case "symbol":
+ updateIdentClass(tokenText);
+ return doc.createTextNode(tokenText);
+ case "whitespace":
+ return doc.createTextNode(tokenText);
+ case "comment":
+ return createStyledNode(tokenText, COMMENT_COLOR);
+ default:
+ return createStyledNode(tokenText, PROPERTY_VALUE_COLOR);
+ }
+ }
+
+ let token = lexer.nextToken();
+ while (token) {
+ let tokenText = cssText.slice(token.startOffset, token.endOffset);
+ let newNode = tokenToNode(token, tokenText);
+ parentElement.appendChild(newNode);
+ token = lexer.nextToken();
+ }
+}
+
+exports.appendSyntaxHighlightedCSS = appendSyntaxHighlightedCSS;
+
+/**
+ * Fetch an MDN page.
+ *
+ * @param {string} pageUrl
+ * URL of the page to fetch.
+ *
+ * @return {promise}
+ * The promise is resolved with the page as an XML document.
+ *
+ * The promise is rejected with an error message if
+ * we could not load the page.
+ */
+function getMdnPage(pageUrl) {
+ let deferred = defer();
+
+ let xhr = new XMLHttpRequest();
+
+ xhr.addEventListener("load", onLoaded, false);
+ xhr.addEventListener("error", onError, false);
+
+ xhr.open("GET", pageUrl);
+ xhr.responseType = "document";
+ xhr.send();
+
+ function onLoaded(e) {
+ if (xhr.status != 200) {
+ deferred.reject({page: pageUrl, status: xhr.status});
+ } else {
+ deferred.resolve(xhr.responseXML);
+ }
+ }
+
+ function onError(e) {
+ deferred.reject({page: pageUrl, status: xhr.status});
+ }
+
+ return deferred.promise;
+}
+
+/**
+ * Gets some docs for the given CSS property.
+ * Loads an MDN page for the property and gets some
+ * information about the property.
+ *
+ * @param {string} cssProperty
+ * The property for which we want docs.
+ *
+ * @return {promise}
+ * The promise is resolved with an object containing:
+ * - summary: a short summary of the property
+ * - syntax: some example syntax
+ *
+ * The promise is rejected with an error message if
+ * we could not load the page.
+ */
+function getCssDocs(cssProperty) {
+ let deferred = defer();
+ let pageUrl = XHR_CSS_URL + cssProperty + XHR_PARAMS;
+
+ getMdnPage(pageUrl).then(parseDocsFromResponse, handleRejection);
+
+ function parseDocsFromResponse(responseDocument) {
+ let theDocs = {};
+ theDocs.summary = getSummary(responseDocument);
+ theDocs.syntax = getSyntax(responseDocument);
+ if (theDocs.summary || theDocs.syntax) {
+ deferred.resolve(theDocs);
+ } else {
+ deferred.reject("Couldn't find the docs in the page.");
+ }
+ }
+
+ function handleRejection(e) {
+ deferred.reject(e.status);
+ }
+
+ return deferred.promise;
+}
+
+exports.getCssDocs = getCssDocs;
+
+/**
+ * The MdnDocsWidget is used by tooltip code that needs to display docs
+ * from MDN in a tooltip.
+ *
+ * In the constructor, the widget does some general setup that's not
+ * dependent on the particular item we need docs for.
+ *
+ * After that, when the tooltip code needs to display docs for an item, it
+ * asks the widget to retrieve the docs and update the document with them.
+ *
+ * @param {Element} tooltipContainer
+ * A DOM element where the MdnDocs widget markup should be created.
+ */
+function MdnDocsWidget(tooltipContainer) {
+ EventEmitter.decorate(this);
+
+ tooltipContainer.innerHTML =
+ `<header>
+ <h1 class="mdn-property-name theme-fg-color5"></h1>
+ </header>
+ <div class="mdn-property-info">
+ <div class="mdn-summary"></div>
+ <pre class="mdn-syntax devtools-monospace"></pre>
+ </div>
+ <footer>
+ <a class="mdn-visit-page theme-link" href="#">Visit MDN (placeholder)</a>
+ </footer>`;
+
+ // fetch all the bits of the document that we will manipulate later
+ this.elements = {
+ heading: tooltipContainer.querySelector(".mdn-property-name"),
+ summary: tooltipContainer.querySelector(".mdn-summary"),
+ syntax: tooltipContainer.querySelector(".mdn-syntax"),
+ info: tooltipContainer.querySelector(".mdn-property-info"),
+ linkToMdn: tooltipContainer.querySelector(".mdn-visit-page")
+ };
+
+ // get the localized string for the link text
+ this.elements.linkToMdn.textContent = L10N.getStr("docsTooltip.visitMDN");
+
+ // listen for clicks and open in the browser window instead
+ let mainWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ this.elements.linkToMdn.addEventListener("click", (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ mainWindow.openUILinkIn(e.target.href, "tab");
+ this.emit("visitlink");
+ });
+}
+
+exports.MdnDocsWidget = MdnDocsWidget;
+
+MdnDocsWidget.prototype = {
+ /**
+ * This is called just before the tooltip is displayed, and is
+ * passed the CSS property for which we want to display help.
+ *
+ * Its job is to make sure the document contains the docs
+ * content for that CSS property.
+ *
+ * First, it initializes the document, setting the things it can
+ * set synchronously, resetting the things it needs to get
+ * asynchronously, and making sure the throbber is throbbing.
+ *
+ * Then it tries to get the content asynchronously, updating
+ * the document with the content or with an error message.
+ *
+ * It returns immediately, so the caller can display the tooltip
+ * without waiting for the asynch operation to complete.
+ *
+ * @param {string} propertyName
+ * The name of the CSS property for which we need to display help.
+ */
+ loadCssDocs: function (propertyName) {
+ /**
+ * Do all the setup we can do synchronously, and get the document in
+ * a state where it can be displayed while we are waiting for the
+ * MDN docs content to be retrieved.
+ */
+ function initializeDocument(propName) {
+ // set property name heading
+ elements.heading.textContent = propName;
+
+ // set link target
+ elements.linkToMdn.setAttribute("href",
+ PAGE_LINK_URL + propName + PAGE_LINK_PARAMS);
+
+ // clear docs summary and syntax
+ elements.summary.textContent = "";
+ while (elements.syntax.firstChild) {
+ elements.syntax.firstChild.remove();
+ }
+
+ // reset the scroll position
+ elements.info.scrollTop = 0;
+ elements.info.scrollLeft = 0;
+
+ // show the throbber
+ elements.info.classList.add("devtools-throbber");
+ }
+
+ /**
+ * This is called if we successfully got the docs content.
+ * Finishes setting up the tooltip content, and disables the throbber.
+ */
+ function finalizeDocument({summary, syntax}) {
+ // set docs summary and syntax
+ elements.summary.textContent = summary;
+ appendSyntaxHighlightedCSS(syntax, elements.syntax);
+
+ // hide the throbber
+ elements.info.classList.remove("devtools-throbber");
+
+ deferred.resolve(this);
+ }
+
+ /**
+ * This is called if we failed to get the docs content.
+ * Sets the content to contain an error message, and disables the throbber.
+ */
+ function gotError(error) {
+ // show error message
+ elements.summary.textContent = L10N.getStr("docsTooltip.loadDocsError");
+
+ // hide the throbber
+ elements.info.classList.remove("devtools-throbber");
+
+ // although gotError is called when there's an error, we have handled
+ // the error, so call resolve not reject.
+ deferred.resolve(this);
+ }
+
+ let deferred = defer();
+ let elements = this.elements;
+
+ initializeDocument(propertyName);
+ getCssDocs(propertyName).then(finalizeDocument, gotError);
+
+ return deferred.promise;
+ },
+
+ destroy: function () {
+ this.elements = null;
+ }
+};
+
+/**
+ * Test whether a node is all whitespace.
+ *
+ * @return {boolean}
+ * True if the node all whitespace, otherwise false.
+ */
+function isAllWhitespace(node) {
+ return !(/[^\t\n\r ]/.test(node.textContent));
+}
+
+/**
+ * Test whether a node is a comment or whitespace node.
+ *
+ * @return {boolean}
+ * True if the node is a comment node or is all whitespace, otherwise false.
+ */
+function isIgnorable(node) {
+ // Comment nodes (8), text nodes (3) or whitespace
+ return (node.nodeType == 8) ||
+ ((node.nodeType == 3) && isAllWhitespace(node));
+}
+
+/**
+ * Get the next node, skipping comments and whitespace.
+ *
+ * @return {node}
+ * The next sibling node that is not a comment or whitespace, or null if
+ * there isn't one.
+ */
+function nodeAfter(sib) {
+ while ((sib = sib.nextSibling)) {
+ if (!isIgnorable(sib)) {
+ return sib;
+ }
+ }
+ return null;
+}
+
+/**
+ * Test whether the argument `node` is a node whose tag is `tagName`.
+ *
+ * @param {node} node
+ * The code to test. May be null.
+ *
+ * @param {string} tagName
+ * The tag name to test against.
+ *
+ * @return {boolean}
+ * True if the node is not null and has the tag name `tagName`,
+ * otherwise false.
+ */
+function hasTagName(node, tagName) {
+ return node && node.tagName &&
+ node.tagName.toLowerCase() == tagName.toLowerCase();
+}
+
+/**
+ * Given an MDN page, get the "summary" portion.
+ *
+ * This is the textContent of the first non-whitespace
+ * element in the #Summary section of the document.
+ *
+ * It's expected to be a <P> element.
+ *
+ * @param {Document} mdnDocument
+ * The document in which to look for the "summary" section.
+ *
+ * @return {string}
+ * The summary section as a string, or null if it could not be found.
+ */
+function getSummary(mdnDocument) {
+ let summary = mdnDocument.getElementById("Summary");
+ if (!hasTagName(summary, "H2")) {
+ return null;
+ }
+
+ let firstParagraph = nodeAfter(summary);
+ if (!hasTagName(firstParagraph, "P")) {
+ return null;
+ }
+
+ return firstParagraph.textContent;
+}
+
+/**
+ * Given an MDN page, get the "syntax" portion.
+ *
+ * First we get the #Syntax section of the document. The syntax
+ * section we want is somewhere inside there.
+ *
+ * If the page is in the old structure, then the *first two*
+ * non-whitespace elements in the #Syntax section will be <PRE>
+ * nodes, and the second of these will be the syntax section.
+ *
+ * If the page is in the new structure, then the only the *first*
+ * non-whitespace element in the #Syntax section will be a <PRE>
+ * node, and it will be the syntax section.
+ *
+ * @param {Document} mdnDocument
+ * The document in which to look for the "syntax" section.
+ *
+ * @return {string}
+ * The syntax section as a string, or null if it could not be found.
+ */
+function getSyntax(mdnDocument) {
+ let syntax = mdnDocument.getElementById("Syntax");
+ if (!hasTagName(syntax, "H2")) {
+ return null;
+ }
+
+ let firstParagraph = nodeAfter(syntax);
+ if (!hasTagName(firstParagraph, "PRE")) {
+ return null;
+ }
+
+ let secondParagraph = nodeAfter(firstParagraph);
+ if (hasTagName(secondParagraph, "PRE")) {
+ return secondParagraph.textContent;
+ }
+ return firstParagraph.textContent;
+}
+
+/**
+ * Use a different URL for CSS docs pages. Used only for testing.
+ *
+ * @param {string} baseUrl
+ * The baseURL to use.
+ */
+function setBaseCssDocsUrl(baseUrl) {
+ PAGE_LINK_URL = baseUrl;
+ XHR_CSS_URL = baseUrl;
+}
+
+exports.setBaseCssDocsUrl = setBaseCssDocsUrl;
diff --git a/devtools/client/shared/widgets/MountainGraphWidget.js b/devtools/client/shared/widgets/MountainGraphWidget.js
new file mode 100644
index 000000000..394ac4584
--- /dev/null
+++ b/devtools/client/shared/widgets/MountainGraphWidget.js
@@ -0,0 +1,195 @@
+"use strict";
+
+const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");
+
+// Bar graph constants.
+
+const GRAPH_DAMPEN_VALUES_FACTOR = 0.9;
+
+const GRAPH_BACKGROUND_COLOR = "#ddd";
+// px
+const GRAPH_STROKE_WIDTH = 1;
+const GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
+// px
+const GRAPH_HELPER_LINES_DASH = [5];
+const GRAPH_HELPER_LINES_WIDTH = 1;
+
+const GRAPH_CLIPHEAD_LINE_COLOR = "#fff";
+const GRAPH_SELECTION_LINE_COLOR = "#fff";
+const GRAPH_SELECTION_BACKGROUND_COLOR = "rgba(44,187,15,0.25)";
+const GRAPH_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+const GRAPH_REGION_BACKGROUND_COLOR = "transparent";
+const GRAPH_REGION_STRIPES_COLOR = "rgba(237,38,85,0.2)";
+
+/**
+ * A mountain graph, plotting sets of values as line graphs.
+ *
+ * @see AbstractCanvasGraph for emitted events and other options.
+ *
+ * Example usage:
+ * let graph = new MountainGraphWidget(node);
+ * graph.format = ...;
+ * graph.once("ready", () => {
+ * graph.setData(src);
+ * });
+ *
+ * The `graph.format` traits are mandatory and will determine how each
+ * section of the moutain will be styled:
+ * [
+ * { color: "#f00", ... },
+ * { color: "#0f0", ... },
+ * ...
+ * { color: "#00f", ... }
+ * ]
+ *
+ * Data source format:
+ * [
+ * { delta: x1, values: [y11, y12, ... y1n] },
+ * { delta: x2, values: [y21, y22, ... y2n] },
+ * ...
+ * { delta: xm, values: [ym1, ym2, ... ymn] }
+ * ]
+ * where the [ymn] values is assumed to aready be normalized from [0..1].
+ *
+ * @param nsIDOMNode parent
+ * The parent node holding the graph.
+ */
+this.MountainGraphWidget = function (parent, ...args) {
+ AbstractCanvasGraph.apply(this, [parent, "mountain-graph", ...args]);
+};
+
+MountainGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+ backgroundColor: GRAPH_BACKGROUND_COLOR,
+ strokeColor: GRAPH_STROKE_COLOR,
+ strokeWidth: GRAPH_STROKE_WIDTH,
+ clipheadLineColor: GRAPH_CLIPHEAD_LINE_COLOR,
+ selectionLineColor: GRAPH_SELECTION_LINE_COLOR,
+ selectionBackgroundColor: GRAPH_SELECTION_BACKGROUND_COLOR,
+ selectionStripesColor: GRAPH_SELECTION_STRIPES_COLOR,
+ regionBackgroundColor: GRAPH_REGION_BACKGROUND_COLOR,
+ regionStripesColor: GRAPH_REGION_STRIPES_COLOR,
+
+ /**
+ * List of rules used to style each section of the mountain.
+ * @see constructor
+ * @type array
+ */
+ format: null,
+
+ /**
+ * Optionally offsets the `delta` in the data source by this scalar.
+ */
+ dataOffsetX: 0,
+
+ /**
+ * Optionally uses this value instead of the last tick in the data source
+ * to compute the horizontal scaling.
+ */
+ dataDuration: 0,
+
+ /**
+ * The scalar used to multiply the graph values to leave some headroom
+ * on the top.
+ */
+ dampenValuesFactor: GRAPH_DAMPEN_VALUES_FACTOR,
+
+ /**
+ * Renders the graph's background.
+ * @see AbstractCanvasGraph.prototype.buildBackgroundImage
+ */
+ buildBackgroundImage: function () {
+ let { canvas, ctx } = this._getNamedCanvas("mountain-graph-background");
+ let width = this._width;
+ let height = this._height;
+
+ ctx.fillStyle = this.backgroundColor;
+ ctx.fillRect(0, 0, width, height);
+
+ return canvas;
+ },
+
+ /**
+ * Renders the graph's data source.
+ * @see AbstractCanvasGraph.prototype.buildGraphImage
+ */
+ buildGraphImage: function () {
+ if (!this.format || !this.format.length) {
+ throw new Error("The graph format traits are mandatory to style " +
+ "the data source.");
+ }
+ let { canvas, ctx } = this._getNamedCanvas("mountain-graph-data");
+ let width = this._width;
+ let height = this._height;
+
+ let totalSections = this.format.length;
+ let totalTicks = this._data.length;
+ let firstTick = totalTicks ? this._data[0].delta : 0;
+ let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0;
+
+ let duration = this.dataDuration || lastTick;
+ let dataScaleX = this.dataScaleX = width / (duration - this.dataOffsetX);
+ let dataScaleY = this.dataScaleY = height * this.dampenValuesFactor;
+
+ // Draw the graph.
+
+ let prevHeights = Array.from({ length: totalTicks }).fill(0);
+
+ ctx.globalCompositeOperation = "destination-over";
+ ctx.strokeStyle = this.strokeColor;
+ ctx.lineWidth = this.strokeWidth * this._pixelRatio;
+
+ for (let section = 0; section < totalSections; section++) {
+ ctx.fillStyle = this.format[section].color || "#000";
+ ctx.beginPath();
+
+ for (let tick = 0; tick < totalTicks; tick++) {
+ let { delta, values } = this._data[tick];
+ let currX = (delta - this.dataOffsetX) * dataScaleX;
+ let currY = values[section] * dataScaleY;
+ let prevY = prevHeights[tick];
+
+ if (delta == firstTick) {
+ ctx.moveTo(-GRAPH_STROKE_WIDTH, height);
+ ctx.lineTo(-GRAPH_STROKE_WIDTH, height - currY - prevY);
+ }
+
+ ctx.lineTo(currX, height - currY - prevY);
+
+ if (delta == lastTick) {
+ ctx.lineTo(width + GRAPH_STROKE_WIDTH, height - currY - prevY);
+ ctx.lineTo(width + GRAPH_STROKE_WIDTH, height);
+ }
+
+ prevHeights[tick] += currY;
+ }
+
+ ctx.fill();
+ ctx.stroke();
+ }
+
+ ctx.globalCompositeOperation = "source-over";
+ ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
+ ctx.setLineDash(GRAPH_HELPER_LINES_DASH);
+
+ // Draw the maximum value horizontal line.
+
+ ctx.beginPath();
+ let maximumY = height * this.dampenValuesFactor;
+ ctx.moveTo(0, maximumY);
+ ctx.lineTo(width, maximumY);
+ ctx.stroke();
+
+ // Draw the average value horizontal line.
+
+ ctx.beginPath();
+ let averageY = height / 2 * this.dampenValuesFactor;
+ ctx.moveTo(0, averageY);
+ ctx.lineTo(width, averageY);
+ ctx.stroke();
+
+ return canvas;
+ }
+});
+
+module.exports = MountainGraphWidget;
diff --git a/devtools/client/shared/widgets/SideMenuWidget.jsm b/devtools/client/shared/widgets/SideMenuWidget.jsm
new file mode 100644
index 000000000..0c132f232
--- /dev/null
+++ b/devtools/client/shared/widgets/SideMenuWidget.jsm
@@ -0,0 +1,725 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const EventEmitter = require("devtools/shared/event-emitter");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
+
+this.EXPORTED_SYMBOLS = ["SideMenuWidget"];
+
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(SHARED_STRINGS_URI);
+
+/**
+ * A simple side menu, with the ability of grouping menu items.
+ *
+ * Note: this widget should be used in tandem with the WidgetMethods in
+ * view-helpers.js.
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ * @param Object aOptions
+ * - contextMenu: optional element or element ID that serves as a context menu.
+ * - showArrows: specifies if items should display horizontal arrows.
+ * - showItemCheckboxes: specifies if items should display checkboxes.
+ * - showGroupCheckboxes: specifies if groups should display checkboxes.
+ */
+this.SideMenuWidget = function SideMenuWidget(aNode, aOptions = {}) {
+ this.document = aNode.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = aNode;
+
+ let { contextMenu, showArrows, showItemCheckboxes, showGroupCheckboxes } = aOptions;
+ this._contextMenu = contextMenu || null;
+ this._showArrows = showArrows || false;
+ this._showItemCheckboxes = showItemCheckboxes || false;
+ this._showGroupCheckboxes = showGroupCheckboxes || false;
+
+ // Create an internal scrollbox container.
+ this._list = this.document.createElement("scrollbox");
+ this._list.className = "side-menu-widget-container theme-sidebar";
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("orient", "vertical");
+ this._list.setAttribute("with-arrows", this._showArrows);
+ this._list.setAttribute("with-item-checkboxes", this._showItemCheckboxes);
+ this._list.setAttribute("with-group-checkboxes", this._showGroupCheckboxes);
+ this._list.setAttribute("tabindex", "0");
+ this._list.addEventListener("contextmenu", e => this._showContextMenu(e), false);
+ this._list.addEventListener("keypress", e => this.emit("keyPress", e), false);
+ this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false);
+ this._parent.appendChild(this._list);
+
+ // Menu items can optionally be grouped.
+ this._groupsByName = new Map(); // Can't use a WeakMap because keys are strings.
+ this._orderedGroupElementsArray = [];
+ this._orderedMenuElementsArray = [];
+ this._itemsByElement = new Map();
+
+ // This widget emits events that can be handled in a MenuContainer.
+ EventEmitter.decorate(this);
+
+ // Delegate some of the associated node's methods to satisfy the interface
+ // required by MenuContainer instances.
+ ViewHelpers.delegateWidgetAttributeMethods(this, aNode);
+ ViewHelpers.delegateWidgetEventMethods(this, aNode);
+};
+
+SideMenuWidget.prototype = {
+ /**
+ * Specifies if groups in this container should be sorted.
+ */
+ sortedGroups: true,
+
+ /**
+ * The comparator used to sort groups.
+ */
+ groupSortPredicate: (a, b) => a.localeCompare(b),
+
+ /**
+ * Inserts an item in this container at the specified index, optionally
+ * grouping by name.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param nsIDOMNode aContents
+ * The node displayed in the container.
+ * @param object aAttachment [optional]
+ * Some attached primitive/object. Custom options supported:
+ * - group: a string specifying the group to place this item into
+ * - checkboxState: the checked state of the checkbox, if shown
+ * - checkboxTooltip: the tooltip text for the checkbox, if shown
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertItemAt: function (aIndex, aContents, aAttachment = {}) {
+ let group = this._getMenuGroupForName(aAttachment.group);
+ let item = this._getMenuItemForGroup(group, aContents, aAttachment);
+ let element = item.insertSelfAt(aIndex);
+
+ return element;
+ },
+
+ /**
+ * Checks to see if the list is scrolled all the way to the bottom.
+ * Uses getBoundsWithoutFlushing to limit the performance impact
+ * of this function.
+ *
+ * @return bool
+ */
+ isScrolledToBottom: function () {
+ if (this._list.lastElementChild) {
+ let utils = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ let childRect = utils.getBoundsWithoutFlushing(this._list.lastElementChild);
+ let listRect = utils.getBoundsWithoutFlushing(this._list);
+
+ // Cheap way to check if it's scrolled all the way to the bottom.
+ return (childRect.height + childRect.top) <= listRect.bottom;
+ }
+
+ return false;
+ },
+
+ /**
+ * Scroll the list to the bottom after a timeout.
+ * If the user scrolls in the meantime, cancel this operation.
+ */
+ scrollToBottom: function () {
+ this._list.scrollTop = this._list.scrollHeight;
+ this.emit("scroll-to-bottom");
+ },
+
+ /**
+ * Returns the child node in this container situated at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ getItemAtIndex: function (aIndex) {
+ return this._orderedMenuElementsArray[aIndex];
+ },
+
+ /**
+ * Removes the specified child node from this container.
+ *
+ * @param nsIDOMNode aChild
+ * The element associated with the displayed item.
+ */
+ removeChild: function (aChild) {
+ this._getNodeForContents(aChild).remove();
+
+ this._orderedMenuElementsArray.splice(
+ this._orderedMenuElementsArray.indexOf(aChild), 1);
+
+ this._itemsByElement.delete(aChild);
+
+ if (this._selectedItem == aChild) {
+ this._selectedItem = null;
+ }
+ },
+
+ /**
+ * Removes all of the child nodes from this container.
+ */
+ removeAllItems: function () {
+ let parent = this._parent;
+ let list = this._list;
+
+ while (list.hasChildNodes()) {
+ list.firstChild.remove();
+ }
+
+ this._selectedItem = null;
+
+ this._groupsByName.clear();
+ this._orderedGroupElementsArray.length = 0;
+ this._orderedMenuElementsArray.length = 0;
+ this._itemsByElement.clear();
+ },
+
+ /**
+ * Gets the currently selected child node in this container.
+ * @return nsIDOMNode
+ */
+ get selectedItem() {
+ return this._selectedItem;
+ },
+
+ /**
+ * Sets the currently selected child node in this container.
+ * @param nsIDOMNode aChild
+ */
+ set selectedItem(aChild) {
+ let menuArray = this._orderedMenuElementsArray;
+
+ if (!aChild) {
+ this._selectedItem = null;
+ }
+ for (let node of menuArray) {
+ if (node == aChild) {
+ this._getNodeForContents(node).classList.add("selected");
+ this._selectedItem = node;
+ } else {
+ this._getNodeForContents(node).classList.remove("selected");
+ }
+ }
+ },
+
+ /**
+ * Ensures the specified element is visible.
+ *
+ * @param nsIDOMNode aElement
+ * The element to make visible.
+ */
+ ensureElementIsVisible: function (aElement) {
+ if (!aElement) {
+ return;
+ }
+
+ // Ensure the element is visible but not scrolled horizontally.
+ let boxObject = this._list.boxObject;
+ boxObject.ensureElementIsVisible(aElement);
+ boxObject.scrollBy(-this._list.clientWidth, 0);
+ },
+
+ /**
+ * Shows all the groups, even the ones with no visible children.
+ */
+ showEmptyGroups: function () {
+ for (let group of this._orderedGroupElementsArray) {
+ group.hidden = false;
+ }
+ },
+
+ /**
+ * Hides all the groups which have no visible children.
+ */
+ hideEmptyGroups: function () {
+ let visibleChildNodes = ".side-menu-widget-item-contents:not([hidden=true])";
+
+ for (let group of this._orderedGroupElementsArray) {
+ group.hidden = group.querySelectorAll(visibleChildNodes).length == 0;
+ }
+ for (let menuItem of this._orderedMenuElementsArray) {
+ menuItem.parentNode.hidden = menuItem.hidden;
+ }
+ },
+
+ /**
+ * Adds a new attribute or changes an existing attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ * @param string aValue
+ * The desired attribute value.
+ */
+ setAttribute: function (aName, aValue) {
+ this._parent.setAttribute(aName, aValue);
+
+ if (aName == "emptyText") {
+ this._textWhenEmpty = aValue;
+ }
+ },
+
+ /**
+ * Removes an attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ */
+ removeAttribute: function (aName) {
+ this._parent.removeAttribute(aName);
+
+ if (aName == "emptyText") {
+ this._removeEmptyText();
+ }
+ },
+
+ /**
+ * Set the checkbox state for the item associated with the given node.
+ *
+ * @param nsIDOMNode aNode
+ * The dom node for an item we want to check.
+ * @param boolean aCheckState
+ * True to check, false to uncheck.
+ */
+ checkItem: function (aNode, aCheckState) {
+ const widgetItem = this._itemsByElement.get(aNode);
+ if (!widgetItem) {
+ throw new Error("No item for " + aNode);
+ }
+ widgetItem.check(aCheckState);
+ },
+
+ /**
+ * Sets the text displayed in this container when empty.
+ * @param string aValue
+ */
+ set _textWhenEmpty(aValue) {
+ if (this._emptyTextNode) {
+ this._emptyTextNode.setAttribute("value", aValue);
+ }
+ this._emptyTextValue = aValue;
+ this._showEmptyText();
+ },
+
+ /**
+ * Creates and appends a label signaling that this container is empty.
+ */
+ _showEmptyText: function () {
+ if (this._emptyTextNode || !this._emptyTextValue) {
+ return;
+ }
+ let label = this.document.createElement("label");
+ label.className = "plain side-menu-widget-empty-text";
+ label.setAttribute("value", this._emptyTextValue);
+
+ this._parent.insertBefore(label, this._list);
+ this._emptyTextNode = label;
+ },
+
+ /**
+ * Removes the label representing a notice in this container.
+ */
+ _removeEmptyText: function () {
+ if (!this._emptyTextNode) {
+ return;
+ }
+
+ this._parent.removeChild(this._emptyTextNode);
+ this._emptyTextNode = null;
+ },
+
+ /**
+ * Gets a container representing a group for menu items. If the container
+ * is not available yet, it is immediately created.
+ *
+ * @param string aName
+ * The required group name.
+ * @return SideMenuGroup
+ * The newly created group.
+ */
+ _getMenuGroupForName: function (aName) {
+ let cachedGroup = this._groupsByName.get(aName);
+ if (cachedGroup) {
+ return cachedGroup;
+ }
+
+ let group = new SideMenuGroup(this, aName, {
+ showCheckbox: this._showGroupCheckboxes
+ });
+
+ this._groupsByName.set(aName, group);
+ group.insertSelfAt(this.sortedGroups ? group.findExpectedIndexForSelf(this.groupSortPredicate) : -1);
+
+ return group;
+ },
+
+ /**
+ * Gets a menu item to be displayed inside a group.
+ * @see SideMenuWidget.prototype._getMenuGroupForName
+ *
+ * @param SideMenuGroup aGroup
+ * The group to contain the menu item.
+ * @param nsIDOMNode aContents
+ * The node displayed in the container.
+ * @param object aAttachment [optional]
+ * Some attached primitive/object.
+ */
+ _getMenuItemForGroup: function (aGroup, aContents, aAttachment) {
+ return new SideMenuItem(aGroup, aContents, aAttachment, {
+ showArrow: this._showArrows,
+ showCheckbox: this._showItemCheckboxes
+ });
+ },
+
+ /**
+ * Returns the .side-menu-widget-item node corresponding to a SideMenuItem.
+ * To optimize the markup, some redundant elemenst are skipped when creating
+ * these child items, in which case we need to be careful on which nodes
+ * .selected class names are added, or which nodes are removed.
+ *
+ * @param nsIDOMNode aChild
+ * An element which is the target node of a SideMenuItem.
+ * @return nsIDOMNode
+ * The wrapper node if there is one, or the same child otherwise.
+ */
+ _getNodeForContents: function (aChild) {
+ if (aChild.hasAttribute("merged-item-contents")) {
+ return aChild;
+ } else {
+ return aChild.parentNode;
+ }
+ },
+
+ /**
+ * Shows the contextMenu element.
+ */
+ _showContextMenu: function (e) {
+ if (!this._contextMenu) {
+ return;
+ }
+
+ // Don't show the menu if a descendant node is going to be visible also.
+ let node = e.originalTarget;
+ while (node && node !== this._list) {
+ if (node.hasAttribute("contextmenu")) {
+ return;
+ }
+ node = node.parentNode;
+ }
+
+ this._contextMenu.openPopupAtScreen(e.screenX, e.screenY, true);
+ },
+
+ window: null,
+ document: null,
+ _showArrows: false,
+ _showItemCheckboxes: false,
+ _showGroupCheckboxes: false,
+ _parent: null,
+ _list: null,
+ _selectedItem: null,
+ _groupsByName: null,
+ _orderedGroupElementsArray: null,
+ _orderedMenuElementsArray: null,
+ _itemsByElement: null,
+ _emptyTextNode: null,
+ _emptyTextValue: ""
+};
+
+/**
+ * A SideMenuGroup constructor for the BreadcrumbsWidget.
+ * Represents a group which should contain SideMenuItems.
+ *
+ * @param SideMenuWidget aWidget
+ * The widget to contain this menu item.
+ * @param string aName
+ * The string displayed in the container.
+ * @param object aOptions [optional]
+ * An object containing the following properties:
+ * - showCheckbox: specifies if a checkbox should be displayed.
+ */
+function SideMenuGroup(aWidget, aName, aOptions = {}) {
+ this.document = aWidget.document;
+ this.window = aWidget.window;
+ this.ownerView = aWidget;
+ this.identifier = aName;
+
+ // Create an internal title and list container.
+ if (aName) {
+ let target = this._target = this.document.createElement("vbox");
+ target.className = "side-menu-widget-group";
+ target.setAttribute("name", aName);
+
+ let list = this._list = this.document.createElement("vbox");
+ list.className = "side-menu-widget-group-list";
+
+ let title = this._title = this.document.createElement("hbox");
+ title.className = "side-menu-widget-group-title";
+
+ let name = this._name = this.document.createElement("label");
+ name.className = "plain name";
+ name.setAttribute("value", aName);
+ name.setAttribute("crop", "end");
+ name.setAttribute("flex", "1");
+
+ // Show a checkbox before the content.
+ if (aOptions.showCheckbox) {
+ let checkbox = this._checkbox = makeCheckbox(title, {
+ description: aName,
+ checkboxTooltip: L10N.getStr("sideMenu.groupCheckbox.tooltip")
+ });
+ checkbox.className = "side-menu-widget-group-checkbox";
+ }
+
+ title.appendChild(name);
+ target.appendChild(title);
+ target.appendChild(list);
+ }
+ // Skip a few redundant nodes when no title is shown.
+ else {
+ let target = this._target = this._list = this.document.createElement("vbox");
+ target.className = "side-menu-widget-group side-menu-widget-group-list";
+ target.setAttribute("merged-group-contents", "");
+ }
+}
+
+SideMenuGroup.prototype = {
+ get _orderedGroupElementsArray() {
+ return this.ownerView._orderedGroupElementsArray;
+ },
+ get _orderedMenuElementsArray() {
+ return this.ownerView._orderedMenuElementsArray;
+ },
+ get _itemsByElement() { return this.ownerView._itemsByElement; },
+
+ /**
+ * Inserts this group in the parent container at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this group.
+ */
+ insertSelfAt: function (aIndex) {
+ let ownerList = this.ownerView._list;
+ let groupsArray = this._orderedGroupElementsArray;
+
+ if (aIndex >= 0) {
+ ownerList.insertBefore(this._target, groupsArray[aIndex]);
+ groupsArray.splice(aIndex, 0, this._target);
+ } else {
+ ownerList.appendChild(this._target);
+ groupsArray.push(this._target);
+ }
+ },
+
+ /**
+ * Finds the expected index of this group based on its name.
+ *
+ * @return number
+ * The expected index.
+ */
+ findExpectedIndexForSelf: function (sortPredicate) {
+ let identifier = this.identifier;
+ let groupsArray = this._orderedGroupElementsArray;
+
+ for (let group of groupsArray) {
+ let name = group.getAttribute("name");
+ if (sortPredicate(name, identifier) > 0 && // Insertion sort at its best :)
+ !name.includes(identifier)) { // Least significant group should be last.
+ return groupsArray.indexOf(group);
+ }
+ }
+ return -1;
+ },
+
+ window: null,
+ document: null,
+ ownerView: null,
+ identifier: "",
+ _target: null,
+ _checkbox: null,
+ _title: null,
+ _name: null,
+ _list: null
+};
+
+/**
+ * A SideMenuItem constructor for the BreadcrumbsWidget.
+ *
+ * @param SideMenuGroup aGroup
+ * The group to contain this menu item.
+ * @param nsIDOMNode aContents
+ * The node displayed in the container.
+ * @param object aAttachment [optional]
+ * The attachment object.
+ * @param object aOptions [optional]
+ * An object containing the following properties:
+ * - showArrow: specifies if a horizontal arrow should be displayed.
+ * - showCheckbox: specifies if a checkbox should be displayed.
+ */
+function SideMenuItem(aGroup, aContents, aAttachment = {}, aOptions = {}) {
+ this.document = aGroup.document;
+ this.window = aGroup.window;
+ this.ownerView = aGroup;
+
+ if (aOptions.showArrow || aOptions.showCheckbox) {
+ let container = this._container = this.document.createElement("hbox");
+ container.className = "side-menu-widget-item";
+
+ let target = this._target = this.document.createElement("vbox");
+ target.className = "side-menu-widget-item-contents";
+
+ // Show a checkbox before the content.
+ if (aOptions.showCheckbox) {
+ let checkbox = this._checkbox = makeCheckbox(container, aAttachment);
+ checkbox.className = "side-menu-widget-item-checkbox";
+ }
+
+ container.appendChild(target);
+
+ // Show a horizontal arrow towards the content.
+ if (aOptions.showArrow) {
+ let arrow = this._arrow = this.document.createElement("hbox");
+ arrow.className = "side-menu-widget-item-arrow";
+ container.appendChild(arrow);
+ }
+ }
+ // Skip a few redundant nodes when no horizontal arrow or checkbox is shown.
+ else {
+ let target = this._target = this._container = this.document.createElement("hbox");
+ target.className = "side-menu-widget-item side-menu-widget-item-contents";
+ target.setAttribute("merged-item-contents", "");
+ }
+
+ this._target.setAttribute("flex", "1");
+ this.contents = aContents;
+}
+
+SideMenuItem.prototype = {
+ get _orderedGroupElementsArray() {
+ return this.ownerView._orderedGroupElementsArray;
+ },
+ get _orderedMenuElementsArray() {
+ return this.ownerView._orderedMenuElementsArray;
+ },
+ get _itemsByElement() { return this.ownerView._itemsByElement; },
+
+ /**
+ * Inserts this item in the parent group at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertSelfAt: function (aIndex) {
+ let ownerList = this.ownerView._list;
+ let menuArray = this._orderedMenuElementsArray;
+
+ if (aIndex >= 0) {
+ ownerList.insertBefore(this._container, ownerList.childNodes[aIndex]);
+ menuArray.splice(aIndex, 0, this._target);
+ } else {
+ ownerList.appendChild(this._container);
+ menuArray.push(this._target);
+ }
+ this._itemsByElement.set(this._target, this);
+
+ return this._target;
+ },
+
+ /**
+ * Check or uncheck the checkbox associated with this item.
+ *
+ * @param boolean aCheckState
+ * True to check, false to uncheck.
+ */
+ check: function (aCheckState) {
+ if (!this._checkbox) {
+ throw new Error("Cannot check items that do not have checkboxes.");
+ }
+ // Don't set or remove the "checked" attribute, assign the property instead.
+ // Otherwise, the "CheckboxStateChange" event will not be fired. XUL!!
+ this._checkbox.checked = !!aCheckState;
+ },
+
+ /**
+ * Sets the contents displayed in this item's view.
+ *
+ * @param string | nsIDOMNode aContents
+ * The string or node displayed in the container.
+ */
+ set contents(aContents) {
+ // If there are already some contents displayed, replace them.
+ if (this._target.hasChildNodes()) {
+ this._target.replaceChild(aContents, this._target.firstChild);
+ return;
+ }
+ // These are the first contents ever displayed.
+ this._target.appendChild(aContents);
+ },
+
+ window: null,
+ document: null,
+ ownerView: null,
+ _target: null,
+ _container: null,
+ _checkbox: null,
+ _arrow: null
+};
+
+/**
+ * Creates a checkbox to a specified parent node. Emits a "check" event
+ * whenever the checkbox is checked or unchecked by the user.
+ *
+ * @param nsIDOMNode aParentNode
+ * The parent node to contain this checkbox.
+ * @param object aOptions
+ * An object containing some or all of the following properties:
+ * - description: defaults to "item" if unspecified
+ * - checkboxState: true for checked, false for unchecked
+ * - checkboxTooltip: the tooltip text of the checkbox
+ */
+function makeCheckbox(aParentNode, aOptions) {
+ let checkbox = aParentNode.ownerDocument.createElement("checkbox");
+
+ checkbox.setAttribute("tooltiptext", aOptions.checkboxTooltip || "");
+
+ if (aOptions.checkboxState) {
+ checkbox.setAttribute("checked", true);
+ } else {
+ checkbox.removeAttribute("checked");
+ }
+
+ // Stop the toggling of the checkbox from selecting the list item.
+ checkbox.addEventListener("mousedown", e => {
+ e.stopPropagation();
+ }, false);
+
+ // Emit an event from the checkbox when it is toggled. Don't listen for the
+ // "command" event! It won't fire for programmatic changes. XUL!!
+ checkbox.addEventListener("CheckboxStateChange", e => {
+ ViewHelpers.dispatchEvent(checkbox, "check", {
+ description: aOptions.description || "item",
+ checked: checkbox.checked
+ });
+ }, false);
+
+ aParentNode.appendChild(checkbox);
+ return checkbox;
+}
diff --git a/devtools/client/shared/widgets/SimpleListWidget.jsm b/devtools/client/shared/widgets/SimpleListWidget.jsm
new file mode 100644
index 000000000..ec47ab0da
--- /dev/null
+++ b/devtools/client/shared/widgets/SimpleListWidget.jsm
@@ -0,0 +1,255 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
+
+this.EXPORTED_SYMBOLS = ["SimpleListWidget"];
+
+/**
+ * A very simple vertical list view.
+ *
+ * Note: this widget should be used in tandem with the WidgetMethods in
+ * view-helpers.js.
+ *
+ * @param nsIDOMNode aNode
+ * The element associated with the widget.
+ */
+function SimpleListWidget(aNode) {
+ this.document = aNode.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = aNode;
+
+ // Create an internal list container.
+ this._list = this.document.createElement("scrollbox");
+ this._list.className = "simple-list-widget-container theme-body";
+ this._list.setAttribute("flex", "1");
+ this._list.setAttribute("orient", "vertical");
+ this._parent.appendChild(this._list);
+
+ // Delegate some of the associated node's methods to satisfy the interface
+ // required by WidgetMethods instances.
+ ViewHelpers.delegateWidgetAttributeMethods(this, aNode);
+ ViewHelpers.delegateWidgetEventMethods(this, aNode);
+}
+this.SimpleListWidget = SimpleListWidget;
+
+SimpleListWidget.prototype = {
+ /**
+ * Inserts an item in this container at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @param nsIDOMNode aContents
+ * The node displayed in the container.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ insertItemAt: function (aIndex, aContents) {
+ aContents.classList.add("simple-list-widget-item");
+
+ let list = this._list;
+ return list.insertBefore(aContents, list.childNodes[aIndex]);
+ },
+
+ /**
+ * Returns the child node in this container situated at the specified index.
+ *
+ * @param number aIndex
+ * The position in the container intended for this item.
+ * @return nsIDOMNode
+ * The element associated with the displayed item.
+ */
+ getItemAtIndex: function (aIndex) {
+ return this._list.childNodes[aIndex];
+ },
+
+ /**
+ * Immediately removes the specified child node from this container.
+ *
+ * @param nsIDOMNode aChild
+ * The element associated with the displayed item.
+ */
+ removeChild: function (aChild) {
+ this._list.removeChild(aChild);
+
+ if (this._selectedItem == aChild) {
+ this._selectedItem = null;
+ }
+ },
+
+ /**
+ * Removes all of the child nodes from this container.
+ */
+ removeAllItems: function () {
+ let list = this._list;
+ let parent = this._parent;
+
+ while (list.hasChildNodes()) {
+ list.firstChild.remove();
+ }
+
+ parent.scrollTop = 0;
+ parent.scrollLeft = 0;
+ this._selectedItem = null;
+ },
+
+ /**
+ * Gets the currently selected child node in this container.
+ * @return nsIDOMNode
+ */
+ get selectedItem() {
+ return this._selectedItem;
+ },
+
+ /**
+ * Sets the currently selected child node in this container.
+ * @param nsIDOMNode aChild
+ */
+ set selectedItem(aChild) {
+ let childNodes = this._list.childNodes;
+
+ if (!aChild) {
+ this._selectedItem = null;
+ }
+ for (let node of childNodes) {
+ if (node == aChild) {
+ node.classList.add("selected");
+ this._selectedItem = node;
+ } else {
+ node.classList.remove("selected");
+ }
+ }
+ },
+
+ /**
+ * Adds a new attribute or changes an existing attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ * @param string aValue
+ * The desired attribute value.
+ */
+ setAttribute: function (aName, aValue) {
+ this._parent.setAttribute(aName, aValue);
+
+ if (aName == "emptyText") {
+ this._textWhenEmpty = aValue;
+ } else if (aName == "headerText") {
+ this._textAsHeader = aValue;
+ }
+ },
+
+ /**
+ * Removes an attribute on this container.
+ *
+ * @param string aName
+ * The name of the attribute.
+ */
+ removeAttribute: function (aName) {
+ this._parent.removeAttribute(aName);
+
+ if (aName == "emptyText") {
+ this._removeEmptyText();
+ }
+ },
+
+ /**
+ * Ensures the specified element is visible.
+ *
+ * @param nsIDOMNode aElement
+ * The element to make visible.
+ */
+ ensureElementIsVisible: function (aElement) {
+ if (!aElement) {
+ return;
+ }
+
+ // Ensure the element is visible but not scrolled horizontally.
+ let boxObject = this._list.boxObject;
+ boxObject.ensureElementIsVisible(aElement);
+ boxObject.scrollBy(-this._list.clientWidth, 0);
+ },
+
+ /**
+ * Sets the text displayed permanently in this container as a header.
+ * @param string aValue
+ */
+ set _textAsHeader(aValue) {
+ if (this._headerTextNode) {
+ this._headerTextNode.setAttribute("value", aValue);
+ }
+ this._headerTextValue = aValue;
+ this._showHeaderText();
+ },
+
+ /**
+ * Sets the text displayed in this container when empty.
+ * @param string aValue
+ */
+ set _textWhenEmpty(aValue) {
+ if (this._emptyTextNode) {
+ this._emptyTextNode.setAttribute("value", aValue);
+ }
+ this._emptyTextValue = aValue;
+ this._showEmptyText();
+ },
+
+ /**
+ * Creates and appends a label displayed as this container's header.
+ */
+ _showHeaderText: function () {
+ if (this._headerTextNode || !this._headerTextValue) {
+ return;
+ }
+ let label = this.document.createElement("label");
+ label.className = "plain simple-list-widget-perma-text";
+ label.setAttribute("value", this._headerTextValue);
+
+ this._parent.insertBefore(label, this._list);
+ this._headerTextNode = label;
+ },
+
+ /**
+ * Creates and appends a label signaling that this container is empty.
+ */
+ _showEmptyText: function () {
+ if (this._emptyTextNode || !this._emptyTextValue) {
+ return;
+ }
+ let label = this.document.createElement("label");
+ label.className = "plain simple-list-widget-empty-text";
+ label.setAttribute("value", this._emptyTextValue);
+
+ this._parent.appendChild(label);
+ this._emptyTextNode = label;
+ },
+
+ /**
+ * Removes the label signaling that this container is empty.
+ */
+ _removeEmptyText: function () {
+ if (!this._emptyTextNode) {
+ return;
+ }
+ this._parent.removeChild(this._emptyTextNode);
+ this._emptyTextNode = null;
+ },
+
+ window: null,
+ document: null,
+ _parent: null,
+ _list: null,
+ _selectedItem: null,
+ _headerTextNode: null,
+ _headerTextValue: "",
+ _emptyTextNode: null,
+ _emptyTextValue: ""
+};
diff --git a/devtools/client/shared/widgets/Spectrum.js b/devtools/client/shared/widgets/Spectrum.js
new file mode 100644
index 000000000..00110f13e
--- /dev/null
+++ b/devtools/client/shared/widgets/Spectrum.js
@@ -0,0 +1,336 @@
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * Spectrum creates a color picker widget in any container you give it.
+ *
+ * Simple usage example:
+ *
+ * const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
+ * let s = new Spectrum(containerElement, [255, 126, 255, 1]);
+ * s.on("changed", (event, rgba, color) => {
+ * console.log("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " +
+ * rgba[3] + ")");
+ * });
+ * s.show();
+ * s.destroy();
+ *
+ * Note that the color picker is hidden by default and you need to call show to
+ * make it appear. This 2 stages initialization helps in cases you are creating
+ * the color picker in a parent element that hasn't been appended anywhere yet
+ * or that is hidden. Calling show() when the parent element is appended and
+ * visible will allow spectrum to correctly initialize its various parts.
+ *
+ * Fires the following events:
+ * - changed : When the user changes the current color
+ */
+function Spectrum(parentEl, rgb) {
+ EventEmitter.decorate(this);
+
+ this.element = parentEl.ownerDocument.createElementNS(XHTML_NS, "div");
+ this.parentEl = parentEl;
+
+ this.element.className = "spectrum-container";
+ this.element.innerHTML = `
+ <div class="spectrum-top">
+ <div class="spectrum-fill"></div>
+ <div class="spectrum-top-inner">
+ <div class="spectrum-color spectrum-box">
+ <div class="spectrum-sat">
+ <div class="spectrum-val">
+ <div class="spectrum-dragger"></div>
+ </div>
+ </div>
+ </div>
+ <div class="spectrum-hue spectrum-box">
+ <div class="spectrum-slider spectrum-slider-control"></div>
+ </div>
+ </div>
+ </div>
+ <div class="spectrum-alpha spectrum-checker spectrum-box">
+ <div class="spectrum-alpha-inner">
+ <div class="spectrum-alpha-handle spectrum-slider-control"></div>
+ </div>
+ </div>
+ `;
+
+ this.onElementClick = this.onElementClick.bind(this);
+ this.element.addEventListener("click", this.onElementClick, false);
+
+ this.parentEl.appendChild(this.element);
+
+ this.slider = this.element.querySelector(".spectrum-hue");
+ this.slideHelper = this.element.querySelector(".spectrum-slider");
+ Spectrum.draggable(this.slider, this.onSliderMove.bind(this));
+
+ this.dragger = this.element.querySelector(".spectrum-color");
+ this.dragHelper = this.element.querySelector(".spectrum-dragger");
+ Spectrum.draggable(this.dragger, this.onDraggerMove.bind(this));
+
+ this.alphaSlider = this.element.querySelector(".spectrum-alpha");
+ this.alphaSliderInner = this.element.querySelector(".spectrum-alpha-inner");
+ this.alphaSliderHelper = this.element.querySelector(".spectrum-alpha-handle");
+ Spectrum.draggable(this.alphaSliderInner, this.onAlphaSliderMove.bind(this));
+
+ if (rgb) {
+ this.rgb = rgb;
+ this.updateUI();
+ }
+}
+
+module.exports.Spectrum = Spectrum;
+
+Spectrum.hsvToRgb = function (h, s, v, a) {
+ let r, g, b;
+
+ let i = Math.floor(h * 6);
+ let f = h * 6 - i;
+ let p = v * (1 - s);
+ let q = v * (1 - f * s);
+ let t = v * (1 - (1 - f) * s);
+
+ switch (i % 6) {
+ case 0: r = v; g = t; b = p; break;
+ case 1: r = q; g = v; b = p; break;
+ case 2: r = p; g = v; b = t; break;
+ case 3: r = p; g = q; b = v; break;
+ case 4: r = t; g = p; b = v; break;
+ case 5: r = v; g = p; b = q; break;
+ }
+
+ return [r * 255, g * 255, b * 255, a];
+};
+
+Spectrum.rgbToHsv = function (r, g, b, a) {
+ r = r / 255;
+ g = g / 255;
+ b = b / 255;
+
+ let max = Math.max(r, g, b), min = Math.min(r, g, b);
+ let h, s, v = max;
+
+ let d = max - min;
+ s = max == 0 ? 0 : d / max;
+
+ if (max == min) {
+ // achromatic
+ h = 0;
+ } else {
+ switch (max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+ h /= 6;
+ }
+ return [h, s, v, a];
+};
+
+Spectrum.draggable = function (element, onmove, onstart, onstop) {
+ onmove = onmove || function () {};
+ onstart = onstart || function () {};
+ onstop = onstop || function () {};
+
+ let doc = element.ownerDocument;
+ let dragging = false;
+ let offset = {};
+ let maxHeight = 0;
+ let maxWidth = 0;
+
+ function prevent(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ function move(e) {
+ if (dragging) {
+ if (e.buttons === 0) {
+ // The button is no longer pressed but we did not get a mouseup event.
+ stop();
+ return;
+ }
+ let pageX = e.pageX;
+ let pageY = e.pageY;
+
+ let dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
+ let dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
+
+ onmove.apply(element, [dragX, dragY]);
+ }
+ }
+
+ function start(e) {
+ let rightclick = e.which === 3;
+
+ if (!rightclick && !dragging) {
+ if (onstart.apply(element, arguments) !== false) {
+ dragging = true;
+ maxHeight = element.offsetHeight;
+ maxWidth = element.offsetWidth;
+
+ offset = element.getBoundingClientRect();
+
+ move(e);
+
+ doc.addEventListener("selectstart", prevent, false);
+ doc.addEventListener("dragstart", prevent, false);
+ doc.addEventListener("mousemove", move, false);
+ doc.addEventListener("mouseup", stop, false);
+
+ prevent(e);
+ }
+ }
+ }
+
+ function stop() {
+ if (dragging) {
+ doc.removeEventListener("selectstart", prevent, false);
+ doc.removeEventListener("dragstart", prevent, false);
+ doc.removeEventListener("mousemove", move, false);
+ doc.removeEventListener("mouseup", stop, false);
+ onstop.apply(element, arguments);
+ }
+ dragging = false;
+ }
+
+ element.addEventListener("mousedown", start, false);
+};
+
+Spectrum.prototype = {
+ set rgb(color) {
+ this.hsv = Spectrum.rgbToHsv(color[0], color[1], color[2], color[3]);
+ },
+
+ get rgb() {
+ let rgb = Spectrum.hsvToRgb(this.hsv[0], this.hsv[1], this.hsv[2],
+ this.hsv[3]);
+ return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]),
+ Math.round(rgb[3] * 100) / 100];
+ },
+
+ get rgbNoSatVal() {
+ let rgb = Spectrum.hsvToRgb(this.hsv[0], 1, 1);
+ return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), rgb[3]];
+ },
+
+ get rgbCssString() {
+ let rgb = this.rgb;
+ return "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " +
+ rgb[3] + ")";
+ },
+
+ show: function () {
+ this.element.classList.add("spectrum-show");
+
+ this.slideHeight = this.slider.offsetHeight;
+ this.dragWidth = this.dragger.offsetWidth;
+ this.dragHeight = this.dragger.offsetHeight;
+ this.dragHelperHeight = this.dragHelper.offsetHeight;
+ this.slideHelperHeight = this.slideHelper.offsetHeight;
+ this.alphaSliderWidth = this.alphaSliderInner.offsetWidth;
+ this.alphaSliderHelperWidth = this.alphaSliderHelper.offsetWidth;
+
+ this.updateUI();
+ },
+
+ onElementClick: function (e) {
+ e.stopPropagation();
+ },
+
+ onSliderMove: function (dragX, dragY) {
+ this.hsv[0] = (dragY / this.slideHeight);
+ this.updateUI();
+ this.onChange();
+ },
+
+ onDraggerMove: function (dragX, dragY) {
+ this.hsv[1] = dragX / this.dragWidth;
+ this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
+ this.updateUI();
+ this.onChange();
+ },
+
+ onAlphaSliderMove: function (dragX, dragY) {
+ this.hsv[3] = dragX / this.alphaSliderWidth;
+ this.updateUI();
+ this.onChange();
+ },
+
+ onChange: function () {
+ this.emit("changed", this.rgb, this.rgbCssString);
+ },
+
+ updateHelperLocations: function () {
+ // If the UI hasn't been shown yet then none of the dimensions will be
+ // correct
+ if (!this.element.classList.contains("spectrum-show")) {
+ return;
+ }
+
+ let h = this.hsv[0];
+ let s = this.hsv[1];
+ let v = this.hsv[2];
+
+ // Placing the color dragger
+ let dragX = s * this.dragWidth;
+ let dragY = this.dragHeight - (v * this.dragHeight);
+ let helperDim = this.dragHelperHeight / 2;
+
+ dragX = Math.max(
+ -helperDim,
+ Math.min(this.dragWidth - helperDim, dragX - helperDim)
+ );
+ dragY = Math.max(
+ -helperDim,
+ Math.min(this.dragHeight - helperDim, dragY - helperDim)
+ );
+
+ this.dragHelper.style.top = dragY + "px";
+ this.dragHelper.style.left = dragX + "px";
+
+ // Placing the hue slider
+ let slideY = (h * this.slideHeight) - this.slideHelperHeight / 2;
+ this.slideHelper.style.top = slideY + "px";
+
+ // Placing the alpha slider
+ let alphaSliderX = (this.hsv[3] * this.alphaSliderWidth) -
+ (this.alphaSliderHelperWidth / 2);
+ this.alphaSliderHelper.style.left = alphaSliderX + "px";
+ },
+
+ updateUI: function () {
+ this.updateHelperLocations();
+
+ let rgb = this.rgb;
+ let rgbNoSatVal = this.rgbNoSatVal;
+
+ let flatColor = "rgb(" + rgbNoSatVal[0] + ", " + rgbNoSatVal[1] + ", " +
+ rgbNoSatVal[2] + ")";
+
+ this.dragger.style.backgroundColor = flatColor;
+
+ let rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
+ let rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)";
+ let alphaGradient = "linear-gradient(to right, " + rgbAlpha0 + ", " +
+ rgbNoAlpha + ")";
+ this.alphaSliderInner.style.background = alphaGradient;
+ },
+
+ destroy: function () {
+ this.element.removeEventListener("click", this.onElementClick, false);
+
+ this.parentEl.removeChild(this.element);
+
+ this.slider = null;
+ this.dragger = null;
+ this.alphaSlider = this.alphaSliderInner = this.alphaSliderHelper = null;
+ this.parentEl = null;
+ this.element = null;
+ }
+};
diff --git a/devtools/client/shared/widgets/TableWidget.js b/devtools/client/shared/widgets/TableWidget.js
new file mode 100644
index 000000000..5dacd1b67
--- /dev/null
+++ b/devtools/client/shared/widgets/TableWidget.js
@@ -0,0 +1,1817 @@
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "setNamedTimeout",
+ "devtools/client/shared/widgets/view-helpers", true);
+loader.lazyRequireGetter(this, "clearNamedTimeout",
+ "devtools/client/shared/widgets/view-helpers", true);
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const AFTER_SCROLL_DELAY = 100;
+
+// Different types of events emitted by the Various components of the
+// TableWidget.
+const EVENTS = {
+ CELL_EDIT: "cell-edit",
+ COLUMN_SORTED: "column-sorted",
+ COLUMN_TOGGLED: "column-toggled",
+ FIELDS_EDITABLE: "fields-editable",
+ HEADER_CONTEXT_MENU: "header-context-menu",
+ ROW_EDIT: "row-edit",
+ ROW_CONTEXT_MENU: "row-context-menu",
+ ROW_REMOVED: "row-removed",
+ ROW_SELECTED: "row-selected",
+ ROW_UPDATED: "row-updated",
+ TABLE_CLEARED: "table-cleared",
+ TABLE_FILTERED: "table-filtered",
+ SCROLL_END: "scroll-end"
+};
+Object.defineProperty(this, "EVENTS", {
+ value: EVENTS,
+ enumerable: true,
+ writable: false
+});
+
+/**
+ * A table widget with various features like resizble/toggleable columns,
+ * sorting, keyboard navigation etc.
+ *
+ * @param {nsIDOMNode} node
+ * The container element for the table widget.
+ * @param {object} options
+ * - initialColumns: map of key vs display name for initial columns of
+ * the table. See @setupColumns for more info.
+ * - uniqueId: the column which will be the unique identifier of each
+ * entry in the table. Default: name.
+ * - wrapTextInElements: Don't ever use 'value' attribute on labels.
+ * Default: false.
+ * - emptyText: text to display when no entries in the table to display.
+ * - highlightUpdated: true to highlight the changed/added row.
+ * - removableColumns: Whether columns are removeable. If set to false,
+ * the context menu in the headers will not appear.
+ * - firstColumn: key of the first column that should appear.
+ * - cellContextMenuId: ID of a <menupopup> element to be set as a
+ * context menu of every cell.
+ */
+function TableWidget(node, options = {}) {
+ EventEmitter.decorate(this);
+
+ this.document = node.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = node;
+
+ let {initialColumns, emptyText, uniqueId, highlightUpdated, removableColumns,
+ firstColumn, wrapTextInElements, cellContextMenuId} = options;
+ this.emptyText = emptyText || "";
+ this.uniqueId = uniqueId || "name";
+ this.wrapTextInElements = wrapTextInElements || false;
+ this.firstColumn = firstColumn || "";
+ this.highlightUpdated = highlightUpdated || false;
+ this.removableColumns = removableColumns !== false;
+ this.cellContextMenuId = cellContextMenuId;
+
+ this.tbody = this.document.createElementNS(XUL_NS, "hbox");
+ this.tbody.className = "table-widget-body theme-body";
+ this.tbody.setAttribute("flex", "1");
+ this.tbody.setAttribute("tabindex", "0");
+ this._parent.appendChild(this.tbody);
+ this.afterScroll = this.afterScroll.bind(this);
+ this.tbody.addEventListener("scroll", this.onScroll.bind(this));
+
+ this.placeholder = this.document.createElementNS(XUL_NS, "label");
+ this.placeholder.className = "plain table-widget-empty-text";
+ this.placeholder.setAttribute("flex", "1");
+ this._parent.appendChild(this.placeholder);
+
+ this.items = new Map();
+ this.columns = new Map();
+
+ // Setup the column headers context menu to allow users to hide columns at
+ // will.
+ if (this.removableColumns) {
+ this.onPopupCommand = this.onPopupCommand.bind(this);
+ this.setupHeadersContextMenu();
+ }
+
+ if (initialColumns) {
+ this.setColumns(initialColumns, uniqueId);
+ } else if (this.emptyText) {
+ this.setPlaceholderText(this.emptyText);
+ }
+
+ this.bindSelectedRow = (event, id) => {
+ this.selectedRow = id;
+ };
+ this.on(EVENTS.ROW_SELECTED, this.bindSelectedRow);
+
+ this.onChange = this.onChange.bind(this);
+ this.onEditorDestroyed = this.onEditorDestroyed.bind(this);
+ this.onEditorTab = this.onEditorTab.bind(this);
+ this.onKeydown = this.onKeydown.bind(this);
+ this.onMousedown = this.onMousedown.bind(this);
+ this.onRowRemoved = this.onRowRemoved.bind(this);
+
+ this.document.addEventListener("keydown", this.onKeydown, false);
+ this.document.addEventListener("mousedown", this.onMousedown, false);
+}
+
+TableWidget.prototype = {
+
+ items: null,
+
+ /**
+ * Getter for the headers context menu popup id.
+ */
+ get headersContextMenu() {
+ if (this.menupopup) {
+ return this.menupopup.id;
+ }
+ return null;
+ },
+
+ /**
+ * Select the row corresponding to the json object `id`
+ */
+ set selectedRow(id) {
+ for (let column of this.columns.values()) {
+ column.selectRow(id[this.uniqueId] || id);
+ }
+ },
+
+ /**
+ * Returns the json object corresponding to the selected row.
+ */
+ get selectedRow() {
+ return this.items.get(this.columns.get(this.uniqueId).selectedRow);
+ },
+
+ /**
+ * Selects the row at index `index`.
+ */
+ set selectedIndex(index) {
+ for (let column of this.columns.values()) {
+ column.selectRowAt(index);
+ }
+ },
+
+ /**
+ * Returns the index of the selected row.
+ */
+ get selectedIndex() {
+ return this.columns.get(this.uniqueId).selectedIndex;
+ },
+
+ /**
+ * Returns the index of the selected row disregarding hidden rows.
+ */
+ get visibleSelectedIndex() {
+ let cells = this.columns.get(this.uniqueId).visibleCellNodes;
+
+ for (let i = 0; i < cells.length; i++) {
+ if (cells[i].classList.contains("theme-selected")) {
+ return i;
+ }
+ }
+
+ return -1;
+ },
+
+ /**
+ * returns all editable columns.
+ */
+ get editableColumns() {
+ let filter = columns => {
+ columns = [...columns].filter(col => {
+ if (col.clientWidth === 0) {
+ return false;
+ }
+
+ let cell = col.querySelector(".table-widget-cell");
+
+ for (let selector of this._editableFieldsEngine.selectors) {
+ if (cell.matches(selector)) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+
+ return columns;
+ };
+
+ let columns = this._parent.querySelectorAll(".table-widget-column");
+ return filter(columns);
+ },
+
+ /**
+ * Emit all cell edit events.
+ */
+ onChange: function (type, data) {
+ let changedField = data.change.field;
+ let colName = changedField.parentNode.id;
+ let column = this.columns.get(colName);
+ let uniqueId = column.table.uniqueId;
+ let itemIndex = column.cellNodes.indexOf(changedField);
+ let items = {};
+
+ for (let [name, col] of this.columns) {
+ items[name] = col.cellNodes[itemIndex].value;
+ }
+
+ let change = {
+ host: this.host,
+ key: uniqueId,
+ field: colName,
+ oldValue: data.change.oldValue,
+ newValue: data.change.newValue,
+ items: items
+ };
+
+ // A rows position in the table can change as the result of an edit. In
+ // order to ensure that the correct row is highlighted after an edit we
+ // save the uniqueId in editBookmark.
+ this.editBookmark = colName === uniqueId ? change.newValue
+ : items[uniqueId];
+ this.emit(EVENTS.CELL_EDIT, change);
+ },
+
+ onEditorDestroyed: function () {
+ this._editableFieldsEngine = null;
+ },
+
+ /**
+ * Called by the inplace editor when Tab / Shift-Tab is pressed in edit-mode.
+ * Because tables are live any row, column, cell or table can be added,
+ * deleted or moved by deleting and adding e.g. a row again.
+ *
+ * This presents various challenges when navigating via the keyboard so please
+ * keep this in mind whenever editing this method.
+ *
+ * @param {Event} event
+ * Keydown event
+ */
+ onEditorTab: function (event) {
+ let textbox = event.target;
+ let editor = this._editableFieldsEngine;
+
+ if (textbox.id !== editor.INPUT_ID) {
+ return;
+ }
+
+ let column = textbox.parentNode;
+
+ // Changing any value can change the position of the row depending on which
+ // column it is currently sorted on. In addition to this, the table cell may
+ // have been edited and had to be recreated when the user has pressed tab or
+ // shift+tab. Both of these situations require us to recover our target,
+ // select the appropriate row and move the textbox on to the next cell.
+ if (editor.changePending) {
+ // We need to apply a change, which can mean that the position of cells
+ // within the table can change. Because of this we need to wait for
+ // EVENTS.ROW_EDIT and then move the textbox.
+ this.once(EVENTS.ROW_EDIT, (e, uniqueId) => {
+ let cell;
+ let cells;
+ let columnObj;
+ let cols = this.editableColumns;
+ let rowIndex = this.visibleSelectedIndex;
+ let colIndex = cols.indexOf(column);
+ let newIndex;
+
+ // If the row has been deleted we should bail out.
+ if (!uniqueId) {
+ return;
+ }
+
+ // Find the column we need to move to.
+ if (event.shiftKey) {
+ // Navigate backwards on shift tab.
+ if (colIndex === 0) {
+ if (rowIndex === 0) {
+ return;
+ }
+ newIndex = cols.length - 1;
+ } else {
+ newIndex = colIndex - 1;
+ }
+ } else if (colIndex === cols.length - 1) {
+ let id = cols[0].id;
+ columnObj = this.columns.get(id);
+ let maxRowIndex = columnObj.visibleCellNodes.length - 1;
+ if (rowIndex === maxRowIndex) {
+ return;
+ }
+ newIndex = 0;
+ } else {
+ newIndex = colIndex + 1;
+ }
+
+ let newcol = cols[newIndex];
+ columnObj = this.columns.get(newcol.id);
+
+ // Select the correct row even if it has moved due to sorting.
+ let dataId = editor.currentTarget.getAttribute("data-id");
+ if (this.items.get(dataId)) {
+ this.emit(EVENTS.ROW_SELECTED, dataId);
+ } else {
+ this.emit(EVENTS.ROW_SELECTED, uniqueId);
+ }
+
+ // EVENTS.ROW_SELECTED may have changed the selected row so let's save
+ // the result in rowIndex.
+ rowIndex = this.visibleSelectedIndex;
+
+ // Edit the appropriate cell.
+ cells = columnObj.visibleCellNodes;
+ cell = cells[rowIndex];
+ editor.edit(cell);
+
+ // Remove flash-out class... it won't have been auto-removed because the
+ // cell was hidden for editing.
+ cell.classList.remove("flash-out");
+ });
+ }
+
+ // Begin cell edit. We always do this so that we can begin editing even in
+ // the case that the previous edit will cause the row to move.
+ let cell = this.getEditedCellOnTab(event, column);
+ editor.edit(cell);
+ },
+
+ /**
+ * Get the cell that will be edited next on tab / shift tab and highlight the
+ * appropriate row. Edits etc. are not taken into account.
+ *
+ * This is used to tab from one field to another without editing and makes the
+ * editor much more responsive.
+ *
+ * @param {Event} event
+ * Keydown event
+ */
+ getEditedCellOnTab: function (event, column) {
+ let cell = null;
+ let cols = this.editableColumns;
+ let rowIndex = this.visibleSelectedIndex;
+ let colIndex = cols.indexOf(column);
+ let maxCol = cols.length - 1;
+ let maxRow = this.columns.get(column.id).visibleCellNodes.length - 1;
+
+ if (event.shiftKey) {
+ // Navigate backwards on shift tab.
+ if (colIndex === 0) {
+ if (rowIndex === 0) {
+ this._editableFieldsEngine.completeEdit();
+ return null;
+ }
+
+ column = cols[cols.length - 1];
+ let cells = this.columns.get(column.id).visibleCellNodes;
+ cell = cells[rowIndex - 1];
+
+ let rowId = cell.getAttribute("data-id");
+ this.emit(EVENTS.ROW_SELECTED, rowId);
+ } else {
+ column = cols[colIndex - 1];
+ let cells = this.columns.get(column.id).visibleCellNodes;
+ cell = cells[rowIndex];
+ }
+ } else if (colIndex === maxCol) {
+ // If in the rightmost column on the last row stop editing.
+ if (rowIndex === maxRow) {
+ this._editableFieldsEngine.completeEdit();
+ return null;
+ }
+
+ // If in the rightmost column of a row then move to the first column of
+ // the next row.
+ column = cols[0];
+ let cells = this.columns.get(column.id).visibleCellNodes;
+ cell = cells[rowIndex + 1];
+
+ let rowId = cell.getAttribute("data-id");
+ this.emit(EVENTS.ROW_SELECTED, rowId);
+ } else {
+ // Navigate forwards on tab.
+ column = cols[colIndex + 1];
+ let cells = this.columns.get(column.id).visibleCellNodes;
+ cell = cells[rowIndex];
+ }
+
+ return cell;
+ },
+
+ /**
+ * Reset the editable fields engine if the currently edited row is removed.
+ *
+ * @param {String} event
+ * The event name "event-removed."
+ * @param {Object} row
+ * The values from the removed row.
+ */
+ onRowRemoved: function (event, row) {
+ if (!this._editableFieldsEngine || !this._editableFieldsEngine.isEditing) {
+ return;
+ }
+
+ let removedKey = row[this.uniqueId];
+ let column = this.columns.get(this.uniqueId);
+
+ if (removedKey in column.items) {
+ return;
+ }
+
+ // The target is lost so we need to hide the remove the textbox from the DOM
+ // and reset the target nodes.
+ this.onEditorTargetLost();
+ },
+
+ /**
+ * Cancel an edit because the edit target has been lost.
+ */
+ onEditorTargetLost: function () {
+ let editor = this._editableFieldsEngine;
+
+ if (!editor || !editor.isEditing) {
+ return;
+ }
+
+ editor.cancelEdit();
+ },
+
+ /**
+ * Keydown event handler for the table. Used for keyboard navigation amongst
+ * rows.
+ */
+ onKeydown: function (event) {
+ // If we are in edit mode bail out.
+ if (this._editableFieldsEngine && this._editableFieldsEngine.isEditing) {
+ return;
+ }
+
+ let selectedCell = this.tbody.querySelector(".theme-selected");
+ if (!selectedCell) {
+ return;
+ }
+
+ let colName;
+ let column;
+ let visibleCells;
+ let index;
+ let cell;
+
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_UP:
+ event.preventDefault();
+
+ colName = selectedCell.parentNode.id;
+ column = this.columns.get(colName);
+ visibleCells = column.visibleCellNodes;
+ index = visibleCells.indexOf(selectedCell);
+
+ if (index > 0) {
+ index--;
+ } else {
+ index = visibleCells.length - 1;
+ }
+
+ cell = visibleCells[index];
+
+ this.emit(EVENTS.ROW_SELECTED, cell.getAttribute("data-id"));
+ break;
+ case KeyCodes.DOM_VK_DOWN:
+ event.preventDefault();
+
+ colName = selectedCell.parentNode.id;
+ column = this.columns.get(colName);
+ visibleCells = column.visibleCellNodes;
+ index = visibleCells.indexOf(selectedCell);
+
+ if (index === visibleCells.length - 1) {
+ index = 0;
+ } else {
+ index++;
+ }
+
+ cell = visibleCells[index];
+
+ this.emit(EVENTS.ROW_SELECTED, cell.getAttribute("data-id"));
+ break;
+ }
+ },
+
+ /**
+ * Close any editors if the area "outside the table" is clicked. In reality,
+ * the table covers the whole area but there are labels filling the top few
+ * rows. This method clears any inline editors if an area outside a textbox or
+ * label is clicked.
+ */
+ onMousedown: function ({target}) {
+ let nodeName = target.nodeName;
+
+ if (nodeName === "textbox" || !this._editableFieldsEngine) {
+ return;
+ }
+
+ // Force any editor fields to hide due to XUL focus quirks.
+ this._editableFieldsEngine.blur();
+ },
+
+ /**
+ * Make table fields editable.
+ *
+ * @param {String|Array} editableColumns
+ * An array or comma separated list of editable column names.
+ */
+ makeFieldsEditable: function (editableColumns) {
+ let selectors = [];
+
+ if (typeof editableColumns === "string") {
+ editableColumns = [editableColumns];
+ }
+
+ for (let id of editableColumns) {
+ selectors.push("#" + id + " .table-widget-cell");
+ }
+
+ for (let [name, column] of this.columns) {
+ if (!editableColumns.includes(name)) {
+ column.column.setAttribute("readonly", "");
+ }
+ }
+
+ if (this._editableFieldsEngine) {
+ this._editableFieldsEngine.selectors = selectors;
+ } else {
+ this._editableFieldsEngine = new EditableFieldsEngine({
+ root: this.tbody,
+ onTab: this.onEditorTab,
+ onTriggerEvent: "dblclick",
+ selectors: selectors
+ });
+
+ this._editableFieldsEngine.on("change", this.onChange);
+ this._editableFieldsEngine.on("destroyed", this.onEditorDestroyed);
+
+ this.on(EVENTS.ROW_REMOVED, this.onRowRemoved);
+ this.on(EVENTS.TABLE_CLEARED, this._editableFieldsEngine.cancelEdit);
+
+ this.emit(EVENTS.FIELDS_EDITABLE, this._editableFieldsEngine);
+ }
+ },
+
+ destroy: function () {
+ this.off(EVENTS.ROW_SELECTED, this.bindSelectedRow);
+ this.off(EVENTS.ROW_REMOVED, this.onRowRemoved);
+
+ this.document.removeEventListener("keydown", this.onKeydown, false);
+ this.document.removeEventListener("mousedown", this.onMousedown, false);
+
+ if (this._editableFieldsEngine) {
+ this.off(EVENTS.TABLE_CLEARED, this._editableFieldsEngine.cancelEdit);
+ this._editableFieldsEngine.off("change", this.onChange);
+ this._editableFieldsEngine.off("destroyed", this.onEditorDestroyed);
+ this._editableFieldsEngine.destroy();
+ this._editableFieldsEngine = null;
+ }
+
+ if (this.menupopup) {
+ this.menupopup.removeEventListener("command", this.onPopupCommand);
+ this.menupopup.remove();
+ }
+ },
+
+ /**
+ * Sets the text to be shown when the table is empty.
+ */
+ setPlaceholderText: function (text) {
+ this.placeholder.setAttribute("value", text);
+ },
+
+ /**
+ * Prepares the context menu for the headers of the table columns. This
+ * context menu allows users to toggle various columns, only with an exception
+ * of the unique columns and when only two columns are visible in the table.
+ */
+ setupHeadersContextMenu: function () {
+ let popupset = this.document.getElementsByTagName("popupset")[0];
+ if (!popupset) {
+ popupset = this.document.createElementNS(XUL_NS, "popupset");
+ this.document.documentElement.appendChild(popupset);
+ }
+
+ this.menupopup = this.document.createElementNS(XUL_NS, "menupopup");
+ this.menupopup.id = "table-widget-column-select";
+ this.menupopup.addEventListener("command", this.onPopupCommand);
+ popupset.appendChild(this.menupopup);
+ this.populateMenuPopup();
+ },
+
+ /**
+ * Populates the header context menu with the names of the columns along with
+ * displaying which columns are hidden or visible.
+ */
+ populateMenuPopup: function () {
+ if (!this.menupopup) {
+ return;
+ }
+
+ while (this.menupopup.firstChild) {
+ this.menupopup.firstChild.remove();
+ }
+
+ for (let column of this.columns.values()) {
+ let menuitem = this.document.createElementNS(XUL_NS, "menuitem");
+ menuitem.setAttribute("label", column.header.getAttribute("value"));
+ menuitem.setAttribute("data-id", column.id);
+ menuitem.setAttribute("type", "checkbox");
+ menuitem.setAttribute("checked", !column.wrapper.getAttribute("hidden"));
+ if (column.id == this.uniqueId) {
+ menuitem.setAttribute("disabled", "true");
+ }
+ this.menupopup.appendChild(menuitem);
+ }
+ let checked = this.menupopup.querySelectorAll("menuitem[checked]");
+ if (checked.length == 2) {
+ checked[checked.length - 1].setAttribute("disabled", "true");
+ }
+ },
+
+ /**
+ * Event handler for the `command` event on the column headers context menu
+ */
+ onPopupCommand: function (event) {
+ let item = event.originalTarget;
+ let checked = !!item.getAttribute("checked");
+ let id = item.getAttribute("data-id");
+ this.emit(EVENTS.HEADER_CONTEXT_MENU, id, checked);
+ checked = this.menupopup.querySelectorAll("menuitem[checked]");
+ let disabled = this.menupopup.querySelectorAll("menuitem[disabled]");
+ if (checked.length == 2) {
+ checked[checked.length - 1].setAttribute("disabled", "true");
+ } else if (disabled.length > 1) {
+ disabled[disabled.length - 1].removeAttribute("disabled");
+ }
+ },
+
+ /**
+ * Creates the columns in the table. Without calling this method, data cannot
+ * be inserted into the table unless `initialColumns` was supplied.
+ *
+ * @param {object} columns
+ * A key value pair representing the columns of the table. Where the
+ * key represents the id of the column and the value is the displayed
+ * label in the header of the column.
+ * @param {string} sortOn
+ * The id of the column on which the table will be initially sorted on.
+ * @param {array} hiddenColumns
+ * Ids of all the columns that are hidden by default.
+ */
+ setColumns: function (columns, sortOn = this.sortedOn, hiddenColumns = []) {
+ for (let column of this.columns.values()) {
+ column.destroy();
+ }
+
+ this.columns.clear();
+
+ if (!(sortOn in columns)) {
+ sortOn = null;
+ }
+
+ if (!(this.firstColumn in columns)) {
+ this.firstColumn = null;
+ }
+
+ if (this.firstColumn) {
+ this.columns.set(this.firstColumn,
+ new Column(this, this.firstColumn, columns[this.firstColumn]));
+ }
+
+ for (let id in columns) {
+ if (!sortOn) {
+ sortOn = id;
+ }
+
+ if (this.firstColumn && id == this.firstColumn) {
+ continue;
+ }
+
+ this.columns.set(id, new Column(this, id, columns[id]));
+ if (hiddenColumns.indexOf(id) > -1) {
+ this.columns.get(id).toggleColumn();
+ }
+ }
+ this.sortedOn = sortOn;
+ this.sortBy(this.sortedOn);
+ this.populateMenuPopup();
+ },
+
+ /**
+ * Returns true if the passed string or the row json object corresponds to the
+ * selected item in the table.
+ */
+ isSelected: function (item) {
+ if (typeof item == "object") {
+ item = item[this.uniqueId];
+ }
+
+ return this.selectedRow && item == this.selectedRow[this.uniqueId];
+ },
+
+ /**
+ * Selects the row corresponding to the `id` json.
+ */
+ selectRow: function (id) {
+ this.selectedRow = id;
+ },
+
+ /**
+ * Selects the next row. Cycles over to the first row if last row is selected
+ */
+ selectNextRow: function () {
+ for (let column of this.columns.values()) {
+ column.selectNextRow();
+ }
+ },
+
+ /**
+ * Selects the previous row. Cycles over to the last row if first row is
+ * selected.
+ */
+ selectPreviousRow: function () {
+ for (let column of this.columns.values()) {
+ column.selectPreviousRow();
+ }
+ },
+
+ /**
+ * Clears any selected row.
+ */
+ clearSelection: function () {
+ this.selectedIndex = -1;
+ },
+
+ /**
+ * Adds a row into the table.
+ *
+ * @param {object} item
+ * The object from which the key-value pairs will be taken and added
+ * into the row. This object can have any arbitarary key value pairs,
+ * but only those will be used whose keys match to the ids of the
+ * columns.
+ * @param {boolean} suppressFlash
+ * true to not flash the row while inserting the row.
+ */
+ push: function (item, suppressFlash) {
+ if (!this.sortedOn || !this.columns) {
+ console.error("Can't insert item without defining columns first");
+ return;
+ }
+
+ if (this.items.has(item[this.uniqueId])) {
+ this.update(item);
+ return;
+ }
+
+ let index = this.columns.get(this.sortedOn).push(item);
+ for (let [key, column] of this.columns) {
+ if (key != this.sortedOn) {
+ column.insertAt(item, index);
+ }
+ column.updateZebra();
+ }
+ this.items.set(item[this.uniqueId], item);
+ this.tbody.removeAttribute("empty");
+
+ if (!suppressFlash) {
+ this.emit(EVENTS.ROW_UPDATED, item[this.uniqueId]);
+ }
+
+ this.emit(EVENTS.ROW_EDIT, item[this.uniqueId]);
+ },
+
+ /**
+ * Removes the row associated with the `item` object.
+ */
+ remove: function (item) {
+ if (typeof item != "object") {
+ item = this.items.get(item);
+ }
+ if (!item) {
+ return;
+ }
+ let removed = this.items.delete(item[this.uniqueId]);
+
+ if (!removed) {
+ return;
+ }
+ for (let column of this.columns.values()) {
+ column.remove(item);
+ column.updateZebra();
+ }
+ if (this.items.size == 0) {
+ this.tbody.setAttribute("empty", "empty");
+ }
+
+ this.emit(EVENTS.ROW_REMOVED, item);
+ },
+
+ /**
+ * Updates the items in the row corresponding to the `item` object previously
+ * used to insert the row using `push` method. The linking is done via the
+ * `uniqueId` key's value.
+ */
+ update: function (item) {
+ let oldItem = this.items.get(item[this.uniqueId]);
+ if (!oldItem) {
+ return;
+ }
+ this.items.set(item[this.uniqueId], item);
+
+ let changed = false;
+ for (let column of this.columns.values()) {
+ if (item[column.id] != oldItem[column.id]) {
+ column.update(item);
+ changed = true;
+ }
+ }
+ if (changed) {
+ this.emit(EVENTS.ROW_UPDATED, item[this.uniqueId]);
+ this.emit(EVENTS.ROW_EDIT, item[this.uniqueId]);
+ }
+ },
+
+ /**
+ * Removes all of the rows from the table.
+ */
+ clear: function () {
+ this.items.clear();
+ for (let column of this.columns.values()) {
+ column.clear();
+ }
+ this.tbody.setAttribute("empty", "empty");
+ this.setPlaceholderText(this.emptyText);
+
+ this.emit(EVENTS.TABLE_CLEARED, this);
+ },
+
+ /**
+ * Sorts the table by a given column.
+ *
+ * @param {string} column
+ * The id of the column on which the table should be sorted.
+ */
+ sortBy: function (column) {
+ this.emit(EVENTS.COLUMN_SORTED, column);
+ this.sortedOn = column;
+
+ if (!this.items.size) {
+ return;
+ }
+
+ let sortedItems = this.columns.get(column).sort([...this.items.values()]);
+ for (let [id, col] of this.columns) {
+ if (id != col) {
+ col.sort(sortedItems);
+ }
+ }
+ },
+
+ /**
+ * Filters the table based on a specific value
+ *
+ * @param {String} value: The filter value
+ * @param {Array} ignoreProps: Props to ignore while filtering
+ */
+ filterItems(value, ignoreProps = []) {
+ if (this.filteredValue == value) {
+ return;
+ }
+ if (this._editableFieldsEngine) {
+ this._editableFieldsEngine.completeEdit();
+ }
+
+ this.filteredValue = value;
+ if (!value) {
+ this.emit(EVENTS.TABLE_FILTERED, []);
+ return;
+ }
+ // Shouldn't be case-sensitive
+ value = value.toLowerCase();
+
+ let itemsToHide = [...this.items.keys()];
+ // Loop through all items and hide unmatched items
+ for (let [id, val] of this.items) {
+ for (let prop in val) {
+ if (ignoreProps.includes(prop)) {
+ continue;
+ }
+ let propValue = val[prop].toString().toLowerCase();
+ if (propValue.includes(value)) {
+ itemsToHide.splice(itemsToHide.indexOf(id), 1);
+ break;
+ }
+ }
+ }
+ this.emit(EVENTS.TABLE_FILTERED, itemsToHide);
+ },
+
+ /**
+ * Calls the afterScroll function when the user has stopped scrolling
+ */
+ onScroll: function () {
+ clearNamedTimeout("table-scroll");
+ setNamedTimeout("table-scroll", AFTER_SCROLL_DELAY, this.afterScroll);
+ },
+
+ /**
+ * Emits the "scroll-end" event when the whole table is scrolled
+ */
+ afterScroll: function () {
+ let scrollHeight = this.tbody.getBoundingClientRect().height -
+ this.tbody.querySelector(".table-widget-column-header").clientHeight;
+
+ // Emit scroll-end event when 9/10 of the table is scrolled
+ if (this.tbody.scrollTop >= 0.9 * scrollHeight) {
+ this.emit("scroll-end");
+ }
+ }
+};
+
+TableWidget.EVENTS = EVENTS;
+
+module.exports.TableWidget = TableWidget;
+
+/**
+ * A single column object in the table.
+ *
+ * @param {TableWidget} table
+ * The table object to which the column belongs.
+ * @param {string} id
+ * Id of the column.
+ * @param {String} header
+ * The displayed string on the column's header.
+ */
+function Column(table, id, header) {
+ this.tbody = table.tbody;
+ this.document = table.document;
+ this.window = table.window;
+ this.id = id;
+ this.uniqueId = table.uniqueId;
+ this.wrapTextInElements = table.wrapTextInElements;
+ this.table = table;
+ this.cells = [];
+ this.items = {};
+
+ this.highlightUpdated = table.highlightUpdated;
+
+ // This wrapping element is required solely so that position:sticky works on
+ // the headers of the columns.
+ this.wrapper = this.document.createElementNS(XUL_NS, "vbox");
+ this.wrapper.className = "table-widget-wrapper";
+ this.wrapper.setAttribute("flex", "1");
+ this.wrapper.setAttribute("tabindex", "0");
+ this.tbody.appendChild(this.wrapper);
+
+ this.splitter = this.document.createElementNS(XUL_NS, "splitter");
+ this.splitter.className = "devtools-side-splitter";
+ this.tbody.appendChild(this.splitter);
+
+ this.column = this.document.createElementNS(HTML_NS, "div");
+ this.column.id = id;
+ this.column.className = "table-widget-column";
+ this.wrapper.appendChild(this.column);
+
+ this.header = this.document.createElementNS(XUL_NS, "label");
+ this.header.className = "devtools-toolbar table-widget-column-header";
+ this.header.setAttribute("value", header);
+ this.column.appendChild(this.header);
+ if (table.headersContextMenu) {
+ this.header.setAttribute("context", table.headersContextMenu);
+ }
+ this.toggleColumn = this.toggleColumn.bind(this);
+ this.table.on(EVENTS.HEADER_CONTEXT_MENU, this.toggleColumn);
+
+ this.onColumnSorted = this.onColumnSorted.bind(this);
+ this.table.on(EVENTS.COLUMN_SORTED, this.onColumnSorted);
+
+ this.onRowUpdated = this.onRowUpdated.bind(this);
+ this.table.on(EVENTS.ROW_UPDATED, this.onRowUpdated);
+
+ this.onTableFiltered = this.onTableFiltered.bind(this);
+ this.table.on(EVENTS.TABLE_FILTERED, this.onTableFiltered);
+
+ this.onClick = this.onClick.bind(this);
+ this.onMousedown = this.onMousedown.bind(this);
+ this.column.addEventListener("click", this.onClick);
+ this.column.addEventListener("mousedown", this.onMousedown);
+}
+
+Column.prototype = {
+
+ // items is a cell-id to cell-index map. It is basically a reverse map of the
+ // this.cells object and is used to quickly reverse lookup a cell by its id
+ // instead of looping through the cells array. This reverse map is not kept
+ // upto date in sync with the cells array as updating it is in itself a loop
+ // through all the cells of the columns. Thus update it on demand when it goes
+ // out of sync with this.cells.
+ items: null,
+
+ // _itemsDirty is a flag which becomes true when this.items goes out of sync
+ // with this.cells
+ _itemsDirty: null,
+
+ selectedRow: null,
+
+ cells: null,
+
+ /**
+ * Gets whether the table is sorted on this column or not.
+ * 0 - not sorted.
+ * 1 - ascending order
+ * 2 - descending order
+ */
+ get sorted() {
+ return this._sortState || 0;
+ },
+
+ /**
+ * Sets the sorted value
+ */
+ set sorted(value) {
+ if (!value) {
+ this.header.removeAttribute("sorted");
+ } else {
+ this.header.setAttribute("sorted",
+ value == 1 ? "ascending" : "descending");
+ }
+ this._sortState = value;
+ },
+
+ /**
+ * Gets the selected row in the column.
+ */
+ get selectedIndex() {
+ if (!this.selectedRow) {
+ return -1;
+ }
+ return this.items[this.selectedRow];
+ },
+
+ get cellNodes() {
+ return [...this.column.querySelectorAll(".table-widget-cell")];
+ },
+
+ get visibleCellNodes() {
+ let editor = this.table._editableFieldsEngine;
+ let nodes = this.cellNodes.filter(node => {
+ // If the cell is currently being edited we should class it as visible.
+ if (editor && editor.currentTarget === node) {
+ return true;
+ }
+ return node.clientWidth !== 0;
+ });
+
+ return nodes;
+ },
+
+ /**
+ * Called when the column is sorted by.
+ *
+ * @param {string} event
+ * The event name of the event. i.e. EVENTS.COLUMN_SORTED
+ * @param {string} column
+ * The id of the column being sorted by.
+ */
+ onColumnSorted: function (event, column) {
+ if (column != this.id) {
+ this.sorted = 0;
+ return;
+ } else if (this.sorted == 0 || this.sorted == 2) {
+ this.sorted = 1;
+ } else {
+ this.sorted = 2;
+ }
+ this.updateZebra();
+ },
+
+ onTableFiltered: function (event, itemsToHide) {
+ this._updateItems();
+ if (!this.cells) {
+ return;
+ }
+ for (let cell of this.cells) {
+ cell.hidden = false;
+ }
+ for (let id of itemsToHide) {
+ this.cells[this.items[id]].hidden = true;
+ }
+ this.updateZebra();
+ },
+
+ /**
+ * Called when a row is updated.
+ *
+ * @param {string} event
+ * The event name of the event. i.e. EVENTS.ROW_UPDATED
+ * @param {string} id
+ * The unique id of the object associated with the row.
+ */
+ onRowUpdated: function (event, id) {
+ this._updateItems();
+ if (this.highlightUpdated && this.items[id] != null) {
+ if (this.table.editBookmark) {
+ // A rows position in the table can change as the result of an edit. In
+ // order to ensure that the correct row is highlighted after an edit we
+ // save the uniqueId in editBookmark. Here we send the signal that the
+ // row has been edited and that the row needs to be selected again.
+ this.table.emit(EVENTS.ROW_SELECTED, this.table.editBookmark);
+ this.table.editBookmark = null;
+ }
+
+ this.cells[this.items[id]].flash();
+ }
+ this.updateZebra();
+ },
+
+ destroy: function () {
+ this.table.off(EVENTS.COLUMN_SORTED, this.onColumnSorted);
+ this.table.off(EVENTS.HEADER_CONTEXT_MENU, this.toggleColumn);
+ this.table.off(EVENTS.ROW_UPDATED, this.onRowUpdated);
+ this.table.off(EVENTS.TABLE_FILTERED, this.onTableFiltered);
+
+ this.column.removeEventListener("click", this.onClick);
+ this.column.removeEventListener("mousedown", this.onMousedown);
+
+ this.splitter.remove();
+ this.column.parentNode.remove();
+ this.cells = null;
+ this.items = null;
+ this.selectedRow = null;
+ },
+
+ /**
+ * Selects the row at the `index` index
+ */
+ selectRowAt: function (index) {
+ if (this.selectedRow != null) {
+ this.cells[this.items[this.selectedRow]].toggleClass("theme-selected");
+ }
+ if (index < 0) {
+ this.selectedRow = null;
+ return;
+ }
+ let cell = this.cells[index];
+ cell.toggleClass("theme-selected");
+ this.selectedRow = cell.id;
+ },
+
+ /**
+ * Selects the row with the object having the `uniqueId` value as `id`
+ */
+ selectRow: function (id) {
+ this._updateItems();
+ this.selectRowAt(this.items[id]);
+ },
+
+ /**
+ * Selects the next row. Cycles to first if last row is selected.
+ */
+ selectNextRow: function () {
+ this._updateItems();
+ let index = this.items[this.selectedRow] + 1;
+ if (index == this.cells.length) {
+ index = 0;
+ }
+ this.selectRowAt(index);
+ },
+
+ /**
+ * Selects the previous row. Cycles to last if first row is selected.
+ */
+ selectPreviousRow: function () {
+ this._updateItems();
+ let index = this.items[this.selectedRow] - 1;
+ if (index == -1) {
+ index = this.cells.length - 1;
+ }
+ this.selectRowAt(index);
+ },
+
+ /**
+ * Pushes the `item` object into the column. If this column is sorted on,
+ * then inserts the object at the right position based on the column's id
+ * key's value.
+ *
+ * @returns {number}
+ * The index of the currently pushed item.
+ */
+ push: function (item) {
+ let value = item[this.id];
+
+ if (this.sorted) {
+ let index;
+ if (this.sorted == 1) {
+ index = this.cells.findIndex(element => {
+ return value < element.value;
+ });
+ } else {
+ index = this.cells.findIndex(element => {
+ return value > element.value;
+ });
+ }
+ index = index >= 0 ? index : this.cells.length;
+ if (index < this.cells.length) {
+ this._itemsDirty = true;
+ }
+ this.items[item[this.uniqueId]] = index;
+ this.cells.splice(index, 0, new Cell(this, item, this.cells[index]));
+ return index;
+ }
+
+ this.items[item[this.uniqueId]] = this.cells.length;
+ return this.cells.push(new Cell(this, item)) - 1;
+ },
+
+ /**
+ * Inserts the `item` object at the given `index` index in the table.
+ */
+ insertAt: function (item, index) {
+ if (index < this.cells.length) {
+ this._itemsDirty = true;
+ }
+ this.items[item[this.uniqueId]] = index;
+ this.cells.splice(index, 0, new Cell(this, item, this.cells[index]));
+ this.updateZebra();
+ },
+
+ /**
+ * Event handler for the command event coming from the header context menu.
+ * Toggles the column if it was requested by the user.
+ * When called explicitly without parameters, it toggles the corresponding
+ * column.
+ *
+ * @param {string} event
+ * The name of the event. i.e. EVENTS.HEADER_CONTEXT_MENU
+ * @param {string} id
+ * Id of the column to be toggled
+ * @param {string} checked
+ * true if the column is visible
+ */
+ toggleColumn: function (event, id, checked) {
+ if (arguments.length == 0) {
+ // Act like a toggling method when called with no params
+ id = this.id;
+ checked = this.wrapper.hasAttribute("hidden");
+ }
+ if (id != this.id) {
+ return;
+ }
+ if (checked) {
+ this.wrapper.removeAttribute("hidden");
+ } else {
+ this.wrapper.setAttribute("hidden", "true");
+ }
+ },
+
+ /**
+ * Removes the corresponding item from the column.
+ */
+ remove: function (item) {
+ this._updateItems();
+ let index = this.items[item[this.uniqueId]];
+ if (index == null) {
+ return;
+ }
+
+ if (index < this.cells.length) {
+ this._itemsDirty = true;
+ }
+ this.cells[index].destroy();
+ this.cells.splice(index, 1);
+ delete this.items[item[this.uniqueId]];
+ },
+
+ /**
+ * Updates the corresponding item from the column.
+ */
+ update: function (item) {
+ this._updateItems();
+
+ let index = this.items[item[this.uniqueId]];
+ if (index == null) {
+ return;
+ }
+
+ this.cells[index].value = item[this.id];
+ },
+
+ /**
+ * Updates the `this.items` cell-id vs cell-index map to be in sync with
+ * `this.cells`.
+ */
+ _updateItems: function () {
+ if (!this._itemsDirty) {
+ return;
+ }
+ for (let i = 0; i < this.cells.length; i++) {
+ this.items[this.cells[i].id] = i;
+ }
+ this._itemsDirty = false;
+ },
+
+ /**
+ * Clears the current column
+ */
+ clear: function () {
+ this.cells = [];
+ this.items = {};
+ this._itemsDirty = false;
+ this.selectedRow = null;
+ while (this.header.nextSibling) {
+ this.header.nextSibling.remove();
+ }
+ },
+
+ /**
+ * Sorts the given items and returns the sorted list if the table was sorted
+ * by this column.
+ */
+ sort: function (items) {
+ // Only sort the array if we are sorting based on this column
+ if (this.sorted == 1) {
+ items.sort((a, b) => {
+ let val1 = (a[this.id] instanceof Node) ?
+ a[this.id].textContent : a[this.id];
+ let val2 = (b[this.id] instanceof Node) ?
+ b[this.id].textContent : b[this.id];
+ return val1 > val2;
+ });
+ } else if (this.sorted > 1) {
+ items.sort((a, b) => {
+ let val1 = (a[this.id] instanceof Node) ?
+ a[this.id].textContent : a[this.id];
+ let val2 = (b[this.id] instanceof Node) ?
+ b[this.id].textContent : b[this.id];
+ return val2 > val1;
+ });
+ }
+
+ if (this.selectedRow) {
+ this.cells[this.items[this.selectedRow]].toggleClass("theme-selected");
+ }
+ this.items = {};
+ // Otherwise, just use the sorted array passed to update the cells value.
+ items.forEach((item, i) => {
+ this.items[item[this.uniqueId]] = i;
+ this.cells[i].value = item[this.id];
+ this.cells[i].id = item[this.uniqueId];
+ });
+ if (this.selectedRow) {
+ this.cells[this.items[this.selectedRow]].toggleClass("theme-selected");
+ }
+ this._itemsDirty = false;
+ this.updateZebra();
+ return items;
+ },
+
+ updateZebra() {
+ this._updateItems();
+ let i = 0;
+ for (let cell of this.cells) {
+ if (!cell.hidden) {
+ i++;
+ }
+ cell.toggleClass("even", !(i % 2));
+ }
+ },
+
+ /**
+ * Click event handler for the column. Used to detect click on header for
+ * for sorting.
+ */
+ onClick: function (event) {
+ let target = event.originalTarget;
+
+ if (target.nodeType !== target.ELEMENT_NODE || target == this.column) {
+ return;
+ }
+
+ if (event.button == 0 && target == this.header) {
+ this.table.sortBy(this.id);
+ }
+ },
+
+ /**
+ * Mousedown event handler for the column. Used to select rows.
+ */
+ onMousedown: function (event) {
+ let target = event.originalTarget;
+
+ if (target.nodeType !== target.ELEMENT_NODE ||
+ target == this.column ||
+ target == this.header) {
+ return;
+ }
+ if (event.button == 0) {
+ let closest = target.closest("[data-id]");
+ if (!closest) {
+ return;
+ }
+
+ let dataid = closest.getAttribute("data-id");
+ this.table.emit(EVENTS.ROW_SELECTED, dataid);
+ }
+ },
+};
+
+/**
+ * A single cell in a column
+ *
+ * @param {Column} column
+ * The column object to which the cell belongs.
+ * @param {object} item
+ * The object representing the row. It contains a key value pair
+ * representing the column id and its associated value. The value
+ * can be a DOMNode that is appended or a string value.
+ * @param {Cell} nextCell
+ * The cell object which is next to this cell. null if this cell is last
+ * cell of the column
+ */
+function Cell(column, item, nextCell) {
+ let document = column.document;
+
+ this.wrapTextInElements = column.wrapTextInElements;
+ this.label = document.createElementNS(XUL_NS, "label");
+ this.label.setAttribute("crop", "end");
+ this.label.className = "plain table-widget-cell";
+
+ if (nextCell) {
+ column.column.insertBefore(this.label, nextCell.label);
+ } else {
+ column.column.appendChild(this.label);
+ }
+
+ if (column.table.cellContextMenuId) {
+ this.label.setAttribute("context", column.table.cellContextMenuId);
+ this.label.addEventListener("contextmenu", (event) => {
+ // Make the ID of the clicked cell available as a property on the table.
+ // It's then available for the popupshowing or command handler.
+ column.table.contextMenuRowId = this.id;
+ }, false);
+ }
+
+ this.value = item[column.id];
+ this.id = item[column.uniqueId];
+}
+
+Cell.prototype = {
+
+ set id(value) {
+ this._id = value;
+ this.label.setAttribute("data-id", value);
+ },
+
+ get id() {
+ return this._id;
+ },
+
+ get hidden() {
+ return this.label.hasAttribute("hidden");
+ },
+
+ set hidden(value) {
+ if (value) {
+ this.label.setAttribute("hidden", "hidden");
+ } else {
+ this.label.removeAttribute("hidden");
+ }
+ },
+
+ set value(value) {
+ this._value = value;
+ if (value == null) {
+ this.label.setAttribute("value", "");
+ return;
+ }
+
+ if (this.wrapTextInElements && !(value instanceof Node)) {
+ let span = this.label.ownerDocument.createElementNS(HTML_NS, "span");
+ span.textContent = value;
+ value = span;
+ }
+
+ if (value instanceof Node) {
+ this.label.removeAttribute("value");
+
+ while (this.label.firstChild) {
+ this.label.removeChild(this.label.firstChild);
+ }
+
+ this.label.appendChild(value);
+ } else {
+ this.label.setAttribute("value", value + "");
+ }
+ },
+
+ get value() {
+ return this._value;
+ },
+
+ toggleClass: function (className, condition) {
+ this.label.classList.toggle(className, condition);
+ },
+
+ /**
+ * Flashes the cell for a brief time. This when done for with cells in all
+ * columns, makes it look like the row is being highlighted/flashed.
+ */
+ flash: function () {
+ if (!this.label.parentNode) {
+ return;
+ }
+ this.label.classList.remove("flash-out");
+ // Cause a reflow so that the animation retriggers on adding back the class
+ let a = this.label.parentNode.offsetWidth; // eslint-disable-line
+ let onAnimEnd = () => {
+ this.label.classList.remove("flash-out");
+ this.label.removeEventListener("animationend", onAnimEnd);
+ };
+ this.label.addEventListener("animationend", onAnimEnd);
+ this.label.classList.add("flash-out");
+ },
+
+ focus: function () {
+ this.label.focus();
+ },
+
+ destroy: function () {
+ this.label.remove();
+ this.label = null;
+ }
+};
+
+/**
+ * Simple widget to make nodes matching a CSS selector editable.
+ *
+ * @param {Object} options
+ * An object with the following format:
+ * {
+ * // The node that will act as a container for the editor e.g. a
+ * // div or table.
+ * root: someNode,
+ *
+ * // The onTab event to be handled by the caller.
+ * onTab: function(event) { ... }
+ *
+ * // Optional event used to trigger the editor. By default this is
+ * // dblclick.
+ * onTriggerEvent: "dblclick",
+ *
+ * // Array or comma separated string of CSS Selectors matching
+ * // elements that are to be made editable.
+ * selectors: [
+ * "#name .table-widget-cell",
+ * "#value .table-widget-cell"
+ * ]
+ * }
+ */
+function EditableFieldsEngine(options) {
+ EventEmitter.decorate(this);
+
+ if (!Array.isArray(options.selectors)) {
+ options.selectors = [options.selectors];
+ }
+
+ this.root = options.root;
+ this.selectors = options.selectors;
+ this.onTab = options.onTab;
+ this.onTriggerEvent = options.onTriggerEvent || "dblclick";
+
+ this.edit = this.edit.bind(this);
+ this.cancelEdit = this.cancelEdit.bind(this);
+ this.destroy = this.destroy.bind(this);
+
+ this.onTrigger = this.onTrigger.bind(this);
+ this.root.addEventListener(this.onTriggerEvent, this.onTrigger);
+}
+
+EditableFieldsEngine.prototype = {
+ INPUT_ID: "inlineEditor",
+
+ get changePending() {
+ return this.isEditing && (this.textbox.value !== this.currentValue);
+ },
+
+ get isEditing() {
+ return this.root && !this.textbox.hidden;
+ },
+
+ get textbox() {
+ if (!this._textbox) {
+ let doc = this.root.ownerDocument;
+ this._textbox = doc.createElementNS(XUL_NS, "textbox");
+ this._textbox.id = this.INPUT_ID;
+
+ this._textbox.setAttribute("flex", "1");
+
+ this.onKeydown = this.onKeydown.bind(this);
+ this._textbox.addEventListener("keydown", this.onKeydown);
+
+ this.completeEdit = this.completeEdit.bind(this);
+ doc.addEventListener("blur", this.completeEdit);
+ }
+
+ return this._textbox;
+ },
+
+ /**
+ * Called when a trigger event is detected (default is dblclick).
+ *
+ * @param {EventTarget} target
+ * Calling event's target.
+ */
+ onTrigger: function ({target}) {
+ this.edit(target);
+ },
+
+ /**
+ * Handle keypresses when in edit mode:
+ * - <escape> revert the value and close the textbox.
+ * - <return> apply the value and close the textbox.
+ * - <tab> Handled by the consumer's `onTab` callback.
+ * - <shift><tab> Handled by the consumer's `onTab` callback.
+ *
+ * @param {Event} event
+ * The calling event.
+ */
+ onKeydown: function (event) {
+ if (!this.textbox) {
+ return;
+ }
+
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_ESCAPE:
+ this.cancelEdit();
+ event.preventDefault();
+ break;
+ case KeyCodes.DOM_VK_RETURN:
+ this.completeEdit();
+ break;
+ case KeyCodes.DOM_VK_TAB:
+ if (this.onTab) {
+ this.onTab(event);
+ }
+ break;
+ }
+ },
+
+ /**
+ * Overlay the target node with an edit field.
+ *
+ * @param {Node} target
+ * Dom node to be edited.
+ */
+ edit: function (target) {
+ if (!target) {
+ return;
+ }
+
+ target.scrollIntoView(false);
+ target.focus();
+
+ if (!target.matches(this.selectors.join(","))) {
+ return;
+ }
+
+ // If we are actively editing something complete the edit first.
+ if (this.isEditing) {
+ this.completeEdit();
+ }
+
+ this.copyStyles(target, this.textbox);
+
+ target.parentNode.insertBefore(this.textbox, target);
+ this.currentTarget = target;
+ this.textbox.value = this.currentValue = target.value;
+ target.hidden = true;
+ this.textbox.hidden = false;
+
+ this.textbox.focus();
+ this.textbox.select();
+ },
+
+ completeEdit: function () {
+ if (!this.isEditing) {
+ return;
+ }
+
+ let oldValue = this.currentValue;
+ let newValue = this.textbox.value;
+ let changed = oldValue !== newValue;
+
+ this.textbox.hidden = true;
+
+ if (!this.currentTarget) {
+ return;
+ }
+
+ this.currentTarget.hidden = false;
+ if (changed) {
+ this.currentTarget.value = newValue;
+
+ let data = {
+ change: {
+ field: this.currentTarget,
+ oldValue: oldValue,
+ newValue: newValue
+ }
+ };
+
+ this.emit("change", data);
+ }
+ },
+
+ /**
+ * Cancel an edit.
+ */
+ cancelEdit: function () {
+ if (!this.isEditing) {
+ return;
+ }
+ if (this.currentTarget) {
+ this.currentTarget.hidden = false;
+ }
+
+ this.textbox.hidden = true;
+ },
+
+ /**
+ * Stop edit mode and apply changes.
+ */
+ blur: function () {
+ if (this.isEditing) {
+ this.completeEdit();
+ }
+ },
+
+ /**
+ * Copies various styles from one node to another.
+ *
+ * @param {Node} source
+ * The node to copy styles from.
+ * @param {Node} destination [description]
+ * The node to copy styles to.
+ */
+ copyStyles: function (source, destination) {
+ let style = source.ownerDocument.defaultView.getComputedStyle(source);
+ let props = [
+ "borderTopWidth",
+ "borderRightWidth",
+ "borderBottomWidth",
+ "borderLeftWidth",
+ "fontFamily",
+ "fontSize",
+ "fontWeight",
+ "height",
+ "marginTop",
+ "marginRight",
+ "marginBottom",
+ "marginLeft",
+ "marginInlineStart",
+ "marginInlineEnd"
+ ];
+
+ for (let prop of props) {
+ destination.style[prop] = style[prop];
+ }
+
+ // We need to set the label width to 100% to work around a XUL flex bug.
+ destination.style.width = "100%";
+ },
+
+ /**
+ * Destroys all editors in the current document.
+ */
+ destroy: function () {
+ if (this.textbox) {
+ this.textbox.removeEventListener("keydown", this.onKeydown);
+ this.textbox.remove();
+ }
+
+ if (this.root) {
+ this.root.removeEventListener(this.onTriggerEvent, this.onTrigger);
+ this.root.ownerDocument.removeEventListener("blur", this.completeEdit);
+ }
+
+ this._textbox = this.root = this.selectors = this.onTab = null;
+ this.currentTarget = this.currentValue = null;
+
+ this.emit("destroyed");
+ },
+};
diff --git a/devtools/client/shared/widgets/TreeWidget.js b/devtools/client/shared/widgets/TreeWidget.js
new file mode 100644
index 000000000..1f766cc6b
--- /dev/null
+++ b/devtools/client/shared/widgets/TreeWidget.js
@@ -0,0 +1,605 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+/**
+ * A tree widget with keyboard navigation and collapsable structure.
+ *
+ * @param {nsIDOMNode} node
+ * The container element for the tree widget.
+ * @param {Object} options
+ * - emptyText {string}: text to display when no entries in the table.
+ * - defaultType {string}: The default type of the tree items. For ex.
+ * 'js'
+ * - sorted {boolean}: Defaults to true. If true, tree items are kept in
+ * lexical order. If false, items will be kept in insertion order.
+ * - contextMenuId {string}: ID of context menu to be displayed on
+ * tree items.
+ */
+function TreeWidget(node, options = {}) {
+ EventEmitter.decorate(this);
+
+ this.document = node.ownerDocument;
+ this.window = this.document.defaultView;
+ this._parent = node;
+
+ this.emptyText = options.emptyText || "";
+ this.defaultType = options.defaultType;
+ this.sorted = options.sorted !== false;
+ this.contextMenuId = options.contextMenuId;
+
+ this.setupRoot();
+
+ this.placeholder = this.document.createElementNS(HTML_NS, "label");
+ this.placeholder.className = "tree-widget-empty-text";
+ this._parent.appendChild(this.placeholder);
+
+ if (this.emptyText) {
+ this.setPlaceholderText(this.emptyText);
+ }
+ // A map to hold all the passed attachment to each leaf in the tree.
+ this.attachments = new Map();
+}
+
+TreeWidget.prototype = {
+
+ _selectedLabel: null,
+ _selectedItem: null,
+
+ /**
+ * Select any node in the tree.
+ *
+ * @param {array} ids
+ * An array of ids leading upto the selected item
+ */
+ set selectedItem(ids) {
+ if (this._selectedLabel) {
+ this._selectedLabel.classList.remove("theme-selected");
+ }
+ let currentSelected = this._selectedLabel;
+ if (ids == -1) {
+ this._selectedLabel = this._selectedItem = null;
+ return;
+ }
+ if (!Array.isArray(ids)) {
+ return;
+ }
+ this._selectedLabel = this.root.setSelectedItem(ids);
+ if (!this._selectedLabel) {
+ this._selectedItem = null;
+ } else {
+ if (currentSelected != this._selectedLabel) {
+ this.ensureSelectedVisible();
+ }
+ this._selectedItem = ids;
+ this.emit("select", this._selectedItem,
+ this.attachments.get(JSON.stringify(ids)));
+ }
+ },
+
+ /**
+ * Gets the selected item in the tree.
+ *
+ * @return {array}
+ * An array of ids leading upto the selected item
+ */
+ get selectedItem() {
+ return this._selectedItem;
+ },
+
+ /**
+ * Returns if the passed array corresponds to the selected item in the tree.
+ *
+ * @return {array}
+ * An array of ids leading upto the requested item
+ */
+ isSelected: function (item) {
+ if (!this._selectedItem || this._selectedItem.length != item.length) {
+ return false;
+ }
+
+ for (let i = 0; i < this._selectedItem.length; i++) {
+ if (this._selectedItem[i] != item[i]) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ destroy: function () {
+ this.root.remove();
+ this.root = null;
+ },
+
+ /**
+ * Sets up the root container of the TreeWidget.
+ */
+ setupRoot: function () {
+ this.root = new TreeItem(this.document);
+ if (this.contextMenuId) {
+ this.root.children.addEventListener("contextmenu", (event) => {
+ let menu = this.document.getElementById(this.contextMenuId);
+ menu.openPopupAtScreen(event.screenX, event.screenY, true);
+ });
+ }
+
+ this._parent.appendChild(this.root.children);
+
+ this.root.children.addEventListener("mousedown", e => this.onClick(e));
+ this.root.children.addEventListener("keypress", e => this.onKeypress(e));
+ },
+
+ /**
+ * Sets the text to be shown when no node is present in the tree
+ */
+ setPlaceholderText: function (text) {
+ this.placeholder.textContent = text;
+ },
+
+ /**
+ * Select any node in the tree.
+ *
+ * @param {array} id
+ * An array of ids leading upto the selected item
+ */
+ selectItem: function (id) {
+ this.selectedItem = id;
+ },
+
+ /**
+ * Selects the next visible item in the tree.
+ */
+ selectNextItem: function () {
+ let next = this.getNextVisibleItem();
+ if (next) {
+ this.selectedItem = next;
+ }
+ },
+
+ /**
+ * Selects the previos visible item in the tree
+ */
+ selectPreviousItem: function () {
+ let prev = this.getPreviousVisibleItem();
+ if (prev) {
+ this.selectedItem = prev;
+ }
+ },
+
+ /**
+ * Returns the next visible item in the tree
+ */
+ getNextVisibleItem: function () {
+ let node = this._selectedLabel;
+ if (node.hasAttribute("expanded") && node.nextSibling.firstChild) {
+ return JSON.parse(node.nextSibling.firstChild.getAttribute("data-id"));
+ }
+ node = node.parentNode;
+ if (node.nextSibling) {
+ return JSON.parse(node.nextSibling.getAttribute("data-id"));
+ }
+ node = node.parentNode;
+ while (node.parentNode && node != this.root.children) {
+ if (node.parentNode && node.parentNode.nextSibling) {
+ return JSON.parse(node.parentNode.nextSibling.getAttribute("data-id"));
+ }
+ node = node.parentNode;
+ }
+ return null;
+ },
+
+ /**
+ * Returns the previous visible item in the tree
+ */
+ getPreviousVisibleItem: function () {
+ let node = this._selectedLabel.parentNode;
+ if (node.previousSibling) {
+ node = node.previousSibling.firstChild;
+ while (node.hasAttribute("expanded") && !node.hasAttribute("empty")) {
+ if (!node.nextSibling.lastChild) {
+ break;
+ }
+ node = node.nextSibling.lastChild.firstChild;
+ }
+ return JSON.parse(node.parentNode.getAttribute("data-id"));
+ }
+ node = node.parentNode;
+ if (node.parentNode && node != this.root.children) {
+ node = node.parentNode;
+ while (node.hasAttribute("expanded") && !node.hasAttribute("empty")) {
+ if (!node.nextSibling.firstChild) {
+ break;
+ }
+ node = node.nextSibling.firstChild.firstChild;
+ }
+ return JSON.parse(node.getAttribute("data-id"));
+ }
+ return null;
+ },
+
+ clearSelection: function () {
+ this.selectedItem = -1;
+ },
+
+ /**
+ * Adds an item in the tree. The item can be added as a child to any node in
+ * the tree. The method will also create any subnode not present in the
+ * process.
+ *
+ * @param {[string|object]} items
+ * An array of either string or objects where each increasing index
+ * represents an item corresponding to an equivalent depth in the tree.
+ * Each array element can be either just a string with the value as the
+ * id of of that item as well as the display value, or it can be an
+ * object with the following propeties:
+ * - id {string} The id of the item
+ * - label {string} The display value of the item
+ * - node {DOMNode} The dom node if you want to insert some custom
+ * element as the item. The label property is not used in this
+ * case
+ * - attachment {object} Any object to be associated with this item.
+ * - type {string} The type of this particular item. If this is null,
+ * then defaultType will be used.
+ * For example, if items = ["foo", "bar", { id: "id1", label: "baz" }]
+ * and the tree is empty, then the following hierarchy will be created
+ * in the tree:
+ * foo
+ * └ bar
+ * └ baz
+ * Passing the string id instead of the complete object helps when you
+ * are simply adding children to an already existing node and you know
+ * its id.
+ */
+ add: function (items) {
+ this.root.add(items, this.defaultType, this.sorted);
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].attachment) {
+ this.attachments.set(JSON.stringify(
+ items.slice(0, i + 1).map(item => item.id || item)
+ ), items[i].attachment);
+ }
+ }
+ // Empty the empty-tree-text
+ this.setPlaceholderText("");
+ },
+
+ /**
+ * Removes the specified item and all of its child items from the tree.
+ *
+ * @param {array} item
+ * The array of ids leading up to the item.
+ */
+ remove: function (item) {
+ this.root.remove(item);
+ this.attachments.delete(JSON.stringify(item));
+ // Display the empty tree text
+ if (this.root.items.size == 0 && this.emptyText) {
+ this.setPlaceholderText(this.emptyText);
+ }
+ },
+
+ /**
+ * Removes all of the child nodes from this tree.
+ */
+ clear: function () {
+ this.root.remove();
+ this.setupRoot();
+ this.attachments.clear();
+ if (this.emptyText) {
+ this.setPlaceholderText(this.emptyText);
+ }
+ },
+
+ /**
+ * Expands the tree completely
+ */
+ expandAll: function () {
+ this.root.expandAll();
+ },
+
+ /**
+ * Collapses the tree completely
+ */
+ collapseAll: function () {
+ this.root.collapseAll();
+ },
+
+ /**
+ * Click handler for the tree. Used to select, open and close the tree nodes.
+ */
+ onClick: function (event) {
+ let target = event.originalTarget;
+ while (target && !target.classList.contains("tree-widget-item")) {
+ if (target == this.root.children) {
+ return;
+ }
+ target = target.parentNode;
+ }
+ if (!target) {
+ return;
+ }
+
+ if (target.hasAttribute("expanded")) {
+ target.removeAttribute("expanded");
+ } else {
+ target.setAttribute("expanded", "true");
+ }
+
+ if (this._selectedLabel != target) {
+ let ids = target.parentNode.getAttribute("data-id");
+ this.selectedItem = JSON.parse(ids);
+ }
+ },
+
+ /**
+ * Keypress handler for this tree. Used to select next and previous visible
+ * items, as well as collapsing and expanding any item.
+ */
+ onKeypress: function (event) {
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_UP:
+ this.selectPreviousItem();
+ break;
+
+ case KeyCodes.DOM_VK_DOWN:
+ this.selectNextItem();
+ break;
+
+ case KeyCodes.DOM_VK_RIGHT:
+ if (this._selectedLabel.hasAttribute("expanded")) {
+ this.selectNextItem();
+ } else {
+ this._selectedLabel.setAttribute("expanded", "true");
+ }
+ break;
+
+ case KeyCodes.DOM_VK_LEFT:
+ if (this._selectedLabel.hasAttribute("expanded") &&
+ !this._selectedLabel.hasAttribute("empty")) {
+ this._selectedLabel.removeAttribute("expanded");
+ } else {
+ this.selectPreviousItem();
+ }
+ break;
+
+ default: return;
+ }
+ event.preventDefault();
+ },
+
+ /**
+ * Scrolls the viewport of the tree so that the selected item is always
+ * visible.
+ */
+ ensureSelectedVisible: function () {
+ let {top, bottom} = this._selectedLabel.getBoundingClientRect();
+ let height = this.root.children.parentNode.clientHeight;
+ if (top < 0) {
+ this._selectedLabel.scrollIntoView();
+ } else if (bottom > height) {
+ this._selectedLabel.scrollIntoView(false);
+ }
+ }
+};
+
+module.exports.TreeWidget = TreeWidget;
+
+/**
+ * Any item in the tree. This can be an empty leaf node also.
+ *
+ * @param {HTMLDocument} document
+ * The document element used for creating new nodes.
+ * @param {TreeItem} parent
+ * The parent item for this item.
+ * @param {string|DOMElement} label
+ * Either the dom node to be used as the item, or the string to be
+ * displayed for this node in the tree
+ * @param {string} type
+ * The type of the current node. For ex. "js"
+ */
+function TreeItem(document, parent, label, type) {
+ this.document = document;
+ this.node = this.document.createElementNS(HTML_NS, "li");
+ this.node.setAttribute("tabindex", "0");
+ this.isRoot = !parent;
+ this.parent = parent;
+ if (this.parent) {
+ this.level = this.parent.level + 1;
+ }
+ if (label) {
+ this.label = this.document.createElementNS(HTML_NS, "div");
+ this.label.setAttribute("empty", "true");
+ this.label.setAttribute("level", this.level);
+ this.label.className = "tree-widget-item";
+ if (type) {
+ this.label.setAttribute("type", type);
+ }
+ if (typeof label == "string") {
+ this.label.textContent = label;
+ } else {
+ this.label.appendChild(label);
+ }
+ this.node.appendChild(this.label);
+ }
+ this.children = this.document.createElementNS(HTML_NS, "ul");
+ if (this.isRoot) {
+ this.children.className = "tree-widget-container";
+ } else {
+ this.children.className = "tree-widget-children";
+ }
+ this.node.appendChild(this.children);
+ this.items = new Map();
+}
+
+TreeItem.prototype = {
+
+ items: null,
+
+ isSelected: false,
+
+ expanded: false,
+
+ isRoot: false,
+
+ parent: null,
+
+ children: null,
+
+ level: 0,
+
+ /**
+ * Adds the item to the sub tree contained by this node. The item to be
+ * inserted can be a direct child of this node, or further down the tree.
+ *
+ * @param {array} items
+ * Same as TreeWidget.add method's argument
+ * @param {string} defaultType
+ * The default type of the item to be used when items[i].type is null
+ * @param {boolean} sorted
+ * true if the tree items are inserted in a lexically sorted manner.
+ * Otherwise, false if the item are to be appended to their parent.
+ */
+ add: function (items, defaultType, sorted) {
+ if (items.length == this.level) {
+ // This is the exit condition of recursive TreeItem.add calls
+ return;
+ }
+ // Get the id and label corresponding to this level inside the tree.
+ let id = items[this.level].id || items[this.level];
+ if (this.items.has(id)) {
+ // An item with same id already exists, thus calling the add method of
+ // that child to add the passed node at correct position.
+ this.items.get(id).add(items, defaultType, sorted);
+ return;
+ }
+ // No item with the id `id` exists, so we create one and call the add
+ // method of that item.
+ // The display string of the item can be the label, the id, or the item
+ // itself if its a plain string.
+ let label = items[this.level].label ||
+ items[this.level].id ||
+ items[this.level];
+ let node = items[this.level].node;
+ if (node) {
+ // The item is supposed to be a DOMNode, so we fetch the textContent in
+ // order to find the correct sorted location of this new item.
+ label = node.textContent;
+ }
+ let treeItem = new TreeItem(this.document, this, node || label,
+ items[this.level].type || defaultType);
+
+ treeItem.add(items, defaultType, sorted);
+ treeItem.node.setAttribute("data-id", JSON.stringify(
+ items.slice(0, this.level + 1).map(item => item.id || item)
+ ));
+
+ if (sorted) {
+ // Inserting this newly created item at correct position
+ let nextSibling = [...this.items.values()].find(child => {
+ return child.label.textContent >= label;
+ });
+
+ if (nextSibling) {
+ this.children.insertBefore(treeItem.node, nextSibling.node);
+ } else {
+ this.children.appendChild(treeItem.node);
+ }
+ } else {
+ this.children.appendChild(treeItem.node);
+ }
+
+ if (this.label) {
+ this.label.removeAttribute("empty");
+ }
+ this.items.set(id, treeItem);
+ },
+
+ /**
+ * If this item is to be removed, then removes this item and thus all of its
+ * subtree. Otherwise, call the remove method of appropriate child. This
+ * recursive method goes on till we have reached the end of the branch or the
+ * current item is to be removed.
+ *
+ * @param {array} items
+ * Ids of items leading up to the item to be removed.
+ */
+ remove: function (items = []) {
+ let id = items.shift();
+ if (id && this.items.has(id)) {
+ let deleted = this.items.get(id);
+ if (!items.length) {
+ this.items.delete(id);
+ }
+ if (this.items.size == 0) {
+ this.label.setAttribute("empty", "true");
+ }
+ deleted.remove(items);
+ } else if (!id) {
+ this.destroy();
+ }
+ },
+
+ /**
+ * If this item is to be selected, then selected and expands the item.
+ * Otherwise, if a child item is to be selected, just expands this item.
+ *
+ * @param {array} items
+ * Ids of items leading up to the item to be selected.
+ */
+ setSelectedItem: function (items) {
+ if (!items[this.level]) {
+ this.label.classList.add("theme-selected");
+ this.label.setAttribute("expanded", "true");
+ return this.label;
+ }
+ if (this.items.has(items[this.level])) {
+ let label = this.items.get(items[this.level]).setSelectedItem(items);
+ if (label && this.label) {
+ this.label.setAttribute("expanded", true);
+ }
+ return label;
+ }
+ return null;
+ },
+
+ /**
+ * Collapses this item and all of its sub tree items
+ */
+ collapseAll: function () {
+ if (this.label) {
+ this.label.removeAttribute("expanded");
+ }
+ for (let child of this.items.values()) {
+ child.collapseAll();
+ }
+ },
+
+ /**
+ * Expands this item and all of its sub tree items
+ */
+ expandAll: function () {
+ if (this.label) {
+ this.label.setAttribute("expanded", "true");
+ }
+ for (let child of this.items.values()) {
+ child.expandAll();
+ }
+ },
+
+ destroy: function () {
+ this.children.remove();
+ this.node.remove();
+ this.label = null;
+ this.items = null;
+ this.children = null;
+ }
+};
diff --git a/devtools/client/shared/widgets/VariablesView.jsm b/devtools/client/shared/widgets/VariablesView.jsm
new file mode 100644
index 000000000..c291066ba
--- /dev/null
+++ b/devtools/client/shared/widgets/VariablesView.jsm
@@ -0,0 +1,4182 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
+const LAZY_EMPTY_DELAY = 150; // ms
+const SCROLL_PAGE_SIZE_DEFAULT = 0;
+const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
+const PAGE_SIZE_MAX_JUMPS = 30;
+const SEARCH_ACTION_MAX_DELAY = 300; // ms
+const ITEM_FLASH_DURATION = 300; // ms
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+const EventEmitter = require("devtools/shared/event-emitter");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const Services = require("Services");
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const { Heritage, ViewHelpers, setNamedTimeout } =
+ require("devtools/client/shared/widgets/view-helpers");
+const { Task } = require("devtools/shared/task");
+const nodeConstants = require("devtools/shared/dom-node-constants");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+const {PluralForm} = require("devtools/shared/plural-form");
+const {LocalizationHelper, ELLIPSIS} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper(DBG_STRINGS_URI);
+
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+
+Object.defineProperty(this, "WebConsoleUtils", {
+ get: function () {
+ return require("devtools/client/webconsole/utils").Utils;
+ },
+ configurable: true,
+ enumerable: true
+});
+
+Object.defineProperty(this, "NetworkHelper", {
+ get: function () {
+ return require("devtools/shared/webconsole/network-helper");
+ },
+ configurable: true,
+ enumerable: true
+});
+
+this.EXPORTED_SYMBOLS = ["VariablesView", "escapeHTML"];
+
+/**
+ * A tree view for inspecting scopes, objects and properties.
+ * Iterable via "for (let [id, scope] of instance) { }".
+ * Requires the devtools common.css and debugger.css skin stylesheets.
+ *
+ * To allow replacing variable or property values in this view, provide an
+ * "eval" function property. To allow replacing variable or property names,
+ * provide a "switch" function. To handle deleting variables or properties,
+ * provide a "delete" function.
+ *
+ * @param nsIDOMNode aParentNode
+ * The parent node to hold this view.
+ * @param object aFlags [optional]
+ * An object contaning initialization options for this view.
+ * e.g. { lazyEmpty: true, searchEnabled: true ... }
+ */
+this.VariablesView = function VariablesView(aParentNode, aFlags = {}) {
+ this._store = []; // Can't use a Map because Scope names needn't be unique.
+ this._itemsByElement = new WeakMap();
+ this._prevHierarchy = new Map();
+ this._currHierarchy = new Map();
+
+ this._parent = aParentNode;
+ this._parent.classList.add("variables-view-container");
+ this._parent.classList.add("theme-body");
+ this._appendEmptyNotice();
+
+ this._onSearchboxInput = this._onSearchboxInput.bind(this);
+ this._onSearchboxKeyPress = this._onSearchboxKeyPress.bind(this);
+ this._onViewKeyPress = this._onViewKeyPress.bind(this);
+ this._onViewKeyDown = this._onViewKeyDown.bind(this);
+
+ // Create an internal scrollbox container.
+ this._list = this.document.createElement("scrollbox");
+ this._list.setAttribute("orient", "vertical");
+ this._list.addEventListener("keypress", this._onViewKeyPress, false);
+ this._list.addEventListener("keydown", this._onViewKeyDown, false);
+ this._parent.appendChild(this._list);
+
+ for (let name in aFlags) {
+ this[name] = aFlags[name];
+ }
+
+ EventEmitter.decorate(this);
+};
+
+VariablesView.prototype = {
+ /**
+ * Helper setter for populating this container with a raw object.
+ *
+ * @param object aObject
+ * The raw object to display. You can only provide this object
+ * if you want the variables view to work in sync mode.
+ */
+ set rawObject(aObject) {
+ this.empty();
+ this.addScope()
+ .addItem(undefined, { enumerable: true })
+ .populate(aObject, { sorted: true });
+ },
+
+ /**
+ * Adds a scope to contain any inspected variables.
+ *
+ * This new scope will be considered the parent of any other scope
+ * added afterwards.
+ *
+ * @param string aName
+ * The scope's name (e.g. "Local", "Global" etc.).
+ * @param string aCustomClass
+ * An additional class name for the containing element.
+ * @return Scope
+ * The newly created Scope instance.
+ */
+ addScope: function (aName = "", aCustomClass = "") {
+ this._removeEmptyNotice();
+ this._toggleSearchVisibility(true);
+
+ let scope = new Scope(this, aName, { customClass: aCustomClass });
+ this._store.push(scope);
+ this._itemsByElement.set(scope._target, scope);
+ this._currHierarchy.set(aName, scope);
+ scope.header = !!aName;
+
+ return scope;
+ },
+
+ /**
+ * Removes all items from this container.
+ *
+ * @param number aTimeout [optional]
+ * The number of milliseconds to delay the operation if
+ * lazy emptying of this container is enabled.
+ */
+ empty: function (aTimeout = this.lazyEmptyDelay) {
+ // If there are no items in this container, emptying is useless.
+ if (!this._store.length) {
+ return;
+ }
+
+ this._store.length = 0;
+ this._itemsByElement = new WeakMap();
+ this._prevHierarchy = this._currHierarchy;
+ this._currHierarchy = new Map(); // Don't clear, this is just simple swapping.
+
+ // Check if this empty operation may be executed lazily.
+ if (this.lazyEmpty && aTimeout > 0) {
+ this._emptySoon(aTimeout);
+ return;
+ }
+
+ while (this._list.hasChildNodes()) {
+ this._list.firstChild.remove();
+ }
+
+ this._appendEmptyNotice();
+ this._toggleSearchVisibility(false);
+ },
+
+ /**
+ * Emptying this container and rebuilding it immediately afterwards would
+ * result in a brief redraw flicker, because the previously expanded nodes
+ * may get asynchronously re-expanded, after fetching the prototype and
+ * properties from a server.
+ *
+ * To avoid such behaviour, a normal container list is rebuild, but not
+ * immediately attached to the parent container. The old container list
+ * is kept around for a short period of time, hopefully accounting for the
+ * data fetching delay. In the meantime, any operations can be executed
+ * normally.
+ *
+ * @see VariablesView.empty
+ * @see VariablesView.commitHierarchy
+ */
+ _emptySoon: function (aTimeout) {
+ let prevList = this._list;
+ let currList = this._list = this.document.createElement("scrollbox");
+
+ this.window.setTimeout(() => {
+ prevList.removeEventListener("keypress", this._onViewKeyPress, false);
+ prevList.removeEventListener("keydown", this._onViewKeyDown, false);
+ currList.addEventListener("keypress", this._onViewKeyPress, false);
+ currList.addEventListener("keydown", this._onViewKeyDown, false);
+ currList.setAttribute("orient", "vertical");
+
+ this._parent.removeChild(prevList);
+ this._parent.appendChild(currList);
+
+ if (!this._store.length) {
+ this._appendEmptyNotice();
+ this._toggleSearchVisibility(false);
+ }
+ }, aTimeout);
+ },
+
+ /**
+ * Optional DevTools toolbox containing this VariablesView. Used to
+ * communicate with the inspector and highlighter.
+ */
+ toolbox: null,
+
+ /**
+ * The controller for this VariablesView, if it has one.
+ */
+ controller: null,
+
+ /**
+ * The amount of time (in milliseconds) it takes to empty this view lazily.
+ */
+ lazyEmptyDelay: LAZY_EMPTY_DELAY,
+
+ /**
+ * Specifies if this view may be emptied lazily.
+ * @see VariablesView.prototype.empty
+ */
+ lazyEmpty: false,
+
+ /**
+ * Specifies if nodes in this view may be searched lazily.
+ */
+ lazySearch: true,
+
+ /**
+ * The number of elements in this container to jump when Page Up or Page Down
+ * keys are pressed. If falsy, then the page size will be based on the
+ * container height.
+ */
+ scrollPageSize: SCROLL_PAGE_SIZE_DEFAULT,
+
+ /**
+ * Function called each time a variable or property's value is changed via
+ * user interaction. If null, then value changes are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ eval: null,
+
+ /**
+ * Function called each time a variable or property's name is changed via
+ * user interaction. If null, then name changes are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ switch: null,
+
+ /**
+ * Function called each time a variable or property is deleted via
+ * user interaction. If null, then deletions are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ delete: null,
+
+ /**
+ * Function called each time a property is added via user interaction. If
+ * null, then property additions are disabled.
+ *
+ * This property is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ new: null,
+
+ /**
+ * Specifies if after an eval or switch operation, the variable or property
+ * which has been edited should be disabled.
+ */
+ preventDisableOnChange: false,
+
+ /**
+ * Specifies if, whenever a variable or property descriptor is available,
+ * configurable, enumerable, writable, frozen, sealed and extensible
+ * attributes should not affect presentation.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ preventDescriptorModifiers: false,
+
+ /**
+ * The tooltip text shown on a variable or property's value if an |eval|
+ * function is provided, in order to change the variable or property's value.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ editableValueTooltip: L10N.getStr("variablesEditableValueTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's name if a |switch|
+ * function is provided, in order to change the variable or property's name.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ editableNameTooltip: L10N.getStr("variablesEditableNameTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's edit button if an
+ * |eval| function is provided and a getter/setter descriptor is present,
+ * in order to change the variable or property to a plain value.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ editButtonTooltip: L10N.getStr("variablesEditButtonTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's value if that value is
+ * a DOMNode that can be highlighted and selected in the inspector.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ domNodeValueTooltip: L10N.getStr("variablesDomNodeValueTooltip"),
+
+ /**
+ * The tooltip text shown on a variable or property's delete button if a
+ * |delete| function is provided, in order to delete the variable or property.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ deleteButtonTooltip: L10N.getStr("variablesCloseButtonTooltip"),
+
+ /**
+ * Specifies the context menu attribute set on variables and properties.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ contextMenuId: "",
+
+ /**
+ * The separator label between the variables or properties name and value.
+ *
+ * This flag is applied recursively onto each scope in this view and
+ * affects only the child nodes when they're created.
+ */
+ separatorStr: L10N.getStr("variablesSeparatorLabel"),
+
+ /**
+ * Specifies if enumerable properties and variables should be displayed.
+ * These variables and properties are visible by default.
+ * @param boolean aFlag
+ */
+ set enumVisible(aFlag) {
+ this._enumVisible = aFlag;
+
+ for (let scope of this._store) {
+ scope._enumVisible = aFlag;
+ }
+ },
+
+ /**
+ * Specifies if non-enumerable properties and variables should be displayed.
+ * These variables and properties are visible by default.
+ * @param boolean aFlag
+ */
+ set nonEnumVisible(aFlag) {
+ this._nonEnumVisible = aFlag;
+
+ for (let scope of this._store) {
+ scope._nonEnumVisible = aFlag;
+ }
+ },
+
+ /**
+ * Specifies if only enumerable properties and variables should be displayed.
+ * Both types of these variables and properties are visible by default.
+ * @param boolean aFlag
+ */
+ set onlyEnumVisible(aFlag) {
+ if (aFlag) {
+ this.enumVisible = true;
+ this.nonEnumVisible = false;
+ } else {
+ this.enumVisible = true;
+ this.nonEnumVisible = true;
+ }
+ },
+
+ /**
+ * Sets if the variable and property searching is enabled.
+ * @param boolean aFlag
+ */
+ set searchEnabled(aFlag) {
+ aFlag ? this._enableSearch() : this._disableSearch();
+ },
+
+ /**
+ * Gets if the variable and property searching is enabled.
+ * @return boolean
+ */
+ get searchEnabled() {
+ return !!this._searchboxContainer;
+ },
+
+ /**
+ * Sets the text displayed for the searchbox in this container.
+ * @param string aValue
+ */
+ set searchPlaceholder(aValue) {
+ if (this._searchboxNode) {
+ this._searchboxNode.setAttribute("placeholder", aValue);
+ }
+ this._searchboxPlaceholder = aValue;
+ },
+
+ /**
+ * Gets the text displayed for the searchbox in this container.
+ * @return string
+ */
+ get searchPlaceholder() {
+ return this._searchboxPlaceholder;
+ },
+
+ /**
+ * Enables variable and property searching in this view.
+ * Use the "searchEnabled" setter to enable searching.
+ */
+ _enableSearch: function () {
+ // If searching was already enabled, no need to re-enable it again.
+ if (this._searchboxContainer) {
+ return;
+ }
+ let document = this.document;
+ let ownerNode = this._parent.parentNode;
+
+ let container = this._searchboxContainer = document.createElement("hbox");
+ container.className = "devtools-toolbar";
+
+ // Hide the variables searchbox container if there are no variables or
+ // properties to display.
+ container.hidden = !this._store.length;
+
+ let searchbox = this._searchboxNode = document.createElement("textbox");
+ searchbox.className = "variables-view-searchinput devtools-filterinput";
+ searchbox.setAttribute("placeholder", this._searchboxPlaceholder);
+ searchbox.setAttribute("type", "search");
+ searchbox.setAttribute("flex", "1");
+ searchbox.addEventListener("command", this._onSearchboxInput, false);
+ searchbox.addEventListener("keypress", this._onSearchboxKeyPress, false);
+
+ container.appendChild(searchbox);
+ ownerNode.insertBefore(container, this._parent);
+ },
+
+ /**
+ * Disables variable and property searching in this view.
+ * Use the "searchEnabled" setter to disable searching.
+ */
+ _disableSearch: function () {
+ // If searching was already disabled, no need to re-disable it again.
+ if (!this._searchboxContainer) {
+ return;
+ }
+ this._searchboxContainer.remove();
+ this._searchboxNode.removeEventListener("command", this._onSearchboxInput, false);
+ this._searchboxNode.removeEventListener("keypress", this._onSearchboxKeyPress, false);
+
+ this._searchboxContainer = null;
+ this._searchboxNode = null;
+ },
+
+ /**
+ * Sets the variables searchbox container hidden or visible.
+ * It's hidden by default.
+ *
+ * @param boolean aVisibleFlag
+ * Specifies the intended visibility.
+ */
+ _toggleSearchVisibility: function (aVisibleFlag) {
+ // If searching was already disabled, there's no need to hide it.
+ if (!this._searchboxContainer) {
+ return;
+ }
+ this._searchboxContainer.hidden = !aVisibleFlag;
+ },
+
+ /**
+ * Listener handling the searchbox input event.
+ */
+ _onSearchboxInput: function () {
+ this.scheduleSearch(this._searchboxNode.value);
+ },
+
+ /**
+ * Listener handling the searchbox key press event.
+ */
+ _onSearchboxKeyPress: function (e) {
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_RETURN:
+ this._onSearchboxInput();
+ return;
+ case KeyCodes.DOM_VK_ESCAPE:
+ this._searchboxNode.value = "";
+ this._onSearchboxInput();
+ return;
+ }
+ },
+
+ /**
+ * Schedules searching for variables or properties matching the query.
+ *
+ * @param string aToken
+ * The variable or property to search for.
+ * @param number aWait
+ * The amount of milliseconds to wait until draining.
+ */
+ scheduleSearch: function (aToken, aWait) {
+ // Check if this search operation may not be executed lazily.
+ if (!this.lazySearch) {
+ this._doSearch(aToken);
+ return;
+ }
+
+ // The amount of time to wait for the requests to settle.
+ let maxDelay = SEARCH_ACTION_MAX_DELAY;
+ let delay = aWait === undefined ? maxDelay / aToken.length : aWait;
+
+ // Allow requests to settle down first.
+ setNamedTimeout("vview-search", delay, () => this._doSearch(aToken));
+ },
+
+ /**
+ * Performs a case insensitive search for variables or properties matching
+ * the query, and hides non-matched items.
+ *
+ * If aToken is falsy, then all the scopes are unhidden and expanded,
+ * while the available variables and properties inside those scopes are
+ * just unhidden.
+ *
+ * @param string aToken
+ * The variable or property to search for.
+ */
+ _doSearch: function (aToken) {
+ if (this.controller && this.controller.supportsSearch()) {
+ // Retrieve the main Scope in which we add attributes
+ let scope = this._store[0]._store.get(undefined);
+ if (!aToken) {
+ // Prune the view from old previous content
+ // so that we delete the intermediate search results
+ // we created in previous searches
+ for (let property of scope._store.values()) {
+ property.remove();
+ }
+ }
+ // Retrieve new attributes eventually hidden in splits
+ this.controller.performSearch(scope, aToken);
+ // Filter already displayed attributes
+ if (aToken) {
+ scope._performSearch(aToken.toLowerCase());
+ }
+ return;
+ }
+ for (let scope of this._store) {
+ switch (aToken) {
+ case "":
+ case null:
+ case undefined:
+ scope.expand();
+ scope._performSearch("");
+ break;
+ default:
+ scope._performSearch(aToken.toLowerCase());
+ break;
+ }
+ }
+ },
+
+ /**
+ * Find the first item in the tree of visible items in this container that
+ * matches the predicate. Searches in visual order (the order seen by the
+ * user). Descends into each scope to check the scope and its children.
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The first visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItems: function (aPredicate) {
+ for (let scope of this._store) {
+ let result = scope._findInVisibleItems(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Find the last item in the tree of visible items in this container that
+ * matches the predicate. Searches in reverse visual order (opposite of the
+ * order seen by the user). Descends into each scope to check the scope and
+ * its children.
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The last visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItemsReverse: function (aPredicate) {
+ for (let i = this._store.length - 1; i >= 0; i--) {
+ let scope = this._store[i];
+ let result = scope._findInVisibleItemsReverse(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Gets the scope at the specified index.
+ *
+ * @param number aIndex
+ * The scope's index.
+ * @return Scope
+ * The scope if found, undefined if not.
+ */
+ getScopeAtIndex: function (aIndex) {
+ return this._store[aIndex];
+ },
+
+ /**
+ * Recursively searches this container for the scope, variable or property
+ * displayed by the specified node.
+ *
+ * @param nsIDOMNode aNode
+ * The node to search for.
+ * @return Scope | Variable | Property
+ * The matched scope, variable or property, or null if nothing is found.
+ */
+ getItemForNode: function (aNode) {
+ return this._itemsByElement.get(aNode);
+ },
+
+ /**
+ * Gets the scope owning a Variable or Property.
+ *
+ * @param Variable | Property
+ * The variable or property to retrieven the owner scope for.
+ * @return Scope
+ * The owner scope.
+ */
+ getOwnerScopeForVariableOrProperty: function (aItem) {
+ if (!aItem) {
+ return null;
+ }
+ // If this is a Scope, return it.
+ if (!(aItem instanceof Variable)) {
+ return aItem;
+ }
+ // If this is a Variable or Property, find its owner scope.
+ if (aItem instanceof Variable && aItem.ownerView) {
+ return this.getOwnerScopeForVariableOrProperty(aItem.ownerView);
+ }
+ return null;
+ },
+
+ /**
+ * Gets the parent scopes for a specified Variable or Property.
+ * The returned list will not include the owner scope.
+ *
+ * @param Variable | Property
+ * The variable or property for which to find the parent scopes.
+ * @return array
+ * A list of parent Scopes.
+ */
+ getParentScopesForVariableOrProperty: function (aItem) {
+ let scope = this.getOwnerScopeForVariableOrProperty(aItem);
+ return this._store.slice(0, Math.max(this._store.indexOf(scope), 0));
+ },
+
+ /**
+ * Gets the currently focused scope, variable or property in this view.
+ *
+ * @return Scope | Variable | Property
+ * The focused scope, variable or property, or null if nothing is found.
+ */
+ getFocusedItem: function () {
+ let focused = this.document.commandDispatcher.focusedElement;
+ return this.getItemForNode(focused);
+ },
+
+ /**
+ * Focuses the first visible scope, variable, or property in this container.
+ */
+ focusFirstVisibleItem: function () {
+ let focusableItem = this._findInVisibleItems(item => item.focusable);
+ if (focusableItem) {
+ this._focusItem(focusableItem);
+ }
+ this._parent.scrollTop = 0;
+ this._parent.scrollLeft = 0;
+ },
+
+ /**
+ * Focuses the last visible scope, variable, or property in this container.
+ */
+ focusLastVisibleItem: function () {
+ let focusableItem = this._findInVisibleItemsReverse(item => item.focusable);
+ if (focusableItem) {
+ this._focusItem(focusableItem);
+ }
+ this._parent.scrollTop = this._parent.scrollHeight;
+ this._parent.scrollLeft = 0;
+ },
+
+ /**
+ * Focuses the next scope, variable or property in this view.
+ */
+ focusNextItem: function () {
+ this.focusItemAtDelta(+1);
+ },
+
+ /**
+ * Focuses the previous scope, variable or property in this view.
+ */
+ focusPrevItem: function () {
+ this.focusItemAtDelta(-1);
+ },
+
+ /**
+ * Focuses another scope, variable or property in this view, based on
+ * the index distance from the currently focused item.
+ *
+ * @param number aDelta
+ * A scalar specifying by how many items should the selection change.
+ */
+ focusItemAtDelta: function (aDelta) {
+ let direction = aDelta > 0 ? "advanceFocus" : "rewindFocus";
+ let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta));
+ while (distance--) {
+ if (!this._focusChange(direction)) {
+ break; // Out of bounds.
+ }
+ }
+ },
+
+ /**
+ * Focuses the next or previous scope, variable or property in this view.
+ *
+ * @param string aDirection
+ * Either "advanceFocus" or "rewindFocus".
+ * @return boolean
+ * False if the focus went out of bounds and the first or last element
+ * in this view was focused instead.
+ */
+ _focusChange: function (aDirection) {
+ let commandDispatcher = this.document.commandDispatcher;
+ let prevFocusedElement = commandDispatcher.focusedElement;
+ let currFocusedItem = null;
+
+ do {
+ commandDispatcher.suppressFocusScroll = true;
+ commandDispatcher[aDirection]();
+
+ // Make sure the newly focused item is a part of this view.
+ // If the focus goes out of bounds, revert the previously focused item.
+ if (!(currFocusedItem = this.getFocusedItem())) {
+ prevFocusedElement.focus();
+ return false;
+ }
+ } while (!currFocusedItem.focusable);
+
+ // Focus remained within bounds.
+ return true;
+ },
+
+ /**
+ * Focuses a scope, variable or property and makes sure it's visible.
+ *
+ * @param aItem Scope | Variable | Property
+ * The item to focus.
+ * @param boolean aCollapseFlag
+ * True if the focused item should also be collapsed.
+ * @return boolean
+ * True if the item was successfully focused.
+ */
+ _focusItem: function (aItem, aCollapseFlag) {
+ if (!aItem.focusable) {
+ return false;
+ }
+ if (aCollapseFlag) {
+ aItem.collapse();
+ }
+ aItem._target.focus();
+ this.boxObject.ensureElementIsVisible(aItem._arrow);
+ return true;
+ },
+
+ /**
+ * Listener handling a key press event on the view.
+ */
+ _onViewKeyPress: function (e) {
+ let item = this.getFocusedItem();
+
+ // Prevent scrolling when pressing navigation keys.
+ ViewHelpers.preventScrolling(e);
+
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_UP:
+ // Always rewind focus.
+ this.focusPrevItem(true);
+ return;
+
+ case KeyCodes.DOM_VK_DOWN:
+ // Always advance focus.
+ this.focusNextItem(true);
+ return;
+
+ case KeyCodes.DOM_VK_LEFT:
+ // Collapse scopes, variables and properties before rewinding focus.
+ if (item._isExpanded && item._isArrowVisible) {
+ item.collapse();
+ } else {
+ this._focusItem(item.ownerView);
+ }
+ return;
+
+ case KeyCodes.DOM_VK_RIGHT:
+ // Nothing to do here if this item never expands.
+ if (!item._isArrowVisible) {
+ return;
+ }
+ // Expand scopes, variables and properties before advancing focus.
+ if (!item._isExpanded) {
+ item.expand();
+ } else {
+ this.focusNextItem(true);
+ }
+ return;
+
+ case KeyCodes.DOM_VK_PAGE_UP:
+ // Rewind a certain number of elements based on the container height.
+ this.focusItemAtDelta(-(this.scrollPageSize || Math.min(Math.floor(this._list.scrollHeight /
+ PAGE_SIZE_SCROLL_HEIGHT_RATIO),
+ PAGE_SIZE_MAX_JUMPS)));
+ return;
+
+ case KeyCodes.DOM_VK_PAGE_DOWN:
+ // Advance a certain number of elements based on the container height.
+ this.focusItemAtDelta(+(this.scrollPageSize || Math.min(Math.floor(this._list.scrollHeight /
+ PAGE_SIZE_SCROLL_HEIGHT_RATIO),
+ PAGE_SIZE_MAX_JUMPS)));
+ return;
+
+ case KeyCodes.DOM_VK_HOME:
+ this.focusFirstVisibleItem();
+ return;
+
+ case KeyCodes.DOM_VK_END:
+ this.focusLastVisibleItem();
+ return;
+
+ case KeyCodes.DOM_VK_RETURN:
+ // Start editing the value or name of the Variable or Property.
+ if (item instanceof Variable) {
+ if (e.metaKey || e.altKey || e.shiftKey) {
+ item._activateNameInput();
+ } else {
+ item._activateValueInput();
+ }
+ }
+ return;
+
+ case KeyCodes.DOM_VK_DELETE:
+ case KeyCodes.DOM_VK_BACK_SPACE:
+ // Delete the Variable or Property if allowed.
+ if (item instanceof Variable) {
+ item._onDelete(e);
+ }
+ return;
+
+ case KeyCodes.DOM_VK_INSERT:
+ item._onAddProperty(e);
+ return;
+ }
+ },
+
+ /**
+ * Listener handling a key down event on the view.
+ */
+ _onViewKeyDown: function (e) {
+ if (e.keyCode == KeyCodes.DOM_VK_C) {
+ // Copy current selection to clipboard.
+ if (e.ctrlKey || e.metaKey) {
+ let item = this.getFocusedItem();
+ clipboardHelper.copyString(
+ item._nameString + item.separatorStr + item._valueString
+ );
+ }
+ }
+ },
+
+ /**
+ * Sets the text displayed in this container when there are no available items.
+ * @param string aValue
+ */
+ set emptyText(aValue) {
+ if (this._emptyTextNode) {
+ this._emptyTextNode.setAttribute("value", aValue);
+ }
+ this._emptyTextValue = aValue;
+ this._appendEmptyNotice();
+ },
+
+ /**
+ * Creates and appends a label signaling that this container is empty.
+ */
+ _appendEmptyNotice: function () {
+ if (this._emptyTextNode || !this._emptyTextValue) {
+ return;
+ }
+
+ let label = this.document.createElement("label");
+ label.className = "variables-view-empty-notice";
+ label.setAttribute("value", this._emptyTextValue);
+
+ this._parent.appendChild(label);
+ this._emptyTextNode = label;
+ },
+
+ /**
+ * Removes the label signaling that this container is empty.
+ */
+ _removeEmptyNotice: function () {
+ if (!this._emptyTextNode) {
+ return;
+ }
+
+ this._parent.removeChild(this._emptyTextNode);
+ this._emptyTextNode = null;
+ },
+
+ /**
+ * Gets if all values should be aligned together.
+ * @return boolean
+ */
+ get alignedValues() {
+ return this._alignedValues;
+ },
+
+ /**
+ * Sets if all values should be aligned together.
+ * @param boolean aFlag
+ */
+ set alignedValues(aFlag) {
+ this._alignedValues = aFlag;
+ if (aFlag) {
+ this._parent.setAttribute("aligned-values", "");
+ } else {
+ this._parent.removeAttribute("aligned-values");
+ }
+ },
+
+ /**
+ * Gets if action buttons (like delete) should be placed at the beginning or
+ * end of a line.
+ * @return boolean
+ */
+ get actionsFirst() {
+ return this._actionsFirst;
+ },
+
+ /**
+ * Sets if action buttons (like delete) should be placed at the beginning or
+ * end of a line.
+ * @param boolean aFlag
+ */
+ set actionsFirst(aFlag) {
+ this._actionsFirst = aFlag;
+ if (aFlag) {
+ this._parent.setAttribute("actions-first", "");
+ } else {
+ this._parent.removeAttribute("actions-first");
+ }
+ },
+
+ /**
+ * Gets the parent node holding this view.
+ * @return nsIDOMNode
+ */
+ get boxObject() {
+ return this._list.boxObject;
+ },
+
+ /**
+ * Gets the parent node holding this view.
+ * @return nsIDOMNode
+ */
+ get parentNode() {
+ return this._parent;
+ },
+
+ /**
+ * Gets the owner document holding this view.
+ * @return nsIHTMLDocument
+ */
+ get document() {
+ return this._document || (this._document = this._parent.ownerDocument);
+ },
+
+ /**
+ * Gets the default window holding this view.
+ * @return nsIDOMWindow
+ */
+ get window() {
+ return this._window || (this._window = this.document.defaultView);
+ },
+
+ _document: null,
+ _window: null,
+
+ _store: null,
+ _itemsByElement: null,
+ _prevHierarchy: null,
+ _currHierarchy: null,
+
+ _enumVisible: true,
+ _nonEnumVisible: true,
+ _alignedValues: false,
+ _actionsFirst: false,
+
+ _parent: null,
+ _list: null,
+ _searchboxNode: null,
+ _searchboxContainer: null,
+ _searchboxPlaceholder: "",
+ _emptyTextNode: null,
+ _emptyTextValue: ""
+};
+
+VariablesView.NON_SORTABLE_CLASSES = [
+ "Array",
+ "Int8Array",
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Int16Array",
+ "Uint16Array",
+ "Int32Array",
+ "Uint32Array",
+ "Float32Array",
+ "Float64Array",
+ "NodeList"
+];
+
+/**
+ * Determine whether an object's properties should be sorted based on its class.
+ *
+ * @param string aClassName
+ * The class of the object.
+ */
+VariablesView.isSortable = function (aClassName) {
+ return VariablesView.NON_SORTABLE_CLASSES.indexOf(aClassName) == -1;
+};
+
+/**
+ * Generates the string evaluated when performing simple value changes.
+ *
+ * @param Variable | Property aItem
+ * The current variable or property.
+ * @param string aCurrentString
+ * The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ * Prefix for the symbolic name.
+ * @return string
+ * The string to be evaluated.
+ */
+VariablesView.simpleValueEvalMacro = function (aItem, aCurrentString, aPrefix = "") {
+ return aPrefix + aItem.symbolicName + "=" + aCurrentString;
+};
+
+/**
+ * Generates the string evaluated when overriding getters and setters with
+ * plain values.
+ *
+ * @param Property aItem
+ * The current getter or setter property.
+ * @param string aCurrentString
+ * The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ * Prefix for the symbolic name.
+ * @return string
+ * The string to be evaluated.
+ */
+VariablesView.overrideValueEvalMacro = function (aItem, aCurrentString, aPrefix = "") {
+ let property = escapeString(aItem._nameString);
+ let parent = aPrefix + aItem.ownerView.symbolicName || "this";
+
+ return "Object.defineProperty(" + parent + "," + property + "," +
+ "{ value: " + aCurrentString +
+ ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
+ ", configurable: true" +
+ ", writable: true" +
+ "})";
+};
+
+/**
+ * Generates the string evaluated when performing getters and setters changes.
+ *
+ * @param Property aItem
+ * The current getter or setter property.
+ * @param string aCurrentString
+ * The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ * Prefix for the symbolic name.
+ * @return string
+ * The string to be evaluated.
+ */
+VariablesView.getterOrSetterEvalMacro = function (aItem, aCurrentString, aPrefix = "") {
+ let type = aItem._nameString;
+ let propertyObject = aItem.ownerView;
+ let parentObject = propertyObject.ownerView;
+ let property = escapeString(propertyObject._nameString);
+ let parent = aPrefix + parentObject.symbolicName || "this";
+
+ switch (aCurrentString) {
+ case "":
+ case "null":
+ case "undefined":
+ let mirrorType = type == "get" ? "set" : "get";
+ let mirrorLookup = type == "get" ? "__lookupSetter__" : "__lookupGetter__";
+
+ // If the parent object will end up without any getter or setter,
+ // morph it into a plain value.
+ if ((type == "set" && propertyObject.getter.type == "undefined") ||
+ (type == "get" && propertyObject.setter.type == "undefined")) {
+ // Make sure the right getter/setter to value override macro is applied
+ // to the target object.
+ return propertyObject.evaluationMacro(propertyObject, "undefined", aPrefix);
+ }
+
+ // Construct and return the getter/setter removal evaluation string.
+ // e.g: Object.defineProperty(foo, "bar", {
+ // get: foo.__lookupGetter__("bar"),
+ // set: undefined,
+ // enumerable: true,
+ // configurable: true
+ // })
+ return "Object.defineProperty(" + parent + "," + property + "," +
+ "{" + mirrorType + ":" + parent + "." + mirrorLookup + "(" + property + ")" +
+ "," + type + ":" + undefined +
+ ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
+ ", configurable: true" +
+ "})";
+
+ default:
+ // Wrap statements inside a function declaration if not already wrapped.
+ if (!aCurrentString.startsWith("function")) {
+ let header = "function(" + (type == "set" ? "value" : "") + ")";
+ let body = "";
+ // If there's a return statement explicitly written, always use the
+ // standard function definition syntax
+ if (aCurrentString.includes("return ")) {
+ body = "{" + aCurrentString + "}";
+ }
+ // If block syntax is used, use the whole string as the function body.
+ else if (aCurrentString.startsWith("{")) {
+ body = aCurrentString;
+ }
+ // Prefer an expression closure.
+ else {
+ body = "(" + aCurrentString + ")";
+ }
+ aCurrentString = header + body;
+ }
+
+ // Determine if a new getter or setter should be defined.
+ let defineType = type == "get" ? "__defineGetter__" : "__defineSetter__";
+
+ // Make sure all quotes are escaped in the expression's syntax,
+ let defineFunc = "eval(\"(" + aCurrentString.replace(/"/g, "\\$&") + ")\")";
+
+ // Construct and return the getter/setter evaluation string.
+ // e.g: foo.__defineGetter__("bar", eval("(function() { return 42; })"))
+ return parent + "." + defineType + "(" + property + "," + defineFunc + ")";
+ }
+};
+
+/**
+ * Function invoked when a getter or setter is deleted.
+ *
+ * @param Property aItem
+ * The current getter or setter property.
+ */
+VariablesView.getterOrSetterDeleteCallback = function (aItem) {
+ aItem._disable();
+
+ // Make sure the right getter/setter to value override macro is applied
+ // to the target object.
+ aItem.ownerView.eval(aItem, "");
+
+ return true; // Don't hide the element.
+};
+
+
+/**
+ * A Scope is an object holding Variable instances.
+ * Iterable via "for (let [name, variable] of instance) { }".
+ *
+ * @param VariablesView aView
+ * The view to contain this scope.
+ * @param string aName
+ * The scope's name.
+ * @param object aFlags [optional]
+ * Additional options or flags for this scope.
+ */
+function Scope(aView, aName, aFlags = {}) {
+ this.ownerView = aView;
+
+ this._onClick = this._onClick.bind(this);
+ this._openEnum = this._openEnum.bind(this);
+ this._openNonEnum = this._openNonEnum.bind(this);
+
+ // Inherit properties and flags from the parent view. You can override
+ // each of these directly onto any scope, variable or property instance.
+ this.scrollPageSize = aView.scrollPageSize;
+ this.eval = aView.eval;
+ this.switch = aView.switch;
+ this.delete = aView.delete;
+ this.new = aView.new;
+ this.preventDisableOnChange = aView.preventDisableOnChange;
+ this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
+ this.editableNameTooltip = aView.editableNameTooltip;
+ this.editableValueTooltip = aView.editableValueTooltip;
+ this.editButtonTooltip = aView.editButtonTooltip;
+ this.deleteButtonTooltip = aView.deleteButtonTooltip;
+ this.domNodeValueTooltip = aView.domNodeValueTooltip;
+ this.contextMenuId = aView.contextMenuId;
+ this.separatorStr = aView.separatorStr;
+
+ this._init(aName, aFlags);
+}
+
+Scope.prototype = {
+ /**
+ * Whether this Scope should be prefetched when it is remoted.
+ */
+ shouldPrefetch: true,
+
+ /**
+ * Whether this Scope should paginate its contents.
+ */
+ allowPaginate: false,
+
+ /**
+ * The class name applied to this scope's target element.
+ */
+ targetClassName: "variables-view-scope",
+
+ /**
+ * Create a new Variable that is a child of this Scope.
+ *
+ * @param string aName
+ * The name of the new Property.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ * @param object aOptions
+ * Options of the form accepted by addItem.
+ * @return Variable
+ * The newly created child Variable.
+ */
+ _createChild: function (aName, aDescriptor, aOptions) {
+ return new Variable(this, aName, aDescriptor, aOptions);
+ },
+
+ /**
+ * Adds a child to contain any inspected properties.
+ *
+ * @param string aName
+ * The child's name.
+ * @param object aDescriptor
+ * Specifies the value and/or type & class of the child,
+ * or 'get' & 'set' accessor properties. If the type is implicit,
+ * it will be inferred from the value. If this parameter is omitted,
+ * a property without a value will be added (useful for branch nodes).
+ * e.g. - { value: 42 }
+ * - { value: true }
+ * - { value: "nasu" }
+ * - { value: { type: "undefined" } }
+ * - { value: { type: "null" } }
+ * - { value: { type: "object", class: "Object" } }
+ * - { get: { type: "object", class: "Function" },
+ * set: { type: "undefined" } }
+ * @param object aOptions
+ * Specifies some options affecting the new variable.
+ * Recognized properties are
+ * * boolean relaxed true if name duplicates should be allowed.
+ * You probably shouldn't do it. Use this
+ * with caution.
+ * * boolean internalItem true if the item is internally generated.
+ * This is used for special variables
+ * like <return> or <exception> and distinguishes
+ * them from ordinary properties that happen
+ * to have the same name
+ * @return Variable
+ * The newly created Variable instance, null if it already exists.
+ */
+ addItem: function (aName, aDescriptor = {}, aOptions = {}) {
+ let {relaxed} = aOptions;
+ if (this._store.has(aName) && !relaxed) {
+ return this._store.get(aName);
+ }
+
+ let child = this._createChild(aName, aDescriptor, aOptions);
+ this._store.set(aName, child);
+ this._variablesView._itemsByElement.set(child._target, child);
+ this._variablesView._currHierarchy.set(child.absoluteName, child);
+ child.header = aName !== undefined;
+
+ return child;
+ },
+
+ /**
+ * Adds items for this variable.
+ *
+ * @param object aItems
+ * An object containing some { name: descriptor } data properties,
+ * specifying the value and/or type & class of the variable,
+ * or 'get' & 'set' accessor properties. If the type is implicit,
+ * it will be inferred from the value.
+ * e.g. - { someProp0: { value: 42 },
+ * someProp1: { value: true },
+ * someProp2: { value: "nasu" },
+ * someProp3: { value: { type: "undefined" } },
+ * someProp4: { value: { type: "null" } },
+ * someProp5: { value: { type: "object", class: "Object" } },
+ * someProp6: { get: { type: "object", class: "Function" },
+ * set: { type: "undefined" } } }
+ * @param object aOptions [optional]
+ * Additional options for adding the properties. Supported options:
+ * - sorted: true to sort all the properties before adding them
+ * - callback: function invoked after each item is added
+ */
+ addItems: function (aItems, aOptions = {}) {
+ let names = Object.keys(aItems);
+
+ // Sort all of the properties before adding them, if preferred.
+ if (aOptions.sorted) {
+ names.sort(this._naturalSort);
+ }
+
+ // Add the properties to the current scope.
+ for (let name of names) {
+ let descriptor = aItems[name];
+ let item = this.addItem(name, descriptor);
+
+ if (aOptions.callback) {
+ aOptions.callback(item, descriptor && descriptor.value);
+ }
+ }
+ },
+
+ /**
+ * Remove this Scope from its parent and remove all children recursively.
+ */
+ remove: function () {
+ let view = this._variablesView;
+ view._store.splice(view._store.indexOf(this), 1);
+ view._itemsByElement.delete(this._target);
+ view._currHierarchy.delete(this._nameString);
+
+ this._target.remove();
+
+ for (let variable of this._store.values()) {
+ variable.remove();
+ }
+ },
+
+ /**
+ * Gets the variable in this container having the specified name.
+ *
+ * @param string aName
+ * The name of the variable to get.
+ * @return Variable
+ * The matched variable, or null if nothing is found.
+ */
+ get: function (aName) {
+ return this._store.get(aName);
+ },
+
+ /**
+ * Recursively searches for the variable or property in this container
+ * displayed by the specified node.
+ *
+ * @param nsIDOMNode aNode
+ * The node to search for.
+ * @return Variable | Property
+ * The matched variable or property, or null if nothing is found.
+ */
+ find: function (aNode) {
+ for (let [, variable] of this._store) {
+ let match;
+ if (variable._target == aNode) {
+ match = variable;
+ } else {
+ match = variable.find(aNode);
+ }
+ if (match) {
+ return match;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Determines if this scope is a direct child of a parent variables view,
+ * scope, variable or property.
+ *
+ * @param VariablesView | Scope | Variable | Property
+ * The parent to check.
+ * @return boolean
+ * True if the specified item is a direct child, false otherwise.
+ */
+ isChildOf: function (aParent) {
+ return this.ownerView == aParent;
+ },
+
+ /**
+ * Determines if this scope is a descendant of a parent variables view,
+ * scope, variable or property.
+ *
+ * @param VariablesView | Scope | Variable | Property
+ * The parent to check.
+ * @return boolean
+ * True if the specified item is a descendant, false otherwise.
+ */
+ isDescendantOf: function (aParent) {
+ if (this.isChildOf(aParent)) {
+ return true;
+ }
+
+ // Recurse to parent if it is a Scope, Variable, or Property.
+ if (this.ownerView instanceof Scope) {
+ return this.ownerView.isDescendantOf(aParent);
+ }
+
+ return false;
+ },
+
+ /**
+ * Shows the scope.
+ */
+ show: function () {
+ this._target.hidden = false;
+ this._isContentVisible = true;
+
+ if (this.onshow) {
+ this.onshow(this);
+ }
+ },
+
+ /**
+ * Hides the scope.
+ */
+ hide: function () {
+ this._target.hidden = true;
+ this._isContentVisible = false;
+
+ if (this.onhide) {
+ this.onhide(this);
+ }
+ },
+
+ /**
+ * Expands the scope, showing all the added details.
+ */
+ expand: function () {
+ if (this._isExpanded || this._isLocked) {
+ return;
+ }
+ if (this._variablesView._enumVisible) {
+ this._openEnum();
+ }
+ if (this._variablesView._nonEnumVisible) {
+ Services.tm.currentThread.dispatch({ run: this._openNonEnum }, 0);
+ }
+ this._isExpanded = true;
+
+ if (this.onexpand) {
+ // We return onexpand as it sometimes returns a promise
+ // (up to the user of VariableView to do it)
+ // that can indicate when the view is done expanding
+ // and attributes are available. (Mostly used for tests)
+ return this.onexpand(this);
+ }
+ },
+
+ /**
+ * Collapses the scope, hiding all the added details.
+ */
+ collapse: function () {
+ if (!this._isExpanded || this._isLocked) {
+ return;
+ }
+ this._arrow.removeAttribute("open");
+ this._enum.removeAttribute("open");
+ this._nonenum.removeAttribute("open");
+ this._isExpanded = false;
+
+ if (this.oncollapse) {
+ this.oncollapse(this);
+ }
+ },
+
+ /**
+ * Toggles between the scope's collapsed and expanded state.
+ */
+ toggle: function (e) {
+ if (e && e.button != 0) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+ this.expanded ^= 1;
+
+ // Make sure the scope and its contents are visibile.
+ for (let [, variable] of this._store) {
+ variable.header = true;
+ variable._matched = true;
+ }
+ if (this.ontoggle) {
+ this.ontoggle(this);
+ }
+ },
+
+ /**
+ * Shows the scope's title header.
+ */
+ showHeader: function () {
+ if (this._isHeaderVisible || !this._nameString) {
+ return;
+ }
+ this._target.removeAttribute("untitled");
+ this._isHeaderVisible = true;
+ },
+
+ /**
+ * Hides the scope's title header.
+ * This action will automatically expand the scope.
+ */
+ hideHeader: function () {
+ if (!this._isHeaderVisible) {
+ return;
+ }
+ this.expand();
+ this._target.setAttribute("untitled", "");
+ this._isHeaderVisible = false;
+ },
+
+ /**
+ * Sort in ascending order
+ * This only needs to compare non-numbers since it is dealing with an array
+ * which numeric-based indices are placed in order.
+ *
+ * @param string a
+ * @param string b
+ * @return number
+ * -1 if a is less than b, 0 if no change in order, +1 if a is greater than 0
+ */
+ _naturalSort: function (a, b) {
+ if (isNaN(parseFloat(a)) && isNaN(parseFloat(b))) {
+ return a < b ? -1 : 1;
+ }
+ },
+
+ /**
+ * Shows the scope's expand/collapse arrow.
+ */
+ showArrow: function () {
+ if (this._isArrowVisible) {
+ return;
+ }
+ this._arrow.removeAttribute("invisible");
+ this._isArrowVisible = true;
+ },
+
+ /**
+ * Hides the scope's expand/collapse arrow.
+ */
+ hideArrow: function () {
+ if (!this._isArrowVisible) {
+ return;
+ }
+ this._arrow.setAttribute("invisible", "");
+ this._isArrowVisible = false;
+ },
+
+ /**
+ * Gets the visibility state.
+ * @return boolean
+ */
+ get visible() {
+ return this._isContentVisible;
+ },
+
+ /**
+ * Gets the expanded state.
+ * @return boolean
+ */
+ get expanded() {
+ return this._isExpanded;
+ },
+
+ /**
+ * Gets the header visibility state.
+ * @return boolean
+ */
+ get header() {
+ return this._isHeaderVisible;
+ },
+
+ /**
+ * Gets the twisty visibility state.
+ * @return boolean
+ */
+ get twisty() {
+ return this._isArrowVisible;
+ },
+
+ /**
+ * Gets the expand lock state.
+ * @return boolean
+ */
+ get locked() {
+ return this._isLocked;
+ },
+
+ /**
+ * Sets the visibility state.
+ * @param boolean aFlag
+ */
+ set visible(aFlag) {
+ aFlag ? this.show() : this.hide();
+ },
+
+ /**
+ * Sets the expanded state.
+ * @param boolean aFlag
+ */
+ set expanded(aFlag) {
+ aFlag ? this.expand() : this.collapse();
+ },
+
+ /**
+ * Sets the header visibility state.
+ * @param boolean aFlag
+ */
+ set header(aFlag) {
+ aFlag ? this.showHeader() : this.hideHeader();
+ },
+
+ /**
+ * Sets the twisty visibility state.
+ * @param boolean aFlag
+ */
+ set twisty(aFlag) {
+ aFlag ? this.showArrow() : this.hideArrow();
+ },
+
+ /**
+ * Sets the expand lock state.
+ * @param boolean aFlag
+ */
+ set locked(aFlag) {
+ this._isLocked = aFlag;
+ },
+
+ /**
+ * Specifies if this target node may be focused.
+ * @return boolean
+ */
+ get focusable() {
+ // Check if this target node is actually visibile.
+ if (!this._nameString ||
+ !this._isContentVisible ||
+ !this._isHeaderVisible ||
+ !this._isMatch) {
+ return false;
+ }
+ // Check if all parent objects are expanded.
+ let item = this;
+
+ // Recurse while parent is a Scope, Variable, or Property
+ while ((item = item.ownerView) && item instanceof Scope) {
+ if (!item._isExpanded) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Focus this scope.
+ */
+ focus: function () {
+ this._variablesView._focusItem(this);
+ },
+
+ /**
+ * Adds an event listener for a certain event on this scope's title.
+ * @param string aName
+ * @param function aCallback
+ * @param boolean aCapture
+ */
+ addEventListener: function (aName, aCallback, aCapture) {
+ this._title.addEventListener(aName, aCallback, aCapture);
+ },
+
+ /**
+ * Removes an event listener for a certain event on this scope's title.
+ * @param string aName
+ * @param function aCallback
+ * @param boolean aCapture
+ */
+ removeEventListener: function (aName, aCallback, aCapture) {
+ this._title.removeEventListener(aName, aCallback, aCapture);
+ },
+
+ /**
+ * Gets the id associated with this item.
+ * @return string
+ */
+ get id() {
+ return this._idString;
+ },
+
+ /**
+ * Gets the name associated with this item.
+ * @return string
+ */
+ get name() {
+ return this._nameString;
+ },
+
+ /**
+ * Gets the displayed value for this item.
+ * @return string
+ */
+ get displayValue() {
+ return this._valueString;
+ },
+
+ /**
+ * Gets the class names used for the displayed value.
+ * @return string
+ */
+ get displayValueClassName() {
+ return this._valueClassName;
+ },
+
+ /**
+ * Gets the element associated with this item.
+ * @return nsIDOMNode
+ */
+ get target() {
+ return this._target;
+ },
+
+ /**
+ * Initializes this scope's id, view and binds event listeners.
+ *
+ * @param string aName
+ * The scope's name.
+ * @param object aFlags [optional]
+ * Additional options or flags for this scope.
+ */
+ _init: function (aName, aFlags) {
+ this._idString = generateId(this._nameString = aName);
+ this._displayScope(aName, `${this.targetClassName} ${aFlags.customClass}`,
+ "devtools-toolbar");
+ this._addEventListeners();
+ this.parentNode.appendChild(this._target);
+ },
+
+ /**
+ * Creates the necessary nodes for this scope.
+ *
+ * @param string aName
+ * The scope's name.
+ * @param string aTargetClassName
+ * A custom class name for this scope's target element.
+ * @param string aTitleClassName [optional]
+ * A custom class name for this scope's title element.
+ */
+ _displayScope: function (aName = "", aTargetClassName, aTitleClassName = "") {
+ let document = this.document;
+
+ let element = this._target = document.createElement("vbox");
+ element.id = this._idString;
+ element.className = aTargetClassName;
+
+ let arrow = this._arrow = document.createElement("hbox");
+ arrow.className = "arrow theme-twisty";
+
+ let name = this._name = document.createElement("label");
+ name.className = "plain name";
+ name.setAttribute("value", aName.trim());
+ name.setAttribute("crop", "end");
+
+ let title = this._title = document.createElement("hbox");
+ title.className = "title " + aTitleClassName;
+ title.setAttribute("align", "center");
+
+ let enumerable = this._enum = document.createElement("vbox");
+ let nonenum = this._nonenum = document.createElement("vbox");
+ enumerable.className = "variables-view-element-details enum";
+ nonenum.className = "variables-view-element-details nonenum";
+
+ title.appendChild(arrow);
+ title.appendChild(name);
+
+ element.appendChild(title);
+ element.appendChild(enumerable);
+ element.appendChild(nonenum);
+ },
+
+ /**
+ * Adds the necessary event listeners for this scope.
+ */
+ _addEventListeners: function () {
+ this._title.addEventListener("mousedown", this._onClick, false);
+ },
+
+ /**
+ * The click listener for this scope's title.
+ */
+ _onClick: function (e) {
+ if (this.editing ||
+ e.button != 0 ||
+ e.target == this._editNode ||
+ e.target == this._deleteNode ||
+ e.target == this._addPropertyNode) {
+ return;
+ }
+ this.toggle();
+ this.focus();
+ },
+
+ /**
+ * Opens the enumerable items container.
+ */
+ _openEnum: function () {
+ this._arrow.setAttribute("open", "");
+ this._enum.setAttribute("open", "");
+ },
+
+ /**
+ * Opens the non-enumerable items container.
+ */
+ _openNonEnum: function () {
+ this._nonenum.setAttribute("open", "");
+ },
+
+ /**
+ * Specifies if enumerable properties and variables should be displayed.
+ * @param boolean aFlag
+ */
+ set _enumVisible(aFlag) {
+ for (let [, variable] of this._store) {
+ variable._enumVisible = aFlag;
+
+ if (!this._isExpanded) {
+ continue;
+ }
+ if (aFlag) {
+ this._enum.setAttribute("open", "");
+ } else {
+ this._enum.removeAttribute("open");
+ }
+ }
+ },
+
+ /**
+ * Specifies if non-enumerable properties and variables should be displayed.
+ * @param boolean aFlag
+ */
+ set _nonEnumVisible(aFlag) {
+ for (let [, variable] of this._store) {
+ variable._nonEnumVisible = aFlag;
+
+ if (!this._isExpanded) {
+ continue;
+ }
+ if (aFlag) {
+ this._nonenum.setAttribute("open", "");
+ } else {
+ this._nonenum.removeAttribute("open");
+ }
+ }
+ },
+
+ /**
+ * Performs a case insensitive search for variables or properties matching
+ * the query, and hides non-matched items.
+ *
+ * @param string aLowerCaseQuery
+ * The lowercased name of the variable or property to search for.
+ */
+ _performSearch: function (aLowerCaseQuery) {
+ for (let [, variable] of this._store) {
+ let currentObject = variable;
+ let lowerCaseName = variable._nameString.toLowerCase();
+ let lowerCaseValue = variable._valueString.toLowerCase();
+
+ // Non-matched variables or properties require a corresponding attribute.
+ if (!lowerCaseName.includes(aLowerCaseQuery) &&
+ !lowerCaseValue.includes(aLowerCaseQuery)) {
+ variable._matched = false;
+ }
+ // Variable or property is matched.
+ else {
+ variable._matched = true;
+
+ // If the variable was ever expanded, there's a possibility it may
+ // contain some matched properties, so make sure they're visible
+ // ("expand downwards").
+ if (variable._store.size) {
+ variable.expand();
+ }
+
+ // If the variable is contained in another Scope, Variable, or Property,
+ // the parent may not be a match, thus hidden. It should be visible
+ // ("expand upwards").
+ while ((variable = variable.ownerView) && variable instanceof Scope) {
+ variable._matched = true;
+ variable.expand();
+ }
+ }
+
+ // Proceed with the search recursively inside this variable or property.
+ if (currentObject._store.size || currentObject.getter || currentObject.setter) {
+ currentObject._performSearch(aLowerCaseQuery);
+ }
+ }
+ },
+
+ /**
+ * Sets if this object instance is a matched or non-matched item.
+ * @param boolean aStatus
+ */
+ set _matched(aStatus) {
+ if (this._isMatch == aStatus) {
+ return;
+ }
+ if (aStatus) {
+ this._isMatch = true;
+ this.target.removeAttribute("unmatched");
+ } else {
+ this._isMatch = false;
+ this.target.setAttribute("unmatched", "");
+ }
+ },
+
+ /**
+ * Find the first item in the tree of visible items in this item that matches
+ * the predicate. Searches in visual order (the order seen by the user).
+ * Tests itself, then descends into first the enumerable children and then
+ * the non-enumerable children (since they are presented in separate groups).
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The first visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItems: function (aPredicate) {
+ if (aPredicate(this)) {
+ return this;
+ }
+
+ if (this._isExpanded) {
+ if (this._variablesView._enumVisible) {
+ for (let item of this._enumItems) {
+ let result = item._findInVisibleItems(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ if (this._variablesView._nonEnumVisible) {
+ for (let item of this._nonEnumItems) {
+ let result = item._findInVisibleItems(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Find the last item in the tree of visible items in this item that matches
+ * the predicate. Searches in reverse visual order (opposite of the order
+ * seen by the user). Descends into first the non-enumerable children, then
+ * the enumerable children (since they are presented in separate groups), and
+ * finally tests itself.
+ *
+ * @param function aPredicate
+ * A function that returns true when a match is found.
+ * @return Scope | Variable | Property
+ * The last visible scope, variable or property, or null if nothing
+ * is found.
+ */
+ _findInVisibleItemsReverse: function (aPredicate) {
+ if (this._isExpanded) {
+ if (this._variablesView._nonEnumVisible) {
+ for (let i = this._nonEnumItems.length - 1; i >= 0; i--) {
+ let item = this._nonEnumItems[i];
+ let result = item._findInVisibleItemsReverse(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ if (this._variablesView._enumVisible) {
+ for (let i = this._enumItems.length - 1; i >= 0; i--) {
+ let item = this._enumItems[i];
+ let result = item._findInVisibleItemsReverse(aPredicate);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ }
+
+ if (aPredicate(this)) {
+ return this;
+ }
+
+ return null;
+ },
+
+ /**
+ * Gets top level variables view instance.
+ * @return VariablesView
+ */
+ get _variablesView() {
+ return this._topView || (this._topView = (() => {
+ let parentView = this.ownerView;
+ let topView;
+
+ while ((topView = parentView.ownerView)) {
+ parentView = topView;
+ }
+ return parentView;
+ })());
+ },
+
+ /**
+ * Gets the parent node holding this scope.
+ * @return nsIDOMNode
+ */
+ get parentNode() {
+ return this.ownerView._list;
+ },
+
+ /**
+ * Gets the owner document holding this scope.
+ * @return nsIHTMLDocument
+ */
+ get document() {
+ return this._document || (this._document = this.ownerView.document);
+ },
+
+ /**
+ * Gets the default window holding this scope.
+ * @return nsIDOMWindow
+ */
+ get window() {
+ return this._window || (this._window = this.ownerView.window);
+ },
+
+ _topView: null,
+ _document: null,
+ _window: null,
+
+ ownerView: null,
+ eval: null,
+ switch: null,
+ delete: null,
+ new: null,
+ preventDisableOnChange: false,
+ preventDescriptorModifiers: false,
+ editing: false,
+ editableNameTooltip: "",
+ editableValueTooltip: "",
+ editButtonTooltip: "",
+ deleteButtonTooltip: "",
+ domNodeValueTooltip: "",
+ contextMenuId: "",
+ separatorStr: "",
+
+ _store: null,
+ _enumItems: null,
+ _nonEnumItems: null,
+ _fetched: false,
+ _committed: false,
+ _isLocked: false,
+ _isExpanded: false,
+ _isContentVisible: true,
+ _isHeaderVisible: true,
+ _isArrowVisible: true,
+ _isMatch: true,
+ _idString: "",
+ _nameString: "",
+ _target: null,
+ _arrow: null,
+ _name: null,
+ _title: null,
+ _enum: null,
+ _nonenum: null,
+};
+
+// Creating maps and arrays thousands of times for variables or properties
+// with a large number of children fills up a lot of memory. Make sure
+// these are instantiated only if needed.
+DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_store", () => new Map());
+DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_enumItems", Array);
+DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_nonEnumItems", Array);
+
+/**
+ * A Variable is a Scope holding Property instances.
+ * Iterable via "for (let [name, property] of instance) { }".
+ *
+ * @param Scope aScope
+ * The scope to contain this variable.
+ * @param string aName
+ * The variable's name.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ * @param object aOptions
+ * Options of the form accepted by Scope.addItem
+ */
+function Variable(aScope, aName, aDescriptor, aOptions) {
+ this._setTooltips = this._setTooltips.bind(this);
+ this._activateNameInput = this._activateNameInput.bind(this);
+ this._activateValueInput = this._activateValueInput.bind(this);
+ this.openNodeInInspector = this.openNodeInInspector.bind(this);
+ this.highlightDomNode = this.highlightDomNode.bind(this);
+ this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
+ this._internalItem = aOptions.internalItem;
+
+ // Treat safe getter descriptors as descriptors with a value.
+ if ("getterValue" in aDescriptor) {
+ aDescriptor.value = aDescriptor.getterValue;
+ delete aDescriptor.get;
+ delete aDescriptor.set;
+ }
+
+ Scope.call(this, aScope, aName, this._initialDescriptor = aDescriptor);
+ this.setGrip(aDescriptor.value);
+}
+
+Variable.prototype = Heritage.extend(Scope.prototype, {
+ /**
+ * Whether this Variable should be prefetched when it is remoted.
+ */
+ get shouldPrefetch() {
+ return this.name == "window" || this.name == "this";
+ },
+
+ /**
+ * Whether this Variable should paginate its contents.
+ */
+ get allowPaginate() {
+ return this.name != "window" && this.name != "this";
+ },
+
+ /**
+ * The class name applied to this variable's target element.
+ */
+ targetClassName: "variables-view-variable variable-or-property",
+
+ /**
+ * Create a new Property that is a child of Variable.
+ *
+ * @param string aName
+ * The name of the new Property.
+ * @param object aDescriptor
+ * The property's descriptor.
+ * @param object aOptions
+ * Options of the form accepted by Scope.addItem
+ * @return Property
+ * The newly created child Property.
+ */
+ _createChild: function (aName, aDescriptor, aOptions) {
+ return new Property(this, aName, aDescriptor, aOptions);
+ },
+
+ /**
+ * Remove this Variable from its parent and remove all children recursively.
+ */
+ remove: function () {
+ if (this._linkedToInspector) {
+ this.unhighlightDomNode();
+ this._valueLabel.removeEventListener("mouseover", this.highlightDomNode, false);
+ this._valueLabel.removeEventListener("mouseout", this.unhighlightDomNode, false);
+ this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, false);
+ }
+
+ this.ownerView._store.delete(this._nameString);
+ this._variablesView._itemsByElement.delete(this._target);
+ this._variablesView._currHierarchy.delete(this.absoluteName);
+
+ this._target.remove();
+
+ for (let property of this._store.values()) {
+ property.remove();
+ }
+ },
+
+ /**
+ * Populates this variable to contain all the properties of an object.
+ *
+ * @param object aObject
+ * The raw object you want to display.
+ * @param object aOptions [optional]
+ * Additional options for adding the properties. Supported options:
+ * - sorted: true to sort all the properties before adding them
+ * - expanded: true to expand all the properties after adding them
+ */
+ populate: function (aObject, aOptions = {}) {
+ // Retrieve the properties only once.
+ if (this._fetched) {
+ return;
+ }
+ this._fetched = true;
+
+ let propertyNames = Object.getOwnPropertyNames(aObject);
+ let prototype = Object.getPrototypeOf(aObject);
+
+ // Sort all of the properties before adding them, if preferred.
+ if (aOptions.sorted) {
+ propertyNames.sort(this._naturalSort);
+ }
+
+ // Add all the variable properties.
+ for (let name of propertyNames) {
+ let descriptor = Object.getOwnPropertyDescriptor(aObject, name);
+ if (descriptor.get || descriptor.set) {
+ let prop = this._addRawNonValueProperty(name, descriptor);
+ if (aOptions.expanded) {
+ prop.expanded = true;
+ }
+ } else {
+ let prop = this._addRawValueProperty(name, descriptor, aObject[name]);
+ if (aOptions.expanded) {
+ prop.expanded = true;
+ }
+ }
+ }
+ // Add the variable's __proto__.
+ if (prototype) {
+ this._addRawValueProperty("__proto__", {}, prototype);
+ }
+ },
+
+ /**
+ * Populates a specific variable or property instance to contain all the
+ * properties of an object
+ *
+ * @param Variable | Property aVar
+ * The target variable to populate.
+ * @param object aObject [optional]
+ * The raw object you want to display. If unspecified, the object is
+ * assumed to be defined in a _sourceValue property on the target.
+ */
+ _populateTarget: function (aVar, aObject = aVar._sourceValue) {
+ aVar.populate(aObject);
+ },
+
+ /**
+ * Adds a property for this variable based on a raw value descriptor.
+ *
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * Specifies the exact property descriptor as returned by a call to
+ * Object.getOwnPropertyDescriptor.
+ * @param object aValue
+ * The raw property value you want to display.
+ * @return Property
+ * The newly added property instance.
+ */
+ _addRawValueProperty: function (aName, aDescriptor, aValue) {
+ let descriptor = Object.create(aDescriptor);
+ descriptor.value = VariablesView.getGrip(aValue);
+
+ let propertyItem = this.addItem(aName, descriptor);
+ propertyItem._sourceValue = aValue;
+
+ // Add an 'onexpand' callback for the property, lazily handling
+ // the addition of new child properties.
+ if (!VariablesView.isPrimitive(descriptor)) {
+ propertyItem.onexpand = this._populateTarget;
+ }
+ return propertyItem;
+ },
+
+ /**
+ * Adds a property for this variable based on a getter/setter descriptor.
+ *
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * Specifies the exact property descriptor as returned by a call to
+ * Object.getOwnPropertyDescriptor.
+ * @return Property
+ * The newly added property instance.
+ */
+ _addRawNonValueProperty: function (aName, aDescriptor) {
+ let descriptor = Object.create(aDescriptor);
+ descriptor.get = VariablesView.getGrip(aDescriptor.get);
+ descriptor.set = VariablesView.getGrip(aDescriptor.set);
+
+ return this.addItem(aName, descriptor);
+ },
+
+ /**
+ * Gets this variable's path to the topmost scope in the form of a string
+ * meant for use via eval() or a similar approach.
+ * For example, a symbolic name may look like "arguments['0']['foo']['bar']".
+ * @return string
+ */
+ get symbolicName() {
+ return this._nameString || "";
+ },
+
+ /**
+ * Gets full path to this variable, including name of the scope.
+ * @return string
+ */
+ get absoluteName() {
+ if (this._absoluteName) {
+ return this._absoluteName;
+ }
+
+ this._absoluteName = this.ownerView._nameString + "[" + escapeString(this._nameString) + "]";
+ return this._absoluteName;
+ },
+
+ /**
+ * Gets this variable's symbolic path to the topmost scope.
+ * @return array
+ * @see Variable._buildSymbolicPath
+ */
+ get symbolicPath() {
+ if (this._symbolicPath) {
+ return this._symbolicPath;
+ }
+ this._symbolicPath = this._buildSymbolicPath();
+ return this._symbolicPath;
+ },
+
+ /**
+ * Build this variable's path to the topmost scope in form of an array of
+ * strings, one for each segment of the path.
+ * For example, a symbolic path may look like ["0", "foo", "bar"].
+ * @return array
+ */
+ _buildSymbolicPath: function (path = []) {
+ if (this.name) {
+ path.unshift(this.name);
+ if (this.ownerView instanceof Variable) {
+ return this.ownerView._buildSymbolicPath(path);
+ }
+ }
+ return path;
+ },
+
+ /**
+ * Returns this variable's value from the descriptor if available.
+ * @return any
+ */
+ get value() {
+ return this._initialDescriptor.value;
+ },
+
+ /**
+ * Returns this variable's getter from the descriptor if available.
+ * @return object
+ */
+ get getter() {
+ return this._initialDescriptor.get;
+ },
+
+ /**
+ * Returns this variable's getter from the descriptor if available.
+ * @return object
+ */
+ get setter() {
+ return this._initialDescriptor.set;
+ },
+
+ /**
+ * Sets the specific grip for this variable (applies the text content and
+ * class name to the value label).
+ *
+ * The grip should contain the value or the type & class, as defined in the
+ * remote debugger protocol. For convenience, undefined and null are
+ * both considered types.
+ *
+ * @param any aGrip
+ * Specifies the value and/or type & class of the variable.
+ * e.g. - 42
+ * - true
+ * - "nasu"
+ * - { type: "undefined" }
+ * - { type: "null" }
+ * - { type: "object", class: "Object" }
+ */
+ setGrip: function (aGrip) {
+ // Don't allow displaying grip information if there's no name available
+ // or the grip is malformed.
+ if (this._nameString === undefined || aGrip === undefined || aGrip === null) {
+ return;
+ }
+ // Getters and setters should display grip information in sub-properties.
+ if (this.getter || this.setter) {
+ return;
+ }
+
+ let prevGrip = this._valueGrip;
+ if (prevGrip) {
+ this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
+ }
+ this._valueGrip = aGrip;
+
+ if (aGrip && (aGrip.optimizedOut || aGrip.uninitialized || aGrip.missingArguments)) {
+ if (aGrip.optimizedOut) {
+ this._valueString = L10N.getStr("variablesViewOptimizedOut");
+ }
+ else if (aGrip.uninitialized) {
+ this._valueString = L10N.getStr("variablesViewUninitialized");
+ }
+ else if (aGrip.missingArguments) {
+ this._valueString = L10N.getStr("variablesViewMissingArgs");
+ }
+ this.eval = null;
+ }
+ else {
+ this._valueString = VariablesView.getString(aGrip, {
+ concise: true,
+ noEllipsis: true,
+ });
+ this.eval = this.ownerView.eval;
+ }
+
+ this._valueClassName = VariablesView.getClass(aGrip);
+
+ this._valueLabel.classList.add(this._valueClassName);
+ this._valueLabel.setAttribute("value", this._valueString);
+ this._separatorLabel.hidden = false;
+
+ // DOMNodes get special treatment since they can be linked to the inspector
+ if (this._valueGrip.preview && this._valueGrip.preview.kind === "DOMNode") {
+ this._linkToInspector();
+ }
+ },
+
+ /**
+ * Marks this variable as overridden.
+ *
+ * @param boolean aFlag
+ * Whether this variable is overridden or not.
+ */
+ setOverridden: function (aFlag) {
+ if (aFlag) {
+ this._target.setAttribute("overridden", "");
+ } else {
+ this._target.removeAttribute("overridden");
+ }
+ },
+
+ /**
+ * Briefly flashes this variable.
+ *
+ * @param number aDuration [optional]
+ * An optional flash animation duration.
+ */
+ flash: function (aDuration = ITEM_FLASH_DURATION) {
+ let fadeInDelay = this._variablesView.lazyEmptyDelay + 1;
+ let fadeOutDelay = fadeInDelay + aDuration;
+
+ setNamedTimeout("vview-flash-in" + this.absoluteName,
+ fadeInDelay, () => this._target.setAttribute("changed", ""));
+
+ setNamedTimeout("vview-flash-out" + this.absoluteName,
+ fadeOutDelay, () => this._target.removeAttribute("changed"));
+ },
+
+ /**
+ * Initializes this variable's id, view and binds event listeners.
+ *
+ * @param string aName
+ * The variable's name.
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+ _init: function (aName, aDescriptor) {
+ this._idString = generateId(this._nameString = aName);
+ this._displayScope(aName, this.targetClassName);
+ this._displayVariable();
+ this._customizeVariable();
+ this._prepareTooltips();
+ this._setAttributes();
+ this._addEventListeners();
+
+ if (this._initialDescriptor.enumerable ||
+ this._nameString == "this" ||
+ this._internalItem) {
+ this.ownerView._enum.appendChild(this._target);
+ this.ownerView._enumItems.push(this);
+ } else {
+ this.ownerView._nonenum.appendChild(this._target);
+ this.ownerView._nonEnumItems.push(this);
+ }
+ },
+
+ /**
+ * Creates the necessary nodes for this variable.
+ */
+ _displayVariable: function () {
+ let document = this.document;
+ let descriptor = this._initialDescriptor;
+
+ let separatorLabel = this._separatorLabel = document.createElement("label");
+ separatorLabel.className = "plain separator";
+ separatorLabel.setAttribute("value", this.separatorStr + " ");
+
+ let valueLabel = this._valueLabel = document.createElement("label");
+ valueLabel.className = "plain value";
+ valueLabel.setAttribute("flex", "1");
+ valueLabel.setAttribute("crop", "center");
+
+ this._title.appendChild(separatorLabel);
+ this._title.appendChild(valueLabel);
+
+ if (VariablesView.isPrimitive(descriptor)) {
+ this.hideArrow();
+ }
+
+ // If no value will be displayed, we don't need the separator.
+ if (!descriptor.get && !descriptor.set && !("value" in descriptor)) {
+ separatorLabel.hidden = true;
+ }
+
+ // If this is a getter/setter property, create two child pseudo-properties
+ // called "get" and "set" that display the corresponding functions.
+ if (descriptor.get || descriptor.set) {
+ separatorLabel.hidden = true;
+ valueLabel.hidden = true;
+
+ // Changing getter/setter names is never allowed.
+ this.switch = null;
+
+ // Getter/setter properties require special handling when it comes to
+ // evaluation and deletion.
+ if (this.ownerView.eval) {
+ this.delete = VariablesView.getterOrSetterDeleteCallback;
+ this.evaluationMacro = VariablesView.overrideValueEvalMacro;
+ }
+ // Deleting getters and setters individually is not allowed if no
+ // evaluation method is provided.
+ else {
+ this.delete = null;
+ this.evaluationMacro = null;
+ }
+
+ let getter = this.addItem("get", { value: descriptor.get });
+ let setter = this.addItem("set", { value: descriptor.set });
+ getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
+ setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
+
+ getter.hideArrow();
+ setter.hideArrow();
+ this.expand();
+ }
+ },
+
+ /**
+ * Adds specific nodes for this variable based on custom flags.
+ */
+ _customizeVariable: function () {
+ let ownerView = this.ownerView;
+ let descriptor = this._initialDescriptor;
+
+ if (ownerView.eval && this.getter || this.setter) {
+ let editNode = this._editNode = this.document.createElement("toolbarbutton");
+ editNode.className = "plain variables-view-edit";
+ editNode.addEventListener("mousedown", this._onEdit.bind(this), false);
+ this._title.insertBefore(editNode, this._spacer);
+ }
+
+ if (ownerView.delete) {
+ let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
+ deleteNode.className = "plain variables-view-delete";
+ deleteNode.addEventListener("click", this._onDelete.bind(this), false);
+ this._title.appendChild(deleteNode);
+ }
+
+ if (ownerView.new) {
+ let addPropertyNode = this._addPropertyNode = this.document.createElement("toolbarbutton");
+ addPropertyNode.className = "plain variables-view-add-property";
+ addPropertyNode.addEventListener("mousedown", this._onAddProperty.bind(this), false);
+ this._title.appendChild(addPropertyNode);
+
+ // Can't add properties to primitive values, hide the node in those cases.
+ if (VariablesView.isPrimitive(descriptor)) {
+ addPropertyNode.setAttribute("invisible", "");
+ }
+ }
+
+ if (ownerView.contextMenuId) {
+ this._title.setAttribute("context", ownerView.contextMenuId);
+ }
+
+ if (ownerView.preventDescriptorModifiers) {
+ return;
+ }
+
+ if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
+ let nonWritableIcon = this.document.createElement("hbox");
+ nonWritableIcon.className = "plain variable-or-property-non-writable-icon";
+ nonWritableIcon.setAttribute("optional-visibility", "");
+ this._title.appendChild(nonWritableIcon);
+ }
+ if (descriptor.value && typeof descriptor.value == "object") {
+ if (descriptor.value.frozen) {
+ let frozenLabel = this.document.createElement("label");
+ frozenLabel.className = "plain variable-or-property-frozen-label";
+ frozenLabel.setAttribute("optional-visibility", "");
+ frozenLabel.setAttribute("value", "F");
+ this._title.appendChild(frozenLabel);
+ }
+ if (descriptor.value.sealed) {
+ let sealedLabel = this.document.createElement("label");
+ sealedLabel.className = "plain variable-or-property-sealed-label";
+ sealedLabel.setAttribute("optional-visibility", "");
+ sealedLabel.setAttribute("value", "S");
+ this._title.appendChild(sealedLabel);
+ }
+ if (!descriptor.value.extensible) {
+ let nonExtensibleLabel = this.document.createElement("label");
+ nonExtensibleLabel.className = "plain variable-or-property-non-extensible-label";
+ nonExtensibleLabel.setAttribute("optional-visibility", "");
+ nonExtensibleLabel.setAttribute("value", "N");
+ this._title.appendChild(nonExtensibleLabel);
+ }
+ }
+ },
+
+ /**
+ * Prepares all tooltips for this variable.
+ */
+ _prepareTooltips: function () {
+ this._target.addEventListener("mouseover", this._setTooltips, false);
+ },
+
+ /**
+ * Sets all tooltips for this variable.
+ */
+ _setTooltips: function () {
+ this._target.removeEventListener("mouseover", this._setTooltips, false);
+
+ let ownerView = this.ownerView;
+ if (ownerView.preventDescriptorModifiers) {
+ return;
+ }
+
+ let tooltip = this.document.createElement("tooltip");
+ tooltip.id = "tooltip-" + this._idString;
+ tooltip.setAttribute("orient", "horizontal");
+
+ let labels = [
+ "configurable", "enumerable", "writable",
+ "frozen", "sealed", "extensible", "overridden", "WebIDL"];
+
+ for (let type of labels) {
+ let labelElement = this.document.createElement("label");
+ labelElement.className = type;
+ labelElement.setAttribute("value", L10N.getStr(type + "Tooltip"));
+ tooltip.appendChild(labelElement);
+ }
+
+ this._target.appendChild(tooltip);
+ this._target.setAttribute("tooltip", tooltip.id);
+
+ if (this._editNode && ownerView.eval) {
+ this._editNode.setAttribute("tooltiptext", ownerView.editButtonTooltip);
+ }
+ if (this._openInspectorNode && this._linkedToInspector) {
+ this._openInspectorNode.setAttribute("tooltiptext", this.ownerView.domNodeValueTooltip);
+ }
+ if (this._valueLabel && ownerView.eval) {
+ this._valueLabel.setAttribute("tooltiptext", ownerView.editableValueTooltip);
+ }
+ if (this._name && ownerView.switch) {
+ this._name.setAttribute("tooltiptext", ownerView.editableNameTooltip);
+ }
+ if (this._deleteNode && ownerView.delete) {
+ this._deleteNode.setAttribute("tooltiptext", ownerView.deleteButtonTooltip);
+ }
+ },
+
+ /**
+ * Get the parent variablesview toolbox, if any.
+ */
+ get toolbox() {
+ return this._variablesView.toolbox;
+ },
+
+ /**
+ * Checks if this variable is a DOMNode and is part of a variablesview that
+ * has been linked to the toolbox, so that highlighting and jumping to the
+ * inspector can be done.
+ */
+ _isLinkableToInspector: function () {
+ let isDomNode = this._valueGrip && this._valueGrip.preview.kind === "DOMNode";
+ let hasBeenLinked = this._linkedToInspector;
+ let hasToolbox = !!this.toolbox;
+
+ return isDomNode && !hasBeenLinked && hasToolbox;
+ },
+
+ /**
+ * If the variable is a DOMNode, and if a toolbox is set, then link it to the
+ * inspector (highlight on hover, and jump to markup-view on click)
+ */
+ _linkToInspector: function () {
+ if (!this._isLinkableToInspector()) {
+ return;
+ }
+
+ // Listen to value mouseover/click events to highlight and jump
+ this._valueLabel.addEventListener("mouseover", this.highlightDomNode, false);
+ this._valueLabel.addEventListener("mouseout", this.unhighlightDomNode, false);
+
+ // Add a button to open the node in the inspector
+ this._openInspectorNode = this.document.createElement("toolbarbutton");
+ this._openInspectorNode.className = "plain variables-view-open-inspector";
+ this._openInspectorNode.addEventListener("mousedown", this.openNodeInInspector, false);
+ this._title.appendChild(this._openInspectorNode);
+
+ this._linkedToInspector = true;
+ },
+
+ /**
+ * In case this variable is a DOMNode and part of a variablesview that has been
+ * linked to the toolbox's inspector, then select the corresponding node in
+ * the inspector, and switch the inspector tool in the toolbox
+ * @return a promise that resolves when the node is selected and the inspector
+ * has been switched to and is ready
+ */
+ openNodeInInspector: function (event) {
+ if (!this.toolbox) {
+ return promise.reject(new Error("Toolbox not available"));
+ }
+
+ event && event.stopPropagation();
+
+ return Task.spawn(function* () {
+ yield this.toolbox.initInspector();
+
+ let nodeFront = this._nodeFront;
+ if (!nodeFront) {
+ nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this._valueGrip.actor);
+ }
+
+ if (nodeFront) {
+ yield this.toolbox.selectTool("inspector");
+
+ let inspectorReady = defer();
+ this.toolbox.getPanel("inspector").once("inspector-updated", inspectorReady.resolve);
+ yield this.toolbox.selection.setNodeFront(nodeFront, "variables-view");
+ yield inspectorReady.promise;
+ }
+ }.bind(this));
+ },
+
+ /**
+ * In case this variable is a DOMNode and part of a variablesview that has been
+ * linked to the toolbox's inspector, then highlight the corresponding node
+ */
+ highlightDomNode: function () {
+ if (this.toolbox) {
+ if (this._nodeFront) {
+ // If the nodeFront has been retrieved before, no need to ask the server
+ // again for it
+ this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
+ return;
+ }
+
+ this.toolbox.highlighterUtils.highlightDomValueGrip(this._valueGrip).then(front => {
+ this._nodeFront = front;
+ });
+ }
+ },
+
+ /**
+ * Unhighlight a previously highlit node
+ * @see highlightDomNode
+ */
+ unhighlightDomNode: function () {
+ if (this.toolbox) {
+ this.toolbox.highlighterUtils.unhighlight();
+ }
+ },
+
+ /**
+ * Sets a variable's configurable, enumerable and writable attributes,
+ * and specifies if it's a 'this', '<exception>', '<return>' or '__proto__'
+ * reference.
+ */
+ _setAttributes: function () {
+ let ownerView = this.ownerView;
+ if (ownerView.preventDescriptorModifiers) {
+ return;
+ }
+
+ let descriptor = this._initialDescriptor;
+ let target = this._target;
+ let name = this._nameString;
+
+ if (ownerView.eval) {
+ target.setAttribute("editable", "");
+ }
+
+ if (!descriptor.configurable) {
+ target.setAttribute("non-configurable", "");
+ }
+ if (!descriptor.enumerable) {
+ target.setAttribute("non-enumerable", "");
+ }
+ if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
+ target.setAttribute("non-writable", "");
+ }
+
+ if (descriptor.value && typeof descriptor.value == "object") {
+ if (descriptor.value.frozen) {
+ target.setAttribute("frozen", "");
+ }
+ if (descriptor.value.sealed) {
+ target.setAttribute("sealed", "");
+ }
+ if (!descriptor.value.extensible) {
+ target.setAttribute("non-extensible", "");
+ }
+ }
+
+ if (descriptor && "getterValue" in descriptor) {
+ target.setAttribute("safe-getter", "");
+ }
+
+ if (name == "this") {
+ target.setAttribute("self", "");
+ }
+ else if (this._internalItem && name == "<exception>") {
+ target.setAttribute("exception", "");
+ target.setAttribute("pseudo-item", "");
+ }
+ else if (this._internalItem && name == "<return>") {
+ target.setAttribute("return", "");
+ target.setAttribute("pseudo-item", "");
+ }
+ else if (name == "__proto__") {
+ target.setAttribute("proto", "");
+ target.setAttribute("pseudo-item", "");
+ }
+
+ if (Object.keys(descriptor).length == 0) {
+ target.setAttribute("pseudo-item", "");
+ }
+ },
+
+ /**
+ * Adds the necessary event listeners for this variable.
+ */
+ _addEventListeners: function () {
+ this._name.addEventListener("dblclick", this._activateNameInput, false);
+ this._valueLabel.addEventListener("mousedown", this._activateValueInput, false);
+ this._title.addEventListener("mousedown", this._onClick, false);
+ },
+
+ /**
+ * Makes this variable's name editable.
+ */
+ _activateNameInput: function (e) {
+ if (!this._variablesView.alignedValues) {
+ this._separatorLabel.hidden = true;
+ this._valueLabel.hidden = true;
+ }
+
+ EditableName.create(this, {
+ onSave: aKey => {
+ if (!this._variablesView.preventDisableOnChange) {
+ this._disable();
+ }
+ this.ownerView.switch(this, aKey);
+ },
+ onCleanup: () => {
+ if (!this._variablesView.alignedValues) {
+ this._separatorLabel.hidden = false;
+ this._valueLabel.hidden = false;
+ }
+ }
+ }, e);
+ },
+
+ /**
+ * Makes this variable's value editable.
+ */
+ _activateValueInput: function (e) {
+ EditableValue.create(this, {
+ onSave: aString => {
+ if (this._linkedToInspector) {
+ this.unhighlightDomNode();
+ }
+ if (!this._variablesView.preventDisableOnChange) {
+ this._disable();
+ }
+ this.ownerView.eval(this, aString);
+ }
+ }, e);
+ },
+
+ /**
+ * Disables this variable prior to a new name switch or value evaluation.
+ */
+ _disable: function () {
+ // Prevent the variable from being collapsed or expanded.
+ this.hideArrow();
+
+ // Hide any nodes that may offer information about the variable.
+ for (let node of this._title.childNodes) {
+ node.hidden = node != this._arrow && node != this._name;
+ }
+ this._enum.hidden = true;
+ this._nonenum.hidden = true;
+ },
+
+ /**
+ * The current macro used to generate the string evaluated when performing
+ * a variable or property value change.
+ */
+ evaluationMacro: VariablesView.simpleValueEvalMacro,
+
+ /**
+ * The click listener for the edit button.
+ */
+ _onEdit: function (e) {
+ if (e.button != 0) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+ this._activateValueInput();
+ },
+
+ /**
+ * The click listener for the delete button.
+ */
+ _onDelete: function (e) {
+ if ("button" in e && e.button != 0) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this.ownerView.delete) {
+ if (!this.ownerView.delete(this)) {
+ this.hide();
+ }
+ }
+ },
+
+ /**
+ * The click listener for the add property button.
+ */
+ _onAddProperty: function (e) {
+ if ("button" in e && e.button != 0) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.expanded = true;
+
+ let item = this.addItem(" ", {
+ value: undefined,
+ configurable: true,
+ enumerable: true,
+ writable: true
+ }, {relaxed: true});
+
+ // Force showing the separator.
+ item._separatorLabel.hidden = false;
+
+ EditableNameAndValue.create(item, {
+ onSave: ([aKey, aValue]) => {
+ if (!this._variablesView.preventDisableOnChange) {
+ this._disable();
+ }
+ this.ownerView.new(this, aKey, aValue);
+ }
+ }, e);
+ },
+
+ _symbolicName: null,
+ _symbolicPath: null,
+ _absoluteName: null,
+ _initialDescriptor: null,
+ _separatorLabel: null,
+ _valueLabel: null,
+ _spacer: null,
+ _editNode: null,
+ _deleteNode: null,
+ _addPropertyNode: null,
+ _tooltip: null,
+ _valueGrip: null,
+ _valueString: "",
+ _valueClassName: "",
+ _prevExpandable: false,
+ _prevExpanded: false
+});
+
+/**
+ * A Property is a Variable holding additional child Property instances.
+ * Iterable via "for (let [name, property] of instance) { }".
+ *
+ * @param Variable aVar
+ * The variable to contain this property.
+ * @param string aName
+ * The property's name.
+ * @param object aDescriptor
+ * The property's descriptor.
+ * @param object aOptions
+ * Options of the form accepted by Scope.addItem
+ */
+function Property(aVar, aName, aDescriptor, aOptions) {
+ Variable.call(this, aVar, aName, aDescriptor, aOptions);
+}
+
+Property.prototype = Heritage.extend(Variable.prototype, {
+ /**
+ * The class name applied to this property's target element.
+ */
+ targetClassName: "variables-view-property variable-or-property",
+
+ /**
+ * @see Variable.symbolicName
+ * @return string
+ */
+ get symbolicName() {
+ if (this._symbolicName) {
+ return this._symbolicName;
+ }
+
+ this._symbolicName = this.ownerView.symbolicName + "[" + escapeString(this._nameString) + "]";
+ return this._symbolicName;
+ },
+
+ /**
+ * @see Variable.absoluteName
+ * @return string
+ */
+ get absoluteName() {
+ if (this._absoluteName) {
+ return this._absoluteName;
+ }
+
+ this._absoluteName = this.ownerView.absoluteName + "[" + escapeString(this._nameString) + "]";
+ return this._absoluteName;
+ }
+});
+
+/**
+ * A generator-iterator over the VariablesView, Scopes, Variables and Properties.
+ */
+VariablesView.prototype[Symbol.iterator] =
+Scope.prototype[Symbol.iterator] =
+Variable.prototype[Symbol.iterator] =
+Property.prototype[Symbol.iterator] = function* () {
+ yield* this._store;
+};
+
+/**
+ * Forget everything recorded about added scopes, variables or properties.
+ * @see VariablesView.commitHierarchy
+ */
+VariablesView.prototype.clearHierarchy = function () {
+ this._prevHierarchy.clear();
+ this._currHierarchy.clear();
+};
+
+/**
+ * Perform operations on all the VariablesView Scopes, Variables and Properties
+ * after you've added all the items you wanted.
+ *
+ * Calling this method is optional, and does the following:
+ * - styles the items overridden by other items in parent scopes
+ * - reopens the items which were previously expanded
+ * - flashes the items whose values changed
+ */
+VariablesView.prototype.commitHierarchy = function () {
+ for (let [, currItem] of this._currHierarchy) {
+ // Avoid performing expensive operations.
+ if (this.commitHierarchyIgnoredItems[currItem._nameString]) {
+ continue;
+ }
+ let overridden = this.isOverridden(currItem);
+ if (overridden) {
+ currItem.setOverridden(true);
+ }
+ let expanded = !currItem._committed && this.wasExpanded(currItem);
+ if (expanded) {
+ currItem.expand();
+ }
+ let changed = !currItem._committed && this.hasChanged(currItem);
+ if (changed) {
+ currItem.flash();
+ }
+ currItem._committed = true;
+ }
+ if (this.oncommit) {
+ this.oncommit(this);
+ }
+};
+
+// Some variables are likely to contain a very large number of properties.
+// It would be a bad idea to re-expand them or perform expensive operations.
+VariablesView.prototype.commitHierarchyIgnoredItems = Heritage.extend(null, {
+ "window": true,
+ "this": true
+});
+
+/**
+ * Checks if the an item was previously expanded, if it existed in a
+ * previous hierarchy.
+ *
+ * @param Scope | Variable | Property aItem
+ * The item to verify.
+ * @return boolean
+ * Whether the item was expanded.
+ */
+VariablesView.prototype.wasExpanded = function (aItem) {
+ if (!(aItem instanceof Scope)) {
+ return false;
+ }
+ let prevItem = this._prevHierarchy.get(aItem.absoluteName || aItem._nameString);
+ return prevItem ? prevItem._isExpanded : false;
+};
+
+/**
+ * Checks if the an item's displayed value (a representation of the grip)
+ * has changed, if it existed in a previous hierarchy.
+ *
+ * @param Variable | Property aItem
+ * The item to verify.
+ * @return boolean
+ * Whether the item has changed.
+ */
+VariablesView.prototype.hasChanged = function (aItem) {
+ // Only analyze Variables and Properties for displayed value changes.
+ // Scopes are just collections of Variables and Properties and
+ // don't have a "value", so they can't change.
+ if (!(aItem instanceof Variable)) {
+ return false;
+ }
+ let prevItem = this._prevHierarchy.get(aItem.absoluteName);
+ return prevItem ? prevItem._valueString != aItem._valueString : false;
+};
+
+/**
+ * Checks if the an item was previously expanded, if it existed in a
+ * previous hierarchy.
+ *
+ * @param Scope | Variable | Property aItem
+ * The item to verify.
+ * @return boolean
+ * Whether the item was expanded.
+ */
+VariablesView.prototype.isOverridden = function (aItem) {
+ // Only analyze Variables for being overridden in different Scopes.
+ if (!(aItem instanceof Variable) || aItem instanceof Property) {
+ return false;
+ }
+ let currVariableName = aItem._nameString;
+ let parentScopes = this.getParentScopesForVariableOrProperty(aItem);
+
+ for (let otherScope of parentScopes) {
+ for (let [otherVariableName] of otherScope) {
+ if (otherVariableName == currVariableName) {
+ return true;
+ }
+ }
+ }
+ return false;
+};
+
+/**
+ * Returns true if the descriptor represents an undefined, null or
+ * primitive value.
+ *
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+VariablesView.isPrimitive = function (aDescriptor) {
+ // For accessor property descriptors, the getter and setter need to be
+ // contained in 'get' and 'set' properties.
+ let getter = aDescriptor.get;
+ let setter = aDescriptor.set;
+ if (getter || setter) {
+ return false;
+ }
+
+ // As described in the remote debugger protocol, the value grip
+ // must be contained in a 'value' property.
+ let grip = aDescriptor.value;
+ if (typeof grip != "object") {
+ return true;
+ }
+
+ // For convenience, undefined, null, Infinity, -Infinity, NaN, -0, and long
+ // strings are considered types.
+ let type = grip.type;
+ if (type == "undefined" ||
+ type == "null" ||
+ type == "Infinity" ||
+ type == "-Infinity" ||
+ type == "NaN" ||
+ type == "-0" ||
+ type == "symbol" ||
+ type == "longString") {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Returns true if the descriptor represents an undefined value.
+ *
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+VariablesView.isUndefined = function (aDescriptor) {
+ // For accessor property descriptors, the getter and setter need to be
+ // contained in 'get' and 'set' properties.
+ let getter = aDescriptor.get;
+ let setter = aDescriptor.set;
+ if (typeof getter == "object" && getter.type == "undefined" &&
+ typeof setter == "object" && setter.type == "undefined") {
+ return true;
+ }
+
+ // As described in the remote debugger protocol, the value grip
+ // must be contained in a 'value' property.
+ let grip = aDescriptor.value;
+ if (typeof grip == "object" && grip.type == "undefined") {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Returns true if the descriptor represents a falsy value.
+ *
+ * @param object aDescriptor
+ * The variable's descriptor.
+ */
+VariablesView.isFalsy = function (aDescriptor) {
+ // As described in the remote debugger protocol, the value grip
+ // must be contained in a 'value' property.
+ let grip = aDescriptor.value;
+ if (typeof grip != "object") {
+ return !grip;
+ }
+
+ // For convenience, undefined, null, NaN, and -0 are all considered types.
+ let type = grip.type;
+ if (type == "undefined" ||
+ type == "null" ||
+ type == "NaN" ||
+ type == "-0") {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Returns true if the value is an instance of Variable or Property.
+ *
+ * @param any aValue
+ * The value to test.
+ */
+VariablesView.isVariable = function (aValue) {
+ return aValue instanceof Variable;
+};
+
+/**
+ * Returns a standard grip for a value.
+ *
+ * @param any aValue
+ * The raw value to get a grip for.
+ * @return any
+ * The value's grip.
+ */
+VariablesView.getGrip = function (aValue) {
+ switch (typeof aValue) {
+ case "boolean":
+ case "string":
+ return aValue;
+ case "number":
+ if (aValue === Infinity) {
+ return { type: "Infinity" };
+ } else if (aValue === -Infinity) {
+ return { type: "-Infinity" };
+ } else if (Number.isNaN(aValue)) {
+ return { type: "NaN" };
+ } else if (1 / aValue === -Infinity) {
+ return { type: "-0" };
+ }
+ return aValue;
+ case "undefined":
+ // document.all is also "undefined"
+ if (aValue === undefined) {
+ return { type: "undefined" };
+ }
+ case "object":
+ if (aValue === null) {
+ return { type: "null" };
+ }
+ case "function":
+ return { type: "object",
+ class: WebConsoleUtils.getObjectClassName(aValue) };
+ default:
+ console.error("Failed to provide a grip for value of " + typeof value +
+ ": " + aValue);
+ return null;
+ }
+};
+
+/**
+ * Returns a custom formatted property string for a grip.
+ *
+ * @param any aGrip
+ * @see Variable.setGrip
+ * @param object aOptions
+ * Options:
+ * - concise: boolean that tells you want a concisely formatted string.
+ * - noStringQuotes: boolean that tells to not quote strings.
+ * - noEllipsis: boolean that tells to not add an ellipsis after the
+ * initial text of a longString.
+ * @return string
+ * The formatted property string.
+ */
+VariablesView.getString = function (aGrip, aOptions = {}) {
+ if (aGrip && typeof aGrip == "object") {
+ switch (aGrip.type) {
+ case "undefined":
+ case "null":
+ case "NaN":
+ case "Infinity":
+ case "-Infinity":
+ case "-0":
+ return aGrip.type;
+ default:
+ let stringifier = VariablesView.stringifiers.byType[aGrip.type];
+ if (stringifier) {
+ let result = stringifier(aGrip, aOptions);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ if (aGrip.displayString) {
+ return VariablesView.getString(aGrip.displayString, aOptions);
+ }
+
+ if (aGrip.type == "object" && aOptions.concise) {
+ return aGrip.class;
+ }
+
+ return "[" + aGrip.type + " " + aGrip.class + "]";
+ }
+ }
+
+ switch (typeof aGrip) {
+ case "string":
+ return VariablesView.stringifiers.byType.string(aGrip, aOptions);
+ case "boolean":
+ return aGrip ? "true" : "false";
+ case "number":
+ if (!aGrip && 1 / aGrip === -Infinity) {
+ return "-0";
+ }
+ default:
+ return aGrip + "";
+ }
+};
+
+/**
+ * The VariablesView stringifiers are used by VariablesView.getString(). These
+ * are organized by object type, object class and by object actor preview kind.
+ * Some objects share identical ways for previews, for example Arrays, Sets and
+ * NodeLists.
+ *
+ * Any stringifier function must return a string. If null is returned, * then
+ * the default stringifier will be used. When invoked, the stringifier is
+ * given the same two arguments as those given to VariablesView.getString().
+ */
+VariablesView.stringifiers = {};
+
+VariablesView.stringifiers.byType = {
+ string: function (aGrip, {noStringQuotes}) {
+ if (noStringQuotes) {
+ return aGrip;
+ }
+ return '"' + aGrip + '"';
+ },
+
+ longString: function ({initial}, {noStringQuotes, noEllipsis}) {
+ let ellipsis = noEllipsis ? "" : ELLIPSIS;
+ if (noStringQuotes) {
+ return initial + ellipsis;
+ }
+ let result = '"' + initial + '"';
+ if (!ellipsis) {
+ return result;
+ }
+ return result.substr(0, result.length - 1) + ellipsis + '"';
+ },
+
+ object: function (aGrip, aOptions) {
+ let {preview} = aGrip;
+ let stringifier;
+ if (aGrip.class) {
+ stringifier = VariablesView.stringifiers.byObjectClass[aGrip.class];
+ }
+ if (!stringifier && preview && preview.kind) {
+ stringifier = VariablesView.stringifiers.byObjectKind[preview.kind];
+ }
+ if (stringifier) {
+ return stringifier(aGrip, aOptions);
+ }
+ return null;
+ },
+
+ symbol: function (aGrip, aOptions) {
+ const name = aGrip.name || "";
+ return "Symbol(" + name + ")";
+ },
+
+ mapEntry: function (aGrip, {concise}) {
+ let { preview: { key, value }} = aGrip;
+
+ let keyString = VariablesView.getString(key, {
+ concise: true,
+ noStringQuotes: true,
+ });
+ let valueString = VariablesView.getString(value, { concise: true });
+
+ return keyString + " \u2192 " + valueString;
+ },
+
+}; // VariablesView.stringifiers.byType
+
+VariablesView.stringifiers.byObjectClass = {
+ Function: function (aGrip, {concise}) {
+ // TODO: Bug 948484 - support arrow functions and ES6 generators
+
+ let name = aGrip.userDisplayName || aGrip.displayName || aGrip.name || "";
+ name = VariablesView.getString(name, { noStringQuotes: true });
+
+ // TODO: Bug 948489 - Support functions with destructured parameters and
+ // rest parameters
+ let params = aGrip.parameterNames || "";
+ if (!concise) {
+ return "function " + name + "(" + params + ")";
+ }
+ return (name || "function ") + "(" + params + ")";
+ },
+
+ RegExp: function ({displayString}) {
+ return VariablesView.getString(displayString, { noStringQuotes: true });
+ },
+
+ Date: function ({preview}) {
+ if (!preview || !("timestamp" in preview)) {
+ return null;
+ }
+
+ if (typeof preview.timestamp != "number") {
+ return new Date(preview.timestamp).toString(); // invalid date
+ }
+
+ return "Date " + new Date(preview.timestamp).toISOString();
+ },
+
+ Number: function (aGrip) {
+ let {preview} = aGrip;
+ if (preview === undefined) {
+ return null;
+ }
+ return aGrip.class + " { " + VariablesView.getString(preview.wrappedValue) +
+ " }";
+ },
+}; // VariablesView.stringifiers.byObjectClass
+
+VariablesView.stringifiers.byObjectClass.Boolean =
+ VariablesView.stringifiers.byObjectClass.Number;
+
+VariablesView.stringifiers.byObjectKind = {
+ ArrayLike: function (aGrip, {concise}) {
+ let {preview} = aGrip;
+ if (concise) {
+ return aGrip.class + "[" + preview.length + "]";
+ }
+
+ if (!preview.items) {
+ return null;
+ }
+
+ let shown = 0, result = [], lastHole = null;
+ for (let item of preview.items) {
+ if (item === null) {
+ if (lastHole !== null) {
+ result[lastHole] += ",";
+ } else {
+ result.push("");
+ }
+ lastHole = result.length - 1;
+ } else {
+ lastHole = null;
+ result.push(VariablesView.getString(item, { concise: true }));
+ }
+ shown++;
+ }
+
+ if (shown < preview.length) {
+ let n = preview.length - shown;
+ result.push(VariablesView.stringifiers._getNMoreString(n));
+ } else if (lastHole !== null) {
+ // make sure we have the right number of commas...
+ result[lastHole] += ",";
+ }
+
+ let prefix = aGrip.class == "Array" ? "" : aGrip.class + " ";
+ return prefix + "[" + result.join(", ") + "]";
+ },
+
+ MapLike: function (aGrip, {concise}) {
+ let {preview} = aGrip;
+ if (concise || !preview.entries) {
+ let size = typeof preview.size == "number" ?
+ "[" + preview.size + "]" : "";
+ return aGrip.class + size;
+ }
+
+ let entries = [];
+ for (let [key, value] of preview.entries) {
+ let keyString = VariablesView.getString(key, {
+ concise: true,
+ noStringQuotes: true,
+ });
+ let valueString = VariablesView.getString(value, { concise: true });
+ entries.push(keyString + ": " + valueString);
+ }
+
+ if (typeof preview.size == "number" && preview.size > entries.length) {
+ let n = preview.size - entries.length;
+ entries.push(VariablesView.stringifiers._getNMoreString(n));
+ }
+
+ return aGrip.class + " {" + entries.join(", ") + "}";
+ },
+
+ ObjectWithText: function (aGrip, {concise}) {
+ if (concise) {
+ return aGrip.class;
+ }
+
+ return aGrip.class + " " + VariablesView.getString(aGrip.preview.text);
+ },
+
+ ObjectWithURL: function (aGrip, {concise}) {
+ let result = aGrip.class;
+ let url = aGrip.preview.url;
+ if (!VariablesView.isFalsy({ value: url })) {
+ result += ` \u2192 ${getSourceNames(url)[concise ? "short" : "long"]}`;
+ }
+ return result;
+ },
+
+ // Stringifier for any kind of object.
+ Object: function (aGrip, {concise}) {
+ if (concise) {
+ return aGrip.class;
+ }
+
+ let {preview} = aGrip;
+ let props = [];
+
+ if (aGrip.class == "Promise" && aGrip.promiseState) {
+ let { state, value, reason } = aGrip.promiseState;
+ props.push("<state>: " + VariablesView.getString(state));
+ if (state == "fulfilled") {
+ props.push("<value>: " + VariablesView.getString(value, { concise: true }));
+ } else if (state == "rejected") {
+ props.push("<reason>: " + VariablesView.getString(reason, { concise: true }));
+ }
+ }
+
+ for (let key of Object.keys(preview.ownProperties || {})) {
+ let value = preview.ownProperties[key];
+ let valueString = "";
+ if (value.get) {
+ valueString = "Getter";
+ } else if (value.set) {
+ valueString = "Setter";
+ } else {
+ valueString = VariablesView.getString(value.value, { concise: true });
+ }
+ props.push(key + ": " + valueString);
+ }
+
+ for (let key of Object.keys(preview.safeGetterValues || {})) {
+ let value = preview.safeGetterValues[key];
+ let valueString = VariablesView.getString(value.getterValue,
+ { concise: true });
+ props.push(key + ": " + valueString);
+ }
+
+ if (!props.length) {
+ return null;
+ }
+
+ if (preview.ownPropertiesLength) {
+ let previewLength = Object.keys(preview.ownProperties).length;
+ let diff = preview.ownPropertiesLength - previewLength;
+ if (diff > 0) {
+ props.push(VariablesView.stringifiers._getNMoreString(diff));
+ }
+ }
+
+ let prefix = aGrip.class != "Object" ? aGrip.class + " " : "";
+ return prefix + "{" + props.join(", ") + "}";
+ }, // Object
+
+ Error: function (aGrip, {concise}) {
+ let {preview} = aGrip;
+ let name = VariablesView.getString(preview.name, { noStringQuotes: true });
+ if (concise) {
+ return name || aGrip.class;
+ }
+
+ let msg = name + ": " +
+ VariablesView.getString(preview.message, { noStringQuotes: true });
+
+ if (!VariablesView.isFalsy({ value: preview.stack })) {
+ msg += "\n" + L10N.getStr("variablesViewErrorStacktrace") +
+ "\n" + preview.stack;
+ }
+
+ return msg;
+ },
+
+ DOMException: function (aGrip, {concise}) {
+ let {preview} = aGrip;
+ if (concise) {
+ return preview.name || aGrip.class;
+ }
+
+ let msg = aGrip.class + " [" + preview.name + ": " +
+ VariablesView.getString(preview.message) + "\n" +
+ "code: " + preview.code + "\n" +
+ "nsresult: 0x" + (+preview.result).toString(16);
+
+ if (preview.filename) {
+ msg += "\nlocation: " + preview.filename;
+ if (preview.lineNumber) {
+ msg += ":" + preview.lineNumber;
+ }
+ }
+
+ return msg + "]";
+ },
+
+ DOMEvent: function (aGrip, {concise}) {
+ let {preview} = aGrip;
+ if (!preview.type) {
+ return null;
+ }
+
+ if (concise) {
+ return aGrip.class + " " + preview.type;
+ }
+
+ let result = preview.type;
+
+ if (preview.eventKind == "key" && preview.modifiers &&
+ preview.modifiers.length) {
+ result += " " + preview.modifiers.join("-");
+ }
+
+ let props = [];
+ if (preview.target) {
+ let target = VariablesView.getString(preview.target, { concise: true });
+ props.push("target: " + target);
+ }
+
+ for (let prop in preview.properties) {
+ let value = preview.properties[prop];
+ props.push(prop + ": " + VariablesView.getString(value, { concise: true }));
+ }
+
+ return result + " {" + props.join(", ") + "}";
+ }, // DOMEvent
+
+ DOMNode: function (aGrip, {concise}) {
+ let {preview} = aGrip;
+
+ switch (preview.nodeType) {
+ case nodeConstants.DOCUMENT_NODE: {
+ let result = aGrip.class;
+ if (preview.location) {
+ result += ` \u2192 ${getSourceNames(preview.location)[concise ? "short" : "long"]}`;
+ }
+
+ return result;
+ }
+
+ case nodeConstants.ATTRIBUTE_NODE: {
+ let value = VariablesView.getString(preview.value, { noStringQuotes: true });
+ return preview.nodeName + '="' + escapeHTML(value) + '"';
+ }
+
+ case nodeConstants.TEXT_NODE:
+ return preview.nodeName + " " +
+ VariablesView.getString(preview.textContent);
+
+ case nodeConstants.COMMENT_NODE: {
+ let comment = VariablesView.getString(preview.textContent,
+ { noStringQuotes: true });
+ return "<!--" + comment + "-->";
+ }
+
+ case nodeConstants.DOCUMENT_FRAGMENT_NODE: {
+ if (concise || !preview.childNodes) {
+ return aGrip.class + "[" + preview.childNodesLength + "]";
+ }
+ let nodes = [];
+ for (let node of preview.childNodes) {
+ nodes.push(VariablesView.getString(node));
+ }
+ if (nodes.length < preview.childNodesLength) {
+ let n = preview.childNodesLength - nodes.length;
+ nodes.push(VariablesView.stringifiers._getNMoreString(n));
+ }
+ return aGrip.class + " [" + nodes.join(", ") + "]";
+ }
+
+ case nodeConstants.ELEMENT_NODE: {
+ let attrs = preview.attributes;
+ if (!concise) {
+ let n = 0, result = "<" + preview.nodeName;
+ for (let name in attrs) {
+ let value = VariablesView.getString(attrs[name],
+ { noStringQuotes: true });
+ result += " " + name + '="' + escapeHTML(value) + '"';
+ n++;
+ }
+ if (preview.attributesLength > n) {
+ result += " " + ELLIPSIS;
+ }
+ return result + ">";
+ }
+
+ let result = "<" + preview.nodeName;
+ if (attrs.id) {
+ result += "#" + attrs.id;
+ }
+
+ if (attrs.class) {
+ result += "." + attrs.class.trim().replace(/\s+/, ".");
+ }
+ return result + ">";
+ }
+
+ default:
+ return null;
+ }
+ }, // DOMNode
+}; // VariablesView.stringifiers.byObjectKind
+
+
+/**
+ * Get the "N more…" formatted string, given an N. This is used for displaying
+ * how many elements are not displayed in an object preview (eg. an array).
+ *
+ * @private
+ * @param number aNumber
+ * @return string
+ */
+VariablesView.stringifiers._getNMoreString = function (aNumber) {
+ let str = L10N.getStr("variablesViewMoreObjects");
+ return PluralForm.get(aNumber, str).replace("#1", aNumber);
+};
+
+/**
+ * Returns a custom class style for a grip.
+ *
+ * @param any aGrip
+ * @see Variable.setGrip
+ * @return string
+ * The custom class style.
+ */
+VariablesView.getClass = function (aGrip) {
+ if (aGrip && typeof aGrip == "object") {
+ if (aGrip.preview) {
+ switch (aGrip.preview.kind) {
+ case "DOMNode":
+ return "token-domnode";
+ }
+ }
+
+ switch (aGrip.type) {
+ case "undefined":
+ return "token-undefined";
+ case "null":
+ return "token-null";
+ case "Infinity":
+ case "-Infinity":
+ case "NaN":
+ case "-0":
+ return "token-number";
+ case "longString":
+ return "token-string";
+ }
+ }
+ switch (typeof aGrip) {
+ case "string":
+ return "token-string";
+ case "boolean":
+ return "token-boolean";
+ case "number":
+ return "token-number";
+ default:
+ return "token-other";
+ }
+};
+
+/**
+ * A monotonically-increasing counter, that guarantees the uniqueness of scope,
+ * variables and properties ids.
+ *
+ * @param string aName
+ * An optional string to prefix the id with.
+ * @return number
+ * A unique id.
+ */
+var generateId = (function () {
+ let count = 0;
+ return function (aName = "") {
+ return aName.toLowerCase().trim().replace(/\s+/g, "-") + (++count);
+ };
+})();
+
+/**
+ * Serialize a string to JSON. The result can be inserted in a string evaluated by `eval`.
+ *
+ * @param string aString
+ * The string to be escaped. If undefined, the function returns the empty string.
+ * @return string
+ */
+function escapeString(aString) {
+ return JSON.stringify(aString) || "";
+}
+
+/**
+ * Escape some HTML special characters. We do not need full HTML serialization
+ * here, we just want to make strings safe to display in HTML attributes, for
+ * the stringifiers.
+ *
+ * @param string aString
+ * @return string
+ */
+function escapeHTML(aString) {
+ return aString.replace(/&/g, "&amp;")
+ .replace(/"/g, "&quot;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;");
+}
+
+
+/**
+ * An Editable encapsulates the UI of an edit box that overlays a label,
+ * allowing the user to edit the value.
+ *
+ * @param Variable aVariable
+ * The Variable or Property to make editable.
+ * @param object aOptions
+ * - onSave
+ * The callback to call with the value when editing is complete.
+ * - onCleanup
+ * The callback to call when the editable is removed for any reason.
+ */
+function Editable(aVariable, aOptions) {
+ this._variable = aVariable;
+ this._onSave = aOptions.onSave;
+ this._onCleanup = aOptions.onCleanup;
+}
+
+Editable.create = function (aVariable, aOptions, aEvent) {
+ let editable = new this(aVariable, aOptions);
+ editable.activate(aEvent);
+ return editable;
+};
+
+Editable.prototype = {
+ /**
+ * The class name for targeting this Editable type's label element. Overridden
+ * by inheriting classes.
+ */
+ className: null,
+
+ /**
+ * Boolean indicating whether this Editable should activate. Overridden by
+ * inheriting classes.
+ */
+ shouldActivate: null,
+
+ /**
+ * The label element for this Editable. Overridden by inheriting classes.
+ */
+ label: null,
+
+ /**
+ * Activate this editable by replacing the input box it overlays and
+ * initialize the handlers.
+ *
+ * @param Event e [optional]
+ * Optionally, the Event object that was used to activate the Editable.
+ */
+ activate: function (e) {
+ if (!this.shouldActivate) {
+ this._onCleanup && this._onCleanup();
+ return;
+ }
+
+ let { label } = this;
+ let initialString = label.getAttribute("value");
+
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ // Create a texbox input element which will be shown in the current
+ // element's specified label location.
+ let input = this._input = this._variable.document.createElement("textbox");
+ input.className = "plain " + this.className;
+ input.setAttribute("value", initialString);
+ input.setAttribute("flex", "1");
+
+ // Replace the specified label with a textbox input element.
+ label.parentNode.replaceChild(input, label);
+ this._variable._variablesView.boxObject.ensureElementIsVisible(input);
+ input.select();
+
+ // When the value is a string (displayed as "value"), then we probably want
+ // to change it to another string in the textbox, so to avoid typing the ""
+ // again, tackle with the selection bounds just a bit.
+ if (initialString.match(/^".+"$/)) {
+ input.selectionEnd--;
+ input.selectionStart++;
+ }
+
+ this._onKeypress = this._onKeypress.bind(this);
+ this._onBlur = this._onBlur.bind(this);
+ input.addEventListener("keypress", this._onKeypress);
+ input.addEventListener("blur", this._onBlur);
+
+ this._prevExpandable = this._variable.twisty;
+ this._prevExpanded = this._variable.expanded;
+ this._variable.collapse();
+ this._variable.hideArrow();
+ this._variable.locked = true;
+ this._variable.editing = true;
+ },
+
+ /**
+ * Remove the input box and restore the Variable or Property to its previous
+ * state.
+ */
+ deactivate: function () {
+ this._input.removeEventListener("keypress", this._onKeypress);
+ this._input.removeEventListener("blur", this.deactivate);
+ this._input.parentNode.replaceChild(this.label, this._input);
+ this._input = null;
+
+ let { boxObject } = this._variable._variablesView;
+ boxObject.scrollBy(-this._variable._target, 0);
+ this._variable.locked = false;
+ this._variable.twisty = this._prevExpandable;
+ this._variable.expanded = this._prevExpanded;
+ this._variable.editing = false;
+ this._onCleanup && this._onCleanup();
+ },
+
+ /**
+ * Save the current value and deactivate the Editable.
+ */
+ _save: function () {
+ let initial = this.label.getAttribute("value");
+ let current = this._input.value.trim();
+ this.deactivate();
+ if (initial != current) {
+ this._onSave(current);
+ }
+ },
+
+ /**
+ * Called when tab is pressed, allowing subclasses to link different
+ * behavior to tabbing if desired.
+ */
+ _next: function () {
+ this._save();
+ },
+
+ /**
+ * Called when escape is pressed, indicating a cancelling of editing without
+ * saving.
+ */
+ _reset: function () {
+ this.deactivate();
+ this._variable.focus();
+ },
+
+ /**
+ * Event handler for when the input loses focus.
+ */
+ _onBlur: function () {
+ this.deactivate();
+ },
+
+ /**
+ * Event handler for when the input receives a key press.
+ */
+ _onKeypress: function (e) {
+ e.stopPropagation();
+
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_TAB:
+ this._next();
+ break;
+ case KeyCodes.DOM_VK_RETURN:
+ this._save();
+ break;
+ case KeyCodes.DOM_VK_ESCAPE:
+ this._reset();
+ break;
+ }
+ },
+};
+
+
+/**
+ * An Editable specific to editing the name of a Variable or Property.
+ */
+function EditableName(aVariable, aOptions) {
+ Editable.call(this, aVariable, aOptions);
+}
+
+EditableName.create = Editable.create;
+
+EditableName.prototype = Heritage.extend(Editable.prototype, {
+ className: "element-name-input",
+
+ get label() {
+ return this._variable._name;
+ },
+
+ get shouldActivate() {
+ return !!this._variable.ownerView.switch;
+ },
+});
+
+
+/**
+ * An Editable specific to editing the value of a Variable or Property.
+ */
+function EditableValue(aVariable, aOptions) {
+ Editable.call(this, aVariable, aOptions);
+}
+
+EditableValue.create = Editable.create;
+
+EditableValue.prototype = Heritage.extend(Editable.prototype, {
+ className: "element-value-input",
+
+ get label() {
+ return this._variable._valueLabel;
+ },
+
+ get shouldActivate() {
+ return !!this._variable.ownerView.eval;
+ },
+});
+
+
+/**
+ * An Editable specific to editing the key and value of a new property.
+ */
+function EditableNameAndValue(aVariable, aOptions) {
+ EditableName.call(this, aVariable, aOptions);
+}
+
+EditableNameAndValue.create = Editable.create;
+
+EditableNameAndValue.prototype = Heritage.extend(EditableName.prototype, {
+ _reset: function (e) {
+ // Hide the Variable or Property if the user presses escape.
+ this._variable.remove();
+ this.deactivate();
+ },
+
+ _next: function (e) {
+ // Override _next so as to set both key and value at the same time.
+ let key = this._input.value;
+ this.label.setAttribute("value", key);
+
+ let valueEditable = EditableValue.create(this._variable, {
+ onSave: aValue => {
+ this._onSave([key, aValue]);
+ }
+ });
+ valueEditable._reset = () => {
+ this._variable.remove();
+ valueEditable.deactivate();
+ };
+ },
+
+ _save: function (e) {
+ // Both _save and _next activate the value edit box.
+ this._next(e);
+ }
+});
diff --git a/devtools/client/shared/widgets/VariablesView.xul b/devtools/client/shared/widgets/VariablesView.xul
new file mode 100644
index 000000000..fe8bb13ec
--- /dev/null
+++ b/devtools/client/shared/widgets/VariablesView.xul
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % viewDTD SYSTEM "chrome://devtools/locale/VariablesView.dtd">
+ %viewDTD;
+]>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&PropertiesViewWindowTitle;">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+ <vbox id="variables" flex="1"/>
+</window>
diff --git a/devtools/client/shared/widgets/VariablesViewController.jsm b/devtools/client/shared/widgets/VariablesViewController.jsm
new file mode 100644
index 000000000..5413ce1bf
--- /dev/null
+++ b/devtools/client/shared/widgets/VariablesViewController.jsm
@@ -0,0 +1,858 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { utils: Cu } = Components;
+
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+var {VariablesView} = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
+var Services = require("Services");
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+var {LocalizationHelper, ELLIPSIS} = require("devtools/shared/l10n");
+
+Object.defineProperty(this, "WebConsoleUtils", {
+ get: function () {
+ return require("devtools/client/webconsole/utils").Utils;
+ },
+ configurable: true,
+ enumerable: true
+});
+
+XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () =>
+ Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled")
+);
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+
+const MAX_LONG_STRING_LENGTH = 200000;
+const MAX_PROPERTY_ITEMS = 2000;
+const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
+
+this.EXPORTED_SYMBOLS = ["VariablesViewController", "StackFrameUtils"];
+
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(DBG_STRINGS_URI);
+
+/**
+ * Controller for a VariablesView that handles interfacing with the debugger
+ * protocol. Is able to populate scopes and variables via the protocol as well
+ * as manage actor lifespans.
+ *
+ * @param VariablesView aView
+ * The view to attach to.
+ * @param object aOptions [optional]
+ * Options for configuring the controller. Supported options:
+ * - getObjectClient: @see this._setClientGetters
+ * - getLongStringClient: @see this._setClientGetters
+ * - getEnvironmentClient: @see this._setClientGetters
+ * - releaseActor: @see this._setClientGetters
+ * - overrideValueEvalMacro: @see _setEvaluationMacros
+ * - getterOrSetterEvalMacro: @see _setEvaluationMacros
+ * - simpleValueEvalMacro: @see _setEvaluationMacros
+ */
+function VariablesViewController(aView, aOptions = {}) {
+ this.addExpander = this.addExpander.bind(this);
+
+ this._setClientGetters(aOptions);
+ this._setEvaluationMacros(aOptions);
+
+ this._actors = new Set();
+ this.view = aView;
+ this.view.controller = this;
+}
+this.VariablesViewController = VariablesViewController;
+
+VariablesViewController.prototype = {
+ /**
+ * The default getter/setter evaluation macro.
+ */
+ _getterOrSetterEvalMacro: VariablesView.getterOrSetterEvalMacro,
+
+ /**
+ * The default override value evaluation macro.
+ */
+ _overrideValueEvalMacro: VariablesView.overrideValueEvalMacro,
+
+ /**
+ * The default simple value evaluation macro.
+ */
+ _simpleValueEvalMacro: VariablesView.simpleValueEvalMacro,
+
+ /**
+ * Set the functions used to retrieve debugger client grips.
+ *
+ * @param object aOptions
+ * Options for getting the client grips. Supported options:
+ * - getObjectClient: callback for creating an object grip client
+ * - getLongStringClient: callback for creating a long string grip client
+ * - getEnvironmentClient: callback for creating an environment client
+ * - releaseActor: callback for releasing an actor when it's no longer needed
+ */
+ _setClientGetters: function (aOptions) {
+ if (aOptions.getObjectClient) {
+ this._getObjectClient = aOptions.getObjectClient;
+ }
+ if (aOptions.getLongStringClient) {
+ this._getLongStringClient = aOptions.getLongStringClient;
+ }
+ if (aOptions.getEnvironmentClient) {
+ this._getEnvironmentClient = aOptions.getEnvironmentClient;
+ }
+ if (aOptions.releaseActor) {
+ this._releaseActor = aOptions.releaseActor;
+ }
+ },
+
+ /**
+ * Sets the functions used when evaluating strings in the variables view.
+ *
+ * @param object aOptions
+ * Options for configuring the macros. Supported options:
+ * - overrideValueEvalMacro: callback for creating an overriding eval macro
+ * - getterOrSetterEvalMacro: callback for creating a getter/setter eval macro
+ * - simpleValueEvalMacro: callback for creating a simple value eval macro
+ */
+ _setEvaluationMacros: function (aOptions) {
+ if (aOptions.overrideValueEvalMacro) {
+ this._overrideValueEvalMacro = aOptions.overrideValueEvalMacro;
+ }
+ if (aOptions.getterOrSetterEvalMacro) {
+ this._getterOrSetterEvalMacro = aOptions.getterOrSetterEvalMacro;
+ }
+ if (aOptions.simpleValueEvalMacro) {
+ this._simpleValueEvalMacro = aOptions.simpleValueEvalMacro;
+ }
+ },
+
+ /**
+ * Populate a long string into a target using a grip.
+ *
+ * @param Variable aTarget
+ * The target Variable/Property to put the retrieved string into.
+ * @param LongStringActor aGrip
+ * The long string grip that use to retrieve the full string.
+ * @return Promise
+ * The promise that will be resolved when the string is retrieved.
+ */
+ _populateFromLongString: function (aTarget, aGrip) {
+ let deferred = defer();
+
+ let from = aGrip.initial.length;
+ let to = Math.min(aGrip.length, MAX_LONG_STRING_LENGTH);
+
+ this._getLongStringClient(aGrip).substring(from, to, aResponse => {
+ // Stop tracking the actor because it's no longer needed.
+ this.releaseActor(aGrip);
+
+ // Replace the preview with the full string and make it non-expandable.
+ aTarget.onexpand = null;
+ aTarget.setGrip(aGrip.initial + aResponse.substring);
+ aTarget.hideArrow();
+
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Adds pseudo items in case there is too many properties to display.
+ * Each item can expand into property slices.
+ *
+ * @param Scope aTarget
+ * The Scope where the properties will be placed into.
+ * @param object aGrip
+ * The property iterator grip.
+ */
+ _populatePropertySlices: function (aTarget, aGrip) {
+ if (aGrip.count < MAX_PROPERTY_ITEMS) {
+ return this._populateFromPropertyIterator(aTarget, aGrip);
+ }
+
+ // Divide the keys into quarters.
+ let items = Math.ceil(aGrip.count / 4);
+ let iterator = aGrip.propertyIterator;
+ let promises = [];
+ for (let i = 0; i < 4; i++) {
+ let start = aGrip.start + i * items;
+ let count = i != 3 ? items : aGrip.count - i * items;
+
+ // Create a new kind of grip, with additional fields to define the slice
+ let sliceGrip = {
+ type: "property-iterator",
+ propertyIterator: iterator,
+ start: start,
+ count: count
+ };
+
+ // Query the name of the first and last items for this slice
+ let deferred = defer();
+ iterator.names([start, start + count - 1], ({ names }) => {
+ let label = "[" + names[0] + ELLIPSIS + names[1] + "]";
+ let item = aTarget.addItem(label, {}, { internalItem: true });
+ item.showArrow();
+ this.addExpander(item, sliceGrip);
+ deferred.resolve();
+ });
+ promises.push(deferred.promise);
+ }
+
+ return promise.all(promises);
+ },
+
+ /**
+ * Adds a property slice for a Variable in the view using the already
+ * property iterator
+ *
+ * @param Scope aTarget
+ * The Scope where the properties will be placed into.
+ * @param object aGrip
+ * The property iterator grip.
+ */
+ _populateFromPropertyIterator: function (aTarget, aGrip) {
+ if (aGrip.count >= MAX_PROPERTY_ITEMS) {
+ // We already started to split, but there is still too many properties, split again.
+ return this._populatePropertySlices(aTarget, aGrip);
+ }
+ // We started slicing properties, and the slice is now small enough to be displayed
+ let deferred = defer();
+ aGrip.propertyIterator.slice(aGrip.start, aGrip.count,
+ ({ ownProperties }) => {
+ // Add all the variable properties.
+ if (Object.keys(ownProperties).length > 0) {
+ aTarget.addItems(ownProperties, {
+ sorted: true,
+ // Expansion handlers must be set after the properties are added.
+ callback: this.addExpander
+ });
+ }
+ deferred.resolve();
+ });
+ return deferred.promise;
+ },
+
+ /**
+ * Adds the properties for a Variable in the view using a new feature in FF40+
+ * that allows iteration over properties in slices.
+ *
+ * @param Scope aTarget
+ * The Scope where the properties will be placed into.
+ * @param object aGrip
+ * The grip to use to populate the target.
+ * @param string aQuery [optional]
+ * The query string used to fetch only a subset of properties
+ */
+ _populateFromObjectWithIterator: function (aTarget, aGrip, aQuery) {
+ // FF40+ starts exposing `ownPropertyLength` on ObjectActor's grip,
+ // as well as `enumProperties` request.
+ let deferred = defer();
+ let objectClient = this._getObjectClient(aGrip);
+ let isArray = aGrip.preview && aGrip.preview.kind === "ArrayLike";
+ if (isArray) {
+ // First enumerate array items, e.g. properties from `0` to `array.length`.
+ let options = {
+ ignoreNonIndexedProperties: true,
+ query: aQuery
+ };
+ objectClient.enumProperties(options, ({ iterator }) => {
+ let sliceGrip = {
+ type: "property-iterator",
+ propertyIterator: iterator,
+ start: 0,
+ count: iterator.count
+ };
+ this._populatePropertySlices(aTarget, sliceGrip)
+ .then(() => {
+ // Then enumerate the rest of the properties, like length, buffer, etc.
+ let options = {
+ ignoreIndexedProperties: true,
+ sort: true,
+ query: aQuery
+ };
+ objectClient.enumProperties(options, ({ iterator }) => {
+ let sliceGrip = {
+ type: "property-iterator",
+ propertyIterator: iterator,
+ start: 0,
+ count: iterator.count
+ };
+ deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip));
+ });
+ });
+ });
+ } else {
+ // For objects, we just enumerate all the properties sorted by name.
+ objectClient.enumProperties({ sort: true, query: aQuery }, ({ iterator }) => {
+ let sliceGrip = {
+ type: "property-iterator",
+ propertyIterator: iterator,
+ start: 0,
+ count: iterator.count
+ };
+ deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip));
+ });
+
+ }
+ return deferred.promise;
+ },
+
+ /**
+ * Adds the given prototype in the view.
+ *
+ * @param Scope aTarget
+ * The Scope where the properties will be placed into.
+ * @param object aProtype
+ * The prototype grip.
+ */
+ _populateObjectPrototype: function (aTarget, aPrototype) {
+ // Add the variable's __proto__.
+ if (aPrototype && aPrototype.type != "null") {
+ let proto = aTarget.addItem("__proto__", { value: aPrototype });
+ this.addExpander(proto, aPrototype);
+ }
+ },
+
+ /**
+ * Adds properties to a Scope, Variable, or Property in the view. Triggered
+ * when a scope is expanded or certain variables are hovered.
+ *
+ * @param Scope aTarget
+ * The Scope where the properties will be placed into.
+ * @param object aGrip
+ * The grip to use to populate the target.
+ */
+ _populateFromObject: function (aTarget, aGrip) {
+ if (aGrip.class === "Proxy") {
+ this.addExpander(
+ aTarget.addItem("<target>", { value: aGrip.proxyTarget }, { internalItem: true }),
+ aGrip.proxyTarget);
+ this.addExpander(
+ aTarget.addItem("<handler>", { value: aGrip.proxyHandler }, { internalItem: true }),
+ aGrip.proxyHandler);
+
+ // Refuse to play the proxy's stupid game and return immediately
+ let deferred = defer();
+ deferred.resolve();
+ return deferred.promise;
+ }
+
+ if (aGrip.class === "Promise" && aGrip.promiseState) {
+ const { state, value, reason } = aGrip.promiseState;
+ aTarget.addItem("<state>", { value: state }, { internalItem: true });
+ if (state === "fulfilled") {
+ this.addExpander(
+ aTarget.addItem("<value>", { value }, { internalItem: true }),
+ value);
+ } else if (state === "rejected") {
+ this.addExpander(
+ aTarget.addItem("<reason>", { value: reason }, { internalItem: true }),
+ reason);
+ }
+ } else if (["Map", "WeakMap", "Set", "WeakSet"].includes(aGrip.class)) {
+ let entriesList = aTarget.addItem("<entries>", {}, { internalItem: true });
+ entriesList.showArrow();
+ this.addExpander(entriesList, {
+ type: "entries-list",
+ obj: aGrip
+ });
+ }
+
+ // Fetch properties by slices if there is too many in order to prevent UI freeze.
+ if ("ownPropertyLength" in aGrip && aGrip.ownPropertyLength >= MAX_PROPERTY_ITEMS) {
+ return this._populateFromObjectWithIterator(aTarget, aGrip)
+ .then(() => {
+ let deferred = defer();
+ let objectClient = this._getObjectClient(aGrip);
+ objectClient.getPrototype(({ prototype }) => {
+ this._populateObjectPrototype(aTarget, prototype);
+ deferred.resolve();
+ });
+ return deferred.promise;
+ });
+ }
+
+ return this._populateProperties(aTarget, aGrip);
+ },
+
+ _populateProperties: function (aTarget, aGrip, aOptions) {
+ let deferred = defer();
+
+ let objectClient = this._getObjectClient(aGrip);
+ objectClient.getPrototypeAndProperties(aResponse => {
+ let ownProperties = aResponse.ownProperties || {};
+ let prototype = aResponse.prototype || null;
+ // 'safeGetterValues' is new and isn't necessary defined on old actors.
+ let safeGetterValues = aResponse.safeGetterValues || {};
+ let sortable = VariablesView.isSortable(aGrip.class);
+
+ // Merge the safe getter values into one object such that we can use it
+ // in VariablesView.
+ for (let name of Object.keys(safeGetterValues)) {
+ if (name in ownProperties) {
+ let { getterValue, getterPrototypeLevel } = safeGetterValues[name];
+ ownProperties[name].getterValue = getterValue;
+ ownProperties[name].getterPrototypeLevel = getterPrototypeLevel;
+ } else {
+ ownProperties[name] = safeGetterValues[name];
+ }
+ }
+
+ // Add all the variable properties.
+ aTarget.addItems(ownProperties, {
+ // Not all variables need to force sorted properties.
+ sorted: sortable,
+ // Expansion handlers must be set after the properties are added.
+ callback: this.addExpander
+ });
+
+ // Add the variable's __proto__.
+ this._populateObjectPrototype(aTarget, prototype);
+
+ // If the object is a function we need to fetch its scope chain
+ // to show them as closures for the respective function.
+ if (aGrip.class == "Function") {
+ objectClient.getScope(aResponse => {
+ if (aResponse.error) {
+ // This function is bound to a built-in object or it's not present
+ // in the current scope chain. Not necessarily an actual error,
+ // it just means that there's no closure for the function.
+ console.warn(aResponse.error + ": " + aResponse.message);
+ return void deferred.resolve();
+ }
+ this._populateWithClosure(aTarget, aResponse.scope).then(deferred.resolve);
+ });
+ } else {
+ deferred.resolve();
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Adds the scope chain elements (closures) of a function variable.
+ *
+ * @param Variable aTarget
+ * The variable where the properties will be placed into.
+ * @param Scope aScope
+ * The lexical environment form as specified in the protocol.
+ */
+ _populateWithClosure: function (aTarget, aScope) {
+ let objectScopes = [];
+ let environment = aScope;
+ let funcScope = aTarget.addItem("<Closure>");
+ funcScope.target.setAttribute("scope", "");
+ funcScope.showArrow();
+
+ do {
+ // Create a scope to contain all the inspected variables.
+ let label = StackFrameUtils.getScopeLabel(environment);
+
+ // Block scopes may have the same label, so make addItem allow duplicates.
+ let closure = funcScope.addItem(label, undefined, {relaxed: true});
+ closure.target.setAttribute("scope", "");
+ closure.showArrow();
+
+ // Add nodes for every argument and every other variable in scope.
+ if (environment.bindings) {
+ this._populateWithEnvironmentBindings(closure, environment.bindings);
+ } else {
+ let deferred = defer();
+ objectScopes.push(deferred.promise);
+ this._getEnvironmentClient(environment).getBindings(response => {
+ this._populateWithEnvironmentBindings(closure, response.bindings);
+ deferred.resolve();
+ });
+ }
+ } while ((environment = environment.parent));
+
+ return promise.all(objectScopes).then(() => {
+ // Signal that scopes have been fetched.
+ this.view.emit("fetched", "scopes", funcScope);
+ });
+ },
+
+ /**
+ * Adds nodes for every specified binding to the closure node.
+ *
+ * @param Variable aTarget
+ * The variable where the bindings will be placed into.
+ * @param object aBindings
+ * The bindings form as specified in the protocol.
+ */
+ _populateWithEnvironmentBindings: function (aTarget, aBindings) {
+ // Add nodes for every argument in the scope.
+ aTarget.addItems(aBindings.arguments.reduce((accumulator, arg) => {
+ let name = Object.getOwnPropertyNames(arg)[0];
+ let descriptor = arg[name];
+ accumulator[name] = descriptor;
+ return accumulator;
+ }, {}), {
+ // Arguments aren't sorted.
+ sorted: false,
+ // Expansion handlers must be set after the properties are added.
+ callback: this.addExpander
+ });
+
+ // Add nodes for every other variable in the scope.
+ aTarget.addItems(aBindings.variables, {
+ // Not all variables need to force sorted properties.
+ sorted: VARIABLES_SORTING_ENABLED,
+ // Expansion handlers must be set after the properties are added.
+ callback: this.addExpander
+ });
+ },
+
+ _populateFromEntries: function (target, grip) {
+ let objGrip = grip.obj;
+ let objectClient = this._getObjectClient(objGrip);
+
+ return new promise((resolve, reject) => {
+ objectClient.enumEntries((response) => {
+ if (response.error) {
+ // Older server might not support the enumEntries method
+ console.warn(response.error + ": " + response.message);
+ resolve();
+ } else {
+ let sliceGrip = {
+ type: "property-iterator",
+ propertyIterator: response.iterator,
+ start: 0,
+ count: response.iterator.count
+ };
+
+ resolve(this._populatePropertySlices(target, sliceGrip));
+ }
+ });
+ });
+ },
+
+ /**
+ * Adds an 'onexpand' callback for a variable, lazily handling
+ * the addition of new properties.
+ *
+ * @param Variable aTarget
+ * The variable where the properties will be placed into.
+ * @param any aSource
+ * The source to use to populate the target.
+ */
+ addExpander: function (aTarget, aSource) {
+ // Attach evaluation macros as necessary.
+ if (aTarget.getter || aTarget.setter) {
+ aTarget.evaluationMacro = this._overrideValueEvalMacro;
+ let getter = aTarget.get("get");
+ if (getter) {
+ getter.evaluationMacro = this._getterOrSetterEvalMacro;
+ }
+ let setter = aTarget.get("set");
+ if (setter) {
+ setter.evaluationMacro = this._getterOrSetterEvalMacro;
+ }
+ } else {
+ aTarget.evaluationMacro = this._simpleValueEvalMacro;
+ }
+
+ // If the source is primitive then an expander is not needed.
+ if (VariablesView.isPrimitive({ value: aSource })) {
+ return;
+ }
+
+ // If the source is a long string then show the arrow.
+ if (WebConsoleUtils.isActorGrip(aSource) && aSource.type == "longString") {
+ aTarget.showArrow();
+ }
+
+ // Make sure that properties are always available on expansion.
+ aTarget.onexpand = () => this.populate(aTarget, aSource);
+
+ // Some variables are likely to contain a very large number of properties.
+ // It's a good idea to be prepared in case of an expansion.
+ if (aTarget.shouldPrefetch) {
+ aTarget.addEventListener("mouseover", aTarget.onexpand, false);
+ }
+
+ // Register all the actors that this controller now depends on.
+ for (let grip of [aTarget.value, aTarget.getter, aTarget.setter]) {
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ this._actors.add(grip.actor);
+ }
+ }
+ },
+
+ /**
+ * Adds properties to a Scope, Variable, or Property in the view. Triggered
+ * when a scope is expanded or certain variables are hovered.
+ *
+ * This does not expand the target, it only populates it.
+ *
+ * @param Scope aTarget
+ * The Scope to be expanded.
+ * @param object aSource
+ * The source to use to populate the target.
+ * @return Promise
+ * The promise that is resolved once the target has been expanded.
+ */
+ populate: function (aTarget, aSource) {
+ // Fetch the variables only once.
+ if (aTarget._fetched) {
+ return aTarget._fetched;
+ }
+ // Make sure the source grip is available.
+ if (!aSource) {
+ return promise.reject(new Error("No actor grip was given for the variable."));
+ }
+
+ let deferred = defer();
+ aTarget._fetched = deferred.promise;
+
+ if (aSource.type === "property-iterator") {
+ return this._populateFromPropertyIterator(aTarget, aSource);
+ }
+
+ if (aSource.type === "entries-list") {
+ return this._populateFromEntries(aTarget, aSource);
+ }
+
+ if (aSource.type === "mapEntry") {
+ aTarget.addItems({
+ key: { value: aSource.preview.key },
+ value: { value: aSource.preview.value }
+ }, {
+ callback: this.addExpander
+ });
+
+ return promise.resolve();
+ }
+
+ // If the target is a Variable or Property then we're fetching properties.
+ if (VariablesView.isVariable(aTarget)) {
+ this._populateFromObject(aTarget, aSource).then(() => {
+ // Signal that properties have been fetched.
+ this.view.emit("fetched", "properties", aTarget);
+ // Commit the hierarchy because new items were added.
+ this.view.commitHierarchy();
+ deferred.resolve();
+ });
+ return deferred.promise;
+ }
+
+ switch (aSource.type) {
+ case "longString":
+ this._populateFromLongString(aTarget, aSource).then(() => {
+ // Signal that a long string has been fetched.
+ this.view.emit("fetched", "longString", aTarget);
+ deferred.resolve();
+ });
+ break;
+ case "with":
+ case "object":
+ this._populateFromObject(aTarget, aSource.object).then(() => {
+ // Signal that variables have been fetched.
+ this.view.emit("fetched", "variables", aTarget);
+ // Commit the hierarchy because new items were added.
+ this.view.commitHierarchy();
+ deferred.resolve();
+ });
+ break;
+ case "block":
+ case "function":
+ this._populateWithEnvironmentBindings(aTarget, aSource.bindings);
+ // No need to signal that variables have been fetched, since
+ // the scope arguments and variables are already attached to the
+ // environment bindings, so pausing the active thread is unnecessary.
+ // Commit the hierarchy because new items were added.
+ this.view.commitHierarchy();
+ deferred.resolve();
+ break;
+ default:
+ let error = "Unknown Debugger.Environment type: " + aSource.type;
+ console.error(error);
+ deferred.reject(error);
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Indicates to the view if the targeted actor supports properties search
+ *
+ * @return boolean True, if the actor supports enumProperty request
+ */
+ supportsSearch: function () {
+ // FF40+ starts exposing ownPropertyLength on object actor's grip
+ // as well as enumProperty which allows to query a subset of properties.
+ return this.objectActor && ("ownPropertyLength" in this.objectActor);
+ },
+
+ /**
+ * Try to use the actor to perform an attribute search.
+ *
+ * @param Scope aScope
+ * The Scope instance to populate with properties
+ * @param string aToken
+ * The query string
+ */
+ performSearch: function (aScope, aToken) {
+ this._populateFromObjectWithIterator(aScope, this.objectActor, aToken)
+ .then(() => {
+ this.view.emit("fetched", "search", aScope);
+ });
+ },
+
+ /**
+ * Release an actor from the controller.
+ *
+ * @param object aActor
+ * The actor to release.
+ */
+ releaseActor: function (aActor) {
+ if (this._releaseActor) {
+ this._releaseActor(aActor);
+ }
+ this._actors.delete(aActor);
+ },
+
+ /**
+ * Release all the actors referenced by the controller, optionally filtered.
+ *
+ * @param function aFilter [optional]
+ * Callback to filter which actors are released.
+ */
+ releaseActors: function (aFilter) {
+ for (let actor of this._actors) {
+ if (!aFilter || aFilter(actor)) {
+ this.releaseActor(actor);
+ }
+ }
+ },
+
+ /**
+ * Helper function for setting up a single Scope with a single Variable
+ * contained within it.
+ *
+ * This function will empty the variables view.
+ *
+ * @param object options
+ * Options for the contents of the view:
+ * - objectActor: the grip of the new ObjectActor to show.
+ * - rawObject: the raw object to show.
+ * - label: the label for the inspected object.
+ * @param object configuration
+ * Additional options for the controller:
+ * - overrideValueEvalMacro: @see _setEvaluationMacros
+ * - getterOrSetterEvalMacro: @see _setEvaluationMacros
+ * - simpleValueEvalMacro: @see _setEvaluationMacros
+ * @return Object
+ * - variable: the created Variable.
+ * - expanded: the Promise that resolves when the variable expands.
+ */
+ setSingleVariable: function (options, configuration = {}) {
+ this._setEvaluationMacros(configuration);
+ this.view.empty();
+
+ let scope = this.view.addScope(options.label);
+ scope.expanded = true; // Expand the scope by default.
+ scope.locked = true; // Prevent collapsing the scope.
+
+ let variable = scope.addItem(undefined, { enumerable: true });
+ let populated;
+
+ if (options.objectActor) {
+ // Save objectActor for properties filtering
+ this.objectActor = options.objectActor;
+ if (VariablesView.isPrimitive({ value: this.objectActor })) {
+ populated = promise.resolve();
+ } else {
+ populated = this.populate(variable, options.objectActor);
+ variable.expand();
+ }
+ } else if (options.rawObject) {
+ variable.populate(options.rawObject, { expanded: true });
+ populated = promise.resolve();
+ }
+
+ return { variable: variable, expanded: populated };
+ },
+};
+
+
+/**
+ * Attaches a VariablesViewController to a VariablesView if it doesn't already
+ * have one.
+ *
+ * @param VariablesView aView
+ * The view to attach to.
+ * @param object aOptions
+ * The options to use in creating the controller.
+ * @return VariablesViewController
+ */
+VariablesViewController.attach = function (aView, aOptions) {
+ if (aView.controller) {
+ return aView.controller;
+ }
+ return new VariablesViewController(aView, aOptions);
+};
+
+/**
+ * Utility functions for handling stackframes.
+ */
+var StackFrameUtils = this.StackFrameUtils = {
+ /**
+ * Create a textual representation for the specified stack frame
+ * to display in the stackframes container.
+ *
+ * @param object aFrame
+ * The stack frame to label.
+ */
+ getFrameTitle: function (aFrame) {
+ if (aFrame.type == "call") {
+ let c = aFrame.callee;
+ return (c.name || c.userDisplayName || c.displayName || "(anonymous)");
+ }
+ return "(" + aFrame.type + ")";
+ },
+
+ /**
+ * Constructs a scope label based on its environment.
+ *
+ * @param object aEnv
+ * The scope's environment.
+ * @return string
+ * The scope's label.
+ */
+ getScopeLabel: function (aEnv) {
+ let name = "";
+
+ // Name the outermost scope Global.
+ if (!aEnv.parent) {
+ name = L10N.getStr("globalScopeLabel");
+ }
+ // Otherwise construct the scope name.
+ else {
+ name = aEnv.type.charAt(0).toUpperCase() + aEnv.type.slice(1);
+ }
+
+ let label = L10N.getFormatStr("scopeLabel", name);
+ switch (aEnv.type) {
+ case "with":
+ case "object":
+ label += " [" + aEnv.object.class + "]";
+ break;
+ case "function":
+ let f = aEnv.function;
+ label += " [" +
+ (f.name || f.userDisplayName || f.displayName || "(anonymous)") +
+ "]";
+ break;
+ }
+ return label;
+ }
+};
diff --git a/devtools/client/shared/widgets/cubic-bezier.css b/devtools/client/shared/widgets/cubic-bezier.css
new file mode 100644
index 000000000..203fe336a
--- /dev/null
+++ b/devtools/client/shared/widgets/cubic-bezier.css
@@ -0,0 +1,216 @@
+/* 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/. */
+
+/* Based on Lea Verou www.cubic-bezier.com
+ See https://github.com/LeaVerou/cubic-bezier */
+
+.cubic-bezier-container {
+ display: flex;
+ width: 510px;
+ height: 370px;
+ flex-direction: row-reverse;
+ overflow: hidden;
+ padding: 5px;
+ box-sizing: border-box;
+}
+
+.display-wrap {
+ width: 50%;
+ height: 100%;
+ text-align: center;
+ overflow: hidden;
+}
+
+/* Coordinate Plane */
+
+.coordinate-plane {
+ width: 150px;
+ height: 370px;
+ margin: 0 auto;
+ position: relative;
+}
+
+.control-point {
+ position: absolute;
+ z-index: 1;
+ height: 10px;
+ width: 10px;
+ border: 0;
+ background: #666;
+ display: block;
+ margin: -5px 0 0 -5px;
+ outline: none;
+ border-radius: 5px;
+ padding: 0;
+ cursor: pointer;
+}
+
+.display-wrap {
+ background:
+ repeating-linear-gradient(0deg,
+ transparent,
+ var(--bezier-grid-color) 0,
+ var(--bezier-grid-color) 1px,
+ transparent 1px,
+ transparent 15px) no-repeat,
+ repeating-linear-gradient(90deg,
+ transparent,
+ var(--bezier-grid-color) 0,
+ var(--bezier-grid-color) 1px,
+ transparent 1px,
+ transparent 15px) no-repeat;
+ background-size: 100% 100%, 100% 100%;
+ background-position: -2px 5px, -2px 5px;
+
+ -moz-user-select: none;
+}
+
+canvas.curve {
+ background:
+ linear-gradient(-45deg,
+ transparent 49.7%,
+ var(--bezier-diagonal-color) 49.7%,
+ var(--bezier-diagonal-color) 50.3%,
+ transparent 50.3%) center no-repeat;
+ background-size: 100% 100%;
+ background-position: 0 0;
+}
+
+/* Timing Function Preview Widget */
+
+.timing-function-preview {
+ position: absolute;
+ bottom: 20px;
+ right: 45px;
+ width: 150px;
+}
+
+.timing-function-preview .scale {
+ position: absolute;
+ top: 6px;
+ left: 0;
+ z-index: 1;
+
+ width: 150px;
+ height: 1px;
+
+ background: #ccc;
+}
+
+.timing-function-preview .dot {
+ position: absolute;
+ top: 0;
+ left: -7px;
+ z-index: 2;
+
+ width: 10px;
+ height: 10px;
+
+ border-radius: 50%;
+ border: 2px solid white;
+ background: #4C9ED9;
+}
+
+/* Preset Widget */
+
+.preset-pane {
+ width: 50%;
+ height: 100%;
+ border-right: 1px solid var(--theme-splitter-color);
+ padding-right: 4px; /* Visual balance for the panel-arrowcontent border on the left */
+}
+
+#preset-categories {
+ display: flex;
+ width: 95%;
+ border: 1px solid var(--theme-splitter-color);
+ border-radius: 2px;
+ background-color: var(--theme-toolbar-background);
+ margin: 3px auto 0 auto;
+}
+
+#preset-categories .category:last-child {
+ border-right: none;
+}
+
+.category {
+ padding: 5px 0px;
+ width: 33.33%;
+ text-align: center;
+ text-transform: capitalize;
+ border-right: 1px solid var(--theme-splitter-color);
+ cursor: default;
+ color: var(--theme-body-color);
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.category:hover {
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+.active-category {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.active-category:hover {
+ background-color: var(--theme-selection-background);
+}
+
+#preset-container {
+ padding: 0px;
+ width: 100%;
+ height: 331px;
+ overflow-y: auto;
+}
+
+.preset-list {
+ display: none;
+ padding-top: 6px;
+}
+
+.active-preset-list {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+}
+
+.preset {
+ cursor: pointer;
+ width: 33.33%;
+ margin: 5px 0px;
+ text-align: center;
+}
+
+.preset canvas {
+ display: block;
+ border: 1px solid var(--theme-splitter-color);
+ border-radius: 3px;
+ background-color: var(--theme-body-background);
+ margin: 0 auto;
+}
+
+.preset p {
+ font-size: 80%;
+ margin: 2px auto 0px auto;
+ color: var(--theme-body-color-alt);
+ text-transform: capitalize;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.active-preset p, .active-preset:hover p {
+ color: var(--theme-body-color);
+}
+
+.preset:hover canvas {
+ border-color: var(--theme-selection-background);
+}
+
+.active-preset canvas,
+.active-preset:hover canvas {
+ background-color: var(--theme-selection-background-semitransparent);
+ border-color: var(--theme-selection-background);
+}
diff --git a/devtools/client/shared/widgets/filter-widget.css b/devtools/client/shared/widgets/filter-widget.css
new file mode 100644
index 000000000..d015cb5b1
--- /dev/null
+++ b/devtools/client/shared/widgets/filter-widget.css
@@ -0,0 +1,238 @@
+/* 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/. */
+
+/* Main container: Displays the filters and presets in 2 columns */
+
+#filter-container {
+ width: 510px;
+ height: 200px;
+ display: flex;
+ position: relative;
+ padding: 5px;
+ box-sizing: border-box;
+ /* when opened in a xul:panel, a gray color is applied to text */
+ color: var(--theme-body-color);
+}
+
+#filter-container.dragging {
+ -moz-user-select: none;
+}
+
+.filters-list,
+.presets-list {
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+.filters-list {
+ /* Allow the filters list to take the full width when the presets list is
+ hidden */
+ flex-grow: 1;
+ padding: 0 6px;
+}
+
+.presets-list {
+ /* Make sure that when the presets list is shown, it has a fixed width */
+ width: 200px;
+ padding-left: 6px;
+ transition: width .1s;
+ flex-shrink: 0;
+ border-left: 1px solid var(--theme-splitter-color);
+}
+
+#filter-container:not(.show-presets) .presets-list {
+ width: 0;
+ border-left: none;
+ padding-left: 0;
+}
+
+#filter-container.show-presets .filters-list {
+ width: 300px;
+}
+
+/* The list of filters and list of presets should push their footers to the
+ bottom, so they can take as much space as there is */
+
+#filters,
+#presets {
+ flex-grow: 1;
+ /* Avoid pushing below the tooltip's area */
+ overflow-y: auto;
+}
+
+/* The filters and presets list both have footers displayed at the bottom.
+ These footers have some input (taking up as much space as possible) and an
+ add button next */
+
+.footer {
+ display: flex;
+ margin: 10px 3px;
+ align-items: center;
+}
+
+.footer :not(button) {
+ flex-grow: 1;
+ margin-right: 3px;
+}
+
+/* Styles for 1 filter function item */
+
+.filter,
+.filter-name,
+.filter-value {
+ display: flex;
+ align-items: center;
+}
+
+.filter {
+ margin: 5px 0;
+}
+
+.filter-name {
+ width: 120px;
+ margin-right: 10px;
+}
+
+.filter-name label {
+ -moz-user-select: none;
+ flex-grow: 1;
+}
+
+.filter-name label.devtools-draglabel {
+ cursor: ew-resize;
+}
+
+/* drag/drop handle */
+
+.filter-name i {
+ width: 10px;
+ height: 10px;
+ margin-right: 10px;
+ cursor: grab;
+ background: linear-gradient(to bottom,
+ currentColor 0,
+ currentcolor 1px,
+ transparent 1px,
+ transparent 2px);
+ background-repeat: repeat-y;
+ background-size: auto 4px;
+ background-position: 0 1px;
+}
+
+.filter-value {
+ min-width: 150px;
+ margin-right: 10px;
+ flex: 1;
+}
+
+.filter-value input {
+ flex-grow: 1;
+}
+
+/* Fix the size of inputs */
+/* Especially needed on Linux where input are bigger */
+input {
+ width: 8em;
+}
+
+.preset {
+ display: flex;
+ margin-bottom: 10px;
+ cursor: pointer;
+ padding: 3px 5px;
+
+ flex-direction: row;
+ flex-wrap: wrap;
+}
+
+.preset label,
+.preset span {
+ display: flex;
+ align-items: center;
+}
+
+.preset label {
+ flex: 1 0;
+ cursor: pointer;
+ color: var(--theme-body-color);
+}
+
+.preset:hover {
+ background: var(--theme-selection-background);
+}
+
+.preset:hover label, .preset:hover span {
+ color: var(--theme-selection-color);
+}
+
+.preset .remove-button {
+ order: 2;
+}
+
+.preset span {
+ flex: 2 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: block;
+ order: 3;
+ color: var(--theme-body-color-alt);
+}
+
+.remove-button {
+ width: 16px;
+ height: 16px;
+ background: url(chrome://devtools/skin/images/close.svg);
+ background-size: cover;
+ font-size: 0;
+ border: none;
+ cursor: pointer;
+}
+
+.hidden {
+ display: none !important;
+}
+
+#filter-container .dragging {
+ position: relative;
+ z-index: 10;
+ cursor: grab;
+}
+
+/* message shown when there's no filter specified */
+#filter-container p {
+ text-align: center;
+ line-height: 20px;
+}
+
+.add,
+#toggle-presets {
+ background-size: cover;
+ border: none;
+ width: 16px;
+ height: 16px;
+ font-size: 0;
+ vertical-align: middle;
+ cursor: pointer;
+ margin: 0 5px;
+}
+
+.add {
+ background: url(chrome://devtools/skin/images/add.svg);
+}
+
+#toggle-presets {
+ background: url(chrome://devtools/skin/images/pseudo-class.svg);
+}
+
+.add,
+.remove-button,
+#toggle-presets {
+ filter: var(--icon-filter);
+}
+
+.show-presets #toggle-presets {
+ filter: url(chrome://devtools/skin/images/filters.svg#checked-icon-state);
+}
diff --git a/devtools/client/shared/widgets/graphs-frame.xhtml b/devtools/client/shared/widgets/graphs-frame.xhtml
new file mode 100644
index 000000000..8c6f45e03
--- /dev/null
+++ b/devtools/client/shared/widgets/graphs-frame.xhtml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/widgets.css" ype="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
+ <style>
+ body {
+ overflow: hidden;
+ margin: 0;
+ padding: 0;
+ font-size: 0;
+ }
+ </style>
+</head>
+<body role="application">
+ <div id="graph-container">
+ <canvas id="graph-canvas"></canvas>
+ </div>
+</body>
+</html>
diff --git a/devtools/client/shared/widgets/mdn-docs.css b/devtools/client/shared/widgets/mdn-docs.css
new file mode 100644
index 000000000..e3547489f
--- /dev/null
+++ b/devtools/client/shared/widgets/mdn-docs.css
@@ -0,0 +1,39 @@
+/* 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/. */
+
+.mdn-container {
+ height: 300px;
+ margin: 4px;
+ overflow: auto;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+}
+
+.mdn-container header,
+.mdn-container footer {
+ flex: 1;
+ padding: 0 1em;
+}
+
+.mdn-property-info {
+ flex: 10;
+ padding: 0 1em;
+ overflow: auto;
+ transition: opacity 400ms ease-in;
+}
+
+.mdn-syntax {
+ margin-top: 1em;
+}
+
+.devtools-throbber {
+ align-self: center;
+ opacity: 0;
+}
+
+.mdn-visit-page {
+ display: inline-block;
+ padding: 1em 0;
+}
diff --git a/devtools/client/shared/widgets/moz.build b/devtools/client/shared/widgets/moz.build
new file mode 100644
index 000000000..5a28d21ca
--- /dev/null
+++ b/devtools/client/shared/widgets/moz.build
@@ -0,0 +1,34 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ 'tooltip',
+]
+
+DevToolsModules(
+ 'AbstractTreeItem.jsm',
+ 'BarGraphWidget.js',
+ 'BreadcrumbsWidget.jsm',
+ 'Chart.jsm',
+ 'CubicBezierPresets.js',
+ 'CubicBezierWidget.js',
+ 'FastListWidget.js',
+ 'FilterWidget.js',
+ 'FlameGraph.js',
+ 'Graphs.js',
+ 'GraphsWorker.js',
+ 'LineGraphWidget.js',
+ 'MdnDocsWidget.js',
+ 'MountainGraphWidget.js',
+ 'SideMenuWidget.jsm',
+ 'SimpleListWidget.jsm',
+ 'Spectrum.js',
+ 'TableWidget.js',
+ 'TreeWidget.js',
+ 'VariablesView.jsm',
+ 'VariablesViewController.jsm',
+ 'view-helpers.js',
+)
diff --git a/devtools/client/shared/widgets/spectrum.css b/devtools/client/shared/widgets/spectrum.css
new file mode 100644
index 000000000..46826f2e1
--- /dev/null
+++ b/devtools/client/shared/widgets/spectrum.css
@@ -0,0 +1,155 @@
+/* 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/. */
+
+#eyedropper-button {
+ margin-inline-start: 5px;
+ display: block;
+}
+
+#eyedropper-button::before {
+ background-image: url(chrome://devtools/skin/images/command-eyedropper.svg);
+}
+
+/* Mix-in classes */
+
+.spectrum-checker {
+ background-color: #eee;
+ background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
+ linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
+ background-size: 12px 12px;
+ background-position: 0 0, 6px 6px;
+}
+
+.spectrum-slider-control {
+ cursor: pointer;
+ box-shadow: 0 0 2px rgba(0,0,0,.6);
+ background: #fff;
+ border-radius: 10px;
+ opacity: .8;
+}
+
+.spectrum-box {
+ border: 1px solid rgba(0,0,0,0.2);
+ border-radius: 2px;
+ background-clip: content-box;
+}
+
+/* Elements */
+
+#spectrum-tooltip {
+ padding: 4px;
+}
+
+.spectrum-container {
+ position: relative;
+ display: none;
+ top: 0;
+ left: 0;
+ border-radius: 0;
+ width: 200px;
+ padding: 5px;
+}
+
+.spectrum-show {
+ display: inline-block;
+}
+
+/* Keep aspect ratio:
+http://www.briangrinstead.com/blog/keep-aspect-ratio-with-html-and-css */
+.spectrum-top {
+ position: relative;
+ width: 100%;
+ display: inline-block;
+}
+
+.spectrum-top-inner {
+ position: absolute;
+ top:0;
+ left:0;
+ bottom:0;
+ right:0;
+}
+
+.spectrum-color {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 20%;
+}
+
+.spectrum-hue {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 83%;
+}
+
+.spectrum-fill {
+ /* Same as spectrum-color width */
+ margin-top: 85%;
+}
+
+.spectrum-sat, .spectrum-val {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+.spectrum-dragger, .spectrum-slider {
+ -moz-user-select: none;
+}
+
+.spectrum-alpha {
+ position: relative;
+ height: 8px;
+ margin-top: 3px;
+}
+
+.spectrum-alpha-inner {
+ height: 100%;
+}
+
+.spectrum-alpha-handle {
+ position: absolute;
+ top: -3px;
+ bottom: -3px;
+ width: 5px;
+ left: 50%;
+}
+
+.spectrum-sat {
+ background-image: linear-gradient(to right, #FFF, rgba(204, 154, 129, 0));
+}
+
+.spectrum-val {
+ background-image: linear-gradient(to top, #000000, rgba(204, 154, 129, 0));
+}
+
+.spectrum-hue {
+ background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+}
+
+.spectrum-dragger {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ cursor: pointer;
+ border-radius: 50%;
+ height: 8px;
+ width: 8px;
+ border: 1px solid white;
+ box-shadow: 0 0 2px rgba(0,0,0,.6);
+}
+
+.spectrum-slider {
+ position: absolute;
+ top: 0;
+ height: 5px;
+ left: -3px;
+ right: -3px;
+}
diff --git a/devtools/client/shared/widgets/tooltip/CssDocsTooltip.js b/devtools/client/shared/widgets/tooltip/CssDocsTooltip.js
new file mode 100644
index 000000000..880c34de3
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/CssDocsTooltip.js
@@ -0,0 +1,93 @@
+/* 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 {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const {MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const TOOLTIP_WIDTH = 418;
+const TOOLTIP_HEIGHT = 308;
+
+/**
+ * Tooltip for displaying docs for CSS properties from MDN.
+ *
+ * @param {Document} toolboxDoc
+ * The toolbox document to attach the CSS docs tooltip.
+ */
+function CssDocsTooltip(toolboxDoc) {
+ this.tooltip = new HTMLTooltip(toolboxDoc, {
+ type: "arrow",
+ consumeOutsideClicks: true,
+ autofocus: true,
+ useXulWrapper: true,
+ stylesheet: "chrome://devtools/content/shared/widgets/mdn-docs.css",
+ });
+ this.widget = this.setMdnDocsContent();
+ this._onVisitLink = this._onVisitLink.bind(this);
+ this.widget.on("visitlink", this._onVisitLink);
+
+ // Initialize keyboard shortcuts
+ this.shortcuts = new KeyShortcuts({ window: this.tooltip.topWindow });
+ this._onShortcut = this._onShortcut.bind(this);
+
+ this.shortcuts.on("Escape", this._onShortcut);
+}
+
+CssDocsTooltip.prototype = {
+ /**
+ * Load CSS docs for the given property,
+ * then display the tooltip.
+ */
+ show: function (anchor, propertyName) {
+ this.tooltip.once("shown", () => {
+ this.widget.loadCssDocs(propertyName);
+ });
+ this.tooltip.show(anchor);
+ },
+
+ hide: function () {
+ this.tooltip.hide();
+ },
+
+ _onShortcut: function (shortcut, event) {
+ if (!this.tooltip.isVisible()) {
+ return;
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ this.hide();
+ },
+
+ _onVisitLink: function () {
+ this.hide();
+ },
+
+ /**
+ * Set the content of this tooltip to the MDN docs widget. This is called when the
+ * tooltip is first constructed.
+ * The caller can use the MdnDocsWidget to update the tooltip's UI with new content
+ * each time the tooltip is shown.
+ *
+ * @return {MdnDocsWidget} the created MdnDocsWidget instance.
+ */
+ setMdnDocsContent: function () {
+ let container = this.tooltip.doc.createElementNS(XHTML_NS, "div");
+ container.setAttribute("class", "mdn-container theme-body");
+ this.tooltip.setContent(container, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+ return new MdnDocsWidget(container);
+ },
+
+ destroy: function () {
+ this.widget.off("visitlink", this._onVisitLink);
+ this.widget.destroy();
+
+ this.shortcuts.destroy();
+ this.tooltip.destroy();
+ }
+};
+
+module.exports = CssDocsTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js b/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js
new file mode 100644
index 000000000..63507bc5e
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js
@@ -0,0 +1,313 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+const Editor = require("devtools/client/sourceeditor/editor");
+const beautify = require("devtools/shared/jsbeautify/beautify");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const CONTAINER_WIDTH = 500;
+
+/**
+ * Set the content of a provided HTMLTooltip instance to display a list of event
+ * listeners, with their event type, capturing argument and a link to the code
+ * of the event handler.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The tooltip instance on which the event details content should be set
+ * @param {Array} eventListenerInfos
+ * A list of event listeners
+ * @param {Toolbox} toolbox
+ * Toolbox used to select debugger panel
+ */
+function setEventTooltip(tooltip, eventListenerInfos, toolbox) {
+ let eventTooltip = new EventTooltip(tooltip, eventListenerInfos, toolbox);
+ eventTooltip.init();
+}
+
+function EventTooltip(tooltip, eventListenerInfos, toolbox) {
+ this._tooltip = tooltip;
+ this._eventListenerInfos = eventListenerInfos;
+ this._toolbox = toolbox;
+ this._eventEditors = new WeakMap();
+
+ // Used in tests: add a reference to the EventTooltip instance on the HTMLTooltip.
+ this._tooltip.eventTooltip = this;
+
+ this._headerClicked = this._headerClicked.bind(this);
+ this._debugClicked = this._debugClicked.bind(this);
+ this.destroy = this.destroy.bind(this);
+}
+
+EventTooltip.prototype = {
+ init: function () {
+ let config = {
+ mode: Editor.modes.js,
+ lineNumbers: false,
+ lineWrapping: true,
+ readOnly: true,
+ styleActiveLine: true,
+ extraKeys: {},
+ theme: "mozilla markup-view"
+ };
+
+ let doc = this._tooltip.doc;
+ this.container = doc.createElementNS(XHTML_NS, "div");
+ this.container.className = "devtools-tooltip-events-container";
+
+ for (let listener of this._eventListenerInfos) {
+ let phase = listener.capturing ? "Capturing" : "Bubbling";
+ let level = listener.DOM0 ? "DOM0" : "DOM2";
+
+ // Header
+ let header = doc.createElementNS(XHTML_NS, "div");
+ header.className = "event-header devtools-toolbar";
+ this.container.appendChild(header);
+
+ if (!listener.hide.debugger) {
+ let debuggerIcon = doc.createElementNS(XHTML_NS, "img");
+ debuggerIcon.className = "event-tooltip-debugger-icon";
+ debuggerIcon.setAttribute("src",
+ "chrome://devtools/skin/images/tool-debugger.svg");
+ let openInDebugger = L10N.getStr("eventsTooltip.openInDebugger");
+ debuggerIcon.setAttribute("title", openInDebugger);
+ header.appendChild(debuggerIcon);
+ }
+
+ if (!listener.hide.type) {
+ let eventTypeLabel = doc.createElementNS(XHTML_NS, "span");
+ eventTypeLabel.className = "event-tooltip-event-type";
+ eventTypeLabel.textContent = listener.type;
+ eventTypeLabel.setAttribute("title", listener.type);
+ header.appendChild(eventTypeLabel);
+ }
+
+ if (!listener.hide.filename) {
+ let filename = doc.createElementNS(XHTML_NS, "span");
+ filename.className = "event-tooltip-filename devtools-monospace";
+ filename.textContent = listener.origin;
+ filename.setAttribute("title", listener.origin);
+ header.appendChild(filename);
+ }
+
+ let attributesContainer = doc.createElementNS(XHTML_NS, "div");
+ attributesContainer.className = "event-tooltip-attributes-container";
+ header.appendChild(attributesContainer);
+
+ if (!listener.hide.capturing) {
+ let attributesBox = doc.createElementNS(XHTML_NS, "div");
+ attributesBox.className = "event-tooltip-attributes-box";
+ attributesContainer.appendChild(attributesBox);
+
+ let capturing = doc.createElementNS(XHTML_NS, "span");
+ capturing.className = "event-tooltip-attributes";
+ capturing.textContent = phase;
+ capturing.setAttribute("title", phase);
+ attributesBox.appendChild(capturing);
+ }
+
+ if (listener.tags) {
+ for (let tag of listener.tags.split(",")) {
+ let attributesBox = doc.createElementNS(XHTML_NS, "div");
+ attributesBox.className = "event-tooltip-attributes-box";
+ attributesContainer.appendChild(attributesBox);
+
+ let tagBox = doc.createElementNS(XHTML_NS, "span");
+ tagBox.className = "event-tooltip-attributes";
+ tagBox.textContent = tag;
+ tagBox.setAttribute("title", tag);
+ attributesBox.appendChild(tagBox);
+ }
+ }
+
+ if (!listener.hide.dom0) {
+ let attributesBox = doc.createElementNS(XHTML_NS, "div");
+ attributesBox.className = "event-tooltip-attributes-box";
+ attributesContainer.appendChild(attributesBox);
+
+ let dom0 = doc.createElementNS(XHTML_NS, "span");
+ dom0.className = "event-tooltip-attributes";
+ dom0.textContent = level;
+ dom0.setAttribute("title", level);
+ attributesBox.appendChild(dom0);
+ }
+
+ // Content
+ let content = doc.createElementNS(XHTML_NS, "div");
+ let editor = new Editor(config);
+ this._eventEditors.set(content, {
+ editor: editor,
+ handler: listener.handler,
+ searchString: listener.searchString,
+ uri: listener.origin,
+ dom0: listener.DOM0,
+ appended: false
+ });
+
+ content.className = "event-tooltip-content-box";
+ this.container.appendChild(content);
+
+ this._addContentListeners(header);
+ }
+
+ this._tooltip.setContent(this.container, {width: CONTAINER_WIDTH});
+ this._tooltip.on("hidden", this.destroy);
+ },
+
+ _addContentListeners: function (header) {
+ header.addEventListener("click", this._headerClicked);
+ },
+
+ _headerClicked: function (event) {
+ if (event.target.classList.contains("event-tooltip-debugger-icon")) {
+ this._debugClicked(event);
+ event.stopPropagation();
+ return;
+ }
+
+ let doc = this._tooltip.doc;
+ let header = event.currentTarget;
+ let content = header.nextElementSibling;
+
+ if (content.hasAttribute("open")) {
+ content.removeAttribute("open");
+ } else {
+ let contentNodes = doc.querySelectorAll(".event-tooltip-content-box");
+
+ for (let node of contentNodes) {
+ if (node !== content) {
+ node.removeAttribute("open");
+ }
+ }
+
+ content.setAttribute("open", "");
+
+ let eventEditor = this._eventEditors.get(content);
+
+ if (eventEditor.appended) {
+ return;
+ }
+
+ let {editor, handler} = eventEditor;
+
+ let iframe = doc.createElementNS(XHTML_NS, "iframe");
+ iframe.setAttribute("style", "width: 100%; height: 100%; border-style: none;");
+
+ editor.appendTo(content, iframe).then(() => {
+ let tidied = beautify.js(handler, { "indent_size": 2 });
+ editor.setText(tidied);
+
+ eventEditor.appended = true;
+
+ let container = header.parentElement.getBoundingClientRect();
+ if (header.getBoundingClientRect().top < container.top) {
+ header.scrollIntoView(true);
+ } else if (content.getBoundingClientRect().bottom > container.bottom) {
+ content.scrollIntoView(false);
+ }
+
+ this._tooltip.emit("event-tooltip-ready");
+ });
+ }
+ },
+
+ _debugClicked: function (event) {
+ let header = event.currentTarget;
+ let content = header.nextElementSibling;
+
+ let {uri, searchString, dom0} = this._eventEditors.get(content);
+
+ if (uri && uri !== "?") {
+ // Save a copy of toolbox as it will be set to null when we hide the tooltip.
+ let toolbox = this._toolbox;
+
+ this._tooltip.hide();
+
+ uri = uri.replace(/"/g, "");
+
+ let showSource = ({ DebuggerView }) => {
+ let matches = uri.match(/(.*):(\d+$)/);
+ let line = 1;
+
+ if (matches) {
+ uri = matches[1];
+ line = matches[2];
+ }
+
+ let item = DebuggerView.Sources.getItemForAttachment(a => a.source.url === uri);
+ if (item) {
+ let actor = item.attachment.source.actor;
+ DebuggerView.setEditorLocation(
+ actor, line, {noDebug: true}
+ ).then(() => {
+ if (dom0) {
+ let text = DebuggerView.editor.getText();
+ let index = text.indexOf(searchString);
+ let lastIndex = text.lastIndexOf(searchString);
+
+ // To avoid confusion we only search for DOM0 event handlers when
+ // there is only one possible match in the file.
+ if (index !== -1 && index === lastIndex) {
+ text = text.substr(0, index);
+ let newlineMatches = text.match(/\n/g);
+
+ if (newlineMatches) {
+ DebuggerView.editor.setCursor({
+ line: newlineMatches.length
+ });
+ }
+ }
+ }
+ });
+ }
+ };
+
+ let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
+ toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
+ if (debuggerAlreadyOpen) {
+ showSource(dbg);
+ } else {
+ dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
+ }
+ });
+ }
+ },
+
+ destroy: function () {
+ if (this._tooltip) {
+ this._tooltip.off("hidden", this.destroy);
+
+ let boxes = this.container.querySelectorAll(".event-tooltip-content-box");
+
+ for (let box of boxes) {
+ let {editor} = this._eventEditors.get(box);
+ editor.destroy();
+ }
+
+ this._eventEditors = null;
+ this._tooltip.eventTooltip = null;
+ }
+
+ let headerNodes = this.container.querySelectorAll(".event-header");
+
+ for (let node of headerNodes) {
+ node.removeEventListener("click", this._headerClicked);
+ }
+
+ let sourceNodes = this.container.querySelectorAll(".event-tooltip-debugger-icon");
+ for (let node of sourceNodes) {
+ node.removeEventListener("click", this._debugClicked);
+ }
+
+ this._eventListenerInfos = this._toolbox = this._tooltip = null;
+ }
+};
+
+module.exports.setEventTooltip = setEventTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/HTMLTooltip.js b/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
new file mode 100644
index 000000000..749878220
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
@@ -0,0 +1,638 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
+const {listenOnce} = require("devtools/shared/async-utils");
+const {Task} = require("devtools/shared/task");
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const POSITION = {
+ TOP: "top",
+ BOTTOM: "bottom",
+};
+
+module.exports.POSITION = POSITION;
+
+const TYPE = {
+ NORMAL: "normal",
+ ARROW: "arrow",
+};
+
+module.exports.TYPE = TYPE;
+
+const ARROW_WIDTH = 32;
+
+// Default offset between the tooltip's left edge and the tooltip arrow.
+const ARROW_OFFSET = 20;
+
+const EXTRA_HEIGHT = {
+ "normal": 0,
+ // The arrow is 16px tall, but merges on 3px with the panel border
+ "arrow": 13,
+};
+
+const EXTRA_BORDER = {
+ "normal": 0,
+ "arrow": 3,
+};
+
+/**
+ * Calculate the vertical position & offsets to use for the tooltip. Will attempt to
+ * respect the provided height and position preferences, unless the available height
+ * prevents this.
+ *
+ * @param {DOMRect} anchorRect
+ * Bounding rectangle for the anchor, relative to the tooltip document.
+ * @param {DOMRect} viewportRect
+ * Bounding rectangle for the viewport. top/left can be different from 0 if some
+ * space should not be used by tooltips (for instance OS toolbars, taskbars etc.).
+ * @param {Number} height
+ * Preferred height for the tooltip.
+ * @param {String} pos
+ * Preferred position for the tooltip. Possible values: "top" or "bottom".
+ * @return {Object}
+ * - {Number} top: the top offset for the tooltip.
+ * - {Number} height: the height to use for the tooltip container.
+ * - {String} computedPosition: Can differ from the preferred position depending
+ * on the available height). "top" or "bottom"
+ */
+const calculateVerticalPosition =
+function (anchorRect, viewportRect, height, pos, offset) {
+ let {TOP, BOTTOM} = POSITION;
+
+ let {top: anchorTop, height: anchorHeight} = anchorRect;
+
+ // Translate to the available viewport space before calculating dimensions and position.
+ anchorTop -= viewportRect.top;
+
+ // Calculate available space for the tooltip.
+ let availableTop = anchorTop;
+ let availableBottom = viewportRect.height - (anchorTop + anchorHeight);
+
+ // Find POSITION
+ let keepPosition = false;
+ if (pos === TOP) {
+ keepPosition = availableTop >= height + offset;
+ } else if (pos === BOTTOM) {
+ keepPosition = availableBottom >= height + offset;
+ }
+ if (!keepPosition) {
+ pos = availableTop > availableBottom ? TOP : BOTTOM;
+ }
+
+ // Calculate HEIGHT.
+ let availableHeight = pos === TOP ? availableTop : availableBottom;
+ height = Math.min(height, availableHeight - offset);
+ height = Math.floor(height);
+
+ // Calculate TOP.
+ let top = pos === TOP ? anchorTop - height - offset : anchorTop + anchorHeight + offset;
+
+ // Translate back to absolute coordinates by re-including viewport top margin.
+ top += viewportRect.top;
+
+ return {top, height, computedPosition: pos};
+};
+
+/**
+ * Calculate the vertical position & offsets to use for the tooltip. Will attempt to
+ * respect the provided height and position preferences, unless the available height
+ * prevents this.
+ *
+ * @param {DOMRect} anchorRect
+ * Bounding rectangle for the anchor, relative to the tooltip document.
+ * @param {DOMRect} viewportRect
+ * Bounding rectangle for the viewport. top/left can be different from 0 if some
+ * space should not be used by tooltips (for instance OS toolbars, taskbars etc.).
+ * @param {Number} width
+ * Preferred width for the tooltip.
+ * @param {String} type
+ * The tooltip type (e.g. "arrow").
+ * @param {Number} offset
+ * Horizontal offset in pixels.
+ * @param {Boolean} isRtl
+ * If the anchor is in RTL, the tooltip should be aligned to the right.
+ * @return {Object}
+ * - {Number} left: the left offset for the tooltip.
+ * - {Number} width: the width to use for the tooltip container.
+ * - {Number} arrowLeft: the left offset to use for the arrow element.
+ */
+const calculateHorizontalPosition =
+function (anchorRect, viewportRect, width, type, offset, isRtl) {
+ let anchorWidth = anchorRect.width;
+ let anchorStart = isRtl ? anchorRect.right : anchorRect.left;
+
+ // Translate to the available viewport space before calculating dimensions and position.
+ anchorStart -= viewportRect.left;
+
+ // Calculate WIDTH.
+ width = Math.min(width, viewportRect.width);
+
+ // Calculate LEFT.
+ // By default the tooltip is aligned with the anchor left edge. Unless this
+ // makes it overflow the viewport, in which case is shifts to the left.
+ let left = anchorStart + offset - (isRtl ? width : 0);
+ left = Math.min(left, viewportRect.width - width);
+ left = Math.max(0, left);
+
+ // Calculate ARROW LEFT (tooltip's LEFT might be updated)
+ let arrowLeft;
+ // Arrow style tooltips may need to be shifted to the left
+ if (type === TYPE.ARROW) {
+ let arrowCenter = left + ARROW_OFFSET + ARROW_WIDTH / 2;
+ let anchorCenter = anchorStart + anchorWidth / 2;
+ // If the anchor is too narrow, align the arrow and the anchor center.
+ if (arrowCenter > anchorCenter) {
+ left = Math.max(0, left - (arrowCenter - anchorCenter));
+ }
+ // Arrow's left offset relative to the anchor.
+ arrowLeft = Math.min(ARROW_OFFSET, (anchorWidth - ARROW_WIDTH) / 2) | 0;
+ // Translate the coordinate to tooltip container
+ arrowLeft += anchorStart - left;
+ // Make sure the arrow remains in the tooltip container.
+ arrowLeft = Math.min(arrowLeft, width - ARROW_WIDTH);
+ arrowLeft = Math.max(arrowLeft, 0);
+ }
+
+ // Translate back to absolute coordinates by re-including viewport left margin.
+ left += viewportRect.left;
+
+ return {left, width, arrowLeft};
+};
+
+/**
+ * Get the bounding client rectangle for a given node, relative to a custom
+ * reference element (instead of the default for getBoundingClientRect which
+ * is always the element's ownerDocument).
+ */
+const getRelativeRect = function (node, relativeTo) {
+ // Width and Height can be taken from the rect.
+ let {width, height} = node.getBoundingClientRect();
+
+ let quads = node.getBoxQuads({relativeTo});
+ let top = quads[0].bounds.top;
+ let left = quads[0].bounds.left;
+
+ // Compute right and bottom coordinates using the rest of the data.
+ let right = left + width;
+ let bottom = top + height;
+
+ return {top, right, bottom, left, width, height};
+};
+
+/**
+ * The HTMLTooltip can display HTML content in a tooltip popup.
+ *
+ * @param {Document} toolboxDoc
+ * The toolbox document to attach the HTMLTooltip popup.
+ * @param {Object}
+ * - {String} type
+ * Display type of the tooltip. Possible values: "normal", "arrow"
+ * - {Boolean} autofocus
+ * Defaults to false. Should the tooltip be focused when opening it.
+ * - {Boolean} consumeOutsideClicks
+ * Defaults to true. The tooltip is closed when clicking outside.
+ * Should this event be stopped and consumed or not.
+ * - {Boolean} useXulWrapper
+ * Defaults to false. If the tooltip is hosted in a XUL document, use a XUL panel
+ * in order to use all the screen viewport available.
+ * - {String} stylesheet
+ * Style sheet URL to apply to the tooltip content.
+ */
+function HTMLTooltip(toolboxDoc, {
+ type = "normal",
+ autofocus = false,
+ consumeOutsideClicks = true,
+ useXulWrapper = false,
+ stylesheet = "",
+ } = {}) {
+ EventEmitter.decorate(this);
+
+ this.doc = toolboxDoc;
+ this.type = type;
+ this.autofocus = autofocus;
+ this.consumeOutsideClicks = consumeOutsideClicks;
+ this.useXulWrapper = this._isXUL() && useXulWrapper;
+
+ // The top window is used to attach click event listeners to close the tooltip if the
+ // user clicks on the content page.
+ this.topWindow = this._getTopWindow();
+
+ this._position = null;
+
+ this._onClick = this._onClick.bind(this);
+ this._onXulPanelHidden = this._onXulPanelHidden.bind(this);
+
+ this._toggle = new TooltipToggle(this);
+ this.startTogglingOnHover = this._toggle.start.bind(this._toggle);
+ this.stopTogglingOnHover = this._toggle.stop.bind(this._toggle);
+
+ this.container = this._createContainer();
+
+ if (stylesheet) {
+ this._applyStylesheet(stylesheet);
+ }
+ if (this.useXulWrapper) {
+ // When using a XUL panel as the wrapper, the actual markup for the tooltip is as
+ // follows :
+ // <panel> <!-- XUL panel used to position the tooltip anywhere on screen -->
+ // <div> <!-- div wrapper used to isolate the tooltip container -->
+ // <div> <! the actual tooltip.container element -->
+ this.xulPanelWrapper = this._createXulPanelWrapper();
+ let inner = this.doc.createElementNS(XHTML_NS, "div");
+ inner.classList.add("tooltip-xul-wrapper-inner");
+
+ this.doc.documentElement.appendChild(this.xulPanelWrapper);
+ this.xulPanelWrapper.appendChild(inner);
+ inner.appendChild(this.container);
+ } else if (this._isXUL()) {
+ this.doc.documentElement.appendChild(this.container);
+ } else {
+ // In non-XUL context the container is ready to use as is.
+ this.doc.body.appendChild(this.container);
+ }
+}
+
+module.exports.HTMLTooltip = HTMLTooltip;
+
+HTMLTooltip.prototype = {
+ /**
+ * The tooltip panel is the parentNode of the tooltip content provided in
+ * setContent().
+ */
+ get panel() {
+ return this.container.querySelector(".tooltip-panel");
+ },
+
+ /**
+ * The arrow element. Might be null depending on the tooltip type.
+ */
+ get arrow() {
+ return this.container.querySelector(".tooltip-arrow");
+ },
+
+ /**
+ * Retrieve the displayed position used for the tooltip. Null if the tooltip is hidden.
+ */
+ get position() {
+ return this.isVisible() ? this._position : null;
+ },
+
+ /**
+ * Set the tooltip content element. The preferred width/height should also be
+ * specified here.
+ *
+ * @param {Element} content
+ * The tooltip content, should be a HTML element.
+ * @param {Object}
+ * - {Number} width: preferred width for the tooltip container. If not specified
+ * the tooltip container will be measured before being displayed, and the
+ * measured width will be used as preferred width.
+ * - {Number} height: optional, preferred height for the tooltip container. If
+ * not specified, the tooltip will be able to use all the height available.
+ */
+ setContent: function (content, {width = "auto", height = Infinity} = {}) {
+ this.preferredWidth = width;
+ this.preferredHeight = height;
+
+ this.panel.innerHTML = "";
+ this.panel.appendChild(content);
+ },
+
+ /**
+ * Show the tooltip next to the provided anchor element. A preferred position
+ * can be set. The event "shown" will be fired after the tooltip is displayed.
+ *
+ * @param {Element} anchor
+ * The reference element with which the tooltip should be aligned
+ * @param {Object}
+ * - {String} position: optional, possible values: top|bottom
+ * If layout permits, the tooltip will be displayed on top/bottom
+ * of the anchor. If ommitted, the tooltip will be displayed where
+ * more space is available.
+ * - {Number} x: optional, horizontal offset between the anchor and the tooltip
+ * - {Number} y: optional, vertical offset between the anchor and the tooltip
+ */
+ show: Task.async(function* (anchor, {position, x = 0, y = 0} = {}) {
+ // Get anchor geometry
+ let anchorRect = getRelativeRect(anchor, this.doc);
+ if (this.useXulWrapper) {
+ anchorRect = this._convertToScreenRect(anchorRect);
+ }
+
+ // Get viewport size
+ let viewportRect = this._getViewportRect();
+
+ let themeHeight = EXTRA_HEIGHT[this.type] + 2 * EXTRA_BORDER[this.type];
+ let preferredHeight = this.preferredHeight + themeHeight;
+
+ let {top, height, computedPosition} =
+ calculateVerticalPosition(anchorRect, viewportRect, preferredHeight, position, y);
+
+ this._position = computedPosition;
+ // Apply height before measuring the content width (if width="auto").
+ let isTop = computedPosition === POSITION.TOP;
+ this.container.classList.toggle("tooltip-top", isTop);
+ this.container.classList.toggle("tooltip-bottom", !isTop);
+
+ // If the preferred height is set to Infinity, the tooltip container should grow based
+ // on its content's height and use as much height as possible.
+ this.container.classList.toggle("tooltip-flexible-height",
+ this.preferredHeight === Infinity);
+
+ this.container.style.height = height + "px";
+
+ let preferredWidth;
+ if (this.preferredWidth === "auto") {
+ preferredWidth = this._measureContainerWidth();
+ } else {
+ let themeWidth = 2 * EXTRA_BORDER[this.type];
+ preferredWidth = this.preferredWidth + themeWidth;
+ }
+
+ let anchorWin = anchor.ownerDocument.defaultView;
+ let isRtl = anchorWin.getComputedStyle(anchor).direction === "rtl";
+ let {left, width, arrowLeft} = calculateHorizontalPosition(
+ anchorRect, viewportRect, preferredWidth, this.type, x, isRtl);
+
+ this.container.style.width = width + "px";
+
+ if (this.type === TYPE.ARROW) {
+ this.arrow.style.left = arrowLeft + "px";
+ }
+
+ if (this.useXulWrapper) {
+ yield this._showXulWrapperAt(left, top);
+ } else {
+ this.container.style.left = left + "px";
+ this.container.style.top = top + "px";
+ }
+
+ this.container.classList.add("tooltip-visible");
+
+ // Keep a pointer on the focused element to refocus it when hiding the tooltip.
+ this._focusedElement = this.doc.activeElement;
+
+ this.doc.defaultView.clearTimeout(this.attachEventsTimer);
+ this.attachEventsTimer = this.doc.defaultView.setTimeout(() => {
+ this._maybeFocusTooltip();
+ // Updated the top window reference each time in case the host changes.
+ this.topWindow = this._getTopWindow();
+ this.topWindow.addEventListener("click", this._onClick, true);
+ this.emit("shown");
+ }, 0);
+ }),
+
+ /**
+ * Calculate the rect of the viewport that limits the tooltip dimensions. When using a
+ * XUL panel wrapper, the viewport will be able to use the whole screen (excluding space
+ * reserved by the OS for toolbars etc.). Otherwise, the viewport is limited to the
+ * tooltip's document.
+ *
+ * @return {Object} DOMRect-like object with the Number properties: top, right, bottom,
+ * left, width, height
+ */
+ _getViewportRect: function () {
+ if (this.useXulWrapper) {
+ // availLeft/Top are the coordinates first pixel available on the screen for
+ // applications (excluding space dedicated for OS toolbars, menus etc...)
+ // availWidth/Height are the dimensions available to applications excluding all
+ // the OS reserved space
+ let {availLeft, availTop, availHeight, availWidth} = this.doc.defaultView.screen;
+ return {
+ top: availTop,
+ right: availLeft + availWidth,
+ bottom: availTop + availHeight,
+ left: availLeft,
+ width: availWidth,
+ height: availHeight,
+ };
+ }
+
+ return this.doc.documentElement.getBoundingClientRect();
+ },
+
+ _measureContainerWidth: function () {
+ let xulParent = this.container.parentNode;
+ if (this.useXulWrapper && !this.isVisible()) {
+ // Move the container out of the XUL Panel to measure it.
+ this.doc.documentElement.appendChild(this.container);
+ }
+
+ this.container.classList.add("tooltip-hidden");
+ this.container.style.width = "auto";
+ let width = this.container.getBoundingClientRect().width;
+ this.container.classList.remove("tooltip-hidden");
+
+ if (this.useXulWrapper && !this.isVisible()) {
+ xulParent.appendChild(this.container);
+ }
+
+ return width;
+ },
+
+ /**
+ * Hide the current tooltip. The event "hidden" will be fired when the tooltip
+ * is hidden.
+ */
+ hide: Task.async(function* () {
+ this.doc.defaultView.clearTimeout(this.attachEventsTimer);
+ if (!this.isVisible()) {
+ this.emit("hidden");
+ return;
+ }
+
+ this.topWindow.removeEventListener("click", this._onClick, true);
+ this.container.classList.remove("tooltip-visible");
+ if (this.useXulWrapper) {
+ yield this._hideXulWrapper();
+ }
+
+ this.emit("hidden");
+
+ let tooltipHasFocus = this.container.contains(this.doc.activeElement);
+ if (tooltipHasFocus && this._focusedElement) {
+ this._focusedElement.focus();
+ this._focusedElement = null;
+ }
+ }),
+
+ /**
+ * Check if the tooltip is currently displayed.
+ * @return {Boolean} true if the tooltip is visible
+ */
+ isVisible: function () {
+ return this.container.classList.contains("tooltip-visible");
+ },
+
+ /**
+ * Destroy the tooltip instance. Hide the tooltip if displayed, remove the
+ * tooltip container from the document.
+ */
+ destroy: function () {
+ this.hide();
+ this.container.remove();
+ if (this.xulPanelWrapper) {
+ this.xulPanelWrapper.remove();
+ }
+ },
+
+ _createContainer: function () {
+ let container = this.doc.createElementNS(XHTML_NS, "div");
+ container.setAttribute("type", this.type);
+ container.classList.add("tooltip-container");
+
+ let html = '<div class="tooltip-filler"></div>';
+ html += '<div class="tooltip-panel"></div>';
+
+ if (this.type === TYPE.ARROW) {
+ html += '<div class="tooltip-arrow"></div>';
+ }
+ container.innerHTML = html;
+ return container;
+ },
+
+ _onClick: function (e) {
+ if (this._isInTooltipContainer(e.target)) {
+ return;
+ }
+
+ this.hide();
+ if (this.consumeOutsideClicks && e.button === 0) {
+ // Consume only left click events (button === 0).
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ },
+
+ _isInTooltipContainer: function (node) {
+ // Check if the target is the tooltip arrow.
+ if (this.arrow && this.arrow === node) {
+ return true;
+ }
+
+ let tooltipWindow = this.panel.ownerDocument.defaultView;
+ let win = node.ownerDocument.defaultView;
+
+ // Check if the tooltip panel contains the node if they live in the same document.
+ if (win === tooltipWindow) {
+ return this.panel.contains(node);
+ }
+
+ // Check if the node window is in the tooltip container.
+ while (win.parent && win.parent !== win) {
+ if (win.parent === tooltipWindow) {
+ // If the parent window is the tooltip window, check if the tooltip contains
+ // the current frame element.
+ return this.panel.contains(win.frameElement);
+ }
+ win = win.parent;
+ }
+
+ return false;
+ },
+
+ _onXulPanelHidden: function () {
+ if (this.isVisible()) {
+ this.hide();
+ }
+ },
+
+ /**
+ * If the tootlip is configured to autofocus and a focusable element can be found,
+ * focus it.
+ */
+ _maybeFocusTooltip: function () {
+ // Simplied selector targetting elements that can receive the focus, full version at
+ // http://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus .
+ let focusableSelector = "a, button, iframe, input, select, textarea";
+ let focusableElement = this.panel.querySelector(focusableSelector);
+ if (this.autofocus && focusableElement) {
+ focusableElement.focus();
+ }
+ },
+
+ _getTopWindow: function () {
+ return this.doc.defaultView.top;
+ },
+
+ /**
+ * Check if the tooltip's owner document is a XUL document.
+ */
+ _isXUL: function () {
+ return this.doc.documentElement.namespaceURI === XUL_NS;
+ },
+
+ _createXulPanelWrapper: function () {
+ let panel = this.doc.createElementNS(XUL_NS, "panel");
+
+ // XUL panel is only a way to display DOM elements outside of the document viewport,
+ // so disable all features that impact the behavior.
+ panel.setAttribute("animate", false);
+ panel.setAttribute("consumeoutsideclicks", false);
+ panel.setAttribute("noautofocus", true);
+ panel.setAttribute("ignorekeys", true);
+ panel.setAttribute("tooltip", "aHTMLTooltip");
+
+ // Use type="arrow" to prevent side effects (see Bug 1285206)
+ panel.setAttribute("type", "arrow");
+
+ panel.setAttribute("level", "top");
+ panel.setAttribute("class", "tooltip-xul-wrapper");
+
+ return panel;
+ },
+
+ _showXulWrapperAt: function (left, top) {
+ this.xulPanelWrapper.addEventListener("popuphidden", this._onXulPanelHidden);
+ let onPanelShown = listenOnce(this.xulPanelWrapper, "popupshown");
+ this.xulPanelWrapper.openPopupAtScreen(left, top, false);
+ return onPanelShown;
+ },
+
+ _hideXulWrapper: function () {
+ this.xulPanelWrapper.removeEventListener("popuphidden", this._onXulPanelHidden);
+
+ if (this.xulPanelWrapper.state === "closed") {
+ // XUL panel is already closed, resolve immediately.
+ return Promise.resolve();
+ }
+
+ let onPanelHidden = listenOnce(this.xulPanelWrapper, "popuphidden");
+ this.xulPanelWrapper.hidePopup();
+ return onPanelHidden;
+ },
+
+ /**
+ * Convert from coordinates relative to the tooltip's document, to coordinates relative
+ * to the "available" screen. By "available" we mean the screen, excluding the OS bars
+ * display on screen edges.
+ */
+ _convertToScreenRect: function ({left, top, width, height}) {
+ // mozInnerScreenX/Y are the coordinates of the top left corner of the window's
+ // viewport, excluding chrome UI.
+ left += this.doc.defaultView.mozInnerScreenX;
+ top += this.doc.defaultView.mozInnerScreenY;
+ return {top, right: left + width, bottom: top + height, left, width, height};
+ },
+
+ /**
+ * Apply a scoped stylesheet to the container so that this css file only
+ * applies to it.
+ */
+ _applyStylesheet: function (url) {
+ let style = this.doc.createElementNS(XHTML_NS, "style");
+ style.setAttribute("scoped", "true");
+ url = url.replace(/"/g, "\\\"");
+ style.textContent = `@import url("${url}");`;
+ this.container.appendChild(style);
+ }
+};
diff --git a/devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js b/devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js
new file mode 100644
index 000000000..04c932005
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js
@@ -0,0 +1,131 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+// Default image tooltip max dimension
+const MAX_DIMENSION = 200;
+const CONTAINER_MIN_WIDTH = 100;
+const LABEL_HEIGHT = 20;
+const IMAGE_PADDING = 4;
+
+/**
+ * Image preview tooltips should be provided with the naturalHeight and
+ * naturalWidth value for the image to display. This helper loads the provided
+ * image URL in an image object in order to retrieve the image dimensions after
+ * the load.
+ *
+ * @param {Document} doc the document element to use to create the image object
+ * @param {String} imageUrl the url of the image to measure
+ * @return {Promise} returns a promise that will resolve after the iamge load:
+ * - {Number} naturalWidth natural width of the loaded image
+ * - {Number} naturalHeight natural height of the loaded image
+ */
+function getImageDimensions(doc, imageUrl) {
+ return new Promise(resolve => {
+ let imgObj = new doc.defaultView.Image();
+ imgObj.onload = () => {
+ imgObj.onload = null;
+ let { naturalWidth, naturalHeight } = imgObj;
+ resolve({ naturalWidth, naturalHeight });
+ };
+ imgObj.src = imageUrl;
+ });
+}
+
+/**
+ * Set the tooltip content of a provided HTMLTooltip instance to display an
+ * image preview matching the provided imageUrl.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The tooltip instance on which the image preview content should be set
+ * @param {Document} doc
+ * A document element to create the HTML elements needed for the tooltip
+ * @param {String} imageUrl
+ * Absolute URL of the image to display in the tooltip
+ * @param {Object} options
+ * - {Number} naturalWidth mandatory, width of the image to display
+ * - {Number} naturalHeight mandatory, height of the image to display
+ * - {Number} maxDim optional, max width/height of the preview
+ * - {Boolean} hideDimensionLabel optional, pass true to hide the label
+ */
+function setImageTooltip(tooltip, doc, imageUrl, options) {
+ let {naturalWidth, naturalHeight, hideDimensionLabel, maxDim} = options;
+ maxDim = maxDim || MAX_DIMENSION;
+
+ let imgHeight = naturalHeight;
+ let imgWidth = naturalWidth;
+ if (imgHeight > maxDim || imgWidth > maxDim) {
+ let scale = maxDim / Math.max(imgHeight, imgWidth);
+ // Only allow integer values to avoid rounding errors.
+ imgHeight = Math.floor(scale * naturalHeight);
+ imgWidth = Math.ceil(scale * naturalWidth);
+ }
+
+ // Create tooltip content
+ let div = doc.createElementNS(XHTML_NS, "div");
+ div.style.cssText = `
+ height: 100%;
+ min-width: 100px;
+ display: flex;
+ flex-direction: column;
+ text-align: center;`;
+ let html = `
+ <div style="flex: 1;
+ display: flex;
+ padding: ${IMAGE_PADDING}px;
+ align-items: center;
+ justify-content: center;
+ min-height: 1px;">
+ <img style="height: ${imgHeight}px; max-height: 100%;"
+ src="${encodeURI(imageUrl)}"/>
+ </div>`;
+
+ if (!hideDimensionLabel) {
+ let label = naturalWidth + " \u00D7 " + naturalHeight;
+ html += `
+ <div style="height: ${LABEL_HEIGHT}px;
+ text-align: center;">
+ <span class="theme-comment devtools-tooltip-caption">${label}</span>
+ </div>`;
+ }
+ div.innerHTML = html;
+
+ // Calculate tooltip dimensions
+ let height = imgHeight + 2 * IMAGE_PADDING;
+ if (!hideDimensionLabel) {
+ height += LABEL_HEIGHT;
+ }
+ let width = Math.max(CONTAINER_MIN_WIDTH, imgWidth + 2 * IMAGE_PADDING);
+
+ tooltip.setContent(div, {width, height});
+}
+
+/*
+ * Set the tooltip content of a provided HTMLTooltip instance to display a
+ * fallback error message when an image preview tooltip can not be displayed.
+ *
+ * @param {HTMLTooltip} tooltip
+ * The tooltip instance on which the image preview content should be set
+ * @param {Document} doc
+ * A document element to create the HTML elements needed for the tooltip
+ */
+function setBrokenImageTooltip(tooltip, doc) {
+ let div = doc.createElementNS(XHTML_NS, "div");
+ div.className = "theme-comment devtools-tooltip-image-broken";
+ let message = L10N.getStr("previewTooltip.image.brokenImage");
+ div.textContent = message;
+ tooltip.setContent(div, {width: 150, height: 30});
+}
+
+module.exports.getImageDimensions = getImageDimensions;
+module.exports.setImageTooltip = setImageTooltip;
+module.exports.setBrokenImageTooltip = setBrokenImageTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js b/devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js
new file mode 100644
index 000000000..52bf565e2
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js
@@ -0,0 +1,209 @@
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+
+/**
+ * Base class for all (color, gradient, ...)-swatch based value editors inside
+ * tooltips
+ *
+ * @param {Document} document
+ * The document to attach the SwatchBasedEditorTooltip. This is either the toolbox
+ * document if the tooltip is a popup tooltip or the panel's document if it is an
+ * inline editor.
+ */
+function SwatchBasedEditorTooltip(document, stylesheet) {
+ EventEmitter.decorate(this);
+ // Creating a tooltip instance
+ // This one will consume outside clicks as it makes more sense to let the user
+ // close the tooltip by clicking out
+ // It will also close on <escape> and <enter>
+ this.tooltip = new HTMLTooltip(document, {
+ type: "arrow",
+ consumeOutsideClicks: true,
+ useXulWrapper: true,
+ stylesheet
+ });
+
+ // By default, swatch-based editor tooltips revert value change on <esc> and
+ // commit value change on <enter>
+ this.shortcuts = new KeyShortcuts({
+ window: this.tooltip.topWindow
+ });
+ this.shortcuts.on("Escape", (name, event) => {
+ if (!this.tooltip.isVisible()) {
+ return;
+ }
+ this.revert();
+ this.hide();
+ event.stopPropagation();
+ event.preventDefault();
+ });
+ this.shortcuts.on("Return", (name, event) => {
+ if (!this.tooltip.isVisible()) {
+ return;
+ }
+ this.commit();
+ this.hide();
+ event.stopPropagation();
+ event.preventDefault();
+ });
+
+ // All target swatches are kept in a map, indexed by swatch DOM elements
+ this.swatches = new Map();
+
+ // When a swatch is clicked, and for as long as the tooltip is shown, the
+ // activeSwatch property will hold the reference to the swatch DOM element
+ // that was clicked
+ this.activeSwatch = null;
+
+ this._onSwatchClick = this._onSwatchClick.bind(this);
+}
+
+SwatchBasedEditorTooltip.prototype = {
+ /**
+ * Show the editor tooltip for the currently active swatch.
+ *
+ * @return {Promise} a promise that resolves once the editor tooltip is displayed, or
+ * immediately if there is no currently active swatch.
+ */
+ show: function () {
+ if (this.activeSwatch) {
+ let onShown = this.tooltip.once("shown");
+ this.tooltip.show(this.activeSwatch, "topcenter bottomleft");
+
+ // When the tooltip is closed by clicking outside the panel we want to
+ // commit any changes.
+ this.tooltip.once("hidden", () => {
+ if (!this._reverted && !this.eyedropperOpen) {
+ this.commit();
+ }
+ this._reverted = false;
+
+ // Once the tooltip is hidden we need to clean up any remaining objects.
+ if (!this.eyedropperOpen) {
+ this.activeSwatch = null;
+ }
+ });
+
+ return onShown;
+ }
+
+ return Promise.resolve();
+ },
+
+ hide: function () {
+ this.tooltip.hide();
+ },
+
+ /**
+ * Add a new swatch DOM element to the list of swatch elements this editor
+ * tooltip knows about. That means from now on, clicking on that swatch will
+ * toggle the editor.
+ *
+ * @param {node} swatchEl
+ * The element to add
+ * @param {object} callbacks
+ * Callbacks that will be executed when the editor wants to preview a
+ * value change, or revert a change, or commit a change.
+ * - onShow: will be called when one of the swatch tooltip is shown
+ * - onPreview: will be called when one of the sub-classes calls
+ * preview
+ * - onRevert: will be called when the user ESCapes out of the tooltip
+ * - onCommit: will be called when the user presses ENTER or clicks
+ * outside the tooltip.
+ */
+ addSwatch: function (swatchEl, callbacks = {}) {
+ if (!callbacks.onShow) {
+ callbacks.onShow = function () {};
+ }
+ if (!callbacks.onPreview) {
+ callbacks.onPreview = function () {};
+ }
+ if (!callbacks.onRevert) {
+ callbacks.onRevert = function () {};
+ }
+ if (!callbacks.onCommit) {
+ callbacks.onCommit = function () {};
+ }
+
+ this.swatches.set(swatchEl, {
+ callbacks: callbacks
+ });
+ swatchEl.addEventListener("click", this._onSwatchClick, false);
+ },
+
+ removeSwatch: function (swatchEl) {
+ if (this.swatches.has(swatchEl)) {
+ if (this.activeSwatch === swatchEl) {
+ this.hide();
+ this.activeSwatch = null;
+ }
+ swatchEl.removeEventListener("click", this._onSwatchClick, false);
+ this.swatches.delete(swatchEl);
+ }
+ },
+
+ _onSwatchClick: function (event) {
+ let swatch = this.swatches.get(event.target);
+
+ if (event.shiftKey) {
+ event.stopPropagation();
+ return;
+ }
+ if (swatch) {
+ this.activeSwatch = event.target;
+ this.show();
+ swatch.callbacks.onShow();
+ event.stopPropagation();
+ }
+ },
+
+ /**
+ * Not called by this parent class, needs to be taken care of by sub-classes
+ */
+ preview: function (value) {
+ if (this.activeSwatch) {
+ let swatch = this.swatches.get(this.activeSwatch);
+ swatch.callbacks.onPreview(value);
+ }
+ },
+
+ /**
+ * This parent class only calls this on <esc> keypress
+ */
+ revert: function () {
+ if (this.activeSwatch) {
+ this._reverted = true;
+ let swatch = this.swatches.get(this.activeSwatch);
+ this.tooltip.once("hidden", () => {
+ swatch.callbacks.onRevert();
+ });
+ }
+ },
+
+ /**
+ * This parent class only calls this on <enter> keypress
+ */
+ commit: function () {
+ if (this.activeSwatch) {
+ let swatch = this.swatches.get(this.activeSwatch);
+ swatch.callbacks.onCommit();
+ }
+ },
+
+ destroy: function () {
+ this.swatches.clear();
+ this.activeSwatch = null;
+ this.tooltip.off("keypress", this._onTooltipKeypress);
+ this.tooltip.destroy();
+ this.shortcuts.destroy();
+ }
+};
+
+module.exports = SwatchBasedEditorTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
new file mode 100644
index 000000000..bf211b8b9
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -0,0 +1,182 @@
+/* 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 {Task} = require("devtools/shared/task");
+const {colorUtils} = require("devtools/shared/css/color");
+const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
+const SwatchBasedEditorTooltip = require("devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+const Heritage = require("sdk/core/heritage");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * The swatch color picker tooltip class is a specific class meant to be used
+ * along with output-parser's generated color swatches.
+ * It extends the parent SwatchBasedEditorTooltip class.
+ * It just wraps a standard Tooltip and sets its content with an instance of a
+ * color picker.
+ *
+ * @param {Document} document
+ * The document to attach the SwatchColorPickerTooltip. This is either the toolbox
+ * document if the tooltip is a popup tooltip or the panel's document if it is an
+ * inline editor.
+ * @param {InspectorPanel} inspector
+ * The inspector panel, needed for the eyedropper.
+ */
+function SwatchColorPickerTooltip(document, inspector) {
+ let stylesheet = "chrome://devtools/content/shared/widgets/spectrum.css";
+ SwatchBasedEditorTooltip.call(this, document, stylesheet);
+
+ this.inspector = inspector;
+
+ // Creating a spectrum instance. this.spectrum will always be a promise that
+ // resolves to the spectrum instance
+ this.spectrum = this.setColorPickerContent([0, 0, 0, 1]);
+ this._onSpectrumColorChange = this._onSpectrumColorChange.bind(this);
+ this._openEyeDropper = this._openEyeDropper.bind(this);
+}
+
+SwatchColorPickerTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, {
+ /**
+ * Fill the tooltip with a new instance of the spectrum color picker widget
+ * initialized with the given color, and return the instance of spectrum
+ */
+ setColorPickerContent: function (color) {
+ let { doc } = this.tooltip;
+
+ let container = doc.createElementNS(XHTML_NS, "div");
+ container.id = "spectrum-tooltip";
+ let spectrumNode = doc.createElementNS(XHTML_NS, "div");
+ spectrumNode.id = "spectrum";
+ container.appendChild(spectrumNode);
+ let eyedropper = doc.createElementNS(XHTML_NS, "button");
+ eyedropper.id = "eyedropper-button";
+ eyedropper.className = "devtools-button";
+ /* pointerEvents for eyedropper has to be set auto to display tooltip when
+ * eyedropper is disabled in non-HTML documents.
+ */
+ eyedropper.style.pointerEvents = "auto";
+ container.appendChild(eyedropper);
+
+ this.tooltip.setContent(container, { width: 218, height: 224 });
+
+ let spectrum = new Spectrum(spectrumNode, color);
+
+ // Wait for the tooltip to be shown before calling spectrum.show
+ // as it expect to be visible in order to compute DOM element sizes.
+ this.tooltip.once("shown", () => {
+ spectrum.show();
+ });
+
+ return spectrum;
+ },
+
+ /**
+ * Overriding the SwatchBasedEditorTooltip.show function to set spectrum's
+ * color.
+ */
+ show: Task.async(function* () {
+ // Call then parent class' show function
+ yield SwatchBasedEditorTooltip.prototype.show.call(this);
+ // Then set spectrum's color and listen to color changes to preview them
+ if (this.activeSwatch) {
+ this.currentSwatchColor = this.activeSwatch.nextSibling;
+ this._originalColor = this.currentSwatchColor.textContent;
+ let color = this.activeSwatch.style.backgroundColor;
+ this.spectrum.off("changed", this._onSpectrumColorChange);
+ this.spectrum.rgb = this._colorToRgba(color);
+ this.spectrum.on("changed", this._onSpectrumColorChange);
+ this.spectrum.updateUI();
+ }
+
+ let {target} = this.inspector;
+ target.actorHasMethod("inspector", "pickColorFromPage").then(value => {
+ let tooltipDoc = this.tooltip.doc;
+ let eyeButton = tooltipDoc.querySelector("#eyedropper-button");
+ if (value && this.inspector.selection.nodeFront.isInHTMLDocument) {
+ eyeButton.disabled = false;
+ eyeButton.removeAttribute("title");
+ eyeButton.addEventListener("click", this._openEyeDropper);
+ } else {
+ eyeButton.disabled = true;
+ eyeButton.title = L10N.getStr("eyedropper.disabled.title");
+ }
+ this.emit("ready");
+ }, e => console.error(e));
+ }),
+
+ _onSpectrumColorChange: function (event, rgba, cssColor) {
+ this._selectColor(cssColor);
+ },
+
+ _selectColor: function (color) {
+ if (this.activeSwatch) {
+ this.activeSwatch.style.backgroundColor = color;
+ this.activeSwatch.parentNode.dataset.color = color;
+
+ color = this._toDefaultType(color);
+ this.currentSwatchColor.textContent = color;
+ this.preview(color);
+
+ if (this.eyedropperOpen) {
+ this.commit();
+ }
+ }
+ },
+
+ _openEyeDropper: function () {
+ let {inspector, toolbox, telemetry} = this.inspector;
+ telemetry.toolOpened("pickereyedropper");
+ inspector.pickColorFromPage(toolbox, {copyOnSelect: false}).then(() => {
+ this.eyedropperOpen = true;
+
+ // close the colorpicker tooltip so that only the eyedropper is open.
+ this.hide();
+
+ this.tooltip.emit("eyedropper-opened");
+ }, e => console.error(e));
+
+ inspector.once("color-picked", color => {
+ toolbox.win.focus();
+ this._selectColor(color);
+ this._onEyeDropperDone();
+ });
+
+ inspector.once("color-pick-canceled", () => {
+ this._onEyeDropperDone();
+ });
+ },
+
+ _onEyeDropperDone: function () {
+ this.eyedropperOpen = false;
+ this.activeSwatch = null;
+ },
+
+ _colorToRgba: function (color) {
+ color = new colorUtils.CssColor(color);
+ let rgba = color._getRGBATuple();
+ return [rgba.r, rgba.g, rgba.b, rgba.a];
+ },
+
+ _toDefaultType: function (color) {
+ let colorObj = new colorUtils.CssColor(color);
+ colorObj.setAuthoredUnitFromColor(this._originalColor);
+ return colorObj.toString();
+ },
+
+ destroy: function () {
+ SwatchBasedEditorTooltip.prototype.destroy.call(this);
+ this.inspector = null;
+ this.currentSwatchColor = null;
+ this.spectrum.off("changed", this._onSpectrumColorChange);
+ this.spectrum.destroy();
+ }
+});
+
+module.exports = SwatchColorPickerTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip.js b/devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip.js
new file mode 100644
index 000000000..02f6fbea4
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip.js
@@ -0,0 +1,102 @@
+/* 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 defer = require("devtools/shared/defer");
+const {Task} = require("devtools/shared/task");
+const {CubicBezierWidget} = require("devtools/client/shared/widgets/CubicBezierWidget");
+const SwatchBasedEditorTooltip = require("devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip");
+
+const Heritage = require("sdk/core/heritage");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * The swatch cubic-bezier tooltip class is a specific class meant to be used
+ * along with rule-view's generated cubic-bezier swatches.
+ * It extends the parent SwatchBasedEditorTooltip class.
+ * It just wraps a standard Tooltip and sets its content with an instance of a
+ * CubicBezierWidget.
+ *
+ * @param {Document} document
+ * The document to attach the SwatchCubicBezierTooltip. This is either the toolbox
+ * document if the tooltip is a popup tooltip or the panel's document if it is an
+ * inline editor.
+ */
+function SwatchCubicBezierTooltip(document) {
+ let stylesheet = "chrome://devtools/content/shared/widgets/cubic-bezier.css";
+ SwatchBasedEditorTooltip.call(this, document, stylesheet);
+
+ // Creating a cubic-bezier instance.
+ // this.widget will always be a promise that resolves to the widget instance
+ this.widget = this.setCubicBezierContent([0, 0, 1, 1]);
+ this._onUpdate = this._onUpdate.bind(this);
+}
+
+SwatchCubicBezierTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, {
+ /**
+ * Fill the tooltip with a new instance of the cubic-bezier widget
+ * initialized with the given value, and return a promise that resolves to
+ * the instance of the widget
+ */
+ setCubicBezierContent: function (bezier) {
+ let { doc } = this.tooltip;
+
+ let container = doc.createElementNS(XHTML_NS, "div");
+ container.className = "cubic-bezier-container";
+
+ this.tooltip.setContent(container, { width: 510, height: 370 });
+
+ let def = defer();
+
+ // Wait for the tooltip to be shown before calling instanciating the widget
+ // as it expect its DOM elements to be visible.
+ this.tooltip.once("shown", () => {
+ let widget = new CubicBezierWidget(container, bezier);
+ def.resolve(widget);
+ });
+
+ return def.promise;
+ },
+
+ /**
+ * Overriding the SwatchBasedEditorTooltip.show function to set the cubic
+ * bezier curve in the widget
+ */
+ show: Task.async(function* () {
+ // Call the parent class' show function
+ yield SwatchBasedEditorTooltip.prototype.show.call(this);
+ // Then set the curve and listen to changes to preview them
+ if (this.activeSwatch) {
+ this.currentBezierValue = this.activeSwatch.nextSibling;
+ this.widget.then(widget => {
+ widget.off("updated", this._onUpdate);
+ widget.cssCubicBezierValue = this.currentBezierValue.textContent;
+ widget.on("updated", this._onUpdate);
+ this.emit("ready");
+ });
+ }
+ }),
+
+ _onUpdate: function (event, bezier) {
+ if (!this.activeSwatch) {
+ return;
+ }
+
+ this.currentBezierValue.textContent = bezier + "";
+ this.preview(bezier + "");
+ },
+
+ destroy: function () {
+ SwatchBasedEditorTooltip.prototype.destroy.call(this);
+ this.currentBezierValue = null;
+ this.widget.then(widget => {
+ widget.off("updated", this._onUpdate);
+ widget.destroy();
+ });
+ }
+});
+
+module.exports = SwatchCubicBezierTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js b/devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js
new file mode 100644
index 000000000..bc69c3b70
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js
@@ -0,0 +1,116 @@
+/* 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 {Task} = require("devtools/shared/task");
+const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
+const SwatchBasedEditorTooltip = require("devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip");
+
+const Heritage = require("sdk/core/heritage");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * The swatch-based css filter tooltip class is a specific class meant to be
+ * used along with rule-view's generated css filter swatches.
+ * It extends the parent SwatchBasedEditorTooltip class.
+ * It just wraps a standard Tooltip and sets its content with an instance of a
+ * CSSFilterEditorWidget.
+ *
+ * @param {Document} document
+ * The document to attach the SwatchFilterTooltip. This is either the toolbox
+ * document if the tooltip is a popup tooltip or the panel's document if it is an
+ * inline editor.
+ * @param {function} cssIsValid
+ * A function to check that css declaration's name and values are valid together.
+ * This can be obtained from "shared/fronts/css-properties.js".
+ */
+function SwatchFilterTooltip(document, cssIsValid) {
+ let stylesheet = "chrome://devtools/content/shared/widgets/filter-widget.css";
+ SwatchBasedEditorTooltip.call(this, document, stylesheet);
+ this._cssIsValid = cssIsValid;
+
+ // Creating a filter editor instance.
+ this.widget = this.setFilterContent("none");
+ this._onUpdate = this._onUpdate.bind(this);
+}
+
+SwatchFilterTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, {
+ /**
+ * Fill the tooltip with a new instance of the CSSFilterEditorWidget
+ * widget initialized with the given filter value, and return a promise
+ * that resolves to the instance of the widget when ready.
+ */
+ setFilterContent: function (filter) {
+ let { doc } = this.tooltip;
+
+ let container = doc.createElementNS(XHTML_NS, "div");
+ container.id = "filter-container";
+
+ this.tooltip.setContent(container, { width: 510, height: 200 });
+
+ return new CSSFilterEditorWidget(container, filter, this._cssIsValid);
+ },
+
+ show: Task.async(function* () {
+ // Call the parent class' show function
+ yield SwatchBasedEditorTooltip.prototype.show.call(this);
+ // Then set the filter value and listen to changes to preview them
+ if (this.activeSwatch) {
+ this.currentFilterValue = this.activeSwatch.nextSibling;
+ this.widget.off("updated", this._onUpdate);
+ this.widget.on("updated", this._onUpdate);
+ this.widget.setCssValue(this.currentFilterValue.textContent);
+ this.widget.render();
+ this.emit("ready");
+ }
+ }),
+
+ _onUpdate: function (event, filters) {
+ if (!this.activeSwatch) {
+ return;
+ }
+
+ // Remove the old children and reparse the property value to
+ // recompute them.
+ while (this.currentFilterValue.firstChild) {
+ this.currentFilterValue.firstChild.remove();
+ }
+ let node = this._parser.parseCssProperty("filter", filters, this._options);
+ this.currentFilterValue.appendChild(node);
+
+ this.preview();
+ },
+
+ destroy: function () {
+ SwatchBasedEditorTooltip.prototype.destroy.call(this);
+ this.currentFilterValue = null;
+ this.widget.off("updated", this._onUpdate);
+ this.widget.destroy();
+ },
+
+ /**
+ * Like SwatchBasedEditorTooltip.addSwatch, but accepts a parser object
+ * to use when previewing the updated property value.
+ *
+ * @param {node} swatchEl
+ * @see SwatchBasedEditorTooltip.addSwatch
+ * @param {object} callbacks
+ * @see SwatchBasedEditorTooltip.addSwatch
+ * @param {object} parser
+ * A parser object; @see OutputParser object
+ * @param {object} options
+ * options to pass to the output parser, with
+ * the option |filterSwatch| set.
+ */
+ addSwatch: function (swatchEl, callbacks, parser, options) {
+ SwatchBasedEditorTooltip.prototype.addSwatch.call(this, swatchEl,
+ callbacks);
+ this._parser = parser;
+ this._options = options;
+ }
+});
+
+module.exports = SwatchFilterTooltip;
diff --git a/devtools/client/shared/widgets/tooltip/Tooltip.js b/devtools/client/shared/widgets/tooltip/Tooltip.js
new file mode 100644
index 000000000..c3c365152
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/Tooltip.js
@@ -0,0 +1,410 @@
+/* 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 defer = require("devtools/shared/defer");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const ESCAPE_KEYCODE = KeyCodes.DOM_VK_ESCAPE;
+const POPUP_EVENTS = ["shown", "hidden", "showing", "hiding"];
+
+/**
+ * Tooltip widget.
+ *
+ * This widget is intended at any tool that may need to show rich content in the
+ * form of floating panels.
+ * A common use case is image previewing in the CSS rule view, but more complex
+ * use cases may include color pickers, object inspection, etc...
+ *
+ * Tooltips are based on XUL (namely XUL arrow-type <panel>s), and therefore
+ * need a XUL Document to live in.
+ * This is pretty much the only requirement they have on their environment.
+ *
+ * The way to use a tooltip is simply by instantiating a tooltip yourself and
+ * attaching some content in it, or using one of the ready-made content types.
+ *
+ * A convenient `startTogglingOnHover` method may avoid having to register event
+ * handlers yourself if the tooltip has to be shown when hovering over a
+ * specific element or group of elements (which is usually the most common case)
+ */
+
+/**
+ * Tooltip class.
+ *
+ * Basic usage:
+ * let t = new Tooltip(xulDoc);
+ * t.content = someXulContent;
+ * t.show();
+ * t.hide();
+ * t.destroy();
+ *
+ * Better usage:
+ * let t = new Tooltip(xulDoc);
+ * t.startTogglingOnHover(container, target => {
+ * if (<condition based on target>) {
+ * t.content = el;
+ * return true;
+ * }
+ * });
+ * t.destroy();
+ *
+ * @param {XULDocument} doc
+ * The XUL document hosting this tooltip
+ * @param {Object} options
+ * Optional options that give options to consumers:
+ * - consumeOutsideClick {Boolean} Wether the first click outside of the
+ * tooltip should close the tooltip and be consumed or not.
+ * Defaults to false.
+ * - closeOnKeys {Array} An array of key codes that should close the
+ * tooltip. Defaults to [27] (escape key).
+ * - closeOnEvents [{emitter: {Object}, event: {String},
+ * useCapture: {Boolean}}]
+ * Provide an optional list of emitter objects and event names here to
+ * trigger the closing of the tooltip when these events are fired by the
+ * emitters. The emitter objects should either implement
+ * on/off(event, cb) or addEventListener/removeEventListener(event, cb).
+ * Defaults to [].
+ * For instance, the following would close the tooltip whenever the
+ * toolbox selects a new tool and when a DOM node gets scrolled:
+ * new Tooltip(doc, {
+ * closeOnEvents: [
+ * {emitter: toolbox, event: "select"},
+ * {emitter: myContainer, event: "scroll", useCapture: true}
+ * ]
+ * });
+ * - noAutoFocus {Boolean} Should the focus automatically go to the panel
+ * when it opens. Defaults to true.
+ *
+ * Fires these events:
+ * - showing : just before the tooltip shows
+ * - shown : when the tooltip is shown
+ * - hiding : just before the tooltip closes
+ * - hidden : when the tooltip gets hidden
+ * - keypress : when any key gets pressed, with keyCode
+ */
+function Tooltip(doc, {
+ consumeOutsideClick = false,
+ closeOnKeys = [ESCAPE_KEYCODE],
+ noAutoFocus = true,
+ closeOnEvents = [],
+ } = {}) {
+ EventEmitter.decorate(this);
+
+ this.doc = doc;
+ this.consumeOutsideClick = consumeOutsideClick;
+ this.closeOnKeys = closeOnKeys;
+ this.noAutoFocus = noAutoFocus;
+ this.closeOnEvents = closeOnEvents;
+
+ this.panel = this._createPanel();
+
+ // Create tooltip toggle helper and decorate the Tooltip instance with
+ // shortcut methods.
+ this._toggle = new TooltipToggle(this);
+ this.startTogglingOnHover = this._toggle.start.bind(this._toggle);
+ this.stopTogglingOnHover = this._toggle.stop.bind(this._toggle);
+
+ // Emit show/hide events when the panel does.
+ for (let eventName of POPUP_EVENTS) {
+ this["_onPopup" + eventName] = (name => {
+ return e => {
+ if (e.target === this.panel) {
+ this.emit(name);
+ }
+ };
+ })(eventName);
+ this.panel.addEventListener("popup" + eventName,
+ this["_onPopup" + eventName], false);
+ }
+
+ // Listen to keypress events to close the tooltip if configured to do so
+ let win = this.doc.querySelector("window");
+ this._onKeyPress = event => {
+ if (this.panel.hidden) {
+ return;
+ }
+
+ this.emit("keypress", event.keyCode);
+ if (this.closeOnKeys.indexOf(event.keyCode) !== -1 &&
+ this.isShown()) {
+ event.stopPropagation();
+ this.hide();
+ }
+ };
+ win.addEventListener("keypress", this._onKeyPress, false);
+
+ // Listen to custom emitters' events to close the tooltip
+ this.hide = this.hide.bind(this);
+ for (let {emitter, event, useCapture} of this.closeOnEvents) {
+ for (let add of ["addEventListener", "on"]) {
+ if (add in emitter) {
+ emitter[add](event, this.hide, useCapture);
+ break;
+ }
+ }
+ }
+}
+
+Tooltip.prototype = {
+ defaultPosition: "before_start",
+ // px
+ defaultOffsetX: 0,
+ // px
+ defaultOffsetY: 0,
+ // px
+
+ /**
+ * Show the tooltip. It might be wise to append some content first if you
+ * don't want the tooltip to be empty. You may access the content of the
+ * tooltip by setting a XUL node to t.content.
+ * @param {node} anchor
+ * Which node should the tooltip be shown on
+ * @param {string} position [optional]
+ * Optional tooltip position. Defaults to before_start
+ * https://developer.mozilla.org/en-US/docs/XUL/PopupGuide/Positioning
+ * @param {number} x, y [optional]
+ * The left and top offset coordinates, in pixels.
+ */
+ show: function (anchor,
+ position = this.defaultPosition,
+ x = this.defaultOffsetX,
+ y = this.defaultOffsetY) {
+ this.panel.hidden = false;
+ this.panel.openPopup(anchor, position, x, y);
+ },
+
+ /**
+ * Hide the tooltip
+ */
+ hide: function () {
+ this.panel.hidden = true;
+ this.panel.hidePopup();
+ },
+
+ isShown: function () {
+ return this.panel &&
+ this.panel.state !== "closed" &&
+ this.panel.state !== "hiding";
+ },
+
+ setSize: function (width, height) {
+ this.panel.sizeTo(width, height);
+ },
+
+ /**
+ * Empty the tooltip's content
+ */
+ empty: function () {
+ while (this.panel.hasChildNodes()) {
+ this.panel.removeChild(this.panel.firstChild);
+ }
+ },
+
+ /**
+ * Gets this panel's visibility state.
+ * @return boolean
+ */
+ isHidden: function () {
+ return this.panel.state == "closed" || this.panel.state == "hiding";
+ },
+
+ /**
+ * Gets if this panel has any child nodes.
+ * @return boolean
+ */
+ isEmpty: function () {
+ return !this.panel.hasChildNodes();
+ },
+
+ /**
+ * Get rid of references and event listeners
+ */
+ destroy: function () {
+ this.hide();
+
+ for (let eventName of POPUP_EVENTS) {
+ this.panel.removeEventListener("popup" + eventName,
+ this["_onPopup" + eventName], false);
+ }
+
+ let win = this.doc.querySelector("window");
+ win.removeEventListener("keypress", this._onKeyPress, false);
+
+ for (let {emitter, event, useCapture} of this.closeOnEvents) {
+ for (let remove of ["removeEventListener", "off"]) {
+ if (remove in emitter) {
+ emitter[remove](event, this.hide, useCapture);
+ break;
+ }
+ }
+ }
+
+ this.content = null;
+
+ this._toggle.destroy();
+
+ this.doc = null;
+
+ this.panel.remove();
+ this.panel = null;
+ },
+
+ /**
+ * Returns the outer container node (that includes the arrow etc.). Happens
+ * to be identical to this.panel here, can be different element in other
+ * Tooltip implementations.
+ */
+ get container() {
+ return this.panel;
+ },
+
+ /**
+ * Set the content of this tooltip. Will first empty the tooltip and then
+ * append the new content element.
+ * Consider using one of the set<type>Content() functions instead.
+ * @param {node} content
+ * A node that can be appended in the tooltip XUL element
+ */
+ set content(content) {
+ if (this.content == content) {
+ return;
+ }
+
+ this.empty();
+ this.panel.removeAttribute("clamped-dimensions");
+ this.panel.removeAttribute("clamped-dimensions-no-min-height");
+ this.panel.removeAttribute("clamped-dimensions-no-max-or-min-height");
+ this.panel.removeAttribute("wide");
+
+ if (content) {
+ this.panel.appendChild(content);
+ }
+ },
+
+ get content() {
+ return this.panel.firstChild;
+ },
+
+ /**
+ * Sets some text as the content of this tooltip.
+ *
+ * @param {array} messages
+ * A list of text messages.
+ * @param {string} messagesClass [optional]
+ * A style class for the text messages.
+ * @param {string} containerClass [optional]
+ * A style class for the text messages container.
+ */
+ setTextContent: function (
+ {
+ messages,
+ messagesClass,
+ containerClass
+ },
+ extraButtons = []) {
+ messagesClass = messagesClass || "default-tooltip-simple-text-colors";
+ containerClass = containerClass || "default-tooltip-simple-text-colors";
+
+ let vbox = this.doc.createElement("vbox");
+ vbox.className = "devtools-tooltip-simple-text-container " + containerClass;
+ vbox.setAttribute("flex", "1");
+
+ for (let text of messages) {
+ let description = this.doc.createElement("description");
+ description.setAttribute("flex", "1");
+ description.className = "devtools-tooltip-simple-text " + messagesClass;
+ description.textContent = text;
+ vbox.appendChild(description);
+ }
+
+ for (let { label, className, command } of extraButtons) {
+ let button = this.doc.createElement("button");
+ button.className = className;
+ button.setAttribute("label", label);
+ button.addEventListener("command", command);
+ vbox.appendChild(button);
+ }
+
+ this.content = vbox;
+ },
+
+ /**
+ * Load a document into an iframe, and set the iframe
+ * to be the tooltip's content.
+ *
+ * Used by tooltips that want to load their interface
+ * into an iframe from a URL.
+ *
+ * @param {string} width
+ * Width of the iframe.
+ * @param {string} height
+ * Height of the iframe.
+ * @param {string} url
+ * URL of the document to load into the iframe.
+ *
+ * @return {promise} A promise which is resolved with
+ * the iframe.
+ *
+ * This function creates an iframe, loads the specified document
+ * into it, sets the tooltip's content to the iframe, and returns
+ * a promise.
+ *
+ * When the document is loaded, the function gets the content window
+ * and resolves the promise with the content window.
+ */
+ setIFrameContent: function ({width, height}, url) {
+ let def = defer();
+
+ // Create an iframe
+ let iframe = this.doc.createElementNS(XHTML_NS, "iframe");
+ iframe.setAttribute("transparent", true);
+ iframe.setAttribute("width", width);
+ iframe.setAttribute("height", height);
+ iframe.setAttribute("flex", "1");
+ iframe.setAttribute("tooltip", "aHTMLTooltip");
+ iframe.setAttribute("class", "devtools-tooltip-iframe");
+
+ // Wait for the load to initialize the widget
+ function onLoad() {
+ iframe.removeEventListener("load", onLoad, true);
+ def.resolve(iframe);
+ }
+ iframe.addEventListener("load", onLoad, true);
+
+ // load the document from url into the iframe
+ iframe.setAttribute("src", url);
+
+ // Put the iframe in the tooltip
+ this.content = iframe;
+
+ return def.promise;
+ },
+
+ /**
+ * Create the tooltip panel
+ */
+ _createPanel() {
+ let panel = this.doc.createElement("panel");
+ panel.setAttribute("hidden", true);
+ panel.setAttribute("ignorekeys", true);
+ panel.setAttribute("animate", false);
+
+ panel.setAttribute("consumeoutsideclicks",
+ this.consumeOutsideClick);
+ panel.setAttribute("noautofocus", this.noAutoFocus);
+ panel.setAttribute("type", "arrow");
+ panel.setAttribute("level", "top");
+
+ panel.setAttribute("class", "devtools-tooltip theme-tooltip-panel");
+ this.doc.querySelector("window").appendChild(panel);
+
+ return panel;
+ }
+};
+
+module.exports = Tooltip;
diff --git a/devtools/client/shared/widgets/tooltip/TooltipToggle.js b/devtools/client/shared/widgets/tooltip/TooltipToggle.js
new file mode 100644
index 000000000..b53664f34
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/TooltipToggle.js
@@ -0,0 +1,182 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {Task} = require("devtools/shared/task");
+
+const DEFAULT_TOGGLE_DELAY = 50;
+
+/**
+ * Tooltip helper designed to show/hide the tooltip when the mouse hovers over
+ * particular nodes.
+ *
+ * This works by tracking mouse movements on a base container node (baseNode)
+ * and showing the tooltip when the mouse stops moving. A callback can be
+ * provided to the start() method to know whether or not the node being
+ * hovered over should indeed receive the tooltip.
+ */
+function TooltipToggle(tooltip) {
+ this.tooltip = tooltip;
+ this.win = tooltip.doc.defaultView;
+
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onMouseOut = this._onMouseOut.bind(this);
+
+ this._onTooltipMouseOver = this._onTooltipMouseOver.bind(this);
+ this._onTooltipMouseOut = this._onTooltipMouseOut.bind(this);
+}
+
+module.exports.TooltipToggle = TooltipToggle;
+
+TooltipToggle.prototype = {
+ /**
+ * Start tracking mouse movements on the provided baseNode to show the
+ * tooltip.
+ *
+ * 2 Ways to make this work:
+ * - Provide a single node to attach the tooltip to, as the baseNode, and
+ * omit the second targetNodeCb argument
+ * - Provide a baseNode that is the container of possibly numerous children
+ * elements that may receive a tooltip. In this case, provide the second
+ * targetNodeCb argument to decide wether or not a child should receive
+ * a tooltip.
+ *
+ * Note that if you call this function a second time, it will itself call
+ * stop() before adding mouse tracking listeners again.
+ *
+ * @param {node} baseNode
+ * The container for all target nodes
+ * @param {Function} targetNodeCb
+ * A function that accepts a node argument and that checks if a tooltip
+ * should be displayed. Possible return values are:
+ * - false (or a falsy value) if the tooltip should not be displayed
+ * - true if the tooltip should be displayed
+ * - a DOM node to display the tooltip on the returned anchor
+ * The function can also return a promise that will resolve to one of
+ * the values listed above.
+ * If omitted, the tooltip will be shown everytime.
+ * @param {Object} options
+ Set of optional arguments:
+ * - {Number} toggleDelay
+ * An optional delay (in ms) that will be observed before showing
+ * and before hiding the tooltip. Defaults to DEFAULT_TOGGLE_DELAY.
+ * - {Boolean} interactive
+ * If enabled, the tooltip is not hidden when mouse leaves the
+ * target element and enters the tooltip. Allows the tooltip
+ * content to be interactive.
+ */
+ start: function (baseNode, targetNodeCb,
+ {toggleDelay = DEFAULT_TOGGLE_DELAY, interactive = false} = {}) {
+ this.stop();
+
+ if (!baseNode) {
+ // Calling tool is in the process of being destroyed.
+ return;
+ }
+
+ this._baseNode = baseNode;
+ this._targetNodeCb = targetNodeCb || (() => true);
+ this._toggleDelay = toggleDelay;
+ this._interactive = interactive;
+
+ baseNode.addEventListener("mousemove", this._onMouseMove);
+ baseNode.addEventListener("mouseout", this._onMouseOut);
+
+ if (this._interactive) {
+ this.tooltip.container.addEventListener("mouseover", this._onTooltipMouseOver);
+ this.tooltip.container.addEventListener("mouseout", this._onTooltipMouseOut);
+ }
+ },
+
+ /**
+ * If the start() function has been used previously, and you want to get rid
+ * of this behavior, then call this function to remove the mouse movement
+ * tracking
+ */
+ stop: function () {
+ this.win.clearTimeout(this.toggleTimer);
+
+ if (!this._baseNode) {
+ return;
+ }
+
+ this._baseNode.removeEventListener("mousemove", this._onMouseMove);
+ this._baseNode.removeEventListener("mouseout", this._onMouseOut);
+
+ if (this._interactive) {
+ this.tooltip.container.removeEventListener("mouseover", this._onTooltipMouseOver);
+ this.tooltip.container.removeEventListener("mouseout", this._onTooltipMouseOut);
+ }
+
+ this._baseNode = null;
+ this._targetNodeCb = null;
+ this._lastHovered = null;
+ },
+
+ _onMouseMove: function (event) {
+ if (event.target !== this._lastHovered) {
+ this._lastHovered = event.target;
+
+ this.win.clearTimeout(this.toggleTimer);
+ this.toggleTimer = this.win.setTimeout(() => {
+ this.tooltip.hide();
+ this.isValidHoverTarget(event.target).then(target => {
+ if (target === null) {
+ return;
+ }
+ this.tooltip.show(target);
+ }, reason => {
+ console.error("isValidHoverTarget rejected with unexpected reason:");
+ console.error(reason);
+ });
+ }, this._toggleDelay);
+ }
+ },
+
+ /**
+ * Is the given target DOMNode a valid node for toggling the tooltip on hover.
+ * This delegates to the user-defined _targetNodeCb callback.
+ * @return {Promise} a promise that will resolve the anchor to use for the
+ * tooltip or null if no valid target was found.
+ */
+ isValidHoverTarget: Task.async(function* (target) {
+ let res = yield this._targetNodeCb(target, this.tooltip);
+ if (res) {
+ return res.nodeName ? res : target;
+ }
+
+ return null;
+ }),
+
+ _onMouseOut: function (event) {
+ // Only hide the tooltip if the mouse leaves baseNode.
+ if (event && this._baseNode && !this._baseNode.contains(event.relatedTarget)) {
+ return;
+ }
+
+ this._lastHovered = null;
+ this.win.clearTimeout(this.toggleTimer);
+ this.toggleTimer = this.win.setTimeout(() => {
+ this.tooltip.hide();
+ }, this._toggleDelay);
+ },
+
+ _onTooltipMouseOver() {
+ this.win.clearTimeout(this.toggleTimer);
+ },
+
+ _onTooltipMouseOut() {
+ this.win.clearTimeout(this.toggleTimer);
+ this.toggleTimer = this.win.setTimeout(() => {
+ this.tooltip.hide();
+ }, this._toggleDelay);
+ },
+
+ destroy: function () {
+ this.stop();
+ }
+};
diff --git a/devtools/client/shared/widgets/tooltip/VariableContentHelper.js b/devtools/client/shared/widgets/tooltip/VariableContentHelper.js
new file mode 100644
index 000000000..4dc02da9b
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/VariableContentHelper.js
@@ -0,0 +1,89 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
+ "resource://devtools/client/shared/widgets/VariablesView.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
+ "resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+
+/**
+ * Fill the tooltip with a variables view, inspecting an object via its
+ * corresponding object actor, as specified in the remote debugging protocol.
+ *
+ * @param {Tooltip} tooltip
+ * The tooltip to use
+ * @param {object} objectActor
+ * The value grip for the object actor.
+ * @param {object} viewOptions [optional]
+ * Options for the variables view visualization.
+ * @param {object} controllerOptions [optional]
+ * Options for the variables view controller.
+ * @param {object} relayEvents [optional]
+ * A collection of events to listen on the variables view widget.
+ * For example, { fetched: () => ... }
+ * @param {array} extraButtons [optional]
+ * An array of extra buttons to add. Each element of the array
+ * should be of the form {label, className, command}.
+ * @param {Toolbox} toolbox [optional]
+ * Pass the instance of the current toolbox if you want the variables
+ * view widget to allow highlighting and selection of DOM nodes
+ */
+
+function setTooltipVariableContent(tooltip, objectActor,
+ viewOptions = {}, controllerOptions = {},
+ relayEvents = {}, extraButtons = [],
+ toolbox = null) {
+ let doc = tooltip.doc;
+ let vbox = doc.createElement("vbox");
+ vbox.className = "devtools-tooltip-variables-view-box";
+ vbox.setAttribute("flex", "1");
+
+ let innerbox = doc.createElement("vbox");
+ innerbox.className = "devtools-tooltip-variables-view-innerbox";
+ innerbox.setAttribute("flex", "1");
+ vbox.appendChild(innerbox);
+
+ for (let { label, className, command } of extraButtons) {
+ let button = doc.createElement("button");
+ button.className = className;
+ button.setAttribute("label", label);
+ button.addEventListener("command", command);
+ vbox.appendChild(button);
+ }
+
+ let widget = new VariablesView(innerbox, viewOptions);
+
+ // If a toolbox was provided, link it to the vview
+ if (toolbox) {
+ widget.toolbox = toolbox;
+ }
+
+ // Analyzing state history isn't useful with transient object inspectors.
+ widget.commitHierarchy = () => {};
+
+ for (let e in relayEvents) {
+ widget.on(e, relayEvents[e]);
+ }
+ VariablesViewController.attach(widget, controllerOptions);
+
+ // Some of the view options are allowed to change between uses.
+ widget.searchPlaceholder = viewOptions.searchPlaceholder;
+ widget.searchEnabled = viewOptions.searchEnabled;
+
+ // Use the object actor's grip to display it as a variable in the widget.
+ // The controller options are allowed to change between uses.
+ widget.controller.setSingleVariable(
+ { objectActor: objectActor }, controllerOptions);
+
+ tooltip.content = vbox;
+ tooltip.panel.setAttribute("clamped-dimensions", "");
+}
+
+exports.setTooltipVariableContent = setTooltipVariableContent;
diff --git a/devtools/client/shared/widgets/tooltip/moz.build b/devtools/client/shared/widgets/tooltip/moz.build
new file mode 100644
index 000000000..93172227a
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/moz.build
@@ -0,0 +1,19 @@
+# -*- 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/.
+
+DevToolsModules(
+ 'CssDocsTooltip.js',
+ 'EventTooltipHelper.js',
+ 'HTMLTooltip.js',
+ 'ImageTooltipHelper.js',
+ 'SwatchBasedEditorTooltip.js',
+ 'SwatchColorPickerTooltip.js',
+ 'SwatchCubicBezierTooltip.js',
+ 'SwatchFilterTooltip.js',
+ 'Tooltip.js',
+ 'TooltipToggle.js',
+ 'VariableContentHelper.js',
+)
diff --git a/devtools/client/shared/widgets/view-helpers.js b/devtools/client/shared/widgets/view-helpers.js
new file mode 100644
index 000000000..4686d4e1c
--- /dev/null
+++ b/devtools/client/shared/widgets/view-helpers.js
@@ -0,0 +1,1625 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const PANE_APPEARANCE_DELAY = 50;
+const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
+const WIDGET_FOCUSABLE_NODES = new Set(["vbox", "hbox"]);
+
+var namedTimeoutsStore = new Map();
+
+/**
+ * Inheritance helpers from the addon SDK's core/heritage.
+ * Remove these when all devtools are loadered.
+ */
+exports.Heritage = {
+ /**
+ * @see extend in sdk/core/heritage.
+ */
+ extend: function (prototype, properties = {}) {
+ return Object.create(prototype, this.getOwnPropertyDescriptors(properties));
+ },
+
+ /**
+ * @see getOwnPropertyDescriptors in sdk/core/heritage.
+ */
+ getOwnPropertyDescriptors: function (object) {
+ return Object.getOwnPropertyNames(object).reduce((descriptor, name) => {
+ descriptor[name] = Object.getOwnPropertyDescriptor(object, name);
+ return descriptor;
+ }, {});
+ }
+};
+
+/**
+ * Helper for draining a rapid succession of events and invoking a callback
+ * once everything settles down.
+ *
+ * @param string id
+ * A string identifier for the named timeout.
+ * @param number wait
+ * The amount of milliseconds to wait after no more events are fired.
+ * @param function callback
+ * Invoked when no more events are fired after the specified time.
+ */
+const setNamedTimeout = function setNamedTimeout(id, wait, callback) {
+ clearNamedTimeout(id);
+
+ namedTimeoutsStore.set(id, setTimeout(() =>
+ namedTimeoutsStore.delete(id) && callback(), wait));
+};
+exports.setNamedTimeout = setNamedTimeout;
+
+/**
+ * Clears a named timeout.
+ * @see setNamedTimeout
+ *
+ * @param string id
+ * A string identifier for the named timeout.
+ */
+const clearNamedTimeout = function clearNamedTimeout(id) {
+ if (!namedTimeoutsStore) {
+ return;
+ }
+ clearTimeout(namedTimeoutsStore.get(id));
+ namedTimeoutsStore.delete(id);
+};
+exports.clearNamedTimeout = clearNamedTimeout;
+
+/**
+ * Same as `setNamedTimeout`, but invokes the callback only if the provided
+ * predicate function returns true. Otherwise, the timeout is re-triggered.
+ *
+ * @param string id
+ * A string identifier for the conditional timeout.
+ * @param number wait
+ * The amount of milliseconds to wait after no more events are fired.
+ * @param function predicate
+ * The predicate function used to determine whether the timeout restarts.
+ * @param function callback
+ * Invoked when no more events are fired after the specified time, and
+ * the provided predicate function returns true.
+ */
+const setConditionalTimeout = function setConditionalTimeout(id, wait,
+ predicate,
+ callback) {
+ setNamedTimeout(id, wait, function maybeCallback() {
+ if (predicate()) {
+ callback();
+ return;
+ }
+ setConditionalTimeout(id, wait, predicate, callback);
+ });
+};
+exports.setConditionalTimeout = setConditionalTimeout;
+
+/**
+ * Clears a conditional timeout.
+ * @see setConditionalTimeout
+ *
+ * @param string id
+ * A string identifier for the conditional timeout.
+ */
+const clearConditionalTimeout = function clearConditionalTimeout(id) {
+ clearNamedTimeout(id);
+};
+exports.clearConditionalTimeout = clearConditionalTimeout;
+
+/**
+ * Helpers for creating and messaging between UI components.
+ */
+const ViewHelpers = exports.ViewHelpers = {
+ /**
+ * Convenience method, dispatching a custom event.
+ *
+ * @param nsIDOMNode target
+ * A custom target element to dispatch the event from.
+ * @param string type
+ * The name of the event.
+ * @param any detail
+ * The data passed when initializing the event.
+ * @return boolean
+ * True if the event was cancelled or a registered handler
+ * called preventDefault.
+ */
+ dispatchEvent: function (target, type, detail) {
+ if (!(target instanceof Node)) {
+ // Event cancelled.
+ return true;
+ }
+ let document = target.ownerDocument || target;
+ let dispatcher = target.ownerDocument ? target : document.documentElement;
+
+ let event = document.createEvent("CustomEvent");
+ event.initCustomEvent(type, true, true, detail);
+ return dispatcher.dispatchEvent(event);
+ },
+
+ /**
+ * Helper delegating some of the DOM attribute methods of a node to a widget.
+ *
+ * @param object widget
+ * The widget to assign the methods to.
+ * @param nsIDOMNode node
+ * A node to delegate the methods to.
+ */
+ delegateWidgetAttributeMethods: function (widget, node) {
+ widget.getAttribute =
+ widget.getAttribute || node.getAttribute.bind(node);
+ widget.setAttribute =
+ widget.setAttribute || node.setAttribute.bind(node);
+ widget.removeAttribute =
+ widget.removeAttribute || node.removeAttribute.bind(node);
+ },
+
+ /**
+ * Helper delegating some of the DOM event methods of a node to a widget.
+ *
+ * @param object widget
+ * The widget to assign the methods to.
+ * @param nsIDOMNode node
+ * A node to delegate the methods to.
+ */
+ delegateWidgetEventMethods: function (widget, node) {
+ widget.addEventListener =
+ widget.addEventListener || node.addEventListener.bind(node);
+ widget.removeEventListener =
+ widget.removeEventListener || node.removeEventListener.bind(node);
+ },
+
+ /**
+ * Checks if the specified object looks like it's been decorated by an
+ * event emitter.
+ *
+ * @return boolean
+ * True if it looks, walks and quacks like an event emitter.
+ */
+ isEventEmitter: function (object) {
+ return object && object.on && object.off && object.once && object.emit;
+ },
+
+ /**
+ * Checks if the specified object is an instance of a DOM node.
+ *
+ * @return boolean
+ * True if it's a node, false otherwise.
+ */
+ isNode: function (object) {
+ return object instanceof Node ||
+ object instanceof Element ||
+ object instanceof DocumentFragment;
+ },
+
+ /**
+ * Prevents event propagation when navigation keys are pressed.
+ *
+ * @param Event e
+ * The event to be prevented.
+ */
+ preventScrolling: function (e) {
+ switch (e.keyCode) {
+ case KeyCodes.DOM_VK_UP:
+ case KeyCodes.DOM_VK_DOWN:
+ case KeyCodes.DOM_VK_LEFT:
+ case KeyCodes.DOM_VK_RIGHT:
+ case KeyCodes.DOM_VK_PAGE_UP:
+ case KeyCodes.DOM_VK_PAGE_DOWN:
+ case KeyCodes.DOM_VK_HOME:
+ case KeyCodes.DOM_VK_END:
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ },
+
+ /**
+ * Check if the enter key or space was pressed
+ *
+ * @param event event
+ * The event triggered by a keypress on an element
+ */
+ isSpaceOrReturn: function (event) {
+ return event.keyCode === KeyCodes.DOM_VK_SPACE ||
+ event.keyCode === KeyCodes.DOM_VK_RETURN;
+ },
+
+ /**
+ * Sets a toggled pane hidden or visible. The pane can either be displayed on
+ * the side (right or left depending on the locale) or at the bottom.
+ *
+ * @param object flags
+ * An object containing some of the following properties:
+ * - visible: true if the pane should be shown, false to hide
+ * - animated: true to display an animation on toggle
+ * - delayed: true to wait a few cycles before toggle
+ * - callback: a function to invoke when the toggle finishes
+ * @param nsIDOMNode pane
+ * The element representing the pane to toggle.
+ */
+ togglePane: function (flags, pane) {
+ // Make sure a pane is actually available first.
+ if (!pane) {
+ return;
+ }
+
+ // Hiding is always handled via margins, not the hidden attribute.
+ pane.removeAttribute("hidden");
+
+ // Add a class to the pane to handle min-widths, margins and animations.
+ pane.classList.add("generic-toggled-pane");
+
+ // Avoid toggles in the middle of animation.
+ if (pane.hasAttribute("animated")) {
+ return;
+ }
+
+ // Avoid useless toggles.
+ if (flags.visible == !pane.classList.contains("pane-collapsed")) {
+ if (flags.callback) {
+ flags.callback();
+ }
+ return;
+ }
+
+ // The "animated" attributes enables animated toggles (slide in-out).
+ if (flags.animated) {
+ pane.setAttribute("animated", "");
+ } else {
+ pane.removeAttribute("animated");
+ }
+
+ // Computes and sets the pane margins in order to hide or show it.
+ let doToggle = () => {
+ // Negative margins are applied to "right" and "left" to support RTL and
+ // LTR directions, as well as to "bottom" to support vertical layouts.
+ // Unnecessary negative margins are forced to 0 via CSS in widgets.css.
+ if (flags.visible) {
+ pane.style.marginLeft = "0";
+ pane.style.marginRight = "0";
+ pane.style.marginBottom = "0";
+ pane.classList.remove("pane-collapsed");
+ } else {
+ let width = Math.floor(pane.getAttribute("width")) + 1;
+ let height = Math.floor(pane.getAttribute("height")) + 1;
+ pane.style.marginLeft = -width + "px";
+ pane.style.marginRight = -width + "px";
+ pane.style.marginBottom = -height + "px";
+ }
+
+ // Wait for the animation to end before calling afterToggle()
+ if (flags.animated) {
+ let options = {
+ useCapture: false,
+ once: true
+ };
+
+ pane.addEventListener("transitionend", () => {
+ // Prevent unwanted transitions: if the panel is hidden and the layout
+ // changes margins will be updated and the panel will pop out.
+ pane.removeAttribute("animated");
+
+ if (!flags.visible) {
+ pane.classList.add("pane-collapsed");
+ }
+ if (flags.callback) {
+ flags.callback();
+ }
+ }, options);
+ } else {
+ if (!flags.visible) {
+ pane.classList.add("pane-collapsed");
+ }
+
+ // Invoke the callback immediately since there's no transition.
+ if (flags.callback) {
+ flags.callback();
+ }
+ }
+ };
+
+ // Sometimes it's useful delaying the toggle a few ticks to ensure
+ // a smoother slide in-out animation.
+ if (flags.delayed) {
+ pane.ownerDocument.defaultView.setTimeout(doToggle,
+ PANE_APPEARANCE_DELAY);
+ } else {
+ doToggle();
+ }
+ }
+};
+
+/**
+ * A generic Item is used to describe children present in a Widget.
+ *
+ * This is basically a very thin wrapper around an nsIDOMNode, with a few
+ * characteristics, like a `value` and an `attachment`.
+ *
+ * The characteristics are optional, and their meaning is entirely up to you.
+ * - The `value` should be a string, passed as an argument.
+ * - The `attachment` is any kind of primitive or object, passed as an argument.
+ *
+ * Iterable via "for (let childItem of parentItem) { }".
+ *
+ * @param object ownerView
+ * The owner view creating this item.
+ * @param nsIDOMNode element
+ * A prebuilt node to be wrapped.
+ * @param string value
+ * A string identifying the node.
+ * @param any attachment
+ * Some attached primitive/object.
+ */
+function Item(ownerView, element, value, attachment) {
+ this.ownerView = ownerView;
+ this.attachment = attachment;
+ this._value = value + "";
+ this._prebuiltNode = element;
+ this._itemsByElement = new Map();
+}
+
+Item.prototype = {
+ get value() {
+ return this._value;
+ },
+ get target() {
+ return this._target;
+ },
+ get prebuiltNode() {
+ return this._prebuiltNode;
+ },
+
+ /**
+ * Immediately appends a child item to this item.
+ *
+ * @param nsIDOMNode element
+ * An nsIDOMNode representing the child element to append.
+ * @param object options [optional]
+ * Additional options or flags supported by this operation:
+ * - attachment: some attached primitive/object for the item
+ * - attributes: a batch of attributes set to the displayed element
+ * - finalize: function invoked when the child item is removed
+ * @return Item
+ * The item associated with the displayed element.
+ */
+ append: function (element, options = {}) {
+ let item = new Item(this, element, "", options.attachment);
+
+ // Entangle the item with the newly inserted child node.
+ // Make sure this is done with the value returned by appendChild(),
+ // to avoid storing a potential DocumentFragment.
+ this._entangleItem(item, this._target.appendChild(element));
+
+ // Handle any additional options after entangling the item.
+ if (options.attributes) {
+ options.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
+ }
+ if (options.finalize) {
+ item.finalize = options.finalize;
+ }
+
+ // Return the item associated with the displayed element.
+ return item;
+ },
+
+ /**
+ * Immediately removes the specified child item from this item.
+ *
+ * @param Item item
+ * The item associated with the element to remove.
+ */
+ remove: function (item) {
+ if (!item) {
+ return;
+ }
+ this._target.removeChild(item._target);
+ this._untangleItem(item);
+ },
+
+ /**
+ * Entangles an item (model) with a displayed node element (view).
+ *
+ * @param Item item
+ * The item describing a target element.
+ * @param nsIDOMNode element
+ * The element displaying the item.
+ */
+ _entangleItem: function (item, element) {
+ this._itemsByElement.set(element, item);
+ item._target = element;
+ },
+
+ /**
+ * Untangles an item (model) from a displayed node element (view).
+ *
+ * @param Item item
+ * The item describing a target element.
+ */
+ _untangleItem: function (item) {
+ if (item.finalize) {
+ item.finalize(item);
+ }
+ for (let childItem of item) {
+ item.remove(childItem);
+ }
+
+ this._unlinkItem(item);
+ item._target = null;
+ },
+
+ /**
+ * Deletes an item from the its parent's storage maps.
+ *
+ * @param Item item
+ * The item describing a target element.
+ */
+ _unlinkItem: function (item) {
+ this._itemsByElement.delete(item._target);
+ },
+
+ /**
+ * Returns a string representing the object.
+ * Avoid using `toString` to avoid accidental JSONification.
+ * @return string
+ */
+ stringify: function () {
+ return JSON.stringify({
+ value: this._value,
+ target: this._target + "",
+ prebuiltNode: this._prebuiltNode + "",
+ attachment: this.attachment
+ }, null, 2);
+ },
+
+ _value: "",
+ _target: null,
+ _prebuiltNode: null,
+ finalize: null,
+ attachment: null
+};
+
+/**
+ * Some generic Widget methods handling Item instances.
+ * Iterable via "for (let childItem of wrappedView) { }".
+ *
+ * Usage:
+ * function MyView() {
+ * this.widget = new MyWidget(document.querySelector(".my-node"));
+ * }
+ *
+ * MyView.prototype = Heritage.extend(WidgetMethods, {
+ * myMethod: function() {},
+ * ...
+ * });
+ *
+ * See https://gist.github.com/victorporof/5749386 for more details.
+ * The devtools/shared/widgets/SimpleListWidget.jsm is an implementation
+ * example.
+ *
+ * Language:
+ * - An "item" is an instance of an Item.
+ * - An "element" or "node" is a nsIDOMNode.
+ *
+ * The supplied widget can be any object implementing the following
+ * methods:
+ * - function:nsIDOMNode insertItemAt(aIndex:number, aNode:nsIDOMNode,
+ * aValue:string)
+ * - function:nsIDOMNode getItemAtIndex(aIndex:number)
+ * - function removeChild(aChild:nsIDOMNode)
+ * - function removeAllItems()
+ * - get:nsIDOMNode selectedItem()
+ * - set selectedItem(aChild:nsIDOMNode)
+ * - function getAttribute(aName:string)
+ * - function setAttribute(aName:string, aValue:string)
+ * - function removeAttribute(aName:string)
+ * - function addEventListener(aName:string, aCallback:function,
+ * aBubbleFlag:boolean)
+ * - function removeEventListener(aName:string, aCallback:function,
+ * aBubbleFlag:boolean)
+ *
+ * Optional methods that can be implemented by the widget:
+ * - function ensureElementIsVisible(aChild:nsIDOMNode)
+ *
+ * Optional attributes that may be handled (when calling
+ * get/set/removeAttribute):
+ * - "emptyText": label temporarily added when there are no items present
+ * - "headerText": label permanently added as a header
+ *
+ * For automagical keyboard and mouse accessibility, the widget should be an
+ * event emitter with the following events:
+ * - "keyPress" -> (aName:string, aEvent:KeyboardEvent)
+ * - "mousePress" -> (aName:string, aEvent:MouseEvent)
+ */
+const WidgetMethods = exports.WidgetMethods = {
+ /**
+ * Sets the element node or widget associated with this container.
+ * @param nsIDOMNode | object widget
+ */
+ set widget(widget) {
+ this._widget = widget;
+
+ // Can't use a WeakMap for _itemsByValue because keys are strings, and
+ // can't use one for _itemsByElement either, since it needs to be iterable.
+ this._itemsByValue = new Map();
+ this._itemsByElement = new Map();
+ this._stagedItems = [];
+
+ // Handle internal events emitted by the widget if necessary.
+ if (ViewHelpers.isEventEmitter(widget)) {
+ widget.on("keyPress", this._onWidgetKeyPress.bind(this));
+ widget.on("mousePress", this._onWidgetMousePress.bind(this));
+ }
+ },
+
+ /**
+ * Gets the element node or widget associated with this container.
+ * @return nsIDOMNode | object
+ */
+ get widget() {
+ return this._widget;
+ },
+
+ /**
+ * Prepares an item to be added to this container. This allows, for example,
+ * for a large number of items to be batched up before being sorted & added.
+ *
+ * If the "staged" flag is *not* set to true, the item will be immediately
+ * inserted at the correct position in this container, so that all the items
+ * still remain sorted. This can (possibly) be much slower than batching up
+ * multiple items.
+ *
+ * By default, this container assumes that all the items should be displayed
+ * sorted by their value. This can be overridden with the "index" flag,
+ * specifying on which position should an item be appended. The "staged" and
+ * "index" flags are mutually exclusive, meaning that all staged items
+ * will always be appended.
+ *
+ * @param nsIDOMNode element
+ * A prebuilt node to be wrapped.
+ * @param string value
+ * A string identifying the node.
+ * @param object options [optional]
+ * Additional options or flags supported by this operation:
+ * - attachment: some attached primitive/object for the item
+ * - staged: true to stage the item to be appended later
+ * - index: specifies on which position should the item be appended
+ * - attributes: a batch of attributes set to the displayed element
+ * - finalize: function invoked when the item is removed
+ * @return Item
+ * The item associated with the displayed element if an unstaged push,
+ * undefined if the item was staged for a later commit.
+ */
+ push: function ([element, value], options = {}) {
+ let item = new Item(this, element, value, options.attachment);
+
+ // Batch the item to be added later.
+ if (options.staged) {
+ // An ulterior commit operation will ignore any specified index, so
+ // no reason to keep it around.
+ options.index = undefined;
+ return void this._stagedItems.push({ item: item, options: options });
+ }
+ // Find the target position in this container and insert the item there.
+ if (!("index" in options)) {
+ return this._insertItemAt(this._findExpectedIndexFor(item), item,
+ options);
+ }
+ // Insert the item at the specified index. If negative or out of bounds,
+ // the item will be simply appended.
+ return this._insertItemAt(options.index, item, options);
+ },
+
+ /**
+ * Flushes all the prepared items into this container.
+ * Any specified index on the items will be ignored. Everything is appended.
+ *
+ * @param object options [optional]
+ * Additional options or flags supported by this operation:
+ * - sorted: true to sort all the items before adding them
+ */
+ commit: function (options = {}) {
+ let stagedItems = this._stagedItems;
+
+ // Sort the items before adding them to this container, if preferred.
+ if (options.sorted) {
+ stagedItems.sort((a, b) => this._currentSortPredicate(a.item, b.item));
+ }
+ // Append the prepared items to this container.
+ for (let { item, opt } of stagedItems) {
+ this._insertItemAt(-1, item, opt);
+ }
+ // Recreate the temporary items list for ulterior pushes.
+ this._stagedItems.length = 0;
+ },
+
+ /**
+ * Immediately removes the specified item from this container.
+ *
+ * @param Item item
+ * The item associated with the element to remove.
+ */
+ remove: function (item) {
+ if (!item) {
+ return;
+ }
+ this._widget.removeChild(item._target);
+ this._untangleItem(item);
+
+ if (!this._itemsByElement.size) {
+ this._preferredValue = this.selectedValue;
+ this._widget.selectedItem = null;
+ this._widget.setAttribute("emptyText", this._emptyText);
+ }
+ },
+
+ /**
+ * Removes the item at the specified index from this container.
+ *
+ * @param number index
+ * The index of the item to remove.
+ */
+ removeAt: function (index) {
+ this.remove(this.getItemAtIndex(index));
+ },
+
+ /**
+ * Removes the items in this container based on a predicate.
+ */
+ removeForPredicate: function (predicate) {
+ let item;
+ while ((item = this.getItemForPredicate(predicate))) {
+ this.remove(item);
+ }
+ },
+
+ /**
+ * Removes all items from this container.
+ */
+ empty: function () {
+ this._preferredValue = this.selectedValue;
+ this._widget.selectedItem = null;
+ this._widget.removeAllItems();
+ this._widget.setAttribute("emptyText", this._emptyText);
+
+ for (let [, item] of this._itemsByElement) {
+ this._untangleItem(item);
+ }
+
+ this._itemsByValue.clear();
+ this._itemsByElement.clear();
+ this._stagedItems.length = 0;
+ },
+
+ /**
+ * Ensures the specified item is visible in this container.
+ *
+ * @param Item item
+ * The item to bring into view.
+ */
+ ensureItemIsVisible: function (item) {
+ this._widget.ensureElementIsVisible(item._target);
+ },
+
+ /**
+ * Ensures the item at the specified index is visible in this container.
+ *
+ * @param number index
+ * The index of the item to bring into view.
+ */
+ ensureIndexIsVisible: function (index) {
+ this.ensureItemIsVisible(this.getItemAtIndex(index));
+ },
+
+ /**
+ * Sugar for ensuring the selected item is visible in this container.
+ */
+ ensureSelectedItemIsVisible: function () {
+ this.ensureItemIsVisible(this.selectedItem);
+ },
+
+ /**
+ * If supported by the widget, the label string temporarily added to this
+ * container when there are no child items present.
+ */
+ set emptyText(value) {
+ this._emptyText = value;
+
+ // Apply the emptyText attribute right now if there are no child items.
+ if (!this._itemsByElement.size) {
+ this._widget.setAttribute("emptyText", value);
+ }
+ },
+
+ /**
+ * If supported by the widget, the label string permanently added to this
+ * container as a header.
+ * @param string value
+ */
+ set headerText(value) {
+ this._headerText = value;
+ this._widget.setAttribute("headerText", value);
+ },
+
+ /**
+ * Toggles all the items in this container hidden or visible.
+ *
+ * This does not change the default filtering predicate, so newly inserted
+ * items will always be visible. Use WidgetMethods.filterContents if you care.
+ *
+ * @param boolean visibleFlag
+ * Specifies the intended visibility.
+ */
+ toggleContents: function (visibleFlag) {
+ for (let [element] of this._itemsByElement) {
+ element.hidden = !visibleFlag;
+ }
+ },
+
+ /**
+ * Toggles all items in this container hidden or visible based on a predicate.
+ *
+ * @param function predicate [optional]
+ * Items are toggled according to the return value of this function,
+ * which will become the new default filtering predicate in this
+ * container.
+ * If unspecified, all items will be toggled visible.
+ */
+ filterContents: function (predicate = this._currentFilterPredicate) {
+ this._currentFilterPredicate = predicate;
+
+ for (let [element, item] of this._itemsByElement) {
+ element.hidden = !predicate(item);
+ }
+ },
+
+ /**
+ * Sorts all the items in this container based on a predicate.
+ *
+ * @param function predicate [optional]
+ * Items are sorted according to the return value of the function,
+ * which will become the new default sorting predicate in this
+ * container. If unspecified, all items will be sorted by their value.
+ */
+ sortContents: function (predicate = this._currentSortPredicate) {
+ let sortedItems = this.items.sort(this._currentSortPredicate = predicate);
+
+ for (let i = 0, len = sortedItems.length; i < len; i++) {
+ this.swapItems(this.getItemAtIndex(i), sortedItems[i]);
+ }
+ },
+
+ /**
+ * Visually swaps two items in this container.
+ *
+ * @param Item first
+ * The first item to be swapped.
+ * @param Item second
+ * The second item to be swapped.
+ */
+ swapItems: function (first, second) {
+ if (first == second) {
+ // We're just dandy, thank you.
+ return;
+ }
+ let { _prebuiltNode: firstPrebuiltTarget, _target: firstTarget } = first;
+ let { _prebuiltNode: secondPrebuiltTarget, _target: secondTarget } = second;
+
+ // If the two items were constructed with prebuilt nodes as
+ // DocumentFragments, then those DocumentFragments are now
+ // empty and need to be reassembled.
+ if (firstPrebuiltTarget instanceof DocumentFragment) {
+ for (let node of firstTarget.childNodes) {
+ firstPrebuiltTarget.appendChild(node.cloneNode(true));
+ }
+ }
+ if (secondPrebuiltTarget instanceof DocumentFragment) {
+ for (let node of secondTarget.childNodes) {
+ secondPrebuiltTarget.appendChild(node.cloneNode(true));
+ }
+ }
+
+ // 1. Get the indices of the two items to swap.
+ let i = this._indexOfElement(firstTarget);
+ let j = this._indexOfElement(secondTarget);
+
+ // 2. Remeber the selection index, to reselect an item, if necessary.
+ let selectedTarget = this._widget.selectedItem;
+ let selectedIndex = -1;
+ if (selectedTarget == firstTarget) {
+ selectedIndex = i;
+ } else if (selectedTarget == secondTarget) {
+ selectedIndex = j;
+ }
+
+ // 3. Silently nuke both items, nobody needs to know about this.
+ this._widget.removeChild(firstTarget);
+ this._widget.removeChild(secondTarget);
+ this._unlinkItem(first);
+ this._unlinkItem(second);
+
+ // 4. Add the items again, but reversing their indices.
+ this._insertItemAt.apply(this, i < j ? [i, second] : [j, first]);
+ this._insertItemAt.apply(this, i < j ? [j, first] : [i, second]);
+
+ // 5. Restore the previous selection, if necessary.
+ if (selectedIndex == i) {
+ this._widget.selectedItem = first._target;
+ } else if (selectedIndex == j) {
+ this._widget.selectedItem = second._target;
+ }
+
+ // 6. Let the outside world know that these two items were swapped.
+ ViewHelpers.dispatchEvent(first.target, "swap", [second, first]);
+ },
+
+ /**
+ * Visually swaps two items in this container at specific indices.
+ *
+ * @param number first
+ * The index of the first item to be swapped.
+ * @param number second
+ * The index of the second item to be swapped.
+ */
+ swapItemsAtIndices: function (first, second) {
+ this.swapItems(this.getItemAtIndex(first), this.getItemAtIndex(second));
+ },
+
+ /**
+ * Checks whether an item with the specified value is among the elements
+ * shown in this container.
+ *
+ * @param string value
+ * The item's value.
+ * @return boolean
+ * True if the value is known, false otherwise.
+ */
+ containsValue: function (value) {
+ return this._itemsByValue.has(value) ||
+ this._stagedItems.some(({ item }) => item._value == value);
+ },
+
+ /**
+ * Gets the "preferred value". This is the latest selected item's value,
+ * remembered just before emptying this container.
+ * @return string
+ */
+ get preferredValue() {
+ return this._preferredValue;
+ },
+
+ /**
+ * Retrieves the item associated with the selected element.
+ * @return Item | null
+ */
+ get selectedItem() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._itemsByElement.get(selectedElement);
+ }
+ return null;
+ },
+
+ /**
+ * Retrieves the selected element's index in this container.
+ * @return number
+ */
+ get selectedIndex() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._indexOfElement(selectedElement);
+ }
+ return -1;
+ },
+
+ /**
+ * Retrieves the value of the selected element.
+ * @return string
+ */
+ get selectedValue() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._itemsByElement.get(selectedElement)._value;
+ }
+ return "";
+ },
+
+ /**
+ * Retrieves the attachment of the selected element.
+ * @return object | null
+ */
+ get selectedAttachment() {
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ return this._itemsByElement.get(selectedElement).attachment;
+ }
+ return null;
+ },
+
+ _selectItem: function (item) {
+ // A falsy item is allowed to invalidate the current selection.
+ let targetElement = item ? item._target : null;
+ let prevElement = this._widget.selectedItem;
+
+ // Make sure the selected item's target element is focused and visible.
+ if (this.autoFocusOnSelection && targetElement) {
+ targetElement.focus();
+ }
+
+ if (targetElement != prevElement) {
+ this._widget.selectedItem = targetElement;
+ }
+ },
+
+ /**
+ * Selects the element with the entangled item in this container.
+ * @param Item | function item
+ */
+ set selectedItem(item) {
+ // A predicate is allowed to select a specific item.
+ // If no item is matched, then the current selection is removed.
+ if (typeof item == "function") {
+ item = this.getItemForPredicate(item);
+ }
+
+ let targetElement = item ? item._target : null;
+ let prevElement = this._widget.selectedItem;
+
+ if (this.maintainSelectionVisible && targetElement) {
+ // Some methods are optional. See the WidgetMethods object documentation
+ // for a comprehensive list.
+ if ("ensureElementIsVisible" in this._widget) {
+ this._widget.ensureElementIsVisible(targetElement);
+ }
+ }
+
+ this._selectItem(item);
+
+ // Prevent selecting the same item again and avoid dispatching
+ // a redundant selection event, so return early.
+ if (targetElement != prevElement) {
+ let dispTarget = targetElement || prevElement;
+ let dispName = this.suppressSelectionEvents ? "suppressed-select"
+ : "select";
+ ViewHelpers.dispatchEvent(dispTarget, dispName, item);
+ }
+ },
+
+ /**
+ * Selects the element at the specified index in this container.
+ * @param number index
+ */
+ set selectedIndex(index) {
+ let targetElement = this._widget.getItemAtIndex(index);
+ if (targetElement) {
+ this.selectedItem = this._itemsByElement.get(targetElement);
+ return;
+ }
+ this.selectedItem = null;
+ },
+
+ /**
+ * Selects the element with the specified value in this container.
+ * @param string value
+ */
+ set selectedValue(value) {
+ this.selectedItem = this._itemsByValue.get(value);
+ },
+
+ /**
+ * Deselects and re-selects an item in this container.
+ *
+ * Useful when you want a "select" event to be emitted, even though
+ * the specified item was already selected.
+ *
+ * @param Item | function item
+ * @see `set selectedItem`
+ */
+ forceSelect: function (item) {
+ this.selectedItem = null;
+ this.selectedItem = item;
+ },
+
+ /**
+ * Specifies if this container should try to keep the selected item visible.
+ * (For example, when new items are added the selection is brought into view).
+ */
+ maintainSelectionVisible: true,
+
+ /**
+ * Specifies if "select" events dispatched from the elements in this container
+ * when their respective items are selected should be suppressed or not.
+ *
+ * If this flag is set to true, then consumers of this container won't
+ * be normally notified when items are selected.
+ */
+ suppressSelectionEvents: false,
+
+ /**
+ * Focus this container the first time an element is inserted?
+ *
+ * If this flag is set to true, then when the first item is inserted in
+ * this container (and thus it's the only item available), its corresponding
+ * target element is focused as well.
+ */
+ autoFocusOnFirstItem: true,
+
+ /**
+ * Focus on selection?
+ *
+ * If this flag is set to true, then whenever an item is selected in
+ * this container (e.g. via the selectedIndex or selectedItem setters),
+ * its corresponding target element is focused as well.
+ *
+ * You can disable this flag, for example, to maintain a certain node
+ * focused but visually indicate a different selection in this container.
+ */
+ autoFocusOnSelection: true,
+
+ /**
+ * Focus on input (e.g. mouse click)?
+ *
+ * If this flag is set to true, then whenever an item receives user input in
+ * this container, its corresponding target element is focused as well.
+ */
+ autoFocusOnInput: true,
+
+ /**
+ * When focusing on input, allow right clicks?
+ * @see WidgetMethods.autoFocusOnInput
+ */
+ allowFocusOnRightClick: false,
+
+ /**
+ * The number of elements in this container to jump when Page Up or Page Down
+ * keys are pressed. If falsy, then the page size will be based on the
+ * number of visible items in the container.
+ */
+ pageSize: 0,
+
+ /**
+ * Focuses the first visible item in this container.
+ */
+ focusFirstVisibleItem: function () {
+ this.focusItemAtDelta(-this.itemCount);
+ },
+
+ /**
+ * Focuses the last visible item in this container.
+ */
+ focusLastVisibleItem: function () {
+ this.focusItemAtDelta(+this.itemCount);
+ },
+
+ /**
+ * Focuses the next item in this container.
+ */
+ focusNextItem: function () {
+ this.focusItemAtDelta(+1);
+ },
+
+ /**
+ * Focuses the previous item in this container.
+ */
+ focusPrevItem: function () {
+ this.focusItemAtDelta(-1);
+ },
+
+ /**
+ * Focuses another item in this container based on the index distance
+ * from the currently focused item.
+ *
+ * @param number delta
+ * A scalar specifying by how many items should the selection change.
+ */
+ focusItemAtDelta: function (delta) {
+ // Make sure the currently selected item is also focused, so that the
+ // command dispatcher mechanism has a relative node to work with.
+ // If there's no selection, just select an item at a corresponding index
+ // (e.g. the first item in this container if delta <= 1).
+ let selectedElement = this._widget.selectedItem;
+ if (selectedElement) {
+ selectedElement.focus();
+ } else {
+ this.selectedIndex = Math.max(0, delta - 1);
+ return;
+ }
+
+ let direction = delta > 0 ? "advanceFocus" : "rewindFocus";
+ let distance = Math.abs(Math[delta > 0 ? "ceil" : "floor"](delta));
+ while (distance--) {
+ if (!this._focusChange(direction)) {
+ // Out of bounds.
+ break;
+ }
+ }
+
+ // Synchronize the selected item as being the currently focused element.
+ this.selectedItem = this.getItemForElement(this._focusedElement);
+ },
+
+ /**
+ * Focuses the next or previous item in this container.
+ *
+ * @param string direction
+ * Either "advanceFocus" or "rewindFocus".
+ * @return boolean
+ * False if the focus went out of bounds and the first or last item
+ * in this container was focused instead.
+ */
+ _focusChange: function (direction) {
+ let commandDispatcher = this._commandDispatcher;
+ let prevFocusedElement = commandDispatcher.focusedElement;
+ let currFocusedElement;
+
+ do {
+ commandDispatcher.suppressFocusScroll = true;
+ commandDispatcher[direction]();
+ currFocusedElement = commandDispatcher.focusedElement;
+
+ // Make sure the newly focused item is a part of this container. If the
+ // focus goes out of bounds, revert the previously focused item.
+ if (!this.getItemForElement(currFocusedElement)) {
+ prevFocusedElement.focus();
+ return false;
+ }
+ } while (!WIDGET_FOCUSABLE_NODES.has(currFocusedElement.tagName));
+
+ // Focus remained within bounds.
+ return true;
+ },
+
+ /**
+ * Gets the command dispatcher instance associated with this container's DOM.
+ * If there are no items displayed in this container, null is returned.
+ * @return nsIDOMXULCommandDispatcher | null
+ */
+ get _commandDispatcher() {
+ if (this._cachedCommandDispatcher) {
+ return this._cachedCommandDispatcher;
+ }
+ let someElement = this._widget.getItemAtIndex(0);
+ if (someElement) {
+ let commandDispatcher = someElement.ownerDocument.commandDispatcher;
+ this._cachedCommandDispatcher = commandDispatcher;
+ return commandDispatcher;
+ }
+ return null;
+ },
+
+ /**
+ * Gets the currently focused element in this container.
+ *
+ * @return nsIDOMNode
+ * The focused element, or null if nothing is found.
+ */
+ get _focusedElement() {
+ let commandDispatcher = this._commandDispatcher;
+ if (commandDispatcher) {
+ return commandDispatcher.focusedElement;
+ }
+ return null;
+ },
+
+ /**
+ * Gets the item in the container having the specified index.
+ *
+ * @param number index
+ * The index used to identify the element.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemAtIndex: function (index) {
+ return this.getItemForElement(this._widget.getItemAtIndex(index));
+ },
+
+ /**
+ * Gets the item in the container having the specified value.
+ *
+ * @param string value
+ * The value used to identify the element.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemByValue: function (value) {
+ return this._itemsByValue.get(value);
+ },
+
+ /**
+ * Gets the item in the container associated with the specified element.
+ *
+ * @param nsIDOMNode element
+ * The element used to identify the item.
+ * @param object flags [optional]
+ * Additional options for showing the source. Supported options:
+ * - noSiblings: if siblings shouldn't be taken into consideration
+ * when searching for the associated item.
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemForElement: function (element, flags = {}) {
+ while (element) {
+ let item = this._itemsByElement.get(element);
+
+ // Also search the siblings if allowed.
+ if (!flags.noSiblings) {
+ item = item ||
+ this._itemsByElement.get(element.nextElementSibling) ||
+ this._itemsByElement.get(element.previousElementSibling);
+ }
+ if (item) {
+ return item;
+ }
+ element = element.parentNode;
+ }
+ return null;
+ },
+
+ /**
+ * Gets a visible item in this container validating a specified predicate.
+ *
+ * @param function predicate
+ * The first item which validates this predicate is returned
+ * @return Item
+ * The matched item, or null if nothing is found.
+ */
+ getItemForPredicate: function (predicate, owner = this) {
+ // Recursively check the items in this widget for a predicate match.
+ for (let [element, item] of owner._itemsByElement) {
+ let match;
+ if (predicate(item) && !element.hidden) {
+ match = item;
+ } else {
+ match = this.getItemForPredicate(predicate, item);
+ }
+ if (match) {
+ return match;
+ }
+ }
+ // Also check the staged items. No need to do this recursively since
+ // they're not even appended to the view yet.
+ for (let { item } of this._stagedItems) {
+ if (predicate(item)) {
+ return item;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Shortcut function for getItemForPredicate which works on item attachments.
+ * @see getItemForPredicate
+ */
+ getItemForAttachment: function (predicate, owner = this) {
+ return this.getItemForPredicate(e => predicate(e.attachment));
+ },
+
+ /**
+ * Finds the index of an item in the container.
+ *
+ * @param Item item
+ * The item get the index for.
+ * @return number
+ * The index of the matched item, or -1 if nothing is found.
+ */
+ indexOfItem: function (item) {
+ return this._indexOfElement(item._target);
+ },
+
+ /**
+ * Finds the index of an element in the container.
+ *
+ * @param nsIDOMNode element
+ * The element get the index for.
+ * @return number
+ * The index of the matched element, or -1 if nothing is found.
+ */
+ _indexOfElement: function (element) {
+ for (let i = 0; i < this._itemsByElement.size; i++) {
+ if (this._widget.getItemAtIndex(i) == element) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Gets the total number of items in this container.
+ * @return number
+ */
+ get itemCount() {
+ return this._itemsByElement.size;
+ },
+
+ /**
+ * Returns a list of items in this container, in the displayed order.
+ * @return array
+ */
+ get items() {
+ let store = [];
+ let itemCount = this.itemCount;
+ for (let i = 0; i < itemCount; i++) {
+ store.push(this.getItemAtIndex(i));
+ }
+ return store;
+ },
+
+ /**
+ * Returns a list of values in this container, in the displayed order.
+ * @return array
+ */
+ get values() {
+ return this.items.map(e => e._value);
+ },
+
+ /**
+ * Returns a list of attachments in this container, in the displayed order.
+ * @return array
+ */
+ get attachments() {
+ return this.items.map(e => e.attachment);
+ },
+
+ /**
+ * Returns a list of all the visible (non-hidden) items in this container,
+ * in the displayed order
+ * @return array
+ */
+ get visibleItems() {
+ return this.items.filter(e => !e._target.hidden);
+ },
+
+ /**
+ * Checks if an item is unique in this container. If an item's value is an
+ * empty string, "undefined" or "null", it is considered unique.
+ *
+ * @param Item item
+ * The item for which to verify uniqueness.
+ * @return boolean
+ * True if the item is unique, false otherwise.
+ */
+ isUnique: function (item) {
+ let value = item._value;
+ if (value == "" || value == "undefined" || value == "null") {
+ return true;
+ }
+ return !this._itemsByValue.has(value);
+ },
+
+ /**
+ * Checks if an item is eligible for this container. By default, this checks
+ * whether an item is unique and has a prebuilt target node.
+ *
+ * @param Item item
+ * The item for which to verify eligibility.
+ * @return boolean
+ * True if the item is eligible, false otherwise.
+ */
+ isEligible: function (item) {
+ return this.isUnique(item) && item._prebuiltNode;
+ },
+
+ /**
+ * Finds the expected item index in this container based on the default
+ * sort predicate.
+ *
+ * @param Item item
+ * The item for which to get the expected index.
+ * @return number
+ * The expected item index.
+ */
+ _findExpectedIndexFor: function (item) {
+ let itemCount = this.itemCount;
+ for (let i = 0; i < itemCount; i++) {
+ if (this._currentSortPredicate(this.getItemAtIndex(i), item) > 0) {
+ return i;
+ }
+ }
+ return itemCount;
+ },
+
+ /**
+ * Immediately inserts an item in this container at the specified index.
+ *
+ * @param number index
+ * The position in the container intended for this item.
+ * @param Item item
+ * The item describing a target element.
+ * @param object options [optional]
+ * Additional options or flags supported by this operation:
+ * - attributes: a batch of attributes set to the displayed element
+ * - finalize: function when the item is untangled (removed)
+ * @return Item
+ * The item associated with the displayed element, null if rejected.
+ */
+ _insertItemAt: function (index, item, options = {}) {
+ if (!this.isEligible(item)) {
+ return null;
+ }
+
+ // Entangle the item with the newly inserted node.
+ // Make sure this is done with the value returned by insertItemAt(),
+ // to avoid storing a potential DocumentFragment.
+ let node = item._prebuiltNode;
+ let attachment = item.attachment;
+ this._entangleItem(item,
+ this._widget.insertItemAt(index, node, attachment));
+
+ // Handle any additional options after entangling the item.
+ if (!this._currentFilterPredicate(item)) {
+ item._target.hidden = true;
+ }
+ if (this.autoFocusOnFirstItem && this._itemsByElement.size == 1) {
+ item._target.focus();
+ }
+ if (options.attributes) {
+ options.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
+ }
+ if (options.finalize) {
+ item.finalize = options.finalize;
+ }
+
+ // Hide the empty text if the selection wasn't lost.
+ this._widget.removeAttribute("emptyText");
+
+ // Return the item associated with the displayed element.
+ return item;
+ },
+
+ /**
+ * Entangles an item (model) with a displayed node element (view).
+ *
+ * @param Item item
+ * The item describing a target element.
+ * @param nsIDOMNode element
+ * The element displaying the item.
+ */
+ _entangleItem: function (item, element) {
+ this._itemsByValue.set(item._value, item);
+ this._itemsByElement.set(element, item);
+ item._target = element;
+ },
+
+ /**
+ * Untangles an item (model) from a displayed node element (view).
+ *
+ * @param Item item
+ * The item describing a target element.
+ */
+ _untangleItem: function (item) {
+ if (item.finalize) {
+ item.finalize(item);
+ }
+ for (let childItem of item) {
+ item.remove(childItem);
+ }
+
+ this._unlinkItem(item);
+ item._target = null;
+ },
+
+ /**
+ * Deletes an item from the its parent's storage maps.
+ *
+ * @param Item item
+ * The item describing a target element.
+ */
+ _unlinkItem: function (item) {
+ this._itemsByValue.delete(item._value);
+ this._itemsByElement.delete(item._target);
+ },
+
+ /**
+ * The keyPress event listener for this container.
+ * @param string name
+ * @param KeyboardEvent event
+ */
+ _onWidgetKeyPress: function (name, event) {
+ // Prevent scrolling when pressing navigation keys.
+ ViewHelpers.preventScrolling(event);
+
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_UP:
+ case KeyCodes.DOM_VK_LEFT:
+ this.focusPrevItem();
+ return;
+ case KeyCodes.DOM_VK_DOWN:
+ case KeyCodes.DOM_VK_RIGHT:
+ this.focusNextItem();
+ return;
+ case KeyCodes.DOM_VK_PAGE_UP:
+ this.focusItemAtDelta(-(this.pageSize ||
+ (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
+ return;
+ case KeyCodes.DOM_VK_PAGE_DOWN:
+ this.focusItemAtDelta(+(this.pageSize ||
+ (this.itemCount / PAGE_SIZE_ITEM_COUNT_RATIO)));
+ return;
+ case KeyCodes.DOM_VK_HOME:
+ this.focusFirstVisibleItem();
+ return;
+ case KeyCodes.DOM_VK_END:
+ this.focusLastVisibleItem();
+ return;
+ }
+ },
+
+ /**
+ * The mousePress event listener for this container.
+ * @param string name
+ * @param MouseEvent event
+ */
+ _onWidgetMousePress: function (name, event) {
+ if (event.button != 0 && !this.allowFocusOnRightClick) {
+ // Only allow left-click to trigger this event.
+ return;
+ }
+
+ let item = this.getItemForElement(event.target);
+ if (item) {
+ // The container is not empty and we clicked on an actual item.
+ this.selectedItem = item;
+ // Make sure the current event's target element is also focused.
+ this.autoFocusOnInput && item._target.focus();
+ }
+ },
+
+ /**
+ * The predicate used when filtering items. By default, all items in this
+ * view are visible.
+ *
+ * @param Item item
+ * The item passing through the filter.
+ * @return boolean
+ * True if the item should be visible, false otherwise.
+ */
+ _currentFilterPredicate: function (item) {
+ return true;
+ },
+
+ /**
+ * The predicate used when sorting items. By default, items in this view
+ * are sorted by their label.
+ *
+ * @param Item first
+ * The first item used in the comparison.
+ * @param Item second
+ * The second item used in the comparison.
+ * @return number
+ * -1 to sort first to a lower index than second
+ * 0 to leave first and second unchanged with respect to each other
+ * 1 to sort second to a lower index than first
+ */
+ _currentSortPredicate: function (first, second) {
+ return +(first._value.toLowerCase() > second._value.toLowerCase());
+ },
+
+ /**
+ * Call a method on this widget named `methodName`. Any further arguments are
+ * passed on to the method. Returns the result of the method call.
+ *
+ * @param String methodName
+ * The name of the method you want to call.
+ * @param args
+ * Optional. Any arguments you want to pass through to the method.
+ */
+ callMethod: function (methodName, ...args) {
+ return this._widget[methodName].apply(this._widget, args);
+ },
+
+ _widget: null,
+ _emptyText: "",
+ _headerText: "",
+ _preferredValue: "",
+ _cachedCommandDispatcher: null
+};
+
+/**
+ * A generator-iterator over all the items in this container.
+ */
+Item.prototype[Symbol.iterator] =
+WidgetMethods[Symbol.iterator] = function* () {
+ yield* this._itemsByElement.values();
+};
diff --git a/devtools/client/shared/widgets/widgets.css b/devtools/client/shared/widgets/widgets.css
new file mode 100644
index 000000000..b979cf266
--- /dev/null
+++ b/devtools/client/shared/widgets/widgets.css
@@ -0,0 +1,109 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* BreacrumbsWidget */
+
+.breadcrumbs-widget-item {
+ direction: ltr;
+}
+
+.breadcrumbs-widget-item {
+ -moz-user-focus: normal;
+}
+
+/* SimpleListWidget */
+
+.simple-list-widget-container {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+/* FastListWidget */
+
+.fast-list-widget-container {
+ overflow: auto;
+}
+
+/* SideMenuWidget */
+
+.side-menu-widget-container {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.side-menu-widget-item-contents {
+ -moz-user-focus: normal;
+}
+
+.side-menu-widget-group-checkbox .checkbox-label-box,
+.side-menu-widget-item-checkbox .checkbox-label-box {
+ display: none; /* See bug 669507 */
+}
+
+/* VariablesView */
+
+.variables-view-container {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.variables-view-element-details:not([open]) {
+ display: none;
+}
+
+.variables-view-scope,
+.variable-or-property {
+ -moz-user-focus: normal;
+}
+
+.variables-view-scope > .title,
+.variable-or-property > .title {
+ overflow: hidden;
+}
+
+.variables-view-scope[untitled] > .title,
+.variable-or-property[untitled] > .title,
+.variable-or-property[unmatched] > .title {
+ display: none;
+}
+
+.variable-or-property:not([safe-getter]) > tooltip > label.WebIDL,
+.variable-or-property:not([overridden]) > tooltip > label.overridden,
+.variable-or-property:not([non-extensible]) > tooltip > label.extensible,
+.variable-or-property:not([frozen]) > tooltip > label.frozen,
+.variable-or-property:not([sealed]) > tooltip > label.sealed {
+ display: none;
+}
+
+.variable-or-property[pseudo-item] > tooltip,
+.variable-or-property[pseudo-item] > .title > .variables-view-edit,
+.variable-or-property[pseudo-item] > .title > .variables-view-delete,
+.variable-or-property[pseudo-item] > .title > .variables-view-add-property,
+.variable-or-property[pseudo-item] > .title > .variables-view-open-inspector,
+.variable-or-property[pseudo-item] > .title > .variable-or-property-frozen-label,
+.variable-or-property[pseudo-item] > .title > .variable-or-property-sealed-label,
+.variable-or-property[pseudo-item] > .title > .variable-or-property-non-extensible-label,
+.variable-or-property[pseudo-item] > .title > .variable-or-property-non-writable-icon {
+ display: none;
+}
+
+.variable-or-property > .title .toolbarbutton-text {
+ display: none;
+}
+
+*:not(:hover) .variables-view-delete,
+*:not(:hover) .variables-view-add-property,
+*:not(:hover) .variables-view-open-inspector {
+ visibility: hidden;
+}
+
+.variables-view-container[aligned-values] [optional-visibility] {
+ display: none;
+}
+
+/* Table Widget */
+.table-widget-body > .devtools-side-splitter:last-child {
+ display: none;
+}
diff --git a/devtools/client/shared/zoom-keys.js b/devtools/client/shared/zoom-keys.js
new file mode 100644
index 000000000..80f4386fb
--- /dev/null
+++ b/devtools/client/shared/zoom-keys.js
@@ -0,0 +1,85 @@
+/* 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 { Ci } = require("chrome");
+const Services = require("Services");
+const { KeyShortcuts } = require("devtools/client/shared/key-shortcuts");
+
+const ZOOM_PREF = "devtools.toolbox.zoomValue";
+const MIN_ZOOM = 0.5;
+const MAX_ZOOM = 2;
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+/**
+ * Register generic keys to control zoom level of the given document.
+ * Used by both the toolboxes and the browser console.
+ *
+ * @param {DOMWindow} The window on which we should listent to key strokes and
+ * modify the zoom factor.
+ */
+exports.register = function (window) {
+ let shortcuts = new KeyShortcuts({
+ window
+ });
+ let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ let contViewer = docShell.contentViewer;
+ let zoomValue = parseFloat(Services.prefs.getCharPref(ZOOM_PREF));
+ let zoomIn = function (name, event) {
+ setZoom(zoomValue + 0.1);
+ event.preventDefault();
+ };
+
+ let zoomOut = function (name, event) {
+ setZoom(zoomValue - 0.1);
+ event.preventDefault();
+ };
+
+ let zoomReset = function (name, event) {
+ setZoom(1);
+ event.preventDefault();
+ };
+
+ let setZoom = function (newValue) {
+ // cap zoom value
+ zoomValue = Math.max(newValue, MIN_ZOOM);
+ zoomValue = Math.min(zoomValue, MAX_ZOOM);
+
+ contViewer.fullZoom = zoomValue;
+
+ Services.prefs.setCharPref(ZOOM_PREF, zoomValue);
+ };
+
+ // Set zoom to whatever the last setting was.
+ setZoom(zoomValue);
+
+ shortcuts.on(L10N.getStr("toolbox.zoomIn.key"), zoomIn);
+ let zoomIn2 = L10N.getStr("toolbox.zoomIn2.key");
+ if (zoomIn2) {
+ shortcuts.on(zoomIn2, zoomIn);
+ }
+ let zoomIn3 = L10N.getStr("toolbox.zoomIn2.key");
+ if (zoomIn3) {
+ shortcuts.on(zoomIn3, zoomIn);
+ }
+
+ shortcuts.on(L10N.getStr("toolbox.zoomOut.key"),
+ zoomOut);
+ let zoomOut2 = L10N.getStr("toolbox.zoomOut2.key");
+ if (zoomOut2) {
+ shortcuts.on(zoomOut2, zoomOut);
+ }
+
+ shortcuts.on(L10N.getStr("toolbox.zoomReset.key"),
+ zoomReset);
+ let zoomReset2 = L10N.getStr("toolbox.zoomReset2.key");
+ if (zoomReset2) {
+ shortcuts.on(zoomReset2, zoomReset);
+ }
+};