]> git.gir.st - VimFx.git/blob - extension/lib/marker.coffee
Merge pull request #668 from akhodakivskiy/devtoolszoom
[VimFx.git] / extension / lib / marker.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013.
3 # Copyright Simon Lydell 2013, 2014, 2015.
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 # This file contains an abstraction for hint markers. It creates the UI for a
22 # marker and provides methods to manipulate the markers.
23
24 utils = require('./utils')
25
26 class Marker
27 # `@wrapper` is a stand-in for the element that the marker represents. See
28 # `injectHints` in hints.coffee for more information.
29 constructor: (@wrapper, @document) ->
30 @elementShape = @wrapper.shape
31 @markerElement = utils.createBox(@document, 'marker')
32 @markerElement.setAttribute('data-type', @wrapper.type)
33 @weight = @elementShape.area
34
35 reset: ->
36 @setHint(@hint)
37 @show()
38
39 show: -> @setVisibility(true)
40 hide: -> @setVisibility(false)
41 setVisibility: (visible) ->
42 @markerElement.classList.toggle('marker--hidden', not visible)
43
44 # To be called when the marker has been both assigned a hint and inserted
45 # into the DOM, and thus gotten a height and width.
46 setPosition: (viewport, zoom) ->
47 {
48 markerElement: {clientHeight: height, clientWidth: width}
49 elementShape: {nonCoveredPoint: {x: left, y: top, offset}}
50 } = this
51
52 height /= zoom
53 width /= zoom
54
55 # Center the marker vertically on the non-covered point.
56 top -= Math.ceil(height / 2)
57
58 # Make the position relative to the top frame.
59 left += offset.left
60 top += offset.top
61
62 # Make sure that the marker stays within the viewport.
63 left = Math.min(left, viewport.right - width)
64 top = Math.min(top, viewport.bottom - height)
65 left = Math.max(left, viewport.left)
66 top = Math.max(top, viewport.top)
67
68 # Take the current zoom into account.
69 left = Math.round(left * zoom)
70 top = Math.round(top * zoom)
71
72 # The positioning is absolute.
73 @markerElement.style.left = "#{left}px"
74 @markerElement.style.top = "#{top}px"
75
76 # For quick access.
77 @position = {
78 left, right: left + width,
79 top, bottom: top + height,
80 height, width
81 }
82
83 setHint: (@hint) ->
84 @hintIndex = 0
85 @markerElement.textContent = ''
86 fragment = @document.createDocumentFragment()
87 utils.createBox(@document, 'marker-char', fragment, char) for char in @hint
88 @markerElement.appendChild(fragment)
89
90 matchHintChar: (char) ->
91 if char == @hint[@hintIndex]
92 @toggleLastHintChar(true)
93 @hintIndex++
94 return true
95 return false
96
97 deleteHintChar: ->
98 if @hintIndex > 0
99 @hintIndex--
100 @toggleLastHintChar(false)
101
102 toggleLastHintChar: (visible) ->
103 @markerElement.children[@hintIndex]
104 .classList.toggle('marker-char--matched', visible)
105
106 isMatched: -> (@hintIndex == @hint.length)
107
108 markMatched: (matched) ->
109 @markerElement.classList.toggle('marker--matched', matched)
110
111 # Finds all stacks of markers that overlap each other (by using `getStackFor`)
112 # (#1), and rotates their `z-index`:es (#2), thus alternating which markers are
113 # visible.
114 rotateOverlappingMarkers = (originalMarkers, forward) ->
115 # Shallow working copy. This is necessary since `markers` will be mutated and
116 # eventually empty.
117 markers = originalMarkers[..]
118
119 # (#1)
120 stacks = (getStackFor(markers.pop(), markers) while markers.length > 0)
121
122 # (#2)
123 # Stacks of length 1 don't participate in any overlapping, and can therefore
124 # be skipped.
125 for stack in stacks when stack.length > 1
126 # This sort is not required, but makes the rotation more predictable.
127 stack.sort((a, b) -> a.markerElement.style.zIndex -
128 b.markerElement.style.zIndex)
129
130 # Array of z-indices.
131 indexStack = (marker.markerElement.style.zIndex for marker in stack)
132 # Shift the array of indices one item forward or back.
133 if forward
134 indexStack.unshift(indexStack.pop())
135 else
136 indexStack.push(indexStack.shift())
137
138 for marker, index in stack
139 marker.markerElement.style.zIndex = indexStack[index]
140
141 return
142
143 # Get an array containing `marker` and all markers that overlap `marker`, if
144 # any, which is called a "stack". All markers in the returned stack are spliced
145 # out from `markers`, thus mutating it.
146 getStackFor = (marker, markers) ->
147 stack = [marker]
148
149 {top, bottom, left, right} = marker.position
150
151 index = 0
152 while index < markers.length
153 nextMarker = markers[index]
154
155 next = nextMarker.position
156 overlapsVertically = (next.bottom >= top and next.top <= bottom)
157 overlapsHorizontally = (next.right >= left and next.left <= right)
158
159 if overlapsVertically and overlapsHorizontally
160 # Also get all markers overlapping this one.
161 markers.splice(index, 1)
162 stack = stack.concat(getStackFor(nextMarker, markers))
163 else
164 # Continue the search.
165 index++
166
167 return stack
168
169 module.exports = {
170 Marker
171 rotateOverlappingMarkers
172 }
Imprint / Impressum