Performance improvements (#287)

* Perf improvements. Limit rerender on keystrokes

* Spread settings props, make dropdowns and carbon pure

* Remove last TODO

* Pass state into value of titleBar input

* Clean up
main
Michael Fix 7 years ago committed by Jake Dexheimer
parent 7f40e9b469
commit a6324586eb

@ -7,9 +7,9 @@ import { COLORS, DEFAULT_BG_COLOR } from '../lib/constants'
import { validateColor } from '../lib/colors' import { validateColor } from '../lib/colors'
import { parseRGBA, capitalizeFirstLetter } from '../lib/util' import { parseRGBA, capitalizeFirstLetter } from '../lib/util'
class BackgroundSelect extends React.Component { class BackgroundSelect extends React.PureComponent {
constructor() { constructor(props) {
super() super(props)
this.state = { isVisible: false, selectedTab: 'color', mounted: false } this.state = { isVisible: false, selectedTab: 'color', mounted: false }
this.toggle = this.toggle.bind(this) this.toggle = this.toggle.bind(this)
this.selectTab = this.selectTab.bind(this) this.selectTab = this.selectTab.bind(this)
@ -25,7 +25,7 @@ class BackgroundSelect extends React.Component {
} }
selectTab(name) { selectTab(name) {
if (this.props.config.backgroundMode !== name) { if (this.props.mode !== name) {
this.props.onChange({ backgroundMode: name }) this.props.onChange({ backgroundMode: name })
} }
} }
@ -39,7 +39,7 @@ class BackgroundSelect extends React.Component {
} }
render() { render() {
let background = this.props.config.backgroundColor || config.backgroundColor let background = this.props.color
background = background =
typeof background === 'string' typeof background === 'string'
? background ? background
@ -72,7 +72,7 @@ class BackgroundSelect extends React.Component {
{['color', 'image'].map((tab, i) => ( {['color', 'image'].map((tab, i) => (
<div <div
key={i} key={i}
className={`picker-tab ${this.props.config.backgroundMode === tab ? 'active' : ''}`} className={`picker-tab ${this.props.mode === tab ? 'active' : ''}`}
onClick={this.selectTab.bind(null, tab)} onClick={this.selectTab.bind(null, tab)}
> >
{capitalizeFirstLetter(tab)} {capitalizeFirstLetter(tab)}
@ -80,19 +80,16 @@ class BackgroundSelect extends React.Component {
))} ))}
</div> </div>
<div className="picker-tabs-contents"> <div className="picker-tabs-contents">
<div style={this.props.config.backgroundMode === 'color' ? {} : { display: 'none' }}> <div style={this.props.mode === 'color' ? {} : { display: 'none' }}>
{this.state.mounted && ( {this.state.mounted && (
<SketchPicker <SketchPicker color={this.props.color} onChangeComplete={this.handlePickColor} />
color={this.props.config.backgroundColor}
onChangeComplete={this.handlePickColor}
/>
)} )}
</div> </div>
<div style={this.props.config.backgroundMode === 'image' ? {} : { display: 'none' }}> <div style={this.props.mode === 'image' ? {} : { display: 'none' }}>
<ImagePicker <ImagePicker
onChange={this.props.onChange} onChange={this.props.onChange}
imageDataURL={this.props.config.backgroundImage} imageDataURL={this.props.image}
aspectRatio={this.props.config.aspectRatio} aspectRatio={this.props.aspectRatio}
/> />
</div> </div>
</div> </div>
@ -135,8 +132,8 @@ class BackgroundSelect extends React.Component {
right: 0px; right: 0px;
bottom: 0px; bottom: 0px;
left: 0px; left: 0px;
${this.props.config.backgroundMode === 'image' ${this.props.mode === 'image'
? `background: url(${this.props.config.backgroundImage}); ? `background: url(${this.props.image});
background-size: cover; background-size: cover;
background-repeat: no-repeat;` background-repeat: no-repeat;`
: `background: ${background};`}; : `background: ${background};`};

@ -1,6 +1,6 @@
import { EOL } from 'os' import { EOL } from 'os'
import * as hljs from 'highlight.js' import * as hljs from 'highlight.js'
import React from 'react' import React, { PureComponent } from 'react'
import Spinner from 'react-spinner' import Spinner from 'react-spinner'
import ResizeObserver from 'resize-observer-polyfill' import ResizeObserver from 'resize-observer-polyfill'
import toHash from 'tohash' import toHash from 'tohash'
@ -19,7 +19,7 @@ import {
DEFAULT_SETTINGS DEFAULT_SETTINGS
} from '../lib/constants' } from '../lib/constants'
class Carbon extends React.Component { class Carbon extends PureComponent {
constructor(props) { constructor(props) {
super(props) super(props)
@ -113,6 +113,7 @@ class Carbon extends React.Component {
<div id="container"> <div id="container">
{config.windowControls ? ( {config.windowControls ? (
<WindowControls <WindowControls
titleBar={this.props.titleBar}
theme={config.windowTheme} theme={config.windowTheme}
handleTitleBarChange={this.handleTitleBarChange} handleTitleBarChange={this.handleTitleBarChange}
/> />

@ -1,11 +1,11 @@
import React, { Component } from 'react' import React, { PureComponent } from 'react'
import Downshift from 'downshift' import Downshift from 'downshift'
import matchSorter from 'match-sorter' import matchSorter from 'match-sorter'
import ArrowDown from './svg/Arrowdown' import ArrowDown from './svg/Arrowdown'
import CheckMark from './svg/Checkmark' import CheckMark from './svg/Checkmark'
import { COLORS } from '../lib/constants' import { COLORS } from '../lib/constants'
class Dropdown extends Component { class Dropdown extends PureComponent {
state = { state = {
inputValue: this.props.selected.name, inputValue: this.props.selected.name,
itemsToShow: this.props.list itemsToShow: this.props.list

@ -5,7 +5,7 @@ import { EXPORT_SIZES } from '../lib/constants'
export default class extends React.Component { export default class extends React.Component {
constructor(props) { constructor(props) {
super() super(props)
this.state = { isVisible: false } this.state = { isVisible: false }
this.select = this.select.bind(this) this.select = this.select.bind(this)
this.toggle = this.toggle.bind(this) this.toggle = this.toggle.bind(this)

@ -4,7 +4,7 @@ import { COLORS, FONTS } from '../lib/constants'
export default class extends React.Component { export default class extends React.Component {
constructor(props) { constructor(props) {
super() super(props)
this.state = { isVisible: false } this.state = { isVisible: false }
this.select = this.select.bind(this) this.select = this.select.bind(this)
this.toggle = this.toggle.bind(this) this.toggle = this.toggle.bind(this)

@ -2,7 +2,7 @@ import React from 'react'
import Link from 'next/link' import Link from 'next/link'
import { COLORS } from '../lib/constants' import { COLORS } from '../lib/constants'
const Footer = props => ( const Footer = () => (
<footer role="contentinfo" className="mt3"> <footer role="contentinfo" className="mt3">
<nav role="navigation" className="mt3"> <nav role="navigation" className="mt3">
<Link href="/about"> <Link href="/about">

@ -36,8 +36,8 @@ const getCroppedImg = (imageDataURL, pixelCrop) => {
const INITIAL_STATE = { crop: null, imageAspectRatio: null, pixelCrop: null } const INITIAL_STATE = { crop: null, imageAspectRatio: null, pixelCrop: null }
export default class extends React.Component { export default class extends React.Component {
constructor() { constructor(props) {
super() super(props)
this.state = INITIAL_STATE this.state = INITIAL_STATE
this.selectImage = this.selectImage.bind(this) this.selectImage = this.selectImage.bind(this)
this.removeImage = this.removeImage.bind(this) this.removeImage = this.removeImage.bind(this)

@ -13,8 +13,8 @@ const largerImage = url => url.replace(/w=\d+/, 'w=1920').replace(/&h=\d+/, '')
const smallerImage = url => url.replace(/w=\d+/, 'w=240') const smallerImage = url => url.replace(/w=\d+/, 'w=240')
export default class extends React.Component { export default class extends React.Component {
constructor() { constructor(props) {
super() super(props)
this.state = { cacheIndex: 0, loading: false } this.state = { cacheIndex: 0, loading: false }
this.selectImage = this.selectImage.bind(this) this.selectImage = this.selectImage.bind(this)
this.updateCache = this.updateCache.bind(this) this.updateCache = this.updateCache.bind(this)

@ -12,7 +12,7 @@ import { COLORS } from '../lib/constants'
class Settings extends React.Component { class Settings extends React.Component {
constructor(props) { constructor(props) {
super() super(props)
this.state = { this.state = {
isVisible: false isVisible: false
} }
@ -39,74 +39,74 @@ class Settings extends React.Component {
<div className="settings-settings"> <div className="settings-settings">
<WindowPointer fromLeft="15px" /> <WindowPointer fromLeft="15px" />
<ThemeSelect <ThemeSelect
selected={this.props.enabled.windowTheme || 'none'} selected={this.props.windowTheme || 'none'}
onChange={this.props.onChange.bind(null, 'windowTheme')} onChange={this.props.onChange.bind(null, 'windowTheme')}
/> />
<FontSelect <FontSelect
selected={this.props.enabled.fontFamily || 'Hack'} selected={this.props.fontFamily || 'Hack'}
onChange={this.props.onChange.bind(null, 'fontFamily')} onChange={this.props.onChange.bind(null, 'fontFamily')}
/> />
<Slider <Slider
label="Font size" label="Font size"
value={this.props.enabled.fontSize || 13} value={this.props.fontSize || 13}
minValue={10} minValue={10}
maxValue={18} maxValue={18}
onChange={this.props.onChange.bind(null, 'fontSize')} onChange={this.props.onChange.bind(null, 'fontSize')}
/> />
<Toggle <Toggle
label="Window controls" label="Window controls"
enabled={this.props.enabled.windowControls} enabled={this.props.windowControls}
onChange={this.props.onChange.bind(null, 'windowControls')} onChange={this.props.onChange.bind(null, 'windowControls')}
/> />
<Toggle <Toggle
label="Line numbers" label="Line numbers"
enabled={this.props.enabled.lineNumbers} enabled={this.props.lineNumbers}
onChange={this.props.onChange.bind(null, 'lineNumbers')} onChange={this.props.onChange.bind(null, 'lineNumbers')}
/> />
<Toggle <Toggle
label="Auto-adjust width" label="Auto-adjust width"
enabled={this.props.enabled.widthAdjustment} enabled={this.props.widthAdjustment}
onChange={this.props.onChange.bind(null, 'widthAdjustment')} onChange={this.props.onChange.bind(null, 'widthAdjustment')}
/> />
<Collapse label="Advanced"> <Collapse label="Advanced">
<Slider <Slider
label="Padding (vertical)" label="Padding (vertical)"
value={this.props.enabled.paddingVertical || 16} value={this.props.paddingVertical || 16}
maxValue={200} maxValue={200}
onChange={this.props.onChange.bind(null, 'paddingVertical')} onChange={this.props.onChange.bind(null, 'paddingVertical')}
/> />
<Slider <Slider
label="Padding (horizontal)" label="Padding (horizontal)"
value={this.props.enabled.paddingHorizontal || 32} value={this.props.paddingHorizontal || 32}
onChange={this.props.onChange.bind(null, 'paddingHorizontal')} onChange={this.props.onChange.bind(null, 'paddingHorizontal')}
/> />
<Toggle <Toggle
label="Drop shadow" label="Drop shadow"
enabled={this.props.enabled.dropShadow} enabled={this.props.dropShadow}
onChange={this.props.onChange.bind(null, 'dropShadow')} onChange={this.props.onChange.bind(null, 'dropShadow')}
/> />
<Slider <Slider
label="Drop shadow (offset-y)" label="Drop shadow (offset-y)"
value={this.props.enabled.dropShadowOffsetY || 20} value={this.props.dropShadowOffsetY || 20}
onChange={this.props.onChange.bind(null, 'dropShadowOffsetY')} onChange={this.props.onChange.bind(null, 'dropShadowOffsetY')}
/> />
<Slider <Slider
label="Drop shadow (blur-radius)" label="Drop shadow (blur-radius)"
value={this.props.enabled.dropShadowBlurRadius || 68} value={this.props.dropShadowBlurRadius || 68}
onChange={this.props.onChange.bind(null, 'dropShadowBlurRadius')} onChange={this.props.onChange.bind(null, 'dropShadowBlurRadius')}
/> />
<Toggle <Toggle
label="Squared image" label="Squared image"
enabled={this.props.enabled.squaredImage} enabled={this.props.squaredImage}
onChange={this.props.onChange.bind(null, 'squaredImage')} onChange={this.props.onChange.bind(null, 'squaredImage')}
/> />
<Toggle <Toggle
label="Watermark" label="Watermark"
enabled={this.props.enabled.watermark} enabled={this.props.watermark}
onChange={this.props.onChange.bind(null, 'watermark')} onChange={this.props.onChange.bind(null, 'watermark')}
/> />
<ExportSizeSelect <ExportSizeSelect
selected={this.props.enabled.exportSize || '2x'} selected={this.props.exportSize || '2x'}
onChange={this.props.onChange.bind(null, 'exportSize')} onChange={this.props.onChange.bind(null, 'exportSize')}
/> />
<Toggle <Toggle

@ -2,7 +2,7 @@ import React from 'react'
export default class extends React.Component { export default class extends React.Component {
constructor(props) { constructor(props) {
super() super(props)
this.handleChange = this.handleChange.bind(this) this.handleChange = this.handleChange.bind(this)
} }

@ -7,7 +7,7 @@ export const WINDOW_THEMES = Object.keys(WINDOW_THEMES_MAP)
export default class extends React.Component { export default class extends React.Component {
constructor(props) { constructor(props) {
super() super(props)
this.select = this.select.bind(this) this.select = this.select.bind(this)
} }

@ -3,7 +3,7 @@ import Checkmark from './svg/Checkmark'
export default class extends React.Component { export default class extends React.Component {
constructor(props) { constructor(props) {
super() super(props)
this.toggle = this.toggle.bind(this) this.toggle = this.toggle.bind(this)
} }

@ -1,11 +1,16 @@
import React from 'react' import React from 'react'
import { Controls, ControlsBW } from './svg/Controls' import { Controls, ControlsBW } from './svg/Controls'
export default ({ theme, handleTitleBarChange }) => ( export default ({ titleBar, theme, handleTitleBarChange }) => (
<div className="window-controls"> <div className="window-controls">
{theme === 'bw' ? <ControlsBW /> : <Controls />} {theme === 'bw' ? <ControlsBW /> : <Controls />}
<div className="window-title-container"> <div className="window-title-container">
<input type="text" spellCheck="false" onChange={e => handleTitleBarChange(e.target.value)} /> <input
value={titleBar}
type="text"
spellCheck="false"
onChange={e => handleTitleBarChange(e.target.value)}
/>
</div> </div>
<style jsx> <style jsx>
{` {`

@ -86,12 +86,15 @@ export const getQueryStringState = query => {
} }
export const updateQueryString = state => { export const updateQueryString = state => {
let mappedState = mapper.map(reverseMappings, state) // If react_perf is set as a queryParm, don't update
serializeCode(mappedState) if (history.location.search.indexOf('react_perf') < 0) {
let mappedState = mapper.map(reverseMappings, state)
serializeCode(mappedState)
history.replace({ history.replace({
search: encodeURI(keysToQuery(mappedState)) search: encodeURI(keysToQuery(mappedState))
}) })
}
} }
// private // private

@ -36,12 +36,11 @@ import {
import { getQueryStringState, updateQueryString, serializeState } from '../lib/routing' import { getQueryStringState, updateQueryString, serializeState } from '../lib/routing'
import { getState, saveState } from '../lib/util' import { getState, saveState } from '../lib/util'
const removeQueryString = str => { const saveButtonOptions = {
const qI = str.indexOf('?') button: true,
return (qI >= 0 ? str.substr(0, qI) : str) color: '#c198fb',
.replace(/</g, '&lt;') selected: { id: 'SAVE_IMAGE', name: 'Save Image' },
.replace(/>/g, '&gt;') list: ['png', 'svg'].map(id => ({ id, name: id.toUpperCase() }))
.replace(/\//g, '&#x2F;')
} }
class Editor extends React.Component { class Editor extends React.Component {
@ -75,11 +74,16 @@ class Editor extends React.Component {
this.save = this.save.bind(this) this.save = this.save.bind(this)
this.upload = this.upload.bind(this) this.upload = this.upload.bind(this)
this.updateCode = this.updateCode.bind(this) this.updateSetting = this.updateSetting.bind(this)
this.updateTitleBar = this.updateTitleBar.bind(this) this.updateCode = this.updateSetting.bind(this, 'code')
this.updateAspectRatio = this.updateAspectRatio.bind(this) 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.resetDefaultSettings = this.resetDefaultSettings.bind(this)
this.getCarbonImage = this.getCarbonImage.bind(this) this.getCarbonImage = this.getCarbonImage.bind(this)
this.onDrop = this.onDrop.bind(this)
} }
componentDidMount() { componentDidMount() {
@ -133,19 +137,11 @@ class Editor extends React.Component {
: domtoimage.toPng(node, config) : domtoimage.toPng(node, config)
} }
updateCode(code) { updateSetting(key, value) {
this.setState({ code }) this.setState({ [key]: value })
}
updateAspectRatio(aspectRatio) {
this.setState({ aspectRatio })
}
updateTitleBar(titleBar) {
this.setState({ titleBar })
} }
save({ format } = { format: 'png' }) { save({ id: format = 'png' }) {
this.getCarbonImage({ format }).then(dataUrl => { this.getCarbonImage({ format }).then(dataUrl => {
if (format === 'svg') { if (format === 'svg') {
dataUrl = dataUrl.split('&nbsp;').join('&#160;') dataUrl = dataUrl.split('&nbsp;').join('&#160;')
@ -176,19 +172,39 @@ class Editor extends React.Component {
}) })
} }
isImage(file) { onDrop([file]) {
return file.type.split('/')[0] === 'image' 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(changes, cb) {
this.setState(changes, cb)
} }
render() { render() {
return ( return (
<Page enableHeroText> <Page enableHeroText={true}>
<div id="editor"> <div id="editor">
<Toolbar> <Toolbar>
<Dropdown <Dropdown
selected={THEMES_HASH[this.state.theme] || DEFAULT_THEME} selected={THEMES_HASH[this.state.theme] || DEFAULT_THEME}
list={THEMES} list={THEMES}
onChange={theme => this.setState({ theme: theme.id })} onChange={this.updateTheme}
/> />
<Dropdown <Dropdown
selected={ selected={
@ -197,12 +213,18 @@ class Editor extends React.Component {
LANGUAGE_MODE_HASH[this.state.language] LANGUAGE_MODE_HASH[this.state.language]
} }
list={LANGUAGES} list={LANGUAGES}
onChange={language => this.setState({ language: language.mime || language.mode })} onChange={this.updateLanguage}
/>
<BackgroundSelect
onChange={this.updateBackground}
mode={this.state.backgroundMode}
color={this.state.backgroundColor}
image={this.state.backgroundImage}
aspectRatio={this.state.aspectRatio}
/> />
<BackgroundSelect onChange={changes => this.setState(changes)} config={this.state} />
<Settings <Settings
onChange={(key, value) => this.setState({ [key]: value })} {...this.state}
enabled={this.state} onChange={this.updateSetting}
resetDefaultSettings={this.resetDefaultSettings} resetDefaultSettings={this.resetDefaultSettings}
/> />
<div className="buttons"> <div className="buttons">
@ -213,35 +235,11 @@ class Editor extends React.Component {
color="#57b5f9" color="#57b5f9"
style={{ marginRight: '8px' }} style={{ marginRight: '8px' }}
/> />
<Dropdown <Dropdown {...saveButtonOptions} onChange={this.save} />
button
color="#c198fb"
selected={{ id: 'SAVE_IMAGE', name: 'Save Image' }}
list={['png', 'svg'].map(id => ({ id, name: id.toUpperCase() }))}
onChange={saveAs => this.save({ format: saveAs.id })}
/>
</div> </div>
</Toolbar> </Toolbar>
<ReadFileDropContainer <ReadFileDropContainer readAs={readAs} onDrop={this.onDrop}>
readAs={file => {
if (this.isImage(file)) {
return DATA_URL
}
return TEXT
}}
onDrop={([file]) => {
if (this.isImage(file)) {
this.setState({
backgroundImage: file.content,
backgroundImageSelection: null,
backgroundMode: 'image'
})
} else {
this.setState({ code: file.content, language: 'auto' })
}
}}
>
{({ isOver, canDrop }) => ( {({ isOver, canDrop }) => (
<Overlay <Overlay
isOver={isOver || canDrop} isOver={isOver || canDrop}
@ -249,8 +247,9 @@ class Editor extends React.Component {
> >
<Carbon <Carbon
config={this.state} config={this.state}
updateCode={code => this.updateCode(code)} updateCode={this.updateCode}
onAspectRatioChange={this.updateAspectRatio} onAspectRatioChange={this.updateAspectRatio}
titleBar={this.state.titleBar}
updateTitleBar={this.updateTitleBar} updateTitleBar={this.updateTitleBar}
> >
{this.state.code != null ? this.state.code : DEFAULT_CODE} {this.state.code != null ? this.state.code : DEFAULT_CODE}
@ -277,4 +276,23 @@ class Editor extends React.Component {
} }
} }
function removeQueryString(str) {
const qI = str.indexOf('?')
return (qI >= 0 ? str.substr(0, qI) : str)
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\//g, '&#x2F;')
}
function isImage(file) {
return file.type.split('/')[0] === 'image'
}
function readAs(file) {
if (isImage(file)) {
return DATA_URL
}
return TEXT
}
export default DragDropContext(HTML5Backend)(Editor) export default DragDropContext(HTML5Backend)(Editor)

Loading…
Cancel
Save