mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 09:58:11 +08:00
@@ -3,10 +3,10 @@
|
||||
|
||||
<div class="unsupported-modal-backdrop" v-if="apiUnsupported" ref="unsupported">
|
||||
<LayoutCol class="unsupported-modal">
|
||||
<h2>Your browser currently doesn't support Graphite</h2>
|
||||
<h2>This browser currently doesn't support Graphite</h2>
|
||||
<p>Unfortunately, some features won't work properly. Please upgrade to a modern browser such as Firefox, Chrome, Edge, or Safari version 15 or later.</p>
|
||||
<p>
|
||||
Your browser is missing support for the
|
||||
This browser is missing support for the
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array#browser_compatibility" target="_blank"><code>BigInt64Array</code></a> JavaScript
|
||||
API which is required for using the editor. However, you can still explore the user interface.
|
||||
</p>
|
||||
|
||||
@@ -2,7 +2,9 @@ import { DialogState } from "@/state-providers/dialog";
|
||||
import { FullscreenState } from "@/state-providers/fullscreen";
|
||||
import { PortfolioState } from "@/state-providers/portfolio";
|
||||
import { makeKeyboardModifiersBitfield, textInputCleanup, getLatinKey } from "@/utility-functions/keyboard-entry";
|
||||
import { stripIndents } from "@/utility-functions/strip-indents";
|
||||
import { Editor } from "@/wasm-communication/editor";
|
||||
import { TriggerPaste } from "@/wasm-communication/messages";
|
||||
|
||||
type EventName = keyof HTMLElementEventMap | keyof WindowEventHandlersEventMap | "modifyinputfield";
|
||||
type EventListenerTarget = {
|
||||
@@ -14,6 +16,16 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
|
||||
const app = window.document.querySelector("[data-app]") as HTMLElement | undefined;
|
||||
app?.focus();
|
||||
|
||||
let viewportPointerInteractionOngoing = false;
|
||||
let textInput = undefined as undefined | HTMLDivElement;
|
||||
let canvasFocused = true;
|
||||
|
||||
function blurApp(): void {
|
||||
canvasFocused = false;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const listeners: { target: EventListenerTarget; eventName: EventName; action: (event: any) => void; options?: boolean | AddEventListenerOptions }[] = [
|
||||
{ target: window, eventName: "resize", action: (): void => onWindowResize(container) },
|
||||
@@ -37,12 +49,15 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
|
||||
},
|
||||
];
|
||||
|
||||
let viewportPointerInteractionOngoing = false;
|
||||
let textInput = undefined as undefined | HTMLDivElement;
|
||||
let canvasFocused = true;
|
||||
// Event bindings
|
||||
|
||||
function blurApp(): void {
|
||||
canvasFocused = false;
|
||||
function bindListeners(): void {
|
||||
// Add event bindings for the lifetime of the application
|
||||
listeners.forEach(({ target, eventName, action, options }) => target.addEventListener(eventName, action, options));
|
||||
}
|
||||
function unbindListeners(): void {
|
||||
// Remove event bindings after the lifetime of the application (or on hot-module replacement during development)
|
||||
listeners.forEach(({ target, eventName, action, options }) => target.removeEventListener(eventName, action, options));
|
||||
}
|
||||
|
||||
// Keyboard events
|
||||
@@ -259,26 +274,80 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
|
||||
file.arrayBuffer().then((buffer): void => {
|
||||
const u8Array = new Uint8Array(buffer);
|
||||
|
||||
editor.instance.paste_image(file.type, u8Array, undefined, undefined);
|
||||
editor.instance.paste_image(file.type, u8Array);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function targetIsTextField(target: EventTarget | HTMLElement | null): boolean {
|
||||
return target instanceof HTMLElement && (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable);
|
||||
}
|
||||
// Frontend message subscriptions
|
||||
|
||||
// Event bindings
|
||||
editor.subscriptions.subscribeJsMessage(TriggerPaste, async () => {
|
||||
// In the try block, attempt to read from the Clipboard API, which may not have permission and may not be supported in all browsers
|
||||
// In the catch block, explain to the user why the paste failed and how to fix or work around the problem
|
||||
try {
|
||||
// Attempt to check if the clipboard permission is denied, and throw an error if that is the case
|
||||
// In Firefox, the `clipboard-read` permission isn't supported, so attemping to query it throws an error
|
||||
// In Safari, the entire Permissions API isn't supported, so the query never occurs and this block is skipped without an error and we assume we might have permission
|
||||
const clipboardRead = "clipboard-read" as PermissionName;
|
||||
const permission = await navigator.permissions?.query({ name: clipboardRead });
|
||||
if (permission?.state === "denied") throw new Error("Permission denied");
|
||||
|
||||
function bindListeners(): void {
|
||||
// Add event bindings for the lifetime of the application
|
||||
listeners.forEach(({ target, eventName, action, options }) => target.addEventListener(eventName, action, options));
|
||||
}
|
||||
function unbindListeners(): void {
|
||||
// Remove event bindings after the lifetime of the application (or on hot-module replacement during development)
|
||||
listeners.forEach(({ target, eventName, action, options }) => target.removeEventListener(eventName, action, options));
|
||||
}
|
||||
// Read the clipboard contents if the Clipboard API is available
|
||||
const clipboardItems = await navigator.clipboard.read();
|
||||
if (!clipboardItems) throw new Error("Clipboard API unsupported");
|
||||
|
||||
// Read any layer data or images from the clipboard
|
||||
Array.from(clipboardItems).forEach(async (item) => {
|
||||
// Read plain text and, if it is a layer, pass it to the editor
|
||||
if (item.types.includes("text/plain")) {
|
||||
const blob = await item.getType("text/plain");
|
||||
const reader = new FileReader();
|
||||
reader.onload = (): void => {
|
||||
const text = reader.result as string;
|
||||
|
||||
if (text.startsWith("graphite/layer: ")) {
|
||||
editor.instance.paste_serialized_data(text.substring(16, text.length));
|
||||
}
|
||||
};
|
||||
reader.readAsText(blob);
|
||||
}
|
||||
|
||||
// Read an image from the clipboard and pass it to the editor to be loaded
|
||||
const imageType = item.types.find((type) => type.startsWith("image/"));
|
||||
if (imageType) {
|
||||
const blob = await item.getType(imageType);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (): void => {
|
||||
const u8Array = new Uint8Array(reader.result as ArrayBuffer);
|
||||
|
||||
editor.instance.paste_image(imageType, u8Array);
|
||||
};
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const unsupported = stripIndents`
|
||||
This browser does not support reading from the clipboard.
|
||||
Use the keyboard shortcut to paste instead.
|
||||
`;
|
||||
const denied = stripIndents`
|
||||
The browser's clipboard permission has been denied.
|
||||
|
||||
Open the browser's website settings (usually accessible
|
||||
just left of the URL) to allow this permission.
|
||||
`;
|
||||
|
||||
const matchMessage = {
|
||||
"clipboard-read": unsupported,
|
||||
"Clipboard API unsupported": unsupported,
|
||||
"Permission denied": denied,
|
||||
};
|
||||
const message = Object.entries(matchMessage).find(([key]) => String(err).includes(key))?.[1] || String(err);
|
||||
|
||||
editor.instance.error_dialog("Cannot access clipboard", message);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialization
|
||||
|
||||
@@ -290,3 +359,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
|
||||
// Return the destructor
|
||||
return unbindListeners;
|
||||
}
|
||||
|
||||
function targetIsTextField(target: EventTarget | HTMLElement | null): boolean {
|
||||
return target instanceof HTMLElement && (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable);
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@ export class TriggerFileDownload extends JsMessage {
|
||||
|
||||
export class TriggerFileUpload extends JsMessage {}
|
||||
|
||||
export class TriggerPaste extends JsMessage {}
|
||||
|
||||
export class TriggerRasterDownload extends JsMessage {
|
||||
readonly document!: string;
|
||||
|
||||
@@ -584,6 +586,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||
TriggerIndexedDbRemoveDocument,
|
||||
TriggerFontLoad,
|
||||
TriggerIndexedDbWriteDocument,
|
||||
TriggerPaste,
|
||||
TriggerRasterDownload,
|
||||
TriggerTextCommit,
|
||||
TriggerTextCopy,
|
||||
|
||||
Reference in New Issue
Block a user