« get me outta code hell

client: image-overlay: be a client module - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2024-11-30 18:07:37 -0400
committer(quasar) nebula <qznebula@protonmail.com>2024-11-30 18:07:37 -0400
commit611c82294aecf54dd365aebcd4d34d0eda9a5f0a (patch)
treeab7747b554c85e810d84d10b2ce4314dc9314a36
parentdda1966d87ffc88bea11f9efdca6bfe617839e57 (diff)
client: image-overlay: be a client module
-rw-r--r--src/static/js/client/image-overlay.js306
-rw-r--r--src/static/js/client/index.js3
-rw-r--r--src/static/js/image-overlay.js256
3 files changed, 308 insertions, 257 deletions
diff --git a/src/static/js/client/image-overlay.js b/src/static/js/client/image-overlay.js
new file mode 100644
index 00000000..bdfefeea
--- /dev/null
+++ b/src/static/js/client/image-overlay.js
@@ -0,0 +1,306 @@
+/* eslint-env browser */
+
+import {cssProp} from '../client-util.js';
+import {fetchWithProgress} from '../xhr-util.js';
+
+export const info = {
+  id: 'imageOverlayInfo',
+
+  container: null,
+  actionContainer: null,
+
+  viewOriginalLinks: null,
+  mainImage: null,
+  thumbImage: null,
+
+  actionContentWithoutSize: null,
+  actionContentWithSize: null,
+
+  megabytesContainer: null,
+  kilobytesContainer: null,
+  megabytesContent: null,
+  kilobytesContent: null,
+  fileSizeWarning: null,
+
+  links: null,
+};
+
+export function getPageReferences() {
+  info.container =
+    document.getElementById('image-overlay-container');
+
+  if (!info.container) return;
+
+  info.actionContainer =
+    document.getElementById('image-overlay-action-container');
+
+  info.viewOriginalLinks =
+    document.getElementsByClassName('image-overlay-view-original');
+
+  info.mainImage =
+    document.getElementById('image-overlay-image');
+
+  info.thumbImage =
+    document.getElementById('image-overlay-image-thumb');
+
+  info.actionContentWithoutSize =
+    document.getElementById('image-overlay-action-content-without-size');
+
+  info.actionContentWithSize =
+    document.getElementById('image-overlay-action-content-with-size');
+
+  info.megabytesContainer =
+    document.getElementById('image-overlay-file-size-megabytes');
+
+  info.kilobytesContainer =
+    document.getElementById('image-overlay-file-size-kilobytes');
+
+  info.megabytesContent =
+    info.megabytesContainer.querySelector('.image-overlay-file-size-count');
+
+  info.kilobytesContent =
+    info.kilobytesContainer.querySelector('.image-overlay-file-size-count');
+
+  info.fileSizeWarning =
+    document.getElementById('image-overlay-file-size-warning');
+
+  info.links =
+    Array.from(document.querySelectorAll('.image-link'))
+      .filter(link => !link.closest('.no-image-preview'));
+}
+
+export function addPageListeners() {
+  if (!info.container) return;
+
+  for (const link of info.links) {
+    link.addEventListener('click', handleImageLinkClicked);
+  }
+
+  info.container.addEventListener('click', handleContainerClicked);
+  document.body.addEventListener('keydown', handleKeyDown);
+}
+
+function handleContainerClicked(evt) {
+  // Only hide the image overlay if actually clicking the background.
+  if (evt.target !== info.container) {
+    return;
+  }
+
+  // If you clicked anything close to or beneath the action bar, don't hide
+  // the image overlay.
+  const rect = info.actionContainer.getBoundingClientRect();
+  if (evt.clientY >= rect.top - 40) {
+    return;
+  }
+
+  info.container.classList.remove('visible');
+}
+
+function handleKeyDown(evt) {
+  if (evt.key === 'Escape' || evt.key === 'Esc' || evt.keyCode === 27) {
+    info.container.classList.remove('visible');
+  }
+}
+
+async function handleImageLinkClicked(evt) {
+  if (evt.metaKey || evt.shiftKey || evt.altKey) {
+    return;
+  }
+
+  evt.preventDefault();
+
+  // Don't show the overlay if the image still needs to be revealed.
+  if (evt.target.closest('.reveal:not(.revealed)')) {
+    return;
+  }
+
+  info.container.classList.add('visible');
+  info.container.classList.remove('loaded');
+  info.container.classList.remove('errored');
+
+  const details = getImageLinkDetails(evt.target);
+
+  updateFileSizeInformation(details.originalFileSize);
+
+  let mainSrc = null;
+  let thumbSrc = null;
+
+  if (details.availableThumbList) {
+    const {thumb: mainThumb, length: mainLength} = getPreferredThumbSize(details.availableThumbList);
+    const {thumb: smallThumb, length: smallLength} = getSmallestThumbSize(details.availableThumbList);
+    mainSrc = details.embeddedSrc.replace(/\.[a-z]+\.(jpg|png)$/, `.${mainThumb}.jpg`);
+    thumbSrc = details.embeddedSrc.replace(/\.[a-z]+\.(jpg|png)$/, `.${smallThumb}.jpg`);
+    // Show the thumbnail size on each <img> element's data attributes.
+    // Y'know, just for debugging convenience.
+    info.mainImage.dataset.displayingThumb = `${mainThumb}:${mainLength}`;
+    info.thumbImage.dataset.displayingThumb = `${smallThumb}:${smallLength}`;
+  } else {
+    mainSrc = details.originalSrc;
+    thumbSrc = null;
+    info.mainImage.dataset.displayingThumb = '';
+    info.thumbImage.dataset.displayingThumb = '';
+  }
+
+  for (const link of info.viewOriginalLinks) {
+    link.href = details.originalSrc;
+  }
+
+  await loadOverlayImage(mainSrc, thumbSrc);
+}
+
+function getImageLinkDetails(imageLink) {
+  const a = imageLink.closest('a');
+  const img = a.querySelector('img');
+
+  return {
+    originalSrc:
+      a.href,
+
+    embeddedSrc:
+      img.src,
+
+    originalFileSize:
+      img.dataset.originalSize,
+
+    availableThumbList:
+      img.dataset.thumbs,
+  };
+}
+
+async function loadOverlayImage(mainSrc, thumbSrc) {
+  if (thumbSrc) {
+    info.thumbImage.src = thumbSrc;
+    info.thumbImage.style.display = null;
+  } else {
+    info.thumbImage.src = '';
+    info.thumbImage.style.display = 'none';
+  }
+
+  info.mainImage.addEventListener('load', handleMainImageLoaded);
+  info.mainImage.addEventListener('error', handleMainImageErrored);
+
+  const showProgress = amount => {
+    cssProp(info.container, '--download-progress', `${amount * 100}%`);
+  };
+
+  showProgress(0.00);
+
+  const response =
+    await fetchWithProgress(mainSrc, progress => {
+      if (progress === -1) {
+        // TODO: Indeterminate response progress cue
+        showProgress(0.00);
+      } else {
+        showProgress(0.20 + 0.80 * progress);
+      }
+    });
+
+  if (!response.status.toString().startsWith('2')) {
+    handleMainImageErrored();
+    return;
+  }
+
+  const blob = await response.blob();
+  const blobSrc = URL.createObjectURL(blob);
+
+  info.mainImage.src = blobSrc;
+  showProgress(1.00);
+
+  function handleMainImageLoaded() {
+    info.container.classList.add('loaded');
+    removeEventListeners();
+  }
+
+  function handleMainImageErrored() {
+    info.container.classList.add('errored');
+    removeEventListeners();
+  }
+
+  function removeEventListeners() {
+    info.mainImage.removeEventListener('load', handleMainImageLoaded);
+    info.mainImage.removeEventListener('error', handleMainImageErrored);
+  }
+}
+
+function parseThumbList(availableThumbList) {
+  // Parse all the available thumbnail sizes! These are provided by the actual
+  // content generation on each image.
+  const defaultThumbList = 'huge:1400 semihuge:1200 large:800 medium:400 small:250'
+  const availableSizes =
+    (availableThumbList || defaultThumbList)
+      .split(' ')
+      .map(part => part.split(':'))
+      .map(([thumb, length]) => ({thumb, length: parseInt(length)}))
+      .sort((a, b) => a.length - b.length);
+
+  return availableSizes;
+}
+
+function getPreferredThumbSize(availableThumbList) {
+  // Assuming a square, the image will be constrained to the lesser window
+  // dimension. Coefficient here matches CSS dimensions for image overlay.
+  const constrainedLength = Math.floor(Math.min(
+    0.80 * window.innerWidth,
+    0.80 * window.innerHeight));
+
+  // Match device pixel ratio, which is 2x for "retina" displays and certain
+  // device configurations.
+  const visualLength = window.devicePixelRatio * constrainedLength;
+
+  const availableSizes = parseThumbList(availableThumbList);
+
+  // Starting from the smallest dimensions, find (and return) the first
+  // available length which hits a "good enough" threshold - it's got to be
+  // at least that percent of the way to the actual displayed dimensions.
+  const goodEnoughThreshold = 0.90;
+
+  // (The last item is skipped since we'd be falling back to it anyway.)
+  for (const {thumb, length} of availableSizes.slice(0, -1)) {
+    if (Math.floor(visualLength * goodEnoughThreshold) <= length) {
+      return {thumb, length};
+    }
+  }
+
+  // If none of the items in the list were big enough to hit the "good enough"
+  // threshold, just use the largest size available.
+  return availableSizes[availableSizes.length - 1];
+}
+
+function getSmallestThumbSize(availableThumbList) {
+  // Just snag the smallest size. This'll be used for displaying the "preview"
+  // as the bigger one is loading.
+  const availableSizes = parseThumbList(availableThumbList);
+  return availableSizes[0];
+}
+
+function updateFileSizeInformation(fileSize) {
+  const fileSizeWarningThreshold = 8 * 10 ** 6;
+
+  if (!fileSize) {
+    info.actionContentWithSize.classList.remove('visible');
+    info.actionContentWithoutSize.classList.add('visible');
+    return;
+  }
+
+  info.actionContentWithoutSize.classList.remove('visible');
+  info.actionContentWithSize.classList.add('visible');
+
+  fileSize = parseInt(fileSize);
+  const round = (exp) => Math.round(fileSize / 10 ** (exp - 1)) / 10;
+
+  if (fileSize > fileSizeWarningThreshold) {
+    info.fileSizeWarning.classList.add('visible');
+  } else {
+    info.fileSizeWarning.classList.remove('visible');
+  }
+
+  if (fileSize > 10 ** 6) {
+    info.megabytesContainer.classList.add('visible');
+    info.kilobytesContainer.classList.remove('visible');
+    info.megabytesContent.innerText = round(6);
+  } else {
+    info.megabytesContainer.classList.remove('visible');
+    info.kilobytesContainer.classList.add('visible');
+    info.kilobytesContent.innerText = round(3);
+  }
+}
diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js
index 1c291bfc..e2e5b75b 100644
--- a/src/static/js/client/index.js
+++ b/src/static/js/client/index.js
@@ -1,7 +1,6 @@
 /* eslint-env browser */
 
 import '../group-contributions-table.js';
-import '../image-overlay.js';
 
 import * as additionalNamesBoxModule from './additional-names-box.js';
 import * as albumCommentarySidebarModule from './album-commentary-sidebar.js';
@@ -10,6 +9,7 @@ import * as cssCompatibilityAssistantModule from './css-compatibility-assistant.
 import * as datetimestampTooltipModule from './datetimestamp-tooltip.js';
 import * as hashLinkModule from './hash-link.js';
 import * as hoverableTooltipModule from './hoverable-tooltip.js';
+import * as imageOverlayModule from './image-overlay.js';
 import * as intrapageDotSwitcherModule from './intrapage-dot-switcher.js';
 import * as liveMousePositionModule from './live-mouse-position.js';
 import * as quickDescriptionModule from './quick-description.js';
@@ -28,6 +28,7 @@ export const modules = [
   datetimestampTooltipModule,
   hashLinkModule,
   hoverableTooltipModule,
+  imageOverlayModule,
   intrapageDotSwitcherModule,
   liveMousePositionModule,
   quickDescriptionModule,
diff --git a/src/static/js/image-overlay.js b/src/static/js/image-overlay.js
deleted file mode 100644
index 1eee60e5..00000000
--- a/src/static/js/image-overlay.js
+++ /dev/null
@@ -1,256 +0,0 @@
-/* eslint-env browser */
-
-import {cssProp} from './client-util.js';
-import {fetchWithProgress} from './xhr-util.js';
-
-// TODO: Update to clientSteps style.
-
-function addImageOverlayClickHandlers() {
-  const container = document.getElementById('image-overlay-container');
-
-  if (!container) {
-    console.warn(`#image-overlay-container missing, image overlay module disabled.`);
-    return;
-  }
-
-  for (const link of document.querySelectorAll('.image-link')) {
-    if (link.closest('.no-image-preview')) {
-      continue;
-    }
-
-    link.addEventListener('click', handleImageLinkClicked);
-  }
-
-  const actionContainer = document.getElementById('image-overlay-action-container');
-
-  container.addEventListener('click', handleContainerClicked);
-  document.body.addEventListener('keydown', handleKeyDown);
-
-  function handleContainerClicked(evt) {
-    // Only hide the image overlay if actually clicking the background.
-    if (evt.target !== container) {
-      return;
-    }
-
-    // If you clicked anything close to or beneath the action bar, don't hide
-    // the image overlay.
-    const rect = actionContainer.getBoundingClientRect();
-    if (evt.clientY >= rect.top - 40) {
-      return;
-    }
-
-    container.classList.remove('visible');
-  }
-
-  function handleKeyDown(evt) {
-    if (evt.key === 'Escape' || evt.key === 'Esc' || evt.keyCode === 27) {
-      container.classList.remove('visible');
-    }
-  }
-}
-
-async function handleImageLinkClicked(evt) {
-  if (evt.metaKey || evt.shiftKey || evt.altKey) {
-    return;
-  }
-
-  evt.preventDefault();
-
-  // Don't show the overlay if the image still needs to be revealed.
-  if (evt.target.closest('.reveal:not(.revealed)')) {
-    return;
-  }
-
-  const container = document.getElementById('image-overlay-container');
-  container.classList.add('visible');
-  container.classList.remove('loaded');
-  container.classList.remove('errored');
-
-  const allViewOriginal = document.getElementsByClassName('image-overlay-view-original');
-  const mainImage = document.getElementById('image-overlay-image');
-  const thumbImage = document.getElementById('image-overlay-image-thumb');
-
-  const {href: originalSrc} = evt.target.closest('a');
-
-  const {
-    src: embeddedSrc,
-    dataset: {
-      originalSize: originalFileSize,
-      thumbs: availableThumbList,
-    },
-  } = evt.target.closest('a').querySelector('img');
-
-  updateFileSizeInformation(originalFileSize);
-
-  let mainSrc = null;
-  let thumbSrc = null;
-
-  if (availableThumbList) {
-    const {thumb: mainThumb, length: mainLength} = getPreferredThumbSize(availableThumbList);
-    const {thumb: smallThumb, length: smallLength} = getSmallestThumbSize(availableThumbList);
-    mainSrc = embeddedSrc.replace(/\.[a-z]+\.(jpg|png)$/, `.${mainThumb}.jpg`);
-    thumbSrc = embeddedSrc.replace(/\.[a-z]+\.(jpg|png)$/, `.${smallThumb}.jpg`);
-    // Show the thumbnail size on each <img> element's data attributes.
-    // Y'know, just for debugging convenience.
-    mainImage.dataset.displayingThumb = `${mainThumb}:${mainLength}`;
-    thumbImage.dataset.displayingThumb = `${smallThumb}:${smallLength}`;
-  } else {
-    mainSrc = originalSrc;
-    thumbSrc = null;
-    mainImage.dataset.displayingThumb = '';
-    thumbImage.dataset.displayingThumb = '';
-  }
-
-  if (thumbSrc) {
-    thumbImage.src = thumbSrc;
-    thumbImage.style.display = null;
-  } else {
-    thumbImage.src = '';
-    thumbImage.style.display = 'none';
-  }
-
-  for (const viewOriginal of allViewOriginal) {
-    viewOriginal.href = originalSrc;
-  }
-
-  mainImage.addEventListener('load', handleMainImageLoaded);
-  mainImage.addEventListener('error', handleMainImageErrored);
-
-  const showProgress = amount => {
-    cssProp(container, '--download-progress', `${amount * 100}%`);
-  };
-
-  showProgress(0.00);
-
-  const response =
-    await fetchWithProgress(mainSrc, progress => {
-      if (progress === -1) {
-        // TODO: Indeterminate response progress cue
-        showProgress(0.00);
-      } else {
-        showProgress(0.20 + 0.80 * progress);
-      }
-    });
-
-  if (!response.status.toString().startsWith('2')) {
-    handleMainImageErrored();
-    return;
-  }
-
-  const blob = await response.blob();
-  const blobSrc = URL.createObjectURL(blob);
-
-  mainImage.src = blobSrc;
-  showProgress(1.00);
-
-  function handleMainImageLoaded() {
-    container.classList.add('loaded');
-    removeEventListeners();
-  }
-
-  function handleMainImageErrored() {
-    container.classList.add('errored');
-    removeEventListeners();
-  }
-
-  function removeEventListeners() {
-    mainImage.removeEventListener('load', handleMainImageLoaded);
-    mainImage.removeEventListener('error', handleMainImageErrored);
-  }
-}
-
-function parseThumbList(availableThumbList) {
-  // Parse all the available thumbnail sizes! These are provided by the actual
-  // content generation on each image.
-  const defaultThumbList = 'huge:1400 semihuge:1200 large:800 medium:400 small:250'
-  const availableSizes =
-    (availableThumbList || defaultThumbList)
-      .split(' ')
-      .map(part => part.split(':'))
-      .map(([thumb, length]) => ({thumb, length: parseInt(length)}))
-      .sort((a, b) => a.length - b.length);
-
-  return availableSizes;
-}
-
-function getPreferredThumbSize(availableThumbList) {
-  // Assuming a square, the image will be constrained to the lesser window
-  // dimension. Coefficient here matches CSS dimensions for image overlay.
-  const constrainedLength = Math.floor(Math.min(
-    0.80 * window.innerWidth,
-    0.80 * window.innerHeight));
-
-  // Match device pixel ratio, which is 2x for "retina" displays and certain
-  // device configurations.
-  const visualLength = window.devicePixelRatio * constrainedLength;
-
-  const availableSizes = parseThumbList(availableThumbList);
-
-  // Starting from the smallest dimensions, find (and return) the first
-  // available length which hits a "good enough" threshold - it's got to be
-  // at least that percent of the way to the actual displayed dimensions.
-  const goodEnoughThreshold = 0.90;
-
-  // (The last item is skipped since we'd be falling back to it anyway.)
-  for (const {thumb, length} of availableSizes.slice(0, -1)) {
-    if (Math.floor(visualLength * goodEnoughThreshold) <= length) {
-      return {thumb, length};
-    }
-  }
-
-  // If none of the items in the list were big enough to hit the "good enough"
-  // threshold, just use the largest size available.
-  return availableSizes[availableSizes.length - 1];
-}
-
-function getSmallestThumbSize(availableThumbList) {
-  // Just snag the smallest size. This'll be used for displaying the "preview"
-  // as the bigger one is loading.
-  const availableSizes = parseThumbList(availableThumbList);
-  return availableSizes[0];
-}
-
-function updateFileSizeInformation(fileSize) {
-  const fileSizeWarningThreshold = 8 * 10 ** 6;
-
-  const actionContentWithoutSize = document.getElementById('image-overlay-action-content-without-size');
-  const actionContentWithSize = document.getElementById('image-overlay-action-content-with-size');
-
-  if (!fileSize) {
-    actionContentWithSize.classList.remove('visible');
-    actionContentWithoutSize.classList.add('visible');
-    return;
-  }
-
-  actionContentWithoutSize.classList.remove('visible');
-  actionContentWithSize.classList.add('visible');
-
-  const megabytesContainer = document.getElementById('image-overlay-file-size-megabytes');
-  const kilobytesContainer = document.getElementById('image-overlay-file-size-kilobytes');
-  const megabytesContent = megabytesContainer.querySelector('.image-overlay-file-size-count');
-  const kilobytesContent = kilobytesContainer.querySelector('.image-overlay-file-size-count');
-  const fileSizeWarning = document.getElementById('image-overlay-file-size-warning');
-
-  fileSize = parseInt(fileSize);
-  const round = (exp) => Math.round(fileSize / 10 ** (exp - 1)) / 10;
-
-  if (fileSize > fileSizeWarningThreshold) {
-    fileSizeWarning.classList.add('visible');
-  } else {
-    fileSizeWarning.classList.remove('visible');
-  }
-
-  if (fileSize > 10 ** 6) {
-    megabytesContainer.classList.add('visible');
-    kilobytesContainer.classList.remove('visible');
-    megabytesContent.innerText = round(6);
-  } else {
-    megabytesContainer.classList.remove('visible');
-    kilobytesContainer.classList.add('visible');
-    kilobytesContent.innerText = round(3);
-  }
-
-  void fileSizeWarning;
-}
-
-addImageOverlayClickHandlers();