[pve-devel] [PATCH manager v10 09/13] ui: add form/TagEdit.js
Dominik Csapak
d.csapak at proxmox.com
Tue Nov 15 14:02:44 CET 2022
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 <d.csapak at proxmox.com>
---
changes from v9:
* listen to glable event and render list of tags new
www/css/ext6-pve.css | 23 ++-
www/manager6/Makefile | 1 +
www/manager6/form/TagEdit.js | 325 +++++++++++++++++++++++++++++++++++
3 files changed, 345 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 21be8da8e..481c2bebc 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..7200e3c09
--- /dev/null
+++ b/www/manager6/form/TagEdit.js
@@ -0,0 +1,325 @@
+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: '<i class="fa fa-long-arrow-up"></i>',
+ 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);
+ }
+
+ me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => {
+ me.loadTags(me.oldTags, true); // refresh tag colors
+ });
+ },
+ },
+
+ 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 `<i data-qtip="${qtip}" class="fa fa-${cls}"></i>`;
+ },
+ },
+ },
+
+ 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: `<span>${gettext('Add Tag')}</span><i class="action fa fa-plus-square"></i>`,
+ 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: `<i data-qtip="${gettext('Cancel')}" class="fa fa-times"></i>`,
+ 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
More information about the pve-devel
mailing list