import { Grid } from '../core';
import { IGridDataChange, IGridDataResult } from '../data-model';
import debounce from '../debounce';
import { RawPositionRange } from '../position-range';
import * as tsv from '../tsv';
const innerText = require('inner-text-shim');
export interface ICopyPaste {
_maybeSelectText(): void;
}
export function create(grid: Grid): ICopyPaste {
function getCopyPasteRange(): RawPositionRange {
let selectionRange = grid.navigationModel.selection;
// valid selection range cannot go to -1
if (selectionRange.top === -1) {
selectionRange = {
top: grid.navigationModel.focus.row,
left: grid.navigationModel.focus.col,
width: 1,
height: 1,
};
}
return selectionRange;
}
grid.eventLoop.bind('copy', (e) => {
if (!grid.focused) {
if (e.target === grid.textarea) {
e.preventDefault();
}
return;
}
// prepare for copy
const copyTable = document.createElement('table');
const tableBody = document.createElement('tbody');
copyTable.appendChild(tableBody);
const tsvData: string[][] = [];
const selectionRange = getCopyPasteRange();
let gotNull = false;
grid.data.iterate(
selectionRange,
() => {
const row = document.createElement('tr');
tableBody.appendChild(row);
const array: string[] = [];
tsvData.push(array);
return {
row,
array,
};
},
(r: number, c: number, rowResult: { row: HTMLTableRowElement; array: string[] }) => {
const data = grid.dataModel.get(r, c, true);
// intentional == checks null or undefined
if (data == null) {
return (gotNull = true); // this breaks the col loop
}
const td = document.createElement('td');
if (data.value) {
td.setAttribute('grid-data', JSON.stringify(data.value));
}
td.textContent = data.formatted || ' ';
td.innerHTML = td.innerHTML.replace(/\n/g, '
') || ' ';
rowResult.row.appendChild(td);
rowResult.array.push(data.formatted);
return undefined;
},
);
if (!gotNull) {
e.clipboardData?.setData('text/plain', tsv.stringify(tsvData));
e.clipboardData?.setData('text/html', copyTable.outerHTML);
e.preventDefault();
setTimeout(() => {
grid.eventLoop.fire('grid-copy');
}, 1);
}
});
function makePasteDataChange(r: number, c: number, data: IGridDataResult | string): IGridDataChange {
let value;
let formatted;
if (typeof data === 'string') {
formatted = data;
} else {
value = data.value;
formatted = data.formatted;
}
return {
row: r,
col: c,
value,
formatted,
paste: true,
};
}
grid.eventLoop.bind('paste', (e) => {
if (!grid.focused) {
return;
}
const selectionRange = getCopyPasteRange();
if (!e.clipboardData || !e.clipboardData.getData) {
console.warn('no clipboard data on paste event');
return;
}
const tsvPasteData = tsv.parse(e.clipboardData.getData('text/plain'));
let pasteHtml = e.clipboardData.getData('text/html');
e.preventDefault();
setTimeout(() => {
const tempDiv = document.createElement('div');
// this nonsense is required so .innerText converts
to \n
tempDiv.style.opacity = '0';
tempDiv.style.pointerEvents = 'none';
tempDiv.style.position = 'absolute';
document.body.appendChild(tempDiv);
//////
tempDiv.innerHTML = pasteHtml;
const table = tempDiv.querySelector('table');
let pasteData: Array>> = tsvPasteData;
if (table) {
let tablePasteData: Array>>;
table.style.whiteSpace = 'pre';
tablePasteData = [];
const trs = tempDiv.querySelectorAll('tr');
[].forEach.call(trs, (tr: typeof trs[0]) => {
const row: Array> = [];
tablePasteData.push(row);
const tds = tr.querySelectorAll('td');
[].forEach.call(tds, (td: typeof tds[0]) => {
const text = innerText(td);
const dataResult: IGridDataResult = {
formatted: text && text.trim(),
value: undefined,
};
const gridData = td.getAttribute('grid-data');
if (gridData) {
try {
dataResult.value = JSON.parse(gridData);
} catch (error) {
console.warn("somehow couldn't parse grid data");
}
}
row.push(dataResult);
});
});
pasteData = tablePasteData;
}
document.body.removeChild(tempDiv);
const dataChanges: Array> = [];
let singlePasteValue: string | IGridDataResult | undefined;
if (pasteData.length === 1 && pasteData[0].length === 1) {
singlePasteValue = pasteData[0][0];
}
if (singlePasteValue) {
const singlePasteString = singlePasteValue;
// this will do nothing if no other selections as it will be an empty array
let ranges = [selectionRange];
ranges = ranges.concat(grid.navigationModel.otherSelections);
ranges.forEach((range) => {
grid.data.iterate(range, (r, c) => {
dataChanges.push(makePasteDataChange(r, c, singlePasteString));
});
});
} else {
const top = selectionRange.top;
const left = selectionRange.left;
pasteData.forEach((row, r) => {
const dataRow = r + top;
if (dataRow > grid.data.row.count() - 1) {
return;
}
row.forEach((pasteValue, c) => {
const dataCol = c + left;
// intention == to match null and undefined
if (pasteValue == undefined || dataCol > grid.data.col.count() - 1) {
return;
}
dataChanges.push(makePasteDataChange(dataRow, dataCol, pasteValue));
});
});
const newSelection = {
top,
left,
height: pasteData.length,
width: pasteData[0].length,
};
grid.navigationModel.clearSelection();
grid.navigationModel.setSelection(newSelection);
}
grid.dataModel.set(dataChanges);
}, 1);
});
const maybeSelectText = debounce(function maybeSelectTextInner() {
if (!(grid.editModel && grid.editModel.editing) && grid.focused) {
grid.textarea.value =
grid.dataModel.get(grid.navigationModel.focus.row, grid.navigationModel.focus.col).formatted || '.';
grid.textarea.select();
}
}, 1);
grid.eventLoop.bind('keyup', (_e) => {
maybeSelectText();
});
grid.eventLoop.bind('grid-focus', (_e) => {
maybeSelectText();
});
grid.eventLoop.bind('mousedown', (e) => {
if (e.target !== grid.textarea) {
return;
}
maybeSelectText();
});
return {
_maybeSelectText: maybeSelectText,
};
}
export default create;