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 97D9A89E29 for ; Tue, 18 Oct 2022 16:02:59 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 40B2129736 for ; Tue, 18 Oct 2022 16:02:39 +0200 (CEST) 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 ; Tue, 18 Oct 2022 16:02:32 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 75DF144A98 for ; Tue, 18 Oct 2022 16:02:30 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Tue, 18 Oct 2022 16:02:22 +0200 Message-Id: <20221018140226.598710-17-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20221018140226.598710-1-d.csapak@proxmox.com> References: <20221018140226.598710-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.067 Adjusted score from AWL reputation of From: address 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 Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [event.data] Subject: [pve-devel] [PATCH manager v8 08/12] ui: add form/Tag 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: Tue, 18 Oct 2022 14:02:59 -0000 displays a single tag, with the ability to edit inline on click (when the mode is set to editable). This brings up a list of globally available tags for simple selection. Signed-off-by: Dominik Csapak --- www/manager6/Makefile | 1 + www/manager6/form/Tag.js | 233 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 www/manager6/form/Tag.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 60ae421e..9d610f71 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -74,6 +74,7 @@ JSSRC= \ form/ViewSelector.js \ form/iScsiProviderSelector.js \ form/TagColorGrid.js \ + form/Tag.js \ grid/BackupView.js \ grid/FirewallAliases.js \ grid/FirewallOptions.js \ diff --git a/www/manager6/form/Tag.js b/www/manager6/form/Tag.js new file mode 100644 index 00000000..aa6ae867 --- /dev/null +++ b/www/manager6/form/Tag.js @@ -0,0 +1,233 @@ +Ext.define('Proxmox.Tag', { + extend: 'Ext.Component', + alias: 'widget.pmxTag', + + mode: 'editable', + + icons: { + editable: 'fa fa-minus-square', + normal: '', + inEdit: 'fa fa-check-square', + }, + + tag: '', + cls: 'pve-edit-tag', + + tpl: [ + '', + '{tag}', + '', + ], + + // we need to do this in mousedown, because that triggers before + // focusleave (which triggers before click) + onMouseDown: function(event) { + let me = this; + if (event.target.tagName !== 'I' || event.target.classList.contains('handle')) { + return; + } + switch (me.mode) { + case 'editable': + me.setVisible(false); + me.setTag(''); + break; + case 'inEdit': + me.setTag(me.tagEl().innerHTML); + me.setMode('editable'); + break; + default: break; + } + }, + + onClick: function(event) { + let me = this; + if (event.target.tagName !== 'SPAN' || me.mode !== 'editable') { + return; + } + me.setMode('inEdit'); + + // select text in the element + let tagEl = me.tagEl(); + tagEl.contentEditable = true; + let range = document.createRange(); + range.selectNodeContents(tagEl); + let sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + + me.showPicker(); + }, + + showPicker: function() { + let me = this; + if (!me.picker) { + me.picker = Ext.widget({ + xtype: 'boundlist', + minWidth: 70, + scrollable: true, + floating: true, + hidden: true, + userCls: 'proxmox-tags-full', + displayField: 'tag', + itemTpl: [ + '{[Proxmox.Utils.getTagElement(values.tag, PVE.Utils.tagOverrides)]}', + ], + store: [], + listeners: { + select: function(picker, rec) { + me.setTag(rec.data.tag); + me.setMode('editable'); + me.picker.hide(); + }, + }, + }); + } + me.picker.getStore()?.clearFilter(); + let taglist = PVE.Utils.tagList.map(v => ({ tag: v })); + if (taglist.length < 1) { + return; + } + me.picker.getStore().setData(taglist); + me.picker.showBy(me, 'tl-bl'); + me.picker.setMaxHeight(200); + }, + + setMode: function(mode) { + let me = this; + if (me.icons[mode] === undefined) { + throw "invalid mode"; + } + let tagEl = me.tagEl(); + if (tagEl) { + tagEl.contentEditable = mode === 'inEdit'; + } + me.removeCls(me.mode); + me.addCls(mode); + me.mode = mode; + me.updateData(); + }, + + onKeyPress: function(event) { + let me = this; + let key = event.browserEvent.key; + switch (key) { + case 'Enter': + if (me.tagEl().innerHTML !== '') { + me.setTag(me.tagEl().innerHTML); + me.setMode('editable'); + return; + } + break; + case 'Escape': + me.cancelEdit(); + return; + case 'Backspace': + case 'Delete': + return; + default: + if (key.match(PVE.Utils.tagCharRegex)) { + return; + } + } + event.browserEvent.preventDefault(); + event.browserEvent.stopPropagation(); + }, + + beforeInput: function(event) { + let me = this; + me.updateLayout(); + let tag = event.event.data ?? event.event.dataTransfer?.getData('text/plain'); + if (!tag) { + return; + } + if (tag.match(PVE.Utils.tagCharRegex) === null) { + event.event.preventDefault(); + event.event.stopPropagation(); + } + }, + + onInput: function(event) { + let me = this; + me.picker.getStore().filter({ + property: 'tag', + value: me.tagEl().innerHTML, + anyMatch: true, + }); + }, + + cancelEdit: function(list, event) { + let me = this; + if (me.mode === 'inEdit') { + me.setTag(me.tag); + me.setMode('editable'); + } + me.picker?.hide(); + }, + + + setTag: function(tag) { + let me = this; + let oldtag = me.tag; + me.tag = tag; + let rgb = PVE.Utils.tagOverrides[tag] ?? Proxmox.Utils.stringToRGB(tag); + + let cls = Proxmox.Utils.getTextContrastClass(rgb); + let color = Proxmox.Utils.rgbToCss(rgb); + me.setUserCls(`proxmox-tag-${cls}`); + me.setStyle('background-color', color); + if (rgb.length > 3) { + let fgcolor = Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]]); + + me.setStyle('color', fgcolor); + } else { + me.setStyle('color'); + } + me.updateData(); + if (oldtag !== tag) { + me.fireEvent('change', me, tag, oldtag); + } + }, + + updateData: function() { + let me = this; + if (me.destroying || me.destroyed) { + return; + } + me.update({ + tag: me.tag, + iconCls: me.icons[me.mode], + }); + }, + + tagEl: function() { + return this.el?.dom?.getElementsByTagName('span')?.[0]; + }, + + listeners: { + mousedown: 'onMouseDown', + click: 'onClick', + focusleave: 'cancelEdit', + keydown: 'onKeyPress', + beforeInput: 'beforeInput', + input: 'onInput', + element: 'el', + scope: 'this', + }, + + initComponent: function() { + let me = this; + + me.setTag(me.tag); + me.setMode(me.mode ?? 'normal'); + me.callParent(); + me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => { me.setTag(me.tag); }); // refresh tag color + }, + + destroy: function() { + let me = this; + if (me.picker) { + Ext.destroy(me.picker); + } + me.callParent(); + }, +}); -- 2.30.2