From 39ba1643d2d6ebc746307b39ced282a1254a0b83 Mon Sep 17 00:00:00 2001 From: mgusmano Date: Fri, 23 Aug 2019 12:58:14 -0400 Subject: [PATCH] KS dist --- .gitignore | 2 +- demos/ext-grid-inline-1-prop/index.html | 70 +- .../dist/css.development.js | 389 + .../dist/css.production.js | 1 + .../dist/css.production.js.gz | Bin 0 -> 1409126 bytes .../dist/ewc.development.js | 6149 + .../dist/ewc.production.js | 1 + .../dist/ewc.production.js.gz | Bin 0 -> 9279 bytes .../dist/ext.development.js | 118065 +++++++++++++++ .../dist/ext.production.js | 1 + .../dist/ext.production.js.gz | Bin 0 -> 288676 bytes .../package.json | 12 - .../dist/Ext/AbstractComponent.js | 68 + .../dist/Ext/ActionSheet.js | 68 + packages/ext-web-components/dist/Ext/Audio.js | 81 + packages/ext-web-components/dist/Ext/Base.js | 146 + .../dist/Ext/BreadcrumbBar.js | 153 + .../ext-web-components/dist/Ext/Button.js | 188 + .../ext-web-components/dist/Ext/Carousel.js | 129 + packages/ext-web-components/dist/Ext/Chip.js | 75 + .../ext-web-components/dist/Ext/Component.js | 722 + .../ext-web-components/dist/Ext/Container.js | 372 + .../ext-web-components/dist/Ext/DataView.js | 219 + .../ext-web-components/dist/Ext/DatePicker.js | 139 + .../ext-web-components/dist/Ext/Decorator.js | 95 + .../ext-web-components/dist/Ext/Dialog.js | 147 + .../ext-web-components/dist/Ext/Editor.js | 197 + .../ext-web-components/dist/Ext/Evented.js | 68 + .../dist/Ext/EventedBase.js | 68 + .../ext-web-components/dist/Ext/Gadget.js | 484 + packages/ext-web-components/dist/Ext/Image.js | 111 + packages/ext-web-components/dist/Ext/Img.js | 111 + .../ext-web-components/dist/Ext/IndexBar.js | 114 + .../ext-web-components/dist/Ext/Indicator.js | 151 + packages/ext-web-components/dist/Ext/Label.js | 70 + packages/ext-web-components/dist/Ext/List.js | 316 + .../ext-web-components/dist/Ext/LoadMask.js | 88 + packages/ext-web-components/dist/Ext/Map.js | 280 + packages/ext-web-components/dist/Ext/Mask.js | 85 + packages/ext-web-components/dist/Ext/Media.js | 230 + .../ext-web-components/dist/Ext/MessageBox.js | 147 + packages/ext-web-components/dist/Ext/Msg.js | 68 + .../dist/Ext/NavigationView.js | 202 + .../ext-web-components/dist/Ext/NestedList.js | 276 + packages/ext-web-components/dist/Ext/Panel.js | 252 + .../ext-web-components/dist/Ext/Picker.js | 178 + .../ext-web-components/dist/Ext/Progress.js | 71 + .../dist/Ext/ProgressBarWidget.js | 71 + .../dist/Ext/SegmentedButton.js | 135 + packages/ext-web-components/dist/Ext/Sheet.js | 77 + .../ext-web-components/dist/Ext/Spacer.js | 76 + .../dist/Ext/SplitButton.js | 86 + packages/ext-web-components/dist/Ext/Tab.js | 96 + .../ext-web-components/dist/Ext/TabBar.js | 153 + .../ext-web-components/dist/Ext/TabPanel.js | 117 + packages/ext-web-components/dist/Ext/Title.js | 75 + .../ext-web-components/dist/Ext/TitleBar.js | 85 + packages/ext-web-components/dist/Ext/Toast.js | 126 + packages/ext-web-components/dist/Ext/Tool.js | 112 + .../ext-web-components/dist/Ext/Toolbar.js | 105 + packages/ext-web-components/dist/Ext/Video.js | 92 + .../ext-web-components/dist/Ext/Viewport.js | 68 + .../ext-web-components/dist/Ext/Widget.js | 484 + .../ext-web-components/dist/Ext/Window.js | 147 + .../dist/Ext/button/Segmented.js | 135 + .../dist/Ext/calendar/AbstractList.js | 68 + .../dist/Ext/calendar/Event.js | 70 + .../dist/Ext/calendar/EventBase.js | 84 + .../dist/Ext/calendar/List.js | 70 + .../dist/Ext/calendar/form/AbstractForm.js | 80 + .../dist/Ext/calendar/form/Add.js | 70 + .../dist/Ext/calendar/form/CalendarPicker.js | 68 + .../dist/Ext/calendar/form/Edit.js | 70 + .../dist/Ext/calendar/form/Form.js | 71 + .../dist/Ext/calendar/form/TimeField.js | 68 + .../dist/Ext/calendar/header/Base.js | 75 + .../dist/Ext/calendar/header/Days.js | 71 + .../dist/Ext/calendar/header/Weeks.js | 70 + .../dist/Ext/calendar/panel/AbstractBase.js | 68 + .../dist/Ext/calendar/panel/AbstractPanel.js | 91 + .../dist/Ext/calendar/panel/Base.js | 264 + .../dist/Ext/calendar/panel/Day.js | 71 + .../dist/Ext/calendar/panel/Days.js | 155 + .../dist/Ext/calendar/panel/Month.js | 92 + .../dist/Ext/calendar/panel/Panel.js | 200 + .../dist/Ext/calendar/panel/Week.js | 73 + .../dist/Ext/calendar/panel/Weeks.js | 118 + .../dist/Ext/calendar/view/Base.js | 475 + .../dist/Ext/calendar/view/Day.js | 68 + .../dist/Ext/calendar/view/Days.js | 337 + .../dist/Ext/calendar/view/Month.js | 101 + .../dist/Ext/calendar/view/Multi.js | 97 + .../dist/Ext/calendar/view/Week.js | 87 + .../dist/Ext/calendar/view/Weeks.js | 292 + .../dist/Ext/carousel/Carousel.js | 129 + .../dist/Ext/carousel/Infinite.js | 68 + .../dist/Ext/carousel/Item.js | 68 + .../dist/Ext/chart/AbstractChart.js | 369 + .../dist/Ext/chart/CartesianChart.js | 76 + .../dist/Ext/chart/Chart.js | 76 + .../dist/Ext/chart/Legend.js | 72 + .../dist/Ext/chart/PolarChart.js | 72 + .../dist/Ext/chart/SpaceFillingChart.js | 68 + .../dist/Ext/chart/legend/Legend.js | 72 + .../dist/Ext/chart/legend/LegendBase.js | 68 + .../dist/Ext/chart/navigator/Container.js | 71 + .../dist/Ext/chart/navigator/ContainerBase.js | 68 + .../dist/Ext/chart/navigator/Navigator.js | 88 + .../dist/Ext/chart/navigator/NavigatorBase.js | 68 + .../dist/Ext/container/Container.js | 372 + .../dist/Ext/d3/Component.js | 93 + .../dist/Ext/d3/ComponentBase.js | 68 + .../ext-web-components/dist/Ext/d3/HeatMap.js | 80 + .../dist/Ext/d3/canvas/Canvas.js | 107 + .../dist/Ext/d3/hierarchy/Hierarchy.js | 205 + .../dist/Ext/d3/hierarchy/Pack.js | 78 + .../dist/Ext/d3/hierarchy/TreeMap.js | 104 + .../Ext/d3/hierarchy/partition/Partition.js | 73 + .../Ext/d3/hierarchy/partition/Sunburst.js | 96 + .../Ext/d3/hierarchy/tree/HorizontalTree.js | 78 + .../dist/Ext/d3/hierarchy/tree/Tree.js | 72 + .../ext-web-components/dist/Ext/d3/svg/Svg.js | 119 + .../dist/Ext/dataview/Abstract.js | 436 + .../dist/Ext/dataview/BoundList.js | 75 + .../dist/Ext/dataview/ChipView.js | 79 + .../dist/Ext/dataview/Component.js | 202 + .../dist/Ext/dataview/DataItem.js | 87 + .../dist/Ext/dataview/DataView.js | 219 + .../dist/Ext/dataview/EmptyText.js | 68 + .../dist/Ext/dataview/IndexBar.js | 114 + .../dist/Ext/dataview/ItemHeader.js | 71 + .../dist/Ext/dataview/List.js | 316 + .../dist/Ext/dataview/ListItem.js | 68 + .../dist/Ext/dataview/ListItemPlaceholder.js | 68 + .../dist/Ext/dataview/NestedList.js | 276 + .../dist/Ext/dataview/SimpleListItem.js | 68 + .../dist/Ext/dataview/component/DataItem.js | 87 + .../dist/Ext/dataview/component/ListItem.js | 68 + .../Ext/dataview/component/SimpleListItem.js | 68 + .../dist/Ext/dataview/listswiper/Item.js | 73 + .../dist/Ext/dataview/listswiper/Stepper.js | 85 + .../dist/Ext/dataview/plugin/ItemTip.js | 70 + .../dist/Ext/dataview/pullrefresh/Bar.js | 76 + .../dist/Ext/dataview/pullrefresh/Item.js | 72 + .../dist/Ext/dataview/pullrefresh/Spinner.js | 68 + .../dist/Ext/draw/Component.js | 214 + .../dist/Ext/draw/Container.js | 214 + .../dist/Ext/draw/ContainerBase.js | 68 + .../dist/Ext/draw/Surface.js | 189 + .../dist/Ext/draw/SurfaceBase.js | 68 + .../dist/Ext/draw/engine/Canvas.js | 276 + .../dist/Ext/draw/engine/Svg.js | 120 + .../dist/Ext/field/Checkbox.js | 163 + .../dist/Ext/field/CheckboxGroup.js | 78 + .../dist/Ext/field/ComboBox.js | 175 + .../dist/Ext/field/Container.js | 132 + .../ext-web-components/dist/Ext/field/Date.js | 101 + .../dist/Ext/field/DatePicker.js | 101 + .../dist/Ext/field/DatePickerNative.js | 68 + .../dist/Ext/field/Display.js | 79 + .../dist/Ext/field/Email.js | 71 + .../dist/Ext/field/Field.js | 259 + .../dist/Ext/field/FieldGroupContainer.js | 115 + .../ext-web-components/dist/Ext/field/File.js | 82 + .../dist/Ext/field/FileButton.js | 89 + .../dist/Ext/field/Hidden.js | 70 + .../dist/Ext/field/Input.js | 77 + .../dist/Ext/field/Number.js | 83 + .../dist/Ext/field/Panel.js | 100 + .../dist/Ext/field/Password.js | 78 + .../dist/Ext/field/Picker.js | 126 + .../dist/Ext/field/Radio.js | 88 + .../dist/Ext/field/RadioGroup.js | 80 + .../dist/Ext/field/Search.js | 68 + .../dist/Ext/field/Select.js | 220 + .../dist/Ext/field/SingleSlider.js | 127 + .../dist/Ext/field/Slider.js | 150 + .../dist/Ext/field/Spinner.js | 145 + .../ext-web-components/dist/Ext/field/Text.js | 239 + .../dist/Ext/field/TextArea.js | 72 + .../ext-web-components/dist/Ext/field/Time.js | 76 + .../dist/Ext/field/Toggle.js | 140 + .../ext-web-components/dist/Ext/field/Url.js | 71 + .../dist/Ext/field/trigger/Base.js | 74 + .../dist/Ext/field/trigger/Clear.js | 68 + .../dist/Ext/field/trigger/Component.js | 70 + .../dist/Ext/field/trigger/Date.js | 68 + .../dist/Ext/field/trigger/Expand.js | 68 + .../dist/Ext/field/trigger/Menu.js | 72 + .../dist/Ext/field/trigger/Reveal.js | 68 + .../dist/Ext/field/trigger/SpinDown.js | 68 + .../dist/Ext/field/trigger/SpinUp.js | 68 + .../dist/Ext/field/trigger/Time.js | 68 + .../dist/Ext/field/trigger/Trigger.js | 74 + .../dist/Ext/form/Checkbox.js | 163 + .../dist/Ext/form/DatePicker.js | 101 + .../dist/Ext/form/DatePickerNative.js | 68 + .../dist/Ext/form/Display.js | 79 + .../ext-web-components/dist/Ext/form/Email.js | 71 + .../ext-web-components/dist/Ext/form/Field.js | 259 + .../dist/Ext/form/FieldSet.js | 107 + .../dist/Ext/form/FormPanel.js | 150 + .../dist/Ext/form/Hidden.js | 70 + .../dist/Ext/form/Number.js | 83 + .../ext-web-components/dist/Ext/form/Panel.js | 150 + .../dist/Ext/form/Password.js | 78 + .../ext-web-components/dist/Ext/form/Radio.js | 88 + .../dist/Ext/form/Search.js | 68 + .../dist/Ext/form/Select.js | 220 + .../dist/Ext/form/Slider.js | 150 + .../dist/Ext/form/Spinner.js | 145 + .../ext-web-components/dist/Ext/form/Text.js | 239 + .../dist/Ext/form/TextArea.js | 72 + .../dist/Ext/form/Toggle.js | 140 + .../ext-web-components/dist/Ext/form/Url.js | 71 + .../dist/Ext/form/field/ComboBox.js | 175 + .../dist/Ext/froala/Editor.js | 90 + .../dist/Ext/froala/EditorField.js | 90 + .../dist/Ext/grid/CellEditor.js | 80 + .../ext-web-components/dist/Ext/grid/Grid.js | 361 + .../dist/Ext/grid/HeaderContainer.js | 99 + .../dist/Ext/grid/LockedGrid.js | 82 + .../dist/Ext/grid/LockedGridRegion.js | 73 + .../dist/Ext/grid/PagingToolbar.js | 73 + .../ext-web-components/dist/Ext/grid/Row.js | 88 + .../dist/Ext/grid/RowBody.js | 68 + .../dist/Ext/grid/RowHeader.js | 68 + .../dist/Ext/grid/SummaryRow.js | 68 + .../ext-web-components/dist/Ext/grid/Tree.js | 202 + .../dist/Ext/grid/cell/Base.js | 84 + .../dist/Ext/grid/cell/Boolean.js | 72 + .../dist/Ext/grid/cell/Cell.js | 73 + .../dist/Ext/grid/cell/Check.js | 80 + .../dist/Ext/grid/cell/Date.js | 70 + .../dist/Ext/grid/cell/Number.js | 70 + .../dist/Ext/grid/cell/RowNumberer.js | 70 + .../dist/Ext/grid/cell/Text.js | 72 + .../dist/Ext/grid/cell/Tree.js | 126 + .../dist/Ext/grid/cell/Widget.js | 73 + .../dist/Ext/grid/column/Boolean.js | 72 + .../dist/Ext/grid/column/Check.js | 98 + .../dist/Ext/grid/column/Column.js | 152 + .../dist/Ext/grid/column/Date.js | 70 + .../dist/Ext/grid/column/Drag.js | 73 + .../dist/Ext/grid/column/Number.js | 70 + .../dist/Ext/grid/column/RowNumberer.js | 70 + .../dist/Ext/grid/column/Selection.js | 70 + .../dist/Ext/grid/column/Template.js | 152 + .../dist/Ext/grid/column/Text.js | 68 + .../dist/Ext/grid/column/Tree.js | 68 + .../dist/Ext/grid/filters/menu/Base.js | 70 + .../dist/Ext/grid/filters/menu/Boolean.js | 68 + .../dist/Ext/grid/filters/menu/Date.js | 68 + .../dist/Ext/grid/filters/menu/Number.js | 68 + .../dist/Ext/grid/filters/menu/String.js | 68 + .../dist/Ext/grid/locked/Grid.js | 82 + .../dist/Ext/grid/locked/Region.js | 73 + .../dist/Ext/grid/menu/Columns.js | 70 + .../dist/Ext/grid/menu/GroupByThis.js | 70 + .../dist/Ext/grid/menu/Shared.js | 68 + .../dist/Ext/grid/menu/ShowInGroups.js | 70 + .../dist/Ext/grid/menu/SortAsc.js | 70 + .../dist/Ext/grid/menu/SortDesc.js | 70 + .../dist/Ext/grid/plugin/ColumnResizing.js | 70 + .../dist/Ext/grid/rowedit/Bar.js | 68 + .../dist/Ext/grid/rowedit/Cell.js | 68 + .../dist/Ext/grid/rowedit/Editor.js | 118 + .../dist/Ext/grid/rowedit/Gap.js | 68 + .../dist/Ext/lib/Component.js | 722 + .../dist/Ext/lib/Container.js | 372 + .../dist/Ext/list/AbstractTreeItem.js | 213 + .../dist/Ext/list/RootTreeItem.js | 68 + .../ext-web-components/dist/Ext/list/Tree.js | 185 + .../dist/Ext/list/TreeItem.js | 71 + .../dist/Ext/menu/CheckItem.js | 104 + .../ext-web-components/dist/Ext/menu/Item.js | 101 + .../ext-web-components/dist/Ext/menu/Menu.js | 103 + .../dist/Ext/menu/RadioItem.js | 72 + .../dist/Ext/menu/Separator.js | 68 + .../dist/Ext/menu/TextItem.js | 101 + .../dist/Ext/navigation/Bar.js | 157 + .../dist/Ext/navigation/View.js | 202 + .../dist/Ext/panel/Accordion.js | 72 + .../ext-web-components/dist/Ext/panel/Date.js | 104 + .../dist/Ext/panel/DateTitle.js | 68 + .../dist/Ext/panel/Header.js | 77 + .../dist/Ext/panel/Panel.js | 252 + .../ext-web-components/dist/Ext/panel/Time.js | 82 + .../dist/Ext/panel/Title.js | 78 + .../ext-web-components/dist/Ext/panel/Tool.js | 112 + .../dist/Ext/panel/YearPicker.js | 68 + .../dist/Ext/picker/Date.js | 139 + .../dist/Ext/picker/Picker.js | 178 + .../dist/Ext/picker/SelectPicker.js | 68 + .../dist/Ext/picker/Slot.js | 153 + .../dist/Ext/picker/Tablet.js | 68 + .../ext-web-components/dist/Ext/pivot/Grid.js | 586 + .../ext-web-components/dist/Ext/pivot/Row.js | 68 + .../dist/Ext/pivot/cell/Cell.js | 68 + .../dist/Ext/pivot/cell/Group.js | 68 + .../dist/Ext/pivot/d3/AbstractContainer.js | 72 + .../dist/Ext/pivot/d3/Container.js | 68 + .../dist/Ext/pivot/d3/HeatMap.js | 71 + .../dist/Ext/pivot/d3/TreeMap.js | 71 + .../Ext/pivot/plugin/configurator/Column.js | 73 + .../pivot/plugin/configurator/Container.js | 80 + .../Ext/pivot/plugin/configurator/Form.js | 75 + .../Ext/pivot/plugin/configurator/Panel.js | 147 + .../Ext/pivot/plugin/configurator/Settings.js | 75 + .../Ext/pivot/plugin/rangeeditor/Panel.js | 68 + .../dist/Ext/scroll/indicator/Bar.js | 70 + .../dist/Ext/scroll/indicator/Indicator.js | 73 + .../dist/Ext/scroll/indicator/Overlay.js | 97 + .../dist/Ext/slider/Slider.js | 213 + .../dist/Ext/slider/Thumb.js | 72 + .../dist/Ext/slider/Toggle.js | 70 + .../dist/Ext/sparkline/Bar.js | 82 + .../dist/Ext/sparkline/BarBase.js | 68 + .../dist/Ext/sparkline/Base.js | 88 + .../dist/Ext/sparkline/Box.js | 83 + .../dist/Ext/sparkline/Bullet.js | 74 + .../dist/Ext/sparkline/Discrete.js | 75 + .../dist/Ext/sparkline/Line.js | 86 + .../dist/Ext/sparkline/Pie.js | 73 + .../dist/Ext/sparkline/TriState.js | 75 + .../ext-web-components/dist/Ext/tab/Bar.js | 153 + .../ext-web-components/dist/Ext/tab/Panel.js | 117 + .../ext-web-components/dist/Ext/tab/Tab.js | 96 + .../dist/Ext/tip/ToolTip.js | 93 + .../ext-web-components/dist/Ext/tree/Tree.js | 202 + .../ext-web-components/dist/Ext/ux/Gauge.js | 144 + .../dist/Ext/ux/colorpick/Button.js | 81 + .../dist/Ext/ux/colorpick/ColorPreview.js | 68 + .../dist/Ext/ux/colorpick/Field.js | 81 + .../dist/Ext/ux/colorpick/Selector.js | 108 + .../dist/Ext/ux/gauge/Gauge.js | 144 + .../dist/Ext/ux/google/Map.js | 280 + .../dist/Ext/ux/rating/Picker.js | 113 + .../dist/Ext/viewport/Android.js | 68 + .../dist/Ext/viewport/Default.js | 202 + .../dist/Ext/viewport/Ios.js | 68 + .../dist/Ext/viewport/WP.js | 68 + .../dist/Ext/viewport/WindowsPhone.js | 68 + .../dist/Ext/window/Window.js | 147 + .../dist/ewc-base.component.js | 477 + .../dist/ext-router.component.js | 229 + 346 files changed, 165040 insertions(+), 16 deletions(-) create mode 100644 packages/ext-web-components-bundler/dist/css.development.js create mode 100644 packages/ext-web-components-bundler/dist/css.production.js create mode 100644 packages/ext-web-components-bundler/dist/css.production.js.gz create mode 100644 packages/ext-web-components-bundler/dist/ewc.development.js create mode 100644 packages/ext-web-components-bundler/dist/ewc.production.js create mode 100644 packages/ext-web-components-bundler/dist/ewc.production.js.gz create mode 100644 packages/ext-web-components-bundler/dist/ext.development.js create mode 100644 packages/ext-web-components-bundler/dist/ext.production.js create mode 100644 packages/ext-web-components-bundler/dist/ext.production.js.gz create mode 100644 packages/ext-web-components/dist/Ext/AbstractComponent.js create mode 100644 packages/ext-web-components/dist/Ext/ActionSheet.js create mode 100644 packages/ext-web-components/dist/Ext/Audio.js create mode 100644 packages/ext-web-components/dist/Ext/Base.js create mode 100644 packages/ext-web-components/dist/Ext/BreadcrumbBar.js create mode 100644 packages/ext-web-components/dist/Ext/Button.js create mode 100644 packages/ext-web-components/dist/Ext/Carousel.js create mode 100644 packages/ext-web-components/dist/Ext/Chip.js create mode 100644 packages/ext-web-components/dist/Ext/Component.js create mode 100644 packages/ext-web-components/dist/Ext/Container.js create mode 100644 packages/ext-web-components/dist/Ext/DataView.js create mode 100644 packages/ext-web-components/dist/Ext/DatePicker.js create mode 100644 packages/ext-web-components/dist/Ext/Decorator.js create mode 100644 packages/ext-web-components/dist/Ext/Dialog.js create mode 100644 packages/ext-web-components/dist/Ext/Editor.js create mode 100644 packages/ext-web-components/dist/Ext/Evented.js create mode 100644 packages/ext-web-components/dist/Ext/EventedBase.js create mode 100644 packages/ext-web-components/dist/Ext/Gadget.js create mode 100644 packages/ext-web-components/dist/Ext/Image.js create mode 100644 packages/ext-web-components/dist/Ext/Img.js create mode 100644 packages/ext-web-components/dist/Ext/IndexBar.js create mode 100644 packages/ext-web-components/dist/Ext/Indicator.js create mode 100644 packages/ext-web-components/dist/Ext/Label.js create mode 100644 packages/ext-web-components/dist/Ext/List.js create mode 100644 packages/ext-web-components/dist/Ext/LoadMask.js create mode 100644 packages/ext-web-components/dist/Ext/Map.js create mode 100644 packages/ext-web-components/dist/Ext/Mask.js create mode 100644 packages/ext-web-components/dist/Ext/Media.js create mode 100644 packages/ext-web-components/dist/Ext/MessageBox.js create mode 100644 packages/ext-web-components/dist/Ext/Msg.js create mode 100644 packages/ext-web-components/dist/Ext/NavigationView.js create mode 100644 packages/ext-web-components/dist/Ext/NestedList.js create mode 100644 packages/ext-web-components/dist/Ext/Panel.js create mode 100644 packages/ext-web-components/dist/Ext/Picker.js create mode 100644 packages/ext-web-components/dist/Ext/Progress.js create mode 100644 packages/ext-web-components/dist/Ext/ProgressBarWidget.js create mode 100644 packages/ext-web-components/dist/Ext/SegmentedButton.js create mode 100644 packages/ext-web-components/dist/Ext/Sheet.js create mode 100644 packages/ext-web-components/dist/Ext/Spacer.js create mode 100644 packages/ext-web-components/dist/Ext/SplitButton.js create mode 100644 packages/ext-web-components/dist/Ext/Tab.js create mode 100644 packages/ext-web-components/dist/Ext/TabBar.js create mode 100644 packages/ext-web-components/dist/Ext/TabPanel.js create mode 100644 packages/ext-web-components/dist/Ext/Title.js create mode 100644 packages/ext-web-components/dist/Ext/TitleBar.js create mode 100644 packages/ext-web-components/dist/Ext/Toast.js create mode 100644 packages/ext-web-components/dist/Ext/Tool.js create mode 100644 packages/ext-web-components/dist/Ext/Toolbar.js create mode 100644 packages/ext-web-components/dist/Ext/Video.js create mode 100644 packages/ext-web-components/dist/Ext/Viewport.js create mode 100644 packages/ext-web-components/dist/Ext/Widget.js create mode 100644 packages/ext-web-components/dist/Ext/Window.js create mode 100644 packages/ext-web-components/dist/Ext/button/Segmented.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/AbstractList.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/Event.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/EventBase.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/List.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/form/AbstractForm.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/form/Add.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/form/CalendarPicker.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/form/Edit.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/form/Form.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/form/TimeField.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/header/Base.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/header/Days.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/header/Weeks.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/panel/AbstractBase.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/panel/AbstractPanel.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/panel/Base.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/panel/Day.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/panel/Days.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/panel/Month.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/panel/Panel.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/panel/Week.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/panel/Weeks.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/view/Base.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/view/Day.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/view/Days.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/view/Month.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/view/Multi.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/view/Week.js create mode 100644 packages/ext-web-components/dist/Ext/calendar/view/Weeks.js create mode 100644 packages/ext-web-components/dist/Ext/carousel/Carousel.js create mode 100644 packages/ext-web-components/dist/Ext/carousel/Infinite.js create mode 100644 packages/ext-web-components/dist/Ext/carousel/Item.js create mode 100644 packages/ext-web-components/dist/Ext/chart/AbstractChart.js create mode 100644 packages/ext-web-components/dist/Ext/chart/CartesianChart.js create mode 100644 packages/ext-web-components/dist/Ext/chart/Chart.js create mode 100644 packages/ext-web-components/dist/Ext/chart/Legend.js create mode 100644 packages/ext-web-components/dist/Ext/chart/PolarChart.js create mode 100644 packages/ext-web-components/dist/Ext/chart/SpaceFillingChart.js create mode 100644 packages/ext-web-components/dist/Ext/chart/legend/Legend.js create mode 100644 packages/ext-web-components/dist/Ext/chart/legend/LegendBase.js create mode 100644 packages/ext-web-components/dist/Ext/chart/navigator/Container.js create mode 100644 packages/ext-web-components/dist/Ext/chart/navigator/ContainerBase.js create mode 100644 packages/ext-web-components/dist/Ext/chart/navigator/Navigator.js create mode 100644 packages/ext-web-components/dist/Ext/chart/navigator/NavigatorBase.js create mode 100644 packages/ext-web-components/dist/Ext/container/Container.js create mode 100644 packages/ext-web-components/dist/Ext/d3/Component.js create mode 100644 packages/ext-web-components/dist/Ext/d3/ComponentBase.js create mode 100644 packages/ext-web-components/dist/Ext/d3/HeatMap.js create mode 100644 packages/ext-web-components/dist/Ext/d3/canvas/Canvas.js create mode 100644 packages/ext-web-components/dist/Ext/d3/hierarchy/Hierarchy.js create mode 100644 packages/ext-web-components/dist/Ext/d3/hierarchy/Pack.js create mode 100644 packages/ext-web-components/dist/Ext/d3/hierarchy/TreeMap.js create mode 100644 packages/ext-web-components/dist/Ext/d3/hierarchy/partition/Partition.js create mode 100644 packages/ext-web-components/dist/Ext/d3/hierarchy/partition/Sunburst.js create mode 100644 packages/ext-web-components/dist/Ext/d3/hierarchy/tree/HorizontalTree.js create mode 100644 packages/ext-web-components/dist/Ext/d3/hierarchy/tree/Tree.js create mode 100644 packages/ext-web-components/dist/Ext/d3/svg/Svg.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/Abstract.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/BoundList.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/ChipView.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/Component.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/DataItem.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/DataView.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/EmptyText.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/IndexBar.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/ItemHeader.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/List.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/ListItem.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/ListItemPlaceholder.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/NestedList.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/SimpleListItem.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/component/DataItem.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/component/ListItem.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/component/SimpleListItem.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/listswiper/Item.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/listswiper/Stepper.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/plugin/ItemTip.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/pullrefresh/Bar.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/pullrefresh/Item.js create mode 100644 packages/ext-web-components/dist/Ext/dataview/pullrefresh/Spinner.js create mode 100644 packages/ext-web-components/dist/Ext/draw/Component.js create mode 100644 packages/ext-web-components/dist/Ext/draw/Container.js create mode 100644 packages/ext-web-components/dist/Ext/draw/ContainerBase.js create mode 100644 packages/ext-web-components/dist/Ext/draw/Surface.js create mode 100644 packages/ext-web-components/dist/Ext/draw/SurfaceBase.js create mode 100644 packages/ext-web-components/dist/Ext/draw/engine/Canvas.js create mode 100644 packages/ext-web-components/dist/Ext/draw/engine/Svg.js create mode 100644 packages/ext-web-components/dist/Ext/field/Checkbox.js create mode 100644 packages/ext-web-components/dist/Ext/field/CheckboxGroup.js create mode 100644 packages/ext-web-components/dist/Ext/field/ComboBox.js create mode 100644 packages/ext-web-components/dist/Ext/field/Container.js create mode 100644 packages/ext-web-components/dist/Ext/field/Date.js create mode 100644 packages/ext-web-components/dist/Ext/field/DatePicker.js create mode 100644 packages/ext-web-components/dist/Ext/field/DatePickerNative.js create mode 100644 packages/ext-web-components/dist/Ext/field/Display.js create mode 100644 packages/ext-web-components/dist/Ext/field/Email.js create mode 100644 packages/ext-web-components/dist/Ext/field/Field.js create mode 100644 packages/ext-web-components/dist/Ext/field/FieldGroupContainer.js create mode 100644 packages/ext-web-components/dist/Ext/field/File.js create mode 100644 packages/ext-web-components/dist/Ext/field/FileButton.js create mode 100644 packages/ext-web-components/dist/Ext/field/Hidden.js create mode 100644 packages/ext-web-components/dist/Ext/field/Input.js create mode 100644 packages/ext-web-components/dist/Ext/field/Number.js create mode 100644 packages/ext-web-components/dist/Ext/field/Panel.js create mode 100644 packages/ext-web-components/dist/Ext/field/Password.js create mode 100644 packages/ext-web-components/dist/Ext/field/Picker.js create mode 100644 packages/ext-web-components/dist/Ext/field/Radio.js create mode 100644 packages/ext-web-components/dist/Ext/field/RadioGroup.js create mode 100644 packages/ext-web-components/dist/Ext/field/Search.js create mode 100644 packages/ext-web-components/dist/Ext/field/Select.js create mode 100644 packages/ext-web-components/dist/Ext/field/SingleSlider.js create mode 100644 packages/ext-web-components/dist/Ext/field/Slider.js create mode 100644 packages/ext-web-components/dist/Ext/field/Spinner.js create mode 100644 packages/ext-web-components/dist/Ext/field/Text.js create mode 100644 packages/ext-web-components/dist/Ext/field/TextArea.js create mode 100644 packages/ext-web-components/dist/Ext/field/Time.js create mode 100644 packages/ext-web-components/dist/Ext/field/Toggle.js create mode 100644 packages/ext-web-components/dist/Ext/field/Url.js create mode 100644 packages/ext-web-components/dist/Ext/field/trigger/Base.js create mode 100644 packages/ext-web-components/dist/Ext/field/trigger/Clear.js create mode 100644 packages/ext-web-components/dist/Ext/field/trigger/Component.js create mode 100644 packages/ext-web-components/dist/Ext/field/trigger/Date.js create mode 100644 packages/ext-web-components/dist/Ext/field/trigger/Expand.js create mode 100644 packages/ext-web-components/dist/Ext/field/trigger/Menu.js create mode 100644 packages/ext-web-components/dist/Ext/field/trigger/Reveal.js create mode 100644 packages/ext-web-components/dist/Ext/field/trigger/SpinDown.js create mode 100644 packages/ext-web-components/dist/Ext/field/trigger/SpinUp.js create mode 100644 packages/ext-web-components/dist/Ext/field/trigger/Time.js create mode 100644 packages/ext-web-components/dist/Ext/field/trigger/Trigger.js create mode 100644 packages/ext-web-components/dist/Ext/form/Checkbox.js create mode 100644 packages/ext-web-components/dist/Ext/form/DatePicker.js create mode 100644 packages/ext-web-components/dist/Ext/form/DatePickerNative.js create mode 100644 packages/ext-web-components/dist/Ext/form/Display.js create mode 100644 packages/ext-web-components/dist/Ext/form/Email.js create mode 100644 packages/ext-web-components/dist/Ext/form/Field.js create mode 100644 packages/ext-web-components/dist/Ext/form/FieldSet.js create mode 100644 packages/ext-web-components/dist/Ext/form/FormPanel.js create mode 100644 packages/ext-web-components/dist/Ext/form/Hidden.js create mode 100644 packages/ext-web-components/dist/Ext/form/Number.js create mode 100644 packages/ext-web-components/dist/Ext/form/Panel.js create mode 100644 packages/ext-web-components/dist/Ext/form/Password.js create mode 100644 packages/ext-web-components/dist/Ext/form/Radio.js create mode 100644 packages/ext-web-components/dist/Ext/form/Search.js create mode 100644 packages/ext-web-components/dist/Ext/form/Select.js create mode 100644 packages/ext-web-components/dist/Ext/form/Slider.js create mode 100644 packages/ext-web-components/dist/Ext/form/Spinner.js create mode 100644 packages/ext-web-components/dist/Ext/form/Text.js create mode 100644 packages/ext-web-components/dist/Ext/form/TextArea.js create mode 100644 packages/ext-web-components/dist/Ext/form/Toggle.js create mode 100644 packages/ext-web-components/dist/Ext/form/Url.js create mode 100644 packages/ext-web-components/dist/Ext/form/field/ComboBox.js create mode 100644 packages/ext-web-components/dist/Ext/froala/Editor.js create mode 100644 packages/ext-web-components/dist/Ext/froala/EditorField.js create mode 100644 packages/ext-web-components/dist/Ext/grid/CellEditor.js create mode 100644 packages/ext-web-components/dist/Ext/grid/Grid.js create mode 100644 packages/ext-web-components/dist/Ext/grid/HeaderContainer.js create mode 100644 packages/ext-web-components/dist/Ext/grid/LockedGrid.js create mode 100644 packages/ext-web-components/dist/Ext/grid/LockedGridRegion.js create mode 100644 packages/ext-web-components/dist/Ext/grid/PagingToolbar.js create mode 100644 packages/ext-web-components/dist/Ext/grid/Row.js create mode 100644 packages/ext-web-components/dist/Ext/grid/RowBody.js create mode 100644 packages/ext-web-components/dist/Ext/grid/RowHeader.js create mode 100644 packages/ext-web-components/dist/Ext/grid/SummaryRow.js create mode 100644 packages/ext-web-components/dist/Ext/grid/Tree.js create mode 100644 packages/ext-web-components/dist/Ext/grid/cell/Base.js create mode 100644 packages/ext-web-components/dist/Ext/grid/cell/Boolean.js create mode 100644 packages/ext-web-components/dist/Ext/grid/cell/Cell.js create mode 100644 packages/ext-web-components/dist/Ext/grid/cell/Check.js create mode 100644 packages/ext-web-components/dist/Ext/grid/cell/Date.js create mode 100644 packages/ext-web-components/dist/Ext/grid/cell/Number.js create mode 100644 packages/ext-web-components/dist/Ext/grid/cell/RowNumberer.js create mode 100644 packages/ext-web-components/dist/Ext/grid/cell/Text.js create mode 100644 packages/ext-web-components/dist/Ext/grid/cell/Tree.js create mode 100644 packages/ext-web-components/dist/Ext/grid/cell/Widget.js create mode 100644 packages/ext-web-components/dist/Ext/grid/column/Boolean.js create mode 100644 packages/ext-web-components/dist/Ext/grid/column/Check.js create mode 100644 packages/ext-web-components/dist/Ext/grid/column/Column.js create mode 100644 packages/ext-web-components/dist/Ext/grid/column/Date.js create mode 100644 packages/ext-web-components/dist/Ext/grid/column/Drag.js create mode 100644 packages/ext-web-components/dist/Ext/grid/column/Number.js create mode 100644 packages/ext-web-components/dist/Ext/grid/column/RowNumberer.js create mode 100644 packages/ext-web-components/dist/Ext/grid/column/Selection.js create mode 100644 packages/ext-web-components/dist/Ext/grid/column/Template.js create mode 100644 packages/ext-web-components/dist/Ext/grid/column/Text.js create mode 100644 packages/ext-web-components/dist/Ext/grid/column/Tree.js create mode 100644 packages/ext-web-components/dist/Ext/grid/filters/menu/Base.js create mode 100644 packages/ext-web-components/dist/Ext/grid/filters/menu/Boolean.js create mode 100644 packages/ext-web-components/dist/Ext/grid/filters/menu/Date.js create mode 100644 packages/ext-web-components/dist/Ext/grid/filters/menu/Number.js create mode 100644 packages/ext-web-components/dist/Ext/grid/filters/menu/String.js create mode 100644 packages/ext-web-components/dist/Ext/grid/locked/Grid.js create mode 100644 packages/ext-web-components/dist/Ext/grid/locked/Region.js create mode 100644 packages/ext-web-components/dist/Ext/grid/menu/Columns.js create mode 100644 packages/ext-web-components/dist/Ext/grid/menu/GroupByThis.js create mode 100644 packages/ext-web-components/dist/Ext/grid/menu/Shared.js create mode 100644 packages/ext-web-components/dist/Ext/grid/menu/ShowInGroups.js create mode 100644 packages/ext-web-components/dist/Ext/grid/menu/SortAsc.js create mode 100644 packages/ext-web-components/dist/Ext/grid/menu/SortDesc.js create mode 100644 packages/ext-web-components/dist/Ext/grid/plugin/ColumnResizing.js create mode 100644 packages/ext-web-components/dist/Ext/grid/rowedit/Bar.js create mode 100644 packages/ext-web-components/dist/Ext/grid/rowedit/Cell.js create mode 100644 packages/ext-web-components/dist/Ext/grid/rowedit/Editor.js create mode 100644 packages/ext-web-components/dist/Ext/grid/rowedit/Gap.js create mode 100644 packages/ext-web-components/dist/Ext/lib/Component.js create mode 100644 packages/ext-web-components/dist/Ext/lib/Container.js create mode 100644 packages/ext-web-components/dist/Ext/list/AbstractTreeItem.js create mode 100644 packages/ext-web-components/dist/Ext/list/RootTreeItem.js create mode 100644 packages/ext-web-components/dist/Ext/list/Tree.js create mode 100644 packages/ext-web-components/dist/Ext/list/TreeItem.js create mode 100644 packages/ext-web-components/dist/Ext/menu/CheckItem.js create mode 100644 packages/ext-web-components/dist/Ext/menu/Item.js create mode 100644 packages/ext-web-components/dist/Ext/menu/Menu.js create mode 100644 packages/ext-web-components/dist/Ext/menu/RadioItem.js create mode 100644 packages/ext-web-components/dist/Ext/menu/Separator.js create mode 100644 packages/ext-web-components/dist/Ext/menu/TextItem.js create mode 100644 packages/ext-web-components/dist/Ext/navigation/Bar.js create mode 100644 packages/ext-web-components/dist/Ext/navigation/View.js create mode 100644 packages/ext-web-components/dist/Ext/panel/Accordion.js create mode 100644 packages/ext-web-components/dist/Ext/panel/Date.js create mode 100644 packages/ext-web-components/dist/Ext/panel/DateTitle.js create mode 100644 packages/ext-web-components/dist/Ext/panel/Header.js create mode 100644 packages/ext-web-components/dist/Ext/panel/Panel.js create mode 100644 packages/ext-web-components/dist/Ext/panel/Time.js create mode 100644 packages/ext-web-components/dist/Ext/panel/Title.js create mode 100644 packages/ext-web-components/dist/Ext/panel/Tool.js create mode 100644 packages/ext-web-components/dist/Ext/panel/YearPicker.js create mode 100644 packages/ext-web-components/dist/Ext/picker/Date.js create mode 100644 packages/ext-web-components/dist/Ext/picker/Picker.js create mode 100644 packages/ext-web-components/dist/Ext/picker/SelectPicker.js create mode 100644 packages/ext-web-components/dist/Ext/picker/Slot.js create mode 100644 packages/ext-web-components/dist/Ext/picker/Tablet.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/Grid.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/Row.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/cell/Cell.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/cell/Group.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/d3/AbstractContainer.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/d3/Container.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/d3/HeatMap.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/d3/TreeMap.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/plugin/configurator/Column.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/plugin/configurator/Container.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/plugin/configurator/Form.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/plugin/configurator/Panel.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/plugin/configurator/Settings.js create mode 100644 packages/ext-web-components/dist/Ext/pivot/plugin/rangeeditor/Panel.js create mode 100644 packages/ext-web-components/dist/Ext/scroll/indicator/Bar.js create mode 100644 packages/ext-web-components/dist/Ext/scroll/indicator/Indicator.js create mode 100644 packages/ext-web-components/dist/Ext/scroll/indicator/Overlay.js create mode 100644 packages/ext-web-components/dist/Ext/slider/Slider.js create mode 100644 packages/ext-web-components/dist/Ext/slider/Thumb.js create mode 100644 packages/ext-web-components/dist/Ext/slider/Toggle.js create mode 100644 packages/ext-web-components/dist/Ext/sparkline/Bar.js create mode 100644 packages/ext-web-components/dist/Ext/sparkline/BarBase.js create mode 100644 packages/ext-web-components/dist/Ext/sparkline/Base.js create mode 100644 packages/ext-web-components/dist/Ext/sparkline/Box.js create mode 100644 packages/ext-web-components/dist/Ext/sparkline/Bullet.js create mode 100644 packages/ext-web-components/dist/Ext/sparkline/Discrete.js create mode 100644 packages/ext-web-components/dist/Ext/sparkline/Line.js create mode 100644 packages/ext-web-components/dist/Ext/sparkline/Pie.js create mode 100644 packages/ext-web-components/dist/Ext/sparkline/TriState.js create mode 100644 packages/ext-web-components/dist/Ext/tab/Bar.js create mode 100644 packages/ext-web-components/dist/Ext/tab/Panel.js create mode 100644 packages/ext-web-components/dist/Ext/tab/Tab.js create mode 100644 packages/ext-web-components/dist/Ext/tip/ToolTip.js create mode 100644 packages/ext-web-components/dist/Ext/tree/Tree.js create mode 100644 packages/ext-web-components/dist/Ext/ux/Gauge.js create mode 100644 packages/ext-web-components/dist/Ext/ux/colorpick/Button.js create mode 100644 packages/ext-web-components/dist/Ext/ux/colorpick/ColorPreview.js create mode 100644 packages/ext-web-components/dist/Ext/ux/colorpick/Field.js create mode 100644 packages/ext-web-components/dist/Ext/ux/colorpick/Selector.js create mode 100644 packages/ext-web-components/dist/Ext/ux/gauge/Gauge.js create mode 100644 packages/ext-web-components/dist/Ext/ux/google/Map.js create mode 100644 packages/ext-web-components/dist/Ext/ux/rating/Picker.js create mode 100644 packages/ext-web-components/dist/Ext/viewport/Android.js create mode 100644 packages/ext-web-components/dist/Ext/viewport/Default.js create mode 100644 packages/ext-web-components/dist/Ext/viewport/Ios.js create mode 100644 packages/ext-web-components/dist/Ext/viewport/WP.js create mode 100644 packages/ext-web-components/dist/Ext/viewport/WindowsPhone.js create mode 100644 packages/ext-web-components/dist/Ext/window/Window.js create mode 100644 packages/ext-web-components/dist/ewc-base.component.js create mode 100644 packages/ext-web-components/dist/ext-router.component.js diff --git a/.gitignore b/.gitignore index 6cdd77694..8733ac6b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ copy/ build/ -dist/ +dist2/ buildext/ ext-components-prod diff --git a/demos/ext-grid-inline-1-prop/index.html b/demos/ext-grid-inline-1-prop/index.html index 6f3f7e5a3..b78435488 100644 --- a/demos/ext-grid-inline-1-prop/index.html +++ b/demos/ext-grid-inline-1-prop/index.html @@ -8,16 +8,79 @@ + * + * + * + * Refer to config options of {@link Ext.Loader} for the list of possible properties + * + * @param {Object} config The config object to override the default values + * @return {Ext.Loader} this + */ + setConfig: Ext.Function.flexSetter(function(name, value) { + var delegated = delegatedConfigs[name]; + if (name === 'paths') { + Loader.setPath(value); + } else { + _config[name] = value; + if (delegated) { + Boot.setConfig((delegated === true) ? name : delegated, value); + } + } + return Loader; + }), + /** + * Get the config value corresponding to the specified name. If no name is given, + * will return the config object + * + * @param {String} name The config property name + * @return {Object} + */ + getConfig: function(name) { + return name ? _config[name] : _config; + }, + /** + * Sets the path of a namespace. + * For Example: + * + * Ext.Loader.setPath('Ext', '.'); + * + * @param {String/Object} name See {@link Ext.Function#flexSetter flexSetter} + * @param {String} [path] See {@link Ext.Function#flexSetter flexSetter} + * @return {Ext.Loader} this + * @method + */ + setPath: function() { + // Paths are an Ext.Inventory thing and ClassManager is an instance of that: + Manager.setPath.apply(Manager, arguments); + return Loader; + }, + /** + * Sets a batch of path entries + * + * @param {Object} paths a set of className: path mappings + * @return {Ext.Loader} this + */ + addClassPathMappings: function(paths) { + // Paths are an Ext.Inventory thing and ClassManager is an instance of that: + Manager.setPath(paths); + return Loader; + }, + /** + * fixes up loader path configs by prepending Ext.Boot#baseUrl to the beginning + * of the path, then delegates to Ext.Loader#addClassPathMappings + * @param pathConfig + */ + addBaseUrlClassPathMappings: function(pathConfig) { + var name; + for (name in pathConfig) { + pathConfig[name] = Boot.baseUrl + pathConfig[name]; + } + Ext.Loader.addClassPathMappings(pathConfig); + }, + /** + * Translates a className to a file path by adding the + * the proper prefix and converting the .'s to /'s. For example: + * + * Ext.Loader.setPath('My', '/path/to/My'); + * + * // alerts '/path/to/My/awesome/Class.js' + * alert(Ext.Loader.getPath('My.awesome.Class')); + * + * Note that the deeper namespace levels, if explicitly set, are always resolved first. + * For example: + * + * Ext.Loader.setPath({ + * 'My': '/path/to/lib', + * 'My.awesome': '/other/path/for/awesome/stuff', + * 'My.awesome.more': '/more/awesome/path' + * }); + * + * // alerts '/other/path/for/awesome/stuff/Class.js' + * alert(Ext.Loader.getPath('My.awesome.Class')); + * + * // alerts '/more/awesome/path/Class.js' + * alert(Ext.Loader.getPath('My.awesome.more.Class')); + * + * // alerts '/path/to/lib/cool/Class.js' + * alert(Ext.Loader.getPath('My.cool.Class')); + * + * // alerts 'Unknown/strange/Stuff.js' + * alert(Ext.Loader.getPath('Unknown.strange.Stuff')); + * + * @param {String} className + * @return {String} path + */ + getPath: function(className) { + // Paths are an Ext.Inventory thing and ClassManager is an instance of that: + return Manager.getPath(className); + }, + require: function(expressions, fn, scope, excludes) { + var classNames; + if (excludes) { + return Loader.exclude(excludes).require(expressions, fn, scope); + } + classNames = Manager.getNamesByExpression(expressions); + return Loader.load(classNames, fn, scope); + }, + syncRequire: function() { + var wasEnabled = Loader.syncModeEnabled, + ret; + Loader.syncModeEnabled = true; + ret = Loader.require.apply(Loader, arguments); + Loader.syncModeEnabled = wasEnabled; + return ret; + }, + exclude: function(excludes) { + var selector = Manager.select({ + require: function(classNames, fn, scope) { + return Loader.load(classNames, fn, scope); + }, + syncRequire: function(classNames, fn, scope) { + var wasEnabled = Loader.syncModeEnabled, + ret; + Loader.syncModeEnabled = true; + ret = Loader.load(classNames, fn, scope); + Loader.syncModeEnabled = wasEnabled; + return ret; + } + }); + selector.exclude(excludes); + return selector; + }, + load: function(classNames, callback, scope) { + if (callback) { + if (callback.length) { + // If callback expects arguments, shim it with a function that will map + // the requires class(es) from the names we are given. + callback = Loader.makeLoadCallback(classNames, callback); + } + callback = callback.bind(scope || Ext.global); + } + /* eslint-disable-next-line vars-on-top */ + var state = Manager.classState, + missingClassNames = [], + urls = [], + urlByClass = {}, + numClasses = classNames.length, + className, i, numMissing; + for (i = 0; i < numClasses; ++i) { + className = Manager.resolveName(classNames[i]); + if (!Manager.isCreated(className)) { + missingClassNames.push(className); + if (!state[className]) { + urlByClass[className] = Loader.getPath(className); + urls.push(urlByClass[className]); + } + } + } + // If the dynamic dependency feature is not being used, throw an error + // if the dependencies are not defined + numMissing = missingClassNames.length; + if (numMissing) { + Loader.missingCount += numMissing; + Manager.onCreated(function() { + if (callback) { + Ext.callback(callback, scope, arguments); + } + Loader.checkReady(); + }, Loader, missingClassNames); + if (!_config.enabled) { + Ext.raise("Ext.Loader is not enabled, so dependencies cannot be resolved " + "dynamically. Missing required class" + ((missingClassNames.length > 1) ? "es" : "") + ": " + missingClassNames.join(', ')); + } + if (urls.length) { + Loader.loadScripts({ + url: urls, + // scope will be this options object so we can pass these along: + _classNames: missingClassNames, + _urlByClass: urlByClass + }); + } else { + // need to call checkReady here, as the _missingCoun + // may have transitioned from 0 to > 0, meaning we + // need to block ready + Loader.checkReady(); + } + } else { + if (callback) { + callback.call(scope); + } + // need to call checkReady here, as the _missingCoun + // may have transitioned from 0 to > 0, meaning we + // need to block ready + Loader.checkReady(); + } + if (Loader.syncModeEnabled) { + // Class may have been just loaded or was already loaded + if (numClasses === 1) { + return Manager.get(classNames[0]); + } + } + return Loader; + }, + makeLoadCallback: function(classNames, callback) { + return function() { + var classes = [], + i = classNames.length; + while (i-- > 0) { + classes[i] = Manager.get(classNames[i]); + } + return callback.apply(this, classes); + }; + }, + onLoadFailure: function(request) { + var options = this, + entries = request.entries || [], + onError = options.onError, + error, entry, i; + Loader.hasFileLoadError = true; + --Loader.scriptsLoading; + if (onError) { + for (i = 0; i < entries.length; i++) { + entry = entries[i]; + if (entry.error) { + error = new Error('Failed to load: ' + entry.url); + break; + } + } + error = error || new Error('Failed to load'); + onError.call(options.userScope, options, error, request); + } else { + Ext.log.error("[Ext.Loader] Some requested files failed to load."); + } + Loader.checkReady(); + }, + onLoadSuccess: function() { + var options = this, + onLoad = options.onLoad, + classNames = options._classNames, + urlByClass = options._urlByClass, + state = Manager.classState, + missingQueue = Loader.missingQueue, + className, i, len; + --Loader.scriptsLoading; + if (onLoad) { + // TODO: need an adapter to convert to v4 onLoad signatures + onLoad.call(options.userScope, options); + } + // onLoad can cause more loads to start, so it must run first + // classNames is the array of *all* classes that load() was asked to load, + // including those that might have been already loaded but not yet created. + // urlByClass is a map of only those classes that we asked Boot to load. + for (i = 0 , len = classNames.length; i < len; i++) { + className = classNames[i]; + // When a script is loaded and executed, we should have Ext.define() called + // for at least one of the classes in the list, which will set the state + // for that class. That by itself does not mean that the class is available + // *now* but it means that ClassManager is tracking it and will fire the + // onCreated callback that we set back in load(). + // However if there is no state for the class, that may mean two things: + // either it is not a Ext class, or it is truly missing. In any case we need + // to watch for that thing ourselves, which we will do every checkReady(). + if (!state[className]) { + missingQueue[className] = urlByClass[className]; + } + } + Loader.checkReady(); + }, + // TODO: this timing of this needs to be deferred until all classes have had + // a chance to be created + reportMissingClasses: function() { + var missingQueue = Loader.missingQueue, + missingClasses = [], + missingPaths = [], + missingClassName; + if (!Loader.syncModeEnabled && !Loader.scriptsLoading && Loader.isLoading && !Loader.hasFileLoadError) { + for (missingClassName in missingQueue) { + missingClasses.push(missingClassName); + missingPaths.push(missingQueue[missingClassName]); + } + if (missingClasses.length) { + throw new Error("The following classes are not declared even if their files " + "have been loaded: '" + missingClasses.join("', '") + "'. Please check the source code of their " + "corresponding files for possible typos: '" + missingPaths.join("', '")); + } + } + }, + /** + * Add a new listener to be executed when all required scripts are fully loaded + * + * @param {Function} fn The function callback to be executed + * @param {Object} scope The execution scope (`this`) of the callback function. + * @param {Boolean} [withDomReady=true] Pass `false` to not also wait for document + * dom ready. + * @param {Object} [options] Additional callback options. + * @param {Number} [options.delay=0] A number of milliseconds to delay. + * @param {Number} [options.priority=0] Relative priority of this callback. Negative + * numbers are reserved. + */ + onReady: function(fn, scope, withDomReady, options) { + var listener; + if (withDomReady) { + Ready.on(fn, scope, options); + } else { + listener = Ready.makeListener(fn, scope, options); + if (Loader.isLoading) { + readyListeners.push(listener); + } else { + Ready.invoke(listener); + } + } + }, + /** + * @private + * Ensure that any classes referenced in the `uses` property are loaded. + */ + addUsedClasses: function(classes) { + var cls, i, ln; + if (classes) { + classes = (typeof classes === 'string') ? [ + classes + ] : classes; + for (i = 0 , ln = classes.length; i < ln; i++) { + cls = classes[i]; + if (typeof cls === 'string' && !Ext.Array.contains(usedClasses, cls)) { + usedClasses.push(cls); + } + } + } + return Loader; + }, + /** + * @private + */ + triggerReady: function() { + var listener, + refClasses = usedClasses; + if (Loader.isLoading && refClasses.length) { + // Empty the array to eliminate potential recursive loop issue + usedClasses = []; + // this may immediately call us back if all 'uses' classes + // have been loaded + Loader.require(refClasses); + } else { + // Must clear this before calling callbacks. This will cause any new loads + // to call Ready.block() again. See below for more on this. + Loader.isLoading = false; + // These listeners are just those attached directly to Loader to wait for + // class loading only. + readyListeners.sort(Ready.sortFn); + // this method can be called with Loader.isLoading either true or false + // (can be called with false when all 'uses' classes are already loaded) + // this may bypass the above if condition + while (readyListeners.length && !Loader.isLoading) { + // we may re-enter triggerReady so we cannot necessarily iterate the + // readyListeners array + listener = readyListeners.pop(); + Ready.invoke(listener); + } + // If the DOM is also ready, this will fire the normal onReady listeners. + // An astute observer would note that we may now be back to isLoading and + // so ask "Why you call unblock?". The reason is that we must match the + // calls to block and since we transitioned from isLoading to !isLoading + // here we must call unblock. If we have transitioned back to isLoading in + // the above loop it will have called block again so the counter will be + // increased and this call will not reduce the block count to 0. This is + // done by loadScripts. + Ready.unblock(); + } + }, + /** + * @private + * @param {String} className + */ + historyPush: function(className) { + if (className && !isInHistory[className] && !Manager.overrideMap[className]) { + isInHistory[className] = true; + history.push(className); + } + return Loader; + }, + /** + * This is an internal method that delegate content loading to the + * bootstrap layer. + * @private + * @param params + */ + loadScripts: function(params) { + var manifest = Ext.manifest, + loadOrder = manifest && manifest.loadOrder, + loadOrderMap = manifest && manifest.loadOrderMap, + options; + ++Loader.scriptsLoading; + // if the load order map hasn't been created, create it now + // and cache on the manifest + if (loadOrder && !loadOrderMap) { + manifest.loadOrderMap = loadOrderMap = Boot.createLoadOrderMap(loadOrder); + } + // verify the loading state, as this may have transitioned us from + // not loading to loading + Loader.checkReady(); + options = Ext.apply({ + loadOrder: loadOrder, + loadOrderMap: loadOrderMap, + charset: _config.scriptCharset, + success: Loader.onLoadSuccess, + failure: Loader.onLoadFailure, + sync: Loader.syncModeEnabled, + _classNames: [] + }, params); + options.userScope = options.scope; + options.scope = options; + Boot.load(options); + }, + /** + * This method is provide for use by the bootstrap layer. + * @private + * @param {String[]} urls + */ + loadScriptsSync: function(urls) { + var syncwas = Loader.syncModeEnabled; + Loader.syncModeEnabled = true; + Loader.loadScripts({ + url: urls + }); + Loader.syncModeEnabled = syncwas; + }, + /** + * This method is provide for use by the bootstrap layer. + * @private + * @param {String[]} urls + */ + loadScriptsSyncBasePrefix: function(urls) { + var syncwas = Loader.syncModeEnabled; + Loader.syncModeEnabled = true; + Loader.loadScripts({ + url: urls, + prependBaseUrl: true + }); + Loader.syncModeEnabled = syncwas; + }, + /** + * Loads the specified script URL and calls the supplied callbacks. If this method + * is called before {@link Ext#isReady}, the script's load will delay the transition + * to ready. This can be used to load arbitrary scripts that may contain further + * {@link Ext#require Ext.require} calls. + * + * @param {Object/String/String[]} options The options object or simply the URL(s) to load. + * @param {String} options.url The URL from which to load the script. + * @param {Function} [options.onLoad] The callback to call on successful load. + * @param {Function} [options.onError] The callback to call on failure to load. + * @param {Object} [options.scope] The scope (`this`) for the supplied callbacks. + */ + loadScript: function(options) { + var isString = typeof options === 'string', + isArray = options instanceof Array, + isObject = !isArray && !isString, + url = isObject ? options.url : options, + onError = isObject && options.onError, + onLoad = isObject && options.onLoad, + scope = isObject && options.scope, + request = { + url: url, + scope: scope, + onLoad: onLoad, + onError: onError, + _classNames: [] + }; + Loader.loadScripts(request); + }, + /** + * @private + */ + checkMissingQueue: function() { + var missingQueue = Loader.missingQueue, + newQueue = {}, + missing = 0, + name; + for (name in missingQueue) { + // If class state is available for the name, that means ClassManager + // is tracking it and will fire callback when it is created. + // We only need to track non-class things in the Loader. + if (!(Manager.classState[name] || Manager.isCreated(name))) { + newQueue[name] = missingQueue[name]; + missing++; + } + } + Loader.missingCount = missing; + Loader.missingQueue = newQueue; + }, + /** + * @private + */ + checkReady: function() { + var wasLoading = Loader.isLoading, + isLoading; + Loader.checkMissingQueue(); + isLoading = Loader.missingCount + Loader.scriptsLoading; + if (isLoading && !wasLoading) { + Ready.block(); + Loader.isLoading = !!isLoading; + } else if (!isLoading && wasLoading) { + Loader.triggerReady(); + } + if (!Loader.scriptsLoading && Loader.missingCount) { + // Things look bad, but since load requests may come later, defer this + // for a bit then check if things are still stuck. + Ext.defer(function() { + var name; + if (!Loader.scriptsLoading && Loader.missingCount) { + Ext.log.error('[Loader] The following classes failed to load:'); + for (name in Loader.missingQueue) { + Ext.log.error('[Loader] ' + name + ' from ' + Loader.missingQueue[name]); + } + } + }, 1000); + } + } + }); + /** + * Loads all classes by the given names and all their direct dependencies; optionally + * executes the given callback function when finishes, within the optional scope. + * + * @param {String/String[]} expressions The class, classes or wildcards to load. + * @param {Function} [fn] The callback function. + * @param {Object} [scope] The execution scope (`this`) of the callback function. + * @member Ext + * @method require + */ + Ext.require = alias(Loader, 'require'); + /** + * Synchronously loads all classes by the given names and all their direct dependencies; + * optionally executes the given callback function when finishes, within the optional scope. + * + * @param {String/String[]} expressions The class, classes or wildcards to load. + * @param {Function} [fn] The callback function. + * @param {Object} [scope] The execution scope (`this`) of the callback function. + * @member Ext + * @method syncRequire + */ + Ext.syncRequire = alias(Loader, 'syncRequire'); + /** + * Explicitly exclude files from being loaded. Useful when used in conjunction with a + * broad include expression. Can be chained with more `require` and `exclude` methods, + * for example: + * + * Ext.exclude('Ext.data.*').require('*'); + * + * Ext.exclude('widget.button*').require('widget.*'); + * + * @param {String/String[]} excludes + * @return {Object} Contains `exclude`, `require` and `syncRequire` methods for chaining. + * @member Ext + * @method exclude + */ + Ext.exclude = alias(Loader, 'exclude'); + /** + * @cfg {String[]} requires + * @member Ext.Class + * List of classes that have to be loaded before instantiating this class. + * For example: + * + * Ext.define('Mother', { + * requires: ['Child'], + * giveBirth: function() { + * // we can be sure that child class is available. + * return new Child(); + * } + * }); + */ + Class.registerPreprocessor('loader', function(cls, data, hooks, continueFn) { + if (Ext.classSystemMonitor) { + Ext.classSystemMonitor(cls, 'Ext.Loader#loaderPreprocessor', arguments); + } + /* eslint-disable-next-line vars-on-top */ + var me = this, + dependencies = [], + dependency, + className = Manager.getName(cls), + i, j, ln, subLn, value, propertyName, propertyValue, requiredMap; + /* + Loop through the dependencyProperties, look for string class names and push + them into a stack, regardless of whether the property's value is a string, array or object. + For example: + { + extend: 'Ext.MyClass', + requires: ['Ext.some.OtherClass'], + mixins: { + thing: 'Foo.bar.Thing'; + } + } + which will later be transformed into: + { + extend: Ext.MyClass, + requires: [Ext.some.OtherClass], + mixins: { + thing: Foo.bar.Thing; + } + } + */ + for (i = 0 , ln = dependencyProperties.length; i < ln; i++) { + propertyName = dependencyProperties[i]; + if (data.hasOwnProperty(propertyName)) { + propertyValue = data[propertyName]; + if (typeof propertyValue === 'string') { + dependencies.push(propertyValue); + } else if (propertyValue instanceof Array) { + for (j = 0 , subLn = propertyValue.length; j < subLn; j++) { + value = propertyValue[j]; + if (typeof value === 'string') { + dependencies.push(value); + } + } + } else if (typeof propertyValue !== 'function') { + for (j in propertyValue) { + if (propertyValue.hasOwnProperty(j)) { + value = propertyValue[j]; + if (typeof value === 'string') { + dependencies.push(value); + } + } + } + } + } + } + if (dependencies.length === 0) { + return; + } + if (className) { + _requiresMap[className] = dependencies; + } + /* eslint-disable-next-line vars-on-top */ + var manifestClasses = Ext.manifest && Ext.manifest.classes, + deadlockPath = [], + detectDeadlock; + /* + * Automatically detect deadlocks before-hand, + * will throw an error with detailed path for ease of debugging. Examples + * of deadlock cases: + * + * - A extends B, then B extends A + * - A requires B, B requires C, then C requires A + * + * The detectDeadlock function will recursively transverse till the leaf, hence + * it can detect deadlocks no matter how deep the path is. However we don't need + * to run this check if the class name is in the manifest: that means Cmd has + * already resolved all dependencies for this class with no deadlocks. + */ + if (className && (!manifestClasses || !manifestClasses[className])) { + requiredMap = Loader.requiredByMap || (Loader.requiredByMap = {}); + for (i = 0 , ln = dependencies.length; i < ln; i++) { + dependency = dependencies[i]; + (requiredMap[dependency] || (requiredMap[dependency] = [])).push(className); + } + detectDeadlock = function(cls) { + var requires = _requiresMap[cls], + dep, i, ln; + deadlockPath.push(cls); + if (requires) { + if (Ext.Array.contains(requires, className)) { + Ext.Error.raise("Circular requirement detected! '" + className + "' and '" + deadlockPath[1] + "' mutually require each other. " + "Path: " + deadlockPath.join(' -> ') + " -> " + deadlockPath[0]); + } + for (i = 0 , ln = requires.length; i < ln; i++) { + dep = requires[i]; + if (!isInHistory[dep]) { + detectDeadlock(requires[i]); + } + } + } + }; + detectDeadlock(className); + } + (className ? Loader.exclude(className) : Loader).require(dependencies, function() { + var i, ln, j, subLn, k; + for (i = 0 , ln = dependencyProperties.length; i < ln; i++) { + propertyName = dependencyProperties[i]; + if (data.hasOwnProperty(propertyName)) { + propertyValue = data[propertyName]; + if (typeof propertyValue === 'string') { + data[propertyName] = Manager.get(propertyValue); + } else if (propertyValue instanceof Array) { + for (j = 0 , subLn = propertyValue.length; j < subLn; j++) { + value = propertyValue[j]; + if (typeof value === 'string') { + data[propertyName][j] = Manager.get(value); + } + } + } else if (typeof propertyValue !== 'function') { + for (k in propertyValue) { + if (propertyValue.hasOwnProperty(k)) { + value = propertyValue[k]; + if (typeof value === 'string') { + data[propertyName][k] = Manager.get(value); + } + } + } + } + } + } + continueFn.call(me, cls, data, hooks); + }); + return false; + }, true, 'after', 'className'); + /** + * @cfg {String[]} uses + * @member Ext.Class + * List of optional classes to load together with this class. These aren't neccessarily loaded + * before this class is created, but are guaranteed to be available before Ext.onReady + * listeners are invoked. For example: + * + * Ext.define('Mother', { + * uses: ['Child'], + * giveBirth: function() { + * // This code might, or might not work: + * // return new Child(); + * + * // Instead use Ext.create() to load the class at the spot if not loaded already: + * return Ext.create('Child'); + * } + * }); + */ + Manager.registerPostprocessor('uses', function(name, cls, data) { + var uses = data.uses, + classNames; + if (Ext.classSystemMonitor) { + Ext.classSystemMonitor(cls, 'Ext.Loader#usesPostprocessor', arguments); + } + if (uses) { + classNames = Manager.getNamesByExpression(data.uses); + Loader.addUsedClasses(classNames); + } + }); + Manager.onCreated(Loader.historyPush); + Loader.init(); +}()); +//----------------------------------------------------------------------------- +// Use performance.now when available to keep timestamps consistent. +Ext._endTime = Ext.ticks(); +// This hook is to allow tools like DynaTrace to deterministically detect the availability +// of Ext.onReady. Since Loader takes over Ext.onReady this must be done here and not in +// Ext.env.Ready. +if (Ext._beforereadyhandler) { + Ext._beforereadyhandler(); +} + +/** + * This class is a base class for mixins. These are classes that extend this class and are + * designed to be used as a `mixin` by user code. + * + * It provides mixins with the ability to "hook" class methods of the classes in to which + * they are mixed. For example, consider the `destroy` method pattern. If a mixin class + * had cleanup requirements, it would need to be called as part of `destroy`. + * + * Starting with a basic class we might have: + * + * Ext.define('Foo.bar.Base', { + * destroy: function() { + * console.log('B'); + * // cleanup + * } + * }); + * + * A derived class would look like this: + * + * Ext.define('Foo.bar.Derived', { + * extend: 'Foo.bar.Base', + * + * destroy: function() { + * console.log('D'); + * // more cleanup + * + * this.callParent(); // let Foo.bar.Base cleanup as well + * } + * }); + * + * To see how using this class help, start with a "normal" mixin class that also needs to + * cleanup its resources. These mixins must be called explicitly by the classes that use + * them. For example: + * + * Ext.define('Foo.bar.Util', { + * destroy: function() { + * console.log('U'); + * } + * }); + * + * Ext.define('Foo.bar.Derived', { + * extend: 'Foo.bar.Base', + * + * mixins: { + * util: 'Foo.bar.Util' + * }, + * + * destroy: function() { + * console.log('D'); + * // more cleanup + * + * this.mixins.util.destroy.call(this); + * + * this.callParent(); // let Foo.bar.Base cleanup as well + * } + * }); + * + * var obj = new Foo.bar.Derived(); + * + * obj.destroy(); + * // logs D then U then B + * + * This class is designed to solve the above in simpler and more reliable way. + * + * ## mixinConfig + * + * Using `mixinConfig` the mixin class can provide "before" or "after" hooks that do not + * involve the derived class implementation. This also means the derived class cannot + * adjust parameters to the hook methods. + * + * Ext.define('Foo.bar.Util', { + * extend: 'Ext.Mixin', + * + * mixinConfig: { + * after: { + * destroy: 'destroyUtil' + * } + * }, + * + * destroyUtil: function() { + * console.log('U'); + * } + * }); + * + * Ext.define('Foo.bar.Class', { + * mixins: { + * util: 'Foo.bar.Util' + * }, + * + * destroy: function() { + * console.log('D'); + * } + * }); + * + * var obj = new Foo.bar.Derived(); + * + * obj.destroy(); + * // logs D then U + * + * If the destruction should occur in the other order, you can use `before`: + * + * Ext.define('Foo.bar.Util', { + * extend: 'Ext.Mixin', + * + * mixinConfig: { + * before: { + * destroy: 'destroyUtil' + * } + * }, + * + * destroyUtil: function() { + * console.log('U'); + * } + * }); + * + * Ext.define('Foo.bar.Class', { + * mixins: { + * util: 'Foo.bar.Util' + * }, + * + * destroy: function() { + * console.log('D'); + * } + * }); + * + * var obj = new Foo.bar.Derived(); + * + * obj.destroy(); + * // logs U then D + * + * ### Configs + * + * Normally when a mixin defines `config` properties and the class into which the mixin is + * initially mixed needs to specify values for those configs, the class processor does not + * yet recognize these config and instead retains the properties on te target class + * prototype. + * + * Changing the above behavior would potentially break application code, so this class + * provides a way to remedy this: + * + * Ext.define('Foo.bar.Util', { + * extend: 'Ext.Mixin', + * + * mixinConfig: { + * configs: true + * }, + * + * config: { + * foo: null + * } + * }); + * + * Now the direct user class can correctly specify `foo` config properties: + * + * Ext.define('Some.other.Class', { + * mixins: [ + * 'Foo.bar.Util' + * ], + * + * foo: 'bar' // recognized as the foo config form Foo.bar.Util + * }); + * + * ### Chaining + * + * One way for a mixin to provide methods that act more like normal inherited methods is + * to use an `on` declaration. These methods will be injected into the `callParent` chain + * between the derived and superclass. For example: + * + * Ext.define('Foo.bar.Util', { + * extend: 'Ext.Mixin', + * + * mixinConfig: { + * on: { + * destroy: function() { + * console.log('M'); + * } + * } + * } + * }); + * + * Ext.define('Foo.bar.Base', { + * destroy: function() { + * console.log('B'); + * } + * }); + * + * Ext.define('Foo.bar.Derived', { + * extend: 'Foo.bar.Base', + * + * mixins: { + * util: 'Foo.bar.Util' + * }, + * + * destroy: function() { + * this.callParent(); + * console.log('D'); + * } + * }); + * + * var obj = new Foo.bar.Derived(); + * + * obj.destroy(); + * // logs M then B then D + * + * As with `before` and `after`, the value of `on` can be a method name. + * + * Ext.define('Foo.bar.Util', { + * extend: 'Ext.Mixin', + * + * mixinConfig: { + * on: { + * destroy: 'onDestroy' + * } + * } + * + * onDestroy: function() { + * console.log('M'); + * } + * }); + * + * Because this technique leverages `callParent`, the derived class controls the time and + * parameters for the call to all of its bases (be they `extend` or `mixin` flavor). + * + * ### Derivations + * + * Some mixins need to process class extensions of their target class. To do this you can + * define an `extended` method like so: + * + * Ext.define('Foo.bar.Util', { + * extend: 'Ext.Mixin', + * + * mixinConfig: { + * extended: function(baseClass, derivedClass, classBody) { + * // This function is called whenever a new "derivedClass" is created + * // that extends a "baseClass" in to which this mixin was mixed. + * } + * } + * }); + * + * @protected + */ +Ext.define('Ext.Mixin', function(Mixin) { + return { + // eslint-disable-line brace-style + statics: { + addHook: function(hookFn, targetClass, methodName, mixinClassPrototype) { + var isFunc = Ext.isFunction(hookFn), + hook = function() { + var a = arguments, + fn = isFunc ? hookFn : mixinClassPrototype[hookFn], + result = this.callParent(a); + fn.apply(this, a); + return result; + }, + existingFn = targetClass.hasOwnProperty(methodName) && targetClass[methodName]; + if (isFunc) { + hookFn.$previous = Ext.emptyFn; + } + // no callParent for these guys + hook.$name = methodName; + hook.$owner = targetClass.self; + if (existingFn) { + hook.$previous = existingFn.$previous; + existingFn.$previous = hook; + } else { + targetClass[methodName] = hook; + } + } + }, + onClassExtended: function(cls, data) { + var mixinConfig = data.mixinConfig, + hooks = data.xhooks, + superclass = cls.superclass, + onClassMixedIn = data.onClassMixedIn, + afterClassMixedIn = data.afterClassMixedIn, + afters, befores, configs, extended, mixed, parentMixinConfig; + if (hooks) { + // Legacy way + delete data.xhooks; + (mixinConfig || (data.mixinConfig = mixinConfig = {})).on = hooks; + } + if (mixinConfig) { + parentMixinConfig = superclass.mixinConfig; + if (parentMixinConfig) { + data.mixinConfig = mixinConfig = Ext.merge({}, parentMixinConfig, mixinConfig); + } + data.mixinId = mixinConfig.id; + if (mixinConfig.beforeHooks) { + Ext.raise('Use of "beforeHooks" is deprecated - use "before" instead'); + } + if (mixinConfig.hooks) { + Ext.raise('Use of "hooks" is deprecated - use "after" instead'); + } + if (mixinConfig.afterHooks) { + Ext.raise('Use of "afterHooks" is deprecated - use "after" instead'); + } + afters = mixinConfig.after; + befores = mixinConfig.before; + configs = mixinConfig.configs; + extended = mixinConfig.extended; + hooks = mixinConfig.on; + mixed = mixinConfig.mixed; + } + if (afters || befores || hooks || extended) { + // Note: tests are with Ext.Class + data.onClassMixedIn = function(targetClass) { + var mixin = this.prototype, + targetProto = targetClass.prototype, + key; + if (befores) { + Ext.Object.each(befores, function(key, value) { + targetClass.hookMember(key, function() { + if (mixin[value].apply(this, arguments) !== false) { + return this.callParent(arguments); + } + }); + }); + } + if (afters) { + Ext.Object.each(afters, function(key, value) { + targetClass.hookMember(key, function() { + var ret = this.callParent(arguments); + mixin[value].apply(this, arguments); + return ret; + }); + }); + } + if (hooks) { + for (key in hooks) { + Mixin.addHook(hooks[key], targetProto, key, mixin); + } + } + if (extended) { + targetClass.onExtended(function() { + var args = Ext.Array.slice(arguments, 0); + args.unshift(targetClass); + return extended.apply(this, args); + }, this); + } + if (onClassMixedIn) { + onClassMixedIn.apply(this, arguments); + } + }; + } + if (configs || mixed) { + data.afterClassMixedIn = function(targetClass) { + if (configs) { + // eslint-disable-next-line vars-on-top + var proto = targetClass.prototype, + hoistable = this.$config.configs, + cfg, name, hoist; + for (name in proto) { + cfg = hoistable[name]; + if (cfg && cfg.isConfig && proto.hasOwnProperty(name)) { + (hoist || (hoist = {}))[name] = proto[name]; + delete proto[name]; + } + } + if (hoist) { + targetClass.$config.add(hoist); + } + } + if (afterClassMixedIn) { + afterClassMixedIn.apply(this, arguments); + } + if (mixed) { + mixed.apply(this, arguments); + } + }; + } + } + }; +}); +// eslint-disable-line block-spacing, brace-style + +// @tag core +/** + * @class Ext.util.DelayedTask + * + * The DelayedTask class provides a convenient way to "buffer" the execution of a method, + * performing setTimeout where a new timeout cancels the old timeout. When called, the + * task will wait the specified time period before executing. If durng that time period, + * the task is called again, the original call will be cancelled. This continues so that + * the function is only called a single time for each iteration. + * + * This method is especially useful for things like detecting whether a user has finished + * typing in a text field. An example would be performing validation on a keypress. You can + * use this class to buffer the keypress events for a certain number of milliseconds, and + * perform only if they stop for that amount of time. + * + * ## Usage + * + * var task = new Ext.util.DelayedTask(function(){ + * alert(Ext.getDom('myInputField').value.length); + * }); + * + * // Wait 500ms before calling our function. If the user presses another key + * // during that 500ms, it will be cancelled and we'll wait another 500ms. + * Ext.get('myInputField').on('keypress', function() { + * task.delay(500); + * }); + * + * Note that we are using a DelayedTask here to illustrate a point. The configuration + * option `buffer` for {@link Ext.util.Observable#addListener addListener/on} will + * also setup a delayed task for you to buffer events. + * + * @constructor The parameters to this constructor serve as defaults and are not required. + * @param {Function} [fn] The default function to call. If not specified here, it must be + * specified during the {@link #delay} call. + * @param {Object} [scope] The default scope (The **`this`** reference) in which the + * function is called. If not specified, `this` will refer to the browser window. + * @param {Array} [args] The default Array of arguments. + * @param {Boolean} [cancelOnDelay=true] By default, each call to {@link #delay} cancels + * any pending invocation and reschedules a new invocation. Specifying this as `false` means + * that calls to {@link #delay} when an invocation is pending just update the call settings, + * `newDelay`, `newFn`, `newScope` or `newArgs`, whichever are passed. + */ +Ext.util = Ext.util || {}; +Ext.util.DelayedTask = function(fn, scope, args, cancelOnDelay, fireIdleEvent) { + // @define Ext.util.DelayedTask + // @uses Ext.GlobalEvents + var me = this, + delay, + call = function() { + me.id = null; + if (!(scope && scope.destroyed)) { + if (args) { + fn.apply(scope, args); + } else { + fn.call(scope); + } + } + if (fireIdleEvent === false) { + Ext._suppressIdle = true; + } + }; + // DelayedTask can be called with no function upfront + if (fn) { + call.$origFn = fn.$origFn || fn; + call.$skipTimerCheck = call.$origFn.$skipTimerCheck; + } + cancelOnDelay = typeof cancelOnDelay === 'boolean' ? cancelOnDelay : true; + /** + * @property {Number} id + * The id of the currently pending invocation. Will be set to `null` if there is no + * invocation pending. + */ + me.id = null; + /** + * @method delay + * By default, cancels any pending timeout and queues a new one. + * + * If the `cancelOnDelay` parameter was specified as `false` in the constructor, this does not + * cancel and reschedule, but just updates the call settings, `newDelay`, `newFn`, `newScope` + * or `newArgs`, whichever are passed. + * + * @param {Number} newDelay The milliseconds to delay. `-1` means schedule for the next + * animation frame if supported. + * @param {Function} [newFn] Overrides function passed to constructor + * @param {Object} [newScope] Overrides scope passed to constructor. Remember that if no scope + * is specified, `this` will refer to the browser window. + * @param {Array} [newArgs] Overrides args passed to constructor + * @return {Number} The timer id being used. + */ + me.delay = function(newDelay, newFn, newScope, newArgs) { + if (cancelOnDelay) { + me.cancel(); + } + if (typeof newDelay === 'number') { + delay = newDelay; + } + fn = newFn || fn; + scope = newScope || scope; + args = newArgs || args; + me.delayTime = delay; + if (fn) { + call.$origFn = fn.$origFn || fn; + call.$skipTimerCheck = call.$origFn.$skipTimerCheck; + } + if (!me.id) { + if (delay === -1) { + me.id = Ext.raf(call); + } else { + me.id = Ext.defer(call, delay || 1); + } + } + // 0 == immediate call + return me.id; + }; + /** + * Cancel the last queued timeout + */ + me.cancel = function() { + if (me.id) { + if (me.delayTime === -1) { + Ext.unraf(me.id); + } else { + Ext.undefer(me.id); + } + me.id = null; + } + }; + me.flush = function() { + var was; + if (me.id) { + me.cancel(); + // we're not running on our own timer so don't mess with whatever thread + // is calling us... + was = fireIdleEvent; + fireIdleEvent = true; + call(); + fireIdleEvent = was; + } + }; + /** + * @private + * Cancel the timeout if it was set for the specified fn and scope. + */ + me.stop = function(stopFn, stopScope) { + // This kludginess is here because Classic components use shared focus task + // and we need to be sure the task's current timeout was set for that + // particular component before we can safely cancel it. + if (stopFn && stopFn === fn && (!stopScope || stopScope === scope)) { + me.cancel(); + } + }; +}; + +// @tag core +/** + * Represents single event type that an Observable object listens to. + * All actual listeners are tracked inside here. When the event fires, + * it calls all the registered listener functions. + * + * @private + */ +Ext.define('Ext.util.Event', function() { + var arraySlice = Array.prototype.slice, + arrayInsert = Ext.Array.insert, + toArray = Ext.Array.toArray, + fireArgs = {}; + return { + /** + * @property {Boolean} isEvent + * `true` in this class to identify an object as an instantiated Event, or subclass thereof. + */ + isEvent: true, + // Private. Event suspend count + suspended: 0, + noOptions: {}, + constructor: function(observable, name) { + this.name = name; + this.observable = observable; + this.listeners = []; + }, + addListener: function(fn, scope, options, caller, manager) { + var me = this, + added = false, + observable = me.observable, + eventName = me.name, + listeners, listener, priority, isNegativePriority, highestNegativePriorityIndex, hasNegativePriorityIndex, length, index, i, listenerPriority, managedListeners; + if (scope && !Ext._namedScopes[scope] && (typeof fn === 'string') && (typeof scope[fn] !== 'function')) { + Ext.raise("No method named '" + fn + "' found on scope object"); + } + if (me.findListener(fn, scope) === -1) { + listener = me.createListener(fn, scope, options, caller, manager); + if (me.firing) { + // if we are currently firing this event, don't disturb the listener loop + me.listeners = me.listeners.slice(0); + } + listeners = me.listeners; + index = length = listeners.length; + priority = options && options.priority; + highestNegativePriorityIndex = me._highestNegativePriorityIndex; + hasNegativePriorityIndex = highestNegativePriorityIndex !== undefined; + if (priority) { + // Find the index at which to insert the listener into the listeners array, + // sorted by priority highest to lowest. + isNegativePriority = (priority < 0); + if (!isNegativePriority || hasNegativePriorityIndex) { + // If the priority is a positive number, or if it is a negative number + // and there are other existing negative priority listenrs, then we + // need to calcuate the listeners priority-order index. + // If the priority is a negative number, begin the search for priority + // order index at the index of the highest existing negative priority + // listener, otherwise begin at 0 + // eslint-disable-next-line max-len + for (i = (isNegativePriority ? highestNegativePriorityIndex : 0); i < length; i++) { + // Listeners created without options will have no "o" property + listenerPriority = listeners[i].o ? listeners[i].o.priority || 0 : 0; + if (listenerPriority < priority) { + index = i; + break; + } + } + } else { + // if the priority is a negative number, and there are no other negative + // priority listeners, then no calculation is needed - the negative + // priority listener gets appended to the end of the listeners array. + me._highestNegativePriorityIndex = index; + } + } else if (hasNegativePriorityIndex) { + // listeners with a priority of 0 or undefined are appended to the end of + // the listeners array unless there are negative priority listeners in the + // listeners array, then they are inserted before the highest negative + // priority listener. + index = highestNegativePriorityIndex; + } + if (!isNegativePriority && index <= highestNegativePriorityIndex) { + me._highestNegativePriorityIndex++; + } + if (index === length) { + listeners[length] = listener; + } else { + arrayInsert(listeners, index, [ + listener + ]); + } + if (observable.isElement) { + // It is the role of Ext.util.Event (vs Ext.Element) to handle subscribe/ + // unsubscribe because it is the lowest level place to intercept the + // listener before it is added/removed. For addListener this could easily + // be done in Ext.Element's doAddListener override, but since there are + // multiple paths for listener removal (un, clearListeners), it is best + // to keep all subscribe/unsubscribe logic here. + observable._getPublisher(eventName, options.translate === false).subscribe(observable, eventName, options.delegated !== false, options.capture); + } + // If the listener was passed with a manager, add it to the manager's list. + if (manager) { + // if scope is an observable, the listener will be automatically managed + // this eliminates the need to call mon() in a majority of cases + managedListeners = manager.managedListeners || (manager.managedListeners = []); + managedListeners.push({ + item: me.observable, + ename: (options && options.managedName) || me.name, + fn: fn, + scope: scope, + options: options + }); + } + added = true; + } + return added; + }, + createListener: function(fn, scope, o, caller, manager) { + var me = this, + namedScopes = Ext._namedScopes, + namedScope = namedScopes[scope], + listener = { + fn: fn, + scope: scope, + ev: me, + caller: caller, + manager: manager, + namedScope: namedScope, + defaultScope: namedScope ? (scope || me.observable) : undefined, + lateBound: typeof fn === 'string' + }, + handler = fn, + wrapped = false, + type; + if (listener.lateBound && fn[2] === '.') { + if (fn.substr(0, 2) !== 'up') { + Ext.raise('Invalid listener method: ' + fn); + } + listener.defaultScope = null; + listener.namedScope = namedScopes[listener.scope = scope = 'up']; + listener.fn = handler = fn.substr(3); + } + // The order is important. The 'single' wrapper must be wrapped by the 'buffer' + // and 'delayed' wrapper because the event removal that the single listener + // does destroys the listener's DelayedTask(s) + if (o) { + listener.o = o; + if (o.single) { + handler = me.createSingle(handler, listener, o, scope); + wrapped = true; + } + if (o.target) { + handler = me.createTargeted(handler, listener, o, scope, wrapped); + wrapped = true; + } + if (o.onFrame) { + handler = me.createAnimFrame(handler, listener, o, scope, wrapped); + wrapped = true; + } + if (o.delay) { + handler = me.createDelayed(handler, listener, o, scope, wrapped); + wrapped = true; + } + if (o.buffer) { + handler = me.createBuffered(handler, listener, o, scope, wrapped); + wrapped = true; + } + if (me.observable.isElement) { + // If the event type was translated, e.g. mousedown -> touchstart, we need + // to save the original type in the listener object so that the Ext.event.Event + // object can reflect the correct type at firing time + type = o.type; + if (type) { + listener.type = type; + } + } + } + listener.fireFn = handler; + listener.wrapped = wrapped; + return listener; + }, + findListener: function(fn, scope) { + var listeners = this.listeners, + i = listeners.length, + listener; + while (i--) { + listener = listeners[i]; + if (listener) { + // use ==, not === for scope comparison, so that undefined and null are equal + // eslint-disable-next-line eqeqeq + if (listener.fn === fn && listener.scope == scope) { + return i; + } + } + } + return -1; + }, + removeListener: function(fn, scope, index) { + var me = this, + removed = false, + observable = me.observable, + eventName = me.name, + listener, options, manager, managedListeners, managedListener, i; + index = index != null ? index : me.findListener(fn, scope); + if (index !== -1) { + listener = me.listeners[index]; + if (me.firing) { + me.listeners = me.listeners.slice(0); + } + // Remove this listener from the listeners array. We can use splice directly here. + // The IE8 bug which Ext.Array works around only affects *insertion* + // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/ + me.listeners.splice(index, 1); + // if the listeners array contains negative priority listeners, adjust the + // internal index if needed. + if (me._highestNegativePriorityIndex) { + if (index < me._highestNegativePriorityIndex) { + me._highestNegativePriorityIndex--; + } else if (index === me._highestNegativePriorityIndex && index === me.listeners.length) { + delete me._highestNegativePriorityIndex; + } + } + if (listener) { + options = listener.o; + // cancel and remove a buffered handler that hasn't fired yet. + // When the buffered listener is invoked, it must check whether + // it still has a task. + if (listener.task) { + listener.task.cancel(); + delete listener.task; + } + // cancel and remove all delayed handlers that haven't fired yet + i = listener.tasks && listener.tasks.length; + if (i) { + while (i--) { + listener.tasks[i].cancel(); + } + delete listener.tasks; + } + // Cancel the timer that could have been set if the event has already fired + listener.fireFn.timerId = Ext.undefer(listener.fireFn.timerId); + manager = listener.manager; + if (manager) { + // If this is a managed listener we need to remove it from the manager's + // managedListeners array. This ensures that if we listen using mon + // and then remove without using mun, the managedListeners array is updated + // accordingly, for example + // + // manager.on(target, 'foo', fn); + // + // target.un('foo', fn); + managedListeners = manager.managedListeners; + if (managedListeners) { + for (i = managedListeners.length; i--; ) { + managedListener = managedListeners[i]; + if (managedListener.item === me.observable && managedListener.ename === eventName && managedListener.fn === fn && managedListener.scope === scope) { + managedListeners.splice(i, 1); + } + } + } + } + if (observable.isElement) { + // eslint-disable-next-line max-len + observable._getPublisher(eventName, options.translate === false).unsubscribe(observable, eventName, options.delegated !== false, options.capture); + } + } + removed = true; + } + return removed; + }, + // Iterate to stop any buffered/delayed events + clearListeners: function() { + var listeners = this.listeners, + i = listeners.length, + listener; + while (i--) { + listener = listeners[i]; + this.removeListener(listener.fn, listener.scope); + } + }, + suspend: function() { + ++this.suspended; + }, + resume: function() { + if (this.suspended) { + --this.suspended; + } + }, + isSuspended: function() { + return this.suspended > 0; + }, + fireDelegated: function(firingObservable, args) { + this.firingObservable = firingObservable; + return this.fire.apply(this, args); + }, + fire: function() { + var me = this, + CQ = Ext.ComponentQuery, + listeners = me.listeners, + count = listeners.length, + observable = me.observable, + isElement = observable.isElement, + isComponent = observable.isComponent, + firingObservable = me.firingObservable, + options, delegate, fireInfo, i, args, listener, len, delegateEl, currentTarget, type, chained, firingArgs, e, fireFn, fireScope; + if (!me.suspended && count > 0) { + me.firing = true; + args = arguments.length ? arraySlice.call(arguments, 0) : []; + len = args.length; + if (isElement) { + e = args[0]; + } + for (i = 0; i < count; i++) { + listener = listeners[i]; + // Listener may be undefined if one of the previous listeners + // destroyed the observable that was listening to these events. + // We'd be still in the middle of the loop here, unawares. + if (!listener) { + + continue; + } + options = listener.o; + if (isElement) { + if (currentTarget) { + // restore the previous currentTarget if we changed it last time + // around the loop while processing the delegate option. + e.setCurrentTarget(currentTarget); + } + // For events that have been translated to provide device compatibility, + // e.g. mousedown -> touchstart, we want the event object to reflect the + // type that was originally listened for, not the type of the actual event + // that fired. The listener's "type" property reflects the original type. + type = listener.type; + if (type) { + // chain a new object to the event object before changing the type. + // This is more efficient than creating a new event object, and we + // don't want to change the type of the original event because it may + // be used asynchronously by other handlers + // Translated events are not gestures. They must appear to be + // atomic events, so that they can be stopped. + chained = e; + e = args[0] = chained.chain({ + type: type, + isGesture: false + }); + } + // In Ext4 Ext.EventObject was a singleton event object that was reused + // as events were fired. Set Ext.EventObject to the last fired event + // for compatibility. + Ext.EventObject = e; + } + firingArgs = args; + if (options) { + delegate = options.delegate; + if (delegate) { + if (isElement) { + // prepending the currentTarget.id to the delegate selector + // allows us to match selectors such as "> div" + // eslint-disable-next-line max-len + delegateEl = e.getTarget(typeof delegate === 'function' ? delegate : '#' + e.currentTarget.id + ' ' + delegate); + if (delegateEl) { + args[1] = delegateEl; + // save the current target before changing it to the delegateEl + // so that we can restore it next time around + currentTarget = e.currentTarget; + e.setCurrentTarget(delegateEl); + } else { + + continue; + } + } + // eslint-disable-next-line max-len + else if (isComponent && !CQ.is(firingObservable, delegate, observable)) { + + continue; + } + } + if (isElement) { + if (options.preventDefault) { + e.preventDefault(); + } + if (options.stopPropagation) { + e.stopPropagation(); + } + if (options.stopEvent) { + e.stopEvent(); + } + } + args[len] = options; + if (options.args) { + firingArgs = options.args.concat(args); + } + } + fireInfo = me.getFireInfo(listener); + fireFn = fireInfo.fn; + fireScope = fireInfo.scope; + // We don't want to keep closure and scope on the Event prototype! + fireInfo.fn = fireInfo.scope = null; + // If the scope is already destroyed, we absolutely cannot deliver events to it. + // We also need to clean up the listener to avoid it hanging around forever + // like a zombie. Scope can be null/undefined, that's normal. + if (fireScope && fireScope.destroyed) { + me.removeListener(fireFn, fireScope, i); + fireFn = null; + // Skip warnings for Ext.container.Monitor + // It is to be deprecated and removed shortly. + if (fireScope.$className !== 'Ext.container.Monitor') { + (Ext.raiseOnDestroyed ? Ext.raise : Ext.log.warn)({ + msg: 'Attempting to fire "' + me.name + '" event on destroyed ' + (fireScope.$className || 'object') + ' instance with id: ' + (fireScope.id || 'unknown'), + instance: fireScope + }); + } + } + // N.B. This is where actual listener code is called. Step boldly into! + if (fireFn && fireFn.apply(fireScope, firingArgs) === false) { + Ext.EventObject = null; + return (me.firing = false); + } + // We should remove the last item here to avoid future listeners + // in the Array to inherit these options by mistake + if (options) { + args.length--; + } + if (chained) { + // if we chained the event object for type translation we need to + // un-chain it before proceeding to process the next listener, which + // may not be a translated event. + e = args[0] = chained; + chained = null; + } + // We don't guarantee Ext.EventObject existence outside of the immediate + // event propagation scope + Ext.EventObject = null; + } + } + me.firing = false; + return true; + }, + getFireInfo: function(listener, fromWrapped) { + var observable = this.observable, + fireFn = listener.fireFn, + scope = listener.scope, + namedScope = listener.namedScope, + fn, origin; + // If we are called with a wrapped listener, only attempt to do scope + // resolution if we are explicitly called by the last wrapped function + if (!fromWrapped && listener.wrapped) { + fireArgs.fn = fireFn; + return fireArgs; + } + fn = fromWrapped ? listener.fn : fireFn; + var name = fn; + // eslint-disable-line vars-on-top + if (listener.lateBound) { + // handler is a function name - need to resolve it to a function reference + origin = listener.caller || observable; + if (namedScope && namedScope.isUp) { + scope = Ext.lookUpFn(origin, fn); + } else if (!scope || namedScope) { + // Only invoke resolveListenerScope if the user did not specify a scope, + // or if the user specified a named scope. Named function handlers that + // use an arbitrary object as the scope just skip this part, and just + // use the given scope object to resolve the method. + scope = origin.resolveListenerScope(listener.defaultScope); + } + if (!scope) { + Ext.raise('Unable to dynamically resolve scope for "' + listener.ev.name + '" listener on ' + this.observable.id); + } + if (!Ext.isFunction(scope[fn])) { + Ext.raise('No method named "' + fn + '" on ' + (scope.$className || 'scope object.')); + } + fn = scope[fn]; + } else if (namedScope && namedScope.isController) { + // If handler is a function reference and scope:'controller' was requested + // we'll do our best to look up a controller. + scope = (listener.caller || observable).resolveListenerScope(listener.defaultScope); + if (!scope) { + Ext.raise('Unable to dynamically resolve scope for "' + listener.ev.name + '" listener on ' + this.observable.id); + } + } else if (!scope || namedScope) { + // If handler is a function reference we use the observable instance as + // the default scope + scope = observable; + } + // We can only ever be firing one event at a time, so just keep + // overwriting the object we've got in our closure, otherwise we'll be + // creating a whole bunch of garbage objects + fireArgs.fn = fn; + fireArgs.scope = scope; + if (!fn) { + Ext.raise('Unable to dynamically resolve method "' + name + '" on ' + this.observable.$className); + } + return fireArgs; + }, + createAnimFrame: function(handler, listener, o, scope, wrapped) { + var fireInfo; + if (!wrapped) { + fireInfo = listener.ev.getFireInfo(listener, true); + handler = fireInfo.fn; + scope = fireInfo.scope; + // We don't want to keep closure and scope references on the Event prototype! + fireInfo.fn = fireInfo.scope = null; + } + return Ext.Function.createAnimationFrame(handler, scope, o.args); + }, + createTargeted: function(handler, listener, o, scope, wrapped) { + return function() { + var fireInfo; + if (o.target === arguments[0]) { + if (!wrapped) { + fireInfo = listener.ev.getFireInfo(listener, true); + handler = fireInfo.fn; + scope = fireInfo.scope; + // We don't want to keep closure and scope references + // on the Event prototype! + fireInfo.fn = fireInfo.scope = null; + } + return handler.apply(scope, arguments); + } + }; + }, + createBuffered: function(handler, listener, o, scope, wrapped) { + listener.task = new Ext.util.DelayedTask(); + return function() { + var fireInfo; + // If the listener is removed during the event call, the listener stays in the + // list of listeners to be invoked in the fire method, but the task is deleted + // So if we get here with no task, it's because the listener has been removed. + if (listener.task) { + if (Ext._unitTesting) { + o.$delayedTask = listener.task; + } + // for unit test access + if (!wrapped) { + fireInfo = listener.ev.getFireInfo(listener, true); + handler = fireInfo.fn; + scope = fireInfo.scope; + // We don't want to keep closure and scope references + // on the Event prototype! + fireInfo.fn = fireInfo.scope = null; + } + listener.task.delay(o.buffer, handler, scope, toArray(arguments)); + } + }; + }, + createDelayed: function(handler, listener, o, scope, wrapped) { + return function() { + var task = new Ext.util.DelayedTask(), + fireInfo; + if (!wrapped) { + fireInfo = listener.ev.getFireInfo(listener, true); + handler = fireInfo.fn; + scope = fireInfo.scope; + // We don't want to keep closure and scope references on the Event prototype! + fireInfo.fn = fireInfo.scope = null; + } + if (!listener.tasks) { + listener.tasks = []; + } + listener.tasks.push(task); + if (Ext._unitTesting) { + o.$delayedTask = task; + } + // for unit test access + task.delay(o.delay || 10, handler, scope, toArray(arguments)); + }; + }, + createSingle: function(handler, listener, o, scope, wrapped) { + return function() { + var event = listener.ev, + observable = event.observable, + fn = listener.fn, + fireInfo; + // If we have an observable, use that to clean up because there + // can be special cases that need handling. For example element + // listeners may bind multiple events (mousemove+touchmove) and they + // need to act in tandem. + if (observable) { + if (!observable.destroyed) { + observable.removeListener(event.name, fn, scope); + } + } else { + event.removeListener(fn, scope); + } + if (!wrapped) { + fireInfo = event.getFireInfo(listener, true); + handler = fireInfo.fn; + scope = fireInfo.scope; + // We don't want to keep closure and scope references on the Event prototype! + fireInfo.fn = fireInfo.scope = null; + } + return handler.apply(scope, arguments); + }; + } + }; +}); + +// @tag dom,core +/* eslint-disable max-len */ +/** + * An Identifiable mixin. + * @private + */ +Ext.define('Ext.mixin.Identifiable', function(Identifiable) { + return { + // eslint-disable-line brace-style + /* eslint-enable max-len */ + isIdentifiable: true, + mixinId: 'identifiable', + /** + * Retrieves the `id`. This method Will auto-generate an id if one has not already + * been configured. + * @return {String} id + */ + getId: function() { + var me = this, + id = me.id, + cfg; + if (!(id || id === 0)) { + cfg = me.initialConfig; + // The id config can be requested so early (e.g., by stateful) that the + // property has not been put on the instance yet. + if (cfg && cfg.id) { + id = cfg.id; + } else { + id = me.generateAutoId(); + me.autoGenId = true; + } + me.setId(id); + } + me.getId = Identifiable._getId; + return id; + }, + setId: function(id) { + // The double assignment here and in setId is intentional to workaround a JIT + // issue that prevents me.id from being assigned in random scenarios. The issue + // occurs on 4th gen iPads and lower, possibly other older iOS devices. + // See EXTJS-16494. + this.id = this.id = id; + }, + privates: { + statics: { + _idCleanRe: /\.|[^\w-]/g, + uniqueIds: {}, + _getId: function() { + return this.id; + } + }, + defaultIdPrefix: 'ext-', + defaultIdSeparator: '-', + id: null, + /** + * @property {Boolean} autoGenId + * `true` indicates an `id` was auto-generated rather than provided by configuration. + * @private + * @since 6.7.0 + */ + autoGenId: false, + generateAutoId: function() { + var me = this, + prototype = me.self.prototype, + sep = me.defaultIdSeparator, + uniqueIds = Identifiable.uniqueIds, + cleanRe, defaultIdPrefix, prefix, xtype; + if (!prototype.hasOwnProperty('identifiablePrefix')) { + cleanRe = Identifiable._idCleanRe; + defaultIdPrefix = me.defaultIdPrefix; + xtype = me.xtype; + if (xtype) { + prefix = defaultIdPrefix + xtype.replace(cleanRe, sep) + sep; + } else if (!(prefix = prototype.$className)) { + prefix = defaultIdPrefix + 'anonymous' + sep; + } else { + prefix = prefix.replace(cleanRe, sep).toLowerCase() + sep; + } + prototype.identifiablePrefix = prefix; + } + prefix = me.identifiablePrefix; + if (!uniqueIds.hasOwnProperty(prefix)) { + uniqueIds[prefix] = 0; + } + return prefix + (++uniqueIds[prefix]); + } + } + }; +}); +// privates + +// @tag core +/** + * Base class that provides a common interface for publishing events. Subclasses are + * expected to have a property "events" which is populated as event listeners register, + * and, optionally, a property "listeners" with configured listeners defined. + * + * *Note*: This mixin requires the constructor to be called, which is typically done + * during the construction of your object. The Observable constructor will call + * {@link #initConfig}, so it does not need to be called a second time. + * + * For example: + * + * Ext.define('Employee', { + * mixins: ['Ext.mixin.Observable'], + * + * config: { + * name: '' + * }, + * + * constructor: function (config) { + * // The `listeners` property is processed to add listeners and the config + * // is applied to the object. + * this.mixins.observable.constructor.call(this, config); + * // Config has been initialized + * console.log(this.getEmployeeName()); + * } + * }); + * + * This could then be used like this: + * + * var newEmployee = new Employee({ + * name: employeeName, + * listeners: { + * quit: function() { + * // By default, "this" will be the object that fired the event. + * alert(this.getName() + " has quit!"); + * } + * } + * }); + */ +Ext.define('Ext.mixin.Observable', function(Observable) { + var emptyFn = Ext.emptyFn, + emptyArray = [], + arrayProto = Array.prototype, + arraySlice = arrayProto.slice, + // Destroyable class which removes listeners + ListenerRemover = function(observable) { + // Passed a ListenerRemover: return it + if (observable instanceof ListenerRemover) { + return observable; + } + this.observable = observable; + // Called when addManagedListener is used with the event source as the second arg: + // (owner, eventSource, args...) + if (arguments[1].isObservable) { + this.managedListeners = true; + } + this.args = arraySlice.call(arguments, 1); + }, + // These properties should not be nulled during Base destroy(), + // we will take care of them in destroyObservable() + protectedProps = [ + 'events', + 'hasListeners', + 'managedListeners', + 'eventedBeforeEventNames' + ]; + ListenerRemover.prototype.destroy = function() { + var me = this, + args = me.args, + observable = me.observable, + // Extract the element reference from the options object. + // Single arg form first, options is args[3] in multi arg form. + elementName = args[0].element || (args[3] && args[3].element); + // If it was an element listener, then the listener is added to that element reference. + if (elementName) { + if (Ext.Array.indexOf(observable.referenceList, elementName) === -1) { + Ext.Logger.error("Destroying event listener with an invalid element reference " + "of '" + elementName + "' for this component. Available values are: '" + observable.referenceList.join("', '") + "'", observable); + } + observable = observable[elementName]; + } + // If that observable is already destroyed, all its listeners were cleared + if (!observable.destroyed) { + observable[me.managedListeners ? 'mun' : 'un'].apply(observable, me.args); + } + me.destroy = Ext.emptyFn; + }; + return { + extend: Ext.Mixin, + mixinConfig: { + id: 'observable', + after: { + destroy: 'destroyObservable' + } + }, + mixins: [ + Ext.mixin.Identifiable + ], + statics: { + /** + * Removes **all** added captures from the Observable. + * + * @param {Ext.util.Observable} o The Observable to release + * @static + */ + releaseCapture: function(o) { + o.fireEventArgs = this.prototype.fireEventArgs; + }, + /** + * Starts capture on the specified Observable. All events will be passed to the supplied + * function with the event name + standard signature of the event **before** the event + * is fired. If the supplied function returns false, the event will not fire. + * + * @param {Ext.util.Observable} o The Observable to capture events from. + * @param {Function} fn The function to call when an event is fired. + * @param {Object} scope (optional) The scope (`this` reference) in which the function + * is executed. Defaults to the Observable firing the event. + * @static + */ + capture: function(o, fn, scope) { + // We're capturing calls to fireEventArgs to avoid duplication of events; + // however fn expects fireEvent's signature so we have to convert it here. + // To avoid unnecessary conversions, observe() below is aware of the changes + // and will capture fireEventArgs instead. + var newFn = function(eventName, args) { + return fn.apply(scope, [ + eventName + ].concat(args)); + }; + this.captureArgs(o, newFn, scope); + }, + /** + * @method + * @private + */ + captureArgs: function(o, fn, scope) { + o.fireEventArgs = Ext.Function.createInterceptor(o.fireEventArgs, fn, scope); + }, + /** + * Sets observability on the passed class constructor. + * + * This makes any event fired on any instance of the passed class also fire a single + * event through the **class** allowing for central handling of events on many instances + * at once. + * + * Usage: + * + * Ext.util.Observable.observe(Ext.data.Connection); + * Ext.data.Connection.on('beforerequest', function(con, options) { + * console.log('Ajax request made to ' + options.url); + * }); + * + * @param {Function} cls The class constructor to make observable. + * @param {Object} listeners An object containing a series of listeners to + * add. See {@link Ext.util.Observable#addListener addListener}. + * @static + */ + observe: function(cls, listeners) { + if (cls) { + if (!cls.isObservable) { + Ext.applyIf(cls, new this()); + this.captureArgs(cls.prototype, cls.fireEventArgs, cls); + } + if (Ext.isObject(listeners)) { + cls.on(listeners); + } + } + return cls; + }, + /** + * @method prepareClass + * Prepares a given class for observable instances. This method is called when a + * class derives from this class or uses this class as a mixin. + * @param {Function} T The class constructor to prepare. + * @param {Ext.util.Observable} mixin The mixin if being used as a mixin. + * @param {Object} data The raw class creation data if this is an extend. + * @private + */ + prepareClass: function(T, mixin, data) { + // T.hasListeners is the object to track listeners on class T. This object's + // prototype (__proto__) is the "hasListeners" of T.superclass. + // Instances of T will create "hasListeners" that have T.hasListeners as their + // immediate prototype (__proto__). + var listeners = T.listeners = [], + // If this function was called as a result of an "onExtended", it will + // receive the class as "T", but the members will not yet have been + // applied to the prototype. If this is the case, just grab listeners + // off of the raw data object. + target = data || T.prototype, + targetListeners = target.listeners, + superListeners = mixin ? mixin.listeners : T.superclass.self.listeners, + scope, namedScope, i, len; + // Process listeners that have been declared on the class body. These + // listeners must not override each other, but each must be added + // separately. This is accomplished by maintaining a nested array + // of listeners for the class and it's superclasses/mixins + if (superListeners) { + listeners.push(superListeners); + } + if (targetListeners) { + // Allow listener scope resolution mechanism to know if the listeners + // were declared on the class. This is only necessary when scope + // is unspecified, or when scope is 'controller'. We use special private + // named scopes of "self" and "self.controller" to indicate either + // unspecified scope, or scope declared as controller on the class + // body. To avoid iterating the listeners object multiple times, we + // only put this special scope on the outermost object at this point + // and allow addListener to handle scope:'controller' declared on + // inner objects of the listeners config. + scope = targetListeners.scope; + if (!scope) { + targetListeners.scope = 'self'; + } else { + namedScope = Ext._namedScopes[scope]; + if (namedScope && namedScope.isController) { + targetListeners.scope = 'self.controller'; + } + } + listeners.push(targetListeners); + // After adding the target listeners to the declared listeners array + // we can delete it off of the prototype (or data object). This ensures + // that we don't attempt to add the listeners twice, once during + // addDeclaredListeners, and again when we add this.listeners in the + // constructor. + target.listeners = null; + } + if (!T.HasListeners) { + // We create a HasListeners "class" for this class. The "prototype" of the + // HasListeners class is an instance of the HasListeners class associated + // with this class's super class (or with Observable). + // eslint-disable-next-line vars-on-top + var HasListeners = function() {}, + SuperHL = T.superclass.HasListeners || (mixin && mixin.HasListeners) || Observable.HasListeners; + // Make the HasListener class available on the class and its prototype: + T.prototype.HasListeners = T.HasListeners = HasListeners; + // And connect its "prototype" to the new HasListeners of our super class + // (which is also the class-level "hasListeners" instance). + HasListeners.prototype = T.hasListeners = new SuperHL(); + } + // Reusing a variable here + scope = T.prototype.$noClearOnDestroy || {}; + for (i = 0 , len = protectedProps.length; i < len; i++) { + scope[protectedProps[i]] = true; + } + T.prototype.$noClearOnDestroy = scope; + } + }, + /* End Definitions */ + /** + * @cfg {Object} listeners + * + * A config object containing one or more event handlers to be added to this object during + * initialization. This should be a valid listeners config object as specified in the + * {@link Ext.util.Observable#addListener addListener} example for attaching + * multiple handlers at once. + * + * **DOM events from Ext JS {@link Ext.Component Components}** + * + * While _some_ Ext JS Component classes export selected DOM events (e.g. "click", + * "mouseover" etc), this is usually only done when extra value can be added. For example + * the {@link Ext.view.View DataView}'s **`{@link Ext.view.View#itemclick itemclick}`** + * event passing the node clicked on. To access DOM events directly from a child element + * of a Component, we need to specify the `element` option to identify the Component + * property to add a DOM listener to: + * + * new Ext.panel.Panel({ + * width: 400, + * height: 200, + * dockedItems: [{ + * xtype: 'toolbar' + * }], + * listeners: { + * click: { + * element: 'el', //bind to the underlying el property on the panel + * fn: function(){ console.log('click el'); } + * }, + * dblclick: { + * element: 'body', //bind to the underlying body property on the panel + * fn: function(){ console.log('dblclick body'); } + * } + * } + * }); + */ + /** + * @property {Boolean} isObservable + * `true` in this class to identify an object as an instantiated Observable, or subclass + * thereof. + */ + isObservable: true, + /** + * @private + * We don't want the base destructor to clear the prototype because + * our destroyObservable handler must be called the very last. It will take care + * of the prototype after completing Observable destruction sequence. + */ + $vetoClearingPrototypeOnDestroy: true, + /** + * @private + * Initial suspended call count. Incremented when {@link #suspendEvents} is called, + * decremented when {@link #resumeEvents} is called. + */ + eventsSuspended: 0, + /** + * @property {Object} hasListeners + * @readonly + * This object holds a key for any event that has a listener. The listener may be set + * directly on the instance, or on its class or a super class (via {@link #observe}) or + * on the {@link Ext.app.EventBus MVC EventBus}. The values of this object are truthy + * (a non-zero number) and falsy (0 or undefined). They do not represent an exact count + * of listeners. The value for an event is truthy if the event must be fired and is + * falsy if there is no need to fire the event. + * + * The intended use of this property is to avoid the expense of fireEvent calls when + * there are no listeners. This can be particularly helpful when one would otherwise + * have to call fireEvent hundreds or thousands of times. It is used like this: + * + * if (this.hasListeners.foo) { + * this.fireEvent('foo', this, arg1); + * } + */ + constructor: function(config) { + var me = this, + self = me.self, + declaredListeners, listeners, bubbleEvents, len, i; + // Observable can be extended and/or mixed in at multiple levels in a Class + // hierarchy, and may have its constructor invoked multiple times for a given + // instance. The following ensures we only perform initialization the first + // time the constructor is called. + if (me.$observableInitialized) { + return; + } + me.$observableInitialized = true; + // This double assignment is intentional - it works around a strange JIT + // bug that prevents this.hasListeners from being assigned in some cases on + // some versions of iOS and iOS simulator. + // (This bug manifests itself in the unit tests for Ext.data.NodeInterface + // where we repeatedly create tree nodes in each spec. Sometimes node.hasListeners + // is undefined immediately after node construction). + // A similar issue occurs with the data property of Ext.data.Model (see + // constructor) + me.hasListeners = me.hasListeners = new me.HasListeners(); + me.eventedBeforeEventNames = {}; + me.events = me.events || {}; + declaredListeners = self.listeners; + if (declaredListeners && !me._addDeclaredListeners(declaredListeners)) { + // Nulling out declared listeners allows future instances to avoid + // recursing into the declared listeners arrays if the first instance + // discovers that there are no declarative listeners in its hierarchy + self.listeners = null; + } + listeners = (config && config.listeners) || me.listeners; + if (listeners) { + if (listeners instanceof Array) { + // Support for listeners declared as an array: + // + // listeners: [ + // { foo: fooHandler }, + // { bar: barHandler } + // ] + for (i = 0 , len = listeners.length; i < len; ++i) { + me.addListener(listeners[i]); + } + } else { + me.addListener(listeners); + } + } + bubbleEvents = (config && config.bubbleEvents) || me.bubbleEvents; + if (bubbleEvents) { + me.enableBubble(bubbleEvents); + } + if (me.$applyConfigs) { + // Ext.util.Observable applies config properties directly to the instance + if (config) { + Ext.apply(me, config); + } + } else { + // Ext.mixin.Observable uses the config system + me.initConfig(config); + } + if (listeners) { + // Set as an instance property to preempt the prototype in case any are set there. + // Prevents listeners from being added multiple times if this constructor + // is called more than once by multiple parties in the inheritance hierarchy + me.listeners = null; + } + }, + onClassExtended: function(T, data) { + if (!T.HasListeners) { + // Some classes derive from us and some others derive from those classes. All + // of these are passed to this method. + Observable.prepareClass(T, T.prototype.$observableMixedIn ? undefined : data); + } + }, + /** + * @private + * Matches options property names within a listeners specification object - property names + * which are never used as event names. + */ + $eventOptions: { + scope: 1, + delay: 1, + buffer: 1, + onFrame: 1, + single: 1, + args: 1, + destroyable: 1, + priority: 1, + order: 1 + }, + $orderToPriority: { + before: 100, + current: 0, + after: -100 + }, + /** + * Adds declarative listeners as nested arrays of listener objects. + * @private + * @param {Array} listeners + * @return {Boolean} `true` if any listeners were added + */ + _addDeclaredListeners: function(listeners) { + var me = this; + if (listeners instanceof Array) { + Ext.each(listeners, me._addDeclaredListeners, me); + } else { + me._addedDeclaredListeners = true; + me.addListener(listeners); + } + return me._addedDeclaredListeners; + }, + /** + * The addManagedListener method is used when some object (call it "A") is listening + * to an event on another observable object ("B") and you want to remove that listener + * from "B" when "A" is destroyed. This is not an issue when "B" is destroyed because + * all of its listeners will be removed at that time. + * + * Example: + * + * Ext.define('Foo', { + * extend: 'Ext.Component', + * + * initComponent: function () { + * this.addManagedListener(MyApp.SomeSharedMenu, 'show', this.doSomething); + * this.callParent(); + * } + * }); + * + * As you can see, when an instance of Foo is destroyed, it ensures that the 'show' + * listener on the menu (`MyApp.SomeGlobalSharedMenu`) is also removed. + * + * As of version 5.1 it is no longer necessary to use this method in most cases because + * listeners are automatically managed if the scope object provided to + * {@link Ext.util.Observable#addListener addListener} is an Observable instance. + * However, if the observable instance and scope are not the same object you + * still need to use `mon` or `addManagedListener` if you want the listener to be + * managed. + * + * @param {Ext.util.Observable/Ext.dom.Element} item The item to which to add + * a listener/listeners. + * @param {Object/String} ename The event name, or an object containing event name + * properties. + * @param {Function/String} fn (optional) If the `ename` parameter was an event + * name, this is the handler function or the name of a method on the specified + * `scope`. + * @param {Object} scope (optional) If the `ename` parameter was an event name, this is + * the scope (`this` reference) in which the handler function is executed. + * @param {Object} options (optional) If the `ename` parameter was an event name, this is + * the {@link Ext.util.Observable#addListener addListener} options. + * @param {Boolean} noDestroy (private) + * @return {Object} **Only when the `destroyable` option is specified.** + * + * A `Destroyable` object. An object which implements the `destroy` method which removes + * all listeners added in this call. For example: + * + * this.btnListeners = myButton.mon({ + * destroyable: true + * mouseover: function() { console.log('mouseover'); }, + * mouseout: function() { console.log('mouseout'); }, + * click: function() { console.log('click'); } + * }); + * + * And when those listeners need to be removed: + * + * Ext.destroy(this.btnListeners); + * + * or + * + * this.btnListeners.destroy(); + */ + addManagedListener: function(item, ename, fn, scope, options, noDestroy) { + var me = this, + config, passedOptions; + me.managedListeners = me.managedListeners || []; + if (typeof ename !== 'string') { + // When creating listeners using the object form, allow caller to override + // the default of using the listeners object as options. + // This is used by relayEvents, when adding its relayer so that it does not + // contribute a spurious options param to the end of the arg list. + passedOptions = arguments.length > 4 ? options : ename; + options = ename; + for (ename in options) { + if (options.hasOwnProperty(ename)) { + config = options[ename]; + if (!item.$eventOptions[ename]) { + // recurse, but pass the noDestroy parameter as true so that lots + // of individual Destroyables are not created. + // We create a single one at the end if necessary. + me.addManagedListener(item, ename, config.fn || config, config.scope || options.scope || scope, config.fn ? config : passedOptions, true); + } + } + } + if (options && options.destroyable) { + return new ListenerRemover(me, item, options); + } + } else { + if (fn !== emptyFn) { + item.doAddListener(ename, fn, scope, options, null, me, me); + // The 'noDestroy' flag is sent if we're looping through a hash + // of listeners passing each one to addManagedListener separately + if (!noDestroy && options && options.destroyable) { + return new ListenerRemover(me, item, ename, fn, scope); + } + } + } + }, + /** + * Removes listeners that were added by the {@link #mon} method. + * + * @param {Ext.util.Observable/Ext.dom.Element} item The item from which to remove + * a listener/listeners. + * @param {Object/String} ename The event name, or an object containing event name + * properties. + * @param {Function} fn (optional) If the `ename` parameter was an event name, this is + * the handler function. + * @param {Object} scope (optional) If the `ename` parameter was an event name, this is + * the scope (`this` reference) in which the handler function is executed. + */ + removeManagedListener: function(item, ename, fn, scope) { + var me = this, + options, config, managedListeners, length, i; + if (item.$observableDestroyed) { + return; + } + if (typeof ename !== 'string') { + options = ename; + for (ename in options) { + if (options.hasOwnProperty(ename)) { + config = options[ename]; + if (!item.$eventOptions[ename]) { + me.removeManagedListener(item, ename, config.fn || config, config.scope || options.scope || scope); + } + } + } + } else { + managedListeners = me.managedListeners ? me.managedListeners.slice() : []; + ename = Ext.canonicalEventName(ename); + for (i = 0 , length = managedListeners.length; i < length; i++) { + me.removeManagedListenerItem(false, managedListeners[i], item, ename, fn, scope); + } + } + }, + /** + * Fires the specified event with the passed parameters (minus the event name, plus + * the `options` object passed to {@link Ext.util.Observable#addListener addListener}). + * + * An event may be set to bubble up an Observable parent hierarchy (See + * {@link Ext.Component#getBubbleTarget}) by calling {@link #enableBubble}. + * + * @param {String} eventName The name of the event to fire. + * @param {Object...} args Variable number of parameters are passed to handlers. + * @return {Boolean} returns false if any of the handlers return false otherwise it + * returns true. + */ + fireEvent: function(eventName) { + return this.fireEventArgs(eventName, arraySlice.call(arguments, 1)); + }, + /** + * Gets the default scope for firing late bound events (string names with + * no scope attached) at runtime. + * @param {Object} [defaultScope=this] The default scope to return if none is found. + * @return {Object} The default event scope + * @protected + */ + resolveListenerScope: function(defaultScope) { + var namedScope = Ext._namedScopes[defaultScope]; + if (namedScope) { + if (namedScope.isController) { + Ext.raise('scope: "controller" can only be specified on classes ' + 'that derive from Ext.Component or Ext.Widget'); + } + if (namedScope.isSelf || namedScope.isThis) { + defaultScope = null; + } + } + return defaultScope || this; + }, + /** + * Fires the specified event with the passed parameter list. + * + * An event may be set to bubble up an Observable parent hierarchy + * (See {@link Ext.Component#getBubbleTarget}) by calling {@link #enableBubble}. + * + * @param {String} eventName The name of the event to fire. + * @param {Object[]} args An array of parameters which are passed to handlers. + * @return {Boolean} returns false if any of the handlers return false otherwise + * it returns true. + */ + fireEventArgs: function(eventName, args) { + var me = this, + // no need to make events since we need an Event with listeners + events = me.events, + ret = true, + event; + eventName = Ext.canonicalEventName(eventName); + event = events && events[eventName]; + // Only continue firing the event if there are listeners to be informed. + // Bubbled events will always have a listener count, so will be fired. + if (me.hasListeners[eventName]) { + ret = me.doFireEvent(eventName, args || emptyArray, event ? event.bubble : false); + } + return ret; + }, + /** + * Fires the specified event with the passed parameters and executes a function (action). + * By default, the action function will be executed after any "before" event handlers + * (as specified using the `order` option of + * `{@link Ext.util.Observable#addListener addListener}`), but before any other + * handlers are fired. This gives the "before" handlers an opportunity to + * cancel the event by returning `false`, and prevent the action function from + * being called. + * + * The action can also be configured to run after normal handlers, but before any "after" + * handlers (as specified using the `order` event option) by passing `'after'` + * as the `order` parameter. This configuration gives any event handlers except + * for "after" handlers the opportunity to cancel the event and prevent the action + * function from being called. + * + * @param {String} eventName The name of the event to fire. + * @param {Array} args Arguments to pass to handlers and to the action function. + * @param {Function} fn The action function. + * @param {Object} [scope] The scope (`this` reference) in which the handler function is + * executed. **If omitted, defaults to the object which fired the event.** + * @param {Object} [options] Event options for the action function. Accepts any + * of the options of `{@link Ext.util.Observable#addListener addListener}` + * @param {String} [order='before'] The order to call the action function relative + * too the event handlers (`'before'` or `'after'`). Note that this option is + * simply used to sort the action function relative to the event handlers by "priority". + * An order of `'before'` is equivalent to a priority of `99.5`, while an order of + * `'after'` is equivalent to a priority of `-99.5`. See the `priority` option + * of `{@link Ext.util.Observable#addListener addListener}` for more details. + * @deprecated 5.5 Use {@link #fireEventedAction} instead. + */ + fireAction: function(eventName, args, fn, scope, options, order) { + // The historical behaviour has been to default the scope to `this`. + if (typeof fn === 'string' && !scope) { + fn = this[fn]; + } + // chain options to avoid mutating the user's options object + options = options ? Ext.Object.chain(options) : {}; + options.single = true; + options.priority = ((order === 'after') ? -99.5 : 99.5); + this.doAddListener(eventName, fn, scope, options); + this.fireEventArgs(eventName, args); + }, + $eventedController: { + _paused: 1, + pause: function() { + ++this._paused; + }, + resume: function() { + var me = this, + fn = me.fn, + scope = me.scope, + fnArgs = me.fnArgs, + args, ret; + if (!--me._paused) { + if (fn) { + args = Ext.Array.slice(fnArgs || me.args); + if (fnArgs === false) { + // Passing false will remove the first item (typically the owner) + args.shift(); + } + me.fn = null; + // only call fn once + args.push(me); + if (Ext.isFunction(fn)) { + ret = fn.apply(scope, args); + } else if (scope && Ext.isString(fn) && Ext.isFunction(scope[fn])) { + ret = scope[fn].apply(scope, args); + } + if (ret === false) { + return false; + } + } + if (!me._paused) { + // fn could have paused us + return me.owner.fireEventArgs(me.eventName, me.args); + } + } + } + }, + /** + * Fires the specified event with the passed parameters and executes a function (action). + * Evented Actions will automatically dispatch a 'before' event passing. This event will + * be given a special controller that allows for pausing/resuming of the event flow. + * + * By pausing the controller the updater and events will not run until resumed. Pausing, + * however, will not stop the processing of any other before events. + * + * @param {String} eventName The name of the event to fire. + * @param {Array} args Arguments to pass to handlers and to the action function. + * @param {Function/String} fn The action function. + * @param {Object} [scope] The scope (`this` reference) in which the handler function is + * executed. **If omitted, defaults to the object which fired the event.** + * @param {Array/Boolean} [fnArgs] Optional arguments for the action `fn`. If not + * given, the normal `args` will be used to call `fn`. If `false` is passed, the + * `args` are used but if the first argument is this instance it will be removed + * from the args passed to the action function. + */ + fireEventedAction: function(eventName, args, fn, scope, fnArgs) { + var me = this, + eventedBeforeEventNames = me.eventedBeforeEventNames, + beforeEventName = eventedBeforeEventNames[eventName] || (eventedBeforeEventNames[eventName] = 'before' + eventName), + controller = Ext.apply({ + owner: me, + eventName: eventName, + fn: fn, + scope: scope, + fnArgs: fnArgs, + args: args + }, me.$eventedController), + value; + args.push(controller); + value = me.fireEventArgs(beforeEventName, args); + args.pop(); + if (value === false) { + return false; + } + return controller.resume(); + }, + /** + * Continue to fire event. + * @private + * + * @param {String} eventName + * @param {Array} args + * @param {Boolean} bubbles + */ + doFireEvent: function(eventName, args, bubbles) { + var target = this, + queue, event, + ret = true; + do { + if (target.eventsSuspended) { + if ((queue = target.eventQueue)) { + queue.push([ + eventName, + args + ]); + } + return ret; + } else { + event = target.events && target.events[eventName]; + if (event && event !== true) { + if ((ret = event.fire.apply(event, args)) === false) { + break; + } + } + } + } while (// Continue bubbling if event exists and it is `true` or the handler + // didn't returns false and it configure to bubble. + bubbles && (target = target.getBubbleParent())); + return ret; + }, + /** + * Gets the bubbling parent for an Observable + * @private + * @return {Ext.util.Observable} The bubble parent. null is returned if + * no bubble target exists + */ + getBubbleParent: function() { + var me = this, + parent = me.getBubbleTarget && me.getBubbleTarget(); + if (parent && parent.isObservable) { + return parent; + } + return null; + }, + /** + * The {@link #on} method is shorthand for + * {@link Ext.util.Observable#addListener addListener}. + * + * Appends an event handler to this object. For example: + * + * myGridPanel.on("itemclick", this.onItemClick, this); + * + * The method also allows for a single argument to be passed which is a config object + * containing properties which specify multiple events. For example: + * + * myGridPanel.on({ + * cellclick: this.onCellClick, + * select: this.onSelect, + * viewready: this.onViewReady, + * scope: this // Important. Ensure "this" is correct during handler execution + * }); + * + * One can also specify options for each event handler separately: + * + * myGridPanel.on({ + * cellclick: {fn: this.onCellClick, scope: this, single: true}, + * viewready: {fn: panel.onViewReady, scope: panel} + * }); + * + * *Names* of methods in a specified scope may also be used: + * + * myGridPanel.on({ + * cellclick: {fn: 'onCellClick', scope: this, single: true}, + * viewready: {fn: 'onViewReady', scope: panel} + * }); + * + * @param {String/Object} eventName The name of the event to listen for. + * May also be an object who's property names are event names. + * + * @param {Function/String} [fn] The method the event invokes or the *name* of + * the method within the specified `scope`. Will be called with arguments + * given to {@link Ext.util.Observable#fireEvent} plus the `options` parameter described + * below. + * + * @param {Object} [scope] The scope (`this` reference) in which the handler function is + * executed. **If omitted, defaults to the object which fired the event.** + * + * @param {Object} [options] An object containing handler configuration. + * + * **Note:** The options object will also be passed as the last argument to every + * event handler. + * + * This object may contain any of the following properties: + * + * @param {Object} options.scope + * The scope (`this` reference) in which the handler function is executed. **If omitted, + * defaults to the object which fired the event.** + * + * @param {Number} options.delay + * The number of milliseconds to delay the invocation of the handler after the event + * fires. + * + * @param {Boolean} options.single + * True to add a handler to handle just the next firing of the event, and then remove + * itself. + * + * @param {Number} options.buffer + * Causes the handler to be scheduled to run in an {@link Ext.util.DelayedTask} delayed + * by the specified number of milliseconds. If the event fires again within that time, + * the original handler is _not_ invoked, but the new handler is scheduled in its place. + * + * @param {Number} options.onFrame + * Causes the handler to be scheduled to run at the next + * {@link Ext.Function#requestAnimationFrame animation frame event}. If the + * event fires again before that time, the handler is not rescheduled - the handler + * will only be called once when the next animation frame is fired, with the last set + * of arguments passed. + * + * @param {Ext.util.Observable} options.target + * Only call the handler if the event was fired on the target Observable, _not_ if the + * event was bubbled up from a child Observable. + * + * @param {String} options.element + * **This option is only valid for listeners bound to {@link Ext.Component Components}.** + * The name of a Component property which references an {@link Ext.dom.Element element} + * to add a listener to. + * + * This option is useful during Component construction to add DOM event listeners to + * elements of {@link Ext.Component Components} which will exist only after the + * Component is rendered. + * + * For example, to add a click listener to a Panel's body: + * + * var panel = new Ext.panel.Panel({ + * title: 'The title', + * listeners: { + * click: this.handlePanelClick, + * element: 'body' + * } + * }); + * + * In order to remove listeners attached using the element, you'll need to reference + * the element itself as seen below. + * + * panel.body.un(...) + * + * @param {String} [options.delegate] + * A simple selector to filter the event target or look for a descendant of the target. + * + * The "delegate" option is only available on Ext.dom.Element instances (or + * when attaching a listener to a Ext.dom.Element via a Component using the + * element option). + * + * See the *delegate* example below. + * + * @param {Boolean} [options.capture] + * When set to `true`, the listener is fired in the capture phase of the event propagation + * sequence, instead of the default bubble phase. + * + * The `capture` option is only available on Ext.dom.Element instances (or + * when attaching a listener to a Ext.dom.Element via a Component using the + * element option). + * + * @param {Boolean} [options.stopPropagation] + * **This option is only valid for listeners bound to {@link Ext.dom.Element Elements}.** + * `true` to call {@link Ext.event.Event#stopPropagation stopPropagation} on the event + * object before firing the handler. + * + * @param {Boolean} [options.preventDefault] + * **This option is only valid for listeners bound to {@link Ext.dom.Element Elements}.** + * `true` to call {@link Ext.event.Event#preventDefault preventDefault} on the event + * object before firing the handler. + * + * @param {Boolean} [options.stopEvent] + * **This option is only valid for listeners bound to {@link Ext.dom.Element Elements}.** + * `true` to call {@link Ext.event.Event#stopEvent stopEvent} on the event object + * before firing the handler. + * + * @param {Array} [options.args] + * + * Optional set of arguments to pass to the handler function before the actual + * fired event arguments. For example, if `args` is set to `['foo', 42]`, + * the event handler function will be called with an arguments list like this: + * + * handler('foo', 42, ...); + * + * @param {Boolean} [options.destroyable=false] + * When specified as `true`, the function returns a `destroyable` object. An object + * which implements the `destroy` method which removes all listeners added in this call. + * This syntax can be a helpful shortcut to using {@link #un}; particularly when + * removing multiple listeners. *NOTE* - not compatible when using the _element_ + * option. See {@link #un} for the proper syntax for removing listeners added using the + * _element_ config. + * + * @param {Number} [options.priority] + * An optional numeric priority that determines the order in which event handlers + * are run. Event handlers with no priority will be run as if they had a priority + * of 0. Handlers with a higher priority will be prioritized to run sooner than + * those with a lower priority. Negative numbers can be used to set a priority + * lower than the default. Internally, the framework uses a range of 1000 or + * greater, and -1000 or lesser for handlers that are intended to run before or + * after all others, so it is recommended to stay within the range of -999 to 999 + * when setting the priority of event handlers in application-level code. + * A priority must be an integer to be valid. Fractional values are reserved for + * internal framework use. + * + * @param {String} [options.order='current'] + * A legacy option that is provided for backward compatibility. + * It is recommended to use the `priority` option instead. Available options are: + * + * - `'before'`: equal to a priority of `100` + * - `'current'`: equal to a priority of `0` or default priority + * - `'after'`: equal to a priority of `-100` + * + * @param {String} [order='current'] + * A shortcut for the `order` event option. Provided for backward compatibility. + * Please use the `priority` event option instead. + * + * @param caller (private) + * + * **Combining Options** + * + * Using the options argument, it is possible to combine different types of listeners: + * + * A delayed, one-time listener. + * + * myPanel.on('hide', this.handleClick, this, { + * single: true, + * delay: 100 + * }); + * + * **Attaching multiple handlers in 1 call** + * + * The method also allows for a single argument to be passed which is a config object + * containing properties which specify multiple handlers and handler configs. + * + * grid.on({ + * itemclick: 'onItemClick', + * itemcontextmenu: grid.onItemContextmenu, + * destroy: { + * fn: function () { + * // function called within the 'altCmp' scope instead of grid + * }, + * scope: altCmp // unique scope for the destroy handler + * }, + * scope: grid // default scope - provided for example clarity + * }); + * + * **Delegate** + * + * This is a configuration option that you can pass along when registering a handler for + * an event to assist with event delegation. By setting this configuration option + * to a simple selector, the target element will be filtered to look for a + * descendant of the target. For example: + * + * var panel = Ext.create({ + * xtype: 'panel', + * renderTo: document.body, + * title: 'Delegate Handler Example', + * frame: true, + * height: 220, + * width: 220, + * html: '

BODY TITLE

Body content' + * }); + * + * // The click handler will only be called when the click occurs on the + * // delegate: h1.myTitle ("h1" tag with class "myTitle") + * panel.on({ + * click: function (e) { + * console.log(e.getTarget().innerHTML); + * }, + * element: 'body', + * delegate: 'h1.myTitle' + * }); + * + * @return {Object} **Only when the `destroyable` option is specified.** + * + * A `Destroyable` object. An object which implements the `destroy` method which removes + * all listeners added in this call. For example: + * + * this.btnListeners = = myButton.on({ + * destroyable: true + * mouseover: function() { console.log('mouseover'); }, + * mouseout: function() { console.log('mouseout'); }, + * click: function() { console.log('click'); } + * }); + * + * And when those listeners need to be removed: + * + * Ext.destroy(this.btnListeners); + * + * or + * + * this.btnListeners.destroy(); + */ + addListener: function(eventName, fn, scope, options, order, caller) { + var me = this, + namedScopes = Ext._namedScopes, + config, namedScope, isClassListener, innerScope, eventOptions; + // Object listener hash passed + if (typeof eventName !== 'string') { + options = eventName; + scope = options.scope; + namedScope = scope && namedScopes[scope]; + isClassListener = namedScope && namedScope.isSelf; + // give subclasses the opportunity to switch the valid eventOptions + // (Ext.Component uses this when the "element" option is used) + eventOptions = ((me.isComponent || me.isWidget) && options.element) ? me.$elementEventOptions : me.$eventOptions; + for (eventName in options) { + config = options[eventName]; + if (!eventOptions[eventName]) { + /* This would be an API change so check removed until https://sencha.jira.com/browse/EXTJSIV-7183 is fully implemented in 4.2 + // Test must go here as well as in the simple form because of + // the attempted property access here on the config object. + if (!config || (typeof config !== 'function' && !config.fn)) { + Ext.raise('No function passed for event ' + me.$className + '.' + + ename); + } + */ + innerScope = config.scope; + // for proper scope resolution, scope:'controller' specified on an + // inner object, must be translated to 'self.controller' if the + // listeners object was declared on the class body. + // see also Ext.util.Observable#prepareClass and + // Ext.mixin.Inheritable#resolveListenerScope + if (innerScope && isClassListener) { + namedScope = namedScopes[innerScope]; + if (namedScope && namedScope.isController) { + innerScope = 'self.controller'; + } + } + me.doAddListener(eventName, config.fn || config, innerScope || scope, config.fn ? config : options, order, caller); + } + } + if (options && options.destroyable) { + return new ListenerRemover(me, options); + } + } else { + me.doAddListener(eventName, fn, scope, options, order, caller); + if (options && options.destroyable) { + return new ListenerRemover(me, eventName, fn, scope, options); + } + } + return me; + }, + /** + * Removes an event handler. + * + * @param {String} eventName The type of event the handler was associated with. + * @param {Function} fn The handler to remove. **This must be a reference to the function + * passed into the + * {@link Ext.util.Observable#addListener addListener} call.** + * @param {Object} scope (optional) The scope originally specified for the handler. It + * must be the same as the scope argument specified in the original call to + * {@link Ext.util.Observable#addListener} or the listener will not be removed. + * @param eventOptions (private) + * + * **Convenience Syntax** + * + * You can use the {@link Ext.util.Observable#addListener addListener} + * `destroyable: true` config option in place of calling un(). For example: + * + * var listeners = cmp.on({ + * scope: cmp, + * afterrender: cmp.onAfterrender, + * beforehide: cmp.onBeforeHide, + * destroyable: true + * }); + * + * // Remove listeners + * listeners.destroy(); + * // or + * cmp.un( + * scope: cmp, + * afterrender: cmp.onAfterrender, + * beforehide: cmp.onBeforeHide + * ); + * + * **Exception - DOM event handlers using the element config option** + * + * You must go directly through the element to detach an event handler attached using + * the {@link Ext.util.Observable#addListener addListener} _element_ option. + * + * panel.on({ + * element: 'body', + * click: 'onBodyCLick' + * }); + * + * panel.body.un({ + * click: 'onBodyCLick' + * }); + */ + removeListener: function(eventName, fn, scope, eventOptions) { + var me = this, + config, options; + if (typeof eventName !== 'string') { + options = eventName; + // give subclasses the opportunity to switch the valid eventOptions + // (Ext.Component uses this when the "element" option is used) + eventOptions = eventOptions || me.$eventOptions; + for (eventName in options) { + if (options.hasOwnProperty(eventName)) { + config = options[eventName]; + if (!me.$eventOptions[eventName]) { + me.doRemoveListener(eventName, config.fn || config, config.scope || options.scope); + } + } + } + } else { + me.doRemoveListener(eventName, fn, scope); + } + return me; + }, + /** + * Appends a before-event handler. Returning `false` from the handler will stop the event. + * + * Same as {@link Ext.util.Observable#addListener addListener} with `order` set + * to `'before'`. + * + * @param {String/String[]/Object} eventName The name of the event to listen for. + * @param {Function/String} fn The method the event invokes. + * @param {Object} [scope] The scope for `fn`. + * @param {Object} [options] An object containing handler configuration. + */ + onBefore: function(eventName, fn, scope, options) { + return this.addListener(eventName, fn, scope, options, 'before'); + }, + /** + * Appends an after-event handler. + * + * Same as {@link Ext.util.Observable#addListener addListener} with `order` set + * to `'after'`. + * + * @param {String/String[]/Object} eventName The name of the event to listen for. + * @param {Function/String} fn The method the event invokes. + * @param {Object} [scope] The scope for `fn`. + * @param {Object} [options] An object containing handler configuration. + */ + onAfter: function(eventName, fn, scope, options) { + return this.addListener(eventName, fn, scope, options, 'after'); + }, + /** + * Removes a before-event handler. + * + * Same as {@link #removeListener} with `order` set to `'before'`. + * + * @param {String/String[]/Object} eventName The name of the event the handler + * was associated with. + * @param {Function/String} fn The handler to remove. + * @param {Object} [scope] The scope originally specified for `fn`. + * @param {Object} [options] Extra options object. + */ + unBefore: function(eventName, fn, scope, options) { + return this.removeListener(eventName, fn, scope, options, 'before'); + }, + /** + * Removes a before-event handler. + * + * Same as {@link #removeListener} with `order` set to `'after'`. + * + * @param {String/String[]/Object} eventName The name of the event the handler + * was associated with. + * @param {Function/String} fn The handler to remove. + * @param {Object} [scope] The scope originally specified for `fn`. + * @param {Object} [options] Extra options object. + */ + unAfter: function(eventName, fn, scope, options) { + return this.removeListener(eventName, fn, scope, options, 'after'); + }, + /** + * Alias for {@link #onBefore}. + */ + addBeforeListener: function() { + return this.onBefore.apply(this, arguments); + }, + /** + * Alias for {@link #onAfter}. + */ + addAfterListener: function() { + return this.onAfter.apply(this, arguments); + }, + /** + * Alias for {@link #unBefore}. + */ + removeBeforeListener: function() { + return this.unBefore.apply(this, arguments); + }, + /** + * Alias for {@link #unAfter}. + */ + removeAfterListener: function() { + return this.unAfter.apply(this, arguments); + }, + /** + * Removes all listeners for this object including the managed listeners + */ + clearListeners: function() { + var me = this, + events = me.events, + hasListeners = me.hasListeners, + event, key; + if (events) { + for (key in events) { + if (events.hasOwnProperty(key)) { + event = events[key]; + if (event.isEvent) { + delete hasListeners[key]; + event.clearListeners(); + } + } + } + me.events = null; + } + me.clearManagedListeners(); + }, + purgeListeners: function() { + if (Ext.global.console) { + Ext.global.console.warn('Observable: purgeListeners has been deprecated. ' + 'Please use clearListeners.'); + } + return this.clearListeners.apply(this, arguments); + }, + /** + * Removes all managed listeners for this object. + */ + clearManagedListeners: function() { + var me = this, + managedListeners = me.managedListeners, + i, len; + if (managedListeners) { + // So that Event#removeListener doesn't find a managedListeners array from which + // to remove the listener it is removing. It iterates the array to find a match, + // and splices it. + me.managedListeners = null; + for (i = 0 , len = managedListeners.length; i < len; i++) { + me.removeManagedListenerItem(true, managedListeners[i]); + } + managedListeners.length = 0; + } + me.managedListeners = managedListeners; + }, + /** + * Remove a single managed listener item + * @private + * @param {Boolean} isClear True if this is being called during a clear + * @param {Object} managedListener The managed listener item + * @param {Object} item + * @param {String} ename + * @param {Function} fn + * @param {Object} scope + * See removeManagedListener for other args + */ + removeManagedListenerItem: function(isClear, managedListener, item, ename, fn, scope) { + if (isClear || (managedListener.item === item && managedListener.ename === ename && (!fn || managedListener.fn === fn) && (!scope || managedListener.scope === scope))) { + // Pass along the options for mixin.Observable, for example if using delegate. + // If the item has already been destroyed, its listeners were already cleared. + if (!managedListener.item.destroyed) { + managedListener.item.doRemoveListener(managedListener.ename, managedListener.fn, managedListener.scope, managedListener.options); + } + if (!isClear) { + Ext.Array.remove(this.managedListeners, managedListener); + } + } + }, + purgeManagedListeners: function() { + if (Ext.global.console) { + Ext.global.console.warn('Observable: purgeManagedListeners has been deprecated. ' + 'Please use clearManagedListeners.'); + } + return this.clearManagedListeners.apply(this, arguments); + }, + /** + * Checks to see if this object has any listeners for a specified event, or whether + * the event bubbles. The answer indicates whether the event needs firing or not. + * + * @param {String} eventName The name of the event to check for + * @return {Boolean} `true` if the event is being listened for or bubbles, else `false` + */ + hasListener: function(eventName) { + eventName = Ext.canonicalEventName(eventName); + return !!this.hasListeners[eventName]; + }, + /** + * Checks if all events, or a specific event, is suspended. + * @param {String} [event] The name of the specific event to check + * @return {Boolean} `true` if events are suspended + */ + isSuspended: function(event) { + var suspended = this.eventsSuspended > 0, + events = this.events; + if (!suspended && event && events) { + event = events[event]; + if (event && event.isEvent) { + return event.isSuspended(); + } + } + return suspended; + }, + /** + * Suspends the firing of all events. (see {@link #resumeEvents}) + * + * @param {Boolean} queueSuspended `true` to queue up suspended events to be fired + * after the {@link #resumeEvents} call instead of discarding all suspended events. + */ + suspendEvents: function(queueSuspended) { + ++this.eventsSuspended; + if (queueSuspended && !this.eventQueue) { + this.eventQueue = []; + } + }, + /** + * Suspends firing of the named event(s). + * + * After calling this method to suspend events, the events will no longer fire when + * requested to fire. + * + * **Note that if this is called multiple times for a certain event, the converse method + * {@link #resumeEvent} will have to be called the same number of times for it to resume + * firing.** + * + * @param {String...} eventName Multiple event names to suspend. + */ + suspendEvent: function() { + var me = this, + events = me.events, + len = arguments.length, + i, event, ename; + for (i = 0; i < len; i++) { + ename = arguments[i]; + ename = Ext.canonicalEventName(ename); + event = events[ename]; + // we need to spin up the Event instance so it can hold the suspend count + if (!event || !event.isEvent) { + event = me._initEvent(ename); + } + event.suspend(); + } + }, + /** + * Resumes firing of the named event(s). + * + * After calling this method to resume events, the events will fire when requested to fire. + * + * **Note that if the {@link #suspendEvent} method is called multiple times for a certain + * event, this converse method will have to be called the same number of times for it + * to resume firing.** + * + * @param {String...} eventName Multiple event names to resume. + */ + resumeEvent: function() { + var events = this.events || 0, + len = events && arguments.length, + i, event, ename; + for (i = 0; i < len; i++) { + ename = Ext.canonicalEventName(arguments[i]); + event = events[ename]; + // If it exists, and is an Event object (not still a boolean placeholder), resume it + if (event && event.resume) { + event.resume(); + } + } + }, + /** + * Resumes firing events (see {@link #suspendEvents}). + * + * If events were suspended using the `queueSuspended` parameter, then all events fired + * during event suspension will be sent to any listeners now. + * + * @param {Boolean} [discardQueue] `true` to prevent any previously queued events from firing + * while we were suspended. See {@link #suspendEvents}. + */ + resumeEvents: function(discardQueue) { + var me = this, + queued = me.eventQueue, + qLen, q; + if (me.eventsSuspended && !--me.eventsSuspended) { + delete me.eventQueue; + if (!discardQueue && queued) { + qLen = queued.length; + for (q = 0; q < qLen; q++) { + // Important to call fireEventArgs here so MVC can hook in + me.fireEventArgs.apply(me, queued[q]); + } + } + } + }, + /** + * Relays selected events from the specified Observable as if the events were fired + * by `this`. + * + * For example if you are extending Grid, you might decide to forward some events from + * store. So you can do this inside your initComponent: + * + * this.relayEvents(this.getStore(), ['load']); + * + * The grid instance will then have an observable 'load' event which will be passed + * the parameters of the store's load event and any function fired with the grid's + * load event would have access to the grid using the this keyword (unless the event + * is handled by a controller's control/listen event listener in which case 'this' + * will be the controller rather than the grid). + * + * @param {Object} origin The Observable whose events this object is to relay. + * @param {String[]/Object} events Array of event names to relay or an Object with key/value + * pairs translating to ActualEventName/NewEventName respectively. For example: + * this.relayEvents(this, {add:'push', remove:'pop'}); + * + * Would now redispatch the add event of this as a push event and the remove event + * as a pop event. + * + * @param {String} [prefix] A common prefix to prepend to the event names. For example: + * + * this.relayEvents(this.getStore(), ['load', 'clear'], 'store'); + * + * Now the grid will forward 'load' and 'clear' events of store as 'storeload' and + * 'storeclear'. + * + * @return {Object} A `Destroyable` object. An object which implements the `destroy` method + * which, when destroyed, removes all relayers. For example: + * + * this.storeRelayers = this.relayEvents(this.getStore(), ['load', 'clear'], 'store'); + * + * Can be undone by calling + * + * Ext.destroy(this.storeRelayers); + * + * or + * this.store.relayers.destroy(); + */ + relayEvents: function(origin, events, prefix) { + var me = this, + len = events.length, + i = 0, + oldName, newName, + relayers = {}; + if (Ext.isObject(events)) { + for (i in events) { + newName = events[i]; + relayers[i] = me.createRelayer(newName); + } + } else { + for (; i < len; i++) { + oldName = events[i]; + // Build up the listener hash. + relayers[oldName] = me.createRelayer(prefix ? prefix + oldName : oldName); + } + } + // Add the relaying listeners as ManagedListeners so that they are removed when + // this.clearListeners is called (usually when _this_ is destroyed) + // Explicitly pass options as undefined so that the listener does not get + // an extra options param which then has to be sliced off in the relayer. + me.mon(origin, relayers, null, null, undefined); + // relayed events are always destroyable. + return new ListenerRemover(me, origin, relayers); + }, + /** + * @private + * Creates an event handling function which re-fires the event from this object + * as the passed event name. + * @param {String} newName The name under which to re-fire the passed parameters. + * @param {Array} beginEnd (optional) The caller can specify on which indices to slice. + * @return {Function} + */ + createRelayer: function(newName, beginEnd) { + var me = this; + return function() { + // eslint-disable-next-line max-len + return me.fireEventArgs.call(me, newName, beginEnd ? arraySlice.apply(arguments, beginEnd) : arguments); + }; + }, + /** + * Enables events fired by this Observable to bubble up an owner hierarchy by calling + * `this.getBubbleTarget()` if present. There is no implementation in the Observable + * base class. + * + * This is commonly used by Ext.Components to bubble events to owner Containers. + * See {@link Ext.Component#getBubbleTarget}. The default implementation in Ext.Component + * returns the Component's immediate owner. But if a known target is required, this can be + * overridden to access the required target more quickly. + * + * Example: + * + * Ext.define('Ext.overrides.form.field.Base', { + * override: 'Ext.form.field.Base', + * + * // Add functionality to Field's initComponent to enable + * // the change event to bubble + * initComponent: function () { + * this.callParent(); + * this.enableBubble('change'); + * } + * }); + * + * var myForm = Ext.create('Ext.form.Panel', { + * title: 'User Details', + * items: [{ + * ... + * }], + * listeners: { + * change: function() { + * // Title goes red if form has been modified. + * myForm.header.setStyle('color', 'red'); + * } + * } + * }); + * + * @param {String/String[]} eventNames The event name to bubble, or an Array of event names. + */ + enableBubble: function(eventNames) { + if (eventNames) { + // eslint-disable-next-line vars-on-top + var me = this, + names = (typeof eventNames === 'string') ? arguments : eventNames, + // we must create events now if we have not yet + events = me.events, + length = events && names.length, + ename, event, i; + for (i = 0; i < length; ++i) { + ename = names[i]; + ename = Ext.canonicalEventName(ename); + event = events[ename]; + if (!event || !event.isEvent) { + event = me._initEvent(ename); + } + // Event must fire if it bubbles (We don't know if anyone up the + // bubble hierarchy has listeners added) + me.hasListeners._incr_(ename); + event.bubble = true; + } + } + }, + /** + * @private + * Destructor for classes that extend Observable. + */ + destroy: function() { + this.clearListeners(); + this.callParent(); + this.destroyObservable(true); + }, + destroyObservable: function(skipClearListeners) { + var me = this, + clearPropertiesOnDestroy = me.clearPropertiesOnDestroy; + if (me.$observableDestroyed) { + return; + } + if (!skipClearListeners) { + me.clearListeners(); + } + // This method is called after the Base destructor, and most of the instances + // should be already destroyed at this point. However Classic Components are + // conditionally destructible and so can possibly *not* be destroyed before + // our mixed-in destructor is called. Component's destructor will take care + // of that by calling this method explicitly. + if (me.destroyed) { + if (clearPropertiesOnDestroy) { + if (clearPropertiesOnDestroy === true && !me.$nulled) { + me.$reap(); + } + // At this point we can safely assume that the instance is completely + // destroyed and should not be able to fire events anymore. We don't + // want to do this when the prototype is going to be cleared below, + // because having these emptyFns on the object instance will defy + // the purpose of prototype clearing. + if (!me.clearPrototypeOnDestroy) { + me.fireEvent = me.fireEventArgs = me.fireAction = me.fireEventedAction = Ext.emptyFn; + } + // We do not null hasListeners reference since it's a) very special, + // and b) can't possibly lead to significant leaks. (In theory, right). + me.events = me.managedListeners = me.eventedBeforeEventNames = null; + me.$observableDestroyed = true; + } + // Due to the way Observable mixin installs the after handler, + // this can be called twice in a row. Doing that the second time + // will most probably blow up on some method call -- and that is + // totally what we are about, except in this particular case. + if (me.clearPrototypeOnDestroy && Object.setPrototypeOf && !me.$alreadyNulled) { + Object.setPrototypeOf(me, null); + me.$alreadyNulled = true; + } + } + }, + privates: { + doAddListener: function(ename, fn, scope, options, order, caller, manager) { + var me = this, + ret = false, + event, priority; + order = order || (options && options.order); + if (order) { + priority = (options && options.priority); + if (!priority) { + // priority option takes precedence over order + // do not mutate the user's options + options = options ? Ext.Object.chain(options) : {}; + options.priority = me.$orderToPriority[order]; + } + } + ename = Ext.canonicalEventName(ename); + if (!fn) { + Ext.raise("Cannot add '" + ename + "' listener to " + me.$className + " instance. No function specified."); + } + event = (me.events || (me.events = {}))[ename]; + if (!event || !event.isEvent) { + event = me._initEvent(ename); + } + if (fn !== emptyFn) { + // Check whether the listener should be managed. + // Event#addListener will add it to the manager's managedListeners stack + // upon successful add of the listener to the event. + if (!manager && (scope && scope.isObservable && (scope !== me))) { + manager = scope; + } + if (event.addListener(fn, scope, options, caller, manager)) { + // If a new listener has been added (Event.addListener rejects + // duplicates of the same fn+scope) + // then increment the hasListeners counter + me.hasListeners._incr_(ename); + ret = true; + } + } + return ret; + }, + doRemoveListener: function(ename, fn, scope) { + var me = this, + ret = false, + events = me.events, + event; + ename = Ext.canonicalEventName(ename); + event = events && events[ename]; + if (!fn) { + Ext.raise("Cannot remove '" + ename + "' listener to " + me.$className + " instance. No function specified."); + } + if (event && event.isEvent) { + if (event.removeListener(fn, scope)) { + me.hasListeners._decr_(ename); + ret = true; + } + } + return ret; + }, + _initEvent: function(eventName) { + return (this.events[eventName] = new Ext.util.Event(this, eventName)); + } + }, + deprecated: { + '5.0': { + methods: { + addEvents: null + } + } + } + }; +}, function() { + var Observable = this, + proto = Observable.prototype, + HasListeners = function() {}, + prepareMixin = function(T) { + var proto = T.prototype; + if (!T.HasListeners) { + // Keep track of whether we were added via a mixin or not, this becomes + // important later when discovering merged listeners on the class. + proto.$observableMixedIn = 1; + // Classes that use us as a mixin (best practice) need to be prepared. + Observable.prepareClass(T, this); + // Now that we are mixed in to class T, we need to watch T for derivations + // and prepare them also. + T.onExtended(function(U, data) { + if (Ext.classSystemMonitor) { + Ext.classSystemMonitor('extend mixin', arguments); + } + Observable.prepareClass(U, null, data); + }); + // Also, if a class uses us as a mixin and that class is then used as + // a mixin, we need to be notified of that as well. + if (proto.onClassMixedIn) { + // play nice with other potential overrides... + Ext.override(T, { + onClassMixedIn: function(U) { + prepareMixin.call(this, U); + this.callParent(arguments); + } + }); + } else { + // just us chickens, so add the method... + proto.onClassMixedIn = function(U) { + prepareMixin.call(this, U); + }; + } + } + superOnClassMixedIn.call(this, T); + }, + // We are overriding the onClassMixedIn of Ext.Mixin. Save a reference to it + // so we can call it after our onClassMixedIn. + superOnClassMixedIn = proto.onClassMixedIn; + HasListeners.prototype = { + // $$: 42 // to make sure we have a proper prototype + _decr_: function(ev, count) { + // count is optionally passed when clearing listeners in bulk + // e.g. when clearListeners is called on a component that has listeners that + // were attached using the "delegate" option + if (count == null) { + count = 1; + } + if (!(this[ev] -= count)) { + // Delete this entry, since 0 does not mean no one is listening, just + // that no one is *directly* listening. This allows the eventBus or + // class observers to "poke" through and expose their presence. + delete this[ev]; + } + }, + _incr_: function(ev) { + if (this.hasOwnProperty(ev)) { + // if we already have listeners at this level, just increment the count... + ++this[ev]; + } else { + // otherwise, start the count at 1 (which hides whatever is in our prototype + // chain)... + this[ev] = 1; + } + } + }; + proto.HasListeners = Observable.HasListeners = HasListeners; + Observable.createAlias({ + /** + * @method on + * @inheritdoc Ext.util.Observable#method-addListener + */ + on: 'addListener', + /** + * @method un + * Shorthand for {@link #removeListener}. + * @inheritdoc Ext.util.Observable#method-removeListener + */ + un: 'removeListener', + /** + * @method mon + * Shorthand for {@link #addManagedListener}. + * @inheritdoc Ext.util.Observable#method-addManagedListener + */ + mon: 'addManagedListener', + /** + * @method mun + * Shorthand for {@link #removeManagedListener}. + * @inheritdoc Ext.util.Observable#method-removeManagedListener + */ + mun: 'removeManagedListener', + /** + * @method + * An alias for {@link Ext.util.Observable#addListener addListener}. In + * versions prior to 5.1, {@link #listeners} had a generated setter which could + * be called to add listeners. In 5.1 the listeners config is not processed + * using the config system and has no generated setter, so this method is + * provided for backward compatibility. The preferred way of adding listeners + * is to use the {@link #on} method. + * @param {Object} listeners The listeners + */ + setListeners: 'addListener' + }); + // deprecated, will be removed in 5.0 + Observable.observeClass = Observable.observe; + // Used by Ext.mixin.Hookable to create sequences. + function getMethodEvent(method) { + var event = (this.methodEvents = this.methodEvents || {})[method], + returnValue, v, cancel, + me = this, + makeCall; + if (!event) { + me.methodEvents[method] = event = {}; + event.originalFn = me[method]; + event.methodName = method; + event.before = []; + event.after = []; + makeCall = function(fn, scope, args) { + scope = scope || me; + if (typeof fn === 'string') { + fn = scope[fn]; + } + if ((v = fn.apply(scope, args)) !== undefined) { + if (typeof v === 'object') { + if (v.returnValue !== undefined) { + returnValue = v.returnValue; + } else { + returnValue = v; + } + cancel = !!v.cancel; + } else if (v === false) { + cancel = true; + } else { + returnValue = v; + } + } + }; + me[method] = function() { + var args = Array.prototype.slice.call(arguments, 0), + argsLen = args.length, + b, i, len; + returnValue = v = undefined; + cancel = false; + for (i = 0 , len = event.before.length; i < len; i++) { + b = event.before[i]; + if (b.extraArgs) { + args.push.apply(args, b.extraArgs); + } + makeCall(b.fn, b.scope, args); + args.length = argsLen; + if (cancel || b.preventDefault) { + return returnValue; + } + } + if ((v = event.originalFn.apply(me, args)) !== undefined) { + returnValue = v; + } + for (i = 0 , len = event.after.length; i < len; i++) { + b = event.after[i]; + if (b.extraArgs) { + args.push.apply(args, b.extraArgs); + } + makeCall(b.fn, b.scope, args); + args.length = argsLen; + if (cancel || b.preventDefault) { + return returnValue; + } + } + return returnValue; + }; + } + return event; + } + Ext.apply(proto, { + onClassMixedIn: prepareMixin, + // adds an 'interceptor' called before the original method + beforeMethod: function(method, fn, scope, preventDefault, extraArgs) { + getMethodEvent.call(this, method).before.push({ + fn: fn, + scope: scope, + extraArgs: extraArgs, + preventDefault: preventDefault + }); + }, + // adds a 'sequence' called after the original method + afterMethod: function(method, fn, scope, preventDefault, extraArgs) { + getMethodEvent.call(this, method).after.push({ + fn: fn, + scope: scope, + extraArgs: extraArgs, + preventDefault: preventDefault + }); + }, + removeMethodListener: function(method, fn, scope) { + var e = getMethodEvent.call(this, method), + i, len; + for (i = 0 , len = e.before.length; i < len; i++) { + // eslint-disable-next-line eqeqeq + if (e.before[i].fn == fn && e.before[i].scope == scope) { + Ext.Array.erase(e.before, i, 1); + return; + } + } + for (i = 0 , len = e.after.length; i < len; i++) { + // eslint-disable-next-line eqeqeq + if (e.after[i].fn == fn && e.after[i].scope == scope) { + Ext.Array.erase(e.after, i, 1); + return; + } + } + }, + toggleEventLogging: function(toggle) { + Ext.util.Observable[toggle ? 'capture' : 'releaseCapture'](this, function(en) { + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.log(en, arguments); + } + }); + } + }); +}); + +/** + * Represents a collection of a set of key and value pairs. Each key in the HashMap + * must be unique, the same key cannot exist twice. Access to items is provided via + * the key only. Sample usage: + * + * var map = new Ext.util.HashMap(); + * map.add('key1', 1); + * map.add('key2', 2); + * map.add('key3', 3); + * + * map.each(function(key, value, length){ + * console.log(key, value, length); + * }); + * + * The HashMap is an unordered class, + * there is no guarantee when iterating over the items that they will be in any particular + * order. If this is required, then use a {@link Ext.util.MixedCollection}. + */ +Ext.define('Ext.util.HashMap', { + mixins: [ + Ext.mixin.Observable + ], + /** + * Mutation counter which is incremented upon add and remove. + * @readonly + */ + generation: 0, + config: { + /** + * @cfg {Function} keyFn A function that is used to retrieve a default key for a passed + * object. A default is provided that returns the `id` property on the object. This function + * is only used if the `add` method is called with a single argument. + */ + keyFn: null + }, + /** + * @event add + * Fires when a new item is added to the hash. + * @param {Ext.util.HashMap} this + * @param {String} key The key of the added item. + * @param {Object} value The value of the added item. + */ + /** + * @event clear + * Fires when the hash is cleared. + * @param {Ext.util.HashMap} this + */ + /** + * @event remove + * Fires when an item is removed from the hash. + * @param {Ext.util.HashMap} this + * @param {String} key The key of the removed item. + * @param {Object} value The value of the removed item. + */ + /** + * @event replace + * Fires when an item is replaced in the hash. + * @param {Ext.util.HashMap} this + * @param {String} key The key of the replaced item. + * @param {Object} value The new value for the item. + * @param {Object} old The old value for the item. + */ + /** + * Creates new HashMap. + * @param {Object} config (optional) Config object. + */ + constructor: function(config) { + var me = this, + fn; + // Will call initConfig + me.mixins.observable.constructor.call(me, config); + me.clear(true); + fn = me.getKeyFn(); + if (fn) { + me.getKey = fn; + } + }, + /** + * Gets the number of items in the hash. + * @return {Number} The number of items in the hash. + */ + getCount: function() { + return this.length; + }, + /** + * Implementation for being able to extract the key from an object if only + * a single argument is passed. + * @private + * @param {String} key The key + * @param {Object} value The value + * @return {Array} [key, value] + */ + getData: function(key, value) { + // if we have no value, it means we need to get the key from the object + if (value === undefined) { + value = key; + key = this.getKey(value); + } + return [ + key, + value + ]; + }, + /** + * Extracts the key from an object. This is a default implementation, it may be overridden + * @param {Object} o The object to get the key from + * @return {String} The key to use. + */ + getKey: function(o) { + return o.id; + }, + /** + * Adds an item to the collection. Fires the {@link #event-add} event when complete. + * + * @param {String/Object} key The key to associate with the item, or the new item. + * + * If a {@link #getKey} implementation was specified for this HashMap, + * or if the key of the stored items is in a property called `id`, + * the HashMap will be able to *derive* the key for the new item. + * In this case just pass the new item in this parameter. + * + * @param {Object} [value] The item to add. + * @return {Object} The item added. + */ + add: function(key, value) { + var me = this; + // Need to check arguments length here, since we could have called: + // map.add('foo', undefined); + if (arguments.length === 1) { + value = key; + key = me.getKey(value); + } + if (me.containsKey(key)) { + return me.replace(key, value); + } + me.map[key] = value; + ++me.length; + me.generation++; + if (me.hasListeners.add) { + me.fireEvent('add', me, key, value); + } + return value; + }, + /** + * Replaces an item in the hash. If the key doesn't exist, the + * {@link #method-add} method will be used. + * @param {String} key The key of the item. + * @param {Object} value The new value for the item. + * @return {Object} The new value of the item. + */ + replace: function(key, value) { + var me = this, + map = me.map, + old; + // Need to check arguments length here, since we could have called: + // map.replace('foo', undefined); + if (arguments.length === 1) { + value = key; + key = me.getKey(value); + } + if (!me.containsKey(key)) { + me.add(key, value); + } + old = map[key]; + map[key] = value; + me.generation++; + if (me.hasListeners.replace) { + me.fireEvent('replace', me, key, value, old); + } + return value; + }, + /** + * Remove an item from the hash. + * @param {Object} o The value of the item to remove. + * @return {Boolean} True if the item was successfully removed. + */ + remove: function(o) { + var key = this.findKey(o); + if (key !== undefined) { + return this.removeAtKey(key); + } + return false; + }, + /** + * Remove an item from the hash. + * @param {String} key The key to remove. + * @return {Boolean} True if the item was successfully removed. + */ + removeAtKey: function(key) { + var me = this, + value; + if (me.containsKey(key)) { + value = me.map[key]; + delete me.map[key]; + --me.length; + me.generation++; + if (me.hasListeners.remove) { + me.fireEvent('remove', me, key, value); + } + return true; + } + return false; + }, + /** + * Retrieves an item with a particular key. + * @param {String} key The key to lookup. + * @return {Object} The value at that key. If it doesn't exist, `undefined` is returned. + */ + get: function(key) { + var map = this.map; + return map.hasOwnProperty(key) ? map[key] : undefined; + }, + /** + * @ignore + */ + clear: function(initial) { + // We use the above syntax because we don't want the initial param to be part + // of the public API + var me = this; + // Only clear if it has ever had any content + if (initial || me.generation) { + me.map = {}; + me.length = 0; + me.generation = initial ? 0 : me.generation + 1; + } + if (initial !== true && me.hasListeners.clear) { + me.fireEvent('clear', me); + } + return me; + }, + /** + * Checks whether a key exists in the hash. + * @param {String} key The key to check for. + * @return {Boolean} True if they key exists in the hash. + */ + containsKey: function(key) { + var map = this.map; + return map.hasOwnProperty(key) && map[key] !== undefined; + }, + /** + * Checks whether a value exists in the hash. + * @param {Object} value The value to check for. + * @return {Boolean} True if the value exists in the dictionary. + */ + contains: function(value) { + return this.containsKey(this.findKey(value)); + }, + /** + * Return all of the keys in the hash. + * @return {Array} An array of keys. + */ + getKeys: function() { + return this.getArray(true); + }, + /** + * Return all of the values in the hash. + * @return {Array} An array of values. + */ + getValues: function() { + return this.getArray(false); + }, + /** + * Gets either the keys/values in an array from the hash. + * @private + * @param {Boolean} isKey True to extract the keys, otherwise, the value + * @return {Array} An array of either keys/values from the hash. + */ + getArray: function(isKey) { + var arr = [], + key, + map = this.map; + for (key in map) { + if (map.hasOwnProperty(key)) { + arr.push(isKey ? key : map[key]); + } + } + return arr; + }, + /** + * Executes the specified function once for each item in the hash. + * Returning false from the function will cease iteration. + * + * @param {Function} fn The function to execute. + * @param {String} fn.key The key of the item. + * @param {Number} fn.value The value of the item. + * @param {Number} fn.length The total number of items in the hash. + * @param {Object} [scope] The scope to execute in. Defaults to this. + * @return {Ext.util.HashMap} this + */ + each: function(fn, scope) { + // copy items so they may be removed during iteration. + var items = Ext.apply({}, this.map), + key, + length = this.length; + scope = scope || this; + for (key in items) { + if (items.hasOwnProperty(key)) { + if (fn.call(scope, key, items[key], length) === false) { + break; + } + } + } + return this; + }, + /** + * Performs a shallow copy on this hash. + * @return {Ext.util.HashMap} The new hash object. + */ + clone: function() { + var hash = new this.self(this.initialConfig), + map = this.map, + key; + hash.suspendEvents(); + for (key in map) { + if (map.hasOwnProperty(key)) { + hash.add(key, map[key]); + } + } + hash.resumeEvents(); + return hash; + }, + /** + * @private + * Find the key for a value. + * @param {Object} value The value to find. + * @return {Object} The value of the item. Returns undefined if not found. + */ + findKey: function(value) { + var key, + map = this.map; + for (key in map) { + if (map.hasOwnProperty(key) && map[key] === value) { + return key; + } + } + return undefined; + } +}, function(HashMap) { + var prototype = HashMap.prototype; + /** + * @method removeByKey + * An alias for {@link #removeAtKey} + * @inheritdoc Ext.util.HashMap#removeAtKey + */ + prototype.removeByKey = prototype.removeAtKey; +}); + +// +// Ext.promise.Consequence adapted from: +// [DeftJS](https://github.com/deftjs/deftjs5) +// Copyright (c) 2012-2013 [DeftJS Framework Contributors](http://deftjs.org) +// Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License). +// +/** + * Consequences are used internally by a Deferred to capture and notify callbacks, and + * propagate their transformed results as fulfillment or rejection. + * + * Developers never directly interact with a Consequence. + * + * A Consequence forms a chain between two Deferreds, where the result of the first + * Deferred is transformed by the corresponding callback before being applied to the + * second Deferred. + * + * Each time a Deferred's `then` method is called, it creates a new Consequence that will + * be triggered once its originating Deferred has been fulfilled or rejected. A Consequence + * captures a pair of optional onFulfilled and onRejected callbacks. + * + * Each Consequence has its own Deferred (which in turn has a Promise) that is resolved or + * rejected when the Consequence is triggered. When a Consequence is triggered by its + * originating Deferred, it calls the corresponding callback and propagates the transformed + * result to its own Deferred; resolved with the callback return value or rejected with any + * error thrown by the callback. + * + * @since 6.0.0 + * @private + */ +Ext.define('Ext.promise.Consequence', function(Consequence) { + return { + // eslint-disable-line brace-style, max-len + /** + * @property {Ext.promise.Promise} + * Promise of the future value of this Consequence. + */ + promise: null, + /** + * @property {Ext.promise.Deferred} deferred Internal Deferred for this Consequence. + * + * @private + */ + deferred: null, + /** + * @property {Function} onFulfilled Callback to execute when this Consequence is triggered + * with a fulfillment value. + * + * @private + */ + onFulfilled: null, + /** + * @property {Function} onRejected Callback to execute when this Consequence is triggered + * with a rejection reason. + * + * @private + */ + onRejected: null, + /** + * @property {Function} onProgress Callback to execute when this Consequence is updated + * with a progress value. + * + * @private + */ + onProgress: null, + /** + * @param {Function} onFulfilled Callback to execute to transform a fulfillment value. + * @param {Function} onRejected Callback to execute to transform a rejection reason. + * @param {Function} onProgress Callback to execute to transform a progress value. + */ + constructor: function(onFulfilled, onRejected, onProgress) { + var me = this; + me.onFulfilled = onFulfilled; + me.onRejected = onRejected; + me.onProgress = onProgress; + me.deferred = new Ext.promise.Deferred(); + me.promise = me.deferred.promise; + }, + /** + * Trigger this Consequence with the specified action and value. + * + * @param {String} action Completion action (i.e. fulfill or reject). + * @param {Mixed} value Fulfillment value or rejection reason. + */ + trigger: function(action, value) { + var me = this, + deferred = me.deferred; + switch (action) { + case 'fulfill': + me.propagate(value, me.onFulfilled, deferred, deferred.resolve); + break; + case 'reject': + me.propagate(value, me.onRejected, deferred, deferred.reject); + break; + } + }, + /** + * Update this Consequence with the specified progress value. + * + * @param {Mixed} progress Progress value. + */ + update: function(progress) { + if (Ext.isFunction(this.onProgress)) { + progress = this.onProgress(progress); + } + this.deferred.update(progress); + }, + /** + * Transform and propagate the specified value using the + * optional callback and propagate the transformed result. + * + * @param {Mixed} value Value to transform and/or propagate. + * @param {Function} [callback] Callback to use to transform the value. + * @param {Function} deferred Deferred to use to propagate the value, if no callback + * was specified. + * @param {Function} deferredMethod Deferred method to call to propagate the value, + * if no callback was specified. + * + * @private + */ + propagate: function(value, callback, deferred, deferredMethod) { + if (Ext.isFunction(callback)) { + this.schedule(function() { + try { + deferred.resolve(callback(value)); + } catch (e) { + deferred.reject(e); + } + }); + } else { + deferredMethod.call(this.deferred, value); + } + }, + /** + * Schedules the specified callback function to be executed on the next turn of the + * event loop. + * + * @param {Function} callback Callback function. + * + * @private + */ + schedule: function(callback) { + var n = Consequence.queueSize++; + Consequence.queue[n] = callback; + if (!n) { + // if (queue was empty) + Ext.asap(Consequence.dispatch); + } + }, + statics: { + /** + * @property {Function[]} queue The queue of callbacks pending. This array is never + * shrunk to reduce GC thrash but instead its elements will be set to `null`. + * + * @private + */ + queue: new Array(10000), + /** + * @property {Number} queueSize The number of callbacks in the `queue`. + * + * @private + */ + queueSize: 0, + /** + * This method drains the callback queue and calls each callback in order. + * + * @private + */ + dispatch: function() { + var queue = Consequence.queue, + fn, i; + // The queue could grow on each call, so we cannot cache queueSize here. + for (i = 0; i < Consequence.queueSize; ++i) { + fn = queue[i]; + queue[i] = null; + // release our reference on the callback + fn(); + } + Consequence.queueSize = 0; + } + } + }; +}, function(Consequence) { + // eslint-disable-line comma-style + Consequence.dispatch.$skipTimerCheck = true; +}); + +/* + Ext.promise.Deferred adapted from: + [DeftJS](https://github.com/deftjs/deftjs5) + Copyright (c) 2012-2013 [DeftJS Framework Contributors](http://deftjs.org) + Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License). + */ +/** + * Deferreds are the mechanism used to create new Promises. A Deferred has a single + * associated Promise that can be safely returned to external consumers to ensure they do + * not interfere with the resolution or rejection of the deferred operation. + * + * A Deferred is typically used within the body of a function that performs an asynchronous + * operation. When that operation succeeds, the Deferred should be resolved; if that + * operation fails, the Deferred should be rejected. + * + * Each Deferred has an associated Promise. A Promise delegates `then` calls to its + * Deferred's `then` method. In this way, access to Deferred operations are divided between + * producer (Deferred) and consumer (Promise) roles. + * + * When a Deferred's `resolve` method is called, it fulfills with the optionally specified + * value. If `resolve` is called with a then-able (i.e.a Function or Object with a `then` + * function, such as another Promise) it assimilates the then-able's result; the Deferred + * provides its own `resolve` and `reject` methods as the onFulfilled or onRejected + * arguments in a call to that then-able's `then` function. If an error is thrown while + * calling the then-able's `then` function (prior to any call back to the specified + * `resolve` or `reject` methods), the Deferred rejects with that error. If a Deferred's + * `resolve` method is called with its own Promise, it rejects with a TypeError. + * + * When a Deferred's `reject` method is called, it rejects with the optionally specified + * reason. + * + * Each time a Deferred's `then` method is called, it captures a pair of optional + * onFulfilled and onRejected callbacks and returns a Promise of the Deferred's future + * value as transformed by those callbacks. + * + * @private + * @since 6.0.0 + */ +Ext.define('Ext.promise.Deferred', { + /** + * @property {Ext.promise.Promise} promise Promise of the future value of this Deferred. + */ + promise: null, + /** + * @property {Ext.promise.Consequence[]} consequences Pending Consequences chained + * to this Deferred. + * + * @private + */ + consequences: [], + /** + * @property {Boolean} completed Indicates whether this Deferred has been completed. + * + * @private + */ + completed: false, + /** + * @property {String} completeAction The completion action (i.e. 'fulfill' or 'reject'). + * + * @private + */ + completionAction: null, + /** + * @property {Mixed} completionValue The completion value (i.e. resolution value + * or rejection error). + * + * @private + */ + completionValue: null, + constructor: function() { + var me = this; + me.promise = new Ext.promise.Promise(me); + me.consequences = []; + me.completed = false; + me.completionAction = null; + me.completionValue = null; + }, + /** + * Used to specify onFulfilled and onRejected callbacks that will be + * notified when the future value becomes available. + * + * Those callbacks can subsequently transform the value that was + * fulfilled or the error that was rejected. Each call to `then` + * returns a new Promise of that transformed value; i.e., a Promise + * that is fulfilled with the callback return value or rejected with + * any error thrown by the callback. + * + * @param {Function} [onFulfilled] Callback to execute to transform a fulfillment value. + * @param {Function} [onRejected] Callback to execute to transform a rejection reason. + * @param {Function} [onProgress] Callback to execute to transform a progress value. + * + * @return Promise that is fulfilled with the callback return value or rejected with + * any error thrown by the callback. + */ + then: function(onFulfilled, onRejected, onProgress) { + var me = this, + consequence = new Ext.promise.Consequence(onFulfilled, onRejected, onProgress); + if (me.completed) { + consequence.trigger(me.completionAction, me.completionValue); + } else { + me.consequences.push(consequence); + } + return consequence.promise; + }, + /** + * Resolve this Deferred with the (optional) specified value. + * + * If called with a then-able (i.e.a Function or Object with a `then` + * function, such as another Promise) it assimilates the then-able's + * result; the Deferred provides its own `resolve` and `reject` methods + * as the onFulfilled or onRejected arguments in a call to that + * then-able's `then` function. If an error is thrown while calling + * the then-able's `then` function (prior to any call back to the + * specified `resolve` or `reject` methods), the Deferred rejects with + * that error. If a Deferred's `resolve` method is called with its own + * Promise, it rejects with a TypeError. + * + * Once a Deferred has been fulfilled or rejected, it is considered to be complete + * and subsequent calls to `resolve` or `reject` are ignored. + * + * @param {Mixed} value Value to resolve as either a fulfillment value or rejection + * reason. + */ + resolve: function(value) { + var me = this, + isHandled, thenFn; + if (me.completed) { + return; + } + try { + if (value === me.promise) { + throw new TypeError('A Promise cannot be resolved with itself.'); + } + if (value != null && (typeof value === 'object' || Ext.isFunction(value)) && Ext.isFunction(thenFn = value.then)) { + isHandled = false; + try { + thenFn.call(value, function(value) { + if (!isHandled) { + isHandled = true; + me.resolve(value); + } + }, function(error) { + if (!isHandled) { + isHandled = true; + me.reject(error); + } + }); + } catch (e1) { + if (!isHandled) { + me.reject(e1); + } + } + } else { + me.complete('fulfill', value); + } + } catch (e2) { + me.reject(e2); + } + }, + /** + * Reject this Deferred with the specified reason. + * + * Once a Deferred has been rejected, it is considered to be complete + * and subsequent calls to `resolve` or `reject` are ignored. + * + * @param {Error} reason Rejection reason. + */ + reject: function(reason) { + if (this.completed) { + return; + } + this.complete('reject', reason); + }, + /** + * Updates progress for this Deferred, if it is still pending, triggering it to + * execute the `onProgress` callback and propagate the resulting transformed progress + * value to Deferreds that originate from this Deferred. + * + * @param {Mixed} progress The progress value. + */ + update: function(progress) { + var consequences = this.consequences, + consequence, i, len; + if (this.completed) { + return; + } + for (i = 0 , len = consequences.length; i < len; i++) { + consequence = consequences[i]; + consequence.update(progress); + } + }, + /** + * Complete this Deferred with the specified action and value. + * + * @param {String} action Completion action (i.e. 'fufill' or 'reject'). + * @param {Mixed} value Fulfillment value or rejection reason. + * + * @private + */ + complete: function(action, value) { + var me = this, + consequences = me.consequences, + consequence, i, len; + me.completionAction = action; + me.completionValue = value; + me.completed = true; + for (i = 0 , len = consequences.length; i < len; i++) { + consequence = consequences[i]; + consequence.trigger(me.completionAction, me.completionValue); + } + me.consequences = null; + } +}); + +/* + Ext.promise.Deferred adapted from: + [DeftJS](https://github.com/deftjs/deftjs5) + Copyright (c) 2012-2014 [DeftJS Framework Contributors](http://deftjs.org) + Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License). + */ +/** + * Promises represent a future value; i.e., a value that may not yet be available. + * + * Users should **not** create instances of this class directly. Instead user code should + * use `new {@link Ext.Promise}()` or `new {@link Ext.Deferred}()` to create and manage + * promises. If the browser supports the standard `Promise` constructor, this class will + * not be used by `Ext.Promise`. This class will always be used by `Ext.Deferred` in order + * to provide enhanced capabilities beyond standard promises. + * + * A Promise's `{@link #then then()}` method is used to specify onFulfilled and onRejected + * callbacks that will be notified when the future value becomes available. Those callbacks + * can subsequently transform the value that was resolved or the reason that was rejected. + * Each call to `then` returns a new Promise of that transformed value; i.e., a Promise + * that is resolved with the callback return value or rejected with any error thrown by + * the callback. + * + * ## Basic Usage + * + * this.companyService.loadCompanies().then( + * function(records) { + * // Do something with result. + * }, + * function(error) { + * // Do something on failure. + * }). + * always(function() { + * // Do something whether call succeeded or failed + * }); + * + * The above code uses the `Promise` returned from the `companyService.loadCompanies()` + * method and uses `then()` to attach success and failure handlers. Finally, an `always()` + * method call is chained onto the returned promise. This specifies a callback function + * that will run whether the underlying call succeeded or failed. + * + * See `{@link Ext.Deferred}` for an example of using the returned Promise. + * + * [1]: http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts#april_14_2015_rev_38_final_draft + * + * @since 6.0.0 + */ +Ext.define('Ext.promise.Promise', function(ExtPromise) { + var Deferred; + /* eslint-disable indent */ + return { + statics: { + /** + * @property CancellationError + * @static + * The type of `Error` propagated by the `{@link #method-cancel}` method. If + * the browser provides a native `CancellationError` then that type is used. If + * not, a basic `Error` type is used. + */ + CancellationError: Ext.global.CancellationError || Error, + _ready: function() { + // Our requires are met, so we can cache Ext.promise.Deferred + Deferred = Ext.promise.Deferred; + }, + /** + * Returns a new Promise that will only resolve once all the specified + * `promisesOrValues` have resolved. + * + * The resolution value will be an Array containing the resolution value of each + * of the `promisesOrValues`. + * + * The public API's to use instead of this method are `{@link Ext.Promise#all}` + * and `{@link Ext.Deferred#all}`. + * + * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An + * Array of values or Promises, or a Promise of an Array of values or Promises. + * @return {Ext.promise.Promise} A Promise of an Array of the resolved values. + * + * @static + * @private + */ + all: function(promisesOrValues) { + if (!(Ext.isArray(promisesOrValues) || ExtPromise.is(promisesOrValues))) { + Ext.raise('Invalid parameter: expected an Array or Promise of an Array.'); + } + return ExtPromise.when(promisesOrValues).then(function(promisesOrValues) { + var deferred = new Deferred(), + remainingToResolve = promisesOrValues.length, + results = new Array(remainingToResolve), + index, promiseOrValue, resolve, i, len; + if (!remainingToResolve) { + deferred.resolve(results); + } else { + resolve = function(item, index) { + return ExtPromise.when(item).then(function(value) { + results[index] = value; + if (!--remainingToResolve) { + deferred.resolve(results); + } + return value; + }, function(reason) { + return deferred.reject(reason); + }); + }; + for (index = i = 0 , len = promisesOrValues.length; i < len; index = ++i) { + promiseOrValue = promisesOrValues[index]; + if (index in promisesOrValues) { + resolve(promiseOrValue, index); + } else { + remainingToResolve--; + } + } + } + return deferred.promise; + }); + }, + /** + * Determines whether the specified value is a Promise (including third-party + * untrusted Promises or then()-ables), based on the Promises/A specification + * feature test. + * + * @param {Mixed} value A potential Promise. + * @return {Boolean} `true` if the given value is a Promise, otherwise `false`. + * @static + * @private + */ + is: function(value) { + return value != null && (typeof value === 'object' || Ext.isFunction(value)) && Ext.isFunction(value.then); + }, + /** + * Returns a promise that resolves or rejects as soon as one of the promises in the array + * resolves or rejects, with the value or reason from that promise. + * @param {Ext.promise.Promise[]} promises The promises. + * @return {Ext.promise.Promise} The promise to be resolved when the race completes. + * + * @private + * @static + * @since 6.5.0 + */ + race: function(promises) { + var deferred = new Deferred(), + len = promises.length, + i; + if (!Ext.isArray(promises)) { + Ext.raise('Invalid parameter: expected an Array.'); + } + for (i = 0; i < len; ++i) { + deferred.resolve(promises[i]); + } + return deferred.promise; + }, + /** + * Rethrows the specified Error on the next turn of the event loop. + * @static + * @private + */ + rethrowError: function(error) { + Ext.asap(function() { + throw error; + }); + }, + /** + * Returns a new Promise that either + * + * * Resolves immediately for the specified value, or + * * Resolves or rejects when the specified promise (or third-party Promise or + * then()-able) is resolved or rejected. + * + * The public API's to use instead of this method are `{@link Ext.Promise#resolve}` + * and `{@link Ext.Deferred#resolved}`. + * + * @param {Mixed} value A Promise (or third-party Promise or then()-able) + * or value. + * @return {Ext.Promise} A Promise of the specified Promise or value. + * + * @static + * @private + */ + when: function(value) { + var deferred = new Deferred(); + deferred.resolve(value); + return deferred.promise; + } + }, + /** + * @property {Ext.promise.Deferred} Reference to this promise's + * `{@link Ext.promise.Deferred Deferred}` instance. + * + * @readonly + * @private + */ + owner: null, + /** + * NOTE: {@link Ext.promise.Deferred Deferreds} are the mechanism used to create new + * Promises. + * @param {Ext.promise.Deferred} owner The owning `Deferred` instance. + * + * @private + */ + constructor: function(owner) { + this.owner = owner; + }, + /** + * Attaches onFulfilled and onRejected callbacks that will be notified when the future + * value becomes available. + * + * Those callbacks can subsequently transform the value that was fulfilled or the error + * that was rejected. Each call to `then` returns a new Promise of that transformed + * value; i.e., a Promise that is fulfilled with the callback return value or rejected + * with any error thrown by the callback. + * + * @param {Function} onFulfilled Optional callback to execute to transform a + * fulfillment value. + * @param {Function} onRejected Optional callback to execute to transform a rejection + * reason. + * @param {Function} onProgress Optional callback function to be called with progress + * updates. + * @param {Object} scope Optional scope for the callback(s). + * @return {Ext.promise.Promise} Promise that is fulfilled with the callback return + * value or rejected with any error thrown by the callback. + */ + then: function(onFulfilled, onRejected, onProgress, scope) { + var ref; + if (arguments.length === 1 && Ext.isObject(arguments[0])) { + ref = arguments[0]; + onFulfilled = ref.success; + onRejected = ref.failure; + onProgress = ref.progress; + scope = ref.scope; + } + if (scope) { + if (onFulfilled) { + onFulfilled = onFulfilled.bind(scope); + } + if (onRejected) { + onRejected = onRejected.bind(scope); + } + if (onProgress) { + onProgress = onProgress.bind(scope); + } + } + return this.owner.then(onFulfilled, onRejected, onProgress); + }, + /** + * Attaches an onRejected callback that will be notified if this Promise is rejected. + * + * The callback can subsequently transform the reason that was rejected. Each call to + * `otherwise` returns a new Promise of that transformed value; i.e., a Promise that + * is resolved with the original resolved value, or resolved with the callback return + * value or rejected with any error thrown by the callback. + * + * @param {Function} onRejected Callback to execute to transform a rejection reason. + * @param {Object} scope Optional scope for the callback. + * @return {Ext.promise.Promise} Promise of the transformed future value. + * + * @since 6.5.0 + */ + 'catch': function(onRejected, scope) { + var ref; + if (arguments.length === 1 && Ext.isObject(arguments[0])) { + ref = arguments[0]; + onRejected = ref.fn; + scope = ref.scope; + } + if (scope != null) { + onRejected = onRejected.bind(scope); + } + return this.owner.then(null, onRejected); + }, + /** + * An alias for the {@link #catch} method. To be used for browsers + * where catch cannot be used as a method name. + */ + otherwise: function(onRejected, scope) { + return this['catch'].apply(this, arguments); + }, + // eslint-disable-line dot-notation + /** + * Attaches an onCompleted callback that will be notified when this Promise is completed. + * + * Similar to `finally` in `try... catch... finally`. + * + * NOTE: The specified callback does not affect the resulting Promise's outcome; any + * return value is ignored and any Error is rethrown. + * + * @param {Function} onCompleted Callback to execute when the Promise is resolved or + * rejected. + * @param {Object} scope Optional scope for the callback. + * @return {Ext.promise.Promise} A new "pass-through" Promise that is resolved with + * the original value or rejected with the original reason. + */ + always: function(onCompleted, scope) { + var ref; + if (arguments.length === 1 && Ext.isObject(arguments[0])) { + ref = arguments[0]; + onCompleted = ref.fn; + scope = ref.scope; + } + if (scope != null) { + onCompleted = onCompleted.bind(scope); + } + return this.owner.then(function(value) { + try { + onCompleted(); + } catch (e) { + ExtPromise.rethrowError(e); + } + return value; + }, function(reason) { + try { + onCompleted(); + } catch (e) { + ExtPromise.rethrowError(e); + } + throw reason; + }); + }, + /** + * Terminates a Promise chain, ensuring that unhandled rejections will be rethrown as + * Errors. + * + * One of the pitfalls of interacting with Promise-based APIs is the tendency for + * important errors to be silently swallowed unless an explicit rejection handler is + * specified. + * + * For example: + * + * promise.then(function() { + * // logic in your callback throws an error and it is interpreted as a + * // rejection. throw new Error("Boom!"); + * }); + * + * // The Error was not handled by the Promise chain and is silently swallowed. + * + * This problem can be addressed by terminating the Promise chain with the done() + * method: + * + * promise.then(function() { + * // logic in your callback throws an error and it is interpreted as a + * // rejection. throw new Error("Boom!"); + * }).done(); + * + * // The Error was not handled by the Promise chain and is rethrown by done() on + * // the next tick. + * + * The `done()` method ensures that any unhandled rejections are rethrown as Errors. + */ + done: function() { + this.owner.then(null, ExtPromise.rethrowError); + }, + /** + * Cancels this Promise if it is still pending, triggering a rejection with a + * `{@link #CancellationError}` that will propagate to any Promises originating from + * this Promise. + * + * NOTE: Cancellation only propagates to Promises that branch from the target Promise. + * It does not traverse back up to parent branches, as this would reject nodes from + * which other Promises may have branched, causing unintended side-effects. + * + * @param {Error} reason Cancellation reason. + */ + cancel: function(reason) { + if (reason == null) { + reason = null; + } + this.owner.reject(new this.self.CancellationError(reason)); + }, + /** + * Logs the resolution or rejection of this Promise with the specified category and + * optional identifier. Messages are logged via all registered custom logger functions. + * + * @param {String} identifier An optional identifier to incorporate into the + * resulting log entry. + * + * @return {Ext.promise.Promise} A new "pass-through" Promise that is resolved with + * the original value or rejected with the original reason. + */ + log: function(identifier) { + if (identifier == null) { + identifier = ''; + } + return this.owner.then(function(value) { + Ext.log("" + (identifier || 'Promise') + " resolved with value: " + value); + return value; + }, function(reason) { + Ext.log("" + (identifier || 'Promise') + " rejected with reason: " + reason); + throw reason; + }); + } + }; +}, function(ExtPromise) { + ExtPromise._ready(); +}); + +/** + * This class provides an API compatible implementation of the ECMAScript 6 Promises API + * (providing an implementation as necessary for browsers that do not natively support the + * `Promise` class). + * + * This class will use the native `Promise` implementation if one is available. The + * native implementation, while standard, does not provide all of the features of the + * Ext JS Promises implementation. + * + * To use the Ext JS enhanced Promises implementation, see `{@link Ext.Deferred}` for + * creating enhanced promises and additional static utility methods. + * + * Typical usage: + * + * function getAjax (url) { + * // The function passed to Ext.Promise() is called immediately to start + * // the asynchronous action. + * // + * return new Ext.Promise(function (resolve, reject) { + * Ext.Ajax.request({ + * url: url, + * + * success: function (response) { + * // Use the provided "resolve" method to deliver the result. + * // + * resolve(response.responseText); + * }, + * + * failure: function (response) { + * // Use the provided "reject" method to deliver error message. + * // + * reject(response.status); + * } + * }); + * }); + * } + * + * getAjax('http://stuff').then(function (content) { + * // content is responseText of ajax response + * }); + * + * To adapt the Ext JS `{@link Ext.data.Store store}` to use a Promise, you might do + * something like this: + * + * loadCompanies: function() { + * var companyStore = this.companyStore; + * + * return new Ext.Promise(function (resolve, reject) { + * companyStore.load({ + * callback: function(records, operation, success) { + * if (success) { + * // Use the provided "resolve" method to drive the promise: + * resolve(records); + * } + * else { + * // Use the provided "reject" method to drive the promise: + * reject("Error loading Companies."); + * } + * } + * }); + * }); + * } + * + * @since 6.0.0 + */ +Ext.define('Ext.Promise', function() { + /* eslint-disable indent */ + var Polyfiller; + return { + statics: { + _ready: function() { + // We can cache this now that our requires are met + Polyfiller = Ext.promise.Promise; + }, + /** + * Returns a new Promise that will only resolve once all the specified + * `promisesOrValues` have resolved. + * + * The resolution value will be an Array containing the resolution value of each + * of the `promisesOrValues`. + * + * @param {Mixed[]/Ext.Promise[]/Ext.Promise} promisesOrValues An Array of values + * or Promises, or a Promise of an Array of values or Promises. + * + * @return {Ext.Promise} A Promise of an Array of the resolved values. + * @static + */ + all: function() { + return Polyfiller.all.apply(Polyfiller, arguments); + }, + /** + * Returns a promise that resolves or rejects as soon as one of the promises in the + * array resolves or rejects, with the value or reason from that promise. + * @param {Ext.promise.Promise[]} promises The promises. + * @return {Ext.promise.Promise} The promise to be resolved when the race completes. + * + * @static + * @since 6.5.0 + */ + race: function() { + return Polyfiller.race.apply(Polyfiller, arguments); + }, + /** + * Convenience method that returns a new Promise rejected with the specified + * reason. + * + * @param {Error} reason Rejection reason. + * @return {Ext.Promise} The rejected Promise. + * @static + */ + reject: function(reason) { + var deferred = new Ext.promise.Deferred(); + deferred.reject(reason); + return deferred.promise; + }, + /** + * Returns a new Promise that either + * + * * Resolves immediately for the specified value, or + * * Resolves or rejects when the specified promise (or third-party Promise or + * then()-able) is resolved or rejected. + * + * @param {Mixed} value A Promise (or third-party Promise or then()-able) + * or value. + * @return {Ext.Promise} A Promise of the specified Promise or value. + * @static + */ + resolve: function(value) { + var deferred = new Ext.promise.Deferred(); + deferred.resolve(value); + return deferred.promise; + } + }, + constructor: function(action) { + var deferred = new Ext.promise.Deferred(); + action(deferred.resolve.bind(deferred), deferred.reject.bind(deferred)); + return deferred.promise; + } + }; +}, function(ExtPromise) { + var P = Ext.global.Promise; + if (P && P.resolve && !Ext.useExtPromises) { + Ext.Promise = P; + } else { + ExtPromise._ready(); + } +}); + +// +// Ext.Deferred adapted from: +// [DeftJS](https://github.com/deftjs/deftjs5) +// Copyright (c) 2012-2013 [DeftJS Framework Contributors](http://deftjs.org) +// Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License). +// +// when(), all(), any(), some(), map(), reduce(), delay() and timeout() +// sequence(), parallel(), pipeline() +// methods adapted from: [when.js](https://github.com/cujojs/when) +// Copyright (c) B Cavalier & J Hann +// Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License). +// +/** + * Deferreds are the mechanism used to create new Promises. A Deferred has a single + * associated Promise that can be safely returned to external consumers to ensure they do + * not interfere with the resolution or rejection of the deferred operation. + * + * This implementation of Promises is an extension of the ECMAScript 6 Promises API as + * detailed [here][1]. For a compatible, though less full featured, API see `{@link Ext.Promise}`. + * + * A Deferred is typically used within the body of a function that performs an asynchronous + * operation. When that operation succeeds, the Deferred should be resolved; if that + * operation fails, the Deferred should be rejected. + * + * Each Deferred has an associated Promise. A Promise delegates `then` calls to its + * Deferred's `then` method. In this way, access to Deferred operations are divided between + * producer (Deferred) and consumer (Promise) roles. + * + * ## Basic Usage + * + * In it's most common form, a method will create and return a Promise like this: + * + * // A method in a service class which uses a Store and returns a Promise + * // + * loadCompanies: function () { + * var deferred = new Ext.Deferred(); // create the Ext.Deferred object + * + * this.companyStore.load({ + * callback: function (records, operation, success) { + * if (success) { + * // Use "deferred" to drive the promise: + * deferred.resolve(records); + * } + * else { + * // Use "deferred" to drive the promise: + * deferred.reject("Error loading Companies."); + * } + * } + * }); + * + * return deferred.promise; // return the Promise to the caller + * } + * + * You can see this method first creates a `{@link Ext.Deferred Deferred}` object. It then + * returns its `Promise` object for use by the caller. Finally, in the asynchronous + * callback, it resolves the `deferred` object if the call was successful, and rejects the + * `deferred` if the call failed. + * + * When a Deferred's `resolve` method is called, it fulfills with the optionally specified + * value. If `resolve` is called with a then-able (i.e.a Function or Object with a `then` + * function, such as another Promise) it assimilates the then-able's result; the Deferred + * provides its own `resolve` and `reject` methods as the onFulfilled or onRejected + * arguments in a call to that then-able's `then` function. If an error is thrown while + * calling the then-able's `then` function (prior to any call back to the specified + * `resolve` or `reject` methods), the Deferred rejects with that error. If a Deferred's + * `resolve` method is called with its own Promise, it rejects with a TypeError. + * + * When a Deferred's `reject` method is called, it rejects with the optionally specified + * reason. + * + * Each time a Deferred's `then` method is called, it captures a pair of optional + * onFulfilled and onRejected callbacks and returns a Promise of the Deferred's future + * value as transformed by those callbacks. + * + * See `{@link Ext.promise.Promise}` for an example of using the returned Promise. + * + * @since 6.0.0 + */ +Ext.define('Ext.Deferred', function(Deferred) { + /* eslint indent: "off" */ + var ExtPromise, rejected, resolved, when; + // eslint-disable-line + return { + extend: Ext.promise.Deferred, + statics: { + _ready: function() { + // Our requires are met, so we can cache Ext.promise.Deferred + ExtPromise = Ext.promise.Promise; + when = Ext.Promise.resolve; + }, + /** + * Returns a new Promise that will only resolve once all the specified + * `promisesOrValues` have resolved. + * + * The resolution value will be an Array containing the resolution value of each + * of the `promisesOrValues`. + * + * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An + * Array of values or Promises, or a Promise of an Array of values or Promises. + * @return {Ext.promise.Promise} A Promise of an Array of the resolved values. + * @static + */ + all: function() { + return ExtPromise.all.apply(ExtPromise, arguments); + }, + /** + * Initiates a competitive race, returning a new Promise that will resolve when + * any one of the specified `promisesOrValues` have resolved, or will reject when + * all `promisesOrValues` have rejected or cancelled. + * + * The resolution value will the first value of `promisesOrValues` to resolve. + * + * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An + * Array of values or Promises, or a Promise of an Array of values or Promises. + * @return {Ext.promise.Promise} A Promise of the first resolved value. + * @static + */ + any: function(promisesOrValues) { + if (!(Ext.isArray(promisesOrValues) || ExtPromise.is(promisesOrValues))) { + Ext.raise('Invalid parameter: expected an Array or Promise of an Array.'); + } + return Deferred.some(promisesOrValues, 1).then(function(array) { + return array[0]; + }, function(error) { + if (error instanceof Error && error.message === 'Too few Promises were resolved.') { + Ext.raise('No Promises were resolved.'); + } else { + throw error; + } + }); + }, + /** + * Returns a new Promise that will automatically resolve with the specified + * Promise or value after the specified delay (in milliseconds). + * + * @param {Mixed} promiseOrValue A Promise or value. + * @param {Number} milliseconds A delay duration (in milliseconds). + * @return {Ext.promise.Promise} A Promise of the specified Promise or value that + * will resolve after the specified delay. + * @static + */ + delay: function(promiseOrValue, milliseconds) { + var deferred; + if (arguments.length === 1) { + milliseconds = promiseOrValue; + promiseOrValue = undefined; + } + milliseconds = Math.max(milliseconds, 1); + deferred = new Deferred(); + deferred.timeoutId = Ext.defer(function() { + delete deferred.timeoutId; + deferred.resolve(promiseOrValue); + }, milliseconds); + return deferred.promise; + }, + /** + * Get a shared cached rejected promise. Assumes Promises + * have been required. + * @return {Ext.Promise} + * + * @private + * @since 6.5.0 + */ + getCachedRejected: function() { + if (!rejected) { + // Prevent Cmd from requiring + rejected = Ext.Promise.reject(); + } + return rejected; + }, + /** + * Get a shared cached resolved promise. Assumes Promises + * have been required. + * @return {Ext.Promise} + * + * @private + * @since 6.5.0 + */ + getCachedResolved: function() { + if (!resolved) { + // Prevent Cmd from requiring + resolved = Ext.Promise.resolve(); + } + return resolved; + }, + /** + * Traditional map function, similar to `Array.prototype.map()`, that allows + * input to contain promises and/or values. + * + * The specified map function may return either a value or a promise. + * + * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An + * Array of values or Promises, or a Promise of an Array of values or Promises. + * @param {Function} mapFn A Function to call to transform each resolved value in + * the Array. + * @return {Ext.promise.Promise} A Promise of an Array of the mapped resolved + * values. + * @static + */ + map: function(promisesOrValues, mapFn) { + if (!(Ext.isArray(promisesOrValues) || ExtPromise.is(promisesOrValues))) { + Ext.raise('Invalid parameter: expected an Array or Promise of an Array.'); + } + if (!Ext.isFunction(mapFn)) { + Ext.raise('Invalid parameter: expected a function.'); + } + return Deferred.resolved(promisesOrValues).then(function(promisesOrValues) { + var deferred, index, promiseOrValue, remainingToResolve, resolve, results, i, len; + remainingToResolve = promisesOrValues.length; + results = new Array(promisesOrValues.length); + deferred = new Deferred(); + if (!remainingToResolve) { + deferred.resolve(results); + } else { + resolve = function(item, index) { + return Deferred.resolved(item).then(function(value) { + return mapFn(value, index, results); + }).then(function(value) { + results[index] = value; + if (!--remainingToResolve) { + deferred.resolve(results); + } + return value; + }, function(reason) { + return deferred.reject(reason); + }); + }; + for (index = i = 0 , len = promisesOrValues.length; i < len; index = ++i) { + promiseOrValue = promisesOrValues[index]; + if (index in promisesOrValues) { + resolve(promiseOrValue, index); + } else { + remainingToResolve--; + } + } + } + return deferred.promise; + }); + }, + /** + * Returns a new function that wraps the specified function and caches the + * results for previously processed inputs. + * + * Similar to {@link Ext.Function#memoize Ext.Function.memoize()}, except it + * allows for parameters that are Promises and/or values. + * + * @param {Function} fn A Function to wrap. + * @param {Object} scope An optional scope in which to execute the wrapped function. + * @param {Function} hashFn An optional function used to compute a hash key for + * storing the result, based on the arguments to the original function. + * @return {Function} The new wrapper function. + * @static + */ + memoize: function(fn, scope, hashFn) { + var memoizedFn = Ext.Function.memoize(fn, scope, hashFn); + return function() { + return Deferred.all(Ext.Array.slice(arguments)).then(function(values) { + return memoizedFn.apply(scope, values); + }); + }; + }, + /** + * Execute an Array (or {@link Ext.promise.Promise Promise} of an Array) of + * functions in parallel. + * + * The specified functions may optionally return their results as + * {@link Ext.promise.Promise Promises}. + * + * @param {Function[]/Ext.promise.Promise} fns The Array (or Promise of an Array) + * of functions to execute. + * @param {Object} scope Optional scope in which to execute the specified functions. + * @return {Ext.promise.Promise} Promise of an Array of results for each function + * call (in the same order). + * @static + */ + parallel: function(fns, scope) { + var args; + if (scope == null) { + scope = null; + } + args = Ext.Array.slice(arguments, 2); + return Deferred.map(fns, function(fn) { + if (!Ext.isFunction(fn)) { + throw new Error('Invalid parameter: expected a function.'); + } + return fn.apply(scope, args); + }); + }, + /** + * Execute an Array (or {@link Ext.promise.Promise Promise} of an Array) of + * functions as a pipeline, where each function's result is passed to the + * subsequent function as input. + * + * The specified functions may optionally return their results as + * {@link Ext.promise.Promise Promises}. + * + * @param {Function[]/Ext.promise.Promise} fns The Array (or Promise of an Array) + * of functions to execute. + * @param {Object} initialValue Initial value to be passed to the first function + * in the pipeline. + * @param {Object} scope Optional scope in which to execute the specified functions. + * @return {Ext.promise.Promise} Promise of the result value for the final + * function in the pipeline. + * @static + */ + pipeline: function(fns, initialValue, scope) { + if (scope == null) { + scope = null; + } + return Deferred.reduce(fns, function(value, fn) { + if (!Ext.isFunction(fn)) { + throw new Error('Invalid parameter: expected a function.'); + } + return fn.call(scope, value); + }, initialValue); + }, + /** + * Returns a promise that resolves or rejects as soon as one of the promises + * in the array resolves or rejects, with the value or reason from that promise. + * @param {Ext.promise.Promise[]} promises The promises. + * @return {Ext.promise.Promise} The promise to be resolved when the race completes. + * + * @static + * @since 6.5.0 + */ + race: function() { + return ExtPromise.race.apply(ExtPromise, arguments); + }, + /** + * Traditional reduce function, similar to `Array.reduce()`, that allows input to + * contain promises and/or values. + * + * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} values An + * Array of values or Promises, or a Promise of an Array of values or Promises. + * @param {Function} reduceFn A Function to call to transform each successive + * item in the Array into the final reduced value. + * @param {Mixed} initialValue An initial Promise or value. + * @return {Ext.promise.Promise} A Promise of the reduced value. + * @static + */ + reduce: function(values, reduceFn, initialValue) { + var initialValueSpecified; + if (!(Ext.isArray(values) || ExtPromise.is(values))) { + Ext.raise('Invalid parameter: expected an Array or Promise of an Array.'); + } + if (!Ext.isFunction(reduceFn)) { + Ext.raise('Invalid parameter: expected a function.'); + } + initialValueSpecified = arguments.length === 3; + return Deferred.resolved(values).then(function(promisesOrValues) { + var reduceArguments = [ + promisesOrValues, + function(previousValueOrPromise, currentValueOrPromise, currentIndex) { + return Deferred.resolved(previousValueOrPromise).then(function(previousValue) { + return Deferred.resolved(currentValueOrPromise).then(function(currentValue) { + return reduceFn(previousValue, currentValue, currentIndex, promisesOrValues); + }); + }); + } + ]; + if (initialValueSpecified) { + reduceArguments.push(initialValue); + } + return Ext.Array.reduce.apply(Ext.Array, reduceArguments); + }); + }, + /** + * Convenience method that returns a new Promise rejected with the specified + * reason. + * + * @param {Error} reason Rejection reason. + * @return {Ext.promise.Promise} The rejected Promise. + * @static + */ + rejected: function(reason) { + var deferred = new Ext.Deferred(); + deferred.reject(reason); + return deferred.promise; + }, + /** + * Returns a new Promise that either + * + * * Resolves immediately for the specified value, or + * * Resolves or rejects when the specified promise (or third-party Promise or + * then()-able) is resolved or rejected. + * + * @param {Mixed} promiseOrValue A Promise (or third-party Promise or then()-able) + * or value. + * @return {Ext.promise.Promise} A Promise of the specified Promise or value. + * @static + */ + resolved: function(promiseOrValue) { + var deferred = new Ext.Deferred(); + deferred.resolve(promiseOrValue); + return deferred.promise; + }, + /** + * Execute an Array (or {@link Ext.promise.Promise Promise} of an Array) of + * functions sequentially. + * + * The specified functions may optionally return their results as {@link + * Ext.promise.Promise Promises}. + * + * @param {Function[]/Ext.promise.Promise} fns The Array (or Promise of an Array) + * of functions to execute. + * @param {Object} scope Optional scope in which to execute the specified functions. + * @return {Ext.promise.Promise} Promise of an Array of results for each function + * call (in the same order). + * @static + */ + sequence: function(fns, scope) { + var args; + if (scope == null) { + scope = null; + } + args = Ext.Array.slice(arguments, 2); + return Deferred.reduce(fns, function(results, fn) { + if (!Ext.isFunction(fn)) { + throw new Error('Invalid parameter: expected a function.'); + } + return Deferred.resolved(fn.apply(scope, args)).then(function(result) { + results.push(result); + return results; + }); + }, []); + }, + /** + * Initiates a competitive race, returning a new Promise that will resolve when + * `howMany` of the specified `promisesOrValues` have resolved, or will reject + * when it becomes impossible for `howMany` to resolve. + * + * The resolution value will be an Array of the first `howMany` values of + * `promisesOrValues` to resolve. + * + * @param {Mixed[]/Ext.promise.Promise[]/Ext.promise.Promise} promisesOrValues An + * Array of values or Promises, or a Promise of an Array of values or Promises. + * @param {Number} howMany The expected number of resolved values. + * @return {Ext.promise.Promise} A Promise of the expected number of resolved + * values. + * @static + */ + some: function(promisesOrValues, howMany) { + if (!(Ext.isArray(promisesOrValues) || ExtPromise.is(promisesOrValues))) { + Ext.raise('Invalid parameter: expected an Array or Promise of an Array.'); + } + if (!Ext.isNumeric(howMany) || howMany <= 0) { + Ext.raise('Invalid parameter: expected a positive integer.'); + } + return Deferred.resolved(promisesOrValues).then(function(promisesOrValues) { + var deferred, index, onReject, onResolve, promiseOrValue, remainingToReject, remainingToResolve, values, i, len; + values = []; + remainingToResolve = howMany; + remainingToReject = (promisesOrValues.length - remainingToResolve) + 1; + deferred = new Deferred(); + if (promisesOrValues.length < howMany) { + deferred.reject(new Error('Too few Promises were resolved.')); + } else { + onResolve = function(value) { + if (remainingToResolve > 0) { + values.push(value); + } + remainingToResolve--; + if (remainingToResolve === 0) { + deferred.resolve(values); + } + return value; + }; + onReject = function(reason) { + remainingToReject--; + if (remainingToReject === 0) { + deferred.reject(new Error('Too few Promises were resolved.')); + } + return reason; + }; + for (index = i = 0 , len = promisesOrValues.length; i < len; index = ++i) { + promiseOrValue = promisesOrValues[index]; + if (index in promisesOrValues) { + Deferred.resolved(promiseOrValue).then(onResolve, onReject); + } + } + } + return deferred.promise; + }); + }, + /** + * Returns a new Promise that will automatically reject after the specified + * timeout (in milliseconds) if the specified promise has not resolved or + * rejected. + * + * @param {Mixed} promiseOrValue A Promise or value. + * @param {Number} milliseconds A timeout duration (in milliseconds). + * @return {Ext.promise.Promise} A Promise of the specified Promise or value that + * enforces the specified timeout. + * @static + */ + timeout: function(promiseOrValue, milliseconds) { + var deferred = new Deferred(), + timeoutId; + timeoutId = Ext.defer(function() { + if (timeoutId) { + deferred.reject(new Error('Promise timed out.')); + } + }, milliseconds); + Deferred.resolved(promiseOrValue).then(function(value) { + Ext.undefer(timeoutId); + timeoutId = null; + deferred.resolve(value); + }, function(reason) { + Ext.undefer(timeoutId); + timeoutId = null; + deferred.reject(reason); + }); + return deferred.promise; + } + } + }; +}, function(Deferred) { + Deferred._ready(); +}); + +// @define Ext.Factory +/** + * @class Ext.Factory + * Manages factories for families of classes (classes with a common `alias` prefix). The + * factory for a class family is a function stored as a `static` on `Ext.Factory`. These + * are created either by directly calling `Ext.Factory.define` or by using the + * `Ext.mixin.Factoryable` interface. + * + * To illustrate, consider the layout system's use of aliases. The `hbox` layout maps to + * the `"layout.hbox"` alias that one typically provides via the `layout` config on a + * Container. + * + * Under the covers this maps to a call like this: + * + * Ext.Factory.layout('hbox'); + * + * Or possibly: + * + * Ext.Factory.layout({ + * type: 'hbox' + * }); + * + * The value of the `layout` config is passed to the `Ext.Factory.layout` function. The + * exact signature of a factory method matches `{@link Ext.Factory#method!create}`. + * + * To define this factory directly, one could call `Ext.Factory.define` like so: + * + * Ext.Factory.define('layout', 'auto'); // "layout.auto" is the default type + * + * @since 5.0.0 + */ +Ext.Factory = function(type) { + var me = this; + me.aliasPrefix = type + '.'; + me.cache = {}; + me.name = type.replace(me.fixNameRe, me.fixNameFn); + me.type = type; + /** + * @cfg {String} [creator] + * The name of the method used to prepare config objects for creation. This defaults + * to `'create'` plus the capitalized name (e.g., `'createLayout'` for the 'laoyut' + * alias family). + */ + me.creator = 'create' + Ext.String.capitalize(me.name); +}; +Ext.Factory.prototype = { + /** + * @cfg {String} [aliasPrefix] + * The prefix to apply to `type` values to form a complete alias. This defaults to the + * proper value in most all cases and should not need to be specified. + * + * @since 5.0.0 + */ + /** + * @cfg {String} [defaultProperty="type"] + * The config property to set when the factory is given a config that is a string. + * + * @since 5.0.0 + */ + defaultProperty: 'type', + /** + * @cfg {String} [defaultType=null] + * An optional type to use if none is given to the factory at invocation. This is a + * suffix added to the `aliasPrefix`. For example, if `aliasPrefix="layout."` and + * `defaultType="hbox"` the default alias is `"layout.hbox"`. This is an alternative + * to `xclass` so only one should be provided. + * + * @since 5.0.0 + */ + /** + * @cfg {String} [instanceProp="isInstance"] + * The property that identifies an object as instance vs a config. + * + * @since 5.0.0 + */ + instanceProp: 'isInstance', + /** + * @cfg {String} [xclass=null] + * The full classname of the type of instance to create when none is provided to the + * factory. This is an alternative to `defaultType` so only one should be specified. + * + * @since 5.0.0 + */ + /** + * @property {Ext.Class} [defaultClass=null] + * The Class reference of the type of instance to create when none is provided to the + * factory. This property is set from `xclass` when the factory instance is created. + * @private + * @readonly + * + * @since 5.0.0 + */ + /** + * @cfg {String} [typeProperty="type"] + * The property from which to read the type alias suffix. + * @since 6.5.0 + */ + typeProperty: 'type', + /** + * Creates an instance of this class family given configuration options. + * + * @param {Object/String} [config] The configuration or instance (if an Object) or + * just the type (if a String) describing the instance to create. + * @param {String} [config.xclass] The full class name of the class to create. + * @param {String} [config.type] The type string to add to the alias prefix for this + * factory. + * @param {String/Object} [defaultType] The type to create if no type is contained in the + * `config`, or an object containing a default set of configs. + * @return {Object} The newly created instance. + * + * @since 5.0.0 + */ + create: function(config, defaultType) { + var me = this, + Manager = Ext.ClassManager, + cache = me.cache, + typeProperty = me.typeProperty, + alias, className, klass, suffix; + if (config) { + if (config[me.instanceProp]) { + return config; + } + if (typeof config === 'string') { + suffix = config; + config = {}; + config[me.defaultProperty] = suffix; + } + className = config.xclass; + suffix = config[typeProperty]; + } + if (defaultType && defaultType.constructor === Object) { + config = Ext.apply({}, config, defaultType); + defaultType = defaultType[typeProperty]; + } + if (className) { + if (!(klass = Manager.get(className))) { + return Manager.instantiate(className, config); + } + } else { + if (!(suffix = suffix || defaultType || me.defaultType)) { + klass = me.defaultClass; + } + if (!suffix && !klass) { + Ext.raise('No type specified for ' + me.type + '.create'); + } + if (!klass && !(klass = cache[suffix])) { + alias = me.aliasPrefix + suffix; + className = Manager.getNameByAlias(alias); + // this is needed to support demand loading of the class + if (!(klass = className && Manager.get(className))) { + return Manager.instantiateByAlias(alias, config); + } + cache[suffix] = klass; + } + } + return klass.isInstance ? klass : new klass(config); + }, + fixNameRe: /\.[a-z]/ig, + fixNameFn: function(match) { + return match.substring(1).toUpperCase(); + }, + clearCache: function() { + this.cache = {}; + this.instanceCache = {}; + }, + /** + * Sets a hook on the creation process. If the hook `fn` returns `undefined` then + * the original `create` method is called. + * + * @param {Function} fn The hook function to call when `create` is invoked. + * @param {Function} fn.original The original `create` method. + * @param {String/Object} fn.config See {@link #method!create create}. + * @param {String/Object} fn.defaultType See {@link #method!create create}. + * @private + * @since 6.5.0 + */ + hook: function(fn) { + var me = this, + original = me.create; + me.create = function(config, defaultType) { + var ret = fn.call(me, original, config, defaultType); + if (ret === undefined) { + ret = original.call(me, config, defaultType); + } + return ret; + }; + }, + /** + * This method accepts a `config` object and an existing `instance` if one exists + * (can be `null`). + * + * The details are best explained by example: + * + * config: { + * header: { + * xtype: 'itemheader' + * } + * }, + * + * applyHeader: function (header, oldHeader) { + * return Ext.Factory.widget.update(oldHeader, header, + * this, 'createHeader'); + * }, + * + * createHeader: function (header) { + * return Ext.apply({ + * xtype: 'itemheader', + * ownerCmp: this + * }, header); + * } + * + * Normally the `applyHeader` method would have to coordinate potential reuse of + * the `oldHeader` and perhaps call `setConfig` on it with the new `header` config + * options. If there was no `oldHeader`, of course, a new instance must be created + * instead. These details are handled by this method. If the `oldHeader` is not + * reused, it will be {@link Ext.Base#method!destroy destroyed}. + * + * For derived class flexibility, the pattern of calling out to a "creator" method + * that only returns the config object has become widely used in many components. + * This pattern is also covered in this method. The goal is to allow the derived + * class to `callParent` and yet not end up with an instantiated component (since + * the type may not yet be known). + * + * This mechanism should be used in favor of `Ext.factory()`. + * + * @param {Ext.Base} instance + * @param {Object/String} config The configuration (see {@link #method!create}). + * @param {Object} [creator] If passed, this object must provide the `creator` + * method or the `creatorMethod` parameter. + * @param {String} [creatorMethod] The name of a creation wrapper method on the + * given `creator` instance that "upgrades" the raw `config` object into a final + * form for creation. + * @param {String} [defaultsConfig] The name of a config property (on the provided + * `creator` instance) that contains defaults to be used to create instances. These + * defaults are present in the config object passed to the `creatorMethod`. + * @return {Object} The reconfigured `instance` or a newly created one. + * @since 6.5.0 + */ + update: function(instance, config, creator, creatorMethod, defaultsConfig) { + var me = this, + aliases, defaults, reuse, type; + // If config is falsy or a valid instance, destroy the current instance + // (if it exists) and replace with the new one + if (!config || config.isInstance) { + if (config && !config[me.instanceProp]) { + Ext.raise('Config instance failed ' + me.instanceProp + ' requirement'); + } + if (instance && instance !== config) { + instance.destroy(); + } + return config; + } + if (typeof config === 'string') { + type = config; + config = {}; + config[me.defaultProperty] = type; + } + // See if the existing instance can just be reconfigured: + if (instance) { + if (config === true) { + return instance; + } + if (!(type = config.xclass)) { + if (!(type = config.xtype)) { + type = config[me.typeProperty]; + if (type) { + // instance must have the right alias... + type = me.aliasPrefix + type; + aliases = instance.self.prototype; + // The alias for the class is on the prototype (derived + // classes do not really own their inherited aliases since + // they won't be created when using them): + if (aliases.hasOwnProperty('alias')) { + aliases = aliases.alias; + if (aliases) { + reuse = aliases === type || aliases.indexOf(type) > -1; + } + } + } + } else { + // config = { xtype: ... } + reuse = instance.isXType(type, /* shallow= */ + true); + } + } else { + // config = { xclass: ... } so we're good if they match + reuse = instance.$className === type; + } + if (reuse) { + instance.setConfig(config); + return instance; + } + instance.destroy(); + } + if (config === true) { + config = {}; + } + if (creator) { + if (defaultsConfig) { + defaults = Ext.Config.map[defaultsConfig]; + defaults = creator[defaults.names.get](); + if (defaults) { + config = Ext.merge(Ext.clone(defaults), config); + } + } + creatorMethod = creatorMethod || me.creator; + if (creator[creatorMethod]) { + config = creator[creatorMethod](config); + if (!config) { + Ext.raise('Missing return value from ' + creatorMethod + ' on class ' + creator.$className); + } + } + } + return me.create(config); + } +}; +/** + * For example, the layout alias family could be defined like this: + * + * Ext.Factory.define('layout', { + * defaultType: 'auto' + * }); + * + * To define multiple families at once: + * + * Ext.Factory.define({ + * layout: { + * defaultType: 'auto' + * } + * }); + * + * @param {String} type The alias family (e.g., "layout"). + * @param {Object/String} [config] An object specifying the config for the `Ext.Factory` + * to be created. If a string is passed it is treated as the `defaultType`. + * @return {Function} + * @static + * @since 5.0.0 + */ +Ext.Factory.define = function(type, config) { + var Factory = Ext.Factory, + cacheable = config && config.cacheable, + defaultClass, factory, fn; + if (type.constructor === Object) { + Ext.Object.each(type, Factory.define, Factory); + } else { + factory = new Ext.Factory(type); + if (config) { + if (config.constructor === Object) { + Ext.apply(factory, config); + if (typeof (defaultClass = factory.xclass) === 'string') { + factory.defaultClass = Ext.ClassManager.get(defaultClass); + } + } else { + factory.defaultType = config; + } + } + /* + * layout = Ext.Factory.layout('hbox'); + */ + Factory[factory.name] = fn = function(config, defaultType) { + // maintain indirection through "create" name on instance to allow + // the hook() mechanism to replace it. + return factory.create(config, defaultType); + }; + if (cacheable) { + factory.instanceCache = {}; + factory.hook(function(original, config, defaultType) { + var cache = this.instanceCache, + v; + if (typeof config === 'string' && !(v = cache[config])) { + v = original.call(this, config, defaultType); + // Validator may have cacheable:false to force new instances each time, + // avoiding the cache + if (v.cacheable !== false) { + cache[config] = v; + // this should catch some improper modifications to the shared + // cached instance, during development but not in production. + Ext.Object.freeze(v); + } + } + return v; + }); + } + fn.instance = factory; + /* + * Typically called by an applier: + * + * applyLayout: function (layout, oldLayout) { + * return Ext.Factory.layout.update(oldLayout, layout, this); + * }, + * + * createLayout: function (config) { + * return Ext.apply({ + * //.. stuff + * }, config); + * } + */ + fn.update = function(instance, config, creator, creatorMethod, defaultsConfig) { + return factory.update(instance, config, creator, creatorMethod, defaultsConfig); + }; + } + return fn; +}; +Ext.Factory.clearCaches = function() { + var Factory = Ext.Factory, + key, item; + for (key in Factory) { + item = Factory[key]; + item = item.instance; + if (item) { + item.clearCache(); + } + } +}; +Ext.Factory.on = function(name, fn) { + Ext.Factory[name].instance.hook(fn); +}; +/** + * This mixin automates use of `Ext.Factory`. When mixed in to a class, the `alias` of the + * class is retrieved and combined with an optional `factoryConfig` property on that class + * to produce the configuration to pass to `Ext.Factory`. + * + * The factory method created by `Ext.Factory` is also added as a static method to the + * target class. + * + * Given a class declared like so: + * + * Ext.define('App.bar.Thing', { + * mixins: [ + * 'Ext.mixin.Factoryable' + * ], + * + * alias: 'bar.thing', // this is detected by Factoryable + * + * factoryConfig: { + * defaultType: 'thing', // this is the default deduced from the alias + * // other configs + * }, + * + * ... + * }); + * + * The produced factory function can be used to create instances using the following + * forms: + * + * var obj; + * + * obj = App.bar.Thing.create('thing'); // same as "new App.bar.Thing()" + * + * obj = App.bar.Thing.create({ + * type: 'thing' // same as above + * }); + * + * obj = App.bar.Thing.create({ + * xclass: 'App.bar.Thing' // same as above + * }); + * + * var obj2 = App.bar.Thing.create(obj); + * // obj === obj2 (passing an instance returns the instance) + * + * Alternatively the produced factory is available as a static method of `Ext.Factory`. + * + * @since 5.0.0 + */ +Ext.define('Ext.mixin.Factoryable', { + mixinId: 'factoryable', + onClassMixedIn: function(targetClass) { + var proto = targetClass.prototype, + factoryConfig = proto.factoryConfig, + alias = proto.alias, + config = {}, + dot, createFn; + alias = alias && alias.length && alias[0]; + if (alias && (dot = alias.lastIndexOf('.')) > 0) { + config.type = alias.substring(0, dot); + config.defaultType = alias.substring(dot + 1); + } + if (factoryConfig) { + delete proto.factoryConfig; + Ext.apply(config, factoryConfig); + } + createFn = Ext.Factory.define(config.type, config); + if (targetClass.create === Ext.Base.create) { + // allow targetClass to override the create method + targetClass.create = createFn; + } + } +}); +/** + * @property {Object} [factoryConfig] + * If this property is specified by the target class of this mixin its properties are + * used to configure the created `Ext.Factory`. + */ + +/** + * This class manages a pending Ajax request. Instances of this type are created by the + * `{@link Ext.data.Connection#request}` method. + * @since 6.0.0 + */ +Ext.define('Ext.data.request.Base', { + mixins: [ + Ext.mixin.Factoryable + ], + // Since this class is abstract, we don't have an alias of our own for Factoryable + // to use. + factoryConfig: { + type: 'request', + defaultType: 'ajax' + }, + // this is the default deduced from the alias + result: null, + success: null, + timer: null, + constructor: function(config) { + var me = this; + // ownerConfig contains default values for config options + // applicable to every Request spawned by that owner; + // however the values can be overridden in the options + // object passed to owner's request() method. + Ext.apply(me, config.options || {}, config.ownerConfig); + me.id = ++Ext.data.Connection.requestId; + me.owner = config.owner; + me.options = config.options; + me.requestOptions = config.requestOptions; + }, + /** + * Start the request. + */ + start: function() { + var me = this, + timeout = me.getTimeout(); + if (timeout && me.async) { + me.timer = Ext.defer(me.onTimeout, timeout, me); + } + }, + abort: function() { + var me = this; + me.clearTimer(); + if (!me.timedout) { + me.aborted = true; + } + me.abort = Ext.emptyFn; + }, + createDeferred: function() { + var me = this, + result = me.result, + d = new Ext.Deferred(); + if (me.completed) { + if (me.success) { + d.resolve(result); + } else { + d.reject(result); + } + } + me.deferred = d; + return d; + }, + getDeferred: function() { + return this.deferred || this.createDeferred(); + }, + getPromise: function() { + return this.getDeferred().promise; + }, + /** + * @method then + * Returns a new promise resolving to the value of the called method. + * @param {Function} success Called when the Promise is fulfilled. + * @param {Function} failure Called when the Promise is rejected. + * @returns {Ext.promise.Promise} + */ + then: function() { + var promise = this.getPromise(); + return promise.then.apply(promise, arguments); + }, + /** + * @method isLoading + * Determines whether this request is in progress. + * + * @return {Boolean} `true` if this request is in progress, `false` if complete. + */ + onComplete: function() { + var me = this, + deferred = me.deferred, + result = me.result; + me.clearTimer(); + if (deferred) { + if (me.success) { + deferred.resolve(result); + } else { + deferred.reject(result); + } + } + me.completed = true; + }, + onTimeout: function() { + var me = this; + me.timedout = true; + me.timer = null; + me.abort(true); + }, + getTimeout: function() { + return this.timeout; + }, + clearTimer: function() { + this.timer = Ext.undefer(this.timer); + }, + destroy: function() { + var me = this; + me.abort(); + me.owner = me.options = me.requestOptions = me.result = null; + me.callParent(); + }, + privates: { + /** + * Creates the exception object + * @param {Object} request + * @private + */ + createException: function() { + var me = this, + result; + result = { + request: me, + requestId: me.id, + status: me.aborted ? -1 : 0, + statusText: me.aborted ? 'transaction aborted' : 'communication failure', + getResponseHeader: me._getHeader, + getAllResponseHeaders: me._getHeaders + }; + if (me.aborted) { + result.aborted = true; + } + if (me.timedout) { + result.timedout = true; + } + return result; + }, + _getHeader: function(name) { + var headers = this.headers; + return headers && headers[name.toLowerCase()]; + }, + _getHeaders: function() { + return this.headers; + } + } +}); + +/** + * + * Simulates an XMLHttpRequest object's methods and properties as returned + * form the flash polyfill plugin. Used in submitting binary data in browsers that do + * not support doing so from JavaScript. + * NOTE: By default this will look for the flash object in the ext directory. When packaging and + * deploying the app, copy the `ext/plugins` directory and its contents to your root + * directory. For custom deployments where just the `FlashPlugin.swf` file gets copied + * (e.g. to `/resources/FlashPlugin.swf`), make sure to notify the framework of the location + * of the plugin before making the first attempt to post binary data, e.g. in the `launch` + * method of your app do: + * + * Ext.flashPluginPath="/resources/FlashPlugin.swf"; + * + * @private + */ +Ext.define('Ext.data.flash.BinaryXhr', { + statics: { + /** + * Called by the flash plugin once it's installed and open for business. + * @private + */ + flashPluginActivated: function() { + Ext.data.flash.BinaryXhr.flashPluginActive = true; + Ext.data.flash.BinaryXhr.flashPlugin = document.getElementById("ext-flash-polyfill"); + Ext.GlobalEvents.fireEvent("flashready"); + }, + // let all pending connections know + /** + * Set to `trut` once the plugin registers and is active. + * @private + */ + flashPluginActive: false, + /** + * Flag to avoid installing the plugin twice. + * @private + */ + flashPluginInjected: false, + /** + * Counts IDs for new connections. + * @private + */ + connectionIndex: 1, + /** + * Placeholder for active connections. + * @private + */ + liveConnections: {}, + /** + * Reference to the actual plugin, once activated. + * @private + */ + flashPlugin: null, + /** + * Called by the flash plugin once the state of one of the active connections changes. + * @param {Number/number} javascriptId the ID of the connection. + * @param {number} state the state of the connection. Equivalent to readyState numbers in + * XHR. + * @param {Object} data optional object containing the returned data, error and status + * codes. + * @private + */ + onFlashStateChange: function(javascriptId, state, data) { + var connection; + // Identify the request this is for. Make sure its a native number + connection = this.liveConnections[Number(javascriptId)]; + if (connection) { + connection.onFlashStateChange(state, data); + } else { + Ext.warn.log("onFlashStateChange for unknown connection ID: " + javascriptId); + } + }, + /** + * Adds the BinaryXhr object to the tracked connection list and assigns it an ID + * @param {Ext.data.flash.BinaryXhr} conn the connection to register + * @return {Number} id + * @private + */ + registerConnection: function(conn) { + var i = this.connectionIndex; + this.conectionIndex = this.connectionIndex + 1; + this.liveConnections[i] = conn; + return i; + }, + /** + * Injects the flash polyfill plugin to allow posting binary data. + * This is done in two steps: First we load the javascript loader for flash objects, + * then we call it to inject the flash object. + * @private + */ + injectFlashPlugin: function() { + var me = this, + flashLoaderPath, flashObjectPath; + /* eslint-disable max-len */ + // Generate the following HTML set of tags: + // + '
' + // + '

To view this page ensure that Adobe Flash Player version 11.1.0 or greater is installed, and that the FlashPlugin.swf file was correctly placed in the /resources directory.

' + // + 'Get Adobe Flash player' + // + '
' + me.flashPolyfillEl = Ext.getBody().appendChild({ + id: 'ext-flash-polyfill', + cn: [ + { + tag: 'p', + html: 'To view this page ensure that Adobe Flash Player version 11.1.0 or greater is installed.' + }, + { + tag: 'a', + href: 'http://www.adobe.com/go/getflashplayer', + cn: [ + { + tag: 'img', + src: window.location.protocol + '//www.adobe.com/images/shared/download_buttons/get_flash_player.gif', + alt: 'Get Adobe Flash player' + } + ] + } + ] + }); + // Now load the flash-loading script + flashLoaderPath = [ + Ext.Loader.getPath('Ext.data.Connection'), + '../../../plugins/flash/swfobject.js' + ].join('/'); + flashObjectPath = "/plugins/flash/FlashPlugin.swf"; + flashObjectPath = [ + Ext.Loader.getPath('Ext.data.Connection'), + '../../plugins/flash/FlashPlugin.swf' + ].join('/'); + /* eslint-enable max-len */ + if (Ext.flashPluginPath) { + flashObjectPath = Ext.flashPluginPath; + } + Ext.Loader.loadScript({ + url: flashLoaderPath, + onLoad: function() { + // For version detection, set to min. required Flash Player version, + // or 0 (or 0.0.0), for no version detection. + // To use express install, set to playerProductInstall.swf, otherwise + // the empty string. + var swfVersionStr = "11.4.0", + xiSwfUrlStr = "playerProductInstall.swf", + flashvars = {}, + params = {}, + attributes = {}; + params.quality = "high"; + params.bgcolor = "#ffffff"; + params.allowscriptaccess = "sameDomain"; + params.allowfullscreen = "true"; + attributes.id = "ext-flash-polyfill"; + attributes.name = "polyfill"; + attributes.align = "middle"; + /* eslint-disable-next-line no-undef */ + swfobject.embedSWF(flashObjectPath, "ext-flash-polyfill", "0", "0", // no size so it's not visible. + swfVersionStr, xiSwfUrlStr, flashvars, params, attributes); + }, + onError: function() { + /* eslint-disable-next-line no-undef */ + Ext.raise("Could not load flash-loader file swfobject.js from " + flashLoader); + }, + scope: me + }); + Ext.data.flash.BinaryXhr.flashPluginInjected = true; + } + }, + /** + * @property {number} readyState The connection's simulated readyState. Note that the only + * supported values are 0, 1 and 4. States 2 and 3 will never be reported. + */ + readyState: 0, + /** + * @property {number} status Connection status code returned by flash or the server. + */ + status: 0, + /** + * Status text (if any) returned by flash or the server. + */ + statusText: "", + /** + * @property {Array} responseBytes The binary bytes returned. + */ + responseBytes: null, + /** + * An ID representing this connection with flash. + * @private + */ + javascriptId: null, + /** + * Creates a new instance of BinaryXhr. + */ + constructor: function(config) { + var me = this; + // first, make sure flash is loading if needed + if (!Ext.data.flash.BinaryXhr.flashPluginInjected) { + Ext.data.flash.BinaryXhr.injectFlashPlugin(); + } + Ext.apply(me, config); + me.requestHeaders = {}; + }, + /** + * Abort this connection. Sets its readyState to 4. + */ + abort: function() { + var me = this; + // if complete, nothing to abort + if (me.readyState === 4) { + Ext.warn.log("Aborting a connection that's completed its transfer: " + this.url); + return; + } + // Mark as aborted + me.aborted = true; + // Remove ourselves from the listeners if flash isn't active yet + if (!Ext.data.flash.BinaryXhr.flashPluginActive) { + Ext.GlobalEvents.removeListener("flashready", me.onFlashReady, me); + return; + } + // Flash is already live, so we should have a javascriptID and should have called flash + // to get the request going. Cancel: + Ext.data.flash.BinaryXhr.flashPlugin.abortRequest(me.javascriptId); + // remove from list + delete Ext.data.flash.BinaryXhr.liveConnections[me.javascriptId]; + }, + /** + * As in XMLHttpRequest. + */ + getAllResponseHeaders: function() { + var headers = []; + Ext.Object.each(this.responseHeaders, function(name, value) { + headers.push(name + ': ' + value); + }); + return headers.join('\r\n'); + }, + /** + * As in XMLHttpRequest. + */ + getResponseHeader: function(header) { + var headers = this.responseHeaders; + return (headers && headers[header]) || null; + }, + /** + * As in XMLHttpRequest. + */ + open: function(method, url, isAsync, user, password) { + var me = this; + me.method = method; + me.url = url; + me.async = isAsync !== false; + // eslint-disable-line id-blacklist + me.user = user; + me.password = password; + if (!me.async) { + Ext.raise("Binary posts are only supported in async mode: " + url); + } + if (me.method !== "POST") { + Ext.log.warn("Binary data can only be sent as a POST request: " + url); + } + }, + /** + * As in XMLHttpRequest. + */ + overrideMimeType: function(mimeType) { + this.mimeType = mimeType; + }, + /** + * Initiate the request. + * @param {Array} body an array of byte values to send. + */ + send: function(body) { + var me = this; + me.body = body; + if (!Ext.data.flash.BinaryXhr.flashPluginActive) { + Ext.GlobalEvents.addListener("flashready", me.onFlashReady, me); + } else { + this.onFlashReady(); + } + }, + /** + * Called by send, or once flash is loaded, to actually send the bytes. + * @private + */ + onFlashReady: function() { + var me = this, + req; + me.javascriptId = Ext.data.flash.BinaryXhr.registerConnection(me); + // Create the request object we're sending to flash + req = { + method: me.method, + // ignored since we always POST binary data + url: me.url, + user: me.user, + password: me.password, + mimeType: me.mimeType, + requestHeaders: me.requestHeaders, + body: me.body, + javascriptId: me.javascriptId + }; + Ext.data.flash.BinaryXhr.flashPlugin.postBinary(req); + }, + /** + * Updates readyState and notifies listeners. + * @private + */ + setReadyState: function(state) { + var me = this; + if (me.readyState !== state) { + me.readyState = state; + me.onreadystatechange(); + } + }, + /** + * As in XMLHttpRequest. + */ + setRequestHeader: function(header, value) { + this.requestHeaders[header] = value; + }, + /** + * @method + * As in XMLHttpRequest. + */ + onreadystatechange: Ext.emptyFn, + /** + * Parses data returned from flash once a connection is done. + * @param {Object} data the data object send from Flash. + * @private + */ + parseData: function(data) { + var me = this; + // parse data and set up variables so that listeners can use this XHR + this.status = data.status || 0; + // we get back no response headers, so fake what we know: + me.responseHeaders = {}; + if (me.mimeType) { + me.responseHeaders["content-type"] = me.mimeType; + } + if (data.reason === "complete") { + // Transfer complete and data received + this.responseBytes = data.data; + me.responseHeaders["content-length"] = data.data.length; + } else if (data.reason === "error" || data.reason === "securityError") { + this.statusText = data.text; + me.responseHeaders["content-length"] = 0; + } else // we don't get the error response data + { + Ext.raise("Unkown reason code in data: " + data.reason); + } + }, + /** + * Called once flash calls back with updates about the connection + * @param {Number} state the readyState of the connection. + * @param {Object} data optional data object. + * @private + */ + onFlashStateChange: function(state, data) { + var me = this; + if (state === 4) { + // parse data and prepare for handing back to initiator + me.parseData(data); + // remove from list + delete Ext.data.flash.BinaryXhr.liveConnections[me.javascriptId]; + } + me.setReadyState(state); + } +}); +// notify all listeners + +/** + * This class manages a pending Ajax request. Instances of this type are created by the + * `{@link Ext.data.Connection#request}` method. + * @since 6.0.0 + */ +Ext.define('Ext.data.request.Ajax', { + extend: Ext.data.request.Base, + alias: 'request.ajax', + statics: { + /** + * Checks if the response status was successful + * @param {Number} status The status code + * @param {Object} response The Response object + * @return {Object} An object containing success/status state + * @private + */ + parseStatus: function(status, response) { + var type, len, success, isException; + if (response) { + // We have to account for binary and other response types + type = response.responseType; + if (type === 'arraybuffer') { + len = response.byteLength; + } else if (type === 'blob') { + len = response.response.size; + } else if ((type === 'json' || type === 'document') && response.response) { + len = 0; + } else if ((type === 'text' || type === '' || !type) && response.responseText) { + len = response.responseText.length; + } + } + // see: https://prototype.lighthouseapp.com/projects/8886/tickets/129-ie-mangles-http-response-status-code-204-to-1223 + status = status === 1223 ? 204 : status; + isException = false; + // Status can be 0 for file:/// requests + success = (status >= 200 && status < 300) || status === 304 || (status === 0 && Ext.isNumber(len)); + if (!success) { + switch (status) { + case 12002: + case 12029: + case 12030: + case 12031: + case 12152: + case 13030: + isException = true; + break; + } + } + return { + success: success, + isException: isException + }; + } + }, + start: function(data) { + var me = this, + options = me.options, + requestOptions = me.requestOptions, + isXdr = me.isXdr, + xhr; + xhr = me.xhr = me.openRequest(options, requestOptions, me.async, me.username, me.password); + // XDR doesn't support setting any headers + if (!isXdr) { + me.setupHeaders(xhr, options, requestOptions.data, requestOptions.params); + } + if (me.async) { + if (!isXdr) { + xhr.onreadystatechange = me.bindStateChange(); + } + } + if (isXdr) { + me.processXdrRequest(me, xhr); + } + // Parent will set the timeout if needed + me.callParent([ + data + ]); + // start the request! + xhr.send(data); + if (!me.async) { + return me.onComplete(); + } + return me; + }, + /** + * Aborts an active request. + */ + abort: function(force) { + var me = this, + xhr = me.xhr; + if (force || me.isLoading()) { + /* + * Clear out the onreadystatechange here, this allows us + * greater control, the browser may/may not fire the function + * depending on a series of conditions. + */ + try { + xhr.onreadystatechange = null; + } catch (e) { + // Setting onreadystatechange to null can cause problems in IE, see + // http://www.quirksmode.org/blog/archives/2005/09/xmlhttp_notes_a_1.html + xhr.onreadystatechange = Ext.emptyFn; + } + xhr.abort(); + me.callParent([ + force + ]); + me.onComplete(); + me.cleanup(); + } + }, + /** + * Cleans up any left over information from the request + */ + cleanup: function() { + this.xhr = null; + delete this.xhr; + }, + isLoading: function() { + var me = this, + xhr = me.xhr, + state = xhr && xhr.readyState, + C = Ext.data.flash && Ext.data.flash.BinaryXhr; + if (!xhr || me.aborted || me.timedout) { + return false; + } + // if there is a connection and readyState is not 0 or 4, or in case of + // BinaryXHR, not 4 + if (C && xhr instanceof C) { + return state !== 4; + } + return state !== 0 && state !== 4; + }, + /** + * Creates and opens an appropriate XHR transport for a given request on this browser. + * This logic is contained in an individual method to allow for overrides to process all + * of the parameters and options and return a suitable, open connection. + * @private + */ + openRequest: function(options, requestOptions, isAsync, username, password) { + var me = this, + xhr = me.newRequest(options); + if (username) { + xhr.open(requestOptions.method, requestOptions.url, isAsync, username, password); + } else { + if (me.isXdr) { + xhr.open(requestOptions.method, requestOptions.url); + } else { + xhr.open(requestOptions.method, requestOptions.url, isAsync); + } + } + if (options.binary || me.binary) { + if (window.Uint8Array) { + xhr.responseType = 'arraybuffer'; + } else if (xhr.overrideMimeType) { + // In some older non-IE browsers, e.g. ff 3.6, that do not + // support Uint8Array, a mime type override is required so that + // the unprocessed binary data can be read from the responseText + // (see createResponse()) + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + } else if (!Ext.isIE) { + Ext.log.warn("Your browser does not support loading binary data using Ajax."); + } + } + if (options.responseType) { + xhr.responseType = options.responseType; + } + if (options.withCredentials || me.withCredentials) { + xhr.withCredentials = true; + } + return xhr; + }, + /** + * Creates the appropriate XHR transport for a given request on this browser. On IE + * this may be an `XDomainRequest` rather than an `XMLHttpRequest`. + * @private + */ + newRequest: function(options) { + var me = this, + xhr; + if (options.binaryData) { + // This is a binary data request. Handle submission differently for differnet browsers + if (window.Uint8Array) { + xhr = me.getXhrInstance(); + } else { + // catch all for all other browser types + xhr = new Ext.data.flash.BinaryXhr(); + } + } else if (me.cors && Ext.isIE9m) { + xhr = me.getXdrInstance(); + me.isXdr = true; + } else { + xhr = me.getXhrInstance(); + me.isXdr = false; + } + return xhr; + }, + /** + * Setup all the headers for the request + * @private + * @param {Object} xhr The xhr object + * @param {Object} options The options for the request + * @param {Object} data The data for the request + * @param {Object} params The params for the request + */ + setupHeaders: function(xhr, options, data, params) { + var me = this, + headers = Ext.apply({}, options.headers || {}, me.defaultHeaders), + contentType = me.defaultPostHeader, + jsonData = options.jsonData, + xmlData = options.xmlData, + type = 'Content-Type', + useHeader = me.useDefaultXhrHeader, + key, header; + if (!headers.hasOwnProperty(type) && (data || params)) { + if (data) { + if (options.rawData) { + contentType = 'text/plain'; + } else { + if (xmlData && Ext.isDefined(xmlData)) { + contentType = 'text/xml'; + } else if (jsonData && Ext.isDefined(jsonData)) { + contentType = 'application/json'; + } + } + } + headers[type] = contentType; + } + if (useHeader && !headers['X-Requested-With']) { + headers['X-Requested-With'] = me.defaultXhrHeader; + } + // If undefined/null, remove it and don't set the header. + // Allow the browser to do so. + if (headers[type] === undefined || headers[type] === null) { + delete headers[type]; + } + // set up all the request headers on the xhr object + try { + for (key in headers) { + if (headers.hasOwnProperty(key)) { + header = headers[key]; + xhr.setRequestHeader(key, header); + } + } + } catch (e) { + // TODO Request shouldn't fire events from its owner + me.owner.fireEvent('exception', key, header); + } + return headers; + }, + /** + * Creates the appropriate XDR transport for this browser. + * - IE 7 and below don't support CORS + * - IE 8 and 9 support CORS with native XDomainRequest object + * - IE 10 (and above?) supports CORS with native XMLHttpRequest object + * @private + */ + getXdrInstance: function() { + var xdr; + if (Ext.ieVersion >= 8) { + xdr = new XDomainRequest(); + } else // eslint-disable-line no-undef + { + Ext.raise({ + msg: 'Your browser does not support CORS' + }); + } + return xdr; + }, + /** + * @private + * Do not remove this method. This is where Ajax simulator injects request stubs. + */ + getXhrInstance: function() { + return new XMLHttpRequest(); + }, + processXdrRequest: function(request, xhr) { + var me = this; + // Mutate the request object as per XDR spec. + delete request.headers; + request.contentType = request.options.contentType || me.defaultXdrContentType; + xhr.onload = me.bindStateChange(true); + xhr.onerror = xhr.ontimeout = me.bindStateChange(false); + }, + processXdrResponse: function(response, xhr) { + // Mutate the response object as per XDR spec. + response.getAllResponseHeaders = function() { + return []; + }; + response.getResponseHeader = function() { + return ''; + }; + response.contentType = xhr.contentType || this.defaultXdrContentType; + }, + bindStateChange: function(xdrResult) { + var me = this; + return function() { + Ext.elevate(function() { + me.onStateChange(xdrResult); + }); + }; + }, + onStateChange: function(xdrResult) { + var me = this, + xhr = me.xhr; + // Using CORS with IE doesn't support readyState so we fake it. + if ((xhr && xhr.readyState === 4) || me.isXdr) { + me.clearTimer(); + me.onComplete(xdrResult); + me.cleanup(); + } + }, + /** + * To be called when the request has come back from the server + * @param {Object} xdrResult + * @return {Object} The response + * @private + */ + onComplete: function(xdrResult) { + var me = this, + owner = me.owner, + options = me.options, + xhr = me.xhr, + failure = { + success: false, + isException: false + }, + result, success, response; + if (!xhr || me.destroyed) { + return me.result = failure; + } + try { + result = Ext.data.request.Ajax.parseStatus(xhr.status, xhr); + if (result.success) { + // This is quite difficult to reproduce, however if we abort a request + // just before it returns from the server, occasionally the status will be + // returned correctly but the request is still yet to be complete. + result.success = xhr.readyState === 4; + } + } catch (e) { + // In some browsers we can't access the status if the readyState is not 4, + // so the request has failed + result = failure; + } + success = me.success = me.isXdr ? xdrResult : result.success; + if (success) { + response = me.createResponse(xhr); + if (owner.hasListeners.requestcomplete) { + owner.fireEvent('requestcomplete', owner, response, options); + } + if (options.success) { + Ext.callback(options.success, options.scope, [ + response, + options + ]); + } + } else { + if (result.isException || me.aborted || me.timedout) { + response = me.createException(xhr); + } else { + response = me.createResponse(xhr); + } + if (owner.hasListeners.requestexception) { + owner.fireEvent('requestexception', owner, response, options); + } + if (options.failure) { + Ext.callback(options.failure, options.scope, [ + response, + options + ]); + } + } + me.result = response; + if (options.callback) { + Ext.callback(options.callback, options.scope, [ + options, + success, + response + ]); + } + owner.onRequestComplete(me); + me.callParent([ + xdrResult + ]); + return response; + }, + /** + * Creates the response object + * @param {Object} xhr + * @private + */ + createResponse: function(xhr) { + var me = this, + isXdr = me.isXdr, + headers = {}, + lines = isXdr ? [] : xhr.getAllResponseHeaders().replace(/\r\n/g, '\n').split('\n'), + count = lines.length, + line, index, key, response; + while (count--) { + line = lines[count]; + index = line.indexOf(':'); + if (index >= 0) { + key = line.substr(0, index).toLowerCase(); + if (line.charAt(index + 1) === ' ') { + ++index; + } + headers[key] = line.substr(index + 1); + } + } + response = { + request: me, + requestId: me.id, + status: xhr.status, + statusText: xhr.statusText, + getResponseHeader: function(header) { + return headers[header.toLowerCase()]; + }, + getAllResponseHeaders: function() { + return headers; + } + }; + if (isXdr) { + me.processXdrResponse(response, xhr); + } + if (me.binary) { + response.responseBytes = me.getByteArray(xhr); + } else { + if (xhr.responseType) { + response.responseType = xhr.responseType; + } + if (xhr.responseType === 'blob') { + response.responseBlob = xhr.response; + } else if (xhr.responseType === 'json') { + response.responseJson = xhr.response; + } else if (xhr.responseType === 'document') { + response.responseXML = xhr.response; + } else { + // an error is thrown when trying to access responseText or responseXML + // on an xhr object with responseType with any value but "text" or "", + // so only attempt to set these properties in the response if we're not + // dealing with other specified response types + response.responseText = xhr.responseText; + response.responseXML = xhr.responseXML; + } + } + return response; + }, + destroy: function() { + this.xhr = null; + this.callParent(); + }, + privates: { + /** + * Gets binary data from the xhr response object and returns it as a byte array + * @param {Object} xhr the xhr response object + * @return {Uint8Array/Array} + * @private + */ + getByteArray: function(xhr) { + var response = xhr.response, + responseBody = xhr.responseBody, + Cls = Ext.data.flash && Ext.data.flash.BinaryXhr, + byteArray, responseText, len, i; + if (xhr instanceof Cls) { + // If this was a BinaryXHR request via flash, we already have the bytes ready + byteArray = xhr.responseBytes; + } else if (window.Uint8Array) { + // Modern browsers (including IE10) have a native byte array + // which can be created by passing the ArrayBuffer (returned as + // the xhr.response property) to the Uint8Array constructor. + /* eslint-disable-next-line no-undef */ + byteArray = response ? new Uint8Array(response) : []; + } else if (Ext.isIE9p) { + // In IE9 and below the responseBody property contains a byte array + // but it is not directly accessible using javascript. + // In IE9p we can get the bytes by constructing a VBArray + // using the responseBody and then converting it to an Array. + try { + /* eslint-disable-next-line no-undef */ + byteArray = new VBArray(responseBody).toArray(); + } catch (e) { + // If the binary response is empty, the VBArray constructor will + // choke on the responseBody. We can't simply do a null check + // on responseBody because responseBody is always falsy when it + // contains binary data. + byteArray = []; + } + } else if (Ext.isIE) { + // IE8 and below also have a VBArray constructor, but throw a + // "VBArray Expected" error if you try to pass the responseBody to + // the VBArray constructor. + // http://msdn.microsoft.com/en-us/library/ye3x9by3%28v=vs.71%29.aspx + // so we have to use vbscript injection to access the bytes + if (!this.self.vbScriptInjected) { + this.injectVBScript(); + } + /* eslint-disable-next-line no-undef */ + getIEByteArray(xhr.responseBody, byteArray = []); + } else { + // in other older browsers make a best-effort attempt to read the + // bytes from responseText + byteArray = []; + responseText = xhr.responseText; + len = responseText.length; + for (i = 0; i < len; i++) { + // Some characters have an extra byte 0xF7 in the high order + // position. Throw away the high order byte and then push the + // result onto the byteArray. + byteArray.push(responseText.charCodeAt(i) & 255); + } + } + return byteArray; + }, + /** + * Injects a vbscript tag containing a 'getIEByteArray' method for reading + * binary data from an xhr response in IE8 and below. + * @private + */ + injectVBScript: function() { + var scriptTag = document.createElement('script'); + scriptTag.type = 'text/vbscript'; + /* eslint-disable indent */ + scriptTag.text = [ + 'Function getIEByteArray(byteArray, out)', + 'Dim len, i', + 'len = LenB(byteArray)', + 'For i = 1 to len', + 'out.push(AscB(MidB(byteArray, i, 1)))', + 'Next', + 'End Function' + ].join('\n'); + /* eslint-enable indent */ + Ext.getHead().dom.appendChild(scriptTag); + this.self.vbScriptInjected = true; + } + } +}); + +/** + * This class manages a pending form submit. Instances of this type are created by the + * `{@link Ext.data.Connection#request}` method. + * @since 6.0.0 + */ +Ext.define('Ext.data.request.Form', { + extend: Ext.data.request.Base, + alias: 'request.form', + start: function(data) { + var me = this, + options = me.options, + requestOptions = me.requestOptions; + // Parent will set the timeout + me.callParent([ + data + ]); + me.form = me.upload(options.form, requestOptions.url, requestOptions.data, options); + return me; + }, + abort: function(force) { + var me = this, + frame; + if (me.isLoading()) { + try { + frame = me.frame.dom; + if (frame.stop) { + frame.stop(); + } else { + frame.document.execCommand('Stop'); + } + } catch (e) {} + } + // ignore + me.callParent([ + force + ]); + me.onComplete(); + me.cleanup(); + }, + /* + * Clean up any left over information from the form submission. + */ + cleanup: function() { + var me = this, + frame = me.frame; + if (frame) { + // onComplete hasn't fired yet if frame != null so need to clean up + frame.un('load', me.onComplete, me); + Ext.removeNode(frame); + } + me.frame = me.form = null; + }, + isLoading: function() { + return !!this.frame; + }, + /** + * Uploads a form using a hidden iframe. + * @param {String/HTMLElement/Ext.dom.Element} form The form to upload + * @param {String} url The url to post to + * @param {String} params Any extra parameters to pass + * @param {Object} options The initial options + * @private + */ + upload: function(form, url, params, options) { + form = Ext.getDom(form); + options = options || {}; + /* eslint-disable-next-line vars-on-top */ + var frameDom = document.createElement('iframe'), + frame = Ext.get(frameDom), + id = frame.id, + hiddens = [], + encoding = 'multipart/form-data', + buf = { + target: form.target, + method: form.method, + encoding: form.encoding, + enctype: form.enctype, + action: form.action + }, + addField = function(name, value) { + hiddenItem = document.createElement('input'); + Ext.fly(hiddenItem).set({ + type: 'hidden', + value: value, + name: name + }); + form.appendChild(hiddenItem); + hiddens.push(hiddenItem); + }, + hiddenItem, obj, value, name, vLen, v, hLen, h; + /* + * Originally this behaviour was modified for Opera 10 to apply the secure URL after + * the frame had been added to the document. It seems this has since been corrected in + * Opera so the behaviour has been reverted, the URL will be set before being added. + */ + frame.set({ + name: id, + cls: Ext.baseCSSPrefix + 'hidden-display', + src: Ext.SSL_SECURE_URL, + tabIndex: -1 + }); + document.body.appendChild(frameDom); + document.body.appendChild(form); + // This is required so that IE doesn't pop the response up in a new window. + if (document.frames) { + document.frames[id].name = id; + } + Ext.fly(form).set({ + target: id, + method: 'POST', + enctype: encoding, + encoding: encoding, + action: url || buf.action + }); + // add dynamic params + if (params) { + obj = Ext.Object.fromQueryString(params) || {}; + for (name in obj) { + if (obj.hasOwnProperty(name)) { + value = obj[name]; + if (Ext.isArray(value)) { + vLen = value.length; + for (v = 0; v < vLen; v++) { + addField(name, value[v]); + } + } else { + addField(name, value); + } + } + } + } + this.frame = frame; + frame.on({ + load: this.onComplete, + scope: this, + // Opera introduces multiple 'load' events, so account for extras as well + single: !Ext.isOpera + }); + form.submit(); + document.body.removeChild(form); + // Restore form to previous settings + Ext.fly(form).set(buf); + for (hLen = hiddens.length , h = 0; h < hLen; h++) { + Ext.removeNode(hiddens[h]); + } + return form; + }, + getDoc: function() { + var frame = this.frame.dom; + return (frame && (frame.contentWindow.document || frame.contentDocument)) || (window.frames[frame.id] || {}).document; + }, + getTimeout: function() { + // For a form post, since it can include large file uploads, we do not use the + // default timeout from the owner. Only explicit timeouts passed in the options + // are meaningful here. + return this.options.timeout; + }, + /** + * Callback handler for the upload function. After we've submitted the form via the + * iframe this creates a bogus response object to simulate an XHR and populates its + * responseText from the now-loaded iframe's document body (or a textarea inside the + * body). We then clean up by removing the iframe. + * @private + */ + onComplete: function() { + var me = this, + frame = me.frame, + owner = me.owner, + options = me.options, + callback, doc, success, contentNode, response; + // Nulled out frame means onComplete was fired already + if (!frame) { + return; + } + if (me.aborted || me.timedout) { + me.result = response = me.createException(); + response.responseXML = null; + response.responseText = Ext.encode({ + success: false, + message: Ext.String.trim(response.statusText) + }); + response.request = me; + callback = options.failure; + success = false; + } else { + try { + doc = me.getDoc(); + // bogus response object + me.result = response = { + responseText: '', + responseXML: null, + request: me + }; + // Opera will fire an extraneous load event on about:blank + // We want to ignore this since the load event will be fired twice + if (doc) { + // TODO: See if this still applies vs Current opera-webkit releases + if (Ext.isOpera && doc.location === Ext.SSL_SECURE_URL) { + return; + } + if (doc.body) { + // Response sent as Content-Type: text/json or text/plain. + // Browser will embed it in a
 element.
+                        // Note: The statement below tests the result of an assignment.
+                        if ((contentNode = doc.body.firstChild) && /pre/i.test(contentNode.tagName)) {
+                            response.responseText = contentNode.textContent || contentNode.innerText;
+                        }
+                        // Response sent as Content-Type: text/html. We must still support
+                        // JSON response wrapped in textarea.
+                        // Note: The statement below tests the result of an assignment.
+                        else if ((contentNode = doc.getElementsByTagName('textarea')[0])) {
+                            response.responseText = contentNode.value;
+                        } else // Response sent as Content-Type: text/html with no wrapping. Scrape
+                        // JSON response out of text
+                        {
+                            response.responseText = doc.body.textContent || doc.body.innerText;
+                        }
+                    }
+                    // in IE the document may still have a body even if returns XML.
+                    // TODO What is this about?
+                    response.responseXML = doc.XMLDocument || doc;
+                    callback = options.success;
+                    success = true;
+                    response.status = 200;
+                } else {
+                    Ext.raise("Could not acquire a suitable connection for the " + "file upload service.");
+                }
+            } catch (e) {
+                me.result = response = me.createException();
+                // Report any error in the message property
+                response.status = 400;
+                response.statusText = (e.message || e.description) + '';
+                response.responseText = Ext.encode({
+                    success: false,
+                    message: Ext.String.trim(response.statusText)
+                });
+                response.responseXML = null;
+                callback = options.failure;
+                success = false;
+            }
+        }
+        me.frame = null;
+        me.success = success;
+        owner.fireEvent(success ? 'requestcomplete' : 'requestexception', owner, response, options);
+        Ext.callback(callback, options.scope, [
+            response,
+            options
+        ]);
+        Ext.callback(options.callback, options.scope, [
+            options,
+            success,
+            response
+        ]);
+        owner.onRequestComplete(me);
+        // Must defer slightly to permit full exit from load event before destruction
+        Ext.asap(frame.destroy, frame);
+        me.callParent();
+    },
+    destroy: function() {
+        this.cleanup();
+        this.callParent();
+    }
+});
+
+/**
+ * The Connection class encapsulates a connection to the page's originating domain, allowing
+ * requests to be made either to a configured URL, or to a URL specified at request time.
+ *
+ * Requests made by this class are asynchronous, and will return immediately. No data from the
+ * server will be available to the statement immediately following the {@link #request} call.
+ * To process returned data, use a success callback in the request options object, or an
+ * {@link #requestcomplete event listener}.
+ *
+ * # File Uploads
+ *
+ * File uploads are not performed using normal "Ajax" techniques, that is they are not performed
+ * using XMLHttpRequests. Instead the form is submitted in the standard manner with the DOM
+ * <form> element temporarily modified to have its target set to refer to a dynamically
+ * generated, hidden <iframe> which is inserted into the document but removed after the
+ * return data has been gathered.
+ *
+ * The server response is parsed by the browser to create the document for the IFRAME. If the
+ * server is using JSON to send the return object, then the Content-Type header must be set to
+ * "text/html" in order to tell the browser to insert the text unchanged into the document body.
+ *
+ * Characters which are significant to an HTML parser must be sent as HTML entities, so encode
+ * `<` as `<`, `&` as `&` etc.
+ *
+ * The response text is retrieved from the document, and a fake XMLHttpRequest object is created
+ * containing a responseText property in order to conform to the requirements of event handlers and
+ * callbacks.
+ *
+ * Be aware that file upload packets are sent with the content type multipart/form and some server
+ * technologies (notably JEE) may require some custom processing in order to retrieve parameter
+ * names and parameter values from the packet content.
+ *
+ * Also note that it's not possible to check the response code of the hidden iframe, so the success
+ * handler will ALWAYS fire.
+ *
+ * # Binary Posts
+ *
+ * The class supports posting binary data to the server by using native browser capabilities,
+ * or a flash polyfill plugin in browsers that do not support native binary posting (e.g.
+ * Internet Explorer version 9 or less). A number of limitations exist when the polyfill is used:
+ *
+ * - Only asynchronous connections are supported.
+ * - Only the POST method can be used.
+ * - The return data can only be binary for now. Set the {@link Ext.data.Connection#binary binary}
+ * parameter to `true`.
+ * - Only the 0, 1 and 4 (complete) readyState values will be reported to listeners.
+ * - The flash object will be injected at the bottom of the document and should be invisible.
+ * - Important: See note about packaing the flash plugin with the app in the documenetation of
+ * {@link Ext.data.flash.BinaryXhr BinaryXhr}.
+ *
+ */
+Ext.define('Ext.data.Connection', {
+    mixins: {
+        observable: Ext.mixin.Observable
+    },
+    statics: {
+        requestId: 0
+    },
+    enctypeRe: /multipart\/form-data/i,
+    config: {
+        /**
+         * @cfg {String} url
+         * The URL for this connection.
+         */
+        url: null,
+        /**
+         * @cfg {Boolean} async
+         * `true` if this request should run asynchronously. Setting this to `false` should
+         * generally be avoided, since it will cause the UI to be blocked, the user won't be able
+         * to interact with the browser until the request completes.
+         */
+        async: true,
+        // eslint-disable-line id-blacklist
+        /**
+         * @cfg {String} username
+         * The username to pass when using {@link #withCredentials}.
+         */
+        username: '',
+        /**
+         * @cfg {String} password
+         * The password to pass when using {@link #withCredentials}.
+         */
+        password: '',
+        /**
+         * @cfg {Boolean} disableCaching
+         * True to add a unique cache-buster param to GET requests.
+         */
+        disableCaching: true,
+        /**
+         * @cfg {Boolean} withCredentials
+         * True to set `withCredentials = true` on the XHR object
+         */
+        withCredentials: false,
+        /**
+         * @cfg {Boolean} binary
+         * True if the response should be treated as binary data.  If true, the binary
+         * data will be accessible as a "responseBytes" property on the response object.
+         */
+        binary: false,
+        /**
+         * @cfg {Boolean} cors
+         * True to enable CORS support on the XHR object. Currently the only effect of this option
+         * is to use the XDomainRequest object instead of XMLHttpRequest if the browser is IE8
+         * or above.
+         */
+        cors: false,
+        isXdr: false,
+        defaultXdrContentType: 'text/plain',
+        /**
+         * @cfg {String} disableCachingParam
+         * Change the parameter which is sent went disabling caching through a cache buster.
+         */
+        disableCachingParam: '_dc',
+        /**
+         * @cfg {Number} [timeout=30000] The timeout in milliseconds to be used for
+         * requests.
+         * Defaults to 30000 milliseconds (30 seconds).
+         *
+         * When a request fails due to timeout the XMLHttpRequest response object will
+         * contain:
+         *
+         *     timedout: true
+         */
+        timeout: 30000,
+        /**
+         * @cfg {Object} [extraParams] Any parameters to be appended to the request.
+         */
+        extraParams: null,
+        /**
+         * @cfg {Boolean} [autoAbort=false]
+         * Whether this request should abort any pending requests.
+         */
+        autoAbort: false,
+        /**
+         * @cfg {String} method
+         * The default HTTP method to be used for requests.
+         *
+         * If not set, but {@link #request} params are present, POST will be used;
+         * otherwise, GET will be used.
+         */
+        method: null,
+        /**
+         * @cfg {Object} defaultHeaders
+         * An object containing request headers which are added to each request made by this object.
+         */
+        defaultHeaders: null,
+        /**
+         * @cfg {String} defaultPostHeader
+         * The default header to be sent out with any post request.
+         */
+        defaultPostHeader: 'application/x-www-form-urlencoded; charset=UTF-8',
+        /**
+         * @cfg {Boolean} useDefaultXhrHeader
+         * `true` to send the {@link #defaultXhrHeader} along with any request.
+         */
+        useDefaultXhrHeader: true,
+        /**
+         * @cfg {String}
+         * The header to send with Ajax requests. Also see {@link #useDefaultXhrHeader}.
+         */
+        defaultXhrHeader: 'XMLHttpRequest'
+    },
+    /**
+     * @event beforerequest
+     * @preventable
+     * Fires before a network request is made to retrieve a data object.
+     * @param {Ext.data.Connection} conn This Connection object.
+     * @param {Object} options The options config object passed to the {@link #request} method.
+     */
+    /**
+     * @event requestcomplete
+     * Fires if the request was successfully completed.
+     * @param {Ext.data.Connection} conn This Connection object.
+     * @param {Object} response The XHR object containing the response data.
+     * See [The XMLHttpRequest Object](http://www.w3.org/TR/XMLHttpRequest/) for details.
+     * @param {Object} options The options config object passed to the {@link #request} method.
+     */
+    /**
+     * @event requestexception
+     * Fires if an error HTTP status was returned from the server. This event may also
+     * be listened to in the event that a request has timed out or has been aborted.
+     * See [HTTP Status Code Definitions](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
+     * for details of HTTP status codes.
+     * @param {Ext.data.Connection} conn This Connection object.
+     * @param {Object} response The XHR object containing the response data.
+     * See [The XMLHttpRequest Object](http://www.w3.org/TR/XMLHttpRequest/) for details.
+     * @param {Object} options The options config object passed to the {@link #request} method.
+     */
+    constructor: function(config) {
+        // Will call initConfig
+        this.mixins.observable.constructor.call(this, config);
+        this.requests = {};
+    },
+    /**
+     * @method request
+     * Sends an HTTP (Ajax) request to a remote server.
+     *
+     * **Important:** Ajax server requests are asynchronous, and this call will
+     * return before the response has been received.
+     *
+     * Instead, process any returned data using a promise:
+     *
+     *      Ext.Ajax.request({
+     *          url: 'ajax_demo/sample.json'
+     *      }).then(function(response, opts) {
+     *          var obj = Ext.decode(response.responseText);
+     *          console.dir(obj);
+     *      },
+     *      function(response, opts) {
+     *          console.log('server-side failure with status code ' + response.status);
+     *      });
+     *
+     * Or in callback functions:
+     *
+     *      Ext.Ajax.request({
+     *          url: 'ajax_demo/sample.json',
+     *
+     *          success: function(response, opts) {
+     *              var obj = Ext.decode(response.responseText);
+     *              console.dir(obj);
+     *          },
+     *
+     *          failure: function(response, opts) {
+     *              console.log('server-side failure with status code ' + response.status);
+     *          }
+     *      });
+     *
+     * To execute a callback function in the correct scope, use the `scope` option.
+     *
+     * @param {Object} options An object which may contain the following properties:
+     *
+     * (The options object may also contain any other property which might be needed to perform
+     * postprocessing in a callback because it is passed to callback functions.)
+     *
+     * @param {String/Function} options.url The URL to which to send the request, or a function
+     * to call which returns a URL string. The scope of the function is specified by the `scope`
+     * option. Defaults to the configured `url`.
+     *
+     * @param {Boolean} options.async `true` if this request should run asynchronously.
+     * Setting this to `false` should generally be avoided, since it will cause the UI to be
+     * blocked, the user won't be able to interact with the browser until the request completes.
+     * Defaults to `true`.
+     *
+     * @param {Object/String/Function} options.params An object containing properties which are
+     * used as parameters to the request, a url encoded string or a function to call to get either.
+     * The scope of the function is specified by the `scope` option.
+     *
+     * @param {String} options.method The HTTP method to use
+     * for the request. Defaults to the configured method, or if no method was configured,
+     * "GET" if no parameters are being sent, and "POST" if parameters are being sent.  Note that
+     * the method name is case-sensitive and should be all caps.
+     *
+     * @param {Function} options.callback The function to be called upon receipt of the HTTP
+     * response. The callback is called regardless of success or failure and is passed the
+     * following parameters:
+     * @param {Object} options.callback.options The parameter to the request call.
+     * @param {Boolean} options.callback.success True if the request succeeded.
+     * @param {Object} options.callback.response The XMLHttpRequest object containing the response
+     * data. See [www.w3.org/TR/XMLHttpRequest/](http://www.w3.org/TR/XMLHttpRequest/) for details
+     * about accessing elements of the response.
+     *
+     * @param {Function} options.success The function to be called upon success of the request.
+     * The callback is passed the following parameters:
+     * @param {Object} options.success.response The XMLHttpRequest object containing the response
+     * data.
+     * @param {Object} options.success.options The parameter to the request call.
+     *
+     * @param {Function} options.failure The function to be called upon failure of the request.
+     * The callback is passed the following parameters:
+     * @param {Object} options.failure.response The XMLHttpRequest object containing the response
+     * data.
+     * @param {Object} options.failure.options The parameter to the request call.
+     *
+     * @param {Object} options.scope The scope in which to execute the callbacks: The "this" object
+     * for the callback function. If the `url`, or `params` options were specified as functions
+     * from which to draw values, then this also serves as the scope for those function calls.
+     * Defaults to the browser window.
+     *
+     * @param {Number} options.timeout The timeout in milliseconds to be used for this
+     * request.
+     * Defaults to 30000 milliseconds (30 seconds).
+     *
+     * When a request fails due to timeout the XMLHttpRequest response object will
+     * contain:
+     *
+     *     timedout: true
+     *
+     * @param {Ext.Element/HTMLElement/String} options.form The `
` Element or the id of the + * `` to pull parameters from. + * + * @param {Boolean} options.isUpload **Only meaningful when used with the `form` option.** + * + * True if the form object is a file upload (will be set automatically if the form was + * configured with **`enctype`** `"multipart/form-data"`). + * + * File uploads are not performed using normal "Ajax" techniques, that is they are **not** + * performed using XMLHttpRequests. Instead the form is submitted in the standard manner with + * the DOM `<form>` element temporarily modified to have its + * [target](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/target) + * set to refer to a dynamically generated, hidden `<iframe>` which is inserted + * into the document but removed after the return data has been gathered. + * + * The server response is parsed by the browser to create the document for the IFRAME. If the + * server is using JSON to send the return object, then the + * [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) + * header must be set to "text/html" in order to tell the browser to insert the text + * unchanged into the document body. + * + * The response text is retrieved from the document, and a fake XMLHttpRequest object is created + * containing a `responseText` property in order to conform to the requirements of event + * handlers and callbacks. + * + * Be aware that file upload packets are sent with the content type + * [multipart/form](https://tools.ietf.org/html/rfc7233#section-4.1) and some server + * technologies (notably JEE) may require some custom processing in order to retrieve parameter + * names and parameter values from the packet content. + * + * - [target](http://www.w3.org/TR/REC-html40/present/frames.html#adef-target) + * - [Content-Type](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17) + * - [multipart/form](http://www.faqs.org/rfcs/rfc2388.html) + * + * @param {Object} options.headers Request headers to set for the request. + * The XHR will attempt to set an appropriate Content-Type based on the params/data passed + * to the request. To prevent this, setting the Content-Type header to `null` or `undefined` + * will not attempt to set any Content-Type and it will be left to the browser. + * + * @param {Object} options.xmlData XML document to use for the post. Note: This will be used + * instead of params for the post data. Any params will be appended to the URL. + * + * @param {Object/String} options.jsonData JSON data to use as the post. Note: This will be used + * instead of params for the post data. Any params will be appended to the URL. + * + * @param {String} options.rawData A raw string to use as the post. Note: This will be used + * instead of params for the post data. Any params will be appended to the URL. + * + * @param {Array} options.binaryData An array of bytes to submit in binary form. Any params + * will be appended to the URL. If binaryData is present, you must set + * {@link Ext.data.Connection#binary binary} to `true` and options.method to `POST`. + * + * @param {Boolean} options.disableCaching True to add a unique cache-buster param to GET + * requests. + * + * @param {Boolean} options.withCredentials True to add the withCredentials property to the + * XHR object + * + * @param {String} options.username The username to pass when using `withCredentials`. + * + * @param {String} options.password The password to pass when using `withCredentials`. + * + * @param {Boolean} options.binary True if the response should be treated as binary data. + * If true, the binary data will be accessible as a "responseBytes" property on the response + * object. + * + * @return {Ext.data.request.Base} The request object. This may be used to abort the + * request. + */ + request: function(options) { + var me = this, + requestOptions, request; + options = options || {}; + if (me.fireEvent('beforerequest', me, options) !== false) { + requestOptions = me.setOptions(options, options.scope || Ext.global); + request = me.createRequest(options, requestOptions); + return request.start(requestOptions.data); + } + // Reusing for response + request = { + status: -1, + statusText: 'Request cancelled in beforerequest event handler' + }; + Ext.callback(options.callback, options.scope, [ + options, + false, + request + ]); + return Ext.Deferred.rejected([ + options, + false, + request + ]); + }, + createRequest: function(options, requestOptions) { + var me = this, + type = options.type || requestOptions.type, + request; + // If request type is not specified we have to deduce it + if (!type) { + type = me.isFormUpload(options) ? 'form' : 'ajax'; + } + // if autoabort is set, cancel the current transactions + if (options.autoAbort || me.getAutoAbort()) { + me.abort(); + } + // It is possible for the original options object to be mutated if somebody + // had overridden Connection.setOptions method; it is also possible that such + // override would do a sensible thing and mutate outgoing requestOptions instead. + // So we have to pass *both* to the Request constructor, along with the set + // of defaults potentially set on the Connection instance. + // If it looks ridiculous, that's because it is; things we have to do for + // backward compatibility... + request = Ext.Factory.request({ + type: type, + owner: me, + options: options, + requestOptions: requestOptions, + ownerConfig: me.getConfig() + }); + me.requests[request.id] = request; + me.latestId = request.id; + return request; + }, + /** + * Detects whether the form is intended to be used for an upload. + * @private + */ + isFormUpload: function(options) { + var form = this.getForm(options); + if (form) { + return options.isUpload || this.enctypeRe.test(form.getAttribute('enctype')); + } + return false; + }, + /** + * Gets the form object from options. + * @private + * @param {Object} options The request options + * @return {HTMLElement} The form, null if not passed + */ + getForm: function(options) { + return Ext.getDom(options.form); + }, + /** + * Sets various options such as the url, params for the request + * @param {Object} options The initial options + * @param {Object} scope The scope to execute in + * @return {Object} The params for the request + */ + setOptions: function(options, scope) { + var me = this, + params = options.params || {}, + extraParams = me.getExtraParams(), + urlParams = options.urlParams, + url = options.url || me.getUrl(), + cors = options.cors, + jsonData = options.jsonData, + method, disableCache, data; + if (cors !== undefined) { + me.setCors(cors); + } + // allow params to be a method that returns the params object + if (Ext.isFunction(params)) { + params = params.call(scope, options); + } + // allow url to be a method that returns the actual url + if (Ext.isFunction(url)) { + url = url.call(scope, options); + } + url = this.setupUrl(options, url); + if (!url) { + Ext.raise({ + options: options, + msg: 'No URL specified' + }); + } + // check for xml or json data, and make sure json data is encoded + data = options.rawData || options.binaryData || options.xmlData || jsonData || null; + if (jsonData && !Ext.isPrimitive(jsonData)) { + data = Ext.encode(data); + } + // Check for binary data. Transform if needed + if (options.binaryData) { + if (!Ext.isArray(options.binaryData)) { + Ext.log.warn("Binary submission data must be an array of byte values! " + "Instead got " + typeof (options.binaryData)); + } + if (me.nativeBinaryPostSupport()) { + data = (new Uint8Array(options.binaryData)); + // eslint-disable-line no-undef + if ((Ext.isChrome && Ext.chromeVersion < 22) || Ext.isSafari || Ext.isGecko) { + // send the underlying buffer, not the view, since that's not supported + // on versions of chrome older than 22 + data = data.buffer; + } + } + } + // make sure params are a url encoded string and include any extraParams if specified + if (Ext.isObject(params)) { + params = Ext.Object.toQueryString(params); + } + if (Ext.isObject(extraParams)) { + extraParams = Ext.Object.toQueryString(extraParams); + } + params = params + ((extraParams) ? ((params) ? '&' : '') + extraParams : ''); + urlParams = Ext.isObject(urlParams) ? Ext.Object.toQueryString(urlParams) : urlParams; + params = this.setupParams(options, params); + // decide the proper method for this request + method = (options.method || me.getMethod() || ((params || data) ? 'POST' : 'GET')).toUpperCase(); + this.setupMethod(options, method); + disableCache = options.disableCaching !== false ? (options.disableCaching || me.getDisableCaching()) : false; + // if the method is get append date to prevent caching + if (method === 'GET' && disableCache) { + url = Ext.urlAppend(url, (options.disableCachingParam || me.getDisableCachingParam()) + '=' + (new Date().getTime())); + } + // if the method is get or there is json/xml data append the params to the url + if ((method === 'GET' || data) && params) { + url = Ext.urlAppend(url, params); + params = null; + } + // allow params to be forced into the url + if (urlParams) { + url = Ext.urlAppend(url, urlParams); + } + return { + url: url, + method: method, + data: data || params || null + }; + }, + /** + * Template method for overriding url + * @private + * @param {Object} options + * @param {String} url + * @return {String} The modified url + */ + setupUrl: function(options, url) { + var form = this.getForm(options); + if (form) { + url = url || form.action; + } + return url; + }, + /** + * Template method for overriding params + * @private + * @param {Object} options + * @param {String} params + * @return {String} The modified params + */ + setupParams: function(options, params) { + var form = this.getForm(options), + serializedForm; + if (form && !this.isFormUpload(options)) { + serializedForm = Ext.Element.serializeForm(form); + params = params ? (params + '&' + serializedForm) : serializedForm; + } + return params; + }, + /** + * Template method for overriding method + * @private + * @param {Object} options + * @param {String} method + * @return {String} The modified method + */ + setupMethod: function(options, method) { + if (this.isFormUpload(options)) { + return 'POST'; + } + return method; + }, + /** + * Determines whether this object has a request outstanding. + * + * @param {Object} [request] Defaults to the last transaction + * + * @return {Boolean} True if there is an outstanding request. + */ + isLoading: function(request) { + if (!request) { + request = this.getLatest(); + } + return request ? request.isLoading() : false; + }, + /** + * Aborts an active request. + * @param {Ext.ajax.Request} [request] Defaults to the last request + */ + abort: function(request) { + if (!request) { + request = this.getLatest(); + } + if (request && request.isLoading()) { + request.abort(); + } + }, + /** + * Aborts all active requests + */ + abortAll: function() { + var requests = this.requests, + id; + for (id in requests) { + this.abort(requests[id]); + } + }, + /** + * Gets the most recent request + * @return {Object} The request. Null if there is no recent request + * @private + */ + getLatest: function() { + var id = this.latestId, + request; + if (id) { + request = this.requests[id]; + } + return request || null; + }, + /** + * Clears the timeout on the request + * @param {Object} request The request + * @private + */ + clearTimeout: function(request) { + if (!request) { + request = this.getLatest(); + } + if (request) { + request.clearTimer(); + } + }, + onRequestComplete: function(request) { + delete this.requests[request.id]; + }, + /** + * @return {Boolean} `true` if the browser can natively post binary data. + * @private + */ + nativeBinaryPostSupport: function() { + return Ext.isChrome || (Ext.isSafari && Ext.isDefined(window.Uint8Array)) || (Ext.isGecko && Ext.isDefined(window.Uint8Array)); + } +}); + +/** + * A singleton instance of an `{@link Ext.data.Connection}`. This class is used to + * communicate with your server side code. It can be used as follows: + * + * Ext.Ajax.request({ + * url: 'ajax_demo/sample.json', + * + * success: function(response, opts) { + * var obj = Ext.decode(response.responseText); + * console.dir(obj); + * }, + * + * failure: function(response, opts) { + * console.log('server-side failure with status code ' + response.status); + * } + * }); + * + * Default options for all requests can be set by changing a property on the Ext.Ajax class: + * + * Ext.Ajax.setTimeout(60000); // 60 seconds + * + * Any options specified in the request method for the Ajax request will override any + * defaults set on the `Ext.Ajax` singleton. In the code sample below, the timeout for the + * request will be 60 seconds. + * + * Ext.Ajax.setTimeout(120000); // 120 seconds + * + * Ext.Ajax.request({ + * url: 'page.aspx', + * timeout: 60000 + * }); + * + * In general, this class will be used for all Ajax requests in your application. The main + * reason for creating a separate `{@link Ext.data.Connection}` is for a series of + * requests that share common settings that are different to all other requests in the + * application. + */ +Ext.define('Ext.Ajax', { + extend: Ext.data.Connection, + singleton: true, + /** + * @cfg {Object} extraParams + * @hide + */ + /** + * @cfg {Object} defaultHeaders + * @hide + */ + /** + * @cfg {String} method + * @hide + */ + /** + * @cfg {Number} timeout + * @hide + */ + /** + * @cfg {Boolean} autoAbort + * @hide + */ + /** + * @cfg {Boolean} disableCaching + * @hide + */ + /** + * @property {Boolean} disableCaching + * True to add a unique cache-buster param to GET requests. Defaults to true. + */ + /** + * @property {String} url + * The default URL to be used for requests to the server. + * If the server receives all requests through one URL, setting this once is easier than + * entering it on every request. + */ + /** + * @property {Object} extraParams + * An object containing properties which are used as extra parameters to each request made + * by this object. Session information and other data that you need + * to pass with each request are commonly put here. + */ + /** + * @property {Object} defaultHeaders + * An object containing request headers which are added to each request made by this object. + */ + /** + * @property {String} method + * The default HTTP method to be used for requests. Note that this is case-sensitive and + * should be all caps (if not set but params are present will use `POST`, otherwise will + * use `GET`.) + */ + /** + * @property {Number} timeout + * The timeout in milliseconds to be used for requests. Defaults to 30000. + * + * When a request fails due to timeout the XMLHttpRequest response object will + * contain: + * + * timedout: true + */ + /** + * @property {Boolean} autoAbort + * Whether a new request should abort any pending requests. + */ + autoAbort: false +}); + +/** + * @private + */ +Ext.define('Ext.AnimationQueue', { + singleton: true, + constructor: function() { + var me = this; + me.queue = []; + me.taskQueue = []; + me.runningQueue = []; + me.idleQueue = []; + me.isRunning = false; + me.isIdle = true; + me.run = me.run.bind(me); + // iOS has a nasty bug which causes pending requestAnimationFrame to not release + // the callback when the WebView is switched back and forth from / to being background + // process. We use a watchdog timer to workaround this, and restore the pending state + // correctly if this happens. + // This timer has to be set as an interval from the very beginning and we have to keep + // it running for as long as the app lives, setting it later doesn't seem to work. + // The watchdog timer must be accessible for environments to cancel. + if (Ext.os.is.iOS) { + me.watch.$skipTimerCheck = true; + me.watchdogTimer = Ext.interval(me.watch, 500, me); + } + }, + /** + * + * @param {Function} fn + * @param {Object} [scope] + * @param {Object} [args] + */ + start: function(fn, scope, args) { + var me = this; + me.queue.push(arguments); + if (!me.isRunning) { + if (me.hasOwnProperty('idleTimer')) { + Ext.undefer(me.idleTimer); + delete me.idleTimer; + } + if (me.hasOwnProperty('idleQueueTimer')) { + Ext.undefer(me.idleQueueTimer); + delete me.idleQueueTimer; + } + me.isIdle = false; + me.isRunning = true; + me.startCountTime = Ext.now(); + me.count = 0; + me.doStart(); + } + }, + clear: function() { + var me = this; + Ext.undefer(me.idleTimer); + Ext.undefer(me.idleQueueTimer); + Ext.unraf(me.animationFrameId); + me.idleTimer = me.idleQueueTimer = me.animationFrameId = null; + me.queue.length = me.taskQueue.length = me.runningQueue.length = me.idleQueue.length = 0; + me.isRunning = false; + me.isIdle = true; + me.startCountTime = Ext.now(); + me.count = 0; + }, + watch: function() { + if (this.isRunning && Ext.now() - this.lastRunTime >= 500) { + this.run(); + } + }, + run: function() { + var me = this, + queue = me.runningQueue, + now, item, element, i, ln; + // When asked to start or iterate, it will now create a new one + me.animationFrameId = null; + if (!me.isRunning) { + return; + } + now = Ext.now(); + me.lastRunTime = now; + me.frameStartTime = now; + // We are doing cleanup here for any destroyed elements + // this is temporary until we fix CssTransition to properly + // inform an element that it is being animated + // then the element, during destruction, will need to cleanup + // the animation (see Ext.fx.runner.CssTransition#run) + i = me.queue.length; + while (i--) { + item = me.queue[i]; + element = item[1] && item[1].getElement && item[1].getElement(); + if (element && element.destroyed) { + me.queue.splice(i, 1); + } + } + queue.push.apply(queue, me.queue); + // take a snapshot of the current queue and run it + for (i = 0 , ln = queue.length; i < ln; i++) { + me.invoke(queue[i]); + } + queue.length = 0; + /* eslint-disable-next-line vars-on-top */ + var elapse = me.frameStartTime - me.startCountTime, + count = ++me.count; + if (elapse >= 200) { + me.onFpsChanged(count * 1000 / elapse, count, elapse); + me.startCountTime = me.frameStartTime; + me.count = 0; + } + if (!me.queue.length) { + me.stop(); + } + // Could have been stopped while invoking handlers + if (me.isRunning) { + me.doIterate(); + } + }, + onFpsChanged: Ext.emptyFn, + onStop: Ext.emptyFn, + doStart: function() { + if (!this.animationFrameId) { + this.animationFrameId = Ext.raf(this.run); + } + this.lastRunTime = Ext.now(); + }, + doIterate: function() { + if (!this.animationFrameId) { + this.animationFrameId = Ext.raf(this.run); + } + }, + doStop: function() { + if (this.animationFrameId) { + Ext.unraf(this.animationFrameId); + } + this.animationFrameId = null; + }, + /** + * + * @param {Function} fn + * @param {Object} [scope] + * @param {Object} [args] + */ + stop: function(fn, scope, args) { + var me = this, + queue = me.queue, + ln = queue.length, + i, item; + if (!me.isRunning) { + return; + } + for (i = 0; i < ln; i++) { + item = queue[i]; + if (item[0] === fn && item[1] === scope && item[2] === args) { + queue.splice(i, 1); + i--; + ln--; + } + } + if (ln === 0) { + me.doStop(); + me.onStop(); + me.isRunning = false; + if (me.idleQueue.length && !me.idleTimer) { + me.idleTimer = Ext.defer(me.whenIdle, 100, me); + } + } + }, + onIdle: function(fn, scope, args) { + var me = this, + listeners = me.idleQueue, + i, ln, listener; + for (i = 0 , ln = listeners.length; i < ln; i++) { + listener = listeners[i]; + if (fn === listener[0] && scope === listener[1] && args === listener[2]) { + return; + } + } + listeners.push(arguments); + if (me.isIdle) { + me.processIdleQueue(); + } else if (!me.idleTimer) { + me.idleTimer = Ext.defer(me.whenIdle, 100, me); + } + }, + unIdle: function(fn, scope, args) { + var me = this, + listeners = me.idleQueue, + i, ln, listener; + for (i = 0 , ln = listeners.length; i < ln; i++) { + listener = listeners[i]; + if (fn === listener[0] && scope === listener[1] && args === listener[2]) { + listeners.splice(i, 1); + return true; + } + } + if (!listeners.length && me.idleTimer) { + Ext.undefer(me.idleTimer); + delete me.idleTimer; + } + if (!listeners.length && me.idleQueueTimer) { + Ext.undefer(me.idleQueueTimer); + delete me.idleQueueTimer; + } + return false; + }, + queueTask: function(fn, scope, args) { + this.taskQueue.push(arguments); + this.processTaskQueue(); + }, + dequeueTask: function(fn, scope, args) { + var listeners = this.taskQueue, + i, ln, listener; + for (i = 0 , ln = listeners.length; i < ln; i++) { + listener = listeners[i]; + if (fn === listener[0] && scope === listener[1] && args === listener[2]) { + listeners.splice(i, 1); + i--; + ln--; + } + } + }, + invoke: function(listener) { + var fn = listener[0], + scope = listener[1], + args = listener[2]; + fn = (typeof fn === 'string' ? scope[fn] : fn); + if (Ext.isArray(args)) { + fn.apply(scope, args); + } else { + fn.call(scope, args); + } + }, + whenIdle: function() { + delete this.idleTimer; + this.isIdle = true; + this.processIdleQueue(); + }, + processIdleQueue: function() { + if (!this.hasOwnProperty('idleQueueTimer')) { + this.idleQueueTimer = Ext.defer(this.processIdleQueueItem, 1, this); + } + }, + processIdleQueueItem: function() { + var listeners = this.idleQueue, + listener; + delete this.idleQueueTimer; + if (!this.isIdle) { + return; + } + if (listeners.length > 0) { + listener = listeners.shift(); + this.invoke(listener); + this.processIdleQueue(); + } + }, + processTaskQueue: function() { + if (!this.hasOwnProperty('taskQueueTimer')) { + this.taskQueueTimer = Ext.defer(this.processTaskQueueItem, 15, this); + } + }, + processTaskQueueItem: function() { + var listeners = this.taskQueue, + listener; + delete this.taskQueueTimer; + if (listeners.length > 0) { + listener = listeners.shift(); + this.invoke(listener); + this.processTaskQueue(); + } + }, + /* eslint-disable-next-line comma-style */ + /** + * + * @param {Number} fps Frames per second. + * @param {Number} count Actual number of frames rendered during interval. + * @param {Number} interval Interval duration. + */ + showFps: function() { + var styleTpl = { + color: 'white', + 'background-color': 'black', + 'text-align': 'center', + 'font-family': 'sans-serif', + 'font-size': '8px', + 'font-weight': 'normal', + 'font-style': 'normal', + 'line-height': '20px', + '-webkit-font-smoothing': 'antialiased', + 'zIndex': 100000, + position: 'absolute' + }; + Ext.getBody().append([ + // --- Average --- + { + style: Ext.applyIf({ + bottom: '50px', + left: 0, + width: '50px', + height: '20px' + }, styleTpl), + html: 'Average' + }, + { + style: Ext.applyIf({ + 'background-color': 'red', + 'font-size': '18px', + 'line-height': '50px', + bottom: 0, + left: 0, + width: '50px', + height: '50px' + }, styleTpl), + id: '__averageFps', + html: '0' + }, + // --- Min --- + { + style: Ext.applyIf({ + bottom: '50px', + left: '50px', + width: '50px', + height: '20px' + }, styleTpl), + html: 'Min (Last 1k)' + }, + { + style: Ext.applyIf({ + 'background-color': 'orange', + 'font-size': '18px', + 'line-height': '50px', + bottom: 0, + left: '50px', + width: '50px', + height: '50px' + }, styleTpl), + id: '__minFps', + html: '0' + }, + // --- Max --- + { + style: Ext.applyIf({ + bottom: '50px', + left: '100px', + width: '50px', + height: '20px' + }, styleTpl), + html: 'Max (Last 1k)' + }, + { + style: Ext.applyIf({ + 'background-color': 'maroon', + 'font-size': '18px', + 'line-height': '50px', + bottom: 0, + left: '100px', + width: '50px', + height: '50px' + }, styleTpl), + id: '__maxFps', + html: '0' + }, + // --- Current --- + { + style: Ext.applyIf({ + bottom: '50px', + left: '150px', + width: '50px', + height: '20px' + }, styleTpl), + html: 'Current' + }, + { + style: Ext.applyIf({ + 'background-color': 'green', + 'font-size': '18px', + 'line-height': '50px', + bottom: 0, + left: '150px', + width: '50px', + height: '50px' + }, styleTpl), + id: '__currentFps', + html: '0' + } + ]); + Ext.AnimationQueue.resetFps(); + }, + resetFps: function() { + var currentFps = Ext.get('__currentFps'), + averageFps = Ext.get('__averageFps'), + minFps = Ext.get('__minFps'), + maxFps = Ext.get('__maxFps'), + min = 1000, + max = 0, + count = 0, + sum = 0; + if (!currentFps) { + return; + } + Ext.AnimationQueue.onFpsChanged = function(fps) { + count++; + if (!(count % 10)) { + min = 1000; + max = 0; + } + sum += fps; + min = Math.min(min, fps); + max = Math.max(max, fps); + currentFps.setHtml(Math.round(fps)); + // All-time average since last reset. + averageFps.setHtml(Math.round(sum / count)); + minFps.setHtml(Math.round(min)); + maxFps.setHtml(Math.round(max)); + }; + } +}, function() { + /* + Global FPS indicator. Add ?showfps to use in any application. Note that this REQUIRES + true requestAnimationFrame to be accurate. + */ + var paramsString = window.location.search.substr(1), + paramsArray = paramsString.split("&"); + if (Ext.Array.contains(paramsArray, "showfps")) { + Ext.onReady(this.showFps.bind(this)); + } +}); + +/** + * This class makes buffered methods simple and also handles cleanup on `destroy`. + * + * Ext.define('Foo', { + * mixins: [ + * 'Ext.mixin.Bufferable' + * ], + * + * bufferableMethods: { + * // Provides a "foobar" method that calls "doFoobar" with the + * // most recent arguments but delayed by 50ms from the last + * // call. Calls to "foobar" made during the 50ms wait restart + * // the timer and replace the arguments. + * + * foobar: 50 + * }, + * + * method: function() { + * this.foobar(42); // call doFoobar in 50ms + * + * if (this.isFoobarPending) { + * // test if "foobar" is pending + * } + * + * this.flushFoobar(); // actually, call it now + * + * this.cancelFoobar(); // or never mind + * }, + * + * doFoobar: function() { + * // time to do the "foobar" thing + * } + * }); + * + * @since 6.5.0 + * @private + */ +Ext.define('Ext.mixin.Bufferable', function(Bufferable) { + return { + // eslint-disable-line brace-style, max-len + extend: Ext.Mixin, + mixinConfig: { + id: 'bufferable', + after: { + destroy: 'cancelAllCalls' + }, + before: { + // The bufferables need to be destroyed before they get nulled + $reap: 'cancelAllCalls' + }, + extended: function(baseClass, derivedClass, classBody) { + var bufferableMethods = classBody.bufferableMethods; + if (bufferableMethods) { + delete classBody.bufferableMethods; + Bufferable.processClass(derivedClass, bufferableMethods); + } + } + }, + afterClassMixedIn: function(targetClass) { + Bufferable.processClass(targetClass); + }, + privates: { + /** + * Cancel all pending `bufferableMethod` calls on this object. + * @since 6.5.0 + * @private + */ + cancelAllCalls: function() { + var bufferables = this.bufferables, + name; + if (bufferables) { + for (name in bufferables) { + bufferables[name].cancel(); + delete bufferables[name]; + } + } + }, + /** + * Cancel a specific pending `bufferableMethod` call on this object. + * @param {String} name The name of the buffered method to cancel. + * @param {Boolean} invoke (private) + * @return {Boolean} Returns `true` if a cancellation occurred. + * @since 6.5.0 + * @private + */ + cancelBufferedCall: function(name, invoke) { + var bufferables = this.bufferables, + timer = bufferables && bufferables[name]; + if (timer) { + timer[invoke ? 'invoke' : 'cancel'](); + } + return !!timer; + }, + /** + * Flushes a specific pending `bufferableMethod` call on this object if one is + * pending. + * @param {String} name The name of the buffered method to cancel. + * @return {Boolean} Returns `true` if a flush occurred. + * @since 6.5.0 + * @private + */ + flushBufferedCall: function(name) { + return this.cancelBufferedCall(name, true); + }, + /** + * This method initializes an instance when the first bufferable method is called. + * It merges an instance-level `bufferableMethods` config if present. This allows + * an instance to change the buffer timeouts, even to 0 to disable buffering. + * + * Ext.create({ + * ... + * bufferableMethods: { + * foobar: 0 + * } + * }); + * + * Note, this method cannot effect unbuffered methods. The `bufferableMethods` + * config only instruments buffered methods when used on a class declaration. + * + * @return {Object} + * @since 6.5.0 + * @private + */ + initBufferables: function() { + var me = this, + methods = me.hasOwnProperty('bufferableMethods') && me.bufferableMethods, + classMethods; + if (methods) { + Bufferable._canonicalize(methods); + classMethods = me.self.prototype.bufferableMethods; + me.bufferableMethods = Ext.merge(Ext.clone(classMethods), methods); + } + return (me.bufferables = {}); + }, + /** + * Returns `true` if a specific `bufferableMethod` is pending. + * @param {String} name The name of the buffered method to cancel. + * @return {Boolean} + * @since 6.5.0 + * @private + */ + isCallPending: function(name) { + var bufferables = this.bufferables, + timer = bufferables && bufferables[name]; + return !!timer; + }, + statics: { + SINGLE: { + single: true + }, + _canonicalize: function(methods) { + var t, def, s, name; + for (name in methods) { + s = Ext.String.capitalize(name); + def = methods[name]; + t = typeof def; + if (t === 'number' || t === 'string') { + // method: 50 + // method: 'asap' + // method: 'idle' + // method: 'raf' + methods[name] = def = { + delay: def + }; + } + if (typeof (t = def.delay) === 'string') { + // method: { + // delay: 'asap' + // } + def[t] = true; + delete def.delay; + } + def.capitalized = s; + def.name = name; + if (!def.fn) { + def.fn = 'do' + s; + } + if (!def.flag) { + def.flag = 'is' + s + 'Pending'; + } + } + }, + _canceller: function() { + var timer = this, + // this fn is "cancel()" on timer instances + id = timer.id; + if (id) { + if (timer.delay) { + Ext.undefer(id); + } else if (timer.asap) { + Ext.unasap(id); + } else if (timer.idle) { + Ext.un('idle', id, null, Bufferable.SINGLE); + } else if (timer.raf) { + Ext.unraf(id); + } + timer.id = null; + } + timer.args = null; + timer.target[timer.flag] = false; + }, + _invoker: function() { + var timer = this, + // this fn is "invoke()" on timer instances + args = timer.args || Ext.emptyArray, + target = timer.target; + ++timer.invokes; + timer.cancel(); + target[timer.fn].apply(target, args); + }, + delayCall: function(target, def, args) { + if (target.destroying) { + return; + } + // eslint-disable-next-line vars-on-top + var bufferables = target.bufferables || target.initBufferables(), + name = def.name, + timer = bufferables[name] || (bufferables[name] = Ext.apply({ + calls: 0, + invokes: 0, + args: null, + cancel: Bufferable._canceller, + id: null, + target: target, + invoke: Bufferable._invoker + }, def)), + delay = def.delay, + exec = function() { + if (timer.id) { + timer.id = null; + timer.invoke(); + } + }; + if (timer.id) { + timer.cancel(); + } + timer.args = args; + ++timer.calls; + target[timer.flag] = true; + if (delay) { + timer.id = Ext.defer(exec, delay); + } else if (def.asap) { + timer.id = Ext.asap(exec); + } else if (def.idle) { + timer.id = exec; + Ext.on('idle', exec, null, Bufferable.SINGLE); + } else if (def.raf) { + timer.id = Ext.raf(exec); + } else { + // allow bufferableMethods: { foo: 0 } to force immediate call + timer.invoke(); + } + }, + processClass: function(cls, bufferableMethods) { + var proto = cls.prototype, + inherited = proto.bufferableMethods, + def, name; + if (bufferableMethods) { + // if (derived class) + Bufferable._canonicalize(bufferableMethods); + if (inherited) { + // If we have a derived class, it could be just adjusting the + // configuration, not introducing new properties, so clone the + // inherited config and merge on the one from the classBody. + inherited = Ext.merge(Ext.clone(inherited), bufferableMethods); + } + proto.bufferableMethods = inherited || bufferableMethods; + } else { + // else we are being mixed in, so the bufferableMethods on the + // prototype almost certainly belong to the immediate user class + // that is mixing us in... (leave the config on the prototype) + bufferableMethods = inherited; + Bufferable._canonicalize(bufferableMethods); + // prevent shape change + proto.bufferables = null; + } + if (bufferableMethods) { + for (name in bufferableMethods) { + if (!proto[name]) { + def = bufferableMethods[name]; + Bufferable.processMethod(proto, def, Array.prototype.slice); + } + } + } + }, + processMethod: function(proto, def, slice) { + var name = def.name, + cap = def.capitalized; + proto[name] = function() { + return Bufferable.delayCall(this, def, slice.call(arguments)); + }; + proto['cancel' + cap] = function() { + return this.cancelBufferedCall(name); + }; + proto['flush' + cap] = function() { + return this.flushBufferedCall(name); + }; + } + } + } + }; +}); +// statics +// privates + +/** + * Provides a registry of all Components (instances of {@link Ext.Component} or any subclass + * thereof) on a page so that they can be easily accessed by {@link Ext.Component component} + * {@link Ext.Component#id id} (see {@link #get}, or the convenience method + * {@link Ext#getCmp Ext.getCmp}). + * + * This object also provides a registry of available Component *classes* indexed by a + * mnemonic code known as the Component's {@link Ext.Component#xtype xtype}. The `xtype` + * provides a way to avoid instantiating child Components when creating a full, nested + * config object for a complete Ext page. + * + * A child Component may be specified simply as a *config object* as long as the correct + * `{@link Ext.Component#xtype xtype}` is specified so that if and when the Component + * needs rendering, the correct type can be looked up for lazy instantiation. + * + * @singleton + */ +Ext.define('Ext.ComponentManager', { + alternateClassName: 'Ext.ComponentMgr', + singleton: true, + mixins: [ + Ext.mixin.Bufferable + ], + count: 0, + fixReferencesTimer: null, + referenceRepairs: 0, + typeName: 'xtype', + bufferableMethods: { + handleDocumentMouseDown: 'asap' + }, + /** + * @private + */ + constructor: function(config) { + var me = this; + Ext.apply(me, config); + me.all = {}; + me.byInstanceId = {}; + me.holders = {}; + me.onAvailableCallbacks = {}; + }, + /** + * Creates a new Component from the specified config object using the config object's + * `xtype` to determine the class to instantiate. + * + * @param {Object} config A configuration object for the Component you wish to create. + * @param {String} [defaultType] The `xtype` to use if the config object does not + * contain a `xtype`. (Optional if the config contains a `xtype`). + * @return {Ext.Component} The newly instantiated Component. + */ + create: function(config, defaultType) { + if (typeof config === 'string') { + return Ext.widget(config); + } + if (config.isComponent) { + return config; + } + if ('xclass' in config) { + return Ext.create(config.xclass, config); + } + return Ext.widget(config.xtype || defaultType, config); + }, + /** + * Returns an item by id. + * @param {String} id The id of the item + * @return {Object} The item, undefined if not found. + */ + get: function(id) { + return this.all[id]; + }, + register: function(component) { + var me = this, + id = component.getId(), + onAvailableCallbacks = me.onAvailableCallbacks; + if (id === undefined) { + Ext.raise('Component id is undefined. Please ensure the component has an id.'); + } + if (id in me.all) { + Ext.raise('Duplicate component id "' + id + '"'); + } + if (component.$iid in me.byInstanceId) { + Ext.raise('Duplicate component instance id "' + component.$iid + '"'); + } + me.all[id] = component; + me.byInstanceId[component.$iid] = component; + if (component.nameHolder || component.referenceHolder) { + me.holders[id] = component; + } + ++me.count; + if (!me.hasFocusListener) { + me.installFocusListener(); + } + onAvailableCallbacks = onAvailableCallbacks && onAvailableCallbacks[id]; + if (onAvailableCallbacks && onAvailableCallbacks.length) { + me.notifyAvailable(component); + } + }, + unregister: function(component) { + var me = this, + all = me.all, + byInstanceId = me.byInstanceId, + holders = me.holders, + id = component.getId(); + if (id in holders) { + // Helps IE since delete may just mark the entry as "free" and not + // release the object by clearing the entry value. + // TODO find out when IE fixed this + holders[id] = null; + delete holders[id]; + } + all[id] = null; + delete all[id]; + id = component.$iid; + byInstanceId[id] = null; + delete byInstanceId[id]; + --me.count; + }, + markReferencesDirty: function() { + var me = this, + holders = me.holders, + holder, id; + if (!Ext.referencesDirty) { + // Clear all collections (no stale entries) + for (id in holders) { + holder = holders[id]; + holder.refs = holder.nameRefs = null; + if (holder.invalidateChildDirty) { + holder.invalidateChildDirty(); + } + } + Ext.referencesDirty = true; + me.fixReferencesTimer = Ext.asap(function() { + me.fixReferencesTimer = null; + me.fixReferences(); + }); + } + }, + fixReferences: function() { + var me = this, + all = me.all, + holders = me.holders, + holder, id; + if (Ext.referencesDirty) { + me.fixReferencesTimer = Ext.unasap(me.fixReferencesTimer); + // Falsy value but also !== false so we can tell we're fixing the refs + Ext.referencesDirty = 0; + ++me.referenceRepairs; + for (id in holders) { + holder = holders[id]; + if (holder.beginSyncChildDirty) { + holder.beginSyncChildDirty(); + } + } + for (id in all) { + all[id]._fixReference(); + } + for (id in holders) { + holder = holders[id]; + if (holder.finishSyncChildDirty) { + holder.finishSyncChildDirty(); + } + } + Ext.referencesDirty = false; + } + }, + /** + * Registers a function that will be called (a single time) when an item with the specified + * id is added to the manager. This will happen on instantiation. + * @param {String} id The item id + * @param {Function} fn The callback function. Called with a single parameter, the item. + * @param {Object} scope The scope ('this' reference) in which the callback is executed. + * Defaults to the item. + */ + onAvailable: function(id, fn, scope) { + var me = this, + callbacks = me.onAvailableCallbacks, + all = me.all, + item; + if (id in all) { + // if already an instance, callback immediately + item = all[id]; + fn.call(scope || item, item); + } else if (id) { + // otherwise, queue for dispatch + if (!Ext.isArray(callbacks[id])) { + callbacks[id] = []; + } + callbacks[id].push(function(item) { + fn.call(scope || item, item); + }); + } + }, + /** + * @private + */ + notifyAvailable: function(item) { + var callbacks = this.onAvailableCallbacks[item && item.getId()] || []; + while (callbacks.length) { + (callbacks.shift())(item); + } + }, + /** + * Executes the specified function once for each item in the collection. + * @param {Function} fn The function to execute. + * @param {String} fn.key The key of the item + * @param {Number} fn.value The value of the item + * @param {Number} fn.length The total number of items in the collection ** Removed + * in 5.0 ** + * @param {Boolean} fn.return False to cease iteration. + * @param {Object} scope The scope to execute in. Defaults to `this`. + */ + each: function(fn, scope) { + Ext.Object.each(this.all, fn, scope); + }, + /** + * Gets the number of items in the collection. + * @return {Number} The number of items in the collection. + */ + getCount: function() { + return this.count; + }, + /** + * Returns an array of all components + * @return {Array} + */ + getAll: function() { + return Ext.Object.getValues(this.all); + }, + /** + * Return the currently active (focused) Component + * + * @return {Ext.Component/null} Active Component, or null + * @private + */ + getActiveComponent: function() { + return Ext.Component.from(Ext.dom.Element.getActiveElement()); + }, + // Deliver focus events to Component + onGlobalFocus: function(info) { + var me = this, + event = info.event.chain(), + infoCopy = Ext.applyIf({ + event: event + }, info), + to, from, ancestor, target; + to = event.toComponent = infoCopy.toComponent = Ext.Component.from(info.toElement); + from = event.fromComponent = infoCopy.fromComponent = Ext.Component.from(info.fromElement); + ancestor = me.getCommonAncestor(from, to); + // Focus moves *within* a component should not cause component focus leave/enter + if (to !== from) { + if (from && !from.destroyed && !from.isDestructing()) { + if (from.handleBlurEvent) { + from.handleBlurEvent(infoCopy); + } + // Call onFocusLeave on the component axis from which focus is exiting + for (target = from; target && target !== ancestor; target = target.getRefOwner()) { + if (!(target.destroyed || target.destroying)) { + event.type = 'focusleave'; + target.onFocusLeave(event); + } + } + } + if (to && !to.destroyed && !to.isDestructing()) { + if (to.handleFocusEvent) { + to.handleFocusEvent(infoCopy); + } + // Call onFocusEnter on the component axis to which focus is entering + for (target = to; target && target !== ancestor; target = target.getRefOwner()) { + event.type = 'focusenter'; + target.onFocusEnter(event); + } + } + } + for (target = ancestor; target; target = target.getRefOwner()) { + if (!(target.destroying || target.destroyed)) { + target.onFocusMove(infoCopy); + } + } + }, + getCommonAncestor: function(compA, compB) { + if (compA === compB) { + return compA; + } + while (compA && !(compA.isAncestor(compB) || compA === compB)) { + compA = compA.getRefOwner(); + } + return compA; + }, + privates: { + /** + * This method reorders the DOM structure of floated components to arrange that the + * clicked element is last of its siblings, and therefore on the visual "top" of + * the floated component stack. + * + * This is a Bufferable ASAP method invoked directly from Ext.GlobalEvents. + * + * We need to use ASAP, not a low priority listener because we need it + * to run *after* the browser's default response to the event has been + * processed, ie focus consequences. + * For example, a Dialog contains a picker field, and the picker field has + * its floated picker open and focused. + * The user mousedowns on another field in the dialog. The resulting + * immediate DOM shuffle to bring the dialog above the picker results + * in focus being silently lost. + * @param {type} e The mousedown event + * @private + */ + doHandleDocumentMouseDown: function(e) { + var floatedSelector = Ext.Widget.prototype.floatedSelector, + targetFloated; + // If mousedown/pointerdown/touchstart is on a floated Component, ask it to sync + // its place in the hierarchy. + if (floatedSelector) { + targetFloated = Ext.Component.from(e.getTarget(floatedSelector, Ext.getBody())); + // If the mousedown is in a floated, move it to top. + if (targetFloated) { + targetFloated.toFront(true); + } + } + }, + installFocusListener: function() { + var me = this; + Ext.on('focus', me.onGlobalFocus, me); + me.hasFocusListener = true; + }, + clearAll: function() { + var me = this; + me.all = {}; + me.byInstanceId = {}; + me.holders = {}; + me.onAvailableCallbacks = {}; + }, + /** + * Find the Widget or Component to which the given Element belongs. + * + * @param {Ext.dom.Element/HTMLElement} el The element from which to start to find + * an owning Component. + * @param {Ext.dom.Element/HTMLElement} [limit] The element at which to stop upward + * searching for an owning Component, or the number of Components to traverse before + * giving up. Defaults to the document's HTML element. + * @param {String} [selector] An optional {@link Ext.ComponentQuery} selector to + * filter the target. + * @return {Ext.Widget/Ext.Component} The widget, component or `null`. + * + * @private + * @since 6.5.0 + */ + from: function(el, limit, selector) { + var cache = this.all, + depth = 0, + target, topmost, cmpId, cmp; + if (el && el.isEvent) { + el = el.target; + } + target = Ext.getDom(el); + if (typeof limit !== 'number') { + topmost = Ext.getDom(limit); + limit = Number.MAX_VALUE; + } + while (target && target.nodeType === 1 && depth < limit && target !== topmost) { + cmpId = target.getAttribute('data-componentid') || target.id; + if (cmpId) { + cmp = cache[cmpId]; + if (cmp && (!selector || Ext.ComponentQuery.is(cmp, selector))) { + return cmp; + } + // Increment depth on every *Component* found, not Element + depth++; + } + target = target.parentNode; + } + return null; + } + } +}, function(ComponentManager) { + // Backwards compat: + ComponentManager.fromElement = ComponentManager.from; + // No components yet, so nothing is dirty. We need this to be false when the first + // component is created so that it sets our fixup timer. + Ext.referencesDirty = false; + Ext.fixReferences = function() { + ComponentManager.fixReferences(); + }; + Ext.markReferencesDirty = function() { + ComponentManager.markReferencesDirty(); + }; + /** + * This is shorthand reference to {@link Ext.ComponentManager#get}. + * Looks up an existing {@link Ext.Component Component} by {@link Ext.Component#id id} + * + * @method getCmp + * @param {String} id The component {@link Ext.Component#id id} + * @return {Ext.Component} The Component, `undefined` if not found, or `null` if a + * Class was found. + * @member Ext + */ + Ext.getCmp = function(id) { + return ComponentManager.get(id); + }; + Ext.iidToCmp = function(iid) { + return ComponentManager.byInstanceId[iid] || null; + }; + /** + * @private + * @deprecated 6.6.0 Inline event handlers are deprecated + */ + Ext.doEv = function(node, e) { + var cmp, method, event; + // The event here is a raw browser event, so don't pass the event directly + // since from expects an Ext.event.Event + cmp = Ext.Component.from(e.target); + if (cmp && !cmp.destroying && !cmp.destroyed && cmp.getEventHandlers) { + method = cmp.getEventHandlers()[e.type]; + if (method && cmp[method]) { + event = new Ext.event.Event(e); + return cmp[method](event); + } + } + return true; + }; +}); + +/** + * This class defines the operators that are shared by DomQuery and ComponentQuery + * @class Ext.util.Operators + * @private + */ +/* eslint-disable eqeqeq */ +Ext.ns('Ext.util').Operators = { + // @define Ext.util.Operators + "=": function(a, v) { + return a == v; + }, + "!=": function(a, v) { + return a != v; + }, + "^=": function(a, v) { + return a && a.substr(0, v.length) == v; + }, + "$=": function(a, v) { + return a && a.substr(a.length - v.length) == v; + }, + "*=": function(a, v) { + return a && a.indexOf(v) !== -1; + }, + "%=": function(a, v) { + return (a % v) === 0; + }, + "|=": function(a, v) { + return a && (a == v || a.substr(0, v.length + 1) == v + '-'); + }, + "~=": function(a, v) { + return a && (' ' + a + ' ').indexOf(' ' + v + ' ') != -1; + } +}; + +/** + * @private + * @class Ext.util.LruCache + * @extend Ext.util.HashMap + * A linked {@link Ext.util.HashMap HashMap} implementation which maintains most recently accessed + * items at the end of the list, and purges the cache down to the most recently accessed + * {@link #maxSize} items upon add. + */ +Ext.define('Ext.util.LruCache', { + extend: Ext.util.HashMap, + config: { + /** + * @cfg {Number} maxSize + * The maximum size the cache is allowed to grow to before further additions + * cause removal of the least recently used entry. + */ + maxSize: null + }, + /** + * @method add + * @inheritdoc + */ + add: function(key, newValue) { + var me = this, + entry, last; + me.removeAtKey(key); + last = me.last; + entry = { + prev: last, + next: null, + key: key, + value: newValue + }; + if (last) { + // If the list is not empty, update the last entry + last.next = entry; + } else { + // List is empty + me.first = entry; + } + me.last = entry; + me.callParent([ + key, + entry + ]); + me.prune(); + return newValue; + }, + /** + * @private + */ + insertBefore: function(key, newValue, sibling) { + var me = this, + existingKey, entry; + // NOT an assignment. + // If there is a following sibling + // eslint-disable-next-line no-cond-assign + if (sibling = this.map[this.findKey(sibling)]) { + existingKey = me.findKey(newValue); + // "new" value is in the list. + if (existingKey) { + me.unlinkEntry(entry = me.map[existingKey]); + } else // Genuinely new: create an entry for it. + { + entry = { + prev: sibling.prev, + next: sibling, + key: key, + value: newValue + }; + } + if (sibling.prev) { + entry.prev.next = entry; + } else { + me.first = entry; + } + entry.next = sibling; + sibling.prev = entry; + me.prune(); + return newValue; + } else // No following sibling, it's just an add. + { + return me.add(key, newValue); + } + }, + /** + * @method get + * @inheritdoc + */ + get: function(key) { + var entry = this.map[key]; + if (entry) { + // If it's not the end, move to end of list on get + if (entry.next) { + this.moveToEnd(entry); + } + return entry.value; + } + }, + /** + * @private + */ + removeAtKey: function(key) { + this.unlinkEntry(this.map[key]); + return this.callParent(arguments); + }, + /** + * @method clear + * @inheritdoc + * @param initial (private) + */ + clear: function(initial) { + this.first = this.last = null; + return this.callParent([ + initial + ]); + }, + /** + * @private + */ + unlinkEntry: function(entry) { + // Stitch the list back up. + if (entry) { + if (entry.next) { + entry.next.prev = entry.prev; + } else { + this.last = entry.prev; + } + if (entry.prev) { + entry.prev.next = entry.next; + } else { + this.first = entry.next; + } + entry.prev = entry.next = null; + } + }, + /** + * @private + */ + moveToEnd: function(entry) { + this.unlinkEntry(entry); + // NOT an assignment. + // If the list is not empty, update the last entry + // eslint-disable-next-line no-cond-assign + if (entry.prev = this.last) { + this.last.next = entry; + } else // List is empty + { + this.first = entry; + } + this.last = entry; + }, + /** + * @private + */ + getArray: function(isKey) { + var arr = [], + entry = this.first; + while (entry) { + arr.push(isKey ? entry.key : entry.value); + entry = entry.next; + } + return arr; + }, + /** + * Executes the specified function once for each item in the cache. + * Returning false from the function will cease iteration. + * + * By default, iteration is from least recently used to most recent. + * + * The paramaters passed to the function are: + *
    + *
  • key : String

    The key of the item

  • + *
  • value : Number

    The value of the item

  • + *
  • length : Number

    The total number of items in the hash

  • + *
+ * @param {Function} fn The function to execute. + * @param {Object} scope The scope (this reference) to execute in. Defaults + * to this LruCache. + * @param {Boolean} [reverse=false] Pass true to iterate the list in reverse + * (most recent first) order. + * @return {Ext.util.LruCache} this + */ + each: function(fn, scope, reverse) { + var me = this, + entry = reverse ? me.last : me.first, + length = me.length; + scope = scope || me; + while (entry) { + if (fn.call(scope, entry.key, entry.value, length) === false) { + break; + } + entry = reverse ? entry.prev : entry.next; + } + return me; + }, + /** + * @private + */ + findKey: function(value) { + var key, + map = this.map; + for (key in map) { + // Attention. Differs from subclass in that this compares the value property + // of the entry. + if (map.hasOwnProperty(key) && map[key].value === value) { + return key; + } + } + return undefined; + }, + /** + * Performs a shallow copy on this haLruCachesh. + * @return {Ext.util.HashMap} The new hash object. + */ + clone: function() { + var newCache = new this.self(this.initialConfig), + map = this.map, + key; + newCache.suspendEvents(); + for (key in map) { + if (map.hasOwnProperty(key)) { + newCache.add(key, map[key].value); + } + } + newCache.resumeEvents(); + return newCache; + }, + /** + * Purge the least recently used entries if the maxSize has been exceeded. + */ + prune: function() { + var me = this, + max = me.getMaxSize(), + purgeCount = max ? (me.length - max) : 0; + if (purgeCount > 0) { + for (; me.first && purgeCount; purgeCount--) { + me.removeAtKey(me.first.key); + } + } + } +}); +/** + * @method containsKey + * @private + */ +/** + * @method contains + * @private + */ +/** + * @method getKeys + * @private + */ +/** + * @method getValues + * @private + */ + +/** + * Provides searching of Components within Ext.ComponentManager (globally) or a specific + * Ext.container.Container on the document with a similar syntax to a CSS selector. + * Returns Array of matching Components, or empty Array. + * + * ## Basic Component lookup + * + * Components can be retrieved by using their {@link Ext.Component xtype}: + * + * - `component` + * - `gridpanel` + * + * Matching by `xtype` matches inherited types, so in the following code, the previous field + * *of any type which inherits from `TextField`* will be found: + * + * prevField = myField.previousNode('textfield'); + * + * To match only the exact type, pass the "shallow" flag by adding `(true)` to xtype + * (See Component's {@link Ext.Component#isXType isXType} method): + * + * prevTextField = myField.previousNode('textfield(true)'); + * + * You can search Components by their `id` or `itemId` property, prefixed with a #: + * + * #myContainer + * + * Component `xtype` and `id` or `itemId` can be used together to avoid possible + * id collisions between Components of different types: + * + * panel#myPanel + * + * When Component's `id` or `xtype` contains dots, you can escape them in your selector: + * + * my\.panel#myPanel + * + * Keep in mind that JavaScript treats the backslash character in a special way, so you + * need to escape it, too, in the actual code: + * + * var myPanel = Ext.ComponentQuery.query('my\\.panel#myPanel'); + * + * ## Traversing Component tree + * + * Components can be found by their relation to other Components. There are several + * relationship operators, mostly taken from CSS selectors: + * + * - **`E F`** All descendant Components of E that match F + * - **`E > F`** All direct children Components of E that match F + * - **`E ^ F`** All parent Components of E that match F + * + * Expressions between relationship operators are matched left to right, i.e. leftmost + * selector is applied first, then if one or more matches are found, relationship operator + * itself is applied, then next selector expression, etc. It is possible to combine + * relationship operators in complex selectors: + * + * window[title="Input form"] textfield[name=login] ^ form > button[action=submit] + * + * That selector can be read this way: Find a window with title "Input form", in that + * window find a TextField with name "login" at any depth (including subpanels and/or + * FieldSets), then find an `Ext.form.Panel` that is a parent of the TextField, and in + * that form find a direct child that is a button with custom property `action` set to + * value "submit". + * + * Whitespace on both sides of `^` and `>` operators is non-significant, i.e. can be + * omitted, but usually is used for clarity. + * + * ## Searching by Component attributes + * + * Components can be searched by their object property values (attributes). To do that, + * use attribute matching expression in square brackets: + * + * - `component[disabled]` - matches any Component that has `disabled` property with + * any truthy (non-empty, not `false`) value. + * - `panel[title="Test"]` - matches any Component that has `title` property set to + * "Test". Note that if the value does not contain spaces, the quotes are optional. + * + * Attributes can use any of the following operators to compare values: + * `=`, `!=`, `^=`, `$=`, `*=`, `%=`, `|=` and `~=`. + * + * Prefixing the attribute name with an at sign `@` means that the property must be + * the object's `ownProperty`, not a property from the prototype chain. + * + * Specifications like `[propName]` check that the property is a truthy value. To check + * that the object has an `ownProperty` of a certain name, regardless of the value use + * the form `[?propName]`. + * + * The specified value is coerced to match the type of the property found in the + * candidate Component using {@link Ext#coerce}. + * + * If you need to find Components by their `itemId` property, use the `#id` form; it will + * do the same as `[itemId=id]` but is easier to read. + * + * If you need to include a metacharacter like (, ), [, ], etc., in the query, escape it + * by prefixing it with a backslash: + * + * var component = Ext.ComponentQuery.query('[myProperty=\\[foo\\]]'); + * + * ## Attribute matching operators + * + * The '=' operator will return the results that **exactly** match the + * specified object property (attribute): + * + * Ext.ComponentQuery.query('panel[cls=my-cls]'); + * + * Will match the following Component: + * + * Ext.create('Ext.window.Window', { + * cls: 'my-cls' + * }); + * + * But will not match the following Component, because 'my-cls' is one value + * among others: + * + * Ext.create('Ext.panel.Panel', { + * cls: 'foo-cls my-cls bar-cls' + * }); + * + * You can use the '~=' operator instead, it will return Components with + * the property that **exactly** matches one of the whitespace-separated + * values. This is also true for properties that only have *one* value: + * + * Ext.ComponentQuery.query('panel[cls~=my-cls]'); + * + * Will match both Components: + * + * Ext.create('Ext.panel.Panel', { + * cls: 'foo-cls my-cls bar-cls' + * }); + * + * Ext.create('Ext.window.Window', { + * cls: 'my-cls' + * }); + * + * Generally, '=' operator is more suited for object properties other than + * CSS classes, while '~=' operator will work best with properties that + * hold lists of whitespace-separated CSS classes. + * + * The '^=' operator will return Components with specified attribute that + * start with the passed value: + * + * Ext.ComponentQuery.query('panel[title^=Sales]'); + * + * Will match the following Component: + * + * Ext.create('Ext.panel.Panel', { + * title: 'Sales estimate for Q4' + * }); + * + * The '$=' operator will return Components with specified properties that + * end with the passed value: + * + * Ext.ComponentQuery.query('field[fieldLabel$=name]'); + * + * Will match the following Component: + * + * Ext.create('Ext.form.field.Text', { + * fieldLabel: 'Enter your name' + * }); + * + * The '/=' operator will return Components with specified properties that + * match the passed regular expression: + * + * Ext.ComponentQuery.query('button[action/="edit|save"]'); + * + * Will match the following Components with a custom `action` property: + * + * Ext.create('Ext.button.Button', { + * action: 'edit' + * }); + * + * Ext.create('Ext.button.Button', { + * action: 'save' + * }); + * + * When you need to use meta characters like [], (), etc. in your query, make sure + * to escape them with back slashes: + * + * Ext.ComponentQuery.query('panel[title="^Sales for Q\\[1-4\\]"]'); + * + * The following test will find panels with their `ownProperty` collapsed being equal to + * `false`. It will **not** match a collapsed property from the prototype chain. + * + * Ext.ComponentQuery.query('panel[@collapsed=false]'); + * + * Member expressions from candidate Components may be tested. If the expression returns + * a *truthy* value, the candidate Component will be included in the query: + * + * var disabledFields = myFormPanel.query("{isDisabled()}"); + * + * Such expressions are executed in Component's context, and the above expression is + * similar to running this snippet for every Component in your application: + * + * if (component.isDisabled()) { + * matches.push(component); + * } + * + * It is important to use only methods that are available in **every** Component instance + * to avoid run time exceptions. If you need to match your Components with a custom + * condition formula, you can augment `Ext.Component` to provide custom matcher that + * will return `false` by default, and override it in your custom classes: + * + * Ext.define('My.Component', { + * override: 'Ext.Component', + * myMatcher: function() { return false; } + * }); + * + * Ext.define('My.Panel', { + * extend: 'Ext.panel.Panel', + * requires: ['My.Component'], // Ensure that Component override is applied + * myMatcher: function(selector) { + * return selector === 'myPanel'; + * } + * }); + * + * After that you can use a selector with your custom matcher to find all instances + * of `My.Panel`: + * + * Ext.ComponentQuery.query("{myMatcher('myPanel')}"); + * + * However if you really need to use a custom matcher, you may find it easier to implement + * a custom Pseudo class instead (see below). + * + * ## Conditional matching + * + * Attribute matchers can be combined to select only Components that match **all** + * conditions (logical AND operator): + * + * Ext.ComponentQuery.query('panel[cls~=my-cls][floating=true][title$="sales data"]'); + * + * E.g., the query above will match only a Panel-descended Component that has 'my-cls' + * CSS class *and* is floating *and* with a title that ends with "sales data". + * + * Expressions separated with commas will match any Component that satisfies + * *either* expression (logical OR operator): + * + * Ext.ComponentQuery.query('field[fieldLabel^=User], field[fieldLabel*=password]'); + * + * E.g., the query above will match any field with field label starting with "User", + * *or* any field that has "password" in its label. + * + * If you need to include a comma in an attribute matching expression, escape it with a + * backslash: + * + * Ext.ComponentQuery.query('field[fieldLabel^="User\\, foo"], field[fieldLabel*=password]'); + * + * ## Pseudo classes + * + * Pseudo classes may be used to filter results in the same way as in + * {@link Ext.dom.Query}. There are five default pseudo classes: + * + * * `not` Negates a selector. + * * `first` Filters out all except the first matching item for a selector. + * * `last` Filters out all except the last matching item for a selector. + * * `focusable` Filters out all except Components which by definition and configuration are + * potentially able to receieve focus, and can be focused at this time. Component can be + * focused when it is rendered, visible, and not disabled. Some Components can be focusable + * even when disabled (e.g. Menu items) via their parent Container configuration. + * Containers such as Panels generally are not focusable by themselves but can use + * focus delegation (`defaultFocus` config). Some Containers such as Menus and Windows + * are focusable by default. + * * `canfocus` Filters out all except Components which are curently able to receieve focus. + * That is, they are defined and configured focusable, and they are also visible and enabled. + * Note that this selector intentionally bypasses some checks done by `focusable` selector + * and works in a subtly different way. It is used internally by the framework and is not + * a replacement for `:focusable` selector. + * * `nth-child` Filters Components by ordinal position in the selection. + * * `scrollable` Filters out all except Components which are scrollable. + * * `visible` Filters out hidden Components. May test deep visibility using `':visible(true)'` + * + * These pseudo classes can be used with other matchers or without them: + * + * // Select first direct child button in any panel + * Ext.ComponentQuery.query('panel > button:first'); + * + * // Select last field in Profile form + * Ext.ComponentQuery.query('form[title=Profile] field:last'); + * + * // Find first focusable Component in a panel and focus it + * panel.down(':canfocus').focus(); + * + * // Select any field that is not hidden in a form + * form.query('field:not(hiddenfield)'); + * + * // Find last scrollable Component and reset its scroll positions. + * tabpanel.down(':scrollable[hideMode=display]:last').getScrollable().scrollTo(0, 0); + * + * Pseudo class `nth-child` can be used to find any child Component by its + * position relative to its siblings. This class' handler takes one argument + * that specifies the selection formula as `Xn` or `Xn+Y`: + * + * // Find every odd field in a form + * form.query('field:nth-child(2n+1)'); // or use shortcut: :nth-child(odd) + * + * // Find every even field in a form + * form.query('field:nth-child(2n)'); // or use shortcut: :nth-child(even) + * + * // Find every 3rd field in a form + * form.query('field:nth-child(3n)'); + * + * **Note:** The `nth-child` selector returns 1-based result sets. + * + * Pseudo classes can be combined to further filter the results, e.g., in the + * form example above we can modify the query to exclude hidden fields: + * + * // Find every 3rd non-hidden field in a form + * form.query('field:not(hiddenfield):nth-child(3n)'); + * + * Note that when combining pseudo classes, whitespace is significant, i.e. + * there should be no spaces between pseudo classes. This is a common mistake; + * if you accidentally type a space between `field` and `:not`, the query + * will not return any result because it will mean "find *field's children + * Components* that are not hidden fields...". + * + * ## Custom pseudo classes + * + * It is possible to define your own custom pseudo classes. In fact, a + * pseudo class is just a property in `Ext.ComponentQuery.pseudos` object + * that defines pseudo class name (property name) and pseudo class handler + * (property value): + * + * // Function receives array and returns a filtered array. + * Ext.ComponentQuery.pseudos.invalid = function(items) { + * var i = 0, l = items.length, c, result = []; + * for (; i < l; i++) { + * if (!(c = items[i]).isValid()) { + * result.push(c); + * } + * } + * return result; + * }; + * + * var invalidFields = myFormPanel.query('field:invalid'); + * if (invalidFields.length) { + * invalidFields[0].getEl().scrollIntoView(myFormPanel.body); + * for (var i = 0, l = invalidFields.length; i < l; i++) { + * invalidFields[i].getEl().frame("red"); + * } + * } + * + * Pseudo class handlers can be even more flexible, with a selector + * argument used to define the logic: + * + * // Handler receives array of itmes and selector in parentheses + * Ext.ComponentQuery.pseudos.titleRegex = function(components, selector) { + * var i = 0, l = components.length, c, result = [], regex = new RegExp(selector); + * for (; i < l; i++) { + * c = components[i]; + * if (c.title && regex.test(c.title)) { + * result.push(c); + * } + * } + * return result; + * } + * + * var salesTabs = tabPanel.query('panel:titleRegex("sales\\s+for\\s+201[123]")'); + * + * Be careful when using custom pseudo classes with MVC Controllers: when + * you use a pseudo class in Controller's `control` or `listen` component + * selectors, the pseudo class' handler function will be called very often + * and may slow down your application significantly. A good rule of thumb + * is to always specify Component xtype with the pseudo class so that the + * handlers are only called on Components that you need, and try to make + * the condition checks as cheap in terms of execution time as possible. + * Note how in the example above, handler function checks that Component + * *has* a title first, before running regex test on it. + * + * ## Query examples + * + * Queries return an array of Components. Here are some example queries: + * + * // retrieve all Ext.Panels in the document by xtype + * var panelsArray = Ext.ComponentQuery.query('panel'); + * + * // retrieve all Ext.Panels within the container with an id myCt + * var panelsWithinmyCt = Ext.ComponentQuery.query('#myCt panel'); + * + * // retrieve all direct children which are Ext.Panels within myCt + * var directChildPanel = Ext.ComponentQuery.query('#myCt > panel'); + * + * // retrieve all grids or trees + * var gridsAndTrees = Ext.ComponentQuery.query('gridpanel, treepanel'); + * + * // Focus first Component + * myFormPanel.child(':canfocus').focus(); + * + * // Retrieve every odd text field in a form + * myFormPanel.query('textfield:nth-child(odd)'); + * + * // Retrieve every even field in a form, excluding hidden fields + * myFormPanel.query('field:not(hiddenfield):nth-child(even)'); + * + * // Retrieve every scrollable in a tabpanel + * tabpanel.query(':scrollable'); + * + * For easy access to queries based from a particular Container see the + * {@link Ext.container.Container#query}, {@link Ext.container.Container#down} and + * {@link Ext.container.Container#child} methods. Also see + * {@link Ext.Component#up}. + */ +Ext.define('Ext.ComponentQuery', { + singleton: true +}, function() { + var cq = this, + queryOperators = Ext.util.Operators, + nthRe = /(\d*)n\+?(\d*)/, + nthRe2 = /\D/, + stripLeadingSpaceRe = /^(\s)+/, + unescapeRe = /\\(.)/g, + regexCache = new Ext.util.LruCache({ + maxSize: 100 + }), + // A function source code pattern with a placeholder which accepts an expression which + // yields a truth value when applied as a member on each item in the passed array. + /* eslint-disable indent */ + filterFnPattern = [ + 'var r = [],', + 'i = 0,', + 'it = items,', + 'l = it.length,', + 'c;', + 'for (; i < l; i++) {', + 'c = it[i];', + 'if (c.{0}) {', + 'r.push(c);', + '}', + '}', + 'return r;' + ].join(''), + /* eslint-enable indent */ + filterItems = function(items, operation) { + // Argument list for the operation is [ itemsArray, operationArg1, operationArg2...] + // The operation's method loops over each item in the candidate array and + // returns an array of items which match its criteria + return operation.method.apply(this, [ + items + ].concat(operation.args)); + }, + getItems = function(items, mode) { + var result = [], + i = 0, + length = items.length, + candidate, + deep = mode !== '>'; + for (; i < length; i++) { + candidate = items[i]; + if (candidate.getRefItems) { + result = result.concat(candidate.getRefItems(deep)); + } + } + return result; + }, + getAncestors = function(items) { + var result = [], + i = 0, + length = items.length, + candidate; + for (; i < length; i++) { + candidate = items[i]; + while (!!(candidate = candidate.getRefOwner())) { + result.push(candidate); + } + } + return result; + }, + // Filters the passed candidate array and returns only items which match the passed xtype + filterByXType = function(items, xtype, shallow) { + if (xtype === '*') { + return items.slice(); + } else { + /* eslint-disable-next-line vars-on-top */ + var result = [], + i = 0, + length = items.length, + candidate; + for (; i < length; i++) { + candidate = items[i]; + if (!candidate.destroyed && candidate.isXType(xtype, shallow)) { + result.push(candidate); + } + } + return result; + } + }, + // Filters the passed candidate array and returns only items which have the specified + // property match + filterByAttribute = function(items, property, operator, compareTo) { + var result = [], + length = items.length, + mustBeOwnProperty, presenceOnly, candidate, propValue, config, i, j, propLen; + // Prefixing property name with an @ means that the property must be in the candidate, + // not in its prototype + if (property.charAt(0) === '@') { + mustBeOwnProperty = true; + property = property.substr(1); + } + if (property.charAt(0) === '?') { + mustBeOwnProperty = true; + presenceOnly = true; + property = property.substr(1); + } + for (i = 0; i < length; i++) { + candidate = items[i]; + // If the candidate is a product of the Ext class system, then + // use the configurator to call getters to access the property. + // CQ can be used to filter raw Objects. + config = candidate.self && candidate.self.getConfigurator && candidate.self.$config.configs[property]; + if (config) { + propValue = candidate[config.names.get](); + } else if (mustBeOwnProperty && !candidate.hasOwnProperty(property)) { + + continue; + } else { + propValue = candidate[property]; + } + if (presenceOnly) { + result.push(candidate); + } + // implies property is an array, and we must compare value against each element. + else if (operator === '~=') { + if (propValue) { + // We need an array + if (!Ext.isArray(propValue)) { + propValue = propValue.split(' '); + } + for (j = 0 , propLen = propValue.length; j < propLen; j++) { + /* eslint-disable-next-line max-len */ + if (queryOperators[operator](Ext.coerce(propValue[j], compareTo), compareTo)) { + result.push(candidate); + break; + } + } + } + } else if (operator === '/=') { + if (propValue != null && compareTo.test(propValue)) { + result.push(candidate); + } + } + /* eslint-disable-next-line max-len */ + else if (!compareTo ? !!propValue : queryOperators[operator](Ext.coerce(propValue, compareTo), compareTo)) { + result.push(candidate); + } + } + return result; + }, + // Filters the passed candidate array and returns only items which have the specified + // itemId or id + filterById = function(items, id, idOnly) { + var result = [], + i = 0, + length = items.length, + candidate, check; + for (; i < length; i++) { + candidate = items[i]; + check = idOnly ? candidate.id : candidate.getItemId(); + if (check === id) { + result.push(candidate); + } + } + return result; + }, + // Filters the passed candidate array and returns only items which the named pseudo class + // matcher filters in + filterByPseudo = function(items, name, value) { + return cq.pseudos[name](items, value); + }, + // Determines leading mode + // > for direct child, and ^ to switch to ownerCt axis + /* eslint-disable-next-line no-useless-escape */ + modeRe = /^(\s?([>\^])\s?|\s|$)/, + // Matches a token with possibly (true|false) appended for the "shallow" parameter + /* eslint-disable-next-line no-useless-escape */ + tokenRe = /^(#)?((?:\\\.|[\w\-])+|\*)(?:\((true|false)\))?/, + matchers = [ + { + // Checks for .xtype with possibly (true|false) appended for the "shallow" parameter + /* eslint-disable-next-line no-useless-escape */ + re: /^\.((?:\\\.|[\w\-])+)(?:\((true|false)\))?/, + method: filterByXType, + argTransform: function(args) { + var selector = args[0]; + Ext.log.warn('"' + selector + '" ComponentQuery selector style is deprecated,' + ' use "' + selector.replace(/^\./, '') + '" without the leading dot instead'); + if (args[1] !== undefined) { + args[1] = args[1].replace(unescapeRe, '$1'); + } + return args.slice(1); + } + }, + { + // Allow [@attribute] to check truthy ownProperty + // Allow [?attribute] to check for presence of ownProperty + // Allow [$attribute] + // Checks for @|?|$ -> word/hyphen chars -> any special attribute selector characters + // before the '=', etc. It strips out whitespace. + // For example: + // [attribute=value], [attribute^=value], [attribute$=value], [attribute*=value], + // [attribute~=value], [attribute%=value], [attribute!=value] + /* eslint-disable-next-line max-len, no-useless-escape */ + re: /^(?:\[((?:[@?$])?[\w\-]*)\s*(?:([\^$*~%!\/]?=)\s*(['"])?((?:\\\]|.)*?)\3)?(?!\\)\])/, + method: filterByAttribute, + argTransform: function(args) { + var selector = args[0], + property = args[1], + operator = args[2], + compareTo = args[4], + compareRe; + // Unescape the attribute value matcher first + if (compareTo !== undefined) { + compareTo = compareTo.replace(unescapeRe, '$1'); + /* eslint-disable-next-line vars-on-top */ + var format = Ext.String.format, + msg = "ComponentQuery selector '{0}' has an unescaped ({1}) character " + "at the {2} of the attribute value pattern. Usually that indicates " + "an error where the opening quote is not followed by the closing " + "quote. If you need to match a ({1}) character at the {2} of the " + "attribute value, escape the quote character in your pattern: " + "(\\{1})", + match; + /* eslint-disable-next-line no-cond-assign */ + if (match = /^(['"]).*?[^'"]$/.exec(compareTo)) { + Ext.log.warn(format(msg, selector, match[1], 'beginning')); + } + /* eslint-disable-next-line no-cond-assign */ + else if (match = /^[^'"].*?(['"])$/.exec(compareTo)) { + Ext.log.warn(format(msg, selector, match[1], 'end')); + } + } + if (operator === '/=') { + compareRe = regexCache.get(compareTo); + if (compareRe) { + compareTo = compareRe; + } else { + compareTo = regexCache.add(compareTo, new RegExp(compareTo)); + } + } + return [ + property, + operator, + compareTo + ]; + } + }, + { + // checks for #cmpItemId + /* eslint-disable-next-line no-useless-escape */ + re: /^#((?:\\\.|[\w\-])+)/, + method: filterById + }, + { + // checks for :() + /* eslint-disable-next-line no-useless-escape */ + re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/, + method: filterByPseudo, + argTransform: function(args) { + if (args[2] !== undefined) { + args[2] = args[2].replace(unescapeRe, '$1'); + } + return args.slice(1); + } + }, + { + // checks for {} + /* eslint-disable-next-line no-useless-escape */ + re: /^(?:\{([^\}]+)\})/, + method: filterFnPattern + } + ]; + // Internal class Ext.ComponentQuery.Query + cq.Query = Ext.extend(Object, { + constructor: function(cfg) { + cfg = cfg || {}; + Ext.apply(this, cfg); + }, + // Executes this Query upon the selected root. + // The root provides the initial source of candidate Component matches which are + // progressively filtered by iterating through this Query's operations cache. + // If no root is provided, all registered Components are searched via the ComponentManager. + // root may be a Container who's descendant Components are filtered + // root may be a Component with an implementation of getRefItems which provides some nested + // Components such as the docked items within a Panel. + // root may be an array of candidate Components to filter using this Query. + execute: function(root) { + var operations = this.operations, + result = [], + op, i, len; + for (i = 0 , len = operations.length; i < len; i++) { + op = operations[i]; + result = result.concat(this._execute(root, op)); + } + return result; + }, + _execute: function(root, operations) { + var i = 0, + length = operations.length, + operation, workingItems; + // no root, use all Components in the document + if (!root) { + workingItems = Ext.ComponentManager.getAll(); + } + // Root is an iterable object like an Array, or system Collection, eg HtmlCollection + else if (Ext.isIterable(root)) { + workingItems = root; + } + // Root is a MixedCollection + else if (root.isMixedCollection) { + workingItems = root.items; + } + // We are going to loop over our operations and take care of them + // one by one. + for (; i < length; i++) { + operation = operations[i]; + // The mode operation requires some custom handling. + // All other operations essentially filter down our current + // working items, while mode replaces our current working + // items by getting children from each one of our current + // working items. The type of mode determines the type of + // children we get. (e.g. > only gets direct children) + if (operation.mode === '^') { + workingItems = getAncestors(workingItems || [ + root + ]); + } else if (operation.mode) { + workingItems = getItems(workingItems || [ + root + ], operation.mode); + } else { + workingItems = filterItems(workingItems || getItems([ + root + ]), operation); + } + // If this is the last operation, it means our current working + // items are the final matched items. Thus return them! + if (i === length - 1) { + return workingItems; + } + } + return []; + }, + is: function(component, root) { + var operations = this.operations, + result = false, + len = operations.length, + op, i; + if (len === 0) { + return true; + } + for (i = 0; i < len; i++) { + op = operations[i]; + result = this._is(component, root, op); + if (result) { + return result; + } + } + return false; + }, + _is: function(component, root, operations) { + var len = operations.length, + active = [ + component + ], + operation, i, j, mode, items, item; + // Loop backwards, since we're going up the hierarchy + for (i = len - 1; i >= 0; --i) { + operation = operations[i]; + mode = operation.mode; + // Traversing hierarchy + if (mode) { + if (mode === '^') { + active = getItems(active, ' '); + } else if (mode === '>') { + items = []; + for (j = 0 , len = active.length; j < len; ++j) { + item = active[j].getRefOwner(); + if (item) { + items.push(item); + } + } + active = items; + } else { + active = getAncestors(active); + } + } else { + active = filterItems(active, operation); + } + // After traversing the hierarchy, if we have no items, jump out + if (active.length === 0) { + return false; + } + } + // We don't push these on as operations because we don't want to mutate the + // array, but this is essentially a continuation of the loop above. + if (root) { + if (!mode) { + // Last operation wasn't a mode operation, so navigate up to find + // ancestors + active = getAncestors(active); + } + if (active.length > 0) { + // If we have active items, check the root exists there to ensure we're + // part of the tree + active = filterItems(active, { + method: filterById, + args: [ + root.id, + true + ] + }); + } + if (active.length === 0) { + return false; + } + } + return true; + }, + getMatches: function(components, operations) { + var len = operations.length, + i; + for (i = 0; i < len; ++i) { + components = filterItems(components, operations[i]); + // Performance enhancement, if we have nothing, we can + // never add anything new, so jump out + if (components.length === 0) { + break; + } + } + return components; + }, + isMultiMatch: function() { + return this.operations.length > 1; + } + }); + Ext.apply(cq, { + /** + * @private + * Cache of selectors and matching ComponentQuery.Query objects + */ + cache: new Ext.util.LruCache({ + maxSize: 100 + }), + /** + * @private + * Cache of pseudo class filter functions + */ + pseudos: { + not: function(components, selector) { + var i = 0, + length = components.length, + results = [], + index = -1, + component; + for (; i < length; ++i) { + component = components[i]; + if (!cq.is(component, selector)) { + results[++index] = component; + } + } + return results; + }, + first: function(components) { + var ret = []; + if (components.length > 0) { + ret.push(components[0]); + } + return ret; + }, + last: function(components) { + var len = components.length, + ret = []; + if (len > 0) { + ret.push(components[len - 1]); + } + return ret; + }, + // This filters for components which by definition and configuration are + // theoretically focusable. It does not take into account the current app state. + focusable: function(cmps) { + var len = cmps.length, + results = [], + i = 0, + c; + for (; i < len; i++) { + c = cmps[i]; + if (c.isFocusable && c.isFocusable()) { + results.push(c); + } + } + return results; + }, + // This filters for components which are currently able to recieve focus. + canfocus: function(cmps, value) { + var len = cmps.length, + results = [], + i = 0, + c; + for (; i < len; i++) { + c = cmps[i]; + if (c.canFocus && c.canFocus(false, value)) { + results.push(c); + } + } + return results; + }, + "nth-child": function(c, a) { + var result = [], + m, f, i, len, n, nodeIndex; + m = nthRe.exec(a === "even" && "2n" || a === "odd" && "2n+1" || !nthRe2.test(a) && "n+" + a || a); + f = (m[1] || 1) - 0; + len = m[2] - 0; + /* eslint-disable-next-line no-cond-assign */ + for (i = 0; n = c[i]; i++) { + nodeIndex = i + 1; + if (f === 1) { + if (len === 0 || nodeIndex === len) { + result.push(n); + } + } else if ((nodeIndex + len) % f === 0) { + result.push(n); + } + } + return result; + }, + scrollable: function(cmps) { + var len = cmps.length, + results = [], + i = 0, + c; + for (; i < len; i++) { + c = cmps[i]; + // Note that modern toolkit prefixes with an underscore. + if (c.scrollable || c._scrollable) { + results.push(c); + } + } + return results; + }, + visible: function(cmps, deep) { + var len = cmps.length, + results = [], + i = 0, + c; + deep = deep === 'true'; + for (; i < len; i++) { + c = cmps[i]; + // Note that modern toolkit prefixes with an underscore. + if (c.isVisible(deep)) { + results.push(c); + } + } + return results; + } + }, + /** + * Returns an array of matched Components from within the passed root object. + * + * This method filters returned Components in a similar way to how CSS selector based DOM + * queries work using a textual selector string. + * + * See class summary for details. + * + * @param {String} selector The selector string to filter returned Components. + * @param {Ext.container.Container} [root] The Container within which to perform the query. + * If omitted, all Components within the document are included in the search. + * + * This parameter may also be an array of Components to filter according to the selector. + * @return {Ext.Component[]} The matched Components. + * + * @member Ext.ComponentQuery + */ + query: function(selector, root) { + // An empty query will match every Component + if (!selector) { + return Ext.ComponentManager.getAll(); + } + /* eslint-disable-next-line vars-on-top */ + var results = [], + noDupResults = [], + dupMatcher = {}, + query = cq.cache.get(selector), + resultsLn, cmp, i; + if (!query) { + query = cq.cache.add(selector, cq.parse(selector)); + } + results = query.execute(root); + // multiple selectors, potential to find duplicates + // lets filter them out. + if (query.isMultiMatch()) { + resultsLn = results.length; + for (i = 0; i < resultsLn; i++) { + cmp = results[i]; + if (!dupMatcher[cmp.id]) { + noDupResults.push(cmp); + dupMatcher[cmp.id] = true; + } + } + results = noDupResults; + } + return results; + }, + /** + * Traverses the tree rooted at the passed root in pre-order mode, calling the passed + * function on the nodes at each level. That is the function is called upon each node + * **before** being called on its children). + * + * For an object to be queryable, it must implement the `getRefItems` method which returns + * all immediate child items. + * + * This method is used at each level down the cascade. Currently + * {@link Ext.Component Component}s and {@link Ext.data.TreeModel TreeModel}s are queryable. + * + * If you have tree-structured data, you can make your nodes queryable, and use + * ComponentQuery on them. + * + * @param {Object} selector A ComponentQuery selector used to filter candidate nodes before + * calling the function. An empty string matches any node. + * @param {String} root The root queryable object to start from. + * @param {Function} fn The function to call. Return `false` to abort the traverse. + * @param {Object} fn.node The node being visited. + * @param {Object} [scope] The context (`this` reference) in which the function is executed. + * @param {Array} [extraArgs] A set of arguments to be appended to the function's argument + * list to pass down extra data known to the caller **after** the node being visited. + */ + visitPreOrder: function(selector, root, fn, scope, extraArgs) { + cq._visit(true, selector, root, fn, scope, extraArgs); + }, + /** + * Traverses the tree rooted at the passed root in post-order mode, calling the passed + * function on the nodes at each level. That is the function is called upon each node + * **after** being called on its children). + * + * For an object to be queryable, it must implement the `getRefItems` method which returns + * all immediate child items. + * + * This method is used at each level down the cascade. Currently + * {@link Ext.Component Component}s and {@link Ext.data.TreeModel TreeModel}s are queryable. + * + * If you have tree-structured data, you can make your nodes queryable, and use + * ComponentQuery on them. + * + * @param {Object} selector A ComponentQuery selector used to filter candidate nodes + * before calling the function. An empty string matches any node. + * @param {String} root The root queryable object to start from. + * @param {Function} fn The function to call. Return `false` to abort the traverse. + * @param {Object} fn.node The node being visited. + * @param {Object} [scope] The context (`this` reference) in which the function is executed. + * @param {Array} [extraArgs] A set of arguments to be appended to the function's argument + * list to pass down extra data known to the caller **after** the node being visited. + */ + visitPostOrder: function(selector, root, fn, scope, extraArgs) { + cq._visit(false, selector, root, fn, scope, extraArgs); + }, + /** + * @private + * Visit implementation which handles both preOrder and postOrder modes. + */ + _visit: function(preOrder, selector, root, fn, scope, extraArgs) { + var query = cq.cache.get(selector), + callArgs = [ + root + ], + children, + len = 0, + i, rootMatch; + if (!query) { + query = cq.cache.add(selector, cq.parse(selector)); + } + rootMatch = query.is(root); + if (root.getRefItems) { + children = root.getRefItems(); + len = children.length; + } + // append optional extraArgs + if (extraArgs) { + Ext.Array.push(callArgs, extraArgs); + } + if (preOrder) { + if (rootMatch) { + if (fn.apply(scope || root, callArgs) === false) { + return false; + } + } + } + for (i = 0; i < len; i++) { + /* eslint-disable-next-line max-len */ + if (cq._visit.call(cq, preOrder, selector, children[i], fn, scope, extraArgs) === false) { + return false; + } + } + if (!preOrder) { + if (rootMatch) { + if (fn.apply(scope || root, callArgs) === false) { + return false; + } + } + } + }, + /** + * Tests whether the passed Component matches the selector string. + * An empty selector will always match. + * + * @param {Ext.Component} component The Component to test + * @param {String/Function} selector The selector string to test against. + * Or a filter function which returns `true` if the component matches. + * @param {Ext.Component} [root=null] The root component. + * @return {Boolean} True if the Component matches the selector. + * @member Ext.ComponentQuery + */ + is: function(component, selector, root) { + var query; + if (!selector) { + return true; + } + if (typeof selector === 'function') { + return selector(component); + } else { + query = cq.cache.get(selector); + if (!query) { + query = cq.cache.add(selector, cq.parse(selector)); + } + return query.is(component, root); + } + }, + parse: function(selector) { + var operations = [], + selectors, sel, i, len; + selectors = Ext.splitAndUnescape(selector, ','); + for (i = 0 , len = selectors.length; i < len; i++) { + // Trim the whitespace as the parser expects it. + sel = Ext.String.trim(selectors[i]); + // Usually, a dangling comma at the end of a selector means a typo. + // In that case, the last sel value will be an empty string; the parser + // will silently ignore it which is not good, so we throw an error here. + if (sel === '') { + Ext.raise('Invalid ComponentQuery selector: ""'); + } + operations.push(cq._parse(sel)); + } + // Now that we have all our operations in an array, we are going + // to create a new Query using these operations. + return new cq.Query({ + operations: operations + }); + }, + _parse: function(selector) { + var operations = [], + trim = Ext.String.trim, + length = matchers.length, + lastSelector, tokenMatch, token, matchedChar, modeMatch, selectorMatch, transform, i, matcher, method, args; + // We are going to parse the beginning of the selector over and + // over again, slicing off the selector any portions we converted into an + // operation, until it is an empty string. + while (selector && lastSelector !== selector) { + lastSelector = selector; + // First we check if we are dealing with a token like #, * or an xtype + tokenMatch = selector.match(tokenRe); + if (tokenMatch) { + matchedChar = tokenMatch[1]; + token = trim(tokenMatch[2]).replace(unescapeRe, '$1'); + // If the token is prefixed with a # we push a filterById operation to our stack + if (matchedChar === '#') { + operations.push({ + method: filterById, + args: [ + token + ] + }); + } else // If the token is a * or an xtype string, we push a filterByXType + // operation to the stack. + { + operations.push({ + method: filterByXType, + args: [ + token, + Boolean(tokenMatch[3]) + ] + }); + } + // Now we slice of the part we just converted into an operation + selector = selector.replace(tokenMatch[0], '').replace(stripLeadingSpaceRe, '$1'); + } + // If the next part of the query is not a space or > or ^, it means we + // are going to check for more things that our current selection + // has to comply to. + while (!(modeMatch = selector.match(modeRe))) { + // Lets loop over each type of matcher and execute it + // on our current selector. + for (i = 0; selector && i < length; i++) { + matcher = matchers[i]; + selectorMatch = selector.match(matcher.re); + method = matcher.method; + transform = matcher.argTransform; + // If we have a match, add an operation with the method + // associated with this matcher, and pass the regular + // expression matches are arguments to the operation. + if (selectorMatch) { + // Transform function will do unescaping and additional checks + if (transform) { + args = transform(selectorMatch); + } else { + args = selectorMatch.slice(1); + } + operations.push({ + method: Ext.isString(matcher.method) ? // Turn a string method into a function by formatting the + // string with our selector matche expression + // A new method is created for different match expressions, + // eg {id=='textfield-1024'} + // Every expression may be different in different selectors. + /* eslint-disable-next-line max-len */ + Ext.functionFactory('items', Ext.String.format.apply(Ext.String, [ + method + ].concat(selectorMatch.slice(1)))) : matcher.method, + args: args + }); + selector = selector.replace(selectorMatch[0], '').replace(stripLeadingSpaceRe, '$1'); + break; + } + // Break on match + // Exhausted all matches: It's an error + if (i === (length - 1)) { + Ext.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"'); + } + } + } + // Now we are going to check for a mode change. This means a space + // or a > to determine if we are going to select all the children + // of the currently matched items, or a ^ if we are going to use the + // ownerCt axis as the candidate source. + if (modeMatch[1]) { + // Assignment, and test for truthiness! + operations.push({ + mode: modeMatch[2] || modeMatch[1] + }); + // When we have consumed the mode character, clean up leading spaces + selector = selector.replace(modeMatch[0], '').replace(stripLeadingSpaceRe, ''); + } + } + return operations; + } + }); + /** + * Same as {@link Ext.ComponentQuery#query}. + * @param {String} selector The selector string to filter returned Components. + * @param {Ext.container.Container} [root] The Container within which to perform the query. + * If omitted, all Components within the document are included in the search. + * + * This parameter may also be an array of Components to filter according to the selector. + * @return {Ext.Component[]} The matched Components. + * @method all + * @member Ext + */ + Ext.all = function() { + return cq.query.apply(cq, arguments); + }; + /** + * Returns the first match to the given component query. + * See {@link Ext.ComponentQuery#query}. + * @param {String} selector The selector string to filter returned Component. + * @param {Ext.container.Container} [root] The Container within which to perform the query. + * If omitted, all Components within the document are included in the search. + * + * This parameter may also be an array of Components to filter according to the selector. + * @return {Ext.Component} The first matched Component or `null`. + * @method first + * @member Ext + */ + Ext.first = function() { + var matches = cq.query.apply(cq, arguments); + return (matches && matches[0]) || null; + }; +}); + +/** + * @private + */ +Ext.define('Ext.Evented', { + alternateClassName: 'Ext.EventedBase', + mixins: [ + Ext.mixin.Observable + ], + initialized: false, + constructor: function(config) { + // Base constructor is overriden for testing + this.callParent(); + this.mixins.observable.constructor.call(this, config); + this.initialized = true; + }, + onClassExtended: function(cls, data) { + if (!data.hasOwnProperty('eventedConfig')) { + return; + } + /* eslint-disable-next-line vars-on-top */ + var config = data.config, + eventedConfig = data.eventedConfig, + name, cfg; + if (config) { + Ext.applyIf(config, eventedConfig); + } else { + cls.addConfig(eventedConfig); + } + /* + * These are generated setters for eventedConfig + * + * If the component is initialized, it invokes fireAction to fire the event as well, + * which indicate something has changed. Otherwise, it just executes the action + * (happens during initialization) + * + * This is helpful when we only want the event to be fired for subsequent changes. + * Also it's a major performance improvement for instantiation when fired events + * are mostly useless since there's no listeners + */ + // TODO: Move this into Observable + for (name in eventedConfig) { + if (eventedConfig.hasOwnProperty(name)) { + cfg = Ext.Config.get(name); + data[cfg.names.set] = cfg.eventedSetter || cfg.getEventedSetter(); + } + } + } +}); + +/** + * This mixin provides a common interface for objects that can be positioned, e.g. + * {@link Ext.Component Components} and {@link Ext.dom.Element Elements} + * @private + */ +Ext.define('Ext.util.Positionable', { + mixinId: 'positionable', + _positionTopLeft: [ + 'position', + 'top', + 'left' + ], + // Stub implementation called after positioning. + // May be implemented in subclasses. Component has an implementation. + // Hardware acceleration due to the transform:translateZ(0) flickering + // when painting clipped elements. This class allows that to be turned off + // while elements are in a clipped state. + clippedCls: Ext.baseCSSPrefix + 'clipped', + afterSetPosition: Ext.emptyFn, + // *********************** + // Begin Abstract Methods + // *********************** + /** + * Gets the x,y coordinates of an element specified by the anchor position on the + * element. + * @param {Ext.dom.Element} el The element + * @param {String} [anchor='tl'] The specified anchor position. + * See {@link #alignTo} for details on supported anchor positions. + * @param {Boolean} [local] True to get the local (element top/left-relative) anchor + * position instead of page coordinates + * @param {Object} [size] An object containing the size to use for calculating anchor + * position {width: (target width), height: (target height)} (defaults to the + * element's current size) + * @return {Number[]} [x, y] An array containing the element's x and y coordinates + * @private + */ + getAnchorToXY: function() { + Ext.raise("getAnchorToXY is not implemented in " + this.$className); + }, + /** + * Returns the size of the element's borders and padding. + * @return {Object} an object with the following numeric properties + * - beforeX + * - afterX + * - beforeY + * - afterY + * @private + */ + getBorderPadding: function() { + Ext.raise("getBorderPadding is not implemented in " + this.$className); + }, + /** + * Returns the x coordinate of this element reletive to its `offsetParent`. + * @return {Number} The local x coordinate + */ + getLocalX: function() { + Ext.raise("getLocalX is not implemented in " + this.$className); + }, + /** + * Returns the x and y coordinates of this element relative to its `offsetParent`. + * @return {Number[]} The local XY position of the element + */ + getLocalXY: function() { + Ext.raise("getLocalXY is not implemented in " + this.$className); + }, + /** + * Returns the y coordinate of this element reletive to its `offsetParent`. + * @return {Number} The local y coordinate + */ + getLocalY: function() { + Ext.raise("getLocalY is not implemented in " + this.$className); + }, + /** + * Gets the current X position of the DOM element based on page coordinates. + * @return {Number} The X position of the element + */ + getX: function() { + Ext.raise("getX is not implemented in " + this.$className); + }, + /** + * Gets the current position of the DOM element based on page coordinates. + * @return {Number[]} The XY position of the element + */ + getXY: function() { + Ext.raise("getXY is not implemented in " + this.$className); + }, + /** + * Gets the current Y position of the DOM element based on page coordinates. + * @return {Number} The Y position of the element + */ + getY: function() { + Ext.raise("getY is not implemented in " + this.$className); + }, + /** + * Sets the local x coordinate of this element using CSS style. When used on an + * absolute positioned element this method is symmetrical with {@link #getLocalX}, but + * may not be symmetrical when used on a relatively positioned element. + * @param {Number} x The x coordinate. A value of `null` sets the left style to 'auto'. + * @return {Ext.util.Positionable} this + */ + setLocalX: function() { + Ext.raise("setLocalX is not implemented in " + this.$className); + }, + /** + * Sets the local x and y coordinates of this element using CSS style. When used on an + * absolute positioned element this method is symmetrical with {@link #getLocalXY}, but + * may not be symmetrical when used on a relatively positioned element. + * @param {Number/Array} x The x coordinate or an array containing [x, y]. A value of + * `null` sets the left style to 'auto' + * @param {Number} [y] The y coordinate, required if x is not an array. A value of + * `null` sets the top style to 'auto' + * @return {Ext.util.Positionable} this + */ + setLocalXY: function() { + Ext.raise("setLocalXY is not implemented in " + this.$className); + }, + /** + * Sets the local y coordinate of this element using CSS style. When used on an + * absolute positioned element this method is symmetrical with {@link #getLocalY}, but + * may not be symmetrical when used on a relatively positioned element. + * @param {Number} y The y coordinate. A value of `null` sets the top style to 'auto'. + * @return {Ext.util.Positionable} this + */ + setLocalY: function() { + Ext.raise("setLocalY is not implemented in " + this.$className); + }, + /** + * Sets the X position of the DOM element based on page coordinates. + * @param {Number} x The X position + * @return {Ext.util.Positionable} this + */ + setX: function() { + Ext.raise("setX is not implemented in " + this.$className); + }, + /** + * Sets the position of the DOM element in page coordinates. + * @param {Number[]} pos Contains X & Y [x, y] values for new position (coordinates + * are page-based) + * @return {Ext.util.Positionable} this + */ + setXY: function() { + Ext.raise("setXY is not implemented in " + this.$className); + }, + /** + * Sets the Y position of the DOM element based on page coordinates. + * @param {Number} y The Y position + * @return {Ext.util.Positionable} this + */ + setY: function() { + Ext.raise("setY is not implemented in " + this.$className); + }, + // *********************** + // End Abstract Methods + // *********************** + // TODO: currently only used by ToolTip. does this method belong here? + /** + * @private + */ + adjustForConstraints: function(xy, parent) { + var vector = this.getConstrainVector(parent, xy); + if (vector) { + xy[0] += vector[0]; + xy[1] += vector[1]; + } + return xy; + }, + /** + * Aligns the element with another element relative to the specified anchor points. If + * the other element is the document it aligns it to the viewport. The position + * parameter is optional, and can be specified in any one of the following formats: + * + * - **Blank**: Defaults to aligning the element's top-left corner to the target's + * bottom-left corner ("tl-bl"). + * - **Two anchors**: If two values from the table below are passed separated by a dash, + * the first value is used as the element's anchor point, and the second value is + * used as the target's anchor point. + * - **Two edge/offset descriptors:** An edge/offset descriptor is an edge initial + * (`t`/`r`/`b`/`l`) followed by a percentage along that side. This describes a + * point to align with a similar point in the target. So `'t0-b0'` would be + * the same as `'tl-bl'`, `'l0-r50'` would place the top left corner of this item + * halfway down the right edge of the target item. This allows more flexibility + * and also describes which two edges are considered adjacent when positioning a tip pointer. + * + * Following are all of the supported predefined anchor positions: + * + * Value Description + * ----- ----------------------------- + * tl The top left corner + * t The center of the top edge + * tr The top right corner + * l The center of the left edge + * c The center + * r The center of the right edge + * bl The bottom left corner + * b The center of the bottom edge + * br The bottom right corner + * + * You can put a '?' at the end of the alignment string to constrain the positioned element + * to the {@link Ext.Viewport Viewport}. The element will attempt to align as specified, but + * the position will be adjusted to constrain to the viewport if necessary. Note that + * the element being aligned might be swapped to align to a different position than that + * specified in order to enforce the viewport constraints. + * + * Example Usage: + * + * // align el to other-el using the default positioning + * // ("tl-bl", non-constrained) + * el.alignTo("other-el"); + * + * // align the top left corner of el with the top right corner of other-el + * // (constrained to viewport) + * el.alignTo("other-el", "tl-tr?"); + * + * // align the bottom right corner of el with the center left edge of other-el + * el.alignTo("other-el", "br-l?"); + * + * // align the center of el with the bottom left corner of other-el and + * // adjust the x position by -6 pixels (and the y position by 0) + * el.alignTo("other-el", "c-bl", [-6, 0]); + * + * // align the 25% point on the bottom edge of this el + * // with the 75% point on the top edge of other-el. + * el.alignTo("other-el", 'b25-t75'); + * + * @param {Ext.util.Positionable/HTMLElement/String} element The Positionable, + * HTMLElement, or id of the element to align to. + * @param {String} [position="tl-bl?"] The position to align to + * @param {Number[]} [offsets] Offset the positioning by [x, y] + * Element animation config object + * @param {Boolean} animate (private) + * @return {Ext.util.Positionable} this + */ + alignTo: function(element, position, offsets, animate) { + var me = this, + el = me.el; + return me.setXY(me.getAlignToXY(element, position, offsets), el.anim && !!animate ? el.anim(animate) : false); + }, + /** + * Calculates x,y coordinates specified by the anchor position on the element, adding + * extraX and extraY values. + * @param {String} [anchor='tl'] The specified anchor position. + * See {@link #alignTo} for details on supported anchor positions. + * @param {Number} [extraX] value to be added to the x coordinate + * @param {Number} [extraY] value to be added to the y coordinate + * @param {Object} [size] An object containing the size to use for calculating anchor + * position {width: (target width), height: (target height)} (defaults to the + * element's current size) + * @return {Number[]} [x, y] An array containing the element's x and y coordinates + * @private + */ + calculateAnchorXY: function(anchor, extraX, extraY, size) { + var region = this.getRegion(); + region.setPosition(0, 0); + region.translateBy(extraX || 0, extraY || 0); + if (size) { + region.setWidth(size.width); + region.setHeight(size.height); + } + return region.getAnchorPoint(anchor); + }, + /** + * This function converts a legacy alignment string such as 't-b' into a + * pair of edge, offset objects which describe the alignment points of + * the two regions. + * + * So tl-br becomes {myEdge:'t', offset:0}, {otherEdge:'b', offset:100} + * + * This not only allows more flexibility in the alignment possibilities, + * but it also resolves any ambiguity as to chich two edges are desired + * to be adjacent if an anchor pointer is required. + * @private + */ + convertPositionSpec: function(posSpec) { + return Ext.util.Region.getAlignInfo(posSpec); + }, + /** + * Gets the x,y coordinates to align this element with another element. See + * {@link #alignTo} for more info on the supported position values. + * @param {Ext.util.Positionable/HTMLElement/String} alignToEl The Positionable, + * HTMLElement, or id of the element to align to. + * @param {String} [position="tl-bl?"] The position to align to + * @param {Number[]} [offsets] Offset the positioning by [x, y] + * @return {Number[]} [x, y] + */ + getAlignToXY: function(alignToEl, position, offsets) { + var newRegion = this.getAlignToRegion(alignToEl, position, offsets); + return [ + newRegion.x, + newRegion.y + ]; + }, + getAlignToRegion: function(alignToEl, posSpec, offset, minHeight) { + var me = this, + inside, newRegion, bodyScroll; + alignToEl = Ext.fly(alignToEl.el || alignToEl); + if (!alignToEl || !alignToEl.dom) { + Ext.raise({ + sourceClass: 'Ext.util.Positionable', + sourceMethod: 'getAlignToXY', + msg: 'Attempted to align an element that doesn\'t exist' + }); + } + posSpec = me.convertPositionSpec(posSpec); + // If position spec ended with a "?" or "!", then constraining is necessary + if (posSpec.constrain) { + // Constrain to the correct enclosing object: + // If the assertive form was used (like "tl-bl!"), constrain to the alignToEl. + if (posSpec.constrain === '!') { + inside = alignToEl; + } else { + // Otherwise, attempt to use the constrainTo property. + // Otherwise, if we are a Component, there will be a container property. + // Otherwise, use this Positionable's element's parent node. + inside = me.constrainTo || me.container || me.el.parent(); + } + inside = Ext.fly(inside.el || inside).getConstrainRegion(); + } + if (alignToEl === Ext.getBody()) { + bodyScroll = alignToEl.getScroll(); + offset = [ + bodyScroll.left, + bodyScroll.top + ]; + } + newRegion = me.getRegion().alignTo({ + target: alignToEl.getRegion(), + inside: inside, + minHeight: minHeight, + offset: offset, + align: posSpec, + axisLock: true + }); + return newRegion; + }, + /** + * Gets the x,y coordinates specified by the anchor position on the element. + * @param {String} [anchor='tl'] The specified anchor position. + * See {@link #alignTo} for details on supported anchor positions. + * @param {Boolean} [local] True to get the local (element top/left-relative) anchor + * position instead of page coordinates + * @param {Object} [size] An object containing the size to use for calculating anchor + * position {width: (target width), height: (target height)} (defaults to the + * element's current size) + * @return {Number[]} [x, y] An array containing the element's x and y coordinates + */ + getAnchorXY: function(anchor, local, size) { + var me = this, + region = me.getRegion(), + el = me.el, + isViewport = el.dom.nodeName === 'BODY' || el.dom.nodeType === 9, + scroll = el.getScroll(); + if (local) { + region.setPosition(0, 0); + } else if (isViewport) { + region.setPosition(scroll.left, scroll.top); + } + if (size) { + region.setWidth(size.width); + region.setHeight(size.height); + } + return region.getAnchorPoint(anchor); + }, + /** + * Return an object defining the area of this Element which can be passed to + * {@link #setBox} to set another Element's size/location to match this element. + * + * @param {Boolean} [contentBox] If true a box for the content of the element is + * returned. + * @param {Boolean} [local] If true the element's left and top relative to its + * `offsetParent` are returned instead of page x/y. + * @return {Object} An object in the format + * @return {Number} return.x The element's X position. + * @return {Number} return.y The element's Y position. + * @return {Number} return.width The element's width. + * @return {Number} return.height The element's height. + * @return {Number} return.bottom The element's lower bound. + * @return {Number} return.right The element's rightmost bound. + * + * The returned object may also be addressed as an Array where index 0 contains the X + * position and index 1 contains the Y position. The result may also be used for + * {@link #setXY} + */ + getBox: function(contentBox, local) { + var me = this, + xy = local ? me.getLocalXY() : me.getXY(), + x = xy[0], + y = xy[1], + w, h, borderPadding, beforeX, beforeY; + // Document body or document is special case + if (me.el.dom.nodeName === 'BODY' || me.el.dom.nodeType === 9) { + w = Ext.Element.getViewportWidth(); + h = Ext.Element.getViewportHeight(); + } else { + w = me.getWidth(); + h = me.getHeight(); + } + if (contentBox) { + borderPadding = me.getBorderPadding(); + beforeX = borderPadding.beforeX; + beforeY = borderPadding.beforeY; + x += beforeX; + y += beforeY; + w -= (beforeX + borderPadding.afterX); + h -= (beforeY + borderPadding.afterY); + } + return { + x: x, + left: x, + 0: x, + y: y, + top: y, + 1: y, + width: w, + height: h, + right: x + w, + bottom: y + h + }; + }, + /** + * Calculates the new [x,y] position to move this Positionable into a constrain region. + * + * By default, this Positionable is constrained to be within the container it was added to, + * or the element it was rendered to. + * + * Priority is given to constraining the top and left within the constraint. + * + * An alternative constraint may be passed. + * @param {String/HTMLElement/Ext.dom.Element/Ext.util.Region} [constrainTo] The Element + * or {@link Ext.util.Region Region} into which this Component is to be constrained. + * Defaults to the element into which this Positionable was rendered, or this Component's + * {@link Ext.Component#constrainTo}. + * @param {Number[]} [proposedPosition] A proposed `[X, Y]` position to test for validity + * and to coerce into constraints instead of using this Positionable's current position. + * @param {Boolean} [local] The proposedPosition is local *(relative to floatParent + * if a floating Component)* + * @param {Number[]} [proposedSize] A proposed `[width, height]` size to use when calculating + * constraints instead of using this Positionable's current size. + * @return {Number[]} **If** the element *needs* to be translated, the new `[X, Y]` position + * within constraints if possible, giving priority to keeping the top and left edge + * in the constrain region. Otherwise, `false`. + * @private + */ + calculateConstrainedPosition: function(constrainTo, proposedPosition, local, proposedSize) { + var me = this, + vector, + fp = me.floatParent, + parentNode = fp ? fp.getTargetEl() : null, + parentOffset, borderPadding, proposedConstrainPosition, + xy = false; + if (local && fp) { + parentOffset = parentNode.getXY(); + borderPadding = parentNode.getBorderPadding(); + parentOffset[0] += borderPadding.beforeX; + parentOffset[1] += borderPadding.beforeY; + if (proposedPosition) { + proposedConstrainPosition = [ + proposedPosition[0] + parentOffset[0], + proposedPosition[1] + parentOffset[1] + ]; + } + } else { + proposedConstrainPosition = proposedPosition; + } + // Calculate the constrain vector to coerce our position to within our + // constrainTo setting. getConstrainVector will provide a default constraint + // region if there is no explicit constrainTo, *and* there is no floatParent + // owner Component. + constrainTo = constrainTo || me.constrainTo || parentNode || me.container || me.el.parent(); + if (local && proposedConstrainPosition) { + proposedConstrainPosition = me.reverseTranslateXY(proposedConstrainPosition); + } + vector = ((me.constrainHeader && me.header.rendered) ? me.header : me).getConstrainVector(constrainTo, proposedConstrainPosition, proposedSize); + // false is returned if no movement is needed + if (vector) { + xy = proposedPosition || me.getPosition(local); + xy[0] += vector[0]; + xy[1] += vector[1]; + } + return xy; + }, + /** + * Returns the content region of this element for purposes of constraining or clipping floating + * children. That is the region within the borders and scrollbars, but not within the padding. + * + * @return {Ext.util.Region} A Region containing "top, left, bottom, right" properties. + */ + getConstrainRegion: function() { + var me = this, + el = me.el, + isBody = el.dom.nodeName === 'BODY', + dom = el.dom, + borders = el.getBorders(), + pos = el.getXY(), + left = pos[0] + borders.beforeX, + top = pos[1] + borders.beforeY, + scroll, width, height; + // For the body we want to do some special logic. + if (isBody) { + scroll = el.getScroll(); + left = scroll.left; + top = scroll.top; + width = Ext.Element.getViewportWidth(); + height = Ext.Element.getViewportHeight(); + } else { + width = dom.clientWidth; + height = dom.clientHeight; + } + return new Ext.util.Region(top, left + width, top + height, left); + }, + /** + * Returns the `[X, Y]` vector by which this Positionable's element must be translated to make + * a best attempt to constrain within the passed constraint. Returns `false` if the element + * does not need to be moved. + * + * Priority is given to constraining the top and left within the constraint. + * + * The constraint may either be an existing element into which the element is to be + * constrained, or a {@link Ext.util.Region Region} into which this element is to be + * constrained. + * + * By default, any extra shadow around the element is **not** included in the constrain + * calculations - the edges of the element are used as the element bounds. To constrain + * the shadow within the constrain region, set the `constrainShadow` property on this element + * to `true`. + * + * @param {Ext.util.Positionable/HTMLElement/String/Ext.util.Region} [constrainTo] The + * Positionable, HTMLElement, element id, or Region into which the element is to be + * constrained. + * @param {Number[]} [proposedPosition] A proposed `[X, Y]` position to test for validity + * and to produce a vector for instead of using the element's current position + * @param {Number[]} [proposedSize] A proposed `[width, height]` size to constrain + * instead of using the element's current size + * @return {Number[]/Boolean} **If** the element *needs* to be translated, an `[X, Y]` + * vector by which this element must be translated. Otherwise, `false`. + */ + getConstrainVector: function(constrainTo, proposedPosition, proposedSize) { + var me = this, + thisRegion = me.getRegion(), + vector = [ + 0, + 0 + ], + shadowSize = (me.shadow && me.constrainShadow && !me.shadowDisabled) ? me.el.shadow.getShadowSize() : undefined, + overflowed = false, + constraintInsets = me.constraintInsets; + if (!(constrainTo instanceof Ext.util.Region)) { + constrainTo = Ext.get(constrainTo.el || constrainTo); + // getConstrainRegion uses clientWidth and clientHeight. + // so it will clear any scrollbars. + constrainTo = constrainTo.getConstrainRegion(); + } + // Apply constraintInsets + if (constraintInsets) { + constraintInsets = Ext.isObject(constraintInsets) ? constraintInsets : Ext.Element.parseBox(constraintInsets); + constrainTo.adjust(constraintInsets.top, constraintInsets.right, constraintInsets.bottom, constraintInsets.left); + } + // Shift this region to occupy the proposed position + if (proposedPosition) { + thisRegion.translateBy(proposedPosition[0] - thisRegion.x, proposedPosition[1] - thisRegion.y); + } + // Set the size of this region to the proposed size + if (proposedSize) { + thisRegion.right = thisRegion.left + proposedSize[0]; + thisRegion.bottom = thisRegion.top + proposedSize[1]; + } + // Reduce the constrain region to allow for shadow + if (shadowSize) { + constrainTo.adjust(shadowSize[0], -shadowSize[1], -shadowSize[2], shadowSize[3]); + } + // Constrain the X coordinate by however much this Element overflows + if (thisRegion.right > constrainTo.right) { + overflowed = true; + vector[0] = (constrainTo.right - thisRegion.right); + } + // overflowed the right + if (thisRegion.left + vector[0] < constrainTo.left) { + overflowed = true; + vector[0] = (constrainTo.left - thisRegion.left); + } + // overflowed the left + // Constrain the Y coordinate by however much this Element overflows + if (thisRegion.bottom > constrainTo.bottom) { + overflowed = true; + vector[1] = (constrainTo.bottom - thisRegion.bottom); + } + // overflowed the bottom + if (thisRegion.top + vector[1] < constrainTo.top) { + overflowed = true; + vector[1] = (constrainTo.top - thisRegion.top); + } + // overflowed the top + return overflowed ? vector : false; + }, + /** + * Returns the offsets of this element from the passed element. The element must both + * be part of the DOM tree and not have display:none to have page coordinates. + * @param {Ext.util.Positionable/HTMLElement/String} offsetsTo The Positionable, + * HTMLElement, or element id to get get the offsets from. + * @return {Number[]} The XY page offsets (e.g. `[100, -200]`) + */ + getOffsetsTo: function(offsetsTo) { + var o = this.getXY(), + e = offsetsTo.isRegion ? [ + offsetsTo.x, + offsetsTo.y + ] : Ext.fly(offsetsTo.el || offsetsTo).getXY(); + return [ + o[0] - e[0], + o[1] - e[1] + ]; + }, + /** + * Returns a region object that defines the area of this element. + * @param {Boolean} [contentBox] If true a box for the content of the element is + * returned. + * @param {Boolean} [local] If true the element's left and top relative to its + * `offsetParent` are returned instead of page x/y. + * @return {Ext.util.Region} A Region containing "top, left, bottom, right" properties. + */ + getRegion: function(contentBox, local) { + var box = this.getBox(contentBox, local); + return new Ext.util.Region(box.top, box.right, box.bottom, box.left); + }, + /** + * Returns a region object that defines the client area of this element. + * + * That is, the area *within* any scrollbars. + * @return {Ext.util.Region} A Region containing "top, left, bottom, right" properties. + */ + getClientRegion: function() { + var me = this, + el = me.el, + dom = el.dom, + viewContentBox = me.getBox(true), + scrollbarHeight = dom.offsetHeight > dom.clientHeight, + scrollbarWidth = dom.offsetWidth > dom.clientWidth, + padding, scrollSize, isRTL; + if (scrollbarHeight || scrollbarWidth) { + scrollSize = Ext.getScrollbarSize(); + // Capture width taken by any vertical scrollbar. + // If there is a vertical scrollbar, shrink the box. + if (scrollbarWidth) { + scrollbarWidth = scrollSize.width; + isRTL = el.getStyle('direction') === 'rtl' && !Ext.supports.rtlVertScrollbarOnRight; + if (isRTL) { + padding = el.getPadding('l'); + viewContentBox.left -= padding + Math.max(padding, scrollbarWidth); + } else { + padding = el.getPadding('r'); + viewContentBox.right += padding - Math.max(padding, scrollbarWidth); + } + } + // Capture height taken by any horizontal scrollbar. + // If there is a horizontal scrollbar, shrink the box. + if (scrollbarHeight) { + scrollbarHeight = scrollSize.height; + padding = el.getPadding('b'); + viewContentBox.bottom += padding - Math.max(padding, scrollbarHeight); + } + } + // The client region excluding any scrollbars. + return new Ext.util.Region(viewContentBox.top, viewContentBox.right, viewContentBox.bottom, viewContentBox.left); + }, + /** + * Returns the **content** region of this element. That is the region within the borders + * and padding. + * @return {Ext.util.Region} A Region containing "top, left, bottom, right" member data. + */ + getViewRegion: function() { + var me = this, + el = me.el, + isBody = el.dom.nodeName === 'BODY', + borderPadding, scroll, pos, top, left, width, height; + // For the body we want to do some special logic + if (isBody) { + scroll = el.getScroll(); + left = scroll.left; + top = scroll.top; + width = Ext.Element.getViewportWidth(); + height = Ext.Element.getViewportHeight(); + } else { + borderPadding = me.getBorderPadding(); + pos = me.getXY(); + left = pos[0] + borderPadding.beforeX; + top = pos[1] + borderPadding.beforeY; + width = me.getWidth(true); + height = me.getHeight(true); + } + return new Ext.util.Region(top, left + width, top + height, left); + }, + /** + * Move the element relative to its current position. + * @param {String} direction Possible values are: + * + * - `"l"` (or `"left"`) + * - `"r"` (or `"right"`) + * - `"t"` (or `"top"`, or `"up"`) + * - `"b"` (or `"bottom"`, or `"down"`) + * + * @param {Number} distance How far to move the element in pixels + * @param {Boolean} animate (private) + */ + move: function(direction, distance, animate) { + var me = this, + xy = me.getXY(), + x = xy[0], + y = xy[1], + left = [ + x - distance, + y + ], + right = [ + x + distance, + y + ], + top = [ + x, + y - distance + ], + bottom = [ + x, + y + distance + ], + hash = { + l: left, + left: left, + r: right, + right: right, + t: top, + top: top, + up: top, + b: bottom, + bottom: bottom, + down: bottom + }; + direction = direction.toLowerCase(); + me.setXY([ + hash[direction][0], + hash[direction][1] + ], animate); + }, + /** + * Sets the element's box. + * @param {Object} box The box to fill {x, y, width, height} + * @return {Ext.util.Positionable} this + */ + setBox: function(box) { + var me = this, + x, y; + if (box.isRegion) { + box = { + x: box.left, + y: box.top, + width: box.right - box.left, + height: box.bottom - box.top + }; + } + me.constrainBox(box); + x = box.x; + y = box.y; + // Position to the contrained position + // Call setSize *last* so that any possible layout has the last word on position. + me.setXY([ + x, + y + ]); + me.setSize(box.width, box.height); + me.afterSetPosition(x, y); + return me; + }, + /** + * @private + */ + constrainBox: function(box) { + var me = this, + constrainedPos, x, y; + if (me.constrain || me.constrainHeader) { + x = ('x' in box) ? box.x : box.left; + y = ('y' in box) ? box.y : box.top; + constrainedPos = me.calculateConstrainedPosition(null, [ + x, + y + ], false, [ + box.width, + box.height + ]); + // If it *needs* constraining, change the position + if (constrainedPos) { + box.x = constrainedPos[0]; + box.y = constrainedPos[1]; + } + } + }, + /** + * Translates the passed page coordinates into left/top css values for the element + * @param {Number/Array} x The page x or an array containing [x, y] + * @param {Number} [y] The page y, required if x is not an array + * @return {Object} An object with left and top properties. e.g. + * {left: (value), top: (value)} + */ + translatePoints: function(x, y) { + var pos = this.translateXY(x, y); + return { + left: pos.x, + top: pos.y + }; + }, + /** + * Translates the passed page coordinates into x and y css values for the element + * @param {Number/Array} x The page x or an array containing [x, y] + * @param {Number} [y] The page y, required if x is not an array + * @return {Object} An object with x and y properties. e.g. + * {x: (value), y: (value)} + * @private + */ + translateXY: function(x, y) { + var me = this, + el = me.el, + styles = el.getStyle(me._positionTopLeft), + relative = styles.position === 'relative', + left = parseFloat(styles.left), + top = parseFloat(styles.top), + xy = me.getXY(); + if (Ext.isArray(x)) { + y = x[1]; + x = x[0]; + } + if (isNaN(left)) { + left = relative ? 0 : el.dom.offsetLeft; + } + if (isNaN(top)) { + top = relative ? 0 : el.dom.offsetTop; + } + left = (typeof x === 'number') ? x - xy[0] + left : undefined; + top = (typeof y === 'number') ? y - xy[1] + top : undefined; + return { + x: left, + y: top + }; + }, + /** + * Converts local coordinates into page-level coordinates + * @param {Number[]} xy The local x and y coordinates + * @return {Number[]} The translated coordinates + * @private + */ + reverseTranslateXY: function(xy) { + var coords = xy, + el = this.el, + dom = el.dom, + offsetParent = dom.offsetParent, + relative, offsetParentXY, x, y; + if (offsetParent) { + relative = el.isStyle('position', 'relative'); + offsetParentXY = Ext.fly(offsetParent).getXY(); + x = xy[0] + offsetParentXY[0] + offsetParent.clientLeft; + y = xy[1] + offsetParentXY[1] + offsetParent.clientTop; + if (relative) { + // relative positioned elements sit inside the offsetParent's padding, + // while absolute positioned element sit just inside the border + x += el.getPadding('l'); + y += el.getPadding('t'); + } + coords = [ + x, + y + ]; + } + return coords; + }, + privates: { + /** + * Clips this Component/Element to fit within the passed element's or component's view area + * @param {Ext.Component/Ext.Element/Ext.util.Region} clippingEl The Component or element + * or Region which should clip this element even if this element is outside the bounds + * of that region. + * @param {Number} sides The sides to clip 1=top, 2=right, 4=bottom, 8=left. + * + * This is to support components being clipped to their logical owner, such as a grid row + * editor when the row being edited scrolls out of sight. The editor should be clipped + * at the edge of the scrolling element. + * @private + */ + clipTo: function(clippingEl, sides) { + var clippingRegion, + el = this.el, + floaterRegion = el.getRegion(), + overflow, i, + clipValues = [], + clippedCls = this.clippedCls, + clipStyle, clipped, shadow; + // Allow a Region to be passed + if (clippingEl.isRegion) { + clippingRegion = clippingEl; + } else { + // eslint-disable-next-line max-len + clippingRegion = (clippingEl.isComponent ? clippingEl.el : Ext.fly(clippingEl)).getConstrainRegion(); + } + // Default to clipping all round. + if (!sides) { + sides = 15; + } + // Calculate how much all sides exceed the clipping region + if (sides & 1 && (overflow = clippingRegion.top - floaterRegion.top) > 0) { + clipValues[0] = overflow; + clipped = true; + } else { + clipValues[0] = -10000; + } + if (sides & 2 && (overflow = floaterRegion.right - clippingRegion.right) > 0) { + clipValues[1] = Math.max(0, el.getWidth() - overflow); + clipped = true; + } else { + clipValues[1] = 10000; + } + if (sides & 4 && (overflow = floaterRegion.bottom - clippingRegion.bottom) > 0) { + clipValues[2] = Math.max(0, el.getHeight() - overflow); + clipped = true; + } else { + clipValues[2] = 10000; + } + if (sides & 8 && (overflow = clippingRegion.left - floaterRegion.left) > 0) { + clipValues[3] = overflow; + clipped = true; + } else { + clipValues[3] = -10000; + } + clipStyle = 'rect('; + for (i = 0; i < 4; ++i) { + // Use the clipValue if there is one calculated. + // If not, top and left must be 0px, right and bottom must be 'auto'. + clipStyle += Ext.Element.addUnits(clipValues[i], 'px'); + clipStyle += (i === 3) ? ')' : ','; + } + el.dom.style.clip = clipStyle; + // hardware acceleration causes flickering problems on clipped elements. + // disable it while an element is clipped. + el.addCls(clippedCls); + // Clip/unclip shadow too. + // TODO: As SOON as IE8 retires, refactor Ext.dom.Shadow to use CSS3BoxShadow directly + // on its el Then we won't have to bother clipping the shadow as well. We'll just + // have to adjust the clipping on the element outwards in the unclipped dimensions + // to keep the shadow visible. + if ((shadow = el.shadow) && (el = shadow.el) && el.dom) { + clipValues[2] -= shadow.offsets.y; + clipValues[3] -= shadow.offsets.x; + clipStyle = 'rect('; + for (i = 0; i < 4; ++i) { + // Use the clipValue if there is one calculated. + // If not, clear the edges by 10px to allow the shadow's spread to be visible. + clipStyle += Ext.Element.addUnits(clipValues[i], 'px'); + clipStyle += (i === 3) ? ')' : ','; + } + el.dom.style.clip = clipStyle; + // Clip does not work on IE8 shadows + // TODO: As SOON as IE8 retires, refactor Ext.dom.Shadow to use CSS3BoxShadow + // directly on its el + if (clipped && !Ext.supports.CSS3BoxShadow) { + el.dom.style.display = 'none'; + } else { + el.dom.style.display = ''; + // hardware acceleration causes flickering problems on clipped elements. + // disable it while an element is clipped. + el.addCls(clippedCls); + } + } + }, + /** + * Clears any clipping applied to this component by {@link #method-clipTo}. + * @private + */ + clearClip: function() { + var el = this.el, + clippedCls = this.clippedCls; + el.dom.style.clip = Ext.isIE8 ? 'auto' : ''; + // hardware acceleration causes flickering problems on clipped elements. + // re-enable it when an element is unclipped. + el.removeCls(clippedCls); + // unclip shadow too. + if (el.shadow && el.shadow.el && el.shadow.el.dom) { + el.shadow.el.dom.style.clip = Ext.isIE8 ? 'auto' : ''; + // Clip does not work on IE8 shadows + // TODO: As SOON as IE8 retires, refactor Ext.dom.Shadow to use CSS3BoxShadow + // directly on its el + if (!Ext.supports.CSS3BoxShadow) { + el.dom.style.display = ''; + // hardware acceleration causes flickering problems on clipped elements. + // re-enable it when an element is unclipped. + el.removeCls(clippedCls); + } + } + } + } +}); + +/** + * Private utility class that manages the internal cache for {@link Ext.dom.Shadow Underlays} + * and {@link Ext.dom.Shim Shims}. + * @private + */ +Ext.define('Ext.dom.UnderlayPool', { + /** + * @constructor + * @param {Object} elementConfig A {@link Ext.dom.Helper DomHelper} config object to + * use for generating elements in the pool. + */ + constructor: function(elementConfig) { + this.elementConfig = elementConfig; + this.cache = []; + }, + /** + * Checks an element out of the pool. + * @return {Ext.dom.Element} + */ + checkOut: function() { + var el = this.cache.shift(); + if (!el) { + el = Ext.Element.create(this.elementConfig); + el.setVisibilityMode(2); + // tell the spec runner to ignore this element when checking if the dom is clean + el.dom.setAttribute('data-sticky', true); + } + return el; + }, + /** + * Checks an element back into the pool for future reuse + * @param {Ext.dom.Element} el + */ + checkIn: function(el) { + this.cache.push(el); + Ext.getDetachedBody().dom.appendChild(el.dom); + }, + /** + * Reset the pool by emptying the cache and destroying all its elements + */ + reset: function() { + var cache = this.cache, + i = cache.length; + while (i--) { + cache[i].destroy(); + } + this.cache = []; + } +}); + +/** + * A class that provides an underlay element which displays behind an absolutely positioned + * target element and tracks its size and position. Abstract base class for + * {@link Ext.dom.Shadow} and {@link Ext.dom.Shim} + * + * + * @private + * @abstract + */ +Ext.define('Ext.dom.Underlay', { + /** + * @cfg {Ext.dom.Element} target + * The target element + */ + /** + * @cfg {Number} zIndex + * The CSS z-index to use for this underlay. Defaults to the z-index of {@link #target}. + */ + constructor: function(config) { + Ext.apply(this, config); + }, + /** + * @method + * @protected + * Called before the underlay is shown, immediately after its element is retrieved + * from the pool + */ + beforeShow: Ext.emptyFn, + /** + * @protected + * Returns the dom element that this underlay should be inserted before. + * Defaults to the target element + * @return {Ext.dom.Element} + */ + getInsertionTarget: function() { + return this.target; + }, + /** + * @protected + * @return {Ext.dom.UnderlayPool} + */ + getPool: function() { + return this.pool || (this.self.prototype.pool = new Ext.dom.UnderlayPool(this.elementConfig)); + }, + /** + * Hides the underlay + */ + hide: function() { + var me = this, + el = me.el; + if (el) { + if (el.dom) { + el.hide(); + me.getPool().checkIn(el); + } + me.el = null; + } + me.hidden = true; + }, + /** + * Aligns the underlay to its target element + * @param {Number} [x] The x position of the target element. If not provided, the + * x position will be read from the DOM. + * @param {Number} [y] The y position of the target element. If not provided, the + * y position will be read from the DOM. + * @param {Number} [width] The width of the target element. If not provided, the + * width will be read from the DOM. + * @param {Number} [height] The height of the target element. If not provided, the + * height will be read from the DOM. + */ + realign: function(x, y, width, height) { + var me = this, + el = me.el, + target = me.target, + offsets = me.offsets, + max = Math.max; + if (el) { + if (x == null) { + x = target.getX(); + } + if (y == null) { + y = target.getY(); + } + if (width == null) { + width = target.getWidth(); + } + if (height == null) { + height = target.getHeight(); + } + if (offsets) { + x = x + offsets.x; + y = y + offsets.y; + width = max(width + offsets.w, 0); + height = max(height + offsets.h, 0); + } + el.setXY([ + x, + y + ]); + el.setSize(width, height); + } + }, + /** + * Adjust the z-index of this underlay + * @param {Number} zIndex The new z-index + */ + setZIndex: function(zIndex) { + this.zIndex = zIndex; + if (this.el) { + this.el.setStyle("z-index", zIndex); + } + }, + /** + * Shows the underlay + */ + show: function() { + var me = this, + target = me.target, + zIndex = me.zIndex, + el = me.el, + insertionTarget = me.getInsertionTarget().dom, + dom; + if (!el) { + el = me.el = me.getPool().checkOut(); + } + me.beforeShow(); + if (zIndex == null) { + // For best results, we need the underlay to be as close as possible to its + // target element in the z-index stacking order without overlaying the target + // element. Since the UnderlayPool inserted the underlay as high as possible + // in the dom tree when we checked the underlay out of the pool, we can assume + // that it comes before the target element in the dom tree, and therefore can + // give it the exact same index as the target element. + zIndex = (parseInt(target.getStyle("z-index"), 10)); + } + if (zIndex) { + el.setStyle("z-index", zIndex); + } + // Overlay elements are shared, so fix position to match current owner + el.setStyle('position', me.fixed ? 'fixed' : ''); + dom = el.dom; + if (dom.nextSibling !== insertionTarget) { + // inserting the underlay as the previous sibling of the target ensures that + // it will show behind the target, as long as its z-index is less than or equal + // to the z-index of the target element. + target.dom.parentNode.insertBefore(dom, insertionTarget); + } + el.show(); + me.realign(); + me.hidden = false; + } +}); + +/** + * Simple class that can provide a shadow effect for any absolutely positioned {@link + * Ext.dom.Element Element}. + * + * Not meant to be used directly. To apply a shadow to an Element use the + * {@link Ext.dom.Element#enableShadow enableShadow} method. + * + * @private + */ +Ext.define('Ext.dom.Shadow', { + extend: Ext.dom.Underlay, + alternateClassName: 'Ext.Shadow', + /** + * @cfg {String} mode + * The shadow display mode. Supports the following options: + * + * - sides : Shadow displays on both sides and bottom only + * - frame : Shadow displays equally on all four sides + * - drop : Traditional bottom-right drop shadow + */ + mode: 'drop', + /** + * @cfg {Number} offset + * The number of pixels to offset the shadow from the element + */ + offset: 4, + cls: Ext.baseCSSPrefix + (!Ext.supports.CSS3BoxShadow ? 'ie' : 'css') + '-shadow', + /** + * Creates new Shadow. + * @param {Object} config (optional) Config object. + */ + constructor: function(config) { + var me = this, + outerOffsets, offsets, offset, rad; + me.callParent([ + config + ]); + me.elementConfig = { + cls: me.cls, + role: 'presentation' + }; + offset = me.offset; + rad = Math.floor(offset / 2); + me.opacity = 50; + switch (me.mode.toLowerCase()) { + case "drop": + outerOffsets = { + x: 0, + y: 0, + w: offset, + h: offset + }; + if (Ext.supports.CSS3BoxShadow) { + offsets = { + x: offset, + y: offset, + h: -offset, + w: -offset + }; + } else { + offsets = { + x: -rad, + y: -rad, + h: -rad, + w: -rad + }; + }; + break; + case "sides": + outerOffsets = { + x: -offset, + y: 0, + w: offset * 2, + h: offset + }; + if (Ext.supports.CSS3BoxShadow) { + offsets = { + x: 0, + y: offset, + h: -offset, + w: 0 + }; + } else { + offsets = { + x: 1 + rad - 2 * offset, + y: -(1 + rad), + h: -1, + w: rad - 1 + }; + }; + break; + case "frame": + outerOffsets = { + x: -offset, + y: -offset, + w: offset * 2, + h: offset * 2 + }; + if (Ext.supports.CSS3BoxShadow) { + offsets = { + x: 0, + y: 0, + h: 0, + w: 0 + }; + } else { + offsets = { + x: 1 + rad - 2 * offset, + y: 1 + rad - 2 * offset, + h: offset - rad - 1, + w: offset - rad - 1 + }; + }; + break; + case "bottom": + outerOffsets = { + x: -offset, + y: 0, + w: offset * 2, + h: offset + }; + if (Ext.supports.CSS3BoxShadow) { + offsets = { + x: 0, + y: offset, + h: -offset, + w: 0 + }; + } else { + offsets = { + x: 0, + y: offset, + h: 0, + w: 0 + }; + }; + break; + } + /** + * @property {Object} offsets The offsets used for positioning the shadow element + * relative to its target element + */ + me.offsets = offsets; + /** + * @property {Object} outerOffsets Offsets that represent the union of the areas + * of the target element and the shadow combined. Used by Ext.dom.Element for + * ensuring that the shim (if present) extends under the full area of both elements. + */ + me.outerOffsets = outerOffsets; + }, + /** + * @private + * Returns the shadow size on each side of the element in standard CSS order: top, right, + * bottom, left; + * @return {Number[]} Top, right, bottom and left shadow size. + */ + getShadowSize: function() { + var me = this, + offset = me.el ? me.offset : 0, + result = [ + offset, + offset, + offset, + offset + ], + mode = me.mode.toLowerCase(); + // There are only offsets if the shadow element is present. + if (me.el && mode !== 'frame') { + result[0] = 0; + if (mode === 'drop') { + result[3] = 0; + } + } + return result; + }, + /** + * @private + * CSS property used to set the box shadow. + */ + boxShadowProperty: (function() { + var property = 'boxShadow', + style = document.documentElement.style; + if (!('boxShadow' in style)) { + if ('WebkitBoxShadow' in style) { + // Safari prior to version 5.1 and Chrome prior to version 10 + property = 'WebkitBoxShadow'; + } else if ('MozBoxShadow' in style) { + // FF 3.5 & 3.6 + property = 'MozBoxShadow'; + } + } + return property; + }()), + beforeShow: function() { + var me = this, + style = me.el.dom.style, + shim = me.shim; + if (Ext.supports.CSS3BoxShadow) { + style[me.boxShadowProperty] = '0 0 ' + (me.offset + 2) + 'px #888'; + } else { + style.filter = "progid:DXImageTransform.Microsoft.alpha(opacity=" + me.opacity + ") progid:DXImageTransform.Microsoft.Blur(pixelradius=" + (me.offset) + ")"; + } + // if we are showing a shadow, and we already have a visible shim, we need to + // realign the shim to ensure that it includes the size of target and shadow els + if (shim) { + shim.realign(); + } + }, + /** + * Sets the opacity of the shadow + * @param {Number} opacity The opacity + */ + setOpacity: function(opacity) { + var el = this.el; + if (el) { + if (Ext.isIE && !Ext.supports.CSS3BoxShadow) { + opacity = Math.floor(opacity * 100 / 2) / 100; + } + this.opacity = opacity; + el.setOpacity(opacity); + } + } +}); + +/** + * Simple class that provides an iframe shim for any absolutely positioned {@link + * Ext.dom.Element Element} to prevent windowed objects from showing through. + * + * Not meant to be used directly. Internally shims are applied to Elements using + * {@link Ext.dom.Element#enableShim enableShim}. Developers should use the + * {@link Ext.util.Floating#shim shim} config to add shims to their + * {@link Ext.Component Components} or set {@link Ext#useShims Ext.useShims}=true. + * @private + */ +Ext.define('Ext.dom.Shim', { + extend: Ext.dom.Underlay, + cls: Ext.baseCSSPrefix + 'shim', + constructor: function(config) { + this.callParent([ + config + ]); + this.elementConfig = { + tag: 'iframe', + cls: this.cls, + role: 'presentation', + frameBorder: '0', + src: Ext.SSL_SECURE_URL, + // tabIndex of -1 ensures that the iframe is not focusable by the user + tabindex: '-1' + }; + }, + getInsertionTarget: function() { + // ensure that the shim is inserted before the shadow in the dom, so that the + // shadow will be stacked on top of it. + var shadow = this.shadow; + return (shadow && shadow.el) || this.target; + } +}); + +/** + * A special Ext.util.Event subclass that adds support for capture (top-down propagation) + * listeners, and non-delegated (directly attached to the dom) listeners. + * + * An Ext.Element will have one instance of this class per event type that is being listened + * for. The ElementEvent instance provides a single point for attaching event listeners + * and abstracts away important details on the timing and ordering of event firing. + * Internally this class manages up to 3 separate Ext.util.Event instances. These represent + * separate stacks of listeners that may be invoked during different phases of event propagation. + * + * - `captures` - tracks listeners that should fire during the "capture" phase of the + * standard delegated model (listeners attached using capture:true) + * - `direct` - tracks directly attached listeners, that is listeners that should fire + * immediately when the event is dispatched to the dom element, before the event bubbles + * upward and delegated listener processing begins + * (listeners attached using delegated:false) + * - `directCaptures` - tracks directly attached capture listeners (only works in IE10+) + * + * For more detail on the timing of when these event stacks are dispatched please see + * Ext.event.publisher.Dom + * + * @private + */ +Ext.define('Ext.dom.ElementEvent', { + extend: Ext.util.Event, + addListener: function(fn, scope, options, caller, manager) { + var me = this, + added = false, + name = me.name, + isDirectEvent = Ext.event.publisher.Dom.instance.directEvents[name], + captures, directs, directCaptures; + options = options || {}; + if (options.delegated === false || isDirectEvent) { + if (isDirectEvent && options.delegate) { + options.capture = true; + } + if (options.capture) { + directCaptures = me.directCaptures || (me.directCaptures = new Ext.util.Event(me.observable, name)); + added = directCaptures.addListener(fn, scope, options, caller, manager); + } else { + directs = me.directs || (me.directs = new Ext.util.Event(me.observable, name)); + added = directs.addListener(fn, scope, options, caller, manager); + } + } else if (options.capture) { + captures = me.captures || (me.captures = new Ext.util.Event(me.observable, name)); + added = captures.addListener(fn, scope, options, caller, manager); + } else { + added = me.callParent([ + fn, + scope, + options, + caller, + manager + ]); + } + return added; + }, + removeListener: function(fn, scope) { + var me = this, + captures = me.captures, + directs = me.directs, + directCaptures = me.directCaptures, + removed = false, + index = me.findListener(fn, scope); + if (index !== -1) { + removed = me.callParent([ + fn, + scope, + index + ]); + } else { + if (directs) { + index = directs.findListener(fn, scope); + } + if (index !== -1) { + removed = directs.removeListener(fn, scope, index); + } else { + if (captures) { + index = captures.findListener(fn, scope); + } + if (index !== -1) { + removed = captures.removeListener(fn, scope, index); + } else if (directCaptures) { + index = directCaptures.findListener(fn, scope); + if (index !== -1) { + removed = directCaptures.removeListener(fn, scope, index); + } + } + } + } + return removed; + }, + clearListeners: function() { + var me = this, + directCaptures = me.directCaptures, + directs = me.directs, + captures = me.captures; + if (directCaptures) { + directCaptures.clearListeners(); + } + if (directs) { + directs.clearListeners(); + } + if (captures) { + captures.clearListeners(); + } + me.callParent(); + }, + suspend: function() { + var me = this, + directCaptures = me.directCaptures, + directs = me.directs, + captures = me.captures; + if (directCaptures) { + directCaptures.suspend(); + } + if (directs) { + directs.suspend(); + } + if (captures) { + captures.suspend(); + } + me.callParent(); + }, + resume: function() { + var me = this, + directCaptures = me.directCaptures, + directs = me.directs, + captures = me.captures; + if (directCaptures) { + directCaptures.resume(); + } + if (directs) { + directs.resume(); + } + if (captures) { + captures.resume(); + } + me.callParent(); + } +}); + +/** + * Abstract base class for event publishers + * @private + */ +Ext.define('Ext.event.publisher.Publisher', { + isEventPublisher: true, + $vetoClearingPrototypeOnDestroy: true, + /** + * @property {Array} handledEvents + * An array of events that this publisher handles. + */ + handledEvents: [], + statics: { + /** + * @property {Object} publishers + * A map of all publisher singleton instances. Publishers register themselves + * in this map as soon as they are constructed. + */ + publishers: {}, + /** + * @property publishersByEvent + * A map of handled event names to the publisher that handles each event. + * Provides a convenient way for looking up the publisher that handles any given + * event, for example: + * + * // get the publisher that handles click: + * var publisher = Ext.event.publisher.Publisher.publishersByEvent.click; + */ + publishersByEvent: {} + }, + constructor: function() { + var me = this, + type = me.type; + /** + * @property {Object} handles + * @private + * A map for conveniently checking if this publisher handles a given event + */ + me.handles = {}; + if (!type) { + Ext.raise("Event publisher '" + me.$className + "' defined without a 'type' property."); + } + if (me.self.instance) { + Ext.raise("Cannot create multiple instances of '" + me.$className + "'. " + "Use '" + me.$className + ".instance' to retrieve the singleton instance."); + } + me.registerEvents(); + Ext.event.publisher.Publisher.publishers[type] = me; + }, + /** + * Registers all {@link #handledEvents} in the + * {@link Ext.event.publisher.Publisher#publishersByEvent} map. + * @param {String[]} [events] optional events to register instead of handledEvents. + * @protected + */ + registerEvents: function(events) { + var me = this, + publishersByEvent = Ext.event.publisher.Publisher.publishersByEvent, + handledEvents = events || me.handledEvents, + ln = handledEvents.length, + eventName, i; + for (i = 0; i < ln; i++) { + eventName = handledEvents[i]; + me.handles[eventName] = 1; + publishersByEvent[eventName] = me; + } + }, + subscribe: function() { + Ext.raise("Ext.event.publisher.Publisher subclass '" + this.$className + '" has no subscribe method.'); + }, + unsubscribe: function() { + Ext.raise("Ext.event.publisher.Publisher subclass '" + this.$className + '" has no unsubscribe method.'); + }, + fire: function(element, eventName, args) { + var event; + if (element.hasListeners[eventName]) { + event = element.events[eventName]; + if (event) { + event.fire.apply(event, args); + } + } + } +}); + +/** + * @private + */ +Ext.define('Ext.util.Offset', { + /* Begin Definitions */ + statics: { + fromObject: function(obj) { + if (obj instanceof this) { + return obj; + } + if (typeof obj === 'number') { + return new this(obj, obj); + } + if (obj.length) { + return new this(obj[0], obj[1]); + } + return new this(obj.x, obj.y); + } + }, + /* End Definitions */ + constructor: function(x, y) { + this.x = (x != null && !isNaN(x)) ? x : 0; + this.y = (y != null && !isNaN(y)) ? y : 0; + return this; + }, + copy: function() { + return new Ext.util.Offset(this.x, this.y); + }, + copyFrom: function(p) { + this.x = p.x; + this.y = p.y; + }, + toString: function() { + return "Offset[" + this.x + "," + this.y + "]"; + }, + equals: function(offset) { + if (!(offset instanceof this.statics())) { + Ext.raise('Offset must be an instance of Ext.util.Offset'); + } + return (this.x === offset.x && this.y === offset.y); + }, + add: function(offset) { + if (!(offset instanceof this.statics())) { + Ext.raise('Offset must be an instance of Ext.util.Offset'); + } + this.x += offset.x; + this.y += offset.y; + }, + round: function(to) { + var factor; + if (!isNaN(to)) { + factor = Math.pow(10, to); + this.x = Math.round(this.x * factor) / factor; + this.y = Math.round(this.y * factor) / factor; + } else { + this.x = Math.round(this.x); + this.y = Math.round(this.y); + } + }, + isZero: function() { + return this.x === 0 && this.y === 0; + } +}); + +/** + * This class represents a rectangular region in X,Y space, and performs geometric + * transformations or tests upon the region. + * + * This class may be used to compare the document regions occupied by elements. + */ +Ext.define('Ext.util.Region', function() { + var ExtUtil = Ext.util, + // eslint-disable-next-line no-useless-escape + constrainRe = /([^\?!]*)(!|\?)?$/, + alignRe = /^(?:(?:([trbl])(\d+))|(tl|t|tc|tr|l|c|r|bl|b|bc|br))(?:-(?:(?:([trbl])(\d+))|(tl|t|tc|tr|l|c|r|bl|b|bc|br)))?$/i, + // Each side has the first letter as the main align side, so [tlbr] + // The next optional component is a offset factor, so [tb] may be followed by [lr] + // and vice versa + // The offset factor may also be a number along that edge from 0 to 100. + // So 'tl-br' is equal to 't0-b100'. + // The offset factor defaults to 'c' or 50 meaning the 't-b' is equivalent to + // 't50-b50' or 'tc-bc' + LTROffsetFactors = { + l: 0, + r: 100, + t: 0, + b: 100, + c: 50 + }, + RTLOffsetFactors = { + l: 100, + r: 0, + t: 0, + b: 100, + c: 50 + }, + relativePositions = { + b: 0, + l: 1, + t: 2, + r: 3 + }, + alignMap = { + "tl-tr": "l0-r0", + "tl-r": "l0-r50", + "bl-r": "l100-r50", + "bl-br": "l100-r100", + "tr-tl": "r0-l0", + "tr-l": "r0-l50", + "br-l": "r100-l50", + "br-bl": "r100-l100" + }, + rtlAlignMap = { + "tl-tr": "r0-l0", + "tl-r": "r0-l50", + "bl-r": "r100-l50", + "bl-br": "r100-l100", + "tr-tl": "l0-r0", + "tr-l": "l0-r50", + "br-l": "l100-r50", + "br-bl": "l100-r100" + }, + adjustParams = [], + zeroOffset = new ExtUtil.Offset(0, 0), + parseRegion = function(box) { + var Region = ExtUtil.Region, + type = typeof box, + top, right, bottom, left; + if (box == null) { + return Region.EMPTY; + } + if (box.isRegion) { + return box; + } + if (box.isElement || box.nodeType === 1) { + return this.getRegion(box); + } + if (type === 'string') { + box = box.split(' '); + switch (box.length) { + case 1: + box[1] = box[2] = box[3] = box[0]; + break; + case 2: + box[2] = box[0]; + box[3] = box[1]; + break; + case 3: + box[3] = box[1]; + } + top = parseInt(box[0], 10) || 0; + right = parseInt(box[1], 10) || 0; + bottom = parseInt(box[2], 10) || 0; + left = parseInt(box[3], 10) || 0; + } else if (type === 'number') { + top = right = bottom = left = box; + } else if (typeof box.x === 'number') { + top = box.y; + left = box.x; + if (typeof box.right === 'number') { + right = box.right; + bottom = box.bottom; + } else { + right = left + box.width; + bottom = top + box.height; + } + } else { + Ext.raise('Not convertible to a Region: ' + box); + } + return new Region(top, right, bottom, left); + }, + magnitude = [ + -1, + 1, + 1, + -1 + ], + // Depending on the "relativePosition" which will be 0,1,2 or 3 for T,R,B,L + // extend the adjacent edge of the target to account for the offset. + // Also, shrink the adjacent edge to create overlap for the anchor to center in. + addAnchorOffset = function(target, anchorSize, relativePosition) { + // Expand the adjacent edge by the anchor HEIGHT. + if (relativePosition != null && anchorSize) { + adjustParams[0] = adjustParams[1] = adjustParams[2] = adjustParams[3] = 0; + adjustParams[relativePosition] = anchorSize.y * magnitude[relativePosition]; + target = ExtUtil.Region.from(target); + target.adjust.apply(target, adjustParams); + } + return target; + }, + // Shrink the adjacent edge to create overlap for the anchor to center in. + calculateAnchorPosition = function(target, result, relativePosition, anchorSize, inside) { + var anchorWidth = Math.ceil(anchorSize.x), + minOverlap = Math.ceil(anchorWidth / 2) + 3, + min, max, anchorPos, isBefore, overlapLine, x, y; + // target is out of bounds. We can't show an anchor + if (inside && !inside.intersect(target)) { + return; + } + if (relativePosition != null) { + // The result is to the left or right of the target + if (relativePosition & 1) { + // Not enough height to support a side anchor + if (result.getHeight() < anchorWidth + 4) { + return; + } + // + // +------+ <--- min + // | | + // | | + // +---------+ < | <-anchorMax + // | | +------+ + // | | + // | | + // | | + // | | + // | | +-------+ <--- max + // +---------+ < | <-anchorMin + // | | + // | | + // +-------+ + // + // Coerce the result's top to create enough overlap with target. + // Needs at least anchorWidth / 2 + 2 to look right. + min = target.top + minOverlap - result.height; + max = target.bottom - minOverlap - 1; + result.setPosition(result.x, Math.min(Math.max(result.y, min), max)); + // Now calculate the min & max permissible anchor top so that the + // anchor baseline clears the result's corner by ar least 2px. + min = result.top + 2; + max = result.bottom - (anchorWidth + 2); + isBefore = relativePosition === 3; + x = isBefore ? result.right : result.left; + // eslint-disable-next-line max-len + overlapLine = new ExtUtil.Region(Math.max(result.top, target.top), x, Math.min(result.bottom, target.bottom), x); + // Align to the centre of the overlap line, wherever that may be + anchorPos = new ExtUtil.Region(0, 0, 0, 0).setWidth(anchorSize.y).setHeight(anchorWidth).alignTo({ + target: overlapLine, + align: isBefore ? 'l-r' : 'r-l', + overlap: true + }); + // Coerce the anchor into the bounds of the result. + anchorPos.setPosition(anchorPos.x, Math.min(Math.max(anchorPos.y, min), max)); + anchorPos.position = isBefore ? 'right' : 'left'; + } else // The result is above or below the target. + { + // Not enough width to support a top/bottom anchor + if (result.getWidth() < anchorWidth + 4) { + return; + } + // Coerce the result's left to create enough overlap with target. + // Needs at least anchorWidth / 2 + 2 to look right. + min = target.left + minOverlap - result.width; + max = target.right - minOverlap - 1; + result.setPosition(Math.min(Math.max(result.x, min), max), result.y); + // Now calculate the min & max permissible anchor left so that the + // anchor baseline clears the result's corner by ar least 2px. + min = result.left + 2; + max = result.right - (anchorWidth + 2); + // If there is not enough overlap. coerce the result to create enough overlap + isBefore = relativePosition === 0; + y = isBefore ? result.bottom : result.top; + // eslint-disable-next-line max-len + overlapLine = new ExtUtil.Region(y, Math.min(result.right, target.right), y, Math.max(result.left, target.left)); + // Align to the centre of the overlap line, wherever that may be + anchorPos = new ExtUtil.Region(0, 0, 0, 0).setWidth(anchorWidth).setHeight(anchorSize.y).alignTo({ + target: overlapLine, + align: isBefore ? 't-b' : 'b-t', + overlap: true + }); + // Coerce the anchor into the bounds of the result. + anchorPos.setPosition(Math.min(Math.max(anchorPos.x, min), max), anchorPos.y); + anchorPos.position = isBefore ? 'bottom' : 'top'; + } + // If anchor is outside constrain region it cannot be shown. + if (inside && !inside.contains(anchorPos)) { + return; + } + result.anchor = anchorPos; + result.anchor.align = relativePosition; + } + }, + checkMinHeight = function(minHeight, result, target, inside) { + var newHeight; + if (minHeight && inside) { + // Overflows the bottom of the target + if (result.top >= target.bottom && result.bottom > inside.bottom) { + result.setHeight(Math.max(result.getHeight() + inside.bottom - result.bottom, minHeight)); + result.constrainHeight = true; + } + // Overflows the top of the target + else if (result.bottom <= target.top && result.top < inside.top) { + newHeight = Math.max(result.getHeight() + result.top - inside.top, minHeight); + result.adjust(result.getHeight() - newHeight); + result.constrainHeight = true; + } + // Just too high + else if (result.getHeight() > inside.getHeight()) { + result.setHeight(Math.max(minHeight, inside.getHeight())); + result.setPosition(result.x, 0); + result.constrainHeight = true; + } + } + }, + checkMinWidth = function(minWidth, result, target, inside) { + var newWidth; + if (minWidth && inside) { + // Overflows the right of the target + if (result.left >= target.right && result.right > inside.right) { + result.setWidth(Math.max(result.getWidth() + inside.right - result.right, minWidth)); + result.constrainWidth = true; + } + // Overflows the left of the target + else if (result.right <= target.left && result.left < inside.left) { + newWidth = Math.max(result.getWidth() + result.left - inside.left, minWidth); + result.adjust(0, 0, 0, result.getWidth() - newWidth); + result.constrainWidth = true; + } + // Just too wide + else if (result.getWidth() > inside.getWidth()) { + result.setWidth(Math.max(minWidth, inside.getWidth())); + result.setPosition(0, result.y); + result.constrainWidth = true; + } + } + }; + /* eslint-disable indent */ + return { + isRegion: true, + statics: { + /** + * @static + * Retrieves an Ext.util.Region for a particular element. + * @param {String/HTMLElement/Ext.dom.Element} el An element ID, htmlElement or Ext.Element + * representing an element in the document. + * @return {Ext.util.Region} region + */ + getRegion: function(el) { + return Ext.fly(el).getRegion(); + }, + /** + * @static + * Creates a Region from a "box" Object which contains four numeric properties `top`, + * `right`, `bottom` and `left`. + * @param {Object} o An object with `top`, `right`, `bottom` and `left` properties. + * @return {Ext.util.Region} region The Region constructed based on the passed object + */ + from: function(o) { + return new this(o.top, o.right, o.bottom, o.left); + }, + /** + * This function converts a legacy alignment string such as 't-b' into a + * pair of edge, offset objects which describe the alignment points of + * the two regions. + * + * So tl-br becomes {myEdge:'t', offset:0}, {otherEdge:'b', offset:100} + * + * This not only allows more flexibility in the alignment possibilities, + * but it also resolves any ambiguity as to which two edges are desired + * to be adjacent if an anchor pointer is required. + * + * @param {String} align The align spec, eg `"tl-br"` + * @param {Boolean} [rtl] Pass `true` to use RTL calculations. + */ + getAlignInfo: function(align, rtl) { + if (typeof align === 'object') { + return align; + } + align = align ? ((align.indexOf('-') < 0) ? 'tl-' + align : align) : 'tl-bl'; + // Snip any constraint modifier off so that we can match the alignMaps + constrain = constrainRe.exec(align); + align = constrain[1]; + // Convert left to right alignments which are specified using top/bottom + // corner definitions. + align = (rtl ? rtlAlignMap : alignMap)[align] || align; + // eslint-disable-next-line vars-on-top + var offsetFactors = rtl ? RTLOffsetFactors : LTROffsetFactors, + constrain, + parts = alignRe.exec(align), + result; + if (!parts) { + Ext.raise({ + sourceClass: 'Ext.util.Region', + sourceMethod: 'getAlignInfo', + position: align, + msg: 'Attempted to align an element with an invalid position: "' + align + '"' + }); + } + result = { + myEdge: parts[1], + myOffset: parts[2], + otherEdge: parts[4], + otherOffset: parts[5], + constrain: constrain[2] + }; + // t-l, b-r etc. + // Convert points to edge and offset. + if (parts[3]) { + result.myEdge = parts[3][0]; + result.myOffset = offsetFactors[parts[3][1]]; + if (result.myOffset == null) { + result.myOffset = 50; + } + } + if (parts[6]) { + result.otherEdge = parts[6][0]; + result.otherOffset = offsetFactors[parts[6][1]]; + if (result.otherOffset == null) { + result.otherOffset = 50; + } + } + // TOP=0, RIGHT=1, BOTTOM=2, LEFT=3, INSIDE=undefined + result.position = relativePositions[result.myEdge]; + return result; + } + }, + /* End Definitions */ + /** + * Creates a region from the bounding sides. + * @param {Number} top The topmost pixel of the Region. + * @param {Number} right The rightmost pixel of the Region. + * @param {Number} bottom The bottom pixel of the Region. + * @param {Number} left The leftmost pixel of the Region. + */ + constructor: function(top, right, bottom, left) { + var me = this; + me.y = me.top = me[1] = top; + me.right = right; + me.bottom = bottom; + me.x = me.left = me[0] = left; + me.height = me.bottom - me.top; + me.width = me.right - me.left; + }, + /** + * Translates this Region to the specified position + * @param {Number} x The new X position. + * @param {Number} y The new Y position. + * @returns {Ext.util.Region} This region after translation. + */ + setPosition: function(x, y) { + // Allow [x, y] + if (arguments.length === 1) { + y = x[1]; + x = x[0]; + } + return this.translateBy(x - this.x, y - this.y); + }, + /** + * Checks if this region completely contains the region or point that is passed in. + * @param {Ext.util.Region/Ext.util.Point} region + * @return {Boolean} + */ + contains: function(region) { + var me = this; + return (region.x >= me.x && (region.right || region.x) <= me.right && region.y >= me.y && (region.bottom || region.y) <= me.bottom); + }, + /** + * Checks if this region intersects the region passed in. + * @param {Ext.util.Region} region + * @return {Ext.util.Region/Boolean} Returns the intersected region or false + * if there is no intersection. + */ + intersect: function(region) { + var me = this, + t = Math.max(me.y, region.y), + r = Math.min(me.right, region.right), + b = Math.min(me.bottom, region.bottom), + l = Math.max(me.x, region.x); + if (b > t && r > l) { + return new this.self(t, r, b, l); + } else { + return false; + } + }, + /** + * Returns the smallest region that contains the current AND targetRegion. + * @param {Ext.util.Region} region + * @return {Ext.util.Region} a new region + */ + union: function(region) { + var me = this, + t = Math.min(me.y, region.y), + r = Math.max(me.right, region.right), + b = Math.max(me.bottom, region.bottom), + l = Math.min(me.x, region.x); + return new this.self(t, r, b, l); + }, + /** + * Modifies the current region to be constrained to the targetRegion. + * @param {Ext.util.Region} targetRegion + * @return {Ext.util.Region} this + */ + constrainTo: function(targetRegion) { + var me = this, + constrain = Ext.Number.constrain; + me.top = me.y = constrain(me.top, targetRegion.y, targetRegion.bottom); + me.bottom = constrain(me.bottom, targetRegion.y, targetRegion.bottom); + me.left = me.x = constrain(me.left, targetRegion.x, targetRegion.right); + me.right = constrain(me.right, targetRegion.x, targetRegion.right); + me.height = me.bottom - me.top; + me.width = me.right - me.left; + return me; + }, + /** + * Modifies the current region to be adjusted by offsets. + * @param {Number} top Top offset + * @param {Number} right Right offset + * @param {Number} bottom Bottom offset + * @param {Number} left Left offset + * @return {Ext.util.Region} this + */ + adjust: function(top, right, bottom, left) { + var me = this; + me.top = me.y += top || 0; + me.left = me.x += left || 0; + me.right += right || 0; + me.bottom += bottom || 0; + me.height = me.bottom - me.top; + me.width = me.right - me.left; + return me; + }, + /** + * Get the offset amount of a point outside the region + * @param {String} [axis] + * @param {Ext.util.Point} [p] the point + * @return {Ext.util.Offset} + */ + getOutOfBoundOffset: function(axis, p) { + var d; + if (!Ext.isObject(axis)) { + if (axis === 'x') { + return this.getOutOfBoundOffsetX(p); + } else { + return this.getOutOfBoundOffsetY(p); + } + } else { + p = axis; + d = new ExtUtil.Offset(); + d.x = this.getOutOfBoundOffsetX(p.x); + d.y = this.getOutOfBoundOffsetY(p.y); + return d; + } + }, + /** + * Get the offset amount on the x-axis + * @param {Number} p the offset + * @return {Number} + */ + getOutOfBoundOffsetX: function(p) { + if (p <= this.x) { + return this.x - p; + } else if (p >= this.right) { + return this.right - p; + } + return 0; + }, + /** + * Get the offset amount on the y-axis + * @param {Number} p the offset + * @return {Number} + */ + getOutOfBoundOffsetY: function(p) { + if (p <= this.y) { + return this.y - p; + } else if (p >= this.bottom) { + return this.bottom - p; + } + return 0; + }, + /** + * Check whether the point / offset is out of bound + * @param {String} [axis] + * @param {Ext.util.Point/Number} [p] the point / offset + * @return {Boolean} + */ + isOutOfBound: function(axis, p) { + if (!Ext.isObject(axis)) { + if (axis === 'x') { + return this.isOutOfBoundX(p); + } else { + return this.isOutOfBoundY(p); + } + } else { + p = axis; + return (this.isOutOfBoundX(p.x) || this.isOutOfBoundY(p.y)); + } + }, + /** + * Check whether the offset is out of bound in the x-axis + * @param {Number} p the offset + * @return {Boolean} + */ + isOutOfBoundX: function(p) { + return (p < this.x || p > this.right); + }, + /** + * Check whether the offset is out of bound in the y-axis + * @param {Number} p the offset + * @return {Boolean} + */ + isOutOfBoundY: function(p) { + return (p < this.y || p > this.bottom); + }, + /** + * Restrict a point within the region by a certain factor. + * @param {String} [axis] + * @param {Ext.util.Point/Ext.util.Offset/Object} [p] + * @param {Number} [factor] + * @return {Ext.util.Point/Ext.util.Offset/Object/Number} + * @private + */ + restrict: function(axis, p, factor) { + var newP; + if (Ext.isObject(axis)) { + factor = p; + p = axis; + if (p.copy) { + newP = p.copy(); + } else { + newP = { + x: p.x, + y: p.y + }; + } + newP.x = this.restrictX(p.x, factor); + newP.y = this.restrictY(p.y, factor); + return newP; + } else { + if (axis === 'x') { + return this.restrictX(p, factor); + } else { + return this.restrictY(p, factor); + } + } + }, + /** + * Restrict an offset within the region by a certain factor, on the x-axis + * @param {Number} p + * @param {Number} [factor=1] The factor. + * @return {Number} + * @private + */ + restrictX: function(p, factor) { + if (!factor) { + factor = 1; + } + if (p <= this.x) { + p -= (p - this.x) * factor; + } else if (p >= this.right) { + p -= (p - this.right) * factor; + } + return p; + }, + /** + * Restrict an offset within the region by a certain factor, on the y-axis + * @param {Number} p + * @param {Number} [factor] The factor, defaults to 1 + * @return {Number} + * @private + */ + restrictY: function(p, factor) { + if (!factor) { + factor = 1; + } + if (p <= this.y) { + p -= (p - this.y) * factor; + } else if (p >= this.bottom) { + p -= (p - this.bottom) * factor; + } + return p; + }, + /** + * Returns the Region to which this rectangle should be moved in order to + * have the desired alignment with the specified target while remaining within the + * constraint. + * + * The `align` option can be one of these forms: + * + * - **Blank**: Defaults to aligning the region's top-left corner to the target's + * bottom-left corner ("tl-bl"). + * - **Two anchors**: If two values from the table below are passed separated by a dash, + * the first value is used as this region's anchor point, and the second value is + * used as the target's anchor point. + * - **One anchor**: The passed anchor position is used as the target's anchor point. + * This region will position its top-left corner (tl) to that point. + * - **Two edge/offset descriptors:** An edge/offset descriptor is an edge initial + * (`t`/`r`/`b`/`l`) followed by a percentage along that side. This describes a + * point to align with a similar point in the target. So `'t0-b0'` would be + * the same as `'tl-bl'`, `'l0-r50'` would place the top left corner of this item + * halfway down the right edge of the target item. This allows more flexibility + * and also describes which two edges are considered adjacent when positioning an anchor. + * + * If the `inside` option is passed, the Region will attempt to align as specified, + * but the position will be adjusted to constrain to the `inside` Region if necessary. + * Note that the Region being aligned might be swapped to align to a different position + * than that specified in order to enforce the constraints. Following are all of the + * supported anchor positions: + * + * Value Description + * ----- ----------------------------- + * tl The top left corner + * t The center of the top edge + * tr The top right corner + * l The center of the left edge + * c The center + * r The center of the right edge + * bl The bottom left corner + * b The center of the bottom edge + * br The bottom right corner + * + * Example Usage: + * + * var xy = comp.getRegion().alignTo({ + * align: 't-b', // align comp's top/center to el's bottom/center + * target: el.getRegion(), + * anchorSize: new Ext.util.Point(10, 10), + * inside: new Ext.util.Region(0, Ext.Element.getViewportWidth(), + * Ext.Element.getViewportHeight(), 0) + * }); + * + * @param {Object} options The alignment options. + * @param {Ext.util.Region} options.target The rectangle to which this rectangle + * should align. + * @param {String} [options.align=tl-bl] The alignment descriptor for positioning this + * rectangle with respect to the `target`. See {@link Ext.util.Positionable#alignTo}. + * Note that if the requested alignment results in violation of the `inside` constraint, + * the result will be flipped align to the closest edge which conforms to the constraint. + * + * @param {Array/Ext.util.Position} [options.position] The position at which to place the + * resulting region before being excluded from the target area and aligned to the closest + * edge which allows conformity with any passed `inside` option. Used instead of the `align` + * option. + * @param {Ext.util.Offset/Number[]} [options.offset] An offset by which to adjust the result. + * @param {Ext.util.Offset/Number[]} [options.anchorSize] The width and height of any external + * anchor + * element. This is used to calculate the true bounds of the Region inclusive of the anchor. + * The `x` dimension is the height of the arrow in all orientations, and the `y` dimension + * is the width of the baseline of the arrow in all dimensions. + * If this option is used, and the returned region successfully clears the + * bounds of the target, then the anchor region will be returned in the return value + * as the `anchor` property. This will in turn have a `position` property which will + * be `'top'`, `'left`, `'right'`, or `'bottom'`. + * @param {Boolean} [options.overlap] Pass `true` to allow this rectangle to overlap + * the target. + * @param {Boolean} [options.rtl] Pass `true` to swap left/right alignment. + * @param {Ext.util.Region/Ext.dom.Element} [options.inside] The rectangle to + * which this rectangle is constrained. + * @param {Number} [options.minHeight] Used when this Region is to be aligned directly + * below or above the target. Gives the option to reduce the height to fit in the + * available space. + * @param {Boolean} [options.axisLock] If `true`, then fallback on constraint violation will + * only take place along the major align axis. That is, if `align: "l-r"` is being used, and + * `axisLock: true` is used, then if constraints fail, only fallback to `r-l` is considered. + * @return {Ext.util.Region} The Region that will align this rectangle. Note that if + * a `minHeight` option was passed, and alignment is either above or below the target, + * the Region might be reduced to fit within the space. + */ + alignTo: function(options) { + var me = this, + Region = me.self, + Offset = ExtUtil.Offset, + Element = Ext.Element, + target = parseRegion(options.target), + targetPlusAnchorOffset, + rtl = options.rtl, + overlap = options.overlap, + align = options.align, + anchorSize = options.anchorSize, + offset = options.offset, + inside = options.inside, + position = options.position, + allowXTranslate = options.allowXTranslate, + allowYTranslate = options.allowYTranslate, + wasConstrained, result, initialPosition, constrainedPosition; + if (offset) { + offset = Offset.fromObject(offset); + if (!(offset instanceof Offset)) { + Ext.raise('offset option must be an Ext.util.Offset'); + } + } + if (anchorSize) { + anchorSize = Offset.fromObject(anchorSize); + if (!(anchorSize instanceof Offset)) { + Ext.raise('anchorSize option must be an Ext.util.Offset'); + } + } + if (inside && !inside.isRegion) { + if (Ext.getDom(inside) === document.body) { + inside = new Region(0, Element.getDocumentWidth(), Element.getDocumentHeight(), 0); + } else { + inside = Ext.fly(inside).getRegion(); + } + } + // Position the region using an exact position. + // Our purpose is then to constrain within the inside + // Region, while probably not occluding the target. + if (position) { + if (position.length === 2) { + position = new ExtUtil.Point(position[0], position[1]); + } + // Calculate the unconstrained position. + result = new Region().copyFrom(me).setPosition(position.x, position.y); + } else { + // Convert string align spec to informational object + align = me.getAlignInfo(align, rtl); + // target is out of bounds. + // Move it so that it's 1px inside to that the alignment points + if (inside) { + if (target.x >= inside.right) { + target.setPosition(inside.right - 1, target.y); + if (align.position !== 3) { + align = me.getAlignInfo('r-l', rtl); + } + } else if (target.right < inside.x) { + target.setPosition(inside.x - target.getWidth() + 1, target.y); + if (align.position !== 1) { + align = me.getAlignInfo('l-r', rtl); + } + } + if (target.y >= inside.bottom) { + target.setPosition(target.x, inside.bottom - 1); + if (align.position !== 0) { + align = me.getAlignInfo('b-t', rtl); + } + } else if (target.bottom < inside.y) { + target.setPosition(target.x, inside.y - target.getHeight() + 1); + if (align.position !== 2) { + align = me.getAlignInfo('t-b', rtl); + } + } + } + // Adjust the adjacent edge to account for the anchor height. + targetPlusAnchorOffset = anchorSize ? addAnchorOffset(target, anchorSize, align.position) : target; + // Start with requested position. + result = Region.from(me).translateBy(me.getAlignToVector(targetPlusAnchorOffset, align)); + // If they ASKED for it to intersect (eg: c-c, tl-c). we must honour that, + // and not exclude it. + overlap = !!result.intersect(targetPlusAnchorOffset); + if (offset && (overlap || !anchorSize)) { + result.translateBy(offset); + } + // Calculate the anchor position. + // This also forces the adjacent edges to overlap enough to create space + // for the anchor arrow. + if (anchorSize) { + calculateAnchorPosition(target, result, align.position, anchorSize, inside); + } + } + // If we are constraining Region... + if (inside) { + initialPosition = result.copy(); + // Constrain to within left boundary + if (result.left < inside.left) { + result.translateBy(inside.left - result.left, 0); + wasConstrained = true; + } + // If it overflows right, and there is space to move it left, then do so. + if (result.right > inside.right && result.left > inside.left) { + result.translateBy(inside.right - result.right, 0); + wasConstrained = true; + } + // Constrain to within top boundary + if (result.top < inside.top) { + result.translateBy(0, inside.top - result.top); + wasConstrained = true; + } + // If it overflows bottom, and there is space to move it up, then do so. + if (result.bottom > inside.bottom && result.top > inside.top) { + result.translateBy(0, inside.bottom - result.bottom); + wasConstrained = true; + } + // If we've budged the result to within the constrain bounds, + // ensure the result region does not overlay the target + if (wasConstrained && !overlap) { + // Recalculate it. We must return null if anchoring is not possible. + result.anchor = null; + // axisLock means that only flipping in the align axis is allowed, not fallback + // to all other sides. + // + // That is, if align is l-r, and the result won't fit, it only + // falls back to r-l. + // + // This will be used for BoundLists which must only flip from t0-b0 to b0-t0 + if (options.axisLock) { + if (align.position & 1) { + allowYTranslate = false; + } else { + allowXTranslate = false; + } + } + // If using an [X,Y] position, then only total occlusion causes exclusion + if (position) { + if (result.contains(position)) { + position.exclude(result, { + inside: inside, + centerOnSideChange: false + }); + } + } else // If edge aligning, we must completely exclude the region + { + constrainedPosition = result.copy(); + if (result.intersect(targetPlusAnchorOffset)) { + // This will also exclude any additional anchor even if the region itself + // does not intersect. + align.position = target.exclude(result, { + initialPosition: initialPosition, + defaultPosition: align.position, + inside: inside, + minHeight: options.minHeight, + minWidth: options.minWidth, + allowX: allowXTranslate, + allowY: allowYTranslate, + offset: offset, + anchorHeight: anchorSize ? anchorSize.y : 0, + centerOnSideChange: !!anchorSize + }); + } else if (options.minWidth && result.getWidth() > inside.getWidth()) { + result.setPosition(0, result.y); + result.setWidth(Math.max(inside.getWidth(), options.minWidth)); + result.constrainWidth = true; + } else if (options.minHeight && result.getHeight() > inside.getHeight()) { + result.setPosition(result.x, 0); + result.setHeight(Math.max(inside.getHeight(), options.minHeight)); + result.constrainHeight = true; + } + result.align = align; + if (inside.contains(result)) { + // Calculate the anchor position. + // This also forces the adjacent edges to overlap enough to create space + // for the anchor arrow. + if (anchorSize) { + calculateAnchorPosition(target, result, align.position, anchorSize, inside); + } + } else // We tried everything, but couldn't fit in the "inside" region. + // Fall back to the constrained position overlapping the target. + // Usually happens on a phone where there's not enough space to edge-align + // and insist on no overlapping of align target . + { + result = constrainedPosition; + } + } + } + } + return result; + }, + /** + * This method pushes the "other" Region out of this region via the shortest + * translation. If an "inside" Region is passed, the exclusion also honours + * that constraint. + * @param {Region} other The Region to move so that it does not intersect this Region. + * @param {Object} options Object of options passed to exclude. + * @param {Region} options.inside A Region into which the other Region must be constrained. + * @param {Number} [options.minHeight] If passed, indicates that the height may be reduced up + * to a point to fit the "other" region below or above the target but within the "inside" + * Region. + * @param {Boolean} [options.allowX=true] Pass `false` to disallow translation along the X axis. + * @param {Boolean} [options.allowY=true] Pass `false` to disallow translation along the Y axis. + * @return {Number} The edge it is now aligned to, 0=top, 1=right, 2=bottom, 3=left. + */ + exclude: function(other, options) { + options = options || {}; + // eslint-disable-next-line vars-on-top + var me = this, + initialPosition = options.initialPosition || other, + inside = options.inside, + defaultPosition = options.defaultPosition, + centerOnSideChange = options.centerOnSideChange, + minHeight = options.minHeight, + minWidth = options.minWidth, + allowX = options.allowX !== false, + allowY = options.allowY !== false, + anchorHeight = options.anchorHeight, + offset = options.offset, + translations = [], + testRegion, t, i, sizeConstrainedSolution, leastBadSolution, intersection, result; + // Create adjustments for each dimension so we can also exclude any anchor + if (!offset) { + offset = zeroOffset; + } + // Calculate vectors to move the "other" region by to fully clear this region. + // Store the total moved distance, (element [4]) as the distance from the initially + // desired position, not the constrained, overlapped position. + /* eslint-disable max-len */ + if (allowY) { + translations.push([ + 0, + me.top - other.bottom - anchorHeight + offset.y, + 'b-t', + 0, + Math.abs(me.top - initialPosition.bottom - anchorHeight + offset.y) + ]); + translations.push([ + 0, + me.bottom - other.top + anchorHeight + offset.y, + 't-b', + 2, + Math.abs(me.bottom - initialPosition.top + anchorHeight + offset.y) + ]); + } else { + centerOnSideChange = false; + } + if (allowX) { + translations.push([ + me.left - other.right - anchorHeight + offset.x, + 0, + 'r-l', + 3, + Math.abs(me.left - initialPosition.right - anchorHeight + offset.x) + ]); + translations.push([ + me.right - other.left + anchorHeight + offset.x, + 0, + 'l-r', + 1, + Math.abs(me.right - initialPosition.left + anchorHeight + offset.x) + ]); + } else { + centerOnSideChange = false; + } + /* eslint-enable max-len */ + // Sort the exclusion vectors into order, shortest first + Ext.Array.sort(translations, function(l, r) { + var result = l[4] - r[4]; + // If equidistant, prefer the translation which moves to the defaultPosition + if (!result) { + if (l[3] === defaultPosition) { + return -1; + } + if (r[3] === defaultPosition) { + return 1; + } + } + return result; + }); + // We might have to fall back through the choices of direction + // until we find one which doesn't violate the constraints. + if (inside) { + for (i = 0; i < translations.length; i++) { + t = translations[i]; + testRegion = ExtUtil.Region.from(other); + testRegion.translateBy.apply(testRegion, t); + // When we find a translation that satisfies the constraint, we're done + if (inside.contains(testRegion)) { + other.copyFrom(testRegion); + result = { + align: t[2], + position: t[3], + distance: t[4] + }; + break; + } + // If we are directly above or below and we are allowed to shrink the + // height, and it's too high, then calculate a height constrained solution + // to which we can fall back if no translations are fully successful. + if (minHeight) { + checkMinHeight(minHeight, testRegion, me, inside); + if (inside.contains(testRegion)) { + // eslint-disable-next-line max-len + if (!sizeConstrainedSolution || testRegion.getArea() > sizeConstrainedSolution.region.getArea()) { + sizeConstrainedSolution = { + region: testRegion, + align: t[2], + position: t[3], + distance: t[4] + }; + } + } + } + if (minWidth) { + checkMinWidth(minWidth, testRegion, me, inside); + if (inside.contains(testRegion)) { + // eslint-disable-next-line max-len + if (!sizeConstrainedSolution || testRegion.getArea() > sizeConstrainedSolution.region.getArea()) { + sizeConstrainedSolution = { + region: testRegion, + align: t[2], + position: t[3], + distance: t[4] + }; + } + } + } + // If all else fails, keep track of the translation which yields the largest + // intersection with the "inside" region. If there's no translation which satisfies + // the constraint, use this least bad one. + intersection = inside.intersect(testRegion); + if (intersection) { + intersection = intersection.getArea(); + // eslint-disable-next-line max-len + if (!leastBadSolution || (intersection && leastBadSolution.area < intersection)) { + leastBadSolution = { + region: testRegion, + align: t[2], + position: t[3], + distance: t[4], + area: intersection + }; + } + } + } + if (!result) { + // Only constrain height if other translations fail. + if (sizeConstrainedSolution) { + other.copyFrom(sizeConstrainedSolution.region); + result = sizeConstrainedSolution; + other.constrainWidth = sizeConstrainedSolution.region.constrainWidth; + other.constrainHeight = sizeConstrainedSolution.region.constrainHeight; + } + // Only use the least bad failed solution as a last resort. + else if (leastBadSolution) { + other.copyFrom(leastBadSolution.region); + result = leastBadSolution; + } + } + if (result) { + // The exclude switched align axis (t/b to l/r), flip it to a center align on + // the new side. + if ((result.position & 1) !== (defaultPosition & 1)) { + if (result.distance && centerOnSideChange) { + t = other.alignTo({ + align: result.align, + target: me, + anchorSize: anchorHeight, + offset: offset, + axisLock: true, + inside: inside, + minHeight: options.minHeight, + minWidth: options.minWidth + }); + if (inside.contains(t)) { + other.setPosition(t.x, t.y); + } + } + } + return result.position; + } + } else // No external constraint + { + // Move by the shortest path + other.translateBy.apply(other, translations[0]); + return translations[0][3]; + } + return defaultPosition; + }, + getAlignToXY: function(target, align, rtl) { + var alignVector = this.getAlignToVector(target, align, rtl); + return [ + this.x + alignVector[0], + this.y + alignVector[1] + ]; + }, + getAnchorPoint: function(align, rtl) { + align = (typeof align === 'string') ? this.getAlignInfo(align + '-tl', rtl) : align; + return this['getAnchorPoint_' + align.myEdge](align.myOffset); + }, + getAlignToVector: function(target, align, rtl) { + align = (typeof align === 'string') ? this.getAlignInfo(align, rtl) : align; + // eslint-disable-next-line vars-on-top + var myAnchorPoint = this['getAnchorPoint_' + align.myEdge](align.myOffset), + targetAnchorPoint = target['getAnchorPoint_' + align.otherEdge](align.otherOffset); + return [ + targetAnchorPoint[0] - myAnchorPoint[0], + targetAnchorPoint[1] - myAnchorPoint[1] + ]; + }, + getAnchorPoint_t: function(offset) { + return [ + this.x + Math.round(this.getWidth() * (offset / 100)), + this.y + ]; + }, + getAnchorPoint_b: function(offset) { + return [ + this.x + Math.round(this.getWidth() * (offset / 100)), + this.bottom + ]; + }, + getAnchorPoint_l: function(offset) { + return [ + this.x, + this.y + Math.round(this.getHeight() * (offset / 100)) + ]; + }, + getAnchorPoint_r: function(offset) { + return [ + this.right, + this.y + Math.round(this.getHeight() * (offset / 100)) + ]; + }, + getAnchorPoint_c: function() { + return [ + this.x + Math.round(this.getWidth() / 2), + this.y + Math.round(this.getHeight() / 2) + ]; + }, + getCenter: function() { + return [ + this.x + this.width / 2, + this.y + this.height / 2 + ]; + }, + getHeight: function() { + return this.bottom - this.y; + }, + getWidth: function() { + return this.right - this.x; + }, + getArea: function() { + return this.getHeight() * this.getWidth(); + }, + setHeight: function(h) { + this.height = h; + this.bottom = this.top + h; + return this; + }, + setWidth: function(w) { + this.width = w; + this.right = this.left + w; + return this; + }, + /** + * Get the width / height of this region + * @return {Object} an object with width and height properties + * @private + */ + getSize: function() { + return { + width: this.right - this.x, + height: this.bottom - this.y + }; + }, + setSize: function(w, h) { + if (h === undefined) { + h = w; + } + this.setWidth(w); + return this.setHeight(h); + }, + /** + * Create a copy of this Region. + * @return {Ext.util.Region} + */ + copy: function() { + return new this.self(this.y, this.right, this.bottom, this.x); + }, + /** + * Copy the values of another Region to this Region + * @param {Ext.util.Region} p The region to copy from. + * @return {Ext.util.Region} This Region + */ + copyFrom: function(p) { + var me = this; + me.top = me.y = me[1] = p.y; + me.right = p.right; + me.bottom = p.bottom; + me.left = me.x = me[0] = p.x; + return me; + }, + /* + * Dump this to an eye-friendly string, great for debugging + * @return {String} + */ + toString: function() { + return "Region[" + this.top + "," + this.right + "," + this.bottom + "," + this.left + "]"; + }, + /** + * Translate this Region by the given offset amount + * @param {Ext.util.Offset/Object} x Object containing the `x` and `y` properties. + * Or the x value is using the two argument form. + * @param {Number} y The y value unless using an Offset object. + * @return {Ext.util.Region} this This Region + */ + translateBy: function(x, y) { + var me = this; + if (x.length) { + y = x[1]; + x = x[0]; + } else if (arguments.length === 1) { + y = x.y; + x = x.x; + } + me.top = me.y += y; + me.right += x; + me.bottom += y; + me.left = me.x += x; + return me; + }, + /** + * Round all the properties of this region + * @return {Ext.util.Region} this This Region + */ + round: function() { + var me = this; + me.top = me.y = Math.round(me.y); + me.right = Math.round(me.right); + me.bottom = Math.round(me.bottom); + me.left = me.x = Math.round(me.x); + return me; + }, + /** + * Check whether this region is equivalent to the given region + * @param {Ext.util.Region} region The region to compare with + * @return {Boolean} + */ + equals: function(region) { + return (this.top === region.top && this.right === region.right && this.bottom === region.bottom && this.left === region.left); + }, + /** + * Returns the offsets of this region from the passed region or point. + * @param {Ext.util.Region/Ext.util.Point} offsetsTo The region or point to get get + * the offsets from. + * @return {Object} The XY page offsets + * @return {Number} return.x The x offset + * @return {Number} return.y The y offset + */ + getOffsetsTo: function(offsetsTo) { + return { + x: this.x - offsetsTo.x, + y: this.y - offsetsTo.y + }; + }, + highlight: function() { + // eslint-disable-line comma-style + var highlightEl = Ext.getBody().createChild({ + style: 'background-color:#52a0db;opacity:0.4;position:absolute;z-index:9999999' + }); + highlightEl.setBox(this); + Ext.defer(function() { + highlightEl.destroy(); + }, 5000); + return highlightEl; + } + }; +}, function(Region) { + Region.prototype.getAlignInfo = Region.getAlignInfo; + Region.EMPTY = new Region(0, 0, 0, 0); + if (Object.freeze) { + Object.freeze(Region.EMPTY); + } +}); + +/** + * Represents a 2D point with x and y properties, useful for comparison and instantiation + * from an event: + * + * var point = Ext.util.Point.fromEvent(e); + */ +Ext.define('Ext.util.Point', { + extend: Ext.util.Region, + isPoint: true, + radianToDegreeConstant: 180 / Math.PI, + origin: { + x: 0, + y: 0 + }, + statics: { + /** + * Returns a new instance of {@link Ext.util.Point} based on the `pageX` / `pageY` values + * of the given event. + * @static + * @param {Event} e The event. + * @return {Ext.util.Point} + */ + fromEvent: function(e) { + var changedTouches = e.changedTouches, + touch = (changedTouches && changedTouches.length > 0) ? changedTouches[0] : e; + return this.fromTouch(touch); + }, + /** + * Returns a new instance of {@link Ext.util.Point} based on the `pageX` / `pageY` values + * of the given touch. + * @static + * @param {Event} touch + * @return {Ext.util.Point} + */ + fromTouch: function(touch) { + return new this(touch.pageX, touch.pageY); + }, + /** + * Returns a new point from an object that has `x` and `y` properties, if that object + * is not an instance of {@link Ext.util.Point}. Otherwise, returns the given point itself. + * @param {Object} object + * @return {Ext.util.Point} + */ + from: function(object) { + if (!object) { + return new this(0, 0); + } + if (!(object instanceof this)) { + return new this(object.x, object.y); + } + return object; + } + }, + /** + * Creates point on 2D plane. + * @param {Number} [x=0] X coordinate. + * @param {Number} [y=0] Y coordinate. + */ + constructor: function(x, y) { + if (x == null) { + x = 0; + } + if (y == null) { + y = 0; + } + this.callParent([ + y, + x, + y, + x + ]); + }, + /** + * Copy a new instance of this point. + * @return {Ext.util.Point} The new point. + */ + clone: function() { + return new this.self(this.x, this.y); + }, + /** + * Clones this Point. + * @deprecated 2.0.0 Please use {@link #clone} instead. + * @return {Ext.util.Point} The new point. + */ + copy: function() { + return this.clone.apply(this, arguments); + }, + /** + * Copy the `x` and `y` values of another point / object to this point itself. + * @param {Ext.util.Point/Object} point. + * @return {Ext.util.Point} This point. + */ + copyFrom: function(point) { + this.x = point.x; + this.y = point.y; + return this; + }, + /** + * Returns a human-eye-friendly string that represents this point, + * useful for debugging. + * @return {String} For example `Point[12,8]`. + */ + toString: function() { + return "Point[" + this.x + "," + this.y + "]"; + }, + /** + * Compare this point and another point. + * @param {Ext.util.Point/Object} point The point to compare with, either an instance + * of {@link Ext.util.Point} or an object with `x` and `y` properties. + * @return {Boolean} Returns whether they are equivalent. + */ + equals: function(point) { + return (this.x === point.x && this.y === point.y); + }, + /** + * Returns `true` if the passed point is within a certain distance of this point. + * @param {Ext.util.Point/Object} point The point to check with, either an instance + * of {@link Ext.util.Point} or an object with `x` and `y` properties. + * @param {Object/Number} threshold Can be either an object with `x` and `y` properties + * or a number. + * @return {Boolean} + */ + isCloseTo: function(point, threshold) { + if (typeof threshold === 'number') { + return this.getDistanceTo(point) <= threshold; + } + // eslint-disable-next-line vars-on-top + var x = point.x, + y = point.y, + thresholdX = threshold.x, + thresholdY = threshold.y; + return (this.x <= x + thresholdX && this.x >= x - thresholdX && this.y <= y + thresholdY && this.y >= y - thresholdY); + }, + /** + * Returns `true` if this point is close to another one. + * @deprecated 2.0.0 Please use {@link #isCloseTo} instead. + * @return {Boolean} + */ + isWithin: function() { + return this.isCloseTo.apply(this, arguments); + }, + /** + * Determins whether this Point contained by the passed Region, Component or element. + * @param {Ext.util.Region/Ext.Component/Ext.dom.Element/HTMLElement} region + * The rectangle to check that this Point is within. + * @return {Boolean} + */ + isContainedBy: function(region) { + if (!(region instanceof Ext.util.Region)) { + region = Ext.get(region.el || region).getRegion(); + } + return region.contains(this); + }, + /** + * Compare this point with another point when the `x` and `y` values of both points are rounded. + * For example: [100.3,199.8] will equals to [100, 200]. + * @param {Ext.util.Point/Object} point The point to compare with, either an instance + * of Ext.util.Point or an object with `x` and `y` properties. + * @return {Boolean} + */ + roundedEquals: function(point) { + if (!point || typeof point !== 'object') { + point = this.origin; + } + return (Math.round(this.x) === Math.round(point.x) && Math.round(this.y) === Math.round(point.y)); + }, + getDistanceTo: function(point) { + if (!point || typeof point !== 'object') { + point = this.origin; + } + // eslint-disable-next-line vars-on-top + var deltaX = this.x - point.x, + deltaY = this.y - point.y; + return Math.sqrt(deltaX * deltaX + deltaY * deltaY); + }, + getAngleTo: function(point) { + if (!point || typeof point !== 'object') { + point = this.origin; + } + // eslint-disable-next-line vars-on-top + var deltaX = this.x - point.x, + deltaY = this.y - point.y; + return Math.atan2(deltaY, deltaX) * this.radianToDegreeConstant; + } +}, function() { + /** + * @method translate + * @member Ext.util.Point + * Alias for {@link #translateBy} + * @inheritdoc Ext.util.Region#method-translateBy + */ + this.prototype.translate = this.prototype.translateBy; +}); + +/** + * Just as {@link Ext.dom.Element} wraps around a native DOM node, {@link Ext.event.Event} wraps + * the browser's native event-object normalizing cross-browser differences such as mechanisms + * to stop event-propagation along with a method to prevent default actions from taking place. + * + * Here is a simple example of how you use it: + * + * @example + * var container = Ext.create('Ext.Container', { + * layout: 'fit', + * renderTo: Ext.getBody(), + * items: [{ + * id: 'logger', + * html: 'Click somewhere!', + * padding: 5 + * }] + * }); + * + * container.getEl().on({ + * click: function(e, node) { + * var string = ''; + * + * string += 'You clicked at: { x: ' + e.pageX + ', y: ' + e.pageY + ' } ' + + '(e.pageX & e.pageY)'; + * string += '
'; + * string += 'The HTMLElement you clicked has the className of: ' + + e.target.className + ' (e.target)'; + * string += '
'; + * string += 'The HTMLElement which has the listener has a className of: ' + + e.currentTarget.className + ' (e.currentTarget)'; + * + * Ext.getCmp('logger').setHtml(string); + * } + * }); + * + * ## Recognizers + * + * Ext JS includes many default event recognizers to know when a user interacts with + * the application. + * + * For a full list of default recognizers, and more information, please view the + * {@link Ext.event.gesture.Recognizer} documentation. + * + * This class also provides a set of constants for use with key events. These are useful + * for determining if a specific key was pressed, and are available both on instances, + * and as static properties of the class. The following two statements are equivalent: + * + * if (e.getKey() === Ext.event.Event.TAB) { + * // tab key was pressed + * } + * + * if (e.getKey() === e.TAB) { + * // tab key was pressed + * } + */ +Ext.define('Ext.event.Event', { + alternateClassName: 'Ext.EventObjectImpl', + /** + * @property {Number} distance + * The distance of the event. + * + * **This is only available when the event type is `swipe` and `pinch`.** + */ + /** + * @property {HTMLElement} target + * The element that fired this event. For the element whose handlers are currently + * being processed, i.e. the element that the event handler was attached to, use + * `currentTarget` + */ + /** + * @property {HTMLElement} currentTarget + * Refers to the element the event handler was attached to, vs the `target`, which is + * the actual element that fired the event. For example, if the event bubbles, the + * `target` element may be a descendant of the `currentTarget`, as the event may + * have been triggered on the `target` and then bubbled up to the `currentTarget` + * where it was handled. + */ + /** + * @property {HTMLElement} delegatedTarget + * Same as `currentTarget` + * @deprecated 5.0.0 use {@link #currentTarget} instead. + */ + /** + * @property {Number} button + * Indicates which mouse button caused the event for mouse events, for example + * `mousedown`, `click`, `mouseup`: + * - `0` for left button. + * - `1` for middle button. + * - `2` for right button. + * + * *Note*: In IE8 & IE9, the `click` event does not provide the button. + */ + /** + * @property {Number} pageX The browsers x coordinate of the event. + * Note: this only works in browsers that support pageX on the native browser event + * object (pageX is not natively supported in IE9 and earlier). In Ext JS, for a + * cross browser normalized x-coordinate use {@link #getX} + */ + /** + * @property {Number} pageY The browsers y coordinate of the event. + * Note: this only works in browsers that support pageY on the native browser event + * object (pageY is not natively supported in IE9 and earlier). In Ext JS, for a + * cross browser normalized y-coordinate use {@link #getY} + */ + /** + * @property {Boolean} ctrlKey + * True if the control key was down during the event. + * In Mac this will also be true when meta key was down. + */ + /** + * @property {Boolean} altKey + * True if the alt key was down during the event. + */ + /** + * @property {Boolean} shiftKey + * True if the shift key was down during the event. + */ + /** + * @property {Event} browserEvent + * The raw browser event which this object wraps. + */ + /** + * @property {String} pointerType + * The pointer type for this event. May be empty if the event was + * not triggered by a pointer. Current available types are: + * - `mouse` + * - `touch` + * - `pen` + */ + /** + * @property {Boolean} + * `true` if {@link #stopPropagation} has been called on this instance + * @private + */ + stopped: false, + /** + * @property {Boolean} + * `true` if {@link #claimGesture} has been called on this instance + * @private + */ + claimed: false, + /** + * @property {Boolean} + * Indicates whether or not {@link #preventDefault preventDefault()} was called on the event. + */ + defaultPrevented: false, + isEvent: true, + // With these events, the relatedTarget can sometimes be an Object literal in FF. + geckoRelatedTargetEvents: { + blur: 1, + dragenter: 1, + dragleave: 1, + focus: 1 + }, + statics: { + resolveTextNode: function(node) { + return (node && node.nodeType === 3) ? node.parentNode : node; + }, + /** + * @private + * An amalgamation of pointerEvents/mouseEvents/touchEvents. + * Will be populated in class callback. + */ + gestureEvents: {}, + /** + * @private + */ + pointerEvents: { + pointerdown: 1, + pointermove: 1, + pointerup: 1, + pointercancel: 1, + pointerover: 1, + pointerout: 1, + pointerenter: 1, + pointerleave: 1, + MSPointerDown: 1, + MSPointerMove: 1, + MSPointerUp: 1, + MSPointerOver: 1, + MSPointerOut: 1, + MSPointerCancel: 1, + MSPointerEnter: 1, + MSPointerLeave: 1 + }, + /** + * @private + */ + mouseEvents: { + mousedown: 1, + mousemove: 1, + mouseup: 1, + mouseover: 1, + mouseout: 1, + mouseenter: 1, + mouseleave: 1 + }, + /** + * @private + * These are tracked separately from mouseEvents because the mouseEvents map + * is used by Dom publisher to eliminate duplicate events on devices that fire + * multiple kinds of events (mouse, touch, pointer). Adding click events to the + * mouse events map can cause click events to be blocked from firing in some cases. + */ + clickEvents: { + click: 1, + dblclick: 1 + }, + /** + * @private + */ + touchEvents: { + touchstart: 1, + touchmove: 1, + touchend: 1, + touchcancel: 1 + }, + /** + * @private + */ + focusEvents: { + focus: 1, + focusin: 1, + focusenter: 1 + }, + /** + * @private + */ + blurEvents: { + blur: 1, + focusout: 1, + focusleave: 1 + }, + /** + * @private + */ + wheelEvents: { + wheel: 1, + mousewheel: 1 + }, + // msPointerTypes in IE10 are numbers, in the w3c spec they are strings. + // this map allows us to normalize the pointerType for an event + // http://www.w3.org/TR/pointerevents/#widl-PointerEvent-pointerType + // http://msdn.microsoft.com/en-us/library/ie/hh772359(v=vs.85).aspx + pointerTypeMap: { + 2: 'touch', + 3: 'pen', + 4: 'mouse', + touch: 'touch', + pen: 'pen', + mouse: 'mouse' + }, + keyEventRe: /^key/, + keyFlags: { + CTRL: 'ctrlKey', + CONTROL: 'ctrlKey', + ALT: 'altKey', + SHIFT: 'shiftKey', + CMD: 'metaKey', + COMMAND: 'metaKey', + CMDORCTRL: Ext.isMac ? 'metaKey' : 'ctrlKey', + COMMANDORCONTROL: Ext.isMac ? 'metaKey' : 'ctrlKey', + META: 'metaKey' + }, + modifierGlyphs: { + ctrlKey: '⌃', + altKey: '⌥', + metaKey: Ext.isMac ? '⌘' : '⊞', + shiftKey: '⇧' + }, + specialKeyGlyphs: { + BACKSPACE: '⌫', + TAB: '⇥', + ENTER: '⏎', + RETURN: '⏎', + SPACE: '␣', + PAGE_UP: '⇞', + PAGE_DOWN: '⇟', + END: '⇲', + HOME: '⌂', + LEFT: '←', + UP: '↑', + RIGHT: '→', + DOWN: '↓', + PRINT_SCREEN: '⎙', + INSERT: '⎀', + DELETE: '⌦', + CONTEXT_MENU: '☰' + }, + // eslint-disable-next-line no-useless-escape + _hyphenRe: /^[a-z]+\-/i, + // eg "Ctrl-" (instead of "Ctrl+") + /** + * Convert a key specification in the form eg: "CTRL+ALT+DELETE" to the glyph sequence + * for use in menu items, eg "⌃⌥⌦". + * @private + */ + getKeyId: function(keyName) { + // Translate eg: 27 to "ESC" + if (typeof keyName === 'number') { + keyName = this.keyCodes[keyName]; + } else { + keyName = keyName.toUpperCase(); + } + // eslint-disable-next-line vars-on-top + var me = this, + delim = me._hyphenRe.test(keyName) ? '-' : '+', + parts = (keyName === delim) ? [ + delim + ] : keyName.split(delim), + numModifiers = parts.length - 1, + rawKey = parts[numModifiers], + result = [], + eventFlag, i; + if (!Ext.event.Event[rawKey]) { + Ext.raise('Invalid key name: "' + rawKey + '"'); + } + for (i = 0; i < numModifiers; i++) { + eventFlag = me.keyFlags[parts[i]]; + if (!eventFlag) { + Ext.raise('Invalid key modifier: "' + parts[i] + '"'); + } + result[eventFlag] = true; + } + if (result.ctrlKey) { + result.push(me.modifierGlyphs.ctrlKey); + } + if (result.altKey) { + result.push(me.modifierGlyphs.altKey); + } + if (result.shiftKey) { + result.push(me.modifierGlyphs.shiftKey); + } + if (result.metaKey) { + result.push(me.modifierGlyphs.metaKey); + } + result.push(this.specialKeyGlyphs[rawKey] || rawKey); + return result.join(''); + }, + /** + * @private + */ + globalTabKeyDown: function(e) { + if (e.keyCode === 9) { + Ext.event.Event.forwardTab = !e.shiftKey; + } + }, + /** + * @private + */ + globalTabKeyUp: function(e) { + if (e.keyCode === 9) { + delete Ext.event.Event.forwardTab; + } + } + }, + constructor: function(event) { + var me = this, + self = me.self, + resolveTextNode = me.self.resolveTextNode, + changedTouches = event.changedTouches, + // The target object from which to obtain the coordinates (pageX, pageY). For + // mouse and pointer events this is simply the event object itself, but touch + // events have their coordinates on the "Touch" object(s) instead. + coordinateOwner = changedTouches ? changedTouches[0] : event, + type = event.type, + pointerType, relatedTarget; + // This is a milliseconds value with decimal precision. + me.timeStamp = me.time = Ext.ticks(); + me.pageX = coordinateOwner.pageX; + me.pageY = coordinateOwner.pageY; + me.clientX = coordinateOwner.clientX; + me.clientY = coordinateOwner.clientY; + me.target = me.delegatedTarget = resolveTextNode(event.target); + me.currentTarget = resolveTextNode(event.currentTarget); + relatedTarget = event.relatedTarget; + if (relatedTarget) { + if (Ext.isGecko && me.geckoRelatedTargetEvents[type]) { + try { + me.relatedTarget = resolveTextNode(relatedTarget); + } catch (e) { + me.relatedTarget = null; + } + } else { + me.relatedTarget = resolveTextNode(relatedTarget); + } + } + me.browserEvent = me.event = event; + me.type = type; + // set button to 0 if undefined so that touchstart, touchend, and tap will quack + // like left mouse button mousedown mouseup, and click + me.button = event.button || 0; + me.shiftKey = event.shiftKey; + // mac metaKey behaves like ctrlKey + me.ctrlKey = event.ctrlKey || event.metaKey || false; + me.altKey = event.altKey; + me.charCode = event.charCode; + me.keyCode = event.keyCode; + me.buttons = event.buttons; + // When invoking synthetic events, current APIs do not + // have the ability to specify the buttons config, which + // defaults to button. For buttons, 0 means no button + // is pressed, whereas for button, 0 means left click. + // Normalize that here + if (me.button === 0 && me.buttons === 0) { + me.buttons = 1; + } + if (self.focusEvents[type] || self.blurEvents[type]) { + if (self.forwardTab !== undefined) { + me.forwardTab = self.forwardTab; + } + if (self.focusEvents[type]) { + me.fromElement = event.relatedTarget; + me.toElement = event.target; + } else { + me.fromElement = event.target; + me.toElement = event.relatedTarget; + } + } else if (type !== 'keydown') { + // Normally this property should be cleaned up in keyup handler; + // however that one might never come if something prevented default + // on the keydown. Make sure the property won't get stuck. + delete self.forwardTab; + } + if (self.mouseEvents[type]) { + pointerType = 'mouse'; + } else if (self.clickEvents[type]) { + // On pointer events browsers like IE11 and Edge click events have a pointerType + // property. On touch events devices we check if the last touchend event + // happened less than a second ago - if so we can reasonably assume that + // the click event happened because of a tap on the screen. + pointerType = self.pointerTypeMap[event.pointerType] || (((Ext.now() - Ext.event.publisher.Dom.lastTouchEndTime) < 1000) ? 'touch' : 'mouse'); + } + // eslint-disable-line max-len + else if (self.pointerEvents[type]) { + // In electron the mousemove event when the mouse first enters the document + // can have a pointerType of "", so default it to mouse if not found in the map. + pointerType = self.pointerTypeMap[event.pointerType] || 'mouse'; + } else if (self.touchEvents[type]) { + pointerType = 'touch'; + } + if (pointerType) { + me.pointerType = pointerType; + } + // Is this is not the primary touch for PointerEvents (first touch) + // or there are multiples touches for Touch Events + me.isMultitouch = event.isPrimary === false || (event.touches && event.touches.length > 1); + if (self.wheelEvents[type]) { + me.getWheelDeltas(); + } + }, + // call deprecated method to initialize deltaX and deltaY props + /** + * Creates a new Event object that is prototype-chained to this event. Useful for + * creating identical events so that certain properties can be changed without + * affecting the original event. For example, translated events have their "type" + * corrected in this manner. + * @param {Object} props properties to set on the chained event + * @private + */ + chain: function(props) { + var e = Ext.Object.chain(this); + e.parentEvent = this; + // needed for stopPropagation + return Ext.apply(e, props); + }, + /** + * Correctly scales a given wheel delta. + * @param {Number} delta The delta value. + * @private + */ + correctWheelDelta: function(delta) { + var me = this, + // 0 = pixel + // 1 = line + // 2 = page + deltaMode = me.browserEvent.deltaMode, + correctedDelta = delta; + if (deltaMode === 0) { + correctedDelta = delta * me.WHEEL_PIXEL_SIZE; + } else if (deltaMode === 1) { + correctedDelta = delta * me.WHEEL_LINE_SIZE; + } else if (deltaMode === 2) { + correctedDelta = delta * me.WHEEL_PAGE_SIZE; + } + return Math.round(correctedDelta); + }, + getChar: function() { + var r = this.which(); + return String.fromCharCode(r); + }, + /** + * Gets the character code for the event. + * @return {Number} + */ + getCharCode: function() { + return this.charCode || this.keyCode; + }, + /** + * Returns a normalized keyCode for the event. + * @return {Number} The key code + */ + getKey: function() { + return this.keyCode || this.charCode; + }, + /** + * Returns the name of the keyCode for the event. + * @return {String} The key name + */ + getKeyName: function() { + return this.type === 'keypress' ? String.fromCharCode(this.getCharCode()) : this.keyCodes[this.keyCode]; + }, + /** + * Returns the `key` property of a keyboard event. + * @return {String} + */ + key: function() { + return this.browserEvent.key; + }, + which: function() { + var me = this, + e = me.browserEvent, + r = e.which; + if (r == null) { + if (me.self.keyEventRe.test(e.type)) { + r = e.charCode || e.keyCode; + } else if ((r = e.button) !== undefined) { + // L M R button codes (1, 4, 2): + r = (r & 1) ? 1 : ((r & 4) ? 2 : ((r & 2) ? 3 : 0)); + } + } + return r; + }, + /** + * If this is an event of {@link #type} `paste`, this returns the clipboard data + * of the pasesd mime type. + * + * @param {String} [type='text/plain'] The mime type of the data to extract from the + * clipabord. + * + * Note that this uses non-standard browaer APIs and may not work reliably on all + * platforms. + * @return {Mixed} The clipboard data. + * @since 6.5.1 + */ + getClipboardData: function(type) { + var clipboardData = this.browserEvent.clipboardData, + clipIE = Ext.global.clipboardData, + // IE + result = null, + typeIE; + type = type || 'text/plain'; + if (clipboardData && clipboardData.getData) { + result = clipboardData.getData(type); + } else if (clipIE && clipIE.getData) { + typeIE = this.ieMimeType[type]; + if (typeIE) { + result = clipIE.getData(typeIE); + } + } + return result; + }, + /** + * Returns a point object that consists of the object coordinates. + * @return {Ext.util.Point} point + */ + getPoint: function() { + var me = this, + point = me.point, + xy; + if (!point) { + xy = me.getXY(); + point = me.point = new Ext.util.Point(xy[0], xy[1]); + } + return point; + }, + /** + * Gets the related target. + * @param {String} [selector] A simple selector to filter the target or look for an + * ancestor of the target. See {@link Ext.dom.Query} for information about simple + * selectors. + * @param {Number/HTMLElement} [maxDepth] The max depth to search as a number or + * element (defaults to 10 || document.body). + * @param {Boolean} [returnEl] `true` to return a Ext.Element object instead of DOM + * node. + * @return {HTMLElement} + */ + getRelatedTarget: function(selector, maxDepth, returnEl) { + var relatedTarget = this.relatedTarget, + target = null; + // In some cases in IE10/11, when the mouse is leaving the document over a scrollbar + // the relatedTarget will be an empty object literal. So just check we have an element + // looking object here before we proceed. + if (relatedTarget && relatedTarget.nodeType) { + if (selector) { + target = Ext.fly(relatedTarget).findParent(selector, maxDepth, returnEl); + } else { + target = returnEl ? Ext.get(relatedTarget) : relatedTarget; + } + } + return target; + }, + /** + * Gets the target for the event. + * @param {String} selector (optional) A simple selector to filter the target or look + * for an ancestor of the target + * @param {Number/Mixed} [maxDepth=10||document.body] (optional) The max depth to + * search as a number or element (defaults to 10 || document.body) + * @param {Boolean} returnEl (optional) `true` to return a Ext.Element object instead + * of DOM node. + * @return {HTMLElement} + */ + getTarget: function(selector, maxDepth, returnEl) { + return selector ? Ext.fly(this.target).findParent(selector, maxDepth, returnEl) : (returnEl ? Ext.get(this.target) : this.target); + }, + /** + * Returns the time of the event. + * @return {Date} + */ + getTime: function() { + return this.time; + }, + /** + * Normalizes mouse wheel y-delta across browsers. To get x-delta information, use + * {@link #getWheelDeltas} instead. + * @return {Number} The mouse wheel y-delta + * @deprecated 6.6.0 Use {@link #deltaY} instead + */ + getWheelDelta: function() { + var deltas = this.getWheelDeltas(); + return deltas.y; + }, + /** + * Returns the mouse wheel deltas for this event. + * @return {Object} An object with "x" and "y" properties holding the mouse wheel deltas. + * @deprecated 6.7.0 Use {@link #deltaX} and {@link #deltaY} instead + */ + getWheelDeltas: function() { + var me = this, + wheelDeltas = me.wheelDeltas, + browserEvent, deltaX, deltaY; + if (!wheelDeltas) { + browserEvent = me.browserEvent; + deltaX = me.correctWheelDelta(browserEvent.deltaX || 0); + deltaY = browserEvent.deltaY; + deltaY = me.correctWheelDelta(deltaY == null ? -browserEvent.wheelDelta : deltaY); + /** + * @property {Number} deltaX + */ + me.deltaX = deltaX; + /** + * @property {Number} deltaY + */ + me.deltaY = deltaY; + me.wheelDeltas = wheelDeltas = { + x: deltaX, + y: deltaY + }; + } + return wheelDeltas; + }, + /** + * Gets the x coordinate of the event. + * @return {Number} + */ + getX: function() { + return this.getXY()[0]; + }, + /** + * Gets the X and Y coordinates of the event. + * @return {Number[]} The xy values like [x, y] + */ + getXY: function() { + var me = this, + xy = me.xy; + if (!xy) { + xy = me.xy = [ + me.pageX, + me.pageY + ]; + // eslint-disable-next-line vars-on-top, one-var + var x = xy[0], + browserEvent, doc, docEl, body; + // pageX/pageY not available (undefined, not null), use clientX/clientY instead + if (!x && x !== 0) { + browserEvent = me.browserEvent; + doc = document; + docEl = doc.documentElement; + body = doc.body; + xy[0] = browserEvent.clientX + (docEl && docEl.scrollLeft || body && body.scrollLeft || 0) - (docEl && docEl.clientLeft || body && body.clientLeft || 0); + xy[1] = browserEvent.clientY + (docEl && docEl.scrollTop || body && body.scrollTop || 0) - (docEl && docEl.clientTop || body && body.clientTop || 0); + } + } + return xy; + }, + /** + * @private + * Gets the event coordinates relative to the `currentTarget`s position. + * @param {Boolean} clip + * The returned coordinates are guaranteed to be within + * [0, width] and [0, height] of the target, if `clip` is set to `true`. + * @return {Number[]} The xy values like [x, y] + */ + getLocalXY: function(clip) { + // TODO: check with RTL + var pageXY = this.getXY(), + targetXY = Ext.fly(this.currentTarget).getXY(), + localX = pageXY[0] - targetXY[0], + localY = pageXY[1] - targetXY[1], + size; + if (clip) { + size = Ext.fly(this.currentTarget).getSize(); + localX = Math.max(0, Math.min(localX, size.width)); + localY = Math.max(0, Math.min(localY, size.height)); + } + return [ + localX, + localY + ]; + }, + /** + * Gets the y coordinate of the event. + * @return {Number} + */ + getY: function() { + return this.getXY()[1]; + }, + /** + * Returns true if the control, meta, shift or alt key was pressed during this event. + * @return {Boolean} + */ + hasModifier: function() { + var me = this; + return !!(me.ctrlKey || me.altKey || me.shiftKey || me.metaKey); + }, + /** + * Checks if the key pressed was a "navigation" key. A navigation key is defined by + * these keys: + * + * - Page Up + * - Page Down + * - End + * - Home + * - Left + * - Up + * - Right + * - Down + * - Return + * - Tab + * - Esc + * + * @param {Boolean} [scrollableOnly] Only check navigation keys that can cause + * element scrolling by their default action. + * + * @return {Boolean} `true` if the press is a navigation keypress + */ + isNavKeyPress: function(scrollableOnly) { + var me = this, + k = me.keyCode, + isKeyPress = me.type === 'keypress'; + // See specs for description of behaviour + // Page Up/Down, End, Home, Left, Up, Right, Down + return ((!isKeyPress || Ext.isGecko) && k >= 33 && k <= 40) || (!scrollableOnly && (k === me.RETURN || k === me.TAB || k === me.ESC)); + }, + /** + * Checks if the key pressed was a "special" key. A special key is defined as one of + * these keys: + * + * - Page Up + * - Page Down + * - End + * - Home + * - Left arrow + * - Up arrow + * - Right arrow + * - Down arrow + * - Return + * - Tab + * - Esc + * - Backspace + * - Delete + * - Shift + * - Ctrl + * - Alt + * - Pause + * - Caps Lock + * - Print Screen + * - Insert + * + * @return {Boolean} `true` if the key for this event is special + */ + isSpecialKey: function() { + var me = this, + k = me.keyCode, + isGecko = Ext.isGecko, + isKeyPress = me.type === 'keypress'; + // See specs for description of behaviour + return (isGecko && isKeyPress && me.charCode === 0) || (this.isNavKeyPress()) || (k === me.BACKSPACE) || (k === me.ENTER) || (k >= 16 && k <= 20) || (// Shift, Ctrl, Alt, Pause, Caps Lock + (!isKeyPress || isGecko) && k >= 44 && k <= 46); + }, + // Print Screen, Insert, Delete + makeUnpreventable: function() { + this.browserEvent.preventDefault = Ext.emptyFn; + }, + /** + * Prevents the browsers default handling of the event. + * @chainable + */ + preventDefault: function() { + var me = this, + parentEvent = me.parentEvent; + me.defaultPrevented = true; + // if the event was created by prototype-chaining a new object to an existing event + // instance, we need to make sure the parent event is defaultPrevented as well. + if (parentEvent) { + parentEvent.defaultPrevented = true; + } + me.browserEvent.preventDefault(); + return me; + }, + setCurrentTarget: function(target) { + this.currentTarget = this.delegatedTarget = target; + }, + /** + * Stop the event (`{@link #preventDefault}` and `{@link #stopPropagation}`). + * @chainable + */ + stopEvent: function() { + return this.preventDefault().stopPropagation(); + }, + // map of events that should fire global mousedown even if stopped + mousedownEvents: { + mousedown: 1, + pointerdown: 1, + touchstart: 1 + }, + // map of events that should fire global mouseup even if stopped + mouseupEvents: { + mouseup: 1, + pointerup: 1, + touchend: 1 + }, + /** + * Cancels bubbling of the event. + * @chainable + */ + stopPropagation: function() { + var me = this, + browserEvent = me.browserEvent, + parentEvent = me.parentEvent; + // Fire the "unstoppable" global mousedown event + // (used for menu hiding, etc) + if (me.mousedownEvents[me.type]) { + Ext.GlobalEvents.fireMouseDown(me); + } + // Fire the "unstoppable" global mouseup event + // (used for component unpressing, etc) + if (me.mouseupEvents[me.type]) { + Ext.GlobalEvents.fireMouseUp(me); + } + // Set stopped for delegated event listeners. Dom publisher will check this + // property during its emulated propagation phase (see doPublish) + me.stopped = true; + // if the event was created by prototype-chaining a new object to an existing event + // instance, we need to make sure the parent event is stopped. This feature most + // likely comes into play when dealing with event translation. For example on touch + // browsers addListener('mousedown') actually attaches a 'touchstart' listener behind + // the scenes. When the 'touchstart' event is dispatched, the event system will + // create a "chained" copy of the event object before correcting its type back to + // 'mousedown' and calling the handler. When propagating the event we look at the + // original event, not the chained one to determine if propagation should continue, + // so the stopped property must be set on the parentEvent or stopPropagation + // will not work. + if (parentEvent && !me.isGesture) { + parentEvent.stopped = true; + } + if (!browserEvent.stopPropagation) { + // IE < 10 does not have stopPropagation() + browserEvent.cancelBubble = true; + return me; + } + // For non-delegated event listeners (those that are directly attached to the + // DOM element) we need to call the browserEvent's stopPropagation() method. + browserEvent.stopPropagation(); + return me; + }, + /** + * Claims this event as the currently active gesture. Once a gesture is claimed + * no other gestures will fire events until after the current gesture has completed. + * For example, if `claimGesture()` is invoked on a dragstart or drag event, no + * swipestart or swipe events will be fired until the drag gesture completes, even if + * the gesture also meets the required duration and distance requirements to be recognized + * as a swipe. + * + * If `claimGesture()` is invoked on a mouse, touch, or pointer event, it will disable + * all gesture events until termination of the current gesture is indicated by a + * mouseup, touchend, or pointerup event. + * + * @return {Ext.event.Event} + */ + claimGesture: function() { + var me = this, + parentEvent = me.parentEvent; + me.claimed = true; + if (parentEvent && !me.isGesture) { + parentEvent.claimGesture(); + } else { + // Claiming a gesture should also prevent default browser actions like pan/zoom + // if possible (only works on browsers that support touch events - browsers that + // use pointer events must declare a CSS touch-action on elements to prevent the + // default touch action from occurring. + me.preventDefault(); + } + return me; + }, + /** + * Returns true if the target of this event is a child of `el`. If the allowEl + * parameter is set to false, it will return false if the target is `el`. + * Example usage: + * + * // Handle click on any child of an element + * Ext.getBody().on('click', function(e){ + * if(e.within('some-el')){ + * alert('Clicked on a child of some-el!'); + * } + * }); + * + * // Handle click directly on an element, ignoring clicks on child nodes + * Ext.getBody().on('click', function(e,t){ + * if((t.id == 'some-el') && !e.within(t, true)){ + * alert('Clicked directly on some-el!'); + * } + * }); + * + * @param {String/HTMLElement/Ext.dom.Element} el The id, DOM element or Ext.Element to check + * @param {Boolean} [related] `true` to test if the related target is within el instead + * of the target + * @param {Boolean} [allowEl=true] `true` to allow the target to be considered "within" itself. + * `false` to only allow child elements. + * @return {Boolean} + */ + within: function(el, related, allowEl) { + var t; + if (el) { + t = related ? this.getRelatedTarget() : this.getTarget(); + } + if (!t || (allowEl === false && t === Ext.getDom(el))) { + return false; + } + return Ext.fly(el).contains(t); + }, + privates: { + ieMimeType: { + "text/plain": 'Text' + } + }, + deprecated: { + '4.0': { + methods: { + /** + * @method getPageX + * Gets the x coordinate of the event. + * @return {Number} + * @deprecated 4.0 use {@link #getX} instead + * @member Ext.event.Event + */ + getPageX: 'getX', + /** + * @method getPageY + * Gets the y coordinate of the event. + * @return {Number} + * @deprecated 4.0 use {@link #getY} instead + * @member Ext.event.Event + */ + getPageY: 'getY' + } + } + } +}, function(Event) { + var constants = { + /** Key constant @type Number */ + BACKSPACE: 8, + /** Key constant @type Number */ + TAB: 9, + /** Key constant @type Number */ + NUM_CENTER: 12, + /** Key constant @type Number */ + ENTER: 13, + /** Key constant @type Number */ + RETURN: 13, + /** Key constant @type Number */ + SHIFT: 16, + /** Key constant @type Number */ + CTRL: 17, + /** Key constant @type Number */ + ALT: 18, + /** Key constant @type Number */ + PAUSE: 19, + /** Key constant @type Number */ + CAPS_LOCK: 20, + /** Key constant @type Number */ + ESC: 27, + /** Key constant @type Number */ + SPACE: 32, + /** Key constant @type Number */ + PAGE_UP: 33, + /** Key constant @type Number */ + PAGE_DOWN: 34, + /** Key constant @type Number */ + END: 35, + /** Key constant @type Number */ + HOME: 36, + /** Key constant @type Number */ + LEFT: 37, + /** Key constant @type Number */ + UP: 38, + /** Key constant @type Number */ + RIGHT: 39, + /** Key constant @type Number */ + DOWN: 40, + /** Key constant @type Number */ + PRINT_SCREEN: 44, + /** Key constant @type Number */ + INSERT: 45, + /** Key constant @type Number */ + DELETE: 46, + /** Key constant @type Number */ + ZERO: 48, + /** Key constant @type Number */ + ONE: 49, + /** Key constant @type Number */ + TWO: 50, + /** Key constant @type Number */ + THREE: 51, + /** Key constant @type Number */ + FOUR: 52, + /** Key constant @type Number */ + FIVE: 53, + /** Key constant @type Number */ + SIX: 54, + /** Key constant @type Number */ + SEVEN: 55, + /** Key constant @type Number */ + EIGHT: 56, + /** Key constant @type Number */ + NINE: 57, + /** Key constant @type Number */ + A: 65, + /** Key constant @type Number */ + B: 66, + /** Key constant @type Number */ + C: 67, + /** Key constant @type Number */ + D: 68, + /** Key constant @type Number */ + E: 69, + /** Key constant @type Number */ + F: 70, + /** Key constant @type Number */ + G: 71, + /** Key constant @type Number */ + H: 72, + /** Key constant @type Number */ + I: 73, + /** Key constant @type Number */ + J: 74, + /** Key constant @type Number */ + K: 75, + /** Key constant @type Number */ + L: 76, + /** Key constant @type Number */ + M: 77, + /** Key constant @type Number */ + N: 78, + /** Key constant @type Number */ + O: 79, + /** Key constant @type Number */ + P: 80, + /** Key constant @type Number */ + Q: 81, + /** Key constant @type Number */ + R: 82, + /** Key constant @type Number */ + S: 83, + /** Key constant @type Number */ + T: 84, + /** Key constant @type Number */ + U: 85, + /** Key constant @type Number */ + V: 86, + /** Key constant @type Number */ + W: 87, + /** Key constant @type Number */ + X: 88, + /** Key constant @type Number */ + Y: 89, + /** Key constant @type Number */ + Z: 90, + /** Key constant @type Number */ + META: 91, + // Command key on mac, left window key on Windows + /** Key constant @type Number */ + CONTEXT_MENU: 93, + /** Key constant @type Number */ + NUM_ZERO: 96, + /** Key constant @type Number */ + NUM_ONE: 97, + /** Key constant @type Number */ + NUM_TWO: 98, + /** Key constant @type Number */ + NUM_THREE: 99, + /** Key constant @type Number */ + NUM_FOUR: 100, + /** Key constant @type Number */ + NUM_FIVE: 101, + /** Key constant @type Number */ + NUM_SIX: 102, + /** Key constant @type Number */ + NUM_SEVEN: 103, + /** Key constant @type Number */ + NUM_EIGHT: 104, + /** Key constant @type Number */ + NUM_NINE: 105, + /** Key constant @type Number */ + NUM_MULTIPLY: 106, + /** Key constant @type Number */ + NUM_PLUS: 107, + /** Key constant @type Number */ + NUM_MINUS: 109, + /** Key constant @type Number */ + NUM_PERIOD: 110, + /** Key constant @type Number */ + NUM_DIVISION: 111, + /** Key constant @type Number */ + F1: 112, + /** Key constant @type Number */ + F2: 113, + /** Key constant @type Number */ + F3: 114, + /** Key constant @type Number */ + F4: 115, + /** Key constant @type Number */ + F5: 116, + /** Key constant @type Number */ + F6: 117, + /** Key constant @type Number */ + F7: 118, + /** Key constant @type Number */ + F8: 119, + /** Key constant @type Number */ + F9: 120, + /** Key constant @type Number */ + F10: 121, + /** Key constant @type Number */ + F11: 122, + /** Key constant @type Number */ + F12: 123, + /** + * @property {Number} + * The mouse wheel delta scaling factor when the + * [`deltaMode`](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode) + * is DOM_DELTA_PIXEL + * + * To change this value: + * + * Ext.event.Event.prototype.WHEEL_PIXEL_SIZE = 3; + */ + WHEEL_PIXEL_SIZE: 1, + /** + * @property {Number} + * The mouse wheel delta scaling factor when the + * [`deltaMode`](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode) + * is DOM_DELTA_LINE + * + * To change this value: + * + * Ext.event.Event.prototype.WHEEL_LINE_SIZE = 16; + */ + WHEEL_LINE_SIZE: 20, + /** + * @property {Number} + * The mouse wheel delta scaling factor when the + * [`deltaMode`](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode) + * is DOM_DELTA_PAGE + * + * To change this value: + * + * Ext.event.Event.prototype.WHEEL_PAGE_SIZE = 400; + */ + WHEEL_PAGE_SIZE: 600 + }, + keyCodes = {}, + gestureEvents = Event.gestureEvents, + prototype = Event.prototype, + i, keyName, keyCode, keys; + Ext.apply(gestureEvents, Event.mouseEvents); + Ext.apply(gestureEvents, Event.pointerEvents); + Ext.apply(gestureEvents, Event.touchEvents); + Ext.apply(Event, constants); + Ext.apply(prototype, constants); + // ENTER and RETURN has the same keyCode, but since RETURN + // comes later it will win over ENTER down there. However + // ENTER is used more often and feels more natural. + delete constants.RETURN; + // We need this to do reverse lookup of key name by its code + for (keyName in constants) { + keyCode = constants[keyName]; + keyCodes[keyCode] = keyName; + } + Event.keyCodes = prototype.keyCodes = keyCodes; + // Classic Event override will install this handler in IE9- + if (!Ext.isIE9m) { + document.addEventListener('keydown', Event.globalTabKeyDown, true); + document.addEventListener('keyup', Event.globalTabKeyUp, true); + } + /** + * @member Ext.event.Event + * @private + * Returns the X and Y coordinates of this event without regard to any RTL + * direction settings. + */ + prototype.getTrueXY = prototype.getXY; + if (typeof KeyboardEvent !== 'undefined' && !('key' in KeyboardEvent.prototype)) { + prototype._keys = keys = { + 3: 'Cancel', + 6: 'Help', + 8: 'Backspace', + 9: 'Tab', + 12: 'Clear', + 13: 'Enter', + 16: 'Shift', + 17: 'Control', + 18: 'Alt', + 19: 'Pause', + 20: 'CapsLock', + 27: 'Escape', + 28: 'Convert', + 29: 'NonConvert', + 30: 'Accept', + 31: 'ModeChange', + 32: ' ', + 33: 'PageUp', + 34: 'PageDown', + 35: 'End', + 36: 'Home', + 37: 'ArrowLeft', + 38: 'ArrowUp', + 39: 'ArrowRight', + 40: 'ArrowDown', + 41: 'Select', + 42: 'Print', + 43: 'Execute', + 44: 'PrintScreen', + 45: 'Insert', + 46: 'Delete', + 48: [ + '0', + ')' + ], + 49: [ + '1', + '!' + ], + 50: [ + '2', + '@' + ], + 51: [ + '3', + '#' + ], + 52: [ + '4', + '$' + ], + 53: [ + '5', + '%' + ], + 54: [ + '6', + '^' + ], + 55: [ + '7', + '&' + ], + 56: [ + '8', + '*' + ], + 57: [ + '9', + '(' + ], + 91: 'OS', + 93: 'ContextMenu', + 144: 'NumLock', + 145: 'ScrollLock', + 181: 'VolumeMute', + 182: 'VolumeDown', + 183: 'VolumeUp', + 186: [ + ';', + ':' + ], + 187: [ + '=', + '+' + ], + 188: [ + ',', + '<' + ], + 189: [ + '-', + '_' + ], + 190: [ + '.', + '>' + ], + 191: [ + '/', + '?' + ], + 192: [ + '`', + '~' + ], + 219: [ + '[', + '{' + ], + 220: [ + '\\', + '|' + ], + 221: [ + ']', + '}' + ], + 222: [ + "'", + '"' + ], + 224: 'Meta', + 225: 'AltGraph', + 246: 'Attn', + 247: 'CrSel', + 248: 'ExSel', + 249: 'EraseEof', + 250: 'Play', + 251: 'ZoomOut' + }; + for (i = 1; i < 25; ++i) { + keys[i + 111] = 'F' + i; + } + // F1 - F24 + for (i = 0; i < 26; ++i) { + // a-z, A-Z + keys[i] = [ + String.fromCharCode(i + 97), + String.fromCharCode(i + 65) + ]; + } + prototype.key = function() { + var k = keys[this.browserEvent.which || this.keyCode]; + if (k && typeof k !== 'string') { + k = k[+this.shiftKey]; + } + return k; + }; + } +}); + +/** + * @private + */ +Ext.define('Ext.event.publisher.Dom', { + extend: Ext.event.publisher.Publisher, + type: 'dom', + /** + * @property {Array} handledDomEvents + * An array of DOM events that this publisher handles. Events specified in this array + * will be added as global listeners on the {@link #target} + */ + handledDomEvents: [], + reEnterCount: 0, + // The following events do not bubble, but can still be "captured" at the top of + // the DOM, For these events, when the delegated event model is used, we attach a + // single listener on the window object using the "useCapture" option. + captureEvents: { + animationstart: 1, + animationend: 1, + resize: 1, + focus: 1, + blur: 1 + }, + // The following events do not bubble, and cannot be "captured". The only way to + // listen for these events is via a listener attached directly to the target element + directEvents: { + mouseenter: 1, + mouseleave: 1, + pointerenter: 1, + pointerleave: 1, + MSPointerEnter: 1, + MSPointerLeave: 1, + load: 1, + unload: 1, + beforeunload: 1, + error: 1, + DOMContentLoaded: 1, + DOMFrameContentLoaded: 1, + hashchange: 1, + // Scroll can be captured, but it is listed here as one of directEvents instead of + // captureEvents because in some browsers capturing the scroll event does not work + // if the window object itself fired the scroll event. + scroll: 1, + online: 1, + offline: 1 + }, + /** + * In browsers that implement pointerevents when a pointerdown is triggered by touching + * the screen, pointerover and pointerenter events will be fired immmediately before + * the pointerdown. Also pointerout and pointerleave will be fired immediately after + * pointerup when triggered using touch input. For a consistent cross-browser + * experience on touch-screens we block pointerover, pointerout, pointerenter, and + * pointerleave when triggered by touch input, since in most cases pointerover/pointerenter + * behavior is not desired when touching the screen. Note: this should only affect + * events with pointerType === 'touch' or pointerType === 'pen', we do NOT want to + * block these events when triggered using a mouse. + * See also: + * http://www.w3.org/TR/pointerevents/#the-pointerdown-event + * http://www.w3.org/TR/pointerevents/#the-pointerenter-event + * @private + */ + blockedPointerEvents: { + pointerover: 1, + pointerout: 1, + pointerenter: 1, + pointerleave: 1, + MSPointerOver: 1, + MSPointerOut: 1, + MSPointerEnter: 1, + MSPointerLeave: 1 + }, + /** + * Browsers with pointer events may implement "compatibility" mouse events: + * http://www.w3.org/TR/pointerevents/#compatibility-mapping-with-mouse-events + * The behavior implemented in handlers for mouse over/out/enter/leave is not typically + * desired when touching the screen, so we map all of these events to their pointer + * counterparts in Ext.Element event translation code, so that they can be blocked + * via "blockedPointerEvents". The only scenario where this breaks down is in IE10 + * with mouseenter/mouseleave, since MSPointerEnter/MSPointerLeave were not implemented + * in IE10. For these 2 events we have to resort to a different method - capturing + * the timestamp of the last pointer event that has pointerType == 'touch', and if the + * mouse event occurred within a certain threshold we can reasonably assume it occurred + * because of a touch on the screen (see isEventBlocked) + * @private + */ + blockedCompatibilityMouseEvents: { + mouseenter: 1, + mouseleave: 1 + }, + constructor: function() { + var me = this, + supportsPassive = Ext.supports.PassiveEventListener; + me.listenerOptions = supportsPassive ? { + passive: false + } : false; + me.captureOptions = supportsPassive ? { + passive: false, + capture: true + } : true; + me.bubbleSubscribers = {}; + me.captureSubscribers = {}; + me.directSubscribers = {}; + me.directCaptureSubscribers = {}; + // this map tracks all the names of the events that currently have a delegated + // event listener attached so that they can be removed from the dom when the + // publisher is destroyed + me.delegatedListeners = {}; + me.initHandlers(); + Ext.onInternalReady(me.onReady, me); + me.callParent(); + me.registerDomEvents(); + }, + registerDomEvents: function() { + var me = this, + publishersByEvent = Ext.event.publisher.Publisher.publishersByEvent, + domEvents = me.handledDomEvents, + ln = domEvents.length, + i, eventName; + for (i = 0; i < ln; i++) { + eventName = domEvents[i]; + me.handles[eventName] = 1; + publishersByEvent[eventName] = me; + } + }, + onReady: function() { + var me = this, + domEvents = me.handledDomEvents, + ln, i; + if (domEvents) { + // If the publisher has handledDomEvents we attach delegated listeners up front + // for those events. Dom publisher does not have a list of event names, but + // attaches listeners dynamically as subscribers are subscribed. This allows it + // to handle all DOM events that are not explicitly handled by another publisher. + // Subclasses such as Gesture must explicitly list their handledDomEvents. + for (i = 0 , ln = domEvents.length; i < ln; i++) { + me.addDelegatedListener(domEvents[i]); + } + } + // DOM publishers should be the last thing to go since they are used + // to remove any element listeners which is typically part + // of the unload destroy process. + Ext.getWin().on('unload', me.destroy, me, { + priority: -10000 + }); + }, + initHandlers: function() { + var me = this; + me.onDelegatedEvent = Ext.bind(me.onDelegatedEvent, me); + me.onDirectEvent = Ext.bind(me.onDirectEvent, me); + me.onDirectCaptureEvent = Ext.bind(me.onDirectCaptureEvent, me); + }, + addDelegatedListener: function(eventName) { + var me = this; + me.delegatedListeners[eventName] = 1; + me.target.addEventListener(eventName, me.onDelegatedEvent, me.captureEvents[eventName] ? me.captureOptions : me.listenerOptions); + }, + removeDelegatedListener: function(eventName) { + var me = this; + delete me.delegatedListeners[eventName]; + me.target.removeEventListener(eventName, me.onDelegatedEvent, me.captureEvents[eventName] ? me.captureOptions : me.listenerOptions); + }, + addDirectListener: function(eventName, element, capture) { + var me = this; + element.dom.addEventListener(eventName, capture ? me.onDirectCaptureEvent : me.onDirectEvent, capture ? me.captureOptions : me.listenerOptions); + }, + removeDirectListener: function(eventName, element, capture) { + var me = this; + element.dom.removeEventListener(eventName, capture ? me.onDirectCaptureEvent : me.onDirectEvent, capture ? me.captureOptions : me.listenerOptions); + }, + subscribe: function(element, eventName, delegated, capture) { + var me = this, + subscribers, id; + if (delegated && !me.directEvents[eventName]) { + // delegated listeners + subscribers = capture ? me.captureSubscribers : me.bubbleSubscribers; + if (!me.handles[eventName] && !me.delegatedListeners[eventName]) { + // First time we've attached a listener for this eventName - need to begin + // listening at the dom level + me.addDelegatedListener(eventName); + } + if (subscribers[eventName]) { + ++subscribers[eventName]; + } else { + subscribers[eventName] = 1; + } + } else { + subscribers = capture ? me.directCaptureSubscribers : me.directSubscribers; + id = element.id; + // Direct subscribers are tracked by eventName first and by element id second. + // This allows the element id key to be deleted when there are no more subscribers + // so that this map does not grow indefinitely (it can only grow to a finite + // set of event names) - see unsubscribe + subscribers = subscribers[eventName] || (subscribers[eventName] = {}); + if (subscribers[id]) { + ++subscribers[id]; + } else { + subscribers[id] = 1; + me.addDirectListener(eventName, element, capture); + } + } + }, + unsubscribe: function(element, eventName, delegated, capture) { + var me = this, + captureSubscribers, bubbleSubscribers, subscribers, id; + if (delegated && !me.directEvents[eventName]) { + captureSubscribers = me.captureSubscribers; + bubbleSubscribers = me.bubbleSubscribers; + subscribers = capture ? captureSubscribers : bubbleSubscribers; + if (subscribers[eventName]) { + --subscribers[eventName]; + } + if (!me.handles[eventName] && !bubbleSubscribers[eventName] && !captureSubscribers[eventName]) { + // decremented subscribers back to 0 - and the event is not in "handledEvents" + // no longer need to listen at the dom level + this.removeDelegatedListener(eventName); + } + } else { + subscribers = capture ? me.directCaptureSubscribers : me.directSubscribers; + id = element.id; + subscribers = subscribers[eventName]; + if (subscribers[id]) { + --subscribers[id]; + } + if (!subscribers[id]) { + // no more direct subscribers for this element/id/capture, so we can safely + // remove the dom listener + delete subscribers[id]; + me.removeDirectListener(eventName, element, capture); + } + } + }, + getPropagatingTargets: function(target) { + var currentNode = target, + targets = [], + parentNode; + while (currentNode) { + targets.push(currentNode); + parentNode = currentNode.parentNode; + if (!parentNode) { + // If the node has no parentNode it means one of two things - either it is + // not in the dom, or we have looped all the way up to the document object. + // If the latter is the case we need to add the window object to the targets + // to ensure that our propagation mimics browser propagation where events + // can bubble from the document to the window. + parentNode = currentNode.defaultView; + } + currentNode = parentNode; + } + return targets; + }, + /** + * + * @param e {Ext.event.Event/Ext.event.Event[]} An event to publish. Can also be an + * array of events. Gesture publisher passes an array so that gesture events and + * the dom events from which they were synthesized can propagate together. + * @param [targets] {HTMLElement[]} propagation targets. Required if `e` is an array. + * @param {Boolean} [claimed=false] pass true if we are re-entering publish() to + * publish gesture cancellation events that are being fired as a result of something + * being claimed. This ensures that cancellation events cannot be claimed. + * @protected + */ + publish: function(e, targets, claimed) { + var me = this, + hasCaptureSubscribers = false, + hasBubbleSubscribers = false, + events, type, target, el, i, ln, j, eLn; + claimed = claimed || false; + // Gesture publisher passes an already created array of propagating targets. + // For all other events we need to compute the targets for propagation now. + if (!targets) { + if (e instanceof Array) { + Ext.raise("Propagation targets must be supplied when publishing " + "an array of events."); + } + // No targets passed, assume that e is not an array. + target = e.target; + if (me.captureEvents[e.type]) { + el = Ext.cache[target.id]; + targets = el ? [ + el + ] : []; + } else { + targets = me.getPropagatingTargets(target); + } + } + // "e" may be either a single event (as is the case for events fired by dom publisher) + // or it could be an array of events containing a dom event and its recognized + // gesture events. + events = Ext.Array.from(e); + ln = targets.length; + eLn = events.length; + for (i = 0; i < eLn; i++) { + type = events[i].type; + if (!hasCaptureSubscribers && me.captureSubscribers[type]) { + hasCaptureSubscribers = true; + } + if (!hasBubbleSubscribers && me.bubbleSubscribers[type]) { + hasBubbleSubscribers = true; + } + } + // We will now proceed to fire events in both capture and bubble phases. You + // may notice that we are looping all potential targets both times, and only + // firing on the target if there is an Ext.Element wrapper in the cache. This is + // done (vs. eliminating non-cached targets from the array up front) because + // event handlers can add listeners to other elements during propagation. Looping + // all the potential targets ensures that these dynamically added listeners + // are fired. See https://sencha.jira.com/browse/EXTJS-15953 + // capture phase (top-down event propagation). + if (hasCaptureSubscribers) { + for (i = ln; i--; ) { + el = Ext.cache[targets[i].id]; + if (el) { + for (j = 0; j < eLn; j++) { + e = events[j]; + me.fire(el, e.type, e, false, true); + if (!claimed && e.claimed) { + claimed = true; + j = me.filterClaimed(events, e); + eLn = events.length; + } + // filterClaimed may remove items + if (e.stopped) { + events.splice(j, 1); + j--; + eLn--; + } + } + } + } + } + // bubble phase (bottom-up event propagation). + // stopPropagation during capture phase cancels entire bubble phase + if (hasBubbleSubscribers && !e.stopped) { + for (i = 0; i < ln; i++) { + el = Ext.cache[targets[i].id]; + if (el) { + for (j = 0; j < eLn; j++) { + e = events[j]; + me.fire(el, e.type, e, false, false); + if (!claimed && e.claimed && me.filterClaimed) { + claimed = true; + j = me.filterClaimed(events, e); + eLn = events.length; + } + // filterClaimed may remove items + if (e.stopped) { + events.splice(j, 1); + j--; + eLn--; + } + } + } + } + } + }, + /** + * Hook for gesture publisher to override and perform gesture recognition + * @param {Ext.event.Event} e + */ + publishDelegatedDomEvent: function(e) { + this.publish(e); + }, + fire: function(element, eventName, e, direct, capture) { + var event; + if (element.hasListeners[eventName]) { + event = element.events[eventName]; + if (event) { + if (capture && direct) { + event = event.directCaptures; + } else if (capture) { + event = event.captures; + } else if (direct) { + event = event.directs; + } + // yes, this second null check for event is necessary - one of the + // above assignments might have resulted in undefined + if (event) { + e.setCurrentTarget(element.dom); + event.fire(e, e.target); + } + } + } + }, + onDelegatedEvent: function(e) { + if (Ext.elevateFunction) { + // using [e] is faster than using arguments in most browsers + // http://jsperf.com/passing-arguments + Ext.elevateFunction(this.doDelegatedEvent, this, [ + e + ]); + } else { + this.doDelegatedEvent(e); + } + }, + doDelegatedEvent: function(e) { + var me = this, + timeStamp; + e = new Ext.event.Event(e); + timeStamp = e.time; + if (!me.isEventBlocked(e)) { + me.beforeEvent(e); + Ext.frameStartTime = timeStamp; + me.reEnterCountAdjusted = false; + me.reEnterCount++; + me.publishDelegatedDomEvent(e); + // Gesture publisher deals with exceptions in recognizers + if (!me.reEnterCountAdjusted) { + me.reEnterCount--; + } + me.afterEvent(e); + } + }, + /** + * Handler for directly-attached (non-delegated) dom events + * @param {Event} e + * @private + */ + onDirectEvent: function(e) { + if (Ext.elevateFunction) { + // using [e] is faster than using arguments in most browsers + // http://jsperf.com/passing-arguments + Ext.elevateFunction(this.doDirectEvent, this, [ + e, + false + ]); + } else { + this.doDirectEvent(e, false); + } + }, + // When eventPhase is AT_TARGET there's no way to know if we are handling a capture + // or bubble listener, hence the need for this separate handler fn + onDirectCaptureEvent: function(e) { + if (Ext.elevateFunction) { + // using [e] is faster than using arguments in most browsers + // http://jsperf.com/passing-arguments + Ext.elevateFunction(this.doDirectEvent, this, [ + e, + true + ]); + } else { + this.doDirectEvent(e, true); + } + }, + doDirectEvent: function(e, capture) { + var me = this, + currentTarget = e.currentTarget, + timeStamp, el; + e = new Ext.event.Event(e); + timeStamp = e.time; + if (me.isEventBlocked(e)) { + return; + } + me.beforeEvent(e); + Ext.frameStartTime = timeStamp; + el = Ext.cache[currentTarget.id]; + // Element can be removed from the cache by this time, with the node + // still lingering for some reason. This can happen for example when + // load event is fired on an iframe that we constructed when submitting + // a form for file uploads. + if (el) { + // Since natural DOM propagation has occurred, no emulated propagation is needed. + // Simply dispatch the event on the currentTarget element + me.reEnterCountAdjusted = false; + me.reEnterCount++; + me.fire(el, e.type, e, true, capture); + // Gesture publisher deals with exceptions in recognizers + if (!me.reEnterCountAdjusted) { + me.reEnterCount--; + } + } + me.afterEvent(e); + }, + beforeEvent: function(e) { + var browserEvent = e.browserEvent, + // use full class name, not me.self, so that Dom and Gesture publishers will + // both place flags on the same object. + self = Ext.event.publisher.Dom, + touches, touch; + if (browserEvent.type === 'touchstart') { + touches = browserEvent.touches; + if (touches.length === 1) { + // capture the coordinates of the first touchstart event so we can use + // them to eliminate duplicate mouse events if needed, (see isEventBlocked). + touch = touches[0]; + self.lastTouchStartX = touch.pageX; + self.lastTouchStartY = touch.pageY; + } + } + }, + afterEvent: function(e) { + var browserEvent = e.browserEvent, + type = browserEvent.type, + // use full class name, not me.self, so that Dom and Gesture publishers will + // both place flags on the same object. + self = Ext.event.publisher.Dom, + GlobalEvents = Ext.GlobalEvents; + // It is important that the following time stamps are captured after the handlers + // have been invoked because they need to represent the "exit" time, so that they + // can be compared against the next "entry" time into onDelegatedEvent or + // onDirectEvent to detect the time lapse in between the firing of the 2 events. + // We set these flags on "this.self" so that they can be shared between Dom + // publisher and subclasses + if (e.self.pointerEvents[type] && e.pointerType !== 'mouse') { + // track the last time a pointer event was fired as a result of interaction + // with the screen, pointerType === 'touch' most likely but could also be + // pointerType === 'pen' hence the reason we use !== 'mouse', This is used + // to eliminate potential duplicate "compatibility" mouse events + // (see isEventBlocked) + self.lastScreenPointerEventTime = Ext.now(); + } + if (type === 'touchend') { + // Capture a time stamp so we can use it to eliminate potential duplicate + // emulated mouse events on multi-input devices that have touch events, + // e.g. Chrome on Window8 with touch-screen (see isEventBlocked). + self.lastTouchEndTime = Ext.now(); + } + if (!this.reEnterCount && !GlobalEvents.idleEventMask[type]) { + Ext.fireIdle(); + } + }, + /** + * Detects if the given event should be blocked from firing because it is an emulated + * "compatibility" mouse event triggered by a touch on the screen. + * @param {Ext.event.Event} e + * @return {Boolean} + * @private + */ + isEventBlocked: function(e) { + var me = this, + type = e.type, + // use full class name, not me.self, so that Dom and Gesture publishers will + // both look for flags on the same object. + self = Ext.event.publisher.Dom, + now = Ext.now(); + // Gecko has a bug where right clicking will trigger both a contextmenu + // and click event. This only occurs when delegating the event onto the window + // object like we do by default for delegated events. + // This is not possible to feature detect using synthetic events. + // Ticket logged: https://bugzilla.mozilla.org/show_bug.cgi?id=1156023 + if (Ext.isGecko && e.type === 'click' && e.button === 2) { + return true; + } + // prevent emulated pointerover, pointerout, pointerenter, and pointerleave + // events from firing when triggered by touching the screen. + return (me.blockedPointerEvents[type] && e.pointerType !== 'mouse') || // prevent compatibility mouse events from firing on devices with pointer + // events - see comment on blockedCompatibilityMouseEvents for more details + // The time from when the last pointer event fired until when compatibility + // events are received varies depending on the browser, device, and application + // so we use 1 second to be safe + (me.blockedCompatibilityMouseEvents[type] && (now - self.lastScreenPointerEventTime < 1000)) || (Ext.supports.TouchEvents && e.self.mouseEvents[e.type] && // some browsers (e.g. webkit on Windows 8 with touch screen) emulate mouse + // events after touch events have fired. This only seems to happen when there + // is no movement present, so, for example, a touchstart followed immediately + // by a touchend would result in the following sequence of events: + // "touchstart, touchend, mousemove, mousedown, mouseup" + // yes, you read that right, the emulated mousemove fires before mousedown. + // However, touch events with movement (touchstart, touchmove, then touchend) + // do not trigger the emulated mouse events. + // The side effect of this behavior is that single-touch gestures that expect + // no movement (e.g. tap) can double-fire - once when the touchstart/touchend + // occurs, and then again when the emulated mousedown/up occurs. + // We cannot solve the problem by only listening for touch events and ignoring + // mouse events, since we may be on a multi-input device that supports both + // touch and mouse events and we want gestures to respond to both kinds of + // events. Instead we have to detect if the mouse event is a "dupe" by + // checking if its coordinates are near the last touchstart's coordinates, + // and if it's timestamp is within a certain threshold of the last touchend + // event's timestamp. This is because when dealing with multi-touch events, + // the emulated mousedown event (when it does fire) will fire with approximately + // the same coordinates as the first touchstart, but within a short time after + // the last touchend. We use 15px as the distance threshold, to be on the safe + // side because the difference in coordinates can sometimes be up to 6px. + Math.abs(e.pageX - self.lastTouchStartX) < 15 && Math.abs(e.pageY - self.lastTouchStartY) < 15 && // in the majority of cases, the emulated mousedown is observed within + // 5ms of touchend, however, to be certain we avoid a situation where a + // gesture handler gets executed twice we use a threshold of 1000ms. The + // side effect of this is that if a user touches the screen and then quickly + // clicks screen in the same spot, the mousedown/mouseup sequence that + // ensues will not trigger any gesture recognizers. + (Ext.now() - self.lastTouchEndTime) < 1000); + }, + destroy: function() { + var GC = Ext.dom['GarbageCollector'], + // eslint-disable-line dot-notation + eventName; + for (eventName in this.delegatedListeners) { + this.removeDelegatedListener(eventName); + } + // We are wired to the unload event, so we ensure cleanup of low-level stuff + // like the Reaper and the GarbageCollector. + Ext.Reaper.flush(); + if (GC) { + GC.collect(); + } + this.callParent(); + }, + /** + * Resets the internal state of the Dom publisher. Internally the Dom publisher + * keeps track of timing and coordinates of events for eliminating browser duplicates + * (e.g. emulated mousedown after pointerdown etc.). This method resets all this + * cached data to a state similar to when the publisher was first instantiated. + * + * Applications will not typically need to use this method, but it is useful for + * Unit-testing situations where a clean slate is required for each test. + */ + reset: function() { + // use full class name, not me.self, so that Dom and Gesture publishers will + // both reset flags on the same object. + var self = Ext.event.publisher.Dom; + this.reEnterCount = 0; + // set to undefined, not null, because that is the initial state of these vars and + // undefined/null return different results when used in math operations + // (see isEventBlocked) + self.lastScreenPointerEventTime = self.lastTouchEndTime = self.lastTouchStartX = self.lastTouchStartY = undefined; + } +}, function(Dom) { + var doc = document, + defaultView = doc.defaultView, + prototype = Dom.prototype; + // In case of iOS if it is in iFrame then it should enter if condition + if ((Ext.os.is.iOS && window.self !== window.top) || Ext.browser.is.AndroidStock || !(defaultView && defaultView.addEventListener)) { + // Delegated listeners will get attached to the document object because + // attaching to the window object will not work. In IE8 this is needed because + // events do not bubble up to the window - bubbling stops at the document + // object. The iOS < 5 check was carried forward from Sencha Touch 2.3 - + // Not sure why it was needed. The check for (defaultView && defaultView.addEventListener) + // was carried forward as well - it may be required for older mobile browsers. + // see also TOUCH-5408 + prototype.target = doc; + } else { + /** + * @member Ext.event.publisher.Dom + * @property {Object} target the DOM target to which listeners are attached for + * delegated events. + * @private + */ + prototype.target = defaultView; + } + Dom.instance = new Dom(); +}); + +/** + * @private + */ +Ext.define('Ext.event.publisher.Gesture', { + extend: Ext.event.publisher.Dom, + type: 'gesture', + isCancelEvent: { + touchcancel: 1, + pointercancel: 1, + MSPointerCancel: 1 + }, + isEndEvent: { + mouseup: 1, + touchend: 1, + pointerup: 1, + MSPointerUp: 1 + }, + handledEvents: [], + handledDomEvents: [], + constructor: function(config) { + var me = this, + handledDomEvents = me.handledDomEvents, + supports = Ext.supports, + supportsTouchEvents = supports.TouchEvents, + onTouchStart = me.onTouchStart, + onTouchMove = me.onTouchMove, + onTouchEnd = me.onTouchEnd; + me.handlers = { + touchstart: onTouchStart, + touchmove: onTouchMove, + touchend: onTouchEnd, + touchcancel: onTouchEnd, + pointerdown: onTouchStart, + pointermove: onTouchMove, + pointerup: onTouchEnd, + pointercancel: onTouchEnd, + MSPointerDown: onTouchStart, + MSPointerMove: onTouchMove, + MSPointerUp: onTouchEnd, + MSPointerCancel: onTouchEnd, + mousedown: onTouchStart, + mousemove: onTouchMove, + mouseup: onTouchEnd + }; + me.activeTouchesMap = {}; + me.activeTouches = []; + me.changedTouches = []; + me.recognizers = []; + me.eventToRecognizer = {}; + me.cancelEvents = []; + if (supportsTouchEvents) { + // bind handlers that are only invoked when the browser has touchevents + me.onTargetTouchMove = me.onTargetTouchMove.bind(me); + me.onTargetTouchEnd = me.onTargetTouchEnd.bind(me); + } + if (supports.PointerEvents) { + handledDomEvents.push('pointerdown', 'pointermove', 'pointerup', 'pointercancel'); + me.mousePointerType = 'mouse'; + } else if (supports.MSPointerEvents) { + // IE10 uses vendor prefixed pointer events, IE11+ use unprefixed names. + handledDomEvents.push('MSPointerDown', 'MSPointerMove', 'MSPointerUp', 'MSPointerCancel'); + me.mousePointerType = 4; + } else if (supportsTouchEvents) { + handledDomEvents.push('touchstart', 'touchmove', 'touchend', 'touchcancel'); + } + if (!handledDomEvents.length || (supportsTouchEvents && Ext.os.is.Desktop)) { + // If the browser doesn't have pointer events or touch events we use mouse events + // to trigger gestures. The exception to this rule is touch enabled desktop + // browsers such as chrome and firefox on Windows touch screen devices. These + // browsers accept both touch and mouse input, so we need to listen for both + // touch events and mouse events. + handledDomEvents.push('mousedown', 'mousemove', 'mouseup'); + } + me.initConfig(config); + return me.callParent(); + }, + onReady: function() { + this.callParent(); + Ext.Array.sort(this.recognizers, function(recognizerA, recognizerB) { + var a = recognizerA.priority, + b = recognizerB.priority; + return (a > b) ? 1 : (a < b) ? -1 : 0; + }); + }, + registerRecognizer: function(recognizer) { + var me = this, + handledEvents = recognizer.handledEvents, + ln = handledEvents.length, + eventName, i; + // The recognizer will call our onRecognized method when it determines that a + // gesture has occurred. + recognizer.setOnRecognized(me.onRecognized); + recognizer.setCallbackScope(me); + // the gesture publishers handledEvents array is derived from the handledEvents + // of all of its recognizers + for (i = 0; i < ln; i++) { + eventName = handledEvents[i]; + me.handledEvents.push(eventName); + me.eventToRecognizer[eventName] = recognizer; + } + me.registerEvents(handledEvents); + me.recognizers.push(recognizer); + }, + onRecognized: function(recognizer, eventName, e, info, isCancel) { + var me = this, + touches = e.touches, + changedTouches = e.changedTouches, + ln = changedTouches.length, + events = me.events, + queueWasEmpty = !events.length, + cancelEvents = me.cancelEvents, + targetGroups, targets, i, touch; + info = info || {}; + // At this point "e" still refers to the originally dispatched Ext.event.Event that + // wraps a native browser event such as "touchend", or "mousemove". We need to + // dispatch with an an event object that has the correct "recognized" type such + // as "tap", or "drag". We don't want to change the type of the original event + // object because it may be used asynchronously by event handlers, so we create a + // new object that is chained to the original event. + info.type = eventName; + // Touch events have a handy feature - the original target of a touchstart is + // always the target of successive touchmove/touchend events event if the touch + // is dragged off of the original target. Pointer events also have this behavior + // via the setPointerCapture method, unless their target is removed from the dom + // mid-gesture, however, we do not currently use setPointerCapture because it + // can change the target of translated mouse events. Mouse events do not have this + // "capturing" feature at all - the target is always the element that was under + // the mouse at the time the event occurred. To be safe, and to ensure consistency, + // we just always set the target of recognized events to be the original target + // that was cached when the first "start" event was received. + info.target = changedTouches[0].target; + // reset stopped and claimed just in case the event that we are wrapping had + // stoppedPropagation or claimGesture called + info.stopped = false; + info.claimed = false; + info.isGesture = true; + e = e.chain(info); + if (!me.gestureTargets) { + if (ln > 1) { + targetGroups = []; + for (i = 0; i < ln; i++) { + touch = changedTouches[i]; + targetGroups.push(touch.targets); + } + targets = me.getCommonTargets(targetGroups); + } else { + targets = changedTouches[0].targets; + } + // Cache targets so that they only have to be computed once if multiple + // gestures are currently being recognized. + me.gestureTargets = targets; + } + if (isCancel && recognizer.isSingleTouch && (touches.length > 1)) { + // single touch recognizer cancelled by the start of a second touch. + // push into a separate queue which does not use the targets common to all + // touches (this.gestureTargets) as the targets for publishing but rather + // only uses the targets for the initial touch. + e.target = touches[0].target; + cancelEvents.push(e); + } else { + events.push(e); + } + if (queueWasEmpty) { + // if there were no events in the queue previously, it means the dom event + // has already been published, which means a recognizer must have recognized + // a gesture asynchronously (e.g. singletap fires on a timer) + // if this is the case we must publish now, otherwise we wait for the dom + // event handler to publish after it is finished invoking the recognizers + me.publishGestures(); + } + }, + getCommonTargets: function(targetGroups) { + var firstTargetGroup = targetGroups[0], + ln = targetGroups.length, + commonTargets = [], + i = 1, + target, targets, j; + if (ln === 1) { + return firstTargetGroup; + } + while (true) { + // eslint-disable-line no-constant-condition + target = firstTargetGroup[firstTargetGroup.length - i]; + if (!target) { + return commonTargets; + } + for (j = 1; j < ln; j++) { + targets = targetGroups[j]; + if (targets[targets.length - i] !== target) { + return commonTargets; + } + } + commonTargets.unshift(target); + i++; + } + return commonTargets; + }, + // eslint-disable-line no-unreachable + invokeRecognizers: function(methodName, e) { + var recognizers = this.recognizers, + ln = recognizers.length, + i, recognizer; + if (methodName === 'onStart') { + for (i = 0; i < ln; i++) { + recognizers[i].isActive = true; + } + } + for (i = 0; i < ln; i++) { + recognizer = recognizers[i]; + if (recognizer.isActive && recognizer[methodName].call(recognizer, e) === false) { + recognizer.isActive = false; + } + } + }, + /** + * When a gesture has been claimed this method is invoked to remove gesture events of + * other kinds. See implementation in Gesture publisher. + * @param {Ext.event.Event[]} events + * @param {String} claimedEvent + * @return {Number} The new index of the claimed event + * @private + */ + filterClaimed: function(events, claimedEvent) { + var me = this, + eventToRecognizer = me.eventToRecognizer, + claimedEventType = claimedEvent.type, + claimedRecognizer = eventToRecognizer[claimedEventType], + claimedEventIndex, recognizer, type, i; + for (i = events.length; i--; ) { + type = events[i].type; + if (type === claimedEventType) { + claimedEventIndex = i; + } else { + recognizer = eventToRecognizer[type]; + // if there is no claimed recognizer it means the user must have invoked + // claimGesture on a dom event (touchstart, touchmove etc). If this is the + // case we need to cease firing all gesture events, otherwise we allow only + // the "claimed" recognizer to continue to dispatch events. + if (!claimedRecognizer || (recognizer && (recognizer !== claimedRecognizer))) { + events.splice(i, 1); + if (claimedEventIndex) { + claimedEventIndex--; + } + } + } + } + me.claimRecognizer(claimedRecognizer, events[0]); + return claimedEventIndex; + }, + /** + * Deactivates all recognizers other than the "claimed" recognizer + * @param {Ext.event.gesture.Recognizer} claimedRecognizer + * @param {Ext.event.Event} e + * @private + */ + claimRecognizer: function(claimedRecognizer, e) { + var me = this, + recognizers = me.recognizers, + i, ln, recognizer; + for (i = 0 , ln = recognizers.length; i < ln; i++) { + recognizer = recognizers[i]; + // cancel recognition for all recognizers other than the one that was claimed + if (recognizer !== claimedRecognizer) { + recognizer.isActive = false; + recognizer.cancel(e); + } + } + if (me.events.length) { + // if any recognizers added cancelation events... + me.publishGestures(true); + } + }, + publishGestures: function(claimed) { + var me = this, + cancelEvents = me.cancelEvents, + events = me.events, + gestureTargets = me.gestureTargets; + if (cancelEvents.length) { + me.cancelEvents = []; + // Since cancellation events cannot be claimed we pass true here which + // prevents them from being claimed. + me.publish(cancelEvents, me.getPropagatingTargets(cancelEvents[0].target), true); + } + if (events.length) { + // It is important to reset the events property to an empty array before + // publishing since since events may be added to the array during publishing. + // This can happen if an event is claimed, thus triggering "cancel" gesture events. + me.events = []; + me.gestureTargets = null; + me.publish(events, gestureTargets || me.getPropagatingTargets(events[0].target), claimed); + } + }, + updateTouches: function(e) { + var me = this, + browserEvent = e.browserEvent, + type = e.type, + // the touchSource is the object from which we get data about the changed touch + // point or points related to an event object, such as identifier, target, and + // coordinates. For touch event the source is changedTouches, for mouse and + // pointer events it is the event object itself. + touchSources = browserEvent.changedTouches || [ + browserEvent + ], + activeTouches = me.activeTouches, + activeTouchesMap = me.activeTouchesMap, + changedTouches = [], + touchSource, identifier, touch, target, i, ln, x, y; + for (i = 0 , ln = touchSources.length; i < ln; i++) { + touchSource = touchSources[i]; + if ('identifier' in touchSource) { + // touch events have an identifier property on their touches objects. + // It can be 0, hence the "in" check + identifier = touchSource.identifier; + } else if ('pointerId' in touchSource) { + // Pointer events have a pointerId on the event object itself + identifier = touchSource.pointerId; + } else { + // Mouse events don't have an identifier, so we always use 1 since there + // can only be one mouse touch point active at a time + identifier = 1; + } + touch = activeTouchesMap[identifier]; + if (!touch) { + target = Ext.event.Event.resolveTextNode(touchSource.target); + touch = activeTouchesMap[identifier] = { + identifier: identifier, + target: target, + // There are 2 main advantages to caching the targets here, vs. + // waiting until onRecognized + // 1. for "move" events we don't have to construct the targets array + // for every event - a theoretical performance win + // 2. if the target is removed from the dom mid-gesture we still + // want any gestures listeners on elements that were above the + // target to complete. This means the propagating targets must reflect + // the target element's initial hierarchy when the gesture began + targets: me.getPropagatingTargets(target) + }; + activeTouches.push(touch); + } + if (me.isEndEvent[type] || me.isCancelEvent[type]) { + delete activeTouchesMap[identifier]; + Ext.Array.remove(activeTouches, touch); + } + x = Math.round(touchSource.pageX); + y = Math.round(touchSource.pageY); + touch.pageX = x; + touch.pageY = y; + // recognizers frequently use Point methods, so go ahead and create a Point. + touch.point = new Ext.util.Point(x, y); + changedTouches.push(touch); + } + // decorate the event object with the touch point info so that it can be used from + // within gesture recognizers (clone touches, just in case event object is used + // asynchronously since this.activeTouches is continuously modified) + e.touches = Ext.Array.clone(activeTouches); + // no need to clone changedTouches since we just created it from scratch + e.changedTouches = changedTouches; + }, + publishDelegatedDomEvent: function(e) { + var me = this; + if (!e.button || e.button < 1) { + // mouse gestures (and pointer gestures triggered by a mouse) can only be + // initiated using the left button (0). button value < 0 is also acceptable + // (e.g. pointermove has a button value of -1) + // Track the event on the instance so it can be fired after gesture recognition + // completes (if any gestures are recognized they will be added to this array) + me.events = [ + e + ]; + // This property on the browser event object indicates that the event has bubbled + // up to the window object and has begun being handled by the gesture publisher. + // If the user calls stopPropagation on an event that has not yet been "handled" + // it triggers gesture cancellation and cleanup. + e.browserEvent.$extHandled = true; + me.handlers[e.type].call(me, e); + } else { + // mouse events *with* button still need to be published. + me.callParent([ + e + ]); + } + }, + onTouchStart: function(e) { + var me = this, + target = e.target, + touches = e.browserEvent.touches; + if (e.browserEvent.type === 'touchstart') { + // When using touch events, if the target is removed from the dom mid-gesture + // the touchend event cannot be handled normally because it will not bubble + // to the top of the dom since the target el is no longer attached to the dom. + // Add some special handlers to clean everything up. (see onTargetTouchEnd) + // use addEventListener directly so that we don't have to spin up an instance + // of Ext.Element for every event target. + target.addEventListener('touchmove', me.onTargetTouchMove); + target.addEventListener('touchend', me.onTargetTouchEnd); + target.addEventListener('touchcancel', me.onTargetTouchEnd); + } + // There is a bug in IOS8 where touchstart, but not touchend event is + // fired when clicking on controls for audio/video, which can leave + // us in a bad state here. + if (touches && touches.length <= me.activeTouches.length) { + me.removeGhostTouches(touches); + } + me.updateTouches(e); + if (!me.isStarted) { + // Disable garbage collection during gestures so that if the target element + // of a gesture is removed from the dom, it does not get garbage collected + // until the gesture is complete + if (Ext.enableGarbageCollector) { + Ext.dom.GarbageCollector.pause(); + } + // this is the first active touch - invoke "onStart" which indicates the + // beginning of a gesture + me.isStarted = true; + me.invokeRecognizers('onStart', e); + } + me.invokeRecognizers('onTouchStart', e); + me.publishGestures(); + }, + onTouchMove: function(e) { + var me = this, + mousePointerType = me.mousePointerType, + isStarted = me.isStarted; + if (isStarted || (e.pointerType !== 'mouse')) { + me.updateTouches(e); + } + if (isStarted) { + // In IE10/11, the corresponding pointerup event is not fired after the pointerdown + // after the mouse is released from the scrollbar. However, it does fire a pointermove + // event with buttons: 0, so we capture that here and ensure the touch end process + // is completed. + if (mousePointerType && e.browserEvent.pointerType === mousePointerType && e.buttons === 0) { + e.type = Ext.dom.Element.prototype.eventMap.touchend; + e.button = 0; + me.onTouchEnd(e); + return; + } + if (e.changedTouches.length > 0) { + me.invokeRecognizers('onTouchMove', e); + } + } + me.publishGestures(); + }, + // This method serves as the handler for both "end" and "cancel" events. This is + // because they are handled identically with the exception of the recognizer method + // that is called. + onTouchEnd: function(e) { + var me = this, + isStarted = me.isStarted, + touchCount; + if (isStarted || (e.pointerType !== 'mouse')) { + me.updateTouches(e); + } + if (!isStarted) { + me.publishGestures(); + return; + } + touchCount = me.activeTouches.length; + // If an exception is thrown in any of the recognizers, we still need to run + // the cleanup. Otherwise the gesture might get "stuck" and *every* pointer event + // after that will fire the same handlers over and over, potentially spewing + // the same exceptions endlessly. See https://sencha.jira.com/browse/EXTJS-15674. + // We don't want to mask the original exception though, let it propagate. + try { + me.invokeRecognizers(me.isCancelEvent[e.type] ? 'onTouchCancel' : 'onTouchEnd', e); + } finally { + // This can throw too + try { + if (!touchCount) { + // no more active touches - invoke onEnd to indicate the end of the gesture + me.isStarted = false; + me.invokeRecognizers('onEnd', e); + } + } finally { + // Right, THIS can throw again! + try { + me.publishGestures(); + } finally { + if (!touchCount) { + // Gesture is finished, safe to resume garbage collection so that any target + // elements destroyed while gesture was in progress can be collected + if (Ext.enableGarbageCollector) { + Ext.dom.GarbageCollector.resume(); + } + } + // The parent code may not to be reached in this case + me.reEnterCountAdjusted = true; + me.reEnterCount--; + } + } + } + }, + onTargetTouchMove: function(e) { + if (Ext.elevateFunction) { + // using [e] is faster than using arguments in most browsers + // http://jsperf.com/passing-arguments + Ext.elevateFunction(this.doTargetTouchMove, this, [ + e + ]); + } else { + this.doTargetTouchMove(e); + } + }, + doTargetTouchMove: function(e) { + var me = this; + // handle touchmove if the target el was removed from dom mid-gesture. + // see onTouchStart/onTargetTouchEnd for further explanation + if (!Ext.getBody().contains(e.target)) { + me.reEnterCountAdjusted = false; + me.reEnterCount++; + this.onTouchMove(new Ext.event.Event(e)); + if (!me.reEnterCountAdjusted) { + me.reEnterCount--; + } + } + }, + onTargetTouchEnd: function(e) { + if (Ext.elevateFunction) { + // using [e] is faster than using arguments in most browsers + // http://jsperf.com/passing-arguments + Ext.elevateFunction(this.doTargetTouchEnd, this, [ + e + ]); + } else { + this.doTargetTouchEnd(e); + } + }, + doTargetTouchEnd: function(e) { + var me = this, + target = e.target; + target.removeEventListener('touchmove', me.onTargetTouchMove); + target.removeEventListener('touchend', me.onTargetTouchEnd); + target.removeEventListener('touchcancel', me.onTargetTouchEnd); + // if the target el was removed from the dom mid-gesture, then the touchend event, + // when it occurs, will not be handled because it will not bubble to the top of + // the dom. This is because the "target" of the touchend is the removed element. + // If this is the case, go ahead and trigger touchend handling now. + // Detect whether the target was removed from the DOM mid gesture by using Element.contains. + // Originally we attempted to detect this by listening for the DOMNodeRemovedFromDocument + // event, and setting a flag on the element when it was removed, however that + // approach only works when the element is removed using removedChild, and fails + // if the element is removed because some ancestor had innerHTML assigned. + // note: this handling is applicable for actual touchend events, pointer and mouse + // events will fire on whatever element is under the cursor/pointer after the + // original target has been removed. + if (!Ext.getBody().contains(target)) { + me.reEnterCountAdjusted = false; + me.reEnterCount++; + me.onTouchEnd(new Ext.event.Event(e)); + if (!me.reEnterCountAdjusted) { + me.reEnterCount--; + } + } + }, + /** + * Resets the internal state of the Gesture publisher and all of its recognizers. + * Applications will not typically need to use this method, but it is useful for + * Unit-testing situations where a clean slate is required for each test. + * + * Calling this method will also reset the state of Ext.event.publisher.Dom + */ + reset: function() { + var me = this, + recognizers = me.recognizers, + ln = recognizers.length, + i, recognizer; + me.activeTouchesMap = {}; + me.activeTouches = []; + me.changedTouches = []; + me.isStarted = false; + me.gestureTargets = null; + me.events = []; + me.cancelEvents = []; + for (i = 0; i < ln; i++) { + recognizer = recognizers[i]; + recognizer.reset(); + recognizer.isActive = false; + } + this.callParent(); + }, + privates: { + removeGhostTouches: function(touches) { + var ids = {}, + len = touches.length, + activeTouches = this.activeTouches, + map = this.activeTouchesMap, + i, id, touch; + // Collect the actual touches + for (i = 0; i < len; ++i) { + ids[touches[i].identifier] = true; + } + i = activeTouches.length; + while (i--) { + touch = activeTouches[i]; + id = touch.identifier; + if (!touches[id]) { + Ext.Array.remove(activeTouches, touch); + delete map[id]; + } + } + } + } +}, function(Gesture) { + var EventProto = Event.prototype, + stopPropagation = EventProto.stopPropagation; + if (stopPropagation) { + EventProto.stopPropagation = function() { + var me = this, + publisher = Gesture.instance, + type = me.type, + e; + if (!me.$extHandled && publisher.handles[type]) { + // User called stop propagation on a native event used by the gesture publisher + // to synthesize gesture events. Cancel gesture recognition and reset the publisher. + e = new Ext.event.Event(me); + publisher.updateTouches(e); + publisher.invokeRecognizers('onTouchCancel', e); + publisher.reset(); + publisher.reEnterCountAdjusted = true; + } + stopPropagation.apply(me, arguments); + }; + } + Gesture.instance = Ext.$gesturePublisher = new Gesture(); +}); + +/** + * + */ +Ext.define('Ext.mixin.Templatable', { + extend: Ext.Mixin, + mixinConfig: { + id: 'templatable' + }, + referenceAttributeName: 'reference', + referenceSelector: '[reference]', + getElementConfig: function() { + return { + reference: 'element' + }; + }, + getElementTemplate: function() { + var elementTemplate = document.createDocumentFragment(); + elementTemplate.appendChild(Ext.Element.create(this.getElementConfig(), true)); + return elementTemplate; + }, + initElement: function() { + var prototype = this.self.prototype; + prototype.elementTemplate = this.getElementTemplate(); + prototype.initElement = prototype.doInitElement; + this.initElement.apply(this, arguments); + }, + linkElement: function(reference, node) { + this.link(reference, node); + }, + doInitElement: function() { + var referenceAttributeName = this.referenceAttributeName, + renderElement, referenceNodes, i, ln, referenceNode, reference; + renderElement = this.elementTemplate.cloneNode(true); + referenceNodes = renderElement.querySelectorAll(this.referenceSelector); + for (i = 0 , ln = referenceNodes.length; i < ln; i++) { + referenceNode = referenceNodes[i]; + reference = referenceNode.getAttribute(referenceAttributeName); + referenceNode.removeAttribute(referenceAttributeName); + this.linkElement(reference, referenceNode); + } + } +}); + +/** + * @private + * Handle batch read / write of DOMs, currently used in SizeMonitor + PaintMonitor + */ +Ext.define('Ext.TaskQueue', { + singleton: true, + pending: false, + mode: true, + // true for 'read', false for 'write' + protectedReadQueue: [], + protectedWriteQueue: [], + readQueue: [], + writeQueue: [], + readRequestId: 0, + writeRequestId: 0, + timer: null, + constructor: function() { + var me = this; + me.run = me.run.bind(me); + // Some global things like floated wrapper are persistent and will add tasks/ + // add timers all the time, spoiling resource checks in our unit test suite. + // To work around that we're implementing a parallel queue where only trusted + // tasks will go, and fly under the radar of resource checker. + me.runProtected = Ext.Function.bind(me.run, me, [ + me.protectedReadQueue, + me.protectedWriteQueue, + 'runProtected' + ]); + me.runProtected.$skipTimerCheck = true; + // iOS has a nasty bug which causes pending requestAnimationFrame to not release + // the callback when the WebView is switched back and forth from / to being background + // process. We use a watchdog timer to workaround this, and restore the pending state + // correctly if this happens. This timer has to be set as an interval from the very + // beginning and we have to keep it running for as long as the app lives, setting it later + // doesn't seem to work. + // The watchdog timer must be accessible for environments to cancel. + if (Ext.os.is.iOS) { + me.watch.$skipTimerCheck = true; + me.watchdogTimer = Ext.interval(this.watch, 500, this); + } + }, + requestRead: function(fn, scope, args) { + var request = { + id: ++this.readRequestId, + fn: fn, + scope: scope, + args: args + }; + if (arguments[3] === true) { + this.protectedReadQueue.push(request); + this.request(true, 'runProtected'); + } else { + this.readQueue.push(request); + this.request(true); + } + return request.id; + }, + cancelRead: function(id) { + this.cancelRequest(this.readQueue, id, true); + }, + requestWrite: function(fn, scope, args) { + var me = this, + request = { + id: ++me.writeRequestId, + fn: fn, + scope: scope, + args: args + }; + if (arguments[3] === true) { + me.protectedWriteQueue.push(request); + me.request(false, 'runProtected'); + } else { + me.writeQueue.push(request); + me.request(false); + } + return request.id; + }, + cancelWrite: function(id) { + this.cancelRequest(this.writeQueue, id, false); + }, + request: function(mode, method) { + var me = this; + // Used below to cancel the correct timer. + /* eslint-disable-next-line one-var */ + var oldMode = me.mode; + if (!me.pending) { + me.pendingTime = Date.now(); + me.pending = true; + me.mode = mode; + if (mode) { + me.timer = Ext.defer(me[method] || me.run, 1); + } else { + me.timer = Ext.raf(me[method] || me.run); + } + } + // Last one should win + if (me.mode === mode && me.timer) { + if (oldMode) { + Ext.undefer(me.timer); + } else { + Ext.unraf(me.timer); + } + if (mode) { + me.timer = Ext.defer(me[method] || me.run, 1); + } else { + me.timer = Ext.raf(me[method] || me.run); + } + } + }, + cancelRequest: function(queue, id, mode) { + var i; + for (i = 0; i < queue.length; i++) { + if (queue[i].id === id) { + queue.splice(i, 1); + break; + } + } + if (!queue.length && this.mode === mode && this.timer) { + Ext.undefer(this.timer); + } + }, + watch: function() { + if (this.pending && Date.now() - this.pendingTime >= 500) { + this.run(); + } + }, + run: function(readQueue, writeQueue, method) { + var me = this, + mode = null, + queue, tasks, task, fn, scope, args, i, len; + readQueue = readQueue || me.readQueue; + writeQueue = writeQueue || me.writeQueue; + me.pending = false; + me.pending = me.timer = false; + if (me.mode) { + queue = readQueue; + if (writeQueue.length > 0) { + mode = false; + } + } else { + queue = writeQueue; + if (readQueue.length > 0) { + mode = true; + } + } + tasks = queue.slice(); + queue.length = 0; + for (i = 0 , len = tasks.length; i < len; i++) { + task = tasks[i]; + fn = task.fn; + scope = task.scope; + args = task.args; + if (scope && (scope.destroying || scope.destroyed)) { + + continue; + } + if (typeof fn === 'string') { + fn = scope[fn]; + } + if (args) { + fn.apply(scope, args); + } else { + fn.call(scope); + } + } + tasks.length = 0; + if (mode !== null) { + me.request(mode, method); + } + }, + clear: function() { + var me = this, + timer = me.timer; + if (timer) { + if (me.mode) { + Ext.undefer(timer); + } else { + Ext.unraf(timer); + } + } + me.readQueue.length = me.writeQueue.length = 0; + me.pending = me.timer = false; + me.mode = true; + }, + /* eslint-disable-next-line comma-style */ + privates: { + flush: function() { + var me = this, + mode = me.mode; + while (me.readQueue.length || me.writeQueue.length) { + if (mode) { + Ext.undefer(me.timer); + } else { + Ext.unraf(me.timer); + } + me.run(); + } + me.mode = true; + } + } +}); + +/** + * @private + */ +Ext.define('Ext.util.sizemonitor.Abstract', { + mixins: [ + Ext.mixin.Templatable + ], + config: { + element: null, + callback: Ext.emptyFn, + scope: null, + args: [] + }, + width: null, + height: null, + contentWidth: null, + contentHeight: null, + constructor: function(config) { + var me = this; + me.refresh = me.refresh.bind(me); + me.info = { + width: 0, + height: 0, + contentWidth: 0, + contentHeight: 0, + flag: 0 + }; + me.initElement(); + me.initConfig(config); + me.bindListeners(true); + }, + bindListeners: Ext.emptyFn, + applyElement: function(element) { + if (element) { + return Ext.get(element); + } + }, + updateElement: function(element) { + element.append(this.detectorsContainer, true); + element.addCls(Ext.baseCSSPrefix + 'size-monitored'); + }, + applyArgs: function(args) { + return args.concat([ + this.info + ]); + }, + refreshMonitors: Ext.emptyFn, + forceRefresh: function() { + Ext.TaskQueue.requestRead('refresh', this); + }, + getContentBounds: function() { + return this.detectorsContainer.getBoundingClientRect(); + }, + getContentWidth: function() { + return this.detectorsContainer.clientWidth; + }, + getContentHeight: function() { + return this.detectorsContainer.clientHeight; + }, + refreshSize: function() { + var element = this.getElement(); + if (!element || element.destroyed) { + return false; + } + // eslint-disable-next-line vars-on-top + var me = this, + size = element.measure(), + width = size.width, + height = size.height, + contentWidth = me.getContentWidth(), + contentHeight = me.getContentHeight(), + currentContentWidth = me.contentWidth, + currentContentHeight = me.contentHeight, + info = me.info, + resized = false, + flag; + me.width = width; + me.height = height; + me.contentWidth = contentWidth; + me.contentHeight = contentHeight; + flag = ((currentContentWidth !== contentWidth ? 1 : 0) + (currentContentHeight !== contentHeight ? 2 : 0)); + if (flag > 0) { + info.width = width; + info.height = height; + info.contentWidth = contentWidth; + info.contentHeight = contentHeight; + info.flag = flag; + resized = true; + me.getCallback().apply(me.getScope(), me.getArgs()); + } + return resized; + }, + refresh: function() { + if (this.destroying || this.destroyed) { + return; + } + this.refreshSize(); + // We should always refresh the monitors regardless of whether or not refreshSize + // resulted in a new size. This avoids race conditions in situations such as + // panel placeholder expand where we layout the panel in its expanded state momentarily + // just so we can measure its animation destination, then immediately collapse it. + // In such a scenario refreshSize() will be acting on the original size since it + // is asynchronous, so it will not detect a size change, but we still need to + // ensure that the monitoring elements are in sync, or else the next resize event + // will not fire. + Ext.TaskQueue.requestWrite('refreshMonitors', this); + }, + destroy: function() { + var me = this, + element = me.getElement(); + me.bindListeners(false); + if (element && !element.destroyed) { + element.removeCls(Ext.baseCSSPrefix + 'size-monitored'); + } + delete me._element; + // This is a closure so Base destructor won't null it + me.refresh = null; + me.callParent(); + } +}); + +/** + * @private + */ +Ext.define('Ext.util.sizemonitor.Scroll', { + extend: Ext.util.sizemonitor.Abstract, + getElementConfig: function() { + return { + reference: 'detectorsContainer', + classList: [ + Ext.baseCSSPrefix + 'size-monitors', + 'scroll' + ], + children: [ + { + reference: 'expandMonitor', + className: 'expand' + }, + { + reference: 'shrinkMonitor', + className: 'shrink' + } + ] + }; + }, + constructor: function(config) { + this.onScroll = this.onScroll.bind(this); + this.callParent(arguments); + }, + bindListeners: function(bind) { + var method = bind ? 'addEventListener' : 'removeEventListener'; + this.expandMonitor[method]('scroll', this.onScroll, true); + this.shrinkMonitor[method]('scroll', this.onScroll, true); + }, + onScroll: function() { + if (!this.destroyed) { + Ext.TaskQueue.requestRead('refresh', this); + } + }, + refreshMonitors: function() { + var expandMonitor = this.expandMonitor, + shrinkMonitor = this.shrinkMonitor, + end = 1000000; + if (expandMonitor && !expandMonitor.destroyed) { + expandMonitor.scrollLeft = end; + expandMonitor.scrollTop = end; + } + if (shrinkMonitor && !shrinkMonitor.destroyed) { + shrinkMonitor.scrollLeft = end; + shrinkMonitor.scrollTop = end; + } + }, + destroy: function() { + // This is a closure so Base destructor won't null it + this.onScroll = null; + this.callParent(); + } +}); + +/** + * + */ +Ext.define('Ext.util.SizeMonitor', { + // 'Ext.util.sizemonitor.OverflowChange' + constructor: function(config) { + return new Ext.util.sizemonitor.Scroll(config); + } +}); +// var namespace = Ext.util.sizemonitor; +// +// if (Ext.browser.is.Firefox) { +// // this one decreases the grid performance in Firefox +// return new namespace.OverflowChange(config); +// } else { +// return new namespace.Scroll(config); +// } + +/** + * @private + */ +Ext.define('Ext.event.publisher.ElementSize', { + extend: Ext.event.publisher.Publisher, + type: 'size', + handledEvents: [ + 'resize' + ], + constructor: function() { + this.monitors = {}; + this.subscribers = {}; + this.callParent(arguments); + }, + subscribe: function(element) { + var id = element.id, + subscribers = this.subscribers, + monitors = this.monitors; + if (subscribers[id]) { + ++subscribers[id]; + } else { + subscribers[id] = 1; + monitors[id] = new Ext.util.SizeMonitor({ + element: element, + callback: this.onElementResize, + scope: this, + args: [ + element + ] + }); + } + element.on('painted', 'forceRefresh', monitors[id]); + return true; + }, + unsubscribe: function(element) { + var id = element.id, + subscribers = this.subscribers, + monitors = this.monitors, + sizeMonitor; + if (subscribers[id] && !--subscribers[id]) { + delete subscribers[id]; + sizeMonitor = monitors[id]; + element.un('painted', 'forceRefresh', sizeMonitor); + sizeMonitor.destroy(); + delete monitors[id]; + } + if (element.activeRead) { + Ext.TaskQueue.cancelRead(element.activeRead); + } + }, + fireElementResize: function(element, info) { + delete element.activeRead; + this.fire(element, 'resize', [ + element, + info + ]); + }, + onElementResize: function(element, info) { + if (!element.activeRead) { + element.activeRead = Ext.TaskQueue.requestRead('fireElementResize', this, [ + element, + info + ], !!element.$skipResourceCheck); + } + }, + // eslint-disable-line comma-style + // This is useful for unit testing so we can force resizes + // to take place synchronously when we know they have changed + privates: { + // eslint-disable-line comma-style + syncRefresh: function(elements) { + var el, monitor, i, len; + elements = Ext.Array.from(elements); + for (i = 0 , len = elements.length; i < len; ++i) { + el = elements[i]; + if (typeof el !== 'string') { + el = el.id; + } + monitor = this.monitors[el]; + if (monitor) { + monitor.forceRefresh(); + } + } + // This just pushes onto the RAF queue. + Ext.TaskQueue.flush(); + // Flush the RAF queue to make this truly synchronous. + Ext.Function.fireElevatedHandlers(); + } + } +}, function(ElementSize) { + ElementSize.instance = new ElementSize(); +}); + +/** + * @private + */ +Ext.define('Ext.util.paintmonitor.Abstract', { + config: { + element: null, + callback: Ext.emptyFn, + scope: null, + args: [] + }, + eventName: '', + monitorClass: '', + constructor: function(config) { + this.onElementPainted = this.onElementPainted.bind(this); + this.initConfig(config); + }, + bindListeners: function(bind) { + // eslint-disable-next-line max-len + this.monitorElement[bind ? 'addEventListener' : 'removeEventListener'](this.eventName, this.onElementPainted, true); + }, + applyElement: function(element) { + if (element) { + return Ext.get(element); + } + }, + updateElement: function(element) { + this.monitorElement = Ext.Element.create({ + classList: [ + Ext.baseCSSPrefix + 'paint-monitor', + this.monitorClass + ] + }, true); + element.appendChild(this.monitorElement, true); + element.addCls(Ext.baseCSSPrefix + 'paint-monitored'); + this.bindListeners(true); + }, + onElementPainted: function() {}, + destroy: function() { + var me = this, + monitorElement = me.monitorElement, + parentNode = monitorElement.parentNode, + element = me.getElement(); + me.bindListeners(false); + delete me.monitorElement; + if (element && !element.destroyed) { + element.removeCls(Ext.baseCSSPrefix + 'paint-monitored'); + delete me._element; + } + if (parentNode) { + parentNode.removeChild(monitorElement); + } + me.callParent(); + } +}); + +/** + * @private + */ +Ext.define('Ext.util.paintmonitor.CssAnimation', { + extend: Ext.util.paintmonitor.Abstract, + eventName: Ext.browser.is.WebKit ? 'webkitAnimationEnd' : 'animationend', + monitorClass: 'cssanimation', + onElementPainted: function(e) { + if (e.animationName === Ext.baseCSSPrefix + 'paint-monitor-helper') { + this.getCallback().apply(this.getScope(), this.getArgs()); + } + } +}); + +/** + * + */ +Ext.define('Ext.util.PaintMonitor', { + constructor: function(config) { + return new Ext.util.paintmonitor.CssAnimation(config); + } +}); + +/** + * @private + */ +Ext.define('Ext.event.publisher.ElementPaint', { + extend: Ext.event.publisher.Publisher, + type: 'paint', + handledEvents: [ + 'painted' + ], + constructor: function() { + this.monitors = {}; + this.subscribers = {}; + this.callParent(arguments); + }, + subscribe: function(element) { + var me = this, + id = element.id, + subscribers = me.subscribers; + if (subscribers[id]) { + ++subscribers[id]; + } else { + subscribers[id] = 1; + me.monitors[id] = new Ext.util.PaintMonitor({ + element: element, + callback: me.onElementPainted, + scope: me, + args: [ + element + ] + }); + } + }, + unsubscribe: function(element) { + var id = element.id, + subscribers = this.subscribers, + monitors = this.monitors; + if (subscribers[id] && !--subscribers[id]) { + delete subscribers[id]; + monitors[id].destroy(); + delete monitors[id]; + } + if (element.activeRead) { + Ext.TaskQueue.cancelRead(element.activeRead); + } + }, + fireElementPainted: function(element) { + delete element.activeRead; + this.fire(element, 'painted', [ + element + ]); + }, + onElementPainted: function(element) { + if (!element.activeRead) { + element.activeRead = Ext.TaskQueue.requestRead('fireElementPainted', this, [ + element + ], !!element.$skipResourceCheck); + } + } +}, // eslint-disable-line comma-style +function(ElementPaint) { + ElementPaint.instance = new ElementPaint(); +}); + +/** + * @class Ext.dom.Element + * @alternateClassName Ext.Element + * @mixins Ext.util.Positionable + * @mixins Ext.mixin.Observable + * + * Encapsulates a DOM element, adding simple DOM manipulation facilities, normalizing for browser + * differences. + * + * **Note:** The events included in this Class are the ones we've found to be the most commonly + * used. Many events are not listed here due to the expedient rate of change across browsers. + * For a more comprehensive list, please visit the following resources: + * + * + [Mozilla Event Reference Guide](https://developer.mozilla.org/en-US/docs/Web/Events) + * + [W3 Pointer Events](http://www.w3.org/TR/pointerevents/) + * + [W3 Touch Events](http://www.w3.org/TR/touch-events/) + * + [W3 DOM 2 Events](http://www.w3.org/TR/DOM-Level-2-Events/) + * + [W3 DOM 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/) + * + * ## Usage + * + * // by id + * var el = Ext.get("my-div"); + * + * // by DOM element reference + * var el = Ext.get(myDivElement); + * + * ## Selecting Descendant Elements + * + * Ext.dom.Element instances can be used to select descendant nodes using CSS selectors. + * There are 3 methods that can be used for this purpose, each with a slightly different + * twist: + * + * - {@link #method-query} + * - {@link #method-selectNode} + * - {@link #method-select} + * + * These methods can accept any valid CSS selector since they all use + * [querySelectorAll](http://www.w3.org/TR/css3-selectors/) under the hood. The primary + * difference between these three methods is their return type: + * + * To get an array of HTMLElement instances matching the selector '.foo' use the query + * method: + * + * element.query('.foo'); + * + * This can easily be transformed into an array of Ext.dom.Element instances by setting + * the `asDom` parameter to `false`: + * + * element.query('.foo', false); + * + * If the desired result is only the first matching HTMLElement use the selectNode method: + * + * element.selectNode('.foo'); + * + * Once again, the dom node can be wrapped in an Ext.dom.Element by setting the `asDom` + * parameter to `false`: + * + * element.selectNode('.foo', false); + * + * The `select` method is used when the desired return type is a {@link + * Ext.CompositeElementLite CompositeElementLite} or a {@link Ext.CompositeElement + * CompositeElement}. These are collections of elements that can be operated on as a + * group using any of the methods of Ext.dom.Element. The only difference between the two + * is that CompositeElementLite is a collection of HTMLElement instances, while + * CompositeElement is a collection of Ext.dom.Element instances. To retrieve a + * CompositeElementLite that represents a collection of HTMLElements for selector '.foo': + * + * element.select('.foo'); + * + * For a {@link Ext.CompositeElement CompositeElement} simply pass `true` as the + * `composite` parameter: + * + * element.select('.foo', true); + * + * The query selection methods can be used even if you don't have a Ext.dom.Element to + * start with For example to select an array of all HTMLElements in the document that match the + * selector '.foo', simply wrap the document object in an Ext.dom.Element instance using + * {@link Ext#fly}: + * + * Ext.fly(document).query('.foo'); + * + * # Animations + * + * When an element is manipulated, by default there is no animation. + * + * var el = Ext.get("my-div"); + * + * // no animation + * el.setWidth(100); + * + * specified as boolean (true) for default animation effects. + * + * // default animation + * el.setWidth(100, true); + * + * To configure the effects, an object literal with animation options to use as the Element + * animation configuration object can also be specified. Note that the supported Element animation + * configuration options are a subset of the {@link Ext.fx.Anim} animation options specific to Fx + * effects. The supported Element animation configuration options are: + * + * Option Default Description + * --------- -------- --------------------------------------------- + * duration 350 The duration of the animation in milliseconds + * easing easeOut The easing method + * callback none A function to execute when the anim completes + * scope this The scope (this) of the callback function + * + * Usage: + * + * // Element animation options object + * var opt = { + * duration: 1000, + * easing: 'elasticIn', + * callback: this.foo, + * scope: this + * }; + * // animation with some options set + * el.setWidth(100, opt); + * + * The Element animation object being used for the animation will be set on the options object + * as "anim", which allows you to stop or manipulate the animation. Here is an example: + * + * // using the "anim" property to get the Anim object + * if(opt.anim.isAnimated()){ + * opt.anim.stop(); + * } + */ +Ext.define('Ext.dom.Element', function(Element) { + var WIN = window, + DOC = document, + docEl = DOC.documentElement, + WIN_TOP = WIN.top, + EMPTY = [], + elementIdCounter, windowId, documentId, + WIDTH = 'width', + HEIGHT = 'height', + MIN_WIDTH = 'min-width', + MIN_HEIGHT = 'min-height', + MAX_WIDTH = 'max-width', + MAX_HEIGHT = 'max-height', + TOP = 'top', + RIGHT = 'right', + BOTTOM = 'bottom', + LEFT = 'left', + VISIBILITY = 'visibility', + HIDDEN = 'hidden', + DISPLAY = "display", + NONE = "none", + ZINDEX = "z-index", + POSITION = "position", + RELATIVE = "relative", + STATIC = "static", + wordsRe = /\w/g, + spacesRe = /\s+/, + classNameSplitRegex = /[\s]+/, + transparentRe = /^(?:transparent|(?:rgba[(](?:\s*\d+\s*[,]){3}\s*0\s*[)]))$/i, + endsQuestionRe = /\?$/, + topRe = /top/i, + empty = {}, + borders = { + t: 'border-top-width', + r: 'border-right-width', + b: 'border-bottom-width', + l: 'border-left-width' + }, + paddings = { + t: 'padding-top', + r: 'padding-right', + b: 'padding-bottom', + l: 'padding-left' + }, + margins = { + t: 'margin-top', + r: 'margin-right', + b: 'margin-bottom', + l: 'margin-left' + }, + selectDir = { + b: 'backward', + back: 'backward', + f: 'forward' + }, + paddingsTLRB = [ + paddings.l, + paddings.r, + paddings.t, + paddings.b + ], + bordersTLRB = [ + borders.l, + borders.r, + borders.t, + borders.b + ], + numberRe = /\d+$/, + unitRe = /\d+(px|r?em|%|vh|vw|vmin|vmax|en|ch|ex|pt|in|cm|mm|pc)$/i, + defaultUnit = 'px', + msRe = /^-ms-/, + camelRe = /(-[a-z])/gi, + /* eslint-disable-next-line no-useless-escape */ + cssRe = /([a-z0-9\-]+)\s*:\s*([^;\s]+(?:\s*[^;\s]+)*);?/gi, + pxRe = /^\d+(?:\.\d*)?px$/i, + relativeUnitRe = /(%|r?em|auto|vh|vw|vmin|vmax|ch|ex)$/i, + propertyCache = {}, + ORIGINALDISPLAY = 'originalDisplay', + camelReplaceFn = function(m, a) { + return a.charAt(1).toUpperCase(); + }, + clearData = function(node, deep) { + var childNodes, i, len; + // Only Element nodes may have _extData and child nodes to clear. + // IE8 throws an error attempting to set expandos on non-Element nodes. + if (node.nodeType === 1) { + node._extData = null; + if (deep) { + childNodes = node.childNodes; + for (i = 0 , len = childNodes.length; i < len; ++i) { + clearData(childNodes[i], deep); + } + } + } + }, + toFloat = function(v) { + return parseFloat(v) || 0; + }, + opacityCls = Ext.baseCSSPrefix + 'hidden-opacity', + visibilityCls = Ext.baseCSSPrefix + 'hidden-visibility', + displayCls = Ext.baseCSSPrefix + 'hidden-display', + offsetsCls = Ext.baseCSSPrefix + 'hidden-offsets', + clipCls = Ext.baseCSSPrefix + 'hidden-clip', + lastFocusChange = 0, + lastKeyboardClose = 0, + editableHasFocus = false, + isVirtualKeyboardOpen = false, + inputTypeSelectionSupported = /text|password|search|tel|url/i, + visFly, scrollFly, caFly, wrapFly, grannyFly, activeElFly; + // We use element ID counter to prevent assigning the same id to top and nested + // window and document objects when Ext is running in