2025-07-06 15:42:13 +02:00

460 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 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 */}}
{{ with $original.Exif }}
{{ $metadata = merge .Tags $metadata }}
{{ 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>