New export menu (#582)

* New export menu

* Fix lint errors
main
Sean 6 years ago committed by Michael Fix
parent 32e09c9e25
commit 68d90364b0

@ -186,7 +186,7 @@ class BackgroundSelect extends React.Component {
position: absolute;
width: 222px;
margin-left: 36px;
margin-top: 4px;
margin-top: 12px;
border: 1px solid ${COLORS.SECONDARY};
border-radius: 3px;
background: #1a1a1a;

@ -8,7 +8,7 @@ export default props => (
...props.style,
background: COLORS.BLACK,
color: props.color,
border: `1px solid ${props.color}`
border: `${props.selected ? 2 : 1}px solid ${props.color}`
}}
disabled={props.disabled}
>

@ -17,7 +17,7 @@ import Settings from './Settings'
import Toolbar from './Toolbar'
import Overlay from './Overlay'
import Carbon from './Carbon'
import ExportButton from './ExportButton'
import ExportMenu from './ExportMenu'
import {
THEMES,
THEMES_HASH,
@ -36,14 +36,6 @@ import {
import { serializeState, getQueryStringState } from '../lib/routing'
import { getState, escapeHtml, unescapeHtml } from '../lib/util'
const saveButtonOptions = {
button: true,
color: '#c198fb',
selected: { id: 'SAVE_IMAGE', name: 'Export Image' },
list: ['png', 'svg', 'copy embed', 'open ↗'].map(id => ({ id, name: id.toUpperCase() })),
itemWrapper: props => <ExportButton {...props} />
}
class Editor extends React.Component {
constructor(props) {
super(props)
@ -205,18 +197,16 @@ class Editor extends React.Component {
this.setState({ [key]: value })
}
export({ id: format = 'png' }) {
if (format === 'copy embed') {
return
}
export(format = 'png') {
const link = document.createElement('a')
const timestamp = this.state.timestamp ? `_${formatTimestamp()}` : ''
const prefix = this.state.filename || 'carbon'
return this.getCarbonImage({ format, type: 'blob' }).then(url => {
if (format !== 'open ↗') {
if (format === 'open') {
link.target = '_blank'
} else {
link.download = `${prefix}${timestamp}.${format}`
}
link.href = url
@ -272,7 +262,22 @@ class Editor extends React.Component {
}
render() {
if (this.state.loading) {
const {
loading,
theme,
language,
backgroundColor,
backgroundImage,
backgroundMode,
aspectRatio,
uploading,
online,
titleBar,
code,
exportSize
} = this.state
if (loading) {
return (
<div>
<Spinner />
@ -294,15 +299,15 @@ class Editor extends React.Component {
<div className="editor">
<Toolbar>
<Dropdown
selected={THEMES_HASH[this.state.theme] || DEFAULT_THEME}
selected={THEMES_HASH[theme] || DEFAULT_THEME}
list={THEMES}
onChange={this.updateTheme}
/>
<Dropdown
selected={
LANGUAGE_NAME_HASH[this.state.language] ||
LANGUAGE_MIME_HASH[this.state.language] ||
LANGUAGE_MODE_HASH[this.state.language] ||
LANGUAGE_NAME_HASH[language] ||
LANGUAGE_MIME_HASH[language] ||
LANGUAGE_MODE_HASH[language] ||
LANGUAGE_MODE_HASH[DEFAULT_LANGUAGE]
}
list={LANGUAGES}
@ -310,10 +315,10 @@ class Editor extends React.Component {
/>
<BackgroundSelect
onChange={this.updateBackground}
mode={this.state.backgroundMode}
color={this.state.backgroundColor}
image={this.state.backgroundImage}
aspectRatio={this.state.aspectRatio}
mode={backgroundMode}
color={backgroundColor}
image={backgroundImage}
aspectRatio={aspectRatio}
/>
<Settings
{...config}
@ -322,16 +327,20 @@ class Editor extends React.Component {
/>
<div className="buttons">
{this.props.api.tweet &&
this.state.online && (
online && (
<Button
className="tweetButton"
onClick={this.upload}
title={this.state.uploading ? 'Loading...' : 'Tweet Image'}
title={uploading ? 'Loading...' : 'Tweet'}
color="#57b5f9"
style={{ marginRight: '8px' }}
/>
)}
<Dropdown {...saveButtonOptions} onChange={this.export} />
<ExportMenu
onChange={this.updateSetting}
export={this.export}
exportSize={exportSize}
/>
</div>
</Toolbar>
@ -343,15 +352,15 @@ class Editor extends React.Component {
>
{/*key ensures Carbon's internal language state is updated when it's changed by Dropdown*/}
<Carbon
key={this.state.language}
key={language}
config={this.state}
updateCode={this.updateCode}
onAspectRatioChange={this.updateAspectRatio}
titleBar={this.state.titleBar}
titleBar={titleBar}
updateTitleBar={this.updateTitleBar}
innerRef={this.innerRef}
>
{this.state.code != null ? this.state.code : DEFAULT_CODE}
{code != null ? code : DEFAULT_CODE}
</Carbon>
</Overlay>
)}

@ -11,16 +11,12 @@ const toIFrame = url =>
</iframe>
`
function ExportButton({ router, children, color }) {
function ExportButton({ router, color }) {
return (
<React.Fragment>
{children === 'COPY EMBED' ? (
<CopyButton text={toIFrame(router.asPath)}>
{({ copied }) => <button>{copied ? 'COPIED!' : 'COPY EMBED'}</button>}
</CopyButton>
) : (
<button>{children}</button>
)}
<CopyButton text={toIFrame(router.asPath)}>
{({ copied }) => <button>{copied ? 'Copied!' : 'Copy Embed'}</button>}
</CopyButton>
<style jsx>
{`
button {
@ -32,6 +28,7 @@ function ExportButton({ router, children, color }) {
color: ${color};
background: transparent;
cursor: pointer;
user-select: none;
}
&:active {

@ -0,0 +1,222 @@
import React from 'react'
import enhanceWithClickOutside from 'react-click-outside'
import shallowCompare from 'react-addons-shallow-compare'
import { COLORS, EXPORT_SIZES } from '../lib/constants'
import ExportButton from './ExportButton'
import Button from './Button'
import WindowPointer from './WindowPointer'
class ExportMenu extends React.Component {
state = {
isVisible: false
}
shouldComponentUpdate(prevProps, prevState) {
return (
prevState.isVisible !== this.state.isVisible ||
(prevState.isVisible && shallowCompare(this, prevProps, prevState))
)
}
toggle = () => {
this.setState({ isVisible: !this.state.isVisible })
}
handleClickOutside = () => {
this.setState({ isVisible: false })
}
handleInputChange = e => {
this.props.onChange('filename', e.target.value)
}
handleExportSizeChange = selectedSize => () => {
this.props.onChange('exportSize', selectedSize)
}
handleExport = format => () => {
this.props.export(format)
}
render() {
const { exportSize, filename } = this.props
const { isVisible } = this.state
return (
<div className="export-container">
<Button
selected={isVisible}
className="exportButton"
onClick={this.toggle}
title="Export"
color={COLORS.PURPLE}
/>
<div className="export-menu" hidden={!isVisible}>
<WindowPointer fromRight="12px" color={COLORS.PURPLE} />
<div className="export-option">
<input
title="filename"
placeholder="File name..."
value={filename}
name="filename"
onChange={this.handleInputChange}
/>
</div>
<div className="export-option">
<div className="size-container">
<span>Size</span>
<div>
{EXPORT_SIZES.map(({ name }) => (
<button
key={name}
onClick={this.handleExportSizeChange(name)}
className={`size-button ${exportSize === name ? 'selected' : ''}`}
>
{name}
</button>
))}
</div>
</div>
</div>
<div className="export-option">
<div className="open-container">
<button onClick={this.handleExport('open')}>Open </button>
</div>
<div className="copy-container">
<ExportButton color={COLORS.PURPLE}>Copy Embed</ExportButton>
</div>
<div className="save-container">
<span>Save as</span>
<div>
<button onClick={this.handleExport('png')} className="save-button">
PNG
</button>
<button onClick={this.handleExport('svg')} className="save-button">
SVG
</button>
</div>
</div>
</div>
</div>
<style jsx>
{`
button {
display: flex;
user-select: none;
cursor: pointer;
background: inherit;
outline: none;
border: none;
padding: 0;
color: ${COLORS.PURPLE};
}
button:hover {
opacity: 1;
}
input {
padding: 8px 16px;
width: 100%;
font-size: 12px;
color: ${COLORS.PURPLE};
background: transparent;
border: none;
outline: none;
}
input::placeholder {
color: ${COLORS.PURPLE};
opacity: 0.4;
}
.export-container {
position: relative;
color: ${COLORS.PURPLE};
font-size: 12px;
}
.export-menu {
box-sizing: content-box;
position: absolute;
margin-top: 10px;
width: 280px;
border-radius: 3px;
border: 2px solid ${COLORS.PURPLE};
right: 0;
background-color: ${COLORS.BLACK};
}
.export-option {
display: flex;
border-bottom: 1px solid ${COLORS.PURPLE};
}
.export-option:last-child {
border-bottom: none;
}
.size-container {
display: flex;
flex: 1;
padding: 8px 16px;
justify-content: space-between;
}
.size-container > div {
display: flex;
}
.size-button {
opacity: 0.4;
margin-right: 10px;
}
.size-button:last-child {
margin-right: 0;
}
.size-button.selected {
opacity: 1;
}
.copy-container,
.open-container,
.save-container {
display: flex;
flex: 1;
justify-content: center;
align-items: center;
padding: 12px 16px;
}
.copy-container {
flex-basis: 72px;
}
.copy-container,
.open-container {
border-right: 1px solid ${COLORS.PURPLE};
}
.save-container {
flex-direction: column;
}
.save-container > span {
margin-bottom: 6px;
}
.save-container > div {
display: flex;
}
.save-button {
opacity: 0.4;
}
.save-button:first-child {
margin-right: 8px;
}
`}
</style>
</div>
)
}
}
export default enhanceWithClickOutside(ExportMenu)

@ -1,15 +0,0 @@
import React from 'react'
import ListSetting from './ListSetting'
import { EXPORT_SIZES } from '../lib/constants'
const exportSize = size => <span>{size.name}</span>
function ExportSizeSelect(props) {
return (
<ListSetting title="Export size" items={EXPORT_SIZES} {...props}>
{exportSize}
</ListSetting>
)
}
export default ExportSizeSelect

@ -3,7 +3,6 @@ import enhanceWithClickOutside from 'react-click-outside'
import SettingsIcon from './svg/Settings'
import ThemeSelect from './ThemeSelect'
import FontSelect from './FontSelect'
import ExportSizeSelect from './ExportSizeSelect'
import Slider from './Slider'
import Toggle from './Toggle'
import WindowPointer from './WindowPointer'
@ -20,7 +19,6 @@ class Settings extends React.PureComponent {
}
this.toggle = this.toggle.bind(this)
this.format = this.format.bind(this)
this.handleInputChange = this.handleInputChange.bind(this)
}
toggle() {
@ -31,10 +29,6 @@ class Settings extends React.PureComponent {
this.setState({ isVisible: false })
}
handleInputChange(e) {
this.props.onChange(e.target.name, e.target.value)
}
format() {
return formatCode(this.props.code)
.then(this.props.onChange.bind(this, 'code'))
@ -58,13 +52,6 @@ class Settings extends React.PureComponent {
selected={this.props.windowTheme || 'none'}
onChange={this.props.onChange.bind(null, 'windowTheme')}
/>
<input
title="filename"
placeholder="File name..."
value={this.props.filename}
name="filename"
onChange={this.handleInputChange}
/>
<FontSelect
selected={this.props.fontFamily || 'Hack'}
onChange={this.props.onChange.bind(null, 'fontFamily')}
@ -137,15 +124,6 @@ class Settings extends React.PureComponent {
enabled={this.props.watermark}
onChange={this.props.onChange.bind(null, 'watermark')}
/>
<Toggle
label="Timestamp file name"
enabled={this.props.timestamp}
onChange={this.props.onChange.bind(null, 'timestamp')}
/>
<ExportSizeSelect
selected={this.props.exportSize || '2x'}
onChange={this.props.onChange.bind(null, 'exportSize')}
/>
<Toggle label="Prettify code" center={true} enabled={false} onChange={this.format} />
<Toggle
label={<center className="red">Reset settings</center>}
@ -194,7 +172,7 @@ class Settings extends React.PureComponent {
.settings-settings {
display: none;
position: absolute;
top: 44px;
top: 52px;
left: 0;
border: 1px solid ${COLORS.SECONDARY};
width: 184px;
@ -215,17 +193,6 @@ class Settings extends React.PureComponent {
.red {
color: red;
}
input {
padding: 8px;
width: 100%;
font-size: 12px;
color: ${COLORS.SECONDARY};
background: ${COLORS.BLACK};
border: none;
border-bottom: solid 1px ${COLORS.SECONDARY};
outline: none;
}
`}
</style>
</div>

@ -1,7 +1,7 @@
import React from 'react'
export default ({ fromLeft }) => (
<div style={{ left: fromLeft }}>
export default ({ fromLeft, fromRight, color = '#fff' }) => (
<div>
<div className="window-pointer" />
<style jsx>
{`
@ -9,11 +9,12 @@ export default ({ fromLeft }) => (
width: 0px;
height: 0px;
border-style: solid;
border-width: 0 4px 5px 4px;
border-color: transparent transparent #fff transparent;
border-width: 0 5px 10px 5px;
border-color: transparent transparent ${color} transparent;
position: absolute;
top: -5px;
left: 15px;
top: -10px;
left: ${fromLeft || 'initial'};
right: ${fromRight || 'initial'};
}
`}
</style>

@ -478,7 +478,8 @@ export const COLORS = {
PRIMARY: '#F8E81C',
SECONDARY: '#fff',
GRAY: '#858585',
HOVER: '#1F1F1F'
HOVER: '#1F1F1F',
PURPLE: '#C198FB'
}
export const DEFAULT_CODE = `const pluckDeep = key => obj => key.split('.').reduce((accum, key) => accum[key], obj)

@ -1823,10 +1823,10 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codemirror-graphql@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/codemirror-graphql/-/codemirror-graphql-0.8.2.tgz#387fdeb7ffe5023b5d48ef7b1e48dd53812d1383"
integrity sha512-bIRvsIhbGMvaKzyq3CYlOWz55+X9JUNPPgzrJwZUO8KVxj1iZLHdp3tiUxLUsTU9MdjS6iNjHDlhJiC+5IFBeQ==
codemirror-graphql@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/codemirror-graphql/-/codemirror-graphql-0.7.1.tgz#64b995643d511b9aa8f85eeeb2feac7aeb4b94d4"
integrity sha512-HtHXMJAn6iGJYpijkzi3IlqWIdGrB6V0RqJ607yffJTCKk/OgaNtdLOb8hZJyEtHfkw7PZDaKybMAVCi6ScWSQ==
dependencies:
graphql-language-service-interface "^1.3.2"
graphql-language-service-parser "^1.2.2"

Loading…
Cancel
Save