util.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. #
  2. # Traversing
  3. #
  4. @$ = (selector, el = document) ->
  5. try el.querySelector(selector) catch
  6. @$$ = (selector, el = document) ->
  7. try el.querySelectorAll(selector) catch
  8. $.id = (id) ->
  9. document.getElementById(id)
  10. $.hasChild = (parent, el) ->
  11. return unless parent
  12. while el
  13. return true if el is parent
  14. return if el is document.body
  15. el = el.parentNode
  16. $.closestLink = (el, parent = document.body) ->
  17. while el
  18. return el if el.tagName is 'A'
  19. return if el is parent
  20. el = el.parentNode
  21. #
  22. # Events
  23. #
  24. $.on = (el, event, callback, useCapture = false) ->
  25. if event.indexOf(' ') >= 0
  26. $.on el, name, callback for name in event.split(' ')
  27. else
  28. el.addEventListener(event, callback, useCapture)
  29. return
  30. $.off = (el, event, callback, useCapture = false) ->
  31. if event.indexOf(' ') >= 0
  32. $.off el, name, callback for name in event.split(' ')
  33. else
  34. el.removeEventListener(event, callback, useCapture)
  35. return
  36. $.trigger = (el, type, canBubble = true, cancelable = true) ->
  37. event = document.createEvent 'Event'
  38. event.initEvent(type, canBubble, cancelable)
  39. el.dispatchEvent(event)
  40. return
  41. $.click = (el) ->
  42. event = document.createEvent 'MouseEvent'
  43. event.initMouseEvent 'click', true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null
  44. el.dispatchEvent(event)
  45. return
  46. $.stopEvent = (event) ->
  47. event.preventDefault()
  48. event.stopPropagation()
  49. event.stopImmediatePropagation()
  50. return
  51. $.eventTarget = (event) ->
  52. event.target.correspondingUseElement || event.target
  53. #
  54. # Manipulation
  55. #
  56. buildFragment = (value) ->
  57. fragment = document.createDocumentFragment()
  58. if $.isCollection(value)
  59. fragment.appendChild(child) for child in $.makeArray(value)
  60. else
  61. fragment.innerHTML = value
  62. fragment
  63. $.append = (el, value) ->
  64. if typeof value is 'string'
  65. el.insertAdjacentHTML 'beforeend', value
  66. else
  67. value = buildFragment(value) if $.isCollection(value)
  68. el.appendChild(value)
  69. return
  70. $.prepend = (el, value) ->
  71. if not el.firstChild
  72. $.append(value)
  73. else if typeof value is 'string'
  74. el.insertAdjacentHTML 'afterbegin', value
  75. else
  76. value = buildFragment(value) if $.isCollection(value)
  77. el.insertBefore(value, el.firstChild)
  78. return
  79. $.before = (el, value) ->
  80. if typeof value is 'string' or $.isCollection(value)
  81. value = buildFragment(value)
  82. el.parentNode.insertBefore(value, el)
  83. return
  84. $.after = (el, value) ->
  85. if typeof value is 'string' or $.isCollection(value)
  86. value = buildFragment(value)
  87. if el.nextSibling
  88. el.parentNode.insertBefore(value, el.nextSibling)
  89. else
  90. el.parentNode.appendChild(value)
  91. return
  92. $.remove = (value) ->
  93. if $.isCollection(value)
  94. el.parentNode?.removeChild(el) for el in $.makeArray(value)
  95. else
  96. value.parentNode?.removeChild(value)
  97. return
  98. $.empty = (el) ->
  99. el.removeChild(el.firstChild) while el.firstChild
  100. return
  101. # Calls the function while the element is off the DOM to avoid triggering
  102. # unnecessary reflows and repaints.
  103. $.batchUpdate = (el, fn) ->
  104. parent = el.parentNode
  105. sibling = el.nextSibling
  106. parent.removeChild(el)
  107. fn(el)
  108. if (sibling)
  109. parent.insertBefore(el, sibling)
  110. else
  111. parent.appendChild(el)
  112. return
  113. #
  114. # Offset
  115. #
  116. $.rect = (el) ->
  117. el.getBoundingClientRect()
  118. $.offset = (el, container = document.body) ->
  119. top = 0
  120. left = 0
  121. while el and el isnt container
  122. top += el.offsetTop
  123. left += el.offsetLeft
  124. el = el.offsetParent
  125. top: top
  126. left: left
  127. $.scrollParent = (el) ->
  128. while (el = el.parentNode) and el.nodeType is 1
  129. break if el.scrollTop > 0
  130. break if getComputedStyle(el)?.overflowY in ['auto', 'scroll']
  131. el
  132. $.scrollTo = (el, parent, position = 'center', options = {}) ->
  133. return unless el
  134. parent ?= $.scrollParent(el)
  135. return unless parent
  136. parentHeight = parent.clientHeight
  137. parentScrollHeight = parent.scrollHeight
  138. return unless parentScrollHeight > parentHeight
  139. top = $.offset(el, parent).top
  140. offsetTop = parent.firstElementChild.offsetTop
  141. switch position
  142. when 'top'
  143. parent.scrollTop = top - offsetTop - (if options.margin? then options.margin else 0)
  144. when 'center'
  145. parent.scrollTop = top - Math.round(parentHeight / 2 - el.offsetHeight / 2)
  146. when 'continuous'
  147. scrollTop = parent.scrollTop
  148. height = el.offsetHeight
  149. lastElementOffset = parent.lastElementChild.offsetTop + parent.lastElementChild.offsetHeight
  150. offsetBottom = if lastElementOffset > 0 then parentScrollHeight - lastElementOffset else 0
  151. # If the target element is above the visible portion of its scrollable
  152. # ancestor, move it near the top with a gap = options.topGap * target's height.
  153. if top - offsetTop <= scrollTop + height * (options.topGap or 1)
  154. parent.scrollTop = top - offsetTop - height * (options.topGap or 1)
  155. # If the target element is below the visible portion of its scrollable
  156. # ancestor, move it near the bottom with a gap = options.bottomGap * target's height.
  157. else if top + offsetBottom >= scrollTop + parentHeight - height * ((options.bottomGap or 1) + 1)
  158. parent.scrollTop = top + offsetBottom - parentHeight + height * ((options.bottomGap or 1) + 1)
  159. return
  160. $.scrollToWithImageLock = (el, parent, args...) ->
  161. parent ?= $.scrollParent(el)
  162. return unless parent
  163. $.scrollTo el, parent, args...
  164. # Lock the scroll position on the target element for up to 3 seconds while
  165. # nearby images are loaded and rendered.
  166. for image in parent.getElementsByTagName('img') when not image.complete
  167. do ->
  168. onLoad = (event) ->
  169. clearTimeout(timeout)
  170. unbind(event.target)
  171. $.scrollTo el, parent, args...
  172. unbind = (target) ->
  173. $.off target, 'load', onLoad
  174. $.on image, 'load', onLoad
  175. timeout = setTimeout unbind.bind(null, image), 3000
  176. return
  177. # Calls the function while locking the element's position relative to the window.
  178. $.lockScroll = (el, fn) ->
  179. if parent = $.scrollParent(el)
  180. top = $.rect(el).top
  181. top -= $.rect(parent).top unless parent in [document.body, document.documentElement]
  182. fn()
  183. parent.scrollTop = $.offset(el, parent).top - top
  184. else
  185. fn()
  186. return
  187. smoothScroll = smoothStart = smoothEnd = smoothDistance = smoothDuration = null
  188. $.smoothScroll = (el, end) ->
  189. unless window.requestAnimationFrame
  190. el.scrollTop = end
  191. return
  192. smoothEnd = end
  193. if smoothScroll
  194. newDistance = smoothEnd - smoothStart
  195. smoothDuration += Math.min 300, Math.abs(smoothDistance - newDistance)
  196. smoothDistance = newDistance
  197. return
  198. smoothStart = el.scrollTop
  199. smoothDistance = smoothEnd - smoothStart
  200. smoothDuration = Math.min 300, Math.abs(smoothDistance)
  201. startTime = Date.now()
  202. smoothScroll = ->
  203. p = Math.min 1, (Date.now() - startTime) / smoothDuration
  204. y = Math.max 0, Math.floor(smoothStart + smoothDistance * (if p < 0.5 then 2 * p * p else p * (4 - p * 2) - 1))
  205. el.scrollTop = y
  206. if p is 1
  207. smoothScroll = null
  208. else
  209. requestAnimationFrame(smoothScroll)
  210. requestAnimationFrame(smoothScroll)
  211. #
  212. # Utilities
  213. #
  214. $.extend = (target, objects...) ->
  215. for object in objects when object
  216. for key, value of object
  217. target[key] = value
  218. target
  219. $.makeArray = (object) ->
  220. if Array.isArray(object)
  221. object
  222. else
  223. Array::slice.apply(object)
  224. $.arrayDelete = (array, object) ->
  225. index = array.indexOf(object)
  226. if index >= 0
  227. array.splice(index, 1)
  228. true
  229. else
  230. false
  231. # Returns true if the object is an array or a collection of DOM elements.
  232. $.isCollection = (object) ->
  233. Array.isArray(object) or typeof object?.item is 'function'
  234. ESCAPE_HTML_MAP =
  235. '&': '&amp;'
  236. '<': '&lt;'
  237. '>': '&gt;'
  238. '"': '&quot;'
  239. "'": '&#x27;'
  240. '/': '&#x2F;'
  241. ESCAPE_HTML_REGEXP = /[&<>"'\/]/g
  242. $.escape = (string) ->
  243. string.replace ESCAPE_HTML_REGEXP, (match) -> ESCAPE_HTML_MAP[match]
  244. ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g
  245. $.escapeRegexp = (string) ->
  246. string.replace ESCAPE_REGEXP, "\\$1"
  247. $.urlDecode = (string) ->
  248. decodeURIComponent string.replace(/\+/g, '%20')
  249. $.classify = (string) ->
  250. string = string.split('_')
  251. for substr, i in string
  252. string[i] = substr[0].toUpperCase() + substr[1..]
  253. string.join('')
  254. $.framify = (fn, obj) ->
  255. if window.requestAnimationFrame
  256. (args...) -> requestAnimationFrame(fn.bind(obj, args...))
  257. else
  258. fn
  259. $.requestAnimationFrame = (fn) ->
  260. if window.requestAnimationFrame
  261. requestAnimationFrame(fn)
  262. else
  263. setTimeout(fn, 0)
  264. return
  265. #
  266. # Miscellaneous
  267. #
  268. $.noop = ->
  269. $.popup = (value) ->
  270. try
  271. win = window.open()
  272. win.opener = null if win.opener
  273. win.location = value.href or value
  274. catch
  275. window.open value.href or value, '_blank'
  276. return
  277. isMac = null
  278. $.isMac = ->
  279. isMac ?= navigator.userAgent?.indexOf('Mac') >= 0
  280. isIE = null
  281. $.isIE = ->
  282. isIE ?= navigator.userAgent?.indexOf('MSIE') >= 0 || navigator.userAgent?.indexOf('rv:11.0') >= 0
  283. isChromeForAndroid = null
  284. $.isChromeForAndroid = ->
  285. isChromeForAndroid ?= navigator.userAgent?.indexOf('Android') >= 0 && /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent)
  286. isAndroid = null
  287. $.isAndroid = ->
  288. isAndroid ?= navigator.userAgent?.indexOf('Android') >= 0
  289. isIOS = null
  290. $.isIOS = ->
  291. isIOS ?= navigator.userAgent?.indexOf('iPhone') >= 0 || navigator.userAgent?.indexOf('iPad') >= 0
  292. $.overlayScrollbarsEnabled = ->
  293. return false unless $.isMac()
  294. div = document.createElement('div')
  295. div.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll; position: absolute')
  296. document.body.appendChild(div)
  297. result = div.offsetWidth is div.clientWidth
  298. document.body.removeChild(div)
  299. result
  300. HIGHLIGHT_DEFAULTS =
  301. className: 'highlight'
  302. delay: 1000
  303. $.highlight = (el, options = {}) ->
  304. options = $.extend {}, HIGHLIGHT_DEFAULTS, options
  305. el.classList.add(options.className)
  306. setTimeout (-> el.classList.remove(options.className)), options.delay
  307. return
  308. $.copyToClipboard = (string) ->
  309. textarea = document.createElement('textarea')
  310. textarea.style.position = 'fixed'
  311. textarea.style.opacity = 0
  312. textarea.value = string
  313. document.body.appendChild(textarea)
  314. try
  315. textarea.select()
  316. result = !!document.execCommand('copy')
  317. catch
  318. result = false
  319. finally
  320. document.body.removeChild(textarea)
  321. result