joplin/packages/app-desktop/gui/note-viewer/index.html

812 lines
28 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

<!DOCTYPE html>
<html>
<head id="joplin-container-root-head">
<meta charset="UTF-8">
<title>Note viewer</title>
<style>
body {
overflow: hidden;
}
#joplin-container-content {
/* Needs this in case the content contains elements with absolute positioning */
/* Without this they would just stay at a fixed position when scrolling */
position: relative;
overflow-y: auto;
padding-left: 10px;
padding-right: 10px;
/* Note: the height is set via updateBodyHeight(). Setting it here to 100% */
/* won't work with some pages due to the position: relative */
}
#rendered-md {
/* This is used to enable the scroll-past end behaviour. The same height should */
/* be applied to the editor. */
padding-bottom: 400px;
}
.mark-selected {
background: #CF3F00;
color: white;
}
ul ul, ul ol, ol ul, ol ol {
margin-bottom: 0px;
}
</style>
</head>
<body id="joplin-container-body">
<div id="joplin-container-pluginAssetsContainer"></div>
<div id="joplin-container-markScriptContainer"></div>
<div id="joplin-container-content" ondragstart="return false;" ondrop="return false;"></div>
<script src="./lib.js"></script>
<script src="./scrollmap.js"></script>
<script>
// This is function used internally to send message from the webview to
// the host.
const ipcProxySendToHost = (methodName, arg) => {
window.parent.postMessage({ target: 'main', name: methodName, args: [ arg ] }, '*');
}
const webviewApiPromises_ = {};
// This function is reserved for plugin, currently only to allow
// executing a command, but more features could be added to the object
// later on.
const webviewApi = {
postMessage: function(contentScriptId, message) {
const messageId = 'noteViewer_' + Date.now() + Math.random();
const promise = new Promise((resolve, reject) => {
webviewApiPromises_[messageId] = { resolve, reject };
});
ipcProxySendToHost('postMessageService.message', {
contentScriptId: contentScriptId,
viewId: '',
from: 'contentScript',
to: 'plugin',
id: messageId,
content: message,
});
return promise;
},
}
let pluginAssetsAdded_ = {};
try {
const contentElement = document.getElementById('joplin-container-content');
const ipc = {};
window.addEventListener('message', webviewLib.logEnabledEventHandler(event => {
// Here we only deal with messages that are sent from the main Electron process to the webview.
if (!event.data || event.data.target !== 'webview') return;
const callName = event.data.name;
const callData = event.data.data;
if (!ipc[callName]) {
console.warn('Missing IPC function:', event.data);
} else {
ipc[callName](callData);
}
}));
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
// so that it is not greater than contentHeight. On the other hand, with percentScroll it is possible to restore
// it at any time knowing that it's not going to be changed because the content height has changed.
let percentScroll_ = 0;
let ignoreNextScrollTime_ = Date.now();
let ignoreNextScrollEventCount_ = 0;
// ignoreNextScrollEvent() provides a way to skip scroll events for a certain duration.
// In general, it should be called whenever the scroll value is set explicitly (programmatically)
// so as to differentiate scroll events generated by the user (when scrolling the view) and those
// generated by the application.
function ignoreNextScrollEvent() {
const now = Date.now();
if (now >= ignoreNextScrollTime_) ignoreNextScrollEventCount_ = 0;
if (ignoreNextScrollEventCount_ < 10) { // for safety
ignoreNextScrollTime_ = now + 1000;
ignoreNextScrollEventCount_ += 1;
}
};
// Tests the next scroll event should be ignored and then decrements the count.
function isNextScrollEventIgnored() {
if (ignoreNextScrollEventCount_) {
if (Date.now() < ignoreNextScrollTime_) {
ignoreNextScrollEventCount_ -= 1;
return true;
}
ignoreNextScrollEventCount_ = 0;
}
return false;
}
function setPercentScroll(percent) {
// calculates viewer's GUI-dependent pixel-based raw percent
const viewerPercent = scrollmap.translateL2V(percent);
const newScrollTop = viewerPercent * maxScrollTop();
// Even if the scroll position hasn't changed (percent is the same),
// we still ignore the next scroll event, so that it doesn't create
// undesired side effects.
// https://github.com/laurent22/joplin/issues/7617
ignoreNextScrollEvent();
if (Math.floor(contentElement.scrollTop) !== Math.floor(newScrollTop)) {
percentScroll_ = percent;
contentElement.scrollTop = newScrollTop;
}
}
function restorePercentScroll() {
setPercentScroll(percentScroll_);
}
// Note that this function keeps track of what's been added so as not to
// add the same CSS files multiple times.
function addPluginAssets(assets) {
if (!assets) return;
const pluginAssetsContainer = document.getElementById('joplin-container-pluginAssetsContainer');
const processedAssetIds = [];
for (let i = 0; i < assets.length; i++) {
const asset = assets[i];
// # and ? can be used in valid paths and shouldn't be treated as the start of a query or fragment
const encodedPath = asset.path
.replaceAll('#','%23')
.replaceAll('?','%3F')
const assetId = asset.name ? asset.name : encodedPath;
processedAssetIds.push(assetId);
if (pluginAssetsAdded_[assetId]) continue;
let element = null;
// Needed on Windows:
// C:/Path/Here
// is interpreted as a file path, even without a starting file://.
let src = encodedPath;
if (src.match(/^[/]/) || src.match(/^[^:/\\]+[:][\\/]/)) {
src = `joplin-content://note-viewer/${src}`;
}
if (asset.mime === 'application/javascript') {
element = document.createElement('script');
element.src = src;
pluginAssetsContainer.appendChild(element);
} else if (asset.mime === 'text/css') {
element = document.createElement('link');
element.rel = 'stylesheet';
element.href = src;
pluginAssetsContainer.appendChild(element);
}
pluginAssetsAdded_[assetId] = {
element,
}
}
// Once we have added the relevant assets, we also remove those that
// are no longer needed. It's necessary in particular for the CSS
// generated by noteStyle - if we don't remove it, we might end up
// with two or more stylesheet and that will create conflicts.
//
// It was happening for example when automatically switching from
// light to dark theme, and then back to light theme - in that case
// the viewer would remain dark because it would use the dark
// stylesheet that would still be in the DOM.
for (const [assetId, asset] of Object.entries(pluginAssetsAdded_)) {
if (!processedAssetIds.includes(assetId)) {
try {
if (asset?.element) asset.element.remove();
} catch (error) {
// We don't throw an exception but we log it since
// it shouldn't happen
console.warn('Tried to remove asset ' + assetId + ' but got an error:', error);
console.warn('Assets are:', pluginAssetsAdded_);
}
pluginAssetsAdded_[assetId] = null;
}
}
}
ipc.scrollToHash = (event) => {
let retry = 0;
const fn = () => {
if (window.scrollToHashTimeoutID_) {
clearInterval(window.scrollToHashTimeoutID_);
window.scrollToHashTimeoutID_ = null;
}
if (document.readyState === 'complete' ||
// If scrollmap is present, Element.scrollIntoView() is also
// available when document.readyState is interactive.
document.readyState === 'interactive' && scrollmap.isPresent()) {
const hash = event.hash.toLowerCase();
const e = document.getElementById(hash);
if (e) {
e.scrollIntoView();
// It causes a scroll event, whose listener sent a new scroll
// position to Editor.
} else {
console.warn('Cannot find hash', hash);
}
} else {
retry += 1;
if (retry <= 10) {
window.scrollToHashTimeoutID_ = setTimeout(fn, 100);
}
}
};
fn();
}
function isVisible() {
// See the logic of hiding viewer in CoderMirror.tsx
return window.innerWidth > 1;
}
// https://stackoverflow.com/a/1977898/561309
function isImageReady(img) {
if (!img.complete) return false;
if (!img.naturalWidth || !img.naturalHeight) return false;
return true;
}
function allImagesLoaded() {
if (!isVisible()) return true; // In the case, images would not be loaded.
for (const image of document.images) {
if (!isImageReady(image)) return false;
}
return true;
}
let alreadyAllImagesLoaded = false;
// During a note is being rendered, its height is varying. To keep scroll
// consistency, observing the height of the content element and updating its
// scroll position is required. For the purpose, 'ResizeObserver' is used.
// ResizeObserver is standard and an element's counterpart to 'window.resize'
// event. It's overhead is cheaper than observation using an interval timer.
//
// To observe the scroll height of the content element, adding, removing and
// resizing of its children should be observed. So, the combination of
// ResizeObserver (used for resizing) and MutationObserver (used for ading
// and removing) is used.
//
// References:
// https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
// https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
//
// By using them, this observeRendering() function provides a efficient way
// to observe the changes of the scroll height of the content element
// using a callback approach.
function observeRendering(callback, compress = false) {
let lastScrollHeight = 0;
let lastClientHeight = 0;
const fn = (cause) => {
const sh = contentElement.scrollHeight;
const ch = contentElement.clientHeight;
const heightChanged = (sh !== lastScrollHeight || ch !== lastClientHeight);
if (!compress || heightChanged) {
lastScrollHeight = sh;
lastClientHeight = ch;
callback(cause, sh, heightChanged);
}
};
// 'resized' means DOM Layout change or Window resize event
let resizeObserver = new ResizeObserver(() => fn('resized'));
// An HTML document to be rendered is added and removed as a child of
// the content element for each setHtml() invocation.
let mutationObserver = new MutationObserver(entries => {
const e = entries[0];
e.removedNodes.forEach(n => n instanceof Element && resizeObserver.unobserve(n));
e.addedNodes.forEach(n => n instanceof Element && resizeObserver.observe(n));
if (e.removedNodes.length + e.addedNodes.length) fn('dom-changed');
});
mutationObserver.observe(contentElement, { childList: true });
return { mutationObserver, resizeObserver };
};
// To suppress too frequent restoring of scroll positions and refreshing of the scroll map
let restoreAndRefreshTimeoutID_ = null;
let restoreAndRefreshTimeout_ = Date.now();
// If 'noteRenderComplete' message is ongoing, resizing should not trigger a 'percentScroll' messsage.
let noteRenderCompleteMessageIsOngoing_ = false;
// A callback anonymous function invoked when the scroll height changes.
const onRendering = observeRendering((cause, height, heightChanged) => {
if (!alreadyAllImagesLoaded && !scrollmap.isPresent()) {
const loaded = allImagesLoaded();
if (loaded) {
alreadyAllImagesLoaded = true;
scrollmap.refresh();
restorePercentScroll();
noteRenderCompleteMessageIsOngoing_ = true;
ipcProxySendToHost('noteRenderComplete');
return;
}
}
if (!heightChanged && cause !== 'dom-changed') return;
const restoreAndRefresh = () => {
scrollmap.refresh();
restorePercentScroll();
// To ensures Editor's scroll position is synced with Viewer's
if (!noteRenderCompleteMessageIsOngoing_) ipcProxySendToHost('percentScroll', percentScroll_);
};
const now = Date.now();
if (now < restoreAndRefreshTimeout_) {
if (restoreAndRefreshTimeoutID_) {
clearTimeout(restoreAndRefreshTimeoutID_);
restoreAndRefreshTimeoutID_ = null;
}
const msec = Math.min(1000, restoreAndRefreshTimeout_ - now);
restoreAndRefreshTimeoutID_ = setTimeout(restoreAndRefresh, msec);
} else {
restoreAndRefresh();
}
restoreAndRefreshTimeout_ = now + 200;
});
ipc.focus = (event) => {
const dummyID = 'joplin-content-focus-dummy';
if (! document.getElementById(dummyID)) {
const focusDummy = '<div style="width: 0; height: 0; overflow: hidden"><a id="' + dummyID + '" href="#">Note viewer top</a></div>';
contentElement.insertAdjacentHTML("afterbegin", focusDummy);
}
const scrollTop = contentElement.scrollTop;
document.getElementById(dummyID).focus();
contentElement.scrollTop = scrollTop;
}
const rewriteFileUrls = (accessKey) => {
if (!accessKey) return;
// To allow accessing local files from the viewer's non-file URL, file:// URLs are re-written
// to joplin-content:// URLs:
const mediaElements = document.querySelectorAll('video[src], audio[src], source[src], img[src]');
for (const element of mediaElements) {
if (element.src?.startsWith('file:')) {
const newUrl = element.src.replace(/^file:\/\//, 'joplin-content://file-media/');
element.src = `${newUrl}?access-key=${accessKey}`;
}
}
};
ipc.setHtml = (event) => {
const html = event.html;
markJsHackMarkerInserted_ = false;
updateBodyHeight();
alreadyAllImagesLoaded = false;
contentElement.innerHTML = html;
if (html.includes('file://')) {
rewriteFileUrls(event.options.mediaAccessKey);
}
scrollmap.create(event.options.markupLineCount);
if (typeof event.options.percent !== 'number') {
restorePercentScroll(); // First, a quick treatment is applied.
} else {
setPercentScroll(event.options.percent);
}
addPluginAssets(event.options.pluginAssets);
if (event.options.increaseControlSize) {
document.documentElement.classList.add('-larger-controls');
} else {
document.documentElement.classList.remove('-larger-controls');
}
if (event.options.downloadResources === 'manual') {
webviewLib.setupResourceManualDownload();
}
document.dispatchEvent(new Event('joplin-noteDidUpdate'));
if (scrollmap.isPresent()) {
// Now, ready to receive scrollToHash/setPercentScroll from Editor.
noteRenderCompleteMessageIsOngoing_ = true;
ipcProxySendToHost('noteRenderComplete');
}
}
ipc.setPercentScroll = (event) => {
noteRenderCompleteMessageIsOngoing_ = false;
setPercentScroll(event.percent);
}
// HACK for Mark.js bug - https://github.com/julmot/mark.js/issues/127
let markJsHackMarkerInserted_ = false;
function addMarkJsSpaceHack(document) {
if (markJsHackMarkerInserted_) return;
const prepareElementsForMarkJs = (elements, type) => {
// const markJsHackMarker_ = '&#8203; &#8203;'
const markJsHackMarker_ = ' ';
for (let i = 0; i < elements.length; i++) {
if (!type) {
elements[i].insertAdjacentHTML('beforeend', markJsHackMarker_);
} else if (type === 'insertBefore') {
elements[i].insertAdjacentHTML('beforeBegin', markJsHackMarker_);
}
}
}
prepareElementsForMarkJs(contentElement.getElementsByTagName('p'));
prepareElementsForMarkJs(contentElement.getElementsByTagName('div'));
prepareElementsForMarkJs(contentElement.getElementsByTagName('br'), 'insertBefore');
markJsHackMarkerInserted_ = true;
}
let mark_ = null;
let markSelectedElement_ = null;
function setMarkers(keywords, options = null) {
if (!options) options = {};
// TODO: Add support for scriptType on mobile and CLI
if (!mark_) {
mark_ = new Mark(document.getElementById('joplin-container-content'), {
exclude: ['img'],
acrossElements: true,
});
}
addMarkJsSpaceHack(document);
mark_.unmark()
if (markSelectedElement_) markSelectedElement_.classList.remove('mark-selected');
let selectedElement = null;
let elementIndex = 0;
const markKeywordOptions = {};
if ('separateWordSearch' in options) markKeywordOptions.separateWordSearch = options.separateWordSearch;
try {
for (const keyword of keywords) {
markJsUtils.markKeyword(mark_, keyword, {
pregQuote: pregQuote,
replaceRegexDiacritics: replaceRegexDiacritics,
}, markKeywordOptions);
}
} catch (error) {
if (error.name !== 'SyntaxError') {
throw error;
}
// An error of 'Regular expression too large' might occour in the markJs library
// when the input is really big, this catch is here to avoid the application crashing
// https://github.com/laurent22/joplin/issues/7634
console.error('Error while trying to highlight words from search: ', error);
}
}
let markLoader_ = { state: 'idle', whenDone: null };
ipc.setMarkers = (event) => {
const keywords = event.keywords;
const options = event.options;
if (!keywords.length && markLoader_.state === 'idle') return;
if (markLoader_.state === 'idle') {
markLoader_ = {
state: 'loading',
whenDone: {keywords:keywords, options:options},
};
const script = document.createElement('script');
script.onload = function() {
markLoader_.state = 'ready';
setMarkers(markLoader_.whenDone.keywords, markLoader_.whenDone.options);
};
script.src = '../../vendor/lib/mark.js/dist/mark.min.js';
document.getElementById('joplin-container-markScriptContainer').appendChild(script);
} else if (markLoader_.state === 'ready') {
setMarkers(keywords, options);
} else if (markLoader_.state === 'loading') {
markLoader_.whenDone = {keywords:keywords, options:options};
}
}
function maxScrollTop() {
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
}
function maxScrollLeft() {
return Math.max(0, contentElement.scrollWidth - contentElement.clientWidth);
}
// The body element needs to have a fixed height for the content to be scrollable
function updateBodyHeight() {
document.getElementById('joplin-container-body').style.height = window.innerHeight + 'px';
document.getElementById('joplin-container-content').style.height = window.innerHeight + 'px';
}
function getPercentFromViewer() {
const m = maxScrollTop();
// As of 2021, if zoomFactor != 1, underlying Chrome returns scrollTop with
// some numerical error. It can be more than maxScrollTop().
const viewerPecent = m ? Math.min(1, contentElement.scrollTop / m) : 0;
// calculates GUI-independent line-based logical percent
const percent = scrollmap.translateV2L(viewerPecent);
return percent;
}
// If zoom factor is not 1, Electron/Chromium calculates scrollTop incorrectly.
// This is automatically set.
let zoomFactorIsNotOne = false;
// When custom smooth scrolling is ongoing, remainedScrollDx/Dy keep the remaining
// amount of scrolling.
let remainedScrollDx = 0, remainedScrollDy = 0, remainedScrollTimerId = null;
function resetSmoothScroll() { remainedScrollDx = 0; remainedScrollDy = 0; }
// To avoid Electron/Chromium's scrolling bug when zoom fator is not 1,
// Custom scrolling is implemented. This is used only when zoom factor is not 1.
// If smoothly argument is true, smooth scrolling is performed.
// See https://github.com/laurent22/joplin/pull/5606#issuecomment-964293459
function customScroll(wheelEvent, smoothly) {
const linePixels = 100 / 3;
const pagePixelsX = Math.max(linePixels, contentElement.clientWidth);
const pagePixelsY = Math.max(linePixels, contentElement.clientHeight);
let pixelsPerUnitX = 1, pixelsPerUnitY = 1; // for WheelEvent.DOM_DELTA_PIXEL
if (wheelEvent.deltaMode === WheelEvent.DOM_DELTA_LINE) {
pixelsPerUnitX = pixelsPerUnitY = linePixels;
} else if (wheelEvent.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
pixelsPerUnitX = pagePixelsX;
pixelsPerUnitY = pagePixelsY;
}
if (!smoothly) {
if (wheelEvent.deltaX) {
const dx = wheelEvent.deltaX * pixelsPerUnitX;
contentElement.scrollLeft = Math.max(0, Math.min(maxScrollLeft(), contentElement.scrollLeft + dx));
}
if (wheelEvent.deltaY) {
const dy = wheelEvent.deltaY * pixelsPerUnitY;
contentElement.scrollTop = Math.max(0, Math.min(maxScrollTop(), contentElement.scrollTop + dy));
}
} else {
if (Math.sign(remainedScrollDx) !== Math.sign(wheelEvent.deltaX)) remainedScrollDx = 0;
if (Math.sign(remainedScrollDy) !== Math.sign(wheelEvent.deltaY)) remainedScrollDy = 0;
remainedScrollDx += wheelEvent.deltaX * pixelsPerUnitX;
remainedScrollDy += wheelEvent.deltaY * pixelsPerUnitY;
const maxDx = Math.max(8.5, Math.min(pagePixelsX, Math.abs(remainedScrollDx)) / 5);
const maxDy = Math.max(8.5, Math.min(pagePixelsY, Math.abs(remainedScrollDy)) / 5);
const f = () => {
if (remainedScrollTimerId) {
clearTimeout(remainedScrollTimerId);
remainedScrollTimerId = null;
}
if (remainedScrollDx) {
const dx = Math.max(-maxDx, Math.min(maxDx, remainedScrollDx));
remainedScrollDx -= dx;
contentElement.scrollLeft = Math.max(0, Math.min(maxScrollLeft(), contentElement.scrollLeft + dx));
}
if (remainedScrollDy) {
const dy = Math.max(-maxDy, Math.min(maxDy, remainedScrollDy));
remainedScrollDy -= dy;
contentElement.scrollTop = Math.max(0, Math.min(maxScrollTop(), contentElement.scrollTop + dy));
}
if (remainedScrollDx || remainedScrollDy) remainedScrollTimerId = setTimeout(f, 20);
};
f();
}
}
contentElement.addEventListener('wheel', webviewLib.logEnabledEventHandler(e => {
// When zoomFactor is not 1 (using an HD display is a typical case),
// DOM element's scrollTop is incorrectly calculated after wheel scroll events
// in the layer of Electron/Chromium, as of 2021-09-23.
// To avoid this problem, prevent the upstream from calculating scrollTop and
// calculate by yourself by accumulating wheel events.
// https://github.com/laurent22/joplin/pull/5496
// When the Electron/Chromium bug is fixed, remove this listener.
//
// 2024-02-01: The bug seems to be fixed, remove the above when we're not in
// feature-freeze.
// If scrollTop ever has a fraction part, zoomFactor is not 1.
if (zoomFactorIsNotOne || !Number.isInteger(contentElement.scrollTop)) {
zoomFactorIsNotOne = true;
// The custom scroll logic breaks horizontal scroll in child DOM nodes
// (e.g. scrollable code blocks). Disable it:
if (e.deltaY !== 0) {
customScroll(e, true);
e.preventDefault();
}
}
}));
contentElement.addEventListener('scroll', webviewLib.logEnabledEventHandler(e => {
lastScrollTop_ = contentElement.scrollTop;
// If the last scroll event was done by the application, ignoreNextScrollEvent() is called and
// we can use that to skip the event handling. We skip it because in that case
// the scroll position has already been updated. Also we add a 200ms interval
// because otherwise it's most likely a glitch where we called ipc.setPercentScroll
// but the scroll event listener has not been called.
if (isNextScrollEventIgnored()) return;
percentScroll_ = getPercentFromViewer();
ipcProxySendToHost('percentScroll', percentScroll_);
}));
ipc['postMessageService.response'] = function(event) {
const promise = webviewApiPromises_[event.responseId];
if (!promise) {
console.warn('postMessageService.response: could not find callback for message', event);
return;
}
if (event.error) {
promise.reject(event.error);
} else {
promise.resolve(event.response);
}
}
ipc.textSelected = function(event) {
ipcProxySendToHost('contextMenu', {
type: 'text',
textToCopy: event.text,
});
}
ipc.openPdfViewer = function(event) {
ipcProxySendToHost('openPdfViewer', { resourceId: event.resourceId, mime: 'application/pdf', pageNo: event.pageNo || 1 });
}
window.addEventListener('hashchange', webviewLib.logEnabledEventHandler(e => {
if (!window.location.hash) return;
// The timeout is necessary to prevent a race condition and give time for the window to scroll
setTimeout(() => {
// Reset the window hash to allow clicking on the same anchor link more than once
window.location.hash = '';
}, 100);
}));
document.addEventListener('contextmenu', webviewLib.logEnabledEventHandler(event => {
// To handle right clicks on resource icons
let element = event.target;
// Mermaid svgs are wrapped inside a <pre> with class "mermaid"
let mermaidElement = element.closest(".mermaid")?.children[0];
if (mermaidElement) {
const svgString = new XMLSerializer().serializeToString(mermaidElement);
if (!!svgString) {
ipcProxySendToHost('contextMenu', {
type: 'image',
textToCopy: svgString,
mime: 'image/svg+xml',
filename: mermaidElement.id + '.svg',
});
}
return;
}
if (element && !element.getAttribute('data-resource-id')) element = element.parentElement;
if (element && element.getAttribute('data-resource-id')) {
ipcProxySendToHost('contextMenu', {
type: element.getAttribute('src') ? 'image' : 'resource',
resourceId: element.getAttribute('data-resource-id'),
});
} else {
const selectedText = window.getSelection().toString();
if (selectedText) {
const linkToCopy = event.target && event.target.getAttribute('href') ? event.target.getAttribute('href') : null;
ipcProxySendToHost('contextMenu', {
type: 'text',
textToCopy: selectedText,
linkToCopy: linkToCopy,
});
} else if (event.target.getAttribute('href')) {
ipcProxySendToHost('contextMenu', {
type: 'link',
textToCopy: event.target.getAttribute('href'),
});
}
}
}));
webviewLib.initialize({
postMessage: ipcProxySendToHost,
});
// Disable drag and drop otherwise it's possible to drop a URL
// on it and it will open in the view as a website.
document.addEventListener('drop', webviewLib.logEnabledEventHandler(e => {
e.preventDefault();
e.stopPropagation();
}));
document.addEventListener('dragover', webviewLib.logEnabledEventHandler(e => {
e.preventDefault();
e.stopPropagation();
}));
document.addEventListener('dragover', webviewLib.logEnabledEventHandler(e => {
e.preventDefault();
}));
document.addEventListener('click', webviewLib.logEnabledEventHandler(e => {
// Links should all have custom click handlers. Allowing Electron to load custom links
// can cause security issues, particularly if these links have the same domain as the
// top-level page.
if (e.target.hasAttribute('href')) {
e.preventDefault();
}
document.querySelectorAll('.media-pdf').forEach(element => {
if(!!element.contentWindow){
element.contentWindow.postMessage({
type: 'blur'
}, '*');
}
}
);
}));
let lastClientWidth_ = NaN, lastClientHeight_ = NaN, lastScrollTop_ = NaN;
window.addEventListener('resize', webviewLib.logEnabledEventHandler(() => {
updateBodyHeight();
// When zoomFactor is changed, resize event happens.
zoomFactorIsNotOne = false;
resetSmoothScroll();
// If this event resizes contentElement, ignore the scroll event caused by it.
const cw = contentElement.clientWidth;
const ch = contentElement.clientHeight;
const top = contentElement.scrollTop;
if (!(cw === lastClientWidth_ && ch === lastClientHeight_)) {
// Since scroll listeners are invoked before ResizeObserver and
// resize listeners are invoked before scroll listeners,
// this code should be here to ignore scroll events.
if (top !== lastScrollTop_) ignoreNextScrollEvent();
lastClientWidth_ = cw; lastClientHeight_ = ch; lastScrollTop_ = top;
}
}));
// Prevent middle-click as that would open the URL in an Electron window
// https://github.com/laurent22/joplin/issues/3287
window.addEventListener('auxclick', webviewLib.logEnabledEventHandler((event) => {
event.preventDefault();
}));
updateBodyHeight();
} catch (error) {
ipcProxySendToHost('error:' + JSON.stringify(webviewLib.cloneError(error)));
throw error;
}
</script>
</body>
</html>