From a32028f29985bc327951786085f1a6c445332da2 Mon Sep 17 00:00:00 2001 From: Yu Chen Date: Fri, 31 May 2024 15:34:52 +0800 Subject: [PATCH] feat Copy Records --- package.json | 1 + src-tauri/Cargo.lock | 325 ++++++++++++++++++++++++++-- src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 4 +- src-tauri/src/main.rs | 1 + src/lib/IOSection.svelte | 32 ++- src/lib/UDP.svelte | 2 +- src/lib/contextMenu.ts | 250 +++++++++++++++++++++ 8 files changed, 591 insertions(+), 25 deletions(-) create mode 100644 src/lib/contextMenu.ts diff --git a/package.json b/package.json index c52e88d..30eb8dc 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "@kuyoonjo/tauri-plugin-appearance": "^0.4.0", "@tauri-apps/api": "2.0.0-beta.13", + "@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.3", "@tauri-apps/plugin-dialog": "2.0.0-beta.3", "@tauri-apps/plugin-fs": "2.0.0-beta.3", "@tauri-apps/plugin-shell": ">=2.0.0-beta.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c4ccebe..a2f3c4e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -62,6 +62,24 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "arboard" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89" +dependencies = [ + "clipboard-win", + "core-graphics", + "image 0.25.1", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "parking_lot", + "windows-sys 0.48.0", + "x11rb", +] + [[package]] name = "ascii" version = "1.1.0" @@ -348,6 +366,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -579,6 +603,15 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "clipboard-win" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" +dependencies = [ + "error-code", +] + [[package]] name = "cocoa" version = "0.25.0" @@ -609,6 +642,12 @@ dependencies = [ "objc", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -701,12 +740,37 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -833,6 +897,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -843,6 +916,18 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1006,6 +1091,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + [[package]] name = "event-listener" version = "2.5.3" @@ -1065,6 +1156,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1109,6 +1216,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1424,6 +1540,16 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1578,6 +1704,16 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1765,6 +1901,37 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +dependencies = [ + "bytemuck", + "byteorder", + "num-traits", + "png", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1907,6 +2074,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -1957,6 +2133,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -2220,6 +2402,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-appearance", + "tauri-plugin-clipboard-manager", "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-os", @@ -2524,6 +2707,12 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-stream" version = "0.2.0" @@ -2953,6 +3142,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-xml" version = "0.31.0" @@ -3064,6 +3262,26 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.1" @@ -3602,6 +3820,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3774,9 +4001,9 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tauri" -version = "2.0.0-beta.20" +version = "2.0.0-beta.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f44a74520aa677a4461787902b31373d431749b017862203d39df151892ecb1" +checksum = "5a258ecc5ac7ddade525f512c4962fd01cd0f5265e917b4572579c32c027bb31" dependencies = [ "anyhow", "bytes", @@ -3823,9 +4050,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.0.0-beta.16" +version = "2.0.0-beta.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a6829341dde141cd48caabd4f57463444fb1127e7e20e758088be12e866bd77" +checksum = "82b964bb6d03d97e24e12f896aab463b02a3c2ff76a60f728cc37b5548eb470e" dependencies = [ "anyhow", "cargo_toml", @@ -3845,9 +4072,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.0.0-beta.16" +version = "2.0.0-beta.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de94afa2ee0cc6a7ba99c1300b80a9ede416c54a6a68a0ad38c3ebe1d6fe547c" +checksum = "3529cfa977ed7c097f2a5e8da19ecffbe61982450a6c819e6165b6d0cfd3dd3a" dependencies = [ "base64 0.22.1", "brotli", @@ -3872,9 +4099,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.0.0-beta.16" +version = "2.0.0-beta.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305a13d6164bd76f9b407cd1b95031deb1c1a0804c686f5e332a99ee62710ac2" +checksum = "36f97dd80334f29314aa5f40b5fad10cb9feffd08e5a5324fd728613841e5d33" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3886,9 +4113,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.0.0-beta.16" +version = "2.0.0-beta.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f511a86bb648b8d6ea5841719d12966491f472734915b0d510e6eeb299f4371" +checksum = "7c8385fd0a4f661f5652b0d9e2d7256187d553bb174f88564d10ebcfa6a3af53" dependencies = [ "anyhow", "glob", @@ -3919,6 +4146,22 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "tauri-plugin-clipboard-manager" +version = "2.1.0-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76fceedc022b7590245c5bd0ba0ef5909e44480d84b72fb498b76ba8bc0da7db" +dependencies = [ + "arboard", + "image 0.24.9", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror", +] + [[package]] name = "tauri-plugin-dialog" version = "2.0.0-beta.7" @@ -3997,9 +4240,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.0.0-beta.17" +version = "2.0.0-beta.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e81dc86660cc1421af7967e71857201d8ebf114327465bde819913a27bffc1" +checksum = "d7dc96172a43536236ab55b7da7b8461bf75810985e668589e2395cb476937cb" dependencies = [ "dpi", "gtk", @@ -4016,9 +4259,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.0.0-beta.17" +version = "2.0.0-beta.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23beea1d1540fe23c33a0791aeba1054f51d84faadc07ff8637b7e5494eba9fa" +checksum = "5d4fd913b1f14a9b618c7f3ae35656d3aa759767fcb95b72006357c12b9d0b09" dependencies = [ "cocoa", "gtk", @@ -4040,16 +4283,15 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.0.0-beta.16" +version = "2.0.0-beta.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199edb0e8969f53decfb683971faa50eb8b421f9034bfa95121286e1e93beaef" +checksum = "4f24a9c20d676a3f025331cc1c3841256ba88c9f25fb7fae709d2b3089c50d90" dependencies = [ "brotli", "cargo_metadata", "ctor", "dunce", "glob", - "heck 0.5.0", "html5ever", "infer", "json-patch", @@ -4142,6 +4384,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.36" @@ -4404,14 +4657,14 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.13.5" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39240037d755a1832e752d64f99078c3b0b21c09a71c12405070c75ef4e7cd3c" +checksum = "0b27516dfcfa22a9faaf192283a122bfbede38c1e59ef194e3c4db6549b419c0" dependencies = [ "cocoa", "core-graphics", "crossbeam-channel", - "dirs-next", + "dirs", "libappindicator", "muda", "objc", @@ -4831,6 +5084,12 @@ dependencies = [ "windows-core 0.56.0", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "wfd" version = "0.1.7" @@ -5264,6 +5523,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix 0.38.34", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + [[package]] name = "xdg-home" version = "1.1.0" @@ -5397,6 +5673,15 @@ dependencies = [ "zvariant 4.1.1", ] +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zvariant" version = "3.15.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5a05656..11baee4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,6 +16,7 @@ tauri-plugin-shell = "2.0.0-beta.5" tauri-plugin-os = "2.0.0-beta.4" tauri-plugin-fs = "2.0.0-beta.7" tauri-plugin-dialog = "2.0.0-beta.7" +tauri-plugin-clipboard-manager = "2.1.0-beta.4" tauri-plugin-appearance = "0.4.0" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 731297f..13fa90a 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -13,6 +13,8 @@ "menu:default", "tray:default", "shell:allow-open", - "appearance:default" + "appearance:default", + "clipboard-manager:allow-write-html", + "clipboard-manager:allow-write-text" ] } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e4033e0..b609be0 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -112,6 +112,7 @@ fn main() { .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_appearance::init(ctx.config_mut())) .invoke_handler(tauri::generate_handler![ open_save_dialog, diff --git a/src/lib/IOSection.svelte b/src/lib/IOSection.svelte index 17c6c85..ce09c17 100644 --- a/src/lib/IOSection.svelte +++ b/src/lib/IOSection.svelte @@ -6,13 +6,15 @@ import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs"; import { downloadDir } from "@tauri-apps/api/path"; import moment from "moment"; - import { createEventDispatcher, onMount } from "svelte"; + import { createEventDispatcher, onDestroy, onMount } from "svelte"; import type { IFunc, IParser } from "./IO"; + import { ContextMenu } from "./contextMenu"; + import { writeText } from "@tauri-apps/plugin-clipboard-manager"; export let id: string; export let input = ""; export let output: string[] = []; - let maxOutputStr = '50'; + let maxOutputStr = "50"; let maxOutput = 50; function refreshMaxOutput() { maxOutput = Number(maxOutputStr) || 50; @@ -215,6 +217,20 @@ } } + let outputEl: HTMLDivElement; + const contextmenu = new ContextMenu([ + { + text: "Copy Records", + onclick() { + console.log(outputEl.innerText) + writeText(outputEl.innerText); + }, + }, + ]); + async function onContextmenu(e: MouseEvent) { + await contextmenu.open(e); + } + const ed = createEventDispatcher<{ ready: void }>(); onMount(async () => { @@ -223,6 +239,10 @@ ed("ready"); }); + onDestroy(() => { + contextmenu.destroy(); + }); + export const IOHandler = { addOutput, }; @@ -283,7 +303,13 @@
-
+ +
{#each output as item}
{@html item}
{/each} diff --git a/src/lib/UDP.svelte b/src/lib/UDP.svelte index f3178fb..1179686 100644 --- a/src/lib/UDP.svelte +++ b/src/lib/UDP.svelte @@ -57,7 +57,7 @@ async function bind() { const ok = await invoke("udp_bind", { id: windowId, bindAt: local }); if (ok) IOHandler.addOutput(`UDP bond at ${local}`); - else IOHandler.addOutput(`UDP failed to bind at ${local}`); + else return IOHandler.addOutput(`UDP failed to bind at ${local}`); if (!localItems.includes(local)) { exLocalItems.unshift(local); storage.set(k_exLocalItems, exLocalItems); diff --git a/src/lib/contextMenu.ts b/src/lib/contextMenu.ts new file mode 100644 index 0000000..515b82d --- /dev/null +++ b/src/lib/contextMenu.ts @@ -0,0 +1,250 @@ +import { get } from "svelte/store"; + +export interface IContextMenuItem { + text: string; + hotkey?: string; + disabled?: boolean; + submenu?: ContextMenu; + onclick?: (e: IContextMenuItemClickEvent) => void; +} + +export interface IContextMenuItemClickEvent { + handled: false, + item: HTMLElement; + label: HTMLElement; + hotkey: HTMLElement; + items: IContextMenuItem[]; + data: IContextMenuItem; +} + +export class ContextMenu { + private static hideAlls: { + key: symbol; + cb: () => void; + }[] = []; + + static hideAll() { + for (const v of ContextMenu.hideAlls) + v.cb(); + } + + private key = Symbol(); + private container = document.body; + private dom: HTMLElement | undefined; + private root = true; + private parent: ContextMenu | undefined; + private shown = false; + private submenus: ContextMenu[] = []; + + + private _onclick = (e: MouseEvent & { target: HTMLElement }) => { + if (this.dom && e.target != this.dom && + e.target.parentElement && + e.target.parentElement != this.dom && + !e.target.classList.contains('item') && + !e.target.parentElement.classList.contains('item')) { + this.hideAll(); + } + }; + + private _onblur = () => { + this.hideAll(); + }; + + constructor( + public items: IContextMenuItem[], + private preventHideAfterItemClick = false, + ) { + this.container.addEventListener('click', this._onclick as any); + this.container.addEventListener('contextmenu', this._onclick as any); + window.addEventListener('blur', this._onblur); + ContextMenu.hideAlls.push({ key: this.key, cb: this._onblur }); + } + + private getMenuDom() { + const menu = document.createElement('div'); + menu.classList.add('context'); + + for (const item of this.items) { + menu.appendChild(this.itemToDomEl(item)); + } + + return menu; + } + + private itemToDomEl(data: IContextMenuItem) { + const item = document.createElement('div'); + + if (!data.text) { + item.className = 'separator'; + return item; + } + + // if (data.hasOwnProperty('color') && /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(data.color.toString())) { + // item.style.cssText = `color: ${data.color}`; + // } + + item.classList.add('item'); + + const label = document.createElement('span'); + label.className = 'label'; + label.innerHTML = data.text; + item.appendChild(label); + + if (data.disabled) { + item.classList.add('disabled'); + } else { + item.classList.add('enabled'); + } + + const hotkey = document.createElement('span'); + hotkey.className = 'hotkey'; + hotkey.innerHTML = data.hotkey || ''; + item.appendChild(hotkey); + + if (data.submenu) { + const menu = data.submenu; + menu.root = false; + menu.parent = this; + + const openSubItems = () => { + if (data.hasOwnProperty('disabled') && data['disabled'] == true) + return; + + this.hideSubMenus(); + + const x = this.dom!.offsetLeft + this.dom!.clientWidth + item.offsetLeft; + const y = this.dom!.offsetTop + item.offsetTop; + + if (!menu.shown) { + menu.show(x, y); + } else { + menu.hide(); + } + }; + + this.submenus.push(menu); + + item.classList.add('has-subitems'); + item.addEventListener('click', openSubItems); + item.addEventListener('mousemove', openSubItems); + } else { + item.addEventListener('click', e => { + this.hideSubMenus(); + + if (item.classList.contains('disabled')) + return; + + if (data.onclick && !data.disabled) { + const event: IContextMenuItemClickEvent = { + handled: false, + item: item, + label: label, + hotkey: hotkey, + items: this.items, + data: data + }; + + data.onclick(event); + + if (!event.handled && !this.preventHideAfterItemClick) { + this.hide(); + } + } else { + if (!this.preventHideAfterItemClick) + this.hide(); + } + }); + + item.addEventListener('mousemove', e => { + this.hideSubMenus(); + }); + } + + return item; + } + + private hideAll() { + if (this.root && !this.parent) { + if (this.shown) { + this.hideSubMenus(); + + this.shown = false; + this.container.removeChild(this.dom!); + + // if (this.parent && this.parent.shown) { + // this.parent.hide(); + // } + } + + return; + } + + this.parent!.hide(); + } + + private hide() { + if (this.dom && this.shown) { + this.shown = false; + this.hideSubMenus(); + this.container.removeChild(this.dom); + + if (this.parent && this.parent.shown) { + this.parent.hide(); + } + } + } + + private hideSubMenus() { + for (const menu of this.submenus) { + if (menu.shown) { + menu.shown = false; + menu.container.removeChild(menu.dom!); + } + menu.hideSubMenus(); + } + } + + private show(x: number, y: number) { + this.dom = this.getMenuDom(); + this.container.appendChild(this.dom); + setTimeout(() => { + const rect = this.dom!.getClientRects()[0]; + const offset = 8; + const maxWidth = window.innerWidth - offset; + const maxHeight = window.innerHeight - offset; + if (x + rect.width > maxWidth) + x = x - rect.width; + if (y + rect.height > maxHeight) + y = maxHeight - rect.height; + + this.dom!.style.left = `${x}px`; + this.dom!.style.top = `${y}px`; + + this.shown = true; + }, 0); + } + + open(mouseEvent: MouseEvent) { + mouseEvent.preventDefault(); + if (!this.items.length) return; + let e = mouseEvent as MouseEvent & { target: HTMLElement }; + if (e.target != this.dom && + e.target.parentElement && + e.target.parentElement != this.dom && + !e.target.classList.contains('item') && + !e.target.parentElement.classList.contains('item')) { + ContextMenu.hideAll(); + this.show(e.clientX, e.clientY); + } + }; + + destroy() { + delete this.dom; + this.container.removeEventListener('click', this._onclick as any); + this.container.removeEventListener('contextmenu', this._onclick as any); + window.removeEventListener('blur', this._onblur); + const exi = ContextMenu.hideAlls.findIndex(v => v.key === this.key); + if (~exi) ContextMenu.hideAlls.splice(exi, 1); + } +} \ No newline at end of file