mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
Start work on copy & paste handling
This commit is contained in:
parent
545d3dbfed
commit
99396e5811
9 changed files with 496 additions and 42 deletions
|
@ -31,6 +31,9 @@ service FrontendService {
|
|||
|
||||
// Editor
|
||||
rpc editorUpdateNote(notes.UpdateNotesRequest) returns (generic.Empty);
|
||||
rpc convertPastedImage(ConvertPastedImageRequest)
|
||||
returns (ConvertPastedImageResponse);
|
||||
rpc retrieveUrl(generic.String) returns (RetrieveUrlResponse);
|
||||
|
||||
// Profile config
|
||||
rpc GetProfileConfigJson(generic.String) returns (generic.Json);
|
||||
|
@ -53,6 +56,20 @@ message SetSchedulingStatesRequest {
|
|||
scheduler.SchedulingStates states = 2;
|
||||
}
|
||||
|
||||
message ConvertPastedImageRequest {
|
||||
bytes data = 1;
|
||||
string ext = 2;
|
||||
}
|
||||
|
||||
message ConvertPastedImageResponse {
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
message RetrieveUrlResponse {
|
||||
string filename = 1;
|
||||
string error = 2;
|
||||
}
|
||||
|
||||
message SetSettingJsonRequest {
|
||||
string key = 1;
|
||||
bytes value_json = 2;
|
||||
|
|
|
@ -31,7 +31,6 @@ import aqt.sound
|
|||
from anki._legacy import deprecated
|
||||
from anki.cards import Card
|
||||
from anki.collection import Config
|
||||
from anki.consts import MODEL_CLOZE
|
||||
from anki.hooks import runFilter
|
||||
from anki.httpclient import HttpClient
|
||||
from anki.models import NotetypeDict, NotetypeId, StockNotetype
|
||||
|
@ -173,6 +172,7 @@ class Editor:
|
|||
def setupWeb(self) -> None:
|
||||
editor_key = self.mw.pm.editor_key(self.editorMode)
|
||||
self.web.load_sveltekit_page(f"editor/?mode={editor_key}")
|
||||
self.web.allow_drops = True
|
||||
|
||||
def _set_ready(self) -> None:
|
||||
lefttopbtns: list[str] = []
|
||||
|
@ -1124,37 +1124,37 @@ class EditorWebView(AnkiWebView):
|
|||
def onMiddleClickPaste(self) -> None:
|
||||
self._onPaste(QClipboard.Mode.Selection)
|
||||
|
||||
def dragEnterEvent(self, evt: QDragEnterEvent | None) -> None:
|
||||
assert evt is not None
|
||||
evt.accept()
|
||||
# def dragEnterEvent(self, evt: QDragEnterEvent | None) -> None:
|
||||
# assert evt is not None
|
||||
# evt.accept()
|
||||
|
||||
def dropEvent(self, evt: QDropEvent | None) -> None:
|
||||
assert evt is not None
|
||||
extended = self._wantsExtendedPaste()
|
||||
mime = evt.mimeData()
|
||||
assert mime is not None
|
||||
# def dropEvent(self, evt: QDropEvent | None) -> None:
|
||||
# assert evt is not None
|
||||
# extended = self._wantsExtendedPaste()
|
||||
# mime = evt.mimeData()
|
||||
# assert mime is not None
|
||||
|
||||
if (
|
||||
self.editor.state is EditorState.IO_PICKER
|
||||
and (html := self._processUrls(mime, allowed_suffixes=pics))
|
||||
and (path := self.editor.extract_img_path_from_html(html))
|
||||
):
|
||||
self.editor.setup_mask_editor(path)
|
||||
return
|
||||
# if (
|
||||
# self.editor.state is EditorState.IO_PICKER
|
||||
# and (html := self._processUrls(mime, allowed_suffixes=pics))
|
||||
# and (path := self.editor.extract_img_path_from_html(html))
|
||||
# ):
|
||||
# self.editor.setup_mask_editor(path)
|
||||
# return
|
||||
|
||||
evt_pos = evt.position()
|
||||
cursor_pos = QPoint(int(evt_pos.x()), int(evt_pos.y()))
|
||||
# evt_pos = evt.position()
|
||||
# cursor_pos = QPoint(int(evt_pos.x()), int(evt_pos.y()))
|
||||
|
||||
if evt.source() and mime.hasHtml():
|
||||
# don't filter html from other fields
|
||||
html, internal = mime.html(), True
|
||||
else:
|
||||
html, internal = self._processMime(mime, extended, drop_event=True)
|
||||
# if evt.source() and mime.hasHtml():
|
||||
# # don't filter html from other fields
|
||||
# html, internal = mime.html(), True
|
||||
# else:
|
||||
# html, internal = self._processMime(mime, extended, drop_event=True)
|
||||
|
||||
if not html:
|
||||
return
|
||||
# if not html:
|
||||
# return
|
||||
|
||||
self.editor.doDrop(html, internal, extended, cursor_pos)
|
||||
# self.editor.doDrop(html, internal, extended, cursor_pos)
|
||||
|
||||
# returns (html, isInternal)
|
||||
def _processMime(
|
||||
|
|
|
@ -16,7 +16,7 @@ from collections.abc import Callable
|
|||
from dataclasses import dataclass
|
||||
from errno import EPROTOTYPE
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import flask
|
||||
import flask_cors
|
||||
|
@ -665,6 +665,34 @@ def set_config_json() -> bytes:
|
|||
return set_setting_json(aqt.mw.col.set_config)
|
||||
|
||||
|
||||
def convert_pasted_image() -> bytes:
|
||||
req = frontend_pb2.ConvertPastedImageRequest()
|
||||
req.ParseFromString(request.data)
|
||||
image = QImage.fromData(req.data)
|
||||
buffer = QBuffer()
|
||||
buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
||||
if req.ext == "png":
|
||||
quality = 50
|
||||
else:
|
||||
quality = 80
|
||||
image.save(buffer, req.ext, quality)
|
||||
buffer.reset()
|
||||
data = bytes(cast(bytes, buffer.readAll()))
|
||||
return frontend_pb2.ConvertPastedImageResponse(data=data).SerializeToString()
|
||||
|
||||
|
||||
def retrieve_url() -> bytes:
|
||||
from aqt.utils import retrieve_url
|
||||
|
||||
req = generic_pb2.String()
|
||||
req.ParseFromString(request.data)
|
||||
url = req.val
|
||||
filename, error = retrieve_url(url)
|
||||
return frontend_pb2.RetrieveUrlResponse(
|
||||
filename=filename, error=error
|
||||
).SerializeToString()
|
||||
|
||||
|
||||
post_handler_list = [
|
||||
congrats_info,
|
||||
get_deck_configs_for_update,
|
||||
|
@ -686,6 +714,8 @@ post_handler_list = [
|
|||
get_meta_json,
|
||||
set_meta_json,
|
||||
get_config_json,
|
||||
convert_pasted_image,
|
||||
retrieve_url,
|
||||
]
|
||||
|
||||
|
||||
|
@ -739,6 +769,9 @@ exposed_backend_list = [
|
|||
"decode_iri_paths",
|
||||
# ConfigService
|
||||
"set_config_json",
|
||||
"get_config_bool",
|
||||
# MediaService
|
||||
"add_media_file",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -9,16 +9,19 @@ import re
|
|||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib
|
||||
from collections.abc import Callable, Sequence
|
||||
from functools import partial, wraps
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal, Union
|
||||
|
||||
import requests
|
||||
from send2trash import send2trash
|
||||
|
||||
import aqt
|
||||
from anki._legacy import DeprecatedNamesMixinForModule
|
||||
from anki.collection import Collection, HelpPage
|
||||
from anki.httpclient import HttpClient
|
||||
from anki.lang import TR, tr_legacyglobal # pylint: disable=unused-import
|
||||
from anki.utils import (
|
||||
call,
|
||||
|
@ -134,6 +137,49 @@ def openLink(link: str | QUrl) -> None:
|
|||
QDesktopServices.openUrl(QUrl(link))
|
||||
|
||||
|
||||
def retrieve_url(url: str) -> tuple[str, str]:
|
||||
"Download file into media folder and return local filename or None."
|
||||
|
||||
local = url.lower().startswith("file://")
|
||||
content_type = None
|
||||
error_msg: str | None = None
|
||||
try:
|
||||
if local:
|
||||
# urllib doesn't understand percent-escaped utf8, but requires things like
|
||||
# '#' to be escaped.
|
||||
url = urllib.parse.unquote(url)
|
||||
url = url.replace("%", "%25")
|
||||
url = url.replace("#", "%23")
|
||||
req = urllib.request.Request(
|
||||
url, None, {"User-Agent": "Mozilla/5.0 (compatible; Anki)"}
|
||||
)
|
||||
with urllib.request.urlopen(req) as response:
|
||||
filecontents = response.read()
|
||||
else:
|
||||
with HttpClient() as client:
|
||||
client.timeout = 30
|
||||
with client.get(url) as response:
|
||||
if response.status_code != 200:
|
||||
error_msg = tr.qt_misc_unexpected_response_code(
|
||||
val=response.status_code,
|
||||
)
|
||||
return "", error_msg
|
||||
filecontents = response.content
|
||||
content_type = response.headers.get("content-type")
|
||||
except (urllib.error.URLError, requests.exceptions.RequestException) as e:
|
||||
error_msg = tr.editing_an_error_occurred_while_opening(val=str(e))
|
||||
return "", error_msg
|
||||
# strip off any query string
|
||||
url = re.sub(r"\?.*?$", "", url)
|
||||
fname = os.path.basename(urllib.parse.unquote(url))
|
||||
if not fname.strip():
|
||||
fname = "paste"
|
||||
if content_type:
|
||||
fname = aqt.mw.col.media.add_extension_based_on_mime(fname, content_type)
|
||||
|
||||
return aqt.mw.col.media.write_data(fname, filecontents), ""
|
||||
|
||||
|
||||
class MessageBox(QMessageBox):
|
||||
def __init__(
|
||||
self,
|
||||
|
|
|
@ -34,6 +34,12 @@ const allow = (attrs: string[]): FilterMethod => (element: Element): void =>
|
|||
element,
|
||||
);
|
||||
|
||||
function convertToDiv(element: Element): void {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = element.innerHTML;
|
||||
element.replaceWith(div);
|
||||
}
|
||||
|
||||
function unwrapElement(element: Element): void {
|
||||
element.replaceWith(...element.childNodes);
|
||||
}
|
||||
|
@ -50,7 +56,7 @@ const tagsAllowedBasic: TagsAllowed = {
|
|||
BR: allowNone,
|
||||
IMG: allow(["SRC", "ALT"]),
|
||||
DIV: allowNone,
|
||||
P: allowNone,
|
||||
P: convertToDiv,
|
||||
SUB: allowNone,
|
||||
SUP: allowNone,
|
||||
TITLE: removeElement,
|
||||
|
|
|
@ -33,6 +33,11 @@ const outputHTMLProcessors: Record<FilterMode, (outputHTML: string) => string> =
|
|||
};
|
||||
|
||||
export function filterHTML(html: string, internal: boolean, extended: boolean): string {
|
||||
// https://anki.tenderapp.com/discussions/ankidesktop/39543-anki-is-replacing-the-character-by-when-i-exit-the-html-edit-mode-ctrlshiftx
|
||||
if (html.indexOf(">") < 0) {
|
||||
return html;
|
||||
}
|
||||
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = html;
|
||||
|
||||
|
|
|
@ -861,6 +861,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
</script>
|
||||
|
||||
<!-- Block Qt's default drag & drop behavior -->
|
||||
<svelte:body
|
||||
on:dragenter|preventDefault
|
||||
on:dragover|preventDefault
|
||||
on:drop|preventDefault
|
||||
/>
|
||||
|
||||
<!--
|
||||
@component
|
||||
Serves as a pre-slotted convenience component which combines all the common
|
||||
|
|
346
ts/routes/editor/rich-text-input/data-transfer.ts
Normal file
346
ts/routes/editor/rich-text-input/data-transfer.ts
Normal file
|
@ -0,0 +1,346 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { ConfigKey_Bool } from "@generated/anki/config_pb";
|
||||
import { addMediaFile, convertPastedImage, getConfigBool, retrieveUrl as retrieveUrlBackend } from "@generated/backend";
|
||||
import { bridgeCommand } from "@tslib/bridgecommand";
|
||||
import { shiftPressed } from "@tslib/keys";
|
||||
import { pasteHTML } from "../old-editor-adapter";
|
||||
|
||||
type ImageData = string | Uint8Array;
|
||||
const imageSuffixes = ["jpg", "jpeg", "png", "gif", "svg", "webp", "ico", "avif"];
|
||||
const audioSuffixes = [
|
||||
"3gp",
|
||||
"aac",
|
||||
"avi",
|
||||
"flac",
|
||||
"flv",
|
||||
"m4a",
|
||||
"mkv",
|
||||
"mov",
|
||||
"mp3",
|
||||
"mp4",
|
||||
"mpeg",
|
||||
"mpg",
|
||||
"oga",
|
||||
"ogg",
|
||||
"ogv",
|
||||
"ogx",
|
||||
"opus",
|
||||
"spx",
|
||||
"swf",
|
||||
"wav",
|
||||
"webm",
|
||||
];
|
||||
const mediaSuffixes = [...imageSuffixes, ...audioSuffixes];
|
||||
|
||||
function imageDataToUint8Array(data: ImageData): Uint8Array {
|
||||
return typeof data === "string" ? new TextEncoder().encode(data) : data;
|
||||
}
|
||||
|
||||
// TODO
|
||||
async function _wantsExtendedPaste(event: MouseEvent | KeyboardEvent): Promise<boolean> {
|
||||
let stripHtml = (await getConfigBool({
|
||||
key: ConfigKey_Bool.PASTE_STRIPS_FORMATTING,
|
||||
})).val;
|
||||
if (shiftPressed(event)) {
|
||||
stripHtml = !stripHtml;
|
||||
}
|
||||
return !stripHtml;
|
||||
}
|
||||
|
||||
function escapeHtml(text: string, quote = true): string {
|
||||
text = text
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
if (quote) {
|
||||
text = text.replaceAll("\"", """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
function getUrls(data: DataTransfer): string[] {
|
||||
const urls = data.getData("text/uri-list").split("\n");
|
||||
return urls[0] ? urls : [];
|
||||
}
|
||||
|
||||
function getText(data: DataTransfer): string {
|
||||
return data.getData("text/plain") ?? "";
|
||||
}
|
||||
|
||||
const QIMAGE_FORMATS = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/svg+xml",
|
||||
"image/bmp",
|
||||
"image/x-portable-bitmap",
|
||||
"image/x-portable-graymap",
|
||||
"image/x-portable-pixmap",
|
||||
"image/x-xbitmap",
|
||||
"image/x-xpixmap",
|
||||
];
|
||||
|
||||
async function getImageData(data: DataTransfer): Promise<ImageData | null> {
|
||||
for (const type of QIMAGE_FORMATS) {
|
||||
const image = data.getData(type);
|
||||
if (image) {
|
||||
return image;
|
||||
} else {
|
||||
for (const file of data.files ?? []) {
|
||||
if (file.type === type) {
|
||||
return new Uint8Array(await file.arrayBuffer());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function retrieveUrl(url: string): Promise<string | null> {
|
||||
const response = await retrieveUrlBackend({ val: url });
|
||||
if (response.error) {
|
||||
alert(response.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.filename;
|
||||
}
|
||||
|
||||
async function urlToFile(url: string): Promise<string | null> {
|
||||
const lowerUrl = url.toLowerCase();
|
||||
for (const suffix of mediaSuffixes) {
|
||||
if (lowerUrl.endsWith(`.${suffix}`)) {
|
||||
return await retrieveUrl(url);
|
||||
}
|
||||
}
|
||||
// Not a supported type
|
||||
return null;
|
||||
}
|
||||
|
||||
function filenameToLink(filename: string): string {
|
||||
const filenameParts = filename.split(".");
|
||||
const ext = filenameParts[filenameParts.length - 1].toLowerCase();
|
||||
if (imageSuffixes.includes(ext)) {
|
||||
return `<img src="${encodeURI(filename)}">`;
|
||||
} else {
|
||||
return `[sound:${escapeHtml(filename, false)}]`;
|
||||
}
|
||||
}
|
||||
|
||||
async function urlToLink(url: string): Promise<string> {
|
||||
const filename = await urlToFile(url);
|
||||
if (!filename) {
|
||||
const escapedTitle = escapeHtml(decodeURI(url));
|
||||
return `<a href="${url}">${escapedTitle}</a>`;
|
||||
}
|
||||
return filenameToLink(filename);
|
||||
}
|
||||
|
||||
async function checksum(data: string | Uint8Array): Promise<string> {
|
||||
const bytes = imageDataToUint8Array(data);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-1", bytes);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
|
||||
return hashHex;
|
||||
}
|
||||
|
||||
async function addMediaFromData(filename: string, data: ImageData): Promise<string> {
|
||||
filename = (await addMediaFile({
|
||||
desiredName: filename,
|
||||
data: imageDataToUint8Array(data),
|
||||
})).val;
|
||||
return filenameToLink(filename);
|
||||
}
|
||||
|
||||
async function pastedImageFilename(data: ImageData, ext: string): Promise<string> {
|
||||
const csum = await checksum(data);
|
||||
return `paste-${csum}.${ext}`;
|
||||
}
|
||||
|
||||
async function addPastedImage(data: ImageData, ext: string, convert = false): Promise<string> {
|
||||
const filename = await pastedImageFilename(data, ext);
|
||||
if (convert) {
|
||||
data = (await convertPastedImage({ data: imageDataToUint8Array(data), ext })).data;
|
||||
}
|
||||
return await addMediaFromData(filename, data);
|
||||
}
|
||||
|
||||
async function inlinedImageToFilename(src: string): Promise<string> {
|
||||
const prefix = "data:image/";
|
||||
const suffix = ";base64,";
|
||||
for (let ext of ["jpg", "jpeg", "png", "gif"]) {
|
||||
const fullPrefix = prefix + ext + suffix;
|
||||
if (src.startsWith(fullPrefix)) {
|
||||
const b64data = src.slice(fullPrefix.length).trim();
|
||||
const data = atob(b64data);
|
||||
if (ext === "jpeg") {
|
||||
ext = "jpg";
|
||||
}
|
||||
return await addPastedImage(data, ext);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function inlinedImageToLink(src: string): Promise<string> {
|
||||
const filename = await inlinedImageToFilename(src);
|
||||
if (filename) {
|
||||
return filenameToLink(filename);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function isURL(s: string): boolean {
|
||||
s = s.toLowerCase();
|
||||
const prefixes = ["http://", "https://", "ftp://", "file://"];
|
||||
return prefixes.some(prefix => s.startsWith(prefix));
|
||||
}
|
||||
|
||||
async function processUrls(data: DataTransfer, _extended: boolean): Promise<string | null> {
|
||||
const urls = getUrls(data);
|
||||
if (urls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let text = "";
|
||||
for (let url of urls) {
|
||||
// Chrome likes to give us the URL twice with a \n
|
||||
const lines = url.split("\n");
|
||||
url = lines[0];
|
||||
text += await urlToLink(url);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
async function processImages(data: DataTransfer, _extended: boolean): Promise<string | null> {
|
||||
const image = await getImageData(data);
|
||||
if (!image) {
|
||||
return null;
|
||||
}
|
||||
let ext: string;
|
||||
if (await getConfigBool({ key: ConfigKey_Bool.PASTE_IMAGES_AS_PNG })) {
|
||||
ext = "png";
|
||||
} else {
|
||||
ext = "jpg";
|
||||
}
|
||||
return await addPastedImage(image, ext, true);
|
||||
}
|
||||
|
||||
async function processText(data: DataTransfer, extended: boolean): Promise<string | null> {
|
||||
function replaceSpaces(match: string, p1: string): string {
|
||||
return `${p1.replaceAll(" ", " ")} `;
|
||||
}
|
||||
|
||||
const text = getText(data);
|
||||
if (text.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const processed: string[] = [];
|
||||
for (const line of text.split("\n")) {
|
||||
for (let token of line.split(/(\S+)/g)) {
|
||||
// Inlined data in base64?
|
||||
if (extended && token.startsWith("data:image/")) {
|
||||
processed.push(await inlinedImageToLink(token));
|
||||
} else if (extended && isURL(token)) {
|
||||
// If the user is pasting an image or sound link, convert it to local, otherwise paste as a hyperlink
|
||||
processed.push(await urlToLink(token));
|
||||
} else {
|
||||
token = escapeHtml(token).replaceAll("\t", " ".repeat(4));
|
||||
|
||||
// If there's more than one consecutive space,
|
||||
// use non-breaking spaces for the second one on
|
||||
token = token.replace(/ ( +)/g, replaceSpaces);
|
||||
processed.push(token);
|
||||
}
|
||||
}
|
||||
processed.push("<br>");
|
||||
}
|
||||
processed.pop();
|
||||
return processed.join("");
|
||||
}
|
||||
|
||||
async function processClipboardData(data: DataTransfer, extended = false): Promise<string | null> {
|
||||
const html = data.getData("text/html");
|
||||
if (html) {
|
||||
return html;
|
||||
}
|
||||
const urls = getUrls(data);
|
||||
let handlers: ((data: DataTransfer, extended: boolean) => Promise<string | null>)[];
|
||||
if (urls.length > 0 && urls[0].startsWith("file://")) {
|
||||
handlers = [processUrls, processImages, processText];
|
||||
} else {
|
||||
handlers = [processImages, processUrls, processText];
|
||||
}
|
||||
|
||||
for (const handler of handlers) {
|
||||
const html = await handler(data, extended);
|
||||
if (html) {
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function runPreFilter(html: string, internal = false): Promise<string> {
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = html;
|
||||
const content = template.content;
|
||||
for (const img of content.querySelectorAll("img")) {
|
||||
let src = img.getAttribute("src");
|
||||
if (!src) {
|
||||
continue;
|
||||
}
|
||||
// In internal pastes, rewrite mediasrv references to relative
|
||||
if (internal) {
|
||||
const match = /http:\/\/127\.0\.0\.1:\d+\/(.*)$/.exec(src);
|
||||
if (match) {
|
||||
src = match[1];
|
||||
}
|
||||
} else {
|
||||
if (isURL(src)) {
|
||||
const filename = await retrieveUrl(src);
|
||||
if (filename) {
|
||||
src = filename;
|
||||
}
|
||||
} else if (src.startsWith("data:image/")) {
|
||||
src = await inlinedImageToFilename(src);
|
||||
}
|
||||
}
|
||||
img.src = src;
|
||||
}
|
||||
return template.innerHTML;
|
||||
}
|
||||
|
||||
export async function handlePaste(event: ClipboardEvent) {
|
||||
// bridgeCommand("paste");
|
||||
event.preventDefault();
|
||||
const data = event.clipboardData!;
|
||||
let html = await processClipboardData(data, true);
|
||||
if (html) {
|
||||
html = await runPreFilter(html);
|
||||
pasteHTML(html, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
const data = event.dataTransfer!;
|
||||
let html = await processClipboardData(data, true);
|
||||
if (html) {
|
||||
html = await runPreFilter(html);
|
||||
pasteHTML(html, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleDragover(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
export function handleCutOrCopy() {
|
||||
bridgeCommand("cutOrCopy");
|
||||
}
|
|
@ -1,29 +1,23 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { bridgeCommand } from "@tslib/bridgecommand";
|
||||
import { on } from "@tslib/events";
|
||||
import { promiseWithResolver } from "@tslib/promise";
|
||||
import { handleCutOrCopy, handleDragover, handleDrop, handlePaste } from "./data-transfer";
|
||||
|
||||
function bridgeCopyPasteCommands(input: HTMLElement): { destroy(): void } {
|
||||
function onPaste(event: Event): void {
|
||||
event.preventDefault();
|
||||
bridgeCommand("paste");
|
||||
}
|
||||
|
||||
function onCutOrCopy(): void {
|
||||
bridgeCommand("cutOrCopy");
|
||||
}
|
||||
|
||||
const removePaste = on(input, "paste", onPaste);
|
||||
const removeCopy = on(input, "copy", onCutOrCopy);
|
||||
const removeCut = on(input, "cut", onCutOrCopy);
|
||||
|
||||
const removePaste = on(input, "paste", handlePaste);
|
||||
const removeCopy = on(input, "copy", handleCutOrCopy);
|
||||
const removeCut = on(input, "cut", handleCutOrCopy);
|
||||
const removeDragover = on(input, "dragover", handleDragover);
|
||||
const removeDrop = on(input, "drop", handleDrop);
|
||||
return {
|
||||
destroy() {
|
||||
removePaste();
|
||||
removeCopy();
|
||||
removeCut();
|
||||
removeDragover();
|
||||
removeDrop();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue