Background image settings (#169)

* Implement drag-n-drop image for backgrounds

- Closes #122

* In progress

* Add background-size, background-position settings

* Add ReactCrop

* Remove old bg sizing sliders

* Add resize observer

* Fix typo

* Disable select on sliders

* onAspectRatioChange hook

* Rename to backgroundColor

* Fix state issues

* Add backgroundMode toggle

* Rename ColorPicker

* Add upload-image margin

* Minor font-size change

* Fix build
main
Jake Dexheimer 7 years ago committed by Michael Fix
parent 77af47b59e
commit cbc6824b83

1
.gitignore vendored

@ -3,4 +3,3 @@ node_modules
.next
cypress/videos
cypress/screenshots
package-lock.json

@ -1 +0,0 @@
package-lock=false

@ -0,0 +1,208 @@
import React from 'react'
import enhanceWithClickOutside from 'react-click-outside'
import { SketchPicker } from 'react-color'
import WindowPointer from './WindowPointer'
import ImagePicker from './ImagePicker'
import { COLORS } from '../lib/constants'
import { parseRGBA, capitalizeFirstLetter } from '../lib/util'
class BackgroundSelect extends React.Component {
constructor() {
super()
this.state = { isVisible: false, selectedTab: 'color' }
this.toggle = this.toggle.bind(this)
this.selectTab = this.selectTab.bind(this)
this.handlePickColor = this.handlePickColor.bind(this)
}
toggle() {
this.setState({ isVisible: !this.state.isVisible })
}
selectTab(name) {
if (this.props.config.backgroundMode !== name) {
this.props.onChange({ backgroundMode: name })
}
}
handleClickOutside() {
this.setState({ isVisible: false })
}
handlePickColor(color) {
this.props.onChange({ backgroundColor: parseRGBA(color.rgb) })
}
render() {
return (
<div className="bg-select-container">
<div className="bg-select-display">
<div className="bg-select-label">
<span>BG</span>
</div>
<div className="bg-color-container" onClick={this.toggle}>
<div className="bg-color-alpha" />
<div className="bg-color" />
</div>
</div>
<div className="bg-select-pickers" hidden={!this.state.isVisible}>
<WindowPointer fromLeft="15px" />
<div className="picker-tabs">
{['color', 'image'].map((tab, i) => (
<div
key={i}
className={`picker-tab ${this.props.config.backgroundMode === tab ? 'active' : ''}`}
onClick={this.selectTab.bind(null, tab)}
>
{capitalizeFirstLetter(tab)}
</div>
))}
</div>
<div className="picker-tabs-contents">
<div style={this.props.config.backgroundMode === 'color' ? {} : { display: 'none' }}>
<SketchPicker
color={this.props.config.backgroundColor}
onChangeComplete={this.handlePickColor}
/>
</div>
<div style={this.props.config.backgroundMode === 'image' ? {} : { display: 'none' }}>
<ImagePicker
onChange={this.props.onChange}
imageDataURL={this.props.config.backgroundImage}
aspectRatio={this.props.config.aspectRatio}
/>
</div>
</div>
</div>
<style jsx>{`
.bg-select-container {
height: 100%;
}
.bg-select-display {
display: flex;
height: 100%;
width: 72px;
border: 0.5px solid ${COLORS.SECONDARY};
border-radius: 3px;
}
.bg-select-label {
display: flex;
align-items: center;
justify-content: center;
user-select: none;
cursor: default;
height: 100%;
padding: 0 8px;
border-right: 0.5px solid ${COLORS.SECONDARY};
}
.bg-color-container {
position: relative;
width: 34px;
margin-bottom: 1px;
background: #fff;
border-radius: 0px 2px 2px 0px;
cursor: pointer;
}
.bg-color {
border-radius: 0px 2px 2px 0px;
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
${this.props.config.backgroundMode === 'image'
? `background: url(${this.props.config.backgroundImage});
background-size: cover;
background-repeat: no-repeat;`
: `background: ${this.props.config.backgroundColor || config.backgroundColor};
background-size: auto;
background-repeat: repeat;`};
}
.bg-color-alpha {
border-radius: 0px 2px 2px 0px;
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==)
left center;
}
.picker-tabs {
display: flex;
border-bottom: 1px solid ${COLORS.SECONDARY};
}
.picker-tab {
user-select: none;
cursor: pointer;
background: rgba(255, 255, 255, 0.165);
width: 50%;
text-align: center;
padding: 8px 0;
border-right: 1px solid ${COLORS.SECONDARY};
}
.picker-tab:last-child {
border-right: none;
}
.picker-tab.active {
background: none;
}
.bg-select-pickers {
position: absolute;
width: 222px;
margin-left: 36px;
margin-top: 4px;
border: 0.5px solid ${COLORS.SECONDARY};
border-radius: 3px;
background: #1a1a1a;
}
/* react-color overrides */
.bg-select-pickers :global(.sketch-picker) {
background: #1a1a1a !important;
padding: 8px 8px 0 !important;
margin: 0 auto 1px !important;
}
.bg-select-pickers :global(.sketch-picker > div:nth-child(3) > div > div > span) {
color: ${COLORS.SECONDARY} !important;
}
/* TODO remove once base64 url issue fixed in react-color */
/* prettier-ignore */
.bg-select-pickers :global(.sketch-picker > div:nth-child(2) > div:nth-child(1) > div:nth-child(2) > div > div:nth-child(1) > div),
.bg-select-pickers :global(.sketch-picker > div:nth-child(2) > div:nth-child(2) > div:nth-child(1)) {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==)
left center !important;
}
.bg-select-pickers :global(.sketch-picker > div:nth-child(3) > div > div > input) {
width: 100% !important;
box-shadow: none;
outline: none;
border-radius: 2px;
background: rgba(255, 255, 255, 0.165);
color: #fff !important;
}
/* prettier-ignore */
.bg-select-pickers :global(.sketch-picker > div:nth-child(2) > div:nth-child(1) > div:nth-child(2), .sketch-picker > div:nth-child(2) > div:nth-child(2)) {
background: #fff;
}
`}</style>
</div>
)
}
}
export default enhanceWithClickOutside(BackgroundSelect)

@ -2,6 +2,7 @@ import { EOL } from 'os'
import * as hljs from 'highlight.js'
import React from 'react'
import Spinner from 'react-spinner'
import ResizeObserver from 'resize-observer-polyfill'
import toHash from 'tohash'
import debounce from 'lodash.debounce'
import ms from 'ms'
@ -20,7 +21,8 @@ const DEFAULT_SETTINGS = {
paddingHorizontal: '50px',
marginVertical: '45px',
marginHorizontal: '45px',
background: 'rgba(171, 184, 195, 1)',
backgroundMode: 'color',
backgroundColor: 'rgba(171, 184, 195, 1)',
dropShadowOffsetY: '20px',
dropShadowBlurRadius: '68px',
theme: 'seti',
@ -49,6 +51,12 @@ class Carbon extends React.Component {
})
this.handleLanguageChange(this.props.children)
const ro = new ResizeObserver(entries => {
const cr = entries[0].contentRect
this.props.onAspectRatioChange(cr.width / cr.height)
})
ro.observe(this.exportContainerNode)
}
componentWillReceiveProps(newProps) {
@ -91,6 +99,9 @@ class Carbon extends React.Component {
viewportMargin: Infinity,
lineWrapping: true
}
const backgroundImage =
(this.props.config.backgroundImage && this.props.config.backgroundImageSelection) ||
this.props.config.backgroundImage
// set content to spinner if loading, else editor
let content = (
@ -147,11 +158,11 @@ class Carbon extends React.Component {
}
#container .bg {
${this.props.config.backgroundImage
? `background: url(${this.props.config.backgroundImage});
${this.props.config.backgroundMode === 'image'
? `background: url(${backgroundImage});
background-size: cover;
background-repeat: no-repeat;`
: `background: ${this.props.config.background || config.background};
: `background: ${this.props.config.backgroundColor || config.backgroundColor};
background-size: auto;
background-repeat: repeat;`} position: absolute;
top: 0px;
@ -233,7 +244,7 @@ class Carbon extends React.Component {
return (
<div id="section">
<div id="export-container">
<div id="export-container" ref={ele => (this.exportContainerNode = ele)}>
{content}
<div id="twitter-png-fix" />
</div>

@ -1,141 +0,0 @@
import React from 'react'
import enhanceWithClickOutside from 'react-click-outside'
import { SketchPicker } from 'react-color'
import WindowPointer from './WindowPointer'
import { COLORS } from '../lib/constants'
import { parseRGBA } from '../lib/util'
class ColorPicker extends React.Component {
constructor() {
super()
this.state = { isVisible: false }
this.toggle = this.toggle.bind(this)
this.handlePickColor = this.handlePickColor.bind(this)
}
toggle() {
this.setState({ isVisible: !this.state.isVisible })
}
handleClickOutside() {
this.setState({ isVisible: false })
}
handlePickColor(color) {
this.props.onChange(parseRGBA(color.rgb))
}
render() {
return (
<div className="colorpicker-container">
<div className="colorpicker-display">
<div className="colorpicker-label">
<span>BG</span>
</div>
<div className="bg-color-container" onClick={this.toggle}>
<div className="bg-color-alpha" />
<div className="bg-color" style={{ background: this.props.bg }} />
</div>
</div>
<div className="colorpicker-picker" hidden={!this.state.isVisible}>
<WindowPointer fromLeft="15px" />
<SketchPicker color={this.props.bg} onChangeComplete={this.handlePickColor} />
</div>
<style jsx>{`
.colorpicker-container {
height: 100%;
}
.colorpicker-display {
display: flex;
height: 100%;
width: 72px;
border: 0.5px solid ${COLORS.SECONDARY};
border-radius: 3px;
}
.colorpicker-label {
display: flex;
align-items: center;
justify-content: center;
user-select: none;
cursor: default;
height: 100%;
padding: 0 8px;
border-right: 0.5px solid ${COLORS.SECONDARY};
}
.bg-color-container {
position: relative;
width: 34px;
margin-bottom: 1px;
background: #fff;
border-radius: 0px 2px 2px 0px;
cursor: pointer;
}
.bg-color {
border-radius: 0px 2px 2px 0px;
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
.bg-color-alpha {
border-radius: 0px 2px 2px 0px;
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==)
left center;
}
.colorpicker-picker {
position: absolute;
margin-left: 36px;
margin-top: 4px;
}
/* react-color overrides */
.colorpicker-picker :global(.sketch-picker) {
border: 0.5px solid ${COLORS.SECONDARY} !important;
border-radius: 3px !important;
background: #1a1a1a !important;
}
.colorpicker-picker > :global(.sketch-picker > div:nth-child(3) > div > div > span) {
color: ${COLORS.SECONDARY} !important;
}
/* TODO remove once base64 url issue fixed in react-color */
/* prettier-ignore */
.colorpicker-picker > :global(.sketch-picker > div:nth-child(2) > div:nth-child(1) > div:nth-child(2) > div > div:nth-child(1) > div),
.colorpicker-picker > :global(.sketch-picker > div:nth-child(2) > div:nth-child(2) > div:nth-child(1)) {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==)
left center !important;
}
.colorpicker-picker > :global(.sketch-picker > div:nth-child(3) > div > div > input) {
width: 100% !important;
box-shadow: none;
outline: none;
border-radius: 2px;
background: rgba(255, 255, 255, 0.165);
color: #fff !important;
}
/* prettier-ignore */
.colorpicker-picker :global(.sketch-picker > div:nth-child(2) > div:nth-child(1) > div:nth-child(2), .sketch-picker > div:nth-child(2) > div:nth-child(2)) {
background: #fff;
}
`}</style>
</div>
)
}
}
export default enhanceWithClickOutside(ColorPicker)

@ -9,7 +9,7 @@ const Header = ({ enableHeroText }) => (
</a>
{enableHeroText ? (
<h2 className="mt3">
Create and share beautiful images of your source code.<br /> Start typing, or drop a file
Create and share beautiful images of your source code.<br /> Start typing or drop a file
into the text area to get started.
</h2>
) : null}

@ -0,0 +1,188 @@
import React from 'react'
import ReactCrop, { makeAspectCrop } from 'react-image-crop'
import Slider from './Slider'
import { COLORS } from '../lib/constants'
const getCroppedImg = (imageDataURL, pixelCrop) => {
const canvas = document.createElement('canvas')
canvas.width = pixelCrop.width
canvas.height = pixelCrop.height
const ctx = canvas.getContext('2d')
return new Promise((resolve, reject) => {
const image = new Image()
image.src = imageDataURL
image.onload = () => {
ctx.drawImage(
image,
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height,
0,
0,
pixelCrop.width,
pixelCrop.height
)
resolve(canvas.toDataURL('image/jpeg'))
}
})
}
const INITIAL_STATE = { crop: null, imageAspectRatio: null, pixelCrop: null }
export default class extends React.Component {
constructor() {
super()
this.state = INITIAL_STATE
this.selectImage = this.selectImage.bind(this)
this.removeImage = this.removeImage.bind(this)
this.onImageLoaded = this.onImageLoaded.bind(this)
this.onCropChange = this.onCropChange.bind(this)
this.onDragEnd = this.onDragEnd.bind(this)
}
componentWillReceiveProps(nextProps) {
if (this.state.crop && this.props.aspectRatio != nextProps.aspectRatio) {
// update crop for editor container aspect-ratio change
this.setState({
crop: makeAspectCrop(
{
...this.state.crop,
aspect: nextProps.aspectRatio
},
this.state.imageAspectRatio
)
})
}
}
selectImage(e) {
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = e =>
this.props.onChange({ backgroundImage: e.target.result, backgroundImageSelection: null })
reader.readAsDataURL(file)
}
removeImage() {
this.setState(INITIAL_STATE, () => {
this.props.onChange({
backgroundMode: 'color',
backgroundImage: null,
backgroundImageSelection: null
})
})
}
onImageLoaded(image) {
const imageAspectRatio = image.width / image.height
const initialCrop = {
x: 0,
y: 0,
width: 100,
aspect: this.props.aspectRatio
}
this.setState({
imageAspectRatio,
crop: makeAspectCrop(initialCrop, imageAspectRatio)
})
}
onCropChange(crop, pixelCrop) {
this.setState({
crop: { ...crop, aspect: this.props.aspectRatio },
pixelCrop
})
}
async onDragEnd() {
if (this.state.pixelCrop) {
const croppedImg = await getCroppedImg(this.props.imageDataURL, this.state.pixelCrop)
this.props.onChange({ backgroundImageSelection: croppedImg })
}
}
render() {
let content = (
<div className="upload-image">
<span>Click the button below to upload a background image</span>
<input type="file" accept="image/x-png,image/jpeg,image/jpg" onChange={this.selectImage} />
<style jsx>{`
.upload-image {
padding: 8px;
}
span {
display: block;
margin-bottom: 16px;
}
`}</style>
</div>
)
if (this.props.imageDataURL) {
content = (
<div className="settings-container">
<div className="image-container">
<div className="label">
<span>Background image</span>
<a href="#" onClick={this.removeImage}>
&times;
</a>
</div>
<ReactCrop
src={this.props.imageDataURL}
onImageLoaded={this.onImageLoaded}
crop={this.state.crop}
onChange={this.onCropChange}
onDragEnd={this.onDragEnd}
minHeight={10}
minWidth={10}
keepSelection
/>
</div>
<style jsx>{`
.settings-container img {
width: 100%;
}
.label {
user-select: none;
margin-bottom: 4px;
}
:global(.ReactCrop__image) {
user-select: none;
user-drag: none;
}
.image-container {
padding: 8px;
}
.image-container .label {
display: flex;
justify-content: space-between;
align-items: center;
}
`}</style>
</div>
)
}
return (
<div>
<div className="image-picker-container">{content}</div>
<style jsx>{`
.image-picker-container {
font-size: 12px;
}
`}</style>
</div>
)
}
}

@ -47,6 +47,7 @@ export default () => (
/>
))}
<link rel="stylesheet" type="text/css" href="/static/react-spinner.css" />
<link rel="stylesheet" type="text/css" href="/static/react-crop.css" />
</Head>
<Reset />
<Font />

@ -7,7 +7,7 @@ export default class extends React.Component {
}
handleChange(e) {
this.props.onChange(`${e.target.value}px`)
this.props.onChange(`${e.target.value}${this.props.usePercentage ? '%' : 'px'}`)
}
render() {
@ -38,6 +38,7 @@ export default class extends React.Component {
position: relative;
height: 32px;
overflow: hidden;
user-select: none;
}
.slider:last-of-type {

@ -50,7 +50,7 @@ export default class extends React.Component {
render() {
return (
<div className="window-theme">
<span className="label">Window Theme</span>
<span className="label">Window theme</span>
<div className="themes">{this.renderThemes()}</div>
<style jsx>{`
.window-theme {

@ -12,22 +12,18 @@ describe('background color', () => {
const picker = '.colorpicker-picker'
const openPicker = () => {
cy.get(bgColor)
.click()
return cy.get(picker)
.should('be.visible')
cy.get(bgColor).click()
return cy.get(picker).should('be.visible')
}
// clicking anywhere else closes it
const closePicker = () =>
cy.get('body').click()
const closePicker = () => cy.get('body').click()
it('opens BG color pick', () => {
cy.visit('/')
openPicker()
closePicker()
cy.get(picker)
.should('not.be.visible')
cy.get(picker).should('not.be.visible')
})
it('changes background color to dark red', () => {
@ -35,22 +31,23 @@ describe('background color', () => {
const darkRed = '#D0021B'
const darkRedTile = `[title="${darkRed}"]`
openPicker()
cy.get(picker).find(darkRedTile).click()
cy
.get(picker)
.find(darkRedTile)
.click()
closePicker()
// changing background color triggers url change
cy.url().should('contain', '?bg=')
// confirm color change
cy.get('#container-bg .bg')
.should('have.css', 'background-color', hex2rgb(darkRed).rgbString)
cy.get('#container-bg .bg').should('have.css', 'background-color', hex2rgb(darkRed).rgbString)
})
it('specifies color in url', () => {
cy.visit('/?bg=rgb(255,0,0)')
editorVisible()
cy.get('#container-bg .bg')
.should('have.css', 'background-color', 'rgb(255, 0, 0)')
cy.get('#container-bg .bg').should('have.css', 'background-color', 'rgb(255, 0, 0)')
})
it('enters neon pink', () => {
@ -58,12 +55,13 @@ describe('background color', () => {
editorVisible()
const pink = 'ff00ff'
openPicker().find(`input[value="FF0000"]`)
.clear().type(`${pink}{enter}`)
openPicker()
.find(`input[value="FF0000"]`)
.clear()
.type(`${pink}{enter}`)
closePicker()
cy.url().should('contain', '?bg=rgba(255,0,255,1')
cy.get('#container-bg .bg')
.should('have.css', 'background-color', 'rgb(255, 0, 255)')
cy.get('#container-bg .bg').should('have.css', 'background-color', 'rgb(255, 0, 255)')
})
})

@ -7,8 +7,7 @@ import {editorVisible} from '../support'
// so instead visit the desired url in each test
describe('localStorage', () => {
const themeDropdown = () =>
cy.get('#toolbar .dropdown-container').first()
const themeDropdown = () => cy.get('#toolbar .dropdown-container').first()
const pickTheme = (name = 'Blackboard') =>
themeDropdown()
@ -19,7 +18,9 @@ describe('localStorage', () => {
it('is empty initially', () => {
cy.visit('/')
editorVisible()
cy.window().its('localStorage')
cy
.window()
.its('localStorage')
.should('have.length', 0)
})
@ -29,9 +30,12 @@ describe('localStorage', () => {
pickTheme('Blackboard')
themeDropdown().contains('Blackboard')
cy.window().its('localStorage.CARBON_STATE')
cy
.window()
.its('localStorage.CARBON_STATE')
.then(JSON.parse)
.its('theme').should('equal', 'blackboard')
.its('theme')
.should('equal', 'blackboard')
// visiting page again restores theme from localStorage
cy.visit('/')

@ -15,8 +15,7 @@
/* global cy */
export const editorVisible = () =>
cy.get('#editor').should('be.visible')
export const editorVisible = () => cy.get('#editor').should('be.visible')
// Import commands.js using ES2015 syntax:
// import './commands'

@ -10,7 +10,7 @@ if (typeof window !== 'undefined') {
const mapper = new Morph()
const mappings = [
'bg:background',
'bg:backgroundColor',
't:theme',
'l:language',
'ds:dropShadow',

@ -13,3 +13,5 @@ export const parseRGBA = obj => `rgba(${obj.r},${obj.g},${obj.b},${obj.a})`
export const getState = morph.compose(parse, morph.get(KEY))
export const saveState = (window, v) => assign(window, JSON.stringify(v))
export const capitalizeFirstLetter = s => s.charAt(0).toUpperCase() + s.slice(1)

9646
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -40,9 +40,11 @@
"react-dnd": "^2.4.0",
"react-dnd-html5-backend": "^2.4.1",
"react-dom": "^16.2.0",
"react-image-crop": "^3.0.9",
"react-spinkit": "^3.0.0",
"react-spinner": "^0.2.7",
"react-syntax-highlight": "^15.3.1",
"resize-observer-polyfill": "^1.5.0",
"tohash": "^1.0.2",
"twitter": "^1.7.1"
},

@ -9,7 +9,7 @@ import ReadFileDropContainer, { DATA_URL, TEXT } from 'dropperx'
import Page from '../components/Page'
import Button from '../components/Button'
import Dropdown from '../components/Dropdown'
import ColorPicker from '../components/ColorPicker'
import BackgroundSelect from '../components/BackgroundSelect'
import Settings from '../components/Settings'
import Toolbar from '../components/Toolbar'
import Overlay from '../components/Overlay'
@ -56,7 +56,10 @@ class Editor extends React.Component {
super(props)
this.state = Object.assign(
{
background: 'rgba(171, 184, 195, 1)',
backgroundMode: 'color',
backgroundColor: 'rgba(171, 184, 195, 1)',
backgroundImage: null,
backgroundImageSelection: null,
theme: DEFAULT_THEME.id,
language: DEFAULT_LANGUAGE,
dropShadow: true,
@ -68,7 +71,6 @@ class Editor extends React.Component {
paddingVertical: '48px',
paddingHorizontal: '32px',
uploading: false,
backgroundImage: null,
code: props.content,
_initialState: this.props.initialState
},
@ -78,6 +80,7 @@ class Editor extends React.Component {
this.save = this.save.bind(this)
this.upload = this.upload.bind(this)
this.updateCode = this.updateCode.bind(this)
this.updateAspectRatio = this.updateAspectRatio.bind(this)
}
componentDidMount() {
@ -95,6 +98,7 @@ class Editor extends React.Component {
const s = { ...this.state }
delete s.code
delete s.backgroundImage
delete s.backgroundImageSelection
saveState(localStorage, s)
}
@ -118,6 +122,10 @@ class Editor extends React.Component {
this.setState({ code })
}
updateAspectRatio(aspectRatio) {
this.setState({ aspectRatio })
}
save() {
this.getCarbonImage().then(dataUrl => {
const link = document.createElement('a')
@ -163,10 +171,7 @@ class Editor extends React.Component {
list={LANGUAGES}
onChange={language => this.setState({ language: language.mime || language.mode })}
/>
<ColorPicker
onChange={color => this.setState({ background: color })}
bg={this.state.background}
/>
<BackgroundSelect onChange={changes => this.setState(changes)} config={this.state} />
<Settings
onChange={(key, value) => this.setState({ [key]: value })}
enabled={this.state}
@ -192,7 +197,11 @@ class Editor extends React.Component {
}}
onDrop={([file]) => {
if (this.isImage(file)) {
this.setState({ backgroundImage: file.content })
this.setState({
backgroundImage: file.content,
backgroundImageSelection: null,
backgroundMode: 'image'
})
} else {
this.setState({ code: file.content })
}
@ -203,7 +212,11 @@ class Editor extends React.Component {
isOver={isOver || canDrop}
title={`Drop your file here to import ${isOver ? '✋' : '✊'}`}
>
<Carbon config={this.state} updateCode={code => this.updateCode(code)}>
<Carbon
config={this.state}
updateCode={code => this.updateCode(code)}
onAspectRatioChange={this.updateAspectRatio}
>
{this.state.code || DEFAULT_CODE}
</Carbon>
</Overlay>

@ -0,0 +1,163 @@
.ReactCrop {
position: relative;
display: inline-block;
cursor: crosshair;
overflow: hidden;
max-width: 100%;
background-color: #000; }
.ReactCrop:focus {
outline: none; }
.ReactCrop--disabled {
cursor: inherit; }
.ReactCrop__image {
display: block;
max-width: 100%; }
.ReactCrop--crop-invisible .ReactCrop__image {
opacity: 0.5; }
.ReactCrop__crop-selection {
position: absolute;
top: 0;
left: 0;
transform: translate3d(0, 0, 0);
box-sizing: border-box;
cursor: move;
box-shadow: 0 0 0 9999em rgba(0, 0, 0, 0.5);
border: 1px solid;
border-image-source: url("data:image/gif;base64,R0lGODlhCgAKAJECAAAAAP///////wAAACH/C05FVFNDQVBFMi4wAwEAAAAh/wtYTVAgRGF0YVhNUDw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OEI5RDc5MTFDNkE2MTFFM0JCMDZEODI2QTI4MzJBOTIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OEI5RDc5MTBDNkE2MTFFM0JCMDZEODI2QTI4MzJBOTIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuZGlkOjAyODAxMTc0MDcyMDY4MTE4MDgzQzNDMjA5MzREQ0ZDIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjAyODAxMTc0MDcyMDY4MTE4MDgzQzNDMjA5MzREQ0ZDIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEBQoAAgAsAAAAAAoACgAAAhWEERkn7W3ei7KlagMWF/dKgYeyGAUAIfkEBQoAAgAsAAAAAAoACgAAAg+UYwLJ7RnQm7QmsCyVKhUAIfkEBQoAAgAsAAAAAAoACgAAAhCUYgLJHdiinNSAVfOEKoUCACH5BAUKAAIALAAAAAAKAAoAAAIRVISAdusPo3RAzYtjaMIaUQAAIfkEBQoAAgAsAAAAAAoACgAAAg+MDiem7Q8bSLFaG5il6xQAIfkEBQoAAgAsAAAAAAoACgAAAg+UYRLJ7QnQm7SmsCyVKhUAIfkEBQoAAgAsAAAAAAoACgAAAhCUYBLJDdiinNSEVfOEKoECACH5BAUKAAIALAAAAAAKAAoAAAIRFISBdusPo3RBzYsjaMIaUQAAOw==");
border-image-slice: 1;
border-image-repeat: repeat; }
.ReactCrop--disabled .ReactCrop__crop-selection {
cursor: inherit; }
.ReactCrop__drag-handle {
position: absolute;
width: 9px;
height: 9px;
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.7);
box-sizing: border-box;
outline: 1px solid transparent; }
.ReactCrop .ord-nw {
top: 0;
left: 0;
margin-top: -5px;
margin-left: -5px;
cursor: nw-resize; }
.ReactCrop .ord-n {
top: 0;
left: 50%;
margin-top: -5px;
margin-left: -5px;
cursor: n-resize; }
.ReactCrop .ord-ne {
top: 0;
right: 0;
margin-top: -5px;
margin-right: -5px;
cursor: ne-resize; }
.ReactCrop .ord-e {
top: 50%;
right: 0;
margin-top: -5px;
margin-right: -5px;
cursor: e-resize; }
.ReactCrop .ord-se {
bottom: 0;
right: 0;
margin-bottom: -5px;
margin-right: -5px;
cursor: se-resize; }
.ReactCrop .ord-s {
bottom: 0;
left: 50%;
margin-bottom: -5px;
margin-left: -5px;
cursor: s-resize; }
.ReactCrop .ord-sw {
bottom: 0;
left: 0;
margin-bottom: -5px;
margin-left: -5px;
cursor: sw-resize; }
.ReactCrop .ord-w {
top: 50%;
left: 0;
margin-top: -5px;
margin-left: -5px;
cursor: w-resize; }
.ReactCrop__disabled .ReactCrop__drag-handle {
cursor: inherit; }
.ReactCrop__drag-bar {
position: absolute; }
.ReactCrop__drag-bar.ord-n {
top: 0;
left: 0;
width: 100%;
height: 6px;
margin-top: -3px; }
.ReactCrop__drag-bar.ord-e {
right: 0;
top: 0;
width: 6px;
height: 100%;
margin-right: -3px; }
.ReactCrop__drag-bar.ord-s {
bottom: 0;
left: 0;
width: 100%;
height: 6px;
margin-bottom: -3px; }
.ReactCrop__drag-bar.ord-w {
top: 0;
left: 0;
width: 6px;
height: 100%;
margin-left: -3px; }
.ReactCrop--new-crop .ReactCrop__drag-bar,
.ReactCrop--new-crop .ReactCrop__drag-handle,
.ReactCrop--fixed-aspect .ReactCrop__drag-bar {
display: none; }
.ReactCrop--fixed-aspect .ReactCrop__drag-handle.ord-n,
.ReactCrop--fixed-aspect .ReactCrop__drag-handle.ord-e,
.ReactCrop--fixed-aspect .ReactCrop__drag-handle.ord-s,
.ReactCrop--fixed-aspect .ReactCrop__drag-handle.ord-w {
display: none; }
@media (max-width: 768px) {
.ReactCrop__drag-handle {
width: 17px;
height: 17px; }
.ReactCrop .ord-nw {
margin-top: -9px;
margin-left: -9px; }
.ReactCrop .ord-n {
margin-top: -9px;
margin-left: -9px; }
.ReactCrop .ord-ne {
margin-top: -9px;
margin-right: -9px; }
.ReactCrop .ord-e {
margin-top: -9px;
margin-right: -9px; }
.ReactCrop .ord-se {
margin-bottom: -9px;
margin-right: -9px; }
.ReactCrop .ord-s {
margin-bottom: -9px;
margin-left: -9px; }
.ReactCrop .ord-sw {
margin-bottom: -9px;
margin-left: -9px; }
.ReactCrop .ord-w {
margin-top: -9px;
margin-left: -9px; }
.ReactCrop__drag-bar.ord-n {
height: 14px;
margin-top: -7px; }
.ReactCrop__drag-bar.ord-e {
width: 14px;
margin-right: -7px; }
.ReactCrop__drag-bar.ord-s {
height: 14px;
margin-bottom: -7px; }
.ReactCrop__drag-bar.ord-w {
width: 14px;
margin-left: -7px; } }

@ -4893,6 +4893,10 @@ react-hot-loader@3.1.1:
redbox-react "^1.3.6"
source-map "^0.6.1"
react-image-crop@^3.0.9:
version "3.0.9"
resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-3.0.9.tgz#5f1601a50c6fda31b3ad927a56380c81bfa466db"
react-proxy@^3.0.0-alpha.0:
version "3.0.0-alpha.1"
resolved "https://registry.yarnpkg.com/react-proxy/-/react-proxy-3.0.0-alpha.1.tgz#4400426bcfa80caa6724c7755695315209fa4b07"
@ -5184,6 +5188,10 @@ require-relative@^0.8.7:
version "0.8.7"
resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de"
resize-observer-polyfill@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz#660ff1d9712a2382baa2cad450a4716209f9ca69"
resolve-from@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"

Loading…
Cancel
Save