import { debounce, editorEvents, editorOptions, getAceInstance } from "./editorOptions"; const ace = getAceInstance(); import { Ace, Range } from "ace-builds"; import { Split } from "ace-builds/src-noconflict/ext-split"; import * as PropTypes from "prop-types"; import * as React from "react"; const isEqual = require("lodash.isequal"); const get = require("lodash.get"); import { IAceEditor, IAceOptions, IAnnotation, ICommand, IEditorProps, IMarker } from "./types"; interface IAceEditorClass extends IAceEditor { [index: string]: any; $options?: any; } export interface ISplitEditorProps { [index: string]: any; name?: string; style?: object; /** For available modes see https://github.com/thlorenz/brace/tree/master/mode */ mode?: string; /** For available themes see https://github.com/thlorenz/brace/tree/master/theme */ theme?: string; height?: string; width?: string; className?: string; fontSize?: number | string; showGutter?: boolean; showPrintMargin?: boolean; highlightActiveLine?: boolean; focus?: boolean; splits: number; debounceChangePeriod?: number; cursorStart?: number; wrapEnabled?: boolean; readOnly?: boolean; minLines?: number; maxLines?: number; enableBasicAutocompletion?: boolean | string[]; enableLiveAutocompletion?: boolean | string[]; tabSize?: number; value?: string[]; defaultValue?: string[]; scrollMargin?: number[]; orientation?: string; onSelectionChange?: (value: any, event?: any) => void; onCursorChange?: (value: any, event?: any) => void; onInput?: (event?: any) => void; onLoad?: (editor: IEditorProps) => void; onBeforeLoad?: (ace: any) => void; onChange?: (value: string[], event?: any) => void; onSelection?: (selectedText: string, event?: any) => void; onCopy?: (value: string) => void; onPaste?: (value: string) => void; onFocus?: (value: Event) => void; onBlur?: (value: Event) => void; onScroll?: (editor: IEditorProps) => void; editorProps?: IEditorProps; setOptions?: IAceOptions; keyboardHandler?: string; commands?: ICommand[]; annotations?: IAnnotation[][]; markers?: IMarker[][]; } export default class SplitComponent extends React.Component { [index: string]: any; public static propTypes: PropTypes.ValidationMap = { className: PropTypes.string, debounceChangePeriod: PropTypes.number, defaultValue: PropTypes.arrayOf(PropTypes.string), focus: PropTypes.bool, fontSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), height: PropTypes.string, mode: PropTypes.string, name: PropTypes.string, onBlur: PropTypes.func, onChange: PropTypes.func, onCopy: PropTypes.func, onFocus: PropTypes.func, onInput: PropTypes.func, onLoad: PropTypes.func, onPaste: PropTypes.func, onScroll: PropTypes.func, orientation: PropTypes.string, showGutter: PropTypes.bool, splits: PropTypes.number, theme: PropTypes.string, value: PropTypes.arrayOf(PropTypes.string), width: PropTypes.string, onSelectionChange: PropTypes.func, onCursorChange: PropTypes.func, onBeforeLoad: PropTypes.func, minLines: PropTypes.number, maxLines: PropTypes.number, readOnly: PropTypes.bool, highlightActiveLine: PropTypes.bool, tabSize: PropTypes.number, showPrintMargin: PropTypes.bool, cursorStart: PropTypes.number, editorProps: PropTypes.object, setOptions: PropTypes.object, style: PropTypes.object, scrollMargin: PropTypes.array, annotations: PropTypes.array, markers: PropTypes.array, keyboardHandler: PropTypes.string, wrapEnabled: PropTypes.bool, enableBasicAutocompletion: PropTypes.oneOfType([ PropTypes.bool, PropTypes.array ]), enableLiveAutocompletion: PropTypes.oneOfType([ PropTypes.bool, PropTypes.array ]), commands: PropTypes.array }; public static defaultProps: Partial = { name: "ace-editor", focus: false, orientation: "beside", splits: 2, mode: "", theme: "", height: "500px", width: "500px", value: [], fontSize: 12, showGutter: true, onChange: null, onPaste: null, onLoad: null, onScroll: null, minLines: null, maxLines: null, readOnly: false, highlightActiveLine: true, showPrintMargin: true, tabSize: 4, cursorStart: 1, editorProps: {}, style: {}, scrollMargin: [0, 0, 0, 0], setOptions: {}, wrapEnabled: false, enableBasicAutocompletion: false, enableLiveAutocompletion: false }; public editor: IAceEditorClass; public refEditor: HTMLElement; public silent: boolean; public split: IAceEditorClass; public splitEditor: IAceEditorClass; public debounce: (fn: any, delay: number) => (...args: any) => void; constructor(props: ISplitEditorProps) { super(props); editorEvents.forEach(method => { this[method] = this[method].bind(this); }); this.debounce = debounce; } public isInShadow(node: HTMLElement): boolean { let parent = node && node.parentNode; while (parent) { if (parent.toString() === "[object ShadowRoot]") { return true; } parent = parent.parentNode; } return false; } public componentDidMount() { const { className, onBeforeLoad, mode, focus, theme, fontSize, value, defaultValue, cursorStart, showGutter, wrapEnabled, showPrintMargin, scrollMargin = [0, 0, 0, 0], keyboardHandler, onLoad, commands, annotations, markers, splits } = this.props; this.editor = ace.edit(this.refEditor) as IAceEditor; if (this.isInShadow(this.refEditor)) { this.editor.renderer.attachToShadowRoot(); } this.editor.setTheme(`ace/theme/${theme}`); if (onBeforeLoad) { onBeforeLoad(ace); } const editorProps = Object.keys(this.props.editorProps); const split = new Split( this.editor.container, `ace/theme/${theme}`, splits ); this.editor.env.split = split; this.splitEditor = split.getEditor(0); this.split = split; // in a split scenario we don't want a print margin for the entire application this.editor.setShowPrintMargin(false); this.editor.renderer.setShowGutter(false); // get a list of possible options to avoid 'misspelled option errors' const availableOptions = this.splitEditor.$options; if (this.props.debounceChangePeriod) { this.onChange = this.debounce( this.onChange, this.props.debounceChangePeriod ); } split.forEach((editor: IAceEditorClass, index: number) => { for (let i = 0; i < editorProps.length; i++) { editor[editorProps[i]] = this.props.editorProps[editorProps[i]]; } const defaultValueForEditor = get(defaultValue, index); const valueForEditor = get(value, index, ""); editor.session.setUndoManager(new ace.UndoManager()); editor.setTheme(`ace/theme/${theme}`); editor.renderer.setScrollMargin( scrollMargin[0], scrollMargin[1], scrollMargin[2], scrollMargin[3] ); editor.getSession().setMode(`ace/mode/${mode}`); editor.setFontSize(fontSize as any); editor.renderer.setShowGutter(showGutter); editor.getSession().setUseWrapMode(wrapEnabled); editor.setShowPrintMargin(showPrintMargin); editor.on("focus", this.onFocus); editor.on("blur", this.onBlur); editor.on("input" as any, this.onInput); editor.on("copy", this.onCopy as any); editor.on("paste", this.onPaste as any); editor.on("change", this.onChange); editor .getSession() .selection.on("changeSelection", this.onSelectionChange); editor.getSession().selection.on("changeCursor", this.onCursorChange); editor.session.on("changeScrollTop", this.onScroll); editor.setValue( defaultValueForEditor === undefined ? valueForEditor : defaultValueForEditor, cursorStart ); const newAnnotations = get(annotations, index, []); const newMarkers = get(markers, index, []); editor.getSession().setAnnotations(newAnnotations); if (newMarkers && newMarkers.length > 0) { this.handleMarkers(newMarkers, editor); } for (let i = 0; i < editorOptions.length; i++) { const option = editorOptions[i]; if (availableOptions.hasOwnProperty(option)) { editor.setOption(option as any, this.props[option]); } else if (this.props[option]) { console.warn( `ReaceAce: editor option ${option} was activated but not found. Did you need to import a related tool or did you possibly mispell the option?` ); } } this.handleOptions(this.props, editor); if (Array.isArray(commands)) { commands.forEach(command => { if (typeof command.exec === "string") { (editor.commands as any).bindKey(command.bindKey, command.exec); } else { (editor.commands as any).addCommand(command); } }); } if (keyboardHandler) { editor.setKeyboardHandler("ace/keyboard/" + keyboardHandler); } }); if (className) { this.refEditor.className += " " + className; } if (focus) { this.splitEditor.focus(); } const sp = this.editor.env.split; sp.setOrientation( this.props.orientation === "below" ? sp.BELOW : sp.BESIDE ); sp.resize(true); if (onLoad) { onLoad(sp); } } public componentDidUpdate(prevProps: ISplitEditorProps) { const oldProps = prevProps; const nextProps = this.props; const split = this.editor.env.split; if (nextProps.splits !== oldProps.splits) { split.setSplits(nextProps.splits); } if (nextProps.orientation !== oldProps.orientation) { split.setOrientation( nextProps.orientation === "below" ? split.BELOW : split.BESIDE ); } split.forEach((editor: IAceEditorClass, index: number) => { if (nextProps.mode !== oldProps.mode) { editor.getSession().setMode("ace/mode/" + nextProps.mode); } if (nextProps.keyboardHandler !== oldProps.keyboardHandler) { if (nextProps.keyboardHandler) { editor.setKeyboardHandler( "ace/keyboard/" + nextProps.keyboardHandler ); } else { editor.setKeyboardHandler(null); } } if (nextProps.fontSize !== oldProps.fontSize) { editor.setFontSize(nextProps.fontSize as any); } if (nextProps.wrapEnabled !== oldProps.wrapEnabled) { editor.getSession().setUseWrapMode(nextProps.wrapEnabled); } if (nextProps.showPrintMargin !== oldProps.showPrintMargin) { editor.setShowPrintMargin(nextProps.showPrintMargin); } if (nextProps.showGutter !== oldProps.showGutter) { editor.renderer.setShowGutter(nextProps.showGutter); } for (let i = 0; i < editorOptions.length; i++) { const option = editorOptions[i]; if (nextProps[option] !== oldProps[option]) { editor.setOption(option as any, nextProps[option]); } } if (!isEqual(nextProps.setOptions, oldProps.setOptions)) { this.handleOptions(nextProps, editor); } const nextValue = get(nextProps.value, index, ""); if (editor.getValue() !== nextValue) { // editor.setValue is a synchronous function call, change event is emitted before setValue return. this.silent = true; const pos = (editor.session.selection as any).toJSON(); editor.setValue(nextValue, nextProps.cursorStart); (editor.session.selection as any).fromJSON(pos); this.silent = false; } const newAnnotations = get(nextProps.annotations, index, []); const oldAnnotations = get(oldProps.annotations, index, []); if (!isEqual(newAnnotations, oldAnnotations)) { editor.getSession().setAnnotations(newAnnotations); } const newMarkers = get(nextProps.markers, index, []); const oldMarkers = get(oldProps.markers, index, []); if (!isEqual(newMarkers, oldMarkers) && Array.isArray(newMarkers)) { this.handleMarkers(newMarkers, editor); } }); if (nextProps.className !== oldProps.className) { const appliedClasses = this.refEditor.className; const appliedClassesArray = appliedClasses.trim().split(" "); const oldClassesArray = oldProps.className.trim().split(" "); oldClassesArray.forEach(oldClass => { const index = appliedClassesArray.indexOf(oldClass); appliedClassesArray.splice(index, 1); }); this.refEditor.className = " " + nextProps.className + " " + appliedClassesArray.join(" "); } if (nextProps.theme !== oldProps.theme) { split.setTheme("ace/theme/" + nextProps.theme); } if (nextProps.focus && !oldProps.focus) { this.splitEditor.focus(); } if ( nextProps.height !== this.props.height || nextProps.width !== this.props.width ) { this.editor.resize(); } } public componentWillUnmount() { this.editor.destroy(); this.editor = null; } public onChange(event: any) { if (this.props.onChange && !this.silent) { const value: any = []; this.editor.env.split.forEach((editor: IAceEditorClass) => { value.push(editor.getValue()); }); this.props.onChange(value, event); } } public onSelectionChange(event: any) { if (this.props.onSelectionChange) { const value: any = []; this.editor.env.split.forEach((editor: IAceEditorClass) => { value.push(editor.getSelection()); }); this.props.onSelectionChange(value, event); } } public onCursorChange(event: any) { if (this.props.onCursorChange) { const value: any = []; this.editor.env.split.forEach((editor: IAceEditorClass) => { value.push(editor.getSelection()); }); this.props.onCursorChange(value, event); } } public onFocus(event: any) { if (this.props.onFocus) { this.props.onFocus(event); } } public onInput(event: any) { if (this.props.onInput) { this.props.onInput(event); } } public onBlur(event: any) { if (this.props.onBlur) { this.props.onBlur(event); } } public onCopy(text: string) { if (this.props.onCopy) { this.props.onCopy(text); } } public onPaste(text: string) { if (this.props.onPaste) { this.props.onPaste(text); } } public onScroll() { if (this.props.onScroll) { this.props.onScroll(this.editor); } } public handleOptions(props: ISplitEditorProps, editor: IAceEditorClass) { const setOptions = Object.keys(props.setOptions); for (let y = 0; y < setOptions.length; y++) { editor.setOption(setOptions[y] as any, props.setOptions[setOptions[y]]); } } public handleMarkers(markers: IMarker[], editor: IAceEditorClass) { // remove foreground markers let currentMarkers = editor.getSession().getMarkers(true); for (const i in currentMarkers) { if (currentMarkers.hasOwnProperty(i)) { editor.getSession().removeMarker(currentMarkers[i].id); } } // remove background markers currentMarkers = editor.getSession().getMarkers(false); for (const i in currentMarkers) { if (currentMarkers.hasOwnProperty(i)) { editor.getSession().removeMarker(currentMarkers[i].id); } } // add new markers markers.forEach( ({ startRow, startCol, endRow, endCol, className, type, inFront = false }) => { const range = new Range(startRow, startCol, endRow, endCol); editor .getSession() .addMarker(range as any, className, type as any, inFront); } ); } public updateRef(item: HTMLElement) { this.refEditor = item; } public render() { const { name, width, height, style } = this.props; const divStyle = { width, height, ...style }; return
; } }