]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - Toolkit.js
bb9157f8ec0dd3f5b1e538517655f46ad33af1b0
[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 DnsOrIp: function(v) {
112 if (!Proxmox.Utils.DnsName_match.test(v) &&
113 !Proxmox.Utils.IP64_match.test(v))
114 {
115 return false;
116 }
117
118 return true;
119 },
120 DnsOrIpText: gettext('Not a valid DNS name or IP Address.'),
121
122 HostList: function(v) {
123 var list = v.split(/[\ \,\;]+/);
124 var i;
125 for (i = 0; i < list.length; i++) {
126 if (list[i] == "") {
127 continue;
128 }
129
130 if (!Proxmox.Utils.HostPort_match.test(list[i]) &&
131 !Proxmox.Utils.HostPortBrackets_match.test(list[i]) &&
132 !Proxmox.Utils.IP6_dotnotation_match.test(list[i])) {
133 return false;
134 }
135 }
136
137 return true;
138 },
139 HostListText: gettext('Not a valid list of hosts'),
140
141 password: function(val, field) {
142 if (field.initialPassField) {
143 var pwd = field.up('form').down(
144 '[name=' + field.initialPassField + ']');
145 return (val == pwd.getValue());
146 }
147 return true;
148 },
149
150 passwordText: gettext('Passwords do not match')
151 });
152
153 // Firefox 52+ Touchscreen bug
154 // see https://www.sencha.com/forum/showthread.php?336762-Examples-don-t-work-in-Firefox-52-touchscreen/page2
155 // and https://bugzilla.proxmox.com/show_bug.cgi?id=1223
156 Ext.define('EXTJS_23846.Element', {
157 override: 'Ext.dom.Element'
158 }, function(Element) {
159 var supports = Ext.supports,
160 proto = Element.prototype,
161 eventMap = proto.eventMap,
162 additiveEvents = proto.additiveEvents;
163
164 if (Ext.os.is.Desktop && supports.TouchEvents && !supports.PointerEvents) {
165 eventMap.touchstart = 'mousedown';
166 eventMap.touchmove = 'mousemove';
167 eventMap.touchend = 'mouseup';
168 eventMap.touchcancel = 'mouseup';
169
170 additiveEvents.mousedown = 'mousedown';
171 additiveEvents.mousemove = 'mousemove';
172 additiveEvents.mouseup = 'mouseup';
173 additiveEvents.touchstart = 'touchstart';
174 additiveEvents.touchmove = 'touchmove';
175 additiveEvents.touchend = 'touchend';
176 additiveEvents.touchcancel = 'touchcancel';
177
178 additiveEvents.pointerdown = 'mousedown';
179 additiveEvents.pointermove = 'mousemove';
180 additiveEvents.pointerup = 'mouseup';
181 additiveEvents.pointercancel = 'mouseup';
182 }
183 });
184
185 Ext.define('EXTJS_23846.Gesture', {
186 override: 'Ext.event.publisher.Gesture'
187 }, function(Gesture) {
188 var me = Gesture.instance;
189
190 if (Ext.supports.TouchEvents && !Ext.isWebKit && Ext.os.is.Desktop) {
191 me.handledDomEvents.push('mousedown', 'mousemove', 'mouseup');
192 me.registerEvents();
193 }
194 });
195
196 // we always want the number in x.y format and never in, e.g., x,y
197 Ext.define('PVE.form.field.Number', {
198 override: 'Ext.form.field.Number',
199 submitLocaleSeparator: false
200 });
201
202 // ExtJs 5-6 has an issue with caching
203 // see https://www.sencha.com/forum/showthread.php?308989
204 Ext.define('Proxmox.UnderlayPool', {
205 override: 'Ext.dom.UnderlayPool',
206
207 checkOut: function () {
208 var cache = this.cache,
209 len = cache.length,
210 el;
211
212 // do cleanup because some of the objects might have been destroyed
213 while (len--) {
214 if (cache[len].destroyed) {
215 cache.splice(len, 1);
216 }
217 }
218 // end do cleanup
219
220 el = cache.shift();
221
222 if (!el) {
223 el = Ext.Element.create(this.elementConfig);
224 el.setVisibilityMode(2);
225 //<debug>
226 // tell the spec runner to ignore this element when checking if the dom is clean
227 el.dom.setAttribute('data-sticky', true);
228 //</debug>
229 }
230
231 return el;
232 }
233 });
234
235 // 'Enter' in Textareas and aria multiline fields should not activate the
236 // defaultbutton, fixed in extjs 6.0.2
237 Ext.define('PVE.panel.Panel', {
238 override: 'Ext.panel.Panel',
239
240 fireDefaultButton: function(e) {
241 if (e.target.getAttribute('aria-multiline') === 'true' ||
242 e.target.tagName === "TEXTAREA") {
243 return true;
244 }
245 return this.callParent(arguments);
246 }
247 });
248
249 // if the order of the values are not the same in originalValue and value
250 // extjs will not overwrite value, but marks the field dirty and thus
251 // the reset button will be enabled (but clicking it changes nothing)
252 // so if the arrays are not the same after resetting, we
253 // clear and set it
254 Ext.define('Proxmox.form.ComboBox', {
255 override: 'Ext.form.field.ComboBox',
256
257 reset: function() {
258 // copied from combobox
259 var me = this;
260 me.callParent();
261
262 // clear and set when not the same
263 var value = me.getValue();
264 if (Ext.isArray(me.originalValue) && Ext.isArray(value) && !Ext.Array.equals(value, me.originalValue)) {
265 me.clearValue();
266 me.setValue(me.originalValue);
267 }
268 }
269 });
270
271 // when refreshing a grid/tree view, restoring the focus moves the view back to
272 // the previously focused item. Save scroll position before refocusing.
273 Ext.define(null, {
274 override: 'Ext.view.Table',
275
276 jumpToFocus: false,
277
278 saveFocusState: function() {
279 var me = this,
280 store = me.dataSource,
281 actionableMode = me.actionableMode,
282 navModel = me.getNavigationModel(),
283 focusPosition = actionableMode ? me.actionPosition : navModel.getPosition(true),
284 refocusRow, refocusCol;
285
286 if (focusPosition) {
287 // Separate this from the instance that the nav model is using.
288 focusPosition = focusPosition.clone();
289
290 // Exit actionable mode.
291 // We must inform any Actionables that they must relinquish control.
292 // Tabbability must be reset.
293 if (actionableMode) {
294 me.ownerGrid.setActionableMode(false);
295 }
296
297 // Blur the focused descendant, but do not trigger focusLeave.
298 me.el.dom.focus();
299
300 // Exiting actionable mode navigates to the owning cell, so in either focus mode we must
301 // clear the navigation position
302 navModel.setPosition();
303
304 // The following function will attempt to refocus back in the same mode to the same cell
305 // as it was at before based upon the previous record (if it's still inthe store), or the row index.
306 return function() {
307 // If we still have data, attempt to refocus in the same mode.
308 if (store.getCount()) {
309
310 // Adjust expectations of where we are able to refocus according to what kind of destruction
311 // might have been wrought on this view's DOM during focus save.
312 refocusRow = Math.min(focusPosition.rowIdx, me.all.getCount() - 1);
313 refocusCol = Math.min(focusPosition.colIdx, me.getVisibleColumnManager().getColumns().length - 1);
314 focusPosition = new Ext.grid.CellContext(me).setPosition(
315 store.contains(focusPosition.record) ? focusPosition.record : refocusRow, refocusCol);
316
317 if (actionableMode) {
318 me.ownerGrid.setActionableMode(true, focusPosition);
319 } else {
320 me.cellFocused = true;
321
322 // we sometimes want to scroll back to where we were
323 var x = me.getScrollX();
324 var y = me.getScrollY();
325
326 // Pass "preventNavigation" as true so that that does not cause selection.
327 navModel.setPosition(focusPosition, null, null, null, true);
328
329 if (!me.jumpToFocus) {
330 me.scrollTo(x,y);
331 }
332 }
333 }
334 // No rows - focus associated column header
335 else {
336 focusPosition.column.focus();
337 }
338 };
339 }
340 return Ext.emptyFn;
341 }
342 });
343
344 // should be fixed with ExtJS 6.0.2, see:
345 // https://www.sencha.com/forum/showthread.php?307244-Bug-with-datefield-in-window-with-scroll
346 Ext.define('Proxmox.Datepicker', {
347 override: 'Ext.picker.Date',
348 hideMode: 'visibility'
349 });
350
351 // ExtJS 6.0.1 has no setSubmitValue() (although you find it in the docs).
352 // Note: this.submitValue is a boolean flag, whereas getSubmitValue() returns
353 // data to be submitted.
354 Ext.define('Proxmox.form.field.Text', {
355 override: 'Ext.form.field.Text',
356
357 setSubmitValue: function(v) {
358 this.submitValue = v;
359 },
360 });
361
362 // this should be fixed with ExtJS 6.0.2
363 // make mousescrolling work in firefox in the containers overflowhandler
364 Ext.define(null, {
365 override: 'Ext.layout.container.boxOverflow.Scroller',
366
367 createWheelListener: function() {
368 var me = this;
369 if (Ext.isFirefox) {
370 me.wheelListener = me.layout.innerCt.on('wheel', me.onMouseWheelFirefox, me, {destroyable: true});
371 } else {
372 me.wheelListener = me.layout.innerCt.on('mousewheel', me.onMouseWheel, me, {destroyable: true});
373 }
374 },
375
376 // special wheel handler for firefox. differs from the default onMouseWheel
377 // handler by using deltaY instead of wheelDeltaY and no normalizing,
378 // because it is already
379 onMouseWheelFirefox: function(e) {
380 e.stopEvent();
381 var delta = e.browserEvent.deltaY || 0;
382 this.scrollBy(delta * this.wheelIncrement, false);
383 }
384
385 });
386
387 // force alert boxes to be rendered with an Error Icon
388 // since Ext.Msg is an object and not a prototype, we need to override it
389 // after the framework has been initiated
390 Ext.onReady(function() {
391 /*jslint confusion: true */
392 Ext.override(Ext.Msg, {
393 alert: function(title, message, fn, scope) {
394 if (Ext.isString(title)) {
395 var config = {
396 title: title,
397 message: message,
398 icon: this.ERROR,
399 buttons: this.OK,
400 fn: fn,
401 scope : scope,
402 minWidth: this.minWidth
403 };
404 return this.show(config);
405 }
406 }
407 });
408 /*jslint confusion: false */
409 });
410 Ext.define('Ext.ux.IFrame', {
411 extend: 'Ext.Component',
412
413 alias: 'widget.uxiframe',
414
415 loadMask: 'Loading...',
416
417 src: 'about:blank',
418
419 renderTpl: [
420 '<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>'
421 ],
422 childEls: ['iframeEl'],
423
424 initComponent: function () {
425 this.callParent();
426
427 this.frameName = this.frameName || this.id + '-frame';
428 },
429
430 initEvents : function() {
431 var me = this;
432 me.callParent();
433 me.iframeEl.on('load', me.onLoad, me);
434 },
435
436 initRenderData: function() {
437 return Ext.apply(this.callParent(), {
438 src: this.src,
439 frameName: this.frameName
440 });
441 },
442
443 getBody: function() {
444 var doc = this.getDoc();
445 return doc.body || doc.documentElement;
446 },
447
448 getDoc: function() {
449 try {
450 return this.getWin().document;
451 } catch (ex) {
452 return null;
453 }
454 },
455
456 getWin: function() {
457 var me = this,
458 name = me.frameName,
459 win = Ext.isIE
460 ? me.iframeEl.dom.contentWindow
461 : window.frames[name];
462 return win;
463 },
464
465 getFrame: function() {
466 var me = this;
467 return me.iframeEl.dom;
468 },
469
470 beforeDestroy: function () {
471 this.cleanupListeners(true);
472 this.callParent();
473 },
474
475 cleanupListeners: function(destroying){
476 var doc, prop;
477
478 if (this.rendered) {
479 try {
480 doc = this.getDoc();
481 if (doc) {
482 /*jslint nomen: true*/
483 Ext.get(doc).un(this._docListeners);
484 /*jslint nomen: false*/
485 if (destroying && doc.hasOwnProperty) {
486 for (prop in doc) {
487 if (doc.hasOwnProperty(prop)) {
488 delete doc[prop];
489 }
490 }
491 }
492 }
493 } catch(e) { }
494 }
495 },
496
497 onLoad: function() {
498 var me = this,
499 doc = me.getDoc(),
500 fn = me.onRelayedEvent;
501
502 if (doc) {
503 try {
504 // These events need to be relayed from the inner document (where they stop
505 // bubbling) up to the outer document. This has to be done at the DOM level so
506 // the event reaches listeners on elements like the document body. The effected
507 // mechanisms that depend on this bubbling behavior are listed to the right
508 // of the event.
509 /*jslint nomen: true*/
510 Ext.get(doc).on(
511 me._docListeners = {
512 mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront)
513 mousemove: fn, // window resize drag detection
514 mouseup: fn, // window resize termination
515 click: fn, // not sure, but just to be safe
516 dblclick: fn, // not sure again
517 scope: me
518 }
519 );
520 /*jslint nomen: false*/
521 } catch(e) {
522 // cannot do this xss
523 }
524
525 // We need to be sure we remove all our events from the iframe on unload or we're going to LEAK!
526 Ext.get(this.getWin()).on('beforeunload', me.cleanupListeners, me);
527
528 this.el.unmask();
529 this.fireEvent('load', this);
530
531 } else if (me.src) {
532
533 this.el.unmask();
534 this.fireEvent('error', this);
535 }
536
537
538 },
539
540 onRelayedEvent: function (event) {
541 // relay event from the iframe's document to the document that owns the iframe...
542
543 var iframeEl = this.iframeEl,
544
545 // Get the left-based iframe position
546 iframeXY = iframeEl.getTrueXY(),
547 originalEventXY = event.getXY(),
548
549 // Get the left-based XY position.
550 // This is because the consumer of the injected event will
551 // perform its own RTL normalization.
552 eventXY = event.getTrueXY();
553
554 // the event from the inner document has XY relative to that document's origin,
555 // so adjust it to use the origin of the iframe in the outer document:
556 event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]];
557
558 event.injectEvent(iframeEl); // blame the iframe for the event...
559
560 event.xy = originalEventXY; // restore the original XY (just for safety)
561 },
562
563 load: function (src) {
564 var me = this,
565 text = me.loadMask,
566 frame = me.getFrame();
567
568 if (me.fireEvent('beforeload', me, src) !== false) {
569 if (text && me.el) {
570 me.el.mask(text);
571 }
572
573 frame.src = me.src = (src || me.src);
574 }
575 }
576 });