]> git.proxmox.com Git - extjs.git/blame - extjs/classic/classic/src/view/NavigationModel.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / view / NavigationModel.js
CommitLineData
6527f429
DM
1/**\r
2 * @class Ext.view.NavigationModel\r
3 * @private\r
4 * This class listens for key events fired from a {@link Ext.view.View DataView}, and moves the currently focused item\r
5 * by adding the class {@link #focusCls}.\r
6 */\r
7Ext.define('Ext.view.NavigationModel', {\r
8 mixins: [\r
9 'Ext.util.Observable',\r
10 'Ext.mixin.Factoryable',\r
11 'Ext.util.StoreHolder'\r
12 ],\r
13\r
14 alias: 'view.navigation.default',\r
15\r
16 config: {\r
17 store: null\r
18 },\r
19\r
20 /**\r
21 * @event navigate Fired when a key has been used to navigate around the view.\r
22 * @param {Object} event\r
23 * @param {Ext.event.Event} keyEvent The key event which caused the navigation.\r
24 * @param {Number} event.previousRecordIndex The previously focused record index.\r
25 * @param {Ext.data.Model} event.previousRecord The previously focused record.\r
26 * @param {HTMLElement} event.previousItem The previously focused view item.\r
27 * @param {Number} event.recordIndex The newly focused record index.\r
28 * @param {Ext.data.Model} event.record the newly focused record.\r
29 * @param {HTMLElement} event.item the newly focused view item.\r
30 */\r
31\r
32 /**\r
33 * @private\r
34 */\r
35 focusCls: Ext.baseCSSPrefix + 'view-item-focused',\r
36\r
37 constructor: function() {\r
38 this.mixins.observable.constructor.call(this);\r
39 },\r
40\r
41 bindComponent: function(view) {\r
42 if (this.view !== view) {\r
43 this.view = view;\r
44 this.bindView(view);\r
45 }\r
46 },\r
47\r
48 bindView: function(view) {\r
49 var me = this,\r
50 dataSource = view.dataSource,\r
51 listeners;\r
52\r
53\r
54 me.initKeyNav(view);\r
55 if (!dataSource.isEmptyStore) {\r
56 me.setStore(dataSource);\r
57 }\r
58 listeners = me.getViewListeners();\r
59 listeners.destroyable = true;\r
60 me.viewListeners = me.viewListeners || [];\r
61 me.viewListeners.push(view.on(listeners));\r
62 },\r
63\r
64 updateStore: function(store) {\r
65 this.mixins.storeholder.bindStore.apply(this, [store]);\r
66 },\r
67\r
68 getViewListeners: function() {\r
69 var me = this;\r
70\r
71 return {\r
72 containermousedown: me.onContainerMouseDown,\r
73 itemmousedown: me.onItemMouseDown,\r
74\r
75 // We focus on click if the mousedown handler did not focus because it was a translated "touchstart" event.\r
76 itemclick: me.onItemClick,\r
77 itemcontextmenu: me.onItemMouseDown,\r
78 scope: me\r
79 };\r
80 },\r
81\r
82 initKeyNav: function(view) {\r
83 var me = this;\r
84\r
85 // Drive the KeyNav off the View's itemkeydown event so that beforeitemkeydown listeners may veto.\r
86 // By default KeyNav uses defaultEventAction: 'stopEvent', and this is required for movement keys\r
87 // which by default affect scrolling.\r
88 me.keyNav = new Ext.util.KeyNav({\r
89 target: view,\r
90 ignoreInputFields: true,\r
91 eventName: 'itemkeydown',\r
92 defaultEventAction: 'stopEvent',\r
93 processEvent: me.processViewEvent,\r
94 up: me.onKeyUp,\r
95 down: me.onKeyDown,\r
96 right: me.onKeyRight,\r
97 left: me.onKeyLeft,\r
98 pageDown: me.onKeyPageDown,\r
99 pageUp: me.onKeyPageUp,\r
100 home: me.onKeyHome,\r
101 end: me.onKeyEnd,\r
102 space: me.onKeySpace,\r
103 enter: me.onKeyEnter,\r
104 A: {\r
105 ctrl: true,\r
106 // Need a separate function because we don't want the key\r
107 // events passed on to selectAll (causes event suppression).\r
108 handler: me.onSelectAllKeyPress\r
109 },\r
110 scope: me\r
111 });\r
112 },\r
113\r
114 processViewEvent: function(view, record, node, index, event) {\r
115 return event;\r
116 },\r
117\r
118 addKeyBindings: function(binding) {\r
119 this.keyNav.addBindings(binding);\r
120 },\r
121\r
122 enable: function() {\r
123 this.keyNav.enable();\r
124 this.disabled = false;\r
125 },\r
126\r
127 disable: function() {\r
128 this.keyNav.disable();\r
129 this.disabled = true;\r
130 },\r
131\r
132 onContainerMouseDown: function(view, mousedownEvent) {\r
133 // If the mousedown in the view element is NOT inside the client region,\r
134 // that is, it was on a scrollbar, then prevent default.\r
135 //\r
136 // Mousedowning on a scrollbar will focus the View.\r
137 // If they have scrolled to the bottom, then onFocusEnter will\r
138 // try to focus the lastFocused or first item. This is undesirable.\r
139 // So on mousedown outside of view client area, prevent the default focus behaviour.\r
140 if (mousedownEvent.pointerType === 'mouse' && Ext.getScrollbarSize().width) {\r
141 if (!view.el.getClientRegion().contains(mousedownEvent.getPoint())) {\r
142 mousedownEvent.preventDefault();\r
143 }\r
144 }\r
145 },\r
146\r
147 onItemMouseDown: function(view, record, item, index, mousedownEvent) {\r
148 // If the event is a touchstart, leave it until the click to focus.\r
149 if (mousedownEvent.pointerType !== 'touch') {\r
150 this.setPosition(index);\r
151 }\r
152 },\r
153\r
154 onItemClick: function(view, record, item, index, clickEvent) {\r
155 // If the mousedown that initiated the click has navigated us to the correct spot, just fire the event\r
156 if (this.record === record) {\r
157 this.fireNavigateEvent(clickEvent);\r
158 } else {\r
159 this.setPosition(index, clickEvent);\r
160 }\r
161 },\r
162\r
163 setPosition: function(recordIndex, keyEvent, suppressEvent, preventNavigation) {\r
164 var me = this,\r
165 view = me.view,\r
166 selModel = view.getSelectionModel(),\r
167 dataSource = view.dataSource,\r
168 newRecord,\r
169 newRecordIndex;\r
170\r
171 if (recordIndex == null || !view.all.getCount()) {\r
172 me.record = me.recordIndex = null;\r
173 } else {\r
174 if (typeof recordIndex === 'number') {\r
175 newRecordIndex = Math.max(Math.min(recordIndex, dataSource.getCount() - 1), 0);\r
176 newRecord = dataSource.getAt(recordIndex);\r
177 }\r
178 // row is a Record\r
179 else if (recordIndex.isEntity) {\r
180 newRecord = dataSource.getById(recordIndex.id);\r
181 newRecordIndex = dataSource.indexOf(newRecord);\r
182\r
183 // Previous record is no longer present; revert to first.\r
184 if (newRecordIndex === -1) {\r
185 newRecord = dataSource.getAt(0);\r
186 newRecordIndex = 0;\r
187 }\r
188 }\r
189 // row is a view item\r
190 else if (recordIndex.tagName) {\r
191 newRecord = view.getRecord(recordIndex);\r
192 newRecordIndex = dataSource.indexOf(newRecord);\r
193 }\r
194 else {\r
195 newRecord = newRecordIndex = null;\r
196 }\r
197 }\r
198\r
199 // No change; just ensure the correct item is focused and return early.\r
200 // Do not push current position into previous position, do not fire events.\r
201 // We must check record instances, not indices because of store reloads (combobox remote filtering).\r
202 // If there's a new record, focus it. Note that the index may be different even though\r
203 // the record is the same (filtering, sorting)\r
204 if (newRecord === me.record) {\r
205 me.recordIndex = newRecordIndex;\r
206 return me.focusPosition(newRecordIndex);\r
207 }\r
208\r
209 if (me.item) {\r
210 me.item.removeCls(me.focusCls);\r
211 }\r
212\r
213 // Track the last position.\r
214 // Used by SelectionModels as the navigation "from" position.\r
215 me.previousRecordIndex = me.recordIndex;\r
216 me.previousRecord = me.record;\r
217 me.previousItem = me.item;\r
218\r
219 // Update our position\r
220 me.recordIndex = newRecordIndex;\r
221 me.record = newRecord;\r
222\r
223 // Prevent navigation if focus has not moved\r
224 preventNavigation = preventNavigation || me.record === me.lastFocused;\r
225\r
226 // Maintain lastFocused, so that on non-specific focus of the View, we can focus the correct descendant.\r
227 if (newRecord) {\r
228 me.focusPosition(me.recordIndex);\r
229 } else {\r
230 me.item = null;\r
231 }\r
232\r
233 if (!suppressEvent) {\r
234 selModel.fireEvent('focuschange', selModel, me.previousRecord, me.record);\r
235 }\r
236\r
237 // If we have moved, fire an event\r
238 if (!preventNavigation && keyEvent) {\r
239 me.fireNavigateEvent(keyEvent);\r
240 }\r
241 },\r
242\r
243 /**\r
244 * @private\r
245 * Focuses the currently active position.\r
246 * This is used on view refresh and on replace.\r
247 */\r
248 focusPosition: function(recordIndex) {\r
249 var me = this;\r
250\r
251 if (recordIndex != null && recordIndex !== -1) {\r
252 if (recordIndex.isEntity) {\r
253 recordIndex = me.view.dataSource.indexOf(recordIndex);\r
254 }\r
255 me.item = me.view.all.item(recordIndex);\r
256 if (me.item) {\r
257 me.lastFocused = me.record;\r
258 me.lastFocusedIndex = me.recordIndex;\r
259 me.focusItem(me.item);\r
260 } else {\r
261 me.record = null;\r
262 }\r
263 } else {\r
264 me.item = null;\r
265 }\r
266 },\r
267\r
268 /**\r
269 * @template\r
270 * @protected\r
271 * Called to focus an item in the client {@link Ext.view.View DataView}.\r
272 * The default implementation adds the {@link #focusCls} to the passed item focuses it.\r
273 * Subclasses may choose to keep focus in another target.\r
274 *\r
275 * For example {@link Ext.view.BoundListKeyNav} maintains focus in the input field.\r
276 * @param {Ext.dom.Element} item\r
277 * @return {undefined}\r
278 */\r
279 focusItem: function(item) {\r
280 item.addCls(this.focusCls);\r
281 item.focus();\r
282 },\r
283\r
284 getPosition: function() {\r
285 return this.record ? this.recordIndex : null;\r
286 },\r
287\r
288 getRecordIndex: function() {\r
289 return this.recordIndex;\r
290 },\r
291\r
292 getItem: function() {\r
293 return this.item;\r
294 },\r
295\r
296 getRecord: function() {\r
297 return this.record;\r
298 },\r
299\r
300 getLastFocused: function() {\r
301 // No longer there. The caller must fall back to a default.\r
302 if (this.view.dataSource.indexOf(this.lastFocused) === -1) {\r
303 return null;\r
304 }\r
305 return this.lastFocused;\r
306 },\r
307\r
308 onKeyUp: function(keyEvent) {\r
309 var newPosition = this.recordIndex - 1;\r
310 if (newPosition < 0) {\r
311 newPosition = this.view.all.getCount() - 1;\r
312 }\r
313 this.setPosition(newPosition, keyEvent);\r
314 },\r
315\r
316 onKeyDown: function(keyEvent) {\r
317 var newPosition = this.recordIndex + 1;\r
318 if (newPosition > this.view.all.getCount() - 1) {\r
319 newPosition = 0;\r
320 }\r
321 this.setPosition(newPosition, keyEvent);\r
322 },\r
323 \r
324 onKeyRight: function(keyEvent) {\r
325 var newPosition = this.recordIndex + 1;\r
326 if (newPosition > this.view.all.getCount() - 1) {\r
327 newPosition = 0;\r
328 }\r
329 this.setPosition(newPosition, keyEvent);\r
330 },\r
331 \r
332 onKeyLeft: function(keyEvent) {\r
333 var newPosition = this.recordIndex - 1;\r
334 if (newPosition < 0) {\r
335 newPosition = this.view.all.getCount() - 1;\r
336 }\r
337 this.setPosition(newPosition, keyEvent);\r
338 },\r
339 \r
340 onKeyPageDown: Ext.emptyFn,\r
341 \r
342 onKeyPageUp: Ext.emptyFn,\r
343 \r
344 onKeyHome: function(keyEvent) {\r
345 this.setPosition(0, keyEvent);\r
346 },\r
347 \r
348 onKeyEnd: function(keyEvent) {\r
349 this.setPosition(this.view.all.getCount() - 1, keyEvent);\r
350 },\r
351 \r
352 onKeySpace: function(keyEvent) {\r
353 this.fireNavigateEvent(keyEvent);\r
354 },\r
355\r
356 // ENTER emulates an itemclick event at the View level\r
357 onKeyEnter: function(keyEvent) {\r
358 // Stop the keydown event so that an ENTER keyup does not get delivered to\r
359 // any element which focus is transferred to in a click handler.\r
360 keyEvent.stopEvent();\r
361 keyEvent.view.fireEvent('itemclick', keyEvent.view, keyEvent.record, keyEvent.item, keyEvent.recordIndex, keyEvent);\r
362 },\r
363\r
364 onSelectAllKeyPress: function(keyEvent) {\r
365 this.fireNavigateEvent(keyEvent);\r
366 },\r
367\r
368 fireNavigateEvent: function(keyEvent) {\r
369 var me = this;\r
370\r
371 me.fireEvent('navigate', {\r
372 navigationModel: me,\r
373 keyEvent: keyEvent,\r
374 previousRecordIndex: me.previousRecordIndex,\r
375 previousRecord: me.previousRecord,\r
376 previousItem: me.previousItem, \r
377 recordIndex: me.recordIndex,\r
378 record: me.record,\r
379 item: me.item\r
380 });\r
381 },\r
382\r
383 destroy: function() {\r
384 var me = this;\r
385 \r
386 me.setStore(null);\r
387 Ext.destroy(me.viewListeners, me.keyNav);\r
388 me.keyNav = me.viewListeners = me.dataSource = me.lastFocused = null;\r
389 \r
390 me.callParent();\r
391 }\r
392});\r