import React from 'react' import dynamic from 'next/dynamic' import * as hljs from 'highlight.js' import debounce from 'lodash.debounce' import ms from 'ms' import { Controlled as CodeMirror } from 'react-codemirror2' import SpinnerWrapper from './SpinnerWrapper' import WindowControls from './WindowControls' import { COLORS, LANGUAGES, LANGUAGE_MODE_HASH, LANGUAGE_NAME_HASH, LANGUAGE_MIME_HASH, DEFAULT_SETTINGS, THEMES_HASH } from '../lib/constants' const SelectionEditor = dynamic(() => import('./SelectionEditor'), { loading: () => null }) const Watermark = dynamic(() => import('./svg/Watermark'), { loading: () => null }) function searchLanguage(l) { return LANGUAGE_NAME_HASH[l] || LANGUAGE_MODE_HASH[l] || LANGUAGE_MIME_HASH[l] } class Carbon extends React.PureComponent { static defaultProps = { onChange: () => {} } state = {} handleLanguageChange = debounce( (newCode, language) => { if (language === 'auto') { // try to set the language const detectedLanguage = hljs.highlightAuto(newCode).language const languageMode = searchLanguage(detectedLanguage) if (languageMode) { return languageMode.mime || languageMode.mode } } const languageMode = searchLanguage(language) if (languageMode) { return languageMode.mime || languageMode.mode } return language }, ms('300ms'), { leading: true, trailing: true } ) onBeforeChange = (editor, meta, code) => { if (!this.props.readOnly) { this.props.onChange(code) } } render() { const config = { ...DEFAULT_SETTINGS, ...this.props.config } const languageMode = this.handleLanguageChange( this.props.children, config.language && config.language.toLowerCase() ) const options = { lineNumbers: config.lineNumbers, mode: languageMode || 'plaintext', theme: config.theme, scrollBarStyle: null, viewportMargin: Infinity, lineWrapping: true, smartIndent: true, extraKeys: { 'Shift-Tab': 'indentLess' }, readOnly: this.props.readOnly ? 'nocursor' : false, // needs to be able to refresh every 16ms to hit 60 frames / second pollInterval: 16 } const backgroundImage = (this.props.config.backgroundImage && this.props.config.backgroundImageSelection) || this.props.config.backgroundImage const themeConfig = this.props.theme || THEMES_HASH[config.theme] const light = themeConfig && themeConfig.light /* eslint-disable jsx-a11y/no-static-element-interactions */ const content = (
{ if (this.currentSelection) { const { editor } = this.props.editorRef.current const startPos = editor.charCoords(this.currentSelection.from, 'local') const endPos = editor.charCoords(this.currentSelection.to, 'local') const startWindowPos = editor.charCoords(this.currentSelection.from, 'window') const endWindowPos = editor.charCoords(this.currentSelection.to, 'window') const top = Math.max(startWindowPos.bottom, endWindowPos.bottom) - this.props.innerRef.current.getBoundingClientRect().top - 3 const left = (startPos.left + endPos.right) / 2 this.setState({ modifierOpenAt: { top, left } }) // this.currentSelection = null } else { this.setState({ modifierOpenAt: null }) } }} > {config.windowControls ? ( ) : null} { const selection = data.ranges[0] if ( selection.head.line === selection.anchor.line && selection.head.ch === selection.anchor.ch ) { return (this.currentSelection = null) } if ( selection.head.line + selection.head.ch > selection.anchor.line + selection.anchor.ch ) { this.currentSelection = { from: selection.anchor, to: selection.head } } else { this.currentSelection = { from: selection.head, to: selection.anchor } } }} /> {config.watermark && }
) return (
{content}
{this.state.modifierOpenAt && ( { if (this.currentSelection) { const css = [ `font-weight: ${changes.bold ? 'bold' : 'initial'}`, `font-style: ${changes.italics ? 'italic' : 'initial'}`, `text-decoration: ${changes.underline ? 'underline' : 'initial'}`, changes.color && `color: ${changes.color} !important`, '' ] .filter(Boolean) .join('; ') this.props.editorRef.current.editor.doc.markText( this.currentSelection.from, this.currentSelection.to, { css } ) } }} /> )}
) } } const modesLoaded = new Set() function useModeLoader() { React.useEffect(() => { LANGUAGES.filter(language => language.mode !== 'auto' && language.mode !== 'text').forEach( language => { if (language.mode && !modesLoaded.has(language.mode)) { language.custom ? require(`../lib/custom/modes/${language.mode}`) : require(`codemirror/mode/${language.mode}/${language.mode}`) modesLoaded.add(language.mode) } } ) }, []) } function selectedLinesReducer({ prevLine, selected }, { type, lineNumber, numLines }) { const newState = {} if (type === 'GROUP' && prevLine) { for (let i = Math.min(prevLine, lineNumber); i < Math.max(prevLine, lineNumber) + 1; i++) { newState[i] = selected[prevLine] } } else { for (let i = 0; i < numLines; i++) { if (i != lineNumber) { if (prevLine == null) { newState[i] = false } } else { newState[lineNumber] = selected[lineNumber] === true ? false : true } } } return { selected: { ...selected, ...newState }, prevLine: lineNumber } } function useGutterClickHandler(props) { const editorRef = React.useRef(null) const [state, dispatch] = React.useReducer(selectedLinesReducer, { prevLine: null, selected: {} }) React.useEffect(() => { if (editorRef.current) { editorRef.current.display.view.forEach((line, i) => { if (line.text && line.gutter) { line.text.style.opacity = state.selected[i] === false ? 0.5 : 1 line.gutter.style.opacity = state.selected[i] === false ? 0.5 : 1 } }) } }, [state.selected, props.children, props.config]) return React.useCallback(function onGutterClick(editor, lineNumber, gutter, e) { editorRef.current = editor const numLines = editor.display.view.length if (e.shiftKey) { dispatch({ type: 'GROUP', lineNumber, numLines }) } else { dispatch({ type: 'LINE', lineNumber, numLines }) } }, []) } function CarbonContainer(props, ref) { useModeLoader() const onGutterClick = useGutterClickHandler(props) return } export default React.forwardRef(CarbonContainer)