import { Display } from "../display/Display.js" import { onFocus, onBlur } from "../display/focus.js" import { maybeUpdateLineNumberWidth } from "../display/line_numbers.js" import { endOperation, operation, startOperation } from "../display/operations.js" import { initScrollbars } from "../display/scrollbars.js" import { onScrollWheel } from "../display/scroll_events.js" import { setScrollLeft, updateScrollTop } from "../display/scrolling.js" import { clipPos, Pos } from "../line/pos.js" import { posFromMouse } from "../measurement/position_measurement.js" import { eventInWidget } from "../measurement/widgets.js" import Doc from "../model/Doc.js" import { attachDoc } from "../model/document_data.js" import { Range } from "../model/selection.js" import { extendSelection } from "../model/selection_updates.js" import { ie, ie_version, mobile, webkit } from "../util/browser.js" import { e_preventDefault, e_stop, on, signal, signalDOMEvent } from "../util/event.js" import { bind, copyObj, Delayed } from "../util/misc.js" import { clearDragCursor, onDragOver, onDragStart, onDrop } from "./drop_events.js" import { ensureGlobalHandlers } from "./global_events.js" import { onKeyDown, onKeyPress, onKeyUp } from "./key_events.js" import { clickInGutter, onContextMenu, onMouseDown } from "./mouse_events.js" import { themeChanged } from "./utils.js" import { defaults, optionHandlers, Init } from "./options.js" // A CodeMirror instance represents an editor. This is the object // that user code is usually dealing with. export function CodeMirror(place, options) { if (!(this instanceof CodeMirror)) return new CodeMirror(place, options) this.options = options = options ? copyObj(options) : {} // Determine effective options based on given values and defaults. copyObj(defaults, options, false) let doc = options.value if (typeof doc == "string") doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction) else if (options.mode) doc.modeOption = options.mode this.doc = doc let input = new CodeMirror.inputStyles[options.inputStyle](this) let display = this.display = new Display(place, doc, input, options) display.wrapper.CodeMirror = this themeChanged(this) if (options.lineWrapping) this.display.wrapper.className += " CodeMirror-wrap" initScrollbars(this) this.state = { keyMaps: [], // stores maps added by addKeyMap overlays: [], // highlighting overlays, as added by addOverlay modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info overwrite: false, delayingBlurEvent: false, focused: false, suppressEdits: false, // used to disable editing during key handlers when in readOnly mode pasteIncoming: -1, cutIncoming: -1, // help recognize paste/cut edits in input.poll selectingText: false, draggingText: false, highlight: new Delayed(), // stores highlight worker timeout keySeq: null, // Unfinished key sequence specialChars: null } if (options.autofocus && !mobile) display.input.focus() // Override magic textarea content restore that IE sometimes does // on our hidden textarea on reload if (ie && ie_version < 11) setTimeout(() => this.display.input.reset(true), 20) registerEventHandlers(this) ensureGlobalHandlers() startOperation(this) this.curOp.forceUpdate = true attachDoc(this, doc) if ((options.autofocus && !mobile) || this.hasFocus()) setTimeout(bind(onFocus, this), 20) else onBlur(this) for (let opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) optionHandlers[opt](this, options[opt], Init) maybeUpdateLineNumberWidth(this) if (options.finishInit) options.finishInit(this) for (let i = 0; i < initHooks.length; ++i) initHooks[i](this) endOperation(this) // Suppress optimizelegibility in Webkit, since it breaks text // measuring on line wrapping boundaries. if (webkit && options.lineWrapping && getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") display.lineDiv.style.textRendering = "auto" } // The default configuration options. CodeMirror.defaults = defaults // Functions to run when options are changed. CodeMirror.optionHandlers = optionHandlers export default CodeMirror // Attach the necessary event handlers when initializing the editor function registerEventHandlers(cm) { let d = cm.display on(d.scroller, "mousedown", operation(cm, onMouseDown)) // Older IE's will not fire a second mousedown for a double click if (ie && ie_version < 11) on(d.scroller, "dblclick", operation(cm, e => { if (signalDOMEvent(cm, e)) return let pos = posFromMouse(cm, e) if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return e_preventDefault(e) let word = cm.findWordAt(pos) extendSelection(cm.doc, word.anchor, word.head) })) else on(d.scroller, "dblclick", e => signalDOMEvent(cm, e) || e_preventDefault(e)) // Some browsers fire contextmenu *after* opening the menu, at // which point we can't mess with it anymore. Context menu is // handled in onMouseDown for these browsers. on(d.scroller, "contextmenu", e => onContextMenu(cm, e)) // Used to suppress mouse event handling when a touch happens let touchFinished, prevTouch = {end: 0} function finishTouch() { if (d.activeTouch) { touchFinished = setTimeout(() => d.activeTouch = null, 1000) prevTouch = d.activeTouch prevTouch.end = +new Date } } function isMouseLikeTouchEvent(e) { if (e.touches.length != 1) return false let touch = e.touches[0] return touch.radiusX <= 1 && touch.radiusY <= 1 } function farAway(touch, other) { if (other.left == null) return true let dx = other.left - touch.left, dy = other.top - touch.top return dx * dx + dy * dy > 20 * 20 } on(d.scroller, "touchstart", e => { if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e) && !clickInGutter(cm, e)) { d.input.ensurePolled() clearTimeout(touchFinished) let now = +new Date d.activeTouch = {start: now, moved: false, prev: now - prevTouch.end <= 300 ? prevTouch : null} if (e.touches.length == 1) { d.activeTouch.left = e.touches[0].pageX d.activeTouch.top = e.touches[0].pageY } } }) on(d.scroller, "touchmove", () => { if (d.activeTouch) d.activeTouch.moved = true }) on(d.scroller, "touchend", e => { let touch = d.activeTouch if (touch && !eventInWidget(d, e) && touch.left != null && !touch.moved && new Date - touch.start < 300) { let pos = cm.coordsChar(d.activeTouch, "page"), range if (!touch.prev || farAway(touch, touch.prev)) // Single tap range = new Range(pos, pos) else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap range = cm.findWordAt(pos) else // Triple tap range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))) cm.setSelection(range.anchor, range.head) cm.focus() e_preventDefault(e) } finishTouch() }) on(d.scroller, "touchcancel", finishTouch) // Sync scrolling between fake scrollbars and real scrollable // area, ensure viewport is updated when scrolling. on(d.scroller, "scroll", () => { if (d.scroller.clientHeight) { updateScrollTop(cm, d.scroller.scrollTop) setScrollLeft(cm, d.scroller.scrollLeft, true) signal(cm, "scroll", cm) } }) // Listen to wheel events in order to try and update the viewport on time. on(d.scroller, "mousewheel", e => onScrollWheel(cm, e)) on(d.scroller, "DOMMouseScroll", e => onScrollWheel(cm, e)) // Prevent wrapper from ever scrolling on(d.wrapper, "scroll", () => d.wrapper.scrollTop = d.wrapper.scrollLeft = 0) d.dragFunctions = { enter: e => {if (!signalDOMEvent(cm, e)) e_stop(e)}, over: e => {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e) }}, start: e => onDragStart(cm, e), drop: operation(cm, onDrop), leave: e => {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm) }} } let inp = d.input.getField() on(inp, "keyup", e => onKeyUp.call(cm, e)) on(inp, "keydown", operation(cm, onKeyDown)) on(inp, "keypress", operation(cm, onKeyPress)) on(inp, "focus", e => onFocus(cm, e)) on(inp, "blur", e => onBlur(cm, e)) } let initHooks = [] CodeMirror.defineInitHook = f => initHooks.push(f)