]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/form/GlobalSearchField.js
Add context menu to global search entries
[pve-manager.git] / www / manager6 / form / GlobalSearchField.js
1 /*
2 * This is a global search field
3 * it loads the /cluster/resources on focus
4 * and displays the result in a floating grid
5 *
6 * it filters and sorts the objects by the algorithm in
7 * the customFilter function
8 *
9 * also it does accept key up/down and enter for input
10 * and it opens to ctrl+shift+f and ctrl+space
11 */
12 Ext.define('PVE.form.GlobalSearchField', {
13 extend: 'Ext.form.field.Text',
14 alias: 'widget.pveGlobalSearchField',
15
16 emptyText: gettext('Search'),
17 enableKeyEvents: true,
18 selectOnFocus: true,
19 padding: '0 5 0 5',
20
21 grid: {
22 xtype: 'gridpanel',
23 focusOnToFront: false,
24 floating: true,
25 emptyText: Proxmox.Utils.noneText,
26 width: 600,
27 height: 400,
28 scrollable: {
29 xtype: 'scroller',
30 y: true,
31 x:false
32 },
33 store: {
34 model: 'PVEResources',
35 proxy:{
36 type: 'proxmox',
37 url: '/api2/extjs/cluster/resources'
38 }
39 },
40 plugins: {
41 ptype: 'bufferedrenderer',
42 trailingBufferZone: 20,
43 leadingBufferZone: 20
44 },
45
46 hideMe: function() {
47 var me = this;
48 if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) {
49 return;
50 }
51 me.hasFocus = false;
52 if (!me.textfield.hasFocus) {
53 me.hide();
54 }
55 },
56
57 setFocus: function() {
58 var me = this;
59 me.hasFocus = true;
60 },
61
62 listeners: {
63 rowclick: function(grid, record) {
64 var me = this;
65 me.textfield.selectAndHide(record.id);
66 },
67 itemcontextmenu: function(v, record, item, index, event) {
68 var me = this;
69 me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event);
70 },
71 /* because of lint */
72 focusleave: {
73 fn: 'hideMe'
74 },
75 focusenter: 'setFocus'
76 },
77
78 columns: [
79 {
80 text: gettext('Type'),
81 dataIndex: 'type',
82 width: 100,
83 renderer: PVE.Utils.render_resource_type
84 },
85 {
86 text: gettext('Description'),
87 flex: 1,
88 dataIndex: 'text'
89 },
90 {
91 text: gettext('Node'),
92 dataIndex: 'node'
93 },
94 {
95 text: gettext('Pool'),
96 dataIndex: 'pool'
97 }
98 ]
99 },
100
101 customFilter: function(item) {
102 var me = this;
103 var match = 0;
104 var fieldArr = [];
105 var i,j, fields;
106
107 // different types of objects have different fields to search
108 // for example, a node will never have a pool and vice versa
109 switch (item.data.type) {
110 case 'pool': fieldArr = ['type', 'pool', 'text']; break;
111 case 'node': fieldArr = ['type', 'node', 'text']; break;
112 case 'storage': fieldArr = ['type', 'pool', 'node', 'storage']; break;
113 default: fieldArr = ['name', 'type', 'node', 'pool', 'vmid'];
114 }
115 if (me.filterVal === '') {
116 item.data.relevance = 0;
117 return true;
118 }
119
120 // all text is case insensitive and each word is
121 // searched alone
122 // for every partial match, the row gets
123 // 1 match point, for every exact match
124 // it gets 2 points
125 //
126 // results gets sorted by points (descending)
127 fields = me.filterVal.split(/\s+/);
128 for(i = 0; i < fieldArr.length; i++) {
129 var v = item.data[fieldArr[i]];
130 if (v !== undefined) {
131 v = v.toString().toLowerCase();
132 for(j = 0; j < fields.length; j++) {
133 if (v.indexOf(fields[j]) !== -1) {
134 match++;
135 if(v === fields[j]) {
136 match++;
137 }
138 }
139 }
140 }
141 }
142 // give the row the 'relevance' value
143 item.data.relevance = match;
144 return (match > 0);
145 },
146
147 updateFilter: function(field, newValue, oldValue) {
148 var me = this;
149 // parse input and filter store,
150 // show grid
151 me.grid.store.filterVal = newValue.toLowerCase().trim();
152 me.grid.store.clearFilter(true);
153 me.grid.store.filterBy(me.customFilter);
154 me.grid.getSelectionModel().select(0);
155 },
156
157 selectAndHide: function(id) {
158 var me = this;
159 me.tree.selectById(id);
160 me.grid.hide();
161 me.setValue('');
162 me.blur();
163 },
164
165 onKey: function(field, e) {
166 var me = this;
167 var key = e.getKey();
168
169 switch(key) {
170 case Ext.event.Event.ENTER:
171 // go to first entry if there is one
172 if (me.grid.store.getCount() > 0) {
173 me.selectAndHide(me.grid.getSelection()[0].data.id);
174 }
175 break;
176 case Ext.event.Event.UP:
177 me.grid.getSelectionModel().selectPrevious();
178 break;
179 case Ext.event.Event.DOWN:
180 me.grid.getSelectionModel().selectNext();
181 break;
182 case Ext.event.Event.ESC:
183 me.grid.hide();
184 me.blur();
185 break;
186 }
187 },
188
189 loadValues: function(field) {
190 var me = this;
191 var records = [];
192
193 me.hasFocus = true;
194 me.grid.textfield = me;
195 me.grid.store.load();
196 me.grid.showBy(me, 'tl-bl');
197 },
198
199 hideGrid: function() {
200 var me = this;
201
202 me.hasFocus = false;
203 if (!me.grid.hasFocus) {
204 me.grid.hide();
205 }
206 },
207
208 listeners: {
209 change: {
210 fn: 'updateFilter',
211 buffer: 250
212 },
213 specialkey: 'onKey',
214 focusenter: 'loadValues',
215 focusleave: {
216 fn: 'hideGrid',
217 delay: 100
218 }
219 },
220
221 toggleFocus: function() {
222 var me = this;
223 if (!me.hasFocus) {
224 me.focus();
225 } else {
226 me.blur();
227 }
228 },
229
230 initComponent: function() {
231 var me = this;
232
233 if (!me.tree) {
234 throw "no tree given";
235 }
236
237 me.grid = Ext.create(me.grid);
238
239 me.callParent();
240
241 /*jslint confusion: true*/
242 /*because shift is also a function*/
243 // bind ctrl+shift+f and ctrl+space
244 // to open/close the search
245 me.keymap = new Ext.KeyMap({
246 target: Ext.get(document),
247 binding: [{
248 key:'F',
249 ctrl: true,
250 shift: true,
251 fn: me.toggleFocus,
252 scope: me
253 },{
254 key:' ',
255 ctrl: true,
256 fn: me.toggleFocus,
257 scope: me
258 }]
259 });
260
261 // always select first item and
262 // sort by relevance after load
263 me.mon(me.grid.store, 'load', function() {
264 me.grid.getSelectionModel().select(0);
265 me.grid.store.sort({
266 property: 'relevance',
267 direction: 'DESC'
268 });
269 });
270 }
271
272 });