mirror of
https://github.com/nasa/openmct.git
synced 2025-01-01 10:56:41 +00:00
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 <ozyx@users.noreply.github.com> * fix changed path --------- Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
This commit is contained in:
parent
b18aa48141
commit
e91aba2e37
47
e2e/helper/hotkeys/clipboard.js
Normal file
47
e2e/helper/hotkeys/clipboard.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const isMac = process.platform === 'darwin';
|
||||||
|
const modifier = isMac ? 'Meta' : 'Control';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function selectAll(page) {
|
||||||
|
await page.keyboard.press(`${modifier}+KeyA`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function copy(page) {
|
||||||
|
await page.keyboard.press(`${modifier}+KeyC`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function paste(page) {
|
||||||
|
await page.keyboard.press(`${modifier}+KeyV`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { copy, paste, selectAll };
|
23
e2e/helper/hotkeys/hotkeys.js
Normal file
23
e2e/helper/hotkeys/hotkeys.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
export * from './clipboard.js';
|
@ -28,16 +28,28 @@ import { fileURLToPath } from 'url';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} text
|
||||||
*/
|
*/
|
||||||
async function enterTextEntry(page, text) {
|
async function enterTextEntry(page, text) {
|
||||||
// Click the 'Add Notebook Entry' area
|
await addNotebookEntry(page);
|
||||||
await page.locator(NOTEBOOK_DROP_AREA).click();
|
await enterTextInLastEntry(page, text);
|
||||||
|
|
||||||
// enter text
|
|
||||||
await page.getByLabel('Notebook Entry Input').last().fill(text);
|
|
||||||
await commitEntry(page);
|
await commitEntry(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function addNotebookEntry(page) {
|
||||||
|
await page.locator(NOTEBOOK_DROP_AREA).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function enterTextInLastEntry(page, text) {
|
||||||
|
await page.getByLabel('Notebook Entry Input').last().fill(text);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
@ -140,10 +152,13 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
addNotebookEntry,
|
||||||
|
commitEntry,
|
||||||
createNotebookAndEntry,
|
createNotebookAndEntry,
|
||||||
createNotebookEntryAndTags,
|
createNotebookEntryAndTags,
|
||||||
dragAndDropEmbed,
|
dragAndDropEmbed,
|
||||||
enterTextEntry,
|
enterTextEntry,
|
||||||
|
enterTextInLastEntry,
|
||||||
lockPage,
|
lockPage,
|
||||||
startAndAddRestrictedNotebookObject
|
startAndAddRestrictedNotebookObject
|
||||||
};
|
};
|
||||||
|
@ -27,6 +27,7 @@ This test suite is dedicated to tests which verify the basic operations surround
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||||
|
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
|
||||||
import * as nbUtils from '../../../../helper/notebookUtils.js';
|
import * as nbUtils from '../../../../helper/notebookUtils.js';
|
||||||
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
|
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
@ -546,4 +547,53 @@ test.describe('Notebook entry tests', () => {
|
|||||||
);
|
);
|
||||||
await expect(secondLineOfBlockquoteText).toBeVisible();
|
await expect(secondLineOfBlockquoteText).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paste into notebook entry tests
|
||||||
|
*/
|
||||||
|
test('Can paste text into a notebook entry', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7686'
|
||||||
|
});
|
||||||
|
const TEST_TEXT = 'This is a test';
|
||||||
|
const iterations = 20;
|
||||||
|
const EXPECTED_TEXT = TEST_TEXT.repeat(iterations);
|
||||||
|
|
||||||
|
await page.goto(notebookObject.url);
|
||||||
|
|
||||||
|
await nbUtils.addNotebookEntry(page);
|
||||||
|
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
|
||||||
|
await selectAll(page);
|
||||||
|
await copy(page);
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
await paste(page);
|
||||||
|
}
|
||||||
|
await nbUtils.commitEntry(page);
|
||||||
|
|
||||||
|
await expect(page.locator(`text="${EXPECTED_TEXT}"`)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Prevents pasting text into selected notebook entry if not editing', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7686'
|
||||||
|
});
|
||||||
|
const TEST_TEXT = 'This is a test';
|
||||||
|
|
||||||
|
await page.goto(notebookObject.url);
|
||||||
|
|
||||||
|
await nbUtils.addNotebookEntry(page);
|
||||||
|
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
|
||||||
|
await selectAll(page);
|
||||||
|
await copy(page);
|
||||||
|
await paste(page);
|
||||||
|
await nbUtils.commitEntry(page);
|
||||||
|
|
||||||
|
// This should not paste text into the entry
|
||||||
|
await paste(page);
|
||||||
|
|
||||||
|
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
|
||||||
|
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
@drop.capture="cancelEditMode"
|
@drop.capture="cancelEditMode"
|
||||||
@drop.prevent="dropOnEntry"
|
@drop.prevent="dropOnEntry"
|
||||||
@click="selectAndEmitEntry($event, entry)"
|
@click="selectAndEmitEntry($event, entry)"
|
||||||
@paste="addImageFromPaste"
|
@paste="handlePaste"
|
||||||
>
|
>
|
||||||
<div class="c-ne__time-and-content">
|
<div class="c-ne__time-and-content">
|
||||||
<div class="c-ne__time-and-creator-and-delete">
|
<div class="c-ne__time-and-creator-and-delete">
|
||||||
@ -368,6 +368,28 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handlePaste(event) {
|
||||||
|
const clipboardItems = Array.from(
|
||||||
|
(event.clipboardData || event.originalEvent.clipboardData).items
|
||||||
|
);
|
||||||
|
const hasClipboardText = clipboardItems.some(
|
||||||
|
(clipboardItem) => clipboardItem.kind === 'string'
|
||||||
|
);
|
||||||
|
const clipboardImages = clipboardItems.filter(
|
||||||
|
(clipboardItem) => clipboardItem.kind === 'file' && clipboardItem.type.includes('image')
|
||||||
|
);
|
||||||
|
const hasClipboardImages = clipboardImages?.length > 0;
|
||||||
|
|
||||||
|
if (hasClipboardImages) {
|
||||||
|
if (hasClipboardText) {
|
||||||
|
console.warn('Image and text kinds found in paste. Only processing images.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addImageFromPaste(clipboardImages, event);
|
||||||
|
} else if (hasClipboardText) {
|
||||||
|
this.addTextFromPaste(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
async addNewEmbed(objectPath) {
|
async addNewEmbed(objectPath) {
|
||||||
const bounds = this.openmct.time.bounds();
|
const bounds = this.openmct.time.bounds();
|
||||||
const snapshotMeta = {
|
const snapshotMeta = {
|
||||||
@ -384,32 +406,34 @@ export default {
|
|||||||
|
|
||||||
this.manageEmbedLayout();
|
this.manageEmbedLayout();
|
||||||
},
|
},
|
||||||
async addImageFromPaste(event) {
|
addTextFromPaste(event) {
|
||||||
const clipboardItems = Array.from(
|
if (!this.editMode) {
|
||||||
(event.clipboardData || event.originalEvent.clipboardData).items
|
|
||||||
);
|
|
||||||
const hasImage = clipboardItems.some(
|
|
||||||
(clipboardItem) => clipboardItem.type.includes('image') && clipboardItem.kind === 'file'
|
|
||||||
);
|
|
||||||
// If the clipboard contained an image, prevent the paste event from reaching the textarea.
|
|
||||||
if (hasImage) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async addImageFromPaste(clipboardImages, event) {
|
||||||
|
event?.preventDefault();
|
||||||
|
let updated = false;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from(clipboardItems).map(async (clipboardItem) => {
|
Array.from(clipboardImages).map(async (clipboardImage) => {
|
||||||
const isImage = clipboardItem.type.includes('image') && clipboardItem.kind === 'file';
|
const imageFile = clipboardImage.getAsFile();
|
||||||
if (isImage) {
|
|
||||||
const imageFile = clipboardItem.getAsFile();
|
|
||||||
const imageEmbed = await createNewImageEmbed(imageFile, this.openmct, imageFile?.name);
|
const imageEmbed = await createNewImageEmbed(imageFile, this.openmct, imageFile?.name);
|
||||||
|
|
||||||
if (!this.entry.embeds) {
|
if (!this.entry.embeds) {
|
||||||
this.entry.embeds = [];
|
this.entry.embeds = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.entry.embeds.push(imageEmbed);
|
this.entry.embeds.push(imageEmbed);
|
||||||
}
|
|
||||||
|
updated = true;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
this.manageEmbedLayout();
|
this.manageEmbedLayout();
|
||||||
this.timestampAndUpdate();
|
this.timestampAndUpdate();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
convertMarkDownToHtml(text = '') {
|
convertMarkDownToHtml(text = '') {
|
||||||
let markDownHtml = this.marked.parse(text, {
|
let markDownHtml = this.marked.parse(text, {
|
||||||
|
Loading…
Reference in New Issue
Block a user