feat(autocomplete): implement shift modifier interactions

pull/7082/head
Jakub Michálek 2025-01-13 18:58:16 +01:00
parent f481158d36
commit db25dbae3f
2 changed files with 96 additions and 1 deletions

View File

@ -102,6 +102,26 @@
<td><i>pageDown</i></td> <td><i>pageDown</i></td>
<td>Jumps visual focus to last option.</td> <td>Jumps visual focus to last option.</td>
</tr> </tr>
<tr>
<td><i>shift</i> + <i>down arrow</i></td>
<td>Moves focus to the next option and toggles the selection state.</td>
</tr>
<tr>
<td><i>shift</i> + <i>up arrow</i></td>
<td>Moves focus to the previous option and toggles the selection state.</td>
</tr>
<tr>
<td><i>shift</i> + <i>space</i></td>
<td>Selects the items between the most recently selected option and the focused option.</td>
</tr>
<tr>
<td><i>control</i> + <i>shift</i> + <i>home</i></td>
<td>Selects the focused options and all the options up to the first one.</td>
</tr>
<tr>
<td><i>control</i> + <i>shift</i> + <i>end</i></td>
<td>Selects the focused options and all the options down to the last one.</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -212,6 +212,7 @@ export default {
virtualScroller: null, virtualScroller: null,
searchTimeout: null, searchTimeout: null,
dirty: false, dirty: false,
startRangeIndex: -1,
data() { data() {
return { return {
id: this.$attrs.id, id: this.$attrs.id,
@ -389,6 +390,7 @@ export default {
case 'Enter': case 'Enter':
case 'NumpadEnter': case 'NumpadEnter':
case 'Space':
this.onEnterKey(event); this.onEnterKey(event);
break; break;
@ -400,6 +402,11 @@ export default {
this.onTabKey(event); this.onTabKey(event);
break; break;
case 'ShiftLeft':
case 'ShiftRight':
this.onShiftKey(event);
break;
case 'Backspace': case 'Backspace':
this.onBackspaceKey(event); this.onBackspaceKey(event);
break; break;
@ -553,6 +560,21 @@ export default {
this.changeFocusedOptionIndex(event, index); this.changeFocusedOptionIndex(event, index);
} }
}, },
onOptionSelectRange(event, start = -1, end = -1) {
start === -1 && (start = this.findNearestSelectedOptionIndex(end, true));
end === -1 && (end = this.findNearestSelectedOptionIndex(start));
if (start !== -1 && end !== -1) {
const rangeStart = Math.min(start, end);
const rangeEnd = Math.max(start, end);
const value = this.visibleOptions
.slice(rangeStart, rangeEnd + 1)
.filter((option) => this.isValidOption(option))
.map((option) => this.getOptionValue(option));
this.updateModel(event, value);
}
},
onOverlayClick(event) { onOverlayClick(event) {
OverlayEventBus.emit('overlay-click', { OverlayEventBus.emit('overlay-click', {
originalEvent: event, originalEvent: event,
@ -576,6 +598,10 @@ export default {
const optionIndex = this.focusedOptionIndex !== -1 ? this.findNextOptionIndex(this.focusedOptionIndex) : this.clicked ? this.findFirstOptionIndex() : this.findFirstFocusedOptionIndex(); const optionIndex = this.focusedOptionIndex !== -1 ? this.findNextOptionIndex(this.focusedOptionIndex) : this.clicked ? this.findFirstOptionIndex() : this.findFirstFocusedOptionIndex();
if (event.shiftKey) {
this.onOptionSelectRange(event, this.startRangeIndex, optionIndex);
}
this.changeFocusedOptionIndex(event, optionIndex); this.changeFocusedOptionIndex(event, optionIndex);
event.preventDefault(); event.preventDefault();
@ -595,6 +621,10 @@ export default {
} else { } else {
const optionIndex = this.focusedOptionIndex !== -1 ? this.findPrevOptionIndex(this.focusedOptionIndex) : this.clicked ? this.findLastOptionIndex() : this.findLastFocusedOptionIndex(); const optionIndex = this.focusedOptionIndex !== -1 ? this.findPrevOptionIndex(this.focusedOptionIndex) : this.clicked ? this.findLastOptionIndex() : this.findLastFocusedOptionIndex();
if (event.shiftKey) {
this.onOptionSelectRange(event, optionIndex, this.startRangeIndex);
}
this.changeFocusedOptionIndex(event, optionIndex); this.changeFocusedOptionIndex(event, optionIndex);
event.preventDefault(); event.preventDefault();
@ -622,6 +652,12 @@ export default {
onHomeKey(event) { onHomeKey(event) {
const { currentTarget } = event; const { currentTarget } = event;
const len = currentTarget.value.length; const len = currentTarget.value.length;
const metaKey = event.metaKey || event.ctrlKey;
const optionIndex = this.findFirstOptionIndex();
if (event.shiftKey && metaKey) {
this.onOptionSelectRange(event, optionIndex, this.startRangeIndex);
}
currentTarget.setSelectionRange(0, event.shiftKey ? len : 0); currentTarget.setSelectionRange(0, event.shiftKey ? len : 0);
this.focusedOptionIndex = -1; this.focusedOptionIndex = -1;
@ -631,6 +667,12 @@ export default {
onEndKey(event) { onEndKey(event) {
const { currentTarget } = event; const { currentTarget } = event;
const len = currentTarget.value.length; const len = currentTarget.value.length;
const metaKey = event.metaKey || event.ctrlKey;
const optionIndex = this.findLastOptionIndex();
if (event.shiftKey && metaKey) {
this.onOptionSelectRange(event, this.startRangeIndex, optionIndex);
}
currentTarget.setSelectionRange(event.shiftKey ? 0 : len, len); currentTarget.setSelectionRange(event.shiftKey ? 0 : len, len);
this.focusedOptionIndex = -1; this.focusedOptionIndex = -1;
@ -657,7 +699,12 @@ export default {
this.onArrowDownKey(event); this.onArrowDownKey(event);
} else { } else {
if (this.focusedOptionIndex !== -1) { if (this.focusedOptionIndex !== -1) {
this.onOptionSelect(event, this.visibleOptions[this.focusedOptionIndex]); if (event.shiftKey) {
this.onOptionSelectRange(event, this.focusedOptionIndex);
event.preventDefault();
} else {
this.onOptionSelect(event, this.visibleOptions[this.focusedOptionIndex]);
}
} }
this.hide(); this.hide();
@ -677,6 +724,9 @@ export default {
this.overlayVisible && this.hide(); this.overlayVisible && this.hide();
}, },
onShiftKey() {
this.startRangeIndex = this.focusedOptionIndex;
},
onBackspaceKey(event) { onBackspaceKey(event) {
if (this.multiple) { if (this.multiple) {
if (isNotEmpty(this.d_value) && !this.$refs.focusInput.value) { if (isNotEmpty(this.d_value) && !this.$refs.focusInput.value) {
@ -923,6 +973,31 @@ export default {
}, },
virtualScrollerRef(el) { virtualScrollerRef(el) {
this.virtualScroller = el; this.virtualScroller = el;
},
findNextSelectedOptionIndex(index) {
const matchedOptionIndex = this.$filled && index < this.visibleOptions.length - 1 ? this.visibleOptions.slice(index + 1).findIndex((option) => this.isValidSelectedOption(option)) : -1;
return matchedOptionIndex > -1 ? matchedOptionIndex + index + 1 : -1;
},
findPrevSelectedOptionIndex(index) {
const matchedOptionIndex = this.$filled && index > 0 ? findLastIndex(this.visibleOptions.slice(0, index), (option) => this.isValidSelectedOption(option)) : -1;
return matchedOptionIndex > -1 ? matchedOptionIndex : -1;
},
findNearestSelectedOptionIndex(index, firstCheckUp = false) {
let matchedOptionIndex = -1;
if (this.$filled) {
if (firstCheckUp) {
matchedOptionIndex = this.findPrevSelectedOptionIndex(index);
matchedOptionIndex = matchedOptionIndex === -1 ? this.findNextSelectedOptionIndex(index) : matchedOptionIndex;
} else {
matchedOptionIndex = this.findNextSelectedOptionIndex(index);
matchedOptionIndex = matchedOptionIndex === -1 ? this.findPrevSelectedOptionIndex(index) : matchedOptionIndex;
}
}
return matchedOptionIndex > -1 ? matchedOptionIndex : index;
} }
}, },
computed: { computed: {