mirror of https://github.com/sgoudham/carbon.git
Refactor state components (#338)
* WIP: Unstated works * WIP: extract into its own file * Container instance * Remove unused variables * Move toolbar children into Toolbar * Extract our Coder interface * Remove top level subscription * Bug fix * WIP * Remove old dependencies * Add import eslint plugin * Rename components - add createRef TODO [ ] * Bug fixes * Rename Coder -> CodeWindow * Address comments - Rename to variable - Rename onDrop from Editor containers perspective * More variable renamingmain
parent
f44f944d3a
commit
0a59db56c8
@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
parser: 'babel-eslint',
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true
|
||||
},
|
||||
extends: ['eslint:recommended', 'plugin:react/recommended'],
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
experimentalObjectRestSpread: true,
|
||||
jsx: true
|
||||
},
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: ['react', 'import'],
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/display-name': 'off',
|
||||
'react/jsx-uses-react': 'error',
|
||||
'react/jsx-uses-vars': 'error',
|
||||
'import/no-unresolved': 2
|
||||
}
|
||||
}
|
@ -1,279 +1,41 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import * as hljs from 'highlight.js'
|
||||
import Spinner from 'react-spinner'
|
||||
import ResizeObserver from 'resize-observer-polyfill'
|
||||
import debounce from 'lodash.debounce'
|
||||
import ms from 'ms'
|
||||
|
||||
import WindowControls from '../components/WindowControls'
|
||||
import Watermark from '../components/svg/Watermark'
|
||||
import CodeMirror from '../lib/react-codemirror'
|
||||
import { COLORS, LANGUAGE_MODE_HASH, LANGUAGE_NAME_HASH, DEFAULT_SETTINGS } from '../lib/constants'
|
||||
|
||||
class Carbon extends PureComponent {
|
||||
// Theirs
|
||||
import React from 'react'
|
||||
import { Provider } from 'unstated'
|
||||
import HTML5Backend from 'react-dnd-html5-backend'
|
||||
import { DragDropContext } from 'react-dnd'
|
||||
|
||||
// Ours
|
||||
import EditorContainer from '../containers/Editor'
|
||||
import Editor from './Editor'
|
||||
import Toolbar from './Toolbar'
|
||||
import { COLORS } from '../lib/constants'
|
||||
|
||||
class Carbon extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
loading: true,
|
||||
language: props.config.language
|
||||
}
|
||||
|
||||
this.handleLanguageChange = this.handleLanguageChange.bind(this)
|
||||
this.handleTitleBarChange = this.handleTitleBarChange.bind(this)
|
||||
this.codeUpdated = this.codeUpdated.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
loading: false
|
||||
})
|
||||
|
||||
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) {
|
||||
// TODO use getDerivedStateFromProps() on React@16.3
|
||||
this.handleLanguageChange(newProps.children, { customProps: newProps })
|
||||
}
|
||||
|
||||
codeUpdated(newCode) {
|
||||
this.handleLanguageChange(newCode)
|
||||
this.props.updateCode(newCode)
|
||||
}
|
||||
|
||||
handleTitleBarChange(newTitle) {
|
||||
this.props.updateTitleBar(newTitle)
|
||||
this.inject = [new EditorContainer(props)]
|
||||
}
|
||||
|
||||
handleLanguageChange = debounce(
|
||||
(newCode, config) => {
|
||||
const props = (config && config.customProps) || this.props
|
||||
|
||||
if (props.config.language === 'auto') {
|
||||
// try to set the language
|
||||
const detectedLanguage = hljs.highlightAuto(newCode).language
|
||||
const languageMode =
|
||||
LANGUAGE_MODE_HASH[detectedLanguage] || LANGUAGE_NAME_HASH[detectedLanguage]
|
||||
|
||||
if (languageMode) {
|
||||
this.setState({ language: languageMode.mime || languageMode.mode })
|
||||
}
|
||||
} else {
|
||||
this.setState({ language: props.config.language })
|
||||
}
|
||||
},
|
||||
ms('300ms'),
|
||||
{ trailing: true }
|
||||
)
|
||||
|
||||
render() {
|
||||
const config = { ...DEFAULT_SETTINGS, ...this.props.config }
|
||||
const options = {
|
||||
lineNumbers: config.lineNumbers,
|
||||
mode: this.state.language || 'plaintext',
|
||||
theme: config.theme,
|
||||
scrollBarStyle: null,
|
||||
viewportMargin: Infinity,
|
||||
lineWrapping: true,
|
||||
extraKeys: {
|
||||
'Shift-Tab': 'indentLess'
|
||||
}
|
||||
}
|
||||
const backgroundImage =
|
||||
(this.props.config.backgroundImage && this.props.config.backgroundImageSelection) ||
|
||||
this.props.config.backgroundImage
|
||||
|
||||
// set content to spinner if loading, else editor
|
||||
let content = (
|
||||
<div>
|
||||
<Spinner />
|
||||
<style jsx>
|
||||
{`
|
||||
div {
|
||||
height: 352px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
if (this.state.loading === false) {
|
||||
content = (
|
||||
<div id="container">
|
||||
{config.windowControls ? (
|
||||
<WindowControls
|
||||
titleBar={this.props.titleBar}
|
||||
theme={config.windowTheme}
|
||||
handleTitleBarChange={this.handleTitleBarChange}
|
||||
/>
|
||||
) : null}
|
||||
<CodeMirror
|
||||
className={`CodeMirror__container window-theme__${config.windowTheme}`}
|
||||
onBeforeChange={(editor, meta, code) => this.codeUpdated(code)}
|
||||
value={this.props.children}
|
||||
options={options}
|
||||
/>
|
||||
{config.watermark && <Watermark />}
|
||||
<div id="container-bg">
|
||||
<div className="white eliminateOnRender" />
|
||||
<div className="alpha eliminateOnRender" />
|
||||
<div className="bg" />
|
||||
</div>
|
||||
<style jsx>
|
||||
{`
|
||||
#container {
|
||||
position: relative;
|
||||
min-width: ${config.widthAdjustment ? '90px' : '680px'};
|
||||
max-width: 1024px; /* The Fallback */
|
||||
max-width: 92vw;
|
||||
padding: ${config.paddingVertical} ${config.paddingHorizontal};
|
||||
}
|
||||
|
||||
#container :global(.watermark) {
|
||||
fill-opacity: 0.3;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
bottom: calc(${config.paddingVertical} + 16px);
|
||||
right: calc(${config.paddingHorizontal} + 16px);
|
||||
}
|
||||
|
||||
#container #container-bg {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
#container .white {
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
#container .bg {
|
||||
${this.props.config.backgroundMode === 'image'
|
||||
? `background: url(${backgroundImage});
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;`
|
||||
: `background: ${this.props.config.backgroundColor || config.backgroundColor};
|
||||
background-size: auto;
|
||||
background-repeat: repeat;`} position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
#container .alpha {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==);
|
||||
}
|
||||
|
||||
#container :global(.cm-s-dracula .CodeMirror-cursor) {
|
||||
border-left: solid 2px #159588;
|
||||
}
|
||||
|
||||
#container :global(.cm-s-solarized) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#container :global(.cm-s-solarized.cm-s-light) {
|
||||
text-shadow: #eee8d5 0 1px;
|
||||
}
|
||||
|
||||
#container :global(.CodeMirror-gutters) {
|
||||
background-color: unset;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
#container :global(.CodeMirror__container) {
|
||||
min-width: inherit;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-radius: 5px;
|
||||
${config.dropShadow
|
||||
? `box-shadow: 0 ${config.dropShadowOffsetY} ${
|
||||
config.dropShadowBlurRadius
|
||||
} rgba(0, 0, 0, 0.55)`
|
||||
: ''};
|
||||
}
|
||||
|
||||
#container :global(.CodeMirror__container .CodeMirror) {
|
||||
height: auto;
|
||||
min-width: inherit;
|
||||
padding: 18px 18px;
|
||||
${config.lineNumbers ? 'padding-left: 12px;' : ''} border-radius: 5px;
|
||||
font-family: ${config.fontFamily}, monospace !important;
|
||||
font-size: ${config.fontSize};
|
||||
font-variant-ligatures: contextual;
|
||||
font-feature-settings: 'calt' 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#container :global(.CodeMirror-scroll) {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
#container :global(.window-theme__sharp > .CodeMirror) {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
#container :global(.window-theme__bw > .CodeMirror) {
|
||||
border: 2px solid ${COLORS.SECONDARY};
|
||||
}
|
||||
|
||||
#container :global(.window-controls + .CodeMirror__container > .CodeMirror) {
|
||||
padding-top: 48px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="section">
|
||||
<div id="export-container" ref={ele => (this.exportContainerNode = ele)}>
|
||||
{content}
|
||||
<div id="twitter-png-fix" />
|
||||
<Provider inject={this.inject}>
|
||||
<div id="carbon">
|
||||
<Toolbar />
|
||||
<Editor />
|
||||
</div>
|
||||
<style jsx>
|
||||
{`
|
||||
#section,
|
||||
#export-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* forces twitter to save images as png — https://github.com/dawnlabs/carbon/issues/86 */
|
||||
#twitter-png-fix {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.01);
|
||||
#carbon {
|
||||
background: ${COLORS.BLACK};
|
||||
border: 3px solid ${COLORS.SECONDARY};
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Carbon
|
||||
export default DragDropContext(HTML5Backend)(Carbon)
|
||||
|
@ -0,0 +1,280 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import * as hljs from 'highlight.js'
|
||||
import Spinner from 'react-spinner'
|
||||
import ResizeObserver from 'resize-observer-polyfill'
|
||||
import debounce from 'lodash.debounce'
|
||||
import ms from 'ms'
|
||||
|
||||
import WindowControls from '../components/WindowControls'
|
||||
import Watermark from '../components/svg/Watermark'
|
||||
import CodeMirror from '../lib/react-codemirror'
|
||||
import { COLORS, LANGUAGE_MODE_HASH, LANGUAGE_NAME_HASH, DEFAULT_SETTINGS } from '../lib/constants'
|
||||
|
||||
class CodeWindow extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
loading: true,
|
||||
language: props.config.language
|
||||
}
|
||||
|
||||
this.handleLanguageChange = this.handleLanguageChange.bind(this)
|
||||
this.handleTitleBarChange = this.handleTitleBarChange.bind(this)
|
||||
this.codeUpdated = this.codeUpdated.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
loading: false
|
||||
})
|
||||
|
||||
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) {
|
||||
// TODO use getDerivedStateFromProps() on React@16.3
|
||||
this.handleLanguageChange(newProps.children, { customProps: newProps })
|
||||
}
|
||||
|
||||
codeUpdated(newCode) {
|
||||
this.handleLanguageChange(newCode)
|
||||
this.props.updateCode(newCode)
|
||||
}
|
||||
|
||||
handleTitleBarChange(newTitle) {
|
||||
this.props.updateTitleBar(newTitle)
|
||||
}
|
||||
|
||||
handleLanguageChange = debounce(
|
||||
(newCode, config) => {
|
||||
const props = (config && config.customProps) || this.props
|
||||
|
||||
if (props.config.language === 'auto') {
|
||||
// try to set the language
|
||||
const detectedLanguage = hljs.highlightAuto(newCode).language
|
||||
const languageMode =
|
||||
LANGUAGE_MODE_HASH[detectedLanguage] || LANGUAGE_NAME_HASH[detectedLanguage]
|
||||
|
||||
if (languageMode) {
|
||||
this.setState({ language: languageMode.mime || languageMode.mode })
|
||||
}
|
||||
} else {
|
||||
this.setState({ language: props.config.language })
|
||||
}
|
||||
},
|
||||
ms('300ms'),
|
||||
{ trailing: true }
|
||||
)
|
||||
|
||||
render() {
|
||||
const config = { ...DEFAULT_SETTINGS, ...this.props.config }
|
||||
const options = {
|
||||
lineNumbers: config.lineNumbers,
|
||||
mode: this.state.language || 'plaintext',
|
||||
theme: config.theme,
|
||||
scrollBarStyle: null,
|
||||
viewportMargin: Infinity,
|
||||
lineWrapping: true,
|
||||
extraKeys: {
|
||||
'Shift-Tab': 'indentLess'
|
||||
}
|
||||
}
|
||||
const backgroundImage =
|
||||
(this.props.config.backgroundImage && this.props.config.backgroundImageSelection) ||
|
||||
this.props.config.backgroundImage
|
||||
|
||||
// set content to spinner if loading, else editor
|
||||
let content = (
|
||||
<div>
|
||||
<Spinner />
|
||||
<style jsx>
|
||||
{`
|
||||
div {
|
||||
height: 352px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
if (this.state.loading === false) {
|
||||
content = (
|
||||
<div id="container">
|
||||
{config.windowControls ? (
|
||||
<WindowControls
|
||||
titleBar={this.props.titleBar}
|
||||
theme={config.windowTheme}
|
||||
handleTitleBarChange={this.handleTitleBarChange}
|
||||
/>
|
||||
) : null}
|
||||
<CodeMirror
|
||||
className={`CodeMirror__container window-theme__${config.windowTheme}`}
|
||||
onBeforeChange={(editor, meta, code) => this.codeUpdated(code)}
|
||||
value={this.props.children}
|
||||
options={options}
|
||||
/>
|
||||
{config.watermark && <Watermark />}
|
||||
<div id="container-bg">
|
||||
<div className="white eliminateOnRender" />
|
||||
<div className="alpha eliminateOnRender" />
|
||||
<div className="bg" />
|
||||
</div>
|
||||
<style jsx>
|
||||
{`
|
||||
#container {
|
||||
position: relative;
|
||||
min-width: ${config.widthAdjustment ? '90px' : '680px'};
|
||||
max-width: 1024px; /* The Fallback */
|
||||
max-width: 92vw;
|
||||
padding: ${config.paddingVertical} ${config.paddingHorizontal};
|
||||
}
|
||||
|
||||
#container :global(.watermark) {
|
||||
fill-opacity: 0.3;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
bottom: calc(${config.paddingVertical} + 16px);
|
||||
right: calc(${config.paddingHorizontal} + 16px);
|
||||
}
|
||||
|
||||
#container #container-bg {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
#container .white {
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
#container .bg {
|
||||
${this.props.config.backgroundMode === 'image'
|
||||
? `background: url(${backgroundImage});
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;`
|
||||
: `background: ${this.props.config.backgroundColor || config.backgroundColor};
|
||||
background-size: auto;
|
||||
background-repeat: repeat;`} position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
#container .alpha {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==);
|
||||
}
|
||||
|
||||
#container :global(.cm-s-dracula .CodeMirror-cursor) {
|
||||
border-left: solid 2px #159588;
|
||||
}
|
||||
|
||||
#container :global(.cm-s-solarized) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#container :global(.cm-s-solarized.cm-s-light) {
|
||||
text-shadow: #eee8d5 0 1px;
|
||||
}
|
||||
|
||||
#container :global(.CodeMirror-gutters) {
|
||||
background-color: unset;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
#container :global(.CodeMirror__container) {
|
||||
min-width: inherit;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-radius: 5px;
|
||||
${config.dropShadow
|
||||
? `box-shadow: 0 ${config.dropShadowOffsetY} ${
|
||||
config.dropShadowBlurRadius
|
||||
} rgba(0, 0, 0, 0.55)`
|
||||
: ''};
|
||||
}
|
||||
|
||||
#container :global(.CodeMirror__container .CodeMirror) {
|
||||
height: auto;
|
||||
min-width: inherit;
|
||||
padding: 18px 18px;
|
||||
${config.lineNumbers ? 'padding-left: 12px;' : ''} border-radius: 5px;
|
||||
font-family: ${config.fontFamily}, monospace !important;
|
||||
font-size: ${config.fontSize};
|
||||
font-variant-ligatures: contextual;
|
||||
font-feature-settings: 'calt' 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#container :global(.CodeMirror-scroll) {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
#container :global(.window-theme__sharp > .CodeMirror) {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
#container :global(.window-theme__bw > .CodeMirror) {
|
||||
border: 2px solid ${COLORS.SECONDARY};
|
||||
}
|
||||
|
||||
#container :global(.window-controls + .CodeMirror__container > .CodeMirror) {
|
||||
padding-top: 48px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="section">
|
||||
{/* TODO use createRef */}
|
||||
<div id="export-container" ref={ele => (this.exportContainerNode = ele)}>
|
||||
{content}
|
||||
<div id="twitter-png-fix" />
|
||||
</div>
|
||||
<style jsx>
|
||||
{`
|
||||
#section,
|
||||
#export-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* forces twitter to save images as png — https://github.com/dawnlabs/carbon/issues/86 */
|
||||
#twitter-png-fix {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.01);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CodeWindow
|
@ -1,31 +1,105 @@
|
||||
import React from 'react'
|
||||
import { Subscribe } from 'unstated'
|
||||
|
||||
const Toolbar = props => (
|
||||
<div id="toolbar">
|
||||
{props.children}
|
||||
<style jsx>
|
||||
{`
|
||||
#toolbar {
|
||||
width: 100%;
|
||||
height: 40px; // TODO fix
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
import EditorContainer from '../containers/Editor'
|
||||
import Button from './Button'
|
||||
import Dropdown from './Dropdown'
|
||||
import BackgroundSelect from './BackgroundSelect'
|
||||
import Settings from './Settings'
|
||||
import {
|
||||
THEMES,
|
||||
THEMES_HASH,
|
||||
LANGUAGES,
|
||||
LANGUAGE_MIME_HASH,
|
||||
LANGUAGE_MODE_HASH,
|
||||
LANGUAGE_NAME_HASH,
|
||||
DEFAULT_THEME
|
||||
} from '../lib/constants'
|
||||
|
||||
#toolbar > :global(div) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
const editorContainer = [EditorContainer]
|
||||
const saveButtonOptions = {
|
||||
button: true,
|
||||
color: '#c198fb',
|
||||
selected: { id: 'SAVE_IMAGE', name: 'Save Image' },
|
||||
list: ['png', 'svg'].map(id => ({ id, name: id.toUpperCase() }))
|
||||
}
|
||||
|
||||
function Toolbar() {
|
||||
return <Subscribe to={editorContainer}>{render}</Subscribe>
|
||||
}
|
||||
|
||||
#toolbar > :global(div):last-child {
|
||||
margin-right: 0px;
|
||||
function render(editor) {
|
||||
return (
|
||||
<div id="toolbar">
|
||||
<Dropdown
|
||||
selected={THEMES_HASH[editor.state.theme] || DEFAULT_THEME}
|
||||
list={THEMES}
|
||||
onChange={editor.updateTheme}
|
||||
/>
|
||||
<Dropdown
|
||||
selected={
|
||||
LANGUAGE_NAME_HASH[editor.state.language] ||
|
||||
LANGUAGE_MIME_HASH[editor.state.language] ||
|
||||
LANGUAGE_MODE_HASH[editor.state.language] ||
|
||||
'auto'
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
list={LANGUAGES}
|
||||
onChange={editor.updateLanguage}
|
||||
/>
|
||||
<BackgroundSelect
|
||||
onChange={editor.updateBackground}
|
||||
mode={editor.state.backgroundMode}
|
||||
color={editor.state.backgroundColor}
|
||||
image={editor.state.backgroundImage}
|
||||
aspectRatio={editor.state.aspectRatio}
|
||||
/>
|
||||
<Settings
|
||||
{...editor.state}
|
||||
onChange={editor.updateSetting}
|
||||
resetDefaultSettings={editor.resetDefaultSettings}
|
||||
/>
|
||||
<div className="buttons">
|
||||
{/* TODO don't set container function if no prop */}
|
||||
{editor.upload && (
|
||||
<Button
|
||||
className="tweetButton"
|
||||
onClick={editor.upload}
|
||||
title={editor.state.uploading ? 'Loading...' : 'Tweet Image'}
|
||||
color="#57b5f9"
|
||||
style={{ marginRight: '8px' }}
|
||||
/>
|
||||
)}
|
||||
<Dropdown {...saveButtonOptions} onChange={editor.save} />
|
||||
</div>
|
||||
<style jsx>
|
||||
{`
|
||||
#toolbar {
|
||||
width: 100%;
|
||||
height: 40px; // TODO fix
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#toolbar > :global(div) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#toolbar > :global(div):last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Toolbar
|
||||
|
@ -0,0 +1,168 @@
|
||||
// Theirs
|
||||
import { Container } from 'unstated'
|
||||
import domtoimage from 'dom-to-image'
|
||||
|
||||
// Ours
|
||||
import api from '../lib/api'
|
||||
import {
|
||||
DEFAULT_EXPORT_SIZE,
|
||||
EXPORT_SIZES_HASH,
|
||||
DEFAULT_CODE,
|
||||
DEFAULT_SETTINGS
|
||||
} from '../lib/constants'
|
||||
import { serializeState } from '../lib/routing'
|
||||
import { getState, isImage } from '../lib/util'
|
||||
|
||||
class EditorContainer extends Container {
|
||||
constructor(props = {}) {
|
||||
super(props)
|
||||
this.state = {
|
||||
...DEFAULT_SETTINGS,
|
||||
uploading: false,
|
||||
code: props.content
|
||||
}
|
||||
|
||||
this.save = this.save.bind(this)
|
||||
this.upload = this.upload.bind(this)
|
||||
this.updateSetting = this.updateSetting.bind(this)
|
||||
this.updateCode = this.updateSetting.bind(this, 'code')
|
||||
this.updateAspectRatio = this.updateSetting.bind(this, 'aspectRatio')
|
||||
this.updateTitleBar = this.updateSetting.bind(this, 'titleBar')
|
||||
this.updateTheme = this.updateTheme.bind(this)
|
||||
this.updateLanguage = this.updateLanguage.bind(this)
|
||||
this.updateBackground = this.updateBackground.bind(this)
|
||||
this.resetDefaultSettings = this.resetDefaultSettings.bind(this)
|
||||
this.getCarbonImage = this.getCarbonImage.bind(this)
|
||||
this.handleDroppedFile = this.handleDroppedFile.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Load from localStorage and then URL params
|
||||
this.setState({
|
||||
...getState(localStorage),
|
||||
...this.props.initialState
|
||||
})
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.props.onUpdate(this.state)
|
||||
}
|
||||
|
||||
getCarbonImage({ format } = { format: 'png' }) {
|
||||
// if safari, get image from api
|
||||
if (
|
||||
navigator.userAgent.indexOf('Safari') !== -1 &&
|
||||
navigator.userAgent.indexOf('Chrome') === -1 &&
|
||||
format === 'png'
|
||||
) {
|
||||
const encodedState = serializeState(this.state)
|
||||
return api.image(encodedState)
|
||||
}
|
||||
|
||||
const node = document.getElementById('export-container')
|
||||
|
||||
const exportSize = (EXPORT_SIZES_HASH[this.state.exportSize] || DEFAULT_EXPORT_SIZE).value
|
||||
const width = node.offsetWidth * exportSize
|
||||
const height = this.state.squaredImage
|
||||
? node.offsetWidth * exportSize
|
||||
: node.offsetHeight * exportSize
|
||||
|
||||
const config = {
|
||||
style: {
|
||||
transform: `scale(${exportSize})`,
|
||||
'transform-origin': 'center',
|
||||
background: this.state.squaredImage ? this.state.backgroundColor : 'none'
|
||||
},
|
||||
filter: n => {
|
||||
// %[00 -> 19] cause failures
|
||||
if (n.innerText && n.innerText.match(/%[0-1][0-9]/)) {
|
||||
return false
|
||||
}
|
||||
if (n.className) {
|
||||
return String(n.className).indexOf('eliminateOnRender') < 0
|
||||
}
|
||||
return true
|
||||
},
|
||||
width,
|
||||
height
|
||||
}
|
||||
|
||||
if (format === 'svg') {
|
||||
return domtoimage
|
||||
.toSvg(node, config)
|
||||
.then(dataUrl => dataUrl.split(' ').join(' '))
|
||||
.then(uri => uri.slice(uri.indexOf(',') + 1))
|
||||
.then(data => new Blob([data], { type: 'image/svg+xml' }))
|
||||
.then(data => window.URL.createObjectURL(data))
|
||||
}
|
||||
|
||||
return domtoimage.toBlob(node, config).then(blob => window.URL.createObjectURL(blob))
|
||||
}
|
||||
|
||||
updateSetting(key, value) {
|
||||
this.setState({ [key]: value })
|
||||
}
|
||||
|
||||
save({ id: format = 'png' }) {
|
||||
const link = document.createElement('a')
|
||||
|
||||
return this.getCarbonImage({ format }).then(url => {
|
||||
link.download = `carbon.${format}`
|
||||
link.href = url
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
})
|
||||
}
|
||||
|
||||
resetDefaultSettings() {
|
||||
this.setState(DEFAULT_SETTINGS)
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
upload() {
|
||||
this.setState({ uploading: true })
|
||||
this.getCarbonImage({ format: 'png' })
|
||||
.then(this.props.tweet)
|
||||
// eslint-disable-next-line
|
||||
.catch(console.error)
|
||||
.then(() => this.setState({ uploading: false }))
|
||||
}
|
||||
|
||||
handleDroppedFile([file]) {
|
||||
if (isImage(file)) {
|
||||
this.setState({
|
||||
backgroundImage: file.content,
|
||||
backgroundImageSelection: null,
|
||||
backgroundMode: 'image'
|
||||
})
|
||||
} else {
|
||||
this.setState({ code: file.content, language: 'auto' })
|
||||
}
|
||||
}
|
||||
|
||||
updateTheme(theme) {
|
||||
this.updateSetting('theme', theme.id)
|
||||
}
|
||||
|
||||
updateLanguage(language) {
|
||||
this.updateSetting('language', language.mime || language.mode)
|
||||
}
|
||||
|
||||
updateBackground({ photographer, ...changes } = {}) {
|
||||
if (photographer) {
|
||||
this.setState(({ code = DEFAULT_CODE }) => ({
|
||||
...changes,
|
||||
code: code + `\n\n// Photo by ${photographer.name} on Unsplash`
|
||||
}))
|
||||
} else {
|
||||
this.setState(changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorContainer.defaultProps = {
|
||||
onUpdate: () => {}
|
||||
}
|
||||
|
||||
export default EditorContainer
|
@ -1,4 +1,5 @@
|
||||
/* eslint-disable */
|
||||
import CodeMirror from 'codemirror'
|
||||
|
||||
// Require Codemirror elixir mode from npm modules and register it here
|
||||
import registerElixirMode from 'codemirror-mode-elixir'
|
||||
export * from 'codemirror-mode-elixir'
|
||||
|
@ -1,2 +1,3 @@
|
||||
/* eslint-disable */
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror-graphql/mode'
|
||||
|
Loading…
Reference in New Issue