From 30a4ed6c29d7930ff99c543ca926be26e895c51f Mon Sep 17 00:00:00 2001 From: jebbs Date: Sat, 30 Mar 2024 08:14:38 +0800 Subject: [PATCH] Mod: make links in preview clickable (#572) --- package-lock.json | 4 +- src/plantuml/exporter/exportDiagram.ts | 8 +- src/providers/previewer.ts | 33 ++++- templates/js/hyperlink.js | 27 ++++ templates/js/imageMapResizer.js | 164 +++++++++++++++++++++++++ templates/js/preview.js | 14 +++ templates/js/switcher.js | 13 +- templates/preview.html | 5 +- tsconfig.json | 1 + 9 files changed, 258 insertions(+), 11 deletions(-) create mode 100644 templates/js/hyperlink.js create mode 100644 templates/js/imageMapResizer.js diff --git a/package-lock.json b/package-lock.json index b0cff5d..cb3177d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plantuml", - "version": "2.17.4", + "version": "2.17.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "plantuml", - "version": "2.17.4", + "version": "2.17.6", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "linq-collections": "*", diff --git a/src/plantuml/exporter/exportDiagram.ts b/src/plantuml/exporter/exportDiagram.ts index 4eff509..e622a3f 100644 --- a/src/plantuml/exporter/exportDiagram.ts +++ b/src/plantuml/exporter/exportDiagram.ts @@ -22,7 +22,13 @@ export function exportDiagram(diagram: Diagram, format: string, savePath: string bar.text = localize(7, null, diagram.name + "." + format.split(":")[0]); } let renderTask = appliedRender(diagram.parentUri).render(diagram, format, savePath); - if (!config.exportMapFile(diagram.parentUri) || !savePath) return renderTask; + if (!savePath) { + // when exporting to buffer include map to make links clickable + let mapTask = appliedRender(diagram.parentUri).getMapData(diagram, savePath); + return combine(renderTask, mapTask); + } + + if (!config.exportMapFile(diagram.parentUri)) return renderTask; let bsName = path.basename(savePath); let ext = path.extname(savePath); diff --git a/src/providers/previewer.ts b/src/providers/previewer.ts index 938b2ef..21b46d2 100644 --- a/src/providers/previewer.ts +++ b/src/providers/previewer.ts @@ -55,7 +55,11 @@ class Previewer extends vscode.Disposable { let env = { localize: localize, images: this.images.reduce((p, c) => { - return `${p}` + if (c.startsWith('data:image/')) { + return `${p}` + } else { + return `${p}${c.replaceAll(' { let sigPNG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); let isPNG = buf.slice(0, sigPNG.length).equals(sigPNG); - let b64 = buf.toString('base64'); - if (!b64) return p; - p.push(`data:image/${isPNG ? 'png' : "svg+xml"};base64,${b64}`); + let isSVG = (buf.slice(0, 256).indexOf('= 0); + + if (isPNG || isSVG) { + let b64 = buf.toString('base64'); + if (!b64) return p; + + // push image + p.push(`data:image/${isPNG ? 'png' : "svg+xml"};base64,${b64}`); + } else { + // push image map + let imageMap = ''; + if (buf.toString().trim().length > 0) + imageMap = buf.toString() + + p.push(imageMap); + } return p; }, []); this.updateWebView(); @@ -224,7 +241,13 @@ class Previewer extends vscode.Disposable { ); this._disposables.push(this._uiPreview); - this._uiPreview.addEventListener("message", e => this.setUIStatus(JSON.stringify(e.message))); + this._uiPreview.addEventListener("message", e => { + if (e.message.action == "openExternalLink") { + vscode.env.openExternal(e.message.href); + } else { + this.setUIStatus(JSON.stringify(e.message)); + } + }); this._uiPreview.addEventListener("open", () => this.startWatch()); this._uiPreview.addEventListener("close", () => { this.stopWatch(); this.killTasks(); }); } diff --git a/templates/js/hyperlink.js b/templates/js/hyperlink.js new file mode 100644 index 0000000..e09f487 --- /dev/null +++ b/templates/js/hyperlink.js @@ -0,0 +1,27 @@ +function addHyperlinkManager(vscode) { + let imgContainer= document.getElementById('image-container'); + imgContainer.addEventListener('click', e => { + if (e.button == 0 && e.target.target == "_blank") { + vscode.postMessage({ + "action": "openExternalLink", + "href": e.target.href + }); + + e.stopImmediatePropagation(); + } + }); + + imgContainer.addEventListener('mousedown', e => { + if (e.button == 0 && e.target.target == "_blank") { + // prevent zoom selection when clicking on links + e.stopImmediatePropagation(); + } + }); + + imgContainer.addEventListener('mouseup', e => { + if (e.button == 0 && e.target.target == "_blank") { + // prevent zoom action when clicking on links + e.stopImmediatePropagation(); + } + }); +} \ No newline at end of file diff --git a/templates/js/imageMapResizer.js b/templates/js/imageMapResizer.js new file mode 100644 index 0000000..fc69231 --- /dev/null +++ b/templates/js/imageMapResizer.js @@ -0,0 +1,164 @@ +/*! Image Map Resizer + * Desc: Resize HTML imageMap to scaled image. + * Copyright: (c) 2014-15 David J. Bradshaw - dave@bradshaw.net + * License: MIT + */ + +;(function() { + 'use strict' + + function scaleImageMap() { + function resizeMap() { + function resizeAreaTag(cachedAreaCoords, idx) { + function scale(coord) { + var dimension = 1 === (isWidth = 1 - isWidth) ? 'width' : 'height' + return ( + padding[dimension] + + Math.floor(Number(coord) * scalingFactor[dimension]) + ) + } + + var isWidth = 0 + areas[idx].coords = cachedAreaCoords + .split(',') + .map(scale) + .join(',') + } + + var scalingFactor = { + width: image.width / image.naturalWidth, + height: image.height / image.naturalHeight, + } + + var padding = { + width: parseInt( + window.getComputedStyle(image, null).getPropertyValue('padding-left'), + 10 + ), + height: parseInt( + window.getComputedStyle(image, null).getPropertyValue('padding-top'), + 10 + ), + } + + cachedAreaCoordsArray.forEach(resizeAreaTag) + } + + function getCoords(e) { + //Normalize coord-string to csv format without any space chars + return e.coords.replace(/ *, */g, ',').replace(/ +/g, ',') + } + + function debounce() { + clearTimeout(timer) + timer = setTimeout(resizeMap, 250) + } + + function start() { + if ( + image.width !== image.naturalWidth || + image.height !== image.naturalHeight + ) { + resizeMap() + } + } + + function addEventListeners() { + image.addEventListener('load', resizeMap, false) //Detect late image loads in IE11 + window.addEventListener('focus', resizeMap, false) //Cope with window being resized whilst on another tab + window.addEventListener('resize', debounce, false) + window.addEventListener('readystatechange', resizeMap, false) + document.addEventListener('fullscreenchange', resizeMap, false) + } + + function beenHere() { + return 'function' === typeof map._resize + } + + function getImg(name) { + return document.querySelector('img[usemap="' + name + '"]') + } + + function setup() { + areas = map.getElementsByTagName('area') + cachedAreaCoordsArray = Array.prototype.map.call(areas, getCoords) + image = getImg('#' + map.name) || getImg(map.name) + map._resize = resizeMap //Bind resize method to HTML map element + } + + var /*jshint validthis:true */ + map = this, + areas = null, + cachedAreaCoordsArray = null, + image = null, + timer = null + + if (!beenHere()) { + setup() + addEventListeners() + start() + } else { + map._resize() //Already setup, so just resize map + } + } + + function factory() { + function chkMap(element) { + if (!element.tagName) { + throw new TypeError('Object is not a valid DOM element') + } else if ('MAP' !== element.tagName.toUpperCase()) { + throw new TypeError( + 'Expected tag, found <' + element.tagName + '>.' + ) + } + } + + function init(element) { + if (element) { + chkMap(element) + scaleImageMap.call(element) + maps.push(element) + } + } + + var maps + + return function imageMapResizeF(target) { + maps = [] // Only return maps from this call + + switch (typeof target) { + case 'undefined': + case 'string': + Array.prototype.forEach.call( + document.querySelectorAll(target || 'map'), + init + ) + break + case 'object': + init(target) + break + default: + throw new TypeError('Unexpected data type (' + typeof target + ').') + } + + return maps + } + } + + if (typeof define === 'function' && define.amd) { + define([], factory) + } else if (typeof module === 'object' && typeof module.exports === 'object') { + module.exports = factory() //Node for browserfy + } else { + window.imageMapResize = factory() + } + + if ('jQuery' in window) { + window.jQuery.fn.imageMapResize = function $imageMapResizeF() { + return this.filter('map') + .each(scaleImageMap) + .end() + } + } + })() + \ No newline at end of file diff --git a/templates/js/preview.js b/templates/js/preview.js index 5e981e6..f814e02 100644 --- a/templates/js/preview.js +++ b/templates/js/preview.js @@ -75,6 +75,7 @@ window.addEventListener("load", () => { zoomer = new Zoom(settings); switcher.moveTo(previewStatus.page); addCursorManager(settings); + addHyperlinkManager(vscode); addSelectionBox(settings); addDrageScroll(settings); initializeHelpModal(); @@ -82,6 +83,19 @@ window.addEventListener("load", () => { document.getElementById("ctrl-container").remove(); document.getElementById("image-container").remove(); } + + // see imageMapResizer.js + imageMapResize('#image-map'); + let image = document.getElementById('image') + if (image) { + new MutationObserver(() => { + imageMapResize('#image-map'); + }) + .observe( + image, + {'attributes': true} + ); + } }); window.addEventListener( "resize", diff --git a/templates/js/switcher.js b/templates/js/switcher.js index 32914e2..097a62a 100644 --- a/templates/js/switcher.js +++ b/templates/js/switcher.js @@ -2,11 +2,16 @@ class Switcher { constructor() { this.current = 0; this.images = []; + this.imageMaps = []; this.image = document.getElementById("image"); + this.imageMap = document.getElementById("image-map"); this.pInfo = document.getElementById("pageInfo"); this.pInfoTpl = this.pInfo.innerText; for (let e of document.getElementById("images").getElementsByTagName("img")) { - this.images.push(e.src); + this.images.push(e); + } + for (let e of document.getElementById("images").getElementsByTagName("map")) { + this.imageMaps.push(e); } if (this.images.length <= 1) { document.getElementById("page-ctrls").style.display = "none"; @@ -34,7 +39,11 @@ class Switcher { if (this.current == page) return; // switch page - this.image.src = this.images[page - 1]; + let image = this.images[page - 1]; + let imageMap = this.imageMaps[page - 1]; + + this.image.src = image.src; + this.imageMap.innerHTML = imageMap?.innerHTML ?? ""; this.pInfo.innerText = String.format(this.pInfoTpl, page, this.images.length); this.current = page; diff --git a/templates/preview.html b/templates/preview.html index 9c969b8..81fb848 100644 --- a/templates/preview.html +++ b/templates/preview.html @@ -3,8 +3,10 @@ + + @@ -42,7 +44,8 @@
- + +
diff --git a/tsconfig.json b/tsconfig.json index 569c1fe..997ff85 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "module": "commonjs", + "lib": ["es2021"], "target": "es6", "outDir": "out", "sourceMap": true,