diff --git a/src/config.yml b/src/config.yml index 6b3e7f7..6805895 100644 --- a/src/config.yml +++ b/src/config.yml @@ -1,12 +1,18 @@ baseURL: 'https://oscarmlage.com' languageCode: 'en-us' title: 'oscarmlage' -theme: 'oscar' +theme: + - 'oscar' + - 'hugo-shortcode-gallery' pygmentsStyle: "monokai" canonifyurls: true paginate: 15 summarylength: 30 +timeout: 60000 +params: + galleryLoadJQuery: true + defaultContentLanguage: "en" markup: diff --git a/src/themes/hugo-shortcode-gallery/README.md b/src/themes/hugo-shortcode-gallery/README.md new file mode 100644 index 0000000..2fe0e3a --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/README.md @@ -0,0 +1,134 @@ +# hugo-shortcode-gallery + +This is a theme component for hugo. + +This component contains a shortcode to include a gallery in your .md files. + +The gallery is rendered using autogenerated thumbnails arranged in a +[grid](http://miromannino.github.io/Justified-Gallery/). With a click on the images +a [lightbox](http://brutaldesign.github.io/swipebox/) is opened an all images can be +viewed fullscreen. + +# Demo + +You can see this shortcode-gallery in action on [my website](https://matze.rocks/images/). + +## Installation + +Clone this git repository into your *themes* folder. + +``` +git clone https://github.com/mfg92/hugo-shortcode-gallery.git +``` + +Next edit your projects +*config.toml* and add this theme component to your themes: + +``` +theme = ["your-main-theme", "hugo-shortcode-gallery"] +``` + +To read about hugo's theme components and how to use them have a look at +https://gohugo.io/hugo-modules/theme-components/. + + +## Usage Example + +Here is an usage example: + +``` +{{< gallery match="images/*" sortOrder="desc" rowHeight="150" margins="5" thumbnailResizeOptions="600x600 q90 Lanczos" showExif=true previewType="blur" embedPreview="true" >}} +``` + +This shortcode will generate a gallery containing all images of the folder *images*. +The folder must be next to the .md file where this gallery is used in. This uses [page bundles](https://gohugo.io/content-management/page-bundles/) +so the directory layout should look like this: + +``` +new-post-name/ + index.md + images/ + DSC_0001.jpg + DSC_0002.jpg +``` + +The parameter `sortOrder` decides whether the images are sorted ascending ("asc") or descending ("desc"). + +The `rowHeight` parameter determines the height of the rows that are displayed while the +`margin` parameter defines the gap between the images. + +A thumbnail is generated using the `thumbnailResizeOptions` parameter, they are handed over +to *Hugo's* [image processing](https://gohugo.io/content-management/image-processing/) +function using the fit method. In the example above, the generated thumbnails have a width of max 600 pixel and +a height of max 600, the actual width and height depend on the original aspect ratio. The JPEG image quality is 90% and the +scaling uses the high quality *Lanczos* filter. + +If `previewType` is set to "blur" (or "color"), a very low resolution image (or a single pixel image) will be loaded for every image in the gallery first. +The hight resolution thumbnail images (see `thumbnailResizeOptions`) will only be loaded if they are on the currently visible part of the page (or close to it). +This leads to a faster loading page. You can set `previewType` to "none" to disable this feature and all thumbnails will be directly loaded. + +Enable `embedPreview` to let hugo embed the tiny preview image directly in the page HTML as a base64 strings. This reduces the amount of required network round trip times. + +The setting `thumbnailHoverEffect` configures what should happen when the mouse hovers above a thumbnail in the gallery. +It defaults to "none", but it can be set to "enlarge", in that case the image is scaled up (x1.1) in a short smooth animation. + +The size of the image as shown in the gallery can be customised using the (optional) `imageResizeOptions` parameter. The syntax is the same as for `thumbnailResizeOptions`. If ommited, the image will be displayed in its original size. + +The setting `lastRow` configures the justification of the last row of the grid. When set to "justify", the entire grid including the last row will be fully-justified, right and left. This parameter respects all of the `lastRow` options of Justified Gallery, including "nojustify" and "hide". + +When the users clicks on an image, a lightbox shows up displaying the clicked image in large using the whole available space. +If the image contains a title/description in the EXIF metadata field _ImageDescription_ or a title is defined in the image's sidecar file (see section below) there will be a top bar displaying that. +If the `showExif` option is set to `true` (without quotes), some parts of the image's EXIF data will be shown on the bottom bar e.g.: "Canon EOS 80D + EF100-400mm f/4.5-5.6L IS II USM 400mm f/8 1/400sec ISO 2500". +The EXIF display will only work if you add following lines to your *config.toml*: +```TOML +[imaging.exif] + includeFields = ".*" +``` + +An advanced setting is `filterOptions`: It allows the user to filter the displayed images by using buttons. +The text of the buttons and the regex used to filter has to be specified in a JSON array of objects. Currently it is only supported to filter by EXIF tags, image description, start rating or color labels. In the future it will be possible to filter by image name or other EXIF fields (pull requests are welcome). In addition to the metadata of the EXIF embedded in the image, a metadata sidecar file (see section below) can be used to add metadata for filtering. + +Additionally to the filter buttons, a button to activate full screen mode of the gallery is added. + +An example of the `filterOptions` JSON: +``` +filterOptions="[{label: 'All', tags: '.*'}, {label: 'Birds', tags: 'bird'}, {label: 'Macro', tags: 'macro'}, {label: 'Insects', tags: 'insect'}]" +``` + +When `filterOptions` is used, the switch `storeSelectedFilterInUrl` can be set to `true`. This will instruct the gallery to append the name of the filter to the url displayed in the browser when a filter button is clicked. This has two purposes: The user can share this link and recipients will see the gallery with the same filter as the original user. Furthermore the selected filter is stored in the browsers history. + +As many websites/themes already include *jQuery*, this theme component will use the available *jQuery* lib. +If the page does not already use *jQuery* the parameter `loadJQuery=true` must be used to +instruct the theme component to load the provided *jQuery* lib. + +All settings can be done globally in the site's *config.toml*, for that the prefix `gallery` has to be used. E.g. `galleryLoadJQuery` instead of `loadJQuery`. + +## Sidecar files + +The metadata embedded in a image can be extended/overshadowed by a metadata sidecar file. The file must have the same name as the image plus ".meta" (e.g. "image.jpg.meta"). The content has to be a *JSON* like: + +```JSON +{ +"Tags": ["macro","insect"], +"Title": "Maya the Bee", +"ColorLabels": "RG", +"Rating": 3 +} + ``` + +## Requirements + +This component requires a hugo version >= 0.59. + +## Dependencies + +The component uses (and includes) [*Justified Gallery*](http://miromannino.github.io/Justified-Gallery/) +to render the images between the text and [*Swipebox*](http://brutaldesign.github.io/swipebox/) +to show them full screen. These dependencies are included in this repository. + +## Troubleshooting + +When bigger galleries are processed it can be required to set hugo's timeout property in the *config.toml* to a higher value: +``` +timeout = 60000 # This is required for larger galleries to be build (60 sec) +``` diff --git a/src/themes/hugo-shortcode-gallery/assets/shortcode-gallery/filterbar.sass b/src/themes/hugo-shortcode-gallery/assets/shortcode-gallery/filterbar.sass new file mode 100644 index 0000000..808f801 --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/assets/shortcode-gallery/filterbar.sass @@ -0,0 +1,51 @@ +/* Changes made here sadly only apply after restarting hugo server */ + +/* make 5px space between the button(filter options) in the filter bar +.justified-gallery-filterbar + display: flex + justify-content: flex-start + flex-wrap: wrap + gap: 5px + + button + padding: 6px + border: 1px solid #fff + border-radius: 5px + background-color: transparent + font-weight: bold + color: #fff + line-height: 1em + + &:hover + text-decoration: underline + + &.selected + text-decoration: underline + background-color: #fff3 + + svg + width: 1em + height: 1em + transform: rotate(90deg) + transition: transform .2s linear + + &:hover svg + transform: rotate(90deg) scale(1.3) + + +.fulltab + position: absolute + top: 0 + left: 0 + z-index: 100 + min-height: 100% + background-color: #222 + + &.justified-gallery-filterbar + position: sticky + top: 0 + left: 0 + z-index: 101 + background-color: #222 + padding: 5px + margin: 0px \ No newline at end of file diff --git a/src/themes/hugo-shortcode-gallery/assets/shortcode-gallery/font-awesome/compress-alt-solid.svg b/src/themes/hugo-shortcode-gallery/assets/shortcode-gallery/font-awesome/compress-alt-solid.svg new file mode 100644 index 0000000..4625a19 --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/assets/shortcode-gallery/font-awesome/compress-alt-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/themes/hugo-shortcode-gallery/assets/shortcode-gallery/font-awesome/expand-alt-solid.svg b/src/themes/hugo-shortcode-gallery/assets/shortcode-gallery/font-awesome/expand-alt-solid.svg new file mode 100644 index 0000000..14f859b --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/assets/shortcode-gallery/font-awesome/expand-alt-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/themes/hugo-shortcode-gallery/assets/shortcode-gallery/font-awesome/license.txt b/src/themes/hugo-shortcode-gallery/assets/shortcode-gallery/font-awesome/license.txt new file mode 100644 index 0000000..c27f229 --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/assets/shortcode-gallery/font-awesome/license.txt @@ -0,0 +1 @@ +https://fontawesome.com/license/free \ No newline at end of file diff --git a/src/themes/hugo-shortcode-gallery/config.toml b/src/themes/hugo-shortcode-gallery/config.toml new file mode 100644 index 0000000..f1d0165 --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/config.toml @@ -0,0 +1,3 @@ +# this allows to use resources.Get to get resources of the folder "assets" +# see https://github.com/gohugoio/hugo/commit/dea71670c059ab4d5a42bd22503f18c087dd22d4 +assetDir = "assets" \ No newline at end of file diff --git a/src/themes/hugo-shortcode-gallery/layouts/shortcodes/gallery.html b/src/themes/hugo-shortcode-gallery/layouts/shortcodes/gallery.html new file mode 100644 index 0000000..73e447a --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/layouts/shortcodes/gallery.html @@ -0,0 +1,412 @@ +{{ $currentPage := . }} + +{{ $images := (.Page.Resources.ByType "image") }} +{{ if .Get "match"}} + {{ $images = (.Page.Resources.Match (.Get "match")) }} +{{ 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) }} + +{{ $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) }} + +{{ $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") }} + + +{{ $thumbnailResourceDir := printf "%s%s" (.Site.Params.resourceDir | default "resources") "/_gen/images/" }} + + +{{ if not (.Page.Scratch.Get "galleryLoaded") }} + {{ .Page.Scratch.Set "galleryLoaded" true }} + + + {{ if $loadJQuery }} + + {{ end }} + + {{ if not (eq $previewType "none") }} + + {{ end }} + + + + + + +{{ end }} + + + + +{{ $galleryId := (printf "gallery-%v-%v" .Page.File.UniqueID .Ordinal)}} +{{ $galleryWrapperId := (printf "gallery-%v-%v-wrapper" .Page.File.UniqueID .Ordinal)}} + + + + diff --git a/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/jquery-3.6.0.min.js b/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/jquery-3.6.0.min.js new file mode 100644 index 0000000..200b54e --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/jquery-3.6.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0= 0 ? settings.border : settings.margins; + this.maxRowHeight = this.retrieveMaxRowHeight(); + this.suffixRanges = this.retrieveSuffixRanges(); + this.offY = this.border; + this.rows = 0; + this.spinner = { + phase : 0, + timeSlot : 150, + $el : $('
'), + intervalId : null + }; + this.scrollBarOn = false; + this.checkWidthIntervalId = null; + this.galleryWidth = $gallery.width(); + this.$gallery = $gallery; + + }; + + /** @returns {String} the best suffix given the width and the height */ + JustifiedGallery.prototype.getSuffix = function (width, height) { + var longestSide, i; + longestSide = (width > height) ? width : height; + for (i = 0; i < this.suffixRanges.length; i++) { + if (longestSide <= this.suffixRanges[i]) { + return this.settings.sizeRangeSuffixes[this.suffixRanges[i]]; + } + } + return this.settings.sizeRangeSuffixes[this.suffixRanges[i - 1]]; + }; + + /** + * Remove the suffix from the string + * + * @returns {string} a new string without the suffix + */ + JustifiedGallery.prototype.removeSuffix = function (str, suffix) { + return str.substring(0, str.length - suffix.length); + }; + + /** + * @returns {boolean} a boolean to say if the suffix is contained in the str or not + */ + JustifiedGallery.prototype.endsWith = function (str, suffix) { + return str.indexOf(suffix, str.length - suffix.length) !== -1; + }; + + /** + * Get the used suffix of a particular url + * + * @param str + * @returns {String} return the used suffix + */ + JustifiedGallery.prototype.getUsedSuffix = function (str) { + for (var si in this.settings.sizeRangeSuffixes) { + if (this.settings.sizeRangeSuffixes.hasOwnProperty(si)) { + if (this.settings.sizeRangeSuffixes[si].length === 0) continue; + if (this.endsWith(str, this.settings.sizeRangeSuffixes[si])) return this.settings.sizeRangeSuffixes[si]; + } + } + return ''; + }; + + /** + * Given an image src, with the width and the height, returns the new image src with the + * best suffix to show the best quality thumbnail. + * + * @returns {String} the suffix to use + */ + JustifiedGallery.prototype.newSrc = function (imageSrc, imgWidth, imgHeight, image) { + var newImageSrc; + + if (this.settings.thumbnailPath) { + newImageSrc = this.settings.thumbnailPath(imageSrc, imgWidth, imgHeight, image); + } else { + var matchRes = imageSrc.match(this.settings.extension); + var ext = (matchRes !== null) ? matchRes[0] : ''; + newImageSrc = imageSrc.replace(this.settings.extension, ''); + newImageSrc = this.removeSuffix(newImageSrc, this.getUsedSuffix(newImageSrc)); + newImageSrc += this.getSuffix(imgWidth, imgHeight) + ext; + } + + return newImageSrc; + }; + + /** + * Shows the images that is in the given entry + * + * @param $entry the entry + * @param callback the callback that is called when the show animation is finished + */ + JustifiedGallery.prototype.showImg = function ($entry, callback) { + if (this.settings.cssAnimation) { + $entry.addClass('entry-visible'); + if (callback) callback(); + } else { + $entry.stop().fadeTo(this.settings.imagesAnimationDuration, 1.0, callback); + $entry.find(this.settings.imgSelector).stop().fadeTo(this.settings.imagesAnimationDuration, 1.0, callback); + } + }; + + /** + * Extract the image src form the image, looking from the 'safe-src', and if it can't be found, from the + * 'src' attribute. It saves in the image data the 'jg.originalSrc' field, with the extracted src. + * + * @param $image the image to analyze + * @returns {String} the extracted src + */ + JustifiedGallery.prototype.extractImgSrcFromImage = function ($image) { + var imageSrc = (typeof $image.data('safe-src') !== 'undefined') ? $image.data('safe-src') : $image.attr('src'); + $image.data('jg.originalSrc', imageSrc); + return imageSrc; + }; + + /** @returns {jQuery} the image in the given entry */ + JustifiedGallery.prototype.imgFromEntry = function ($entry) { + var $img = $entry.find(this.settings.imgSelector); + return $img.length === 0 ? null : $img; + }; + + /** @returns {jQuery} the caption in the given entry */ + JustifiedGallery.prototype.captionFromEntry = function ($entry) { + var $caption = $entry.find('> .caption'); + return $caption.length === 0 ? null : $caption; + }; + + /** + * Display the entry + * + * @param {jQuery} $entry the entry to display + * @param {int} x the x position where the entry must be positioned + * @param y the y position where the entry must be positioned + * @param imgWidth the image width + * @param imgHeight the image height + * @param rowHeight the row height of the row that owns the entry + */ + JustifiedGallery.prototype.displayEntry = function ($entry, x, y, imgWidth, imgHeight, rowHeight) { + $entry.width(imgWidth); + $entry.height(rowHeight); + $entry.css('top', y); + $entry.css('left', x); + + var $image = this.imgFromEntry($entry); + if ($image !== null) { + $image.css('width', imgWidth); + $image.css('height', imgHeight); + $image.css('margin-left', - imgWidth / 2); + $image.css('margin-top', - imgHeight / 2); + + // Image reloading for an high quality of thumbnails + var imageSrc = $image.attr('src'); + var newImageSrc = this.newSrc(imageSrc, imgWidth, imgHeight, $image[0]); + + $image.one('error', function () { + $image.attr('src', $image.data('jg.originalSrc')); //revert to the original thumbnail, we got it. + }); + + var loadNewImage = function () { + if (imageSrc !== newImageSrc) { //load the new image after the fadeIn + $image.attr('src', newImageSrc); + } + }; + + if ($entry.data('jg.loaded') === 'skipped') { + this.onImageEvent(imageSrc, $.proxy(function() { + this.showImg($entry, loadNewImage); + $entry.data('jg.loaded', true); + }, this)); + } else { + this.showImg($entry, loadNewImage); + } + + } else { + this.showImg($entry); + } + + this.displayEntryCaption($entry); + }; + + /** + * Display the entry caption. If the caption element doesn't exists, it creates the caption using the 'alt' + * or the 'title' attributes. + * + * @param {jQuery} $entry the entry to process + */ + JustifiedGallery.prototype.displayEntryCaption = function ($entry) { + var $image = this.imgFromEntry($entry); + if ($image !== null && this.settings.captions) { + var $imgCaption = this.captionFromEntry($entry); + + // Create it if it doesn't exists + if ($imgCaption === null) { + var caption = $image.attr('alt'); + if (!this.isValidCaption(caption)) caption = $entry.attr('title'); + if (this.isValidCaption(caption)) { // Create only we found something + $imgCaption = $('
' + caption + '
'); + $entry.append($imgCaption); + $entry.data('jg.createdCaption', true); + } + } + + // Create events (we check again the $imgCaption because it can be still inexistent) + if ($imgCaption !== null) { + if (!this.settings.cssAnimation) $imgCaption.stop().fadeTo(0, this.settings.captionSettings.nonVisibleOpacity); + this.addCaptionEventsHandlers($entry); + } + } else { + this.removeCaptionEventsHandlers($entry); + } + }; + + /** + * Validates the caption + * + * @param caption The caption that should be validated + * @return {boolean} Validation result + */ + JustifiedGallery.prototype.isValidCaption = function (caption) { + return (typeof caption !== 'undefined' && caption.length > 0); + }; + + /** + * The callback for the event 'mouseenter'. It assumes that the event currentTarget is an entry. + * It shows the caption using jQuery (or using CSS if it is configured so) + * + * @param {Event} eventObject the event object + */ + JustifiedGallery.prototype.onEntryMouseEnterForCaption = function (eventObject) { + var $caption = this.captionFromEntry($(eventObject.currentTarget)); + if (this.settings.cssAnimation) { + $caption.addClass('caption-visible').removeClass('caption-hidden'); + } else { + $caption.stop().fadeTo(this.settings.captionSettings.animationDuration, + this.settings.captionSettings.visibleOpacity); + } + }; + + /** + * The callback for the event 'mouseleave'. It assumes that the event currentTarget is an entry. + * It hides the caption using jQuery (or using CSS if it is configured so) + * + * @param {Event} eventObject the event object + */ + JustifiedGallery.prototype.onEntryMouseLeaveForCaption = function (eventObject) { + var $caption = this.captionFromEntry($(eventObject.currentTarget)); + if (this.settings.cssAnimation) { + $caption.removeClass('caption-visible').removeClass('caption-hidden'); + } else { + $caption.stop().fadeTo(this.settings.captionSettings.animationDuration, + this.settings.captionSettings.nonVisibleOpacity); + } + }; + + /** + * Add the handlers of the entry for the caption + * + * @param $entry the entry to modify + */ + JustifiedGallery.prototype.addCaptionEventsHandlers = function ($entry) { + var captionMouseEvents = $entry.data('jg.captionMouseEvents'); + if (typeof captionMouseEvents === 'undefined') { + captionMouseEvents = { + mouseenter: $.proxy(this.onEntryMouseEnterForCaption, this), + mouseleave: $.proxy(this.onEntryMouseLeaveForCaption, this) + }; + $entry.on('mouseenter', undefined, undefined, captionMouseEvents.mouseenter); + $entry.on('mouseleave', undefined, undefined, captionMouseEvents.mouseleave); + $entry.data('jg.captionMouseEvents', captionMouseEvents); + } + }; + + /** + * Remove the handlers of the entry for the caption + * + * @param $entry the entry to modify + */ + JustifiedGallery.prototype.removeCaptionEventsHandlers = function ($entry) { + var captionMouseEvents = $entry.data('jg.captionMouseEvents'); + if (typeof captionMouseEvents !== 'undefined') { + $entry.off('mouseenter', undefined, captionMouseEvents.mouseenter); + $entry.off('mouseleave', undefined, captionMouseEvents.mouseleave); + $entry.removeData('jg.captionMouseEvents'); + } + }; + + /** + * Clear the building row data to be used for a new row + */ + JustifiedGallery.prototype.clearBuildingRow = function () { + this.buildingRow.entriesBuff = []; + this.buildingRow.aspectRatio = 0; + this.buildingRow.width = 0; + }; + + /** + * Justify the building row, preparing it to + * + * @param isLastRow + * @returns a boolean to know if the row has been justified or not + */ + JustifiedGallery.prototype.prepareBuildingRow = function (isLastRow) { + var i, $entry, imgAspectRatio, newImgW, newImgH, justify = true; + var minHeight = 0; + var availableWidth = this.galleryWidth - 2 * this.border - ( + (this.buildingRow.entriesBuff.length - 1) * this.settings.margins); + var rowHeight = availableWidth / this.buildingRow.aspectRatio; + var defaultRowHeight = this.settings.rowHeight; + var justifiable = this.buildingRow.width / availableWidth > this.settings.justifyThreshold; + + //Skip the last row if we can't justify it and the lastRow == 'hide' + if (isLastRow && this.settings.lastRow === 'hide' && !justifiable) { + for (i = 0; i < this.buildingRow.entriesBuff.length; i++) { + $entry = this.buildingRow.entriesBuff[i]; + if (this.settings.cssAnimation) + $entry.removeClass('entry-visible'); + else { + $entry.stop().fadeTo(0, 0.1); + $entry.find('> img, > a > img').fadeTo(0, 0); + } + } + return -1; + } + + // With lastRow = nojustify, justify if is justificable (the images will not become too big) + if (isLastRow && !justifiable && this.settings.lastRow !== 'justify' && this.settings.lastRow !== 'hide') { + justify = false; + + if (this.rows > 0) { + defaultRowHeight = (this.offY - this.border - this.settings.margins * this.rows) / this.rows; + justify = defaultRowHeight * this.buildingRow.aspectRatio / availableWidth > this.settings.justifyThreshold; + } + } + + for (i = 0; i < this.buildingRow.entriesBuff.length; i++) { + $entry = this.buildingRow.entriesBuff[i]; + imgAspectRatio = $entry.data('jg.width') / $entry.data('jg.height'); + + if (justify) { + newImgW = (i === this.buildingRow.entriesBuff.length - 1) ? availableWidth : rowHeight * imgAspectRatio; + newImgH = rowHeight; + } else { + newImgW = defaultRowHeight * imgAspectRatio; + newImgH = defaultRowHeight; + } + + availableWidth -= Math.round(newImgW); + $entry.data('jg.jwidth', Math.round(newImgW)); + $entry.data('jg.jheight', Math.ceil(newImgH)); + if (i === 0 || minHeight > newImgH) minHeight = newImgH; + } + + this.buildingRow.height = minHeight; + return justify; + }; + + /** + * Flush a row: justify it, modify the gallery height accordingly to the row height + * + * @param isLastRow + */ + JustifiedGallery.prototype.flushRow = function (isLastRow) { + var settings = this.settings; + var $entry, buildingRowRes, offX = this.border, i; + + buildingRowRes = this.prepareBuildingRow(isLastRow); + if (isLastRow && settings.lastRow === 'hide' && buildingRowRes === -1) { + this.clearBuildingRow(); + return; + } + + if(this.maxRowHeight) { + if(this.maxRowHeight < this.buildingRow.height) this.buildingRow.height = this.maxRowHeight; + } + + //Align last (unjustified) row + if (isLastRow && (settings.lastRow === 'center' || settings.lastRow === 'right')) { + var availableWidth = this.galleryWidth - 2 * this.border - (this.buildingRow.entriesBuff.length - 1) * settings.margins; + + for (i = 0; i < this.buildingRow.entriesBuff.length; i++) { + $entry = this.buildingRow.entriesBuff[i]; + availableWidth -= $entry.data('jg.jwidth'); + } + + if (settings.lastRow === 'center') + offX += availableWidth / 2; + else if (settings.lastRow === 'right') + offX += availableWidth; + } + + var lastEntryIdx = this.buildingRow.entriesBuff.length - 1; + for (i = 0; i <= lastEntryIdx; i++) { + $entry = this.buildingRow.entriesBuff[ this.settings.rtl ? lastEntryIdx - i : i ]; + this.displayEntry($entry, offX, this.offY, $entry.data('jg.jwidth'), $entry.data('jg.jheight'), this.buildingRow.height); + offX += $entry.data('jg.jwidth') + settings.margins; + } + + //Gallery Height + this.galleryHeightToSet = this.offY + this.buildingRow.height + this.border; + this.setGalleryTempHeight(this.galleryHeightToSet + this.getSpinnerHeight()); + + if (!isLastRow || (this.buildingRow.height <= settings.rowHeight && buildingRowRes)) { + //Ready for a new row + this.offY += this.buildingRow.height + settings.margins; + this.rows += 1; + this.clearBuildingRow(); + this.settings.triggerEvent.call(this, 'jg.rowflush'); + } + }; + + + // Scroll position not restoring: https://github.com/miromannino/Justified-Gallery/issues/221 + var galleryPrevStaticHeight = 0; + + JustifiedGallery.prototype.rememberGalleryHeight = function () { + galleryPrevStaticHeight = this.$gallery.height(); + this.$gallery.height(galleryPrevStaticHeight); + }; + + // grow only + JustifiedGallery.prototype.setGalleryTempHeight = function (height) { + galleryPrevStaticHeight = Math.max(height, galleryPrevStaticHeight); + this.$gallery.height(galleryPrevStaticHeight); + }; + + JustifiedGallery.prototype.setGalleryFinalHeight = function (height) { + galleryPrevStaticHeight = height; + this.$gallery.height(height); + }; + + /** + * @returns {boolean} a boolean saying if the scrollbar is active or not + */ + function hasScrollBar() { + return $("body").height() > $(window).height(); + } + + /** + * Checks the width of the gallery container, to know if a new justification is needed + */ + JustifiedGallery.prototype.checkWidth = function () { + this.checkWidthIntervalId = setInterval($.proxy(function () { + + // if the gallery is not currently visible, abort. + if (!this.$gallery.is(":visible")) return; + + var galleryWidth = parseFloat(this.$gallery.width()); + if (hasScrollBar() === this.scrollBarOn) { + if (Math.abs(galleryWidth - this.galleryWidth) > this.settings.refreshSensitivity) { + this.galleryWidth = galleryWidth; + this.rewind(); + + this.rememberGalleryHeight(); + + // Restart to analyze + this.startImgAnalyzer(true); + } + } else { + this.scrollBarOn = hasScrollBar(); + this.galleryWidth = galleryWidth; + } + }, this), this.settings.refreshTime); + }; + + /** + * @returns {boolean} a boolean saying if the spinner is active or not + */ + JustifiedGallery.prototype.isSpinnerActive = function () { + return this.spinner.intervalId !== null; + }; + + /** + * @returns {int} the spinner height + */ + JustifiedGallery.prototype.getSpinnerHeight = function () { + return this.spinner.$el.innerHeight(); + }; + + /** + * Stops the spinner animation and modify the gallery height to exclude the spinner + */ + JustifiedGallery.prototype.stopLoadingSpinnerAnimation = function () { + clearInterval(this.spinner.intervalId); + this.spinner.intervalId = null; + this.setGalleryTempHeight(this.$gallery.height() - this.getSpinnerHeight()); + this.spinner.$el.detach(); + }; + + /** + * Starts the spinner animation + */ + JustifiedGallery.prototype.startLoadingSpinnerAnimation = function () { + var spinnerContext = this.spinner; + var $spinnerPoints = spinnerContext.$el.find('span'); + clearInterval(spinnerContext.intervalId); + this.$gallery.append(spinnerContext.$el); + this.setGalleryTempHeight(this.offY + this.buildingRow.height + this.getSpinnerHeight()); + spinnerContext.intervalId = setInterval(function () { + if (spinnerContext.phase < $spinnerPoints.length) { + $spinnerPoints.eq(spinnerContext.phase).fadeTo(spinnerContext.timeSlot, 1); + } else { + $spinnerPoints.eq(spinnerContext.phase - $spinnerPoints.length).fadeTo(spinnerContext.timeSlot, 0); + } + spinnerContext.phase = (spinnerContext.phase + 1) % ($spinnerPoints.length * 2); + }, spinnerContext.timeSlot); + }; + + /** + * Rewind the image analysis to start from the first entry. + */ + JustifiedGallery.prototype.rewind = function () { + this.lastFetchedEntry = null; + this.lastAnalyzedIndex = -1; + this.offY = this.border; + this.rows = 0; + this.clearBuildingRow(); + }; + + /** + * Update the entries searching it from the justified gallery HTML element + * + * @param norewind if norewind only the new entries will be changed (i.e. randomized, sorted or filtered) + * @returns {boolean} true if some entries has been founded + */ + JustifiedGallery.prototype.updateEntries = function (norewind) { + var newEntries; + + if (norewind && this.lastFetchedEntry != null) { + newEntries = $(this.lastFetchedEntry).nextAll(this.settings.selector).toArray(); + } else { + this.entries = []; + newEntries = this.$gallery.children(this.settings.selector).toArray(); + } + + if (newEntries.length > 0) { + + // Sort or randomize + if ($.isFunction(this.settings.sort)) { + newEntries = this.sortArray(newEntries); + } else if (this.settings.randomize) { + newEntries = this.shuffleArray(newEntries); + } + this.lastFetchedEntry = newEntries[newEntries.length - 1]; + + // Filter + if (this.settings.filter) { + newEntries = this.filterArray(newEntries); + } else { + this.resetFilters(newEntries); + } + + } + + this.entries = this.entries.concat(newEntries); + return true; + }; + + /** + * Apply the entries order to the DOM, iterating the entries and appending the images + * + * @param entries the entries that has been modified and that must be re-ordered in the DOM + */ + JustifiedGallery.prototype.insertToGallery = function (entries) { + var that = this; + $.each(entries, function () { + $(this).appendTo(that.$gallery); + }); + }; + + /** + * Shuffle the array using the Fisher-Yates shuffle algorithm + * + * @param a the array to shuffle + * @return the shuffled array + */ + JustifiedGallery.prototype.shuffleArray = function (a) { + var i, j, temp; + for (i = a.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + temp = a[i]; + a[i] = a[j]; + a[j] = temp; + } + this.insertToGallery(a); + return a; + }; + + /** + * Sort the array using settings.comparator as comparator + * + * @param a the array to sort (it is sorted) + * @return the sorted array + */ + JustifiedGallery.prototype.sortArray = function (a) { + a.sort(this.settings.sort); + this.insertToGallery(a); + return a; + }; + + /** + * Reset the filters removing the 'jg-filtered' class from all the entries + * + * @param a the array to reset + */ + JustifiedGallery.prototype.resetFilters = function (a) { + for (var i = 0; i < a.length; i++) $(a[i]).removeClass('jg-filtered'); + }; + + /** + * Filter the entries considering theirs classes (if a string has been passed) or using a function for filtering. + * + * @param a the array to filter + * @return the filtered array + */ + JustifiedGallery.prototype.filterArray = function (a) { + var settings = this.settings; + if ($.type(settings.filter) === 'string') { + // Filter only keeping the entries passed in the string + return a.filter(function (el) { + var $el = $(el); + if ($el.is(settings.filter)) { + $el.removeClass('jg-filtered'); + return true; + } else { + $el.addClass('jg-filtered').removeClass('jg-visible'); + return false; + } + }); + } else if ($.isFunction(settings.filter)) { + // Filter using the passed function + var filteredArr = a.filter(settings.filter); + for (var i = 0; i < a.length; i++) { + if (filteredArr.indexOf(a[i]) === -1) { + $(a[i]).addClass('jg-filtered').removeClass('jg-visible'); + } else { + $(a[i]).removeClass('jg-filtered'); + } + } + return filteredArr; + } + }; + + /** + * Destroy the Justified Gallery instance. + * + * It clears all the css properties added in the style attributes. We doesn't backup the original + * values for those css attributes, because it costs (performance) and because in general one + * shouldn't use the style attribute for an uniform set of images (where we suppose the use of + * classes). Creating a backup is also difficult because JG could be called multiple times and + * with different style attributes. + */ + JustifiedGallery.prototype.destroy = function () { + clearInterval(this.checkWidthIntervalId); + + $.each(this.entries, $.proxy(function(_, entry) { + var $entry = $(entry); + + // Reset entry style + $entry.css('width', ''); + $entry.css('height', ''); + $entry.css('top', ''); + $entry.css('left', ''); + $entry.data('jg.loaded', undefined); + $entry.removeClass('jg-entry'); + + // Reset image style + var $img = this.imgFromEntry($entry); + $img.css('width', ''); + $img.css('height', ''); + $img.css('margin-left', ''); + $img.css('margin-top', ''); + $img.attr('src', $img.data('jg.originalSrc')); + $img.data('jg.originalSrc', undefined); + + // Remove caption + this.removeCaptionEventsHandlers($entry); + var $caption = this.captionFromEntry($entry); + if ($entry.data('jg.createdCaption')) { + // remove also the caption element (if created by jg) + $entry.data('jg.createdCaption', undefined); + if ($caption !== null) $caption.remove(); + } else { + if ($caption !== null) $caption.fadeTo(0, 1); + } + + }, this)); + + this.$gallery.css('height', ''); + this.$gallery.removeClass('justified-gallery'); + this.$gallery.data('jg.controller', undefined); + }; + + /** + * Analyze the images and builds the rows. It returns if it found an image that is not loaded. + * + * @param isForResize if the image analyzer is called for resizing or not, to call a different callback at the end + */ + JustifiedGallery.prototype.analyzeImages = function (isForResize) { + for (var i = this.lastAnalyzedIndex + 1; i < this.entries.length; i++) { + var $entry = $(this.entries[i]); + if ($entry.data('jg.loaded') === true || $entry.data('jg.loaded') === 'skipped') { + var availableWidth = this.galleryWidth - 2 * this.border - ( + (this.buildingRow.entriesBuff.length - 1) * this.settings.margins); + var imgAspectRatio = $entry.data('jg.width') / $entry.data('jg.height'); + if (availableWidth / (this.buildingRow.aspectRatio + imgAspectRatio) < this.settings.rowHeight) { + this.flushRow(false); + + if(++this.yield.flushed >= this.yield.every) { + this.startImgAnalyzer(isForResize); + return; + } + } + + this.buildingRow.entriesBuff.push($entry); + this.buildingRow.aspectRatio += imgAspectRatio; + this.buildingRow.width += imgAspectRatio * this.settings.rowHeight; + this.lastAnalyzedIndex = i; + + } else if ($entry.data('jg.loaded') !== 'error') { + return; + } + } + + // Last row flush (the row is not full) + if (this.buildingRow.entriesBuff.length > 0) this.flushRow(true); + + if (this.isSpinnerActive()) { + this.stopLoadingSpinnerAnimation(); + } + + /* Stop, if there is, the timeout to start the analyzeImages. + This is because an image can be set loaded, and the timeout can be set, + but this image can be analyzed yet. + */ + this.stopImgAnalyzerStarter(); + + //On complete callback + this.settings.triggerEvent.call(this, isForResize ? 'jg.resize' : 'jg.complete'); + this.setGalleryFinalHeight(this.galleryHeightToSet); + }; + + /** + * Stops any ImgAnalyzer starter (that has an assigned timeout) + */ + JustifiedGallery.prototype.stopImgAnalyzerStarter = function () { + this.yield.flushed = 0; + if (this.imgAnalyzerTimeout !== null) { + clearTimeout(this.imgAnalyzerTimeout); + this.imgAnalyzerTimeout = null; + } + }; + + /** + * Starts the image analyzer. It is not immediately called to let the browser to update the view + * + * @param isForResize specifies if the image analyzer must be called for resizing or not + */ + JustifiedGallery.prototype.startImgAnalyzer = function (isForResize) { + var that = this; + this.stopImgAnalyzerStarter(); + this.imgAnalyzerTimeout = setTimeout(function () { + that.analyzeImages(isForResize); + }, 0.001); // we can't start it immediately due to a IE different behaviour + }; + + /** + * Checks if the image is loaded or not using another image object. We cannot use the 'complete' image property, + * because some browsers, with a 404 set complete = true. + * + * @param imageSrc the image src to load + * @param onLoad callback that is called when the image has been loaded + * @param onError callback that is called in case of an error + */ + JustifiedGallery.prototype.onImageEvent = function (imageSrc, onLoad, onError) { + if (!onLoad && !onError) return; + + var memImage = new Image(); + var $memImage = $(memImage); + if (onLoad) { + $memImage.one('load', function () { + $memImage.off('load error'); + onLoad(memImage); + }); + } + if (onError) { + $memImage.one('error', function() { + $memImage.off('load error'); + onError(memImage); + }); + } + memImage.src = imageSrc; + }; + + /** + * Init of Justified Gallery controlled + * It analyzes all the entries starting theirs loading and calling the image analyzer (that works with loaded images) + */ + JustifiedGallery.prototype.init = function () { + var imagesToLoad = false, skippedImages = false, that = this; + $.each(this.entries, function (index, entry) { + var $entry = $(entry); + var $image = that.imgFromEntry($entry); + + $entry.addClass('jg-entry'); + + if ($entry.data('jg.loaded') !== true && $entry.data('jg.loaded') !== 'skipped') { + + // Link Rel global overwrite + if (that.settings.rel !== null) $entry.attr('rel', that.settings.rel); + + // Link Target global overwrite + if (that.settings.target !== null) $entry.attr('target', that.settings.target); + + if ($image !== null) { + + // Image src + var imageSrc = that.extractImgSrcFromImage($image); + $image.attr('src', imageSrc); + + /* If we have the height and the width, we don't wait that the image is loaded, but we start directly + * with the justification */ + if (that.settings.waitThumbnailsLoad === false) { + var width = parseFloat($image.attr('width')); + var height = parseFloat($image.attr('height')); + if (!isNaN(width) && !isNaN(height)) { + $entry.data('jg.width', width); + $entry.data('jg.height', height); + $entry.data('jg.loaded', 'skipped'); + skippedImages = true; + that.startImgAnalyzer(false); + return true; // continue + } + } + + $entry.data('jg.loaded', false); + imagesToLoad = true; + + // Spinner start + if (!that.isSpinnerActive()) that.startLoadingSpinnerAnimation(); + + that.onImageEvent(imageSrc, function (loadImg) { // image loaded + $entry.data('jg.width', loadImg.width); + $entry.data('jg.height', loadImg.height); + $entry.data('jg.loaded', true); + that.startImgAnalyzer(false); + }, function () { // image load error + $entry.data('jg.loaded', 'error'); + that.startImgAnalyzer(false); + }); + + } else { + $entry.data('jg.loaded', true); + $entry.data('jg.width', $entry.width() | parseFloat($entry.css('width')) | 1); + $entry.data('jg.height', $entry.height() | parseFloat($entry.css('height')) | 1); + } + + } + + }); + + if (!imagesToLoad && !skippedImages) this.startImgAnalyzer(false); + this.checkWidth(); + }; + + /** + * Checks that it is a valid number. If a string is passed it is converted to a number + * + * @param settingContainer the object that contains the setting (to allow the conversion) + * @param settingName the setting name + */ + JustifiedGallery.prototype.checkOrConvertNumber = function (settingContainer, settingName) { + if ($.type(settingContainer[settingName]) === 'string') { + settingContainer[settingName] = parseFloat(settingContainer[settingName]); + } + + if ($.type(settingContainer[settingName]) === 'number') { + if (isNaN(settingContainer[settingName])) throw 'invalid number for ' + settingName; + } else { + throw settingName + ' must be a number'; + } + }; + + /** + * Checks the sizeRangeSuffixes and, if necessary, converts + * its keys from string (e.g. old settings with 'lt100') to int. + */ + JustifiedGallery.prototype.checkSizeRangesSuffixes = function () { + if ($.type(this.settings.sizeRangeSuffixes) !== 'object') { + throw 'sizeRangeSuffixes must be defined and must be an object'; + } + + var suffixRanges = []; + for (var rangeIdx in this.settings.sizeRangeSuffixes) { + if (this.settings.sizeRangeSuffixes.hasOwnProperty(rangeIdx)) suffixRanges.push(rangeIdx); + } + + var newSizeRngSuffixes = {0: ''}; + for (var i = 0; i < suffixRanges.length; i++) { + if ($.type(suffixRanges[i]) === 'string') { + try { + var numIdx = parseInt(suffixRanges[i].replace(/^[a-z]+/, ''), 10); + newSizeRngSuffixes[numIdx] = this.settings.sizeRangeSuffixes[suffixRanges[i]]; + } catch (e) { + throw 'sizeRangeSuffixes keys must contains correct numbers (' + e + ')'; + } + } else { + newSizeRngSuffixes[suffixRanges[i]] = this.settings.sizeRangeSuffixes[suffixRanges[i]]; + } + } + + this.settings.sizeRangeSuffixes = newSizeRngSuffixes; + }; + + /** + * check and convert the maxRowHeight setting + * requires rowHeight to be already set + * TODO: should be always called when only rowHeight is changed + * @return number or null + */ + JustifiedGallery.prototype.retrieveMaxRowHeight = function () { + var newMaxRowHeight = null; + var rowHeight = this.settings.rowHeight; + + if ($.type(this.settings.maxRowHeight) === 'string') { + if (this.settings.maxRowHeight.match(/^[0-9]+%$/)) { + newMaxRowHeight = rowHeight * parseFloat(this.settings.maxRowHeight.match(/^([0-9]+)%$/)[1]) / 100; + } else { + newMaxRowHeight = parseFloat(this.settings.maxRowHeight); + } + } else if ($.type(this.settings.maxRowHeight) === 'number') { + newMaxRowHeight = this.settings.maxRowHeight; + } else if (this.settings.maxRowHeight === false || this.settings.maxRowHeight == null) { + return null; + } else { + throw 'maxRowHeight must be a number or a percentage'; + } + + // check if the converted value is not a number + if (isNaN(newMaxRowHeight)) throw 'invalid number for maxRowHeight'; + + // check values, maxRowHeight must be >= rowHeight + if (newMaxRowHeight < rowHeight) newMaxRowHeight = rowHeight; + + return newMaxRowHeight; + }; + + /** + * Checks the settings + */ + JustifiedGallery.prototype.checkSettings = function () { + this.checkSizeRangesSuffixes(); + + this.checkOrConvertNumber(this.settings, 'rowHeight'); + this.checkOrConvertNumber(this.settings, 'margins'); + this.checkOrConvertNumber(this.settings, 'border'); + + var lastRowModes = [ + 'justify', + 'nojustify', + 'left', + 'center', + 'right', + 'hide' + ]; + if (lastRowModes.indexOf(this.settings.lastRow) === -1) { + throw 'lastRow must be one of: ' + lastRowModes.join(', '); + } + + this.checkOrConvertNumber(this.settings, 'justifyThreshold'); + if (this.settings.justifyThreshold < 0 || this.settings.justifyThreshold > 1) { + throw 'justifyThreshold must be in the interval [0,1]'; + } + if ($.type(this.settings.cssAnimation) !== 'boolean') { + throw 'cssAnimation must be a boolean'; + } + + if ($.type(this.settings.captions) !== 'boolean') throw 'captions must be a boolean'; + this.checkOrConvertNumber(this.settings.captionSettings, 'animationDuration'); + + this.checkOrConvertNumber(this.settings.captionSettings, 'visibleOpacity'); + if (this.settings.captionSettings.visibleOpacity < 0 || + this.settings.captionSettings.visibleOpacity > 1) { + throw 'captionSettings.visibleOpacity must be in the interval [0, 1]'; + } + + this.checkOrConvertNumber(this.settings.captionSettings, 'nonVisibleOpacity'); + if (this.settings.captionSettings.nonVisibleOpacity < 0 || + this.settings.captionSettings.nonVisibleOpacity > 1) { + throw 'captionSettings.nonVisibleOpacity must be in the interval [0, 1]'; + } + + this.checkOrConvertNumber(this.settings, 'imagesAnimationDuration'); + this.checkOrConvertNumber(this.settings, 'refreshTime'); + this.checkOrConvertNumber(this.settings, 'refreshSensitivity'); + if ($.type(this.settings.randomize) !== 'boolean') throw 'randomize must be a boolean'; + if ($.type(this.settings.selector) !== 'string') throw 'selector must be a string'; + + if (this.settings.sort !== false && !$.isFunction(this.settings.sort)) { + throw 'sort must be false or a comparison function'; + } + + if (this.settings.filter !== false && !$.isFunction(this.settings.filter) && + $.type(this.settings.filter) !== 'string') { + throw 'filter must be false, a string or a filter function'; + } + }; + + /** + * It brings all the indexes from the sizeRangeSuffixes and it orders them. They are then sorted and returned. + * @returns {Array} sorted suffix ranges + */ + JustifiedGallery.prototype.retrieveSuffixRanges = function () { + var suffixRanges = []; + for (var rangeIdx in this.settings.sizeRangeSuffixes) { + if (this.settings.sizeRangeSuffixes.hasOwnProperty(rangeIdx)) suffixRanges.push(parseInt(rangeIdx, 10)); + } + suffixRanges.sort(function (a, b) { return a > b ? 1 : a < b ? -1 : 0; }); + return suffixRanges; + }; + + /** + * Update the existing settings only changing some of them + * + * @param newSettings the new settings (or a subgroup of them) + */ + JustifiedGallery.prototype.updateSettings = function (newSettings) { + // In this case Justified Gallery has been called again changing only some options + this.settings = $.extend({}, this.settings, newSettings); + this.checkSettings(); + + // As reported in the settings: negative value = same as margins, 0 = disabled + this.border = this.settings.border >= 0 ? this.settings.border : this.settings.margins; + + this.maxRowHeight = this.retrieveMaxRowHeight(); + this.suffixRanges = this.retrieveSuffixRanges(); + }; + + JustifiedGallery.prototype.defaults = { + sizeRangeSuffixes: { }, /* e.g. Flickr configuration + { + 100: '_t', // used when longest is less than 100px + 240: '_m', // used when longest is between 101px and 240px + 320: '_n', // ... + 500: '', + 640: '_z', + 1024: '_b' // used as else case because it is the last + } + */ + thumbnailPath: undefined, /* If defined, sizeRangeSuffixes is not used, and this function is used to determine the + path relative to a specific thumbnail size. The function should accept respectively three arguments: + current path, width and height */ + rowHeight: 120, // required? required to be > 0? + maxRowHeight: false, // false or negative value to deactivate. Positive number to express the value in pixels, + // A string '[0-9]+%' to express in percentage (e.g. 300% means that the row height + // can't exceed 3 * rowHeight) + margins: 1, + border: -1, // negative value = same as margins, 0 = disabled, any other value to set the border + + lastRow: 'nojustify', // … which is the same as 'left', or can be 'justify', 'center', 'right' or 'hide' + + justifyThreshold: 0.90, /* if row width / available space > 0.90 it will be always justified + * (i.e. lastRow setting is not considered) */ + waitThumbnailsLoad: true, + captions: true, + cssAnimation: true, + imagesAnimationDuration: 500, // ignored with css animations + captionSettings: { // ignored with css animations + animationDuration: 500, + visibleOpacity: 0.7, + nonVisibleOpacity: 0.0 + }, + rel: null, // rewrite the rel of each analyzed links + target: null, // rewrite the target of all links + extension: /\.[^.\\/]+$/, // regexp to capture the extension of an image + refreshTime: 200, // time interval (in ms) to check if the page changes its width + refreshSensitivity: 0, // change in width allowed (in px) without re-building the gallery + randomize: false, + rtl: false, // right-to-left mode + sort: false, /* + - false: to do not sort + - function: to sort them using the function as comparator (see Array.prototype.sort()) + */ + filter: false, /* + - false, null or undefined: for a disabled filter + - a string: an entry is kept if entry.is(filter string) returns true + see jQuery's .is() function for further information + - a function: invoked with arguments (entry, index, array). Return true to keep the entry, false otherwise. + It follows the specifications of the Array.prototype.filter() function of JavaScript. + */ + selector: 'a, div:not(.spinner)', // The selector that is used to know what are the entries of the gallery + imgSelector: '> img, > a > img', // The selector that is used to know what are the images of each entry + triggerEvent: function (event) { // This is called to trigger events, the default behavior is to call $.trigger + this.$gallery.trigger(event); // Consider that 'this' is this set to the JustifiedGallery object, so it can + } // access to fields such as $gallery, useful to trigger events with jQuery. + }; + + /** + * Justified Gallery plugin for jQuery + * + * Events + * - jg.complete : called when all the gallery has been created + * - jg.resize : called when the gallery has been resized + * - jg.rowflush : when a new row appears + * + * @param arg the action (or the settings) passed when the plugin is called + * @returns {*} the object itself + */ + $.fn.justifiedGallery = function (arg) { + return this.each(function (index, gallery) { + + var $gallery = $(gallery); + $gallery.addClass('justified-gallery'); + + var controller = $gallery.data('jg.controller'); + if (typeof controller === 'undefined') { + // Create controller and assign it to the object data + if (typeof arg !== 'undefined' && arg !== null && $.type(arg) !== 'object') { + if (arg === 'destroy') return; // Just a call to an unexisting object + throw 'The argument must be an object'; + } + controller = new JustifiedGallery($gallery, $.extend({}, JustifiedGallery.prototype.defaults, arg)); + $gallery.data('jg.controller', controller); + } else if (arg === 'norewind') { + // In this case we don't rewind: we analyze only the latest images (e.g. to complete the last unfinished row + // ... left to be more readable + } else if (arg === 'destroy') { + controller.destroy(); + return; + } else { + // In this case Justified Gallery has been called again changing only some options + controller.updateSettings(arg); + controller.rewind(); + } + + // Update the entries list + if (!controller.updateEntries(arg === 'norewind')) return; + + // Init justified gallery + controller.init(); + + }); + }; + +})); \ No newline at end of file diff --git a/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/justified_gallery/jquery.justifiedGallery.min.js b/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/justified_gallery/jquery.justifiedGallery.min.js new file mode 100644 index 0000000..62d8347 --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/justified_gallery/jquery.justifiedGallery.min.js @@ -0,0 +1 @@ +(function(t){"function"==typeof define&&define.amd?define(["jquery"],t):"object"==typeof module&&module.exports?module.exports=function(i,e){return void 0===e&&(e="undefined"!=typeof window?require("jquery"):require("jquery")(i)),t(e),e}:t(jQuery)})(function(t){function i(){return t("body").height()>t(window).height()}var e=function(i,e){this.settings=e,this.checkSettings(),this.imgAnalyzerTimeout=null,this.entries=null,this.buildingRow={entriesBuff:[],width:0,height:0,aspectRatio:0},this.lastFetchedEntry=null,this.lastAnalyzedIndex=-1,this.yield={every:2,flushed:0},this.border=e.border>=0?e.border:e.margins,this.maxRowHeight=this.retrieveMaxRowHeight(),this.suffixRanges=this.retrieveSuffixRanges(),this.offY=this.border,this.rows=0,this.spinner={phase:0,timeSlot:150,$el:t('
'),intervalId:null},this.scrollBarOn=!1,this.checkWidthIntervalId=null,this.galleryWidth=i.width(),this.$gallery=i};e.prototype.getSuffix=function(t,i){var e,s;for(e=t>i?t:i,s=0;s .caption");return 0===i.length?null:i},e.prototype.displayEntry=function(i,e,s,n,r,o){i.width(n),i.height(o),i.css("top",s),i.css("left",e);var a=this.imgFromEntry(i);if(null!==a){a.css("width",n),a.css("height",r),a.css("margin-left",-n/2),a.css("margin-top",-r/2);var h=a.attr("src"),l=this.newSrc(h,n,r,a[0]);a.one("error",function(){a.attr("src",a.data("jg.originalSrc"))});var g=function(){h!==l&&a.attr("src",l)};"skipped"===i.data("jg.loaded")?this.onImageEvent(h,t.proxy(function(){this.showImg(i,g),i.data("jg.loaded",!0)},this)):this.showImg(i,g)}else this.showImg(i);this.displayEntryCaption(i)},e.prototype.displayEntryCaption=function(i){var e=this.imgFromEntry(i);if(null!==e&&this.settings.captions){var s=this.captionFromEntry(i);if(null===s){var n=e.attr("alt");this.isValidCaption(n)||(n=i.attr("title")),this.isValidCaption(n)&&(s=t('
'+n+"
"),i.append(s),i.data("jg.createdCaption",!0))}null!==s&&(this.settings.cssAnimation||s.stop().fadeTo(0,this.settings.captionSettings.nonVisibleOpacity),this.addCaptionEventsHandlers(i))}else this.removeCaptionEventsHandlers(i)},e.prototype.isValidCaption=function(t){return"undefined"!=typeof t&&t.length>0},e.prototype.onEntryMouseEnterForCaption=function(i){var e=this.captionFromEntry(t(i.currentTarget));this.settings.cssAnimation?e.addClass("caption-visible").removeClass("caption-hidden"):e.stop().fadeTo(this.settings.captionSettings.animationDuration,this.settings.captionSettings.visibleOpacity)},e.prototype.onEntryMouseLeaveForCaption=function(i){var e=this.captionFromEntry(t(i.currentTarget));this.settings.cssAnimation?e.removeClass("caption-visible").removeClass("caption-hidden"):e.stop().fadeTo(this.settings.captionSettings.animationDuration,this.settings.captionSettings.nonVisibleOpacity)},e.prototype.addCaptionEventsHandlers=function(i){var e=i.data("jg.captionMouseEvents");"undefined"==typeof e&&(e={mouseenter:t.proxy(this.onEntryMouseEnterForCaption,this),mouseleave:t.proxy(this.onEntryMouseLeaveForCaption,this)},i.on("mouseenter",void 0,void 0,e.mouseenter),i.on("mouseleave",void 0,void 0,e.mouseleave),i.data("jg.captionMouseEvents",e))},e.prototype.removeCaptionEventsHandlers=function(t){var i=t.data("jg.captionMouseEvents");"undefined"!=typeof i&&(t.off("mouseenter",void 0,i.mouseenter),t.off("mouseleave",void 0,i.mouseleave),t.removeData("jg.captionMouseEvents"))},e.prototype.clearBuildingRow=function(){this.buildingRow.entriesBuff=[],this.buildingRow.aspectRatio=0,this.buildingRow.width=0},e.prototype.prepareBuildingRow=function(t){var i,e,s,n,r,o=!0,a=0,h=this.galleryWidth-2*this.border-(this.buildingRow.entriesBuff.length-1)*this.settings.margins,l=h/this.buildingRow.aspectRatio,g=this.settings.rowHeight,f=this.buildingRow.width/h>this.settings.justifyThreshold;if(t&&"hide"===this.settings.lastRow&&!f){for(i=0;i img, > a > img").fadeTo(0,0));return-1}for(t&&!f&&"justify"!==this.settings.lastRow&&"hide"!==this.settings.lastRow&&(o=!1,this.rows>0&&(g=(this.offY-this.border-this.settings.margins*this.rows)/this.rows,o=g*this.buildingRow.aspectRatio/h>this.settings.justifyThreshold)),i=0;ir)&&(a=r);return this.buildingRow.height=a,o},e.prototype.flushRow=function(t){var i,e,s,n=this.settings,r=this.border;if(e=this.prepareBuildingRow(t),t&&"hide"===n.lastRow&&e===-1)return void this.clearBuildingRow();if(this.maxRowHeight&&this.maxRowHeightthis.settings.refreshSensitivity&&(this.galleryWidth=t,this.rewind(),this.rememberGalleryHeight(),this.startImgAnalyzer(!0)):(this.scrollBarOn=i(),this.galleryWidth=t)}},this),this.settings.refreshTime)},e.prototype.isSpinnerActive=function(){return null!==this.spinner.intervalId},e.prototype.getSpinnerHeight=function(){return this.spinner.$el.innerHeight()},e.prototype.stopLoadingSpinnerAnimation=function(){clearInterval(this.spinner.intervalId),this.spinner.intervalId=null,this.setGalleryTempHeight(this.$gallery.height()-this.getSpinnerHeight()),this.spinner.$el.detach()},e.prototype.startLoadingSpinnerAnimation=function(){var t=this.spinner,i=t.$el.find("span");clearInterval(t.intervalId),this.$gallery.append(t.$el),this.setGalleryTempHeight(this.offY+this.buildingRow.height+this.getSpinnerHeight()),t.intervalId=setInterval(function(){t.phase0&&(t.isFunction(this.settings.sort)?e=this.sortArray(e):this.settings.randomize&&(e=this.shuffleArray(e)),this.lastFetchedEntry=e[e.length-1],this.settings.filter?e=this.filterArray(e):this.resetFilters(e)),this.entries=this.entries.concat(e),!0},e.prototype.insertToGallery=function(i){var e=this;t.each(i,function(){t(this).appendTo(e.$gallery)})},e.prototype.shuffleArray=function(t){var i,e,s;for(i=t.length-1;i>0;i--)e=Math.floor(Math.random()*(i+1)),s=t[i],t[i]=t[e],t[e]=s;return this.insertToGallery(t),t},e.prototype.sortArray=function(t){return t.sort(this.settings.sort),this.insertToGallery(t),t},e.prototype.resetFilters=function(i){for(var e=0;e=this.yield.every))return void this.startImgAnalyzer(i);this.buildingRow.entriesBuff.push(s),this.buildingRow.aspectRatio+=r,this.buildingRow.width+=r*this.settings.rowHeight,this.lastAnalyzedIndex=e}else if("error"!==s.data("jg.loaded"))return}this.buildingRow.entriesBuff.length>0&&this.flushRow(!0),this.isSpinnerActive()&&this.stopLoadingSpinnerAnimation(),this.stopImgAnalyzerStarter(),this.settings.triggerEvent.call(this,i?"jg.resize":"jg.complete"),this.setGalleryFinalHeight(this.galleryHeightToSet)},e.prototype.stopImgAnalyzerStarter=function(){this.yield.flushed=0,null!==this.imgAnalyzerTimeout&&(clearTimeout(this.imgAnalyzerTimeout),this.imgAnalyzerTimeout=null)},e.prototype.startImgAnalyzer=function(t){var i=this;this.stopImgAnalyzerStarter(),this.imgAnalyzerTimeout=setTimeout(function(){i.analyzeImages(t)},.001)},e.prototype.onImageEvent=function(i,e,s){if(e||s){var n=new Image,r=t(n);e&&r.one("load",function(){r.off("load error"),e(n)}),s&&r.one("error",function(){r.off("load error"),s(n)}),n.src=i}},e.prototype.init=function(){var i=!1,e=!1,s=this;t.each(this.entries,function(n,r){var o=t(r),a=s.imgFromEntry(o);if(o.addClass("jg-entry"),o.data("jg.loaded")!==!0&&"skipped"!==o.data("jg.loaded"))if(null!==s.settings.rel&&o.attr("rel",s.settings.rel),null!==s.settings.target&&o.attr("target",s.settings.target),null!==a){var h=s.extractImgSrcFromImage(a);if(a.attr("src",h),s.settings.waitThumbnailsLoad===!1){var l=parseFloat(a.attr("width")),g=parseFloat(a.attr("height"));if(!isNaN(l)&&!isNaN(g))return o.data("jg.width",l),o.data("jg.height",g),o.data("jg.loaded","skipped"),e=!0,s.startImgAnalyzer(!1),!0}o.data("jg.loaded",!1),i=!0,s.isSpinnerActive()||s.startLoadingSpinnerAnimation(),s.onImageEvent(h,function(t){o.data("jg.width",t.width),o.data("jg.height",t.height),o.data("jg.loaded",!0),s.startImgAnalyzer(!1)},function(){o.data("jg.loaded","error"),s.startImgAnalyzer(!1)})}else o.data("jg.loaded",!0),o.data("jg.width",o.width()|parseFloat(o.css("width"))|1),o.data("jg.height",o.height()|parseFloat(o.css("height"))|1)}),i||e||this.startImgAnalyzer(!1),this.checkWidth()},e.prototype.checkOrConvertNumber=function(i,e){if("string"===t.type(i[e])&&(i[e]=parseFloat(i[e])),"number"!==t.type(i[e]))throw e+" must be a number";if(isNaN(i[e]))throw"invalid number for "+e},e.prototype.checkSizeRangesSuffixes=function(){if("object"!==t.type(this.settings.sizeRangeSuffixes))throw"sizeRangeSuffixes must be defined and must be an object";var i=[];for(var e in this.settings.sizeRangeSuffixes)this.settings.sizeRangeSuffixes.hasOwnProperty(e)&&i.push(e);for(var s={0:""},n=0;n1)throw"justifyThreshold must be in the interval [0,1]";if("boolean"!==t.type(this.settings.cssAnimation))throw"cssAnimation must be a boolean";if("boolean"!==t.type(this.settings.captions))throw"captions must be a boolean";if(this.checkOrConvertNumber(this.settings.captionSettings,"animationDuration"),this.checkOrConvertNumber(this.settings.captionSettings,"visibleOpacity"),this.settings.captionSettings.visibleOpacity<0||this.settings.captionSettings.visibleOpacity>1)throw"captionSettings.visibleOpacity must be in the interval [0, 1]";if(this.checkOrConvertNumber(this.settings.captionSettings,"nonVisibleOpacity"),this.settings.captionSettings.nonVisibleOpacity<0||this.settings.captionSettings.nonVisibleOpacity>1)throw"captionSettings.nonVisibleOpacity must be in the interval [0, 1]";if(this.checkOrConvertNumber(this.settings,"imagesAnimationDuration"),this.checkOrConvertNumber(this.settings,"refreshTime"),this.checkOrConvertNumber(this.settings,"refreshSensitivity"),"boolean"!==t.type(this.settings.randomize))throw"randomize must be a boolean";if("string"!==t.type(this.settings.selector))throw"selector must be a string";if(this.settings.sort!==!1&&!t.isFunction(this.settings.sort))throw"sort must be false or a comparison function";if(this.settings.filter!==!1&&!t.isFunction(this.settings.filter)&&"string"!==t.type(this.settings.filter))throw"filter must be false, a string or a filter function"},e.prototype.retrieveSuffixRanges=function(){var t=[];for(var i in this.settings.sizeRangeSuffixes)this.settings.sizeRangeSuffixes.hasOwnProperty(i)&&t.push(parseInt(i,10));return t.sort(function(t,i){return t>i?1:t=0?this.settings.border:this.settings.margins,this.maxRowHeight=this.retrieveMaxRowHeight(),this.suffixRanges=this.retrieveSuffixRanges()},e.prototype.defaults={sizeRangeSuffixes:{},thumbnailPath:void 0,rowHeight:120,maxRowHeight:!1,margins:1,border:-1,lastRow:"nojustify",justifyThreshold:.9,waitThumbnailsLoad:!0,captions:!0,cssAnimation:!0,imagesAnimationDuration:500,captionSettings:{animationDuration:500,visibleOpacity:.7,nonVisibleOpacity:0},rel:null,target:null,extension:/\.[^.\\\/]+$/,refreshTime:200,refreshSensitivity:0,randomize:!1,rtl:!1,sort:!1,filter:!1,selector:"a, div:not(.spinner)",imgSelector:"> img, > a > img",triggerEvent:function(t){this.$gallery.trigger(t)}},t.fn.justifiedGallery=function(i){return this.each(function(s,n){var r=t(n);r.addClass("justified-gallery");var o=r.data("jg.controller");if("undefined"==typeof o){if("undefined"!=typeof i&&null!==i&&"object"!==t.type(i)){if("destroy"===i)return;throw"The argument must be an object"}o=new e(r,t.extend({},e.prototype.defaults,i)),r.data("jg.controller",o)}else if("norewind"===i);else{if("destroy"===i)return void o.destroy();o.updateSettings(i),o.rewind()}o.updateEntries("norewind"===i)&&o.init()})}}); \ No newline at end of file diff --git a/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/justified_gallery/justifiedGallery.css b/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/justified_gallery/justifiedGallery.css new file mode 100644 index 0000000..6743cb7 --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/justified_gallery/justifiedGallery.css @@ -0,0 +1,96 @@ +/*! + * justifiedGallery - v3.7.0 + * http://miromannino.github.io/Justified-Gallery/ + * Copyright (c) 2018 Miro Mannino + * Licensed under the MIT license. + */ +.justified-gallery { + width: 100%; + position: relative; + overflow: hidden; +} +.justified-gallery > a, +.justified-gallery > div, +.justified-gallery > figure { + position: absolute; + display: inline-block; + overflow: hidden; + /* background: #888888; To have gray placeholders while the gallery is loading with waitThumbnailsLoad = false */ + filter: "alpha(opacity=10)"; + opacity: 0.1; + margin: 0; + padding: 0; +} +.justified-gallery > a > img, +.justified-gallery > div > img, +.justified-gallery > figure > img, +.justified-gallery > a > a > img, +.justified-gallery > div > a > img, +.justified-gallery > figure > a > img { + position: absolute; + top: 50%; + left: 50%; + margin: 0; + padding: 0; + border: none; + filter: "alpha(opacity=0)"; + opacity: 0; +} +.justified-gallery > a > .caption, +.justified-gallery > div > .caption, +.justified-gallery > figure > .caption { + display: none; + position: absolute; + bottom: 0; + padding: 5px; + background-color: #000000; + left: 0; + right: 0; + margin: 0; + color: white; + font-size: 12px; + font-weight: 300; + font-family: sans-serif; +} +.justified-gallery > a > .caption.caption-visible, +.justified-gallery > div > .caption.caption-visible, +.justified-gallery > figure > .caption.caption-visible { + display: initial; + filter: "alpha(opacity=70)"; + opacity: 0.7; + transition: opacity 500ms ease-in; +} +.justified-gallery > .entry-visible { + filter: "alpha(opacity=100)"; + opacity: 1; + background: none; +} +.justified-gallery > .entry-visible > img, +.justified-gallery > .entry-visible > a > img { + filter: "alpha(opacity=100)"; + opacity: 1; + transition: opacity 500ms ease-in; +} +.justified-gallery > .jg-filtered { + display: none; +} +.justified-gallery > .spinner { + position: absolute; + bottom: 0; + margin-left: -24px; + padding: 10px 0 10px 0; + left: 50%; + filter: "alpha(opacity=100)"; + opacity: 1; + overflow: initial; +} +.justified-gallery > .spinner > span { + display: inline-block; + filter: "alpha(opacity=0)"; + opacity: 0; + width: 8px; + height: 8px; + margin: 0 4px 0 4px; + background-color: #000; + border-radius: 6px; +} diff --git a/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/justified_gallery/justifiedGallery.min.css b/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/justified_gallery/justifiedGallery.min.css new file mode 100644 index 0000000..d8b714c --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/justified_gallery/justifiedGallery.min.css @@ -0,0 +1,6 @@ +/*! + * justifiedGallery - v3.7.0 + * http://miromannino.github.io/Justified-Gallery/ + * Copyright (c) 2018 Miro Mannino + * Licensed under the MIT license. + */.justified-gallery{width:100%;position:relative;overflow:hidden}.justified-gallery>a,.justified-gallery>div,.justified-gallery>figure{position:absolute;display:inline-block;overflow:hidden;filter:"alpha(opacity=10)";opacity:.1;margin:0;padding:0}.justified-gallery>a>a>img,.justified-gallery>a>img,.justified-gallery>div>a>img,.justified-gallery>div>img,.justified-gallery>figure>a>img,.justified-gallery>figure>img{position:absolute;top:50%;left:50%;margin:0;padding:0;border:none;filter:"alpha(opacity=0)";opacity:0}.justified-gallery>a>.caption,.justified-gallery>div>.caption,.justified-gallery>figure>.caption{display:none;position:absolute;bottom:0;padding:5px;background-color:#000;left:0;right:0;margin:0;color:#fff;font-size:12px;font-weight:300;font-family:sans-serif}.justified-gallery>a>.caption.caption-visible,.justified-gallery>div>.caption.caption-visible,.justified-gallery>figure>.caption.caption-visible{display:initial;filter:"alpha(opacity=70)";opacity:.7;transition:opacity .5s ease-in}.justified-gallery>.entry-visible{filter:"alpha(opacity=100)";opacity:1;background:0 0}.justified-gallery>.entry-visible>a>img,.justified-gallery>.entry-visible>img{filter:"alpha(opacity=100)";opacity:1;transition:opacity .5s ease-in}.justified-gallery>.jg-filtered{display:none}.justified-gallery>.spinner{position:absolute;bottom:0;margin-left:-24px;padding:10px 0;left:50%;filter:"alpha(opacity=100)";opacity:1;overflow:initial}.justified-gallery>.spinner>span{display:inline-block;filter:"alpha(opacity=0)";opacity:0;width:8px;height:8px;margin:0 4px;background-color:#000;border-radius:6px} \ No newline at end of file diff --git a/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/lazy/jquery.lazy.js b/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/lazy/jquery.lazy.js new file mode 100644 index 0000000..e97bf95 --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/lazy/jquery.lazy.js @@ -0,0 +1,872 @@ +/*! + * jQuery & Zepto Lazy - v1.7.10 + * http://jquery.eisbehr.de/lazy/ + * + * Copyright 2012 - 2018, Daniel 'Eisbehr' Kern + * + * Dual licensed under the MIT and GPL-2.0 licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl-2.0.html + * + * $("img.lazy").lazy(); + */ + +;(function(window, undefined) { + "use strict"; + + // noinspection JSUnresolvedVariable + /** + * library instance - here and not in construct to be shorter in minimization + * @return void + */ + var $ = window.jQuery || window.Zepto, + + /** + * unique plugin instance id counter + * @type {number} + */ + lazyInstanceId = 0, + + /** + * helper to register window load for jQuery 3 + * @type {boolean} + */ + windowLoaded = false; + + /** + * make lazy available to jquery - and make it a bit more case-insensitive :) + * @access public + * @type {function} + * @param {object} settings + * @return {LazyPlugin} + */ + $.fn.Lazy = $.fn.lazy = function(settings) { + return new LazyPlugin(this, settings); + }; + + /** + * helper to add plugins to lazy prototype configuration + * @access public + * @type {function} + * @param {string|Array} names + * @param {string|Array|function} [elements] + * @param {function} loader + * @return void + */ + $.Lazy = $.lazy = function(names, elements, loader) { + // make second parameter optional + if ($.isFunction(elements)) { + loader = elements; + elements = []; + } + + // exit here if parameter is not a callable function + if (!$.isFunction(loader)) { + return; + } + + // make parameters an array of names to be sure + names = $.isArray(names) ? names : [names]; + elements = $.isArray(elements) ? elements : [elements]; + + var config = LazyPlugin.prototype.config, + forced = config._f || (config._f = {}); + + // add the loader plugin for every name + for (var i = 0, l = names.length; i < l; i++) { + if (config[names[i]] === undefined || $.isFunction(config[names[i]])) { + config[names[i]] = loader; + } + } + + // add forced elements loader + for (var c = 0, a = elements.length; c < a; c++) { + forced[elements[c]] = names[0]; + } + }; + + /** + * contains all logic and the whole element handling + * is packed in a private function outside class to reduce memory usage, because it will not be created on every plugin instance + * @access private + * @type {function} + * @param {LazyPlugin} instance + * @param {object} config + * @param {object|Array} items + * @param {object} events + * @param {string} namespace + * @return void + */ + function _executeLazy(instance, config, items, events, namespace) { + /** + * a helper to trigger the 'onFinishedAll' callback after all other events + * @access private + * @type {number} + */ + var _awaitingAfterLoad = 0, + + /** + * visible content width + * @access private + * @type {number} + */ + _actualWidth = -1, + + /** + * visible content height + * @access private + * @type {number} + */ + _actualHeight = -1, + + /** + * determine possibly detected high pixel density + * @access private + * @type {boolean} + */ + _isRetinaDisplay = false, + + /** + * dictionary entry for better minimization + * @access private + * @type {string} + */ + _afterLoad = 'afterLoad', + + /** + * dictionary entry for better minimization + * @access private + * @type {string} + */ + _load = 'load', + + /** + * dictionary entry for better minimization + * @access private + * @type {string} + */ + _error = 'error', + + /** + * dictionary entry for better minimization + * @access private + * @type {string} + */ + _img = 'img', + + /** + * dictionary entry for better minimization + * @access private + * @type {string} + */ + _src = 'src', + + /** + * dictionary entry for better minimization + * @access private + * @type {string} + */ + _srcset = 'srcset', + + /** + * dictionary entry for better minimization + * @access private + * @type {string} + */ + _sizes = 'sizes', + + /** + * dictionary entry for better minimization + * @access private + * @type {string} + */ + _backgroundImage = 'background-image'; + + /** + * initialize plugin + * bind loading to events or set delay time to load all items at once + * @access private + * @return void + */ + function _initialize() { + // detect actual device pixel ratio + // noinspection JSUnresolvedVariable + _isRetinaDisplay = window.devicePixelRatio > 1; + + // prepare all initial items + items = _prepareItems(items); + + // if delay time is set load all items at once after delay time + if (config.delay >= 0) { + setTimeout(function() { + _lazyLoadItems(true); + }, config.delay); + } + + // if no delay is set or combine usage is active bind events + if (config.delay < 0 || config.combined) { + // create unique event function + events.e = _throttle(config.throttle, function(event) { + // reset detected window size on resize event + if (event.type === 'resize') { + _actualWidth = _actualHeight = -1; + } + + // execute 'lazy magic' + _lazyLoadItems(event.all); + }); + + // create function to add new items to instance + events.a = function(additionalItems) { + additionalItems = _prepareItems(additionalItems); + items.push.apply(items, additionalItems); + }; + + // create function to get all instance items left + events.g = function() { + // filter loaded items before return in case internal filter was not running until now + return (items = $(items).filter(function() { + return !$(this).data(config.loadedName); + })); + }; + + // create function to force loading elements + events.f = function(forcedItems) { + for (var i = 0; i < forcedItems.length; i++) { + // only handle item if available in current instance + // use a compare function, because Zepto can't handle object parameter for filter + // var item = items.filter(forcedItems[i]); + /* jshint loopfunc: true */ + var item = items.filter(function() { + return this === forcedItems[i]; + }); + + if (item.length) { + _lazyLoadItems(false, item); + } + } + }; + + // load initial items + _lazyLoadItems(); + + // bind lazy load functions to scroll and resize event + // noinspection JSUnresolvedVariable + $(config.appendScroll).on('scroll.' + namespace + ' resize.' + namespace, events.e); + } + } + + /** + * prepare items before handle them + * @access private + * @param {Array|object|jQuery} items + * @return {Array|object|jQuery} + */ + function _prepareItems(items) { + // fetch used configurations before loops + var defaultImage = config.defaultImage, + placeholder = config.placeholder, + imageBase = config.imageBase, + srcsetAttribute = config.srcsetAttribute, + loaderAttribute = config.loaderAttribute, + forcedTags = config._f || {}; + + // filter items and only add those who not handled yet and got needed attributes available + items = $(items).filter(function() { + var element = $(this), + tag = _getElementTagName(this); + + return !element.data(config.handledName) && + (element.attr(config.attribute) || element.attr(srcsetAttribute) || element.attr(loaderAttribute) || forcedTags[tag] !== undefined); + }) + + // append plugin instance to all elements + .data('plugin_' + config.name, instance); + + for (var i = 0, l = items.length; i < l; i++) { + var element = $(items[i]), + tag = _getElementTagName(items[i]), + elementImageBase = element.attr(config.imageBaseAttribute) || imageBase; + + // generate and update source set if an image base is set + if (tag === _img && elementImageBase && element.attr(srcsetAttribute)) { + element.attr(srcsetAttribute, _getCorrectedSrcSet(element.attr(srcsetAttribute), elementImageBase)); + } + + // add loader to forced element types + if (forcedTags[tag] !== undefined && !element.attr(loaderAttribute)) { + element.attr(loaderAttribute, forcedTags[tag]); + } + + // set default image on every element without source + if (tag === _img && defaultImage && !element.attr(_src)) { + element.attr(_src, defaultImage); + } + + // set placeholder on every element without background image + else if (tag !== _img && placeholder && (!element.css(_backgroundImage) || element.css(_backgroundImage) === 'none')) { + element.css(_backgroundImage, "url('" + placeholder + "')"); + } + } + + return items; + } + + /** + * the 'lazy magic' - check all items + * @access private + * @param {boolean} [allItems] + * @param {object} [forced] + * @return void + */ + function _lazyLoadItems(allItems, forced) { + // skip if no items where left + if (!items.length) { + // destroy instance if option is enabled + if (config.autoDestroy) { + // noinspection JSUnresolvedFunction + instance.destroy(); + } + + return; + } + + var elements = forced || items, + loadTriggered = false, + imageBase = config.imageBase || '', + srcsetAttribute = config.srcsetAttribute, + handledName = config.handledName; + + // loop all available items + for (var i = 0; i < elements.length; i++) { + // item is at least in loadable area + if (allItems || forced || _isInLoadableArea(elements[i])) { + var element = $(elements[i]), + tag = _getElementTagName(elements[i]), + attribute = element.attr(config.attribute), + elementImageBase = element.attr(config.imageBaseAttribute) || imageBase, + customLoader = element.attr(config.loaderAttribute); + + // is not already handled + if (!element.data(handledName) && + // and is visible or visibility doesn't matter + (!config.visibleOnly || element.is(':visible')) && ( + // and image source or source set attribute is available + (attribute || element.attr(srcsetAttribute)) && ( + // and is image tag where attribute is not equal source or source set + (tag === _img && (elementImageBase + attribute !== element.attr(_src) || element.attr(srcsetAttribute) !== element.attr(_srcset))) || + // or is non image tag where attribute is not equal background + (tag !== _img && elementImageBase + attribute !== element.css(_backgroundImage)) + ) || + // or custom loader is available + customLoader)) + { + // mark element always as handled as this point to prevent double handling + loadTriggered = true; + element.data(handledName, true); + + // load item + _handleItem(element, tag, elementImageBase, customLoader); + } + } + } + + // when something was loaded remove them from remaining items + if (loadTriggered) { + items = $(items).filter(function() { + return !$(this).data(handledName); + }); + } + } + + /** + * load the given element the lazy way + * @access private + * @param {object} element + * @param {string} tag + * @param {string} imageBase + * @param {function} [customLoader] + * @return void + */ + function _handleItem(element, tag, imageBase, customLoader) { + // increment count of items waiting for after load + ++_awaitingAfterLoad; + + // extended error callback for correct 'onFinishedAll' handling + var errorCallback = function() { + _triggerCallback('onError', element); + _reduceAwaiting(); + + // prevent further callback calls + errorCallback = $.noop; + }; + + // trigger function before loading image + _triggerCallback('beforeLoad', element); + + // fetch all double used data here for better code minimization + var srcAttribute = config.attribute, + srcsetAttribute = config.srcsetAttribute, + sizesAttribute = config.sizesAttribute, + retinaAttribute = config.retinaAttribute, + removeAttribute = config.removeAttribute, + loadedName = config.loadedName, + elementRetina = element.attr(retinaAttribute); + + // handle custom loader + if (customLoader) { + // on load callback + var loadCallback = function() { + // remove attribute from element + if (removeAttribute) { + element.removeAttr(config.loaderAttribute); + } + + // mark element as loaded + element.data(loadedName, true); + + // call after load event + _triggerCallback(_afterLoad, element); + + // remove item from waiting queue and possibly trigger finished event + // it's needed to be asynchronous to run after filter was in _lazyLoadItems + setTimeout(_reduceAwaiting, 1); + + // prevent further callback calls + loadCallback = $.noop; + }; + + // bind error event to trigger callback and reduce waiting amount + element.off(_error).one(_error, errorCallback) + + // bind after load callback to element + .one(_load, loadCallback); + + // trigger custom loader and handle response + if (!_triggerCallback(customLoader, element, function(response) { + if(response) { + element.off(_load); + loadCallback(); + } + else { + element.off(_error); + errorCallback(); + } + })) { + element.trigger(_error); + } + } + + // handle images + else { + // create image object + var imageObj = $(new Image()); + + // bind error event to trigger callback and reduce waiting amount + imageObj.one(_error, errorCallback) + + // bind after load callback to image + .one(_load, function() { + // remove element from view + element.hide(); + + // set image back to element + // do it as single 'attr' calls, to be sure 'src' is set after 'srcset' + if (tag === _img) { + element.attr(_sizes, imageObj.attr(_sizes)) + .attr(_srcset, imageObj.attr(_srcset)) + .attr(_src, imageObj.attr(_src)); + } + else { + element.css(_backgroundImage, "url('" + imageObj.attr(_src) + "')"); + } + + // bring it back with some effect! + element[config.effect](config.effectTime); + + // remove attribute from element + if (removeAttribute) { + element.removeAttr(srcAttribute + ' ' + srcsetAttribute + ' ' + retinaAttribute + ' ' + config.imageBaseAttribute); + + // only remove 'sizes' attribute, if it was a custom one + if (sizesAttribute !== _sizes) { + element.removeAttr(sizesAttribute); + } + } + + // mark element as loaded + element.data(loadedName, true); + + // call after load event + _triggerCallback(_afterLoad, element); + + // cleanup image object + imageObj.remove(); + + // remove item from waiting queue and possibly trigger finished event + _reduceAwaiting(); + }); + + // set sources + // do it as single 'attr' calls, to be sure 'src' is set after 'srcset' + var imageSrc = (_isRetinaDisplay && elementRetina ? elementRetina : element.attr(srcAttribute)) || ''; + imageObj.attr(_sizes, element.attr(sizesAttribute)) + .attr(_srcset, element.attr(srcsetAttribute)) + .attr(_src, imageSrc ? imageBase + imageSrc : null); + + // call after load even on cached image + imageObj.complete && imageObj.trigger(_load); // jshint ignore : line + } + } + + /** + * check if the given element is inside the current viewport or threshold + * @access private + * @param {object} element + * @return {boolean} + */ + function _isInLoadableArea(element) { + var elementBound = element.getBoundingClientRect(), + direction = config.scrollDirection, + threshold = config.threshold, + vertical = // check if element is in loadable area from top + ((_getActualHeight() + threshold) > elementBound.top) && + // check if element is even in loadable are from bottom + (-threshold < elementBound.bottom), + horizontal = // check if element is in loadable area from left + ((_getActualWidth() + threshold) > elementBound.left) && + // check if element is even in loadable area from right + (-threshold < elementBound.right); + + if (direction === 'vertical') { + return vertical; + } + else if (direction === 'horizontal') { + return horizontal; + } + + return vertical && horizontal; + } + + /** + * receive the current viewed width of the browser + * @access private + * @return {number} + */ + function _getActualWidth() { + return _actualWidth >= 0 ? _actualWidth : (_actualWidth = $(window).width()); + } + + /** + * receive the current viewed height of the browser + * @access private + * @return {number} + */ + function _getActualHeight() { + return _actualHeight >= 0 ? _actualHeight : (_actualHeight = $(window).height()); + } + + /** + * get lowercase tag name of an element + * @access private + * @param {object} element + * @returns {string} + */ + function _getElementTagName(element) { + return element.tagName.toLowerCase(); + } + + /** + * prepend image base to all srcset entries + * @access private + * @param {string} srcset + * @param {string} imageBase + * @returns {string} + */ + function _getCorrectedSrcSet(srcset, imageBase) { + if (imageBase) { + // trim, remove unnecessary spaces and split entries + var entries = srcset.split(','); + srcset = ''; + + for (var i = 0, l = entries.length; i < l; i++) { + srcset += imageBase + entries[i].trim() + (i !== l - 1 ? ',' : ''); + } + } + + return srcset; + } + + /** + * helper function to throttle down event triggering + * @access private + * @param {number} delay + * @param {function} callback + * @return {function} + */ + function _throttle(delay, callback) { + var timeout, + lastExecute = 0; + + return function(event, ignoreThrottle) { + var elapsed = +new Date() - lastExecute; + + function run() { + lastExecute = +new Date(); + // noinspection JSUnresolvedFunction + callback.call(instance, event); + } + + timeout && clearTimeout(timeout); // jshint ignore : line + + if (elapsed > delay || !config.enableThrottle || ignoreThrottle) { + run(); + } + else { + timeout = setTimeout(run, delay - elapsed); + } + }; + } + + /** + * reduce count of awaiting elements to 'afterLoad' event and fire 'onFinishedAll' if reached zero + * @access private + * @return void + */ + function _reduceAwaiting() { + --_awaitingAfterLoad; + + // if no items were left trigger finished event + if (!items.length && !_awaitingAfterLoad) { + _triggerCallback('onFinishedAll'); + } + } + + /** + * single implementation to handle callbacks, pass element and set 'this' to current instance + * @access private + * @param {string|function} callback + * @param {object} [element] + * @param {*} [args] + * @return {boolean} + */ + function _triggerCallback(callback, element, args) { + if ((callback = config[callback])) { + // jQuery's internal '$(arguments).slice(1)' are causing problems at least on old iPads + // below is shorthand of 'Array.prototype.slice.call(arguments, 1)' + callback.apply(instance, [].slice.call(arguments, 1)); + return true; + } + + return false; + } + + // if event driven or window is already loaded don't wait for page loading + if (config.bind === 'event' || windowLoaded) { + _initialize(); + } + + // otherwise load initial items and start lazy after page load + else { + // noinspection JSUnresolvedVariable + $(window).on(_load + '.' + namespace, _initialize); + } + } + + /** + * lazy plugin class constructor + * @constructor + * @access private + * @param {object} elements + * @param {object} settings + * @return {object|LazyPlugin} + */ + function LazyPlugin(elements, settings) { + /** + * this lazy plugin instance + * @access private + * @type {object|LazyPlugin|LazyPlugin.prototype} + */ + var _instance = this, + + /** + * this lazy plugin instance configuration + * @access private + * @type {object} + */ + _config = $.extend({}, _instance.config, settings), + + /** + * instance generated event executed on container scroll or resize + * packed in an object to be referenceable and short named because properties will not be minified + * @access private + * @type {object} + */ + _events = {}, + + /** + * unique namespace for instance related events + * @access private + * @type {string} + */ + _namespace = _config.name + '-' + (++lazyInstanceId); + + // noinspection JSUndefinedPropertyAssignment + /** + * wrapper to get or set an entry from plugin instance configuration + * much smaller on minify as direct access + * @access public + * @type {function} + * @param {string} entryName + * @param {*} [value] + * @return {LazyPlugin|*} + */ + _instance.config = function(entryName, value) { + if (value === undefined) { + return _config[entryName]; + } + + _config[entryName] = value; + return _instance; + }; + + // noinspection JSUndefinedPropertyAssignment + /** + * add additional items to current instance + * @access public + * @param {Array|object|string} items + * @return {LazyPlugin} + */ + _instance.addItems = function(items) { + _events.a && _events.a($.type(items) === 'string' ? $(items) : items); // jshint ignore : line + return _instance; + }; + + // noinspection JSUndefinedPropertyAssignment + /** + * get all left items of this instance + * @access public + * @returns {object} + */ + _instance.getItems = function() { + return _events.g ? _events.g() : {}; + }; + + // noinspection JSUndefinedPropertyAssignment + /** + * force lazy to load all items in loadable area right now + * by default without throttle + * @access public + * @type {function} + * @param {boolean} [useThrottle] + * @return {LazyPlugin} + */ + _instance.update = function(useThrottle) { + _events.e && _events.e({}, !useThrottle); // jshint ignore : line + return _instance; + }; + + // noinspection JSUndefinedPropertyAssignment + /** + * force element(s) to load directly, ignoring the viewport + * @access public + * @param {Array|object|string} items + * @return {LazyPlugin} + */ + _instance.force = function(items) { + _events.f && _events.f($.type(items) === 'string' ? $(items) : items); // jshint ignore : line + return _instance; + }; + + // noinspection JSUndefinedPropertyAssignment + /** + * force lazy to load all available items right now + * this call ignores throttling + * @access public + * @type {function} + * @return {LazyPlugin} + */ + _instance.loadAll = function() { + _events.e && _events.e({all: true}, true); // jshint ignore : line + return _instance; + }; + + // noinspection JSUndefinedPropertyAssignment + /** + * destroy this plugin instance + * @access public + * @type {function} + * @return undefined + */ + _instance.destroy = function() { + // unbind instance generated events + // noinspection JSUnresolvedFunction, JSUnresolvedVariable + $(_config.appendScroll).off('.' + _namespace, _events.e); + // noinspection JSUnresolvedVariable + $(window).off('.' + _namespace); + + // clear events + _events = {}; + + return undefined; + }; + + // start using lazy and return all elements to be chainable or instance for further use + // noinspection JSUnresolvedVariable + _executeLazy(_instance, _config, elements, _events, _namespace); + return _config.chainable ? elements : _instance; + } + + /** + * settings and configuration data + * @access public + * @type {object|*} + */ + LazyPlugin.prototype.config = { + // general + name : 'lazy', + chainable : true, + autoDestroy : true, + bind : 'load', + threshold : 500, + visibleOnly : false, + appendScroll : window, + scrollDirection : 'both', + imageBase : null, + defaultImage : 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', + placeholder : null, + delay : -1, + combined : false, + + // attributes + attribute : 'data-src', + srcsetAttribute : 'data-srcset', + sizesAttribute : 'data-sizes', + retinaAttribute : 'data-retina', + loaderAttribute : 'data-loader', + imageBaseAttribute : 'data-imagebase', + removeAttribute : true, + handledName : 'handled', + loadedName : 'loaded', + + // effect + effect : 'show', + effectTime : 0, + + // throttle + enableThrottle : true, + throttle : 250, + + // callbacks + beforeLoad : undefined, + afterLoad : undefined, + onError : undefined, + onFinishedAll : undefined + }; + + // register window load event globally to prevent not loading elements + // since jQuery 3.X ready state is fully async and may be executed after 'load' + $(window).on('load', function() { + windowLoaded = true; + }); +})(window); \ No newline at end of file diff --git a/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/lazy/jquery.lazy.min.js b/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/lazy/jquery.lazy.min.js new file mode 100644 index 0000000..0a54f7c --- /dev/null +++ b/src/themes/hugo-shortcode-gallery/static/shortcode-gallery/lazy/jquery.lazy.min.js @@ -0,0 +1 @@ +(function(t,e){"use strict";function r(r,a,i,u,l){function f(){L=t.devicePixelRatio>1,i=c(i),a.delay>=0&&setTimeout(function(){s(!0)},a.delay),(a.delay<0||a.combined)&&(u.e=v(a.throttle,function(t){"resize"===t.type&&(w=B=-1),s(t.all)}),u.a=function(t){t=c(t),i.push.apply(i,t)},u.g=function(){return i=n(i).filter(function(){return!n(this).data(a.loadedName)})},u.f=function(t){for(var e=0;ee.top&&-ne.left&&-n=0?w:w=n(t).width()}function h(){return B>=0?B:B=n(t).height()}function m(t){return t.tagName.toLowerCase()}function b(t,e){if(e){var r=t.split(",");t="";for(var a=0,n=r.length;at||!a.enableThrottle||u?l():n=setTimeout(l,t-f)}}function p(){--z,i.length||z||y("onFinishedAll")}function y(t,e,n){return!!(t=a[t])&&(t.apply(r,[].slice.call(arguments,1)),!0)}var z=0,w=-1,B=-1,L=!1,T="afterLoad",D="load",I="error",N="img",E="src",F="srcset",C="sizes",O="background-image";"event"===a.bind||o?f():n(t).on(D+"."+l,f)}function a(a,o){var u=this,l=n.extend({},u.config,o),f={},c=l.name+"-"+ ++i;return u.config=function(t,r){return r===e?l[t]:(l[t]=r,u)},u.addItems=function(t){return f.a&&f.a("string"===n.type(t)?n(t):t),u},u.getItems=function(){return f.g?f.g():{}},u.update=function(t){return f.e&&f.e({},!t),u},u.force=function(t){return f.f&&f.f("string"===n.type(t)?n(t):t),u},u.loadAll=function(){return f.e&&f.e({all:!0},!0),u},u.destroy=function(){return n(l.appendScroll).off("."+c,f.e),n(t).off("."+c),f={},e},r(u,l,a,f,c),l.chainable?a:u}var n=t.jQuery||t.Zepto,i=0,o=!1;n.fn.Lazy=n.fn.lazy=function(t){return new a(this,t)},n.Lazy=n.lazy=function(t,r,i){if(n.isFunction(r)&&(i=r,r=[]),n.isFunction(i)){t=n.isArray(t)?t:[t],r=n.isArray(r)?r:[r];for(var o=a.prototype.config,u=o._f||(o._f={}),l=0,f=t.length;l