proxmox-widget-toolkit/Toolkit.js
Dominik Csapak aea77b2c69 (partially) fix #1223: add touchscreen override for extjs
the combination of firefox, touchscreen, mouse input and extjs
prevents normal click/touch input for buttons, lists, etc.

the workaround on firefox was to set
dom.w3c_touch_events.enabled
to 0 (in about:config)

or to upgrade to extjs >= 6.5.1 (of which there is no gpl release as of now)

so we introduce that workaround as it seems to not disrupt 'normal'
browsers and non-touchscreen devices

we then still have an issue with scrolling though, since extjs
now expects the user to drag the content instead of using the wheel

but it is still better than a completely non working interface

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2018-03-30 10:10:46 +02:00

566 lines
18 KiB
JavaScript

// ExtJS related things
// do not send '_dc' parameter
Ext.Ajax.disableCaching = false;
// custom Vtypes
Ext.apply(Ext.form.field.VTypes, {
IPAddress: function(v) {
return Proxmox.Utils.IP4_match.test(v);
},
IPAddressText: gettext('Example') + ': 192.168.1.1',
IPAddressMask: /[\d\.]/i,
IPCIDRAddress: function(v) {
var result = Proxmox.Utils.IP4_cidr_match.exec(v);
// limits according to JSON Schema see
// pve-common/src/PVE/JSONSchema.pm
return (result !== null && result[1] >= 8 && result[1] <= 32);
},
IPCIDRAddressText: gettext('Example') + ': 192.168.1.1/24' + "<br>" + gettext('Valid CIDR Range') + ': 8-32',
IPCIDRAddressMask: /[\d\.\/]/i,
IP6Address: function(v) {
return Proxmox.Utils.IP6_match.test(v);
},
IP6AddressText: gettext('Example') + ': 2001:DB8::42',
IP6AddressMask: /[A-Fa-f0-9:]/,
IP6CIDRAddress: function(v) {
var result = Proxmox.Utils.IP6_cidr_match.exec(v);
// limits according to JSON Schema see
// pve-common/src/PVE/JSONSchema.pm
return (result !== null && result[1] >= 8 && result[1] <= 120);
},
IP6CIDRAddressText: gettext('Example') + ': 2001:DB8::42/64' + "<br>" + gettext('Valid CIDR Range') + ': 8-120',
IP6CIDRAddressMask: /[A-Fa-f0-9:\/]/,
IP6PrefixLength: function(v) {
return v >= 0 && v <= 128;
},
IP6PrefixLengthText: gettext('Example') + ': X, where 0 <= X <= 128',
IP6PrefixLengthMask: /[0-9]/,
IP64Address: function(v) {
return Proxmox.Utils.IP64_match.test(v);
},
IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42',
IP64AddressMask: /[A-Fa-f0-9\.:]/,
MacAddress: function(v) {
return (/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/).test(v);
},
MacAddressMask: /[a-fA-F0-9:]/,
MacAddressText: gettext('Example') + ': 01:23:45:67:89:ab',
MacPrefix: function(v) {
return (/^[a-f0-9]{2}(?::[a-f0-9]{2}){0,2}:?$/i).test(v);
},
MacPrefixMask: /[a-fA-F0-9:]/,
MacPrefixText: gettext('Example') + ': 02:8f',
BridgeName: function(v) {
return (/^vmbr\d{1,4}$/).test(v);
},
BridgeNameText: gettext('Format') + ': vmbr<b>N</b>, where 0 <= <b>N</b> <= 9999',
BondName: function(v) {
return (/^bond\d{1,4}$/).test(v);
},
BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 9999',
InterfaceName: function(v) {
return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
},
InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'" + "<br />" +
gettext("Minimum characters") + ": 2" + "<br />" +
gettext("Maximum characters") + ": 21" + "<br />" +
gettext("Must start with") + ": 'a-z'",
StorageId: function(v) {
return (/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i).test(v);
},
StorageIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '-', '_', '.'" + "<br />" +
gettext("Minimum characters") + ": 2" + "<br />" +
gettext("Must start with") + ": 'A-Z', 'a-z'<br />" +
gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'<br />",
ConfigId: function(v) {
return (/^[a-z][a-z0-9\_]+$/i).test(v);
},
ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'" + "<br />" +
gettext("Minimum characters") + ": 2" + "<br />" +
gettext("Must start with") + ": " + gettext("letter"),
HttpProxy: function(v) {
return (/^http:\/\/.*$/).test(v);
},
HttpProxyText: gettext('Example') + ": http://username:password&#64;host:port/",
DnsName: function(v) {
return Proxmox.Utils.DnsName_match.test(v);
},
DnsNameText: gettext('This is not a valid DNS name'),
// workaround for https://www.sencha.com/forum/showthread.php?302150
proxmoxMail: function(v) {
return (/^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,63}$/).test(v);
},
proxmoxMailText: gettext('Example') + ": user@example.com",
HostList: function(v) {
var list = v.split(/[\ \,\;]+/);
var i;
for (i = 0; i < list.length; i++) {
if (list[i] == "") {
continue;
}
if (!Proxmox.Utils.HostPort_match.test(list[i]) &&
!Proxmox.Utils.HostPortBrackets_match.test(list[i]) &&
!Proxmox.Utils.IP6_dotnotation_match.test(list[i])) {
return false;
}
}
return true;
},
HostListText: gettext('Not a valid list of hosts'),
password: function(val, field) {
if (field.initialPassField) {
var pwd = field.up('form').down(
'[name=' + field.initialPassField + ']');
return (val == pwd.getValue());
}
return true;
},
passwordText: gettext('Passwords do not match')
});
// Firefox 52+ Touchscreen bug
// see https://www.sencha.com/forum/showthread.php?336762-Examples-don-t-work-in-Firefox-52-touchscreen/page2
// and https://bugzilla.proxmox.com/show_bug.cgi?id=1223
Ext.define('EXTJS_23846.Element', {
override: 'Ext.dom.Element'
}, function(Element) {
var supports = Ext.supports,
proto = Element.prototype,
eventMap = proto.eventMap,
additiveEvents = proto.additiveEvents;
if (Ext.os.is.Desktop && supports.TouchEvents && !supports.PointerEvents) {
eventMap.touchstart = 'mousedown';
eventMap.touchmove = 'mousemove';
eventMap.touchend = 'mouseup';
eventMap.touchcancel = 'mouseup';
additiveEvents.mousedown = 'mousedown';
additiveEvents.mousemove = 'mousemove';
additiveEvents.mouseup = 'mouseup';
additiveEvents.touchstart = 'touchstart';
additiveEvents.touchmove = 'touchmove';
additiveEvents.touchend = 'touchend';
additiveEvents.touchcancel = 'touchcancel';
additiveEvents.pointerdown = 'mousedown';
additiveEvents.pointermove = 'mousemove';
additiveEvents.pointerup = 'mouseup';
additiveEvents.pointercancel = 'mouseup';
}
});
Ext.define('EXTJS_23846.Gesture', {
override: 'Ext.event.publisher.Gesture'
}, function(Gesture) {
var me = Gesture.instance;
if (Ext.supports.TouchEvents && !Ext.isWebKit && Ext.os.is.Desktop) {
me.handledDomEvents.push('mousedown', 'mousemove', 'mouseup');
me.registerEvents();
}
});
// we always want the number in x.y format and never in, e.g., x,y
Ext.define('PVE.form.field.Number', {
override: 'Ext.form.field.Number',
submitLocaleSeparator: false
});
// ExtJs 5-6 has an issue with caching
// see https://www.sencha.com/forum/showthread.php?308989
Ext.define('Proxmox.UnderlayPool', {
override: 'Ext.dom.UnderlayPool',
checkOut: function () {
var cache = this.cache,
len = cache.length,
el;
// do cleanup because some of the objects might have been destroyed
while (len--) {
if (cache[len].destroyed) {
cache.splice(len, 1);
}
}
// end do cleanup
el = cache.shift();
if (!el) {
el = Ext.Element.create(this.elementConfig);
el.setVisibilityMode(2);
//<debug>
// tell the spec runner to ignore this element when checking if the dom is clean
el.dom.setAttribute('data-sticky', true);
//</debug>
}
return el;
}
});
// 'Enter' in Textareas and aria multiline fields should not activate the
// defaultbutton, fixed in extjs 6.0.2
Ext.define('PVE.panel.Panel', {
override: 'Ext.panel.Panel',
fireDefaultButton: function(e) {
if (e.target.getAttribute('aria-multiline') === 'true' ||
e.target.tagName === "TEXTAREA") {
return true;
}
return this.callParent(arguments);
}
});
// if the order of the values are not the same in originalValue and value
// extjs will not overwrite value, but marks the field dirty and thus
// the reset button will be enabled (but clicking it changes nothing)
// so if the arrays are not the same after resetting, we
// clear and set it
Ext.define('Proxmox.form.ComboBox', {
override: 'Ext.form.field.ComboBox',
reset: function() {
// copied from combobox
var me = this;
me.callParent();
// clear and set when not the same
var value = me.getValue();
if (Ext.isArray(me.originalValue) && Ext.isArray(value) && !Ext.Array.equals(value, me.originalValue)) {
me.clearValue();
me.setValue(me.originalValue);
}
}
});
// when refreshing a grid/tree view, restoring the focus moves the view back to
// the previously focused item. Save scroll position before refocusing.
Ext.define(null, {
override: 'Ext.view.Table',
jumpToFocus: false,
saveFocusState: function() {
var me = this,
store = me.dataSource,
actionableMode = me.actionableMode,
navModel = me.getNavigationModel(),
focusPosition = actionableMode ? me.actionPosition : navModel.getPosition(true),
refocusRow, refocusCol;
if (focusPosition) {
// Separate this from the instance that the nav model is using.
focusPosition = focusPosition.clone();
// Exit actionable mode.
// We must inform any Actionables that they must relinquish control.
// Tabbability must be reset.
if (actionableMode) {
me.ownerGrid.setActionableMode(false);
}
// Blur the focused descendant, but do not trigger focusLeave.
me.el.dom.focus();
// Exiting actionable mode navigates to the owning cell, so in either focus mode we must
// clear the navigation position
navModel.setPosition();
// The following function will attempt to refocus back in the same mode to the same cell
// as it was at before based upon the previous record (if it's still inthe store), or the row index.
return function() {
// If we still have data, attempt to refocus in the same mode.
if (store.getCount()) {
// Adjust expectations of where we are able to refocus according to what kind of destruction
// might have been wrought on this view's DOM during focus save.
refocusRow = Math.min(focusPosition.rowIdx, me.all.getCount() - 1);
refocusCol = Math.min(focusPosition.colIdx, me.getVisibleColumnManager().getColumns().length - 1);
focusPosition = new Ext.grid.CellContext(me).setPosition(
store.contains(focusPosition.record) ? focusPosition.record : refocusRow, refocusCol);
if (actionableMode) {
me.ownerGrid.setActionableMode(true, focusPosition);
} else {
me.cellFocused = true;
// we sometimes want to scroll back to where we were
var x = me.getScrollX();
var y = me.getScrollY();
// Pass "preventNavigation" as true so that that does not cause selection.
navModel.setPosition(focusPosition, null, null, null, true);
if (!me.jumpToFocus) {
me.scrollTo(x,y);
}
}
}
// No rows - focus associated column header
else {
focusPosition.column.focus();
}
};
}
return Ext.emptyFn;
}
});
// should be fixed with ExtJS 6.0.2, see:
// https://www.sencha.com/forum/showthread.php?307244-Bug-with-datefield-in-window-with-scroll
Ext.define('Proxmox.Datepicker', {
override: 'Ext.picker.Date',
hideMode: 'visibility'
});
// ExtJS 6.0.1 has no setSubmitValue() (although you find it in the docs).
// Note: this.submitValue is a boolean flag, whereas getSubmitValue() returns
// data to be submitted.
Ext.define('Proxmox.form.field.Text', {
override: 'Ext.form.field.Text',
setSubmitValue: function(v) {
this.submitValue = v;
},
});
// this should be fixed with ExtJS 6.0.2
// make mousescrolling work in firefox in the containers overflowhandler
Ext.define(null, {
override: 'Ext.layout.container.boxOverflow.Scroller',
createWheelListener: function() {
var me = this;
if (Ext.isFirefox) {
me.wheelListener = me.layout.innerCt.on('wheel', me.onMouseWheelFirefox, me, {destroyable: true});
} else {
me.wheelListener = me.layout.innerCt.on('mousewheel', me.onMouseWheel, me, {destroyable: true});
}
},
// special wheel handler for firefox. differs from the default onMouseWheel
// handler by using deltaY instead of wheelDeltaY and no normalizing,
// because it is already
onMouseWheelFirefox: function(e) {
e.stopEvent();
var delta = e.browserEvent.deltaY || 0;
this.scrollBy(delta * this.wheelIncrement, false);
}
});
// force alert boxes to be rendered with an Error Icon
// since Ext.Msg is an object and not a prototype, we need to override it
// after the framework has been initiated
Ext.onReady(function() {
/*jslint confusion: true */
Ext.override(Ext.Msg, {
alert: function(title, message, fn, scope) {
if (Ext.isString(title)) {
var config = {
title: title,
message: message,
icon: this.ERROR,
buttons: this.OK,
fn: fn,
scope : scope,
minWidth: this.minWidth
};
return this.show(config);
}
}
});
/*jslint confusion: false */
});
Ext.define('Ext.ux.IFrame', {
extend: 'Ext.Component',
alias: 'widget.uxiframe',
loadMask: 'Loading...',
src: 'about:blank',
renderTpl: [
'<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>'
],
childEls: ['iframeEl'],
initComponent: function () {
this.callParent();
this.frameName = this.frameName || this.id + '-frame';
},
initEvents : function() {
var me = this;
me.callParent();
me.iframeEl.on('load', me.onLoad, me);
},
initRenderData: function() {
return Ext.apply(this.callParent(), {
src: this.src,
frameName: this.frameName
});
},
getBody: function() {
var doc = this.getDoc();
return doc.body || doc.documentElement;
},
getDoc: function() {
try {
return this.getWin().document;
} catch (ex) {
return null;
}
},
getWin: function() {
var me = this,
name = me.frameName,
win = Ext.isIE
? me.iframeEl.dom.contentWindow
: window.frames[name];
return win;
},
getFrame: function() {
var me = this;
return me.iframeEl.dom;
},
beforeDestroy: function () {
this.cleanupListeners(true);
this.callParent();
},
cleanupListeners: function(destroying){
var doc, prop;
if (this.rendered) {
try {
doc = this.getDoc();
if (doc) {
/*jslint nomen: true*/
Ext.get(doc).un(this._docListeners);
/*jslint nomen: false*/
if (destroying && doc.hasOwnProperty) {
for (prop in doc) {
if (doc.hasOwnProperty(prop)) {
delete doc[prop];
}
}
}
}
} catch(e) { }
}
},
onLoad: function() {
var me = this,
doc = me.getDoc(),
fn = me.onRelayedEvent;
if (doc) {
try {
// These events need to be relayed from the inner document (where they stop
// bubbling) up to the outer document. This has to be done at the DOM level so
// the event reaches listeners on elements like the document body. The effected
// mechanisms that depend on this bubbling behavior are listed to the right
// of the event.
/*jslint nomen: true*/
Ext.get(doc).on(
me._docListeners = {
mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront)
mousemove: fn, // window resize drag detection
mouseup: fn, // window resize termination
click: fn, // not sure, but just to be safe
dblclick: fn, // not sure again
scope: me
}
);
/*jslint nomen: false*/
} catch(e) {
// cannot do this xss
}
// We need to be sure we remove all our events from the iframe on unload or we're going to LEAK!
Ext.get(this.getWin()).on('beforeunload', me.cleanupListeners, me);
this.el.unmask();
this.fireEvent('load', this);
} else if (me.src) {
this.el.unmask();
this.fireEvent('error', this);
}
},
onRelayedEvent: function (event) {
// relay event from the iframe's document to the document that owns the iframe...
var iframeEl = this.iframeEl,
// Get the left-based iframe position
iframeXY = iframeEl.getTrueXY(),
originalEventXY = event.getXY(),
// Get the left-based XY position.
// This is because the consumer of the injected event will
// perform its own RTL normalization.
eventXY = event.getTrueXY();
// the event from the inner document has XY relative to that document's origin,
// so adjust it to use the origin of the iframe in the outer document:
event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]];
event.injectEvent(iframeEl); // blame the iframe for the event...
event.xy = originalEventXY; // restore the original XY (just for safety)
},
load: function (src) {
var me = this,
text = me.loadMask,
frame = me.getFrame();
if (me.fireEvent('beforeload', me, src) !== false) {
if (text && me.el) {
me.el.mask(text);
}
frame.src = me.src = (src || me.src);
}
}
});