diff options
Diffstat (limited to 'plugins/jetpack/modules/carousel/jetpack-carousel.js')
-rw-r--r-- | plugins/jetpack/modules/carousel/jetpack-carousel.js | 2964 |
1 files changed, 1415 insertions, 1549 deletions
diff --git a/plugins/jetpack/modules/carousel/jetpack-carousel.js b/plugins/jetpack/modules/carousel/jetpack-carousel.js index 2a5ad2d6..c9d295b8 100644 --- a/plugins/jetpack/modules/carousel/jetpack-carousel.js +++ b/plugins/jetpack/modules/carousel/jetpack-carousel.js @@ -1,1257 +1,973 @@ -/* global jetpackCarouselStrings, DocumentTouch */ - -jQuery( document ).ready( function( $ ) { - // gallery faded layer and container elements - var overlay, - comments, - gallery, - container, - nextButton, - previousButton, - info, - transitionBegin, - caption, - resizeTimeout, - photo_info, - close_hint, - commentInterval, - lastSelectedSlide, - screenPadding = 110, - originalOverflow = $( 'body' ).css( 'overflow' ), - originalHOverflow = $( 'html' ).css( 'overflow' ), - proportion = 85, - last_known_location_hash = '', - imageMeta, - titleAndDescription, - commentForm, - leftColWrapper, - scrollPos; - - if ( window.innerWidth <= 760 ) { - screenPadding = Math.round( ( window.innerWidth / 760 ) * 110 ); - - if ( - screenPadding < 40 && - ( 'ontouchstart' in window || ( window.DocumentTouch && document instanceof DocumentTouch ) ) - ) { - screenPadding = 0; +/* global wpcom, jetpackCarouselStrings, DocumentTouch */ + +( function () { + 'use strict'; + var swiper; + ///////////////////////////////////// + // Utility functions + ///////////////////////////////////// + var util = ( function () { + var noop = function () {}; + + function texturize( text ) { + // Ensure we get a string. + text = text + ''; + text = text.replace( /'/g, '’' ).replace( /'/g, '’' ); + text = text + .replace( /"/g, '”' ) + .replace( /"/g, '”' ) + .replace( /"/g, '”' ) + .replace( /[\u201D]/g, '”' ); + // Untexturize allowed HTML tags params double-quotes. + text = text.replace( /([\w]+)=&#[\d]+;(.+?)&#[\d]+;/g, '$1="$2"' ); + return text.trim(); } - } - - // Adding a polyfill for browsers that do not have Date.now - if ( 'undefined' === typeof Date.now ) { - Date.now = function now() { - return new Date().getTime(); - }; - } - var keyListener = function( e ) { - switch ( e.which ) { - case 38: // up - e.preventDefault(); - container.scrollTop( container.scrollTop() - 100 ); - break; - case 40: // down - e.preventDefault(); - container.scrollTop( container.scrollTop() + 100 ); - break; - case 39: // right - e.preventDefault(); - gallery.jp_carousel( 'next' ); - break; - case 37: // left - case 8: // backspace - e.preventDefault(); - gallery.jp_carousel( 'previous' ); - break; - case 27: // escape - e.preventDefault(); - container.jp_carousel( 'close' ); - break; - default: - // making jslint happy - break; + function applyReplacements( text, replacements ) { + if ( ! text ) { + return; + } + if ( ! replacements ) { + return text; + } + return text.replace( /{(\d+)}/g, function ( match, number ) { + return typeof replacements[ number ] !== 'undefined' ? replacements[ number ] : match; + } ); } - }; - - var resizeListener = function(/*e*/) { - clearTimeout( resizeTimeout ); - resizeTimeout = setTimeout( function() { - gallery.jp_carousel( 'slides' ).jp_carousel( 'fitSlide', true ); - gallery.jp_carousel( 'updateSlidePositions', true ); - gallery.jp_carousel( 'fitMeta', true ); - }, 200 ); - }; - - var prepareGallery = function(/*dataCarouselExtra*/) { - if ( ! overlay ) { - overlay = $( '<div></div>' ) - .addClass( 'jp-carousel-overlay' ) - .css( { - position: 'fixed', - top: 0, - right: 0, - bottom: 0, - left: 0, - } ); - var buttons = - '<a class="jp-carousel-commentlink" href="#">' + jetpackCarouselStrings.comment + '</a>'; - if ( 1 === Number( jetpackCarouselStrings.is_logged_in ) ) { + function getBackgroundImage( imgEl ) { + var canvas = document.createElement( 'canvas' ), + context = canvas.getContext && canvas.getContext( '2d' ); + + if ( ! imgEl ) { + return; } - buttons = $( '<div class="jp-carousel-buttons">' + buttons + '</div>' ); + context.filter = 'blur(20px) '; + context.drawImage( imgEl, 0, 0 ); + var url = canvas.toDataURL( 'image/png' ); + canvas = null; - caption = $( '<h2 itemprop="caption description"></h2>' ); - photo_info = $( '<div class="jp-carousel-photo-info"></div>' ).append( caption ); + return url; + } - imageMeta = $( '<div></div>' ) - .addClass( 'jp-carousel-image-meta' ) - .css( { - float: 'right', - 'margin-top': '20px', - width: '250px', - } ); + return { + noop: noop, + texturize: texturize, + applyReplacements: applyReplacements, + getBackgroundImage: getBackgroundImage, + }; + } )(); - imageMeta - .append( buttons ) - .append( "<ul class='jp-carousel-image-exif' style='display:none;'></ul>" ) - .append( "<a class='jp-carousel-image-download' style='display:none;'></a>" ) - .append( "<div class='jp-carousel-image-map' style='display:none;'></div>" ); - - titleAndDescription = $( '<div></div>' ) - .addClass( 'jp-carousel-titleanddesc' ) - .css( { - width: '100%', - 'margin-top': imageMeta.css( 'margin-top' ), - } ); + ///////////////////////////////////// + // DOM-related utility functions + ///////////////////////////////////// + var domUtil = ( function () { + // Helper matches function (not a polyfill), compatible with IE 11. + function matches( el, sel ) { + if ( Element.prototype.matches ) { + return el.matches( sel ); + } - var commentFormMarkup = '<div id="jp-carousel-comment-form-container">'; + if ( Element.prototype.msMatchesSelector ) { + return el.msMatchesSelector( sel ); + } + } - if ( - jetpackCarouselStrings.local_comments_commenting_as && - jetpackCarouselStrings.local_comments_commenting_as.length - ) { - // Comments not enabled, fallback to local comments + // Helper closest parent node function (not a polyfill) based on + // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill + function closest( el, sel ) { + if ( el.closest ) { + return el.closest( sel ); + } - if ( - 1 !== Number( jetpackCarouselStrings.is_logged_in ) && - 1 === Number( jetpackCarouselStrings.comment_registration ) - ) { - commentFormMarkup += - '<div id="jp-carousel-comment-form-commenting-as">' + - jetpackCarouselStrings.local_comments_commenting_as + - '</div>'; - } else { - commentFormMarkup += '<form id="jp-carousel-comment-form">'; - commentFormMarkup += - '<textarea name="comment" class="jp-carousel-comment-form-field jp-carousel-comment-form-textarea" id="jp-carousel-comment-form-comment-field" placeholder="' + - jetpackCarouselStrings.write_comment + - '"></textarea>'; - commentFormMarkup += '<div id="jp-carousel-comment-form-submit-and-info-wrapper">'; - commentFormMarkup += - '<div id="jp-carousel-comment-form-commenting-as">' + - jetpackCarouselStrings.local_comments_commenting_as + - '</div>'; - commentFormMarkup += - '<input type="submit" name="submit" class="jp-carousel-comment-form-button" id="jp-carousel-comment-form-button-submit" value="' + - jetpackCarouselStrings.post_comment + - '" />'; - commentFormMarkup += '<span id="jp-carousel-comment-form-spinner"> </span>'; - commentFormMarkup += '<div id="jp-carousel-comment-post-results"></div>'; - commentFormMarkup += '</div>'; - commentFormMarkup += '</form>'; + var current = el; + + do { + if ( matches( current, sel ) ) { + return current; } - } - commentFormMarkup += '</div>'; + current = current.parentElement || current.parentNode; + } while ( current !== null && current.nodeType === 1 ); - commentForm = $( commentFormMarkup ).css( { - width: '100%', - 'margin-top': '20px', - color: '#999', - } ); + return null; + } - comments = $( '<div></div>' ) - .addClass( 'jp-carousel-comments' ) - .css( { - width: '100%', - bottom: '10px', - 'margin-top': '20px', - } ); + function hide( el ) { + if ( el ) { + el.style.display = 'none'; + } + } - var commentsLoading = $( - '<div id="jp-carousel-comments-loading"><span>' + - jetpackCarouselStrings.loading_comments + - '</span></div>' - ).css( { - width: '100%', - bottom: '10px', - 'margin-top': '20px', - } ); + function show( el ) { + if ( el ) { + // Everything we show and hide in Carousel is currently a block, + // so we can make this really straightforward. + el.style.display = 'block'; + } + } - var leftWidth = $( window ).width() - screenPadding * 2 - ( imageMeta.width() + 40 ); - leftWidth += 'px'; - - leftColWrapper = $( '<div></div>' ) - .addClass( 'jp-carousel-left-column-wrapper' ) - .css( { - width: Math.floor( leftWidth ), - } ) - .append( titleAndDescription ) - .append( commentForm ) - .append( comments ) - .append( commentsLoading ); - - var fadeaway = $( '<div></div>' ).addClass( 'jp-carousel-fadeaway' ); - - info = $( '<div></div>' ) - .addClass( 'jp-carousel-info' ) - .css( { - top: Math.floor( ( $( window ).height() / 100 ) * proportion ), - left: screenPadding, - right: screenPadding, - } ) - .append( photo_info ) - .append( imageMeta ); + function fade( el, start, end, callback ) { + if ( ! el ) { + return callback(); + } + + // Prepare for transition. + // Ensure the item is in the render tree, in its initial state. + el.style.removeProperty( 'display' ); + el.style.opacity = start; + el.style.transition = 'opacity 0.2s'; + el.style.pointerEvents = 'none'; + + var finished = function ( e ) { + if ( e.target === el && e.propertyName === 'opacity' ) { + el.style.removeProperty( 'transition' ); + el.style.removeProperty( 'opacity' ); + el.style.removeProperty( 'pointer-events' ); + el.removeEventListener( 'transitionend', finished ); + el.removeEventListener( 'transitioncancel', finished ); + callback(); + } + }; - if ( window.innerWidth <= 760 ) { - photo_info.remove().insertAfter( titleAndDescription ); - info.prepend( leftColWrapper ); - } else { - info.append( leftColWrapper ); - } - - var targetBottomPos = $( window ).height() - parseInt( info.css( 'top' ), 10 ) + 'px'; - - nextButton = $( '<div><span></span></div>' ) - .addClass( 'jp-carousel-next-button' ) - .css( { - right: '15px', - } ) - .hide(); - - previousButton = $( '<div><span></span></div>' ) - .addClass( 'jp-carousel-previous-button' ) - .css( { - left: 0, - } ) - .hide(); - - nextButton.add( previousButton ).css( { - position: 'fixed', - top: '40px', - bottom: targetBottomPos, - width: screenPadding, + requestAnimationFrame( function () { + // Double rAF for browser compatibility. + requestAnimationFrame( function () { + el.addEventListener( 'transitionend', finished ); + el.addEventListener( 'transitioncancel', finished ); + // Trigger transition. + el.style.opacity = end; + } ); } ); + } - gallery = $( '<div></div>' ) - .addClass( 'jp-carousel' ) - .css( { - position: 'absolute', - top: 0, - bottom: targetBottomPos, - left: 0, - right: 0, - } ); + function fadeIn( el, callback ) { + callback = callback || util.noop; + fade( el, '0', '1', callback ); + } - close_hint = $( '<div class="jp-carousel-close-hint"><span>×</span></div>' ).css( { - position: 'fixed', + function fadeOut( el, callback ) { + callback = callback || util.noop; + fade( el, '1', '0', function () { + if ( el ) { + el.style.display = 'none'; + } + callback(); } ); + } - container = $( '<div></div>' ) - .addClass( 'jp-carousel-wrap' ) - .addClass( 'jp-carousel-transitions' ); - if ( 'white' === jetpackCarouselStrings.background_color ) { - container.addClass( 'jp-carousel-light' ); - } - - container.attr( 'itemscope', '' ); - - container.attr( 'itemtype', 'https://schema.org/ImageGallery' ); - - container - .css( { - position: 'fixed', - top: 0, - right: 0, - bottom: 0, - left: 0, - 'z-index': 2147483647, - 'overflow-x': 'hidden', - 'overflow-y': 'auto', - direction: 'ltr', - } ) - .hide() - .append( overlay ) - .append( gallery ) - .append( fadeaway ) - .append( info ) - .append( nextButton ) - .append( previousButton ) - .append( close_hint ) - .appendTo( $( 'body' ) ) - .click( function( e ) { - var target = $( e.target ), - wrap = target.parents( 'div.jp-carousel-wrap' ), - data = wrap.data( 'carousel-extra' ), - slide = wrap.find( 'div.selected' ), - attachment_id = slide.data( 'attachment-id' ); - data = data || []; - - if ( - target.is( gallery ) || - target - .parents() - .add( target ) - .is( close_hint ) - ) { - container.jp_carousel( 'close' ); - } else if ( target.hasClass( 'jp-carousel-commentlink' ) ) { - e.preventDefault(); - e.stopPropagation(); - $( window ).unbind( 'keydown', keyListener ); - container.animate( { scrollTop: parseInt( info.position()[ 'top' ], 10 ) }, 'fast' ); - $( '#jp-carousel-comment-form-submit-and-info-wrapper' ).slideDown( 'fast' ); - $( '#jp-carousel-comment-form-comment-field' ).focus(); - } else if ( target.hasClass( 'jp-carousel-comment-login' ) ) { - var url = jetpackCarouselStrings.login_url + '%23jp-carousel-' + attachment_id; - - window.location.href = url; - } else if ( target.parents( '#jp-carousel-comment-form-container' ).length ) { - var textarea = $( '#jp-carousel-comment-form-comment-field' ) - .blur( function() { - $( window ).bind( 'keydown', keyListener ); - } ) - .focus( function() { - $( window ).unbind( 'keydown', keyListener ); - } ); - - var emailField = $( '#jp-carousel-comment-form-email-field' ) - .blur( function() { - $( window ).bind( 'keydown', keyListener ); - } ) - .focus( function() { - $( window ).unbind( 'keydown', keyListener ); - } ); - - var authorField = $( '#jp-carousel-comment-form-author-field' ) - .blur( function() { - $( window ).bind( 'keydown', keyListener ); - } ) - .focus( function() { - $( window ).unbind( 'keydown', keyListener ); - } ); - - var urlField = $( '#jp-carousel-comment-form-url-field' ) - .blur( function() { - $( window ).bind( 'keydown', keyListener ); - } ) - .focus( function() { - $( window ).unbind( 'keydown', keyListener ); - } ); - - if ( textarea && textarea.attr( 'id' ) === target.attr( 'id' ) ) { - // For first page load - $( window ).unbind( 'keydown', keyListener ); - $( '#jp-carousel-comment-form-submit-and-info-wrapper' ).slideDown( 'fast' ); - } else if ( target.is( 'input[type="submit"]' ) ) { - e.preventDefault(); - e.stopPropagation(); - - $( '#jp-carousel-comment-form-spinner' ).spin( 'small', 'white' ); - - var ajaxData = { - action: 'post_attachment_comment', - nonce: jetpackCarouselStrings.nonce, - blog_id: data[ 'blog_id' ], - id: attachment_id, - comment: textarea.val(), - }; - - if ( ! ajaxData[ 'comment' ].length ) { - gallery.jp_carousel( 'postCommentError', { - field: 'jp-carousel-comment-form-comment-field', - error: jetpackCarouselStrings.no_comment_text, - } ); - return; - } - - if ( 1 !== Number( jetpackCarouselStrings.is_logged_in ) ) { - ajaxData[ 'email' ] = emailField.val(); - ajaxData[ 'author' ] = authorField.val(); - ajaxData[ 'url' ] = urlField.val(); - - if ( 1 === Number( jetpackCarouselStrings.require_name_email ) ) { - if ( ! ajaxData[ 'email' ].length || ! ajaxData[ 'email' ].match( '@' ) ) { - gallery.jp_carousel( 'postCommentError', { - field: 'jp-carousel-comment-form-email-field', - error: jetpackCarouselStrings.no_comment_email, - } ); - return; - } else if ( ! ajaxData[ 'author' ].length ) { - gallery.jp_carousel( 'postCommentError', { - field: 'jp-carousel-comment-form-author-field', - error: jetpackCarouselStrings.no_comment_author, - } ); - return; - } - } - } - - $.ajax( { - type: 'POST', - url: jetpackCarouselStrings.ajaxurl, - data: ajaxData, - dataType: 'json', - success: function( response /*, status, xhr*/ ) { - if ( 'approved' === response.comment_status ) { - $( '#jp-carousel-comment-post-results' ) - .slideUp( 'fast' ) - .html( - '<span class="jp-carousel-comment-post-success">' + - jetpackCarouselStrings.comment_approved + - '</span>' - ) - .slideDown( 'fast' ); - } else if ( 'unapproved' === response.comment_status ) { - $( '#jp-carousel-comment-post-results' ) - .slideUp( 'fast' ) - .html( - '<span class="jp-carousel-comment-post-success">' + - jetpackCarouselStrings.comment_unapproved + - '</span>' - ) - .slideDown( 'fast' ); - } else { - // 'deleted', 'spam', false - $( '#jp-carousel-comment-post-results' ) - .slideUp( 'fast' ) - .html( - '<span class="jp-carousel-comment-post-error">' + - jetpackCarouselStrings.comment_post_error + - '</span>' - ) - .slideDown( 'fast' ); - } - gallery.jp_carousel( 'clearCommentTextAreaValue' ); - gallery.jp_carousel( 'getComments', { - attachment_id: attachment_id, - offset: 0, - clear: true, - } ); - $( '#jp-carousel-comment-form-button-submit' ).val( - jetpackCarouselStrings.post_comment - ); - $( '#jp-carousel-comment-form-spinner' ).spin( false ); - }, - error: function(/*xhr, status, error*/) { - // TODO: Add error handling and display here - gallery.jp_carousel( 'postCommentError', { - field: 'jp-carousel-comment-form-comment-field', - error: jetpackCarouselStrings.comment_post_error, - } ); - return; - }, - } ); - } - } else if ( ! target.parents( '.jp-carousel-info' ).length ) { - container.jp_carousel( 'next' ); - } - } ) - .bind( 'jp_carousel.afterOpen', function() { - $( window ).bind( 'keydown', keyListener ); - $( window ).bind( 'resize', resizeListener ); - gallery.opened = true; - - resizeListener(); - } ) - .bind( 'jp_carousel.beforeClose', function() { - var scroll = $( window ).scrollTop(); - - $( window ).unbind( 'keydown', keyListener ); - $( window ).unbind( 'resize', resizeListener ); - $( window ).scrollTop( scroll ); - $( '.jp-carousel-previous-button' ).hide(); - $( '.jp-carousel-next-button' ).hide(); - // Set height to original value - // Fix some themes where closing carousel brings view back to top - $( 'html' ).css( 'height', '' ); - } ) - .bind( 'jp_carousel.afterClose', function() { - if ( window.location.hash && history.back ) { - history.back(); - } - last_known_location_hash = ''; - gallery.opened = false; - } ) - .on( 'transitionend.jp-carousel ', '.jp-carousel-slide', function( e ) { - // If the movement transitions take more than twice the allotted time, disable them. - // There is some wiggle room in the 2x, since some of that time is taken up in - // JavaScript, setting up the transition and calling the events. - if ( 'transform' === e.originalEvent.propertyName ) { - var transitionMultiplier = - ( Date.now() - transitionBegin ) / 1000 / e.originalEvent.elapsedTime; - - container.off( 'transitionend.jp-carousel' ); - - if ( transitionMultiplier >= 2 ) { - $( '.jp-carousel-transitions' ).removeClass( 'jp-carousel-transitions' ); - } - } + function emitEvent( el, type, detail ) { + var e; + try { + e = new CustomEvent( type, { + bubbles: true, + cancelable: true, + detail: detail || null, } ); + } catch ( err ) { + e = document.createEvent( 'CustomEvent' ); + e.initCustomEvent( type, true, true, detail || null ); + } + el.dispatchEvent( e ); + } - $( '.jp-carousel-wrap' ).touchwipe( { - wipeLeft: function( e ) { - e.preventDefault(); - gallery.jp_carousel( 'next' ); - }, - wipeRight: function( e ) { - e.preventDefault(); - gallery.jp_carousel( 'previous' ); - }, - preventDefaultEvents: false, - } ); + // From: https://easings.net/#easeInOutQuad + function easeInOutQuad( num ) { + return num < 0.5 ? 2 * num * num : 1 - Math.pow( -2 * num + 2, 2 ) / 2; + } - nextButton.add( previousButton ).click( function( e ) { - e.preventDefault(); - e.stopPropagation(); - if ( nextButton.is( this ) ) { - gallery.jp_carousel( 'next' ); - } else { - gallery.jp_carousel( 'previous' ); - } - } ); + function getFooterClearance( container ) { + var footer = container.querySelector( '.jp-carousel-info-footer' ); + var infoArea = container.querySelector( '.jp-carousel-info-extra' ); + var contentArea = container.querySelector( '.jp-carousel-info-content-wrapper' ); + + if ( footer && infoArea && contentArea ) { + var styles = window.getComputedStyle( infoArea ); + var padding = parseInt( styles.paddingTop, 10 ) + parseInt( styles.paddingBottom, 10 ); + padding = isNaN( padding ) ? 0 : padding; + return contentArea.offsetHeight + footer.offsetHeight + padding; + } + return 0; } - }; - var processSingleImageGallery = function() { - // process links that contain img tag with attribute data-attachment-id - $( 'a img[data-attachment-id]' ).each( function() { - var container = $( this ).parent(); + function isTouch() { + return ( + 'ontouchstart' in window || ( window.DocumentTouch && document instanceof DocumentTouch ) + ); + } - // skip if image was already added to gallery by shortcode - if ( container.parent( '.gallery-icon' ).length ) { + function scrollToElement( el, container, callback ) { + if ( ! el || ! container ) { + if ( callback ) { + return callback(); + } return; } - // skip if the container is not a link - if ( 'undefined' === typeof $( container ).attr( 'href' ) ) { - return; + // For iOS Safari compatibility, use JS to set the minimum height. + var infoArea = container.querySelector( '.jp-carousel-info-extra' ); + if ( infoArea ) { + // 64px is the same height as `.jp-carousel-info-footer` in the CSS. + infoArea.style.minHeight = window.innerHeight - 64 + 'px'; } - var valid = false; + var isScrolling = true; + var startTime = Date.now(); + var duration = 300; + var originalPosition = container.scrollTop; + var targetPosition = Math.max( + 0, + el.offsetTop - Math.max( 0, window.innerHeight - getFooterClearance( container ) ) + ); + var distance = targetPosition - container.scrollTop; + distance = Math.min( distance, container.scrollHeight - window.innerHeight ); - // if link points to 'Media File' (ignoring GET parameters) and flag is set allow it - if ( - $( container ) - .attr( 'href' ) - .split( '?' )[ 0 ] === - $( this ) - .attr( 'data-orig-file' ) - .split( '?' )[ 0 ] && - 1 === Number( jetpackCarouselStrings.single_image_gallery_media_file ) - ) { - valid = true; + function stopScroll() { + isScrolling = false; } - // if link points to 'Attachment Page' allow it - if ( $( container ).attr( 'href' ) === $( this ).attr( 'data-permalink' ) ) { - valid = true; + function runScroll() { + var now = Date.now(); + var progress = easeInOutQuad( ( now - startTime ) / duration ); + + progress = progress > 1 ? 1 : progress; + var newVal = progress * distance; + container.scrollTop = originalPosition + newVal; + + if ( now <= startTime + duration && isScrolling ) { + return requestAnimationFrame( runScroll ); + } + if ( callback ) { + callback(); + } + if ( infoArea ) { + infoArea.style.minHeight = ''; + } + isScrolling = false; + container.removeEventListener( 'wheel', stopScroll ); } - // links to 'Custom URL' or 'Media File' when flag not set are not valid - if ( ! valid ) { - return; + // Allow scroll to be cancelled by user interaction. + container.addEventListener( 'wheel', stopScroll ); + runScroll(); + } + + function getJSONAttribute( el, attr ) { + if ( ! el || ! el.hasAttribute( attr ) ) { + return undefined; } - // make this node a gallery recognizable by event listener above - $( container ).addClass( 'single-image-gallery' ); - // blog_id is needed to allow posting comments to correct blog - $( container ).data( 'carousel-extra', { - blog_id: Number( jetpackCarouselStrings.blog_id ), - } ); - } ); - }; - - var methods = { - testForData: function( gallery ) { - gallery = $( gallery ); // make sure we have it as a jQuery object. - return ! ( ! gallery.length || ! gallery.data( 'carousel-extra' ) ); - }, - - testIfOpened: function() { - return !! ( - 'undefined' !== typeof gallery && - 'undefined' !== typeof gallery.opened && - gallery.opened - ); - }, - - openOrSelectSlide: function( index ) { - // The `open` method triggers an asynchronous effect, so we will get an - // error if we try to use `open` then `selectSlideAtIndex` immediately - // after it. We can only use `selectSlideAtIndex` if the carousel is - // already open. - if ( ! $( this ).jp_carousel( 'testIfOpened' ) ) { - // The `open` method selects the correct slide during the - // initialization. - $( this ).jp_carousel( 'open', { start_index: index } ); - } else { - gallery.jp_carousel( 'selectSlideAtIndex', index ); + try { + return JSON.parse( el.getAttribute( attr ) ); + } catch ( e ) { + return undefined; } - }, + } - open: function( options ) { - var settings = { - items_selector: - '.gallery-item [data-attachment-id], .tiled-gallery-item [data-attachment-id], img[data-attachment-id]', - start_index: 0, - }, - data = $( this ).data( 'carousel-extra' ); + function convertToPlainText( html ) { + var dummy = document.createElement( 'div' ); + dummy.textContent = html; + return dummy.innerHTML; + } - if ( ! data ) { - return; // don't run if the default gallery functions weren't used + function stripHTML( text ) { + return text.replace( /<[^>]*>?/gm, '' ); + } + + return { + closest: closest, + matches: matches, + hide: hide, + show: show, + fadeIn: fadeIn, + fadeOut: fadeOut, + scrollToElement: scrollToElement, + getJSONAttribute: getJSONAttribute, + convertToPlainText: convertToPlainText, + stripHTML: stripHTML, + emitEvent: emitEvent, + isTouch: isTouch, + }; + } )(); + + ///////////////////////////////////// + // Carousel implementation + ///////////////////////////////////// + function init() { + var commentInterval; + var screenPadding; + var originalOverflow; + var originalHOverflow; + var scrollPos; + + var lastKnownLocationHash = ''; + var isUserTyping = false; + + var gallerySelector = + 'div.gallery, div.tiled-gallery, ul.wp-block-gallery, ul.blocks-gallery-grid, ' + + 'figure.wp-block-gallery.has-nested-images, div.wp-block-jetpack-tiled-gallery, a.single-image-gallery'; + + // Selector for items within a gallery or tiled gallery. + var galleryItemSelector = + '.gallery-item, .tiled-gallery-item, .blocks-gallery-item, ' + ' .tiled-gallery__item'; + + // Selector for all items including single images. + var itemSelector = galleryItemSelector + ', .wp-block-image'; + + var carousel = {}; + + var stat = + typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.stat + ? wpcom.carousel.stat + : util.noop; + + var pageview = + typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.pageview + ? wpcom.carousel.pageview + : util.noop; + + function handleKeyboardEvent( e ) { + if ( ! isUserTyping ) { + switch ( e.which ) { + case 38: // up + e.preventDefault(); + carousel.overlay.scrollTop -= 100; + break; + case 40: // down + e.preventDefault(); + carousel.overlay.scrollTop += 100; + break; + case 39: // right + e.preventDefault(); + swiper.slideNext(); + break; + case 37: // left + case 8: // backspace + e.preventDefault(); + swiper.slidePrev(); + break; + case 27: // escape + e.preventDefault(); + closeCarousel(); + break; + default: + break; + } } + } - prepareGallery( data ); + function disableKeyboardNavigation() { + isUserTyping = true; + } - if ( gallery.jp_carousel( 'testIfOpened' ) ) { - return; // don't open if already opened + function enableKeyboardNavigation() { + isUserTyping = false; + } + + function calculatePadding() { + var baseScreenPadding = 110; + screenPadding = baseScreenPadding; + + if ( window.innerWidth <= 760 ) { + screenPadding = Math.round( ( window.innerWidth / 760 ) * baseScreenPadding ); + + if ( screenPadding < 40 && domUtil.isTouch() ) { + screenPadding = 0; + } } + } - // make sure to stop the page from scrolling behind the carousel overlay, so we don't trigger - // infiniscroll for it when enabled (Reader, theme infiniscroll, etc). - originalOverflow = $( 'body' ).css( 'overflow' ); - $( 'body' ).css( 'overflow', 'hidden' ); - // prevent html from overflowing on some of the new themes. - originalHOverflow = $( 'html' ).css( 'overflow' ); - $( 'html' ).css( 'overflow', 'hidden' ); - scrollPos = $( window ).scrollTop(); + function initializeCarousel() { + if ( ! carousel.overlay ) { + carousel.overlay = document.querySelector( '.jp-carousel-overlay' ); + carousel.container = carousel.overlay.querySelector( '.jp-carousel-wrap' ); + carousel.gallery = carousel.container.querySelector( '.jp-carousel' ); + carousel.info = carousel.overlay.querySelector( '.jp-carousel-info' ); + carousel.caption = carousel.info.querySelector( '.jp-carousel-caption' ); + carousel.commentField = carousel.overlay.querySelector( + '#jp-carousel-comment-form-comment-field' + ); + carousel.emailField = carousel.overlay.querySelector( + '#jp-carousel-comment-form-email-field' + ); + carousel.authorField = carousel.overlay.querySelector( + '#jp-carousel-comment-form-author-field' + ); + carousel.urlField = carousel.overlay.querySelector( '#jp-carousel-comment-form-url-field' ); + + calculatePadding(); + + [ + carousel.commentField, + carousel.emailField, + carousel.authorField, + carousel.urlField, + ].forEach( function ( field ) { + if ( field ) { + field.addEventListener( 'focus', disableKeyboardNavigation ); + field.addEventListener( 'blur', enableKeyboardNavigation ); + } + } ); - container.data( 'carousel-extra', data ); + carousel.overlay.addEventListener( 'click', function ( e ) { + var target = e.target; + var isTargetCloseHint = !! domUtil.closest( target, '.jp-carousel-close-hint' ); + var isSmallScreen = !! window.matchMedia( '(max-device-width: 760px)' ).matches; + if ( target === carousel.overlay ) { + if ( isSmallScreen ) { + return; + } else { + closeCarousel(); + } + } else if ( isTargetCloseHint ) { + closeCarousel(); + } else if ( target.classList.contains( 'jp-carousel-image-download' ) ) { + stat( 'download_original_click' ); + } else if ( target.classList.contains( 'jp-carousel-comment-login' ) ) { + handleCommentLoginClick( e ); + } else if ( domUtil.closest( target, '#jp-carousel-comment-form-container' ) ) { + handleCommentFormClick( e ); + } else if ( + domUtil.closest( target, '.jp-carousel-photo-icons-container' ) || + target.classList.contains( 'jp-carousel-photo-title' ) + ) { + handleFooterElementClick( e ); + } else if ( ! domUtil.closest( target, '.jp-carousel-info' ) ) { + return; + } + } ); - return this.each( function() { - // If options exist, lets merge them - // with our default settings - var $this = $( this ); + window.addEventListener( 'keydown', handleKeyboardEvent ); - if ( options ) { - $.extend( settings, options ); - } - if ( -1 === settings.start_index ) { - settings.start_index = 0; //-1 returned if can't find index, so start from beginning - } + carousel.overlay.addEventListener( 'jp_carousel.afterOpen', function () { + enableKeyboardNavigation(); - container.trigger( 'jp_carousel.beforeOpen' ).fadeIn( 'fast', function() { - container.trigger( 'jp_carousel.afterOpen' ); - gallery - .jp_carousel( - 'initSlides', - $this.find( settings.items_selector ), - settings.start_index - ) - .jp_carousel( 'selectSlideAtIndex', settings.start_index ); + // Don't show navigation if there's only one image. + if ( carousel.slides.length <= 1 ) { + return; + } + // Show dot pagination if slide count is <= 5, otherwise show n/total. + if ( carousel.slides.length <= 5 ) { + domUtil.show( carousel.info.querySelector( '.jp-swiper-pagination' ) ); + } else { + domUtil.show( carousel.info.querySelector( '.jp-carousel-pagination' ) ); + } + } ); + + carousel.overlay.addEventListener( 'jp_carousel.beforeClose', function () { + disableKeyboardNavigation(); + + // Fixes some themes where closing carousel brings view back to top. + document.documentElement.style.removeProperty( 'height' ); + + // If we disable the swiper (because there's only one image) + // we have to re-enable it here again as Swiper doesn't, for some reason, + // show the navigation buttons again after reinitialization. + if ( swiper ) { + swiper.enable(); + } + + // Hide pagination. + domUtil.hide( carousel.info.querySelector( '.jp-swiper-pagination' ) ); + domUtil.hide( carousel.info.querySelector( '.jp-carousel-pagination' ) ); } ); - gallery.html( '' ); - } ); - }, - selectSlideAtIndex: function( index ) { - var slides = this.jp_carousel( 'slides' ), - selected = slides.eq( index ); + carousel.overlay.addEventListener( 'jp_carousel.afterClose', function () { + // don't force the browser back when the carousel closes. + if ( window.history.pushState ) { + history.pushState( + '', + document.title, + window.location.pathname + window.location.search + ); + } else { + window.location.href = ''; + } + lastKnownLocationHash = ''; + carousel.isOpen = false; + } ); - if ( 0 === selected.length ) { - selected = slides.eq( 0 ); + // Prevent native browser zooming + carousel.overlay.addEventListener( 'touchstart', function ( e ) { + if ( e.touches.length > 1 ) { + e.preventDefault(); + } + } ); } + } - gallery.jp_carousel( 'selectSlide', selected, false ); - return this; - }, + function handleCommentLoginClick() { + var slide = carousel.currentSlide; + var attachmentId = slide ? slide.attrs.attachmentId : '0'; - close: function() { - // make sure to let the page scroll again - $( 'body' ).css( 'overflow', originalOverflow ); - $( 'html' ).css( 'overflow', originalHOverflow ); - this.jp_carousel( 'clearCommentTextAreaValue' ); - return container.trigger( 'jp_carousel.beforeClose' ).fadeOut( 'fast', function() { - container.trigger( 'jp_carousel.afterClose' ); - $( window ).scrollTop( scrollPos ); - } ); - }, + window.location.href = jetpackCarouselStrings.login_url + '%23jp-carousel-' + attachmentId; + } - next: function() { - this.jp_carousel( 'previousOrNext', 'nextSlide' ); - }, + function updatePostResults( msg, isSuccess ) { + var results = carousel.overlay.querySelector( '#jp-carousel-comment-post-results' ); + var elClass = 'jp-carousel-comment-post-' + ( isSuccess ? 'success' : 'error' ); + results.innerHTML = '<span class="' + elClass + '">' + msg + '</span>'; + domUtil.hide( carousel.overlay.querySelector( '#jp-carousel-comment-form-spinner' ) ); + carousel.overlay + .querySelector( '#jp-carousel-comment-form' ) + .classList.remove( 'jp-carousel-is-disabled' ); + domUtil.show( results ); + } - previous: function() { - this.jp_carousel( 'previousOrNext', 'prevSlide' ); - }, + function handleCommentFormClick( e ) { + var target = e.target; + var data = domUtil.getJSONAttribute( carousel.container, 'data-carousel-extra' ) || {}; + var attachmentId = carousel.currentSlide.attrs.attachmentId; - previousOrNext: function( slideSelectionMethodName ) { - if ( ! this.jp_carousel( 'hasMultipleImages' ) ) { - return false; - } + var wrapper = document.querySelector( '#jp-carousel-comment-form-submit-and-info-wrapper' ); + var spinner = document.querySelector( '#jp-carousel-comment-form-spinner' ); + var submit = document.querySelector( '#jp-carousel-comment-form-button-submit' ); + var form = document.querySelector( '#jp-carousel-comment-form' ); - var slide = gallery.jp_carousel( slideSelectionMethodName ); + if ( + carousel.commentField && + carousel.commentField.getAttribute( 'id' ) === target.getAttribute( 'id' ) + ) { + // For first page load + disableKeyboardNavigation(); + domUtil.show( wrapper ); + } else if ( domUtil.matches( target, 'input[type="submit"]' ) ) { + e.preventDefault(); + e.stopPropagation(); + + domUtil.show( spinner ); + form.classList.add( 'jp-carousel-is-disabled' ); - if ( slide ) { - container.animate( { scrollTop: 0 }, 'fast' ); - this.jp_carousel( 'clearCommentTextAreaValue' ); - this.jp_carousel( 'selectSlide', slide ); + var ajaxData = { + action: 'post_attachment_comment', + nonce: jetpackCarouselStrings.nonce, + blog_id: data.blog_id, + id: attachmentId, + comment: carousel.commentField.value, + }; + + if ( ! ajaxData.comment.length ) { + updatePostResults( jetpackCarouselStrings.no_comment_text, false ); + return; + } + + if ( Number( jetpackCarouselStrings.is_logged_in ) !== 1 ) { + ajaxData.email = carousel.emailField.value; + ajaxData.author = carousel.authorField.value; + ajaxData.url = carousel.urlField.value; + + if ( Number( jetpackCarouselStrings.require_name_email ) === 1 ) { + if ( ! ajaxData.email.length || ! ajaxData.email.match( '@' ) ) { + updatePostResults( jetpackCarouselStrings.no_comment_email, false ); + return; + } else if ( ! ajaxData.author.length ) { + updatePostResults( jetpackCarouselStrings.no_comment_author, false ); + return; + } + } + } + + var xhr = new XMLHttpRequest(); + xhr.open( 'POST', jetpackCarouselStrings.ajaxurl, true ); + xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' ); + xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' ); + + xhr.onreadystatechange = function () { + if ( + this.readyState === XMLHttpRequest.DONE && + this.status >= 200 && + this.status < 300 + ) { + var response; + try { + response = JSON.parse( this.response ); + } catch ( error ) { + updatePostResults( jetpackCarouselStrings.comment_post_error, false ); + return; + } + if ( response.comment_status === 'approved' ) { + updatePostResults( jetpackCarouselStrings.comment_approved, true ); + } else if ( response.comment_status === 'unapproved' ) { + updatePostResults( jetpackCarouselStrings.comment_unapproved, true ); + } else { + // 'deleted', 'spam', false + updatePostResults( jetpackCarouselStrings.comment_post_error, false ); + } + clearCommentTextAreaValue(); + fetchComments( attachmentId ); + submit.value = jetpackCarouselStrings.post_comment; + domUtil.hide( spinner ); + form.classList.remove( 'jp-carousel-is-disabled' ); + } else { + // TODO: Add error handling and display here + updatePostResults( jetpackCarouselStrings.comment_post_error, false ); + } + }; + + var params = []; + for ( var item in ajaxData ) { + if ( item ) { + // Encode each form element into a URI-compatible string. + var encoded = encodeURIComponent( item ) + '=' + encodeURIComponent( ajaxData[ item ] ); + // In x-www-form-urlencoded, spaces should be `+`, not `%20`. + params.push( encoded.replace( /%20/g, '+' ) ); + } + } + var encodedData = params.join( '&' ); + + xhr.send( encodedData ); } - }, + } - selectedSlide: function() { - return this.find( '.selected' ); - }, + /** + * Handles clicks to icons and other action elements in the icon container. + * @param {MouseEvent|TouchEvent|KeyBoardEvent} Event object. + */ + function handleFooterElementClick( e ) { + e.preventDefault(); - setSlidePosition: function( x ) { - transitionBegin = Date.now(); + var target = e.target; + var extraInfoContainer = carousel.info.querySelector( '.jp-carousel-info-extra' ); + var photoMetaContainer = carousel.info.querySelector( '.jp-carousel-image-meta' ); + var commentsContainer = carousel.info.querySelector( '.jp-carousel-comments-wrapper' ); + var infoIcon = carousel.info.querySelector( '.jp-carousel-icon-info' ); + var commentsIcon = carousel.info.querySelector( '.jp-carousel-icon-comments' ); - return this.css( { - '-webkit-transform': 'translate3d(' + x + 'px,0,0)', - '-moz-transform': 'translate3d(' + x + 'px,0,0)', - '-ms-transform': 'translate(' + x + 'px,0)', - '-o-transform': 'translate(' + x + 'px,0)', - transform: 'translate3d(' + x + 'px,0,0)', - } ); - }, - - updateSlidePositions: function( animate ) { - var current = this.jp_carousel( 'selectedSlide' ), - galleryWidth = gallery.width(), - currentWidth = current.width(), - previous = gallery.jp_carousel( 'prevSlide' ), - next = gallery.jp_carousel( 'nextSlide' ), - previousPrevious = previous.prev(), - nextNext = next.next(), - left = Math.floor( ( galleryWidth - currentWidth ) * 0.5 ); - - current.jp_carousel( 'setSlidePosition', left ).show(); - - // minimum width - gallery.jp_carousel( 'fitInfo', animate ); - - // prep the slides - var direction = lastSelectedSlide.is( current.prevAll() ) ? 1 : -1; - - // Since we preload the `previousPrevious` and `nextNext` slides, we need - // to make sure they technically visible in the DOM, but invisible to the - // user. To hide them from the user, we position them outside the edges - // of the window. - // - // This section of code only applies when there are more than three - // slides. Otherwise, the `previousPrevious` and `nextNext` slides will - // overlap with the `previous` and `next` slides which must be visible - // regardless. - if ( 1 === direction ) { - if ( ! nextNext.is( previous ) ) { - nextNext.jp_carousel( 'setSlidePosition', galleryWidth + next.width() ).show(); + function handleInfoToggle() { + if ( commentsIcon ) { + commentsIcon.classList.remove( 'jp-carousel-selected' ); } + infoIcon.classList.toggle( 'jp-carousel-selected' ); - if ( ! previousPrevious.is( next ) ) { - previousPrevious - .jp_carousel( 'setSlidePosition', -previousPrevious.width() - currentWidth ) - .show(); + if ( commentsContainer ) { + commentsContainer.classList.remove( 'jp-carousel-show' ); } - } else { - if ( ! nextNext.is( previous ) ) { - nextNext.jp_carousel( 'setSlidePosition', galleryWidth + currentWidth ).show(); + if ( photoMetaContainer ) { + photoMetaContainer.classList.toggle( 'jp-carousel-show' ); + if ( photoMetaContainer.classList.contains( 'jp-carousel-show' ) ) { + extraInfoContainer.classList.add( 'jp-carousel-show' ); + } else { + extraInfoContainer.classList.remove( 'jp-carousel-show' ); + } } } - previous - .jp_carousel( 'setSlidePosition', Math.floor( -previous.width() + screenPadding * 0.75 ) ) - .show(); - next - .jp_carousel( 'setSlidePosition', Math.ceil( galleryWidth - screenPadding * 0.75 ) ) - .show(); - }, - - selectSlide: function( slide, animate ) { - lastSelectedSlide = this.find( '.selected' ).removeClass( 'selected' ); - - var slides = gallery.jp_carousel( 'slides' ).css( { position: 'fixed' } ), - current = $( slide ) - .addClass( 'selected' ) - .css( { position: 'relative' } ), - attachmentId = current.data( 'attachment-id' ), - previous = gallery.jp_carousel( 'prevSlide' ), - next = gallery.jp_carousel( 'nextSlide' ), - previousPrevious = previous.prev(), - nextNext = next.next(), - animated, - captionHtml; - - // center the main image - gallery.jp_carousel( 'loadFullImage', current ); - - caption.hide(); - - if ( next.length === 0 && slides.length <= 2 ) { - $( '.jp-carousel-next-button' ).hide(); - } else { - $( '.jp-carousel-next-button' ).show(); - } + function handleCommentToggle() { + if ( infoIcon ) { + infoIcon.classList.remove( 'jp-carousel-selected' ); + } + commentsIcon.classList.toggle( 'jp-carousel-selected' ); - if ( previous.length === 0 && slides.length <= 2 ) { - $( '.jp-carousel-previous-button' ).hide(); - } else { - $( '.jp-carousel-previous-button' ).show(); + if ( photoMetaContainer ) { + photoMetaContainer.classList.remove( 'jp-carousel-show' ); + } + if ( commentsContainer ) { + commentsContainer.classList.toggle( 'jp-carousel-show' ); + if ( commentsContainer.classList.contains( 'jp-carousel-show' ) ) { + extraInfoContainer.classList.add( 'jp-carousel-show' ); + } else { + extraInfoContainer.classList.remove( 'jp-carousel-show' ); + } + } } - animated = current - .add( previous ) - .add( previousPrevious ) - .add( next ) - .add( nextNext ) - .jp_carousel( 'loadSlide' ); + if ( + domUtil.closest( target, '.jp-carousel-icon-info' ) || + target.classList.contains( 'jp-carousel-photo-title' ) + ) { + if ( photoMetaContainer && photoMetaContainer.classList.contains( 'jp-carousel-show' ) ) { + domUtil.scrollToElement( carousel.overlay, carousel.overlay, handleInfoToggle ); + } else { + handleInfoToggle(); + domUtil.scrollToElement( carousel.info, carousel.overlay ); + } + } - // slide the whole view to the x we want - slides.not( animated ).hide(); + if ( domUtil.closest( target, '.jp-carousel-icon-comments' ) ) { + if ( commentsContainer && commentsContainer.classList.contains( 'jp-carousel-show' ) ) { + domUtil.scrollToElement( carousel.overlay, carousel.overlay, handleCommentToggle ); + } else { + handleCommentToggle(); + domUtil.scrollToElement( carousel.info, carousel.overlay ); + } + } + } - gallery.jp_carousel( 'updateSlidePositions', animate ); + function processSingleImageGallery() { + var images = document.querySelectorAll( 'a img[data-attachment-id]' ); + Array.prototype.forEach.call( images, function ( image ) { + var link = image.parentElement; + var container = link.parentElement; - container.trigger( 'jp_carousel.selectSlide', [ current ] ); + // Skip if image was already added to gallery by shortcode. + if ( container.classList.contains( 'gallery-icon' ) ) { + return; + } - gallery.jp_carousel( 'getTitleDesc', { - title: current.data( 'title' ), - desc: current.data( 'desc' ), - } ); + // Skip if image is part of a gallery. + if ( domUtil.closest( container, galleryItemSelector ) ) { + return; + } - var imageMeta = current.data( 'image-meta' ); - gallery.jp_carousel( 'updateExif', imageMeta ); - gallery.jp_carousel( 'updateFullSizeLink', current ); - gallery.jp_carousel( 'updateMap', imageMeta ); - gallery.jp_carousel( 'testCommentsOpened', current.data( 'comments-opened' ) ); - gallery.jp_carousel( 'getComments', { - attachment_id: attachmentId, - offset: 0, - clear: true, - } ); - $( '#jp-carousel-comment-post-results' ).slideUp(); + // Skip if the parent is not actually a link. + if ( ! link.hasAttribute( 'href' ) ) { + return; + } - // $('<div />').text(sometext).html() is a trick to go to HTML to plain - // text (including HTML entities decode, etc) - if ( current.data( 'caption' ) ) { - captionHtml = $( '<div />' ) - .text( current.data( 'caption' ) ) - .html(); + var valid = false; + // If link points to 'Media File' (ignoring GET parameters) and flag is set, allow it. if ( - captionHtml === - $( '<div />' ) - .text( current.data( 'title' ) ) - .html() + link.getAttribute( 'href' ).split( '?' )[ 0 ] === + image.getAttribute( 'data-orig-file' ).split( '?' )[ 0 ] && + Number( jetpackCarouselStrings.single_image_gallery_media_file ) === 1 ) { - $( '.jp-carousel-titleanddesc-title' ) - .fadeOut( 'fast' ) - .empty(); + valid = true; } - if ( - captionHtml === - $( '<div />' ) - .text( current.data( 'desc' ) ) - .html() - ) { - $( '.jp-carousel-titleanddesc-desc' ) - .fadeOut( 'fast' ) - .empty(); + // If link points to 'Attachment Page', allow it. + if ( link.getAttribute( 'href' ) === image.getAttribute( 'data-permalink' ) ) { + valid = true; } - caption.html( current.data( 'caption' ) ).fadeIn( 'slow' ); - } else { - caption.fadeOut( 'fast' ).empty(); - } - - // Record pageview in WP Stats, for each new image loaded full-screen. - if ( jetpackCarouselStrings.stats ) { - new Image().src = - document.location.protocol + - '//pixel.wp.com/g.gif?' + - jetpackCarouselStrings.stats + - '&post=' + - encodeURIComponent( attachmentId ) + - '&rand=' + - Math.random(); - } - - // Load the images for the next and previous slides. - $( next ) - .add( previous ) - .each( function() { - gallery.jp_carousel( 'loadFullImage', $( this ) ); - } ); + // Links to 'Custom URL' or 'Media File' when flag is not set are not valid. + if ( ! valid ) { + return; + } - window.location.hash = last_known_location_hash = '#jp-carousel-' + attachmentId; - }, + // Make this node a gallery recognizable by event listener above. + link.classList.add( 'single-image-gallery' ); + // blog_id is needed to allow posting comments to correct blog. + link.setAttribute( + 'data-carousel-extra', + JSON.stringify( { + blog_id: Number( jetpackCarouselStrings.blog_id ), + } ) + ); + } ); + } - slides: function() { - return this.find( '.jp-carousel-slide' ); - }, + function testForData( el ) { + return !! ( el && el.getAttribute( 'data-carousel-extra' ) ); + } - slideDimensions: function() { - return { - width: $( window ).width() - screenPadding * 2, - height: Math.floor( ( $( window ).height() / 100 ) * proportion - 60 ), - }; - }, - - loadSlide: function() { - return this.each( function() { - var slide = $( this ); - slide.find( 'img' ).one( 'load', function() { - // set the width/height of the image if it's too big - slide.jp_carousel( 'fitSlide', false ); - } ); - } ); - }, - - bestFit: function() { - var max = gallery.jp_carousel( 'slideDimensions' ), - orig = this.jp_carousel( 'originalDimensions' ), - orig_ratio = orig.width / orig.height, - w_ratio = 1, - h_ratio = 1, - width, - height; - - if ( orig.width > max.width ) { - w_ratio = max.width / orig.width; - } - if ( orig.height > max.height ) { - h_ratio = max.height / orig.height; - } - - if ( w_ratio < h_ratio ) { - width = max.width; - height = Math.floor( width / orig_ratio ); - } else if ( h_ratio < w_ratio ) { - height = max.height; - width = Math.floor( height * orig_ratio ); + function openOrSelectSlide( gal, index ) { + if ( ! carousel.isOpen ) { + // The `open` method selects the correct slide during the initialization. + loadSwiper( gal, { startIndex: index } ); } else { - width = orig.width; - height = orig.height; + selectSlideAtIndex( index ); + // We have to force swiper to slide to the index onHasChange. + swiper.slideTo( index + 1 ); } + } - return { - width: width, - height: height, - }; - }, - - fitInfo: function(/*animated*/) { - var current = this.jp_carousel( 'selectedSlide' ), - size = current.jp_carousel( 'bestFit' ); + function selectSlideAtIndex( index ) { + if ( ! index || index < 0 || index > carousel.slides.length ) { + index = 0; + } + carousel.currentSlide = carousel.slides[ index ]; - photo_info.css( { - left: Math.floor( ( info.width() - size.width ) * 0.5 ), - width: Math.floor( size.width ), - } ); + var current = carousel.currentSlide; + var attachmentId = current.attrs.attachmentId; + var infoIcon = carousel.info.querySelector( '.jp-carousel-icon-info' ); + var commentsIcon = carousel.info.querySelector( '.jp-carousel-icon-comments' ); - return this; - }, + // If the comment/info section is toggled open, it's kept open, but scroll to top of the next slide. + if ( + ( infoIcon && infoIcon.classList.contains( 'jp-carousel-selected' ) ) || + ( commentsIcon && commentsIcon.classList.contains( 'jp-carousel-selected' ) ) + ) { + if ( carousel.overlay.scrollTop !== 0 ) { + domUtil.scrollToElement( carousel.overlay, carousel.overlay ); + } + } - fitMeta: function( animated ) { - var newInfoTop = { - top: Math.floor( ( $( window ).height() / 100 ) * proportion + 5 ) + 'px', - }; - var newLeftWidth = { width: info.width() - ( imageMeta.width() + 80 ) + 'px' }; + loadFullImage( carousel.slides[ index ] ); - if ( animated ) { - info.animate( newInfoTop ); - leftColWrapper.animate( newLeftWidth ); - } else { - info.animate( newInfoTop ); - leftColWrapper.css( newLeftWidth ); + if ( + Number( jetpackCarouselStrings.display_background_image ) === 1 && + ! carousel.slides[ index ].backgroundImage + ) { + loadBackgroundImage( carousel.slides[ index ] ); } - }, - - fitSlide: function(/*animated*/) { - return this.each( function() { - var $this = $( this ), - dimensions = $this.jp_carousel( 'bestFit' ), - method = 'css', - max = gallery.jp_carousel( 'slideDimensions' ); - dimensions.left = 0; - dimensions.top = Math.floor( ( max.height - dimensions.height ) * 0.5 ) + 40; - $this[ method ]( dimensions ); + domUtil.hide( carousel.caption ); + updateTitleCaptionAndDesc( { + caption: current.attrs.caption, + title: current.attrs.title, + desc: current.attrs.desc, } ); - }, - texturize: function( text ) { - text = '' + text; // make sure we get a string. Title "1" came in as int 1, for example, which did not support .replace(). - text = text - .replace( /'/g, '’' ) - .replace( /'/g, '’' ) - .replace( /[\u2019]/g, '’' ); - text = text - .replace( /"/g, '”' ) - .replace( /"/g, '”' ) - .replace( /"/g, '”' ) - .replace( /[\u201D]/g, '”' ); - text = text.replace( /([\w]+)=&#[\d]+;(.+?)&#[\d]+;/g, '$1="$2"' ); // untexturize allowed HTML tags params double-quotes - return $.trim( text ); - }, + var imageMeta = carousel.slides[ index ].attrs.imageMeta; + updateExif( imageMeta ); + updateFullSizeLink( current ); - initSlides: function( items, start_index ) { - if ( items.length < 2 ) { - $( '.jp-carousel-next-button, .jp-carousel-previous-button' ).hide(); - } else { - $( '.jp-carousel-next-button, .jp-carousel-previous-button' ).show(); - } - - // Calculate the new src. - items.each( function(/*i*/) { - var src_item = $( this ), - orig_size = src_item.data( 'orig-size' ) || '', - max = gallery.jp_carousel( 'slideDimensions' ), - parts = orig_size.split( ',' ), - medium_file = src_item.data( 'medium-file' ) || '', - large_file = src_item.data( 'large-file' ) || '', - src; - orig_size = { width: parseInt( parts[ 0 ], 10 ), height: parseInt( parts[ 1 ], 10 ) }; - - src = src_item.data( 'orig-file' ); - - src = gallery.jp_carousel( 'selectBestImageSize', { - orig_file: src, - orig_width: orig_size.width, - orig_height: orig_size.height, - max_width: max.width, - max_height: max.height, - medium_file: medium_file, - large_file: large_file, - } ); + if ( Number( jetpackCarouselStrings.display_comments ) === 1 ) { + testCommentsOpened( carousel.slides[ index ].attrs.commentsOpened ); + fetchComments( attachmentId ); + domUtil.hide( carousel.info.querySelector( '#jp-carousel-comment-post-results' ) ); + } - // Set the final src - $( this ).data( 'gallery-src', src ); - } ); + // Update pagination in footer. + var pagination = carousel.info.querySelector( '.jp-carousel-pagination' ); + if ( pagination && carousel.slides.length > 5 ) { + var currentPage = index + 1; + pagination.innerHTML = '<span>' + currentPage + ' / ' + carousel.slides.length + '</span>'; + } - // If the start_index is not 0 then preload the clicked image first. - if ( 0 !== start_index ) { - $( '<img/>' )[ 0 ].src = $( items[ start_index ] ).data( 'gallery-src' ); + // Record pageview in WP Stats, for each new image loaded full-screen. + if ( jetpackCarouselStrings.stats ) { + new Image().src = + document.location.protocol + + '//pixel.wp.com/g.gif?' + + jetpackCarouselStrings.stats + + '&post=' + + encodeURIComponent( attachmentId ) + + '&rand=' + + Math.random(); } - var useInPageThumbnails = - items.first().closest( '.tiled-gallery.type-rectangular' ).length > 0; + pageview( attachmentId ); - // create the 'slide' - items.each( function( i ) { - var src_item = $( this ), - attachment_id = src_item.data( 'attachment-id' ) || 0, - comments_opened = src_item.data( 'comments-opened' ) || 0, - image_meta = src_item.data( 'image-meta' ) || {}, - orig_size = src_item.data( 'orig-size' ) || '', - thumb_size = { width: src_item[ 0 ].naturalWidth, height: src_item[ 0 ].naturalHeight }, - title = src_item.data( 'image-title' ) || '', - description = src_item.data( 'image-description' ) || '', - caption = - src_item - .parents( '.gallery-item' ) - .find( '.gallery-caption' ) - .html() || '', - src = src_item.data( 'gallery-src' ) || '', - medium_file = src_item.data( 'medium-file' ) || '', - large_file = src_item.data( 'large-file' ) || '', - orig_file = src_item.data( 'orig-file' ) || ''; - - var tiledCaption = src_item - .parents( 'div.tiled-gallery-item' ) - .find( 'div.tiled-gallery-caption' ) - .html(); - if ( tiledCaption ) { - caption = tiledCaption; - } + window.location.hash = lastKnownLocationHash = '#jp-carousel-' + attachmentId; + } - if ( attachment_id && orig_size.length ) { - title = gallery.jp_carousel( 'texturize', title ); - description = gallery.jp_carousel( 'texturize', description ); - caption = gallery.jp_carousel( 'texturize', caption ); - - // Initially, the image is a 1x1 transparent gif. The preview is shown as a background image on the slide itself. - var image = $( '<img/>' ) - .attr( - 'src', - 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' - ) - .css( 'width', '100%' ) - .css( 'height', '100%' ); - - var slide = $( - '<div class="jp-carousel-slide" itemprop="associatedMedia" itemscope itemtype="https://schema.org/ImageObject"></div>' - ) - .hide() - .css( { - //'position' : 'fixed', - left: i < start_index ? -1000 : gallery.width(), - } ) - .append( image ) - .appendTo( gallery ) - .data( 'src', src ) - .data( 'title', title ) - .data( 'desc', description ) - .data( 'caption', caption ) - .data( 'attachment-id', attachment_id ) - .data( 'permalink', src_item.parents( 'a' ).attr( 'href' ) ) - .data( 'orig-size', orig_size ) - .data( 'comments-opened', comments_opened ) - .data( 'image-meta', image_meta ) - .data( 'medium-file', medium_file ) - .data( 'large-file', large_file ) - .data( 'orig-file', orig_file ) - .data( 'thumb-size', thumb_size ); - if ( useInPageThumbnails ) { - // Use the image already loaded in the gallery as a preview. - slide.data( 'preview-image', src_item.attr( 'src' ) ).css( { - 'background-image': 'url("' + src_item.attr( 'src' ) + '")', - 'background-size': '100% 100%', - 'background-position': 'center center', - } ); - } + function restoreScroll() { + window.scrollTo( window.scrollX || window.pageXOffset || 0, scrollPos || 0 ); + } - slide.jp_carousel( 'fitSlide', false ); - } + function closeCarousel() { + // Make sure to let the page scroll again. + document.body.style.overflow = originalOverflow; + document.documentElement.style.overflow = originalHOverflow; + clearCommentTextAreaValue(); + + disableKeyboardNavigation(); + + domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeClose' ); + restoreScroll(); + swiper.destroy(); + carousel.isOpen = false; + // Clear slide data for DOM garbage collection. + carousel.slides = []; + carousel.currentSlide = undefined; + carousel.gallery.innerHTML = ''; + + domUtil.fadeOut( carousel.overlay, function () { + domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterClose' ); } ); - return this; - }, + } + + function calculateMaxSlideDimensions() { + return { + width: window.innerWidth, + height: window.innerHeight - 64, //subtract height of bottom info bar, + }; + } - selectBestImageSize: function( args ) { - if ( 'object' !== typeof args ) { + function selectBestImageUrl( args ) { + if ( typeof args !== 'object' ) { args = {}; } - if ( 'undefined' === typeof args.orig_file ) { + if ( typeof args.origFile === 'undefined' ) { return ''; } - if ( 'undefined' === typeof args.orig_width || 'undefined' === typeof args.max_width ) { - return args.orig_file; + if ( typeof args.origWidth === 'undefined' || typeof args.maxWidth === 'undefined' ) { + return args.origFile; } - if ( 'undefined' === typeof args.medium_file || 'undefined' === typeof args.large_file ) { - return args.orig_file; + if ( typeof args.mediumFile === 'undefined' || typeof args.largeFile === 'undefined' ) { + return args.origFile; } // Check if the image is being served by Photon (using a regular expression on the hostname). var imageLinkParser = document.createElement( 'a' ); - imageLinkParser.href = args.large_file; + imageLinkParser.href = args.largeFile; - var isPhotonUrl = /^i[0-2].wp.com$/i.test( imageLinkParser.hostname ); + var isPhotonUrl = /^i[0-2]\.wp\.com$/i.test( imageLinkParser.hostname ); - var medium_size_parts = gallery.jp_carousel( - 'getImageSizeParts', - args.medium_file, - args.orig_width, - isPhotonUrl - ); - var large_size_parts = gallery.jp_carousel( - 'getImageSizeParts', - args.large_file, - args.orig_width, - isPhotonUrl - ); + var mediumSizeParts = getImageSizeParts( args.mediumFile, args.origWidth, isPhotonUrl ); + var largeSizeParts = getImageSizeParts( args.largeFile, args.origWidth, isPhotonUrl ); - var large_width = parseInt( large_size_parts[ 0 ], 10 ), - large_height = parseInt( large_size_parts[ 1 ], 10 ), - medium_width = parseInt( medium_size_parts[ 0 ], 10 ), - medium_height = parseInt( medium_size_parts[ 1 ], 10 ); + var largeWidth = parseInt( largeSizeParts[ 0 ], 10 ); + var largeHeight = parseInt( largeSizeParts[ 1 ], 10 ); + var mediumWidth = parseInt( mediumSizeParts[ 0 ], 10 ); + var mediumHeight = parseInt( mediumSizeParts[ 1 ], 10 ); - // Assign max width and height. - args.orig_max_width = args.max_width; - args.orig_max_height = args.max_height; + args.origMaxWidth = args.maxWidth; + args.origMaxHeight = args.maxHeight; // Give devices with a higher devicePixelRatio higher-res images (Retina display = 2, Android phones = 1.5, etc) - if ( 'undefined' !== typeof window.devicePixelRatio && window.devicePixelRatio > 1 ) { - args.max_width = args.max_width * window.devicePixelRatio; - args.max_height = args.max_height * window.devicePixelRatio; + if ( typeof window.devicePixelRatio !== 'undefined' && window.devicePixelRatio > 1 ) { + args.maxWidth = args.maxWidth * window.devicePixelRatio; + args.maxHeight = args.maxHeight * window.devicePixelRatio; } - if ( large_width >= args.max_width || large_height >= args.max_height ) { - return args.large_file; + if ( largeWidth >= args.maxWidth || largeHeight >= args.maxHeight ) { + return args.largeFile; } - if ( medium_width >= args.max_width || medium_height >= args.max_height ) { - return args.medium_file; + if ( mediumWidth >= args.maxWidth || mediumHeight >= args.maxHeight ) { + return args.mediumFile; } if ( isPhotonUrl ) { - // args.orig_file doesn't point to a Photon url, so in this case we use args.large_file + // args.origFile doesn't point to a Photon url, so in this case we use args.largeFile // to return the photon url of the original image. - var largeFileIndex = args.large_file.lastIndexOf( '?' ); - var origPhotonUrl = args.large_file; - if ( -1 !== largeFileIndex ) { - origPhotonUrl = args.large_file.substring( 0, largeFileIndex ); + var largeFileIndex = args.largeFile.lastIndexOf( '?' ); + var origPhotonUrl = args.largeFile; + if ( largeFileIndex !== -1 ) { + origPhotonUrl = args.largeFile.substring( 0, largeFileIndex ); // If we have a really large image load a smaller version // that is closer to the viewable size - if ( args.orig_width > args.max_width || args.orig_height > args.max_height ) { - origPhotonUrl += '?fit=' + args.orig_max_width + '%2C' + args.orig_max_height; + if ( args.origWidth > args.maxWidth || args.origHeight > args.maxHeight ) { + // @2x the max sizes so we get a high enough resolution for zooming. + args.origMaxWidth = args.maxWidth * 2; + args.origMaxHeight = args.maxHeight * 2; + origPhotonUrl += '?fit=' + args.origMaxWidth + '%2C' + args.origMaxHeight; } } return origPhotonUrl; } - return args.orig_file; - }, + return args.origFile; + } - getImageSizeParts: function( file, orig_width, isPhotonUrl ) { + function getImageSizeParts( file, origWidth, isPhotonUrl ) { var size = isPhotonUrl ? file.replace( /.*=([\d]+%2C[\d]+).*$/, '$1' ) : file.replace( /.*-([\d]+x[\d]+)\..+$/, '$1' ); - var size_parts = + var sizeParts = size !== file ? isPhotonUrl ? size.split( '%2C' ) : size.split( 'x' ) - : [ orig_width, 0 ]; + : [ origWidth, 0 ]; // If one of the dimensions is set to 9999, then the actual value of that dimension can't be retrieved from the url. // In that case, we set the value to 0. - if ( '9999' === size_parts[ 0 ] ) { - size_parts[ 0 ] = '0'; + if ( sizeParts[ 0 ] === '9999' ) { + sizeParts[ 0 ] = '0'; } - if ( '9999' === size_parts[ 1 ] ) { - size_parts[ 1 ] = '0'; + if ( sizeParts[ 1 ] === '9999' ) { + sizeParts[ 1 ] = '0'; } - return size_parts; - }, - - originalDimensions: function() { - var splitted = $( this ) - .data( 'orig-size' ) - .split( ',' ); - return { width: parseInt( splitted[ 0 ], 10 ), height: parseInt( splitted[ 1 ], 10 ) }; - }, - - format: function( args ) { - if ( 'object' !== typeof args ) { - args = {}; - } - if ( ! args.text || 'undefined' === typeof args.text ) { - return; - } - if ( ! args.replacements || 'undefined' === typeof args.replacements ) { - return args.text; - } - return args.text.replace( /{(\d+)}/g, function( match, number ) { - return typeof args.replacements[ number ] !== 'undefined' - ? args.replacements[ number ] - : match; - } ); - }, + return sizeParts; + } /** * Returns a number in a fraction format that represents the shutter speed. * @param Number speed * @return String */ - shutterSpeed: function( speed ) { + function formatShutterSpeed( speed ) { var denominator; // round to one decimal if value > 1s by multiplying it by 10, rounding, then dividing by 10 again @@ -1266,70 +982,102 @@ jQuery( document ).ready( function( $ ) { denominator = Math.round( 1 / speed ); return '1/' + denominator + 's'; - }, + } - parseTitleDesc: function( value ) { + function parseTitleOrDesc( value ) { if ( ! value.match( ' ' ) && value.match( '_' ) ) { return ''; } return value; - }, + } - getTitleDesc: function( data ) { - var title = '', - desc = '', - markup = '', - target; + function updateTitleCaptionAndDesc( data ) { + var caption = ''; + var title = ''; + var desc = ''; + var captionMainElement; + var captionInfoExtraElement; + var titleElement; + var descriptionElement; + + captionMainElement = carousel.overlay.querySelector( '.jp-carousel-photo-caption' ); + captionInfoExtraElement = carousel.overlay.querySelector( '.jp-carousel-caption' ); + + titleElement = carousel.overlay.querySelector( '.jp-carousel-photo-title' ); + descriptionElement = carousel.overlay.querySelector( '.jp-carousel-photo-description' ); + + domUtil.hide( captionMainElement ); + domUtil.hide( captionInfoExtraElement ); + domUtil.hide( titleElement ); + domUtil.hide( descriptionElement ); + + caption = parseTitleOrDesc( data.caption ) || ''; + title = parseTitleOrDesc( data.title ) || ''; + desc = parseTitleOrDesc( data.desc ) || ''; + + if ( caption || title || desc ) { + if ( caption ) { + captionMainElement.innerHTML = caption; + captionInfoExtraElement.innerHTML = caption; + + domUtil.show( captionMainElement ); + domUtil.show( captionInfoExtraElement ); + } - target = $( 'div.jp-carousel-titleanddesc', 'div.jp-carousel-wrap' ); - target.hide(); + if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( title ) ) { + title = ''; + } - title = gallery.jp_carousel( 'parseTitleDesc', data.title ) || ''; - desc = gallery.jp_carousel( 'parseTitleDesc', data.desc ) || ''; + if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( desc ) ) { + desc = ''; + } - if ( title.length || desc.length ) { - // Convert from HTML to plain text (including HTML entities decode, etc) - if ( - $( '<div />' ) - .html( title ) - .text() === - $( '<div />' ) - .html( desc ) - .text() - ) { - title = ''; + if ( domUtil.stripHTML( title ) === domUtil.stripHTML( desc ) ) { + desc = ''; } - markup = title.length - ? '<div class="jp-carousel-titleanddesc-title">' + title + '</div>' - : ''; - markup += desc.length - ? '<div class="jp-carousel-titleanddesc-desc">' + desc + '</div>' - : ''; + if ( desc ) { + descriptionElement.innerHTML = desc; + domUtil.show( descriptionElement ); - target.html( markup ).fadeIn( 'slow' ); - } + if ( ! title && ! caption ) { + captionMainElement.innerHTML = domUtil.stripHTML( desc ); + domUtil.show( captionMainElement ); + } + } + + if ( title ) { + var plainTitle = domUtil.stripHTML( title ); + titleElement.innerHTML = plainTitle; + + if ( ! caption ) { + captionMainElement.innerHTML = plainTitle; + captionInfoExtraElement.innerHTML = plainTitle; + + domUtil.show( captionMainElement ); + } - $( 'div#jp-carousel-comment-form-container' ).css( 'margin-top', '20px' ); - $( 'div#jp-carousel-comments-loading' ).css( 'margin-top', '20px' ); - }, + domUtil.show( titleElement ); + } + } + } // updateExif updates the contents of the exif UL (.jp-carousel-image-exif) - updateExif: function( meta ) { - if ( ! meta || 1 !== Number( jetpackCarouselStrings.display_exif ) ) { + function updateExif( meta ) { + if ( ! meta || Number( jetpackCarouselStrings.display_exif ) !== 1 ) { return false; } - var $ul = $( "<ul class='jp-carousel-image-exif'></ul>" ); + var ul = carousel.info.querySelector( '.jp-carousel-image-meta ul.jp-carousel-image-exif' ); + var html = ''; - $.each( meta, function( key, val ) { - if ( - 0 === parseFloat( val ) || - ! val.length || - -1 === $.inArray( key, $.makeArray( jetpackCarouselStrings.meta_data ) ) - ) { - return; + for ( var key in meta ) { + var val = meta[ key ]; + var metaKeys = jetpackCarouselStrings.meta_data || []; + + if ( parseFloat( val ) === 0 || ! val.length || metaKeys.indexOf( key ) === -1 ) { + continue; } switch ( key ) { @@ -1337,517 +1085,635 @@ jQuery( document ).ready( function( $ ) { val = val + 'mm'; break; case 'shutter_speed': - val = gallery.jp_carousel( 'shutterSpeed', val ); + val = formatShutterSpeed( val ); break; case 'aperture': val = 'f/' + val; break; } - $ul.append( '<li><h5>' + jetpackCarouselStrings[ key ] + '</h5>' + val + '</li>' ); - } ); + html += '<li><h5>' + jetpackCarouselStrings[ key ] + '</h5>' + val + '</li>'; + } - // Update (replace) the content of the ul - $( 'div.jp-carousel-image-meta ul.jp-carousel-image-exif' ).replaceWith( $ul ); - }, + ul.innerHTML = html; + ul.style.removeProperty( 'display' ); + } - // updateFullSizeLink updates the contents of the jp-carousel-image-download link - updateFullSizeLink: function( current ) { - if ( ! current || ! current.data ) { + // Update the contents of the jp-carousel-image-download link + function updateFullSizeLink( currentSlide ) { + if ( ! currentSlide ) { return false; } - var original, - origSize = current.data( 'orig-size' ).split( ',' ), - imageLinkParser = document.createElement( 'a' ); + var original; + var origSize = [ currentSlide.attrs.origWidth, currentSlide.attrs.origHeight ]; + var imageLinkParser = document.createElement( 'a' ); - imageLinkParser.href = current.data( 'src' ).replace( /\?.+$/, '' ); + imageLinkParser.href = currentSlide.attrs.src.replace( /\?.+$/, '' ); // Is this a Photon URL? - if ( imageLinkParser.hostname.match( /^i[\d]{1}.wp.com$/i ) !== null ) { + if ( imageLinkParser.hostname.match( /^i[\d]{1}\.wp\.com$/i ) !== null ) { original = imageLinkParser.href; } else { - original = current.data( 'orig-file' ).replace( /\?.+$/, '' ); - } - - var permalink = $( - '<a>' + - gallery.jp_carousel( 'format', { - text: jetpackCarouselStrings.download_original, - replacements: origSize, - } ) + - '</a>' - ) - .addClass( 'jp-carousel-image-download' ) - .attr( 'href', original ) - .attr( 'target', '_blank' ); - - // Update (replace) the content of the anchor - $( 'div.jp-carousel-image-meta a.jp-carousel-image-download' ).replaceWith( permalink ); - }, - - updateMap: function( meta ) { - if ( - ! meta.latitude || - ! meta.longitude || - 1 !== Number( jetpackCarouselStrings.display_geo ) - ) { - return; + original = currentSlide.attrs.origFile.replace( /\?.+$/, '' ); } - var latitude = meta.latitude, - longitude = meta.longitude, - $metabox = $( 'div.jp-carousel-image-meta', 'div.jp-carousel-wrap' ), - $mapbox = $( '<div></div>' ), - style = - '&scale=2&style=feature:all|element:all|invert_lightness:true|hue:0x0077FF|saturation:-50|lightness:-5|gamma:0.91'; - - $mapbox - .addClass( 'jp-carousel-image-map' ) - .html( - '<img width="154" height="154" src="https://maps.googleapis.com/maps/api/staticmap?\ - center=' + - latitude + - ',' + - longitude + - '&\ - zoom=8&\ - size=154x154&\ - sensor=false&\ - markers=size:medium%7Ccolor:blue%7C' + - latitude + - ',' + - longitude + - style + - '" class="gmap-main" />\ - \ - <div class="gmap-topright"><div class="imgclip"><img width="175" height="154" src="https://maps.googleapis.com/maps/api/staticmap?\ - center=' + - latitude + - ',' + - longitude + - '&\ - zoom=3&\ - size=175x154&\ - sensor=false&\ - markers=size:small%7Ccolor:blue%7C' + - latitude + - ',' + - longitude + - style + - '"c /></div></div>\ - \ - ' - ) - .prependTo( $metabox ); - }, - - testCommentsOpened: function( opened ) { - if ( 1 === parseInt( opened, 10 ) ) { - $( '.jp-carousel-buttons' ).fadeIn( 'fast' ); - commentForm.fadeIn( 'fast' ); + var downloadText = carousel.info.querySelector( '.jp-carousel-download-text' ); + var permalink = carousel.info.querySelector( '.jp-carousel-image-download' ); + + downloadText.innerHTML = util.applyReplacements( + jetpackCarouselStrings.download_original, + origSize + ); + permalink.setAttribute( 'href', original ); + permalink.style.removeProperty( 'display' ); + } + + function testCommentsOpened( opened ) { + var commentForm = carousel.container.querySelector( '.jp-carousel-comment-form-container' ); + var isOpened = parseInt( opened, 10 ) === 1; + + if ( isOpened ) { + domUtil.fadeIn( commentForm ); } else { - $( '.jp-carousel-buttons' ).fadeOut( 'fast' ); - commentForm.fadeOut( 'fast' ); + domUtil.fadeOut( commentForm ); } - }, + } + + function fetchComments( attachmentId, offset ) { + var shouldClear = offset === undefined; + var commentsIndicator = carousel.info.querySelector( + '.jp-carousel-icon-comments .jp-carousel-has-comments-indicator' + ); + + commentsIndicator.classList.remove( 'jp-carousel-show' ); - getComments: function( args ) { clearInterval( commentInterval ); - if ( 'object' !== typeof args ) { + if ( ! attachmentId ) { return; } - if ( 'undefined' === typeof args.attachment_id || ! args.attachment_id ) { - return; + if ( ! offset || offset < 1 ) { + offset = 0; } - if ( ! args.offset || 'undefined' === typeof args.offset || args.offset < 1 ) { - args.offset = 0; + var comments = carousel.info.querySelector( '.jp-carousel-comments' ); + var commentsLoading = carousel.info.querySelector( '#jp-carousel-comments-loading' ); + domUtil.show( commentsLoading ); + + if ( shouldClear ) { + domUtil.hide( comments ); + comments.innerHTML = ''; } - var comments = $( '.jp-carousel-comments' ), - commentsLoading = $( '#jp-carousel-comments-loading' ).show(); + var xhr = new XMLHttpRequest(); + var url = + jetpackCarouselStrings.ajaxurl + + '?action=get_attachment_comments' + + '&nonce=' + + jetpackCarouselStrings.nonce + + '&id=' + + attachmentId + + '&offset=' + + offset; + xhr.open( 'GET', url ); + xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' ); - if ( args.clear ) { - comments.hide().empty(); - } + var onError = function () { + domUtil.fadeIn( comments ); + domUtil.fadeOut( commentsLoading ); + }; - $.ajax( { - type: 'GET', - url: jetpackCarouselStrings.ajaxurl, - dataType: 'json', - data: { - action: 'get_attachment_comments', - nonce: jetpackCarouselStrings.nonce, - id: args.attachment_id, - offset: args.offset, - }, - success: function( data /*, status, xhr*/ ) { - if ( args.clear ) { - comments.fadeOut( 'fast' ).empty(); - } + xhr.onload = function () { + // Ignore the results if they arrive late and we're now on a different slide. + if ( + ! carousel.currentSlide || + carousel.currentSlide.attrs.attachmentId !== attachmentId + ) { + return; + } - $( data ).each( function() { - var comment = $( '<div></div>' ) - .addClass( 'jp-carousel-comment' ) - .attr( 'id', 'jp-carousel-comment-' + this[ 'id' ] ) - .html( - '<div class="comment-gravatar">' + - this[ 'gravatar_markup' ] + - '</div>' + - '<div class="comment-author">' + - this[ 'author_markup' ] + - '</div>' + - '<div class="comment-date">' + - this[ 'date_gmt' ] + - '</div>' + - '<div class="comment-content">' + - this[ 'content' ] + - '</div>' - ); - comments.append( comment ); - - // Set the interval to check for a new page of comments. - clearInterval( commentInterval ); - commentInterval = setInterval( function() { - if ( - $( '.jp-carousel-overlay' ).height() - 150 < - $( '.jp-carousel-wrap' ).scrollTop() + $( window ).height() - ) { - gallery.jp_carousel( 'getComments', { - attachment_id: args.attachment_id, - offset: args.offset + 10, - clear: false, - } ); - clearInterval( commentInterval ); - } - }, 300 ); - } ); + var isSuccess = xhr.status >= 200 && xhr.status < 300; + var data; + try { + data = JSON.parse( xhr.responseText ); + } catch ( e ) { + // Do nothing. + } - // Verify (late) that the user didn't repeatldy click the arrows really fast, in which case the requested - // attachment id might no longer match the current attachment id by the time we get the data back or a now - // registered infiniscroll event kicks in, so we don't ever display comments for the wrong image by mistake. - var current = $( '.jp-carousel div.selected' ); - if ( current && current.data && current.data( 'attachment-id' ) != args.attachment_id ) { - comments.fadeOut( 'fast' ); - comments.empty(); - return; - } + if ( ! isSuccess || ! data || ! Array.isArray( data ) ) { + return onError(); + } - // Increase the height of the background, semi-transparent overlay to match the new length of the comments list. - $( '.jp-carousel-overlay' ).height( - $( window ).height() + - titleAndDescription.height() + - commentForm.height() + - ( comments.height() > 0 ? comments.height() : imageMeta.height() ) + - 200 - ); - - comments.show(); - commentsLoading.hide(); - }, - error: function( xhr, status, error ) { - // TODO: proper error handling - console.log( 'Comment get fail...', xhr, status, error ); - comments.fadeIn( 'fast' ); - commentsLoading.fadeOut( 'fast' ); - }, - } ); - }, + if ( shouldClear ) { + comments.innerHTML = ''; + } - postCommentError: function( args ) { - if ( 'object' !== typeof args ) { - args = {}; + for ( var i = 0; i < data.length; i++ ) { + var entry = data[ i ]; + var comment = document.createElement( 'div' ); + comment.classList.add( 'jp-carousel-comment' ); + comment.setAttribute( 'id', 'jp-carousel-comment-' + entry.id ); + comment.innerHTML = + '<div class="comment-gravatar">' + + entry.gravatar_markup + + '</div>' + + '<div class="comment-content">' + + '<div class="comment-author">' + + entry.author_markup + + '</div>' + + '<div class="comment-date">' + + entry.date_gmt + + '</div>' + + entry.content + + '</div>'; + comments.appendChild( comment ); + + // Set the interval to check for a new page of comments. + clearInterval( commentInterval ); + commentInterval = setInterval( function () { + if ( carousel.container.scrollTop + 150 > window.innerHeight ) { + fetchComments( attachmentId, offset + 10 ); + clearInterval( commentInterval ); + } + }, 300 ); + } + + if ( data.length > 0 ) { + domUtil.show( comments ); + commentsIndicator.innerText = data.length; + commentsIndicator.classList.add( 'jp-carousel-show' ); + } + + domUtil.hide( commentsLoading ); + }; + + xhr.onerror = onError; + + xhr.send(); + } + + function loadFullImage( slide ) { + var el = slide.el; + var attrs = slide.attrs; + var image = el.querySelector( 'img' ); + + if ( ! image.hasAttribute( 'data-loaded' ) ) { + var hasPreview = !! attrs.previewImage; + var thumbSize = attrs.thumbSize; + + if ( ! hasPreview || ( thumbSize && el.offsetWidth > thumbSize.width ) ) { + image.src = attrs.src; + } else { + image.src = attrs.previewImage; + } + + image.setAttribute( 'itemprop', 'image' ); + image.setAttribute( 'data-loaded', 1 ); } - if ( - ! args.field || - 'undefined' === typeof args.field || - ! args.error || - 'undefined' === typeof args.error - ) { - return; + } + + function loadBackgroundImage( slide ) { + var currentSlide = slide.el; + + if ( swiper && swiper.slides ) { + currentSlide = swiper.slides[ swiper.activeIndex ]; } - $( '#jp-carousel-comment-post-results' ) - .slideUp( 'fast' ) - .html( '<span class="jp-carousel-comment-post-error">' + args.error + '</span>' ) - .slideDown( 'fast' ); - $( '#jp-carousel-comment-form-spinner' ).spin( false ); - }, - - setCommentIframeSrc: function( attachment_id ) { - var iframe = $( '#jp-carousel-comment-iframe' ); - // Set the proper irame src for the current attachment id - if ( iframe && iframe.length ) { - iframe.attr( 'src', iframe.attr( 'src' ).replace( /(postid=)\d+/, '$1' + attachment_id ) ); - iframe.attr( - 'src', - iframe.attr( 'src' ).replace( /(%23.+)?$/, '%23jp-carousel-' + attachment_id ) - ); + + var image = slide.attrs.originalElement; + var isLoaded = image.complete && image.naturalHeight !== 0; + + if ( isLoaded ) { + applyBackgroundImage( slide, currentSlide, image ); + return; } - }, - clearCommentTextAreaValue: function() { - var commentTextArea = $( '#jp-carousel-comment-form-comment-field' ); - if ( commentTextArea ) { - commentTextArea.val( '' ); + image.onload = function () { + applyBackgroundImage( slide, currentSlide, image ); + }; + } + + function applyBackgroundImage( slide, currentSlide, image ) { + var url = util.getBackgroundImage( image ); + slide.backgroundImage = url; + currentSlide.style.backgroundImage = 'url(' + url + ')'; + currentSlide.style.backgroundSize = 'cover'; + } + + function clearCommentTextAreaValue() { + if ( carousel.commentField ) { + carousel.commentField.value = ''; } - }, + } - nextSlide: function() { - var slides = this.jp_carousel( 'slides' ); - var selected = this.jp_carousel( 'selectedSlide' ); + function getOriginalDimensions( el ) { + var size = el.getAttribute( 'data-orig-size' ) || ''; - if ( selected.length === 0 || ( slides.length > 2 && selected.is( slides.last() ) ) ) { - return slides.first(); + if ( size ) { + var parts = size.split( ',' ); + return { width: parseInt( parts[ 0 ], 10 ), height: parseInt( parts[ 1 ], 10 ) }; + } else { + return { + width: + el.getAttribute( 'data-original-width' ) || el.getAttribute( 'width' ) || undefined, + height: + el.getAttribute( 'data-original-height' ) || el.getAttribute( 'height' ) || undefined, + }; } + } - return selected.next(); - }, + function initCarouselSlides( items, startIndex ) { + carousel.slides = []; - prevSlide: function() { - var slides = this.jp_carousel( 'slides' ); - var selected = this.jp_carousel( 'selectedSlide' ); + var max = calculateMaxSlideDimensions(); - if ( selected.length === 0 || ( slides.length > 2 && selected.is( slides.first() ) ) ) { - return slides.last(); + // If the startIndex is not 0 then preload the clicked image first. + if ( startIndex !== 0 ) { + var img = new Image(); + img.src = items[ startIndex ].getAttribute( 'data-gallery-src' ); } - return selected.prev(); - }, + var useInPageThumbnails = !! domUtil.closest( items[ 0 ], '.tiled-gallery.type-rectangular' ); - loadFullImage: function( slide ) { - var image = slide.find( 'img:first' ); + // create the 'slide' + Array.prototype.forEach.call( items, function ( item, i ) { + var permalinkEl = domUtil.closest( item, 'a' ); + var origFile = item.getAttribute( 'data-orig-file' ) || item.getAttribute( 'src-orig' ); + var attrID = + item.getAttribute( 'data-attachment-id' ) || item.getAttribute( 'data-id' ) || '0'; + var caption = document.querySelector( + 'img[data-attachment-id="' + attrID + '"] + figcaption' + ); - if ( ! image.data( 'loaded' ) ) { - // If the width of the slide is smaller than the width of the "thumbnail" we're already using, - // don't load the full image. + if ( caption ) { + caption = caption.innerHTML; + } else { + caption = item.getAttribute( 'data-image-caption' ); + } - image.on( 'load.jetpack', function() { - image.off( 'load.jetpack' ); - $( this ) - .closest( '.jp-carousel-slide' ) - .css( 'background-image', '' ); - } ); + var attrs = { + originalElement: item, + attachmentId: attrID, + commentsOpened: item.getAttribute( 'data-comments-opened' ) || '0', + imageMeta: domUtil.getJSONAttribute( item, 'data-image-meta' ) || {}, + title: item.getAttribute( 'data-image-title' ) || '', + desc: item.getAttribute( 'data-image-description' ) || '', + mediumFile: item.getAttribute( 'data-medium-file' ) || '', + largeFile: item.getAttribute( 'data-large-file' ) || '', + origFile: origFile || '', + thumbSize: { width: item.naturalWidth, height: item.naturalHeight }, + caption: caption || '', + permalink: permalinkEl && permalinkEl.getAttribute( 'href' ), + src: origFile || item.getAttribute( 'src' ) || '', + }; + + var tiledGalleryItem = domUtil.closest( item, '.tiled-gallery-item' ); + var tiledCaptionEl = + tiledGalleryItem && tiledGalleryItem.querySelector( '.tiled-gallery-caption' ); + var tiledCaption = tiledCaptionEl && tiledCaptionEl.innerHTML; + if ( tiledCaption ) { + attrs.caption = tiledCaption; + } - if ( - ! slide.data( 'preview-image' ) || - ( slide.data( 'thumb-size' ) && slide.width() > slide.data( 'thumb-size' ).width ) - ) { - image - .attr( 'src', image.closest( '.jp-carousel-slide' ).data( 'src' ) ) - .attr( 'itemprop', 'image' ); + var origDimensions = getOriginalDimensions( item ); + + attrs.origWidth = origDimensions.width || attrs.thumbSize.width; + attrs.origHeight = origDimensions.height || attrs.thumbSize.height; + + if ( typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.generateImgSrc ) { + attrs.src = wpcom.carousel.generateImgSrc( item, max ); } else { - image.attr( 'src', slide.data( 'preview-image' ) ).attr( 'itemprop', 'image' ); + attrs.src = selectBestImageUrl( { + origFile: attrs.src, + origWidth: attrs.origWidth, + origHeight: attrs.origHeight, + maxWidth: max.width, + maxHeight: max.height, + mediumFile: attrs.mediumFile, + largeFile: attrs.largeFile, + } ); } - image.data( 'loaded', 1 ); - } - }, + // Set the final src. + item.setAttribute( 'data-gallery-src', attrs.src ); - hasMultipleImages: function() { - return gallery.jp_carousel( 'slides' ).length > 1; - }, - }; + if ( attrs.attachmentId !== '0' ) { + attrs.title = util.texturize( attrs.title ); + attrs.desc = util.texturize( attrs.desc ); + attrs.caption = util.texturize( attrs.caption ); - $.fn.jp_carousel = function( method ) { - // ask for the HTML of the gallery - // Method calling logic - if ( methods[ method ] ) { - return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ) ); - } else if ( typeof method === 'object' || ! method ) { - return methods.open.apply( this, arguments ); - } else { - $.error( 'Method ' + method + ' does not exist on jQuery.jp_carousel' ); + // Initially, the image is a 1x1 transparent gif. + // The preview is shown as a background image on the slide itself. + var image = new Image(); + image.src = attrs.src; + + var slideEl = document.createElement( 'div' ); + slideEl.classList.add( 'swiper-slide' ); + slideEl.setAttribute( 'itemprop', 'associatedMedia' ); + slideEl.setAttribute( 'itemscope', '' ); + slideEl.setAttribute( 'itemtype', 'https://schema.org/ImageObject' ); + var zoomEl = document.createElement( 'div' ); + zoomEl.classList.add( 'swiper-zoom-container' ); + + carousel.gallery.appendChild( slideEl ); + + slideEl.appendChild( zoomEl ); + zoomEl.appendChild( image ); + slideEl.setAttribute( 'data-attachment-id', attrs.attachmentId ); + slideEl.setAttribute( 'data-permalink', attrs.permalink ); + slideEl.setAttribute( 'data-orig-file', attrs.origFile ); + + if ( useInPageThumbnails ) { + // Use the image already loaded in the gallery as a preview. + attrs.previewImage = attrs.src; + } + + var slide = { el: slideEl, attrs: attrs, index: i }; + carousel.slides.push( slide ); + } + } ); } - }; - - // register the event listener for starting the gallery - $( document.body ).on( - 'click.jp-carousel', - 'div.gallery, div.tiled-gallery, ul.wp-block-gallery, ul.blocks-gallery-grid, div.wp-block-jetpack-tiled-gallery, a.single-image-gallery', - function( e ) { - if ( ! $( this ).jp_carousel( 'testForData', e.currentTarget ) ) { + + function loadSwiper( gallery, options ) { + if ( ! window.Swiper670 ) { + var loader = document.querySelector( '#jp-carousel-loading-overlay' ); + domUtil.show( loader ); + var jsScript = document.createElement( 'script' ); + jsScript.id = 'jetpack-carousel-swiper-js'; + jsScript.src = window.jetpackSwiperLibraryPath.url; + jsScript.async = true; + jsScript.onload = function () { + domUtil.hide( loader ); + openCarousel( gallery, options ); + }; + jsScript.onerror = function () { + domUtil.hide( loader ); + }; + document.head.appendChild( jsScript ); return; } + openCarousel( gallery, options ); + } - // Do not open the modal if we are looking at a gallery caption from before WP5, which may contain a link. - if ( - $( e.target ) - .parent() - .hasClass( 'gallery-caption' ) - ) { - return; + function openCarousel( gallery, options ) { + var settings = { + imgSelector: + '.gallery-item [data-attachment-id], .tiled-gallery-item [data-attachment-id], img[data-attachment-id], img[data-id]', + startIndex: 0, + }; + + var data = domUtil.getJSONAttribute( gallery, 'data-carousel-extra' ); + var tapTimeout; + + if ( ! data ) { + return; // don't run if the default gallery functions weren't used } - // Do not open the modal if we are looking at a caption of a gallery block, which may contain a link. - if ( - $( e.target ) - .parent() - .is( 'figcaption' ) - ) { - return; + initializeCarousel(); + + if ( carousel.isOpen ) { + return; // don't open if already opened } + carousel.isOpen = true; - // Set height to auto - // Fix some themes where closing carousel brings view back to top - $( 'html' ).css( 'height', 'auto' ); + // make sure to stop the page from scrolling behind the carousel overlay, so we don't trigger + // infiniscroll for it when enabled (Reader, theme infiniscroll, etc). + originalOverflow = getComputedStyle( document.body ).overflow; + document.body.style.overflow = 'hidden'; + // prevent html from overflowing on some of the new themes. + originalHOverflow = getComputedStyle( document.documentElement ).overflow; + document.documentElement.style.overflow = 'hidden'; + scrollPos = window.scrollY || window.pageYOffset || 0; - e.preventDefault(); + carousel.container.setAttribute( 'data-carousel-extra', JSON.stringify( data ) ); + stat( [ 'open', 'view_image' ] ); + + // If options exist, lets merge them + // with our default settings + for ( var option in options || {} ) { + settings[ option ] = options[ option ]; + } + + if ( settings.startIndex === -1 ) { + settings.startIndex = 0; // -1 returned if can't find index, so start from beginning + } - // Stopping propagation in case there are parent elements - // with .gallery or .tiled-gallery class - e.stopPropagation(); - $( this ).jp_carousel( 'open', { - start_index: $( this ) - .find( '.gallery-item, .tiled-gallery-item, .blocks-gallery-item, .tiled-gallery__item' ) - .index( - $( e.target ).parents( - '.gallery-item, .tiled-gallery-item, .blocks-gallery-item, .tiled-gallery__item' - ) - ), + domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeOpen' ); + carousel.gallery.innerHTML = ''; + + // Need to set the overlay manually to block or swiper does't initialise properly. + carousel.overlay.style.opacity = 1; + carousel.overlay.style.display = 'block'; + + initCarouselSlides( gallery.querySelectorAll( settings.imgSelector ), settings.startIndex ); + + swiper = new window.Swiper670( '.jp-carousel-swiper-container', { + centeredSlides: true, + zoom: true, + loop: carousel.slides.length > 1, + // Turn off interactions and hide navigation arrows if there is only one slide. + enabled: carousel.slides.length > 1, + pagination: { + el: '.jp-swiper-pagination', + clickable: true, + }, + navigation: { + nextEl: '.jp-swiper-button-next', + prevEl: '.jp-swiper-button-prev', + }, + initialSlide: settings.startIndex, + on: { + init: function () { + selectSlideAtIndex( settings.startIndex ); + }, + }, + preventClicks: false, + preventClicksPropagation: false, + preventInteractionOnTransition: ! domUtil.isTouch(), + threshold: 5, } ); - } - ); - // handle lightbox (single image gallery) for images linking to 'Attachment Page' - if ( 1 === Number( jetpackCarouselStrings.single_image_gallery ) ) { - processSingleImageGallery(); - $( document.body ).on( 'post-load', function() { - processSingleImageGallery(); - } ); - } + swiper.on( 'slideChange', function ( swiper ) { + var index; + // Swiper indexes slides from 1, plus when looping to left last slide ends up + // as 0 and looping to right first slide as total slides + 1. These are adjusted + // here to match index of carousel.slides. + if ( swiper.activeIndex === 0 ) { + index = carousel.slides.length - 1; + } else if ( swiper.activeIndex === carousel.slides.length + 1 ) { + index = 0; + } else { + index = swiper.activeIndex - 1; + } + selectSlideAtIndex( index ); - // Makes carousel work on page load and when back button leads to same URL with carousel hash (ie: no actual document.ready trigger) - $( window ).on( 'hashchange.jp-carousel', function() { - var hashRegExp = /jp-carousel-(\d+)/, - matches, - attachmentId, - galleries, - selectedThumbnail; + carousel.overlay.classList.remove( 'jp-carousel-hide-controls' ); + } ); - if ( ! window.location.hash || ! hashRegExp.test( window.location.hash ) ) { - if ( gallery && gallery.opened ) { - container.jp_carousel( 'close' ); - } + swiper.on( 'zoomChange', function ( swiper, scale ) { + if ( scale > 1 ) { + carousel.overlay.classList.add( 'jp-carousel-hide-controls' ); + } - return; - } + if ( scale === 1 ) { + carousel.overlay.classList.remove( 'jp-carousel-hide-controls' ); + } + } ); - if ( window.location.hash === last_known_location_hash && gallery.opened ) { - return; - } + swiper.on( 'doubleTap', function ( swiper ) { + clearTimeout( tapTimeout ); + if ( swiper.zoom.scale === 1 ) { + var zoomTimeout = setTimeout( function () { + carousel.overlay.classList.remove( 'jp-carousel-hide-controls' ); + clearTimeout( zoomTimeout ); + }, 150 ); + } + } ); - if ( window.location.hash && gallery && ! gallery.opened && history.back ) { - history.back(); - return; + swiper.on( 'tap', function () { + if ( swiper.zoom.scale > 1 ) { + tapTimeout = setTimeout( function () { + carousel.overlay.classList.toggle( 'jp-carousel-hide-controls' ); + }, 150 ); + } + } ); + + domUtil.fadeIn( carousel.overlay, function () { + domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterOpen' ); + } ); } - last_known_location_hash = window.location.hash; - matches = window.location.hash.match( hashRegExp ); - attachmentId = parseInt( matches[ 1 ], 10 ); - galleries = $( - 'div.gallery, div.tiled-gallery, a.single-image-gallery, ul.wp-block-gallery, div.wp-block-jetpack-tiled-gallery' - ); - - // Find the first thumbnail that matches the attachment ID in the location - // hash, then open the gallery that contains it. - galleries.each( function( _, galleryEl ) { - $( galleryEl ) - .find( 'img' ) - .each( function( imageIndex, imageEl ) { - if ( $( imageEl ).data( 'attachment-id' ) === parseInt( attachmentId, 10 ) ) { - selectedThumbnail = { index: imageIndex, gallery: galleryEl }; - return false; + // Register the event listener for starting the gallery + document.body.addEventListener( 'click', function ( e ) { + var isCompatible = + window.CSS && window.CSS.supports && window.CSS.supports( 'display', 'grid' ); + + // IE11 support is being dropped in August 2021. The new swiper.js libray is not IE11 compat + // so just default to opening individual image attachment/media pages for IE. + if ( ! isCompatible ) { + return; + } + + var target = e.target; + var gallery = domUtil.closest( target, gallerySelector ); + + if ( gallery ) { + if ( ! testForData( gallery ) ) { + return; + } + + var parent = target.parentElement; + var grandparent = parent.parentElement; + + // If Gallery is made up of individual Image blocks check for custom link before + // loading carousel. + if ( grandparent && grandparent.classList.contains( 'wp-block-image' ) ) { + var parentHref = parent.getAttribute( 'href' ); + + // If the link does not point to the attachment or media file then assume Image has + // a custom link so don't load the carousel. + if ( + parentHref.split( '?' )[ 0 ] !== + target.getAttribute( 'data-orig-file' ).split( '?' )[ 0 ] && + parentHref !== target.getAttribute( 'data-permalink' ) + ) { + return; } - } ); + } - if ( selectedThumbnail ) { - $( selectedThumbnail.gallery ).jp_carousel( 'openOrSelectSlide', selectedThumbnail.index ); - return false; + // Do not open the modal if we are looking at a gallery caption from before WP5, which may contain a link. + if ( parent.classList.contains( 'gallery-caption' ) ) { + return; + } + + // Do not open the modal if we are looking at a caption of a gallery block, which may contain a link. + if ( domUtil.matches( parent, 'figcaption' ) ) { + return; + } + + // Set height to auto. + // Fix some themes where closing carousel brings view back to top. + document.documentElement.style.height = 'auto'; + + e.preventDefault(); + + // Stopping propagation in case there are parent elements + // with .gallery or .tiled-gallery class + e.stopPropagation(); + + var item = domUtil.closest( target, itemSelector ); + var index = Array.prototype.indexOf.call( gallery.querySelectorAll( itemSelector ), item ); + loadSwiper( gallery, { startIndex: index } ); } } ); - } ); - if ( window.location.hash ) { - $( window ).trigger( 'hashchange' ); - } -} ); - -/** - * jQuery Plugin to obtain touch gestures from iPhone, iPod Touch and iPad, should also work with Android mobile phones (not tested yet!) - * Common usage: wipe images (left and right to show the previous or next image) - * - * @author Andreas Waltl, netCU Internetagentur (http://www.netcu.de) - * Version 1.1.1, modified to pass the touchmove event to the callbacks. - */ -( function( $ ) { - $.fn.touchwipe = function( settings ) { - var config = { - min_move_x: 20, - min_move_y: 20, - wipeLeft: function(/*e*/) {}, - wipeRight: function(/*e*/) {}, - wipeUp: function(/*e*/) {}, - wipeDown: function(/*e*/) {}, - preventDefaultEvents: true, - }; - - if ( settings ) { - $.extend( config, settings ); + // Handle lightbox (single image gallery) for images linking to 'Attachment Page'. + if ( Number( jetpackCarouselStrings.single_image_gallery ) === 1 ) { + processSingleImageGallery(); + document.body.addEventListener( 'is.post-load', function () { + processSingleImageGallery(); + } ); } - this.each( function() { - var startX; - var startY; - var isMoving = false; + // Makes carousel work on page load and when back button leads to same URL with carousel hash + // (i.e. no actual document.ready trigger). + window.addEventListener( 'hashchange', function () { + var hashRegExp = /jp-carousel-(\d+)/; - function cancelTouch() { - this.removeEventListener( 'touchmove', onTouchMove ); - startX = null; - isMoving = false; + if ( ! window.location.hash || ! hashRegExp.test( window.location.hash ) ) { + if ( carousel.isOpen ) { + closeCarousel(); + } + + return; } - function onTouchMove( e ) { - if ( config.preventDefaultEvents ) { - e.preventDefault(); - } - if ( isMoving ) { - var x = e.touches[ 0 ].pageX; - var y = e.touches[ 0 ].pageY; - var dx = startX - x; - var dy = startY - y; - if ( Math.abs( dx ) >= config.min_move_x ) { - cancelTouch(); - if ( dx > 0 ) { - config.wipeLeft( e ); - } else { - config.wipeRight( e ); - } - } else if ( Math.abs( dy ) >= config.min_move_y ) { - cancelTouch(); - if ( dy > 0 ) { - config.wipeDown( e ); - } else { - config.wipeUp( e ); - } + if ( window.location.hash === lastKnownLocationHash && carousel.isOpen ) { + return; + } + + if ( window.location.hash && carousel.gallery && ! carousel.isOpen && history.back ) { + history.back(); + return; + } + + lastKnownLocationHash = window.location.hash; + var matchList = window.location.hash.match( hashRegExp ); + var attachmentId = parseInt( matchList[ 1 ], 10 ); + var galleries = document.querySelectorAll( gallerySelector ); + + // Find the first thumbnail that matches the attachment ID in the location + // hash, then open the gallery that contains it. + for ( var i = 0; i < galleries.length; i++ ) { + var gallery = galleries[ i ]; + var selected; + + var images = gallery.querySelectorAll( 'img' ); + for ( var j = 0; j < images.length; j++ ) { + if ( + parseInt( images[ j ].getAttribute( 'data-attachment-id' ), 10 ) === attachmentId || + parseInt( images[ j ].getAttribute( 'data-id' ), 10 ) === attachmentId + ) { + selected = j; + break; } } - } - function onTouchStart( e ) { - if ( e.touches.length === 1 ) { - startX = e.touches[ 0 ].pageX; - startY = e.touches[ 0 ].pageY; - isMoving = true; - this.addEventListener( 'touchmove', onTouchMove, false ); + if ( selected !== undefined ) { + openOrSelectSlide( gallery, selected ); + break; } } - if ( 'ontouchstart' in document.documentElement ) { - this.addEventListener( 'touchstart', onTouchStart, false ); - } } ); - return this; - }; -} )( jQuery ); + if ( window.location.hash ) { + domUtil.emitEvent( window, 'hashchange' ); + } + } + + if ( document.readyState !== 'loading' ) { + init(); + } else { + document.addEventListener( 'DOMContentLoaded', init ); + } +} )(); |