UNPKG

@wordpress/components

Version:
295 lines (261 loc) 9.48 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.FocalPointPicker = FocalPointPicker; exports.default = void 0; var _element = require("@wordpress/element"); var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _classnames = _interopRequireDefault(require("classnames")); var _i18n = require("@wordpress/i18n"); var _compose = require("@wordpress/compose"); var _baseControl = _interopRequireDefault(require("../base-control")); var _controls = _interopRequireDefault(require("./controls")); var _focalPoint = _interopRequireDefault(require("./focal-point")); var _grid = _interopRequireDefault(require("./grid")); var _media = _interopRequireDefault(require("./media")); var _focalPointPickerStyle = require("./styles/focal-point-picker-style"); var _utils = require("./utils"); var _hooks = require("../utils/hooks"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ const GRID_OVERLAY_TIMEOUT = 600; /** * 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%;` * * ```jsx * import { FocalPointPicker } from '@wordpress/components'; * import { useState } from '@wordpress/element'; * * const Example = () => { * const [ focalPoint, setFocalPoint ] = useState( { * x: 0.5, * y: 0.5, * } ); * * const url = '/path/to/image'; * * // Example function to render the CSS styles based on Focal Point Picker value * const style = { * backgroundImage: `url(${ url })`, * backgroundPosition: `${ focalPoint.x * 100 }% ${ focalPoint.y * 100 }%`, * }; * * return ( * <> * <FocalPointPicker * url={ url } * value={ focalPoint } * onDragStart={ setFocalPoint } * onDrag={ setFocalPoint } * onChange={ setFocalPoint } * /> * <div style={ style } /> * </> * ); * }; * ``` */ function FocalPointPicker(_ref) { let { __nextHasNoMarginBottom, autoPlay = true, className, help, label, onChange, onDrag, onDragEnd, onDragStart, resolvePoint, url, value: valueProp = { x: 0.5, y: 0.5 }, ...restProps } = _ref; const [point, setPoint] = (0, _element.useState)(valueProp); const [showGridOverlay, setShowGridOverlay] = (0, _element.useState)(false); const { startDrag, endDrag, isDragging } = (0, _compose.__experimentalUseDragging)({ onDragStart: event => { var _dragAreaRef$current; (_dragAreaRef$current = dragAreaRef.current) === null || _dragAreaRef$current === void 0 ? void 0 : _dragAreaRef$current.focus(); const value = getValueWithinDragArea(event); // `value` can technically be undefined if getValueWithinDragArea() is // called before dragAreaRef is set, but this shouldn't happen in reality. if (!value) return; onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(value, event); setPoint(value); }, onDragMove: event => { // Prevents text-selection when dragging. event.preventDefault(); const value = getValueWithinDragArea(event); if (!value) return; onDrag === null || onDrag === void 0 ? void 0 : onDrag(value, event); setPoint(value); }, onDragEnd: () => { onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(); onChange === null || onChange === void 0 ? void 0 : onChange(point); } }); // Uses the internal point while dragging or else the value from props. const { x, y } = isDragging ? point : valueProp; const dragAreaRef = (0, _element.useRef)(null); const [bounds, setBounds] = (0, _element.useState)(_utils.INITIAL_BOUNDS); const refUpdateBounds = (0, _element.useRef)(() => { if (!dragAreaRef.current) return; const { clientWidth: width, clientHeight: height } = dragAreaRef.current; // Falls back to initial bounds if the ref has no size. Since styles // give the drag area dimensions even when the media has not loaded // this should only happen in unit tests (jsdom). setBounds(width > 0 && height > 0 ? { width, height } : { ..._utils.INITIAL_BOUNDS }); }); (0, _element.useEffect)(() => { const updateBounds = refUpdateBounds.current; if (!dragAreaRef.current) return; const { defaultView } = dragAreaRef.current.ownerDocument; defaultView === null || defaultView === void 0 ? void 0 : defaultView.addEventListener('resize', updateBounds); return () => defaultView === null || defaultView === void 0 ? void 0 : defaultView.removeEventListener('resize', updateBounds); }, []); // Updates the bounds to cover cases of unspecified media or load failures. (0, _compose.useIsomorphicLayoutEffect)(() => void refUpdateBounds.current(), []); // TODO: Consider refactoring getValueWithinDragArea() into a pure function. // https://github.com/WordPress/gutenberg/pull/43872#discussion_r963455173 const getValueWithinDragArea = _ref2 => { let { clientX, clientY, shiftKey } = _ref2; if (!dragAreaRef.current) return; const { top, left } = dragAreaRef.current.getBoundingClientRect(); let nextX = (clientX - left) / bounds.width; let nextY = (clientY - top) / bounds.height; // Enables holding shift to jump values by 10%. if (shiftKey) { nextX = Math.round(nextX / 0.1) * 0.1; nextY = Math.round(nextY / 0.1) * 0.1; } return getFinalValue({ x: nextX, y: nextY }); }; const getFinalValue = value => { var _resolvePoint; const resolvedValue = (_resolvePoint = resolvePoint === null || resolvePoint === void 0 ? void 0 : resolvePoint(value)) !== null && _resolvePoint !== void 0 ? _resolvePoint : value; resolvedValue.x = Math.max(0, Math.min(resolvedValue.x, 1)); resolvedValue.y = Math.max(0, Math.min(resolvedValue.y, 1)); const roundToTwoDecimalPlaces = n => Math.round(n * 1e2) / 1e2; return { x: roundToTwoDecimalPlaces(resolvedValue.x), y: roundToTwoDecimalPlaces(resolvedValue.y) }; }; const arrowKeyStep = event => { const { code, shiftKey } = event; if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(code)) return; event.preventDefault(); const value = { x, y }; const step = shiftKey ? 0.1 : 0.01; const delta = code === 'ArrowUp' || code === 'ArrowLeft' ? -1 * step : step; const axis = code === 'ArrowUp' || code === 'ArrowDown' ? 'y' : 'x'; value[axis] = value[axis] + delta; onChange === null || onChange === void 0 ? void 0 : onChange(getFinalValue(value)); }; const focalPointPosition = { left: x * bounds.width, top: y * bounds.height }; const classes = (0, _classnames.default)('components-focal-point-picker-control', className); const instanceId = (0, _compose.useInstanceId)(FocalPointPicker); const id = `inspector-focal-point-picker-control-${instanceId}`; (0, _hooks.useUpdateEffect)(() => { setShowGridOverlay(true); const timeout = window.setTimeout(() => { setShowGridOverlay(false); }, GRID_OVERLAY_TIMEOUT); return () => window.clearTimeout(timeout); }, [x, y]); return (0, _element.createElement)(_baseControl.default, (0, _extends2.default)({}, restProps, { __nextHasNoMarginBottom: __nextHasNoMarginBottom, label: label, id: id, help: help, className: classes }), (0, _element.createElement)(_focalPointPickerStyle.MediaWrapper, { className: "components-focal-point-picker-wrapper" }, (0, _element.createElement)(_focalPointPickerStyle.MediaContainer, { className: "components-focal-point-picker", onKeyDown: arrowKeyStep, onMouseDown: startDrag, onBlur: () => { if (isDragging) endDrag(); }, ref: dragAreaRef, role: "button", tabIndex: -1 }, (0, _element.createElement)(_grid.default, { bounds: bounds, showOverlay: showGridOverlay }), (0, _element.createElement)(_media.default, { alt: (0, _i18n.__)('Media preview'), autoPlay: autoPlay, onLoad: refUpdateBounds.current, src: url }), (0, _element.createElement)(_focalPoint.default, (0, _extends2.default)({}, focalPointPosition, { isDragging: isDragging })))), (0, _element.createElement)(_controls.default, { __nextHasNoMarginBottom: __nextHasNoMarginBottom, hasHelpText: !!help, point: { x, y }, onChange: value => { onChange === null || onChange === void 0 ? void 0 : onChange(getFinalValue(value)); } })); } var _default = FocalPointPicker; exports.default = _default; //# sourceMappingURL=index.js.map