Improve pasting (#723)

* switch to selection tool on paste

* align pasting via contextMenu with pasting from event

* ensure only plaintext can be pasted

* fix findShapeByKey regression

* simplify wysiwyg pasting

* improve wysiwyg blurriness
This commit is contained in:
David Luzar 2020-02-07 18:42:24 +01:00 committed by GitHub
parent ba1a39c9f3
commit 88eacc9da7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 119 additions and 82 deletions

View File

@ -3,6 +3,9 @@ import { ExcalidrawElement } from "./element/types";
let CLIPBOARD = "";
let PREFER_APP_CLIPBOARD = false;
export const probablySupportsClipboardReadText =
"clipboard" in navigator && "readText" in navigator.clipboard;
export const probablySupportsClipboardWriteText =
"clipboard" in navigator && "writeText" in navigator.clipboard;
@ -47,26 +50,33 @@ export function getAppClipboard(): {
) {
return { elements: clipboardElements };
}
} catch (err) {}
} catch (err) {
console.error(err);
}
return {};
}
export function parseClipboardEvent(
e: ClipboardEvent,
): {
export async function getClipboardContent(
e: ClipboardEvent | null,
): Promise<{
text?: string;
elements?: readonly ExcalidrawElement[];
} {
}> {
try {
const text = e.clipboardData?.getData("text/plain").trim();
const text = e
? e.clipboardData?.getData("text/plain").trim()
: probablySupportsClipboardReadText &&
(await navigator.clipboard.readText());
if (text && !PREFER_APP_CLIPBOARD) {
return { text };
}
return getAppClipboard();
} catch (e) {}
} catch (err) {
console.error(err);
}
return {};
return getAppClipboard();
}
export async function copyCanvasToClipboardAsPng(canvas: HTMLCanvasElement) {

View File

@ -53,8 +53,36 @@ export function textWysiwyg({
outline: "1px solid transparent",
whiteSpace: "nowrap",
minHeight: "1em",
backfaceVisibility: "hidden",
});
editable.onpaste = ev => {
try {
const selection = window.getSelection();
if (!selection?.rangeCount) {
return;
}
selection.deleteFromDocument();
const text = ev.clipboardData!.getData("text").replace(/\r\n?/g, "\n");
const span = document.createElement("span");
span.innerText = text;
const range = selection.getRangeAt(0);
range.insertNode(span);
// deselect
window.getSelection()!.removeAllRanges();
range.setStart(span, span.childNodes.length);
range.setEnd(span, span.childNodes.length);
selection.addRange(range);
ev.preventDefault();
} catch (err) {
console.error(err);
}
};
editable.onkeydown = ev => {
if (ev.key === KEYS.ESCAPE) {
ev.preventDefault();
@ -92,6 +120,7 @@ export function textWysiwyg({
function cleanup() {
editable.onblur = null;
editable.onkeydown = null;
editable.onpaste = null;
window.removeEventListener("wheel", stopEvent, true);
document.body.removeChild(editable);
}

View File

@ -46,6 +46,7 @@ import { ExcalidrawElement } from "./element/types";
import {
isWritableElement,
isInputLike,
isToolIcon,
debounce,
capitalizeString,
distance,
@ -100,11 +101,7 @@ import { t, languages, setLanguage, getLanguage } from "./i18n";
import { StoredScenesList } from "./components/StoredScenesList";
import { HintViewer } from "./components/HintViewer";
import {
getAppClipboard,
copyToAppClipboard,
parseClipboardEvent,
} from "./clipboard";
import { copyToAppClipboard, getClipboardContent } from "./clipboard";
let { elements } = createScene();
const { history } = createHistory();
@ -474,48 +471,6 @@ export class App extends React.Component<any, AppState> {
copyToAppClipboard(elements);
e.preventDefault();
};
private onPaste = (e: ClipboardEvent) => {
// #686
const target = document.activeElement;
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
if (
elementUnderCursor instanceof HTMLCanvasElement &&
!isWritableElement(target)
) {
const data = parseClipboardEvent(e);
if (data.elements) {
this.addElementsFromPaste(data.elements);
} else if (data.text) {
const { x, y } = viewportCoordsToSceneCoords(
{ clientX: cursorX, clientY: cursorY },
this.state,
);
const element = newTextElement(
newElement(
"text",
x,
y,
this.state.currentItemStrokeColor,
this.state.currentItemBackgroundColor,
this.state.currentItemFillStyle,
this.state.currentItemStrokeWidth,
this.state.currentItemRoughness,
this.state.currentItemOpacity,
),
data.text,
this.state.currentItemFont,
);
element.isSelected = true;
elements = [...clearSelection(elements), element];
history.resumeRecording();
this.setState({});
}
e.preventDefault();
}
};
private onUnload = () => {
isHoldingSpace = false;
@ -551,7 +506,7 @@ export class App extends React.Component<any, AppState> {
public async componentDidMount() {
document.addEventListener("copy", this.onCopy);
document.addEventListener("paste", this.onPaste);
document.addEventListener("paste", this.pasteFromClipboard);
document.addEventListener("cut", this.onCut);
document.addEventListener("keydown", this.onKeyDown, false);
@ -583,7 +538,7 @@ export class App extends React.Component<any, AppState> {
public componentWillUnmount() {
document.removeEventListener("copy", this.onCopy);
document.removeEventListener("paste", this.onPaste);
document.removeEventListener("paste", this.pasteFromClipboard);
document.removeEventListener("cut", this.onCut);
document.removeEventListener("keydown", this.onKeyDown, false);
@ -657,14 +612,7 @@ export class App extends React.Component<any, AppState> {
!event.metaKey &&
this.state.draggingElement === null
) {
if (!isHoldingSpace) {
setCursorForShape(shape);
}
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
elements = clearSelection(elements);
this.setState({ elementType: shape });
this.selectShapeTool(shape);
// Undo action
} else if (event[KEYS.META] && /z/i.test(event.key)) {
event.preventDefault();
@ -721,13 +669,65 @@ export class App extends React.Component<any, AppState> {
copyToAppClipboard(elements);
};
private pasteFromClipboard = () => {
const data = getAppClipboard();
if (data.elements) {
this.addElementsFromPaste(data.elements);
private pasteFromClipboard = async (e: ClipboardEvent | null) => {
// #686
const target = document.activeElement;
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
if (
// if no ClipboardEvent supplied, assume we're pasting via contextMenu
// thus these checks don't make sense
!e ||
(elementUnderCursor instanceof HTMLCanvasElement &&
!isWritableElement(target))
) {
const data = await getClipboardContent(e);
if (data.elements) {
this.addElementsFromPaste(data.elements);
} else if (data.text) {
const { x, y } = viewportCoordsToSceneCoords(
{ clientX: cursorX, clientY: cursorY },
this.state,
);
const element = newTextElement(
newElement(
"text",
x,
y,
this.state.currentItemStrokeColor,
this.state.currentItemBackgroundColor,
this.state.currentItemFillStyle,
this.state.currentItemStrokeWidth,
this.state.currentItemRoughness,
this.state.currentItemOpacity,
),
data.text,
this.state.currentItemFont,
);
element.isSelected = true;
elements = [...clearSelection(elements), element];
history.resumeRecording();
}
this.selectShapeTool("selection");
e?.preventDefault();
}
};
private selectShapeTool(elementType: AppState["elementType"]) {
if (!isHoldingSpace) {
setCursorForShape(elementType);
}
if (isToolIcon(document.activeElement)) {
document.activeElement.blur();
}
if (elementType !== "selection") {
elements = clearSelection(elements);
}
this.setState({ elementType });
}
setAppState = (obj: any) => {
this.setState(obj);
};
@ -800,7 +800,7 @@ export class App extends React.Component<any, AppState> {
options: [
navigator.clipboard && {
label: t("labels.paste"),
action: () => this.pasteFromClipboard(),
action: () => this.pasteFromClipboard(null),
},
...this.actionManager.getContextMenuItems(action =>
this.canvasOnlyActions.includes(action),
@ -826,7 +826,7 @@ export class App extends React.Component<any, AppState> {
},
navigator.clipboard && {
label: t("labels.paste"),
action: () => this.pasteFromClipboard(),
action: () => this.pasteFromClipboard(null),
},
...this.actionManager.getContextMenuItems(
action => !this.canvasOnlyActions.includes(action),

View File

@ -65,7 +65,7 @@ export const SHAPES = [
),
value: "text",
},
];
] as const;
export const shapesShortcutKeys = SHAPES.map((shape, index) => [
shape.value[0],
@ -73,12 +73,9 @@ export const shapesShortcutKeys = SHAPES.map((shape, index) => [
]).flat(1);
export function findShapeByKey(key: string) {
const defaultElement = "selection";
return SHAPES.reduce((element, shape, index) => {
if (shape.value[0] !== key && key !== (index + 1).toString()) {
return element;
}
return shape.value;
}, defaultElement);
return (
SHAPES.find((shape, index) => {
return shape.value[0] === key || key === (index + 1).toString();
})?.value || "selection"
);
}

View File

@ -1,4 +1,5 @@
import { ExcalidrawElement } from "./element/types";
import { SHAPES } from "./shapes";
export type AppState = {
draggingElement: ExcalidrawElement | null;
@ -8,7 +9,7 @@ export type AppState = {
// element being edited, but not necessarily added to elements array yet
// (e.g. text element when typing into the input)
editingElement: ExcalidrawElement | null;
elementType: string;
elementType: typeof SHAPES[number]["value"];
elementLocked: boolean;
exportBackground: boolean;
currentItemStrokeColor: string;