From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id CDA0091745 for ; Mon, 14 Nov 2022 10:44:53 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 578092129D for ; Mon, 14 Nov 2022 10:44:23 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Mon, 14 Nov 2022 10:44:15 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 4D04D43D88 for ; Mon, 14 Nov 2022 10:44:09 +0100 (CET) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Mon, 14 Nov 2022 10:44:01 +0100 Message-Id: <20221114094404.1241050-18-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20221114094404.1241050-1-d.csapak@proxmox.com> References: <20221114094404.1241050-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: =?UTF-8?Q?0=0A=09?=AWL 0.065 Adjusted score from AWL reputation of From: =?UTF-8?Q?address=0A=09?=BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict =?UTF-8?Q?Alignment=0A=09?=SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF =?UTF-8?Q?Record=0A=09?=SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH manager v9 09/12] ui: add form/TagEdit.js X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 14 Nov 2022 09:44:53 -0000 this is a wrapper container for holding a list of (editable) tags intended to be used in the lxc/qemu status toolbar to add a new tag, we reuse the 'pmxTag' class, but overwrite some of its behaviour and css classes so that it properly adds tags also handles the drag/drop feature for the tags in the list when done with editing (by clicking the checkmark), sends a 'change' event with the new tags Signed-off-by: Dominik Csapak --- changes from v8: * added missing css classed www/css/ext6-pve.css | 23 ++- www/manager6/Makefile | 1 + www/manager6/form/TagEdit.js | 321 +++++++++++++++++++++++++++++++++++ 3 files changed, 341 insertions(+), 4 deletions(-) create mode 100644 www/manager6/form/TagEdit.js diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css index daaffa6ec..4fc83a878 100644 --- a/www/css/ext6-pve.css +++ b/www/css/ext6-pve.css @@ -657,7 +657,8 @@ table.osds td:first-of-type { padding-bottom: 0px; } -.pve-edit-tag > i { +.pve-edit-tag > i, +.pve-add-tag > i { cursor: pointer; font-size: 14px; } @@ -667,7 +668,8 @@ table.osds td:first-of-type { cursor: grab; } -.pve-edit-tag > i.action { +.pve-edit-tag > i.action, +.pve-add-tag > i.action { padding-left: 5px; } @@ -676,7 +678,9 @@ table.osds td:first-of-type { } .pve-edit-tag.editable span, -.pve-edit-tag.inEdit span { +.pve-edit-tag.inEdit span, +.pve-add-tag.editable span, +.pve-add-tag.inEdit span { background-color: #ffffff; border: 1px solid #a8a8a8; color: #000; @@ -685,6 +689,17 @@ table.osds td:first-of-type { min-width: 2em; } -.pve-edit-tag.inEdit span { +.pve-edit-tag.inEdit span, +.pve-add-tag.inEdit span { border: 1px solid #000; } + +.pve-add-tag { + background-color: #d5d5d5 ! important; + color: #000000 ! important; +} + +.pve-tag-inline-button { + cursor: pointer; + padding-left: 2px; +} diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 7bcc35e8e..396abffcc 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -76,6 +76,7 @@ JSSRC= \ form/TagColorGrid.js \ form/ListField.js \ form/Tag.js \ + form/TagEdit.js \ grid/BackupView.js \ grid/FirewallAliases.js \ grid/FirewallOptions.js \ diff --git a/www/manager6/form/TagEdit.js b/www/manager6/form/TagEdit.js new file mode 100644 index 000000000..ac184a917 --- /dev/null +++ b/www/manager6/form/TagEdit.js @@ -0,0 +1,321 @@ +Ext.define('PVE.panel.TagEditContainer', { + extend: 'Ext.container.Container', + alias: 'widget.pveTagEditContainer', + + layout: { + type: 'hbox', + align: 'stretch', + }, + + controller: { + xclass: 'Ext.app.ViewController', + + loadTags: function(tagstring = '', force = false) { + let me = this; + let view = me.getView(); + + if (me.oldTags === tagstring && !force) { + return; + } + + view.suspendLayout = true; + me.forEachTag((tag) => { + view.remove(tag); + }); + me.getViewModel().set('tagCount', 0); + let newtags = tagstring.split(/[;, ]/).filter((t) => !!t) || []; + newtags.forEach((tag) => { + me.addTag(tag); + }); + view.suspendLayout = false; + view.updateLayout(); + if (!force) { + me.oldTags = tagstring; + } + }, + + onRender: function(v) { + let me = this; + let view = me.getView(); + view.dragzone = Ext.create('Ext.dd.DragZone', v.getEl(), { + getDragData: function(e) { + let source = e.getTarget('.handle'); + if (!source) { + return undefined; + } + let sourceId = source.parentNode.id; + let cmp = Ext.getCmp(sourceId); + let ddel = document.createElement('div'); + ddel.classList.add('proxmox-tags-full'); + ddel.innerHTML = Proxmox.Utils.getTagElement(cmp.tag, PVE.Utils.tagOverrides); + let repairXY = Ext.fly(source).getXY(); + cmp.setDisabled(true); + ddel.id = Ext.id(); + return { + ddel, + repairXY, + sourceId, + }; + }, + onMouseUp: function(target, e, id) { + let cmp = Ext.getCmp(this.dragData.sourceId); + if (cmp && !cmp.isDestroyed) { + cmp.setDisabled(false); + } + }, + getRepairXY: function() { + return this.dragData.repairXY; + }, + beforeInvalidDrop: function(target, e, id) { + let cmp = Ext.getCmp(this.dragData.sourceId); + if (cmp && !cmp.isDestroyed) { + cmp.setDisabled(false); + } + }, + }); + view.dropzone = Ext.create('Ext.dd.DropZone', v.getEl(), { + getTargetFromEvent: function(e) { + return e.getTarget('.proxmox-tag-dark,.proxmox-tag-light'); + }, + getIndicator: function() { + if (!view.indicator) { + view.indicator = Ext.create('Ext.Component', { + floating: true, + html: '', + hidden: true, + shadow: false, + }); + } + return view.indicator; + }, + onContainerOver: function() { + this.getIndicator().setVisible(false); + }, + notifyOut: function() { + this.getIndicator().setVisible(false); + }, + onNodeOver: function(target, dd, e, data) { + let indicator = this.getIndicator(); + indicator.setVisible(true); + indicator.alignTo(Ext.getCmp(target.id), 't50-bl', [-1, -2]); + return this.dropAllowed; + }, + onNodeDrop: function(target, dd, e, data) { + this.getIndicator().setVisible(false); + let sourceCmp = Ext.getCmp(data.sourceId); + if (!sourceCmp) { + return; + } + sourceCmp.setDisabled(false); + let targetCmp = Ext.getCmp(target.id); + view.remove(sourceCmp, { destroy: false }); + view.insert(view.items.indexOf(targetCmp), sourceCmp); + }, + }); + }, + + forEachTag: function(func) { + let me = this; + let view = me.getView(); + view.items.each((field) => { + if (field.reference === 'addTagBtn') { + return false; + } + if (field.getXType() === 'pmxTag') { + func(field); + } + return true; + }); + }, + + toggleEdit: function(cancel) { + let me = this; + let vm = me.getViewModel(); + let editMode = !vm.get('editMode'); + vm.set('editMode', editMode); + + // get a current tag list for editing + if (editMode) { + PVE.Utils.updateUIOptions(); + } + + me.forEachTag((tag) => { + tag.setMode(editMode ? 'editable' : 'normal'); + }); + + if (!vm.get('editMode')) { + let tags = []; + if (cancel) { + me.loadTags(me.oldTags, true); + } else { + me.forEachTag((cmp) => { + if (cmp.isVisible() && cmp.tag) { + tags.push(cmp.tag); + } + }); + tags = tags.join(','); + if (me.oldTags !== tags) { + me.oldTags = tags; + me.getView().fireEvent('change', tags); + } + } + } + me.getView().updateLayout(); + }, + + addTag: function(tag) { + let me = this; + let view = me.getView(); + let vm = me.getViewModel(); + let index = view.items.indexOf(me.lookup('addTagBtn')); + view.insert(index, { + xtype: 'pmxTag', + tag, + mode: vm.get('editMode') ? 'editable' : 'normal', + listeners: { + change: (field, newTag) => { + if (newTag === '') { + view.remove(field); + vm.set('tagCount', vm.get('tagCount') - 1); + } + }, + }, + }); + + vm.set('tagCount', vm.get('tagCount') + 1); + }, + + addTagClick: function(event) { + let me = this; + if (event.target.tagName === 'SPAN') { + me.lookup('addTagBtn').tagEl().innerHTML = ''; + me.lookup('addTagBtn').updateLayout(); + } + }, + + addTagMouseDown: function(event) { + let me = this; + if (event.target.tagName === 'I') { + let tag = me.lookup('addTagBtn').tagEl().innerHTML; + if (tag !== '') { + me.addTag(tag, true); + } + } + }, + + addTagChange: function(field, tag) { + let me = this; + if (tag !== '') { + me.addTag(tag, true); + } + field.tag = ''; + }, + + cancelClick: function() { + this.toggleEdit(true); + }, + + editClick: function() { + this.toggleEdit(false); + }, + + init: function(view) { + let me = this; + if (view.tags) { + me.loadTags(view.tags); + } + }, + }, + + viewModel: { + data: { + tagCount: 0, + editMode: false, + }, + + formulas: { + hideNoTags: function(get) { + return get('editMode') || get('tagCount') !== 0; + }, + editBtnHtml: function(get) { + let cls = get('editMode') ? 'check' : 'pencil'; + let qtip = get('editMode') ? gettext('Apply Changes') : gettext('Edit Tags'); + return ``; + }, + }, + }, + + loadTags: function() { + return this.getController().loadTags(...arguments); + }, + + items: [ + { + xtype: 'box', + bind: { + hidden: '{hideNoTags}', + }, + html: gettext('No Tags'), + }, + { + xtype: 'pmxTag', + reference: 'addTagBtn', + cls: 'pve-add-tag', + mode: 'editable', + tag: '', + tpl: `${gettext('Add Tag')}`, + bind: { + hidden: '{!editMode}', + }, + hidden: true, + onMouseDown: Ext.emptyFn, // prevent default behaviour + listeners: { + click: { + element: 'el', + fn: 'addTagClick', + }, + mousedown: { + element: 'el', + fn: 'addTagMouseDown', + }, + change: 'addTagChange', + }, + }, + { + xtype: 'box', + html: ``, + cls: 'pve-tag-inline-button', + hidden: true, + bind: { + hidden: '{!editMode}', + }, + listeners: { + click: 'cancelClick', + element: 'el', + }, + }, + { + xtype: 'box', + cls: 'pve-tag-inline-button', + bind: { + html: '{editBtnHtml}', + }, + listeners: { + click: 'editClick', + element: 'el', + }, + }, + ], + + listeners: { + render: 'onRender', + }, + + destroy: function() { + let me = this; + Ext.destroy(me.dragzone); + Ext.destroy(me.dropzone); + Ext.destroy(me.indicator); + me.callParent(); + }, +}); -- 2.30.2