jquery.caret.coffee 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. ###
  2. Implement Github like autocomplete mentions
  3. http://ichord.github.com/At.js
  4. Copyright (c) 2013 chord.luo@gmail.com
  5. Licensed under the MIT license.
  6. ###
  7. ###
  8. 本插件操作 textarea 或者 input 内的插入符
  9. 只实现了获得插入符在文本框中的位置,我设置
  10. 插入符的位置.
  11. ###
  12. "use strict";
  13. pluginName = 'caret'
  14. class EditableCaret
  15. constructor: (@$inputor) ->
  16. @domInputor = @$inputor[0]
  17. # NOTE: Duck type
  18. setPos: (pos) ->
  19. if sel = oWindow.getSelection()
  20. offset = 0
  21. found = false
  22. do fn = (pos, parent=@domInputor) ->
  23. for node in parent.childNodes
  24. if found
  25. break
  26. if node.nodeType == 3
  27. if offset + node.length >= pos
  28. found = true
  29. range = oDocument.createRange()
  30. range.setStart(node, pos - offset)
  31. sel.removeAllRanges()
  32. sel.addRange(range)
  33. break
  34. else
  35. offset += node.length
  36. else
  37. fn(pos, node)
  38. @domInputor
  39. getIEPosition: -> this.getPosition()
  40. getPosition: ->
  41. offset = this.getOffset()
  42. inputor_offset = @$inputor.offset()
  43. offset.left -= inputor_offset.left
  44. offset.top -= inputor_offset.top
  45. offset
  46. getOldIEPos: ->
  47. textRange = oDocument.selection.createRange()
  48. preCaretTextRange = oDocument.body.createTextRange()
  49. preCaretTextRange.moveToElementText(@domInputor)
  50. preCaretTextRange.setEndPoint("EndToEnd", textRange)
  51. preCaretTextRange.text.length
  52. getPos: ->
  53. if range = this.range() # Major Browser and IE > 10
  54. clonedRange = range.cloneRange()
  55. clonedRange.selectNodeContents(@domInputor)
  56. clonedRange.setEnd(range.endContainer, range.endOffset)
  57. pos = clonedRange.toString().length
  58. clonedRange.detach()
  59. pos
  60. else if oDocument.selection #IE < 9
  61. this.getOldIEPos()
  62. getOldIEOffset: ->
  63. range = oDocument.selection.createRange().duplicate()
  64. range.moveStart "character", -1
  65. rect = range.getBoundingClientRect()
  66. { height: rect.bottom - rect.top, left: rect.left, top: rect.top }
  67. getOffset: (pos) ->
  68. if oWindow.getSelection and range = this.range()
  69. # endContainer would be the inputor in Firefox at the begnning of a line
  70. if range.endOffset - 1 > 0 and range.endContainer isnt @domInputor
  71. clonedRange = range.cloneRange()
  72. clonedRange.setStart(range.endContainer, range.endOffset - 1)
  73. clonedRange.setEnd(range.endContainer, range.endOffset)
  74. rect = clonedRange.getBoundingClientRect()
  75. offset = { height: rect.height, left: rect.left + rect.width, top: rect.top }
  76. clonedRange.detach()
  77. # At the begnning of the inputor, the offset height is 0 in Chrome and Safari
  78. # This work fine in all browers but except while the inputor break a line into two (wrapped line).
  79. # so we can't use it in all cases.
  80. if !offset or offset?.height == 0
  81. clonedRange = range.cloneRange()
  82. shadowCaret = $ oDocument.createTextNode "|"
  83. clonedRange.insertNode shadowCaret[0]
  84. clonedRange.selectNode shadowCaret[0]
  85. rect = clonedRange.getBoundingClientRect()
  86. offset = {height: rect.height, left: rect.left, top: rect.top }
  87. shadowCaret.remove()
  88. clonedRange.detach()
  89. else if oDocument.selection # ie < 9
  90. offset = this.getOldIEOffset()
  91. if offset
  92. offset.top += $(oWindow).scrollTop()
  93. offset.left += $(oWindow).scrollLeft()
  94. offset
  95. range: ->
  96. return unless oWindow.getSelection
  97. sel = oWindow.getSelection()
  98. if sel.rangeCount > 0 then sel.getRangeAt(0) else null
  99. class InputCaret
  100. constructor: (@$inputor) ->
  101. @domInputor = @$inputor[0]
  102. getIEPos: ->
  103. # https://github.com/ichord/Caret.js/wiki/Get-pos-of-caret-in-IE
  104. inputor = @domInputor
  105. range = oDocument.selection.createRange()
  106. pos = 0
  107. # selection should in the inputor.
  108. if range and range.parentElement() is inputor
  109. normalizedValue = inputor.value.replace /\r\n/g, "\n"
  110. len = normalizedValue.length
  111. textInputRange = inputor.createTextRange()
  112. textInputRange.moveToBookmark range.getBookmark()
  113. endRange = inputor.createTextRange()
  114. endRange.collapse false
  115. if textInputRange.compareEndPoints("StartToEnd", endRange) > -1
  116. pos = len
  117. else
  118. pos = -textInputRange.moveStart "character", -len
  119. pos
  120. getPos: ->
  121. if oDocument.selection then this.getIEPos() else @domInputor.selectionStart
  122. setPos: (pos) ->
  123. inputor = @domInputor
  124. if oDocument.selection #IE
  125. range = inputor.createTextRange()
  126. range.move "character", pos
  127. range.select()
  128. else if inputor.setSelectionRange
  129. inputor.setSelectionRange pos, pos
  130. inputor
  131. getIEOffset: (pos) ->
  132. textRange = @domInputor.createTextRange()
  133. pos ||= this.getPos()
  134. textRange.move('character', pos)
  135. x = textRange.boundingLeft
  136. y = textRange.boundingTop
  137. h = textRange.boundingHeight
  138. {left: x, top: y, height: h}
  139. getOffset: (pos) ->
  140. $inputor = @$inputor
  141. if oDocument.selection
  142. offset = this.getIEOffset(pos)
  143. offset.top += $(oWindow).scrollTop() + $inputor.scrollTop()
  144. offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft()
  145. offset
  146. else
  147. offset = $inputor.offset()
  148. position = this.getPosition(pos)
  149. offset =
  150. left: offset.left + position.left - $inputor.scrollLeft()
  151. top: offset.top + position.top - $inputor.scrollTop()
  152. height: position.height
  153. getPosition: (pos)->
  154. $inputor = @$inputor
  155. format = (value) ->
  156. value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g,"<br/>")
  157. if /firefox/i.test navigator.userAgent
  158. value = value.replace(/\s/g, '&nbsp;')
  159. value
  160. pos = this.getPos() if pos is undefined
  161. start_range = $inputor.val().slice(0, pos)
  162. end_range = $inputor.val().slice(pos)
  163. html = "<span style='position: relative; display: inline;'>"+format(start_range)+"</span>"
  164. html += "<span id='caret' style='position: relative; display: inline;'>|</span>"
  165. html += "<span style='position: relative; display: inline;'>"+format(end_range)+"</span>"
  166. mirror = new Mirror($inputor)
  167. at_rect = mirror.create(html).rect()
  168. getIEPosition: (pos) ->
  169. offset = this.getIEOffset pos
  170. inputorOffset = @$inputor.offset()
  171. x = offset.left - inputorOffset.left
  172. y = offset.top - inputorOffset.top
  173. h = offset.height
  174. {left: x, top: y, height: h}
  175. # @example
  176. # mirror = new Mirror($("textarea#inputor"))
  177. # html = "<p>We will get the rect of <span>@</span>icho</p>"
  178. # mirror.create(html).rect()
  179. class Mirror
  180. css_attr: [
  181. "borderBottomWidth",
  182. "borderLeftWidth",
  183. "borderRightWidth",
  184. "borderTopStyle",
  185. "borderRightStyle",
  186. "borderBottomStyle",
  187. "borderLeftStyle",
  188. "borderTopWidth",
  189. "boxSizing",
  190. "fontFamily",
  191. "fontSize",
  192. "fontWeight",
  193. "height",
  194. "letterSpacing",
  195. "lineHeight",
  196. "marginBottom",
  197. "marginLeft",
  198. "marginRight",
  199. "marginTop",
  200. "outlineWidth",
  201. "overflow",
  202. "overflowX",
  203. "overflowY",
  204. "paddingBottom",
  205. "paddingLeft",
  206. "paddingRight",
  207. "paddingTop",
  208. "textAlign",
  209. "textOverflow",
  210. "textTransform",
  211. "whiteSpace",
  212. "wordBreak",
  213. "wordWrap",
  214. ]
  215. constructor: (@$inputor) ->
  216. mirrorCss: ->
  217. css =
  218. position: 'absolute'
  219. left: -9999
  220. top: 0
  221. zIndex: -20000
  222. if @$inputor.prop( 'tagName' ) == 'TEXTAREA'
  223. @css_attr.push( 'width' )
  224. $.each @css_attr, (i,p) =>
  225. css[p] = @$inputor.css p
  226. css
  227. create: (html) ->
  228. @$mirror = $('<div></div>')
  229. @$mirror.css this.mirrorCss()
  230. @$mirror.html(html)
  231. @$inputor.after(@$mirror)
  232. this
  233. # 获得标记的位置
  234. #
  235. # @return [Object] 标记的坐标
  236. # {left: 0, top: 0, bottom: 0}
  237. rect: ->
  238. $flag = @$mirror.find "#caret"
  239. pos = $flag.position()
  240. rect = {left: pos.left, top: pos.top, height: $flag.height() }
  241. @$mirror.remove()
  242. rect
  243. Utils =
  244. contentEditable: ($inputor)->
  245. !!($inputor[0].contentEditable && $inputor[0].contentEditable == 'true')
  246. methods =
  247. pos: (pos) ->
  248. if pos or pos == 0
  249. this.setPos pos
  250. else
  251. this.getPos()
  252. position: (pos) ->
  253. if oDocument.selection then this.getIEPosition pos else this.getPosition pos
  254. offset: (pos) ->
  255. offset = this.getOffset(pos)
  256. offset
  257. oDocument = null
  258. oWindow = null
  259. oFrame = null
  260. setContextBy = (settings) ->
  261. if iframe = settings?.iframe
  262. oFrame = iframe
  263. oWindow = iframe.contentWindow
  264. oDocument = iframe.contentDocument || oWindow.document
  265. else
  266. oFrame = undefined
  267. oWindow = window
  268. oDocument = document
  269. discoveryIframeOf = ($dom) ->
  270. oDocument = $dom[0].ownerDocument
  271. oWindow = oDocument.defaultView || oDocument.parentWindow
  272. try
  273. oFrame = oWindow.frameElement
  274. catch error
  275. # throws error in cross-domain iframes
  276. $.fn.caret = (method, value, settings) ->
  277. # http://stackoverflow.com/questions/16010204/get-reference-of-window-object-from-a-dom-element
  278. if methods[method]
  279. if $.isPlainObject(value)
  280. setContextBy value
  281. value = undefined
  282. else
  283. setContextBy settings
  284. caret = if Utils.contentEditable(this) then new EditableCaret(this) else new InputCaret(this)
  285. methods[method].apply caret, [value]
  286. else
  287. $.error "Method #{method} does not exist on jQuery.caret"
  288. $.fn.caret.EditableCaret = EditableCaret
  289. $.fn.caret.InputCaret = InputCaret
  290. $.fn.caret.Utils = Utils
  291. $.fn.caret.apis = methods