mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-15 09:04:04 +02:00
442 lines
12 KiB
HTML
442 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
|
|
<style>
|
|
body {
|
|
overflow: hidden;
|
|
}
|
|
|
|
#content {
|
|
overflow-y: auto;
|
|
height: 100%;
|
|
padding-left: 10px;
|
|
padding-right: 10px;
|
|
}
|
|
|
|
mark {
|
|
background: #F3B717;
|
|
color: black;
|
|
}
|
|
|
|
.mark-selected {
|
|
background: #CF3F00;
|
|
color: white;
|
|
}
|
|
|
|
ul ul, ul ol, ol ul, ol ol {
|
|
margin-bottom: 0px;
|
|
}
|
|
|
|
.katex { font-size: 1.3em; } /* This controls the global Katex font size*/
|
|
</style>
|
|
</head>
|
|
|
|
<body id="body">
|
|
<div id="hlScriptContainer"></div>
|
|
<div id="markScriptContainer"></div>
|
|
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
|
|
|
|
<script>
|
|
const contentElement = document.getElementById('content');
|
|
|
|
const ipc = {};
|
|
|
|
window.addEventListener('message', (event) => {
|
|
// Here we only deal with messages that are sent from the main Electro 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);
|
|
}
|
|
});
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Handle dynamically loading HLJS when a code element is present
|
|
// ----------------------------------------------------------------------
|
|
|
|
let hljsScriptAdded = false;
|
|
let hljsLoaded = false;
|
|
|
|
function loadHljs(options) {
|
|
hljsScriptAdded = true;
|
|
|
|
const script = document.createElement('script');
|
|
script.onload = function () {
|
|
hljsLoaded = true;
|
|
applyHljs();
|
|
};
|
|
script.src = 'highlight/highlight.pack.js';
|
|
document.getElementById('hlScriptContainer').appendChild(script);
|
|
|
|
const link = document.createElement('link');
|
|
link.rel = 'stylesheet';
|
|
// https://ace.c9.io/build/kitchen-sink.html
|
|
// https://highlightjs.org/static/demo/
|
|
link.href = 'highlight/styles/' + options.codeTheme;
|
|
document.getElementById('hlScriptContainer').appendChild(link);
|
|
}
|
|
|
|
function loadAndApplyHljs(options) {
|
|
var codeElements = document.getElementsByClassName('code');
|
|
if (!codeElements.length) return;
|
|
|
|
if (!hljsScriptAdded) {
|
|
this.loadHljs(options);
|
|
return;
|
|
}
|
|
|
|
// If HLJS is not loaded yet, no need to do anything. When it loads
|
|
// it will automatically apply the style to all the code elements.
|
|
if (hljsLoaded) applyHljs(codeElements);
|
|
}
|
|
|
|
function applyHljs(codeElements) {
|
|
if (typeof codeElements === 'undefined') codeElements = document.getElementsByClassName('code');
|
|
|
|
for (var i = 0; i < codeElements.length; i++) {
|
|
try {
|
|
hljs.highlightBlock(codeElements[i]);
|
|
} catch (error) {
|
|
console.warn('Cannot highlight code', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// / Handle dynamically loading HLJS when a code element is present
|
|
// ----------------------------------------------------------------------
|
|
|
|
// 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.
|
|
// To restore percentScroll the "checkScrollIID" interval is used. It constantly resets the scroll position during
|
|
// one second after the content has been updated.
|
|
//
|
|
// ignoreNextScroll is used to differentiate between scroll event from the users and those that are the result
|
|
// of programmatically changing scrollTop. We only want to respond to events initiated by the user.
|
|
|
|
let percentScroll_ = 0;
|
|
let checkScrollIID_ = null;
|
|
|
|
function setPercentScroll(percent) {
|
|
percentScroll_ = percent;
|
|
contentElement.scrollTop = percentScroll_ * maxScrollTop();
|
|
}
|
|
|
|
function percentScroll() {
|
|
return percentScroll_;
|
|
}
|
|
|
|
function restorePercentScroll() {
|
|
setPercentScroll(percentScroll_);
|
|
}
|
|
|
|
ipc.setHtml = (event) => {
|
|
const html = event.html;
|
|
|
|
markJsHackMarkerInserted_ = false;
|
|
|
|
updateBodyHeight();
|
|
|
|
contentElement.innerHTML = html;
|
|
|
|
loadAndApplyHljs(event.options);
|
|
|
|
// Remove the bullet from "ul" for checkbox lists and extra padding
|
|
// const checkboxes = document.getElementsByClassName('checkbox');
|
|
// for (let i = 0; i < checkboxes.length; i++) {
|
|
// const cb = checkboxes[i];
|
|
// const ul = cb.parentElement.parentElement;
|
|
// if (!ul) {
|
|
// console.warn('Unexpected layout for checkbox');
|
|
// continue;
|
|
// }
|
|
// ul.style.listStyleType = 'none';
|
|
// ul.style.paddingLeft = 0;
|
|
// }
|
|
|
|
let previousContentHeight = contentElement.scrollHeight;
|
|
let startTime = Date.now();
|
|
ignoreNextScrollEvent = true;
|
|
restorePercentScroll();
|
|
|
|
if (!checkScrollIID_) {
|
|
checkScrollIID_ = setInterval(() => {
|
|
const h = contentElement.scrollHeight;
|
|
if (h !== previousContentHeight) {
|
|
previousContentHeight = h;
|
|
ignoreNextScrollEvent = true;
|
|
restorePercentScroll();
|
|
}
|
|
if (Date.now() - startTime >= 1000) {
|
|
clearInterval(checkScrollIID_);
|
|
checkScrollIID_ = null;
|
|
}
|
|
}, 1);
|
|
}
|
|
}
|
|
|
|
let ignoreNextScrollEvent = false;
|
|
ipc.setPercentScroll = (event) => {
|
|
const percent = event.percent;
|
|
|
|
if (checkScrollIID_) {
|
|
clearInterval(checkScrollIID_);
|
|
checkScrollIID_ = null;
|
|
}
|
|
|
|
ignoreNextScrollEvent = true;
|
|
setPercentScroll(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_ = '​ ​'
|
|
const markJsHackMarker_ = ' ';
|
|
for (let i = 0; i < elements.length; i++) {
|
|
if (!type) {
|
|
elements[i].innerHTML = elements[i].innerHTML + markJsHackMarker_;
|
|
} else if (type === 'insertBefore') {
|
|
elements[i].insertAdjacentHTML('beforeBegin', markJsHackMarker_);
|
|
}
|
|
}
|
|
}
|
|
|
|
prepareElementsForMarkJs(document.getElementsByTagName('p'));
|
|
prepareElementsForMarkJs(document.getElementsByTagName('div'));
|
|
prepareElementsForMarkJs(document.getElementsByTagName('br'), 'insertBefore');
|
|
markJsHackMarkerInserted_ = true;
|
|
}
|
|
|
|
let mark_ = null;
|
|
let markSelectedElement_ = null;
|
|
function setMarkers(keywords, options = null) {
|
|
if (!options) options = {};
|
|
|
|
// TODO: It should highlight queries without accents - eg "penche*" should highlight "penchés"
|
|
// TODO: It should highlight Chinese, Japanese characters, etc.
|
|
// TODO: It should highlight Russian
|
|
// TODO: not working - "oue*" doesn't highlight "ouéé"
|
|
// TODO: Search engine support to mobile app (sync tables)
|
|
// TODO: Update everywhere that uses allParsedQueryTerms to support new format
|
|
// TODO: Add support for scriptType on mobile and CLI
|
|
|
|
if (!mark_) {
|
|
mark_ = new Mark(document.getElementById('content'), {
|
|
exclude: ['img'],
|
|
acrossElements: true,
|
|
});
|
|
}
|
|
|
|
addMarkJsSpaceHack(document);
|
|
|
|
mark_.unmark()
|
|
|
|
if (markSelectedElement_) markSelectedElement_.classList.remove('mark-selected');
|
|
|
|
let selectedElement = null;
|
|
let elementIndex = 0;
|
|
|
|
const onEachElement = (element) => {
|
|
if (!('selectedIndex' in options)) return;
|
|
|
|
if (('selectedIndex' in options) && elementIndex === options.selectedIndex) {
|
|
markSelectedElement_ = element;
|
|
element.classList.add('mark-selected');
|
|
selectedElement = element;
|
|
}
|
|
|
|
elementIndex++;
|
|
}
|
|
|
|
for (let i = 0; i < keywords.length; i++) {
|
|
let keyword = keywords[i];
|
|
|
|
if (typeof keyword === 'string') {
|
|
keyword = {
|
|
type: 'text',
|
|
value: keyword,
|
|
};
|
|
}
|
|
|
|
const isBasicSearch = ['ja', 'zh', 'ko'].indexOf(keyword.scriptType) >= 0;
|
|
|
|
if (keyword.type === 'regex') {
|
|
let regexString = '\\b' + keyword.value + '\\b';
|
|
if (isBasicSearch) regexString = keyword.value;
|
|
|
|
mark_.markRegExp(new RegExp(regexString, 'gmi'), {
|
|
each: onEachElement,
|
|
acrossElements: true,
|
|
});
|
|
} else {
|
|
let accuracy = keyword.accuracy ? keyword.accuracy : 'exactly';
|
|
if (isBasicSearch) accuracy = 'partially';
|
|
mark_.mark([keyword.value], {
|
|
each: onEachElement,
|
|
accuracy: accuracy,
|
|
});
|
|
}
|
|
}
|
|
|
|
ipcProxySendToHost('setMarkerCount', elementIndex);
|
|
|
|
if (selectedElement) selectedElement.scrollIntoView();
|
|
}
|
|
|
|
let markLoaded_ = false;
|
|
ipc.setMarkers = (event) => {
|
|
const keywords = event.keywords;
|
|
const options = event.options;
|
|
|
|
if (!keywords.length && !markLoaded_) return;
|
|
|
|
if (!markLoaded_) {
|
|
const script = document.createElement('script');
|
|
script.onload = function() {
|
|
setMarkers(keywords, options);
|
|
};
|
|
|
|
script.src = '../../node_modules/mark.js/dist/mark.min.js';
|
|
document.getElementById('markScriptContainer').appendChild(script);
|
|
markLoaded_ = true;
|
|
} else {
|
|
setMarkers(keywords, options);
|
|
}
|
|
}
|
|
|
|
function maxScrollTop() {
|
|
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
|
|
}
|
|
|
|
// The body element needs to have a fixed height for the content to be scrollable
|
|
function updateBodyHeight() {
|
|
document.getElementById('body').style.height = window.innerHeight + 'px';
|
|
}
|
|
|
|
const ipcProxySendToHost = (methodName, arg) => {
|
|
window.postMessage({ target: 'main', name: methodName, args: [ arg ] }, '*');
|
|
}
|
|
|
|
contentElement.addEventListener('scroll', function(e) {
|
|
if (ignoreNextScrollEvent) {
|
|
ignoreNextScrollEvent = false;
|
|
return;
|
|
}
|
|
const m = maxScrollTop();
|
|
const percent = m ? contentElement.scrollTop / m : 0;
|
|
setPercentScroll(percent);
|
|
|
|
ipcProxySendToHost('percentScroll', percent);
|
|
});
|
|
|
|
document.addEventListener('contextmenu', function(event) {
|
|
let element = event.target;
|
|
|
|
// To handle right clicks on resource icons
|
|
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) {
|
|
ipcProxySendToHost('contextMenu', {
|
|
type: 'text',
|
|
textToCopy: selectedText,
|
|
});
|
|
} else if (event.target.getAttribute('href')) {
|
|
ipcProxySendToHost('contextMenu', {
|
|
type: 'link',
|
|
textToCopy: event.target.getAttribute('href'),
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
function handleInternalLink(event, anchorNode) {
|
|
const href = anchorNode.getAttribute('href');
|
|
if (href.indexOf('#') === 0) {
|
|
event.preventDefault();
|
|
location.hash = href;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function getParentAnchorElement(element) {
|
|
let counter = 0;
|
|
while (true) {
|
|
|
|
if (counter++ >= 10000) {
|
|
console.warn('been looping for too long - exiting')
|
|
return null;
|
|
}
|
|
|
|
if (!element) return null;
|
|
if (element.nodeName === 'A') return element;
|
|
element = element.parentElement;
|
|
}
|
|
}
|
|
|
|
document.addEventListener('click', function(event) {
|
|
const anchor = getParentAnchorElement(event.target);
|
|
if (!anchor) return;
|
|
|
|
// Prevent URLs added via <a> tags from being opened within the application itself
|
|
// otherwise it would open the whole website within the WebView.
|
|
if (!anchor.hasAttribute('data-from-md')) {
|
|
if (handleInternalLink(event, anchor)) return;
|
|
|
|
event.preventDefault();
|
|
ipcProxySendToHost(anchor.getAttribute('href'));
|
|
return;
|
|
}
|
|
|
|
// If this is an internal link, jump to the anchor directly
|
|
if (anchor.hasAttribute('data-from-md')) {
|
|
if (handleInternalLink(event, anchor)) return;
|
|
}
|
|
});
|
|
|
|
// 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', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
});
|
|
document.addEventListener('dragover', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
});
|
|
document.addEventListener('dragover', function(e) {
|
|
e.preventDefault();
|
|
});
|
|
|
|
window.addEventListener('resize', function() {
|
|
updateBodyHeight();
|
|
});
|
|
|
|
updateBodyHeight();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|