|
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,16 @@ 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 | + deep: true, |
| 269 | + }, |
| 270 | + }, |
| 271 | +
|
242 | 272 | created() { |
243 | 273 | this.queue = new PQueue({ concurrency: 1 }) |
244 | 274 |
|
@@ -266,30 +296,66 @@ export default { |
266 | 296 | * @param {InputEvent} event The input event that triggered adding a new entry |
267 | 297 | */ |
268 | 298 | async onInput({ target, isComposing }) { |
| 299 | + if (this.answer.local) { |
| 300 | + this.localText = target.value |
| 301 | + return |
| 302 | + } |
| 303 | +
|
269 | 304 | if (!isComposing && !this.isIMEComposing && target.value !== '') { |
270 | 305 | // clone answer |
271 | 306 | const answer = { ...this.answer } |
272 | 307 | answer.text = this.$refs.input.value |
273 | 308 |
|
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) |
| 309 | + await this.updateAnswer(answer) |
278 | 310 |
|
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 |
| 311 | + // Forward changes, but use current answer.text to avoid erasing |
| 312 | + // any in-between changes while updating the answer |
| 313 | + answer.text = this.$refs.input.value |
| 314 | + this.$emit('update:answer', this.index, answer) |
| 315 | + } |
| 316 | + }, |
282 | 317 |
|
283 | | - this.$emit('create-answer', this.index, newAnswer) |
284 | | - } else { |
285 | | - await this.updateAnswer(answer) |
| 318 | + /** |
| 319 | + * Handle Enter key: create local answer or move focus |
| 320 | + * |
| 321 | + * @param {KeyboardEvent} e the keydown event |
| 322 | + */ |
| 323 | + onEnter(e) { |
| 324 | + if (this.answer.local) { |
| 325 | + this.createLocalAnswer(e) |
| 326 | + return |
| 327 | + } |
| 328 | + this.focusNextInput(e) |
| 329 | + }, |
286 | 330 |
|
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 | | - } |
| 331 | + /** |
| 332 | + * Create a new local answer option from the current input |
| 333 | + * |
| 334 | + * @param {Event} e the triggering event |
| 335 | + */ |
| 336 | + async createLocalAnswer(e) { |
| 337 | + if (this.isIMEComposing || e?.isComposing) { |
| 338 | + return |
| 339 | + } |
| 340 | +
|
| 341 | + const value = this.localText ?? '' |
| 342 | + if (!value.trim()) { |
| 343 | + return |
292 | 344 | } |
| 345 | +
|
| 346 | + const answer = { ...this.answer } |
| 347 | + answer.text = value |
| 348 | +
|
| 349 | + // Dispatched for creation. Marked as synced |
| 350 | + this.$set(this.answer, 'local', false) |
| 351 | + const newAnswer = await this.createAnswer(answer) |
| 352 | +
|
| 353 | + // Forward changes, but use current answer.text to avoid erasing |
| 354 | + // any in-between changes while creating the answer |
| 355 | + newAnswer.text = this.$refs.input.value |
| 356 | + this.localText = '' |
| 357 | +
|
| 358 | + this.$emit('create-answer', this.index, newAnswer) |
293 | 359 | }, |
294 | 360 |
|
295 | 361 | /** |
|
0 commit comments