parent
a808742523
commit
5bb5ef1d9a
|
|
@ -39,6 +39,11 @@
|
|||
native="true"
|
||||
preference="__prefsPrefix__.workspace.outline.keepLinks"
|
||||
/>
|
||||
<checkbox
|
||||
data-l10n-id="editor-noteLinkPreview"
|
||||
native="true"
|
||||
preference="__prefsPrefix__.editor.noteLinkPreview"
|
||||
/>
|
||||
</groupbox>
|
||||
<groupbox>
|
||||
<label><html:h2 data-l10n-id="sync-title"></html:h2></label>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ editor-title = Note Editor
|
|||
editor-expandLevel-label = Outline expand to heading level
|
||||
editor-keepLinks =
|
||||
.label = Show note links in outline
|
||||
editor-noteLinkPreview =
|
||||
.label = Show note link preview on hover
|
||||
|
||||
sync-title = Sync
|
||||
sync-period-label = Auto-sync period (seconds)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ editor-title = Note Editor
|
|||
editor-expandLevel-label = Espansione dello schema al livello delle intestazioni
|
||||
editor-keepLinks =
|
||||
.label = Mostra i collegamenti delle note nello schema
|
||||
editor-noteLinkPreview =
|
||||
.label = Show note link preview on hover
|
||||
|
||||
sync-title = Sincronizzazione
|
||||
sync-period-label = Intervallo della sincronizzazione automatica (secondi)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ editor-title = Note Editor
|
|||
editor-expandLevel-label = Outline расширить до уровня заголовка
|
||||
editor-keepLinks =
|
||||
.label = Сохранить ссылки
|
||||
editor-noteLinkPreview =
|
||||
.label = Show note link preview on hover
|
||||
|
||||
sync-title = Синк
|
||||
sync-period-label = Авто-синк период (сек)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ editor-title = Note Editor
|
|||
editor-expandLevel-label = Anahat başlık seviyesine genişletildi
|
||||
editor-keepLinks =
|
||||
.label = Not linklerini anahatta göster
|
||||
editor-noteLinkPreview =
|
||||
.label = Show note link preview on hover
|
||||
|
||||
sync-title = Senkronize Et
|
||||
sync-period-label = Oto-Senkronize aralığı (saniye)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ editor-title = 笔记编辑器
|
|||
editor-expandLevel-label = 大纲展开至标题层级
|
||||
editor-keepLinks =
|
||||
.label = 在大纲中显示笔记链接
|
||||
editor-noteLinkPreview =
|
||||
.label = 鼠标悬停时显示笔记链接预览
|
||||
|
||||
sync-title = 同步
|
||||
sync-period-label = 自动同步周期 (秒)
|
||||
|
|
|
|||
|
|
@ -18,15 +18,10 @@ pref("__prefsPrefix__.exportPDF", false);
|
|||
pref("__prefsPrefix__.exportFreeMind", false);
|
||||
pref("__prefsPrefix__.exportNote", false);
|
||||
|
||||
pref("__prefsPrefix__.OCREngine", "bing");
|
||||
pref("__prefsPrefix__.OCRMathpix.Appid", "");
|
||||
pref("__prefsPrefix__.OCRMathpix.Appkey", "");
|
||||
pref("__prefsPrefix__.OCRXunfei.APPID", "");
|
||||
pref("__prefsPrefix__.OCRMathpix.APISecret", "");
|
||||
pref("__prefsPrefix__.OCRMathpix.APIKey", "");
|
||||
|
||||
pref("__prefsPrefix__.workspace.outline.expandLevel", 2);
|
||||
pref("__prefsPrefix__.workspace.outline.keepLinks", true);
|
||||
|
||||
pref("__prefsPrefix__.editor.noteLinkPreview", true);
|
||||
|
||||
pref("__prefsPrefix__.openNote.takeover", true);
|
||||
pref("__prefsPrefix__.openNote.defaultAsWindow", false);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
import { EditorState, Plugin, PluginKey } from "prosemirror-state";
|
||||
import { Popup } from "./popup";
|
||||
|
||||
export { initLinkPreviewPlugin };
|
||||
|
||||
declare const _currentEditorInstance: {
|
||||
_editorCore: EditorCore;
|
||||
};
|
||||
|
||||
interface LinkPreviewOptions {
|
||||
setPreviewContent: (
|
||||
link: string,
|
||||
setContent: (content: string) => void,
|
||||
) => void;
|
||||
|
||||
openURL: (url: string) => void;
|
||||
}
|
||||
|
||||
class LinkPreviewState {
|
||||
state: EditorState;
|
||||
|
||||
options: LinkPreviewOptions;
|
||||
|
||||
popup: Popup | null = null;
|
||||
|
||||
node: HTMLElement | null = null;
|
||||
|
||||
currentLink: string | null = null;
|
||||
|
||||
hasHover = false;
|
||||
|
||||
constructor(state: EditorState, options: LinkPreviewOptions) {
|
||||
this.state = state;
|
||||
this.options = options;
|
||||
this.update(state);
|
||||
}
|
||||
|
||||
update(state: EditorState, prevState?: EditorState) {
|
||||
this.state = state;
|
||||
|
||||
if (
|
||||
prevState &&
|
||||
prevState.doc.eq(state.doc) &&
|
||||
prevState.selection.eq(state.selection)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Handle selection change
|
||||
setTimeout(() => {
|
||||
this.popup?.layoutPopup(this);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.popup?.remove();
|
||||
}
|
||||
|
||||
handleMouseMove = async (event: MouseEvent) => {
|
||||
const { target } = event;
|
||||
|
||||
let isValid = false;
|
||||
if (target instanceof HTMLElement) {
|
||||
const href = target.closest("a")?.getAttribute("href");
|
||||
if (href?.startsWith("zotero://note/")) {
|
||||
isValid = true;
|
||||
if (this.currentLink !== href) {
|
||||
this.node = target;
|
||||
this.currentLink = href;
|
||||
this.hasHover = true;
|
||||
this.tryOpenPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid && this.currentLink) {
|
||||
this.hasHover = false;
|
||||
this.currentLink = null;
|
||||
this.tryClosePopup();
|
||||
}
|
||||
};
|
||||
|
||||
tryOpenPopup() {
|
||||
const href = this.currentLink!;
|
||||
setTimeout(() => {
|
||||
if (this.currentLink === href) {
|
||||
this._openPopup();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
_openPopup() {
|
||||
console.log("Enter Link Preview", this.currentLink, this.options);
|
||||
document.querySelectorAll(".link-preview").forEach((el) => el.remove());
|
||||
this.popup = new Popup(document, "link-preview", [
|
||||
document.createRange().createContextualFragment(`
|
||||
<style>
|
||||
.link-preview > .popup {
|
||||
max-width: 360px;
|
||||
max-height: 360px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.link-preview > .popup > * {
|
||||
margin-block: 0;
|
||||
}
|
||||
.link-preview .primary-editor img:not(.ProseMirror-separator) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>`),
|
||||
]);
|
||||
this.popup.popup.classList.add("primary-editor");
|
||||
this.popup.container.style.display = "none";
|
||||
|
||||
this.popup.layoutPopup(this);
|
||||
|
||||
this.options.setPreviewContent(this.currentLink!, (content: string) => {
|
||||
this.popup?.popup.append(
|
||||
document.createRange().createContextualFragment(content),
|
||||
);
|
||||
this.popup!.container.style.removeProperty("display");
|
||||
this.popup?.layoutPopup(this);
|
||||
});
|
||||
|
||||
this.popup.container.addEventListener("mouseleave", () => {
|
||||
this.currentLink = null;
|
||||
this.tryClosePopup();
|
||||
});
|
||||
|
||||
this.popup.container.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.localName === "a") {
|
||||
const href = target.getAttribute("href");
|
||||
if (href) {
|
||||
this.options.openURL(href);
|
||||
}
|
||||
}
|
||||
this._closePopup();
|
||||
});
|
||||
}
|
||||
|
||||
tryClosePopup() {
|
||||
setTimeout(() => {
|
||||
console.log("Close Link Preview", this.currentLink, this.popup?.hasHover);
|
||||
if (this.hasHover || this.popup?.hasHover) {
|
||||
return;
|
||||
}
|
||||
this._closePopup();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
_closePopup() {
|
||||
this.node = null;
|
||||
document.querySelectorAll(".link-preview").forEach((el) => el.remove());
|
||||
this.popup = null;
|
||||
}
|
||||
}
|
||||
|
||||
function initLinkPreviewPlugin(options: LinkPreviewOptions) {
|
||||
const core = _currentEditorInstance._editorCore;
|
||||
console.log("Init BN Link Preview Plugin");
|
||||
const key = new PluginKey("linkPreviewPlugin");
|
||||
const newState = core.view.state.reconfigure({
|
||||
plugins: [
|
||||
...core.view.state.plugins,
|
||||
new Plugin({
|
||||
key,
|
||||
state: {
|
||||
init(config, state) {
|
||||
return new LinkPreviewState(state, options);
|
||||
},
|
||||
apply: (tr, pluginState, oldState, newState) => {
|
||||
pluginState.update(newState, oldState);
|
||||
return pluginState;
|
||||
},
|
||||
},
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
mousemove: (view, event) => {
|
||||
const pluginState = key.getState(view.state) as LinkPreviewState;
|
||||
pluginState.update(view.state);
|
||||
pluginState.handleMouseMove(event);
|
||||
},
|
||||
wheel: (view, event) => {
|
||||
const pluginState = key.getState(view.state) as LinkPreviewState;
|
||||
pluginState.popup?.layoutPopup(pluginState);
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
core.view.updateState(newState);
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
export { Popup };
|
||||
|
||||
class Popup {
|
||||
_popup: HTMLDivElement;
|
||||
|
||||
hasHover = false;
|
||||
|
||||
get container() {
|
||||
return this._popup;
|
||||
}
|
||||
|
||||
get popup() {
|
||||
return this._popup.querySelector(".popup") as HTMLDivElement;
|
||||
}
|
||||
|
||||
constructor(
|
||||
doc: Document,
|
||||
className?: string,
|
||||
children: (HTMLElement | DocumentFragment)[] = [],
|
||||
) {
|
||||
this._popup = doc.createElement("div");
|
||||
this._popup.className = `popup-container ${className}`;
|
||||
this._popup.innerHTML = `
|
||||
<div class="popup popup-top">
|
||||
</div>
|
||||
`;
|
||||
this.popup.append(...children);
|
||||
doc.querySelector(".relative-container")?.appendChild(this._popup);
|
||||
|
||||
this._popup.addEventListener("mouseenter", () => {
|
||||
this.hasHover = true;
|
||||
});
|
||||
|
||||
this._popup.addEventListener("mouseleave", () => {
|
||||
this.hasHover = false;
|
||||
});
|
||||
}
|
||||
|
||||
layoutPopup(pluginState: any) {
|
||||
const rect = pluginState.rect || pluginState.node?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
const padding = 10;
|
||||
|
||||
const editor = document.querySelector(".editor-core") as HTMLElement;
|
||||
|
||||
const popupParent = this.container.parentElement!;
|
||||
|
||||
const parentScrollTop = popupParent.scrollTop;
|
||||
const parentTop = popupParent.getBoundingClientRect().top;
|
||||
const popupHeight = this.popup.offsetHeight;
|
||||
const maxWidth = this.container.offsetWidth;
|
||||
const topSpace = rect.top - popupHeight - padding;
|
||||
|
||||
let top;
|
||||
|
||||
if (topSpace < 0) {
|
||||
// Bottom
|
||||
const otherPopupHeight = Array.from(
|
||||
popupParent.querySelectorAll(
|
||||
".popup-container:not(.link-preview) > .popup.popup-bottom",
|
||||
),
|
||||
).reduce((acc, el) => acc + (el as HTMLElement).offsetHeight, 0);
|
||||
top =
|
||||
parentScrollTop +
|
||||
(rect.bottom - parentTop) +
|
||||
otherPopupHeight +
|
||||
padding;
|
||||
this.popup.classList.remove("popup-top");
|
||||
this.popup.classList.add("popup-bottom");
|
||||
} else {
|
||||
// Top
|
||||
const otherPopupHeight = Array.from(
|
||||
popupParent.querySelectorAll(
|
||||
".popup-container:not(.link-preview) > .popup.popup-top",
|
||||
),
|
||||
).reduce((acc, el) => acc + (el as HTMLElement).offsetHeight, 0);
|
||||
top =
|
||||
parentScrollTop +
|
||||
(rect.top - parentTop) -
|
||||
popupHeight -
|
||||
otherPopupHeight -
|
||||
padding;
|
||||
this.popup.classList.remove("popup-bottom");
|
||||
this.popup.classList.add("popup-top");
|
||||
}
|
||||
|
||||
const width = this.popup.offsetWidth;
|
||||
let left = rect.left + (rect.right - rect.left) / 2 - width / 2 + 1;
|
||||
|
||||
if (left + width >= maxWidth) {
|
||||
left = maxWidth - width;
|
||||
}
|
||||
|
||||
if (left < 2) {
|
||||
left = 2;
|
||||
}
|
||||
|
||||
this.popup.style.top = Math.round(top) + "px";
|
||||
this.popup.style.left = Math.round(left) + "px";
|
||||
|
||||
// Make sure the popup height is not larger than the editor height
|
||||
// if (editor) {
|
||||
// const editorRect = editor.getBoundingClientRect();
|
||||
// const popupRect = this.popup.getBoundingClientRect();
|
||||
// if (popupRect.bottom > editorRect.bottom + padding) {
|
||||
// this.popup.style.maxHeight =
|
||||
// editorRect.bottom - popupRect.top - padding + "px";
|
||||
// } else {
|
||||
// this.popup.style.maxHeight = "";
|
||||
// }
|
||||
// }
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
remove() {
|
||||
this._popup.remove();
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from "prosemirror-model";
|
||||
import { EditorState, TextSelection } from "prosemirror-state";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import { initLinkPreviewPlugin } from "./editor/linkPreview";
|
||||
|
||||
declare const _currentEditorInstance: {
|
||||
_editorCore: EditorCore;
|
||||
|
|
@ -376,6 +377,7 @@ export const BetterNotesEditorAPI = {
|
|||
getSliceFromHTML,
|
||||
getNodeFromHTML,
|
||||
setSelection,
|
||||
initLinkPreviewPlugin,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { initEditorImagePreviewer } from "./image";
|
||||
import { injectEditorCSS, injectEditorScripts } from "./inject";
|
||||
import { initEditorLinkPreview } from "./linkPreview";
|
||||
import { initEditorMenu } from "./menu";
|
||||
import { initEditorPopup } from "./popup";
|
||||
import { initEditorToolbar } from "./toolbar";
|
||||
|
|
@ -36,4 +37,5 @@ async function onEditorInstanceCreated(editor: Zotero.EditorInstance) {
|
|||
await initEditorToolbar(editor);
|
||||
initEditorPopup(editor);
|
||||
initEditorMenu(editor);
|
||||
initEditorLinkPreview(editor);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { initLinkPreview } from "../../utils/editor";
|
||||
|
||||
export function initEditorLinkPreview(editor: Zotero.EditorInstance) {
|
||||
initLinkPreview(editor);
|
||||
}
|
||||
|
|
@ -186,7 +186,11 @@ function link2params(link: string) {
|
|||
|
||||
async function link2html(
|
||||
link: string,
|
||||
options: { noteItem?: Zotero.Item; dryRun?: boolean } = {},
|
||||
options: {
|
||||
noteItem?: Zotero.Item;
|
||||
dryRun?: boolean;
|
||||
usePosition?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
ztoolkit.log("link2html", link, options);
|
||||
const linkParams = getNoteLinkParams(link);
|
||||
|
|
@ -196,8 +200,22 @@ async function link2html(
|
|||
const refIds = getLinkedNotesRecursively(link);
|
||||
const refNotes = options.noteItem ? Zotero.Items.get(refIds) : [];
|
||||
ztoolkit.log(refIds);
|
||||
const html =
|
||||
addon.api.sync.getNoteStatus(linkParams.noteItem.id)?.content || "";
|
||||
let html;
|
||||
if (options.usePosition) {
|
||||
const item = linkParams.noteItem;
|
||||
let lineIndex = linkParams.lineIndex;
|
||||
|
||||
if (typeof linkParams.sectionName === "string") {
|
||||
const sectionTree = addon.api.note.getNoteTreeFlattened(item);
|
||||
const sectionNode = sectionTree.find(
|
||||
(node) => node.model.name.trim() === linkParams.sectionName!.trim(),
|
||||
);
|
||||
lineIndex = sectionNode?.model.lineIndex;
|
||||
}
|
||||
html = addon.api.note.getLinesInNote(item).slice(lineIndex).join("\n");
|
||||
} else {
|
||||
html = addon.api.sync.getNoteStatus(linkParams.noteItem.id)?.content || "";
|
||||
}
|
||||
if (options.dryRun) {
|
||||
return await renderNoteHTML(html, refNotes);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import TreeModel = require("tree-model");
|
||||
import { TextSelection } from "prosemirror-state";
|
||||
import { getNoteTreeFlattened } from "./note";
|
||||
import { getPref } from "./prefs";
|
||||
|
||||
export {
|
||||
insert,
|
||||
|
|
@ -24,6 +25,7 @@ export {
|
|||
getTextBetween,
|
||||
getTextBetweenLines,
|
||||
isImageAtCursor,
|
||||
initLinkPreview,
|
||||
};
|
||||
|
||||
function insert(
|
||||
|
|
@ -433,3 +435,38 @@ function getTextBetweenLines(
|
|||
const to = getPositionAtLine(editor, toIndex, "end");
|
||||
return core.view.state.doc.textBetween(from, to);
|
||||
}
|
||||
|
||||
function initLinkPreview(editor: Zotero.EditorInstance) {
|
||||
if (!getPref("editor.noteLinkPreview")) {
|
||||
return;
|
||||
}
|
||||
const EditorAPI = getEditorAPI(editor);
|
||||
EditorAPI.initLinkPreviewPlugin(
|
||||
Components.utils.cloneInto(
|
||||
{
|
||||
setPreviewContent: (
|
||||
link: string,
|
||||
setContent: (content: string) => void,
|
||||
) => {
|
||||
const note = addon.api.convert.link2note(link);
|
||||
if (!note) {
|
||||
setContent(`<p style="color: red;">Invalid note link: ${link}</p>`);
|
||||
return;
|
||||
}
|
||||
addon.api.convert
|
||||
.link2html(link, {
|
||||
noteItem: note,
|
||||
dryRun: true,
|
||||
usePosition: true,
|
||||
})
|
||||
.then((content) => setContent(content));
|
||||
},
|
||||
openURL: (url: string) => {
|
||||
Zotero.getActiveZoteroPane().loadURI(url);
|
||||
},
|
||||
},
|
||||
editor._iframeWindow,
|
||||
{ wrapReflectors: true, cloneFunctions: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ async function renderNoteHTML(
|
|||
if (await attachment.fileExists()) {
|
||||
const imageNodes = Array.from(
|
||||
doc.querySelectorAll(`img[data-attachment-key="${attachment.key}"]`),
|
||||
);
|
||||
) as HTMLImageElement[];
|
||||
if (imageNodes.length) {
|
||||
try {
|
||||
const b64 = await getItemDataURL(attachment);
|
||||
|
|
@ -244,6 +244,12 @@ async function renderNoteHTML(
|
|||
);
|
||||
}
|
||||
}
|
||||
if (node.hasAttribute("width")) {
|
||||
node.style.width = `${node.getAttribute("width")}px`;
|
||||
}
|
||||
if (node.hasAttribute("height")) {
|
||||
node.style.width = `${node.getAttribute("height")}px`;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
ztoolkit.log(e);
|
||||
|
|
|
|||
Loading…
Reference in New Issue