]> git.gir.st - VimFx.git/blob - extension/lib/help.coffee
Fix crash when showing the help dialog
[VimFx.git] / extension / lib / help.coffee
1 ###
2 # Copyright Simon Lydell 2015, 2016.
3 #
4 # This file is part of VimFx.
5 #
6 # VimFx is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # VimFx is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
18 ###
19
20 # This file creates VimFx’s Keyboard Shortcuts help screen.
21
22 translate = require('./l10n')
23 utils = require('./utils')
24
25 CONTAINER_ID = 'VimFxHelpDialogContainer'
26 MAX_FONT_SIZE = 20
27 SEARCH_MATCH_CLASS = 'search-match'
28 SEARCH_NON_MATCH_CLASS = 'search-non-match'
29 SEARCH_HIGHLIGHT_CLASS = 'search-highlight'
30
31 shutdownHandlerAdded = false
32
33 injectHelp = (window, vimfx) ->
34 removeHelp(window)
35
36 {document} = window
37
38 container = utils.createBox(document)
39 container.id = CONTAINER_ID
40
41 wrapper = utils.createBox(document, 'wrapper', container)
42
43 header = createHeader(window, vimfx)
44 wrapper.appendChild(header)
45
46 content = createContent(window, vimfx)
47 wrapper.appendChild(content)
48
49 searchInput = document.createElement('textbox')
50 utils.setAttributes(searchInput, {
51 class: 'search-input'
52 placeholder: translate('help.search')
53 })
54 searchInput.oninput = -> search(content, searchInput.value.trimLeft())
55 searchInput.onkeydown = (event) -> searchInput.blur() if event.key == 'Enter'
56 container.appendChild(searchInput)
57
58 window.gBrowser.selectedBrowser.parentNode.appendChild(container)
59
60 # The font size of menu items is used by default, which is usually quite
61 # small. Try to increase it without causing a scrollbar.
62 computedStyle = window.getComputedStyle(container)
63 fontSize = originalFontSize =
64 parseFloat(computedStyle.getPropertyValue('font-size'))
65 while wrapper.scrollTopMax == 0 and fontSize <= MAX_FONT_SIZE
66 fontSize += 1
67 container.style.fontSize = "#{fontSize}px"
68 container.style.fontSize = "#{Math.max(fontSize - 1, originalFontSize)}px"
69
70 # Uncomment this line if you want to use `gulp help.html`!
71 # utils.writeToClipboard(container.outerHTML)
72
73 unless shutdownHandlerAdded
74 module.onShutdown(removeHelp.bind(null, window))
75 shutdownHandlerAdded = true
76
77 removeHelp = (window) -> getHelp(window)?.remove()
78
79 getHelp = (window) -> window.document.getElementById(CONTAINER_ID)
80
81 getSearchInput = (window) -> getHelp(window)?.querySelector('.search-input')
82
83 createHeader = (window, vimfx) ->
84 $ = utils.createBox.bind(null, window.document)
85
86 header = $('header')
87
88 mainHeading = $('heading-main', header)
89 $('logo', mainHeading) # Content is added by CSS.
90 $('title', mainHeading, translate('help.title'))
91
92 closeButton = $('close-button', header, '×')
93 closeButton.onclick = removeHelp.bind(null, window)
94
95 return header
96
97 createContent = (window, vimfx) ->
98 $ = utils.createBox.bind(null, window.document)
99
100 content = $('content')
101
102 for mode in vimfx.getGroupedCommands({enabledOnly: true})
103 modeHeading = $('heading-mode search-item', null, mode.name)
104
105 for category, index in mode.categories
106 categoryContainer = $('category', content)
107 # `data-` attributes are currently unused by VimFx, but provide a great
108 # way to customize the help dialog with custom CSS.
109 utils.setAttributes(categoryContainer, {
110 'data-mode': mode._name
111 'data-category': category._name
112 })
113
114 # Append the mode heading inside the first category container, rather than
115 # before it, for layout purposes.
116 if index == 0
117 categoryContainer.appendChild(modeHeading)
118 categoryContainer.classList.add('first')
119
120 if category.name
121 $('heading-category search-item', categoryContainer, category.name)
122
123 for {command, name, enabledSequences} in category.commands
124 commandContainer = $('command search-item', categoryContainer)
125 commandContainer.setAttribute('data-command', command.name)
126 commandContainer.onclick = goToCommandSetting.bind(null, window, vimfx,
127 command)
128 for sequence in enabledSequences
129 keySequence = $('key-sequence', commandContainer)
130 [specialKeys, rest] =
131 splitSequence(sequence, Object.keys(vimfx.SPECIAL_KEYS))
132 $('key-sequence-special-keys', keySequence, specialKeys)
133 $('key-sequence-rest search-text', keySequence, rest)
134 $('description search-text', commandContainer, command.description)
135
136 return content
137
138 splitSequence = (sequence, specialKeys) ->
139 specialKeyEnds = specialKeys.map((key) ->
140 pos = sequence.lastIndexOf(key)
141 return if pos == -1 then 0 else pos + key.length
142 )
143 splitPos = Math.max(specialKeyEnds...)
144 return [sequence[0...splitPos], sequence[splitPos..]]
145
146 goToCommandSetting = (window, vimfx, command) ->
147 vimfx.goToCommand = command
148 removeHelp(window)
149 # Randomize URI to force a reload of the Add-ons Manager if it’s already open.
150 uri = "addons://detail/#{vimfx.id}/preferences?#{Math.random()}"
151 utils.nextTick(window, ->
152 window.BrowserOpenAddonsMgr(uri)
153 )
154
155 search = (content, term) ->
156 document = content.ownerDocument
157 ignoreCase = (term == term.toLowerCase())
158 regex = RegExp("(#{utils.regexEscape(term)})", if ignoreCase then 'i' else '')
159 clear = (term == '')
160
161 for item in content.querySelectorAll('.search-item')
162 texts = item.querySelectorAll('.search-text')
163 texts = [item] if texts.length == 0
164 className = SEARCH_NON_MATCH_CLASS
165
166 for element in texts
167 {textContent} = element
168 # Clear the previous highlighting. This is possible to do for non-matches
169 # as well, but too slow.
170 if item.classList.contains(SEARCH_MATCH_CLASS)
171 element.textContent = textContent
172
173 continue if clear or not regex.test(textContent)
174
175 className = SEARCH_MATCH_CLASS
176 element.textContent = '' # Empty the element.
177 for part, index in textContent.split(regex)
178 # Even indices are surrounding text, odd ones are matches.
179 if index % 2 == 0
180 element.appendChild(document.createTextNode(part))
181 else
182 utils.createBox(document, SEARCH_HIGHLIGHT_CLASS, element, part)
183
184 item.classList.remove(SEARCH_MATCH_CLASS, SEARCH_NON_MATCH_CLASS)
185 item.classList.add(className) unless clear
186
187 return
188
189 module.exports = {
190 injectHelp
191 removeHelp
192 getHelp
193 getSearchInput
194 }
Imprint / Impressum