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
11 HTMLAnchorElement = Ci.nsIDOMHTMLAnchorElement
13 CONTAINER_ID = 'VimFxHintMarkerContainer'
15 # All the following elements qualify for their own marker in hints mode
23 "input[not(@type='hidden' or @disabled or @readonly)]"
28 # All elements that have one or more of the following properties
29 # qualify for their own marker in hints mode
30 MARKABLE_ELEMENT_PROPERTIES = [
38 "contains(@class, 'button')"
39 "contains(@class, 'js-new-tweets-bar')"
40 "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true'"
44 # Remove previously injected hints from the DOM
45 removeHints = (document) ->
46 if container = document.getElementById(CONTAINER_ID)
47 document.documentElement.removeChild(container)
49 for frame in document.defaultView.frames
50 removeHints(frame.document)
53 # Like `injectMarkers`, but also sets hints for the markers
54 injectHints = (document) ->
55 markers = injectMarkers(document)
56 hintChars = utils.getHintChars()
58 addHuffmanCodeWordsTo(markers, {alphabet: hintChars}, (marker, hint) -> marker.setHint(hint))
63 # Creates and injects markers into the DOM
64 injectMarkers = (document) ->
65 # First remove previous hints container
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 # For performance use Document Fragment
72 fragment = document.createDocumentFragment()
74 # Select all markable elements in the document, create markers
75 # for each of them, and position them on the page.
76 # Note that the markers are not given hints.
77 set = getMarkableElements(document)
79 for i in [0...set.snapshotLength] by 1
80 element = set.snapshotItem(i)
81 if rect = getElementRect(element)
82 marker = new Marker(element)
84 marker.setPosition(rect)
85 fragment.appendChild(marker.markerElement)
87 marker.weight = rect.area * marker.calcBloomRating()
91 container = createHintsContainer(document)
92 container.appendChild(fragment)
93 document.documentElement.appendChild(container)
95 for frame in document.defaultView.frames
96 markers = markers.concat(injectMarkers(frame.document))
101 createHintsContainer = (document) ->
102 container = document.createElement('div')
103 container.id = CONTAINER_ID
104 container.className = 'VimFxReset'
108 # Returns elements that qualify for hint markers in hints mode.
109 # Generates and memoizes an XPath query internally
110 getMarkableElements = do ->
111 # Some preparations done on startup
114 "*[#{ MARKABLE_ELEMENT_PROPERTIES.join(' or ') }]"
117 reduce = (m, rule) -> m.concat(["//#{ rule }", "//xhtml:#{ rule }"])
118 xpath = elements.reduce(reduce, []).join(' | ')
120 namespaceResolver = (namespace) ->
121 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
123 # The actual function that will return the desired elements
124 return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) ->
125 return document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null)
128 # Uses `element.getBoundingClientRect()`. If that does not return a visible rectange, then looks at
129 # the children of the markable node.
131 # The logic has been copied over from Vimiun originally.
132 getElementRect = (element) ->
133 document = element.ownerDocument
134 window = document.defaultView
135 docElem = document.documentElement
138 clientTop = docElem.clientTop or body?.clientTop or 0
139 clientLeft = docElem.clientLeft or body?.clientLeft or 0
140 scrollTop = window.pageYOffset or docElem.scrollTop
141 scrollLeft = window.pageXOffset or docElem.scrollLeft
143 clientRect = element.getBoundingClientRect()
145 if isRectOk(clientRect, window)
147 top: clientRect.top + scrollTop - clientTop
148 left: clientRect.left + scrollLeft - clientLeft
149 width: clientRect.width
150 height: clientRect.height
151 area: clientRect.width * clientRect.height
154 # If the rect has 0 dimensions, then check what's inside.
155 # Floated or absolutely positioned elements are of particular interest.
156 if clientRect.width is 0 or clientRect.height is 0
157 for childElement in element.children
158 if computedStyle = window.getComputedStyle(childElement, null)
159 if computedStyle.getPropertyValue('float') != 'none' or \
160 computedStyle.getPropertyValue('position') == 'absolute'
162 return getElementRect(childElement)
167 # Checks if the given TextRectangle object qualifies
168 # for its own Marker with respect to the `window` object
169 isRectOk = (rect, window) ->
171 rect.width > minimum and rect.height > minimum and \
172 rect.top > -minimum and rect.left > -minimum and \
173 rect.top < window.innerHeight - minimum and \
174 rect.left < window.innerWidth - minimum
177 exports.injectHints = injectHints
178 exports.removeHints = removeHints