Update JSON API
[src/app-framework-demo.git] / afm-client / bower_components / tether / coffee / tether.coffee
1 if not @Tether?
2   throw new Error "You must include the utils.js file before tether.js"
3
4 Tether = @Tether
5
6 {getScrollParent, getSize, getOuterSize, getBounds, getOffsetParent, extend, addClass, removeClass, updateClasses, defer, flush, getScrollBarSize} = Tether.Utils
7
8 within = (a, b, diff=1) ->
9   a + diff >= b >= a - diff
10
11 transformKey = do ->
12   el = document.createElement 'div'
13
14   for key in ['transform', 'webkitTransform', 'OTransform', 'MozTransform', 'msTransform']
15     if el.style[key] isnt undefined
16       return key
17
18 tethers = []
19
20 position = ->
21   for tether in tethers
22     tether.position(false)
23
24   flush()
25
26 now = ->
27   performance?.now?() ? +new Date
28
29 do ->
30   lastCall = null
31   lastDuration = null
32   pendingTimeout = null
33
34   tick = ->
35     if lastDuration? and lastDuration > 16
36       # We voluntarily throttle ourselves if we can't manage 60fps
37       lastDuration = Math.min(lastDuration - 16, 250)
38
39       # Just in case this is the last event, remember to position just once more
40       pendingTimeout = setTimeout tick, 250
41       return
42
43     if lastCall? and (now() - lastCall) < 10
44       # Some browsers call events a little too frequently, refuse to run more than is reasonable
45       return
46
47     if pendingTimeout?
48       clearTimeout pendingTimeout
49       pendingTimeout = null
50
51     lastCall = now()
52
53     position()
54
55     lastDuration = now() - lastCall
56
57   for event in ['resize', 'scroll', 'touchmove']
58     window.addEventListener event, tick
59
60 MIRROR_LR =
61   center: 'center'
62   left: 'right'
63   right: 'left'
64
65 MIRROR_TB =
66   middle: 'middle'
67   top: 'bottom'
68   bottom: 'top'
69
70 OFFSET_MAP =
71   top: 0
72   left: 0
73   middle: '50%'
74   center: '50%'
75   bottom: '100%'
76   right: '100%'
77
78 autoToFixedAttachment = (attachment, relativeToAttachment) ->
79   {left, top} = attachment
80
81   if left is 'auto'
82     left = MIRROR_LR[relativeToAttachment.left]
83
84   if top is 'auto'
85     top = MIRROR_TB[relativeToAttachment.top]
86
87   {left, top}
88
89 attachmentToOffset = (attachment) ->
90   return {
91     left: OFFSET_MAP[attachment.left] ? attachment.left
92     top: OFFSET_MAP[attachment.top] ? attachment.top
93   }
94
95 addOffset = (offsets...) ->
96   out = {top: 0, left: 0}
97
98   for {top, left} in offsets
99     if typeof top is 'string'
100       top = parseFloat(top, 10)
101     if typeof left is 'string'
102       left = parseFloat(left, 10)
103
104     out.top += top
105     out.left += left
106
107   out
108
109 offsetToPx = (offset, size) ->
110   if typeof offset.left is 'string' and offset.left.indexOf('%') isnt -1
111     offset.left = parseFloat(offset.left, 10) / 100 * size.width
112   if typeof offset.top is 'string' and offset.top.indexOf('%') isnt -1
113     offset.top = parseFloat(offset.top, 10) / 100 * size.height
114
115   offset
116
117 parseAttachment = parseOffset = (value) ->
118   [top, left] = value.split(' ')
119
120   {top, left}
121
122 class _Tether
123   @modules: []
124
125   constructor: (options) ->
126     tethers.push @
127
128     @history = []
129
130     @setOptions options, false
131
132     for module in Tether.modules
133       module.initialize?.call(@)
134
135     @position()
136
137   getClass: (key) ->
138     if @options.classes?[key]
139       @options.classes[key]
140     else if @options.classes?[key] isnt false
141       if @options.classPrefix
142         "#{ @options.classPrefix }-#{ key }"
143       else
144         key
145     else
146       ''
147
148   setOptions: (@options, position=true) ->
149     defaults =
150       offset: '0 0'
151       targetOffset: '0 0'
152       targetAttachment: 'auto auto'
153       classPrefix: 'tether'
154
155     @options = extend defaults, @options
156
157     {@element, @target, @targetModifier} = @options
158
159     if @target is 'viewport'
160       @target = document.body
161       @targetModifier = 'visible'
162     else if @target is 'scroll-handle'
163       @target = document.body
164       @targetModifier = 'scroll-handle'
165
166     for key in ['element', 'target']
167       if not @[key]?
168         throw new Error "Tether Error: Both element and target must be defined"
169
170       if @[key].jquery?
171         @[key] = @[key][0]
172       else if typeof @[key] is 'string'
173         @[key] = document.querySelector @[key]
174
175     addClass @element, @getClass 'element'
176     addClass @target, @getClass 'target'
177
178     if not @options.attachment
179       throw new Error "Tether Error: You must provide an attachment"
180
181     @targetAttachment = parseAttachment @options.targetAttachment
182     @attachment = parseAttachment @options.attachment
183     @offset = parseOffset @options.offset
184     @targetOffset = parseOffset @options.targetOffset
185
186     if @scrollParent?
187       @disable()
188
189     if @targetModifier is 'scroll-handle'
190       @scrollParent = @target
191     else
192       @scrollParent = getScrollParent @target
193
194     unless @options.enabled is false
195       @enable(position)
196
197   getTargetBounds: ->
198     if @targetModifier?
199       switch @targetModifier
200         when 'visible'
201           if @target is document.body
202             {top: pageYOffset, left: pageXOffset, height: innerHeight, width: innerWidth}
203           else
204             bounds = getBounds @target
205
206             out =
207               height: bounds.height
208               width: bounds.width
209               top: bounds.top
210               left: bounds.left
211
212             out.height = Math.min(out.height, bounds.height - (pageYOffset - bounds.top))
213             out.height = Math.min(out.height, bounds.height - ((bounds.top + bounds.height) - (pageYOffset + innerHeight)))
214             out.height = Math.min(innerHeight, out.height)
215             out.height -= 2
216
217             out.width = Math.min(out.width, bounds.width - (pageXOffset - bounds.left))
218             out.width = Math.min(out.width, bounds.width - ((bounds.left + bounds.width) - (pageXOffset + innerWidth)))
219             out.width = Math.min(innerWidth, out.width)
220             out.width -= 2
221
222             if out.top < pageYOffset
223               out.top = pageYOffset
224             if out.left < pageXOffset
225               out.left = pageXOffset
226
227             out
228
229         when 'scroll-handle'
230           target = @target
231           if target is document.body
232             target = document.documentElement
233
234             bounds =
235               left: pageXOffset
236               top: pageYOffset
237               height: innerHeight
238               width: innerWidth
239           else
240             bounds = getBounds target
241
242           style = getComputedStyle target
243
244           hasBottomScroll = target.scrollWidth > target.clientWidth or 'scroll' is [style.overflow, style.overflowX] or @target isnt document.body
245
246           scrollBottom = 0
247           if hasBottomScroll
248             scrollBottom = 15
249
250           height = bounds.height - parseFloat(style.borderTopWidth) - parseFloat(style.borderBottomWidth) - scrollBottom
251
252           out =
253             width: 15
254             height: height * 0.975 * (height / target.scrollHeight)
255             left: bounds.left + bounds.width - parseFloat(style.borderLeftWidth) - 15
256
257           fitAdj = 0
258           if height < 408 and @target is document.body
259             fitAdj = -0.00011 * Math.pow(height, 2) - 0.00727 * height + 22.58
260
261           if @target isnt document.body
262             out.height = Math.max out.height, 24
263
264           scrollPercentage = @target.scrollTop / (target.scrollHeight - height)
265           out.top = scrollPercentage * (height - out.height - fitAdj) + bounds.top + parseFloat(style.borderTopWidth)
266
267           if @target is document.body
268             out.height = Math.max out.height, 24
269
270           out
271     else
272       getBounds @target
273
274   clearCache: ->
275     @_cache = {}
276
277   cache: (k, getter) ->
278     # More than one module will often need the same DOM info, so
279     # we keep a cache which is cleared on each position call
280     @_cache ?= {}
281
282     if not @_cache[k]?
283       @_cache[k] = getter.call(@)
284
285     @_cache[k]
286
287   enable: (position=true) ->
288     addClass @target, @getClass 'enabled'
289     addClass @element, @getClass 'enabled'
290     @enabled = true
291
292     if @scrollParent isnt document
293       @scrollParent.addEventListener 'scroll', @position
294
295     if position
296       @position()
297
298   disable: ->
299     removeClass @target, @getClass 'enabled'
300     removeClass @element, @getClass 'enabled'
301     @enabled = false
302
303     if @scrollParent?
304       @scrollParent.removeEventListener 'scroll', @position
305
306   destroy: ->
307     @disable()
308
309     for tether, i in tethers
310       if tether is @
311         tethers.splice i, 1
312         break
313
314   updateAttachClasses: (elementAttach=@attachment, targetAttach=@targetAttachment) ->
315     sides = ['left', 'top', 'bottom', 'right', 'middle', 'center']
316
317     if @_addAttachClasses?.length
318         # updateAttachClasses can be called more than once in a position call, so
319         # we need to clean up after ourselves such that when the last defer gets
320         # ran it doesn't add any extra classes from previous calls.
321         @_addAttachClasses.splice 0, @_addAttachClasses.length
322
323     add = @_addAttachClasses ?= []
324     add.push "#{ @getClass('element-attached') }-#{ elementAttach.top }" if elementAttach.top
325     add.push "#{ @getClass('element-attached') }-#{ elementAttach.left }" if elementAttach.left
326     add.push "#{ @getClass('target-attached') }-#{ targetAttach.top }" if targetAttach.top
327     add.push "#{ @getClass('target-attached') }-#{ targetAttach.left }" if targetAttach.left
328
329     all = []
330     all.push "#{ @getClass('element-attached') }-#{ side }" for side in sides
331     all.push "#{ @getClass('target-attached') }-#{ side }" for side in sides
332
333     defer =>
334       return unless @_addAttachClasses?
335
336       updateClasses @element, @_addAttachClasses, all
337       updateClasses @target, @_addAttachClasses, all
338
339       @_addAttachClasses = undefined
340
341   position: (flushChanges=true) =>
342     # flushChanges commits the changes immediately, leave true unless you are positioning multiple
343     # tethers (in which case call Tether.Utils.flush yourself when you're done)
344
345     return unless @enabled
346
347     @clearCache()
348
349     # Turn 'auto' attachments into the appropriate corner or edge
350     targetAttachment = autoToFixedAttachment(@targetAttachment, @attachment)
351
352     @updateAttachClasses @attachment, targetAttachment
353
354     elementPos = @cache 'element-bounds', => getBounds @element
355     {width, height} = elementPos
356
357     if width is 0 and height is 0 and @lastSize?
358       # We cache the height and width to make it possible to position elements that are
359       # getting hidden.
360       {width, height} = @lastSize
361     else
362       @lastSize = {width, height}
363
364     targetSize = targetPos = @cache 'target-bounds', => @getTargetBounds()
365
366     # Get an actual px offset from the attachment
367     offset = offsetToPx attachmentToOffset(@attachment), {width, height}
368     targetOffset = offsetToPx attachmentToOffset(targetAttachment), targetSize
369
370     manualOffset = offsetToPx(@offset, {width, height})
371     manualTargetOffset = offsetToPx(@targetOffset, targetSize)
372
373     # Add the manually provided offset
374     offset = addOffset offset, manualOffset
375     targetOffset = addOffset targetOffset, manualTargetOffset
376
377     # It's now our goal to make (element position + offset) == (target position + target offset)
378     left = targetPos.left + targetOffset.left - offset.left
379     top = targetPos.top + targetOffset.top - offset.top
380
381     for module in Tether.modules
382       ret = module.position.call(@, {left, top, targetAttachment, targetPos, @attachment, elementPos, offset, targetOffset, manualOffset, manualTargetOffset, scrollbarSize})
383
384       if not ret? or typeof ret isnt 'object'
385         continue
386       else if ret is false
387         return false
388       else
389         {top, left} = ret
390
391     # We describe the position three different ways to give the optimizer
392     # a chance to decide the best possible way to position the element
393     # with the fewest repaints.
394     next = {
395       # It's position relative to the page (absolute positioning when
396       # the element is a child of the body)
397       page:
398         top: top
399         left: left
400
401       # It's position relative to the viewport (fixed positioning)
402       viewport:
403         top: top - pageYOffset
404         bottom: pageYOffset - top - height + innerHeight
405         left: left - pageXOffset
406         right: pageXOffset - left - width + innerWidth
407     }
408
409     if document.body.scrollWidth > window.innerWidth
410       scrollbarSize = @cache 'scrollbar-size', getScrollBarSize
411       next.viewport.bottom -= scrollbarSize.height
412
413     if document.body.scrollHeight > window.innerHeight
414       scrollbarSize = @cache 'scrollbar-size', getScrollBarSize
415       next.viewport.right -= scrollbarSize.width
416
417     if document.body.style.position not in ['', 'static'] or document.body.parentElement.style.position not in ['', 'static']
418       # Absolute positioning in the body will be relative to the page, not the 'initial containing block'
419       next.page.bottom = document.body.scrollHeight - top - height
420       next.page.right = document.body.scrollWidth - left - width
421
422     if @options.optimizations?.moveElement isnt false and not @targetModifier?
423       offsetParent = @cache 'target-offsetparent', => getOffsetParent @target
424       offsetPosition = @cache 'target-offsetparent-bounds', -> getBounds offsetParent
425       offsetParentStyle = getComputedStyle offsetParent
426       elementStyle = getComputedStyle @element
427       offsetParentSize = offsetPosition
428
429       offsetBorder = {}
430       for side in ['Top', 'Left', 'Bottom', 'Right']
431         offsetBorder[side.toLowerCase()] = parseFloat offsetParentStyle["border#{ side }Width"]
432
433       offsetPosition.right = document.body.scrollWidth - offsetPosition.left - offsetParentSize.width + offsetBorder.right
434       offsetPosition.bottom = document.body.scrollHeight - offsetPosition.top - offsetParentSize.height + offsetBorder.bottom
435
436       if next.page.top >= (offsetPosition.top + offsetBorder.top) and next.page.bottom >= offsetPosition.bottom
437         if next.page.left >= (offsetPosition.left + offsetBorder.left) and next.page.right >= offsetPosition.right
438           # We're within the visible part of the target's scroll parent
439
440           scrollTop = offsetParent.scrollTop
441           scrollLeft = offsetParent.scrollLeft
442
443           # It's position relative to the target's offset parent (absolute positioning when
444           # the element is moved to be a child of the target's offset parent).
445           next.offset =
446             top: next.page.top - offsetPosition.top + scrollTop - offsetBorder.top
447             left: next.page.left - offsetPosition.left + scrollLeft - offsetBorder.left
448
449
450     # We could also travel up the DOM and try each containing context, rather than only
451     # looking at the body, but we're gonna get diminishing returns.
452
453     @move next
454
455     @history.unshift next
456
457     if @history.length > 3
458       @history.pop()
459
460     if flushChanges
461       flush()
462
463     true
464
465   move: (position) ->
466     return if not @element.parentNode?
467
468     same = {}
469
470     for type of position
471       same[type] = {}
472
473       for key of position[type]
474         found = false
475
476         for point in @history
477           unless within(point[type]?[key], position[type][key])
478             found = true
479             break
480
481         if not found
482           same[type][key] = true
483
484     css = {top: '', left: '', right: '', bottom: ''}
485
486     transcribe = (same, pos) =>
487       if @options.optimizations?.gpu isnt false
488         if same.top
489           css.top = 0
490           yPos = pos.top
491         else
492           css.bottom = 0
493           yPos = -pos.bottom
494
495         if same.left
496           css.left = 0
497           xPos = pos.left
498         else
499           css.right = 0
500           xPos = -pos.right
501
502
503         css[transformKey] = "translateX(#{ Math.round xPos }px) translateY(#{ Math.round yPos }px)"
504
505         if transformKey isnt 'msTransform'
506           # The Z transform will keep this in the GPU (faster, and prevents artifacts),
507           # but IE9 doesn't support 3d transforms and will choke.
508           css[transformKey] += " translateZ(0)"
509
510       else
511         if same.top
512           css.top = "#{ pos.top }px"
513         else
514           css.bottom = "#{ pos.bottom }px"
515
516         if same.left
517           css.left = "#{ pos.left }px"
518         else
519           css.right = "#{ pos.right }px"
520
521     moved = false
522     if (same.page.top or same.page.bottom) and (same.page.left or same.page.right)
523       css.position = 'absolute'
524       transcribe same.page, position.page
525
526     else if (same.viewport.top or same.viewport.bottom) and (same.viewport.left or same.viewport.right)
527       css.position = 'fixed'
528
529       transcribe same.viewport, position.viewport
530
531     else if same.offset? and same.offset.top and same.offset.left
532       css.position = 'absolute'
533
534       offsetParent = @cache 'target-offsetparent', => getOffsetParent @target
535
536       if getOffsetParent(@element) isnt offsetParent
537         defer =>
538           @element.parentNode.removeChild @element
539           offsetParent.appendChild @element
540
541       transcribe same.offset, position.offset
542
543       moved = true
544
545     else
546       css.position = 'absolute'
547       transcribe {top: true, left: true}, position.page
548
549     if not moved and @element.parentNode.tagName isnt 'BODY'
550       @element.parentNode.removeChild @element
551       document.body.appendChild @element
552
553     # Any css change will trigger a repaint, so let's avoid one if nothing changed
554     writeCSS = {}
555     write = false
556     for key, val of css
557       elVal = @element.style[key]
558
559       if elVal isnt '' and val isnt '' and key in ['top', 'left', 'bottom', 'right']
560         elVal = parseFloat elVal
561         val = parseFloat val
562
563       if elVal isnt val
564         write = true
565         writeCSS[key] = css[key]
566
567     if write
568       defer =>
569         extend @element.style, writeCSS
570
571 Tether.position = position
572
573 @Tether = extend _Tether, Tether