diff options
Diffstat (limited to 'plugins/jetpack/extensions/blocks/tiled-gallery/layout')
14 files changed, 981 insertions, 0 deletions
diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/column.js b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/column.js new file mode 100644 index 00000000..a3ed5cdf --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/column.js @@ -0,0 +1,3 @@ +export default function Column( { children } ) { + return <div className="tiled-gallery__col">{ children }</div>; +} diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/gallery.js b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/gallery.js new file mode 100644 index 00000000..94fc61e4 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/gallery.js @@ -0,0 +1,7 @@ +export default function Gallery( { children, galleryRef } ) { + return ( + <div className="tiled-gallery__gallery" ref={ galleryRef }> + { children } + </div> + ); +} diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/index.js b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/index.js new file mode 100644 index 00000000..abcb5641 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/index.js @@ -0,0 +1,160 @@ +/** + * External dependencies + */ +import photon from 'photon'; +import { __, sprintf } from '@wordpress/i18n'; +import { Component } from '@wordpress/element'; +import { format as formatUrl, parse as parseUrl } from 'url'; +import { isBlobURL } from '@wordpress/blob'; + +/** + * Internal dependencies + */ +import GalleryImageEdit from '../gallery-image/edit'; +import GalleryImageSave from '../gallery-image/save'; +import Mosaic from './mosaic'; +import Square from './square'; +import { PHOTON_MAX_RESIZE } from '../constants'; + +export default class Layout extends Component { + photonize( { height, width, url } ) { + if ( ! url ) { + return; + } + + // Do not Photonize images that are still uploading or from localhost + if ( isBlobURL( url ) || /^https?:\/\/localhost/.test( url ) ) { + return url; + } + + // Drop query args, photon URLs can't handle them + // This should be the "raw" url, we'll add dimensions later + const cleanUrl = url.split( '?', 1 )[ 0 ]; + + const photonImplementation = isWpcomFilesUrl( url ) ? photonWpcomImage : photon; + + const { layoutStyle } = this.props; + + if ( isSquareishLayout( layoutStyle ) && width && height ) { + const size = Math.min( PHOTON_MAX_RESIZE, width, height ); + return photonImplementation( cleanUrl, { resize: `${ size },${ size }` } ); + } + return photonImplementation( cleanUrl ); + } + + // This is tricky: + // - We need to "photonize" to resize the images at appropriate dimensions + // - The resize will depend on the image size and the layout in some cases + // - Handlers need to be created by index so that the image changes can be applied correctly. + // This is because the images are stored in an array in the block attributes. + renderImage( img, i ) { + const { + imageFilter, + images, + isSave, + linkTo, + onRemoveImage, + onSelectImage, + selectedImage, + setImageAttributes, + } = this.props; + + /* translators: %1$d is the order number of the image, %2$d is the total number of images. */ + const ariaLabel = sprintf( + __( 'image %1$d of %2$d in gallery', 'jetpack' ), + i + 1, + images.length + ); + const Image = isSave ? GalleryImageSave : GalleryImageEdit; + + return ( + <Image + alt={ img.alt } + aria-label={ ariaLabel } + height={ img.height } + id={ img.id } + imageFilter={ imageFilter } + isSelected={ selectedImage === i } + key={ i } + link={ img.link } + linkTo={ linkTo } + onRemove={ isSave ? undefined : onRemoveImage( i ) } + onSelect={ isSave ? undefined : onSelectImage( i ) } + origUrl={ img.url } + setAttributes={ isSave ? undefined : setImageAttributes( i ) } + url={ this.photonize( img ) } + width={ img.width } + /> + ); + } + + render() { + const { align, children, className, columns, images, layoutStyle } = this.props; + + const LayoutRenderer = isSquareishLayout( layoutStyle ) ? Square : Mosaic; + + const renderedImages = this.props.images.map( this.renderImage, this ); + + return ( + <div className={ className }> + <LayoutRenderer + align={ align } + columns={ columns } + images={ images } + layoutStyle={ layoutStyle } + renderedImages={ renderedImages } + /> + { children } + </div> + ); + } +} + +function isSquareishLayout( layout ) { + return [ 'circle', 'square' ].includes( layout ); +} + +function isWpcomFilesUrl( url ) { + const { host } = parseUrl( url ); + return /\.files\.wordpress\.com$/.test( host ); +} + +/** + * Apply photon arguments to *.files.wordpress.com images + * + * This function largely duplicates the functionlity of the photon.js lib. + * This is necessary because we want to serve images from *.files.wordpress.com so that private + * WordPress.com sites can use this block which depends on a Photon-like image service. + * + * If we pass all images through Photon servers, some images are unreachable. *.files.wordpress.com + * is already photon-like so we can pass it the same parameters for image resizing. + * + * @param {string} url Image url + * @param {Object} opts Options to pass to photon + * + * @return {string} Url string with options applied + */ +function photonWpcomImage( url, opts = {} ) { + // Adhere to the same options API as the photon.js lib + const photonLibMappings = { + width: 'w', + height: 'h', + letterboxing: 'lb', + removeLetterboxing: 'ulb', + }; + + // Discard some param parts + const { auth, hash, port, query, search, ...urlParts } = parseUrl( url ); + + // Build query + // This reduction intentionally mutates the query as it is built internally. + urlParts.query = Object.keys( opts ).reduce( + ( q, key ) => + Object.assign( q, { + [ photonLibMappings.hasOwnProperty( key ) ? photonLibMappings[ key ] : key ]: opts[ key ], + } ), + {} + ); + + return formatUrl( urlParts ); +} diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/index.js b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/index.js new file mode 100644 index 00000000..8c56b164 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/index.js @@ -0,0 +1,104 @@ +/** + * External dependencies + */ +import { Component, createRef } from '@wordpress/element'; +import ResizeObserver from 'resize-observer-polyfill'; + +/** + * Internal dependencies + */ +import Column from '../column'; +import Gallery from '../gallery'; +import Row from '../row'; +import { getGalleryRows, handleRowResize } from './resize'; +import { imagesToRatios, ratiosToColumns, ratiosToMosaicRows } from './ratios'; + +export default class Mosaic extends Component { + gallery = createRef(); + pendingRaf = null; + ro = null; // resizeObserver instance + + componentDidMount() { + this.observeResize(); + } + + componentWillUnmount() { + this.unobserveResize(); + } + + componentDidUpdate( prevProps ) { + if ( prevProps.images !== this.props.images || prevProps.align !== this.props.align ) { + this.triggerResize(); + } else if ( 'columns' === this.props.layoutStyle && prevProps.columns !== this.props.columns ) { + this.triggerResize(); + } + } + + handleGalleryResize = entries => { + if ( this.pendingRaf ) { + cancelAnimationFrame( this.pendingRaf ); + this.pendingRaf = null; + } + this.pendingRaf = requestAnimationFrame( () => { + for ( const { contentRect, target } of entries ) { + const { width } = contentRect; + getGalleryRows( target ).forEach( row => handleRowResize( row, width ) ); + } + } ); + }; + + triggerResize() { + if ( this.gallery.current ) { + this.handleGalleryResize( [ + { + target: this.gallery.current, + contentRect: { width: this.gallery.current.clientWidth }, + }, + ] ); + } + } + + observeResize() { + this.triggerResize(); + this.ro = new ResizeObserver( this.handleGalleryResize ); + if ( this.gallery.current ) { + this.ro.observe( this.gallery.current ); + } + } + + unobserveResize() { + if ( this.ro ) { + this.ro.disconnect(); + this.ro = null; + } + if ( this.pendingRaf ) { + cancelAnimationFrame( this.pendingRaf ); + this.pendingRaf = null; + } + } + + render() { + const { align, columns, images, layoutStyle, renderedImages } = this.props; + + const ratios = imagesToRatios( images ); + const rows = + 'columns' === layoutStyle + ? ratiosToColumns( ratios, columns ) + : ratiosToMosaicRows( ratios, { isWide: [ 'full', 'wide' ].includes( align ) } ); + + let cursor = 0; + return ( + <Gallery galleryRef={ this.gallery }> + { rows.map( ( row, rowIndex ) => ( + <Row key={ rowIndex }> + { row.map( ( colSize, colIndex ) => { + const columnImages = renderedImages.slice( cursor, cursor + colSize ); + cursor += colSize; + return <Column key={ colIndex }>{ columnImages }</Column>; + } ) } + </Row> + ) ) } + </Gallery> + ); + } +} diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/ratios.js b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/ratios.js new file mode 100644 index 00000000..8accd552 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/ratios.js @@ -0,0 +1,280 @@ +/** + * External dependencies + */ +import { + drop, + every, + isEqual, + map, + overEvery, + some, + sum, + take, + takeRight, + takeWhile, + zipWith, +} from 'lodash'; + +export function imagesToRatios( images ) { + return map( images, ratioFromImage ); +} + +export function ratioFromImage( { height, width } ) { + return height && width ? width / height : 1; +} + +/** + * Build three columns, each of which should contain approximately 1/3 of the total ratio + * + * @param {Array.<number>} ratios Ratios of images put into shape + * @param {number} columnCount Number of columns + * + * @return {Array.<Array.<number>>} Shape of rows and columns + */ +export function ratiosToColumns( ratios, columnCount ) { + // If we don't have more than 1 per column, just return a simple 1 ratio per column shape + if ( ratios.length <= columnCount ) { + return [ Array( ratios.length ).fill( 1 ) ]; + } + + const total = sum( ratios ); + const targetColRatio = total / columnCount; + + const row = []; + let toProcess = ratios; + let accumulatedRatio = 0; + + // We skip the last column in the loop and add rest later + for ( let i = 0; i < columnCount - 1; i++ ) { + const colSize = takeWhile( toProcess, ratio => { + const shouldTake = accumulatedRatio <= ( i + 1 ) * targetColRatio; + if ( shouldTake ) { + accumulatedRatio += ratio; + } + return shouldTake; + } ).length; + row.push( colSize ); + toProcess = drop( toProcess, colSize ); + } + + // Don't calculate last column, just add what's left + row.push( toProcess.length ); + + // A shape is an array of rows. Wrap our row in an array. + return [ row ]; +} + +/** + * These are partially applied functions. + * They rely on helper function (defined below) to create a function that expects to be passed ratios + * during processing. + * + * …FitsNextImages() functions should be passed ratios to be processed + * …IsNotRecent() functions should be passed the processed shapes + */ + +const reverseSymmetricRowIsNotRecent = isNotRecentShape( [ 2, 1, 2 ], 5 ); +const reverseSymmetricFitsNextImages = checkNextRatios( [ + isLandscape, + isLandscape, + isPortrait, + isLandscape, + isLandscape, +] ); +const longSymmetricRowFitsNextImages = checkNextRatios( [ + isLandscape, + isLandscape, + isLandscape, + isPortrait, + isLandscape, + isLandscape, + isLandscape, +] ); +const longSymmetricRowIsNotRecent = isNotRecentShape( [ 3, 1, 3 ], 5 ); +const symmetricRowFitsNextImages = checkNextRatios( [ + isPortrait, + isLandscape, + isLandscape, + isPortrait, +] ); +const symmetricRowIsNotRecent = isNotRecentShape( [ 1, 2, 1 ], 5 ); +const oneThreeFitsNextImages = checkNextRatios( [ + isPortrait, + isLandscape, + isLandscape, + isLandscape, +] ); +const oneThreeIsNotRecent = isNotRecentShape( [ 1, 3 ], 3 ); +const threeOneIsFitsNextImages = checkNextRatios( [ + isLandscape, + isLandscape, + isLandscape, + isPortrait, +] ); +const threeOneIsNotRecent = isNotRecentShape( [ 3, 1 ], 3 ); +const oneTwoFitsNextImages = checkNextRatios( [ + lt( 1.6 ), + overEvery( gte( 0.9 ), lt( 2 ) ), + overEvery( gte( 0.9 ), lt( 2 ) ), +] ); +const oneTwoIsNotRecent = isNotRecentShape( [ 1, 2 ], 3 ); +const fiveIsNotRecent = isNotRecentShape( [ 1, 1, 1, 1, 1 ], 1 ); +const fourIsNotRecent = isNotRecentShape( [ 1, 1, 1, 1 ], 1 ); +const threeIsNotRecent = isNotRecentShape( [ 1, 1, 1 ], 3 ); +const twoOneFitsNextImages = checkNextRatios( [ + overEvery( gte( 0.9 ), lt( 2 ) ), + overEvery( gte( 0.9 ), lt( 2 ) ), + lt( 1.6 ), +] ); +const twoOneIsNotRecent = isNotRecentShape( [ 2, 1 ], 3 ); +const panoramicFitsNextImages = checkNextRatios( [ isPanoramic ] ); + +export function ratiosToMosaicRows( ratios, { isWide } = {} ) { + // This function will recursively process the input until it is consumed + const go = ( processed, toProcess ) => { + if ( ! toProcess.length ) { + return processed; + } + + let next; + + if ( + /* Reverse_Symmetric_Row */ + toProcess.length > 15 && + reverseSymmetricFitsNextImages( toProcess ) && + reverseSymmetricRowIsNotRecent( processed ) + ) { + next = [ 2, 1, 2 ]; + } else if ( + /* Long_Symmetric_Row */ + toProcess.length > 15 && + longSymmetricRowFitsNextImages( toProcess ) && + longSymmetricRowIsNotRecent( processed ) + ) { + next = [ 3, 1, 3 ]; + } else if ( + /* Symmetric_Row */ + toProcess.length !== 5 && + symmetricRowFitsNextImages( toProcess ) && + symmetricRowIsNotRecent( processed ) + ) { + next = [ 1, 2, 1 ]; + } else if ( + /* One_Three */ + oneThreeFitsNextImages( toProcess ) && + oneThreeIsNotRecent( processed ) + ) { + next = [ 1, 3 ]; + } else if ( + /* Three_One */ + threeOneIsFitsNextImages( toProcess ) && + threeOneIsNotRecent( processed ) + ) { + next = [ 3, 1 ]; + } else if ( + /* One_Two */ + oneTwoFitsNextImages( toProcess ) && + oneTwoIsNotRecent( processed ) + ) { + next = [ 1, 2 ]; + } else if ( + /* Five */ + isWide && + ( toProcess.length === 5 || ( toProcess.length !== 10 && toProcess.length > 6 ) ) && + fiveIsNotRecent( processed ) && + sum( take( toProcess, 5 ) ) < 5 + ) { + next = [ 1, 1, 1, 1, 1 ]; + } else if ( + /* Four */ + isFourValidCandidate( processed, toProcess ) + ) { + next = [ 1, 1, 1, 1 ]; + } else if ( + /* Three */ + isThreeValidCandidate( processed, toProcess, isWide ) + ) { + next = [ 1, 1, 1 ]; + } else if ( + /* Two_One */ + twoOneFitsNextImages( toProcess ) && + twoOneIsNotRecent( processed ) + ) { + next = [ 2, 1 ]; + } else if ( /* Panoramic */ panoramicFitsNextImages( toProcess ) ) { + next = [ 1 ]; + } else if ( /* One_One */ toProcess.length > 3 ) { + next = [ 1, 1 ]; + } else { + // Everything left + next = Array( toProcess.length ).fill( 1 ); + } + + // Add row + const nextProcessed = processed.concat( [ next ] ); + + // Trim consumed images from next processing step + const consumedImages = sum( next ); + const nextToProcess = toProcess.slice( consumedImages ); + + return go( nextProcessed, nextToProcess ); + }; + return go( [], ratios ); +} + +function isThreeValidCandidate( processed, toProcess, isWide ) { + const ratio = sum( take( toProcess, 3 ) ); + return ( + toProcess.length >= 3 && + toProcess.length !== 4 && + toProcess.length !== 6 && + threeIsNotRecent( processed ) && + ( ratio < 2.5 || + ( ratio < 5 && + /* nextAreSymettric */ + ( toProcess.length >= 3 && + /* @FIXME floating point equality?? */ toProcess[ 0 ] === toProcess[ 2 ] ) ) || + isWide ) + ); +} + +function isFourValidCandidate( processed, toProcess ) { + const ratio = sum( take( toProcess, 4 ) ); + return ( + ( fourIsNotRecent( processed ) && ( ratio < 3.5 && toProcess.length > 5 ) ) || + ( ratio < 7 && toProcess.length === 4 ) + ); +} + +function isNotRecentShape( shape, numRecents ) { + return recents => + ! some( takeRight( recents, numRecents ), recentShape => isEqual( recentShape, shape ) ); +} + +function checkNextRatios( shape ) { + return ratios => + ratios.length >= shape.length && + every( zipWith( shape, ratios.slice( 0, shape.length ), ( f, r ) => f( r ) ) ); +} + +function isLandscape( ratio ) { + return ratio >= 1 && ratio < 2; +} + +function isPortrait( ratio ) { + return ratio < 1; +} + +function isPanoramic( ratio ) { + return ratio >= 2; +} + +// >= +function gte( n ) { + return m => m >= n; +} + +// < +function lt( n ) { + return m => m < n; +} diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/resize.js b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/resize.js new file mode 100644 index 00000000..022729c8 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/resize.js @@ -0,0 +1,107 @@ +/** + * Internal dependencies + */ +import { GUTTER_WIDTH } from '../../constants'; + +/** + * Distribute a difference across ns so that their sum matches the target + * + * @param {Array<number>} parts Array of numbers to fit + * @param {number} target Number that sum should match + * @return {Array<number>} Adjusted parts + */ +function adjustFit( parts, target ) { + const diff = target - parts.reduce( ( sum, n ) => sum + n, 0 ); + const partialDiff = diff / parts.length; + return parts.map( p => p + partialDiff ); +} + +export function handleRowResize( row, width ) { + applyRowRatio( row, getRowRatio( row ), width ); +} + +function getRowRatio( row ) { + const result = getRowCols( row ) + .map( getColumnRatio ) + .reduce( + ( [ ratioA, weightedRatioA ], [ ratioB, weightedRatioB ] ) => { + return [ ratioA + ratioB, weightedRatioA + weightedRatioB ]; + }, + [ 0, 0 ] + ); + return result; +} + +export function getGalleryRows( gallery ) { + return Array.from( gallery.querySelectorAll( '.tiled-gallery__row' ) ); +} + +function getRowCols( row ) { + return Array.from( row.querySelectorAll( '.tiled-gallery__col' ) ); +} + +function getColImgs( col ) { + return Array.from( + col.querySelectorAll( '.tiled-gallery__item > img, .tiled-gallery__item > a > img' ) + ); +} + +function getColumnRatio( col ) { + const imgs = getColImgs( col ); + const imgCount = imgs.length; + const ratio = + 1 / + imgs.map( getImageRatio ).reduce( ( partialColRatio, imgRatio ) => { + return partialColRatio + 1 / imgRatio; + }, 0 ); + const result = [ ratio, ratio * imgCount || 1 ]; + return result; +} + +function getImageRatio( img ) { + const w = parseInt( img.dataset.width, 10 ); + const h = parseInt( img.dataset.height, 10 ); + const result = w && ! Number.isNaN( w ) && h && ! Number.isNaN( h ) ? w / h : 1; + return result; +} + +function applyRowRatio( row, [ ratio, weightedRatio ], width ) { + const rawHeight = + ( 1 / ratio ) * ( width - GUTTER_WIDTH * ( row.childElementCount - 1 ) - weightedRatio ); + + applyColRatio( row, { + rawHeight, + rowWidth: width - GUTTER_WIDTH * ( row.childElementCount - 1 ), + } ); +} + +function applyColRatio( row, { rawHeight, rowWidth } ) { + const cols = getRowCols( row ); + + const colWidths = cols.map( + col => ( rawHeight - GUTTER_WIDTH * ( col.childElementCount - 1 ) ) * getColumnRatio( col )[ 0 ] + ); + + const adjustedWidths = adjustFit( colWidths, rowWidth ); + + cols.forEach( ( col, i ) => { + const rawWidth = colWidths[ i ]; + const width = adjustedWidths[ i ]; + applyImgRatio( col, { + colHeight: rawHeight - GUTTER_WIDTH * ( col.childElementCount - 1 ), + width, + rawWidth, + } ); + } ); +} + +function applyImgRatio( col, { colHeight, width, rawWidth } ) { + const imgHeights = getColImgs( col ).map( img => rawWidth / getImageRatio( img ) ); + const adjustedHeights = adjustFit( imgHeights, colHeight ); + + // Set size of col children, not the <img /> element + Array.from( col.children ).forEach( ( item, i ) => { + const height = adjustedHeights[ i ]; + item.setAttribute( 'style', `height:${ height }px;width:${ width }px;` ); + } ); +} diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/__snapshots__/index.js.snap b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/__snapshots__/index.js.snap new file mode 100644 index 00000000..e726fa52 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/__snapshots__/index.js.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders as expected 1`] = ` +<Gallery + galleryRef={ + Object { + "current": null, + } + } +> + <Row + key="0" + > + <Column + key="0" + > + 0 + </Column> + </Row> + <Row + key="1" + > + <Column + key="0" + > + 1 + </Column> + </Row> + <Row + key="2" + > + <Column + key="0" + > + 2 + </Column> + <Column + key="1" + > + 3 + </Column> + <Column + key="2" + > + 4 + </Column> + <Column + key="3" + > + 5 + </Column> + </Row> + <Row + key="3" + > + <Column + key="0" + > + 6 + </Column> + <Column + key="1" + > + 7 + </Column> + </Row> + <Row + key="4" + > + <Column + key="0" + > + 8 + </Column> + <Column + key="1" + > + 9 + 10 + </Column> + </Row> + <Row + key="5" + > + <Column + key="0" + > + 11 + 12 + </Column> + <Column + key="1" + > + 13 + </Column> + </Row> +</Gallery> +`; diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/__snapshots__/ratios.js.snap b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/__snapshots__/ratios.js.snap new file mode 100644 index 00000000..df02118c --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/__snapshots__/ratios.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ratiosToMosaicRows transforms as expected 1`] = ` +Array [ + Array [ + 1, + ], + Array [ + 1, + ], + Array [ + 1, + 1, + 1, + 1, + ], + Array [ + 1, + 1, + ], + Array [ + 1, + 2, + ], + Array [ + 2, + 1, + ], +] +`; diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/fixtures/ratios.js b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/fixtures/ratios.js new file mode 100644 index 00000000..77db288c --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/fixtures/ratios.js @@ -0,0 +1,16 @@ +export const ratios = [ + 4, + 2.26056338028169, + 0.6676143094053542, + 0.75, + 0.7444409646100846, + 0.6666666666666666, + 0.8000588062334607, + 3.6392174704276616, + 1.335559265442404, + 1.509433962264151, + 1.6, + 1.3208430913348945, + 1.3553937789543349, + 1.499531396438613, +]; diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/index.js b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/index.js new file mode 100644 index 00000000..72e49ba6 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/index.js @@ -0,0 +1,21 @@ +/** + * External dependencies + */ +import React from 'react'; +import { range } from 'lodash'; +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import Mosaic from '..'; +import * as imageSets from '../../test/fixtures/image-sets'; + +test( 'renders as expected', () => { + Object.keys( imageSets ).forEach( k => { + const images = imageSets[ k ]; + expect( + shallow( <Mosaic images={ images } renderedImages={ range( images.length ) } /> ) + ).toMatchSnapshot(); + } ); +} ); diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/ratios.js b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/ratios.js new file mode 100644 index 00000000..3756b971 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/mosaic/test/ratios.js @@ -0,0 +1,11 @@ +/** + * Internal dependencies + */ +import { ratiosToMosaicRows } from '../ratios'; +import { ratios } from './fixtures/ratios'; + +describe( 'ratiosToMosaicRows', () => { + test( 'transforms as expected', () => { + expect( ratiosToMosaicRows( ratios ) ).toMatchSnapshot(); + } ); +} ); diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/row.js b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/row.js new file mode 100644 index 00000000..200a58c2 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/row.js @@ -0,0 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +export default function Row( { children, className } ) { + return <div className={ classnames( 'tiled-gallery__row', className ) }>{ children }</div>; +} diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/square.js b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/square.js new file mode 100644 index 00000000..2a1ab888 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/square.js @@ -0,0 +1,33 @@ +/** + * External dependencies + */ +import { chunk, drop, take } from 'lodash'; + +/** + * Internal dependencies + */ +import Row from './row'; +import Column from './column'; +import Gallery from './gallery'; +import { MAX_COLUMNS } from '../constants'; + +export default function Square( { columns, renderedImages } ) { + const columnCount = Math.min( MAX_COLUMNS, columns ); + + const remainder = renderedImages.length % columnCount; + + return ( + <Gallery> + { [ + ...( remainder ? [ take( renderedImages, remainder ) ] : [] ), + ...chunk( drop( renderedImages, remainder ), columnCount ), + ].map( ( imagesInRow, rowIndex ) => ( + <Row key={ rowIndex } className={ `columns-${ imagesInRow.length }` }> + { imagesInRow.map( ( image, colIndex ) => ( + <Column key={ colIndex }>{ image }</Column> + ) ) } + </Row> + ) ) } + </Gallery> + ); +} diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/layout/test/fixtures/image-sets.js b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/test/fixtures/image-sets.js new file mode 100644 index 00000000..fd477f5a --- /dev/null +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/layout/test/fixtures/image-sets.js @@ -0,0 +1,103 @@ +export const imageSet1 = [ + { + alt: '', + id: 163, + url: 'https://example.files.wordpress.com/2018/12/architecture-bay-bridge-356830.jpg', + height: 2048, + width: 8192, + }, + { + alt: '', + id: 162, + url: 'https://example.files.wordpress.com/2018/12/bloom-blossom-flora-40797-1.jpg', + height: 1562, + width: 3531, + }, + { + alt: '', + id: 161, + url: 'https://example.files.wordpress.com/2018/12/architecture-building-city-597049.jpg', + height: 4221, + width: 2818, + }, + { + alt: '', + id: 160, + url: 'https://example.files.wordpress.com/2018/12/architecture-art-blue-699466.jpg', + height: 4032, + width: 3024, + }, + { + alt: '', + id: 159, + url: + 'https://example.files.wordpress.com/2018/12/black-and-white-construction-ladder-54335.jpg', + height: 3193, + width: 2377, + }, + { + alt: '', + id: 158, + url: 'https://example.files.wordpress.com/2018/12/architecture-buildings-city-1672110.jpg', + height: 6000, + width: 4000, + }, + { + alt: '', + id: 157, + url: + 'https://example.files.wordpress.com/2018/12/architectural-design-architecture-black-and-white-1672122-1.jpg', + height: 3401, + width: 2721, + }, + { + alt: '', + id: 156, + url: 'https://example.files.wordpress.com/2018/12/grass-hd-wallpaper-lake-127753.jpg', + height: 2198, + width: 7999, + }, + { + alt: '', + id: 122, + url: 'https://example.files.wordpress.com/2018/12/texaco-car-1.jpg', + height: 599, + width: 800, + }, + { + alt: '', + id: 92, + url: 'https://example.files.wordpress.com/2018/12/43824553435_ea38cbc92a_m.jpg', + height: 159, + width: 240, + }, + { + alt: '', + id: 90, + url: 'https://example.files.wordpress.com/2018/12/42924685680_7b5632e58e_m.jpg', + height: 150, + width: 240, + }, + { + alt: '', + id: 89, + url: + 'https://example.files.wordpress.com/2018/12/31962299833_1e106f7f7a_z-1-e1545262352979.jpg', + height: 427, + width: 564, + }, + { + alt: '', + id: 88, + url: 'https://example.files.wordpress.com/2018/12/29797558147_3c72afa8f4_k.jpg', + height: 1511, + width: 2048, + }, + { + alt: '', + id: 8, + url: 'https://example.files.wordpress.com/2018/11/person-smartphone-office-table.jpeg', + height: 1067, + width: 1600, + }, +]; |