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 2F8029175B for ; Mon, 14 Nov 2022 10:49:48 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 11AC92175E for ; Mon, 14 Nov 2022 10:49:18 +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:49:16 +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 0D66943D43 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:00 +0100 Message-Id: <20221114094404.1241050-17-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 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: Mon, 14 Nov 2022 09:49:48 -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. this is a basic ext component, with 'i' tags for the icons (handle and add/remove button) and a span (for the tag text) shows the tag by default, and if put in editable mode, makes the span editable. when actually starting the edit, shows a picker with available tags to select from Signed-off-by: Dominik Csapak --- changes from v8: * added missing css classes * added emulated textbox style (white-background+border) when editing www/css/ext6-pve.css | 32 ++++++ www/manager6/Makefile | 1 + www/manager6/form/Tag.js | 233 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 www/manager6/form/Tag.js diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css index f7d0c4201..daaffa6ec 100644 --- a/www/css/ext6-pve.css +++ b/www/css/ext6-pve.css @@ -656,3 +656,35 @@ table.osds td:first-of-type { padding-top: 0px; padding-bottom: 0px; } + +.pve-edit-tag > i { + cursor: pointer; + font-size: 14px; +} + +.pve-edit-tag > i.handle { + padding-right: 5px; + cursor: grab; +} + +.pve-edit-tag > i.action { + padding-left: 5px; +} + +.pve-edit-tag.normal > i { + display: none; +} + +.pve-edit-tag.editable span, +.pve-edit-tag.inEdit span { + background-color: #ffffff; + border: 1px solid #a8a8a8; + color: #000; + padding-left: 2px; + padding-right: 2px; + min-width: 2em; +} + +.pve-edit-tag.inEdit span { + border: 1px solid #000; +} diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 9c7ced918..7bcc35e8e 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -75,6 +75,7 @@ JSSRC= \ form/iScsiProviderSelector.js \ form/TagColorGrid.js \ form/ListField.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 000000000..aa6ae8674 --- /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