784fc207dc12370632cc30934efab1f09cda1d9e40490c183b2991513f5a7b9c78d732cc337becd7ab4a592ac57619d98c96dee5babd4c59609113dc17cf4c 8.4 KB

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