diff --git a/client/gutenberg/extensions/business-hours/components/day.jsx b/client/gutenberg/extensions/business-hours/components/day-edit.jsx similarity index 95% rename from client/gutenberg/extensions/business-hours/components/day.jsx rename to client/gutenberg/extensions/business-hours/components/day-edit.jsx index 429943b1ffac4b..f0120c621f0754 100644 --- a/client/gutenberg/extensions/business-hours/components/day.jsx +++ b/client/gutenberg/extensions/business-hours/components/day-edit.jsx @@ -14,7 +14,7 @@ import { __ } from 'gutenberg/extensions/presets/jetpack/utils/i18n'; const defaultOpen = '09:00'; const defaultClose = '17:00'; -class Day extends Component { +class DayEdit extends Component { renderInterval = ( interval, intervalIndex ) => { const { day } = this.props; const { opening, closing } = interval; @@ -30,6 +30,7 @@ class Day extends Component { label={ __( 'Opening' ) } value={ opening } className="business-hours__open" + placeHolder={ defaultOpen } onChange={ value => { this.setHour( value, 'opening', intervalIndex ); } } @@ -39,6 +40,7 @@ class Day extends Component { label={ __( 'Closing' ) } value={ closing } className="business-hours__close" + placeHolder={ defaultClose } onChange={ value => { this.setHour( value, 'closing', intervalIndex ); } } @@ -61,13 +63,7 @@ class Day extends Component {
 
- + { __( 'Add Hours' ) }
@@ -201,4 +197,4 @@ class Day extends Component { } } -export default Day; +export default DayEdit; diff --git a/client/gutenberg/extensions/business-hours/components/day-save.jsx b/client/gutenberg/extensions/business-hours/components/day-preview.jsx similarity index 60% rename from client/gutenberg/extensions/business-hours/components/day-save.jsx rename to client/gutenberg/extensions/business-hours/components/day-preview.jsx index 7af57956f66083..34cb6539dd45bd 100644 --- a/client/gutenberg/extensions/business-hours/components/day-save.jsx +++ b/client/gutenberg/extensions/business-hours/components/day-preview.jsx @@ -11,11 +11,14 @@ import { sprintf } from '@wordpress/i18n'; */ import { _x } from 'gutenberg/extensions/presets/jetpack/utils/i18n'; -class DaySave extends Component { +class DayPreview extends Component { formatTime( time ) { const { timeFormat } = this.props; const [ hours, minutes ] = time.split( ':' ); const _date = new Date(); + if ( ! hours || ! minutes ) { + return false; + } _date.setHours( hours ); _date.setMinutes( minutes ); return date( timeFormat, _date ); @@ -23,32 +26,33 @@ class DaySave extends Component { renderInterval = ( interval, key ) => { return ( - ! isEmpty( interval.opening ) && - ! isEmpty( interval.closing ) && ( -
- { sprintf( - _x( 'From %s to %s', 'from business opening hour to closing hour' ), - this.formatTime( interval.opening ), - this.formatTime( interval.closing ) - ) } -
- ) +
+ { sprintf( + _x( 'From %s to %s', 'from business opening hour to closing hour' ), + this.formatTime( interval.opening ), + this.formatTime( interval.closing ) + ) } +
); }; render() { const { day, localization } = this.props; + const hours = day.hours.filter( + // remove any malformed or empty intervals + interval => this.formatTime( interval.opening ) && this.formatTime( interval.closing ) + ); return (
{ localization.days[ day.name ] }
- { isEmpty( day.hours ) ? ( + { isEmpty( hours ) ? (
{ _x( 'Closed', 'business is closed on a full day' ) }
) : ( - day.hours.map( this.renderInterval ) + hours.map( this.renderInterval ) ) }
); } } -export default DaySave; +export default DayPreview; diff --git a/client/gutenberg/extensions/business-hours/edit.jsx b/client/gutenberg/extensions/business-hours/edit.jsx index cfa1665ea2fb8e..a227446b2a92ef 100644 --- a/client/gutenberg/extensions/business-hours/edit.jsx +++ b/client/gutenberg/extensions/business-hours/edit.jsx @@ -1,6 +1,7 @@ /** * External dependencies */ +import { BlockIcon } from '@wordpress/editor'; import { Component } from '@wordpress/element'; import { Placeholder } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; @@ -10,8 +11,8 @@ import { __experimentalGetSettings } from '@wordpress/date'; /** * Internal dependencies */ -import Day from 'gutenberg/extensions/business-hours/components/day'; -import DaySave from 'gutenberg/extensions/business-hours/components/day-save'; +import DayEdit from 'gutenberg/extensions/business-hours/components/day-edit'; +import DayPreview from 'gutenberg/extensions/business-hours/components/day-preview'; import { icon } from 'gutenberg/extensions/business-hours/index'; import { __ } from 'gutenberg/extensions/presets/jetpack/utils/i18n'; @@ -52,13 +53,22 @@ class BusinessHours extends Component { } render() { - const { attributes, className, isEdit, isSelected } = this.props; + const { attributes, className, isSelected } = this.props; const { days } = attributes; const { localization, hasFetched } = this.state; const { startOfWeek } = localization; const localizedWeek = days.concat( days.slice( 0, startOfWeek ) ).slice( startOfWeek ); - if ( ! isEdit || ! isSelected ) { + if ( ! hasFetched ) { + return ( + } + label={ __( 'Loading business hours' ) } + /> + ); + } + + if ( ! isSelected ) { const settings = __experimentalGetSettings(); const { formats: { time }, @@ -67,21 +77,24 @@ class BusinessHours extends Component {
{ localizedWeek.map( ( day, key ) => { return ( - + ); } ) }
); } - if ( ! hasFetched ) { - return ; - } - return (
{ localizedWeek.map( ( day, key ) => { - return ; + return ( + + ); } ) }
); diff --git a/client/gutenberg/extensions/business-hours/editor.scss b/client/gutenberg/extensions/business-hours/editor.scss index ce03966b7cc78e..9eaa449f994eeb 100644 --- a/client/gutenberg/extensions/business-hours/editor.scss +++ b/client/gutenberg/extensions/business-hours/editor.scss @@ -9,7 +9,6 @@ $break-small: 600px; .business-hours__row { display: flex; - align-items: center; &.business-hours-row__add, &.business-hours-row__closed { @@ -22,8 +21,7 @@ $break-small: 600px; align-items: baseline; .business-hours__day-name { - width: 40%; - font-size: small; + width: 60%; font-weight: bold; overflow-x: hidden; text-overflow: ellipsis; @@ -33,10 +31,6 @@ $break-small: 600px; .components-form-toggle { margin-right: 4px; } - - .components-toggle-control__label { - font-size: smaller; - } } .business-hours__hours { @@ -56,21 +50,16 @@ $break-small: 600px; .components-base-control__label { margin-bottom: 0; - font-size: smaller; - } - - .components-text-control__input { - font-size: smaller; - padding: 4px; } } } } .business-hours__remove { - width: 10%; + align-self: flex-end; + margin-bottom: 8px; text-align: center; - margin-top: 16px; + width: 10%; } .business-hours-row__add button { @@ -81,7 +70,8 @@ $break-small: 600px; } .business-hours__remove button { - display: inline; + display: block; + margin: 0 auto; } .business-hours-row__add .components-button.is-default:hover, diff --git a/client/gutenberg/extensions/business-hours/index.js b/client/gutenberg/extensions/business-hours/index.js index 62ca55d01e5a28..3a8c31c8ee29b0 100644 --- a/client/gutenberg/extensions/business-hours/index.js +++ b/client/gutenberg/extensions/business-hours/index.js @@ -6,7 +6,7 @@ import { Path } from '@wordpress/components'; /** * Internal dependencies */ -import { __ } from 'gutenberg/extensions/presets/jetpack/utils/i18n'; +import { __, _x } from 'gutenberg/extensions/presets/jetpack/utils/i18n'; import renderMaterialIcon from 'gutenberg/extensions/presets/jetpack/utils/render-material-icon'; import './editor.scss'; @@ -30,7 +30,11 @@ export const settings = { supports: { html: true, }, - + keywords: [ + _x( 'opening hours', 'block search term' ), + _x( 'closing time', 'block search term' ), + _x( 'schedule', 'block search term' ), + ], attributes: { days: { type: 'array', @@ -92,7 +96,7 @@ export const settings = { }, }, - edit: props => , + edit: props => , - save: props => , + save: () => null, }; diff --git a/client/gutenberg/extensions/contact-form/index.js b/client/gutenberg/extensions/contact-form/index.js index 209849e1c0b9b3..159429b0f9b575 100644 --- a/client/gutenberg/extensions/contact-form/index.js +++ b/client/gutenberg/extensions/contact-form/index.js @@ -399,7 +399,7 @@ export const childBlocks = [ title: __( 'Radio' ), keywords: [ __( 'Choose' ), __( 'Select' ), __( 'Option' ) ], description: __( - 'Inpsired by radios, only one radio item can be selected at a time. Add several radio button items.' + 'Inspired by radios, only one radio item can be selected at a time. Add several radio button items.' ), icon: renderMaterialIcon( diff --git a/client/gutenberg/extensions/contact-info/address/edit.js b/client/gutenberg/extensions/contact-info/address/edit.js index ac65a3ef684f7a..bedb32b13c04d8 100644 --- a/client/gutenberg/extensions/contact-info/address/edit.js +++ b/client/gutenberg/extensions/contact-info/address/edit.js @@ -2,16 +2,15 @@ * External dependencies */ import classnames from 'classnames'; -import { PlainText, InspectorControls } from '@wordpress/editor'; +import { PlainText } from '@wordpress/editor'; import { Component, Fragment } from '@wordpress/element'; -import { ToggleControl, PanelBody, ExternalLink } from '@wordpress/components'; +import { ToggleControl } from '@wordpress/components'; /** * Internal dependencies */ import { __ } from 'gutenberg/extensions/presets/jetpack/utils/i18n'; -import ClipboardInput from 'gutenberg/extensions/presets/jetpack/utils/clipboard-input'; -import { default as save, googleMapsUrl } from './save'; +import { default as save } from './save'; class AddressEdit extends Component { constructor( ...args ) { @@ -116,19 +115,6 @@ class AddressEdit extends Component { onKeyDown={ this.preventEnterKey } /> { externalLink } - - - { externalLink } - { hasContent && } - { hasContent && ( -
- - { __( 'Visit Google Maps' ) } - -
- ) } -
-
) }
diff --git a/client/gutenberg/extensions/contact-info/address/save.js b/client/gutenberg/extensions/contact-info/address/save.js index 554e89171050cf..54957ef0aa5498 100644 --- a/client/gutenberg/extensions/contact-info/address/save.js +++ b/client/gutenberg/extensions/contact-info/address/save.js @@ -17,35 +17,37 @@ const Address = ( { attributes: { address, addressLine2, addressLine3, city, region, postal, country }, } ) => ( - { address &&
{ address }
} + { address && ( +
{ address }
+ ) } { addressLine2 && ( -
{ addressLine2 }
+
{ addressLine2 }
) } { addressLine3 && ( -
{ addressLine3 }
+
{ addressLine3 }
) } - { city && ! ( region || postal ) &&
{ city }
} + { city && ! ( region || postal ) &&
{ city }
} { city && ( region || postal ) && (
{ [ - { city }, + { city }, ', ', - { region }, + { region }, ' ', - { postal }, + { postal }, ] }
) } { ! city && ( region || postal ) && (
{ [ - { region }, + { region }, ' ', - { postal }, + { postal }, ] }
) } - { country &&
{ country }
} + { country &&
{ country }
}
); diff --git a/client/gutenberg/extensions/contact-info/edit.js b/client/gutenberg/extensions/contact-info/edit.js index e3e1ceb03d8dc3..7960eef9ab3790 100644 --- a/client/gutenberg/extensions/contact-info/edit.js +++ b/client/gutenberg/extensions/contact-info/edit.js @@ -13,6 +13,7 @@ const ALLOWED_BLOCKS = [ 'jetpack/email', 'jetpack/phone', 'jetpack/map', + 'jetpack/business-hours', 'core/paragraph', 'core/image', 'core/heading', diff --git a/client/gutenberg/extensions/contact-info/email/save.js b/client/gutenberg/extensions/contact-info/email/save.js index e14da6e3813fe3..f6b9312d9ea497 100644 --- a/client/gutenberg/extensions/contact-info/email/save.js +++ b/client/gutenberg/extensions/contact-info/email/save.js @@ -1,18 +1,33 @@ /** - * Internal dependencies + * External dependencies */ -import textMatchReplace from 'gutenberg/extensions/presets/jetpack/utils/text-match-replace'; +import emailValidator from 'email-validator'; +import { Fragment } from '@wordpress/element'; const renderEmail = inputText => { - return textMatchReplace( - inputText, - /((?:[a-z|0-9+_](?:\.|_\+)*)+[a-z|0-9]\@(?:[a-z|0-9])+(?:(?:\.){0,1}[a-z|0-9]){2}\.[a-z]{2,22})/gim, - ( email, i ) => ( - - { email } - - ) - ); + const explodedInput = inputText.split( /(\s+)/ ).map( ( email, i ) => { + // Remove and punctuation from the end of the email address. + const emailToValidate = email.replace( /([.,\/#!$%\^&\*;:{}=\-_`~()\]\[])+$/g, '' ); + if ( email.indexOf( '@' ) && emailValidator.validate( emailToValidate ) ) { + return email === emailToValidate ? ( + // Email. + + { email } + + ) : ( + // Email with punctionation. + + + { emailToValidate } + + { email.slice( -( email.length - emailToValidate.length ) ) } + + ); + } + // Just a plain string. + return { email }; + } ); + return explodedInput; }; const save = ( { attributes: { email }, className } ) => diff --git a/client/gutenberg/extensions/contact-info/index.js b/client/gutenberg/extensions/contact-info/index.js index d880df4e8c863e..383b054dbb653e 100644 --- a/client/gutenberg/extensions/contact-info/index.js +++ b/client/gutenberg/extensions/contact-info/index.js @@ -37,7 +37,7 @@ export const settings = { _x( 'address', 'block search term' ), ], icon: renderMaterialIcon( - + ), category: 'jetpack', supports: { diff --git a/client/gutenberg/extensions/contact-info/phone/save.js b/client/gutenberg/extensions/contact-info/phone/save.js index c0fb3d98a54f91..50f6791464a06d 100644 --- a/client/gutenberg/extensions/contact-info/phone/save.js +++ b/client/gutenberg/extensions/contact-info/phone/save.js @@ -1,29 +1,44 @@ /** * Internal dependencies */ -import textMatchReplace from 'gutenberg/extensions/presets/jetpack/utils/text-match-replace'; export function renderPhone( inputText ) { - return textMatchReplace( - inputText, - /([0-9\()+]{1}[\ \-().]?[0-9]{1,6}[\ \-().]?[0-9]{0,6}[\ \-()]?[0-9]{0,6}[\ \-().]?[0-9]{0,6}[\ \-().]?[0-9]{0,6}[\ \-().]?[0-9]{0,6})/g, - ( number, i ) => { - if ( number.trim() === '' ) { - return number; - } - let justNumber = number.replace( /\D/g, '' ); - // Phone numbers starting with + shoud be part of the number. - if ( number.substring( 0, 1 ) === '+' ) { - justNumber = '+' + justNumber; - } + const arrayOfNumbers = inputText.match( /\d+\.\d+|\d+\b|\d+(?=\w)/g ); + if ( ! arrayOfNumbers ) { + // No numbers found + return inputText; + } + const indexOfFirstNumber = inputText.indexOf( arrayOfNumbers[ 0 ] ); - return ( - - { number } - - ); + // Assume that eveything after the first number should be part of the phone number. + // care about the first prefix character. + let phoneNumber = indexOfFirstNumber ? inputText.substring( indexOfFirstNumber - 1 ) : inputText; + let prefix = indexOfFirstNumber ? inputText.substring( 0, indexOfFirstNumber ) : ''; + + let justNumber = phoneNumber.replace( /\D/g, '' ); + // Phone numbers starting with + should be part of the number. + if ( /[0-9/+/(]/.test( phoneNumber[ 0 ] ) ) { + // Remove the special character from the prefix so they don't appear twice. + prefix = prefix.slice( 0, -1 ); + // Phone numbers starting with + shoud be part of the number. + if ( phoneNumber[ 0 ] === '+' ) { + justNumber = '+' + justNumber; } - ); + } else { + // Remove the first character. + phoneNumber = phoneNumber.substring( 1 ); + } + const prefixSpan = prefix.trim() ? ( + + { prefix } + + ) : null; + return [ + prefixSpan, + + { phoneNumber } + , + ]; } const save = ( { attributes: { phone }, className } ) => diff --git a/client/gutenberg/extensions/mailchimp/edit.jsx b/client/gutenberg/extensions/mailchimp/edit.jsx index 39cd2bba7f6204..0e8654c056fda5 100644 --- a/client/gutenberg/extensions/mailchimp/edit.jsx +++ b/client/gutenberg/extensions/mailchimp/edit.jsx @@ -144,10 +144,13 @@ class MailchimpSubscribeEdit extends Component { 'You need to connect your Mailchimp account and choose a list in order to start collecting Email subscribers.' ) }
- { __( 'Set up Mailchimp form' ) } +
+

- @@ -191,6 +194,7 @@ class MailchimpSubscribeEdit extends Component {
false } placeholder={ emailPlaceholder } diff --git a/client/gutenberg/extensions/mailchimp/editor.scss b/client/gutenberg/extensions/mailchimp/editor.scss index 311fb2a1ce67d1..84de75481ad0a2 100644 --- a/client/gutenberg/extensions/mailchimp/editor.scss +++ b/client/gutenberg/extensions/mailchimp/editor.scss @@ -18,4 +18,12 @@ display: none; } + .wp-block-jetpack-mailchimp_text-input, .jetpack-submit-button { + margin-bottom: 1.5rem; + } + + .wp-block-button .wp-block-button__link { + margin-top: 0; + } + } diff --git a/client/gutenberg/extensions/presets/jetpack/index.json b/client/gutenberg/extensions/presets/jetpack/index.json index 07c7a10b268c7d..5fd3f798c209e8 100644 --- a/client/gutenberg/extensions/presets/jetpack/index.json +++ b/client/gutenberg/extensions/presets/jetpack/index.json @@ -1,24 +1,24 @@ { "production": [ + "business-hours", "contact-form", + "contact-info", "gif", + "mailchimp", "map", "markdown", "publicize", "related-posts", "shortlinks", "simple-payments", + "slideshow", "subscriptions", - "tiled-gallery" + "tiled-gallery", + "videopress", + "wordads" ], "beta": [ - "business-hours", - "contact-info", - "mailchimp", "repeat-visitor", - "slideshow", - "videopress", - "vr", - "wordads" + "vr" ] } diff --git a/client/gutenberg/extensions/presets/jetpack/package.json b/client/gutenberg/extensions/presets/jetpack/package.json index 79ca190b9adac7..bec5346b8d9a12 100644 --- a/client/gutenberg/extensions/presets/jetpack/package.json +++ b/client/gutenberg/extensions/presets/jetpack/package.json @@ -1,6 +1,6 @@ { "name": "@automattic/jetpack-blocks", - "version": "12.1.0", + "version": "13.1.0", "description": "Gutenberg blocks for the Jetpack WordPress plugin", "main": "build/editor.js", "files": [ diff --git a/client/gutenberg/extensions/related-posts/edit.jsx b/client/gutenberg/extensions/related-posts/edit.jsx index 230f5f3d2dcb9e..9a3b021e2ab0fe 100644 --- a/client/gutenberg/extensions/related-posts/edit.jsx +++ b/client/gutenberg/extensions/related-posts/edit.jsx @@ -23,7 +23,7 @@ function PlaceholderPostEdit( props ) { aria-labelledby={ props.id + '-heading' } > - { __( 'Preview: Not enough related posts found' ) } + { __( "Preview unavailable: you haven't published enough posts with similar content." ) } { props.displayThumbnails && (
{ const imagesNormalized = images.map( image => pickRelevantMediaFiles( image ) ); setAttributes( { - images: [ ...imagesNormalized, ...currentImages ], + images: [ ...currentImages, ...imagesNormalized ], } ); if ( ! imagesNormalized.every( image => isBlobURL( image.url ) ) ) { unlockPostSaving( lockName ); @@ -112,7 +112,7 @@ class SlideshowEdit extends Component { /> { autoplay && ( { setAttributes( { delay: value } ); diff --git a/client/gutenberg/extensions/slideshow/index.js b/client/gutenberg/extensions/slideshow/index.js index b401aeb5ecb16f..4a33de4f611c27 100644 --- a/client/gutenberg/extensions/slideshow/index.js +++ b/client/gutenberg/extensions/slideshow/index.js @@ -71,7 +71,7 @@ export const name = 'slideshow'; export const settings = { title: __( 'Slideshow' ), category: 'jetpack', - keywords: [ __( 'image' ) ], + keywords: [ __( 'image' ), __( 'gallery' ), __( 'slider' ) ], description: __( 'Add an interactive slideshow.' ), attributes, supports: { diff --git a/client/gutenberg/extensions/slideshow/slideshow.js b/client/gutenberg/extensions/slideshow/slideshow.js index d3ada7e4c3bef5..722fbebecda4ea 100644 --- a/client/gutenberg/extensions/slideshow/slideshow.js +++ b/client/gutenberg/extensions/slideshow/slideshow.js @@ -3,7 +3,6 @@ */ import ResizeObserver from 'resize-observer-polyfill'; import classnames from 'classnames'; -import { __ } from 'gutenberg/extensions/presets/jetpack/utils/i18n'; import { Component, createRef } from '@wordpress/element'; import { isBlobURL } from '@wordpress/blob'; import { isEqual } from 'lodash'; @@ -63,7 +62,10 @@ class Slideshow extends Component { delay !== prevProps.delay || images !== prevProps.images ) { - const realIndex = images !== prevProps.images ? 0 : this.swiperInstance.realIndex; + const realIndex = + images.length === prevProps.images.length + ? this.swiperInstance.realIndex + : prevProps.images.length; this.swiperInstance && this.swiperInstance.destroy( true, true ); this.buildSwiper( realIndex ).then( swiper => { this.swiperInstance = swiper; @@ -102,6 +104,7 @@ class Slideshow extends Component { const { autoplay, className, delay, effect, images } = this.props; // Note: React omits the data attribute if the value is null, but NOT if it is false. // This is the reason for the unusual logic related to autoplay below. + /* eslint-disable jsx-a11y/anchor-is-valid */ return (
) ) } -
-
); + /* eslint-enable jsx-a11y/anchor-is-valid */ } prefersReducedMotion = () => { diff --git a/client/gutenberg/extensions/tiled-gallery/css-gram.scss b/client/gutenberg/extensions/tiled-gallery/css-gram.scss new file mode 100644 index 00000000000000..9fd2f49c52aa15 --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/css-gram.scss @@ -0,0 +1,86 @@ +/** + * This code is based on CSS gram: + * https://github.com/una/CSSgram/tree/master + * + * Due to the packaging options available, the source has been duplicated and adapted here + * to best fit our specific needs. + */ + +/* From https://github.com/una/CSSgram/blob/0.1.12/source/scss/_shared.scss */ +@mixin pseudo-elem { + content: ''; + display: block; + height: 100%; + width: 100%; + top: 0; + left: 0; + position: absolute; + pointer-events: none; +} + +@mixin filter-base { + position: relative; + + img { + width: 100%; + z-index: 1; + } + + &::before { + @include pseudo-elem; + z-index: 2; + } + + &::after { + @include pseudo-elem; + z-index: 3; + } +} + +/** + * 1977 + * From https://github.com/una/CSSgram/blob/0.1.12/source/scss/1977.scss + */ +@mixin _1977( $filters... ) { + @include filter-base; + filter: contrast( 1.1 ) brightness( 1.1 ) saturate( 1.3 ) $filters; + + &::after { + background: rgba( 243, 106, 188, 0.3 ); + mix-blend-mode: screen; + } + + @content; +} + +/* + * Clarendon + * From https://github.com/una/CSSgram/blob/0.1.12/source/scss/clarendon.scss + */ +@mixin clarendon( $filters... ) { + @include filter-base; + filter: contrast( 1.2 ) saturate( 1.35 ) $filters; + + &::before { + background: rgba( 127, 187, 227, 0.2 ); + mix-blend-mode: overlay; + } + + @content; +} + +/** + * Gingham + * From https://github.com/una/CSSgram/blob/0.1.12/source/scss/gingham.scss + */ +@mixin gingham( $filters... ) { + @include filter-base; + filter: brightness( 1.05 ) hue-rotate( -10deg ) $filters; + + &::after { + background: rgb( 230, 230, 250 ); + mix-blend-mode: soft-light; + } + + @content; +} diff --git a/client/gutenberg/extensions/tiled-gallery/deprecated/v1/constants.js b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/constants.js new file mode 100644 index 00000000000000..55a451fccf6185 --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/constants.js @@ -0,0 +1,27 @@ +export const ALLOWED_MEDIA_TYPES = [ 'image' ]; +export const GUTTER_WIDTH = 4; +export const MAX_COLUMNS = 20; +export const PHOTON_MAX_RESIZE = 2000; + +/** + * Layouts + */ +export const LAYOUT_CIRCLE = 'circle'; +export const LAYOUT_COLUMN = 'columns'; +export const LAYOUT_DEFAULT = 'rectangular'; +export const LAYOUT_SQUARE = 'square'; +export const LAYOUT_STYLES = [ + { + isDefault: true, + name: LAYOUT_DEFAULT, + }, + { + name: LAYOUT_CIRCLE, + }, + { + name: LAYOUT_SQUARE, + }, + { + name: LAYOUT_COLUMN, + }, +]; diff --git a/client/gutenberg/extensions/tiled-gallery/deprecated/v1/image.js b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/image.js new file mode 100644 index 00000000000000..61d4a2cd05cef6 --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/image.js @@ -0,0 +1,51 @@ +/** + * External Dependencies + */ +import { isBlobURL } from '@wordpress/blob'; + +export default function GalleryImageSave( props ) { + const { + 'aria-label': ariaLabel, + alt, + // caption, + height, + id, + link, + linkTo, + origUrl, + url, + width, + } = props; + + if ( isBlobURL( origUrl ) ) { + return null; + } + + let href; + + switch ( linkTo ) { + case 'media': + href = url; + break; + case 'attachment': + href = link; + break; + } + + const img = ( + { + ); + + return ( +
{ href ? { img } : img }
+ ); +} diff --git a/client/gutenberg/extensions/tiled-gallery/deprecated/v1/index.js b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/index.js new file mode 100644 index 00000000000000..69539d007cdeff --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/index.js @@ -0,0 +1,81 @@ +/** + * Internal dependencies + */ +export { default as save } from './save'; +import { LAYOUT_DEFAULT } from './constants'; + +export const attributes = { + // Set default align + align: { + default: 'center', + type: 'string', + }, + // Set default className (used with block styles) + className: { + default: `is-style-${ LAYOUT_DEFAULT }`, + type: 'string', + }, + columns: { + type: 'number', + }, + ids: { + default: [], + type: 'array', + }, + images: { + type: 'array', + default: [], + source: 'query', + selector: '.tiled-gallery__item', + query: { + alt: { + attribute: 'alt', + default: '', + selector: 'img', + source: 'attribute', + }, + caption: { + selector: 'figcaption', + source: 'html', + type: 'string', + }, + height: { + attribute: 'data-height', + selector: 'img', + source: 'attribute', + type: 'number', + }, + id: { + attribute: 'data-id', + selector: 'img', + source: 'attribute', + }, + link: { + attribute: 'data-link', + selector: 'img', + source: 'attribute', + }, + url: { + attribute: 'data-url', + selector: 'img', + source: 'attribute', + }, + width: { + attribute: 'data-width', + selector: 'img', + source: 'attribute', + type: 'number', + }, + }, + }, + linkTo: { + default: 'none', + type: 'string', + }, +}; + +export const support = { + align: [ 'center', 'wide', 'full' ], + customClassName: false, + html: false, +}; diff --git a/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/column.js b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/column.js new file mode 100644 index 00000000000000..a3ed5cdf04cbb8 --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/column.js @@ -0,0 +1,3 @@ +export default function Column( { children } ) { + return
{ children }
; +} diff --git a/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/gallery.js b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/gallery.js new file mode 100644 index 00000000000000..94fc61e4be980f --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/gallery.js @@ -0,0 +1,7 @@ +export default function Gallery( { children, galleryRef } ) { + return ( +
+ { children } +
+ ); +} diff --git a/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/index.js b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/index.js new file mode 100644 index 00000000000000..6492d78811c466 --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/index.js @@ -0,0 +1,141 @@ +/** + * External dependencies + */ +import photon from 'photon'; +import { __ } from 'gutenberg/extensions/presets/jetpack/utils/i18n'; +import { Component } from '@wordpress/element'; +import { format as formatUrl, parse as parseUrl } from 'url'; +import { isBlobURL } from '@wordpress/blob'; +import { sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import Image from '../image'; +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 { images, linkTo, selectedImage } = 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' ), i + 1, images.length ); + return ( + { + ); + } + + 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 ( +
+ + { children } +
+ ); + } +} + +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/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/mosaic/index.js b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/mosaic/index.js new file mode 100644 index 00000000000000..8c56b1641dd1e5 --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/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 ( + + { rows.map( ( row, rowIndex ) => ( + + { row.map( ( colSize, colIndex ) => { + const columnImages = renderedImages.slice( cursor, cursor + colSize ); + cursor += colSize; + return { columnImages }; + } ) } + + ) ) } + + ); + } +} diff --git a/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/mosaic/ratios.js b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/mosaic/ratios.js new file mode 100644 index 00000000000000..8accd552b710a4 --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/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.} ratios Ratios of images put into shape + * @param {number} columnCount Number of columns + * + * @return {Array.>} 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/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/mosaic/resize.js b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/mosaic/resize.js new file mode 100644 index 00000000000000..022729c8bac724 --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/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} parts Array of numbers to fit + * @param {number} target Number that sum should match + * @return {Array} 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 element + Array.from( col.children ).forEach( ( item, i ) => { + const height = adjustedHeights[ i ]; + item.setAttribute( 'style', `height:${ height }px;width:${ width }px;` ); + } ); +} diff --git a/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/row.js b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/row.js new file mode 100644 index 00000000000000..200a58c2e3acf9 --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/row.js @@ -0,0 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +export default function Row( { children, className } ) { + return
{ children }
; +} diff --git a/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/square.js b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/layout/square.js new file mode 100644 index 00000000000000..2a1ab888b1916d --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/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 ( + + { [ + ...( remainder ? [ take( renderedImages, remainder ) ] : [] ), + ...chunk( drop( renderedImages, remainder ), columnCount ), + ].map( ( imagesInRow, rowIndex ) => ( + + { imagesInRow.map( ( image, colIndex ) => ( + { image } + ) ) } + + ) ) } + + ); +} diff --git a/client/gutenberg/extensions/tiled-gallery/deprecated/v1/save.js b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/save.js new file mode 100644 index 00000000000000..65da6915ab5626 --- /dev/null +++ b/client/gutenberg/extensions/tiled-gallery/deprecated/v1/save.js @@ -0,0 +1,31 @@ +/** + * Internal dependencies + */ +import Layout from './layout'; +import { getActiveStyleName } from 'gutenberg/extensions/utils'; +import { LAYOUT_STYLES } from './constants'; + +export function defaultColumnsNumber( attributes ) { + return Math.min( 3, attributes.images.length ); +} + +export default function TiledGallerySave( { attributes } ) { + const { images } = attributes; + + if ( ! images.length ) { + return null; + } + + const { align, className, columns = defaultColumnsNumber( attributes ), linkTo } = attributes; + + return ( + + ); +} diff --git a/client/gutenberg/extensions/tiled-gallery/edit.jsx b/client/gutenberg/extensions/tiled-gallery/edit.jsx index 1d8c0aec8ecc9d..2e6f2e47e5d52f 100644 --- a/client/gutenberg/extensions/tiled-gallery/edit.jsx +++ b/client/gutenberg/extensions/tiled-gallery/edit.jsx @@ -24,6 +24,7 @@ import { /** * Internal dependencies */ +import FilterToolbar from './filter-toolbar'; import Layout from './layout'; import { __ } from 'gutenberg/extensions/presets/jetpack/utils/i18n'; import { ALLOWED_MEDIA_TYPES, LAYOUT_STYLES, MAX_COLUMNS } from './constants'; @@ -155,31 +156,53 @@ class TiledGalleryEdit extends Component { render() { const { selectedImage } = this.state; - const { attributes, isSelected, className, noticeOperations, noticeUI } = this.props; - const { align, columns = defaultColumnsNumber( attributes ), images, linkTo } = attributes; + const { + attributes, + isSelected, + className, + noticeOperations, + noticeUI, + setAttributes, + } = this.props; + const { + align, + columns = defaultColumnsNumber( attributes ), + imageFilter, + images, + linkTo, + } = attributes; const dropZone = ; const controls = ( { !! images.length && ( - - img.id ) } - render={ ( { open } ) => ( - - ) } + + + img.id ) } + render={ ( { open } ) => ( + + ) } + /> + + { + setAttributes( { imageFilter: value } ); + this.setState( { selectedImage: null } ); + } } /> - + ) } ); @@ -192,7 +215,7 @@ class TiledGalleryEdit extends Component { icon={
{ icon }
} className={ className } labels={ { - title: __( 'Tiled gallery' ), + title: __( 'Tiled Gallery' ), name: __( 'images' ), } } onSelect={ this.onSelectImages } @@ -212,7 +235,7 @@ class TiledGalleryEdit extends Component { { controls } - + { layoutSupportsColumns( layoutStyle ) && images.length > 1 && ( + + + + ), + title: _x( 'Original', 'image style' ), + value: undefined, + }, + { + icon: ( + /* 1 */ + + + + + ), + title: _x( 'Black and White', 'image style' ), + value: 'black-and-white', + }, + { + icon: ( + /* 2 */ + + + + + ), + title: _x( 'Sepia', 'image style' ), + value: 'sepia', + }, + { + icon: ( + /* 3 */ + + + + + ), + title: '1977', + value: '1977', + }, + { + icon: ( + /* 4 */ + + + + + ), + title: _x( 'Clarendon', 'image style' ), + value: 'clarendon', + }, + { + icon: ( + /* 5 */ + + + + + ), + title: _x( 'Gingham', 'image style' ), + value: 'gingham', + }, +]; + +const label = __( 'Pick an image filter' ); + +export default function FilterToolbar( { value, onChange } ) { + return ( + { + return ( + + + + + ), + }, + ] } + /> + ); + } } + renderContent={ ( { onClose } ) => { + const applyOrUnset = nextValue => () => { + onChange( value === nextValue ? undefined : nextValue ); + onClose(); + }; + return ( + + { availableFilters.map( ( { icon, title, value: filterValue } ) => ( + + { title } + + ) ) } + + ); + } } + /> + ); +} diff --git a/client/gutenberg/extensions/tiled-gallery/gallery-image/edit.js b/client/gutenberg/extensions/tiled-gallery/gallery-image/edit.js index 3a1ca4de504fba..004f949c02e71a 100644 --- a/client/gutenberg/extensions/tiled-gallery/gallery-image/edit.js +++ b/client/gutenberg/extensions/tiled-gallery/gallery-image/edit.js @@ -102,6 +102,7 @@ class GalleryImageEdit extends Component { // caption, height, id, + imageFilter, isSelected, link, linkTo, @@ -153,6 +154,7 @@ class GalleryImageEdit extends Component { className={ classnames( 'tiled-gallery__item', { 'is-selected': isSelected, 'is-transient': isBlobURL( origUrl ), + [ `filter__${ imageFilter }` ]: !! imageFilter, } ) } > { isSelected && ( diff --git a/client/gutenberg/extensions/tiled-gallery/gallery-image/save.js b/client/gutenberg/extensions/tiled-gallery/gallery-image/save.js index 6ea37ce40e8274..ac57133da7229d 100644 --- a/client/gutenberg/extensions/tiled-gallery/gallery-image/save.js +++ b/client/gutenberg/extensions/tiled-gallery/gallery-image/save.js @@ -1,6 +1,7 @@ /** * External Dependencies */ +import classnames from 'classnames'; import { isBlobURL } from '@wordpress/blob'; /* @TODO Caption has been commented out */ @@ -8,9 +9,9 @@ import { isBlobURL } from '@wordpress/blob'; export default function GalleryImageSave( props ) { const { - 'aria-label': ariaLabel, alt, // caption, + imageFilter, height, id, link, @@ -38,7 +39,6 @@ export default function GalleryImageSave( props ) { const img = ( { +
{ href ? { img } : img } { /* ! RichText.isEmpty( caption ) && ( diff --git a/client/gutenberg/extensions/tiled-gallery/index.js b/client/gutenberg/extensions/tiled-gallery/index.js index afc3df8a7cb129..d9d4f8adc245f9 100644 --- a/client/gutenberg/extensions/tiled-gallery/index.js +++ b/client/gutenberg/extensions/tiled-gallery/index.js @@ -24,6 +24,8 @@ import { */ import './editor.scss'; +import * as deprecatedV1 from './deprecated/v1'; + // Style names are translated. Avoid introducing an i18n dependency elsewhere (view) // by only including the labels here, the only place they're needed. // @@ -57,6 +59,9 @@ const blockAttributes = { default: [], type: 'array', }, + imageFilter: { + type: 'string', + }, images: { type: 'array', default: [], @@ -181,4 +186,5 @@ export const settings = { }, edit, save, + deprecated: [ deprecatedV1 ], }; diff --git a/client/gutenberg/extensions/tiled-gallery/layout/index.js b/client/gutenberg/extensions/tiled-gallery/layout/index.js index e0905ef7947b5c..a3f1cde91e2f39 100644 --- a/client/gutenberg/extensions/tiled-gallery/layout/index.js +++ b/client/gutenberg/extensions/tiled-gallery/layout/index.js @@ -50,6 +50,7 @@ export default class Layout extends Component { // This is because the images are stored in an array in the block attributes. renderImage( img, i ) { const { + imageFilter, images, isSave, linkTo, @@ -71,13 +72,14 @@ export default class Layout extends Component { // caption={ img.caption } height={ img.height } id={ img.id } - origUrl={ img.url } + 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 } diff --git a/client/gutenberg/extensions/tiled-gallery/save.jsx b/client/gutenberg/extensions/tiled-gallery/save.jsx index 5b39c73e609f3b..377776424995a8 100644 --- a/client/gutenberg/extensions/tiled-gallery/save.jsx +++ b/client/gutenberg/extensions/tiled-gallery/save.jsx @@ -7,7 +7,7 @@ import { getActiveStyleName } from 'gutenberg/extensions/utils'; import { LAYOUT_STYLES } from './constants'; export default function TiledGallerySave( { attributes } ) { - const { images } = attributes; + const { imageFilter, images } = attributes; if ( ! images.length ) { return null; @@ -20,6 +20,7 @@ export default function TiledGallerySave( { attributes } ) { align={ align } className={ className } columns={ columns } + imageFilter={ imageFilter } images={ images } isSave layoutStyle={ getActiveStyleName( LAYOUT_STYLES, className ) } diff --git a/client/gutenberg/extensions/tiled-gallery/view.scss b/client/gutenberg/extensions/tiled-gallery/view.scss index 85f9b588e07190..d7e475c5d6244a 100644 --- a/client/gutenberg/extensions/tiled-gallery/view.scss +++ b/client/gutenberg/extensions/tiled-gallery/view.scss @@ -1,4 +1,5 @@ @import './variables.scss'; +@import './css-gram.scss'; $tiled-gallery-max-column-count: 20; @@ -70,6 +71,26 @@ $tiled-gallery-max-column-count: 20; padding: 0; position: relative; + &.filter__black-and-white { + filter: grayscale( 100% ); + } + + &.filter__sepia { + filter: sepia( 100% ); + } + + &.filter__1977 { + @include _1977; + } + + &.filter__clarendon { + @include clarendon; + } + + &.filter__gingham { + @include gingham; + } + & + & { margin-top: $tiled-gallery-gutter; } diff --git a/client/gutenberg/extensions/videopress/editor.js b/client/gutenberg/extensions/videopress/editor.js index da021d8819d54b..b0f742635bebe9 100644 --- a/client/gutenberg/extensions/videopress/editor.js +++ b/client/gutenberg/extensions/videopress/editor.js @@ -94,6 +94,11 @@ const addVideoPressSupport = ( settings, name ) => { ], }, + supports: { + ...settings.supports, + reusable: false, + }, + edit: withVideoPressEdit( settings.edit ), save: withVideoPressSave( settings.save ), diff --git a/client/gutenberg/extensions/wordads/edit.js b/client/gutenberg/extensions/wordads/edit.js index 5556571071f3c9..b3fcebb0a94d0f 100644 --- a/client/gutenberg/extensions/wordads/edit.js +++ b/client/gutenberg/extensions/wordads/edit.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import classNames from 'classnames'; import { __ } from 'gutenberg/extensions/presets/jetpack/utils/i18n'; import { BlockControls } from '@wordpress/editor'; import { Component, Fragment } from '@wordpress/element'; @@ -19,10 +18,7 @@ import './editor.scss'; class WordAdsEdit extends Component { render() { const { attributes, setAttributes } = this.props; - const { align, format } = attributes; - const classes = classNames( 'wp-block-jetpack-wordads', `jetpack-wordads-${ format }`, { - [ `align${ align }` ]: align, - } ); + const { format } = attributes; const selectedFormatObject = AD_FORMATS.filter( ( { tag } ) => tag === format )[ 0 ]; return ( @@ -33,7 +29,7 @@ class WordAdsEdit extends Component { onChange={ nextFormat => setAttributes( { format: nextFormat } ) } /> -
+
- + );