UNPKG

@bigfishtv/cockpit

Version:

892 lines (794 loc) 29.2 kB
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _dec, _class, _class2, _temp; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } import React, { Component } from 'react'; import { connect } from 'react-redux'; import { Fieldset, createValue } from '@bigfishtv/react-forms'; import classnames from 'classnames'; import deepEqual from 'deep-equal'; import get from 'lodash/get'; import $ from 'jquery'; import { getImageUrl } from '../../utils/fileUtils'; import { titleCase } from '../../utils/stringUtils'; import { curvesHashTable } from '../../utils/colorUtils'; import { post } from '../../api/xhrUtils'; import { notifyWarning } from '../../actions/notifications'; import Spinner from '../Spinner'; import PropTypes from 'prop-types'; import Icon from '../Icon'; import MainContent from '../container/MainContent'; import Bulkhead from '../page/Bulkhead'; import Button from '../button/Button'; import Field from '../form/Field'; import Meter from '../input/MeterInput'; import Cropper from '../editor/Cropper'; import ProgressBarPredictive from '../ProgressBarPredictive'; import DropdownAction from '../button/dropdown/DropdownAction'; import DropdownItem from '../button/dropdown/DropdownItem'; import * as tankCaman from '../../utils/tankCaman'; var presetNames = []; var adjustmentOrder = ['temperature', 'exposure', 'contrast', 'curves', 'fade', 'vibrance', 'saturation', 'sharpen', 'grain']; var defaultFormValue = { ratio: null, rotation: 0, crop: { top: 0, left: 0, width: 1, height: 1 }, temperature: 6600, exposure: 0, contrast: 0, darks: 0, lights: 0, fade: 0, vibrance: 0, saturation: 0, sharpen: 0, blur: 0, grain: 0 }; var filterTimingEstimate = { exposure: 1, contrast: 1, curves: 1, vibrance: 0.5, saturation: 0.5, sharpen: 1.5, blur: 2, noise: 3.8 }; var HeaderToolbar = function HeaderToolbar(props) { return React.createElement( 'div', null, props.saving && props.estimatedTime && React.createElement(ProgressBarPredictive, { estimatedTime: props.estimatedTime }), props.standalone && React.createElement(Button, { text: 'Close', size: 'large', onClick: props.onClose }), !props.saving && props.savedImageUrl && React.createElement(Button, { text: 'View full-resolution', size: 'large', onClick: function onClick() { return window.open(props.savedImageUrl); } }), React.createElement( DropdownAction, { text: props.saving ? 'Saving...' : 'Save', style: 'secondary', size: 'large', pullRight: true, onClick: props.onSave, disabled: props.saving }, React.createElement(DropdownItem, { text: 'Save as a copy', onClick: props.onSaveCopy }) ) ); }; function estimatedSaveDuration(asset) { var steps = [].concat(asset.transform_instructions.editSteps, asset.transform_instructions.filterSteps); var megapixels = asset.width * asset.transform_instructions.settings.crop.width * (asset.height * asset.transform_instructions.settings.crop.height) / 1000000; var equivalentSteps = steps.reduce(function (counter, step) { return counter + (filterTimingEstimate[step.key] || 0.5); }, 0); // 0.3s for base image edit, 0.5s for round-trip // stats can be found here https://docs.google.com/spreadsheets/d/1qosKrfK9pnxC3rkiJ6VVEh-QZ3PhfclfezBGNrTraAQ/edit?usp=sharing var estimatedSeconds = 0.55 * megapixels / 7 * equivalentSteps + 0.3 + 0.5; return estimatedSeconds * 1000; } function getSavedImageUrl(asset) { return asset.transform_instructions ? '/uploads/_transformed/' + asset.filename + '?' + new Date().valueOf() : '/uploads/' + asset.filename; } /** * Template for image editor, is also used in a modal */ var ImageEdit = (_dec = connect(function (_ref) { var imageFilterPresets = _ref.imageFilterPresets; return { imageFilterPresets: imageFilterPresets }; }), _dec(_class = (_temp = _class2 = function (_Component) { _inherits(ImageEdit, _Component); function ImageEdit(props) { _classCallCheck(this, ImageEdit); var _this2 = _possibleConstructorReturn(this, _Component.call(this, props)); _this2.handleSave = function () { var saveAsCopy = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (_this2.state.cropping) { _this2.handleCrop(); } var data = _extends({}, _this2.props.asset, { transform_instructions: _this2.generateInstructions() }); _this2.setState({ saving: true, estimatedSaveDuration: estimatedSaveDuration(data) }); var action = saveAsCopy === true ? 'saveAs' : 'edit'; post({ url: '/tank/assets/' + action + '/' + data.id + '.json', data: data, subject: 'image', callback: function callback(response) { // need this in case it's a belongsToMany with join data response._joinData = _this2.props.asset._joinData; _this2.props.onAssetChange(response, _this2.props.asset); _this2.setState({ saving: false, savedImageUrl: getSavedImageUrl(response) }); }, callbackError: function callbackError(response) { if (response.status === 404) { _this2.props.dispatch(notifyWarning('"Save as a copy" feature requires Tank 3.0.4 or greater')); } _this2.setState({ saving: false }); console.warn('Error saving image', response); } }); }; _this2.handleClose = function () { _this2.props.onClose(); _this2.props.closeModal(); }; _this2.handleFormValueChange = function (nextFormValue) { _this2.setState({ formValue: nextFormValue }); }; _this2.handleReset = function () { _this2.state.formValue.select('filter').update(null); _this2.setState({ currentFilter: null }, function () { _this2.applyImageFilter(); }); }; _this2.toggleCrop = function () { _this2.setState({ cropping: !_this2.state.cropping }); _this2.toggleTray('crop'); }; _this2.updateCrop = function (crop) { _this2.crop = crop; }; _this2.handleCrop = function () { _this2.state.formValue.select('crop').update(_this2.crop); _this2.setState({ cropping: false, cropped: true, currentTray: null }); }; _this2.cancelCrop = function () { _this2.crop = _this2.state.formValue.value.crop; _this2.setState({ cropping: false, cropped: !!_this2.crop, currentTray: null }); }; _this2.handleParamsReset = function () { var newFormValue = _extends({}, defaultFormValue); newFormValue.crop = _this2.state.formValue.value.crop; _this2.state.formValue.update(newFormValue); _this2.setState({ steps: [] }, function () { _this2.applyImageFilter(); }); }; tankCaman.init(window.Caman); presetNames = props.imageFilterPresets.map(function (preset) { return preset.title; }); var formValue = _extends({}, defaultFormValue, get(props.asset, 'transform_instructions.settings', {})); var cropped = !!get(props.asset, 'transform_instructions.settings.crop'); var steps = []; get(props.asset, 'transform_instructions.editSteps', []).map(function (step) { if (step.value) steps[step.key] = step.value; }); _this2.state = { loading: false, cropping: false, saving: false, savedImageUrl: getSavedImageUrl(props.asset), cropped: cropped, currentTray: null, currentFilter: formValue.filter, lights: get(formValue, 'lights', 0), darks: get(formValue, 'darks', 0), steps: steps, url: props.host + getImageUrl(props.asset, 'cockpit-edit', null, true), previewUrl: props.host + getImageUrl(props.asset, 'cockpit-small', null, true), width: 640, height: 480, formValue: createValue({ schema: undefined, value: formValue, onChange: _this2.handleFormValueChange }) }; _this2.crop = formValue.crop; _this2.origWidth = _this2.state.width; _this2.origHeight = _this2.state.height; _this2.stageHeight = 1000; var img = new Image(); img.onload = function () { _this2.setState({ width: img.width, height: img.height }); _this2.origWidth = img.width; _this2.origHeight = img.height; // Go ahead and apply any previous edits, idk why it doesn't work immediately... setTimeout(function () { return _this2.applyImageFilter(); }, 500); }; img.src = _this2.state.url; return _this2; } ImageEdit.prototype.componentDidMount = function componentDidMount() { // Set current filter selection based on saved selection if (this.state.currentFilter) this.handleFilterClick(this.state.currentFilter); // @refactor to not use jquery this.stageHeight = $(this.refs.edit_inner).height(); }; // generates filter preview thumbnail, staggered for performance ImageEdit.prototype.handleFilterImageLoad = function handleFilterImageLoad(elementId, filterName) { var _this3 = this; var delay = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var doIt = function doIt() { return _this3.applyPreviewFilter(elementId, filterName); }; if (delay) setTimeout(doIt, delay);else doIt(); }; ImageEdit.prototype.applyPreviewFilter = function applyPreviewFilter(elementId, filterName) { var _this = this; if (typeof Caman != 'undefined') Caman(this.props.currentDocument.getElementById(elementId), function () { this.resize({ width: 200, height: 150 }); this.render(); var steps = _this.generateFilterSteps(filterName); _this.applyImageSteps.apply(this, [steps, false]); this.render(); }); }; ImageEdit.prototype.applyImageFilter = function applyImageFilter() { // make ref to this because caman binds its own var _this = this; var editSteps = this.generateEditSteps(); var filterSteps = this.generateFilterSteps(); this.setState({ loading: true }); if (typeof Caman != 'undefined') Caman(this.props.currentDocument.getElementById('mainImage'), function () { var _this4 = this; this.revert(); // if any edits have been made do them first if (editSteps.length) { // apply edit steps and render _this.applyImageSteps.apply(this, [editSteps]); this.render(function () { // if filter applied then execute those steps and render if (filterSteps.length) _this.applyImageSteps.apply(_this4, [filterSteps]); _this4.render(function () { _this.setState({ loading: false }); }); }); } else { // if filter applied then execute those steps and render if (filterSteps.length) _this.applyImageSteps.apply(this, [filterSteps]); this.render(function () { _this.setState({ loading: false }); }); } }); }; // this function has scope of caman image applied to it ImageEdit.prototype.applyImageSteps = function applyImageSteps() { var _steps = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; var verbose = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; // copy array var steps = [].concat(_steps); for (var _iterator = steps, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { var _ref2; if (_isArray) { if (_i >= _iterator.length) break; _ref2 = _iterator[_i++]; } else { _i = _iterator.next(); if (_i.done) break; _ref2 = _i.value; } var step = _ref2; var _step = _extends({}, step); var key = _step.key, value = _step.value; // if value is an array the step's function requires more than 1 argument so run apply on it if (value instanceof Array) { if (verbose) console.log('Applying (with args)', key, value); if (key == 'curves' && (value[value.length - 1] === 'curvesHashTable' || value[value.length - 1] === null)) value[value.length - 1] = curvesHashTable; this[key].apply(this, value); // step's function takes no argument so just call it } else if (typeof value == 'undefined') { if (verbose) console.log('Applying (without args)', key, value); this[key](); // in all other cases apply the step with 1 argument, except if value is a numeric 0 } else if (value !== 0) { if (verbose) console.log('Applying (as only arg)', key, value); if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) == 'object') value = _extends({}, value); // if it's an object then copy it this[key](value); } } }; ImageEdit.prototype.generateEditSteps = function generateEditSteps() { var fullSteps = []; var steps = this.state.steps; var keys = Object.keys(steps); adjustmentOrder.map(function (key) { if (keys.indexOf(key) >= 0 && steps[key] !== 0) { var value = steps[key]; if (value instanceof Array && value.length == 1) value = value[0]; if (value !== 0) { var _value = steps[key]; fullSteps.push({ key: key, value: _value }); } } }); return fullSteps; }; ImageEdit.prototype.generateFilterSteps = function generateFilterSteps() { var filterName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; if (filterName === null) filterName = this.state.currentFilter; var fullSteps = []; if (filterName !== null && presetNames.indexOf(filterName) >= 0) { this.props.imageFilterPresets.map(function (preset) { if (preset.title === filterName) fullSteps = [].concat(preset.steps); }); } return fullSteps; }; ImageEdit.prototype.generateInstructions = function generateInstructions() { var formValue = this.state.formValue; var editSteps = this.generateEditSteps(); var filterSteps = this.generateFilterSteps(); var instructions = { settings: _extends({}, formValue.value), editSteps: editSteps, filterSteps: filterSteps }; return instructions; }; ImageEdit.prototype.handleFilterClick = function handleFilterClick(filterName) { var _this5 = this; if (this.state.loading) return; this.state.formValue.select('filter').update(filterName); this.setState({ currentFilter: filterName, loading: true }, function () { _this5.applyImageFilter(); }); }; ImageEdit.prototype.handleRotate = function handleRotate(deg90s) { // keep track of rotation in state var rotation = (this.state.rotation + deg90s * 90) % 360; this.state.formValue.select('rotation').update(rotation); }; ImageEdit.prototype.toggleTray = function toggleTray(trayName) { var _state = this.state, currentTray = _state.currentTray, cropping = _state.cropping; if (currentTray == trayName) currentTray = null;else currentTray = trayName; this.setState({ currentTray: currentTray }); if (cropping && trayName != 'crop') this.setState({ cropping: false }); }; ImageEdit.prototype.handleParamChange = function handleParamChange(param, value) { var _this6 = this; var steps = this.state.steps; var newSteps = steps; newSteps[param] = (typeof value === 'undefined' ? 'undefined' : _typeof(value)) !== 'object' ? [value] : value; this.setState({ steps: newSteps }, function () { _this6.applyImageFilter(); }); }; ImageEdit.prototype.handleCurveChange = function handleCurveChange(tone, value) { var _setState, _this7 = this; this.setState((_setState = {}, _setState[tone] = value, _setState), function () { var _state2 = _this7.state, lights = _state2.lights, darks = _state2.darks; _this7.handleParamChange('curves', ['rgb', [0, darks], [100, 100], [180, 180], [255, 255 - lights], curvesHashTable]); }); }; ImageEdit.prototype.render = function render() { var _this8 = this, _React$createElement; var _state3 = this.state, loading = _state3.loading, cropping = _state3.cropping, saving = _state3.saving, savedImageUrl = _state3.savedImageUrl, currentTray = _state3.currentTray, currentFilter = _state3.currentFilter, formValue = _state3.formValue, url = _state3.url, previewUrl = _state3.previewUrl, height = _state3.height, cropped = _state3.cropped; var resetable = deepEqual(formValue.value, defaultFormValue); var ratio = formValue.value.ratio ? ratioToDecimal(formValue.value.ratio) : null; var _formValue$value = formValue.value, rotation = _formValue$value.rotation, crop = _formValue$value.crop; var originalImageStyles = { position: 'absolute', width: this.origWidth, height: this.origHeight, backgroundSize: '100%', backgroundImage: 'url("' + url + '")', transform: 'translate(-50%, -50%) rotate(' + rotation + 'deg)' // this is not ideal as it messes with cropping but this whole thing needs to be redone soon };if (this.stageHeight < this.origHeight) originalImageStyles.zoom = this.stageHeight / (this.origHeight * 1.1); if (cropped && !cropping) { var cropTop = crop.top * this.origHeight; var cropLeft = crop.left * this.origWidth; var cropWidth = this.origWidth * crop.width; var cropHeight = this.origHeight * crop.height; var cropRight = cropLeft + cropWidth; var cropBottom = cropTop + cropHeight; var negativeWidth = cropLeft - (this.origWidth - cropRight); var negativeHeight = cropTop - (this.origHeight - cropBottom); originalImageStyles.clip = 'rect(' + cropTop + 'px ' + cropRight + 'px ' + cropBottom + 'px ' + cropLeft + 'px)'; originalImageStyles.transform = 'translate(calc(-50% - ' + negativeWidth / 2 + 'px), calc(-50% - ' + negativeHeight / 2 + 'px)) rotate(' + rotation + 'deg)'; } return React.createElement( MainContent, { size: 'full' }, React.createElement(Bulkhead, { title: 'Edit Image', Toolbar: HeaderToolbar, onSave: this.handleSave, onSaveCopy: this.handleSave.bind(this, true), onClose: this.handleClose, saving: saving, savedImageUrl: savedImageUrl, estimatedTime: this.state.estimatedSaveDuration, standalone: this.props.standalone }), React.createElement( 'div', { style: { display: 'flex', flex: 'auto' } }, React.createElement( 'div', { className: 'edit', ref: 'edit_inner' }, React.createElement( 'div', { className: 'edit-toolbar' }, React.createElement( Button, { style: 'icon', onClick: this.toggleCrop }, React.createElement(Icon, { name: 'crop' }) ), React.createElement( Button, { style: 'icon', onClick: function onClick() { return _this8.toggleTray('advanced'); } }, React.createElement(Icon, { name: 'advanced' }) ) ), currentTray == 'advanced' ? React.createElement( 'div', { className: 'edit-toolbar edit-toolbar-advanced' }, React.createElement( Fieldset, { formValue: formValue }, React.createElement( Field, { select: 'temperature', autoLabel: false }, React.createElement(Meter, { min: 4600, max: 8600, icon: 'temperature', text: 'Temperature', onAfterChange: this.handleParamChange.bind(this, 'temperature') }) ), React.createElement( Field, { select: 'exposure', autoLabel: false }, React.createElement(Meter, { min: -50, max: 50, icon: 'sunny', text: 'Exposure', onAfterChange: this.handleParamChange.bind(this, 'exposure') }) ), React.createElement( Field, { select: 'contrast', autoLabel: false }, React.createElement(Meter, { min: -20, max: 20, icon: 'tonality', text: 'Contrast', onAfterChange: this.handleParamChange.bind(this, 'contrast') }) ), React.createElement( Field, { select: 'darks', autoLabel: false }, React.createElement(Meter, { min: -80, max: 80, icon: 'bring-forward', text: 'Shadow recovery', onAfterChange: this.handleCurveChange.bind(this, 'darks') }) ), React.createElement( Field, { select: 'lights', autoLabel: false }, React.createElement(Meter, { min: -50, max: 50, icon: 'send-backwards', text: 'Highlight recovery', onAfterChange: this.handleCurveChange.bind(this, 'lights') }) ), React.createElement( Field, { select: 'fade', autoLabel: false }, React.createElement(Meter, { min: 0, max: 100, icon: 'moon-crescent', text: 'Fade', onAfterChange: this.handleParamChange.bind(this, 'fade') }) ), React.createElement( Field, { select: 'vibrance', autoLabel: false }, React.createElement(Meter, { min: -100, max: 100, icon: 'beach', text: 'Vibrance', onAfterChange: this.handleParamChange.bind(this, 'vibrance') }) ), React.createElement( Field, { select: 'saturation', autoLabel: false }, React.createElement(Meter, { min: -100, max: 100, icon: 'barrel-drop', text: 'Saturation', onAfterChange: this.handleParamChange.bind(this, 'saturation') }) ), React.createElement( Field, { select: 'sharpen', autoLabel: false }, React.createElement(Meter, { min: 0, max: 100, icon: 'magic-wand', text: 'Sharpen', onAfterChange: this.handleParamChange.bind(this, 'sharpen') }) ), React.createElement( Field, { select: 'grain', autoLabel: false }, React.createElement(Meter, { min: 0, max: 10, icon: 'grain', text: 'Grain', onAfterChange: this.handleParamChange.bind(this, 'noise') }) ), React.createElement(Button, { text: React.createElement( 'span', null, React.createElement(Icon, { name: 'reset', size: 14 }), ' Reset' ), size: 'medium', style: 'margin-top-small', onClick: this.handleParamsReset, disabled: resetable }) ) ) : currentTray == 'crop' ? React.createElement( 'div', { className: 'edit-toolbar edit-toolbar-crop' }, React.createElement( Fieldset, { formValue: formValue }, React.createElement( Field, { select: 'ratio', autoLabel: false }, React.createElement(RatioButton, { text: '3:2' }) ), React.createElement( Field, { select: 'ratio', autoLabel: false }, React.createElement(RatioButton, { text: '4:3' }) ), React.createElement( Field, { select: 'ratio', autoLabel: false }, React.createElement(RatioButton, { text: '5:4' }) ), React.createElement( Field, { select: 'ratio', autoLabel: false }, React.createElement(RatioButton, { text: '1:1' }) ), React.createElement( Field, { select: 'ratio', autoLabel: false }, React.createElement(RatioButton, { text: '4:5' }) ), React.createElement( Field, { select: 'ratio', autoLabel: false }, React.createElement(RatioButton, { text: '3:4' }) ), React.createElement( Field, { select: 'ratio', autoLabel: false }, React.createElement(RatioButton, { text: '2:3' }) ), React.createElement( Field, { select: 'ratio', autoLabel: false }, React.createElement(RatioButton, { text: '2:1' }) ), React.createElement( Field, { select: 'ratio', autoLabel: false }, React.createElement(RatioButton, { text: '1:2' }) ), React.createElement( Field, { select: 'ratio', autoLabel: false }, React.createElement(RatioButton, { text: '16:9' }) ), React.createElement( Field, { select: 'ratio', autoLabel: false }, React.createElement(RatioButton, { text: '9:16' }) ) ) ) : null, React.createElement( 'div', { className: '', style: { flex: 'auto', display: 'flex', flexDirection: 'column' } }, React.createElement( 'div', { className: 'edit-inner' }, React.createElement( 'div', { style: originalImageStyles }, React.createElement('img', { src: url, id: 'mainImage' }), cropping && React.createElement(Cropper, { width: this.origWidth, height: height, ApplyButton: null, fixedRatio: ratio, onChange: this.updateCrop, defaultCrop: this.crop }) ), loading && React.createElement( 'div', { className: 'loader-center' }, React.createElement(Spinner, { size: 32 }) ) ), cropping && React.createElement( 'div', { className: '', style: { flex: 'none', display: 'flex', background: '#ECECEF' } }, React.createElement( 'div', { style: { margin: '1.6rem auto' } }, React.createElement(Button, { text: 'Cancel', onClick: this.cancelCrop }), React.createElement(Button, (_React$createElement = { text: 'Apply Crop', style: 'primary', onClick: this.handleCrop }, _React$createElement['style'] = 'primary', _React$createElement)) ) ) ) ) ), React.createElement( 'div', { className: 'image-filters' }, React.createElement( 'div', { className: classnames('image-filter', { active: currentFilter === null }) }, React.createElement( 'div', { className: 'media media-4-3' }, React.createElement('img', { src: previewUrl, onClick: this.handleReset }) ), React.createElement( 'h4', null, 'Original' ) ), presetNames.map(function (filterName, i) { return React.createElement( 'div', { key: i, className: classnames('image-filter', { active: currentFilter == filterName }), onClick: function onClick() { return _this8.handleFilterClick(filterName); } }, React.createElement( 'div', { className: 'media media-4-3' }, React.createElement('img', { src: previewUrl, id: 'filter' + i, onLoad: function onLoad() { return _this8.handleFilterImageLoad('filter' + i, filterName, i * 200); } }) ), React.createElement( 'h4', null, titleCase(filterName) ) ); }) ) ); }; return ImageEdit; }(Component), _class2.propTypes = { /** document that component is on -- hangover from attempting to open editor in new window */ currentDocument: PropTypes.any, /** Host string to append before image url e.g. http://dev.project, default is empty string so image urls will just be /imageurlwhatever */ host: PropTypes.string, /** Is component used outside a page template? If true displays things like a close button */ standalone: PropTypes.bool, /** Called on image save -- useful for updating an asset url if being used in a modal */ onAssetChange: PropTypes.func }, _class2.defaultProps = { currentDocument: window.document, host: '', standalone: false, onAssetChange: function onAssetChange() { return console.warn('[ImageEdit] no onAssetChange prop'); } }, _temp)) || _class); export { ImageEdit as default }; var RatioButton = function RatioButton(props) { return React.createElement(Button, { text: props.text, style: props.value == props.text ? 'primary' : null, onClick: function onClick() { return props.onChange(props.value !== props.text ? props.text : null); } }); }; /** * Converts a string ratio to a decimal fraction. Returns null if doesn't match format * * ratioToDecimal('3:2') // 1.5 * * @param {string} ratio * @returns {number|null} */ function ratioToDecimal(ratio) { var matches = ratio.match(/^(\d+):(\d+)$/); if (matches) { return parseInt(matches[1]) / parseInt(matches[2]); } return null; }