ui: add form/Tag

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 <d.csapak@proxmox.com>
This commit is contained in:
Dominik Csapak 2022-11-16 16:48:10 +01:00 committed by Thomas Lamprecht
parent e2c29a883d
commit d9dba35d00
3 changed files with 265 additions and 0 deletions

View File

@ -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;
}

View File

@ -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 \

232
www/manager6/form/Tag.js Normal file
View File

@ -0,0 +1,232 @@
Ext.define('Proxmox.form.Tag', {
extend: 'Ext.Component',
alias: 'widget.pveTag',
mode: 'editable',
icons: {
editable: 'fa fa-minus-square',
normal: '',
inEdit: 'fa fa-check-square',
},
tag: '',
cls: 'pve-edit-tag',
tpl: [
'<i class="handle fa fa-bars"></i>',
'<span>{tag}</span>',
'<i class="action {iconCls}"></i>',
],
// 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();
},
destroy: function() {
let me = this;
if (me.picker) {
Ext.destroy(me.picker);
}
me.callParent();
},
});