diff --git a/package-lock.json b/package-lock.json index 594354ed0d4a0..c9b3884216339 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4857,6 +4857,7 @@ "classnames": "^2.2.5", "clipboard": "^2.0.1", "dom-scroll-into-view": "^1.2.1", + "gradient-parser": "^0.1.5", "lodash": "^4.17.15", "memize": "^1.0.5", "moment": "^2.22.1", @@ -13831,6 +13832,11 @@ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", "dev": true }, + "gradient-parser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/gradient-parser/-/gradient-parser-0.1.5.tgz", + "integrity": "sha1-DH4heVWeXOfY1x9EI6+TcQCyJIw=" + }, "grapheme-breaker": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/grapheme-breaker/-/grapheme-breaker-0.3.2.tgz", diff --git a/packages/block-editor/src/components/gradient-picker/control.js b/packages/block-editor/src/components/gradient-picker/control.js index 73609ffdf17e2..4b850fa883860 100644 --- a/packages/block-editor/src/components/gradient-picker/control.js +++ b/packages/block-editor/src/components/gradient-picker/control.js @@ -7,7 +7,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { BaseControl } from '@wordpress/components'; +import { BaseControl, CustomGradientPicker } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; /** @@ -15,7 +15,7 @@ import { __ } from '@wordpress/i18n'; */ import GradientPicker from './'; -export default function( { className, ...props } ) { +export default function( { className, value, onChange, ...props } ) { return ( + ); } diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js index 06eabc9f6f11f..c7151f726872d 100644 --- a/packages/block-editor/src/store/defaults.js +++ b/packages/block-editor/src/store/defaults.js @@ -175,56 +175,56 @@ export const SETTINGS_DEFAULTS = { }, { name: __( 'Very light gray to cyan bluish gray' ), - gradient: 'linear-gradient(135deg, rgb(238, 238, 238) 0%, rgb(169, 184, 195)', + gradient: 'linear-gradient(135deg, rgb(238, 238, 238) 0%, rgb(169, 184, 195) 100%)', }, // The following use new, customized colors. { name: __( 'Cool to warm spectrum' ), - gradient: 'linear-gradient(135deg, rgb(74, 234, 220), rgb(151, 120, 209), rgb(207, 42, 186), rgb(238, 44, 130), rgb(251, 105, 98),rgb(254, 248, 76)', + gradient: 'linear-gradient(135deg, rgb(74, 234, 220) 0%, rgb(151, 120, 209) 20%, rgb(207, 42, 186) 40%, rgb(238, 44, 130) 60%, rgb(251, 105, 98) 80%, rgb(254, 248, 76) 100% )', }, { name: __( 'Blush light purple' ), - gradient: 'linear-gradient(135deg, rgb(255, 206, 236), rgb(152, 150, 240)', + gradient: 'linear-gradient(135deg, rgb(255, 206, 236) 0%, rgb(152, 150, 240) 100% )', }, { name: __( 'Blush bordeaux' ), - gradient: 'linear-gradient(135deg, rgb(254, 205, 165), rgb(254, 45, 45), rgb(107, 0, 62)', + gradient: 'linear-gradient(135deg, rgb(254, 205, 165) 0%, rgb(254, 45, 45) 50%, rgb(107, 0, 62) 100% )', }, { name: __( 'Purple crush' ), - gradient: 'linear-gradient(135deg, rgb(52, 226, 228), rgb(71, 33, 251), rgb(171, 29, 254)', + gradient: 'linear-gradient(135deg, rgb(52, 226, 228) 0%, rgb(71, 33, 251) 50%, rgb(171, 29, 254) 100% )', }, { name: __( 'Luminous dusk' ), - gradient: 'linear-gradient(135deg, rgb(255, 203, 112), rgb(199, 81, 192), rgb(65, 88, 208)', + gradient: 'linear-gradient(135deg, rgb(255, 203, 112) 0%, rgb(199, 81, 192) 50%, rgb(65, 88, 208) 100% )', }, { name: __( 'Hazy dawn' ), - gradient: 'linear-gradient(135deg, rgb(250, 172, 168), rgb(218, 208, 236)', + gradient: 'linear-gradient(135deg, rgb(250, 172, 168) 0%, rgb(218, 208, 236) 100% )', }, { name: __( 'Pale ocean' ), - gradient: 'linear-gradient(135deg, rgb(255, 245, 203), rgb(182, 227, 212), rgb(51, 167, 181)', + gradient: 'linear-gradient(135deg, rgb(255, 245, 203) 0%, rgb(182, 227, 212) 50%, rgb(51, 167, 181) 100% )', }, { name: __( 'Electric grass' ), - gradient: 'linear-gradient(135deg, rgb(202, 248, 128), rgb(113, 206, 126)', + gradient: 'linear-gradient(135deg, rgb(202, 248, 128) 0%, rgb(113, 206, 126) 100% )', }, { name: __( 'Subdued olive' ), - gradient: 'linear-gradient(135deg, rgb(250, 250, 225), rgb(103, 166, 113)', + gradient: 'linear-gradient(135deg, rgb(250, 250, 225) 0%, rgb(103, 166, 113) 100% )', }, { name: __( 'Atomic cream' ), - gradient: 'linear-gradient(135deg, rgb(253, 215, 154), rgb(0, 74, 89)', + gradient: 'linear-gradient(135deg, rgb(253, 215, 154) 0%, rgb(0, 74, 89) 100% )', }, { name: __( 'Nightshade' ), - gradient: 'linear-gradient(135deg, rgb(51, 9, 104), rgb(49, 205, 207)', + gradient: 'linear-gradient(135deg, rgb(51, 9, 104) 0%, rgb(49, 205, 207) 100% )', }, { name: __( 'Midnight' ), - gradient: 'linear-gradient(135deg, rgb(2, 3, 129), rgb(40, 116, 252)', + gradient: 'linear-gradient(135deg, rgb(2, 3, 129) 0%, rgb(40, 116, 252) 100% )', }, ], }; diff --git a/packages/components/package.json b/packages/components/package.json index d19667eca46cf..189df5b1d01d9 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -35,6 +35,7 @@ "classnames": "^2.2.5", "clipboard": "^2.0.1", "dom-scroll-into-view": "^1.2.1", + "gradient-parser": "^0.1.5", "lodash": "^4.17.15", "memize": "^1.0.5", "moment": "^2.22.1", diff --git a/packages/components/src/custom-gradient-picker/index.js b/packages/components/src/custom-gradient-picker/index.js new file mode 100644 index 0000000000000..7725cd4de6ccb --- /dev/null +++ b/packages/components/src/custom-gradient-picker/index.js @@ -0,0 +1,405 @@ + +/** + * External dependencies + */ +import gradientParser from 'gradient-parser'; +import { compact, map, get, some } from 'lodash'; +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { useCallback, useEffect, useState, useRef, useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import IconButton from '../icon-button'; +import Button from '../button'; +import ColorPicker from '../color-picker'; +import Dropdown from '../dropdown'; + +const INSERT_POINT_WIDTH = 23; +const GRADIENT_MARKERS_WIDTH = 18; +const MINIMUM_DISTANCE_BETWEEN_MARKERS = ( INSERT_POINT_WIDTH + GRADIENT_MARKERS_WIDTH ) / 2; +const MINIMUM_ABSOLUTE_LEFT_POSITION = 5; +const MINIMUM_SIGNIFICANT_MOVE = 5; +const DEFAULT_GRADIENT = 'linear-gradient(135deg, rgba(6, 147, 227, 1) 0%, rgb(155, 81, 224) 100%)'; + +const serializeGradientColor = ( { type, value } ) => { + return `${ type }( ${ value.join( ',' ) })`; +}; + +const serializeGradientPosition = ( { type, value } ) => { + return `${ value }${ type }`; +}; + +const serializeGradientColorStop = ( { type, value, length } ) => { + return `${ serializeGradientColor( { type, value } ) } ${ serializeGradientPosition( length ) }`; +}; + +const serializeGradientOrientation = ( orientation ) => { + if ( ! orientation || orientation.type !== 'angular' ) { + return; + } + return `${ orientation.value }deg`; +}; + +const tinyColorRgbToGradientColorStop = ( { r, g, b, a } ) => { + if ( a === 1 ) { + return { + type: 'rgb', + value: [ r, g, b ], + }; + } + return { + type: 'rgba', + value: [ r, g, b, a ], + }; +}; + +const serializeGradient = ( { type, orientation, colorStops } ) => { + const serializedOrientation = serializeGradientOrientation( orientation ); + const serializedColorStops = colorStops.sort( ( colorStop1, colorStop2 ) => { + return get( colorStop1, [ 'length', 'value' ], 0 ) - get( colorStop2, [ 'length', 'value' ], 0 ); + } ).map( serializeGradientColorStop ); + return `${ type }( ${ compact( [ serializedOrientation, ...serializedColorStops ] ).join( ', ' ) } )`; +}; + +const getGradientAbsolutePosition = ( relativeValue, maximumAbsoluteValue, minimumAbsoluteValue ) => { + return Math.round( + ( relativeValue * ( maximumAbsoluteValue - minimumAbsoluteValue ) / 100 ) + minimumAbsoluteValue + ); +}; + +const getGradientRelativePosition = ( absoluteValue, maximumAbsoluteValue, minimumAbsoluteValue ) => { + const relativePosition = Math.round( + ( ( absoluteValue - minimumAbsoluteValue ) * 100 ) / ( maximumAbsoluteValue - minimumAbsoluteValue ) + ); + return Math.min( Math.max( relativePosition, 0 ), 100 ); +}; + +export default function CustomGradientPicker( { value = DEFAULT_GRADIENT, onChange } ) { + //return null; + const parsedGradient = useMemo( + () => { + try { + return gradientParser.parse( value )[ 0 ]; + } catch ( error ) { + return gradientParser.parse( DEFAULT_GRADIENT )[ 0 ]; + } + }, + [ value ] + ); + const [ maximumInsertPointPosition, setMaximumInsertPointPosition ] = useState(); + const [ gradientPickerXPosition, setGradientPickerXPosition ] = useState(); + const [ insertPointPosition, setInsertPointPosition ] = useState( null ); + const [ insertPointMoveEnabled, setInsertPointMoveEnabled ] = useState( true ); + const [ isEditingColorAtPosition, setIsEditingColorAtPosition ] = useState( null ); + const controlPointMoveState = useRef(); + const gradientPickerRef = useRef(); + const markerPoints = useMemo( + () => { + if ( ! parsedGradient ) { + return []; + } + return compact( + map( parsedGradient.colorStops, ( colorStop ) => { + if ( ! colorStop || ! colorStop.length || colorStop.length.type !== '%' ) { + return null; + } + return { + color: serializeGradientColor( colorStop ), + absolutePosition: getGradientAbsolutePosition( colorStop.length.value, maximumInsertPointPosition, MINIMUM_ABSOLUTE_LEFT_POSITION ), + position: colorStop.length.value, + }; + } ) + ); + }, + [ parsedGradient, maximumInsertPointPosition ] + ); + const updateInsertPointPosition = useCallback( + ( event ) => { + if ( ! gradientPickerXPosition || ! insertPointMoveEnabled ) { + return; + } + let insertPosition = event.clientX - gradientPickerXPosition - ( INSERT_POINT_WIDTH / 2 ); + if ( insertPosition < 0 ) { + insertPosition = 0; + } + if ( insertPosition > maximumInsertPointPosition ) { + insertPosition = maximumInsertPointPosition; + } + if ( some( + markerPoints, + ( { absolutePosition } ) => { + return Math.abs( insertPosition - absolutePosition ) < MINIMUM_DISTANCE_BETWEEN_MARKERS; + } + ) ) { + setInsertPointPosition( null ); + return; + } + + setInsertPointPosition( insertPosition ); + } + ); + + const onMouseLeave = useCallback( + () => { + if ( ! insertPointMoveEnabled ) { + return; + } + setInsertPointPosition( null ); + } + ); + + useEffect( () => { + if ( ! gradientPickerRef || ! gradientPickerRef.current ) { + return; + } + const rect = gradientPickerRef.current.getBoundingClientRect(); + setGradientPickerXPosition( rect.left ); + setMaximumInsertPointPosition( rect.width - INSERT_POINT_WIDTH ); + }, [] ); + + const controlPointMove = useCallback( + ( event ) => { + const relativePosition = getGradientRelativePosition( + event.clientX - gradientPickerXPosition - ( GRADIENT_MARKERS_WIDTH / 2 ), + maximumInsertPointPosition, + MINIMUM_ABSOLUTE_LEFT_POSITION + ); + const { parsedGradient: referenceParsedGradient, position, significantMoveHappened } = controlPointMoveState.current; + if ( ! significantMoveHappened ) { + const initialPosition = referenceParsedGradient.colorStops[ position ].length.value; + if ( Math.abs( initialPosition - relativePosition ) >= MINIMUM_SIGNIFICANT_MOVE ) { + controlPointMoveState.current.significantMoveHappened = true; + } + } + onChange( + serializeGradient( + { + ...referenceParsedGradient, + colorStops: referenceParsedGradient.colorStops.map( + ( colorStop, colorStopIndex ) => { + if ( colorStopIndex !== position ) { + return colorStop; + } + return { + ...colorStop, + length: { + ...colorStop.length, + value: relativePosition.toString(), + }, + }; + } + ), + } + ) + ); + }, + [ controlPointMoveState, onChange, maximumInsertPointPosition ] + ); + + const unbindEventListeners = useCallback( + () => { + if ( window && window.removeEventListener ) { + window.removeEventListener( 'mousemove', controlPointMove ); + window.removeEventListener( 'mouseup', unbindEventListeners ); + } + }, + [ controlPointMove ] + ); + + const controlPointMouseDown = useMemo( + () => { + return parsedGradient.colorStops.map( + ( colorStop, index ) => ( event ) => { + if ( window && window.addEventListener ) { + controlPointMoveState.current = { + parsedGradient, + position: index, + significantMoveHappened: false, + }; + controlPointMove( event ); + window.addEventListener( 'mousemove', controlPointMove ); + window.addEventListener( 'mouseup', unbindEventListeners ); + } + } + ); + }, + [ parsedGradient, controlPointMove ] + ); + + return ( +
+
+ { insertPointPosition !== null && ( + { + setInsertPointMoveEnabled( true ); + setIsEditingColorAtPosition( null ); + setInsertPointPosition( null ); + } } + renderToggle={ ( { isOpen, onToggle } ) => ( + { + setInsertPointMoveEnabled( false ); + onToggle(); + } } + className="components-custom-gradient-picker__insert-point" + icon="insert" + style={ { + left: insertPointPosition !== null ? insertPointPosition : undefined, + } } + /> + ) } + renderContent={ () => ( + { + const relativePosition = getGradientRelativePosition( insertPointPosition, maximumInsertPointPosition, MINIMUM_ABSOLUTE_LEFT_POSITION ); + const colorStop = tinyColorRgbToGradientColorStop( rgb ); + colorStop.length = { + type: '%', + value: relativePosition, + }; + let newGradient; + if ( isEditingColorAtPosition === null ) { + newGradient = { + ...parsedGradient, + colorStops: [ + ...parsedGradient.colorStops, + colorStop, + ], + }; + setIsEditingColorAtPosition( relativePosition.toString() ); + } else { + newGradient = { + ...parsedGradient, + colorStops: parsedGradient.colorStops.map( + ( currentColorStop ) => { + if ( currentColorStop.length.value !== isEditingColorAtPosition ) { + return currentColorStop; + } + return colorStop; + } + ), + }; + } + onChange( serializeGradient( newGradient ) ); + } } + /> + ) } + popoverProps={ { + className: 'components-custom-gradient-picker__color-picker-popover', + position: 'top', + } } + /> + + ) } + { markerPoints.length > 0 && ( + markerPoints.map( + ( point, index ) => ( + isEditingColorAtPosition !== point.position && ( + { + setInsertPointMoveEnabled( true ); + setIsEditingColorAtPosition( null ); + setInsertPointPosition( null ); + } } + renderToggle={ ( { isOpen, onToggle } ) => ( + + + ) } + popoverProps={ { + className: 'components-custom-gradient-picker__color-picker-popover', + position: 'top', + } } + /> + + ) + ) + ) + ) } +
+
+ ); +} diff --git a/packages/components/src/custom-gradient-picker/style.scss b/packages/components/src/custom-gradient-picker/style.scss new file mode 100644 index 0000000000000..cdd74aef5799d --- /dev/null +++ b/packages/components/src/custom-gradient-picker/style.scss @@ -0,0 +1,45 @@ +.components-custom-gradient-picker { + height: 23px; + width: 100%; + border-radius: 20px; + margin-top: 10px; + + .components-custom-gradient-picker__markers-container { + position: relative; + } + + .components-custom-gradient-picker__insert-point { + border-radius: 50%; + background: $white; + padding: 2px; + width: 23px; + height: 23px; + position: relative; + } + + .components-custom-gradient-picker__marker-point { + border: 2px solid $white; + border-radius: 50%; + height: 18px; + position: absolute; + width: 18px; + top: 2px; + + &.is-active { + background: #fafafa; + color: #23282d; + border-color: #999; + box-shadow: + inset 0 -1px 0 #999, + 0 0 0 1px $white, + 0 0 0 3px $blue-medium-focus; + } + } +} + +.components-custom-gradient-picker__color-picker-popover .components-custom-gradient-picker__remove-control-point { + margin-left: auto; + margin-right: auto; + display: block; + margin-bottom: 8px; +} diff --git a/packages/components/src/dropdown/index.js b/packages/components/src/dropdown/index.js index 6c5cffa6e5e4b..81d1f4dbac078 100644 --- a/packages/components/src/dropdown/index.js +++ b/packages/components/src/dropdown/index.js @@ -62,6 +62,9 @@ class Dropdown extends Component { } close() { + if ( this.props.onClose ) { + this.props.onClose(); + } this.setState( { isOpen: false } ); } diff --git a/packages/components/src/index.js b/packages/components/src/index.js index d13be6e9ac2e3..d0d19cf09932b 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -10,6 +10,7 @@ export { default as ClipboardButton } from './clipboard-button'; export { default as ColorIndicator } from './color-indicator'; export { default as ColorPalette } from './color-palette'; export { default as ColorPicker } from './color-picker'; +export { default as CustomGradientPicker } from './custom-gradient-picker'; export { default as Dashicon } from './dashicon'; export { DateTimePicker, DatePicker, TimePicker } from './date-time'; export { default as Disabled } from './disabled'; diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss index 4830ac6a84b71..cb77a2b49c0e8 100644 --- a/packages/components/src/style.scss +++ b/packages/components/src/style.scss @@ -8,6 +8,7 @@ @import "./color-indicator/style.scss"; @import "./color-palette/style.scss"; @import "./color-picker/style.scss"; +@import "./custom-gradient-picker/style.scss"; @import "./dashicon/style.scss"; @import "./date-time/style.scss"; @import "./disabled/style.scss";