CodeMirror.js 8.4 KB

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