]> git.proxmox.com Git - proxmox-widget-toolkit.git/blobdiff - src/Toolkit.js
safe destroy: allow specifing additional items
[proxmox-widget-toolkit.git] / src / Toolkit.js
index d528ea79aad8e226aad93cc5943f6eece8a4bd90..6ae31ed45b96355ab2e8e5fa405259fd1171dedd 100644 (file)
@@ -147,6 +147,13 @@ Ext.apply(Ext.form.field.VTypes, {
     },
     DnsOrIpText: gettext('Not a valid DNS name or IP address.'),
 
+    HostPort: function(v) {
+       return Proxmox.Utils.HostPort_match.test(v) ||
+               Proxmox.Utils.HostPortBrackets_match.test(v) ||
+               Proxmox.Utils.IP6_dotnotation_match.test(v);
+    },
+    HostPortText: gettext('Host/IP address or optional port is invalid'),
+
     HostList: function(v) {
        let list = v.split(/[ ,;]+/);
        let i;
@@ -175,6 +182,11 @@ Ext.apply(Ext.form.field.VTypes, {
     },
 
     passwordText: gettext('Passwords do not match'),
+
+    email: function(value) {
+       let emailre = /^[\w+~-]+(\.[\w+~-]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)$/;
+       return emailre.test(value);
+    },
 });
 
 // Firefox 52+ Touchscreen bug
@@ -485,11 +497,172 @@ Ext.define('Proxmox.validIdReOverride', {
     validIdRe: /^[a-z_][a-z0-9\-_@]*$/i,
 });
 
+Ext.define('Proxmox.selection.CheckboxModel', {
+    override: 'Ext.selection.CheckboxModel',
+
+    // [P] use whole checkbox cell to multiselect, not only the checkbox
+    checkSelector: '.x-grid-cell-row-checker',
+
+    // TODO: remove all optimizations below to an override for parent 'Ext.selection.Model' ??
+
+    // [ P: optimized to remove all records at once as single remove is O(n^3) slow ]
+    // records can be an index, a record or an array of records
+    doDeselect: function(records, suppressEvent) {
+        var me = this,
+            selected = me.selected,
+            i = 0,
+            len, record,
+            commit;
+        if (me.locked || !me.store) {
+            return false;
+        }
+        if (typeof records === "number") {
+            // No matching record, jump out
+            record = me.store.getAt(records);
+            if (!record) {
+                return false;
+            }
+            records = [
+                record,
+            ];
+        } else if (!Ext.isArray(records)) {
+            records = [
+                records,
+            ];
+        }
+       // [P] a beforedeselection, triggered by me.onSelectChange below, can block removal by
+       // returning false, thus the original implementation removed only here in the commit fn,
+       // which has an abysmal performance O(n^3). As blocking removal is not the norm, go do the
+       // reverse, record blocked records and remove them from the to-be-removed array before
+       // applying it. A FF86 i9-9900K on 10k records goes from >40s to ~33ms for >90% deselection
+       let committed = false;
+       commit = function() {
+           committed = true;
+           if (record === me.selectionStart) {
+               me.selectionStart = null;
+           }
+       };
+       let removalBlocked = [];
+        len = records.length;
+        me.suspendChanges();
+        for (; i < len; i++) {
+            record = records[i];
+            if (me.isSelected(record)) {
+               committed = false;
+                me.onSelectChange(record, false, suppressEvent, commit);
+               if (!committed) {
+                   removalBlocked.push(record);
+               }
+                if (me.destroyed) {
+                    return false;
+                }
+            }
+        }
+       if (removalBlocked.length > 0) {
+           records.remove(removalBlocked);
+       }
+       selected.remove(records); // [P] FAST(er)
+       me.lastSelected = selected.last();
+        me.resumeChanges();
+        // fire selchange if there was a change and there is no suppressEvent flag
+       me.maybeFireSelectionChange(records.length > 0 && !suppressEvent);
+       return records.length;
+    },
+
+
+    doMultiSelect: function(records, keepExisting, suppressEvent) {
+        var me = this,
+            selected = me.selected,
+            change = false,
+            result, i, len, record, commit;
+
+        if (me.locked) {
+            return;
+        }
+
+        records = !Ext.isArray(records) ? [records] : records;
+        len = records.length;
+        if (!keepExisting && selected.getCount() > 0) {
+            result = me.deselectDuringSelect(records, suppressEvent);
+            if (me.destroyed) {
+                return;
+            }
+            if (result[0]) {
+                // We had a failure during selection, so jump out
+                // Fire selection change if we did deselect anything
+                me.maybeFireSelectionChange(result[1] > 0 && !suppressEvent);
+                return;
+            } else {
+                // Means something has been deselected, so we've had a change
+                change = result[1] > 0;
+            }
+        }
+
+       let gotBlocked, blockedRecords = [];
+        commit = function() {
+            if (!selected.getCount()) {
+                me.selectionStart = record;
+            }
+           gotBlocked = false;
+            change = true;
+        };
+
+        for (i = 0; i < len; i++) {
+            record = records[i];
+            if (me.isSelected(record)) {
+                continue;
+            }
+
+           gotBlocked = true;
+            me.onSelectChange(record, true, suppressEvent, commit);
+            if (me.destroyed) {
+                return;
+            }
+           if (gotBlocked) {
+               blockedRecords.push(record);
+           }
+        }
+       if (blockedRecords.length > 0) {
+           records.remove(blockedRecords);
+       }
+        selected.add(records);
+        me.lastSelected = record;
+
+        // fire selchange if there was a change and there is no suppressEvent flag
+        me.maybeFireSelectionChange(change && !suppressEvent);
+    },
+    deselectDuringSelect: function(toSelect, suppressEvent) {
+        var me = this,
+            selected = me.selected.getRange(),
+            changed = 0,
+            failed = false;
+        // Prevent selection change events from firing, will happen during select
+        me.suspendChanges();
+        me.deselectingDuringSelect = true;
+       let toDeselect = selected.filter(item => !Ext.Array.contains(toSelect, item));
+       if (toDeselect.length > 0) {
+           changed = me.doDeselect(toDeselect, suppressEvent);
+           if (!changed) {
+               failed = true;
+            }
+            if (me.destroyed) {
+                failed = true;
+                changed = 0;
+            }
+        }
+        me.deselectingDuringSelect = false;
+        me.resumeChanges();
+        return [
+            failed,
+            changed,
+        ];
+    },
+});
+
 // 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) { // eslint-disable-line consistent-return
            if (Ext.isString(title)) {
@@ -506,7 +679,6 @@ Ext.onReady(function() {
            }
        },
     });
-/*jslint confusion: false */
 });
 Ext.define('Ext.ux.IFrame', {
     extend: 'Ext.Component',
@@ -607,7 +779,6 @@ Ext.define('Ext.ux.IFrame', {
                 // 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)
@@ -618,7 +789,6 @@ Ext.define('Ext.ux.IFrame', {
                         scope: me,
                     },
                 );
-               /*jslint nomen: false*/
             } catch (e) {
                 // cannot do this xss
             }