blob: 18bd963158c9245069cc3ba4235992ccb46db591 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import {LitElement, html} from 'lit-element';
6
7import deepEqual from 'deep-equal';
8import {fieldTypes, EMPTY_FIELD_VALUE} from 'shared/issue-fields.js';
9import {arrayDifference, equalsIgnoreCase} from 'shared/helpers.js';
10import {NON_EDITING_KEY_EVENTS} from 'shared/dom-helpers.js';
11
12import './mr-multi-checkbox.js';
13import 'react/mr-react-autocomplete.tsx';
14
15const AUTOCOMPLETE_INPUT = 'AUTOCOMPLETE_INPUT';
16const CHECKBOX_INPUT = 'CHECKBOX_INPUT';
17const SELECT_INPUT = 'SELECT_INPUT';
18
19/**
20 * `<mr-edit-field>`
21 *
22 * A single edit input for a fieldDef + the values of the field.
23 *
24 */
25export class MrEditField extends LitElement {
26 /** @override */
27 createRenderRoot() {
28 return this;
29 }
30
31 /** @override */
32 render() {
33 return html`
34 <link href="https://fonts.googleapis.com/icon?family=Material+Icons"
35 rel="stylesheet">
36 <style>
37 mr-edit-field {
38 display: block;
39 }
40 mr-edit-field[hidden] {
41 display: none;
42 }
43 mr-edit-field input,
44 mr-edit-field select {
45 width: var(--mr-edit-field-width);
46 padding: var(--mr-edit-field-padding);
47 }
48 </style>
49 ${this._renderInput()}
50 `;
51 }
52
53 /**
54 * Renders a single input field.
55 * @return {TemplateResult}
56 */
57 _renderInput() {
58 switch (this._widgetType) {
59 case CHECKBOX_INPUT:
60 return html`
61 <mr-multi-checkbox
62 .options=${this.options}
63 .values=${[...this.values]}
64 @change=${this._changeHandler}
65 ></mr-multi-checkbox>
66 `;
67 case SELECT_INPUT:
68 return html`
69 <select
70 id="${this.label}"
71 class="editSelect"
72 aria-label=${this.name}
73 @change=${this._changeHandler}
74 >
75 <option value="">${EMPTY_FIELD_VALUE}</option>
76 ${this.options.map((option) => html`
77 <option
78 value=${option.optionName}
79 .selected=${this.value === option.optionName}
80 >
81 ${option.optionName}
82 ${option.docstring ? ' = ' + option.docstring : ''}
83 </option>
84 `)}
85 </select>
86 `;
87 case AUTOCOMPLETE_INPUT:
88 return html`
89 <mr-react-autocomplete
90 .label=${this.label}
91 .vocabularyName=${this.acType || ''}
92 .inputType=${this._html5InputType}
93 .fixedValues=${this.derivedValues}
94 .value=${this.multi ? this.values : this.value}
95 .multiple=${this.multi}
96 .onChange=${this._changeHandlerReact.bind(this)}
97 ></mr-react-autocomplete>
98 `;
99 default:
100 return '';
101 }
102 }
103
104
105 /** @override */
106 static get properties() {
107 return {
108 // TODO(zhangtiff): Redesign this a bit so we don't need two separate
109 // ways of specifying "type" for a field. Right now, "type" is mapped to
110 // the Monorail custom field types whereas "acType" includes additional
111 // data types such as components, and labels.
112 // String specifying what kind of autocomplete to add to this field.
113 acType: {type: String},
114 // "type" is based on the various custom field types available in
115 // Monorail.
116 type: {type: String},
117 label: {type: String},
118 multi: {type: Boolean},
119 name: {type: String},
120 // Only used for basic, non-repeated fields.
121 placeholder: {type: String},
122 initialValues: {
123 type: Array,
124 hasChanged(newVal, oldVal) {
125 // Prevent extra recomputations of the same initial value causing
126 // values to be reset.
127 return !deepEqual(newVal, oldVal);
128 },
129 },
130 // The current user-inputted values for a field.
131 values: {type: Array},
132 derivedValues: {type: Array},
133 // For enum fields, the possible options that you have. Each entry is a
134 // label type with an additional optionName field added.
135 options: {type: Array},
136 };
137 }
138
139 /** @override */
140 constructor() {
141 super();
142 this.initialValues = [];
143 this.values = [];
144 this.derivedValues = [];
145 this.options = [];
146 this.multi = false;
147
148 this.actType = '';
149 this.placeholder = '';
150 this.type = '';
151 }
152
153 /** @override */
154 update(changedProperties) {
155 if (changedProperties.has('initialValues')) {
156 // Assume we always want to reset the user's input when initial
157 // values change.
158 this.reset();
159 }
160 super.update(changedProperties);
161 }
162
163 /**
164 * @return {string}
165 */
166 get value() {
167 return _getSingleValue(this.values);
168 }
169
170 /**
171 * @return {string}
172 */
173 get _widgetType() {
174 const type = this.type;
175 const multi = this.multi;
176 if (type === fieldTypes.ENUM_TYPE) {
177 if (multi) {
178 return CHECKBOX_INPUT;
179 }
180 return SELECT_INPUT;
181 } else {
182 return AUTOCOMPLETE_INPUT;
183 }
184 }
185
186 /**
187 * @return {string} HTML type for the input.
188 */
189 get _html5InputType() {
190 const type = this.type;
191 if (type === fieldTypes.INT_TYPE) {
192 return 'number';
193 } else if (type === fieldTypes.DATE_TYPE) {
194 return 'date';
195 }
196 return 'text';
197 }
198
199 /**
200 * Reset form values to initial state.
201 */
202 reset() {
203 this.values = _wrapInArray(this.initialValues);
204 }
205
206 /**
207 * Return the values that the user added to this input.
208 * @return {Array<string>}åß
209 */
210 getValuesAdded() {
211 if (!this.values || !this.values.length) return [];
212 return arrayDifference(
213 this.values, this.initialValues, equalsIgnoreCase);
214 }
215
216 /**
217 * Return the values that the userremoved from this input.
218 * @return {Array<string>}
219 */
220 getValuesRemoved() {
221 if (!this.multi && (!this.values || this.values.length > 0)) return [];
222 return arrayDifference(
223 this.initialValues, this.values, equalsIgnoreCase);
224 }
225
226 /**
227 * Syncs form values and fires a change event as the user edits the form.
228 * @param {Event} e
229 * @fires Event#change
230 * @private
231 */
232 _changeHandler(e) {
233 if (e instanceof KeyboardEvent) {
234 if (NON_EDITING_KEY_EVENTS.has(e.key)) return;
235 }
236 const input = e.target;
237
238 if (input.getValues) {
239 // <mr-multi-checkbox> support.
240 this.values = input.getValues();
241 } else {
242 // Is a native input element.
243 const value = input.value.trim();
244 this.values = _wrapInArray(value);
245 }
246
247 this.dispatchEvent(new Event('change'));
248 }
249
250 /**
251 * Syncs form values and fires a change event as the user edits the form.
252 * @param {React.SyntheticEvent} _e
253 * @param {string|Array<string>|null} value React autcoomplete form value.
254 * @fires Event#change
255 * @private
256 */
257 _changeHandlerReact(_e, value) {
258 this.values = _wrapInArray(value);
259
260 this.dispatchEvent(new Event('change'));
261 }
262}
263
264/**
265 * Returns the string value for a single field.
266 * @param {Array<string>} arr
267 * @return {string}
268 */
269function _getSingleValue(arr) {
270 return (arr && arr.length) ? arr[0] : '';
271}
272
273/**
274 * Returns the string value for a single field.
275 * @param {Array<string>|string} v
276 * @return {string}
277 */
278function _wrapInArray(v) {
279 if (!v) return [];
280
281 let values = v;
282 if (!Array.isArray(v)) {
283 values = !!v ? [v] : [];
284 }
285 return [...values];
286}
287
288customElements.define('mr-edit-field', MrEditField);