/** * * ----------------------------------------------------------------------------- * * @type {Component} * * --- SLOTS: --- * @slot item-field * The template to use for each field. * > Also note: * > If this slot contains exactly one element with `role="focusable"` or * > `focus-first`, that element will be focused automatically on add/remove. * @param {Ref} item * @param {Function} doSet * @param {Array} allItems * @param {Number} idx * * --- EVENTS EMITTED: --- * @event input * * ----------------------------------------------------------------------------- */ parasails.registerComponent('multifield', { // ╔═╗╦═╗╔═╗╔═╗╔═╗ // ╠═╝╠╦╝║ ║╠═╝╚═╗ // ╩ ╩╚═╚═╝╩ ╚═╝ props: [ 'value',// « 2-way (for v-model) 'addButtonText',//« Custom text for the "+ Add another" button (optional) 'inputType',// For customizing the input type. 'selectOptions',// For select support. An array of objects that have a name and value. ex: [{name: macOS, value: darwin}, {name: Windows, value: windows}] 'nameAndHostCountSelectOptions',// For nameAndHostCountSelect support. An array of objects that have a name, id, and hostCount. ex: [{"name": "Microsoft office for macOS 16.76","id": 289874,"hostCount": 1}] 'cloudError',// For highlighting fields, for this to work, the error response needs to return the value of the invalid fields, see api/controllers/update-priority-vulnerabilities to see an example of this. 'placeholder',// an optional placeholder value for text type inputs ], // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ data: function () { return { currentFieldValues: undefined, //« will be initialized to a single-item array in beforeMount optionsForSelect: [], inputPlaceholder: '', }; }, // ╦ ╦╔╦╗╔╦╗╦ // ╠═╣ ║ ║║║║ // ╩ ╩ ╩ ╩ ╩╩═╝ template: `
`, // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ beforeMount: function() { // Absorb value if (this.value !== undefined && !_.isArray(this.value)) { throw new Error('In , if specified, `v-model`/`:value` must be either an array or `undefined`. But instead, got: '+this.value); }//• if (this.value === undefined || _.isEqual(this.value, [])) { this.currentFieldValues = [ undefined ]; } else { this.currentFieldValues = _.clone(this.value); // ^^ The clone is to prevent entanglement risk. } if(this.inputType === 'nameAndHostCountSelect') { if(!_.isArray(this.selectOptions)){ throw new Error('Missing selectOptions. When using inputType="nameAndHostCountSelect", an array of selectOptions is required.'); } else { for(let option of this.selectOptions){ // If we're using inputType="nameAndHostCountSelect", we will validate all options before cloning the object. if(!option.id){ throw new Error(`Option in selectOptions is missing an id. When using inputType="nameAndHostCountSelect", An id property is required for all objects in the selectOptions array. Object missing an id: ${option}`); } if(!option.name){ throw new Error(`Option in selectOptions is missing a name. When using inputType="nameAndHostCountSelect", A name property is required for all objects in the selectOptions array. Object missing a name: ${option}`); } if(option.hostCount === undefined){ throw new Error(`Option in selectOptions is missing a hostCount. When using inputType="nameAndHostCountSelect", A hostCount property is required for all objects in the selectOptions array. Object missing a hostCount: ${option}`); } } this.optionsForSelect = _.clone(this.selectOptions); } } if(this.inputType === 'teamSelect') { if(!_.isArray(this.selectOptions)){ throw new Error('Missing selectOptions. When using inputType="teamSelect", an array of selectOptions is required.'); } else { for(let option of this.selectOptions){ // If we're using inputType="nameAndHostCountSelect", we will validate all options before cloning the object. if(typeof option.fleetApid !== 'number'){ throw new Error(`Option in selectOptions is missing an fleetApid. When using inputType="teamSelect", An fleetApid property is required for all objects in the selectOptions array. Object missing an fleetApid: ${option}`); } if(!option.teamName){ throw new Error(`Option in selectOptions is missing a teamName. When using inputType="teamSelect", A teamName property is required for all objects in the selectOptions array. Object missing a teamName: ${option}`); } } this.optionsForSelect = _.clone(this.selectOptions); } } if(this.inputType === 'select') { if(!_.isArray(this.selectOptions)){ throw new Error('Missing selectOptions. When using inputType="select", an array of selectOptions is required.'); } else { for(let option of this.selectOptions){ // If we're using inputType="select", we will validate all options before cloning the object. if(!option.value){ throw new Error(`Option in selectOptions is missing a value. When using inputType="select", A value property is required for all objects in the selectOptions array. Object missing a value. ${option}`); } if(!option.name){ throw new Error(`Option in selectOptions is missing a name. When using inputType="select", A name property is required for all objects in the selectOptions array. Object missing a name. ${option}`); } } this.optionsForSelect = _.clone(this.selectOptions); } } if(this.inputType === 'checkboxes') { if(!_.isArray(this.selectOptions)){ throw new Error('Missing selectOptions. When using inputType="select", an array of selectOptions is required.'); } else { for(let option of this.selectOptions){ // If we're using inputType="select", we will validate all options before cloning the object. if(!option.value){ throw new Error(`Option in selectOptions is missing a value. When using inputType="select", A value property is required for all objects in the selectOptions array. Object missing a value. ${option}`); } if(!option.name){ throw new Error(`Option in selectOptions is missing a name. When using inputType="select", A name property is required for all objects in the selectOptions array. Object missing a name. ${option}`); } } this.optionsForSelect = _.clone(this.selectOptions); if(this.currentFieldValues === [null]){ this.currentFieldValues = []; } } } if(this.placeholder){ this.inputPlaceholder = this.placeholder; } }, mounted: async function () { }, beforeDestroy: function() { }, watch: { value: function(those) { // console.log('ran the `value` watcher', those); if (those !== undefined && !_.isArray(those)) { throw new Error('Cannot programmatically set value for : the given value must be an array or `undefined`, but instead got: '+those); } this.currentFieldValues = those; } }, // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ methods: { // ╔═╗╦ ╦╔═╗╔╗╔╔╦╗ ╦ ╦╔═╗╔╗╔╔╦╗╦ ╔═╗╦═╗╔═╗ // ║╣ ╚╗╔╝║╣ ║║║ ║ ╠═╣╠═╣║║║ ║║║ ║╣ ╠╦╝╚═╗ // ╚═╝ ╚╝ ╚═╝╝╚╝ ╩ ╩ ╩╩ ╩╝╚╝═╩╝╩═╝╚═╝╩╚═╚═╝ inputDefaultItemField: async function($event, idx) { var parsedValue = $event.target.value || undefined; this.currentFieldValues[idx] = parsedValue; await this.forceRender(); this._handleChangingFieldValues(); }, inputTeamSelectItemField: async function($event, idx) { var parsedValue = $event.target.value || undefined; this.currentFieldValues[idx] = parsedValue; if(parsedValue === 'allTeams') { this.currentFieldValues = _.pluck(this.optionsForSelect, 'fleetApid'); } await this.forceRender(); this._handleChangingFieldValues(); }, inputCheckboxItemField: async function($event) { let checkboxValue = $event.target.value; if($event.target.checked) { this.currentFieldValues.push(checkboxValue); } else { this.currentFieldValues = this.currentFieldValues .filter(value => value !== checkboxValue); } this.currentFieldValues = this.currentFieldValues.filter(value => value !== undefined && value !== null); await this.forceRender(); this._handleChangingFieldValues(); }, clickAddItem: async function() { this.currentFieldValues.push(undefined); await this.forceRender();//«« this is so that the programmatic focusing code below will work // Autofocus (but only if we're sure it's going to work) var idxToFocus = this.currentFieldValues.length - 1; var focalSelector = `[role="item-${idxToFocus}"] [role="focusable"], [role="item-${idxToFocus}"] [focus-first]`; var focusableEls = this.$find(focalSelector); if (focusableEls.length === 1) { this.$focus(focalSelector); } this.$emit('input', _.clone(this.currentFieldValues)); }, clickRemoveItem: async function(idx) { this.currentFieldValues.splice(idx, 1); this.$emit('input', _.clone(this.currentFieldValues)); // The _.clone() above is to prevent an entanglement issue caused by // emitting the same reference if we were to use this.currentFieldValues // directly. // Autofocus (but only if we're sure it's going to work) var idxToFocus = idx >= this.currentFieldValues.length ? this.currentFieldValues.length - 1 : idx; var focalSelector = `[role="item-${idxToFocus}"] [role="focusable"], [role="item-${idxToFocus}"] [focus-first]`; var focusableEls = this.$find(focalSelector); if (focusableEls.length === 1) { this.$focus(focalSelector); } }, // To accomodate the requested behavior in wireframes. clickResetSingleItem: async function() { this.currentFieldValues[0] = undefined; this.$emit('input', _.clone(this.currentFieldValues)); await this.forceRender(); }, // ╔═╗╦ ╦╔╗ ╦ ╦╔═╗ ╔╦╗╔═╗╔╦╗╦ ╦╔═╗╔╦╗╔═╗ // ╠═╝║ ║╠╩╗║ ║║ ║║║║╣ ║ ╠═╣║ ║ ║║╚═╗ // ╩ ╚═╝╚═╝╩═╝╩╚═╝ ╩ ╩╚═╝ ╩ ╩ ╩╚═╝═╩╝╚═╝ //… // ╔═╗╦═╗╦╦ ╦╔═╗╔╦╗╔═╗ ╔╦╗╔═╗╔╦╗╦ ╦╔═╗╔╦╗╔═╗ // ╠═╝╠╦╝║╚╗╔╝╠═╣ ║ ║╣ ║║║║╣ ║ ╠═╣║ ║ ║║╚═╗ // ╩ ╩╚═╩ ╚╝ ╩ ╩ ╩ ╚═╝ ╩ ╩╚═╝ ╩ ╩ ╩╚═╝═╩╝╚═╝ _getCurriedDoSetFn: function(idx) { return async (newVal)=>{ // Note that it is the responsibility of the userland contents of the // slot to make sure this incoming value is proper. For example, if // the slot contains an ``, then when invoking doSet, you should // do so like: // ``` // // ``` // // The `||undefined` is because otherwise, you get `null`, and you // probably want blank fields to be treated as undefined so we can // automatically splice them out of the array before emitting our input // event. // // The reason this is left as a userland concern is because the `null` // value itself, just like `''`, `0`, `false`, `NaN` or other similar // values, is technically a valid thing that might be relevant under // unusual circumstances. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // console.log('FIRED DOSET for idx',idx,'and newVal', newVal); this.currentFieldValues[idx] = newVal; await this.forceRender(); this._handleChangingFieldValues(); }; }, _handleChangingFieldValues: function() { // > Note that we do a `_.clone()`. This is to prevent an entanglement // > issue caused by emitting the same reference if we were to simply emit // > `this.currentFieldValues` directly. this.$emit('input', _.clone(this.currentFieldValues)); // console.log('emitting in …', _.cloneDeep(this.currentFieldValues)); }, } });