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