Skip to content

Commit

Permalink
[FIX] clipboard: Fix clipboard cross-browser coverage
Browse files Browse the repository at this point in the history
The current implementation of the cross-browser clipboard relies heavily
on an API that is currently only available on Chromium based browsers
(see [table](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#api.clipboarditem)).

There is currently no definitive announce that the feature will be
adopted by other browsers (namely FF and Safari) and since the feature
is still marked as experimental, it'd be better to rely on a more generic
approach.

This revision changes the flow to rely entirely on the `text/html`
mimetype as it is supported by all modern browsers.

closes #5241

Task: 4241877
X-original-commit: 23037c3
Signed-off-by: Adrien Minne (adrm) <[email protected]>
Signed-off-by: Rémi Rahir (rar) <[email protected]>
  • Loading branch information
rrahir committed Nov 20, 2024
1 parent c57f218 commit aabdd2e
Show file tree
Hide file tree
Showing 17 changed files with 187 additions and 164 deletions.
14 changes: 4 additions & 10 deletions src/actions/menu_items_actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CellPopoverStore } from "../components/popover";
import { DEFAULT_FIGURE_HEIGHT, DEFAULT_FIGURE_WIDTH } from "../constants";
import { parseOSClipboardContent } from "../helpers/clipboard/clipboard_helpers";
import {
getChartPositionAtCenterOfViewport,
getSmartChartDefinition,
Expand Down Expand Up @@ -54,20 +55,13 @@ async function paste(env: SpreadsheetChildEnv, pasteOption?: ClipboardPasteOptio
const osClipboard = await env.clipboard.read();
switch (osClipboard.status) {
case "ok":
const htmlDocument = new DOMParser().parseFromString(
osClipboard.content[ClipboardMIMEType.Html] ?? "<div></div>",
"text/html"
);
const osClipboardSpreadsheetContent =
osClipboard.content[ClipboardMIMEType.OSpreadsheet] || "{}";
const clipboardId =
JSON.parse(osClipboardSpreadsheetContent).clipboardId ??
htmlDocument.querySelector("div")?.getAttribute("data-clipboard-id");
const clipboardContent = parseOSClipboardContent(osClipboard.content);
const clipboardId = clipboardContent.data?.clipboardId;

const target = env.model.getters.getSelectedZones();

if (env.model.getters.getClipboardId() !== clipboardId) {
interactivePasteFromOS(env, target, osClipboard.content, pasteOption);
interactivePasteFromOS(env, target, clipboardContent, pasteOption);
} else {
interactivePaste(env, target, pasteOption);
}
Expand Down
2 changes: 1 addition & 1 deletion src/clipboard_handlers/abstract_clipboard_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class ClipboardHandler<T> {
return { zones: [], sheetId };
}

convertOSClipboardData(data: any): T | undefined {
convertTextToClipboardData(data: string): T | undefined {
return;
}
}
2 changes: 1 addition & 1 deletion src/clipboard_handlers/cell_clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ export class CellClipboardHandler extends AbstractCellClipboardHandler<
}
}

convertOSClipboardData(text: string): ClipboardContent {
convertTextToClipboardData(text: string): ClipboardContent {
const locale = this.getters.getLocale();
const copiedData: any = {
cells: [],
Expand Down
29 changes: 10 additions & 19 deletions src/components/grid/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
HEADER_WIDTH,
SCROLLBAR_WIDTH,
} from "../../constants";
import { parseOSClipboardContent } from "../../helpers/clipboard/clipboard_helpers";
import { isInside } from "../../helpers/index";
import { openLink } from "../../helpers/links";
import { isStaticTable } from "../../helpers/table_helpers";
Expand Down Expand Up @@ -628,32 +629,22 @@ export class Grid extends Component<Props, SpreadsheetChildEnv> {
if (!clipboardData) {
return;
}
const clipboardDataTextContent = clipboardData?.getData(ClipboardMIMEType.PlainText);
const clipboardDataHtmlContent = clipboardData?.getData(ClipboardMIMEType.Html);
const htmlDocument = new DOMParser().parseFromString(
clipboardDataHtmlContent ?? "<div></div>",
"text/html"
);
const osClipboardSpreadsheetContent =
clipboardData.getData(ClipboardMIMEType.OSpreadsheet) || "{}";

const osClipboard = {
content: {
[ClipboardMIMEType.PlainText]: clipboardData?.getData(ClipboardMIMEType.PlainText),
[ClipboardMIMEType.Html]: clipboardData?.getData(ClipboardMIMEType.Html),
},
};

const target = this.env.model.getters.getSelectedZones();
const isCutOperation = this.env.model.getters.isCutOperation();

const clipboardId =
JSON.parse(osClipboardSpreadsheetContent).clipboardId ??
htmlDocument.querySelector("div")?.getAttribute("data-clipboard-id");

const clipboardContent = parseOSClipboardContent(osClipboard.content);
const clipboardId = clipboardContent.data?.clipboardId;
if (this.env.model.getters.getClipboardId() === clipboardId) {
interactivePaste(this.env, target);
} else {
const clipboardContent = {
[ClipboardMIMEType.PlainText]: clipboardDataTextContent,
[ClipboardMIMEType.Html]: clipboardDataHtmlContent,
};
if (osClipboardSpreadsheetContent !== "{}") {
clipboardContent[ClipboardMIMEType.OSpreadsheet] = osClipboardSpreadsheetContent;
}
interactivePasteFromOS(this.env, target, clipboardContent);
}
if (isCutOperation) {
Expand Down
29 changes: 28 additions & 1 deletion src/helpers/clipboard/clipboard_helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { ClipboardCellData, UID, Zone } from "../../types";
import {
ClipboardCellData,
ClipboardMIMEType,
OSClipboardContent,
ParsedOSClipboardContent,
UID,
Zone,
} from "../../types";
import { mergeOverlappingZones, positions } from "../zones";

export function getClipboardDataPositions(sheetId: UID, zones: Zone[]): ClipboardCellData {
Expand Down Expand Up @@ -54,3 +61,23 @@ export function getPasteZones<T>(target: Zone[], content: T[][]): Zone[] {
height = content.length;
return target.map((t) => splitZoneForPaste(t, width, height)).flat();
}

export function parseOSClipboardContent(content: OSClipboardContent): ParsedOSClipboardContent {
if (!content[ClipboardMIMEType.Html]) {
return {
text: content[ClipboardMIMEType.PlainText],
};
}
const htmlDocument = new DOMParser().parseFromString(
content[ClipboardMIMEType.Html],
"text/html"
);
const oSheetClipboardData = htmlDocument
.querySelector("div")
?.getAttribute("data-osheet-clipboard");
const spreadsheetContent = oSheetClipboardData && JSON.parse(oSheetClipboardData);
return {
text: content[ClipboardMIMEType.PlainText],
data: spreadsheetContent,
};
}
21 changes: 7 additions & 14 deletions src/helpers/clipboard/navigator_clipboard_wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ClipboardContent, ClipboardMIMEType } from "./../../types/clipboard";
import { ClipboardMIMEType, OSClipboardContent } from "./../../types/clipboard";

export type ClipboardReadResult =
| { status: "ok"; content: ClipboardContent }
| { status: "ok"; content: OSClipboardContent }
| { status: "permissionDenied" | "notImplemented" };

export interface ClipboardInterface {
write(clipboardContent: ClipboardContent): Promise<void>;
write(clipboardContent: OSClipboardContent): Promise<void>;
writeText(text: string): Promise<void>;
read(): Promise<ClipboardReadResult>;
}
Expand All @@ -18,7 +18,7 @@ class WebClipboardWrapper implements ClipboardInterface {
// Can be undefined because navigator.clipboard doesn't exist in old browsers
constructor(private clipboard: Clipboard | undefined) {}

async write(clipboardContent: ClipboardContent): Promise<void> {
async write(clipboardContent: OSClipboardContent): Promise<void> {
if (this.clipboard?.write) {
try {
await this.clipboard?.write(this.getClipboardItems(clipboardContent));
Expand Down Expand Up @@ -60,7 +60,7 @@ class WebClipboardWrapper implements ClipboardInterface {
if (this.clipboard?.read) {
try {
const clipboardItems = await this.clipboard.read();
const clipboardContent: ClipboardContent = {};
const clipboardContent: OSClipboardContent = {};
for (const item of clipboardItems) {
for (const type of item.types) {
const blob = await item.getType(type);
Expand All @@ -82,22 +82,15 @@ class WebClipboardWrapper implements ClipboardInterface {
}
}

private getClipboardItems(content: ClipboardContent): ClipboardItems {
private getClipboardItems(content: OSClipboardContent): ClipboardItems {
const clipboardItemData = {
[ClipboardMIMEType.PlainText]: this.getBlob(content, ClipboardMIMEType.PlainText),
[ClipboardMIMEType.Html]: this.getBlob(content, ClipboardMIMEType.Html),
};
const spreadsheetData = content[ClipboardMIMEType.OSpreadsheet];
if (spreadsheetData) {
clipboardItemData[ClipboardMIMEType.OSpreadsheet] = this.getBlob(
content,
ClipboardMIMEType.OSpreadsheet
);
}
return [new ClipboardItem(clipboardItemData)];
}

private getBlob(clipboardContent: ClipboardContent, type: ClipboardMIMEType): Blob {
private getBlob(clipboardContent: OSClipboardContent, type: ClipboardMIMEType): Blob {
return new Blob([clipboardContent[type] || ""], {
type,
});
Expand Down
14 changes: 6 additions & 8 deletions src/helpers/ui/paste_interactive.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { CURRENT_VERSION } from "../../migrations/data";
import { _t } from "../../translation";
import {
ClipboardContent,
ClipboardMIMEType,
ClipboardPasteOptions,
CommandResult,
DispatchResult,
ParsedOSClipboardContent,
SpreadsheetChildEnv,
Zone,
} from "../../types";
Expand Down Expand Up @@ -45,7 +44,7 @@ export function interactivePaste(
export function interactivePasteFromOS(
env: SpreadsheetChildEnv,
target: Zone[],
clipboardContent: ClipboardContent,
clipboardContent: ParsedOSClipboardContent,
pasteOption?: ClipboardPasteOptions
) {
let result: DispatchResult;
Expand All @@ -59,10 +58,9 @@ export function interactivePasteFromOS(
pasteOption,
});
} catch (error) {
const parsedSpreadsheetContent = clipboardContent[ClipboardMIMEType.OSpreadsheet]
? JSON.parse(clipboardContent[ClipboardMIMEType.OSpreadsheet])
: {};
if (parsedSpreadsheetContent.version && parsedSpreadsheetContent.version !== CURRENT_VERSION) {
const parsedSpreadsheetContent = clipboardContent.data;

if (parsedSpreadsheetContent?.version !== CURRENT_VERSION) {
env.raiseError(
_t(
"An unexpected error occurred while pasting content.\
Expand All @@ -73,7 +71,7 @@ export function interactivePasteFromOS(
result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", {
target,
clipboardContent: {
[ClipboardMIMEType.PlainText]: clipboardContent[ClipboardMIMEType.PlainText],
text: clipboardContent.text,
},
pasteOption,
});
Expand Down
Loading

0 comments on commit aabdd2e

Please sign in to comment.