class ReferenceImageDialog extends XUI.Dialog {
static showDialog(imageData, imageURI, resourceType, docURI,
reference=null) {
// When the image is dropped, imageURI is simply the image file name.
// Otherwise, the image has been "opened" (that is, selected using a
// dialog box) and it's an absolute local or remote file URI.
return new Promise((resolve, reject) => {
let dialog = new ReferenceImageDialog(imageData, imageURI,
resourceType, docURI,
resolve);
dialog.open("center", reference);
});
}
constructor(imageData, imageURI, resourceType, docURI, resolve) {
super({ title: "Change Image",
movable: true, resizable: true, closeable: true,
template: ReferenceImageDialog.TEMPLATE,
buttons: [ { label: "Cancel", action: "cancelAction" },
{ label: "OK", action: "okAction",
default: true } ]});
this._imageData = imageData;
this._resolve = resolve;
let pane = this.contentPane.firstElementChild;
this._refAsToggle = pane.querySelector(".xxe-refimg-refas-toggle");
let id = XUI.Util.uid();
this._refAsToggle.setAttribute("id", id);
let refAsLabel = this._refAsToggle.nextElementSibling;
refAsLabel.setAttribute("for", id);
this._parentURIText = pane.querySelector(".xxe-refimg-parent-uri");
this._fileNameText = pane.querySelector(".xxe-refimg-file-name");
let parentURI, fileName;
if (URIUtil.isAbsoluteURI(imageURI)) {
let uri = URIUtil.relativizeURI(imageURI, docURI);
parentURI = URIUtil.uriParent(uri, /*trailingSepar*/ false);
if (parentURI === null) {
// Example: /foo.png relative to /bar.xml ==> uri=foo.png
// whose parent is null.
parentURI = ".";
}
fileName = URIUtil.uriBasename(uri);
} else {
// Happens when an image is dropped onto the viewport.
parentURI = null; // Must be specified by user.
// Just a file name, not an absolute URI.
fileName = imageURI;
}
let addDataList = true;
if (parentURI !== null) {
this._parentURIText.value = parentURI;
this._parentURIText.setAttribute("readonly", "readonly");
addDataList = false;
}
this.rememberParentURI(parentURI, addDataList);
if (fileName) {
this._fileNameText.value = fileName;
this._fileNameText.setAttribute("readonly", "readonly");
}
// Otherwise, an empty fileName is unlikely.
let docURILabel = pane.querySelector(".xxe-refimg-doc-uri");
docURILabel.textContent = docURI;
this._embedToggle = pane.querySelector(".xxe-refimg-embed-toggle");
id = XUI.Util.uid();
this._embedToggle.setAttribute("id", id);
let embedLabel = this._embedToggle.nextElementSibling;
embedLabel.setAttribute("for", id);
const mimeType =
!imageData.type? "application/octet-stream" : imageData.type;
const imageSize = URIUtil.formatFileSize(imageData.size);
let embedHTML = XUI.Util.escapeHTML(embedLabel.textContent);
embedHTML += " (<code>";
embedHTML += XUI.Util.escapeHTML(mimeType);
embedHTML += "</code>, ";
if (imageData.size <= 1024*1024) {
embedHTML += imageSize;
} else {
embedHTML += "<strong style='color:red'>" + imageSize + "</strong>";
}
embedHTML += ")";
embedLabel.innerHTML = embedHTML;
if (resourceType === "anyURI") {
this._refAsToggle.checked = true;
} else {
this._embedToggle.checked = true;
this._refAsToggle.disabled = true;
}
}
rememberParentURI(parentURI, addDataList) {
let valueList = XUI.Util.rememberDatalistItem(
"XXE.ReferenceImageDialog.lastParentURIs",
parentURI);
if (addDataList) {
XUI.Util.attachDatalist(this._parentURIText, valueList,
this.contentPane.firstElementChild);
}
}
dialogClosed(imageInfo) {
this._resolve(imageInfo);
}
cancelAction() {
this.close(null);
}
okAction() {
let info = { data: this._imageData };
if (this._embedToggle.checked) {
info.embed = true;
} else {
let parentURI =
ReferenceImageDialog.checkPath(this._parentURIText.value);
if (parentURI.length === 0) {
XUI.Util.badTextField(this._parentURIText);
return;
}
if (parentURI !== ".") {
this.rememberParentURI(parentURI, /*addDataList*/ false);
}
let fileName =
ReferenceImageDialog.checkPath(this._fileNameText.value);
if (fileName.length === 0 ||
fileName.indexOf('/') >= 0) {
XUI.Util.badTextField(this._fileNameText);
return;
}
// info.referenceAs is the relative or absolute URI of the image.
if (parentURI === ".") {
info.referenceAs = fileName;
} else {
info.referenceAs = parentURI;
if (!parentURI.endsWith('/')) {
info.referenceAs += "/";
}
info.referenceAs += fileName;
}
}
this.close(info);
}
static checkPath(path) {
path = path.trim();
if (path.length === 0) {
return path;
}
// Allow the Windows user to type a file path and not an URI.
if (URIUtil.FILE_PATH_SEPARATOR !== '/' &&
path.indexOf(URIUtil.FILE_PATH_SEPARATOR) >= 0) {
path = path.replaceAll(URIUtil.FILE_PATH_SEPARATOR, '/');
}
if (path.indexOf(' ') >= 0) {
// This one is a no brainer.
path = path.replaceAll(' ', "%20");
}
return path;
}
}
ReferenceImageDialog.TEMPLATE = document.createElement("template");
ReferenceImageDialog.TEMPLATE.innerHTML = `
<div class="xxe-refimg-pane">
<div class="xxe-refimg-refas-pane">
<input type="radio" name="radio_group" class="xxe-refimg-refas-toggle"/>
<label class="xxe-refimg-refas-label">Reference image using the
following URI:</label>
</div>
<div class="xxe-refimg-uri-pane">
<input type="text" size="50" class="xui-control xxe-refimg-parent-uri"
spellcheck="false" autocomplete="off" />
<span class="xxe-refimg-uri-separ">/</span>
<input type="text" size="20" class="xui-control xxe-refimg-file-name"
spellcheck="false" autocomplete="off" />
</div>
<div class="xxe-refimg-uri-hint">A relative URI is relative to<br />
<span class="xxe-refimg-doc-uri"></span><br />Use
"<b><tt>.</tt></b>" to specify "same directory".</div>
<div class="xxe-refimg-embed-pane">
<input type="radio" name="radio_group" class="xxe-refimg-embed-toggle"/>
<label class="xxe-refimg-embed-label">Embed image</label>
</div>
</div>
`;
// ===========================================================================
/**
* An image viewport.
*/
class ImageViewport extends HTMLElement {
constructor() {
super();
this._xmlEditor = null;
this._setImageParams = null;
this._beginResize = this._doResize = this._endResize = null;
this._editingContextChanged = null;
this._draggedHandle = null;
this._imgMinSize = 3*ImageViewport.RESIZER_SIZE;
this._img = null;
this._imgW0 = this._imgH0 = -1;
this._dragX0 = this._dragY0 = -1;
this._preserveAspect = true;
this._showResizeHandles = this.showResizeHandles.bind(this);
this.addEventListener("click", this._showResizeHandles);
this._chooseImage = this.chooseImage.bind(this);
this.addEventListener("dblclick", this._chooseImage);
this.addEventListener("contextmenu", this._chooseImage);
this._dragOverViewport = this.dragOverViewport.bind(this);
this.addEventListener("dragover", this._dragOverViewport);
this._dropImage = this.dropImage.bind(this);
this.addEventListener("drop", this._dropImage);
}
// -----------------------------------
// showResizeHandles
// -----------------------------------
showResizeHandles(event) {
if (this._beginResize === null) {
this._beginResize = this.beginResize.bind(this);
this._doResize = this.doResize.bind(this);
this._endResize = this.endResize.bind(this);
// This test is made just once: first time showResizeHandles is
// invoked.
const docView = this._xmlEditor.documentView;
if (docView.getAppEventBinding("resize-image") === null &&
docView.getAppEventBinding("rescale-image") === null) {
this.removeEventListener("click", this._showResizeHandles);
return;
}
}
// ---
let img = event.target;
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey ||
event.detail !== 1 || /*Click count*/
img.localName !== "img" ||
img.width < 3*ImageViewport.RESIZER_MIN_SIZE ||
img.height < 3*ImageViewport.RESIZER_MIN_SIZE) {
return;
}
// Do not stop propagation. Let select element happen normally.
this.enableResize(img, true);
}
enableResize(img, enable) {
if (enable) {
if (this._editingContextChanged === null) {
let view = NodeView.lookupView(this);
let uid = null;
if (view === null || (uid = NodeView.getUID(view)) === null) {
// Should not happen.
return;
}
let resizers = document.createElement("span");
resizers.setAttribute("class", "xxe-imgvp-resizers");
let resizerClassPrefix = "xxe-imgvp-resizer xxe-imgvp-handle-";
let resizerSize = ImageViewport.RESIZER_SIZE;
if (img.width < 5*ImageViewport.RESIZER_SIZE ||
img.height < 5*ImageViewport.RESIZER_SIZE) {
resizerSize = ImageViewport.RESIZER_MIN_SIZE;
resizerClassPrefix =
"xxe-imgvp-min-handle " + resizerClassPrefix;
}
let handles = [ "ne", "se", "sw", "nw" ];
if (img.width >= 5*resizerSize) {
handles.push("n", "s");
}
if (img.height >= 5*resizerSize) {
handles.push("w", "e");
}
for (let handle of handles) {
let resizer = document.createElement("span");
resizer.setAttribute("class",
resizerClassPrefix + handle);
resizer.addEventListener("mousedown", this._beginResize);
resizers.appendChild(resizer);
}
let imgParent = img.parentElement;
imgParent.insertBefore(resizers, img);
imgParent.removeChild(img);
resizers.appendChild(img);
this._editingContextChanged = (event) => {
// UID, tag, attr_list triplet list.
const items = event.nodePathItems;
const selectedUID = (items === null)? null :
items[items.length-3];
if (selectedUID !== uid) {
this.enableResize(img, false);
}
}
this._xmlEditor.addEventListener("editingContextChanged",
this._editingContextChanged);
}
} else {
if (this._editingContextChanged !== null) {
this._xmlEditor.removeEventListener("editingContextChanged",
this._editingContextChanged);
this._editingContextChanged = null;
let resizers = img.parentElement;
if (resizers.classList.contains("xxe-imgvp-resizers")) {
resizers.removeChild(img);
resizers.parentNode.replaceChild(img, resizers);
}
}
}
}
beginResize(event) {
event.preventDefault();
event.stopImmediatePropagation();
if (event.altKey || event.shiftKey ||
(PLATFORM_IS_MAC_OS && event.ctrlKey) ||
(!PLATFORM_IS_MAC_OS && event.metaKey)) {
return;
}
this._draggedHandle = null;
this._imgMinSize = 3*ImageViewport.RESIZER_SIZE;
for (let cls of event.target.classList.values()) {
if (cls === "xxe-imgvp-min-handle") {
this._imgMinSize = 2*ImageViewport.RESIZER_MIN_SIZE;
} else if (cls.startsWith("xxe-imgvp-handle-")) {
this._draggedHandle = cls.substring(17);
}
}
if (this._draggedHandle === null) {
// Should not happen.
return;
}
this._img = event.target.parentElement.lastElementChild;
this._imgW0 = this._img.width;
this._imgH0 = this._img.height;
this._dragX0 = event.clientX;
this._dragY0 = event.clientY;
this._preserveAspect =
!(PLATFORM_IS_MAC_OS? event.metaKey : event.ctrlKey);
const docView = this._xmlEditor.documentView;
if (this._preserveAspect) {
if (docView.getAppEventBinding("rescale-image") === null) {
this._preserveAspect = false;
}
} else {
if (docView.getAppEventBinding("resize-image") === null) {
this._preserveAspect = true;
}
}
document.addEventListener("mousemove", this._doResize);
document.addEventListener("mouseup", this._endResize);
document.addEventListener("mouseleave", this._endResize);
}
doResize(event) {
event.preventDefault();
event.stopImmediatePropagation();
// Absolute value (may be zero) and sign of incrX, incrY depends
// on the handle which is dragged.
let resizingX = false;
let resizingY = false;
let incrX = 0;
let incrY = 0;
switch (this._draggedHandle) {
case "nw":
incrX = this._dragX0 - event.clientX;
incrY = this._dragY0 - event.clientY;
resizingX = resizingY = true;
break;
case "ne":
incrX = event.clientX - this._dragX0;
incrY = this._dragY0 - event.clientY;
resizingX = resizingY = true;
break;
case "se":
incrX = event.clientX - this._dragX0;
incrY = event.clientY - this._dragY0;
resizingX = resizingY = true;
break;
case "sw":
incrX = this._dragX0 - event.clientX;
incrY = event.clientY - this._dragY0;
resizingX = resizingY = true;
break;
case "n":
incrY = this._dragY0 - event.clientY;
resizingY = true;
break;
case "e":
incrX = event.clientX - this._dragX0;
resizingX = true;
break;
case "s":
incrY = event.clientY - this._dragY0;
resizingY = true;
break;
case "w":
incrX = this._dragX0 - event.clientX;
resizingX = true;
break;
}
let newW = this._imgW0 + incrX;
if (resizingX && newW < this._imgMinSize) {
newW = this._imgMinSize;
incrX = this._imgMinSize - this._imgW0;
}
let newH = this._imgH0 + incrY;
if (resizingY && newH < this._imgMinSize) {
newH = this._imgMinSize;
incrY = this._imgMinSize - this._imgH0;
}
let scale = -1;
if (this._preserveAspect) {
if (resizingX && resizingY) {
if (Math.abs(incrX) >= Math.abs(incrY)) {
scale = newW / this._imgW0;
} else {
scale = newH / this._imgH0;
}
} else if (resizingX) {
scale = newW / this._imgW0;
} else {
scale = newH / this._imgH0;
}
newW = Math.round(this._imgW0 * scale);
newH = Math.round(this._imgH0 * scale);
}
if (newW !== this._img.width || newH !== this._img.height) {
this._img.width = newW;
this._img.height = newH;
}
}
endResize(event) {
this.doResize(event);
// ---
const img = this._img;
const preserveAspect = this._preserveAspect;
this._draggedHandle = null;
this._imgMinSize = 3*ImageViewport.RESIZER_SIZE;
this._img = null;
this._imgW0 = this._imgH0 = -1;
this._dragX0 = this._dragY0 = -1;
this._preserveAspect = true;
document.removeEventListener("mousemove", this._doResize);
document.removeEventListener("mouseup", this._endResize);
document.removeEventListener("mouseleave", this._endResize);
this.enableResize(img, false);
// ---
const docView = this._xmlEditor.documentView;
let cmdName = null;
let cmdParams = null;
let binding = null;
if (preserveAspect) {
binding = docView.getAppEventBinding("rescale-image");
} else {
binding = docView.getAppEventBinding("resize-image");
}
if (binding !== null) {
cmdName = binding.commandName;
cmdParams = binding.commandParams;
if (cmdParams !== null) {
if (cmdParams.indexOf("%{width}") >= 0) {
cmdParams = cmdParams.replaceAll("%{width}",
String(img.width));
}
if (cmdParams.indexOf("%{height}") >= 0) {
cmdParams = cmdParams.replaceAll("%{height}",
String(img.height));
}
if (cmdParams.indexOf("%{preserveAspect}") >= 0) {
cmdParams = cmdParams.replaceAll("%{preserveAspect}",
String(preserveAspect));
}
}
}
if (cmdName !== null) {
docView.executeCommand(EXECUTE_NORMAL, cmdName, cmdParams);
}
}
// -----------------------------------
// chooseImage
// -----------------------------------
chooseImage(event) {
event.preventDefault();
event.stopPropagation();
let view = NodeView.lookupView(this);
if (view === null) {
// Should not happen.
return;
}
const xmlEditor = this._xmlEditor;
const docView = xmlEditor.documentView;
let imageResource = [];
return docView.selectNode(view)
.then((selected) => {
if (!selected) {
// Not expected to happen.
return null;
} else {
const options = {
title: "Choose Image",
extensions: [ ["Image files",
"image/*",
"png",
"svg",
"jpg", "jpeg", "jpe", "jif", "jfif",
"gif",
"bmp",
"ico",
"webp",
"apng",
"avif"] ]
};
return xmlEditor.openResource(options);
}
})
.then((resource) => {
if (resource === null) {
// Canceled by user.
return null;
} else {
imageResource.push(resource);
return ReferenceImageDialog.showDialog(
resource.data, resource.uri, this._setImageParams[2],
xmlEditor.documentURI, xmlEditor);
}
})
.then((imageInfo) => {
if (imageInfo === null) {
// Canceled by user.
return false;
} else {
if (!imageInfo.embed) {
xmlEditor.cacheResource(imageResource[0]);
}
return this.changeImage(imageInfo, xmlEditor);
}
})
.catch((error) => {
XUI.Alert.showError(`Cannot change image:\n${error}`,
xmlEditor);
return false;
});
}
// -----------------------------------
// dragOverViewport
// -----------------------------------
dragOverViewport(event) {
event.preventDefault();
event.stopPropagation();
// No way to be more precise.
event.dataTransfer.dropEffect =
(event.dataTransfer.types.indexOf("Files") >= 0)?
"copyLink" : "none";
}
// -----------------------------------
// dropImage
// -----------------------------------
dropImage(event) {
event.preventDefault();
event.stopPropagation();
let imageFile = ImageViewport.getDroppedImageFile(event);
if (imageFile === null) {
// Dropped file not an image.
return;
}
let view = NodeView.lookupView(this);
if (view === null) {
// Should not happen.
return;
}
const xmlEditor = this._xmlEditor;
const docView = xmlEditor.documentView;
let imageInfo = {};
return docView.selectNode(view)
.then((selected) => {
if (!selected) {
// Not expected to happen.
return null;
} else {
// Consider imageFile.name as an URI relative
// to an unknown base.
return ReferenceImageDialog.showDialog(
imageFile, encodeURIComponent(imageFile.name),
this._setImageParams[2],
xmlEditor.documentURI, xmlEditor);
}
})
.then((dialogInfo) => {
if (dialogInfo === null) {
// Canceled by user.
return null; // No resource.
} else {
Object.assign(imageInfo, dialogInfo);
if (imageInfo.embed) {
return null; // No resource.
} else {
const uri = URIUtil.resolveURI(imageInfo.referenceAs,
xmlEditor.documentURI);
return xmlEditor.putResource(imageInfo.data, uri);
}
}
})
.then((resource) => {
if (!imageInfo.data) {
// Canceled by user.
return false;
} else {
return this.changeImage(imageInfo, xmlEditor);
}
})
.catch((error) => {
XUI.Alert.showError(`Cannot change image:\n${error}`,
xmlEditor);
return false;
});
}
static getDroppedImageFile(event) {
let imageFile = null;
for (let file of event.dataTransfer.files) {
if (file.type &&
(file.type.startsWith("image/") ||
file.type.startsWith("application/mathml"))) {
imageFile = file;
break;
}
}
return imageFile;
}
changeImage(imageInfo, xmlEditor) {
// this._setImageParams is an array containing:
// 0) source_element_UID|-
// 1) attribute_name|-
// 2) resource_type: anyURI|hexBinary|base64Binary|XML
// 3) gzip|-
const params = this._setImageParams.join(' ');
let imageValue = [null];
return ImageViewport.imageValue(imageInfo)
.then((value) => {
imageValue[0] = value;
return xmlEditor.documentView.executeCommand(
EXECUTE_HELPER,
"changeImage", `${params} ${imageValue[0]}`);
})
.then((result) => {
if (CommandResult.isDone(result)) {
return true;
} else {
const reason = CommandResult.formatCommandResult(
`changeImage ${params} ${imageValue[0]}`,
result);
throw new Error(reason);
}
})
.catch((error) => {
XUI.Alert.showError(`Cannot change image:\n${error}`,
xmlEditor);
return false;
});
}
static imageValue(imageInfo) {
// Note that embed and anyURI means: use a "data" URL.
if (imageInfo.embed) {
return ImageViewport.blobToDataURL(imageInfo.data);
} else {
let value = imageInfo.referenceAs;
if (value.indexOf("\"") >= 0) {
value = value.replace(/"/g, "\\\"");
}
value = "\"" + value + "\"";
return Promise.resolve(value);
}
}
static blobToDataURL(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(reader.result);
reader.onerror = (e) => reject(reader.error);
reader.onabort = (e) => reject(new Error("read aborted"));
reader.readAsDataURL(blob);
});
}
// -----------------------------------------------------------------------
// Custom element
// -----------------------------------------------------------------------
connectedCallback() {
this._xmlEditor = DOMUtil.lookupAncestorByTag(this, "xxe-client");
if (this._xmlEditor === null) {
// Should not happen.
return;
}
let imgParent = this.firstElementChild;
if (imgParent === null) {
// Should not happen.
return;
}
this._setImageParams = this.getAttribute("setimageparam");
if (this._setImageParams) {
// 0) source_element_UID|-
// 1) attribute_name|-
// 2) resource_type: anyURI|hexBinary|base64Binary|XML
// 3) gzip|-
this._setImageParams = this._setImageParams.split(/\s+/);
}
if (!Array.isArray(this._setImageParams) ||
this._setImageParams.length !== 4) {
this._setImageParams = null;
}
let contentURI = imgParent.getAttribute("data-src");
if (contentURI === null) {
this.setTooltip(imgParent);
// Nothing else to do.
return;
}
// Attribute data-label (if any) is processed by setTooltip.
imgParent.classList.remove("xxe-imgvp-placeholder");
contentURI = URIUtil.csriURLToURI(contentURI);
imgParent.removeAttribute("data-src");
let contentWidth = imgParent.getAttribute("data-cw");
if (contentWidth !== null) {
imgParent.removeAttribute("data-cw");
}
let contentHeight = imgParent.getAttribute("data-ch");
if (contentHeight !== null) {
imgParent.removeAttribute("data-ch");
}
let preserveAspectRatio = true;
let par = imgParent.getAttribute("data-par");
if (par !== null) {
preserveAspectRatio = (par !== "no");
imgParent.removeAttribute("data-par");
}
// In Chrome: without this timeout, the result of a "loadResource"
// request (e.g. request #7) may be obtained AFTER the result of a
// request which follows it (e.g. request #8 "executeCommand
// editAttributes").
setTimeout(() => {
this.addImg(this._xmlEditor,
contentURI, contentWidth, contentHeight,
preserveAspectRatio, imgParent);
}, 0 /*ms*/);
}
setTooltip(imgParent) {
if (this._setImageParams) {
let tooltip = imgParent.getAttribute("data-label");
if (!tooltip) {
tooltip = "";
} else {
tooltip += "\n\n";
// Don't remove attribute "data-label". CSS depends on it.
imgParent.setAttribute("data-label", "");
}
tooltip += "\u2022 Drop an image file here.\n\
\u2022 Double-click or right-click to choose an image file.";
this.setAttribute("title", tooltip);
} else {
// Not editable.
this.removeEventListener("dblclick", this._chooseImage);
this.removeEventListener("contextmenu", this._chooseImage);
this.removeEventListener("dragover", this._dragOverViewport);
this.removeEventListener("drop", this._dropImage);
}
}
async addImg(xmlEditor,
contentURI, contentWidth, contentHeight, preserveAspectRatio,
imgParent) {
await this.doAddImg(xmlEditor, contentURI, contentWidth, contentHeight,
preserveAspectRatio, imgParent);
this.setTooltip(imgParent);
}
async doAddImg(xmlEditor,
contentURI, contentWidth, contentHeight, preserveAspectRatio,
imgParent) {
let res = null;
let getError = null;
try {
res = await xmlEditor.getResource(contentURI);
} catch (error) {
getError = error;
}
if (res === null) {
if (getError === null) {
getError = new Error("null resource");
}
ImageViewport.noImg(getError, imgParent);
// Cannot load image. Give up.
return;
}
// ---
if (res.data === null) {
// Image simply cannot be displayed. Not an error. Real placeholder.
imgParent.classList.add("xxe-imgvp-placeholder");
imgParent.textContent = CSSIcon["image"];
return;
}
// ---
let img = document.createElement("img");
const imgURL = URL.createObjectURL(res.data);
img.src = imgURL;
let displayError = null;
try {
await img.decode();
if (img.naturalWidth <= 0 || img.naturalHeight <= 0) {
displayError =
"the intrinsic dimensions of the image are not known";
}
} catch (error) {
displayError = error.message;
} finally {
URL.revokeObjectURL(imgURL);
}
if (displayError !== null) {
ImageViewport.noImg(
`Cannot display image "${contentURI}":\n${displayError}`,
imgParent);
// Give up.
return;
}
if (contentWidth !== null || contentHeight !== null) {
let [width, widthType] =
ImageViewport.parseSizeSpec(contentWidth);
let [height, heightType] =
ImageViewport.parseSizeSpec(contentHeight);
let viewportWidth = 0;
if (this.style.getPropertyValue("width")) {
// If a viewport width has been specified.
viewportWidth = this.clientWidth;
}
let viewportHeight = 0;
if (this.style.getPropertyValue("height")) {
// If a viewport height has been specified.
viewportHeight = this.clientHeight;
}
let size = ImageViewport.computeScaledSize(
viewportWidth, viewportHeight,
img.naturalWidth, img.naturalHeight,
width, widthType, height, heightType, preserveAspectRatio);
if (size !== null) {
img.width = size[0];
img.height = size[1];
}
}
imgParent.replaceChildren(img);
}
static noImg(error, imgParent) {
imgParent.setAttribute("data-label", error);
imgParent.classList.add("xxe-imgvp-error");
imgParent.textContent = CSSIcon["no-image"];
}
static parseSizeSpec(spec) {
let size = -1;
let sizeType = null;
if (spec !== null) {
if (spec.endsWith("px")) {
sizeType = "px";
size = Number(spec.substring(0, spec.length-2));
} else if (spec.endsWith("%")) {
sizeType = "%";
size = Number(spec.substring(0, spec.length-1));
} else if (spec.endsWith("max")) {
sizeType = "max";
size = Number(spec.substring(0, spec.length-3));
} else if (spec === "scale-to-fit") {
sizeType = "fit";
size = 1; // Any positive value is OK here.
}
if (isNaN(size) || size <= 0) {
size = -1;
}
}
return [ size, sizeType ];
}
static computeScaledSize(viewportWidth, viewportHeight,
imageWidth, imageHeight,
width, widthType, height, heightType,
preserveAspectRatio) {
let specWidth = -1;
let specHeight = -1;
if (width > 0) {
switch (widthType) {
case "px":
specWidth = width;
break;
case "%":
specWidth =
(imageWidth * width) / 100.0; // imageWidth may be 0.
break;
case "fit":
specWidth = viewportWidth; // viewportWidth may be 0.
break;
case "max":
specWidth = Math.min(imageWidth, width);
break;
}
}
if (height > 0) {
switch (heightType) {
case "px":
specHeight = height;
break;
case "%":
specHeight =
(imageHeight * height) / 100.0; // imageHeight may be 0.
break;
case "fit":
specHeight = viewportHeight; // viewportHeight may be 0.
break;
case "max":
specHeight = Math.min(imageHeight, height);
break;
}
}
let scaledWidth = -1;
let scaledHeight = -1;
let scaleX, scaleY;
if (specWidth <= 0) {
if (specHeight <= 0) {
return null;
} else {
scaleY = specHeight / imageHeight;
scaledWidth = Math.round(imageWidth * scaleY);
scaledHeight = Math.round(imageHeight * scaleY);
}
} else {
if (specHeight <= 0) {
scaleX = specWidth / imageWidth;
scaledWidth = Math.round(imageWidth * scaleX);
scaledHeight = Math.round(imageHeight * scaleX);
} else {
if (((widthType === "fit" && heightType === "fit") ||
(widthType === "max" && heightType === "max")) &&
preserveAspectRatio) {
scaleX = specWidth / imageWidth;
scaleY = specHeight / imageHeight;
let scale = Math.min(scaleX, scaleY);
scaledWidth = Math.round(imageWidth * scale);
scaledHeight = Math.round(imageHeight * scale);
} else {
scaledWidth = specWidth;
scaledHeight = specHeight;
}
}
}
return [ scaledWidth, scaledHeight ];
}
disconnectedCallback() {
let img = this.getElementsByTagName("img");
if (img.length === 1) {
this.enableResize(img.item(0), false);
}
}
}
ImageViewport.RESIZER_MIN_SIZE = 5; /*px*/
ImageViewport.RESIZER_SIZE = 2*ImageViewport.RESIZER_MIN_SIZE;
window.customElements.define("xxe-image-viewport", ImageViewport);