]> git.gir.st - VimFx.git/blob - extension/lib/marker.coffee
Merge pull request #461 from lydell/scroll
[VimFx.git] / extension / lib / marker.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013.
3 # Copyright Simon Lydell 2013, 2014.
4 #
5 # This file is part of VimFx.
6 #
7 # VimFx is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # VimFx is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
19 ###
20
21 # Wraps the markable element and provides methods to manipulate the markers.
22 class Marker
23 # Creates the marker DOM node.
24 constructor: (@element, @elementShape, { @semantic, @type }) ->
25 document = @element.ownerDocument
26 @markerElement = document.createElement('div')
27 @markerElement.classList.add('VimFxHintMarker')
28 @weight = @elementShape.area
29 @numChildren = 0
30
31 reset: ->
32 @setHint(@hint)
33 @show()
34
35 show: -> @setVisibility(true)
36 hide: -> @setVisibility(false)
37 setVisibility: (visible) ->
38 @markerElement.classList.toggle('VimFxHiddenHintMarker', not visible)
39
40 # To be called when the marker has been both assigned a hint and inserted
41 # into the DOM, and thus gotten a height and width.
42 setPosition: (viewport) ->
43 {
44 markerElement: { clientHeight: height, clientWidth: width }
45 elementShape: { nonCoveredPoint: { x: left, y: top, offset, rect } }
46 } = this
47
48 # Center the marker vertically on the non-covered point.
49 top -= Math.ceil(height / 2)
50
51 # Make sure that the marker stays within its element (vertically).
52 top = Math.min(top, rect.bottom - height)
53 top = Math.max(top, rect.top)
54
55 # Make the position relative to the top frame.
56 left += offset.left
57 top += offset.top
58
59 # Make sure that the marker stays within the viewport.
60 left = Math.min(left, viewport.right - width)
61 top = Math.min(top, viewport.bottom - height)
62 left = Math.max(left, viewport.left)
63 top = Math.max(top, viewport.top)
64
65 # Take the current zoom into account.
66 left = Math.round(left * viewport.zoom)
67 top = Math.round(top * viewport.zoom)
68
69 # The positioning is absolute.
70 @markerElement.style.left = "#{ left }px"
71 @markerElement.style.top = "#{ top }px"
72
73 # For quick access.
74 @position = {
75 left, right: left + width,
76 top, bottom: top + height,
77 height, width
78 }
79
80 setHint: (@hint) ->
81 @hintIndex = 0
82
83 document = @element.ownerDocument
84
85 while @markerElement.hasChildNodes()
86 @markerElement.firstChild.remove()
87
88 fragment = document.createDocumentFragment()
89 for char in @hint
90 charContainer = document.createElement('span')
91 charContainer.textContent = char
92 fragment.appendChild(charContainer)
93
94 @markerElement.appendChild(fragment)
95
96 matchHintChar: (char) ->
97 if char == @hint[@hintIndex]
98 @toggleLastHintChar(true)
99 @hintIndex++
100 return true
101 return false
102
103 deleteHintChar: ->
104 if @hintIndex > 0
105 @hintIndex--
106 @toggleLastHintChar(false)
107
108 toggleLastHintChar: (visible) ->
109 @markerElement.children[@hintIndex]
110 .classList.toggle('VimFxCharMatch', visible)
111
112 isMatched: -> (@hintIndex == @hint.length)
113
114 markMatched: (matched) ->
115 @markerElement.classList.toggle('VimFxMatchedHintMarker', matched)
116
117 # Finds all stacks of markers that overlap each other (by using `getStackFor`)
118 # (#1), and rotates their `z-index`:es (#2), thus alternating which markers are
119 # visible.
120 rotateOverlappingMarkers = (originalMarkers, forward) ->
121 # Shallow working copy. This is necessary since `markers` will be mutated and
122 # eventually empty.
123 markers = originalMarkers[..]
124
125 # (#1)
126 stacks = (getStackFor(markers.pop(), markers) while markers.length > 0)
127
128 # (#2)
129 # Stacks of length 1 don't participate in any overlapping, and can therefore
130 # be skipped.
131 for stack in stacks when stack.length > 1
132 # This sort is not required, but makes the rotation more predictable.
133 stack.sort((a, b) -> a.markerElement.style.zIndex -
134 b.markerElement.style.zIndex)
135
136 # Array of z-indices.
137 indexStack = (marker.markerElement.style.zIndex for marker in stack)
138 # Shift the array of indices one item forward or back.
139 if forward
140 indexStack.unshift(indexStack.pop())
141 else
142 indexStack.push(indexStack.shift())
143
144 for marker, index in stack
145 marker.markerElement.style.zIndex = indexStack[index]
146
147 return
148
149 # Get an array containing `marker` and all markers that overlap `marker`, if
150 # any, which is called a "stack". All markers in the returned stack are spliced
151 # out from `markers`, thus mutating it.
152 getStackFor = (marker, markers) ->
153 stack = [marker]
154
155 { top, bottom, left, right } = marker.position
156
157 index = 0
158 while index < markers.length
159 nextMarker = markers[index]
160
161 next = nextMarker.position
162 overlapsVertically = (next.bottom >= top and next.top <= bottom)
163 overlapsHorizontally = (next.right >= left and next.left <= right)
164
165 if overlapsVertically and overlapsHorizontally
166 # Also get all markers overlapping this one.
167 markers.splice(index, 1)
168 stack = stack.concat(getStackFor(nextMarker, markers))
169 else
170 # Continue the search.
171 index++
172
173 return stack
174
175 exports.Marker = Marker
176 exports.rotateOverlappingMarkers = rotateOverlappingMarkers
Imprint / Impressum