]> git.gir.st - VimFx.git/blob - extension/lib/scrollable-elements.coffee
Change license to MIT
[VimFx.git] / extension / lib / scrollable-elements.coffee
1 # This file contains an abstraction for keeping track of scrollable elements,
2 # automatically keeping the largest scrollable element up-to-date. It stops
3 # tracking elements that are removed from the DOM.
4
5 utils = require('./utils')
6
7 class ScrollableElements
8 constructor: (@window) ->
9 @elements = new Set()
10 @largest = null
11
12 MIN_SCROLL: 5
13 MIN_SCROLLABLE_ELEMENT_AREA: 25
14
15 # Even in quirks mode the 'overflow' event is triggered for `<html>`, _not_
16 # `<body>`. This method takes care of returning the appropriate element, so
17 # we don’t need to think about it anywhere else.
18 quirks: (element) ->
19 document = element.ownerDocument
20 if element == document.documentElement
21 return utils.getRootElement(document)
22 else
23 return element
24
25 # Note: Don’t use `@quirks` here. That causes a hint marker for `<html>` on
26 # quirks mode sites, such as Hackernews.
27 has: (element) -> @elements.has(element)
28
29 add: (element) ->
30 element = @quirks(element)
31 @elements.add(element)
32 @largest = element if @isLargest(element)
33
34 delete: (element) =>
35 element = @quirks(element)
36 @elements.delete(element)
37 @updateLargest() if @largest == element
38
39 reject: (fn) ->
40 @elements.forEach((element) => @elements.delete(element) if fn(element))
41 @updateLargest()
42
43 isScrollable: (element) ->
44 element = @quirks(element)
45 return element.scrollTopMax >= @MIN_SCROLL or
46 element.scrollLeftMax >= @MIN_SCROLL
47
48 addChecked: (element) ->
49 return unless computedStyle = @window.getComputedStyle(element)
50 unless (computedStyle.getPropertyValue('overflow-y') == 'hidden' and
51 computedStyle.getPropertyValue('overflow-x') == 'hidden') or
52 # There’s no need to track elements so small that they don’t even fit
53 # the scrollbars. For example, Gmail has lots of tiny overflowing
54 # iframes. Filter those out.
55 utils.area(element) < @MIN_SCROLLABLE_ELEMENT_AREA or
56 # On some pages, such as Google Groups, 'overflow' events may occur
57 # for elements that aren’t even scrollable.
58 not @isScrollable(element)
59 @add(element)
60
61 # The following scenario can happen (found on 2ality.com): First, the root
62 # element overflows just a pixel. That causes an 'overflow' event, but we
63 # don’t store it because the overflow is too small. Then, the overflow
64 # grows. That does _not_ cause new 'overflow' events. This way, the root
65 # element actually becomes scrollable, but we don’t get to know about it,
66 # making the root element impossible to scroll if there are other
67 # scrollable elements on the page. Therefore, always re-check the root
68 # element when adding new scrollable elements. This could in theory happen
69 # to _any_ scrollable element, but the by far most common thing is that
70 # the root element is scrollable.
71 root = @quirks(@window.document.documentElement)
72 unless @quirks(element) == root
73 @addChecked(root)
74
75 deleteChecked: (element) ->
76 # On some pages, such as Gmail, 'underflow' events may occur for elements
77 # that are actually still scrollable! If so, keep the element.
78 @delete(element) unless @isScrollable(element)
79
80 # It makes the most sense to consider the uppermost scrollable element the
81 # largest. In other words, if a scrollable element contains another scrollable
82 # element (or a frame containing one), the parent should be considered largest
83 # even if the child has greater area.
84 isLargest: (element) ->
85 return true unless @largest
86 return true if utils.containsDeep(element, @largest)
87 return false if utils.containsDeep(@largest, element)
88 return utils.area(element) > utils.area(@largest)
89
90 updateLargest: ->
91 # Reset `@largest` and find a new largest scrollable element (if there are
92 # any left).
93 @largest = null
94 @elements.forEach((element) => @largest = element if @isLargest(element))
95
96 # In theory, this method could return `@largest`. In reality, it is not that
97 # simple. Elements may overflow when zooming in or out, but the
98 # `.scrollHeight` of the element is not correctly updated when the 'overflow'
99 # event occurs, making it possible for unscrollable elements to slip in. So
100 # this method has to check whether the largest element really is scrollable,
101 # and update it if needed. In the case where there is no largest element
102 # (left), it _should_ mean that the page hasn’t got any scrollable elements,
103 # and the whole page itself isn’t scrollable. However, we cannot be 100% sure
104 # that nothing is scrollable (for example, if VimFx is updated in the middle
105 # of a session). So in that case, instead of simply returning `null`, return
106 # the entire page (the best bet). Not being able to scroll is very annoying.
107 filterSuitableDefault: ->
108 if @largest and @isScrollable(@largest)
109 return @largest
110 else
111 @reject((element) => not @isScrollable(element))
112 return @largest ? @quirks(@window.document.documentElement)
113
114 getPageScrollPosition: ->
115 element = @filterSuitableDefault()
116 if element.ownerDocument.documentElement.localName == 'svg'
117 return [element.ownerGlobal.scrollX, element.ownerGlobal.scrollY]
118 else
119 return [element.scrollLeft, element.scrollTop]
120
121 module.exports = ScrollableElements
Imprint / Impressum