diff --git a/src/actions/menu_items_actions.ts b/src/actions/menu_items_actions.ts
index 49dd76a1f6..b7cc62e32a 100644
--- a/src/actions/menu_items_actions.ts
+++ b/src/actions/menu_items_actions.ts
@@ -51,13 +51,22 @@ export const PASTE_ACTION = async (env: SpreadsheetChildEnv) => paste(env);
export const PASTE_AS_VALUE_ACTION = async (env: SpreadsheetChildEnv) => paste(env, "asValue");
async function paste(env: SpreadsheetChildEnv, pasteOption?: ClipboardPasteOptions) {
- const spreadsheetClipboard = env.model.getters.getClipboardTextContent();
- const osClipboard = await env.clipboard.readText();
-
+ const osClipboard = await env.clipboard.read();
switch (osClipboard.status) {
case "ok":
+ const htmlDocument = new DOMParser().parseFromString(
+ osClipboard.content[ClipboardMIMEType.Html] ?? "
",
+ "text/xml"
+ );
+ const osClipboardSpreadsheetContent =
+ osClipboard.content[ClipboardMIMEType.OSpreadsheet] || "{}";
+ const clipboardId =
+ JSON.parse(osClipboardSpreadsheetContent).clipboardId ??
+ htmlDocument.querySelector("div")?.getAttribute("data-clipboard-id");
+
const target = env.model.getters.getSelectedZones();
- if (osClipboard && osClipboard.content !== spreadsheetClipboard) {
+
+ if (env.model.getters.getClipboardId() !== clipboardId) {
interactivePasteFromOS(env, target, osClipboard.content, pasteOption);
} else {
interactivePaste(env, target, pasteOption);
diff --git a/src/clipboard_handlers/cell_clipboard.ts b/src/clipboard_handlers/cell_clipboard.ts
index 3c686aa8dd..8f7ce83141 100644
--- a/src/clipboard_handlers/cell_clipboard.ts
+++ b/src/clipboard_handlers/cell_clipboard.ts
@@ -78,7 +78,12 @@ export class CellClipboardHandler extends AbstractCellClipboardHandler<
}
}
cellsInRow.push({
- cell,
+ content: cell?.content ?? "",
+ style: cell?.style,
+ format: cell?.format,
+ tokens: cell?.isFormula
+ ? cell.compiledFormula.tokens.map(({ value, type }) => ({ value, type }))
+ : [],
border: this.getters.getCellBorder(position) || undefined,
evaluatedCell,
position,
@@ -194,7 +199,7 @@ export class CellClipboardHandler extends AbstractCellClipboardHandler<
private clearClippedZones(content: ClipboardContent) {
for (const row of content.cells) {
for (const cell of row) {
- if (cell.cell) {
+ if (cell.position) {
this.dispatch("CLEAR_CELL", cell.position);
}
}
@@ -234,7 +239,7 @@ export class CellClipboardHandler extends AbstractCellClipboardHandler<
) {
const { sheetId, col, row } = target;
const targetCell = this.getters.getEvaluatedCell(target);
- const originFormat = origin.cell?.format ?? origin.evaluatedCell.format;
+ const originFormat = origin?.format ?? origin.evaluatedCell.format;
if (clipboardOption?.pasteOption === "asValue") {
const locale = this.getters.getLocale();
@@ -246,29 +251,33 @@ export class CellClipboardHandler extends AbstractCellClipboardHandler<
if (clipboardOption?.pasteOption === "onlyFormat") {
this.dispatch("UPDATE_CELL", {
...target,
- style: origin.cell?.style ?? null,
+ style: origin?.style ?? null,
format: originFormat ?? targetCell.format,
});
return;
}
- let content = origin.cell?.content;
- if (origin.cell?.isFormula && !clipboardOption?.isCutOperation) {
+ let content = origin?.content;
+ if (origin?.tokens && origin.tokens.length > 0 && !clipboardOption?.isCutOperation) {
content = this.getters.getTranslatedCellFormula(
sheetId,
col - origin.position.col,
row - origin.position.row,
- origin.cell.compiledFormula
+ origin.tokens
+ );
+ } else if (origin?.tokens && origin.tokens.length > 0) {
+ content = this.getters.getFormulaMovedInSheet(
+ origin.position.sheetId,
+ sheetId,
+ origin.tokens
);
- } else if (origin.cell?.isFormula) {
- content = this.getters.getFormulaMovedInSheet(sheetId, origin.cell.compiledFormula);
}
- if (content !== "" || origin.cell?.format || origin.cell?.style) {
+ if (content !== "" || origin?.format || origin?.style) {
this.dispatch("UPDATE_CELL", {
...target,
content,
- style: origin.cell?.style || null,
- format: origin.cell?.format,
+ style: origin?.style || null,
+ format: origin?.format,
});
} else if (targetCell) {
this.dispatch("CLEAR_CELL", target);
@@ -293,10 +302,7 @@ export class CellClipboardHandler extends AbstractCellClipboardHandler<
for (let i = 0; i < rowLength; i++) {
const content = canonicalizeNumberValue(row[i] || "", locale);
cells.push({
- cell: {
- isFormula: false,
- content,
- },
+ content: content,
evaluatedCell: {
formattedValue: content,
},
diff --git a/src/clipboard_handlers/tables_clipboard.ts b/src/clipboard_handlers/tables_clipboard.ts
index dade0b090d..228a735143 100644
--- a/src/clipboard_handlers/tables_clipboard.ts
+++ b/src/clipboard_handlers/tables_clipboard.ts
@@ -7,7 +7,7 @@ import {
ClipboardPasteTarget,
CoreTableType,
HeaderIndex,
- Range,
+ RangeData,
Style,
TableConfig,
UID,
@@ -21,7 +21,7 @@ interface TableStyle {
}
interface CopiedTable {
- range: Range;
+ range: RangeData;
config: TableConfig;
type: CoreTableType;
}
@@ -68,7 +68,7 @@ export class TableClipboardHandler extends AbstractCellClipboardHandler<
}
tableCellsInRow.push({
table: {
- range: coreTable.range,
+ range: coreTable.range.rangeData,
config: coreTable.config,
type: coreTable.type,
},
@@ -125,7 +125,7 @@ export class TableClipboardHandler extends AbstractCellClipboardHandler<
if (tableCell.table) {
this.dispatch("REMOVE_TABLE", {
sheetId: content.sheetId,
- target: [tableCell.table.range.zone],
+ target: [this.getters.getRangeFromRangeData(tableCell.table.range).zone],
});
}
}
@@ -168,7 +168,7 @@ export class TableClipboardHandler extends AbstractCellClipboardHandler<
) {
if (tableCell.table && !options?.pasteOption) {
const { range: tableRange } = tableCell.table;
- const zoneDims = zoneToDimension(tableRange.zone);
+ const zoneDims = zoneToDimension(this.getters.getRangeFromRangeData(tableRange).zone);
const newTableZone = {
left: position.col,
top: position.row,
diff --git a/src/components/grid/grid.ts b/src/components/grid/grid.ts
index 422cf0e0c7..0a92d826c3 100644
--- a/src/components/grid/grid.ts
+++ b/src/components/grid/grid.ts
@@ -37,7 +37,6 @@ import { Store, useStore } from "../../store_engine";
import { DOMFocusableElementStore } from "../../stores/DOM_focus_store";
import { ArrayFormulaHighlight } from "../../stores/array_formula_highlight";
import { HighlightStore } from "../../stores/highlight_store";
-import { _t } from "../../translation";
import {
Align,
CellValueType,
@@ -595,17 +594,11 @@ export class Grid extends Component {
this.menuState.menuItems = registries[type].getMenuItems();
}
- copy(cut: boolean, ev: ClipboardEvent) {
+ async copy(cut: boolean, ev: ClipboardEvent) {
if (!this.gridEl.contains(document.activeElement)) {
return;
}
- const clipboardData = ev.clipboardData;
- if (!clipboardData) {
- this.displayWarningCopyPasteNotSupported();
- return;
- }
-
/* If we are currently editing a cell, let the default behavior */
if (this.composerFocusStore.activeComposer.editionMode !== "inactive") {
return;
@@ -616,9 +609,7 @@ export class Grid extends Component {
this.env.model.dispatch("COPY");
}
const content = this.env.model.getters.getClipboardContent();
- for (const type in content) {
- clipboardData.setData(type, content[type]);
- }
+ await this.env.clipboard.write(content);
ev.preventDefault();
}
@@ -627,32 +618,33 @@ export class Grid extends Component {
return;
}
- const clipboardData = ev.clipboardData;
- if (!clipboardData) {
- this.displayWarningCopyPasteNotSupported();
+ ev.preventDefault();
+
+ const clipboard = await this.env.clipboard.read();
+ if (clipboard.status !== "ok") {
return;
}
+ const htmlDocument = new DOMParser().parseFromString(
+ clipboard.content[ClipboardMIMEType.Html] ?? "",
+ "text/xml"
+ );
+ const osClipboardSpreadsheetContent = clipboard.content[ClipboardMIMEType.OSpreadsheet] || "{}";
- if (clipboardData.types.indexOf(ClipboardMIMEType.PlainText) > -1) {
- ev.preventDefault();
- const content = clipboardData.getData(ClipboardMIMEType.PlainText);
- const target = this.env.model.getters.getSelectedZones();
- const clipboardString = this.env.model.getters.getClipboardTextContent();
- const isCutOperation = this.env.model.getters.isCutOperation();
- if (clipboardString === content) {
- // the paste actually comes from o-spreadsheet itself
- interactivePaste(this.env, target);
- } else {
- interactivePasteFromOS(this.env, target, content);
- }
- if (isCutOperation) {
- await this.env.clipboard.write({ [ClipboardMIMEType.PlainText]: "" });
- }
- }
- }
+ const target = this.env.model.getters.getSelectedZones();
+ const isCutOperation = this.env.model.getters.isCutOperation();
- private displayWarningCopyPasteNotSupported() {
- this.env.raiseError(_t("Copy/Paste is not supported in this browser."));
+ const clipboardId =
+ JSON.parse(osClipboardSpreadsheetContent).clipboardId ??
+ htmlDocument.querySelector("div")?.getAttribute("data-clipboard-id");
+
+ if (this.env.model.getters.getClipboardId() === clipboardId) {
+ interactivePaste(this.env, target);
+ } else {
+ interactivePasteFromOS(this.env, target, clipboard.content);
+ }
+ if (isCutOperation) {
+ await this.env.clipboard.write({ [ClipboardMIMEType.PlainText]: "" });
+ }
}
private clearFormatting() {
diff --git a/src/helpers/clipboard/navigator_clipboard_wrapper.ts b/src/helpers/clipboard/navigator_clipboard_wrapper.ts
index a12a117861..7f4e389ae3 100644
--- a/src/helpers/clipboard/navigator_clipboard_wrapper.ts
+++ b/src/helpers/clipboard/navigator_clipboard_wrapper.ts
@@ -1,13 +1,13 @@
import { ClipboardContent, ClipboardMIMEType } from "./../../types/clipboard";
export type ClipboardReadResult =
- | { status: "ok"; content: string }
+ | { status: "ok"; content: ClipboardContent }
| { status: "permissionDenied" | "notImplemented" };
export interface ClipboardInterface {
write(clipboardContent: ClipboardContent): Promise;
writeText(text: string): Promise;
- readText(): Promise;
+ read(): Promise;
}
export function instantiateClipboard(): ClipboardInterface {
@@ -19,9 +19,29 @@ class WebClipboardWrapper implements ClipboardInterface {
constructor(private clipboard: Clipboard | undefined) {}
async write(clipboardContent: ClipboardContent): Promise {
- try {
- this.clipboard?.write(this.getClipboardItems(clipboardContent));
- } catch (e) {}
+ if (this.clipboard?.write) {
+ try {
+ await this.clipboard?.write(this.getClipboardItems(clipboardContent));
+ } catch (e) {
+ /**
+ * Some browsers (e.g firefox, safari) do not support writing
+ * custom mimetypes in the clipboard. Therefore, we try to catch
+ * any errors and fallback on writing only standard mimetypes to
+ * prevent the whole copy action from crashing.
+ */
+ await this.clipboard?.write([
+ new ClipboardItem({
+ [ClipboardMIMEType.PlainText]: this.getBlob(
+ clipboardContent,
+ ClipboardMIMEType.PlainText
+ ),
+ [ClipboardMIMEType.Html]: this.getBlob(clipboardContent, ClipboardMIMEType.Html),
+ }),
+ ]);
+ }
+ } else {
+ await this.writeText(clipboardContent[ClipboardMIMEType.PlainText] ?? "");
+ }
}
async writeText(text: string): Promise {
@@ -30,18 +50,35 @@ class WebClipboardWrapper implements ClipboardInterface {
} catch (e) {}
}
- async readText(): Promise {
+ async read(): Promise {
let permissionResult: PermissionStatus | undefined = undefined;
try {
//@ts-ignore - clipboard-read is not implemented in all browsers
permissionResult = await navigator.permissions.query({ name: "clipboard-read" });
} catch (e) {}
- try {
- const clipboardContent = await this.clipboard!.readText();
- return { status: "ok", content: clipboardContent };
- } catch (e) {
- const status = permissionResult?.state === "denied" ? "permissionDenied" : "notImplemented";
- return { status };
+ if (this.clipboard?.read) {
+ try {
+ const clipboardItems = await this.clipboard.read();
+ const clipboardContent: ClipboardContent = {};
+ for (const item of clipboardItems) {
+ for (const type of item.types) {
+ const blob = await item.getType(type);
+ const text = await blob.text();
+ clipboardContent[type as ClipboardMIMEType] = text;
+ }
+ }
+ return { status: "ok", content: clipboardContent };
+ } catch (e) {
+ const status = permissionResult?.state === "denied" ? "permissionDenied" : "notImplemented";
+ return { status };
+ }
+ } else {
+ return {
+ status: "ok",
+ content: {
+ [ClipboardMIMEType.PlainText]: await this.clipboard?.readText(),
+ },
+ };
}
}
@@ -50,6 +87,7 @@ class WebClipboardWrapper implements ClipboardInterface {
new ClipboardItem({
[ClipboardMIMEType.PlainText]: this.getBlob(content, ClipboardMIMEType.PlainText),
[ClipboardMIMEType.Html]: this.getBlob(content, ClipboardMIMEType.Html),
+ [ClipboardMIMEType.OSpreadsheet]: this.getBlob(content, ClipboardMIMEType.OSpreadsheet),
}),
];
}
diff --git a/src/helpers/ui/paste_interactive.ts b/src/helpers/ui/paste_interactive.ts
index c5bbe8231d..3eb5d97d07 100644
--- a/src/helpers/ui/paste_interactive.ts
+++ b/src/helpers/ui/paste_interactive.ts
@@ -1,5 +1,8 @@
+import { CURRENT_VERSION } from "../../migrations/data";
import { _t } from "../../translation";
import {
+ ClipboardContent,
+ ClipboardMIMEType,
ClipboardPasteOptions,
CommandResult,
DispatchResult,
@@ -42,9 +45,38 @@ export function interactivePaste(
export function interactivePasteFromOS(
env: SpreadsheetChildEnv,
target: Zone[],
- text: string,
+ clipboardContent: ClipboardContent,
pasteOption?: ClipboardPasteOptions
) {
- const result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", { target, text, pasteOption });
+ let result: DispatchResult;
+ // We do not trust the clipboard content to be accurate and comprehensive.
+ // Therefore, to ensure reliability, we handle unexpected errors that may
+ // arise from content that would not be suitable for the current version.
+ try {
+ result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", {
+ target,
+ clipboardContent,
+ pasteOption,
+ });
+ } catch (error) {
+ const parsedSpreadsheetContent = clipboardContent[ClipboardMIMEType.OSpreadsheet]
+ ? JSON.parse(clipboardContent[ClipboardMIMEType.OSpreadsheet])
+ : {};
+ if (parsedSpreadsheetContent.version && parsedSpreadsheetContent.version !== CURRENT_VERSION) {
+ env.raiseError(
+ _t(
+ "An unexpected error occurred while pasting content.\
+ This is probably due to a spreadsheet version mismatch."
+ )
+ );
+ }
+ result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", {
+ target,
+ clipboardContent: {
+ [ClipboardMIMEType.PlainText]: clipboardContent[ClipboardMIMEType.PlainText],
+ },
+ pasteOption,
+ });
+ }
handlePasteResult(env, result);
}
diff --git a/src/plugins/core/cell.ts b/src/plugins/core/cell.ts
index 1f6437e785..19dc76ea9e 100644
--- a/src/plugins/core/cell.ts
+++ b/src/plugins/core/cell.ts
@@ -1,5 +1,6 @@
import { DEFAULT_STYLE } from "../../constants";
import { Token, compile } from "../../formulas";
+import { compileTokens } from "../../formulas/compiler";
import { isEvaluationError, toString } from "../../functions/helpers";
import { deepEquals, isExcelCompatible, recomputeZones } from "../../helpers";
import { parseLiteral } from "../../helpers/cells";
@@ -340,16 +341,16 @@ export class CellPlugin extends CorePlugin implements CoreState {
*/
private getFormulaCellContent(
sheetId: UID,
- compiledFormula: RangeCompiledFormula,
+ tokens: Token[],
dependencies: Range[],
useFixedReference: boolean = false
): string {
if (!dependencies.length) {
- return concat(compiledFormula.tokens.map((token) => token.value));
+ return concat(tokens.map((token) => token.value));
}
let rangeIndex = 0;
return concat(
- compiledFormula.tokens.map((token) => {
+ tokens.map((token) => {
if (token.type === "REFERENCE") {
const range = dependencies[rangeIndex++];
return this.getters.getRangeString(range, sheetId, { useFixedReference });
@@ -362,25 +363,22 @@ export class CellPlugin extends CorePlugin implements CoreState {
/*
* Constructs a formula string based on an initial formula and a translation vector
*/
- getTranslatedCellFormula(
- sheetId: UID,
- offsetX: number,
- offsetY: number,
- compiledFormula: RangeCompiledFormula
- ) {
+ getTranslatedCellFormula(sheetId: UID, offsetX: number, offsetY: number, tokens: Token[]) {
const adaptedDependencies = this.getters.createAdaptedRanges(
- compiledFormula.dependencies,
+ compileTokens(tokens).dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)),
offsetX,
offsetY,
sheetId
);
- return this.getFormulaCellContent(sheetId, compiledFormula, adaptedDependencies);
+ return this.getFormulaCellContent(sheetId, tokens, adaptedDependencies);
}
- getFormulaMovedInSheet(targetSheetId: UID, compiledFormula: RangeCompiledFormula) {
- const dependencies = compiledFormula.dependencies;
+ getFormulaMovedInSheet(originSheetId: UID, targetSheetId: UID, tokens: Token[]) {
+ const dependencies = compileTokens(tokens).dependencies.map((d) =>
+ this.getters.getRangeFromSheetXC(originSheetId, d)
+ );
const adaptedDependencies = this.getters.removeRangesSheetPrefix(targetSheetId, dependencies);
- return this.getFormulaCellContent(targetSheetId, compiledFormula, adaptedDependencies);
+ return this.getFormulaCellContent(targetSheetId, tokens, adaptedDependencies);
}
getCellStyle(position: CellPosition): Style {
diff --git a/src/plugins/ui_core_views/evaluation_conditional_format.ts b/src/plugins/ui_core_views/evaluation_conditional_format.ts
index 154314b24f..08da3f0691 100644
--- a/src/plugins/ui_core_views/evaluation_conditional_format.ts
+++ b/src/plugins/ui_core_views/evaluation_conditional_format.ts
@@ -114,12 +114,7 @@ export class EvaluationConditionalFormatPlugin extends UIPlugin {
sheetId,
col - zone.left,
row - zone.top,
- {
- ...compiledFormula,
- dependencies: compiledFormula.dependencies.map((d) =>
- this.getters.getRangeFromSheetXC(sheetId, d)
- ),
- }
+ compiledFormula.tokens
);
}
return value;
diff --git a/src/plugins/ui_core_views/evaluation_data_validation.ts b/src/plugins/ui_core_views/evaluation_data_validation.ts
index 6728c49f3a..885045b211 100644
--- a/src/plugins/ui_core_views/evaluation_data_validation.ts
+++ b/src/plugins/ui_core_views/evaluation_data_validation.ts
@@ -210,12 +210,7 @@ export class EvaluationDataValidationPlugin extends UIPlugin {
sheetId,
offset.col,
offset.row,
- {
- ...formula,
- dependencies: formula.dependencies.map((d) =>
- this.getters.getRangeFromSheetXC(sheetId, d)
- ),
- }
+ formula.tokens
);
const evaluated = this.getters.evaluateFormula(sheetId, translatedFormula);
diff --git a/src/plugins/ui_feature/sort.ts b/src/plugins/ui_feature/sort.ts
index 4fbbae3156..9d42f750f6 100644
--- a/src/plugins/ui_feature/sort.ts
+++ b/src/plugins/ui_feature/sort.ts
@@ -157,7 +157,7 @@ export class SortPlugin extends UIPlugin {
sheetId,
0,
newRow - position.row,
- cell.compiledFormula
+ cell.compiledFormula.tokens
);
}
newCellValues.style = cell.style;
diff --git a/src/plugins/ui_stateful/clipboard.ts b/src/plugins/ui_stateful/clipboard.ts
index d02b27d8d7..dec5e5a66f 100644
--- a/src/plugins/ui_stateful/clipboard.ts
+++ b/src/plugins/ui_stateful/clipboard.ts
@@ -3,7 +3,8 @@ import { ClipboardHandler } from "../../clipboard_handlers/abstract_clipboard_ha
import { cellStyleToCss, cssPropertiesToCss } from "../../components/helpers";
import { SELECTION_BORDER_COLOR } from "../../constants";
import { getClipboardDataPositions } from "../../helpers/clipboard/clipboard_helpers";
-import { isZoneValid, positions, union } from "../../helpers/index";
+import { UuidGenerator, isZoneValid, positions, union } from "../../helpers/index";
+import { CURRENT_VERSION } from "../../migrations/data";
import {
ClipboardContent,
ClipboardData,
@@ -48,6 +49,7 @@ export class ClipboardPlugin extends UIPlugin {
static layers = ["Clipboard"] as const;
static getters = [
"getClipboardContent",
+ "getClipboardId",
"getClipboardTextContent",
"isCutOperation",
"isPaintingFormat",
@@ -58,6 +60,7 @@ export class ClipboardPlugin extends UIPlugin {
private originSheetId?: UID;
private copiedData?: MinimalClipboardData;
private _isCutOperation: boolean = false;
+ private clipboardId = new UuidGenerator().uuidv4();
// ---------------------------------------------------------------------------
// Command Handling
@@ -69,7 +72,9 @@ export class ClipboardPlugin extends UIPlugin {
const zones = this.getters.getSelectedZones();
return this.isCutAllowedOn(zones);
case "PASTE_FROM_OS_CLIPBOARD": {
- const copiedData = this.convertOSClipboardData(cmd.text);
+ const copiedData = this.convertOSClipboardData(
+ cmd.clipboardContent[ClipboardMIMEType.PlainText] ?? ""
+ );
const pasteOption =
cmd.pasteOption || (this.paintFormatStatus !== "inactive" ? "onlyFormat" : undefined);
return this.isPasteAllowed(cmd.target, copiedData, { pasteOption, isCutOperation: false });
@@ -131,7 +136,13 @@ export class ClipboardPlugin extends UIPlugin {
break;
case "PASTE_FROM_OS_CLIPBOARD": {
this._isCutOperation = false;
- this.copiedData = this.convertOSClipboardData(cmd.text);
+ if (cmd.clipboardContent[ClipboardMIMEType.OSpreadsheet]) {
+ this.copiedData = JSON.parse(cmd.clipboardContent[ClipboardMIMEType.OSpreadsheet]);
+ } else {
+ this.copiedData = this.convertOSClipboardData(
+ cmd.clipboardContent[ClipboardMIMEType.PlainText] ?? ""
+ );
+ }
const pasteOption =
cmd.pasteOption || (this.paintFormatStatus !== "inactive" ? "onlyFormat" : undefined);
this.paste(cmd.target, this.copiedData, {
@@ -490,13 +501,32 @@ export class ClipboardPlugin extends UIPlugin {
return this.getPlainTextContent();
}
+ getClipboardId(): string {
+ return this.clipboardId;
+ }
+
getClipboardContent(): ClipboardContent {
return {
[ClipboardMIMEType.PlainText]: this.getPlainTextContent(),
[ClipboardMIMEType.Html]: this.getHTMLContent(),
+ [ClipboardMIMEType.OSpreadsheet]: this.getSerializedGridData(),
};
}
+ private getSerializedGridData(): string | undefined {
+ const data = {
+ version: CURRENT_VERSION,
+ clipboardId: this.clipboardId,
+ };
+ if (this.copiedData && "figureId" in this.copiedData) {
+ return JSON.stringify(data);
+ }
+ return JSON.stringify({
+ ...data,
+ ...this.copiedData,
+ });
+ }
+
private getPlainTextContent(): string {
if (!this.copiedData?.cells) {
return "\t";
@@ -506,8 +536,8 @@ export class ClipboardPlugin extends UIPlugin {
.map((cells) => {
return cells
.map((c) =>
- this.getters.shouldShowFormulas() && c.cell?.isFormula
- ? c.cell?.content || ""
+ this.getters.shouldShowFormulas() && c?.tokens?.length
+ ? c?.content || ""
: c.evaluatedCell?.formattedValue || ""
)
.join("\t");
@@ -518,7 +548,7 @@ export class ClipboardPlugin extends UIPlugin {
private getHTMLContent(): string | undefined {
if (!this.copiedData?.cells) {
- return undefined;
+ return `\t
`;
}
const cells = this.copiedData.cells;
if (cells.length === 1 && cells[0].length === 1) {
@@ -528,7 +558,7 @@ export class ClipboardPlugin extends UIPlugin {
return "";
}
- let htmlTable = '';
+ let htmlTable = ``;
for (const row of cells) {
htmlTable += "";
for (const cell of row) {
@@ -543,7 +573,7 @@ export class ClipboardPlugin extends UIPlugin {
}
htmlTable += "
";
}
- htmlTable += "
";
+ htmlTable += "
";
return htmlTable;
}
diff --git a/src/registries/autofill_modifiers.ts b/src/registries/autofill_modifiers.ts
index ac41b10741..0198ea84b7 100644
--- a/src/registries/autofill_modifiers.ts
+++ b/src/registries/autofill_modifiers.ts
@@ -106,7 +106,7 @@ autofillModifiersRegistry
return { cellData: {} };
}
const sheetId = data.sheetId;
- const content = getters.getTranslatedCellFormula(sheetId, x, y, cell.compiledFormula);
+ const content = getters.getTranslatedCellFormula(sheetId, x, y, cell.compiledFormula.tokens);
return {
cellData: {
border: data.border,
diff --git a/src/types/clipboard.ts b/src/types/clipboard.ts
index 94ed3f7612..846c0ae726 100644
--- a/src/types/clipboard.ts
+++ b/src/types/clipboard.ts
@@ -3,6 +3,7 @@ import { HeaderIndex, UID, Zone } from "./misc";
export enum ClipboardMIMEType {
PlainText = "text/plain",
Html = "text/html",
+ OSpreadsheet = "web application/o-spreadsheet",
}
export type ClipboardContent = { [type in ClipboardMIMEType]?: string };
diff --git a/src/types/commands.ts b/src/types/commands.ts
index 7e6dabe569..709526e371 100644
--- a/src/types/commands.ts
+++ b/src/types/commands.ts
@@ -22,7 +22,7 @@ import {
} from "./misc";
import { ChartDefinition } from "./chart/chart";
-import { ClipboardPasteOptions } from "./clipboard";
+import { ClipboardContent, ClipboardPasteOptions } from "./clipboard";
import { FigureSize } from "./figure";
import { SearchOptions } from "./find_and_replace";
import { Image } from "./image";
@@ -756,7 +756,7 @@ export interface CancelPaintFormatCommand {
export interface PasteFromOSClipboardCommand {
type: "PASTE_FROM_OS_CLIPBOARD";
target: Zone[];
- text: string;
+ clipboardContent: ClipboardContent;
pasteOption?: ClipboardPasteOptions;
}
diff --git a/src/types/misc.ts b/src/types/misc.ts
index 574a4d659b..20dfcb2f6c 100644
--- a/src/types/misc.ts
+++ b/src/types/misc.ts
@@ -1,4 +1,4 @@
-import { Cell, CellValue, EvaluatedCell } from "./cells";
+import { CellValue, EvaluatedCell } from "./cells";
import { CommandResult } from "./commands";
// -----------------------------------------------------------------------------
@@ -199,10 +199,13 @@ export function isMatrix(x: any): x is Matrix {
}
export interface ClipboardCell {
- cell?: Cell;
evaluatedCell: EvaluatedCell;
- border?: Border;
position: CellPosition;
+ content: string;
+ style?: Style | undefined;
+ format?: Format | undefined;
+ tokens?: Token[];
+ border?: Border;
}
export interface HeaderDimensions {
diff --git a/src/types/table.ts b/src/types/table.ts
index a533475cac..67b793b3c3 100644
--- a/src/types/table.ts
+++ b/src/types/table.ts
@@ -54,7 +54,7 @@ export interface TableElementStyle {
size?: number;
}
-interface TableBorder extends Border {
+export interface TableBorder extends Border {
// used to describe borders inside of a zone
horizontal?: BorderDescr;
vertical?: BorderDescr;
diff --git a/tests/clipboard/__snapshots__/clipboard_plugin.test.ts.snap b/tests/clipboard/__snapshots__/clipboard_plugin.test.ts.snap
deleted file mode 100644
index 393f4eecdd..0000000000
--- a/tests/clipboard/__snapshots__/clipboard_plugin.test.ts.snap
+++ /dev/null
@@ -1,3 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`clipboard Copied cells HTML Copied HTML table snapshot 1`] = `""`;
diff --git a/tests/clipboard/clipboard_figure_plugin.test.ts b/tests/clipboard/clipboard_figure_plugin.test.ts
index b77c335daa..d9eb82d2a0 100644
--- a/tests/clipboard/clipboard_figure_plugin.test.ts
+++ b/tests/clipboard/clipboard_figure_plugin.test.ts
@@ -1,6 +1,6 @@
import { CommandResult, Model } from "../../src";
import { DEFAULT_CELL_HEIGHT, DEFAULT_CELL_WIDTH } from "../../src/constants";
-import { UID } from "../../src/types";
+import { ClipboardMIMEType, UID } from "../../src/types";
import { BarChartDefinition } from "../../src/types/chart";
import {
activateSheet,
@@ -176,6 +176,17 @@ describe.each(["chart", "image"])("Clipboard for %s figures", (type: string) =>
});
});
+ test("Chart clipboard content is not serialized at copy", () => {
+ model.dispatch("SELECT_FIGURE", { id: figureId });
+ copy(model);
+ const clipboardSpreadsheetContent = JSON.parse(
+ model.getters.getClipboardContent()[ClipboardMIMEType.OSpreadsheet]!
+ );
+ expect(clipboardSpreadsheetContent.figureId).toBe(undefined);
+ expect(clipboardSpreadsheetContent.copiedFigure).toBe(undefined);
+ expect(clipboardSpreadsheetContent.copiedChart).toBe(undefined);
+ });
+
describe("Paste command result", () => {
test("Cannot paste with empty target", () => {
model.dispatch("SELECT_FIGURE", { id: figureId });
diff --git a/tests/clipboard/clipboard_plugin.test.ts b/tests/clipboard/clipboard_plugin.test.ts
index 946d9a4554..667349dad2 100644
--- a/tests/clipboard/clipboard_plugin.test.ts
+++ b/tests/clipboard/clipboard_plugin.test.ts
@@ -1,7 +1,8 @@
import { clipboardHandlersRegistries } from "../../src/clipboard_handlers";
-import { DEFAULT_BORDER_DESC } from "../../src/constants";
-import { toCartesian, toZone, zoneToXc } from "../../src/helpers";
+import { DEFAULT_BORDER_DESC, LINK_COLOR } from "../../src/constants";
+import { markdownLink, toCartesian, toZone, zoneToXc } from "../../src/helpers";
import { getClipboardDataPositions } from "../../src/helpers/clipboard/clipboard_helpers";
+import { urlRepresentation } from "../../src/helpers/links";
import { Model } from "../../src/model";
import {
ClipboardMIMEType,
@@ -203,9 +204,9 @@ describe("clipboard", () => {
clipboardData.setData(ClipboardMIMEType.PlainText, "Excalibur");
const content = clipboardData.getData(ClipboardMIMEType.PlainText);
- pasteFromOSClipboard(model, "C2", content);
+ pasteFromOSClipboard(model, "C2", { [ClipboardMIMEType.PlainText]: content });
expect(getCellContent(model, "C2")).toBe(content);
- pasteFromOSClipboard(model, "C3", content, "onlyFormat");
+ pasteFromOSClipboard(model, "C3", { [ClipboardMIMEType.PlainText]: content }, "onlyFormat");
expect(getCellContent(model, "C3")).toBe("");
});
@@ -552,7 +553,8 @@ describe("clipboard", () => {
setCellContent(model, "A2", "3");
copy(model, "A1:B2");
const htmlContent = model.getters.getClipboardContent()[ClipboardMIMEType.Html]!;
- expect(htmlContent).toMatchSnapshot();
+ const expectedHtmlContent = ``;
+ expect(htmlContent).toBe(expectedHtmlContent);
});
test("Copied group of cells are represented as a valid HTML table in the clipboard", async () => {
@@ -563,7 +565,7 @@ describe("clipboard", () => {
const htmlContent = model.getters.getClipboardContent()[ClipboardMIMEType.Html]!;
const parsedHTML = parseXML(new XMLString(htmlContent), "text/html");
- expect(parsedHTML.body.firstElementChild?.tagName).toBe("TABLE");
+ expect(parsedHTML.body.firstElementChild?.tagName).toBe("DIV");
const tableRows = parsedHTML.querySelectorAll("tr");
expect(tableRows).toHaveLength(2);
expect(tableRows[0].querySelectorAll("td")).toHaveLength(2);
@@ -663,7 +665,7 @@ describe("clipboard", () => {
test("can paste multiple cells from os clipboard", () => {
const model = new Model();
- pasteFromOSClipboard(model, "C1", "a\t1\nb\t2");
+ pasteFromOSClipboard(model, "C1", { [ClipboardMIMEType.PlainText]: "a\t1\nb\t2" });
expect(getCellContent(model, "C1")).toBe("a");
expect(getCellContent(model, "C2")).toBe("b");
@@ -675,7 +677,9 @@ describe("clipboard", () => {
const model = new Model();
const sheetId = model.getters.getActiveSheetId();
merge(model, "B2:C3");
- const result = pasteFromOSClipboard(model, "B2", "a\t1\nb\t2");
+ const result = pasteFromOSClipboard(model, "B2", {
+ [ClipboardMIMEType.PlainText]: "a\t1\nb\t2",
+ });
expect(result).toBeCancelledBecause(CommandResult.WillRemoveExistingMerge);
expect(model.getters.getMerges(sheetId).map(zoneToXc)).toEqual(["B2:C3"]);
});
@@ -683,13 +687,13 @@ describe("clipboard", () => {
test("pasting from OS will not change the viewport", () => {
const model = new Model();
const viewport = model.getters.getActiveMainViewport();
- pasteFromOSClipboard(model, "C60", "a\t1\nb\t2");
+ pasteFromOSClipboard(model, "C60", { [ClipboardMIMEType.PlainText]: "a\t1\nb\t2" });
expect(model.getters.getActiveMainViewport()).toEqual(viewport);
});
test("pasting numbers from windows clipboard => interpreted as number", () => {
const model = new Model();
- pasteFromOSClipboard(model, "C1", "1\r\n2\r\n3");
+ pasteFromOSClipboard(model, "C1", { [ClipboardMIMEType.PlainText]: "1\r\n2\r\n3" });
expect(getCellContent(model, "C1")).toBe("1");
expect(getEvaluatedCell(model, "C1").value).toBe(1);
@@ -2291,7 +2295,7 @@ describe("clipboard: pasting outside of sheet", () => {
test("can paste multiple cells from os to outside of sheet", () => {
const model = new Model();
createSheet(model, { activate: true, sheetId: "2", rows: 2, cols: 2 });
- pasteFromOSClipboard(model, "B2", "A\nque\tcoucou\nBOB");
+ pasteFromOSClipboard(model, "B2", { [ClipboardMIMEType.PlainText]: "A\nque\tcoucou\nBOB" });
expect(getCellContent(model, "B2")).toBe("A");
expect(getCellContent(model, "B3")).toBe("que");
expect(getCellContent(model, "C3")).toBe("coucou");
@@ -2303,7 +2307,7 @@ describe("clipboard: pasting outside of sheet", () => {
rows: 2,
cols: 2,
});
- pasteFromOSClipboard(model, "B2", "A\nque\tcoucou\tPatrick");
+ pasteFromOSClipboard(model, "B2", { [ClipboardMIMEType.PlainText]: "A\nque\tcoucou\tPatrick" });
expect(getCellContent(model, "B2")).toBe("A");
expect(getCellContent(model, "B3")).toBe("que");
expect(getCellContent(model, "C3")).toBe("coucou");
@@ -2318,7 +2322,7 @@ describe("clipboard: pasting outside of sheet", () => {
formulaArgSeparator: ";",
thousandsSeparator: " ",
});
- pasteFromOSClipboard(model, "A1", "=SUM(5 ; 3,14)");
+ pasteFromOSClipboard(model, "A1", { [ClipboardMIMEType.PlainText]: "=SUM(5 ; 3,14)" });
expect(getCell(model, "A1")?.content).toBe("=SUM(5 , 3.14)");
expect(getEvaluatedCell(model, "A1").value).toBe(8.14);
});
@@ -2623,6 +2627,173 @@ describe("clipboard: pasting outside of sheet", () => {
});
});
+describe("cross spreadsheet copy/paste", () => {
+ test("should copy/paste a cell with basic formatting", () => {
+ const modelA = new Model();
+ const modelB = new Model();
+ const cellStyle = { bold: true, fillColor: "#00FF00", fontSize: 20 };
+
+ setCellContent(modelA, "B2", "b2");
+ setStyle(modelA, "B2", cellStyle);
+
+ expect(getCell(modelA, "B2")).toMatchObject({
+ content: "b2",
+ style: cellStyle,
+ });
+
+ copy(modelA, "B2");
+ const clipboardContent = modelA.getters.getClipboardContent();
+
+ expect(clipboardContent["text/plain"]).toBe("b2");
+
+ pasteFromOSClipboard(modelB, "D2", clipboardContent);
+
+ expect(getCell(modelA, "B2")?.content).toBe("b2");
+ expect(getCell(modelB, "D2")?.content).toBe("b2");
+ expect(getStyle(modelA, "B2")).toEqual(cellStyle);
+ expect(getStyle(modelB, "D2")).toEqual(cellStyle);
+ });
+
+ test("should copy/paste a cell with a border", () => {
+ const modelA = new Model();
+ const modelB = new Model();
+
+ selectCell(modelA, "B2");
+ setZoneBorders(modelA, { position: "top" });
+
+ expect(getBorder(modelA, "B2")).toEqual({ top: DEFAULT_BORDER_DESC });
+
+ copy(modelA, "B2");
+ const clipboardContent = modelA.getters.getClipboardContent();
+
+ pasteFromOSClipboard(modelB, "D2", clipboardContent);
+
+ expect(getBorder(modelA, "B2")).toEqual({ top: DEFAULT_BORDER_DESC });
+ expect(getBorder(modelB, "D2")).toEqual({ top: DEFAULT_BORDER_DESC });
+ });
+
+ test("should copy/paste a cell with a formula", () => {
+ const modelA = new Model();
+ const modelB = new Model();
+
+ setCellContent(modelA, "A1", "=SUM(1,2)");
+ setCellContent(modelA, "A2", "=SUM(1,2)");
+ setCellFormat(modelA, "A2", "0%");
+ setCellContent(modelA, "A3", "=DATE(2024,1,1)");
+ setCellContent(modelA, "A4", "=DATE(2024,1,1)");
+ setCellFormat(modelA, "A4", "m/d/yyyy hh:mm:ss a");
+ setCellContent(modelA, "A5", "=SOMME(1,2)");
+
+ copy(modelA, "A1:A5");
+ const clipboardContent = modelA.getters.getClipboardContent();
+ pasteFromOSClipboard(modelB, "D1", clipboardContent);
+
+ expect(getCell(modelB, "D1")?.content).toBe("=SUM(1,2)");
+ expect(getCell(modelB, "D2")?.content).toBe("=SUM(1,2)");
+ expect(getCell(modelB, "D3")?.content).toBe("=DATE(2024,1,1)");
+ expect(getCell(modelB, "D4")?.content).toBe("=DATE(2024,1,1)");
+ expect(getCell(modelB, "D5")?.content).toBe("=SOMME(1,2)");
+ });
+
+ test("should copy/paste a cell with a markdown link", () => {
+ const modelA = new Model();
+ const modelB = new Model();
+ const url = "https://www.odoo.com";
+ const urlLabel = "Odoo Website";
+
+ setCellContent(modelA, "A1", markdownLink(urlLabel, url));
+ copy(modelA, "A1");
+ const clipboardContent = modelA.getters.getClipboardContent();
+ pasteFromOSClipboard(modelB, "D1", clipboardContent);
+
+ const cell = getEvaluatedCell(modelB, "D1");
+ expect(cell.link?.label).toBe(urlLabel);
+ expect(cell.link?.url).toBe(url);
+ expect(urlRepresentation(cell.link!, modelB.getters)).toBe(url);
+ expect(getCell(modelB, "D1")?.content).toBe("[Odoo Website](https://www.odoo.com)");
+ expect(getStyle(modelB, "D1")).toEqual({ textColor: LINK_COLOR });
+ expect(getCellText(modelB, "D1")).toBe("Odoo Website");
+ });
+
+ test("should copy/paste a table", () => {
+ const modelA = new Model();
+ const modelB = new Model();
+
+ createTable(modelA, "A1:B2");
+ const tableA = modelA.getters.getCoreTables(modelA.getters.getActiveSheetId())[0];
+
+ expect(tableA).toMatchObject({ range: { zone: toZone("A1:B2") }, type: "static" });
+
+ copy(modelA, "A1:B2");
+ const clipboardContent = modelA.getters.getClipboardContent();
+ pasteFromOSClipboard(modelB, "D1", clipboardContent);
+
+ const tableB = modelB.getters.getCoreTables(modelA.getters.getActiveSheetId())[0];
+
+ expect(tableB).toMatchObject({ range: { zone: toZone("D1:E2") }, type: "static" });
+ expect(tableB.config).toEqual(tableA.config);
+ });
+
+ test("should copy/paste a cell with the cell content and format copied last from an external spreadsheet", () => {
+ const modelA = new Model();
+ const modelB = new Model();
+ const cellStyle = { bold: true, fillColor: "#00FF00", fontSize: 20 };
+
+ setCellContent(modelA, "A1", "a1");
+ setStyle(modelA, "A1", cellStyle);
+ setCellContent(modelB, "C1", "c1");
+ setStyle(modelB, "C1", cellStyle);
+
+ expect(getCell(modelA, "A1")).toMatchObject({
+ content: "a1",
+ style: cellStyle,
+ });
+
+ expect(getCell(modelB, "C1")).toMatchObject({
+ content: "c1",
+ style: cellStyle,
+ });
+
+ copy(modelB, "C1");
+ copy(modelA, "A1");
+ const clipboardContent = modelA.getters.getClipboardContent();
+
+ expect(clipboardContent["text/plain"]).toBe("a1");
+
+ pasteFromOSClipboard(modelB, "B1", {
+ [ClipboardMIMEType.PlainText]: clipboardContent["text/plain"]
+ ? clipboardContent["text/plain"]
+ : "",
+ [ClipboardMIMEType.OSpreadsheet]: clipboardContent["web application/o-spreadsheet"],
+ });
+
+ expect(getCell(modelA, "A1")).toMatchObject({
+ content: "a1",
+ });
+ expect(getCell(modelB, "B1")).toMatchObject({
+ content: "a1",
+ });
+ expect(getStyle(modelA, "A1")).toMatchObject(cellStyle);
+ expect(getStyle(modelB, "B1")).toMatchObject(cellStyle);
+ });
+
+ test("should copy/paste a formula cell with dependencies", () => {
+ const modelA = new Model({ sheets: [{ id: "sheetA" }] });
+ const modelB = new Model({ sheets: [{ id: "sheetB" }] });
+
+ setCellContent(modelA, "C1", "=A1*B1");
+ setCellContent(modelA, "C2", "=A2*B2");
+ setCellContent(modelA, "C3", "=A3*B3");
+
+ copy(modelA, "A1:C3");
+ pasteFromOSClipboard(modelB, "E1", modelA.getters.getClipboardContent());
+
+ expect(getCell(modelB, "G1")?.content).toBe("=E1*F1");
+ expect(getCell(modelB, "G2")?.content).toBe("=E2*F2");
+ expect(getCell(modelB, "G3")?.content).toBe("=E3*F3");
+ });
+});
+
test("Can use clipboard handlers to paste in a sheet other than the active sheet", () => {
model = new Model();
const sheetId = model.getters.getActiveSheetId();
diff --git a/tests/evaluation/formulas.test.ts b/tests/evaluation/formulas.test.ts
index be95ed1b2f..854d76a075 100644
--- a/tests/evaluation/formulas.test.ts
+++ b/tests/evaluation/formulas.test.ts
@@ -13,7 +13,12 @@ function moveFormula(model: Model, formula: string, offsetX: number, offsetY: nu
const sheetId = model.getters.getActiveSheetId();
setCellContent(model, "A1", formula);
const cell = getCell(model, "A1") as FormulaCell;
- return model.getters.getTranslatedCellFormula(sheetId, offsetX, offsetY, cell.compiledFormula);
+ return model.getters.getTranslatedCellFormula(
+ sheetId,
+ offsetX,
+ offsetY,
+ cell.compiledFormula.tokens
+ );
}
describe("createAdaptedRanges", () => {
diff --git a/tests/figures/figure_component.test.ts b/tests/figures/figure_component.test.ts
index 60c34720ed..d30e38dc27 100644
--- a/tests/figures/figure_component.test.ts
+++ b/tests/figures/figure_component.test.ts
@@ -12,6 +12,7 @@ import { figureRegistry } from "../../src/registries";
import { CreateFigureCommand, Pixel, SpreadsheetChildEnv, UID } from "../../src/types";
import { FigureComponent } from "../../src/components/figures/figure/figure";
+import { ClipboardMIMEType } from "../../src/types/clipboard";
import {
activateSheet,
addColumns,
@@ -525,9 +526,10 @@ describe("figures", () => {
await simulateClick(".o-figure");
await simulateClick(".o-figure-menu-item");
await simulateClick(".o-menu div[data-name='copy']");
- const envClipBoardContent = await env.clipboard.readText();
+ const envClipBoardContent = await env.clipboard.read();
if (envClipBoardContent.status === "ok") {
- expect(envClipBoardContent.content).toEqual(
+ const envClipboardTextContent = envClipBoardContent.content[ClipboardMIMEType.PlainText];
+ expect(envClipboardTextContent).toEqual(
model.getters.getClipboardContent()["text/plain"]
);
}
@@ -543,9 +545,10 @@ describe("figures", () => {
await simulateClick(".o-figure");
await simulateClick(".o-figure-menu-item");
await simulateClick(".o-menu div[data-name='cut']");
- const envClipBoardContent = await env.clipboard.readText();
+ const envClipBoardContent = await env.clipboard.read();
if (envClipBoardContent.status === "ok") {
- expect(envClipBoardContent.content).toEqual(
+ const envClipboardTextContent = envClipBoardContent.content[ClipboardMIMEType.PlainText];
+ expect(envClipboardTextContent).toEqual(
model.getters.getClipboardContent()["text/plain"]
);
}
diff --git a/tests/grid/grid_component.test.ts b/tests/grid/grid_component.test.ts
index 1c27d1a81a..7873a356c1 100644
--- a/tests/grid/grid_component.test.ts
+++ b/tests/grid/grid_component.test.ts
@@ -1361,18 +1361,26 @@ describe("Copy paste keyboard shortcut", () => {
});
test("Can paste from OS", async () => {
+ await parent.env.clipboard.writeText("Excalibur");
selectCell(model, "A1");
- clipboardData.setText("Excalibur");
document.body.dispatchEvent(getClipboardEvent("paste", clipboardData));
+ await nextTick();
expect(getCellContent(model, "A1")).toEqual("Excalibur");
});
+
test("Can copy/paste cells", async () => {
setCellContent(model, "A1", "things");
selectCell(model, "A1");
document.body.dispatchEvent(getClipboardEvent("copy", clipboardData));
- expect(clipboardData.content).toEqual({ "text/plain": "things", "text/html": "things" });
+ const clipboard = await parent.env.clipboard.read!();
+ const clipboardContent = "content" in clipboard ? clipboard.content : {};
+ expect(clipboardContent).toMatchObject({
+ "text/plain": "things",
+ "text/html": "things",
+ });
selectCell(model, "A2");
document.body.dispatchEvent(getClipboardEvent("paste", clipboardData));
+ await nextTick();
expect(getCellContent(model, "A2")).toEqual("things");
});
@@ -1380,9 +1388,15 @@ describe("Copy paste keyboard shortcut", () => {
setCellContent(model, "A1", "things");
selectCell(model, "A1");
document.body.dispatchEvent(getClipboardEvent("cut", clipboardData));
- expect(clipboardData.content).toEqual({ "text/plain": "things", "text/html": "things" });
+ const clipboard = await parent.env.clipboard.read!();
+ const clipboardContent = "content" in clipboard ? clipboard.content : {};
+ expect(clipboardContent).toMatchObject({
+ "text/plain": "things",
+ "text/html": "things",
+ });
selectCell(model, "A2");
document.body.dispatchEvent(getClipboardEvent("paste", clipboardData));
+ await nextTick();
expect(getCellContent(model, "A1")).toEqual("");
expect(getCellContent(model, "A2")).toEqual("things");
});
@@ -1396,6 +1410,7 @@ describe("Copy paste keyboard shortcut", () => {
setStyle(model, "A1", { bold: false });
selectCell(model, "A2");
document.body.dispatchEvent(getClipboardEvent("paste", clipboardData));
+ await nextTick();
expect(getCellContent(model, "A2")).toEqual("things");
expect(getStyle(model, "A2")).toEqual({ bold: true });
expect(getCell(model, "A1")).toBe(undefined);
@@ -1406,10 +1421,13 @@ describe("Copy paste keyboard shortcut", () => {
setCellContent(model, "A1", "1");
setCellFormat(model, "A1", "m/d/yyyy");
document.body.dispatchEvent(getClipboardEvent("cut", clipboardData));
- expect(clipboardData.getData(ClipboardMIMEType.PlainText)).toEqual(getCellContent(model, "A1"));
+ const clipboard = await parent.env.clipboard.read!();
+ const clipboardContent = "content" in clipboard ? clipboard.content : {};
+ expect(clipboardContent[ClipboardMIMEType.PlainText]).toEqual(getCellContent(model, "A1"));
model.dispatch("SET_FORMULA_VISIBILITY", { show: false });
selectCell(model, "A2");
document.body.dispatchEvent(getClipboardEvent("paste", clipboardData));
+ await nextTick();
expect(getCellContent(model, "A2")).toEqual("12/31/1899");
});
@@ -1417,23 +1435,29 @@ describe("Copy paste keyboard shortcut", () => {
setCellContent(model, "A1", "1");
setCellFormat(model, "A1", "m/d/yyyy");
document.body.dispatchEvent(getClipboardEvent("cut", clipboardData));
- expect(clipboardData.getData(ClipboardMIMEType.PlainText)).toEqual(
+ let clipboard = await parent.env.clipboard.read!();
+ let clipboardContent = "content" in clipboard ? clipboard.content : {};
+ expect(clipboardContent[ClipboardMIMEType.PlainText]).toEqual(
getEvaluatedCell(model, "A1").formattedValue
);
selectCell(model, "A2");
document.body.dispatchEvent(getClipboardEvent("paste", clipboardData));
+ await nextTick();
expect(getCellContent(model, "A2")).toEqual("12/31/1899");
model.dispatch("SET_FORMULA_VISIBILITY", { show: true });
setCellContent(model, "B1", "1");
selectCell(model, "B1");
document.body.dispatchEvent(getClipboardEvent("cut", clipboardData));
- expect(clipboardData.getData(ClipboardMIMEType.PlainText)).toEqual(
+ clipboard = await parent.env.clipboard.read!();
+ clipboardContent = "content" in clipboard ? clipboard.content : {};
+ expect(clipboardContent[ClipboardMIMEType.PlainText]).toEqual(
getEvaluatedCell(model, "B1").formattedValue
);
model.dispatch("SET_FORMULA_VISIBILITY", { show: false });
selectCell(model, "B2");
document.body.dispatchEvent(getClipboardEvent("paste", clipboardData));
+ await nextTick();
expect(getCellContent(model, "B2")).toEqual("1");
});
@@ -1446,7 +1470,6 @@ describe("Copy paste keyboard shortcut", () => {
// Fake OS clipboard should have the same content
// to make paste come from spreadsheet clipboard
// which support paste as values
- parent.env.clipboard.writeText(content);
selectCell(model, "A2");
document.activeElement!.dispatchEvent(
new KeyboardEvent("keydown", { key: "V", ctrlKey: true, bubbles: true, shiftKey: true })
@@ -1542,8 +1565,12 @@ describe("Copy paste keyboard shortcut", () => {
createChart(model, { type: "bar" }, "chartId");
model.dispatch("SELECT_FIGURE", { id: "chartId" });
document.body.dispatchEvent(getClipboardEvent("copy", clipboardData));
- expect(clipboardData.content).toEqual({ "text/plain": "\t" });
- document.body.dispatchEvent(getClipboardEvent("paste", clipboardData));
+ const clipboard = await parent.env.clipboard.read!();
+ const clipboardContent = "content" in clipboard ? clipboard.content : {};
+ expect(clipboardContent).toMatchObject({
+ "text/plain": "\t",
+ });
+ await document.body.dispatchEvent(getClipboardEvent("paste", clipboardData));
expect(model.getters.getChartIds(sheetId)).toHaveLength(2);
});
@@ -1552,8 +1579,12 @@ describe("Copy paste keyboard shortcut", () => {
createChart(model, { type: "bar" }, "chartId");
model.dispatch("SELECT_FIGURE", { id: "chartId" });
document.body.dispatchEvent(getClipboardEvent("cut", clipboardData));
- expect(clipboardData.content).toEqual({ "text/plain": "\t" });
- document.body.dispatchEvent(getClipboardEvent("paste", clipboardData));
+ const clipboard = await parent.env.clipboard.read!();
+ const clipboardContent = "content" in clipboard ? clipboard.content : {};
+ expect(clipboardContent).toMatchObject({
+ "text/plain": "\t",
+ });
+ await document.body.dispatchEvent(getClipboardEvent("paste", clipboardData));
expect(model.getters.getChartIds(sheetId)).toHaveLength(1);
expect(model.getters.getChartIds(sheetId)[0]).not.toEqual("chartId");
});
diff --git a/tests/helpers/ui_helpers.test.ts b/tests/helpers/ui_helpers.test.ts
index 6d1740b59e..9b5f53a336 100644
--- a/tests/helpers/ui_helpers.test.ts
+++ b/tests/helpers/ui_helpers.test.ts
@@ -252,7 +252,9 @@ describe("UI Helpers", () => {
const clipboardString = "a\t1\nb\t2";
test("Can interactive paste", () => {
- interactivePasteFromOS(env, target("D2"), clipboardString);
+ interactivePasteFromOS(env, target("D2"), {
+ "text/plain": clipboardString,
+ });
expect(getCellContent(model, "D2")).toBe("a");
expect(getCellContent(model, "E2")).toBe("1");
expect(getCellContent(model, "D3")).toBe("b");
@@ -262,7 +264,9 @@ describe("UI Helpers", () => {
test("Pasting content that will destroy a merge will notify the user", async () => {
merge(model, "B2:C3");
selectCell(model, "A1");
- interactivePasteFromOS(env, model.getters.getSelectedZones(), clipboardString);
+ interactivePasteFromOS(env, model.getters.getSelectedZones(), {
+ "text/plain": clipboardString,
+ });
expect(notifyUserTextSpy).toHaveBeenCalledWith(
PasteInteractiveContent.willRemoveExistingMerge.toString()
);
diff --git a/tests/menus/menu_items_registry.test.ts b/tests/menus/menu_items_registry.test.ts
index a2b0cb218e..05c4ef627d 100644
--- a/tests/menus/menu_items_registry.test.ts
+++ b/tests/menus/menu_items_registry.test.ts
@@ -30,7 +30,12 @@ import {
updateLocale,
updateTableConfig,
} from "../test_helpers/commands_helpers";
-import { getCell, getCellContent, getEvaluatedCell } from "../test_helpers/getters_helpers";
+import {
+ getCell,
+ getCellContent,
+ getEvaluatedCell,
+ getStyle,
+} from "../test_helpers/getters_helpers";
import {
clearFunctions,
doAction,
@@ -45,6 +50,8 @@ import {
} from "../test_helpers/helpers";
import { Model } from "../../src";
+import { ClipboardMIMEType } from "../../src/types";
+
import { CellComposerStore } from "../../src/components/composer/composer/cell_composer_store";
import { FONT_SIZES } from "../../src/constants";
import { functionRegistry } from "../../src/functions";
@@ -168,31 +175,31 @@ describe("Menu Item actions", () => {
});
test("Edit -> copy", () => {
- const spyWriteClipboard = jest.spyOn(env.clipboard, "write");
+ const spyWriteClipboard = jest.spyOn(env.clipboard!, "write");
doAction(["edit", "copy"], env);
expect(dispatch).toHaveBeenCalledWith("COPY");
expect(spyWriteClipboard).toHaveBeenCalledWith(model.getters.getClipboardContent());
});
test("Edit -> cut", () => {
- const spyWriteClipboard = jest.spyOn(env.clipboard, "write");
+ const spyWriteClipboard = jest.spyOn(env.clipboard!, "write");
doAction(["edit", "cut"], env);
expect(dispatch).toHaveBeenCalledWith("CUT");
expect(spyWriteClipboard).toHaveBeenCalledWith(model.getters.getClipboardContent());
});
test("Edit -> paste from OS clipboard if copied from outside world last", async () => {
+ setCellContent(model, "A1", "a1");
+ selectCell(model, "A1");
doAction(["edit", "copy"], env); // first copy from grid
- await env.clipboard.writeText("Then copy in OS clipboard");
+ await env.clipboard!.writeText("Then copy in OS clipboard");
+ selectCell(model, "C3");
await doAction(["edit", "paste"], env);
- expect(dispatch).toHaveBeenCalledWith("PASTE_FROM_OS_CLIPBOARD", {
- text: "Then copy in OS clipboard",
- target: [{ bottom: 0, left: 0, right: 0, top: 0 }],
- });
+ expect(getCellContent(model, "C3")).toEqual("Then copy in OS clipboard");
});
test("Edit -> paste if copied from grid last", async () => {
- await env.clipboard.writeText("First copy in OS clipboard");
+ await env.clipboard!.writeText("First copy in OS clipboard");
doAction(["edit", "copy"], env); // then copy from grid
await doAction(["edit", "paste"], env);
interactivePaste(env, target("A1"));
@@ -213,11 +220,15 @@ describe("Menu Item actions", () => {
});
test("Paste only-format from OS clipboard should paste nothing", async () => {
- await env.clipboard.writeText("Copy in OS clipboard");
+ await env.clipboard!.writeText("Copy in OS clipboard");
selectCell(model, "A1");
await doAction(["edit", "paste_special", "paste_special_format"], env);
expect(dispatch).toHaveBeenCalledWith("PASTE_FROM_OS_CLIPBOARD", {
- text: "Copy in OS clipboard",
+ clipboardContent: {
+ [ClipboardMIMEType.PlainText]: "Copy in OS clipboard",
+ [ClipboardMIMEType.Html]: "",
+ [ClipboardMIMEType.OSpreadsheet]: "",
+ },
target: target("A1"),
pasteOption: "onlyFormat",
});
@@ -229,14 +240,10 @@ describe("Menu Item actions", () => {
setStyle(model, "C1", { fillColor: "#FA0000" });
selectCell(model, "C1");
doAction(["edit", "copy"], env); // first copy from grid
- await env.clipboard.writeText("Then copy in OS clipboard");
+ await env.clipboard!.writeText("Then copy in OS clipboard");
selectCell(model, "A1");
await doAction(["edit", "paste_special", "paste_special_format"], env);
- expect(dispatch).toHaveBeenCalledWith("PASTE_FROM_OS_CLIPBOARD", {
- text: "Then copy in OS clipboard",
- target: target("A1"),
- pasteOption: "onlyFormat",
- });
+ expect(getStyle(model, "A1").fillColor).toBeUndefined();
expect(getCellContent(model, "A1")).toEqual("");
});
@@ -261,11 +268,15 @@ describe("Menu Item actions", () => {
test("Edit -> paste_special -> paste_special_value from OS clipboard", async () => {
const text = "in OS clipboard";
- await env.clipboard.writeText(text);
+ await env.clipboard!.writeText(text);
await doAction(["edit", "paste_special", "paste_special_value"], env);
expect(dispatch).toHaveBeenCalledWith("PASTE_FROM_OS_CLIPBOARD", {
target: target("A1"),
- text,
+ clipboardContent: {
+ [ClipboardMIMEType.PlainText]: text,
+ [ClipboardMIMEType.Html]: "",
+ [ClipboardMIMEType.OSpreadsheet]: "",
+ },
pasteOption: "asValue",
});
});
@@ -281,11 +292,15 @@ describe("Menu Item actions", () => {
test("Edit -> paste_special -> paste_special_format from OS clipboard", async () => {
const text = "in OS clipboard";
- await env.clipboard.writeText(text);
+ await env.clipboard!.writeText(text);
await doAction(["edit", "paste_special", "paste_special_format"], env);
expect(dispatch).toHaveBeenCalledWith("PASTE_FROM_OS_CLIPBOARD", {
target: target("A1"),
- text,
+ clipboardContent: {
+ [ClipboardMIMEType.PlainText]: text,
+ [ClipboardMIMEType.Html]: "",
+ [ClipboardMIMEType.OSpreadsheet]: "",
+ },
pasteOption: "onlyFormat",
});
});
diff --git a/tests/menus/menu_items_registry_cross_spreadsheet.test.ts b/tests/menus/menu_items_registry_cross_spreadsheet.test.ts
new file mode 100644
index 0000000000..bbc0423a74
--- /dev/null
+++ b/tests/menus/menu_items_registry_cross_spreadsheet.test.ts
@@ -0,0 +1,42 @@
+import { SpreadsheetChildEnv } from "../../src/types";
+import { selectCell, setCellContent, setStyle } from "../test_helpers/commands_helpers";
+import { getCell } from "../test_helpers/getters_helpers";
+import { doAction, makeTestEnv } from "../test_helpers/helpers";
+
+import { Model } from "../../src";
+
+describe("cross spreadsheet copy/paste", () => {
+ test("should copy/paste from Edit menu", async () => {
+ const envA: SpreadsheetChildEnv = makeTestEnv();
+ const envB: SpreadsheetChildEnv = makeTestEnv();
+ const modelA: Model = envA.model;
+ const modelB: Model = envB.model;
+
+ const cellStyle = { bold: true, fillColor: "#00FF00", fontSize: 20 };
+
+ setCellContent(modelA, "A1", "a1");
+ setStyle(modelA, "A1", cellStyle);
+ expect(getCell(modelA, "A1")).toMatchObject({
+ content: "a1",
+ style: cellStyle,
+ });
+
+ selectCell(modelA, "A1");
+ await doAction(["edit", "copy"], envA);
+
+ /**
+ * Copy the clipboard from envA to envB because
+ * in this context we need to simulate that
+ * the clipboard is shared between the two environments
+ * given that in a real world scenario we are using one
+ * clipboard which is the machine clipboard.
+ */
+ envB.clipboard = envA.clipboard;
+
+ selectCell(modelB, "B1");
+ await doAction(["edit", "paste"], envB);
+
+ expect(getCell(modelB, "B1")?.content).toEqual("a1");
+ expect(getCell(modelB, "B1")?.style).toMatchObject(cellStyle);
+ });
+});
diff --git a/tests/spreadsheet/spreadsheet_component.test.ts b/tests/spreadsheet/spreadsheet_component.test.ts
index b36b07a580..7517ca6e8b 100644
--- a/tests/spreadsheet/spreadsheet_component.test.ts
+++ b/tests/spreadsheet/spreadsheet_component.test.ts
@@ -358,7 +358,6 @@ describe("Composer / selectionInput interactions", () => {
}));
});
- jest.setTimeout(500000000);
test("Switching from selection input to composer should update the highlihts", async () => {
const composerStore = env.getStore(CellComposerStore);
//open cf sidepanel
diff --git a/tests/test_helpers/clipboard.ts b/tests/test_helpers/clipboard.ts
index bec423d838..7743a58278 100644
--- a/tests/test_helpers/clipboard.ts
+++ b/tests/test_helpers/clipboard.ts
@@ -5,18 +5,31 @@ import {
import { ClipboardContent, ClipboardMIMEType } from "../../src/types";
export class MockClipboard implements ClipboardInterface {
- private content: string | undefined = "Some random clipboard content";
+ private content: ClipboardContent = {};
- async readText(): Promise {
- return { status: "ok", content: this.content || "" };
+ async read(): Promise {
+ return {
+ status: "ok",
+ content: {
+ [ClipboardMIMEType.PlainText]: this.content[ClipboardMIMEType.PlainText],
+ [ClipboardMIMEType.Html]: this.content[ClipboardMIMEType.Html],
+ [ClipboardMIMEType.OSpreadsheet]: this.content[ClipboardMIMEType.OSpreadsheet],
+ },
+ };
}
async writeText(text: string): Promise {
- this.content = text;
+ this.content[ClipboardMIMEType.PlainText] = text;
+ this.content[ClipboardMIMEType.Html] = "";
+ this.content[ClipboardMIMEType.OSpreadsheet] = "";
}
async write(content: ClipboardContent) {
- this.content = content[ClipboardMIMEType.PlainText];
+ this.content = {
+ [ClipboardMIMEType.PlainText]: content[ClipboardMIMEType.PlainText],
+ [ClipboardMIMEType.Html]: content[ClipboardMIMEType.Html],
+ [ClipboardMIMEType.OSpreadsheet]: content[ClipboardMIMEType.OSpreadsheet],
+ };
}
}
diff --git a/tests/test_helpers/commands_helpers.ts b/tests/test_helpers/commands_helpers.ts
index 61e81e5430..2b4cabe87e 100644
--- a/tests/test_helpers/commands_helpers.ts
+++ b/tests/test_helpers/commands_helpers.ts
@@ -11,6 +11,7 @@ import {
BorderData,
ChartDefinition,
ChartWithAxisDefinition,
+ ClipboardContent,
ClipboardPasteOptions,
CreateSheetCommand,
CreateTableStyleCommand,
@@ -328,11 +329,11 @@ export function paste(
export function pasteFromOSClipboard(
model: Model,
range: string,
- content: string,
+ content: ClipboardContent,
pasteOption?: ClipboardPasteOptions
): DispatchResult {
return model.dispatch("PASTE_FROM_OS_CLIPBOARD", {
- text: content,
+ clipboardContent: content,
target: target(range),
pasteOption,
});