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