1 utils = require 'utils'
2 { getPref } = require 'prefs'
3 { Marker } = require 'marker'
4 { addHuffmanCodeWordsTo } = require 'huffman'
6 { interfaces: Ci } = Components
8 HTMLDocument = Ci.nsIDOMHTMLDocument
9 XULDocument = Ci.nsIDOMXULDocument
10 XPathResult = Ci.nsIDOMXPathResult
12 CONTAINER_ID = 'VimFxHintMarkerContainer'
14 # All the following elements qualify for their own marker in hints mode
22 "input[not(@type='hidden' or @disabled or @readonly)]"
27 # All elements that have one or more of the following properties
28 # qualify for their own marker in hints mode
29 MARKABLE_ELEMENT_PROPERTIES = [
37 "contains(@class, 'button')"
38 "contains(@class, 'js-new-tweets-bar')"
39 "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true'"
43 # Remove previously injected hints from the DOM
44 removeHints = (document) ->
45 if container = document.getElementById(CONTAINER_ID)
46 document.documentElement.removeChild(container)
48 for frame in document.defaultView.frames
49 removeHints(frame.document)
52 # Like `insertHints`, but also sets hints for the markers
53 injectHints = (document) ->
54 markers = createMarkers(document)
55 hintChars = utils.getHintChars()
57 addHuffmanCodeWordsTo(markers, {alphabet: hintChars}, (marker, hint) -> marker.setHint(hint))
62 # Must be done after the hints have been inserted into the DOM (see marker.coffee)
64 marker.completePosition()
68 insertHints = (markers) ->
71 getFrag = (document) ->
72 for [doc, frag] in docFrags
77 doc = marker.element.ownerDocument
79 docFrags.push([doc, doc.createDocumentFragment()])
82 frag.appendChild(marker.markerElement)
84 for [doc, frag] in docFrags
85 container = createHintsContainer(doc)
86 container.appendChild(frag)
87 doc.documentElement.appendChild(container)
90 # Creates and injects markers into the DOM
91 createMarkers = (document) ->
92 # For now we aren't able to handle hint markers in XUL Documents :(
93 if document instanceof HTMLDocument# or document instanceof XULDocument
94 if document.documentElement
95 # Select all markable elements in the document, create markers
96 # for each of them, and position them on the page.
97 # Note that the markers are not given hints.
98 set = getMarkableElements(document)
100 for i in [0...set.snapshotLength] by 1
101 element = set.snapshotItem(i)
102 if rect = getElementRect(element)
103 marker = new Marker(element)
104 marker.setPosition(rect.top, rect.left)
105 marker.weight = rect.area * marker.calcBloomRating()
109 for frame in document.defaultView.frames
110 markers = markers.concat(createMarkers(frame.document))
115 createHintsContainer = (document) ->
116 container = document.createElement('div')
117 container.id = CONTAINER_ID
118 container.className = 'VimFxReset'
122 # Returns elements that qualify for hint markers in hints mode.
123 # Generates and memoizes an XPath query internally
124 getMarkableElements = do ->
125 # Some preparations done on startup
128 "*[#{ MARKABLE_ELEMENT_PROPERTIES.join(' or ') }]"
131 reduce = (m, rule) -> m.concat(["//#{ rule }", "//xhtml:#{ rule }"])
132 xpath = elements.reduce(reduce, []).join(' | ')
134 namespaceResolver = (namespace) ->
135 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
137 # The actual function that will return the desired elements
138 return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) ->
139 return document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null)
142 # Uses `element.getBoundingClientRect()`. If that does not return a visible rectange, then looks at
143 # the children of the markable node.
145 # The logic has been copied over from Vimiun originally.
146 getElementRect = (element) ->
147 document = element.ownerDocument
148 window = document.defaultView
149 docElem = document.documentElement
152 # Prune elements that aren't visible on the page
153 computedStyle = window.getComputedStyle(element, null)
155 if computedStyle.getPropertyValue('visibility') != 'visible' or \
156 computedStyle.getPropertyValue('display') == 'none' or \
157 computedStyle.getPropertyValue('opacity') == '0'
160 clientTop = docElem.clientTop or body?.clientTop or 0
161 clientLeft = docElem.clientLeft or body?.clientLeft or 0
162 scrollTop = window.pageYOffset or docElem.scrollTop
163 scrollLeft = window.pageXOffset or docElem.scrollLeft
165 clientRect = element.getBoundingClientRect()
167 if isRectOk(clientRect, window)
169 top: clientRect.top + scrollTop - clientTop
170 left: clientRect.left + scrollLeft - clientLeft
171 width: clientRect.width
172 height: clientRect.height
173 area: clientRect.width * clientRect.height
176 # If the rect has 0 dimensions, then check what's inside.
177 # Floated or absolutely positioned elements are of particular interest.
178 if clientRect.width is 0 or clientRect.height is 0
179 for childElement in element.children
180 if computedStyle = window.getComputedStyle(childElement, null)
181 if computedStyle.getPropertyValue('float') != 'none' or \
182 computedStyle.getPropertyValue('position') == 'absolute'
184 return getElementRect(childElement)
189 # Checks if the given TextRectangle object qualifies
190 # for its own Marker with respect to the `window` object
191 isRectOk = (rect, window) ->
193 rect.width > minimum and rect.height > minimum and \
194 rect.top > -minimum and rect.left > -minimum and \
195 rect.top < window.innerHeight - minimum and \
196 rect.left < window.innerWidth - minimum
199 exports.injectHints = injectHints
200 exports.removeHints = removeHints