]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - Toolkit.js
7e62e05a6e34e83e46a6638f740ff76f215b0295
[proxmox-widget-toolkit.git] / Toolkit.js
1 // ExtJS related things
2
3 // do not send '_dc' parameter
4 Ext.Ajax.disableCaching = false;
5
6 // custom Vtypes
7 Ext.apply(Ext.form.field.VTypes, {
8 IPAddress: function(v) {
9 return Proxmox.Utils.IP4_match.test(v);
10 },
11 IPAddressText: gettext('Example') + ': 192.168.1.1',
12 IPAddressMask: /[\d\.]/i,
13
14 IPCIDRAddress: function(v) {
15 var result = Proxmox.Utils.IP4_cidr_match.exec(v);
16 // limits according to JSON Schema see
17 // pve-common/src/PVE/JSONSchema.pm
18 return (result !== null && result[1] >= 8 && result[1] <= 32);
19 },
20 IPCIDRAddressText: gettext('Example') + ': 192.168.1.1/24' + "<br>" + gettext('Valid CIDR Range') + ': 8-32',
21 IPCIDRAddressMask: /[\d\.\/]/i,
22
23 IP6Address: function(v) {
24 return Proxmox.Utils.IP6_match.test(v);
25 },
26 IP6AddressText: gettext('Example') + ': 2001:DB8::42',
27 IP6AddressMask: /[A-Fa-f0-9:]/,
28
29 IP6CIDRAddress: function(v) {
30 var result = Proxmox.Utils.IP6_cidr_match.exec(v);
31 // limits according to JSON Schema see
32 // pve-common/src/PVE/JSONSchema.pm
33 return (result !== null && result[1] >= 8 && result[1] <= 120);
34 },
35 IP6CIDRAddressText: gettext('Example') + ': 2001:DB8::42/64' + "<br>" + gettext('Valid CIDR Range') + ': 8-120',
36 IP6CIDRAddressMask: /[A-Fa-f0-9:\/]/,
37
38 IP6PrefixLength: function(v) {
39 return v >= 0 && v <= 128;
40 },
41 IP6PrefixLengthText: gettext('Example') + ': X, where 0 <= X <= 128',
42 IP6PrefixLengthMask: /[0-9]/,
43
44 IP64Address: function(v) {
45 return Proxmox.Utils.IP64_match.test(v);
46 },
47 IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42',
48 IP64AddressMask: /[A-Fa-f0-9\.:]/,
49
50 MacAddress: function(v) {
51 return (/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/).test(v);
52 },
53 MacAddressMask: /[a-fA-F0-9:]/,
54 MacAddressText: gettext('Example') + ': 01:23:45:67:89:ab',
55
56 MacPrefix: function(v) {
57 return (/^[a-f0-9]{2}(?::[a-f0-9]{2}){0,2}:?$/i).test(v);
58 },
59 MacPrefixMask: /[a-fA-F0-9:]/,
60 MacPrefixText: gettext('Example') + ': 02:8f',
61
62 BridgeName: function(v) {
63 return (/^vmbr\d{1,4}$/).test(v);
64 },
65 BridgeNameText: gettext('Format') + ': vmbr<b>N</b>, where 0 <= <b>N</b> <= 9999',
66
67 BondName: function(v) {
68 return (/^bond\d{1,4}$/).test(v);
69 },
70 BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 9999',
71
72 InterfaceName: function(v) {
73 return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
74 },
75 InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'" + "<br />" +
76 gettext("Minimum characters") + ": 2" + "<br />" +
77 gettext("Maximum characters") + ": 21" + "<br />" +
78 gettext("Must start with") + ": 'a-z'",
79
80 StorageId: function(v) {
81 return (/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i).test(v);
82 },
83 StorageIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '-', '_', '.'" + "<br />" +
84 gettext("Minimum characters") + ": 2" + "<br />" +
85 gettext("Must start with") + ": 'A-Z', 'a-z'<br />" +
86 gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'<br />",
87
88 ConfigId: function(v) {
89 return (/^[a-z][a-z0-9\_]+$/i).test(v);
90 },
91 ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'" + "<br />" +
92 gettext("Minimum characters") + ": 2" + "<br />" +
93 gettext("Must start with") + ": " + gettext("letter"),
94
95 HttpProxy: function(v) {
96 return (/^http:\/\/.*$/).test(v);
97 },
98 HttpProxyText: gettext('Example') + ": http://username:password&#64;host:port/",
99
100 DnsName: function(v) {
101 return Proxmox.Utils.DnsName_match.test(v);
102 },
103 DnsNameText: gettext('This is not a valid DNS name'),
104
105 // workaround for https://www.sencha.com/forum/showthread.php?302150
106 proxmoxMail: function(v) {
107 return (/^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,63}$/).test(v);
108 },
109 proxmoxMailText: gettext('Example') + ": user@example.com",
110
111 HostList: function(v) {
112 var list = v.split(/[\ \,\;]+/);
113 var i;
114 for (i = 0; i < list.length; i++) {
115 if (list[i] == "") {
116 continue;
117 }
118
119 if (!Proxmox.Utils.HostPort_match.test(list[i]) &&
120 !Proxmox.Utils.HostPortBrackets_match.test(list[i]) &&
121 !Proxmox.Utils.IP6_dotnotation_match.test(list[i])) {
122 return false;
123 }
124 }
125
126 return true;
127 },
128 HostListText: gettext('Not a valid list of hosts'),
129
130 password: function(val, field) {
131 if (field.initialPassField) {
132 var pwd = field.up('form').down(
133 '[name=' + field.initialPassField + ']');
134 return (val == pwd.getValue());
135 }
136 return true;
137 },
138
139 passwordText: gettext('Passwords do not match')
140 });
141
142 // we always want the number in x.y format and never in, e.g., x,y
143 Ext.define('PVE.form.field.Number', {
144 override: 'Ext.form.field.Number',
145 submitLocaleSeparator: false
146 });
147
148 // ExtJs 5-6 has an issue with caching
149 // see https://www.sencha.com/forum/showthread.php?308989
150 Ext.define('Proxmox.UnderlayPool', {
151 override: 'Ext.dom.UnderlayPool',
152
153 checkOut: function () {
154 var cache = this.cache,
155 len = cache.length,
156 el;
157
158 // do cleanup because some of the objects might have been destroyed
159 while (len--) {
160 if (cache[len].destroyed) {
161 cache.splice(len, 1);
162 }
163 }
164 // end do cleanup
165
166 el = cache.shift();
167
168 if (!el) {
169 el = Ext.Element.create(this.elementConfig);
170 el.setVisibilityMode(2);
171 //<debug>
172 // tell the spec runner to ignore this element when checking if the dom is clean
173 el.dom.setAttribute('data-sticky', true);
174 //</debug>
175 }
176
177 return el;
178 }
179 });
180
181 // 'Enter' in Textareas and aria multiline fields should not activate the
182 // defaultbutton, fixed in extjs 6.0.2
183 Ext.define('PVE.panel.Panel', {
184 override: 'Ext.panel.Panel',
185
186 fireDefaultButton: function(e) {
187 if (e.target.getAttribute('aria-multiline') === 'true' ||
188 e.target.tagName === "TEXTAREA") {
189 return true;
190 }
191 return this.callParent(arguments);
192 }
193 });
194
195 // if the order of the values are not the same in originalValue and value
196 // extjs will not overwrite value, but marks the field dirty and thus
197 // the reset button will be enabled (but clicking it changes nothing)
198 // so if the arrays are not the same after resetting, we
199 // clear and set it
200 Ext.define('Proxmox.form.ComboBox', {
201 override: 'Ext.form.field.ComboBox',
202
203 reset: function() {
204 // copied from combobox
205 var me = this;
206 me.callParent();
207
208 // clear and set when not the same
209 var value = me.getValue();
210 if (Ext.isArray(me.originalValue) && Ext.isArray(value) && !Ext.Array.equals(value, me.originalValue)) {
211 me.clearValue();
212 me.setValue(me.originalValue);
213 }
214 }
215 });
216
217 // when refreshing a grid/tree view, restoring the focus moves the view back to
218 // the previously focused item. Save scroll position before refocusing.
219 Ext.define(null, {
220 override: 'Ext.view.Table',
221
222 jumpToFocus: false,
223
224 saveFocusState: function() {
225 var me = this,
226 store = me.dataSource,
227 actionableMode = me.actionableMode,
228 navModel = me.getNavigationModel(),
229 focusPosition = actionableMode ? me.actionPosition : navModel.getPosition(true),
230 refocusRow, refocusCol;
231
232 if (focusPosition) {
233 // Separate this from the instance that the nav model is using.
234 focusPosition = focusPosition.clone();
235
236 // Exit actionable mode.
237 // We must inform any Actionables that they must relinquish control.
238 // Tabbability must be reset.
239 if (actionableMode) {
240 me.ownerGrid.setActionableMode(false);
241 }
242
243 // Blur the focused descendant, but do not trigger focusLeave.
244 me.el.dom.focus();
245
246 // Exiting actionable mode navigates to the owning cell, so in either focus mode we must
247 // clear the navigation position
248 navModel.setPosition();
249
250 // The following function will attempt to refocus back in the same mode to the same cell
251 // as it was at before based upon the previous record (if it's still inthe store), or the row index.
252 return function() {
253 // If we still have data, attempt to refocus in the same mode.
254 if (store.getCount()) {
255
256 // Adjust expectations of where we are able to refocus according to what kind of destruction
257 // might have been wrought on this view's DOM during focus save.
258 refocusRow = Math.min(focusPosition.rowIdx, me.all.getCount() - 1);
259 refocusCol = Math.min(focusPosition.colIdx, me.getVisibleColumnManager().getColumns().length - 1);
260 focusPosition = new Ext.grid.CellContext(me).setPosition(
261 store.contains(focusPosition.record) ? focusPosition.record : refocusRow, refocusCol);
262
263 if (actionableMode) {
264 me.ownerGrid.setActionableMode(true, focusPosition);
265 } else {
266 me.cellFocused = true;
267
268 // we sometimes want to scroll back to where we were
269 var x = me.getScrollX();
270 var y = me.getScrollY();
271
272 // Pass "preventNavigation" as true so that that does not cause selection.
273 navModel.setPosition(focusPosition, null, null, null, true);
274
275 if (!me.jumpToFocus) {
276 me.scrollTo(x,y);
277 }
278 }
279 }
280 // No rows - focus associated column header
281 else {
282 focusPosition.column.focus();
283 }
284 };
285 }
286 return Ext.emptyFn;
287 }
288 });
289
290 // should be fixed with ExtJS 6.0.2, see:
291 // https://www.sencha.com/forum/showthread.php?307244-Bug-with-datefield-in-window-with-scroll
292 Ext.define('Proxmox.Datepicker', {
293 override: 'Ext.picker.Date',
294 hideMode: 'visibility'
295 });
296
297 // ExtJS 6.0.1 has no setSubmitValue() (although you find it in the docs).
298 // Note: this.submitValue is a boolean flag, whereas getSubmitValue() returns
299 // data to be submitted.
300 Ext.define('Proxmox.form.field.Text', {
301 override: 'Ext.form.field.Text',
302
303 setSubmitValue: function(v) {
304 this.submitValue = v;
305 },
306 });
307
308 // this should be fixed with ExtJS 6.0.2
309 // make mousescrolling work in firefox in the containers overflowhandler
310 Ext.define(null, {
311 override: 'Ext.layout.container.boxOverflow.Scroller',
312
313 createWheelListener: function() {
314 var me = this;
315 if (Ext.isFirefox) {
316 me.wheelListener = me.layout.innerCt.on('wheel', me.onMouseWheelFirefox, me, {destroyable: true});
317 } else {
318 me.wheelListener = me.layout.innerCt.on('mousewheel', me.onMouseWheel, me, {destroyable: true});
319 }
320 },
321
322 // special wheel handler for firefox. differs from the default onMouseWheel
323 // handler by using deltaY instead of wheelDeltaY and no normalizing,
324 // because it is already
325 onMouseWheelFirefox: function(e) {
326 e.stopEvent();
327 var delta = e.browserEvent.deltaY || 0;
328 this.scrollBy(delta * this.wheelIncrement, false);
329 }
330
331 });
332
333 // force alert boxes to be rendered with an Error Icon
334 // since Ext.Msg is an object and not a prototype, we need to override it
335 // after the framework has been initiated
336 Ext.onReady(function() {
337 /*jslint confusion: true */
338 Ext.override(Ext.Msg, {
339 alert: function(title, message, fn, scope) {
340 if (Ext.isString(title)) {
341 var config = {
342 title: title,
343 message: message,
344 icon: this.ERROR,
345 buttons: this.OK,
346 fn: fn,
347 scope : scope,
348 minWidth: this.minWidth
349 };
350 return this.show(config);
351 }
352 }
353 });
354 /*jslint confusion: false */
355 });
356 Ext.define('Ext.ux.IFrame', {
357 extend: 'Ext.Component',
358
359 alias: 'widget.uxiframe',
360
361 loadMask: 'Loading...',
362
363 src: 'about:blank',
364
365 renderTpl: [
366 '<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>'
367 ],
368 childEls: ['iframeEl'],
369
370 initComponent: function () {
371 this.callParent();
372
373 this.frameName = this.frameName || this.id + '-frame';
374 },
375
376 initEvents : function() {
377 var me = this;
378 me.callParent();
379 me.iframeEl.on('load', me.onLoad, me);
380 },
381
382 initRenderData: function() {
383 return Ext.apply(this.callParent(), {
384 src: this.src,
385 frameName: this.frameName
386 });
387 },
388
389 getBody: function() {
390 var doc = this.getDoc();
391 return doc.body || doc.documentElement;
392 },
393
394 getDoc: function() {
395 try {
396 return this.getWin().document;
397 } catch (ex) {
398 return null;
399 }
400 },
401
402 getWin: function() {
403 var me = this,
404 name = me.frameName,
405 win = Ext.isIE
406 ? me.iframeEl.dom.contentWindow
407 : window.frames[name];
408 return win;
409 },
410
411 getFrame: function() {
412 var me = this;
413 return me.iframeEl.dom;
414 },
415
416 beforeDestroy: function () {
417 this.cleanupListeners(true);
418 this.callParent();
419 },
420
421 cleanupListeners: function(destroying){
422 var doc, prop;
423
424 if (this.rendered) {
425 try {
426 doc = this.getDoc();
427 if (doc) {
428 /*jslint nomen: true*/
429 Ext.get(doc).un(this._docListeners);
430 /*jslint nomen: false*/
431 if (destroying && doc.hasOwnProperty) {
432 for (prop in doc) {
433 if (doc.hasOwnProperty(prop)) {
434 delete doc[prop];
435 }
436 }
437 }
438 }
439 } catch(e) { }
440 }
441 },
442
443 onLoad: function() {
444 var me = this,
445 doc = me.getDoc(),
446 fn = me.onRelayedEvent;
447
448 if (doc) {
449 try {
450 // These events need to be relayed from the inner document (where they stop
451 // bubbling) up to the outer document. This has to be done at the DOM level so
452 // the event reaches listeners on elements like the document body. The effected
453 // mechanisms that depend on this bubbling behavior are listed to the right
454 // of the event.
455 /*jslint nomen: true*/
456 Ext.get(doc).on(
457 me._docListeners = {
458 mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront)
459 mousemove: fn, // window resize drag detection
460 mouseup: fn, // window resize termination
461 click: fn, // not sure, but just to be safe
462 dblclick: fn, // not sure again
463 scope: me
464 }
465 );
466 /*jslint nomen: false*/
467 } catch(e) {
468 // cannot do this xss
469 }
470
471 // We need to be sure we remove all our events from the iframe on unload or we're going to LEAK!
472 Ext.get(this.getWin()).on('beforeunload', me.cleanupListeners, me);
473
474 this.el.unmask();
475 this.fireEvent('load', this);
476
477 } else if (me.src) {
478
479 this.el.unmask();
480 this.fireEvent('error', this);
481 }
482
483
484 },
485
486 onRelayedEvent: function (event) {
487 // relay event from the iframe's document to the document that owns the iframe...
488
489 var iframeEl = this.iframeEl,
490
491 // Get the left-based iframe position
492 iframeXY = iframeEl.getTrueXY(),
493 originalEventXY = event.getXY(),
494
495 // Get the left-based XY position.
496 // This is because the consumer of the injected event will
497 // perform its own RTL normalization.
498 eventXY = event.getTrueXY();
499
500 // the event from the inner document has XY relative to that document's origin,
501 // so adjust it to use the origin of the iframe in the outer document:
502 event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]];
503
504 event.injectEvent(iframeEl); // blame the iframe for the event...
505
506 event.xy = originalEventXY; // restore the original XY (just for safety)
507 },
508
509 load: function (src) {
510 var me = this,
511 text = me.loadMask,
512 frame = me.getFrame();
513
514 if (me.fireEvent('beforeload', me, src) !== false) {
515 if (text && me.el) {
516 me.el.mask(text);
517 }
518
519 frame.src = me.src = (src || me.src);
520 }
521 }
522 });