|
11 | 11 | class="question__item__pseudoInput" /> |
12 | 12 | <input |
13 | 13 | ref="input" |
| 14 | + v-model="localText" |
14 | 15 | :aria-label="ariaLabel" |
15 | 16 | :placeholder="placeholder" |
16 | | - :value="answer.text" |
17 | 17 | class="question__input" |
18 | 18 | :class="{ 'question__input--shifted': !isDropdown }" |
19 | 19 | :maxlength="maxOptionLength" |
20 | 20 | type="text" |
21 | 21 | dir="auto" |
22 | 22 | @input="debounceOnInput" |
23 | 23 | @keydown.delete="deleteEntry" |
24 | | - @keydown.enter.prevent="focusNextInput" |
| 24 | + @keydown.enter.prevent="onEnter" |
25 | 25 | @compositionstart="onCompositionStart" |
26 | 26 | @compositionend="onCompositionEnd" /> |
27 | 27 |
|
|
64 | 64 | </template> |
65 | 65 | </NcButton> |
66 | 66 | </div> |
| 67 | + <div v-else class="option__actions"> |
| 68 | + <NcButton |
| 69 | + :aria-label="t('forms', 'Add a new answer option')" |
| 70 | + variant="tertiary" |
| 71 | + :disabled="isIMEComposing || !canCreateLocalAnswer" |
| 72 | + @click="createLocalAnswer"> |
| 73 | + <template #icon> |
| 74 | + <IconPlus :size="20" /> |
| 75 | + </template> |
| 76 | + </NcButton> |
| 77 | + </div> |
67 | 78 | </li> |
68 | 79 | </template> |
69 | 80 |
|
@@ -98,6 +109,7 @@ export default { |
98 | 109 | IconCheckboxBlankOutline, |
99 | 110 | IconDelete, |
100 | 111 | IconDragIndicator, |
| 112 | + IconPlus, |
101 | 113 | IconRadioboxBlank, |
102 | 114 | IconTableColumn, |
103 | 115 | IconTableRow, |
@@ -163,10 +175,18 @@ export default { |
163 | 175 | queue: null, |
164 | 176 | debounceOnInput: null, |
165 | 177 | isIMEComposing: false, |
| 178 | + localText: this.answer?.text ?? '', |
166 | 179 | } |
167 | 180 | }, |
168 | 181 |
|
169 | 182 | computed: { |
| 183 | + canCreateLocalAnswer() { |
| 184 | + if (this.answer.local) { |
| 185 | + return !!this.localText?.trim() |
| 186 | + } |
| 187 | + return !!this.answer.text?.trim() |
| 188 | + }, |
| 189 | +
|
170 | 190 | ariaLabel() { |
171 | 191 | if (this.answer.local) { |
172 | 192 | if (this.optionType === OptionType.Column) { |
@@ -239,6 +259,17 @@ export default { |
239 | 259 | }, |
240 | 260 | }, |
241 | 261 |
|
| 262 | + watch: { |
| 263 | + // Keep localText in sync when the parent replaces/updates the answer prop |
| 264 | + answer: { |
| 265 | + handler(newVal) { |
| 266 | + this.localText = newVal?.text ?? '' |
| 267 | + }, |
| 268 | +
|
| 269 | + deep: true, |
| 270 | + }, |
| 271 | + }, |
| 272 | +
|
242 | 273 | created() { |
243 | 274 | this.queue = new PQueue({ concurrency: 1 }) |
244 | 275 |
|
@@ -266,34 +297,72 @@ export default { |
266 | 297 | * @param {InputEvent} event The input event that triggered adding a new entry |
267 | 298 | */ |
268 | 299 | async onInput({ target, isComposing }) { |
| 300 | + if (this.answer.local) { |
| 301 | + this.localText = target.value |
| 302 | + return |
| 303 | + } |
| 304 | +
|
269 | 305 | if (!isComposing && !this.isIMEComposing && target.value !== '') { |
270 | 306 | // clone answer |
271 | 307 | const answer = { ...this.answer } |
272 | 308 | answer.text = this.$refs.input.value |
273 | 309 |
|
274 | | - if (this.answer.local) { |
275 | | - // Dispatched for creation. Marked as synced |
276 | | - this.$set(this.answer, 'local', false) |
277 | | - const newAnswer = await this.createAnswer(answer) |
| 310 | + await this.updateAnswer(answer) |
278 | 311 |
|
279 | | - // Forward changes, but use current answer.text to avoid erasing |
280 | | - // any in-between changes while creating the answer |
281 | | - newAnswer.text = this.$refs.input.value |
| 312 | + // Forward changes, but use current answer.text to avoid erasing |
| 313 | + // any in-between changes while updating the answer |
| 314 | + answer.text = this.$refs.input.value |
| 315 | + this.$emit('update:answer', this.index, answer) |
| 316 | + } |
| 317 | + }, |
282 | 318 |
|
283 | | - this.$emit('create-answer', this.index, newAnswer) |
284 | | - } else { |
285 | | - await this.updateAnswer(answer) |
| 319 | + /** |
| 320 | + * Handle Enter key: create local answer or move focus |
| 321 | + * |
| 322 | + * @param {KeyboardEvent} e the keydown event |
| 323 | + */ |
| 324 | + onEnter(e) { |
| 325 | + if (this.answer.local) { |
| 326 | + this.createLocalAnswer(e) |
| 327 | + return |
| 328 | + } |
| 329 | + this.focusNextInput(e) |
| 330 | + }, |
286 | 331 |
|
287 | | - // Forward changes, but use current answer.text to avoid erasing |
288 | | - // any in-between changes while updating the answer |
289 | | - answer.text = this.$refs.input.value |
290 | | - this.$emit('update:answer', this.index, answer) |
291 | | - } |
| 332 | + /** |
| 333 | + * Create a new local answer option from the current input |
| 334 | + * |
| 335 | + * @param {Event} e the triggering event |
| 336 | + */ |
| 337 | + async createLocalAnswer(e) { |
| 338 | + if (this.isIMEComposing || e?.isComposing) { |
| 339 | + return |
| 340 | + } |
| 341 | +
|
| 342 | + const value = this.localText ?? '' |
| 343 | + if (!value.trim()) { |
| 344 | + return |
292 | 345 | } |
| 346 | +
|
| 347 | + const answer = { ...this.answer } |
| 348 | + answer.text = value |
| 349 | +
|
| 350 | + // Dispatched for creation. Marked as synced |
| 351 | + this.$set(this.answer, 'local', false) |
| 352 | + const newAnswer = await this.createAnswer(answer) |
| 353 | +
|
| 354 | + // Forward changes, but use current answer.text to avoid erasing |
| 355 | + // any in-between changes while creating the answer |
| 356 | + newAnswer.text = this.$refs.input.value |
| 357 | + this.localText = '' |
| 358 | +
|
| 359 | + this.$emit('create-answer', this.index, newAnswer) |
293 | 360 | }, |
294 | 361 |
|
295 | 362 | /** |
296 | 363 | * Request a new answer |
| 364 | + * |
| 365 | + * @param {Event} e the triggering event |
297 | 366 | */ |
298 | 367 | focusNextInput(e) { |
299 | 368 | if (this.isIMEComposing || e?.isComposing) { |
|
0 commit comments