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