Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

image-focus.js 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. /* Image-focus.js http://share.obormot.net/misc/gwern/image-focus.js */
  2. /* Written by Obormot, 15 February 2019 */
  3. /* Lightweight dependency-free JavaScript library for "click to focus/zoom" images in HTML web pages. Originally coded for Obormot.net / GreaterWrong.com. */
  4. if (typeof window.GW == "undefined")
  5. window.GW = { };
  6. GW.temp = { };
  7. GW.isMobile = ('ontouchstart' in document.documentElement);
  8. /********************/
  9. /* DEBUGGING OUTPUT */
  10. /********************/
  11. function GWLog (string) {
  12. if (GW.loggingEnabled || localStorage.getItem("logging-enabled") == "true")
  13. console.log(string);
  14. }
  15. GW.enableLogging = (permanently = false) => {
  16. if (permanently)
  17. localStorage.setItem("logging-enabled", "true");
  18. else
  19. GW.loggingEnabled = true;
  20. };
  21. GW.disableLogging = (permanently = false) => {
  22. if (permanently)
  23. localStorage.removeItem("logging-enabled");
  24. else
  25. GW.loggingEnabled = false;
  26. };
  27. /****************/
  28. /* MISC HELPERS */
  29. /****************/
  30. /* Given an HTML string, creates an element from that HTML, adds it to
  31. #ui-elements-container (creating the latter if it does not exist), and
  32. returns the created element.
  33. */
  34. function addUIElement(element_html) {
  35. var ui_elements_container = document.querySelector("#ui-elements-container");
  36. if (!ui_elements_container) {
  37. ui_elements_container = document.createElement("div");
  38. ui_elements_container.id = "ui-elements-container";
  39. document.querySelector("body").appendChild(ui_elements_container);
  40. }
  41. ui_elements_container.insertAdjacentHTML("beforeend", element_html);
  42. return ui_elements_container.lastElementChild;
  43. }
  44. /* Toggles whether the page is scrollable.
  45. */
  46. function togglePageScrolling(enable) {
  47. if (!enable) {
  48. window.addEventListener('keydown', GW.imageFocus.keyDown = (event) => {
  49. let forbiddenKeys = [ " ", "Spacebar", "ArrowUp", "ArrowDown", "Up", "Down" ];
  50. if (forbiddenKeys.contains(event.key) &&
  51. event.target == document.body) {
  52. event.preventDefault();
  53. }
  54. });
  55. } else {
  56. window.removeEventListener('keydown', GW.imageFocus.keyDown);
  57. }
  58. }
  59. /* Returns true if the array contains the given element.
  60. */
  61. Array.prototype.contains = function (element) {
  62. return (this.indexOf(element) !== -1);
  63. }
  64. /* Returns true if the string begins with the given prefix.
  65. */
  66. String.prototype.hasPrefix = function (prefix) {
  67. return (this.lastIndexOf(prefix, 0) === 0);
  68. }
  69. /***************/
  70. /* IMAGE FOCUS */
  71. /***************/
  72. function imageFocusSetup() {
  73. if (typeof GW.imageFocus == "undefined")
  74. GW.imageFocus = {
  75. contentImagesSelector: "article img, .gallery img",
  76. focusedImageSelector: "article img.focused, .gallery img.focused",
  77. shrinkRatio: 0.975,
  78. hideUITimerDuration: 1500,
  79. hideUITimerExpired: () => {
  80. GWLog("GW.imageFocus.hideUITimerExpired");
  81. let currentTime = new Date();
  82. let timeSinceLastMouseMove = (new Date()) - GW.imageFocus.mouseLastMovedAt;
  83. if (timeSinceLastMouseMove < GW.imageFocus.hideUITimerDuration) {
  84. GW.imageFocus.hideUITimer = setTimeout(GW.imageFocus.hideUITimerExpired, (GW.imageFocus.hideUITimerDuration - timeSinceLastMouseMove));
  85. } else {
  86. hideImageFocusUI();
  87. cancelImageFocusHideUITimer();
  88. }
  89. }
  90. };
  91. GWLog("imageFocusSetup");
  92. // Create event listener for clicking on images to focus them.
  93. GW.imageClickedToFocus = (event) => {
  94. GWLog("GW.imageClickedToFocus");
  95. focusImage(event.target);
  96. if (!GW.isMobile) {
  97. // Set timer to hide the image focus UI.
  98. unhideImageFocusUI();
  99. GW.imageFocus.hideUITimer = setTimeout(GW.imageFocus.hideUITimerExpired, GW.imageFocus.hideUITimerDuration);
  100. }
  101. };
  102. // Add the listener to all content images.
  103. document.querySelectorAll(GW.imageFocus.contentImagesSelector).forEach(image => {
  104. image.addEventListener("click", GW.imageClickedToFocus);
  105. });
  106. // Wrap all images in a span.
  107. document.querySelectorAll(GW.imageFocus.contentImagesSelector).forEach(image => {
  108. let wrapper = document.createElement("span");
  109. wrapper.classList.add("image-wrapper", "img");
  110. image.parentElement.insertBefore(wrapper, image);
  111. wrapper.appendChild(image);
  112. wrapper.classList.add(...image.classList);
  113. image.classList.remove(...image.classList);
  114. });
  115. // Create the image focus overlay.
  116. let imageFocusOverlay = addUIElement("<div id='image-focus-overlay'>" +
  117. `<div class='help-overlay'>
  118. <p><strong>Arrow keys:</strong> Next/previous image</p>
  119. <p><strong>Escape</strong> or <strong>click</strong>: Hide zoomed image</p>
  120. <p><strong>Space bar:</strong> Reset image size & position</p>
  121. <p><strong>Scroll</strong> to zoom in/out</p>
  122. <p>(When zoomed in, <strong>drag</strong> to pan; <br/><strong>double-click</strong> to close)</p>
  123. </div>
  124. <div class='image-number'></div>
  125. <div class='slideshow-buttons'>
  126. <button type='button' class='slideshow-button previous' tabindex='-1' title='Previous image'>
  127. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
  128. <path d="M34.52 239.03L228.87 44.69c9.37-9.37 24.57-9.37 33.94 0l22.67 22.67c9.36 9.36 9.37 24.52.04 33.9L131.49 256l154.02 154.75c9.34 9.38 9.32 24.54-.04 33.9l-22.67 22.67c-9.37 9.37-24.57 9.37-33.94 0L34.52 272.97c-9.37-9.37-9.37-24.57 0-33.94z"/>
  129. </svg>
  130. </button>
  131. <button type='button' class='slideshow-button next' tabindex='-1' title='Next image'>
  132. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
  133. <path d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"/>
  134. </svg>
  135. </button>
  136. </div>
  137. <div class='caption'></div>` +
  138. "</div>");
  139. imageFocusOverlay.dropShadowFilterForImages = " drop-shadow(10px 10px 10px #000) drop-shadow(0 0 10px #444)";
  140. // On orientation change, reset the size & position.
  141. window.matchMedia('(orientation: portrait)').addListener(() => { setTimeout(resetFocusedImagePosition, 0); });
  142. // Accesskey-L starts the slideshow.
  143. (document.querySelector(GW.imageFocus.contentImagesSelector)||{}).accessKey = 'l';
  144. // Count how many images there are in the post, and set the "… of X" label to that.
  145. ((document.querySelector("#image-focus-overlay .image-number")||{}).dataset||{}).numberOfImages = document.querySelectorAll(GW.imageFocus.contentImagesSelector).length;
  146. // Activate the buttons.
  147. imageFocusOverlay.querySelectorAll(".slideshow-button").forEach(button => {
  148. button.addEventListener("click", GW.imageFocus.slideshowButtonClicked = (event) => {
  149. GWLog("GW.imageFocus.slideshowButtonClicked");
  150. focusNextImage(event.target.classList.contains("next"));
  151. event.target.blur();
  152. });
  153. });
  154. // UI starts out hidden.
  155. hideImageFocusUI();
  156. }
  157. function focusImage(imageToFocus) {
  158. GWLog("focusImage");
  159. // Clear 'last-focused' class of last focused image.
  160. let lastFocusedImage = document.querySelector("img.last-focused");
  161. if (lastFocusedImage) {
  162. lastFocusedImage.classList.remove("last-focused");
  163. lastFocusedImage.removeAttribute("accesskey");
  164. }
  165. // Create the focused version of the image.
  166. imageToFocus.classList.toggle("focused", true);
  167. let imageFocusOverlay = document.querySelector("#image-focus-overlay");
  168. let clonedImage = imageToFocus.cloneNode(true);
  169. clonedImage.style = "";
  170. clonedImage.removeAttribute("width");
  171. clonedImage.removeAttribute("height");
  172. clonedImage.style.filter = imageToFocus.style.filter + imageFocusOverlay.dropShadowFilterForImages;
  173. // Add the image to the overlay.
  174. imageFocusOverlay.appendChild(clonedImage);
  175. imageFocusOverlay.classList.toggle("engaged", true);
  176. // Set image to default size and position.
  177. resetFocusedImagePosition();
  178. // Add listener to zoom image with scroll wheel.
  179. window.addEventListener("wheel", GW.imageFocus.scrollEvent = (event) => {
  180. GWLog("GW.imageFocus.scrollEvent");
  181. event.preventDefault();
  182. let image = document.querySelector("#image-focus-overlay img.focused");
  183. // Remove the filter.
  184. image.savedFilter = image.style.filter;
  185. image.style.filter = 'none';
  186. // Locate point under cursor.
  187. let imageBoundingBox = image.getBoundingClientRect();
  188. // Calculate resize factor.
  189. var factor = (image.height > 10 && image.width > 10) || event.deltaY < 0 ?
  190. 1 + Math.sqrt(Math.abs(event.deltaY))/100.0 :
  191. 1;
  192. // Resize.
  193. image.style.width = (event.deltaY < 0 ?
  194. (image.clientWidth * factor) :
  195. (image.clientWidth / factor))
  196. + "px";
  197. image.style.height = "";
  198. // Designate zoom origin.
  199. var zoomOrigin;
  200. // Zoom from cursor if we're zoomed in to where image exceeds screen, AND
  201. // the cursor is over the image.
  202. let imageSizeExceedsWindowBounds = (image.getBoundingClientRect().width > window.innerWidth || image.getBoundingClientRect().height > window.innerHeight);
  203. let zoomingFromCursor = imageSizeExceedsWindowBounds &&
  204. (imageBoundingBox.left <= event.clientX &&
  205. event.clientX <= imageBoundingBox.right &&
  206. imageBoundingBox.top <= event.clientY &&
  207. event.clientY <= imageBoundingBox.bottom);
  208. // Otherwise, if we're zooming OUT, zoom from window center; if we're
  209. // zooming IN, zoom from image center.
  210. let zoomingFromWindowCenter = event.deltaY > 0;
  211. if (zoomingFromCursor)
  212. zoomOrigin = { x: event.clientX,
  213. y: event.clientY };
  214. else if (zoomingFromWindowCenter)
  215. zoomOrigin = { x: window.innerWidth / 2,
  216. y: window.innerHeight / 2 };
  217. else
  218. zoomOrigin = { x: imageBoundingBox.x + imageBoundingBox.width / 2,
  219. y: imageBoundingBox.y + imageBoundingBox.height / 2 };
  220. // Calculate offset from zoom origin.
  221. let offsetOfImageFromZoomOrigin = {
  222. x: imageBoundingBox.x - zoomOrigin.x,
  223. y: imageBoundingBox.y - zoomOrigin.y
  224. }
  225. // Calculate delta from centered zoom.
  226. let deltaFromCenteredZoom = {
  227. x: image.getBoundingClientRect().x - (zoomOrigin.x + (event.deltaY < 0 ? offsetOfImageFromZoomOrigin.x * factor : offsetOfImageFromZoomOrigin.x / factor)),
  228. y: image.getBoundingClientRect().y - (zoomOrigin.y + (event.deltaY < 0 ? offsetOfImageFromZoomOrigin.y * factor : offsetOfImageFromZoomOrigin.y / factor))
  229. }
  230. // Adjust image position appropriately.
  231. image.style.left = parseInt(getComputedStyle(image).left) - deltaFromCenteredZoom.x + "px";
  232. image.style.top = parseInt(getComputedStyle(image).top) - deltaFromCenteredZoom.y + "px";
  233. // Gradually re-center image, if it's smaller than the window.
  234. if (!imageSizeExceedsWindowBounds) {
  235. let imageCenter = { x: image.getBoundingClientRect().x + image.getBoundingClientRect().width / 2,
  236. y: image.getBoundingClientRect().y + image.getBoundingClientRect().height / 2 }
  237. let windowCenter = { x: window.innerWidth / 2,
  238. y: window.innerHeight / 2 }
  239. let imageOffsetFromCenter = { x: windowCenter.x - imageCenter.x,
  240. y: windowCenter.y - imageCenter.y }
  241. // Divide the offset by 10 because we're nudging the image toward center,
  242. // not jumping it there.
  243. image.style.left = parseInt(getComputedStyle(image).left) + imageOffsetFromCenter.x / 10 + "px";
  244. image.style.top = parseInt(getComputedStyle(image).top) + imageOffsetFromCenter.y / 10 + "px";
  245. }
  246. // Put the filter back.
  247. image.style.filter = image.savedFilter;
  248. // Set the cursor appropriately.
  249. setFocusedImageCursor();
  250. }, { passive: false });
  251. window.addEventListener("MozMousePixelScroll", GW.imageFocus.oldFirefoxCompatibilityScrollEvent = (event) => {
  252. GWLog("GW.imageFocus.oldFirefoxCompatibilityScrollEvent");
  253. event.preventDefault();
  254. });
  255. // If image is bigger than viewport, it's draggable. Otherwise, click unfocuses.
  256. window.addEventListener("mouseup", GW.imageFocus.mouseUp = (event) => {
  257. GWLog("GW.imageFocus.mouseUp");
  258. window.onmousemove = '';
  259. // We only want to do anything on left-clicks.
  260. if (event.button != 0) return;
  261. // Don't unfocus if click was on a slideshow next/prev button!
  262. if (event.target.classList.contains("slideshow-button")) return;
  263. // We also don't want to do anything if clicked on the help overlay.
  264. if (event.target.classList.contains("help-overlay") ||
  265. event.target.closest(".help-overlay"))
  266. return;
  267. let focusedImage = document.querySelector("#image-focus-overlay img.focused");
  268. if ((event.target == focusedImage || event.target.tagName == "HTML") &&
  269. (focusedImage.height >= window.innerHeight || focusedImage.width >= window.innerWidth)) {
  270. // If the mouseup event was the end of a pan of an overside image,
  271. // put the filter back; do not unfocus.
  272. focusedImage.style.filter = focusedImage.savedFilter;
  273. } else if (event.target.tagName != "HTML") {
  274. unfocusImageOverlay();
  275. return;
  276. }
  277. });
  278. window.addEventListener("mousedown", GW.imageFocus.mouseDown = (event) => {
  279. GWLog("GW.imageFocus.mouseDown");
  280. // We only want to do anything on left-clicks.
  281. if (event.button != 0) return;
  282. event.preventDefault();
  283. let focusedImage = document.querySelector("#image-focus-overlay img.focused");
  284. if (focusedImage.height >= window.innerHeight || focusedImage.width >= window.innerWidth) {
  285. let mouseCoordX = event.clientX;
  286. let mouseCoordY = event.clientY;
  287. let imageCoordX = parseInt(getComputedStyle(focusedImage).left);
  288. let imageCoordY = parseInt(getComputedStyle(focusedImage).top);
  289. // Save the filter.
  290. focusedImage.savedFilter = focusedImage.style.filter;
  291. window.onmousemove = (event) => {
  292. // Remove the filter.
  293. focusedImage.style.filter = "none";
  294. focusedImage.style.left = imageCoordX + event.clientX - mouseCoordX + 'px';
  295. focusedImage.style.top = imageCoordY + event.clientY - mouseCoordY + 'px';
  296. };
  297. return false;
  298. }
  299. });
  300. // Double-click on the image unfocuses.
  301. clonedImage.addEventListener('dblclick', GW.imageFocus.doubleClick = (event) => {
  302. GWLog("GW.imageFocus.doubleClick");
  303. if (event.target.classList.contains("slideshow-button")) return;
  304. unfocusImageOverlay();
  305. });
  306. // Escape key unfocuses, spacebar resets.
  307. document.addEventListener("keyup", GW.imageFocus.keyUp = (event) => {
  308. GWLog("GW.imageFocus.keyUp");
  309. let allowedKeys = [ " ", "Spacebar", "Escape", "Esc", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Up", "Down", "Left", "Right" ];
  310. if (!allowedKeys.contains(event.key) ||
  311. getComputedStyle(document.querySelector("#image-focus-overlay")).display == "none") return;
  312. event.preventDefault();
  313. switch (event.key) {
  314. case "Escape":
  315. case "Esc":
  316. unfocusImageOverlay();
  317. break;
  318. case " ":
  319. case "Spacebar":
  320. resetFocusedImagePosition();
  321. break;
  322. case "ArrowDown":
  323. case "Down":
  324. case "ArrowRight":
  325. case "Right":
  326. if (document.querySelector(GW.imageFocus.focusedImageSelector)) focusNextImage(true);
  327. break;
  328. case "ArrowUp":
  329. case "Up":
  330. case "ArrowLeft":
  331. case "Left":
  332. if (document.querySelector(GW.imageFocus.focusedImageSelector)) focusNextImage(false);
  333. break;
  334. }
  335. });
  336. setTimeout(() => {
  337. // Prevent spacebar or arrow keys from scrolling page when image focused.
  338. togglePageScrolling(false);
  339. });
  340. // Mark the overlay as being in slide show mode (to show buttons/count).
  341. imageFocusOverlay.classList.add("slideshow");
  342. // Set state of next/previous buttons.
  343. let images = document.querySelectorAll(GW.imageFocus.contentImagesSelector);
  344. var indexOfFocusedImage = getIndexOfFocusedImage();
  345. imageFocusOverlay.querySelector(".slideshow-button.previous").disabled = (indexOfFocusedImage == 0);
  346. imageFocusOverlay.querySelector(".slideshow-button.next").disabled = (indexOfFocusedImage == images.length - 1);
  347. // Set the image number.
  348. document.querySelector("#image-focus-overlay .image-number").textContent = (indexOfFocusedImage + 1);
  349. // Replace the hash.
  350. history.replaceState(null, null, "#if_slide_" + (indexOfFocusedImage + 1));
  351. // Set the caption.
  352. setImageFocusCaption();
  353. // Moving mouse unhides image focus UI.
  354. window.addEventListener("mousemove", GW.imageFocus.mouseMoved = (event) => {
  355. GWLog("GW.imageFocus.mouseMoved");
  356. let currentDateTime = new Date();
  357. if (!(event.target.tagName == "IMG" || event.target.id == "image-focus-overlay")) {
  358. cancelImageFocusHideUITimer();
  359. } else {
  360. if (!GW.imageFocus.hideUITimer) {
  361. unhideImageFocusUI();
  362. GW.imageFocus.hideUITimer = setTimeout(GW.imageFocus.hideUITimerExpired, GW.imageFocus.hideUITimerDuration);
  363. }
  364. GW.imageFocus.mouseLastMovedAt = currentDateTime;
  365. }
  366. });
  367. }
  368. function resetFocusedImagePosition() {
  369. GWLog("resetFocusedImagePosition");
  370. let focusedImage = document.querySelector("#image-focus-overlay img.focused");
  371. if (!focusedImage) return;
  372. let sourceImage = document.querySelector(GW.imageFocus.focusedImageSelector);
  373. // Make sure that initially, the image fits into the viewport.
  374. let constrainedWidth = Math.min(sourceImage.naturalWidth, window.innerWidth * GW.imageFocus.shrinkRatio);
  375. let widthShrinkRatio = constrainedWidth / sourceImage.naturalWidth;
  376. var constrainedHeight = Math.min(sourceImage.naturalHeight, window.innerHeight * GW.imageFocus.shrinkRatio);
  377. let heightShrinkRatio = constrainedHeight / sourceImage.naturalHeight;
  378. let shrinkRatio = Math.min(widthShrinkRatio, heightShrinkRatio);
  379. focusedImage.style.width = (sourceImage.naturalWidth * shrinkRatio) + "px";
  380. focusedImage.style.height = (sourceImage.naturalHeight * shrinkRatio) + "px";
  381. // Remove modifications to position.
  382. focusedImage.style.left = "";
  383. focusedImage.style.top = "";
  384. // Set the cursor appropriately.
  385. setFocusedImageCursor();
  386. }
  387. function setFocusedImageCursor() {
  388. let focusedImage = document.querySelector("#image-focus-overlay img.focused");
  389. if (!focusedImage) return;
  390. focusedImage.style.cursor = (focusedImage.height >= window.innerHeight || focusedImage.width >= window.innerWidth) ?
  391. 'move' : '';
  392. }
  393. function unfocusImageOverlay() {
  394. GWLog("unfocusImageOverlay");
  395. // Remove event listeners.
  396. window.removeEventListener("wheel", GW.imageFocus.scrollEvent);
  397. window.removeEventListener("MozMousePixelScroll", GW.imageFocus.oldFirefoxCompatibilityScrollEvent);
  398. // NOTE: The double-click listener does not need to be removed manually,
  399. // because the focused (cloned) image will be removed anyway.
  400. document.removeEventListener("keyup", GW.imageFocus.keyUp);
  401. window.removeEventListener("mousemove", GW.imageFocus.mouseMoved);
  402. window.removeEventListener("mousedown", GW.imageFocus.mouseDown);
  403. window.removeEventListener("mouseup", GW.imageFocus.mouseUp);
  404. // Set accesskey of currently focused image.
  405. let currentlyFocusedImage = document.querySelector(GW.imageFocus.focusedImageSelector)
  406. if (currentlyFocusedImage) {
  407. currentlyFocusedImage.classList.toggle("last-focused", true);
  408. currentlyFocusedImage.accessKey = 'l';
  409. }
  410. // Remove focused image and hide overlay.
  411. let imageFocusOverlay = document.querySelector("#image-focus-overlay");
  412. imageFocusOverlay.classList.remove("engaged");
  413. imageFocusOverlay.querySelector("img.focused").remove();
  414. // Unset "focused" class of focused image.
  415. document.querySelector(GW.imageFocus.focusedImageSelector).classList.remove("focused");
  416. setTimeout(() => {
  417. // Re-enable page scrolling.
  418. togglePageScrolling(true);
  419. });
  420. // Reset the hash, if needed.
  421. if (location.hash.hasPrefix("#if_slide_"))
  422. history.replaceState(null, null, "#");
  423. }
  424. function getIndexOfFocusedImage() {
  425. let images = document.querySelectorAll(GW.imageFocus.contentImagesSelector);
  426. var indexOfFocusedImage = -1;
  427. for (i = 0; i < images.length; i++) {
  428. if (images[i].classList.contains("focused")) {
  429. indexOfFocusedImage = i;
  430. break;
  431. }
  432. }
  433. return indexOfFocusedImage;
  434. }
  435. function focusNextImage(next = true) {
  436. GWLog("focusNextImage");
  437. let images = document.querySelectorAll(GW.imageFocus.contentImagesSelector);
  438. var indexOfFocusedImage = getIndexOfFocusedImage();
  439. if (next ? (++indexOfFocusedImage == images.length) : (--indexOfFocusedImage == -1)) return;
  440. // Remove existing image.
  441. document.querySelector("#image-focus-overlay img.focused").remove();
  442. // Unset "focused" class of just-removed image.
  443. document.querySelector(GW.imageFocus.focusedImageSelector).classList.remove("focused");
  444. // Create the focused version of the image.
  445. images[indexOfFocusedImage].classList.toggle("focused", true);
  446. let imageFocusOverlay = document.querySelector("#image-focus-overlay");
  447. let clonedImage = images[indexOfFocusedImage].cloneNode(true);
  448. clonedImage.style = "";
  449. clonedImage.removeAttribute("width");
  450. clonedImage.removeAttribute("height");
  451. clonedImage.style.filter = images[indexOfFocusedImage].style.filter + imageFocusOverlay.dropShadowFilterForImages;
  452. imageFocusOverlay.appendChild(clonedImage);
  453. imageFocusOverlay.classList.toggle("engaged", true);
  454. // Set image to default size and position.
  455. resetFocusedImagePosition();
  456. // Set state of next/previous buttons.
  457. imageFocusOverlay.querySelector(".slideshow-button.previous").disabled = (indexOfFocusedImage == 0);
  458. imageFocusOverlay.querySelector(".slideshow-button.next").disabled = (indexOfFocusedImage == images.length - 1);
  459. // Set the image number display.
  460. document.querySelector("#image-focus-overlay .image-number").textContent = (indexOfFocusedImage + 1);
  461. // Set the caption.
  462. setImageFocusCaption();
  463. // Replace the hash.
  464. history.replaceState(null, null, "#if_slide_" + (indexOfFocusedImage + 1));
  465. }
  466. function setImageFocusCaption() {
  467. GWLog("setImageFocusCaption");
  468. var T = { }; // Temporary storage.
  469. // Clear existing caption, if any.
  470. let captionContainer = document.querySelector("#image-focus-overlay .caption");
  471. Array.from(captionContainer.children).forEach(child => { child.remove(); });
  472. // Determine caption.
  473. let currentlyFocusedImage = document.querySelector(GW.imageFocus.focusedImageSelector);
  474. var captionHTML;
  475. if ((T.enclosingFigure = currentlyFocusedImage.closest("figure")) &&
  476. (T.figcaption = T.enclosingFigure.querySelector("figcaption"))) {
  477. captionHTML = (T.figcaption.querySelector("p")) ?
  478. T.figcaption.innerHTML :
  479. "<p>" + T.figcaption.innerHTML + "</p>";
  480. } else if (currentlyFocusedImage.title != "") {
  481. captionHTML = `<p>${currentlyFocusedImage.title}</p>`;
  482. }
  483. // Insert the caption, if any.
  484. if (captionHTML) captionContainer.insertAdjacentHTML("beforeend", captionHTML);
  485. }
  486. function hideImageFocusUI() {
  487. GWLog("hideImageFocusUI");
  488. let imageFocusOverlay = document.querySelector("#image-focus-overlay");
  489. imageFocusOverlay.querySelectorAll(".slideshow-button, .help-overlay, .image-number, .caption").forEach(element => {
  490. element.classList.toggle("hidden", true);
  491. });
  492. }
  493. function unhideImageFocusUI() {
  494. GWLog("unhideImageFocusUI");
  495. let imageFocusOverlay = document.querySelector("#image-focus-overlay");
  496. imageFocusOverlay.querySelectorAll(".slideshow-button, .help-overlay, .image-number, .caption").forEach(element => {
  497. element.classList.remove("hidden");
  498. });
  499. }
  500. function cancelImageFocusHideUITimer() {
  501. clearTimeout(GW.imageFocus.hideUITimer);
  502. GW.imageFocus.hideUITimer = null;
  503. }
  504. function focusImageSpecifiedByURL() {
  505. GWLog("focusImageSpecifiedByURL");
  506. if (location.hash.hasPrefix("#if_slide_")) {
  507. document.addEventListener("readystatechange", () => {
  508. let images = document.querySelectorAll(GW.imageFocus.contentImagesSelector);
  509. let imageToFocus = (/#if_slide_([0-9]+)/.exec(location.hash)||{})[1];
  510. if (imageToFocus > 0 && imageToFocus <= images.length) {
  511. focusImage(images[imageToFocus - 1]);
  512. if (!GW.isMobile) {
  513. // Set timer to hide the image focus UI.
  514. unhideImageFocusUI();
  515. GW.imageFocus.hideUITimer = setTimeout(GW.imageFocus.hideUITimerExpired, GW.imageFocus.hideUITimerDuration);
  516. }
  517. }
  518. });
  519. }
  520. }
  521. /******************/
  522. /* INITIALIZATION */
  523. /******************/
  524. imageFocusSetup();
  525. focusImageSpecifiedByURL();