// Theirs import url from 'url' import React from 'react' import HTML5Backend from 'react-dnd-html5-backend' import { DragDropContext } from 'react-dnd' import domtoimage from 'dom-to-image' import ReadFileDropContainer, { DATA_URL, TEXT } from 'dropperx' import Spinner from 'react-spinner' import shallowCompare from 'react-addons-shallow-compare' import omit from 'lodash.omit' // Ours import Button from './Button' import Dropdown from './Dropdown' import BackgroundSelect from './BackgroundSelect' import Settings from './Settings' import Toolbar from './Toolbar' import Overlay from './Overlay' import Carbon from './Carbon' import ExportButton from './ExportButton' import { THEMES, THEMES_HASH, LANGUAGES, LANGUAGE_MIME_HASH, LANGUAGE_MODE_HASH, LANGUAGE_NAME_HASH, DEFAULT_THEME, DEFAULT_EXPORT_SIZE, COLORS, EXPORT_SIZES_HASH, DEFAULT_CODE, DEFAULT_SETTINGS, DEFAULT_LANGUAGE } from '../lib/constants' 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 => } class Editor extends React.Component { constructor(props) { super(props) this.state = { ...DEFAULT_SETTINGS, loading: true, uploading: false, code: props.content, online: true } this.export = this.export.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.onDrop = this.onDrop.bind(this) this.setOffline = () => this.setState({ online: false }) this.setOnline = () => this.setState({ online: true }) this.innerRef = node => (this.carbonNode = node) } async componentDidMount() { const { asPath = '' } = this.props.router const { query, pathname } = url.parse(asPath, true) const path = escapeHtml(pathname.split('/').pop()) const queryParams = getQueryStringState(query) const initialState = Object.keys(queryParams).length ? queryParams : {} try { // TODO fix this hack if (this.props.api.getGist && path.length >= 19 && path.indexOf('.') === -1) { const { content, language } = await this.props.api.getGist(path) if (language) { initialState.language = language.toLowerCase() } initialState.code = content } } catch (e) { // eslint-disable-next-line console.log(e) } const newState = { // Load from localStorage ...getState(localStorage), // and then URL params ...initialState, loading: false, online: Boolean(window && window.navigator && window.navigator.onLine) } // Makes sure the slash in 'application/X' is decoded if (newState.language) { newState.language = unescapeHtml(newState.language) } this.setState(newState) window.addEventListener('offline', this.setOffline) window.addEventListener('online', this.setOnline) } componentWillUnmount() { window.removeEventListener('offline', this.setOffline) window.removeEventListener('online', this.setOnline) } componentDidUpdate(prevProps, prevState) { // this.props ensures props are not compared, only state if (shallowCompare(this, this.props, prevState)) { this.props.onUpdate(this.state) } } getCarbonImage({ format, type } = { format: 'png' }) { // if safari, get image from api const isPNG = format !== 'svg' if ( this.props.api.image && navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1 && isPNG ) { const encodedState = serializeState(this.state) return this.props.api.image(encodedState) } const node = this.carbonNode const exportSize = (EXPORT_SIZES_HASH[this.state.exportSize] || DEFAULT_EXPORT_SIZE).value let width = node.offsetWidth * exportSize let height = this.state.squaredImage ? node.offsetWidth * exportSize : node.offsetHeight * exportSize if (isPNG) { const newNode = node.cloneNode(true) newNode.querySelectorAll('.CodeMirror-line span[role="presentation"]').forEach(encodeTextNode) newNode.style.visibility = 'hidden' document.body.appendChild(newNode) width = newNode.offsetWidth * exportSize height = this.state.squaredImage ? width : newNode.offsetHeight * exportSize newNode.remove() } const config = { style: { transform: `scale(${exportSize})`, 'transform-origin': 'center', background: this.state.squaredImage ? this.state.backgroundColor : 'none' }, filter: n => { if (n.className) { return String(n.className).indexOf('eliminateOnRender') < 0 } if ( isPNG && // only occurs when saving PNG n.matches && n.matches('span[role="presentation"]') ) { encodeTextNode(n) } return true }, width, height } if (type === 'blob') { if (format === 'svg') { return domtoimage .toSvg(node, config) .then(dataUrl => dataUrl.replace(/ /g, ' ')) .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)) } // Twitter needs regular dataurls return domtoimage.toPng(node, config) } updateSetting(key, value) { this.setState({ [key]: value }) } export({ id: format = 'png' }) { if (format === 'copy embed') { return } 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 ↗') { link.download = `${prefix}${timestamp}.${format}` } link.href = url document.body.appendChild(link) link.click() link.remove() }) } resetDefaultSettings() { this.setState(DEFAULT_SETTINGS) this.props.onReset() } upload() { this.setState({ uploading: true }) this.getCarbonImage({ format: 'png' }) .then(this.props.api.tweet.bind(null, this.state.code || DEFAULT_CODE)) // eslint-disable-next-line .catch(console.error) .then(() => this.setState({ uploading: false })) } onDrop([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) } } render() { if (this.state.loading) { return (
) } const config = omit(this.state, ['code', 'aspectRatio']) return (
{this.props.api.tweet && this.state.online && (
{({ isOver, canDrop }) => ( {/*key ensures Carbon's internal language state is updated when it's changed by Dropdown*/} {this.state.code != null ? this.state.code : DEFAULT_CODE} )}
) } } function encodeTextNode(node) { if (node.innerText && node.innerText.match(/%\S\S/)) { node.innerText = encodeURIComponent(node.innerText) } } function formatTimestamp() { const timezoneOffset = new Date().getTimezoneOffset() * 60000 const timeString = new Date(Date.now() - timezoneOffset) .toISOString() .slice(0, 19) .replace(/:/g, '-') .replace('T', '_') return timeString } function isImage(file) { return file.type.split('/')[0] === 'image' } function readAs(file) { if (isImage(file)) { return DATA_URL } return TEXT } Editor.defaultProps = { api: {}, onUpdate: () => {}, onReset: () => {} } export default DragDropContext(HTML5Backend)(Editor)