Skip to content

Commit e91aba2

Browse files
davetsayozyx
andauthored
Handle paste events for images and text properly (#7679)
* enable eval source maps for debugging * split image and text paste handling better event handling * change back source maps * image takes precedence over text * break up notebook entry functions for re-use * create hotkeys utils add clipboard functions * add notebook paste test * add test for pasting to selected but not editing entry * link tests to issue * jsdoc addition * jsdocs * no need to import then export Co-authored-by: Jesse Mazzella <[email protected]> * fix changed path --------- Co-authored-by: Jesse Mazzella <[email protected]>
1 parent b18aa48 commit e91aba2

File tree

5 files changed

+183
-24
lines changed

5 files changed

+183
-24
lines changed

e2e/helper/hotkeys/clipboard.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*****************************************************************************
2+
* Open MCT, Copyright (c) 2014-2024, United States Government
3+
* as represented by the Administrator of the National Aeronautics and Space
4+
* Administration. All rights reserved.
5+
*
6+
* Open MCT is licensed under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations
15+
* under the License.
16+
*
17+
* Open MCT includes source code licensed under additional open source
18+
* licenses. See the Open Source Licenses file (LICENSES.md) included with
19+
* this source code distribution or the Licensing information page available
20+
* at runtime from the About dialog for additional information.
21+
*****************************************************************************/
22+
23+
const isMac = process.platform === 'darwin';
24+
const modifier = isMac ? 'Meta' : 'Control';
25+
26+
/**
27+
* @param {import('@playwright/test').Page} page
28+
*/
29+
async function selectAll(page) {
30+
await page.keyboard.press(`${modifier}+KeyA`);
31+
}
32+
33+
/**
34+
* @param {import('@playwright/test').Page} page
35+
*/
36+
async function copy(page) {
37+
await page.keyboard.press(`${modifier}+KeyC`);
38+
}
39+
40+
/**
41+
* @param {import('@playwright/test').Page} page
42+
*/
43+
async function paste(page) {
44+
await page.keyboard.press(`${modifier}+KeyV`);
45+
}
46+
47+
export { copy, paste, selectAll };

e2e/helper/hotkeys/hotkeys.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*****************************************************************************
2+
* Open MCT, Copyright (c) 2014-2024, United States Government
3+
* as represented by the Administrator of the National Aeronautics and Space
4+
* Administration. All rights reserved.
5+
*
6+
* Open MCT is licensed under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations
15+
* under the License.
16+
*
17+
* Open MCT includes source code licensed under additional open source
18+
* licenses. See the Open Source Licenses file (LICENSES.md) included with
19+
* this source code distribution or the Licensing information page available
20+
* at runtime from the About dialog for additional information.
21+
*****************************************************************************/
22+
23+
export * from './clipboard.js';

e2e/helper/notebookUtils.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,26 @@ import { fileURLToPath } from 'url';
2828

2929
/**
3030
* @param {import('@playwright/test').Page} page
31+
* @param {string} text
3132
*/
3233
async function enterTextEntry(page, text) {
33-
// Click the 'Add Notebook Entry' area
34+
await addNotebookEntry(page);
35+
await enterTextInLastEntry(page, text);
36+
await commitEntry(page);
37+
}
38+
39+
/**
40+
* @param {import('@playwright/test').Page} page
41+
*/
42+
async function addNotebookEntry(page) {
3443
await page.locator(NOTEBOOK_DROP_AREA).click();
44+
}
3545

36-
// enter text
46+
/**
47+
* @param {import('@playwright/test').Page} page
48+
*/
49+
async function enterTextInLastEntry(page, text) {
3750
await page.getByLabel('Notebook Entry Input').last().fill(text);
38-
await commitEntry(page);
3951
}
4052

4153
/**
@@ -140,10 +152,13 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
140152
}
141153

142154
export {
155+
addNotebookEntry,
156+
commitEntry,
143157
createNotebookAndEntry,
144158
createNotebookEntryAndTags,
145159
dragAndDropEmbed,
146160
enterTextEntry,
161+
enterTextInLastEntry,
147162
lockPage,
148163
startAndAddRestrictedNotebookObject
149164
};

e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js

+50
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ This test suite is dedicated to tests which verify the basic operations surround
2727
import { fileURLToPath } from 'url';
2828

2929
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
30+
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
3031
import * as nbUtils from '../../../../helper/notebookUtils.js';
3132
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
3233

@@ -546,4 +547,53 @@ test.describe('Notebook entry tests', () => {
546547
);
547548
await expect(secondLineOfBlockquoteText).toBeVisible();
548549
});
550+
551+
/**
552+
* Paste into notebook entry tests
553+
*/
554+
test('Can paste text into a notebook entry', async ({ page }) => {
555+
test.info().annotations.push({
556+
type: 'issue',
557+
description: 'https://github.com/nasa/openmct/issues/7686'
558+
});
559+
const TEST_TEXT = 'This is a test';
560+
const iterations = 20;
561+
const EXPECTED_TEXT = TEST_TEXT.repeat(iterations);
562+
563+
await page.goto(notebookObject.url);
564+
565+
await nbUtils.addNotebookEntry(page);
566+
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
567+
await selectAll(page);
568+
await copy(page);
569+
for (let i = 0; i < iterations; i++) {
570+
await paste(page);
571+
}
572+
await nbUtils.commitEntry(page);
573+
574+
await expect(page.locator(`text="${EXPECTED_TEXT}"`)).toBeVisible();
575+
});
576+
577+
test('Prevents pasting text into selected notebook entry if not editing', async ({ page }) => {
578+
test.info().annotations.push({
579+
type: 'issue',
580+
description: 'https://github.com/nasa/openmct/issues/7686'
581+
});
582+
const TEST_TEXT = 'This is a test';
583+
584+
await page.goto(notebookObject.url);
585+
586+
await nbUtils.addNotebookEntry(page);
587+
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
588+
await selectAll(page);
589+
await copy(page);
590+
await paste(page);
591+
await nbUtils.commitEntry(page);
592+
593+
// This should not paste text into the entry
594+
await paste(page);
595+
596+
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
597+
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
598+
});
549599
});

src/plugins/notebook/components/NotebookEntry.vue

+45-21
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
@drop.capture="cancelEditMode"
3232
@drop.prevent="dropOnEntry"
3333
@click="selectAndEmitEntry($event, entry)"
34-
@paste="addImageFromPaste"
34+
@paste="handlePaste"
3535
>
3636
<div class="c-ne__time-and-content">
3737
<div class="c-ne__time-and-creator-and-delete">
@@ -368,6 +368,28 @@ export default {
368368
}
369369
},
370370
methods: {
371+
handlePaste(event) {
372+
const clipboardItems = Array.from(
373+
(event.clipboardData || event.originalEvent.clipboardData).items
374+
);
375+
const hasClipboardText = clipboardItems.some(
376+
(clipboardItem) => clipboardItem.kind === 'string'
377+
);
378+
const clipboardImages = clipboardItems.filter(
379+
(clipboardItem) => clipboardItem.kind === 'file' && clipboardItem.type.includes('image')
380+
);
381+
const hasClipboardImages = clipboardImages?.length > 0;
382+
383+
if (hasClipboardImages) {
384+
if (hasClipboardText) {
385+
console.warn('Image and text kinds found in paste. Only processing images.');
386+
}
387+
388+
this.addImageFromPaste(clipboardImages, event);
389+
} else if (hasClipboardText) {
390+
this.addTextFromPaste(event);
391+
}
392+
},
371393
async addNewEmbed(objectPath) {
372394
const bounds = this.openmct.time.bounds();
373395
const snapshotMeta = {
@@ -384,32 +406,34 @@ export default {
384406

385407
this.manageEmbedLayout();
386408
},
387-
async addImageFromPaste(event) {
388-
const clipboardItems = Array.from(
389-
(event.clipboardData || event.originalEvent.clipboardData).items
390-
);
391-
const hasImage = clipboardItems.some(
392-
(clipboardItem) => clipboardItem.type.includes('image') && clipboardItem.kind === 'file'
393-
);
394-
// If the clipboard contained an image, prevent the paste event from reaching the textarea.
395-
if (hasImage) {
409+
addTextFromPaste(event) {
410+
if (!this.editMode) {
396411
event.preventDefault();
397412
}
413+
},
414+
async addImageFromPaste(clipboardImages, event) {
415+
event?.preventDefault();
416+
let updated = false;
417+
398418
await Promise.all(
399-
Array.from(clipboardItems).map(async (clipboardItem) => {
400-
const isImage = clipboardItem.type.includes('image') && clipboardItem.kind === 'file';
401-
if (isImage) {
402-
const imageFile = clipboardItem.getAsFile();
403-
const imageEmbed = await createNewImageEmbed(imageFile, this.openmct, imageFile?.name);
404-
if (!this.entry.embeds) {
405-
this.entry.embeds = [];
406-
}
407-
this.entry.embeds.push(imageEmbed);
419+
Array.from(clipboardImages).map(async (clipboardImage) => {
420+
const imageFile = clipboardImage.getAsFile();
421+
const imageEmbed = await createNewImageEmbed(imageFile, this.openmct, imageFile?.name);
422+
423+
if (!this.entry.embeds) {
424+
this.entry.embeds = [];
408425
}
426+
427+
this.entry.embeds.push(imageEmbed);
428+
429+
updated = true;
409430
})
410431
);
411-
this.manageEmbedLayout();
412-
this.timestampAndUpdate();
432+
433+
if (updated) {
434+
this.manageEmbedLayout();
435+
this.timestampAndUpdate();
436+
}
413437
},
414438
convertMarkDownToHtml(text = '') {
415439
let markDownHtml = this.marked.parse(text, {

0 commit comments

Comments
 (0)