Skip to content

Commit 18c5912

Browse files
authored
feat(ui5-multi-combobox): Add cut/copy & paste handling (#4854)
* Add cut/copy & paste handling * Handle the alternative cut/copy and paste shortcuts, add tests * Paste tokens when the focus is on another token * Handle paste of text if there is no token to be created * Merge with master
1 parent 6cb7dc3 commit 18c5912

File tree

3 files changed

+139
-0
lines changed

3 files changed

+139
-0
lines changed

packages/base/src/Keys.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ const isBackSpace = event => (event.key ? event.key === "Backspace" : event.keyC
171171

172172
const isDelete = event => (event.key ? event.key === "Delete" : event.keyCode === KeyCodes.DELETE) && !hasModifierKeys(event);
173173

174+
const isDeleteShift = event => (event.key ? event.key === "Delete" : event.keyCode === KeyCodes.DELETE) && checkModifierKeys(event, false, false, true);
175+
176+
const isInsertShift = event => (event.key ? event.key === "Insert" : event.keyCode === KeyCodes.DELETE) && checkModifierKeys(event, false, false, true);
177+
178+
const isInsertCtrl = event => (event.key ? event.key === "Insert" : event.keyCode === KeyCodes.DELETE) && checkModifierKeys(event, true, false, false);
179+
174180
const isPageUp = event => (event.key ? event.key === "PageUp" : event.keyCode === KeyCodes.PAGE_UP) && !hasModifierKeys(event);
175181

176182
const isPageDown = event => (event.key ? event.key === "PageDown" : event.keyCode === KeyCodes.PAGE_DOWN) && !hasModifierKeys(event);
@@ -221,6 +227,8 @@ const isShift = event => event.key === "Shift" || event.keyCode === KeyCodes.SHI
221227

222228
const isCtrlA = event => ((event.key === "A" || event.key === "a") || event.which === KeyCodes.A) && checkModifierKeys(event, true, false, false);
223229

230+
const isCtrlV = event => ((event.key === "V" || event.key === "v") || event.which === KeyCodes.V) && checkModifierKeys(event, true, false, false);
231+
224232
const hasModifierKeys = event => event.shiftKey || event.altKey || getCtrlKey(event);
225233

226234
const getCtrlKey = event => !!(event.metaKey || event.ctrlKey); // double negation doesn't have effect on boolean but ensures null and undefined are equivalent to false.
@@ -280,4 +288,8 @@ export {
280288
isPageDownShiftCtrl,
281289
isShift,
282290
isCtrlA,
291+
isCtrlV,
292+
isDeleteShift,
293+
isInsertShift,
294+
isInsertCtrl,
283295
};

packages/main/src/MultiComboBox.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import {
2323
isHomeCtrl,
2424
isEndCtrl,
2525
isCtrlA,
26+
isCtrlV,
27+
isDeleteShift,
28+
isInsertShift,
29+
isInsertCtrl,
2630
} from "@ui5/webcomponents-base/dist/Keys.js";
2731
import Integer from "@ui5/webcomponents-base/dist/types/Integer.js";
2832
import "@ui5/webcomponents-icons/dist/slim-arrow-down.js";
@@ -618,6 +622,11 @@ class MultiComboBox extends UI5Element {
618622
return;
619623
}
620624

625+
if (isCtrlV(event) || isInsertShift(event)) {
626+
this._handlePaste(event);
627+
return;
628+
}
629+
621630
if (isSpaceShift(event)) {
622631
event.preventDefault();
623632
}
@@ -626,6 +635,28 @@ class MultiComboBox extends UI5Element {
626635
this[`_handle${event.key}`] && this[`_handle${event.key}`](event);
627636
}
628637

638+
async _handlePaste(event) {
639+
const pastedText = await navigator.clipboard.readText();
640+
641+
if (!pastedText) {
642+
return;
643+
}
644+
645+
const separatedText = pastedText.split(/\r\n|\r|\n/g);
646+
const matchingItems = this.items.filter(item => separatedText.indexOf(item.text) > -1 && !item.selected);
647+
648+
if (matchingItems.length) {
649+
matchingItems.forEach(item => {
650+
item.selected = true;
651+
this.value = "";
652+
this.fireSelectionChange();
653+
});
654+
} else {
655+
this.value = pastedText;
656+
this.fireEvent("input");
657+
}
658+
}
659+
629660
_handleShow(event) {
630661
const items = this.items;
631662
const selectedItem = this._getSelectedItems()[0];
@@ -970,6 +1001,8 @@ class MultiComboBox extends UI5Element {
9701001
}
9711002

9721003
_onTokenizerKeydown(event) {
1004+
const isCtrl = !!(event.metaKey || event.ctrlKey);
1005+
9731006
if (isRight(event)) {
9741007
const lastTokenIndex = this._tokenizer.tokens.length - 1;
9751008

@@ -980,6 +1013,28 @@ class MultiComboBox extends UI5Element {
9801013
}
9811014
}
9821015

1016+
if ((isCtrl && ["c", "x"].includes(event.key.toLowerCase())) || isDeleteShift(event) || isInsertCtrl(event)) {
1017+
event.preventDefault();
1018+
1019+
const isCut = event.key.toLowerCase() === "x" || isDeleteShift(event);
1020+
const selectedTokens = this._tokenizer.tokens.filter(token => token.selected);
1021+
1022+
if (isCut) {
1023+
const cutResult = this._tokenizer._fillClipboard("cut", selectedTokens);
1024+
selectedTokens.forEach(token => {
1025+
this._tokenizer._tokenDelete(event, token);
1026+
});
1027+
1028+
this.focus();
1029+
return cutResult;
1030+
}
1031+
return this._tokenizer._fillClipboard("copy", selectedTokens);
1032+
}
1033+
1034+
if (isCtrlV(event) || isInsertShift(event)) {
1035+
this._handlePaste(event);
1036+
}
1037+
9831038
if (isHome(event)) {
9841039
this._handleHome(event);
9851040
}

packages/main/test/specs/MultiComboBox.spec.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,78 @@ describe("MultiComboBox general interaction", () => {
938938
assert.equal(await tokens.length, 0, "All selected filtered items are deselected");
939939
});
940940

941+
it ("should copy a token with CTRL+C and paste it with CTRL+V", async () => {
942+
await browser.url(`http://localhost:${PORT}/test-resources/pages/MultiComboBox.html`);
943+
944+
const mcb = await browser.$("#multi1");
945+
const mcb2 = await browser.$("#mcb");
946+
const input = await mcb2.shadow$("input");
947+
const tokens = await mcb.shadow$$(".ui5-multi-combobox-token");
948+
949+
await tokens[1].click();
950+
await tokens[1].keys(["Control", "c"]);
951+
await input.click();
952+
await input.keys(["Control", "v"]);
953+
954+
assert.equal(await mcb2.getProperty("value"), "Condensed", "Token is pasted into the second control");
955+
});
956+
957+
it ("should cut a token with CTRL+X and paste it with CTRL+V", async () => {
958+
await browser.url(`http://localhost:${PORT}/test-resources/pages/MultiComboBox.html`);
959+
960+
const mcb = await browser.$("#multi1");
961+
const mcb2 = await browser.$("#mcb");
962+
const input = await mcb2.shadow$("input");
963+
let tokens = await mcb.shadow$$(".ui5-multi-combobox-token");
964+
965+
await tokens[1].click();
966+
await tokens[1].keys(["Control", "x"]);
967+
968+
tokens = await mcb.shadow$$(".ui5-multi-combobox-token");
969+
assert.equal(await tokens.length, 2, "One of the tokens is cut from the control");
970+
971+
await input.click();
972+
await input.keys(["Control", "v"]);
973+
974+
assert.equal(await mcb2.getProperty("value"), "Condensed", "Token is pasted into the second control");
975+
});
976+
977+
it ("should cut a token with SHIFT+DELETE and paste it with SHIFT+INSERT", async () => {
978+
await browser.url(`http://localhost:${PORT}/test-resources/pages/MultiComboBox.html`);
979+
980+
const mcb = await browser.$("#multi1");
981+
const mcb2 = await browser.$("#mcb");
982+
const input = await mcb2.shadow$("input");
983+
let tokens = await mcb.shadow$$(".ui5-multi-combobox-token");
984+
985+
await tokens[1].click();
986+
await tokens[1].keys(["Shift", "Delete"]);
987+
988+
tokens = await mcb.shadow$$(".ui5-multi-combobox-token");
989+
assert.equal(await tokens.length, 2, "One of the tokens is cut from the control");
990+
991+
await input.click();
992+
await input.keys(["Shift", "Insert"]);
993+
994+
assert.equal(await mcb2.getProperty("value"), "Condensed", "Token is pasted into the second control");
995+
});
996+
997+
it ("should copy a token with CTRL+INSERT and paste it with SHIFT+INSERT", async () => {
998+
await browser.url(`http://localhost:${PORT}/test-resources/pages/MultiComboBox.html`);
999+
1000+
const mcb = await browser.$("#multi1");
1001+
const mcb2 = await browser.$("#mcb");
1002+
const input = await mcb2.shadow$("input");
1003+
const tokens = await mcb.shadow$$(".ui5-multi-combobox-token");
1004+
1005+
await tokens[1].click();
1006+
await tokens[1].keys(["Control", "Insert"]);
1007+
await input.click();
1008+
await input.keys(["Shift", "Insert"]);
1009+
1010+
assert.equal(await mcb2.getProperty("value"), "Condensed", "Token is pasted into the second control");
1011+
});
1012+
9411013
it ("should select а token with CTRL+SPACE", async () => {
9421014
await browser.url(`http://localhost:${PORT}/test-resources/pages/MultiComboBox.html`);
9431015

0 commit comments

Comments
 (0)