mirror of
https://github.com/cchuster/connerchu.com
synced 2025-08-02 15:15:32 +00:00
462 lines
28 KiB
HTML
462 lines
28 KiB
HTML
<!-- Force jQuery to load first -->
|
||
<script src="/shortcode-gallery/jquery-3.7.1.min.js"></script>
|
||
<!-- Now load the gallery scripts (these will be loaded again by the shortcode, but this ensures jQuery is present) -->
|
||
<script src="/shortcode-gallery/lazy/jquery.lazy.min.js"></script>
|
||
<script src="/shortcode-gallery/swipebox/js/jquery.swipebox.min.js"></script>
|
||
<script src="/shortcode-gallery/justified_gallery/jquery.justifiedGallery.min.js"></script>
|
||
<link rel="stylesheet" href="/shortcode-gallery/swipebox/css/swipebox.min.css">
|
||
<link rel="stylesheet" href="/shortcode-gallery/justified_gallery/justifiedGallery.min.css">
|
||
|
||
<!-- Render the original gallery content -->
|
||
{{/* The following is a fallback: you may want to copy the full upstream gallery.html here for more advanced features. */}}
|
||
<div>
|
||
{{ .Inner }}
|
||
</div>
|
||
|
||
{{ $currentPage := . }}
|
||
|
||
{{ $images := slice }}
|
||
{{ $globalMatch := .Get "globalMatch" }}
|
||
{{ $localMatch := .Get "match" }}
|
||
|
||
{{ if $localMatch }}
|
||
{{ $images = (.Page.Resources.Match $localMatch )}}
|
||
{{ else if $globalMatch }}
|
||
{{ $images = (resources.Match (.Get "globalMatch")) }}
|
||
{{ else }}
|
||
{{ $images = (.Page.Resources.ByType "image") }}
|
||
{{ end }}
|
||
|
||
{{ $filterOptions := .Get "filterOptions" | default (.Site.Params.galleryFilterOptions | default "[]") }}
|
||
{{ if not $filterOptions }}
|
||
{{ $filterOptions = "[]" }}
|
||
{{ end }}
|
||
|
||
{{ $storeSelectedFilterInUrl := .Get "storeSelectedFilterInUrl" | default (.Site.Params.storeSelectedFilterInUrl | default false) }}
|
||
|
||
{{ $sortOrder := .Get "sortOrder" | default (.Site.Params.gallerySortOrder | default "asc") }}
|
||
|
||
{{ $rowHeight := .Get "rowHeight" | default (.Site.Params.galleryRowHeight | default 150) }}
|
||
|
||
{{ $margins := .Get "margins" | default (.Site.Params.galleryRowMargins | default 5) }}
|
||
|
||
{{ $randomize := .Get "randomize" | default (.Site.Params.galleryRandomize | default false) }}
|
||
|
||
{{ $thumbnailResizeOptions := .Get "thumbnailResizeOptions" | default (.Site.Params.galleryThumbnailResizeOptions | default "300x150 q85 Lanczos") }}
|
||
|
||
{{ $imageResizeOptions := .Get "imageResizeOptions" | default .Site.Params.galleryImageResizeOptions }}
|
||
|
||
{{ $loadJQuery := .Get "loadJQuery" | default (.Site.Params.galleryLoadJQuery | default false) }}
|
||
|
||
{{ $showExif := .Get "showExif" | default (.Site.Params.galleryShowExif | default false) }}
|
||
|
||
{{ $swipeboxParameters := .Get "swipeboxParameters" | default (.Site.Params.gallerySwipeboxParameters | default "") }}
|
||
|
||
{{ $justifiedGalleryParameters := .Get "justifiedGalleryParameters" | default (.Site.Params.galleryJustifiedGalleryParameters | default "") }}
|
||
|
||
{{ $previewType := .Get "previewType" | default (.Site.Params.galleryPreviewType | default "blur") }}
|
||
|
||
{{ $embedPreview := .Get "embedPreview" | default (.Site.Params.galleryEmbedPreview | default true) }}
|
||
|
||
{{ $thumbnailHoverEffect := .Get "thumbnailHoverEffect" | default (.Site.Params.galleryThumbnailHoverEffect | default "none") }}
|
||
|
||
<!-- hugos image processing saves images at resources/_gen/images, if the property resourceDir
|
||
is changed in hugos config.toml file the images are save <resourceDir>/_gen/images.
|
||
Because it is not possible to access the value of resourceDir, users who change resourceDir also have to change
|
||
[params] resourceDir. -->
|
||
{{ $thumbnailResourceDir := printf "%s%s" (.Site.Params.resourceDir | default "resources") "/_gen/images/" }}
|
||
|
||
<!-- Load jquery, jquery-lazy, swipebox and justified_gallery only once per page -->
|
||
{{ if not (.Page.Scratch.Get "galleryLoaded") }}
|
||
{{ .Page.Scratch.Set "galleryLoaded" true }}
|
||
<!-- Use relURL to support hugo projects that run in a subdirectory (bug #18) -->
|
||
|
||
<!-- Always load jQuery first to fix dependency issues -->
|
||
<script src="{{ "shortcode-gallery/jquery-3.7.1.min.js" | relURL }}"></script>
|
||
|
||
{{ if not (eq $previewType "none") }}
|
||
<script src="{{ "shortcode-gallery/lazy/jquery.lazy.min.js" | relURL }}"></script>
|
||
{{ end }}
|
||
|
||
<script src="{{ "shortcode-gallery/swipebox/js/jquery.swipebox.min.js" | relURL }}"></script>
|
||
<link rel="stylesheet" href="{{ "shortcode-gallery/swipebox/css/swipebox.min.css"| relURL }}">
|
||
|
||
<script src="{{ "shortcode-gallery/justified_gallery/jquery.justifiedGallery.min.js"| relURL }}"></script>
|
||
<link rel="stylesheet" href="{{ "shortcode-gallery/justified_gallery/justifiedGallery.min.css"| relURL }}"/>
|
||
{{ end }}
|
||
|
||
<style>
|
||
{{ if (eq $thumbnailHoverEffect "enlarge") }}
|
||
.jg-entry img {
|
||
transition: transform .25s ease-in-out !important;
|
||
}
|
||
|
||
.jg-entry img:hover {
|
||
transform: scale(1.1);
|
||
}
|
||
{{ end }}
|
||
|
||
{{ if not (eq $filterOptions "[]") }}
|
||
/* inline css from assets folder */
|
||
{{ (resources.Get "shortcode-gallery/filterbar.sass" | toCSS).Content | safeCSS }}
|
||
{{ end }}
|
||
</style>
|
||
|
||
<!--
|
||
Ordinal increases every time this shortcode is used in a document
|
||
Ordinal: {{ .Ordinal}}
|
||
-->
|
||
{{ $galleryId := (printf "gallery-%v-%v" .Page.File.UniqueID .Ordinal)}}
|
||
{{ $galleryWrapperId := (printf "gallery-%v-%v-wrapper" .Page.File.UniqueID .Ordinal)}}
|
||
|
||
<div id="{{ $galleryWrapperId }}" class="gallery-wrapper">
|
||
<div id="{{ $galleryId }}" class="justified-gallery">
|
||
{{ range $original := sort $images "Name" $sortOrder}}
|
||
{{ if and (eq $original.ResourceType "image") (in "jpeg png tiff webp" $original.MediaType.SubType) }}
|
||
|
||
{{/* Get metadata from sidecar file, if present. Else an empty dictionary is used. */}}
|
||
{{ $metaFileName := print $original.Name ".meta"}}
|
||
|
||
{{ $metadata := slice }}
|
||
{{ if and $globalMatch (not $localMatch) }}
|
||
{{ $metadata = resources.GetMatch ($metaFileName) }}
|
||
{{ else }}
|
||
{{ $metadata = $currentPage.Page.Resources.GetMatch ($metaFileName) }}
|
||
{{ end }}
|
||
|
||
{{ if $metadata }}
|
||
{{ $metadata = $metadata.Content }}
|
||
{{ $metadata = $metadata | unmarshal }}
|
||
{{ else }}
|
||
{{ $metadata = dict }}
|
||
{{ end }}
|
||
|
||
{{/* If the image has EXIF informations, those are merged together with the metadata from the file */}}
|
||
{{ if $original.Exif }}
|
||
{{ with $original.Exif }}
|
||
{{ $metadata = merge .Tags $metadata }}
|
||
{{ end }}
|
||
{{ end }}
|
||
|
||
|
||
{{/* Correct orientation of the image, this is needed for thumbnails (and downscaled gallery images)
|
||
if the EXIF informations contains an orientation value. See issue #22. */}}
|
||
{{ $rotated := $original.Filter images.AutoOrient }}
|
||
|
||
<div>
|
||
{{ $full := "" }}
|
||
{{ if $imageResizeOptions }}
|
||
{{/* Downscale gallery image, rotate it if needed */}}
|
||
{{ $full = ($rotated.Fit $imageResizeOptions) }}
|
||
{{ else }}
|
||
{{/* No need to use the rotated here, the browser will handle care of EXIF orientation */}}
|
||
{{ $full = $original }}
|
||
{{ end }}
|
||
|
||
{{ $thumbnail := "" }}
|
||
{{ if eq $thumbnailResizeOptions "original" }}
|
||
{{/* If thumbnailResizeOptions is set to 'original', use the original (unmodified) image for the thumbnail */}}
|
||
{{ $thumbnail = $original }}
|
||
{{ else if eq $thumbnailResizeOptions "full" }}
|
||
{{/* If thumbnailResizeOptions is set to 'full', use the same image as the lightbox for the thumbnail */}}
|
||
{{ $thumbnail = $full }}
|
||
{{ else }}
|
||
{{/* Create thumbnail, rotate it if needed */}}
|
||
{{ $thumbnail = ($rotated.Fit $thumbnailResizeOptions) }}
|
||
{{ end }}
|
||
|
||
<a href="{{ $full.RelPermalink }}"
|
||
class="galleryImg"
|
||
{{ with $metadata }}
|
||
{{ if .Title }}
|
||
title="{{ .Title }}"
|
||
{{ else if .ImageDescription }}
|
||
title="{{ .ImageDescription }}"
|
||
{{ end }}
|
||
|
||
{{ if $showExif }}
|
||
data-description="{{ .Model }}{{ if and .Model .LensModel }} + {{ end }}{{ .LensModel }}<br/>{{ if .FocalLength }}{{ .FocalLength }}mm {{ end }}{{ if .FNumber }}f/{{ printf "%.1f" .FNumber.Float64 }} {{ end }}{{ if .ExposureTime }}{{ .ExposureTime }}sec {{ end }} {{ if .ISO }}ISO {{ .ISO }}{{ end }}"
|
||
{{ end }}
|
||
|
||
{{ if not (eq $filterOptions "[]") }}
|
||
{{/* only include fields that are currently supported by the filter mechanism (in JS at the end of the file) */}}
|
||
data-meta="{{ (dict "Tags" .Tags "Rating" .Rating "ColorLabels" .ColorLabels "ImageDescription" .ImageDescription) | jsonify }}"
|
||
{{ end }}
|
||
{{ end }}
|
||
>
|
||
<img
|
||
width="{{ $thumbnail.Width }}" height="{{ $thumbnail.Height }}"
|
||
|
||
{{ if (eq $previewType "blur") }}
|
||
{{ $preview_b := ($thumbnail.Fit ("32x32 q70 box jpg")) }}
|
||
style="filter: blur(25px);"
|
||
{{ if $embedPreview }}
|
||
src="data:image/jpeg;base64,{{ $preview_b.Content | base64Encode }}"
|
||
{{ else }}
|
||
src="{{ $preview_b.RelPermalink }}"
|
||
{{ end }}
|
||
class="lazy"
|
||
data-src="{{ $thumbnail.RelPermalink }}"
|
||
{{ else if (eq $previewType "color") }}
|
||
{{ $preview_1p := ($thumbnail.Resize ("1x1 box png")) }}
|
||
{{ if $embedPreview }}
|
||
src="data:image/png;base64,{{ $preview_1p.Content | base64Encode }}"
|
||
{{ else }}
|
||
src="{{ $preview_1p.RelPermalink }}"
|
||
{{ end }}
|
||
class="lazy"
|
||
data-src="{{ $thumbnail.RelPermalink }}"
|
||
{{ else }}
|
||
src="{{ $thumbnail.RelPermalink }}"
|
||
{{ end }}
|
||
|
||
{{ with $metadata }}
|
||
{{ if .ImageDescription }}
|
||
alt="{{ .ImageDescription }}"
|
||
{{ else }}
|
||
{{ if .Title}}
|
||
alt="{{ .Title }}"
|
||
{{ end }}
|
||
{{ end }}
|
||
{{ end }}
|
||
>
|
||
</a>
|
||
</div>
|
||
{{ end }}
|
||
{{ end }}
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
if (!("HSCGjQuery" in window)) {
|
||
if (!window.jQuery) {
|
||
throw new Error("jQuery is not loaded, hugo-shortcode-gallery wont work without it!");
|
||
}
|
||
window.HSCGjQuery = window.jQuery.noConflict(true);
|
||
}
|
||
|
||
// Isolate all our variables from other scripts.
|
||
// See: https://www.nicoespeon.com/en/2013/05/properly-isolate-variables-in-javascript/
|
||
// We also expect to get jQuery as a parameter, so that if a second instance of jQuery is loaded
|
||
// after this script is executed (e.g. by a Hugo theme), we will still be referencing the previous
|
||
// version of jQuery that has all our plugins loaded.
|
||
;(function($) {
|
||
|
||
$( document ).ready(() => {
|
||
const gallery = $("#{{ $galleryId }}");
|
||
{{ $lastRowJustification := .Get "lastRow" | default (.Site.Params.galleryLastRow | default "justify") }}
|
||
|
||
// the instance of swipebox, it will be set once justifiedGallery is initialized
|
||
let swipeboxInstance = null;
|
||
|
||
// before the gallery initialization the listener has to be added
|
||
// else we can get a race condition and the listener is never called
|
||
gallery.on('jg.complete', () => {
|
||
{{ if or (eq $previewType "blur") (eq $previewType "color") }}
|
||
// if there is already some low resolution image data loaded, then we will wait for loading´
|
||
// the hi-res until the justified gallery has done the layout
|
||
$(() => {
|
||
$('.lazy').Lazy({
|
||
visibleOnly: true,
|
||
afterLoad: element => element.css({filter: "none", transition: "filter 1.0s ease-in-out"})
|
||
});
|
||
});
|
||
{{ end }}
|
||
|
||
swipeboxInstance = $('.galleryImg').swipebox(
|
||
$.extend({},
|
||
{ {{ $swipeboxParameters | safeJS }} }
|
||
)
|
||
);
|
||
});
|
||
|
||
// initialize the justified gallery
|
||
gallery.justifiedGallery($.extend(
|
||
{
|
||
rowHeight : {{ $rowHeight }},
|
||
margins : {{ $margins }},
|
||
border : 0,
|
||
randomize : {{ $randomize }},
|
||
waitThumbnailsLoad : false,
|
||
lastRow : {{ $lastRowJustification }},
|
||
captions : false,
|
||
// if there is at least one filter option
|
||
{{ if not (eq $filterOptions "[]") }}
|
||
// we first show no images at all
|
||
// till the code way below selects a filter and applies it
|
||
// this prevent creating the layout twice
|
||
filter : () => false
|
||
{{ end }}
|
||
},
|
||
{ {{ $justifiedGalleryParameters | safeJS }} }
|
||
));
|
||
|
||
// only include JS code for filter options if there at least one filter option
|
||
{{ if not (eq $filterOptions "[]") }}
|
||
|
||
// this function can be used to create a function that can be used by justifiedGallery
|
||
// for filtering images by their metadata
|
||
function createMetadataFilter(filterFunction) {
|
||
return (entry, index, array) => {
|
||
let meta = $(entry).find("a").attr("data-meta");
|
||
meta = meta ? JSON.parse(meta) : {};
|
||
|
||
let include = filterFunction(meta);
|
||
|
||
// only those images visible in justified gallery should be displayed
|
||
// in swipebox (only <a> with class galleryImg are displayed in swipebox)
|
||
$(entry).find("a").toggleClass("galleryImg", include);
|
||
|
||
return include;
|
||
}
|
||
}
|
||
|
||
// this function returns a function that can be used by justifiedGallery
|
||
// for filtering images by their tags
|
||
function createTagFilter(tagsRegexString) {
|
||
const tagsRegex = RegExp(tagsRegexString);
|
||
return createMetadataFilter(meta => {
|
||
let tags = meta.Tags;
|
||
tags = tags ? tags : [];
|
||
return tags.some(tag => tagsRegex.test(tag));
|
||
});
|
||
};
|
||
|
||
// this function returns a function that can be used by justifiedGallery
|
||
// for filtering images by their description
|
||
function createImageDescriptionFilter(descriptionRegexString) {
|
||
const descriptionRegex = RegExp(descriptionRegexString);
|
||
return createMetadataFilter(meta => {
|
||
let imageDescription = meta.ImageDescription;
|
||
return imageDescription !== null && descriptionRegex.test(imageDescription);
|
||
});
|
||
};
|
||
|
||
// this function returns a function that can be used by justifiedGallery
|
||
// for filtering images by their star rating
|
||
function createRatingFilter(min, max) {
|
||
return createMetadataFilter(meta => {
|
||
let rating = meta.Rating;
|
||
if(rating === null){
|
||
rating = -1;
|
||
}
|
||
return rating >= min && rating <= max;
|
||
});
|
||
};
|
||
|
||
// this function returns a function that can be used by justifiedGallery
|
||
// for filtering images by their color labels
|
||
function createColorLabelFilter(color) {
|
||
color = color.charAt(0).toLowerCase()
|
||
return createMetadataFilter(meta => {
|
||
let colors = meta.ColorLabels;
|
||
return colors && colors.includes(color);
|
||
});
|
||
};
|
||
|
||
|
||
const filterOptions = {{ $filterOptions | safeJS }};
|
||
|
||
// insert a div for inserting filter buttons before the gallery
|
||
const filterBar = $("<div class='justified-gallery-filterbar'/>");
|
||
gallery.before(filterBar);
|
||
|
||
var wrapper = document.getElementById("{{ $galleryWrapperId }}");
|
||
|
||
// inline svg icons
|
||
const expandIcon = '{{ (resources.Get "shortcode-gallery/font-awesome/expand-alt-solid.svg").Content | safeHTML }}';
|
||
const compressIcon = '{{ (resources.Get "shortcode-gallery/font-awesome/compress-alt-solid.svg").Content | safeHTML }}';
|
||
|
||
function setFulltab(activate) {
|
||
if(activate == wrapper.classList.contains("fulltab")){
|
||
return; // nothing to do, we are already in the right state
|
||
}
|
||
|
||
wrapper.classList.toggle("fulltab");
|
||
gallery.justifiedGallery({
|
||
rowHeight : {{ $rowHeight }} * (activate ? 1.5 : 1.0),
|
||
lastRow: (activate ? "nojustify": {{ $lastRowJustification }}),
|
||
// force justifiedGallery to refresh
|
||
refreshTime: 0,
|
||
});
|
||
// force justifiedGallery to refresh
|
||
gallery.data('jg.controller').startImgAnalyzer();
|
||
fullTabButton.html(wrapper.classList.contains("fulltab") ? compressIcon : expandIcon)
|
||
};
|
||
|
||
const fullTabButton = $("<button/>");
|
||
fullTabButton.html(expandIcon);
|
||
fullTabButton.click(() => setFulltab(!wrapper.classList.contains("fulltab")));
|
||
filterBar.append(fullTabButton);
|
||
$(document).keyup(e => {
|
||
// when ESC is pressed
|
||
if (e.keyCode === 27){
|
||
setFulltab(false);
|
||
}
|
||
});
|
||
|
||
function activateFilterButton(filterButton) {
|
||
// activate associated filter
|
||
gallery.justifiedGallery({filter : filterButton.filter});
|
||
// remove select class from all other selected buttons
|
||
filterBar.find('.selected').removeClass('selected');
|
||
filterButton.addClass("selected");
|
||
};
|
||
|
||
// check if the url contains an instruction to apply a specific filter
|
||
// eg. example.com/images/#gallery-filter=Birds
|
||
const params = new URLSearchParams(location.hash.replace(/^\#/,""));
|
||
let activeFilter = params.get('gallery-filter');
|
||
if (!activeFilter) {
|
||
// default to first filter
|
||
activeFilter = filterOptions[0].label;
|
||
}
|
||
|
||
// create a button for each filter entry
|
||
filterOptions.forEach(filterConfig => {
|
||
let filter; // create a filter function based on the available attributes of filterConfig
|
||
if(filterConfig.tags) {
|
||
filter = createTagFilter(filterConfig.tags);
|
||
} else if(filterConfig.rating){
|
||
if(filterConfig.rating.includes("-")){
|
||
minMax = filterConfig.rating.split("-"); // e.g. "3-5"
|
||
} else {
|
||
minMax = [filterConfig.rating, filterConfig.rating]; // e.g. "4"
|
||
}
|
||
filter = createRatingFilter(parseInt(minMax[0]), parseInt(minMax[1]));
|
||
} else if(filterConfig.color_label){
|
||
filter = createColorLabelFilter(filterConfig.color_label);
|
||
} else if(filterConfig.description){
|
||
filter = createImageDescriptionFilter(filterConfig.description);
|
||
} else {
|
||
// default to always true filter
|
||
filter = createMetadataFilter(meta => true);
|
||
}
|
||
|
||
const filterButton = $("<button/>");
|
||
filterButton.text(filterConfig.label);
|
||
filterButton.filter = filter;
|
||
filterButton.click(() => {
|
||
activateFilterButton(filterButton);
|
||
|
||
{{ if $storeSelectedFilterInUrl }}
|
||
// save applied filter in browser url
|
||
const params = new URLSearchParams(location.hash.replace(/^\#/,""));
|
||
params.set('gallery-filter', filterConfig.label);
|
||
window.history.replaceState("", "", location.pathname + location.search + "#" + params.toString());
|
||
{{ end }}
|
||
});
|
||
filterBar.append(filterButton);
|
||
|
||
if(filterConfig.label.toLowerCase() === activeFilter.toLowerCase()) {
|
||
activateFilterButton(filterButton);
|
||
}
|
||
});
|
||
{{ end }}
|
||
});
|
||
|
||
// End of our variable-isolating and self-executing anonymous function.
|
||
// We call it with the one version of jQuery that was used to load our plugins.
|
||
// See: http://blog.nemikor.com/2009/10/03/using-multiple-versions-of-jquery/
|
||
})(window.HSCGjQuery)
|
||
</script> |