/** * boxplusx: a versatile lightweight pop-up window engine * @author Levente Hunyadi * @version 1.0 * @remarks Copyright (C) 2009-2021 Levente Hunyadi * @remarks Licensed under GNU/GPLv3, see https://www.gnu.org/licenses/gpl-3.0.html * @see https://hunyadi.info.hu/projects/boxplusx **/ // NOTE: This file has been generated from a TypeScript source /******/ (() => { // webpackBootstrap /******/ "use strict"; var __webpack_exports__ = {}; // UNUSED EXPORTS: BoxPlusXDialog ;// CONCATENATED MODULE: ./src/urls.ts /** * Parses a query string into name/value pairs. * @param querystring A string of "name=value" pairs, separated by "&". * @return An object where keys are parameter names, and values are parameter values. */ function fromQueryString(querystring) { let parameters = {}; if (querystring.length > 1) { querystring.substr(1).split('&').forEach((keyvalue) => { let index = keyvalue.indexOf('='); let key = index >= 0 ? keyvalue.substr(0, index) : keyvalue; let value = index >= 0 ? keyvalue.substr(index + 1) : ''; parameters[decodeURIComponent(key)] = decodeURIComponent(value); }); } return parameters; } /** * Parses a URL string into URL components. * @param url A URL string. * @return URL components. */ function parseURL(url) { let parser = document.createElement("a"); parser.href = url; const hashBangIndex = parser.hash.indexOf('!'); return { protocol: parser.protocol, host: parser.host, hostname: parser.hostname, port: parser.port, pathname: parser.pathname, search: parser.search, queryparams: fromQueryString(parser.search), hash: parser.hash, id: parser.hash.substr(1, (hashBangIndex >= 0 ? hashBangIndex : parser.hash.length) - 1), /** * Fragment parameters. Recognizes any of the following syntax: * #key1=value1&key2=value2 * #id!key1=value1&key2=value2 */ fragmentparams: fromQueryString(parser.hash.substr(Math.max(0, hashBangIndex))) }; } /** * Determines whether navigating to a URL would entail only a hash change. * @param url A URL string. * @return True if changing the location would trigger only an onhashchange event. */ function isHashChange(url) { let actual = parseURL(url); let expected = parseURL(location.href); // parse location URL for compatibility with Internet Explorer return actual.protocol === expected.protocol && actual.host === expected.host && actual.pathname === expected.pathname // compare path && actual.search === expected.search; // compare query string } /** * Builds a query string from an object. * @param parameters An object where keys are parameter names, and values are parameter values. * @return A URL query string. */ function buildQuery(parameters) { return Object.keys(parameters).map((key) => { return encodeURIComponent(key) + '=' + encodeURIComponent(parameters[key] || ''); }).join('&'); } ;// CONCATENATED MODULE: ./src/htmldom.ts /** * Determines whether an element is either of the listed HTML element types. * @param elem The HTML element to test. * @param types The tag names to test against. */ function isElementOfType(elem, types) { return types.indexOf(elem.tagName.toLowerCase()) >= 0; } function isFormControl(elem) { return isElementOfType(elem, ['input', 'select', 'textarea']); } ;// CONCATENATED MODULE: ./src/draggable.ts /** Allows viewing obscured parts of a scrollable element by making drag gestures with the mouse. */ class Draggable { /** * @param interceptor The element that intercepts drag events. * @param scrollable The element that scrolls in response to mouse movement. */ constructor(interceptor, scrollable) { this.dragged = false; this.lastClientX = 0; this.lastClientY = 0; this.scrollable = scrollable; interceptor.addEventListener('mousedown', ev => this.dragStart(ev)); interceptor.addEventListener('mouseup', ev => this.dragEnd(ev)); interceptor.addEventListener('mouseout', ev => this.dragEnd(ev)); interceptor.addEventListener('mousemove', ev => this.dragMove(ev)); } dragStart(event) { if (isFormControl(event.target)) { return; } let style = window.getComputedStyle(this.scrollable); let canScroll = Draggable.scrollablePropertyValues.indexOf(style['overflowX']) >= 0 || Draggable.scrollablePropertyValues.indexOf(style['overflowY']) >= 0; if (canScroll) { let mouseEvent = event; this.lastClientX = mouseEvent.clientX; this.lastClientY = mouseEvent.clientY; this.dragged = true; mouseEvent.preventDefault(); } } dragEnd(_) { this.dragged = false; } dragMove(event) { if (this.dragged) { let mouseEvent = event; this.scrollable.scrollLeft -= mouseEvent.clientX - this.lastClientX; this.scrollable.scrollTop -= mouseEvent.clientY - this.lastClientY; this.lastClientX = mouseEvent.clientX; this.lastClientY = mouseEvent.clientY; } } } Draggable.scrollablePropertyValues = ['auto', 'scroll']; ;// CONCATENATED MODULE: ./src/orientation.ts class RationalNumber extends Number { constructor(numerator, denominator) { super(numerator / denominator); this.numerator = numerator; this.denominator = denominator; } toString() { return `${this.numerator}/${this.denominator}`; } } function isRationalLike(value) { return value !== undefined && value.numerator !== undefined && value.denominator !== undefined; } /** * Orientation constants. * Position names represent how row #0 and column #0 are oriented, e.g. TopLeft is the upright orientation. * Positive numeric constants are aligned with values in EXIF standard. */ var ImageOrientation; (function (ImageOrientation) { ImageOrientation[ImageOrientation["WrongImageType"] = -2] = "WrongImageType"; ImageOrientation[ImageOrientation["NoInformation"] = -1] = "NoInformation"; ImageOrientation[ImageOrientation["Unknown"] = 0] = "Unknown"; ImageOrientation[ImageOrientation["TopLeft"] = 1] = "TopLeft"; ImageOrientation[ImageOrientation["TopRight"] = 2] = "TopRight"; ImageOrientation[ImageOrientation["BottomRight"] = 3] = "BottomRight"; ImageOrientation[ImageOrientation["BottomLeft"] = 4] = "BottomLeft"; ImageOrientation[ImageOrientation["LeftTop"] = 5] = "LeftTop"; ImageOrientation[ImageOrientation["RightTop"] = 6] = "RightTop"; ImageOrientation[ImageOrientation["RightBottom"] = 7] = "RightBottom"; ImageOrientation[ImageOrientation["LeftBottom"] = 8] = "LeftBottom"; })(ImageOrientation || (ImageOrientation = {})); /** * Retrieves image EXIF orientation of the camera relative to the scene. * @param url The image URL. * @param callback Invoked passing the EXIF orientation. */ function getImageOrientationFromURL(url) { if (!/\.jpe?g$/i.test(url)) { return Promise.resolve(ImageOrientation.WrongImageType); // wrong image format, no EXIF data present in image formats GIF or PNG } else { return new Promise((resolve) => { let xhr = new XMLHttpRequest(); xhr.open('get', url); xhr.responseType = 'blob'; xhr.onload = () => { resolve(getImageOrientationFromBlob(xhr.response)); }; xhr.onerror = () => { resolve(ImageOrientation.NoInformation); }; xhr.send(); }); } } /** * Retrieves image EXIF orientation of the camera relative to the scene. * @param blob The image data as a binary large object. * @param callback Invoked passing the EXIF orientation. */ function getImageOrientationFromBlob(blob) { return new Promise((resolve) => { let reader = new FileReader(); reader.onload = () => { let view = new DataView(reader.result); if (view.getUint16(0) != 0xFFD8) { return resolve(ImageOrientation.WrongImageType); // wrong image format, not a JPEG image } let length = view.byteLength; let offset = 2; while (offset < length) { let marker = view.getUint16(offset); offset += 2; if (marker == 0xFFE1) { // application marker APP1 // EXIF header if (view.getUint32(offset += 2) != 0x45786966) { // corresponds to string "Exif" return resolve(ImageOrientation.NoInformation); // EXIF data absent } // TIFF header let little = view.getUint16(offset += 6) == 0x4949; // check if "Intel" (little-endian) byte alignment is used offset += view.getUint32(offset + 4, little); // last four bytes are offset to Image file directory (IFD) // IFD (Image file directory) let tags = view.getUint16(offset, little); offset += 2; for (let i = 0; i < tags; i++) { if (view.getUint16(offset + (i * 12), little) == 0x0112) { // corresponds to IFD0 (main image) Orientation let orientation = view.getUint16(offset + (i * 12) + 8, little); return resolve(orientation); } } } else if ((marker & 0xFF00) != 0xFF00) { // not an application marker break; } else { offset += view.getUint16(offset); } } return resolve(ImageOrientation.NoInformation); // application marker APP1 not found }; reader.readAsArrayBuffer(blob); }); } /** * Retrieves EXIF image orientation and other metadata. * @param image The image from which to extract information. * @param extractMetadata Whether to attempt obtaining metadata other than image orientation. * @return Promise fulfilled with EXIF orientation and metadata. */ function getImageMetadata(image, extractMetadata = false) { return new Promise((resolve) => { let url = image.src; if (/^file:/.test(url)) { return resolve({ orientation: ImageOrientation.NoInformation }); // cross-origin requests are only supported for protocol schemes such as 'http' and 'https' } let EXIF = window.EXIF; if (extractMetadata && !!EXIF) { // use third-party plugin Exif.js to extract orientation and metadata, see <https://github.com/exif-js/exif-js> EXIF.getData(image, () => { let img = image; let orientation = ImageOrientation.Unknown; let metadata = {}; let m = Object.assign({}, img.iptcdata, img.exifdata); if (Object.keys(m).length > 0) { Object.keys(m).forEach((key) => { let value = m[key]; if (key == 'thumbnail' && value !== undefined) { let blob = value['blob']; if (blob !== undefined) { let image = document.createElement('img'); image.src = URL.createObjectURL(blob); m[key] = image; } } else if (isRationalLike(value)) { m[key] = new RationalNumber(value.numerator, value.denominator); } }); metadata = m; let o = m['Orientation']; if (o) { orientation = (+o); // coerce to enumeration value (number constant) } } resolve({ orientation, metadata }); }); } else { // use simple built-in method to extract orientation getImageOrientationFromURL(url).then((orientation) => { resolve({ orientation }); }); } }); } ;// CONCATENATED MODULE: ./src/timer.ts class TimerController { constructor(eventFn, duration) { /** True if a slideshow is currently active. */ this.active = false; this.eventFn = eventFn; this.duration = duration; } /** Gets if the timer is active. */ get enabled() { return this.active; } /** Sets if the timer is active. */ set enabled(value) { this.active = value; } /** The slideshow will trigger the timer event (when the duration expires) if it is in active state. */ resume() { if (this.active) { this.startTimer(); } } /** The slideshow will no longer trigger the timer event but remains in active state. */ suspend() { this.stopTimer(); } /** The slideshow is set in active state and will trigger the timer event (when the duration expires). */ start() { this.active = true; this.startTimer(); } /** The slideshow is set in inactive state and will no longer trigger the timer event. */ stop() { this.active = false; this.stopTimer(); } /** Restarts the slideshow timer. */ startTimer() { this.stopTimer(); if (this.duration > 0) { this.timer = window.setTimeout(this.eventFn, this.duration); } } /** Stops the slideshow timer. */ stopTimer() { if (this.timer) { window.clearTimeout(this.timer); this.timer = undefined; } } } ;// CONCATENATED MODULE: ./src/touch.ts class touch_TimerController { constructor(elem, actions) { this.touchStartX = 0; this.lastTouch = 0; this.actions = actions; elem.addEventListener('touchstart', (touchEvent) => { this.touchStartX = touchEvent.changedTouches[0].pageX; if (this.actions.start) { this.actions.start(); } }); elem.addEventListener('touchend', (touchEvent) => { let now = new Date().getTime(); let delta = now - this.lastTouch; if (delta > 0 && delta < 500) { // double tap (two successive taps one shortly after the other) touchEvent.preventDefault(); } else { // single tap let x = touchEvent.changedTouches[0].pageX; if (x - this.touchStartX >= 50) { // swipe to the right this.actions.right(); } else if (this.touchStartX - x >= 50) { // swipe to the left this.actions.left(); } } this.lastTouch = now; }); } } ;// CONCATENATED MODULE: ./src/trail.ts /** Converts an integer in the range [0, 255] into a hexadecimal representation. */ function dec2hex(dec) { return ('0' + dec.toString(16)).slice(-2); } /** Generates a hexadecimal string representing n bytes of random data. */ function generateUID(len) { const arr = new Uint8Array(len); window.crypto.getRandomValues(arr); return Array.from(arr, dec2hex).join(''); } class Trail { constructor(navigateFn) { // manage history (browser back and forward buttons) this.eventFn = (event) => { let item = undefined; if (this.isHistoryState(event.state)) { item = event.state.item; } navigateFn(item); }; } /** * Gets the currently shown item. * @return The item currently displayed. */ get current() { const state = history.state; return state.item; } /** * Pushes an item on top of the history stack. * @param item The item to be currently displayed. */ push(item) { if (this.isHistoryState(history.state)) { if (item != this.current) { this.pushHistoryState(item); } } else { this.uid = generateUID(16); this.addEventListener(); this.pushHistoryState(item); } } /** Removes the managed history stack. */ clear() { this.removeEventListener(); this.unroll(); } /** Discards all history items injected by this instance. */ unroll() { if (this.isHistoryState(history.state)) { history.go(-1); window.setTimeout(() => { this.unroll(); }, 0); } else { this.uid = undefined; // inject artificial state to clear any subsequent state entries history.pushState(null, ''); // make sure the artificial state is discarded when we manipulate the history again history.go(-1); } } isHistoryState(historyState) { const state = historyState; if (state) { return state.uid == this.uid; } else { return false; } } pushHistoryState(item) { const state = { uid: this.uid, item: item, }; history.pushState(state, ''); } addEventListener() { window.addEventListener('popstate', this.eventFn); } removeEventListener() { window.removeEventListener('popstate', this.eventFn); } } ;// CONCATENATED MODULE: ./src/index.ts /** * boxplusx: a versatile lightweight pop-up window engine * @author Levente Hunyadi * @version 1.0 * @remarks Copyright (C) 2009-2021 Levente Hunyadi * @remarks Licensed under GNU/GPLv3, see https://www.gnu.org/licenses/gpl-3.0.html * @see https://hunyadi.info.hu/projects/boxplusx **/ var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var BoxPlusXDialog_1; /** Position of control with respect to the viewport area. */ var BoxPlusXPosition; (function (BoxPlusXPosition) { BoxPlusXPosition["Hidden"] = "hidden"; BoxPlusXPosition["Above"] = "above"; BoxPlusXPosition["Top"] = "top"; BoxPlusXPosition["Bottom"] = "bottom"; BoxPlusXPosition["Below"] = "below"; })(BoxPlusXPosition || (BoxPlusXPosition = {})); /** Text writing system. */ var BoxPlusXWritingSystem; (function (BoxPlusXWritingSystem) { BoxPlusXWritingSystem["LeftToRight"] = "ltr"; BoxPlusXWritingSystem["RightToLeft"] = "rtl"; })(BoxPlusXWritingSystem || (BoxPlusXWritingSystem = {})); const BoxPlusXOptionsDefaults = { id: undefined, slideshow: 0, autostart: false, loop: false, preferredWidth: 800, preferredHeight: 600, useDevicePixelRatio: true, navigation: BoxPlusXPosition.Bottom, controls: BoxPlusXPosition.Below, captions: BoxPlusXPosition.Below, contextmenu: true, metadata: false, dir: BoxPlusXWritingSystem.LeftToRight, history: false }; /** @ExportDecoratedItems */ function sealed(_) { } /** Content type shown in the pop-up window. */ var BoxPlusXContentType; (function (BoxPlusXContentType) { BoxPlusXContentType[BoxPlusXContentType["None"] = 0] = "None"; BoxPlusXContentType[BoxPlusXContentType["Unavailable"] = 1] = "Unavailable"; BoxPlusXContentType[BoxPlusXContentType["Image"] = 2] = "Image"; BoxPlusXContentType[BoxPlusXContentType["Video"] = 3] = "Video"; BoxPlusXContentType[BoxPlusXContentType["EmbeddedContent"] = 4] = "EmbeddedContent"; BoxPlusXContentType[BoxPlusXContentType["DocumentFragment"] = 5] = "DocumentFragment"; BoxPlusXContentType[BoxPlusXContentType["Frame"] = 6] = "Frame"; })(BoxPlusXContentType || (BoxPlusXContentType = {})); /** Determine how content behaves when the container is resized. */ var BoxPlusXDimensionBehavior; (function (BoxPlusXDimensionBehavior) { /** The item does not permit resizing (e.g. HTML <object> element with fixed width and height). */ BoxPlusXDimensionBehavior[BoxPlusXDimensionBehavior["FixedSize"] = 0] = "FixedSize"; /** The item has fixed aspect ratio (e.g. HTML <video> element). */ BoxPlusXDimensionBehavior[BoxPlusXDimensionBehavior["FixedAspectRatio"] = 1] = "FixedAspectRatio"; /** The item width and height can be set independently. */ BoxPlusXDimensionBehavior[BoxPlusXDimensionBehavior["Resizable"] = 2] = "Resizable"; /** The item has an intrinsic width and height but either of these may be set to a smaller value when there is insufficient space. */ BoxPlusXDimensionBehavior[BoxPlusXDimensionBehavior["ResizableBestFit"] = 3] = "ResizableBestFit"; })(BoxPlusXDimensionBehavior || (BoxPlusXDimensionBehavior = {})); function* matchAll(str, regExp) { let match; while (match = regExp.exec(str)) { yield match; } return undefined; } function parseOptionalInteger(str, def) { return parseInt(str !== null && str !== void 0 ? str : '', 10) || def; } function parseOptionalBoolean(str) { return str === 'true' || !!parseInt(str !== null && str !== void 0 ? str : '', 10); } /** * Checks if a location identifies an image. * @param path A path or the path component of a URL. * @return True if the path is likely to identify an image. */ function isImageFile(path) { return /\.(gif|jpe?g|png|svg|webp)$/i.test(path); } /** * Sets all undefined properties on an object using a reference object. */ function applyDefaults(obj, ref) { let extended = obj || {}; for (const prop in JSON.parse(JSON.stringify(ref))) { // use JSON functions to clone object if (!Object.prototype.hasOwnProperty.call(extended, prop)) { extended[prop] = ref[prop]; } } return extended; } /** * Removes all children of an HTML element. * @param elem The HTML element whose children to remove. */ function removeChildNodes(elem) { while (elem.hasChildNodes()) { elem.removeChild(elem.lastChild); } } /** * Toggles a CSS class on an element. * @param elem The HTML element to add the class to or remove the class from. * @param cls The CSS class name. * @param state If true, the class is added; if false, removed. */ function toggleClass(elem, cls, state) { elem.classList.toggle(cls, state); } /** * Sets the visibility of an HTML element. * @param elem The HTML element to inspect. * @param True if the object is to be made visible. */ function setVisible(elem, state) { toggleClass(elem, 'boxplusx-hidden', !state); } /** * Determines the visibility of an HTML element. * @param elem The HTML element to inspect. * @return True if the object is visible. */ function isVisible(elem) { return !elem.classList.contains('boxplusx-hidden'); } /** * Creates a HTML <div> element, acting as a building block for the dialog. * @param name The class name the element gets. * @param hidden Whether the element is initially hidden. * @param children Any children the element should have. * @return The newly created element, ready for injection into the DOM. */ function createElement(name, hidden, children) { let elem = document.createElement('div'); elem.classList.add('boxplusx-' + name); setVisible(elem, !hidden); if (children) { elem.append(...children); } return elem; } /** Creates several HTML <div> elements, acting as building blocks for the dialog. */ function createElements(names) { return names.map((name) => { return createElement(name); }); } /** * Returns the title and description text for a content item. * @param elem The HTML element whose textual description to extract. */ function getItemText(elem) { let title = ''; let description = ''; if (elem instanceof HTMLAnchorElement) { const dataset = elem.dataset; const dataTitle = dataset['title']; if (dataTitle !== undefined) { title = dataTitle; } else { // an HTML anchor element that nests an HTML image element with an "alt" attribute const image = elem.querySelector('img'); if (image !== null) { const alternateText = image.getAttribute('alt'); if (alternateText !== null) { title = alternateText; } } } const dataSummary = dataset['summary']; if (dataSummary !== undefined) { description = dataSummary; } else { // an HTML anchor element with a "title" attribute const titleAttrText = elem.getAttribute('title'); if (titleAttrText !== null) { description = titleAttrText; } } if (title === description) { description = ''; } } return { title, description }; } /** Generates item properties from an HTML element collection. */ function elementsToProperties(elems) { return elems.map((elem) => { let { title, description } = getItemText(elem); let url = ''; let poster; let srcset; if (elem instanceof HTMLAnchorElement) { url = elem.href; let dataset = elem.dataset; poster = dataset['poster']; srcset = dataset['srcset']; } // extract the HTML data attribute "download", which tells the engine where to look for the high-resolution // original, should the visitor choose to save a copy of the image to their computer let download = elem.dataset['download']; let image; let images = elem.getElementsByTagName('img'); if (images.length > 0) { image = images[0]; } return { url, image, poster, srcset, title, description, download, }; }); } /** * The boxplusx lightbox pop-up window instance. * Though typically used as a singleton, the interface permits instantiating multiple instances. */ let BoxPlusXDialog = BoxPlusXDialog_1 = class BoxPlusXDialog { /** Initializes the layout and behavior of the pop-up dialog. */ constructor(options) { /** Information about elements, part of the same group, to be displayed in the pop-up window. */ this.members = []; /** Index of current item shown. */ this.current = 0; /** Aspect behavior for the item currently displayed. */ this.aspect = BoxPlusXDimensionBehavior.FixedAspectRatio; /** Content type currently shown in the pop-up window. */ this.contentType = BoxPlusXContentType.None; /** Whether content size is reduced to fit available viewport area. */ this.shrinkToFit = true; /** Whether content size is allowed to grow to take all available viewport area. */ this.expandToFit = false; this.options = applyDefaults(options, BoxPlusXOptionsDefaults); // builds the boxplusx pop-up window HTML structure, as if by injecting the following into the DOM: // // <div class="boxplusx-container boxplusx-hidden"> // <div class="boxplusx-dialog"> // <div class="boxplusx-wrapper boxplusx-hidden"> // <div class="boxplusx-wrapper"> // <div class="boxplusx-wrapper"> // <div class="boxplusx-viewport"> // <div class="boxplusx-aspect"></div> // <div class="boxplusx-content"></div> // <div class="boxplusx-expander"></div> // <div class="boxplusx-previous"></div> // <div class="boxplusx-next"></div> // </div> // <div class="boxplusx-navigation"> // <div class="boxplusx-navbar"> // <div class="boxplusx-navitem"> // <div class="boxplusx-aspect"></div> // <div class="boxplusx-navimage"></div> // </div> // </div> // <div class="boxplusx-rewind"></div> // <div class="boxplusx-forward"></div> // </div> // </div> // <div class="boxplusx-controls"> // <div class="boxplusx-previous"></div> // <div class="boxplusx-next"></div> // <div class="boxplusx-close"></div> // <div class="boxplusx-start"></div> // <div class="boxplusx-stop"></div> // <div class="boxplusx-download"></div> // <div class="boxplusx-metadata"></div> // </div> // </div> // <div class="boxplusx-caption"> // <div class="boxplusx-title"></div> // <div class="boxplusx-description"></div> // </div> // </div> // <div class="boxplusx-progress boxplusx-hidden"></div> // </div> // </div> // create elements this.aspectHolder = createElement('aspect'); this.innerContainer = createElement('content'); this.expander = createElement('expander'); this.navigationBar = createElement('navbar'); this.navigationArea = createElement('navigation', false, [this.navigationBar].concat(createElements(['rewind', 'forward']))); this.viewport = createElement('viewport', false, [this.aspectHolder, this.innerContainer, this.expander].concat(createElements(['previous', 'next']))); let controls = createElement('controls', false, createElements(['previous', 'next', 'close', 'start', 'stop', 'download', 'metadata'])); this.captionTitle = createElement('title'); this.captionDescription = createElement('description'); this.caption = createElement('caption', false, [this.captionTitle, this.captionDescription]); let innerWrapper = createElement('wrapper', false, [this.viewport, this.navigationArea]); let outerWrapper = createElement('wrapper', false, [innerWrapper, controls]); this.contentWrapper = createElement('wrapper', true, [outerWrapper, this.caption]); this.progressIndicator = createElement('progress', true); this.dialog = createElement('dialog', false, [this.contentWrapper, this.progressIndicator]); this.outerContainer = createElement('container', true, [this.dialog]); if (this.options.id) { this.outerContainer.id = this.options.id; } // arrange layout this.caption.classList.add('boxplusx-' + this.options.captions); controls.classList.add('boxplusx-' + this.options.controls); this.navigationArea.classList.add('boxplusx-' + this.options.navigation); document.body.appendChild(this.outerContainer); this.preferredWidth = this.options.preferredWidth; this.preferredHeight = this.options.preferredHeight; this.outerContainer.addEventListener('click', (event) => { if (event.target === this.outerContainer) { this.close(); } }, false); this.addEventToAllElements('click', { 'previous': () => { this.previous(); }, 'next': () => { this.next(); }, 'close': () => { this.close(); }, 'start': () => { this.start(); }, 'stop': () => { this.stop(); }, 'metadata': () => { this.metadata(); }, 'download': () => { this.download(); }, 'rewind': () => { this.stopNavigationBar(); }, 'forward': () => { this.stopNavigationBar(); } }); this.addEventToAllElements('mouseover', { 'rewind': () => { this.rewindNavigationBar(); }, 'forward': () => { this.forwardNavigationBar(); } }); this.addEventToAllElements('mouseout', { 'rewind': () => { this.stopNavigationBar(); }, 'forward': () => { this.stopNavigationBar(); } }); if (!this.options.contextmenu) { this.dialog.addEventListener('contextmenu', (event) => { event.preventDefault(); }); } this.outerContainer.dir = this.options.dir; // set up drag and drop for content new Draggable(this.viewport, this.innerContainer); // set up slideshow controller this.slideshow = new TimerController(() => { this.next(); }, this.options.slideshow); let toggleShrinkToFit = () => { if (this.preferredWidth > this.viewport.clientWidth || this.preferredHeight > this.viewport.clientHeight) { this.shrinkToFit = !this.shrinkToFit; const index = this.current; this.navigateToIndex(index); } }; this.expander.addEventListener('click', toggleShrinkToFit); this.viewport.addEventListener('dblclick', toggleShrinkToFit); // prevent mouse wheel events from view area from propagating to document view this.innerContainer.addEventListener('mousewheel', (event) => { let wheelEvent = event; let canScroll = window.getComputedStyle(this.innerContainer).overflowY != 'hidden'; let maxScroll = this.innerContainer.scrollHeight - this.innerContainer.clientHeight; if (canScroll && maxScroll > 0) { let scrollTop = this.innerContainer.scrollTop; let deltaY = wheelEvent.deltaY; if ((scrollTop === maxScroll && deltaY > 0) || (scrollTop === 0 && deltaY < 0)) { wheelEvent.preventDefault(); } } }); // map key to action let keyActions = new Map(); keyActions.set(27, () => { this.close(); }); // ESC keyActions.set(36, () => { this.first(); }); // HOME keyActions.set(35, () => { this.last(); }); // END const prevFn = () => { this.previous(); }; const nextFn = () => { this.next(); }; switch (this.options.dir) { case BoxPlusXWritingSystem.LeftToRight: keyActions.set(37, prevFn); // left arrow keyActions.set(39, nextFn); // right arrow break; case BoxPlusXWritingSystem.RightToLeft: keyActions.set(39, prevFn); // right arrow keyActions.set(37, nextFn); // left arrow break; } // pressing a key window.addEventListener('keydown', (keyboardEvent) => { if (isVisible(this.outerContainer)) { // let form elements handle their own input if (isFormControl(keyboardEvent.target)) { return; } let keyAction = keyActions.get(keyboardEvent.which || keyboardEvent.keyCode); if (keyAction) { keyAction(); keyboardEvent.preventDefault(); } } }, false); // navigation by swipe new touch_TimerController(this.viewport, { right: () => { if (this.shrinkToFit) { this.previous(); } }, left: () => { if (this.shrinkToFit) { this.next(); } } }); // mobile-friendly forward and rewind for quick-access navigation bar new touch_TimerController(this.navigationBar, { start: () => { this.stopNavigationBar(); }, right: () => { this.rewindNavigationBar(); }, left: () => { this.forwardNavigationBar(); } }); // window resize window.addEventListener('resize', (_) => { if (isVisible(this.outerContainer)) { this.setMaximumDialogSize(); this.repositionNavigationBar(); this.updateExpanderState(); } }); if (this.options.history) { this.trail = new Trail((index) => { if (index !== undefined) { this.navigateToIndex(index); } else { this.close(); } }); } } /** Binds a set of elements to this dialog instance. */ bind(items) { let properties = elementsToProperties(Array.from(items)); let openfun = (index) => { this.open(properties, index); }; items.forEach((elem, index) => { elem.addEventListener('click', (event) => { event.preventDefault(); openfun(index); }, false); }); return openfun; } /** Opens the pop-up window. */ open(members, index) { this.members = members; // populate quick-access navigation bar const isNavigationVisible = members.length > 1 && this.options.navigation != BoxPlusXPosition.Hidden; setVisible(this.navigationArea, isNavigationVisible); if (isNavigationVisible) { members.forEach((member, i) => { let navigationAspect = createElement('aspect'); let navigationImage = createElement('navimage'); let navigationItem = createElement('navitem', false, [navigationAspect, navigationImage]); let allowAction = true; navigationItem.addEventListener('touchstart', () => { if (this.isNavigationBarSliding()) { allowAction = false; } }); navigationItem.addEventListener('click', () => { if (allowAction) { this.navigate(i); } allowAction = true; }); let image = member.image; if (image) { let img = image; let setNavigationImage = () => { let aspectStyle = navigationAspect.style; if (img.naturalWidth && img.naturalHeight) { aspectStyle.setProperty('width', img.naturalWidth + 'px'); aspectStyle.setProperty('padding-top', (100.0 * img.naturalHeight / img.naturalWidth) + '%'); } if (img.src) { navigationImage.style.setProperty('background-image', 'url("' + img.src + '")'); } }; if (img.src && img.complete) { // make sure the image is available setNavigationImage(); } else { // set aspect properties immediately when the image is loaded img.addEventListener('load', setNavigationImage); let preloadableImage = image; // trigger pre-loader service if registered by another script if (preloadableImage.preloader) { preloadableImage.preloader.load(); } } } navigationImage.innerText = (i + 1) + ''; this.navigationBar.appendChild(navigationItem); }); } this.show(index); } /** Show the pop-up window. */ show(index) { if (this.options.autostart && this.options.slideshow > 0) { this.slideshow.enabled = true; } setVisible(this.outerContainer, true); setVisible(this.progressIndicator, true); this.navigateToIndex(index); } close() { this.slideshow.stop(); // clear history track this.current = 0; if (this.trail) { this.trail.clear(); } // call private method that does not manipulate history this.hideWindow(); } navigate(index) { if (index != this.current) { this.navigateToIndex(index); } } first() { this.navigate(0); } previous() { const index = this.current; if (index > 0) { this.navigate(index - 1); } else if (this.options.loop) { this.last(); } } next() { const index = this.current; if (index < this.members.length - 1) { this.navigate(index + 1); } else if (this.options.loop) { this.first(); } } last() { this.navigate(this.members.length - 1); } start() { this.slideshow.start(); if (this.options.slideshow > 0) { this.updateControls(); } } stop() { this.slideshow.stop(); if (this.options.slideshow > 0) { this.updateControls(); } } metadata() { let metadata = this.queryElement('detail'); if (metadata) { setVisible(metadata, !isVisible(metadata)); } } download() { const index = this.current; const url = this.members[index].download; if (url !== undefined) { let anchor = document.createElement('a'); anchor.href = url; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); } } ; queryElement(identifier) { return this.dialog.querySelector('.boxplusx-' + identifier); } queryAllElements(identifier) { return this.dialog.querySelectorAll('.boxplusx-' + identifier); } applyAllElements(identifier, func) { this.queryAllElements(identifier).forEach((elem) => { func(elem); }); } addEventToAllElements(eventName, map) { for (const identifier of Object.getOwnPropertyNames(map)) { const eventFn = map[identifier]; this.applyAllElements(identifier, (elem) => { elem.addEventListener(eventName, eventFn, false); }); } } isContentInteractive(type) { switch (type) { case BoxPlusXContentType.Unavailable: case BoxPlusXContentType.Image: return false; } return true; } /** Sets a content type that helps identify what is shown in the pop-up window viewport area. */ setContentType(contentType) { function getContentTypeString(type) { switch (type) { case BoxPlusXContentType.Unavailable: return 'unavailable'; case BoxPlusXContentType.Image: return 'image'; case BoxPlusXContentType.Video: return 'video'; case BoxPlusXContentType.EmbeddedContent: return 'embed'; case BoxPlusXContentType.DocumentFragment: return 'document'; case BoxPlusXContentType.Frame: return 'frame'; case BoxPlusXContentType.None: return 'none'; } } let classList = this.innerContainer.classList; classList.remove('boxplusx-' + getContentTypeString(this.contentType)); classList.remove('boxplusx-interactive'); this.contentType = contentType; classList.add('boxplusx-' + getContentTypeString(contentType)); if (this.isContentInteractive(contentType)) { classList.add('boxplusx-interactive'); } } updateControls() { let index = this.current; let isFirstItem = index == 0; let members = this.members; let isLastItem = index >= members.length - 1; let loop = this.options.loop && !(isFirstItem && isLastItem); let slideshow = this.options.slideshow > 0; this.applyAllElements('previous', (elem) => { setVisible(elem, loop || !isFirstItem); }); this.applyAllElements('next', (elem) => { setVisible(elem, loop || !isLastItem); }); this.applyAllElements('start', (elem) => { setVisible(elem, slideshow && !this.slideshow.enabled && !isLastItem); }); this.applyAllElements('stop', (elem) => { setVisible(elem, slideshow && this.slideshow.enabled); }); this.applyAllElements('download', (elem) => { setVisible(elem, members[index].download !== undefined); }); this.applyAllElements('metadata', (elem) => { setVisible(elem, this.options.metadata && !!this.queryElement('detail')); }); } updateExpanderState() { let isOversize = this.preferredWidth > this.viewport.clientWidth || this.preferredHeight > this.viewport.clientHeight; setVisible(this.expander, isOversize && !this.isContentInteractive(this.contentType)); toggleClass(this.expander, 'boxplusx-collapse', !this.shrinkToFit); toggleClass(this.expander, 'boxplusx-expand', this.shrinkToFit); } hideWindow() { // reset shrink to fit this.shrinkToFit = true; this.expandToFit = false; this.updateExpanderState(); // reset content displayed this.removeAnimationProperties(); this.clearContent(); this.setContentType(BoxPlusXContentType.None); removeChildNodes(this.navigationBar); setVisible(this.contentWrapper, false); setVisible(this.outerContainer, false); // must come before manipulating history } /** Reveals the content to be displayed. */ showContent() { this.removeAnimationProperties(); setVisible(this.progressIndicator, false); let index = this.current; if (index >= this.members.length - 1) { this.slideshow.stop(); } this.updateControls(); setVisible(this.contentWrapper, true); // dialog must be visible to have valid offset values this.repositionNavigationBar(); this.updateExpanderState(); this.slideshow.resume(); } /** * Trigger dialog animation to morph into a size suitable for the next item. * @param aspect Specifies how the dialog should respond when resized. * @param originalWidth The original dialog CSS width to start with. * @param originalHeight The original dialog CSS height to start with. */ morphDialog(aspect, originalWidth, originalHeight) { this.aspect = aspect; // save current dialog dimensions and aspect ratio let computedStyle = window.getComputedStyle(this.dialog); const currentWidth = originalWidth || computedStyle.getPropertyValue('width'); const currentHeight = originalHeight || computedStyle.getPropertyValue('height'); this.removeAnimationProperties(); // use temporarily exposed elements for calculations setVisible(this.contentWrapper, true); let viewportClassList = this.viewport.classList; viewportClassList.remove('boxplusx-fixedaspect'); viewportClassList.remove('boxplusx-draggable'); if (BoxPlusXDimensionBehavior.FixedSize === aspect || BoxPlusXDimensionBehavior.FixedAspectRatio === aspect) { // set new aspect ratio // if specified as a percentage, CSS padding is expressed in terms of container width (even for top // and bottom padding), which we utilize here to make item grow/shrink vertically as it grows/shrinks // horizontally let aspectStyle = this.aspectHolder.style; if (this.expandToFit) { aspectStyle.setProperty('width', '100vw'); } else { aspectStyle.setProperty('width', this.preferredWidth + 'px'); } aspectStyle.setProperty('padding-top', (100.0 * this.preferredHeight / this.preferredWidth) + '%'); viewportClassList.add('boxplusx-fixedaspect'); } else if (BoxPlusXDimensionBehavior.ResizableBestFit === aspect) { viewportClassList.add('boxplusx-draggable'); } else if (BoxPlusXDimensionBehavior.Resizable === aspect) { let containerStyle = this.innerContainer.style; containerStyle.setProperty('width', this.preferredWidth + 'px'); containerStyle.setProperty('max-height', this.preferredHeight + 'px'); } this.setMaximumDialogSize(); // get desired target size with all inner controls temporarily visible const desiredWidth = computedStyle.getPropertyValue('width'); const desiredHeight = computedStyle.getPropertyValue('height'); const desiredMaxWidth = computedStyle.getPropertyValue('max-width'); // animation transition end function let appliedStyle = this.dialog.style; let fn = () => { if (isVisible(this.outerContainer)) { appliedStyle.setProperty('max-width', desiredMaxWidth); this.showContent(); } }; if (currentWidth != desiredWidth || currentHeight != desiredHeight) { // dialog animation required to fit new content size // hide elements after calculations have been made setVisible(this.contentWrapper, false); // reset previous dialog dimensions appliedStyle.removeProperty('max-width'); appliedStyle.setProperty('width', currentWidth); appliedStyle.setProperty('height', currentHeight); this.dialog.classList.add('boxplusx-animation'); // determine when event "transitionend" would be fired // helps thwart deadlock when event "transitionend" is never fired due to race condition const duration = Math.max.apply(null, computedStyle.getPropertyValue('transition-duration').split(',').map(function (item) { let value = parseFloat(item); if (/\ds$/.test(item)) { return 1000 * value; } else { return value; } })); window.setTimeout(fn, duration); } else { // no dialog animation required, only swap content fn(); } // start CSS transition by setting desired size for pop-up window as transition target appliedStyle.setProperty('width', desiredWidth); appliedStyle.setProperty('height', desiredHeight); } /** * Removes all element properties associated with dialog animation. */ removeAnimationProperties() { this.dialog.classList.remove('boxplusx-animation'); // remove any explicit sizes applied for the sake of the CSS transition animation let appliedStyle = this.dialog.style; appliedStyle.removeProperty('width'); appliedStyle.removeProperty('height'); } /** * Uses the bisection algorithm to determine the dialog size. * @param a Lower bound (percentage) value at which the dialog fits. * @param b Upper bound (percentage) value at which the dialog does not fit. * @param applyFun Applies a value (e.g. sets content width or height). * @return The (percentage) value at which the dialog fits exactly. */ bisectionSearch(a, b, applyFun) { /** * Evaluates the dialog height at a particular value. * @param value A parameter value to apply. * @return The dialog height in pixels (including border and padding) when the value is applied. */ let evaluateFun = (value) => { applyFun(value); return this.dialog.offsetHeight; }; const containerHeight = this.outerContainer.clientHeight; let dlgHeightB = evaluateFun(b); // no extra horizontal constraints if (dlgHeightB <= containerHeight) { return b; // nothing to do; pop-up window fits vertically } let dlgHeightA = evaluateFun(a); // force dialog take its minimum size if (dlgHeightA >= containerHeight) { applyFun(b); // reset constraints return b; // nothing to do; pop-up window too large to fit even with most constraints } // use bisection method to find least restrictive horizontal constraint that still allows the pop-up window // to fit vertically for (let n = 1; n < 10; ++n) { // use a maximum iteration count to avoid problems with slow convergence let c = ((a + b) / 2) | 0; // cast to integer for improved performance let dlgHeightC = evaluateFun(c); if (dlgHeightC < containerHeight) { a = c; // found a better lower bound dlgHeightA = dlgHeightC; } else { b = c; // found a better upper bound dlgHeightB = dlgHeightC; } } // when the algorithm terminates, lower and upper bound are close; apply the lower bound as the value we seek applyFun(a); return a; } /** * Set maximum width for dialog so that it does not exceed viewport dimensions. * CSS property max-height: 100% is not respected by browsers in this context: the height of the containing * block is not specified explicitly (i.e., it depends on content height), and the element is not absolutely * positioned, therefore the percentage value is treated as none (to avoid infinite re-calculation loops in * layout); as a work-around, we set an upper limit on width instead. */ setMaximumDialogSize() { if (BoxPlusXDimensionBehavior.FixedAspectRatio === this.aspect) { // for fixed aspect ratio, we vary the maximum dialog width in terms of the width of the container element // (browser viewport), expressed as a percentage value let dialogStyle = this.dialog.style; this.bisectionSearch(0, 1000, function (value) { dialogStyle.setProperty('max-width', (value / 10) + '%'); }); } else if (BoxPlusXDimensionBehavior.ResizableBestFit === this.aspect || BoxPlusXDimensionBehavior.Resizable === this.aspect) { // for dynamic aspect ratio, we vary the content holder element pixel height let containerStyle = this.innerContainer.style; containerStyle.removeProperty('max-height'); let value = this.bisectionSearch(0, window.innerHeight, function (value) { containerStyle.setProperty('height', value + 'px'); }); containerStyle.removeProperty('height'); containerStyle.setProperty('max-height', Math.min(value, this.preferredHeight) + 'px'); } } /** * Makes the specified item currently active. * @param index The zero-based index of the item to be displayed. */ navigateToIndex(index) { var _a; const member = this.members[index]; this.current = index; if (this.trail) { this.trail.push(index); } let computedStyle = window.getComputedStyle(this.dialog); const currentWidth = computedStyle.getPropertyValue('width'); const currentHeight = computedStyle.getPropertyValue('height'); this.slideshow.suspend(); setVisible(this.progressIndicator, true); // save caption text let title = member.title; let description = member.description; const href = member.url; const urlparts = parseURL(href); const path = urlparts.pathname; const parameters = Object.assign({}, urlparts.queryparams, urlparts.fragmentparams); this.preferredWidth = parseOptionalInteger(parameters['width'], this.options.preferredWidth); this.preferredHeight = parseOptionalInteger(parameters['height'], this.options.preferredHeight); this.expandToFit = parseOptionalBoolean(parameters['fullscreen']); if (isHashChange(href)) { const target = urlparts.id ? urlparts.id : ((_a = parameters['target']) !== null && _a !== void 0 ? _a : ''); if (target) { let elem = document.getElementById(target); if (elem) { let content = elem.cloneNode(true); this.replaceContent(content, title, description); this.setContentType(BoxPlusXContentType.DocumentFragment); this.morphDialog(BoxPlusXDimensionBehavior.Resizable, currentWidth, currentHeight); } else { this.displayUnavailable(); } } else { this.displayUnavailable(); } } else if (isImageFile(path)) { // download image in the background let image = document.createElement('img'); let srcset = member['srcset']; if (srcset) { image.srcset = srcset; } image.addEventListener('load', (_) => { // try extracting image EXIF orientation for photos getImageMetadata(image, this.options.metadata).then(data => { let container = document.createDocumentFragment(); // set image let rotationContainer = document.createElement('div'); let imageElement = document.createElement('div'); if (data.orientation > 0) { imageElement.classList.add('boxplusx-orientation-' + data.orientation); } let imageElementStyle = imageElement.style; imageElementStyle.setProperty('background-image', 'url("' + image.src + '")'); if (image.srcset) { let matches = matchAll(image.srcset, /\b(\S+)\s+([\d.]+x)\b/g); let imageset = Array.from(matches, (match) => { return 'url("' + match[1] + '") ' + match[2]; }).join(); imageElementStyle.setProperty('background-image', '-webkit-image-set(' + imageset + ')'); } let dpr = this.options.useDevicePixelRatio ? (window.devicePixelRatio || 1) : 1; let h = Math.floor(image.naturalHeight / dpr); let w = Math.floor(image.naturalWidth / dpr); if (!CSS.supports("image-orientation", "from-image") && data.orientation >= 5 && data.orientation <= 8) { // image rotated by 90 or 270 degrees this.preferredWidth = h; this.preferredHeight = w; // CSS transform does not affect bounding box for layout, enlarge/shrink CSS width/height // to accommodate for transformation results imageElementStyle.setProperty('width', (100 * w / h) + '%'); imageElementStyle.setProperty('height', (100 * h / w) + '%'); } else { // image rotated by 0 or 180 degrees this.preferredWidth = w; this.preferredHeight = h; // necessary when we re-use existing container accommodating previous image imageElementStyle.removeProperty('width'); imageElementStyle.removeProperty('height'); } if (!this.shrinkToFit) { let rotationContainerStyle = rotationContainer.style; rotationContainerStyle.setProperty('width', this.preferredWidth + 'px'); rotationContainerStyle.setProperty('height', this.preferredHeight + 'px'); } rotationContainer.appendChild(imageElement); container.appendChild(rotationContainer); // get image metadata information if (data.metadata !== undefined) { let metadata = data.metadata; let textElement = createElement('detail', true); let table = document.createElement('table'); let keys = Object.keys(metadata); keys.sort(); keys.forEach((key) => { let value = metadata[key]; if (value !== undefined) { let row = document.createElement('tr'); let headerCell = document.createElement('td'); headerCell.innerText = key; let valueCell = document.createElement('td'); if (value instanceof HTMLElement) { valueCell.append(value); } else { valueCell.innerText = value.toString(); } row.append(headerCell, valueCell); table.appendChild(row); } }); textElement.appendChild(table); container.appendChild(textElement); } this.replaceContent(container, title, description); this.caption.style.setProperty('max-width', this.preferredWidth + 'px'); // must come after replacing content to have any effect this.setContentType(BoxPlusXContentType.Image); // start dialog animation this.morphDialog(this.shrinkToFit ? BoxPlusXDimensionBehavior.FixedAspectRatio : BoxPlusXDimensionBehavior.ResizableBestFit, currentWidth, currentHeight); }); }, false); image.addEventListener('error', () => { this.displayUnavailable(); }, false); image.src = href; // pre-fetch next image (unless last is shown) to speed up slideshows and viewing images one after the other if (index < this.members.length - 1) { const nextmember = this.members[index + 1]; const nexthref = nextmember.url; const nexturlparts = parseURL(nexthref); if (isImageFile(nexturlparts.pathname)) { let nextimage = document.createElement('img'); nextimage.src = nexthref; } } } else if (/\.(mov|mpe?g|mp4|ogg|webm)$/i.test(path)) { // supported by HTML5-native <video> tag let video = document.createElement('video'); let play = createElement('play'); let container = createElement('video', false, [video, play]); video.addEventListener('loadedmetadata', (_) => { // set video this.replaceContent(container, title, description); this.setContentType(BoxPlusXContentType.Video); this.preferredWidth = video.videoWidth; this.preferredHeight = video.videoHeight; this.morphDialog(BoxPlusXDimensionBehavior.FixedAspectRatio, currentWidth, currentHeight); }, false); video.addEventListener('error', () => this.displayUnavailable(), false); video.src = href; let poster = member.poster; if (poster) { video.poster = poster; } play.addEventListener('click', function () { setVisible(play, false); video.controls = true; video.play(); }); } else if (/\.pdf$/.test(path)) { let embed = document.createElement('embed'); embed.src = href; embed.type = 'application/pdf'; this.replaceContent(embed, title, description); this.setContentType(BoxPlusXContentType.EmbeddedContent); this.morphDialog(BoxPlusXDimensionBehavior.FixedAspectRatio, currentWidth, currentHeight); } else { // check for YouTube URLs let match = /^https?:\/\/(?:www\.)youtu(?:\.be|be\.com)\/(?:embed\/|watch\?v=|v\/|)([-_0-9A-Z]{11,})/i.exec(href); if (match !== null) { this.displayFrame('https://www.youtube.com/embed/' + match[1] + '?' + buildQuery({ rel: '0', controls: '1', showinfo: '0' }), title, description); return; } // URL to unrecognized target (a plain URL to an external location) this.displayFrame(href, title, description); } } /** * Clears the content in the inner container. * This function clears all CSS properties set from script so they revert to their values specified * in the stylesheet file. */ clearContent() { // remove all HTML child elements removeChildNodes(this.innerContainer); let dialogStyle = this.dialog.style; let aspectStyle = this.aspectHolder.style; let containerStyle = this.innerContainer.style; // remove CSS properties that force the aspect ratio aspectStyle.removeProperty('padding-top'); aspectStyle.removeProperty('width'); // remove content and content styling containerStyle.removeProperty('width'); // preferred width // remove fit to window constraints dialogStyle.removeProperty('max-width'); containerStyle.removeProperty('max-height'); this.captionTitle.innerHTML = ""; this.captionDescription.innerHTML = ""; } /** * Replaces the content currently displayed in the pop-up window. * @param content HTML content to place in the viewport area. * @param title The caption text title to associate with the item. * @param description The caption text description to associate with the item. */ replaceContent(content, title, description) { this.clearContent(); this.innerContainer.appendChild(content); this.caption.style.removeProperty('max-width'); // reset caption style this.captionTitle.innerHTML = title; this.captionDescription.innerHTML = description; } /** Displays an indicator that the requested content is not available. */ displayUnavailable() { this.clearContent(); // set unavailable image this.setContentType(BoxPlusXContentType.Unavailable); // start dialog animation this.morphDialog(BoxPlusXDimensionBehavior.FixedAspectRatio); } /** * Displays the contents of an external page in the pop-up window. * @param src The URL to the source to be displayed. * @param title The caption text title to associate with the item. * @param description The caption text description to associate with the item. */ displayFrame(src, title, description) { let frame = document.createElement('iframe'); frame.width = '' + this.preferredWidth; frame.height = '' + this.preferredHeight; frame.allow = 'fullscreen'; frame.src = src; // HTML iframe must be added to the DOM in order for the 'load' event to be triggered this.replaceContent(frame, title, description); // must register 'load' event after adding to the DOM to avoid the event being triggered for blank document let hasFired = false; frame.addEventListener('load', (_) => { // make sure spurious 'load' events are ignored // (the third parameter to addEventListener called 'options' is not supported in all browsers) if (hasFired) { return; } hasFired = true; this.setContentType(BoxPlusXContentType.Frame); this.morphDialog(BoxPlusXDimensionBehavior.FixedAspectRatio); }, false); } /** Returns the current offset of an element from the edge, taking into account text directionality. */ getItemEdgeOffset(item) { switch (this.options.dir) { case BoxPlusXWritingSystem.RightToLeft: const parentItem = item.offsetParent; return parentItem.offsetWidth - item.offsetWidth - item.offsetLeft; // an implementation of function offsetRight case BoxPlusXWritingSystem.LeftToRight: return item.offsetLeft; } } /** * Returns the maximum value for positioning the quick-access navigation bar. * Values in the range [-maximum; 0] are permitted as pixel length values for the CSS left property in order for * the navigation bar to remain in view. */ getNavigationRange() { return Math.max(this.navigationBar.offsetWidth - this.navigationArea.offsetWidth, 0); } /** Returns the current navigation bar position, taking into account text directionality. */ getNavigationPosition() { // negate computed value because the property offsetLeft or offsetRight takes values in the range [-maximum; 0] return -this.getItemEdgeOffset(this.navigationBar); } /** * Starts moving the navigation bar towards the specified target position. * @param targetPosition A nonnegative number, indicating target position. * @param duration A nonnegative number, indicating number of milliseconds for the animation to take. */ slideNavigationBar(targetPosition, duration) { const rtl = this.options.dir == BoxPlusXWritingSystem.RightToLeft; let navigationStyle = this.navigationBar.style; navigationStyle.setProperty(rtl ? 'right' : 'left', (-targetPosition) + 'px'); navigationStyle.setProperty('transition-duration', duration > 0 ? (5 * duration) + 'ms' : ''); } isNavigationBarSliding() { return !!this.navigationBar.style.getPropertyValue('transition-duration'); } /** Re-position the navigation bar so that the active item is aligned with the left edge of the navigation area. */ repositionNavigationBar() { if (isVisible(this.navigationArea)) { // remove focus from navigation item corresponding to previously active item for (let k = 0; k < this.navigationBar.childNodes.length; ++k) { this.navigationBar.childNodes[k].classList.remove('boxplusx-current'); } // set focus on navigation item corresponding to currently active item const index = this.current; const maximum = this.getNavigationRange(); // the maximum permitted offset let item = this.navigationBar.childNodes[index]; item.classList.add('boxplusx-current'); // get the current scroll offset, which may possibly be out of view let scrollPosition = this.getNavigationPosition(); const itemEdgeOffset = this.getItemEdgeOffset(item); // the last position to scroll forward to before the current item goes (partially) out of view let lastForwardScrollFit = Math.min(maximum, itemEdgeOffset); if (scrollPosition > lastForwardScrollFit) { scrollPosition = lastForwardScrollFit; } // the last position to scroll backward to before the current item goes (partially) out of view // subtract item width because items are left offset-aligned let lastBackwardScrollFit = Math.max(0, itemEdgeOffset - this.navigationArea.offsetWidth + item.offsetWidth); if (scrollPosition < lastBackwardScrollFit) { scrollPosition = lastBackwardScrollFit; } this.slideNavigationBar(scrollPosition, 0); // temporarily disable any transition animation } } rewindNavigationBar() { const maximum = this.getNavigationRange(); const current = maximum - this.getNavigationPosition(); // set target position for navigation bar, reached via CSS transition animation // furthermost position for rewinding corresponds to the navigation bar pushed to the rightmost permitted // position (left offset value 0), set transition duration depending on how far we are from the furthermost // position to get a constant movement speed, regardless of what the current navigation bar position is this.slideNavigationBar(0, maximum - current); } forwardNavigationBar() { const maximum = this.getNavigationRange(); const current = this.getNavigationPosition(); // set target position for navigation bar, reached via CSS transition animation // furthermost position for forwarding corresponds to the navigation bar pushed to the leftmost permitted // position (greatest absolute value), set transition duration depending on how far we are from the furthermost // position to get a constant movement speed, regardless of what the current navigation bar position is this.slideNavigationBar(maximum, maximum - current); } stopNavigationBar() { // stop CSS transition animation by forcing the current offset values returned by computed style this.slideNavigationBar(this.getNavigationPosition(), 0); // temporarily disable any transition animation } /** * Discovers boxplusx links on a web page. * boxplusx links are regular HTML <a> elements whose 'rel' attribute has a value with the pattern 'boxplusx-NNN' * where NNN is a unique name. All items that share the same unique name are organized into the same gallery. When * the user clicks an item that is part of a gallery, the item opens in the pop-up window and users can navigate * between this and other items in the gallery without closing the pop-up window. */ static discover(strict, tag, options) { let activator = tag !== null && tag !== void 0 ? tag : 'boxplusx'; let dialog = new BoxPlusXDialog_1(options); // discover groups of pop-up window display items on a web page // links with "rel" attribute that start with (but are not identical to) the activation string const items = document.querySelectorAll('a[href][rel^=' + activator + ']:not([rel=' + activator + '])'); // make groups by name const groups = new Map(); items.forEach((item) => { let identifier = item.getAttribute('rel'); if (!identifier) { return; } let group = groups.get(identifier); if (group === undefined) { group = []; groups.set(identifier, group); } group.push(item); }); groups.forEach((group) => { dialog.bind(group); }); [].filter.call(document.querySelectorAll('a[href][rel=' + activator + ']'), (item) => { dialog.bind([item]); }); if (!strict) { // individual links to images or video not part of a gallery let items = document.querySelectorAll('a[href]:not([rel^=' + activator + '])'); [].filter.call(items, (item) => { return /\.(gif|jpe?g|mov|mpe?g|ogg|png|svg|web[mp])$/i.test(item.pathname) && !item.target; }).forEach((item) => { dialog.bind([item]); }); } } ; }; BoxPlusXDialog = BoxPlusXDialog_1 = __decorate([ sealed ], BoxPlusXDialog); // ensure symbol is exported by Closure Compiler window['BoxPlusXDialog'] = BoxPlusXDialog; /******/ })() ;