-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Focal Point Picker #10925
Focal Point Picker #10925
Changes from 8 commits
684e09b
397ea7b
2d6d651
4958adc
b121c90
e019989
021888c
ded1017
97fcd04
7131d07
5567d16
7c7f605
491ede0
8bd268d
b67aaa4
2a1d14e
48d4942
87b63db
1163fe0
1bd8ea0
b44f1b9
d61d15a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,68 @@ | ||||
# Focal Point Picker | ||||
|
||||
Focal Point Picker is a component which creates a UI for identifying the most important visual point of an image. This component addresses a specific problem: with large background images it is common to see undesirable crops, especially when viewing on smaller viewports such as mobile phones. This component allows the selection of the point with the most important visual information and returns it as a pair of numbers between 0 and 1. This value can be easily converted into the CSS `background-position` attribute, and will ensure that the focal point is never cropped out, regardless of viewport. | ||||
|
||||
Example focal point picker value: `{ x: 0.5, y: 0.1 }` | ||||
Corresponding CSS: `background-position: 50% 10%;` | ||||
|
||||
## Usage | ||||
|
||||
```jsx | ||||
import { FocalPointPicker } from '@wordpress/components'; | ||||
|
||||
const MyFocalPointPicker = withState( { | ||||
focalPoint: { | ||||
x: 0.5, | ||||
y: 0.5 | ||||
}, | ||||
} )( ( { focalPoint, setState } ) => { | ||||
const url = '/path/to/image'; | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example is going to be used in auto-generated docs so it should ideally provide a valid url that can be rendered. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there examples of valid url parameters in any other Gutenberg components? I just did a quick scan looking for one and came up blank, but maybe you can point me to a relevant one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See how This is how it renders in Calypso DevDocs: |
||||
const dimensions = { | ||||
width: 400, | ||||
height: 100 | ||||
}; | ||||
return ( | ||||
<FocalPointPicker | ||||
url={ url } | ||||
dimensions={ dimensions } | ||||
value={ focalPoint } | ||||
onChange={ ( focalPoint ) => setState( { focalPoint } ) } | ||||
/> | ||||
) | ||||
} ); | ||||
|
||||
/* Example function to render the CSS styles based on Focal Point Picker value */ | ||||
const renderImageContainerWithFocalPoint = ( url, focalPoint ) => { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How would this function be used? It’s not clear from this document. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is meant to illustrate how the percentage values from the component can be translated into a "real-world" CSS
It would probably make sense for me to use the actual example instead of the hypothetical, but I'd prefer to hold off on updating the README until the PR is ready to merge, in case there are changes still to come. Would this be a reasonable approach? |
||||
const style = { | ||||
backgroundImage: `url(${ url })` , | ||||
backgroundPosition: `${ focalPoint.x * 100 }% ${ focalPoint.y * 100 }%` | ||||
} | ||||
return <div style={ style } />; | ||||
}; | ||||
``` | ||||
|
||||
## Props | ||||
|
||||
### `url` | ||||
|
||||
- Type: `Text` | ||||
- Required: Yes | ||||
- Description: URL of the image to be displayed | ||||
|
||||
### `dimensions` | ||||
|
||||
- Type: `Object` | ||||
- Required: Yes | ||||
- Description: An object describing the height and width of the image. Requires two paramaters: `height`, `width`. | ||||
|
||||
### `value` | ||||
|
||||
- Type: `Object` | ||||
- Required: Yes | ||||
- Description: The focal point. Should be an object containing `x` and `y` params. | ||||
|
||||
### `onChange` | ||||
|
||||
- Type: `Function` | ||||
- Required: Yes | ||||
- Description: Callback which is called when the focal point changes. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
|
||
import { Component, createRef } from '@wordpress/element'; | ||
import { withInstanceId, compose } from '@wordpress/compose'; | ||
import BaseControl from '../base-control/'; | ||
import withFocusOutside from '../higher-order/with-focus-outside'; | ||
import classnames from 'classnames'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only classnames is an external dependency here. Packages with the WordPress namespace should be in their own group. Everything else is internal dependencies. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved in 97fcd04 |
||
|
||
export class FocalPointPicker extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
this.onMouseMove = this.onMouseMove.bind( this ); | ||
this.state = { | ||
isDragging: false, | ||
bounds: {}, | ||
}; | ||
this.containerRef = createRef(); | ||
} | ||
componentDidMount() { | ||
this.setState( { bounds: this.calculateBounds() } ); | ||
} | ||
componentDidUpdate( prevProps ) { | ||
if ( prevProps.dimensions !== this.props.dimensions || prevProps.url !== this.props.url ) { | ||
this.setState( { | ||
bounds: this.calculateBounds(), | ||
isDragging: false, | ||
} ); | ||
} | ||
} | ||
calculateBounds() { | ||
const { dimensions } = this.props; | ||
const pickerDimensions = this.pickerDimensions(); | ||
const bounds = { | ||
top: 0, | ||
left: 0, | ||
bottom: 0, | ||
right: 0, | ||
width: 0, | ||
height: 0, | ||
}; | ||
const widthRatio = pickerDimensions.width / dimensions.width; | ||
const heightRatio = pickerDimensions.height / dimensions.height; | ||
if ( heightRatio >= widthRatio ) { | ||
bounds.width = bounds.right = pickerDimensions.width; | ||
bounds.height = dimensions.height * widthRatio; | ||
bounds.top = ( pickerDimensions.height - bounds.height ) / 2; | ||
bounds.bottom = bounds.top + bounds.height; | ||
} else { | ||
bounds.height = bounds.bottom = pickerDimensions.height; | ||
bounds.width = dimensions.width * heightRatio; | ||
bounds.left = ( pickerDimensions.width - bounds.width ) / 2; | ||
bounds.right = bounds.left + bounds.width; | ||
} | ||
return bounds; | ||
} | ||
onMouseMove( event ) { | ||
const { isDragging, bounds } = this.state; | ||
const { onChange } = this.props; | ||
|
||
if ( isDragging ) { | ||
const pickerDimensions = this.pickerDimensions(); | ||
const cursorPosition = { | ||
left: event.pageX - pickerDimensions.left, | ||
top: event.pageY - pickerDimensions.top, | ||
}; | ||
const left = Math.max( | ||
bounds.left, Math.min( | ||
cursorPosition.left, bounds.right | ||
) | ||
); | ||
const top = Math.max( | ||
bounds.top, Math.min( | ||
cursorPosition.top, bounds.bottom | ||
) | ||
); | ||
onChange( { | ||
x: left / pickerDimensions.width, | ||
y: top / pickerDimensions.height, | ||
} ); | ||
} | ||
} | ||
pickerDimensions() { | ||
if ( this.containerRef.current ) { | ||
return { | ||
width: this.containerRef.current.clientWidth, | ||
height: this.containerRef.current.clientHeight, | ||
top: this.containerRef.current.getBoundingClientRect().top + document.body.scrollTop, | ||
left: this.containerRef.current.getBoundingClientRect().left, | ||
}; | ||
} | ||
return { | ||
width: 0, | ||
height: 0, | ||
left: 0, | ||
top: 0, | ||
}; | ||
} | ||
render() { | ||
const { instanceId, url, value, label, help, className } = this.props; | ||
const { isDragging } = this.state; | ||
const pickerDimensions = this.pickerDimensions(); | ||
const containerStyle = { backgroundImage: `url(${ url })` }; | ||
const iconContainerStyle = { | ||
left: `${ value.x * pickerDimensions.width }px`, | ||
top: `${ value.y * pickerDimensions.height }px`, | ||
}; | ||
const iconContainerClasses = classnames( | ||
'component-focal-point-picker__icon_container', | ||
jeffersonrabb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
isDragging ? 'is-dragging' : null | ||
); | ||
const id = `inspector-focal-point-picker-control-${ instanceId }`; | ||
return ( | ||
<BaseControl label={ label } id={ id } help={ help } className={ className }> | ||
<div | ||
className="component-focal-point-picker" | ||
style={ containerStyle } | ||
onMouseDown={ () => this.setState( { isDragging: true } ) } | ||
onDragStart={ () => this.setState( { isDragging: true } ) } | ||
onMouseUp={ () => this.setState( { isDragging: false } ) } | ||
onDrop={ () => this.setState( { isDragging: false } ) } | ||
onMouseMove={ this.onMouseMove } | ||
ref={ this.containerRef } | ||
role="button" | ||
tabIndex="0" | ||
> | ||
<div className={ iconContainerClasses } style={ iconContainerStyle }> | ||
<i className="component-focal-point-picker__icon"></i> | ||
</div> | ||
</div> | ||
</BaseControl> | ||
); | ||
} | ||
handleFocusOutside() { | ||
jeffersonrabb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.setState( { | ||
isDragging: false, | ||
} ); | ||
} | ||
} | ||
|
||
FocalPointPicker.defaultProps = { | ||
url: null, | ||
dimensions: { | ||
height: 0, | ||
width: 0, | ||
}, | ||
value: { | ||
x: 0.5, | ||
y: 0.5, | ||
}, | ||
onChange: () => {}, | ||
}; | ||
|
||
export default compose( [ | ||
withInstanceId, | ||
withFocusOutside, | ||
] )( FocalPointPicker ); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
$focalPointPickerSVG: "data:image/svg+xml;charset=utf8,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='translate(1 1)' stroke='%23000' strokeWidth='2' fill='none' fillRule='evenodd'%3E%3Cellipse fill='%23fff' cx='11' cy='11.129' rx='9.533' ry='9.645' /%3E%3Cpath d='M0 11.13h22M10.945 22.347V.09' strokeLinecap='square' /%3E%3C/g%3E%3C/svg%3E"; | ||
|
||
.component-focal-point-picker { | ||
background-color: transparent; | ||
background-position: center; | ||
background-repeat: no-repeat; | ||
background-size: contain; | ||
border: 1px solid $light-gray-500; | ||
cursor: pointer; | ||
display: block; | ||
height: 200px; | ||
position: relative; | ||
width: 100%; | ||
} | ||
|
||
.component-focal-point-picker__icon_container { | ||
background-color: transparent; | ||
cursor: -webkit-grab; | ||
height: 30px; | ||
opacity: 0.5; | ||
position: absolute; | ||
width: 30px; | ||
z-index: 10000; | ||
|
||
&.is-dragging { | ||
cursor: -webkit-grabbing; | ||
} | ||
} | ||
|
||
.component-focal-point-picker__icon { | ||
background-image: url($focalPointPickerSVG); | ||
background-size: 100%; | ||
display: block; | ||
height: 100%; | ||
left: -15px; | ||
position: absolute; | ||
top: -15px; | ||
width: 100%; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to store the dimensions? Can't we just use percentage in the focal point instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The focal point picker needs image dimensions for two reasons:
gutenberg/packages/components/src/focal-point-picker/index.js
Lines 39 to 64 in 97fcd04
gutenberg/packages/components/src/focal-point-picker/index.js
Lines 65 to 90 in 97fcd04
However, I don't love requiring dimensions as a parameter for the component. First of all, it's restrictive - ideally the component just takes the image path and handles the rest. And second, it also creates a deprecation problem for Cover blocks created prior to the Focal Point Picker in which the dimensions are not retained. Ideally, I'd prefer that the Focal Point Picker component determine the dimensions programmatically from the image itself. I'm going to experiment with a few approaches to this and will update the PR shortly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 5567d16 I've introduced a different approach to getting the image dimensions. The parameters are gone. Instead, the component creates an invisible
<img />
(opacity: 0) and derives the dimensions from that. This way the block using the component need not supply dimensions and the component will work with legacy blocks. Does this seem like a viable approach?Update: as of b67aaa4 I've removed the background image completely in favor of the IMG element, so it is no longer invisible as I indicated above.