]> git.gir.st - VimFx.git/blob - extension/packages/mode-hints/hints.coffee
Merge branch 'release-0.5.9'
[VimFx.git] / extension / packages / mode-hints / hints.coffee
1 utils = require 'utils'
2 { getPref } = require 'prefs'
3 { Marker } = require 'mode-hints/marker'
4 { addHuffmanCodeWordsTo } = require 'mode-hints/huffman'
5
6 { interfaces: Ci } = Components
7
8 HTMLDocument = Ci.nsIDOMHTMLDocument
9 XULDocument = Ci.nsIDOMXULDocument
10
11 CONTAINER_ID = 'VimFxHintMarkerContainer'
12 Z_INDEX_START = 100000000 # The highest `z-index` used in style.css plus one
13
14 # Remove previously injected hints from the DOM
15 removeHints = (document) ->
16 document.getElementById(CONTAINER_ID)?.remove()
17
18 for frame in document.defaultView.frames
19 removeHints(frame.document)
20
21
22 injectHints = (document) ->
23 markers = createMarkers(document)
24 hintChars = utils.getHintChars()
25
26 # Each marker gets a unique `z-index`, so that it can be determined if a marker overlaps another.
27 # Put more important markers (higher weight) at the end, so that they get higher `z-index`, in
28 # order not to be overlapped.
29 markers.sort((a, b) -> a.weight - b.weight)
30 for marker, index in markers
31 marker.markerElement.style.setProperty('z-index', Z_INDEX_START + index, 'important')
32
33 addHuffmanCodeWordsTo(markers, {alphabet: hintChars}, (marker, hint) -> marker.setHint(hint))
34
35 removeHints(document)
36 insertHints(markers)
37
38 # Must be done after the hints have been inserted into the DOM (see marker.coffee)
39 for marker in markers
40 marker.completePosition()
41
42 return markers
43
44
45 insertHints = (markers) ->
46 docFrags = []
47
48 getFrag = (document) ->
49 for [doc, frag] in docFrags
50 if document == doc
51 return frag
52
53 for marker in markers
54 doc = marker.element.ownerDocument
55 if not getFrag(doc)
56 docFrags.push([doc, doc.createDocumentFragment()])
57
58 frag = getFrag(doc)
59 frag.appendChild(marker.markerElement)
60
61 for [doc, frag] in docFrags
62 container = createHintsContainer(doc)
63 container.appendChild(frag)
64 doc.documentElement.appendChild(container)
65
66
67 createMarkers = (document) ->
68 # For now we aren't able to handle hint markers in XUL Documents :(
69 if document instanceof HTMLDocument # or document instanceof XULDocument
70 if document.documentElement
71 # Select all markable elements in the document, create markers
72 # for each of them, and position them on the page.
73 # Note that the markers are not given hints.
74 set = utils.getMarkableElements(document, {type: 'all'})
75 markers = []
76 for element in set
77 if rect = getElementRect(element)
78 marker = new Marker(element)
79 marker.setPosition(rect.top, rect.left)
80 marker.weight = rect.area * marker.calcBloomRating()
81
82 markers.push(marker)
83
84 for frame in document.defaultView.frames
85 markers = markers.concat(createMarkers(frame.document))
86
87 return markers or []
88
89
90 createHintsContainer = (document) ->
91 container = utils.createElement(document, 'div', {id: CONTAINER_ID})
92 return container
93
94
95 # Uses `element.getBoundingClientRect()`. If that does not return a visible rectange, then looks at
96 # the children of the markable node.
97 #
98 # The logic has been copied over from Vimiun originally.
99 getElementRect = (element) ->
100 document = element.ownerDocument
101 window = document.defaultView
102 docElem = document.documentElement
103 body = document.body
104
105 return unless utils.isElementVisible(element)
106
107 clientTop = docElem.clientTop or body?.clientTop or 0
108 clientLeft = docElem.clientLeft or body?.clientLeft or 0
109 scrollTop = window.pageYOffset or docElem.scrollTop
110 scrollLeft = window.pageXOffset or docElem.scrollLeft
111
112 clientRect = element.getBoundingClientRect()
113
114 if isRectOk(clientRect, window)
115 return {
116 top: clientRect.top + scrollTop - clientTop
117 left: clientRect.left + scrollLeft - clientLeft
118 width: clientRect.width
119 height: clientRect.height
120 area: clientRect.width * clientRect.height
121 }
122
123 # If the rect has 0 dimensions, then check what's inside.
124 # Floated or absolutely positioned elements are of particular interest.
125 if clientRect.width is 0 or clientRect.height is 0
126 for childElement in element.children
127 if computedStyle = window.getComputedStyle(childElement, null)
128 if computedStyle.getPropertyValue('float') != 'none' or \
129 computedStyle.getPropertyValue('position') == 'absolute'
130
131 return getElementRect(childElement)
132
133 return
134
135
136 # Checks if the given TextRectangle object qualifies
137 # for its own Marker with respect to the `window` object
138 isRectOk = (rect, window) ->
139 minimum = 2
140 rect.width > minimum and rect.height > minimum and \
141 rect.top > -minimum and rect.left > -minimum and \
142 rect.top < window.innerHeight - minimum and \
143 rect.left < window.innerWidth - minimum
144
145
146
147 # Finds all stacks of markers that overlap each other (by using `getStackFor`) (#1), and rotates
148 # their `z-index`:es (#2), thus alternating which markers are visible.
149 rotateOverlappingMarkers = (originalMarkers, forward) ->
150 # Shallow working copy. This is necessary since `markers` will be mutated and eventually empty.
151 markers = originalMarkers[..]
152
153 # (#1)
154 stacks = (getStackFor(markers.pop(), markers) while markers.length > 0)
155
156 # (#2)
157 # Stacks of length 1 don't participate in any overlapping, and can therefore be skipped.
158 for stack in stacks when stack.length > 1
159 # This sort is not required, but makes the rotation more predictable.
160 stack.sort((a, b) -> a.markerElement.style.zIndex - b.markerElement.style.zIndex)
161
162 # Array of z-indices
163 indexStack = (marker.markerElement.style.zIndex for marker in stack)
164 # Shift the array of indices one item forward or back
165 if forward
166 indexStack.unshift(indexStack.pop())
167 else
168 indexStack.push(indexStack.shift())
169
170 for marker, index in stack
171 marker.markerElement.style.setProperty('z-index', indexStack[index], 'important')
172
173 return
174
175 # Get an array containing `marker` and all markers that overlap `marker`, if any, which is called
176 # a "stack". All markers in the returned stack are spliced out from `markers`, thus mutating it.
177 getStackFor = (marker, markers) ->
178 stack = [marker]
179
180 { top, bottom, left, right } = marker.position
181
182 index = 0
183 while index < markers.length
184 nextMarker = markers[index]
185
186 { top: nextTop, bottom: nextBottom, left: nextLeft, right: nextRight } = nextMarker.position
187 overlapsVertically = (nextBottom >= top and nextTop <= bottom)
188 overlapsHorizontally = (nextRight >= left and nextLeft <= right)
189
190 if overlapsVertically and overlapsHorizontally
191 # Also get all markers overlapping this one
192 markers.splice(index, 1)
193 stack = stack.concat(getStackFor(nextMarker, markers))
194 else
195 # Continue the search
196 index++
197
198 return stack
199
200
201 exports.injectHints = injectHints
202 exports.removeHints = removeHints
203 exports.rotateOverlappingMarkers = rotateOverlappingMarkers
Imprint / Impressum