]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/form/TagEdit.js
ui: wizards: allow adding tags in the qemu/lxc create wizard
[pve-manager.git] / www / manager6 / form / TagEdit.js
1 Ext.define('PVE.panel.TagEditContainer', {
2 extend: 'Ext.container.Container',
3 alias: 'widget.pveTagEditContainer',
4
5 layout: {
6 type: 'hbox',
7 align: 'middle',
8 },
9
10 // set to false to hide the 'no tags' field and the edit button
11 canEdit: true,
12 editOnly: false,
13
14 controller: {
15 xclass: 'Ext.app.ViewController',
16
17 loadTags: function(tagstring = '', force = false) {
18 let me = this;
19 let view = me.getView();
20
21 if (me.oldTags === tagstring && !force) {
22 return;
23 }
24
25 view.suspendLayout = true;
26 me.forEachTag((tag) => {
27 view.remove(tag);
28 });
29 me.getViewModel().set('tagCount', 0);
30 let newtags = tagstring.split(/[;, ]/).filter((t) => !!t) || [];
31 newtags.forEach((tag) => {
32 me.addTag(tag);
33 });
34 view.suspendLayout = false;
35 view.updateLayout();
36 if (!force) {
37 me.oldTags = tagstring;
38 }
39 me.tagsChanged();
40 },
41
42 onRender: function(v) {
43 let me = this;
44 let view = me.getView();
45 view.toggleCls('hide-handles', PVE.UIOptions.shouldSortTags());
46
47 view.dragzone = Ext.create('Ext.dd.DragZone', v.getEl(), {
48 getDragData: function(e) {
49 let source = e.getTarget('.handle');
50 if (!source) {
51 return undefined;
52 }
53 let sourceId = source.parentNode.id;
54 let cmp = Ext.getCmp(sourceId);
55 let ddel = document.createElement('div');
56 ddel.classList.add('proxmox-tags-full');
57 ddel.innerHTML = Proxmox.Utils.getTagElement(cmp.tag, PVE.UIOptions.tagOverrides);
58 let repairXY = Ext.fly(source).getXY();
59 cmp.setDisabled(true);
60 ddel.id = Ext.id();
61 return {
62 ddel,
63 repairXY,
64 sourceId,
65 };
66 },
67 onMouseUp: function(target, e, id) {
68 let cmp = Ext.getCmp(this.dragData.sourceId);
69 if (cmp && !cmp.isDestroyed) {
70 cmp.setDisabled(false);
71 }
72 },
73 getRepairXY: function() {
74 return this.dragData.repairXY;
75 },
76 beforeInvalidDrop: function(target, e, id) {
77 let cmp = Ext.getCmp(this.dragData.sourceId);
78 if (cmp && !cmp.isDestroyed) {
79 cmp.setDisabled(false);
80 }
81 },
82 });
83 view.dropzone = Ext.create('Ext.dd.DropZone', v.getEl(), {
84 getTargetFromEvent: function(e) {
85 return e.getTarget('.proxmox-tag-dark,.proxmox-tag-light');
86 },
87 getIndicator: function() {
88 if (!view.indicator) {
89 view.indicator = Ext.create('Ext.Component', {
90 floating: true,
91 html: '<i class="fa fa-long-arrow-up"></i>',
92 hidden: true,
93 shadow: false,
94 });
95 }
96 return view.indicator;
97 },
98 onContainerOver: function() {
99 this.getIndicator().setVisible(false);
100 },
101 notifyOut: function() {
102 this.getIndicator().setVisible(false);
103 },
104 onNodeOver: function(target, dd, e, data) {
105 let indicator = this.getIndicator();
106 indicator.setVisible(true);
107 indicator.alignTo(Ext.getCmp(target.id), 't50-bl', [-1, -2]);
108 return this.dropAllowed;
109 },
110 onNodeDrop: function(target, dd, e, data) {
111 this.getIndicator().setVisible(false);
112 let sourceCmp = Ext.getCmp(data.sourceId);
113 if (!sourceCmp) {
114 return;
115 }
116 sourceCmp.setDisabled(false);
117 let targetCmp = Ext.getCmp(target.id);
118 view.remove(sourceCmp, { destroy: false });
119 view.insert(view.items.indexOf(targetCmp), sourceCmp);
120 me.tagsChanged();
121 },
122 });
123 },
124
125 forEachTag: function(func) {
126 let me = this;
127 let view = me.getView();
128 view.items.each((field) => {
129 if (field.getXType() === 'pveTag') {
130 func(field);
131 }
132 return true;
133 });
134 },
135
136 toggleEdit: function(cancel) {
137 let me = this;
138 let vm = me.getViewModel();
139 let view = me.getView();
140 let editMode = !vm.get('editMode');
141 vm.set('editMode', editMode);
142
143 // get a current tag list for editing
144 if (editMode) {
145 PVE.UIOptions.update();
146 }
147
148 me.forEachTag((tag) => {
149 tag.setMode(editMode ? 'editable' : 'normal');
150 });
151
152 if (!vm.get('editMode')) {
153 let tags = [];
154 if (cancel) {
155 me.loadTags(me.oldTags, true);
156 } else {
157 let toRemove = [];
158 me.forEachTag((cmp) => {
159 if (cmp.isVisible() && cmp.tag) {
160 tags.push(cmp.tag);
161 } else {
162 toRemove.push(cmp);
163 }
164 });
165 toRemove.forEach(cmp => view.remove(cmp));
166 tags = tags.join(',');
167 if (me.oldTags !== tags) {
168 me.oldTags = tags;
169 me.loadTags(tags, true);
170 me.getView().fireEvent('change', tags);
171 }
172 }
173 }
174 me.getView().updateLayout();
175 },
176
177 tagsChanged: function() {
178 let me = this;
179 let tags = [];
180 me.forEachTag(cmp => {
181 if (cmp.tag) {
182 tags.push(cmp.tag);
183 }
184 });
185 me.getViewModel().set('isDirty', me.oldTags !== tags.join(','));
186 me.forEachTag(cmp => {
187 cmp.updateFilter(tags);
188 });
189 },
190
191 addTag: function(tag, isNew) {
192 let me = this;
193 let view = me.getView();
194 let vm = me.getViewModel();
195 let index = view.items.length - 5;
196 if (PVE.UIOptions.shouldSortTags() && !isNew) {
197 index = view.items.findIndexBy(tagField => {
198 if (tagField.reference === 'noTagsField') {
199 return false;
200 }
201 if (tagField.xtype !== 'pveTag') {
202 return true;
203 }
204 let a = tagField.tag.toLowerCase();
205 let b = tag.toLowerCase();
206 return a > b ? true : a < b ? false : tagField.tag.localeCompare(tag) > 0;
207 }, 1);
208 }
209 let tagField = view.insert(index, {
210 xtype: 'pveTag',
211 tag,
212 mode: vm.get('editMode') ? 'editable' : 'normal',
213 listeners: {
214 change: 'tagsChanged',
215 destroy: function() {
216 vm.set('tagCount', vm.get('tagCount') - 1);
217 me.tagsChanged();
218 },
219 keypress: function(key) {
220 if (vm.get('hideFinishButtons')) {
221 return;
222 }
223 if (key === 'Enter') {
224 me.editClick();
225 } else if (key === 'Escape') {
226 me.cancelClick();
227 }
228 },
229 },
230 });
231
232 if (isNew) {
233 me.tagsChanged();
234 tagField.selectText();
235 }
236
237 vm.set('tagCount', vm.get('tagCount') + 1);
238 },
239
240 addTagClick: function(event) {
241 let me = this;
242 me.lookup('noTagsField').setVisible(false);
243 me.addTag('', true);
244 },
245
246 cancelClick: function() {
247 this.toggleEdit(true);
248 },
249
250 editClick: function() {
251 this.toggleEdit(false);
252 },
253
254 init: function(view) {
255 let me = this;
256 if (view.tags) {
257 me.loadTags(view.tags);
258 }
259 me.getViewModel().set('canEdit', view.canEdit);
260 me.getViewModel().set('editOnly', view.editOnly);
261
262 me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => {
263 let vm = me.getViewModel();
264 view.toggleCls('hide-handles', PVE.UIOptions.shouldSortTags());
265 me.loadTags(me.oldTags, !vm.get('editMode')); // refresh tag colors and order
266 });
267
268 if (view.editOnly) {
269 me.toggleEdit();
270 }
271 },
272 },
273
274 getTags: function() {
275 let me =this;
276 let controller = me.getController();
277 let tags = [];
278 controller.forEachTag((cmp) => {
279 if (cmp.tag.length) {
280 tags.push(cmp.tag);
281 }
282 });
283
284 return tags;
285 },
286
287 viewModel: {
288 data: {
289 tagCount: 0,
290 editMode: false,
291 canEdit: true,
292 isDirty: false,
293 editOnly: true,
294 },
295
296 formulas: {
297 hideNoTags: function(get) {
298 return get('tagCount') !== 0 || !get('canEdit');
299 },
300 hideEditBtn: function(get) {
301 return get('editMode') || !get('canEdit');
302 },
303 hideFinishButtons: function(get) {
304 return !get('editMode') || get('editOnly');
305 },
306 },
307 },
308
309 loadTags: function() {
310 return this.getController().loadTags(...arguments);
311 },
312
313 items: [
314 {
315 xtype: 'box',
316 reference: 'noTagsField',
317 bind: {
318 hidden: '{hideNoTags}',
319 },
320 html: gettext('No Tags'),
321 style: {
322 opacity: 0.5,
323 },
324 },
325 {
326 xtype: 'button',
327 iconCls: 'fa fa-plus',
328 tooltip: gettext('Add Tag'),
329 bind: {
330 hidden: '{!editMode}',
331 },
332 hidden: true,
333 margin: '0 8 0 5',
334 ui: 'default-toolbar',
335 handler: 'addTagClick',
336 },
337 {
338 xtype: 'tbseparator',
339 ui: 'horizontal',
340 bind: {
341 hidden: '{hideFinishButtons}',
342 },
343 hidden: true,
344 },
345 {
346 xtype: 'button',
347 iconCls: 'fa fa-times',
348 tooltip: gettext('Cancel Edit'),
349 bind: {
350 hidden: '{hideFinishButtons}',
351 },
352 hidden: true,
353 margin: '0 5 0 0',
354 ui: 'default-toolbar',
355 handler: 'cancelClick',
356 },
357 {
358 xtype: 'button',
359 iconCls: 'fa fa-check',
360 tooltip: gettext('Finish Edit'),
361 bind: {
362 hidden: '{hideFinishButtons}',
363 disabled: '{!isDirty}',
364 },
365 hidden: true,
366 handler: 'editClick',
367 },
368 {
369 xtype: 'box',
370 cls: 'pve-tag-inline-button',
371 html: `<i data-qtip="${gettext('Edit Tags')}" class="fa fa-pencil"></i>`,
372 bind: {
373 hidden: '{hideEditBtn}',
374 },
375 listeners: {
376 click: 'editClick',
377 element: 'el',
378 },
379 },
380 ],
381
382 listeners: {
383 render: 'onRender',
384 },
385
386 destroy: function() {
387 let me = this;
388 Ext.destroy(me.dragzone);
389 Ext.destroy(me.dropzone);
390 Ext.destroy(me.indicator);
391 me.callParent();
392 },
393 });