import React from 'react' import ReactDOM from 'react-dom' import dynamic from 'next/dynamic' import hljs from 'highlight.js/lib/core' import javascript from 'highlight.js/lib/languages/javascript' import debounce from 'lodash.debounce' import ms from 'ms' import { Controlled as CodeMirror } from 'react-codemirror2' hljs.registerLanguage('javascript', javascript) import { Spinner } from './Spinner' import WindowControls from './WindowControls' import WidthHandler from './WidthHandler' 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] } function noop() {} function getUnderline(underline) { switch (underline) { case 1: return 'underline' case 2: /** * Chrome will only round to the nearest wave, causing visual inconsistencies * https://stackoverflow.com/questions/57559588/how-to-make-the-wavy-underline-extend-cover-all-the-characters-in-chrome */ return `${COLORS.RED} wavy underline; text-decoration-skip-ink: none` } return 'initial' } class Carbon extends React.PureComponent { static defaultProps = { onChange: noop, onGutterClick: noop, } 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) } } onSelection = (ed, data) => { if (this.props.readOnly) { return } 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, } } } onMouseUp = () => { if (this.currentSelection) { this.setState({ selectionAt: this.currentSelection }, () => { this.currentSelection = null }) } else { this.setState({ selectionAt: null }) } } onSelectionChange = changes => { if (this.state.selectionAt) { const css = [ changes.bold != null && `font-weight: ${changes.bold ? 'bold' : 'initial'}`, changes.italics != null && `font-style: ${changes.italics ? 'italic' : 'initial'}`, changes.underline != null && `text-decoration: ${getUnderline(changes.underline)}`, changes.color != null && `color: ${changes.color} !important`, ] .filter(Boolean) .join('; ') if (css) { this.props.editorRef.current.editor.doc.markText( this.state.selectionAt.from, this.state.selectionAt.to, { css } ) } } } render() { const config = { ...DEFAULT_SETTINGS, ...this.props.config } const languageMode = this.handleLanguageChange( this.props.children, config.language && config.language.toLowerCase() ) const options = { screenReaderLabel: 'Code editor', lineNumbers: config.lineNumbers, firstLineNumber: config.firstLineNumber, mode: languageMode || 'plaintext', theme: config.theme, scrollbarStyle: null, viewportMargin: Infinity, lineWrapping: true, smartIndent: true, extraKeys: { 'Shift-Tab': 'indentLess', }, readOnly: this.props.readOnly, showInvisibles: config.hiddenCharacters, autoCloseBrackets: true, } 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 selectionNode = !this.props.readOnly && !!this.state.selectionAt && document.getElementById('style-editor-button') return (