@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
277 lines (275 loc) • 8.91 kB
JavaScript
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { defineMessages, useIntl } from 'react-intl';
import React, { useEffect, useMemo, useReducer } from 'react';
import { getTranslation } from '../../utils/i18n';
import { makeStyles } from 'tss-react/mui';
import IconButton from '@mui/material/IconButton';
import { ScreenRotationRounded } from '@mui/icons-material';
import Divider from '@mui/material/Divider';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
import FormControlLabel from '@mui/material/FormControlLabel';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import TextField from '@mui/material/TextField';
import { setHostSize } from '../../state/actions/preview';
import { useDispatch } from 'react-redux';
import { useSelection } from '../../hooks/useSelection';
const SIMULATOR_PANEL_RESPONSIVE_MODE = 'previewSimulatorPanel.previewWindowSize';
const SIMULATOR_PANEL_CUSTOM_MODE = 'previewSimulatorPanel.custom';
const translations = defineMessages({
simulatorPanel: {
id: 'previewSimulatorPanel.title',
defaultMessage: 'Device Simulator'
},
smartPhone: {
id: 'words.phone',
defaultMessage: 'Phone'
},
width: {
id: 'words.width',
defaultMessage: 'Width'
},
height: {
id: 'words.height',
defaultMessage: 'Height'
},
tablet: {
id: 'words.tablet',
defaultMessage: 'Tablet'
},
desktop: {
id: 'words.desktop',
defaultMessage: 'Desktop'
},
previewWindowSize: {
id: 'previewSimulatorPanel.previewWindowSize',
defaultMessage: 'Preview Window Size'
},
presets: {
id: 'words.presets',
defaultMessage: 'Presets'
},
custom: {
id: 'words.custom',
defaultMessage: 'Custom'
}
});
const useStyles = makeStyles()(() => ({
topPanel: {
display: 'flex',
padding: '15px',
alignItems: 'flex-end',
'& > div:first-child': {
marginRight: '10px'
}
},
presetFieldset: {
padding: '15px',
width: '100%'
},
margin: {
marginBottom: '10px'
}
}));
const INITIAL_STATE = {
width: '',
height: '',
preset: SIMULATOR_PANEL_RESPONSIVE_MODE
};
const reducer = (a, b) => ({ ...a, ...b });
export function PreviewSimulatorPanel(props) {
const { classes } = useStyles();
const { formatMessage } = useIntl();
const toolsPanelWidth = useSelection((state) => state.preview.toolsPanelWidth);
const maxWidth = window.innerWidth - toolsPanelWidth;
const devices = useMemo(() => {
let _devices = props.devices;
if (_devices) {
// TODO: Fix bad config crashes if not an array. Show error state?
if (!Array.isArray(_devices)) {
_devices = Array.from(_devices);
console.error(`[SimulatorPanel] Expected devices to be array but instead got "${typeof _devices}"`);
}
return _devices.map((device) => ({
...device,
value: `${device.width}_${device.height}`
}));
}
return [];
}, [props.devices]);
const [{ width, height, preset }, setState] = useReducer(reducer, INITIAL_STATE);
const setWidth = (value) => setState({ width: value });
const setHeight = (value) => setState({ height: value });
const handlePresetChange = (e) => {
const value = e.target.value;
if (value === SIMULATOR_PANEL_RESPONSIVE_MODE) {
dispatch(
setHostSize({
width: null,
height: null
})
);
} else {
const device = devices.find((dev) => dev.value === value);
device &&
dispatch(
setHostSize({
width: device.width > maxWidth ? maxWidth : device.width,
height: device.height
})
);
}
};
const dispatch = useDispatch();
const hostSize = useSelection((state) => state.preview.hostSize);
const onDimensionKeyUp = (e) => {
if (e.key === 'Enter') {
let widthToSet, heightToSet;
if (!width) {
widthToSet = null;
} else if (!isNaN(width)) {
widthToSet = parseInt(width);
} else {
widthToSet = hostSize.width;
}
if (!height) {
heightToSet = null;
} else if (!isNaN(height)) {
heightToSet = parseInt(height);
} else {
heightToSet = hostSize.height;
}
dispatch(
setHostSize({
width: widthToSet > maxWidth ? maxWidth : widthToSet,
height: heightToSet
})
);
}
};
useEffect(() => {
const nextState = {};
if (hostSize.width != null) {
nextState.width = `${hostSize.width}`;
} else {
nextState.width = '';
}
if (hostSize.height != null) {
nextState.height = `${hostSize.height}`;
} else {
nextState.height = '';
}
if (hostSize.width != null || hostSize.height != null) {
const matchingPreset = devices.find(
(device) =>
// @ts-ignore
// eslint-disable-next-line
device.width == hostSize.width && device.height == hostSize.height
);
nextState.preset = matchingPreset ? matchingPreset.value : SIMULATOR_PANEL_CUSTOM_MODE;
} else {
nextState.preset = SIMULATOR_PANEL_RESPONSIVE_MODE;
}
setState(nextState);
}, [hostSize, props.config, devices]);
const onFlipDimensions = () => {
const nextWidth = parseInt(height);
const nextHeight = parseInt(width);
dispatch(
setHostSize({
width: nextWidth > maxWidth ? maxWidth : nextWidth,
height: nextHeight
})
);
};
const PRESETS = formatMessage(translations.presets);
return React.createElement(
'section',
null,
React.createElement(
'section',
{ className: classes.topPanel },
React.createElement(TextField, {
label: formatMessage(translations.width),
name: 'width',
type: 'number',
onKeyUp: onDimensionKeyUp,
onChange: (e) => setWidth(e.target.value),
value: width
}),
React.createElement(TextField, {
label: formatMessage(translations.height),
type: 'number',
name: 'height',
onKeyUp: onDimensionKeyUp,
onChange: (e) => setHeight(e.target.value),
value: height
}),
React.createElement(
IconButton,
{ onClick: onFlipDimensions, edge: 'end', size: 'large' },
React.createElement(ScreenRotationRounded, null)
)
),
React.createElement(Divider, null),
React.createElement(
FormControl,
{ className: classes.presetFieldset },
React.createElement(FormLabel, { focused: false, className: classes.margin }, PRESETS),
React.createElement(
RadioGroup,
{ value: preset, onChange: handlePresetChange, 'aria-label': PRESETS, name: 'words.device' },
React.createElement(FormControlLabel, {
value: SIMULATOR_PANEL_CUSTOM_MODE,
control: React.createElement(Radio, null),
label: formatMessage(translations.custom),
disabled: true
}),
React.createElement(FormControlLabel, {
value: SIMULATOR_PANEL_RESPONSIVE_MODE,
control: React.createElement(Radio, null),
label: formatMessage(translations.previewWindowSize)
}),
devices.map((device) =>
React.createElement(FormControlLabel, {
key: device.value,
value: device.value,
control: React.createElement(Radio, null),
label: getTranslation(device.title, translations, formatMessage)
})
)
)
)
);
}
export default PreviewSimulatorPanel;