0
|
1 import { Display } from "../display/Display.js"
|
|
2 import { onFocus, onBlur } from "../display/focus.js"
|
|
3 import { maybeUpdateLineNumberWidth } from "../display/line_numbers.js"
|
|
4 import { endOperation, operation, startOperation } from "../display/operations.js"
|
|
5 import { initScrollbars } from "../display/scrollbars.js"
|
|
6 import { onScrollWheel } from "../display/scroll_events.js"
|
|
7 import { setScrollLeft, updateScrollTop } from "../display/scrolling.js"
|
|
8 import { clipPos, Pos } from "../line/pos.js"
|
|
9 import { posFromMouse } from "../measurement/position_measurement.js"
|
|
10 import { eventInWidget } from "../measurement/widgets.js"
|
|
11 import Doc from "../model/Doc.js"
|
|
12 import { attachDoc } from "../model/document_data.js"
|
|
13 import { Range } from "../model/selection.js"
|
|
14 import { extendSelection } from "../model/selection_updates.js"
|
|
15 import { ie, ie_version, mobile, webkit } from "../util/browser.js"
|
|
16 import { e_preventDefault, e_stop, on, signal, signalDOMEvent } from "../util/event.js"
|
|
17 import { copyObj, Delayed } from "../util/misc.js"
|
|
18
|
|
19 import { clearDragCursor, onDragOver, onDragStart, onDrop } from "./drop_events.js"
|
|
20 import { ensureGlobalHandlers } from "./global_events.js"
|
|
21 import { onKeyDown, onKeyPress, onKeyUp } from "./key_events.js"
|
|
22 import { clickInGutter, onContextMenu, onMouseDown } from "./mouse_events.js"
|
|
23 import { themeChanged } from "./utils.js"
|
|
24 import { defaults, optionHandlers, Init } from "./options.js"
|
|
25
|
|
26 // A CodeMirror instance represents an editor. This is the object
|
|
27 // that user code is usually dealing with.
|
|
28
|
|
29 export function CodeMirror(place, options) {
|
|
30 if (!(this instanceof CodeMirror)) return new CodeMirror(place, options)
|
|
31
|
|
32 this.options = options = options ? copyObj(options) : {}
|
|
33 // Determine effective options based on given values and defaults.
|
|
34 copyObj(defaults, options, false)
|
|
35
|
|
36 let doc = options.value
|
|
37 if (typeof doc == "string") doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction)
|
|
38 else if (options.mode) doc.modeOption = options.mode
|
|
39 this.doc = doc
|
|
40
|
|
41 let input = new CodeMirror.inputStyles[options.inputStyle](this)
|
|
42 let display = this.display = new Display(place, doc, input, options)
|
|
43 display.wrapper.CodeMirror = this
|
|
44 themeChanged(this)
|
|
45 if (options.lineWrapping)
|
|
46 this.display.wrapper.className += " CodeMirror-wrap"
|
|
47 initScrollbars(this)
|
|
48
|
|
49 this.state = {
|
|
50 keyMaps: [], // stores maps added by addKeyMap
|
|
51 overlays: [], // highlighting overlays, as added by addOverlay
|
|
52 modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info
|
|
53 overwrite: false,
|
|
54 delayingBlurEvent: false,
|
|
55 focused: false,
|
|
56 suppressEdits: false, // used to disable editing during key handlers when in readOnly mode
|
|
57 pasteIncoming: -1, cutIncoming: -1, // help recognize paste/cut edits in input.poll
|
|
58 selectingText: false,
|
|
59 draggingText: false,
|
|
60 highlight: new Delayed(), // stores highlight worker timeout
|
|
61 keySeq: null, // Unfinished key sequence
|
|
62 specialChars: null
|
|
63 }
|
|
64
|
|
65 if (options.autofocus && !mobile) display.input.focus()
|
|
66
|
|
67 // Override magic textarea content restore that IE sometimes does
|
|
68 // on our hidden textarea on reload
|
|
69 if (ie && ie_version < 11) setTimeout(() => this.display.input.reset(true), 20)
|
|
70
|
|
71 registerEventHandlers(this)
|
|
72 ensureGlobalHandlers()
|
|
73
|
|
74 startOperation(this)
|
|
75 this.curOp.forceUpdate = true
|
|
76 attachDoc(this, doc)
|
|
77
|
|
78 if ((options.autofocus && !mobile) || this.hasFocus())
|
|
79 setTimeout(() => {
|
|
80 if (this.hasFocus() && !this.state.focused) onFocus(this)
|
|
81 }, 20)
|
|
82 else
|
|
83 onBlur(this)
|
|
84
|
|
85 for (let opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt))
|
|
86 optionHandlers[opt](this, options[opt], Init)
|
|
87 maybeUpdateLineNumberWidth(this)
|
|
88 if (options.finishInit) options.finishInit(this)
|
|
89 for (let i = 0; i < initHooks.length; ++i) initHooks[i](this)
|
|
90 endOperation(this)
|
|
91 // Suppress optimizelegibility in Webkit, since it breaks text
|
|
92 // measuring on line wrapping boundaries.
|
|
93 if (webkit && options.lineWrapping &&
|
|
94 getComputedStyle(display.lineDiv).textRendering == "optimizelegibility")
|
|
95 display.lineDiv.style.textRendering = "auto"
|
|
96 }
|
|
97
|
|
98 // The default configuration options.
|
|
99 CodeMirror.defaults = defaults
|
|
100 // Functions to run when options are changed.
|
|
101 CodeMirror.optionHandlers = optionHandlers
|
|
102
|
|
103 export default CodeMirror
|
|
104
|
|
105 // Attach the necessary event handlers when initializing the editor
|
|
106 function registerEventHandlers(cm) {
|
|
107 let d = cm.display
|
|
108 on(d.scroller, "mousedown", operation(cm, onMouseDown))
|
|
109 // Older IE's will not fire a second mousedown for a double click
|
|
110 if (ie && ie_version < 11)
|
|
111 on(d.scroller, "dblclick", operation(cm, e => {
|
|
112 if (signalDOMEvent(cm, e)) return
|
|
113 let pos = posFromMouse(cm, e)
|
|
114 if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return
|
|
115 e_preventDefault(e)
|
|
116 let word = cm.findWordAt(pos)
|
|
117 extendSelection(cm.doc, word.anchor, word.head)
|
|
118 }))
|
|
119 else
|
|
120 on(d.scroller, "dblclick", e => signalDOMEvent(cm, e) || e_preventDefault(e))
|
|
121 // Some browsers fire contextmenu *after* opening the menu, at
|
|
122 // which point we can't mess with it anymore. Context menu is
|
|
123 // handled in onMouseDown for these browsers.
|
|
124 on(d.scroller, "contextmenu", e => onContextMenu(cm, e))
|
|
125 on(d.input.getField(), "contextmenu", e => {
|
|
126 if (!d.scroller.contains(e.target)) onContextMenu(cm, e)
|
|
127 })
|
|
128
|
|
129 // Used to suppress mouse event handling when a touch happens
|
|
130 let touchFinished, prevTouch = {end: 0}
|
|
131 function finishTouch() {
|
|
132 if (d.activeTouch) {
|
|
133 touchFinished = setTimeout(() => d.activeTouch = null, 1000)
|
|
134 prevTouch = d.activeTouch
|
|
135 prevTouch.end = +new Date
|
|
136 }
|
|
137 }
|
|
138 function isMouseLikeTouchEvent(e) {
|
|
139 if (e.touches.length != 1) return false
|
|
140 let touch = e.touches[0]
|
|
141 return touch.radiusX <= 1 && touch.radiusY <= 1
|
|
142 }
|
|
143 function farAway(touch, other) {
|
|
144 if (other.left == null) return true
|
|
145 let dx = other.left - touch.left, dy = other.top - touch.top
|
|
146 return dx * dx + dy * dy > 20 * 20
|
|
147 }
|
|
148 on(d.scroller, "touchstart", e => {
|
|
149 if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e) && !clickInGutter(cm, e)) {
|
|
150 d.input.ensurePolled()
|
|
151 clearTimeout(touchFinished)
|
|
152 let now = +new Date
|
|
153 d.activeTouch = {start: now, moved: false,
|
|
154 prev: now - prevTouch.end <= 300 ? prevTouch : null}
|
|
155 if (e.touches.length == 1) {
|
|
156 d.activeTouch.left = e.touches[0].pageX
|
|
157 d.activeTouch.top = e.touches[0].pageY
|
|
158 }
|
|
159 }
|
|
160 })
|
|
161 on(d.scroller, "touchmove", () => {
|
|
162 if (d.activeTouch) d.activeTouch.moved = true
|
|
163 })
|
|
164 on(d.scroller, "touchend", e => {
|
|
165 let touch = d.activeTouch
|
|
166 if (touch && !eventInWidget(d, e) && touch.left != null &&
|
|
167 !touch.moved && new Date - touch.start < 300) {
|
|
168 let pos = cm.coordsChar(d.activeTouch, "page"), range
|
|
169 if (!touch.prev || farAway(touch, touch.prev)) // Single tap
|
|
170 range = new Range(pos, pos)
|
|
171 else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap
|
|
172 range = cm.findWordAt(pos)
|
|
173 else // Triple tap
|
|
174 range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0)))
|
|
175 cm.setSelection(range.anchor, range.head)
|
|
176 cm.focus()
|
|
177 e_preventDefault(e)
|
|
178 }
|
|
179 finishTouch()
|
|
180 })
|
|
181 on(d.scroller, "touchcancel", finishTouch)
|
|
182
|
|
183 // Sync scrolling between fake scrollbars and real scrollable
|
|
184 // area, ensure viewport is updated when scrolling.
|
|
185 on(d.scroller, "scroll", () => {
|
|
186 if (d.scroller.clientHeight) {
|
|
187 updateScrollTop(cm, d.scroller.scrollTop)
|
|
188 setScrollLeft(cm, d.scroller.scrollLeft, true)
|
|
189 signal(cm, "scroll", cm)
|
|
190 }
|
|
191 })
|
|
192
|
|
193 // Listen to wheel events in order to try and update the viewport on time.
|
|
194 on(d.scroller, "mousewheel", e => onScrollWheel(cm, e))
|
|
195 on(d.scroller, "DOMMouseScroll", e => onScrollWheel(cm, e))
|
|
196
|
|
197 // Prevent wrapper from ever scrolling
|
|
198 on(d.wrapper, "scroll", () => d.wrapper.scrollTop = d.wrapper.scrollLeft = 0)
|
|
199
|
|
200 d.dragFunctions = {
|
|
201 enter: e => {if (!signalDOMEvent(cm, e)) e_stop(e)},
|
|
202 over: e => {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e) }},
|
|
203 start: e => onDragStart(cm, e),
|
|
204 drop: operation(cm, onDrop),
|
|
205 leave: e => {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm) }}
|
|
206 }
|
|
207
|
|
208 let inp = d.input.getField()
|
|
209 on(inp, "keyup", e => onKeyUp.call(cm, e))
|
|
210 on(inp, "keydown", operation(cm, onKeyDown))
|
|
211 on(inp, "keypress", operation(cm, onKeyPress))
|
|
212 on(inp, "focus", e => onFocus(cm, e))
|
|
213 on(inp, "blur", e => onBlur(cm, e))
|
|
214 }
|
|
215
|
|
216 let initHooks = []
|
|
217 CodeMirror.defineInitHook = f => initHooks.push(f)
|