feat: restore Wolai workspace navigation and assets
This commit is contained in:
+1
-1
@@ -67,8 +67,8 @@ export interface InitOutput {
|
||||
readonly intounderlyingsource_cancel: (a: number) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12: (a: number, b: number, c: any) => [number, number];
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h0accad0f5d87788a: (a: number, b: number, c: any, d: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h87a7c2758b5f97bf: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9_3: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h79038d3430a7951a: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h67ea3224860feb45: (a: number, b: number) => void;
|
||||
|
||||
+61
-24
@@ -33,11 +33,12 @@ import { register_text_style } from './snippets/leptos-tiptap-c355e6ec24c3df4c/s
|
||||
import { register_toc_node } from './snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_toc_node.js';
|
||||
import { register_underline } from './snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_underline.js';
|
||||
import { register_youtube } from './snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_youtube.js';
|
||||
import { find_mnote_block_anchor, write_mnote_text_to_clipboard } from './snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js';
|
||||
import { download_mnote_image_url, find_mnote_block_anchor, open_mnote_page_block_href, write_mnote_text_to_clipboard } from './snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js';
|
||||
import * as import1 from "./snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js"
|
||||
import * as import2 from "./snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js"
|
||||
import * as import3 from "./snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js"
|
||||
import * as import3 from "./snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js"
|
||||
import * as import4 from "./snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js"
|
||||
import * as import5 from "./snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js"
|
||||
|
||||
|
||||
export class IntoUnderlyingByteSource {
|
||||
@@ -314,8 +315,13 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.anchorNode;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
},
|
||||
__wbg_assign_d4fed0f8abb71719: function() { return handleError(function (arg0, arg1, arg2) {
|
||||
arg0.assign(getStringFromWasm0(arg1, arg2));
|
||||
__wbg_appendChild_e95c8b3b936d250a: function() { return handleError(function (arg0, arg1) {
|
||||
const ret = arg0.appendChild(arg1);
|
||||
return ret;
|
||||
}, arguments); },
|
||||
__wbg_attachShadow_a3865128b06993cf: function() { return handleError(function (arg0, arg1) {
|
||||
const ret = arg0.attachShadow(arg1);
|
||||
return ret;
|
||||
}, arguments); },
|
||||
__wbg_blur_583010b6b4026c5d: function() { return handleError(function (arg0) {
|
||||
arg0.blur();
|
||||
@@ -482,6 +488,10 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.done;
|
||||
return ret;
|
||||
},
|
||||
__wbg_download_mnote_image_url_f3cfdff8b9b428d4: function(arg0, arg1, arg2, arg3) {
|
||||
const ret = download_mnote_image_url(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3));
|
||||
return ret;
|
||||
},
|
||||
__wbg_elementFromPoint_69f15053cea2ece4: function(arg0, arg1, arg2) {
|
||||
const ret = arg0.elementFromPoint(arg1, arg2);
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
@@ -738,6 +748,10 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.item(arg1 >>> 0);
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
},
|
||||
__wbg_item_aa5667bdc8374f43: function(arg0, arg1) {
|
||||
const ret = arg0.item(arg1 >>> 0);
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
},
|
||||
__wbg_iterator_013bc09ec998c2a7: function() {
|
||||
const ret = Symbol.iterator;
|
||||
return ret;
|
||||
@@ -769,6 +783,10 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.length;
|
||||
return ret;
|
||||
},
|
||||
__wbg_length_c157a50ce35f2f72: function(arg0) {
|
||||
const ret = arg0.length;
|
||||
return ret;
|
||||
},
|
||||
__wbg_localStorage_f5f66b1ffd2486bc: function() { return handleError(function (arg0) {
|
||||
const ret = arg0.localStorage;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
@@ -847,6 +865,10 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.ok;
|
||||
return ret;
|
||||
},
|
||||
__wbg_open_mnote_page_block_href_a512603dfc2e4bad: function(arg0, arg1) {
|
||||
const ret = open_mnote_page_block_href(getStringFromWasm0(arg0, arg1));
|
||||
return ret;
|
||||
},
|
||||
__wbg_origin_1f038926109a2a37: function() { return handleError(function (arg0, arg1) {
|
||||
const ret = arg1.origin;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
@@ -882,6 +904,10 @@ function __wbg_get_imports() {
|
||||
__wbg_prototypesetcall_a6b02eb00b0f4ce2: function(arg0, arg1, arg2) {
|
||||
Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
|
||||
},
|
||||
__wbg_querySelectorAll_e9e3fbd41310476e: function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.querySelectorAll(getStringFromWasm0(arg1, arg2));
|
||||
return ret;
|
||||
}, arguments); },
|
||||
__wbg_querySelector_12b6c7cdf26a3483: function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.querySelector(getStringFromWasm0(arg1, arg2));
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
@@ -1006,6 +1032,10 @@ function __wbg_get_imports() {
|
||||
__wbg_removeAttribute_c75ac657c944b3f1: function() { return handleError(function (arg0, arg1, arg2) {
|
||||
arg0.removeAttribute(getStringFromWasm0(arg1, arg2));
|
||||
}, arguments); },
|
||||
__wbg_removeChild_172df530ec85cc8a: function() { return handleError(function (arg0, arg1) {
|
||||
const ret = arg0.removeChild(arg1);
|
||||
return ret;
|
||||
}, arguments); },
|
||||
__wbg_removeEventListener_0069d57d090a1674: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||
arg0.removeEventListener(getStringFromWasm0(arg1, arg2), arg3, arg4 !== 0);
|
||||
}, arguments); },
|
||||
@@ -1113,6 +1143,9 @@ function __wbg_get_imports() {
|
||||
__wbg_set_method_c9f1f985f6b6c427: function(arg0, arg1, arg2) {
|
||||
arg0.method = getStringFromWasm0(arg1, arg2);
|
||||
},
|
||||
__wbg_set_mode_43ec0c73d11c9dba: function(arg0, arg1) {
|
||||
arg0.mode = __wbindgen_enum_ShadowRootMode[arg1];
|
||||
},
|
||||
__wbg_set_mode_5e08d503428c06b9: function(arg0, arg1) {
|
||||
arg0.mode = __wbindgen_enum_RequestMode[arg1];
|
||||
},
|
||||
@@ -1232,37 +1265,37 @@ function __wbg_get_imports() {
|
||||
}
|
||||
}, arguments); },
|
||||
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1139, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9);
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1191, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12);
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 993, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1062, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
|
||||
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h87a7c2758b5f97bf);
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1209, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9);
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1261, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12);
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_cast_0000000000000004: function(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1139, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1209, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9_3);
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_cast_0000000000000005: function(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 701, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 800, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h79038d3430a7951a);
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_cast_0000000000000006: function(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1108, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1178, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
|
||||
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h67ea3224860feb45);
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_cast_0000000000000007: function(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1142, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1212, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h29a925adc01712a5);
|
||||
return ret;
|
||||
},
|
||||
@@ -1301,8 +1334,9 @@ function __wbg_get_imports() {
|
||||
"./mnote-leptos-tiptap-spike-island_bg.js": import0,
|
||||
"./snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js": import1,
|
||||
"./snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js": import2,
|
||||
"./snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js": import3,
|
||||
"./snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js": import3,
|
||||
"./snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js": import4,
|
||||
"./snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js": import5,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1314,14 +1348,14 @@ function wasm_bindgen__convert__closures_____invoke__h29a925adc01712a5(arg0, arg
|
||||
wasm.wasm_bindgen__convert__closures_____invoke__h29a925adc01712a5(arg0, arg1);
|
||||
}
|
||||
|
||||
function wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9(arg0, arg1, arg2) {
|
||||
wasm.wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function wasm_bindgen__convert__closures_____invoke__h87a7c2758b5f97bf(arg0, arg1, arg2) {
|
||||
wasm.wasm_bindgen__convert__closures_____invoke__h87a7c2758b5f97bf(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9(arg0, arg1, arg2) {
|
||||
wasm.wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9_3(arg0, arg1, arg2) {
|
||||
wasm.wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9_3(arg0, arg1, arg2);
|
||||
}
|
||||
@@ -1346,6 +1380,9 @@ const __wbindgen_enum_ReadableStreamType = ["bytes"];
|
||||
|
||||
|
||||
const __wbindgen_enum_RequestMode = ["same-origin", "no-cors", "cors", "navigate"];
|
||||
|
||||
|
||||
const __wbindgen_enum_ShadowRootMode = ["open", "closed"];
|
||||
const IntoUnderlyingByteSourceFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||
? { register: () => {}, unregister: () => {} }
|
||||
: new FinalizationRegistry(ptr => wasm.__wbg_intounderlyingbytesource_free(ptr >>> 0, 1));
|
||||
|
||||
BIN
Binary file not shown.
Vendored
+1
-1
@@ -20,8 +20,8 @@ export const intounderlyingsource_pull: (a: number, b: any) => any;
|
||||
export const intounderlyingsource_cancel: (a: number) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12: (a: number, b: number, c: any) => [number, number];
|
||||
export const wasm_bindgen__convert__closures_____invoke__h0accad0f5d87788a: (a: number, b: number, c: any, d: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h87a7c2758b5f97bf: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h43695889c03f47e9_3: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h79038d3430a7951a: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h67ea3224860feb45: (a: number, b: number) => void;
|
||||
|
||||
+65
@@ -74,6 +74,71 @@ export function download_mnote_selected_image() {
|
||||
}
|
||||
}
|
||||
|
||||
export function download_mnote_image_url(src, filenameHint) {
|
||||
try {
|
||||
if (!src) return false;
|
||||
const url = new URL(String(src), window.location.href);
|
||||
if (!(url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'data:')) return false;
|
||||
const link = document.createElement('a');
|
||||
link.href = url.href;
|
||||
link.download = mnoteImageFilename({ getAttribute: (name) => name === 'title' || name === 'alt' ? String(filenameHint || '') : '' }, url.href);
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('mnote image preview download failed', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function request_mnote_image_preview_fullscreen() {
|
||||
try {
|
||||
const target = document.querySelector('[data-testid="image-preview-stage"]')
|
||||
|| document.querySelector('[data-testid="image-preview-dialog"]');
|
||||
const request = target?.requestFullscreen || target?.webkitRequestFullscreen || target?.mozRequestFullScreen || target?.msRequestFullscreen;
|
||||
if (!target || typeof request !== 'function') return false;
|
||||
const result = request.call(target);
|
||||
if (result?.catch) result.catch(() => {});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('mnote image preview fullscreen failed', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function open_mnote_page_block_href(href) {
|
||||
try {
|
||||
const target = new URL(String(href || ''), window.location.href);
|
||||
if (target.origin === window.location.origin && target.pathname.startsWith('/documents/')) {
|
||||
const encodedId = target.pathname.slice('/documents/'.length).split('/')[0] || '';
|
||||
const documentId = decodeURIComponent(encodedId);
|
||||
const runtime = window.__mnoteDocumentPaneRuntime;
|
||||
if (documentId && runtime && typeof runtime.openPrimaryDocument === 'function') {
|
||||
const result = runtime.openPrimaryDocument({
|
||||
documentId,
|
||||
workspaceId: target.searchParams.get('workspaceId') || '',
|
||||
sourceKind: target.searchParams.get('sourceKind') || '',
|
||||
rootUri: target.searchParams.get('rootUri') || '',
|
||||
url: target,
|
||||
pageBlockNavigation: true,
|
||||
});
|
||||
if (result && typeof result.catch === 'function') {
|
||||
result.catch((error) => {
|
||||
console.warn('mnote page block pane navigation failed', error);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.warn('mnote page block navigation failed', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function bump_mnote_e27_ai_bridge_request_count() {
|
||||
try {
|
||||
const key = '__MNOTE_E27_AI_BRIDGE_REQUEST_COUNT__';
|
||||
|
||||
@@ -586,6 +586,12 @@ struct TreeCommandResult {
|
||||
parent_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
struct WorkspaceSourceContext {
|
||||
source_kind: Option<String>,
|
||||
root_uri: Option<String>,
|
||||
}
|
||||
|
||||
fn dispatch_tree_local_command(body: &Value, result: &Value) {
|
||||
let Some(win) = window() else {
|
||||
return;
|
||||
@@ -607,15 +613,16 @@ async fn create_child_page_via_tree_command(
|
||||
parent_id: String,
|
||||
workspace_id: Option<String>,
|
||||
title: String,
|
||||
source_kind: Option<String>,
|
||||
root_uri: Option<String>,
|
||||
) -> Result<TreeCommandResult, String> {
|
||||
let body = json!({
|
||||
"action": "create",
|
||||
"workspaceId": workspace_id,
|
||||
"parentId": parent_id,
|
||||
"title": title,
|
||||
"accessScope": "private",
|
||||
"content": [],
|
||||
});
|
||||
let body = child_page_tree_command_body(
|
||||
parent_id.clone(),
|
||||
workspace_id,
|
||||
title,
|
||||
source_kind,
|
||||
root_uri,
|
||||
);
|
||||
let body_string =
|
||||
serde_json::to_string(&body).map_err(|err| format!("序列化创建子页面请求失败:{err}"))?;
|
||||
|
||||
@@ -706,17 +713,38 @@ fn block_plain_text_from_index(
|
||||
Ok(normalized_page_block_title(&collect_plain_text(block)))
|
||||
}
|
||||
|
||||
fn page_reference_paragraph_node(page_id: &str, workspace_id: Option<&str>, title: &str) -> Value {
|
||||
let href = if page_id.trim().is_empty() {
|
||||
"#".to_string()
|
||||
} else if let Some(workspace_id) = workspace_id
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
format!("/documents/{page_id}?workspaceId={workspace_id}")
|
||||
} else {
|
||||
format!("/documents/{page_id}")
|
||||
};
|
||||
fn child_page_tree_command_body(
|
||||
parent_id: String,
|
||||
workspace_id: Option<String>,
|
||||
title: String,
|
||||
source_kind: Option<String>,
|
||||
root_uri: Option<String>,
|
||||
) -> Value {
|
||||
let mut body = json!({
|
||||
"action": "create",
|
||||
"workspaceId": workspace_id,
|
||||
"parentId": parent_id,
|
||||
"title": title,
|
||||
"accessScope": "private",
|
||||
"content": [],
|
||||
});
|
||||
if let Some(source_kind) = trimmed_optional(source_kind) {
|
||||
body["sourceKind"] = Value::String(source_kind);
|
||||
}
|
||||
if let Some(root_uri) = trimmed_optional(root_uri) {
|
||||
body["rootUri"] = Value::String(root_uri);
|
||||
}
|
||||
body
|
||||
}
|
||||
|
||||
fn page_reference_paragraph_node(
|
||||
page_id: &str,
|
||||
workspace_id: Option<&str>,
|
||||
title: &str,
|
||||
source_kind: Option<&str>,
|
||||
root_uri: Option<&str>,
|
||||
) -> Value {
|
||||
let href = page_reference_href(page_id, workspace_id, source_kind, root_uri);
|
||||
paragraph_node(vec![json!({
|
||||
"type": "text",
|
||||
"text": normalized_page_block_title(title),
|
||||
@@ -732,12 +760,139 @@ fn page_reference_paragraph_node(page_id: &str, workspace_id: Option<&str>, titl
|
||||
})])
|
||||
}
|
||||
|
||||
fn page_reference_href(
|
||||
page_id: &str,
|
||||
workspace_id: Option<&str>,
|
||||
source_kind: Option<&str>,
|
||||
root_uri: Option<&str>,
|
||||
) -> String {
|
||||
if page_id.trim().is_empty() {
|
||||
"#".to_string()
|
||||
} else if has_local_source_context(source_kind, root_uri) {
|
||||
let mut params = Vec::new();
|
||||
if let Some(source_kind) = trimmed_str(source_kind) {
|
||||
params.push(format!(
|
||||
"sourceKind={}",
|
||||
percent_encode_query_component(source_kind)
|
||||
));
|
||||
}
|
||||
if let Some(root_uri) = trimmed_str(root_uri) {
|
||||
params.push(format!(
|
||||
"rootUri={}",
|
||||
percent_encode_query_component(root_uri)
|
||||
));
|
||||
}
|
||||
params.push("treeView=filetree".to_string());
|
||||
format!("/documents/{page_id}?{}", params.join("&"))
|
||||
} else if let Some(workspace_id) = workspace_id
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
format!("/documents/{page_id}?workspaceId={workspace_id}")
|
||||
} else {
|
||||
format!("/documents/{page_id}")
|
||||
}
|
||||
}
|
||||
|
||||
fn current_workspace_source_context() -> WorkspaceSourceContext {
|
||||
WorkspaceSourceContext {
|
||||
source_kind: current_query_param_decoded("sourceKind")
|
||||
.or_else(|| body_attribute("data-mnote-source-kind")),
|
||||
root_uri: current_query_param_decoded("rootUri")
|
||||
.or_else(|| body_attribute("data-mnote-root-uri")),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_query_param_decoded(key: &str) -> Option<String> {
|
||||
let search = window()?.location().search().ok()?;
|
||||
query_param_from_search(&search, key).and_then(|value| percent_decode_query_component(&value))
|
||||
}
|
||||
|
||||
fn query_param_from_search(search: &str, key: &str) -> Option<String> {
|
||||
let query = search.trim_start_matches('?');
|
||||
for pair in query.split('&') {
|
||||
if pair.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
|
||||
if percent_decode_query_component(name).as_deref() == Some(key) {
|
||||
return Some(value.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn body_attribute(name: &str) -> Option<String> {
|
||||
window()?
|
||||
.document()?
|
||||
.body()?
|
||||
.get_attribute(name)
|
||||
.and_then(|value| trimmed_optional(Some(value)))
|
||||
}
|
||||
|
||||
fn has_local_source_context(source_kind: Option<&str>, root_uri: Option<&str>) -> bool {
|
||||
trimmed_str(source_kind) == Some("local_folder") || trimmed_str(root_uri).is_some()
|
||||
}
|
||||
|
||||
fn trimmed_optional(value: Option<String>) -> Option<String> {
|
||||
value
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn trimmed_str(value: Option<&str>) -> Option<&str> {
|
||||
value.map(str::trim).filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn percent_encode_query_component(value: &str) -> String {
|
||||
let mut encoded = String::with_capacity(value.len());
|
||||
for byte in value.as_bytes() {
|
||||
let character = *byte as char;
|
||||
if character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.' | '~') {
|
||||
encoded.push(character);
|
||||
} else {
|
||||
encoded.push('%');
|
||||
encoded.push_str(&format!("{byte:02X}"));
|
||||
}
|
||||
}
|
||||
encoded
|
||||
}
|
||||
|
||||
fn percent_decode_query_component(value: &str) -> Option<String> {
|
||||
let bytes = value.as_bytes();
|
||||
let mut decoded = Vec::with_capacity(bytes.len());
|
||||
let mut index = 0;
|
||||
while index < bytes.len() {
|
||||
match bytes[index] {
|
||||
b'%' => {
|
||||
if index + 2 >= bytes.len() {
|
||||
return None;
|
||||
}
|
||||
let hex = std::str::from_utf8(&bytes[index + 1..index + 3]).ok()?;
|
||||
decoded.push(u8::from_str_radix(hex, 16).ok()?);
|
||||
index += 3;
|
||||
}
|
||||
b'+' => {
|
||||
decoded.push(b' ');
|
||||
index += 1;
|
||||
}
|
||||
byte => {
|
||||
decoded.push(byte);
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
String::from_utf8(decoded).ok()
|
||||
}
|
||||
|
||||
fn replace_block_with_page_reference(
|
||||
editor: TiptapEditorHandle,
|
||||
block_index: usize,
|
||||
page_id: &str,
|
||||
workspace_id: Option<&str>,
|
||||
title: &str,
|
||||
source_kind: Option<&str>,
|
||||
root_uri: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let mut document = editor
|
||||
.get_json()
|
||||
@@ -746,7 +901,8 @@ fn replace_block_with_page_reference(
|
||||
if block_index >= content.len() {
|
||||
return Err(format!("找不到第 {} 个块", block_index + 1));
|
||||
}
|
||||
content[block_index] = page_reference_paragraph_node(page_id, workspace_id, title);
|
||||
content[block_index] =
|
||||
page_reference_paragraph_node(page_id, workspace_id, title, source_kind, root_uri);
|
||||
let next_payload = document
|
||||
.get("content")
|
||||
.and_then(Value::as_array)
|
||||
@@ -787,12 +943,15 @@ pub(crate) fn run_block_turn_into_page_action(
|
||||
return;
|
||||
};
|
||||
let workspace_id_value = workspace_id.get_untracked();
|
||||
let source_context = current_workspace_source_context();
|
||||
set_command_feedback.set(format!("正在创建子页面:{page_title}"));
|
||||
spawn_local(async move {
|
||||
match create_child_page_via_tree_command(
|
||||
parent_document_id.clone(),
|
||||
workspace_id_value.clone(),
|
||||
page_title.clone(),
|
||||
source_context.source_kind.clone(),
|
||||
source_context.root_uri.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -812,6 +971,8 @@ pub(crate) fn run_block_turn_into_page_action(
|
||||
child_page_id,
|
||||
effective_workspace_id,
|
||||
effective_title,
|
||||
source_context.source_kind.as_deref(),
|
||||
source_context.root_uri.as_deref(),
|
||||
) {
|
||||
Ok(()) => {
|
||||
set_block_menu_open.set(false);
|
||||
@@ -840,6 +1001,46 @@ pub(crate) fn run_block_turn_into_page_action(
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{child_page_tree_command_body, page_reference_paragraph_node};
|
||||
|
||||
#[test]
|
||||
fn child_page_tree_command_body_preserves_local_source_context() {
|
||||
let body = child_page_tree_command_body(
|
||||
"local-md:Parent~2FParent.md".to_string(),
|
||||
Some("local:_tmp_mnote".to_string()),
|
||||
"子页面".to_string(),
|
||||
Some("local_folder".to_string()),
|
||||
Some("file:///tmp/mnote".to_string()),
|
||||
);
|
||||
|
||||
assert_eq!(body["action"], "create");
|
||||
assert_eq!(body["sourceKind"], "local_folder");
|
||||
assert_eq!(body["rootUri"], "file:///tmp/mnote");
|
||||
assert_eq!(body["parentId"], "local-md:Parent~2FParent.md");
|
||||
assert_eq!(body["title"], "子页面");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_reference_node_for_local_folder_keeps_openable_source_params() {
|
||||
let node = page_reference_paragraph_node(
|
||||
"local-md:Parent~2FChild~2FChild.md",
|
||||
Some("local:_tmp_mnote"),
|
||||
"Child",
|
||||
Some("local_folder"),
|
||||
Some("file:///tmp/mnote"),
|
||||
);
|
||||
let href = node["content"][0]["marks"][0]["attrs"]["href"]
|
||||
.as_str()
|
||||
.expect("href");
|
||||
|
||||
assert!(href.starts_with("/documents/local-md:Parent~2FChild~2FChild.md?"));
|
||||
assert!(href.contains("sourceKind=local_folder"));
|
||||
assert!(href.contains("rootUri=file%3A%2F%2F%2Ftmp%2Fmnote"));
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 delta operations 应用到 Tiptap JSON 树(原地修改)。
|
||||
/// 返回 true 表示树发生了变更。
|
||||
pub(crate) fn apply_block_delta_to_json(content: &mut Value, operations: &[Value]) -> bool {
|
||||
|
||||
@@ -1768,6 +1768,160 @@ pub(crate) const SPIKE_STYLE: &str = r#"
|
||||
box-shadow: 0 0 0 5px rgba(255, 71, 71, 0.12);
|
||||
}
|
||||
|
||||
.image-preview-dialog {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.88);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.image-preview-counter {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 14px;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.image-preview-close {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.86);
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.image-preview-close:hover,
|
||||
.image-preview-close:focus-visible {
|
||||
background: rgba(32, 32, 32, 0.96);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.image-preview-stage {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 34px 74px 70px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.image-preview-stage:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.image-preview-image {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
background: #fff;
|
||||
transform-origin: center center;
|
||||
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.24);
|
||||
user-select: none;
|
||||
will-change: transform;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.image-preview-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
font-size: 38px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.image-preview-nav-prev {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.image-preview-nav-next {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.image-preview-nav:hover:not(:disabled),
|
||||
.image-preview-nav:focus-visible:not(:disabled) {
|
||||
background: rgba(0, 0, 0, 0.74);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.image-preview-nav:disabled {
|
||||
opacity: 0.36;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.image-preview-toolbar {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 18px;
|
||||
transform: translateX(-50%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 6px 9px;
|
||||
border-radius: 4px;
|
||||
background: rgba(112, 112, 112, 0.9);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.24);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.image-preview-tool {
|
||||
min-width: 32px;
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-preview-tool-wide {
|
||||
min-width: 46px;
|
||||
}
|
||||
|
||||
.image-preview-tool:hover,
|
||||
.image-preview-tool:focus-visible {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.editor-surface .ProseMirror .tiptap-table-of-contents-node {
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
@@ -2255,42 +2409,44 @@ pub(crate) const SPIKE_STYLE: &str = r#"
|
||||
}
|
||||
|
||||
.editor-surface .ProseMirror p:has(> a.mnote-page-block-link) {
|
||||
width: 100%;
|
||||
margin: 0 0 8px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 71, 71, 0.12);
|
||||
margin: 1px 0 4px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.editor-surface .ProseMirror a.mnote-page-block-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 7px;
|
||||
max-width: 100%;
|
||||
padding: 1px 4px;
|
||||
min-height: 28px;
|
||||
padding: 1px 3px;
|
||||
border-radius: 4px;
|
||||
color: #37352f;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
text-decoration: none;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(55, 53, 47, 0.42);
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.editor-surface .ProseMirror a.mnote-page-block-link::before {
|
||||
content: "";
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 auto;
|
||||
border: 1.4px solid #c8c5c0;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f6f4f1 100%);
|
||||
box-shadow: inset 0 -2px 0 rgba(55, 53, 47, 0.08);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: center / 16px 16px no-repeat url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.25 2.25h5.25L12.75 5.5v8.25h-8.5V2.25Z' stroke='%2337352f' stroke-width='1.25' stroke-linejoin='round'/%3E%3Cpath d='M9.5 2.25V5.5h3.25' stroke='%2337352f' stroke-width='1.25' stroke-linejoin='round'/%3E%3Cpath d='M6.25 8h4M6.25 10.25h3' stroke='%2337352f' stroke-width='1.1' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.editor-surface .ProseMirror a.mnote-page-block-link:hover {
|
||||
color: #37352f;
|
||||
background: rgba(55, 53, 47, 0.08);
|
||||
text-decoration: none;
|
||||
text-decoration-color: rgba(55, 53, 47, 0.68);
|
||||
}
|
||||
|
||||
.editor-surface .ProseMirror a.mnote-uploaded-attachment-row {
|
||||
|
||||
@@ -22,12 +22,12 @@ use editor_runtime::block_dnd::{
|
||||
drop_indicator_from_target as drop_indicator_from_target_runtime, element_within_handle_shell,
|
||||
hovered_block_from_target as hovered_block_from_target_runtime,
|
||||
};
|
||||
use editor_runtime::block_hover_state::{
|
||||
BlockMenuLayout, DropIndicatorState, HoveredBlockState, PendingDragState,
|
||||
};
|
||||
use editor_runtime::block_handle_menu_view::{
|
||||
block_handle_menu_view, BlockHandleMenuSignals, BlockHandleMenuViewProps,
|
||||
};
|
||||
use editor_runtime::block_hover_state::{
|
||||
BlockMenuLayout, DropIndicatorState, HoveredBlockState, PendingDragState,
|
||||
};
|
||||
use editor_runtime::block_menu_overlay;
|
||||
use editor_runtime::block_transform::{
|
||||
apply_block_delta_to_json, apply_highlight_color, apply_image_align, apply_text_align,
|
||||
@@ -51,13 +51,13 @@ use editor_runtime::dom_selection::{
|
||||
};
|
||||
use editor_runtime::editor_focus::{active_editor_stage, body_has_focus, schedule_editor_focus};
|
||||
use editor_runtime::mindmap_node_view::{mount_mindmap_shell_impl, unmount_mindmap_shell_impl};
|
||||
#[cfg(test)]
|
||||
use editor_runtime::mount::resolve_runtime_delivery_mode;
|
||||
use editor_runtime::mount::{
|
||||
initial_content, mount_app_into, runtime_delivery_mode, runtime_editor_instance_id,
|
||||
runtime_mount_context, set_runtime_mount_context, set_runtime_mount_options, MountOptions,
|
||||
RuntimeDeliveryMode, RuntimeStandaloneObject,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use editor_runtime::mount::resolve_runtime_delivery_mode;
|
||||
use editor_runtime::overlays::{
|
||||
clamp_overlay_anchor, close_editor_floating_overlays_if_escape, image_element_from_target,
|
||||
image_toolbar_anchor_from_image, open_block_menu_overlay, open_image_toolbar_overlay,
|
||||
@@ -73,8 +73,8 @@ use editor_runtime::persistence::{
|
||||
persisted_document_identity, PersistedDocumentIdentity,
|
||||
};
|
||||
use editor_runtime::runtime_bridge::{
|
||||
dispatch_change_event_to_target, dispatch_ready_event_to_target,
|
||||
dispatch_runtime_event, dispatch_selection_event_to_target, dispatch_state_event_to_target,
|
||||
dispatch_change_event_to_target, dispatch_ready_event_to_target, dispatch_runtime_event,
|
||||
dispatch_selection_event_to_target, dispatch_state_event_to_target,
|
||||
dispatch_status_event_to_target, register_runtime_listener, take_unmount_handle,
|
||||
};
|
||||
use editor_runtime::slash_actions::{SlashActionKind, SLASH_ACTIONS};
|
||||
@@ -170,6 +170,71 @@ export function download_mnote_selected_image() {
|
||||
}
|
||||
}
|
||||
|
||||
export function download_mnote_image_url(src, filenameHint) {
|
||||
try {
|
||||
if (!src) return false;
|
||||
const url = new URL(String(src), window.location.href);
|
||||
if (!(url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'data:')) return false;
|
||||
const link = document.createElement('a');
|
||||
link.href = url.href;
|
||||
link.download = mnoteImageFilename({ getAttribute: (name) => name === 'title' || name === 'alt' ? String(filenameHint || '') : '' }, url.href);
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('mnote image preview download failed', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function request_mnote_image_preview_fullscreen() {
|
||||
try {
|
||||
const target = document.querySelector('[data-testid="image-preview-stage"]')
|
||||
|| document.querySelector('[data-testid="image-preview-dialog"]');
|
||||
const request = target?.requestFullscreen || target?.webkitRequestFullscreen || target?.mozRequestFullScreen || target?.msRequestFullscreen;
|
||||
if (!target || typeof request !== 'function') return false;
|
||||
const result = request.call(target);
|
||||
if (result?.catch) result.catch(() => {});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('mnote image preview fullscreen failed', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function open_mnote_page_block_href(href) {
|
||||
try {
|
||||
const target = new URL(String(href || ''), window.location.href);
|
||||
if (target.origin === window.location.origin && target.pathname.startsWith('/documents/')) {
|
||||
const encodedId = target.pathname.slice('/documents/'.length).split('/')[0] || '';
|
||||
const documentId = decodeURIComponent(encodedId);
|
||||
const runtime = window.__mnoteDocumentPaneRuntime;
|
||||
if (documentId && runtime && typeof runtime.openPrimaryDocument === 'function') {
|
||||
const result = runtime.openPrimaryDocument({
|
||||
documentId,
|
||||
workspaceId: target.searchParams.get('workspaceId') || '',
|
||||
sourceKind: target.searchParams.get('sourceKind') || '',
|
||||
rootUri: target.searchParams.get('rootUri') || '',
|
||||
url: target,
|
||||
pageBlockNavigation: true,
|
||||
});
|
||||
if (result && typeof result.catch === 'function') {
|
||||
result.catch((error) => {
|
||||
console.warn('mnote page block pane navigation failed', error);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.warn('mnote page block navigation failed', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function bump_mnote_e27_ai_bridge_request_count() {
|
||||
try {
|
||||
const key = '__MNOTE_E27_AI_BRIDGE_REQUEST_COUNT__';
|
||||
@@ -199,6 +264,9 @@ extern "C" {
|
||||
fn find_mnote_block_anchor(block_id: &str) -> Option<Element>;
|
||||
fn select_mnote_image_node(image: &Element) -> bool;
|
||||
fn download_mnote_selected_image() -> bool;
|
||||
fn download_mnote_image_url(src: &str, filename_hint: &str) -> bool;
|
||||
fn request_mnote_image_preview_fullscreen() -> bool;
|
||||
fn open_mnote_page_block_href(href: &str) -> bool;
|
||||
fn bump_mnote_e27_ai_bridge_request_count() -> u32;
|
||||
#[wasm_bindgen(catch)]
|
||||
async fn post_mnote_document_save(body: String) -> Result<JsValue, JsValue>;
|
||||
@@ -218,6 +286,99 @@ struct ToolbarAlignOption {
|
||||
alignment: TiptapTextAlign,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct ImagePreviewState {
|
||||
src: String,
|
||||
alt: String,
|
||||
title: String,
|
||||
index: usize,
|
||||
total: usize,
|
||||
zoom: f64,
|
||||
rotation: i32,
|
||||
offset_x: f64,
|
||||
offset_y: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct ImagePreviewPanState {
|
||||
start_x: i32,
|
||||
start_y: i32,
|
||||
origin_offset_x: f64,
|
||||
origin_offset_y: f64,
|
||||
}
|
||||
|
||||
impl ImagePreviewState {
|
||||
fn filename_hint(&self) -> String {
|
||||
let title = self.title.trim();
|
||||
if !title.is_empty() {
|
||||
return title.to_string();
|
||||
}
|
||||
let alt = self.alt.trim();
|
||||
if !alt.is_empty() {
|
||||
return alt.to_string();
|
||||
}
|
||||
"image".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn image_preview_state_from_element(
|
||||
image: &Element,
|
||||
index: usize,
|
||||
total: usize,
|
||||
) -> Option<ImagePreviewState> {
|
||||
let src = image.get_attribute("src")?;
|
||||
Some(ImagePreviewState {
|
||||
src,
|
||||
alt: image.get_attribute("alt").unwrap_or_default(),
|
||||
title: image.get_attribute("title").unwrap_or_default(),
|
||||
index,
|
||||
total,
|
||||
zoom: 1.0,
|
||||
rotation: 0,
|
||||
offset_x: 0.0,
|
||||
offset_y: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
fn image_preview_state_from_image(image: &Element) -> Option<ImagePreviewState> {
|
||||
let document = window()?.document()?;
|
||||
let images = document
|
||||
.query_selector_all(&format!("{EDITOR_ROOT_SELECTOR} img[src]"))
|
||||
.ok()?;
|
||||
let total = images.length() as usize;
|
||||
if total == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let image_node: &web_sys::Node = image.as_ref();
|
||||
let mut index = 0_usize;
|
||||
for current in 0..images.length() {
|
||||
if images
|
||||
.item(current)
|
||||
.map(|node| node.is_same_node(Some(image_node)))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
index = current as usize;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
image_preview_state_from_element(image, index, total)
|
||||
}
|
||||
|
||||
fn image_preview_state_by_index(index: usize) -> Option<ImagePreviewState> {
|
||||
let document = window()?.document()?;
|
||||
let images = document
|
||||
.query_selector_all(&format!("{EDITOR_ROOT_SELECTOR} img[src]"))
|
||||
.ok()?;
|
||||
let total = images.length() as usize;
|
||||
if total == 0 || index >= total {
|
||||
return None;
|
||||
}
|
||||
let element = images.item(index as u32)?.dyn_into::<Element>().ok()?;
|
||||
image_preview_state_from_element(&element, index, total)
|
||||
}
|
||||
|
||||
const TOOLBAR_TEXT_COLORS: [ToolbarColorOption; 10] = [
|
||||
ToolbarColorOption {
|
||||
id: "default",
|
||||
@@ -1166,6 +1327,8 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
||||
let (floating_toolbar_anchor, set_floating_toolbar_anchor) =
|
||||
signal(None::<FloatingToolbarAnchor>);
|
||||
let (image_toolbar_anchor, set_image_toolbar_anchor) = signal(None::<ImageToolbarAnchor>);
|
||||
let (image_preview_state, set_image_preview_state) = signal(None::<ImagePreviewState>);
|
||||
let (image_preview_pan, set_image_preview_pan) = signal(None::<ImagePreviewPanState>);
|
||||
let (locked_toolbar_anchor, set_locked_toolbar_anchor) = signal(None::<FloatingToolbarAnchor>);
|
||||
let (slash_open, set_slash_open) = signal(false);
|
||||
let (slash_index, set_slash_index) = signal(0_usize);
|
||||
@@ -1806,6 +1969,10 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
||||
let set_floating_toolbar_anchor = set_floating_toolbar_anchor;
|
||||
let image_toolbar_anchor = image_toolbar_anchor;
|
||||
let set_image_toolbar_anchor = set_image_toolbar_anchor;
|
||||
let image_preview_state = image_preview_state;
|
||||
let set_image_preview_state = set_image_preview_state;
|
||||
let image_preview_pan = image_preview_pan;
|
||||
let set_image_preview_pan = set_image_preview_pan;
|
||||
let table_toolbar_open = table_toolbar_open;
|
||||
let set_table_toolbar_open = set_table_toolbar_open;
|
||||
let table_options_open = table_options_open;
|
||||
@@ -1876,6 +2043,15 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
||||
drop(keydown_capture_closure);
|
||||
});
|
||||
let keydown_handle = window_event_listener(ev::keydown, move |event| {
|
||||
if image_preview_state.try_get_untracked().flatten().is_some()
|
||||
&& event.key() == "Escape"
|
||||
{
|
||||
event.prevent_default();
|
||||
let _ = set_image_preview_state.try_set(None);
|
||||
let _ = set_command_feedback.try_set("已关闭图片预览".to_string());
|
||||
return;
|
||||
}
|
||||
|
||||
let focused = active_editor_stage();
|
||||
if !focused {
|
||||
if body_has_focus() && (event.ctrl_key() || event.meta_key()) && !event.alt_key() {
|
||||
@@ -2143,7 +2319,36 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
});
|
||||
on_cleanup(move || drop(keydown_handle));
|
||||
let preview_mousemove_handle =
|
||||
window_event_listener(ev::mousemove, move |event: MouseEvent| {
|
||||
let Some(pan) = image_preview_pan.try_get_untracked().flatten() else {
|
||||
return;
|
||||
};
|
||||
if image_preview_state.try_get_untracked().flatten().is_none() {
|
||||
let _ = set_image_preview_pan.try_set(None);
|
||||
return;
|
||||
}
|
||||
event.prevent_default();
|
||||
let offset_x = pan.origin_offset_x + f64::from(event.client_x() - pan.start_x);
|
||||
let offset_y = pan.origin_offset_y + f64::from(event.client_y() - pan.start_y);
|
||||
let _ = set_image_preview_state.try_update(|state| {
|
||||
if let Some(state) = state.as_mut() {
|
||||
state.offset_x = offset_x;
|
||||
state.offset_y = offset_y;
|
||||
}
|
||||
});
|
||||
});
|
||||
let preview_mouseup_handle = window_event_listener(ev::mouseup, move |event: MouseEvent| {
|
||||
if event.button() == 1 || image_preview_pan.try_get_untracked().flatten().is_some() {
|
||||
event.prevent_default();
|
||||
let _ = set_image_preview_pan.try_set(None);
|
||||
}
|
||||
});
|
||||
on_cleanup(move || {
|
||||
drop(keydown_handle);
|
||||
drop(preview_mousemove_handle);
|
||||
drop(preview_mouseup_handle);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
@@ -2191,10 +2396,9 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
event.prevent_default();
|
||||
event.stop_propagation();
|
||||
if let Some(win) = window() {
|
||||
let _ = win.location().assign(&href);
|
||||
if open_mnote_page_block_href(&href) {
|
||||
event.prevent_default();
|
||||
event.stop_propagation();
|
||||
}
|
||||
});
|
||||
on_cleanup(move || drop(click_handle));
|
||||
@@ -2410,7 +2614,7 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(image) = image_element_from_target(target.clone()) {
|
||||
if let Some(image) = image_element_from_target(target.clone()) {
|
||||
let _ = select_mnote_image_node(&image);
|
||||
set_editor_focused.set(true);
|
||||
open_image_toolbar_overlay(
|
||||
@@ -2447,6 +2651,26 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
||||
set_turn_into_open.set(false);
|
||||
set_hovered_block.set(hovered_block_from_target(target));
|
||||
}
|
||||
on:dblclick=move |event: MouseEvent| {
|
||||
let Some(target) = event.target() else {
|
||||
return;
|
||||
};
|
||||
let Some(image) = image_element_from_target(target) else {
|
||||
return;
|
||||
};
|
||||
event.prevent_default();
|
||||
event.stop_propagation();
|
||||
let _ = select_mnote_image_node(&image);
|
||||
set_editor_focused.set(true);
|
||||
set_image_toolbar_anchor.set(None);
|
||||
set_table_toolbar_open.set(false);
|
||||
set_table_options_open.set(false);
|
||||
set_block_menu_open.set(false);
|
||||
set_block_menu_anchor.set(None);
|
||||
set_hovered_block.set(None);
|
||||
set_image_preview_state.set(image_preview_state_from_image(&image));
|
||||
set_command_feedback.set("图片预览已打开".to_string());
|
||||
}
|
||||
on:mousemove=move |event: MouseEvent| {
|
||||
if block_menu_open.get_untracked() || dragging_block_index.get_untracked().is_some() {
|
||||
return;
|
||||
@@ -2614,6 +2838,241 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
||||
workspace_id,
|
||||
set_command_feedback,
|
||||
)}
|
||||
{move || {
|
||||
let Some(preview) = image_preview_state.get() else {
|
||||
return ().into_any();
|
||||
};
|
||||
let preview_index = preview.index;
|
||||
let preview_total = preview.total;
|
||||
let preview_src = preview.src.clone();
|
||||
let preview_alt = preview.alt.clone();
|
||||
let preview_title = preview.title.clone();
|
||||
let filename_hint = preview.filename_hint();
|
||||
let has_prev = preview_index > 0;
|
||||
let has_next = preview_index + 1 < preview_total;
|
||||
let image_style = format!(
|
||||
"transform:translate({}px, {}px) scale({}) rotate({}deg);",
|
||||
preview.offset_x, preview.offset_y, preview.zoom, preview.rotation
|
||||
);
|
||||
view! {
|
||||
<leptos::portal::Portal>
|
||||
<div
|
||||
class="image-preview-dialog"
|
||||
data-testid="image-preview-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="图片预览"
|
||||
data-preview-index=preview_index.to_string()
|
||||
data-preview-total=preview_total.to_string()
|
||||
data-preview-zoom=preview.zoom.to_string()
|
||||
data-preview-rotation=preview.rotation.to_string()
|
||||
data-preview-offset-x=preview.offset_x.to_string()
|
||||
data-preview-offset-y=preview.offset_y.to_string()
|
||||
on:click=move |event: MouseEvent| {
|
||||
let Some(target) = event.target().and_then(target_element) else {
|
||||
return;
|
||||
};
|
||||
if target
|
||||
.get_attribute("data-testid")
|
||||
.as_deref()
|
||||
== Some("image-preview-dialog")
|
||||
{
|
||||
set_image_preview_state.set(None);
|
||||
set_image_preview_pan.set(None);
|
||||
set_command_feedback.set("已关闭图片预览".to_string());
|
||||
}
|
||||
}
|
||||
>
|
||||
<div class="image-preview-counter">
|
||||
{format!("{}/{}", preview_index + 1, preview_total.max(1))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="image-preview-close"
|
||||
data-testid="image-preview-close"
|
||||
title="关闭"
|
||||
aria-label="关闭图片预览"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
set_image_preview_state.set(None);
|
||||
set_image_preview_pan.set(None);
|
||||
set_command_feedback.set("已关闭图片预览".to_string());
|
||||
}
|
||||
>"X"</button>
|
||||
<button
|
||||
type="button"
|
||||
class="image-preview-nav image-preview-nav-prev"
|
||||
data-testid="image-preview-prev"
|
||||
title="上一张"
|
||||
aria-label="上一张图片"
|
||||
disabled=!has_prev
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
if has_prev {
|
||||
if let Some(next) = image_preview_state_by_index(preview_index - 1) {
|
||||
set_image_preview_state.set(Some(next));
|
||||
}
|
||||
}
|
||||
}
|
||||
>"‹"</button>
|
||||
<button
|
||||
type="button"
|
||||
class="image-preview-nav image-preview-nav-next"
|
||||
data-testid="image-preview-next"
|
||||
title="下一张"
|
||||
aria-label="下一张图片"
|
||||
disabled=!has_next
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
if has_next {
|
||||
if let Some(next) = image_preview_state_by_index(preview_index + 1) {
|
||||
set_image_preview_state.set(Some(next));
|
||||
}
|
||||
}
|
||||
}
|
||||
>"›"</button>
|
||||
<div
|
||||
class="image-preview-stage"
|
||||
data-testid="image-preview-stage"
|
||||
on:wheel=move |event: WheelEvent| {
|
||||
event.prevent_default();
|
||||
event.stop_propagation();
|
||||
let delta = if event.delta_y() < 0.0 { 0.15 } else { -0.15 };
|
||||
set_image_preview_state.update(|state| {
|
||||
if let Some(state) = state.as_mut() {
|
||||
state.zoom = (state.zoom + delta).clamp(0.25, 4.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
on:mousedown=move |event: MouseEvent| {
|
||||
if event.button() != 1 {
|
||||
return;
|
||||
}
|
||||
event.prevent_default();
|
||||
event.stop_propagation();
|
||||
let Some(state) = image_preview_state.get_untracked() else {
|
||||
return;
|
||||
};
|
||||
set_image_preview_pan.set(Some(ImagePreviewPanState {
|
||||
start_x: event.client_x(),
|
||||
start_y: event.client_y(),
|
||||
origin_offset_x: state.offset_x,
|
||||
origin_offset_y: state.offset_y,
|
||||
}));
|
||||
}
|
||||
>
|
||||
<img
|
||||
class="image-preview-image"
|
||||
data-testid="image-preview-image"
|
||||
src=preview_src.clone()
|
||||
alt=preview_alt.clone()
|
||||
title=preview_title.clone()
|
||||
style=image_style.clone()
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="image-preview-toolbar"
|
||||
data-testid="image-preview-toolbar"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="image-preview-tool"
|
||||
data-testid="image-preview-zoom-in"
|
||||
title="放大"
|
||||
aria-label="放大图片"
|
||||
on:click=move |_| {
|
||||
set_image_preview_state.update(|state| {
|
||||
if let Some(state) = state.as_mut() {
|
||||
state.zoom = (state.zoom + 0.25).min(4.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
>"+"</button>
|
||||
<button
|
||||
type="button"
|
||||
class="image-preview-tool"
|
||||
data-testid="image-preview-zoom-out"
|
||||
title="缩小"
|
||||
aria-label="缩小图片"
|
||||
on:click=move |_| {
|
||||
set_image_preview_state.update(|state| {
|
||||
if let Some(state) = state.as_mut() {
|
||||
state.zoom = (state.zoom - 0.25).max(0.25);
|
||||
}
|
||||
});
|
||||
}
|
||||
>"-"</button>
|
||||
<button
|
||||
type="button"
|
||||
class="image-preview-tool image-preview-tool-wide"
|
||||
data-testid="image-preview-one-to-one"
|
||||
title="1:1"
|
||||
aria-label="按原始比例查看"
|
||||
on:click=move |_| {
|
||||
set_image_preview_state.update(|state| {
|
||||
if let Some(state) = state.as_mut() {
|
||||
state.zoom = 1.0;
|
||||
state.rotation = 0;
|
||||
state.offset_x = 0.0;
|
||||
state.offset_y = 0.0;
|
||||
}
|
||||
});
|
||||
}
|
||||
>"1:1"</button>
|
||||
<button
|
||||
type="button"
|
||||
class="image-preview-tool"
|
||||
data-testid="image-preview-rotate"
|
||||
title="旋转"
|
||||
aria-label="旋转图片"
|
||||
on:click=move |_| {
|
||||
set_image_preview_state.update(|state| {
|
||||
if let Some(state) = state.as_mut() {
|
||||
state.rotation = (state.rotation + 90) % 360;
|
||||
}
|
||||
});
|
||||
}
|
||||
>"R"</button>
|
||||
<button
|
||||
type="button"
|
||||
class="image-preview-tool"
|
||||
data-testid="image-preview-download"
|
||||
title="下载"
|
||||
aria-label="下载图片"
|
||||
on:click={
|
||||
let preview_src = preview_src.clone();
|
||||
let filename_hint = filename_hint.clone();
|
||||
move |_| {
|
||||
if download_mnote_image_url(&preview_src, &filename_hint) {
|
||||
set_command_feedback.set("已开始下载图片".to_string());
|
||||
} else {
|
||||
set_command_feedback.set("图片下载失败".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
>"D"</button>
|
||||
<button
|
||||
type="button"
|
||||
class="image-preview-tool"
|
||||
data-testid="image-preview-fullscreen"
|
||||
title="全屏"
|
||||
aria-label="全屏查看图片"
|
||||
on:click=move |_| {
|
||||
if request_mnote_image_preview_fullscreen() {
|
||||
set_command_feedback.set("已请求全屏查看图片".to_string());
|
||||
} else {
|
||||
set_command_feedback.set("当前浏览器不支持图片全屏".to_string());
|
||||
}
|
||||
}
|
||||
>"F"</button>
|
||||
</div>
|
||||
</div>
|
||||
</leptos::portal::Portal>
|
||||
}.into_any()
|
||||
}}
|
||||
{move || {
|
||||
let Some(anchor) = image_toolbar_anchor.get() else {
|
||||
return ().into_any();
|
||||
|
||||
Reference in New Issue
Block a user