2 # Copyright Simon Lydell 2015, 2016.
4 # This file is part of VimFx.
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.
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.
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/>.
20 # This file creates VimFx’s Keyboard Shortcuts help screen.
22 translate = require('./translate')
23 utils = require('./utils')
25 CONTAINER_ID = 'VimFxHelpDialogContainer'
27 SEARCH_MATCH_CLASS = 'search-match'
28 SEARCH_NON_MATCH_CLASS = 'search-non-match'
29 SEARCH_HIGHLIGHT_CLASS = 'search-highlight'
31 injectHelp = (window, vimfx) ->
36 container = utils.createBox(document)
37 container.id = CONTAINER_ID
39 wrapper = utils.createBox(document, 'wrapper', container)
41 header = createHeader(window, vimfx)
42 wrapper.appendChild(header)
44 content = createContent(window, vimfx)
45 wrapper.appendChild(content)
47 searchInput = document.createElement('textbox')
48 utils.setAttributes(searchInput, {
50 placeholder: translate('help.search')
52 searchInput.oninput = -> search(content, searchInput.value.trimLeft())
53 searchInput.onkeydown = (event) -> searchInput.blur() if event.key == 'Enter'
54 container.appendChild(searchInput)
56 window.gBrowser.selectedBrowser.parentNode.appendChild(container)
58 # The font size of menu items is used by default, which is usually quite
59 # small. Try to increase it without causing a scrollbar.
60 computedStyle = window.getComputedStyle(container)
61 fontSize = originalFontSize =
62 parseFloat(computedStyle.getPropertyValue('font-size'))
63 while wrapper.scrollTopMax == 0 and fontSize <= MAX_FONT_SIZE
65 container.style.fontSize = "#{fontSize}px"
66 container.style.fontSize = "#{Math.max(fontSize - 1, originalFontSize)}px"
68 # Uncomment this line if you want to use `gulp help.html`!
69 # utils.writeToClipboard(container.outerHTML)
71 removeHelp = (window) -> getHelp(window)?.remove()
73 toggleHelp = (window, vimfx) ->
74 helpContainer = getHelp(window)
76 helpContainer.remove()
78 injectHelp(window, vimfx)
80 getHelp = (window) -> window.document.getElementById(CONTAINER_ID)
82 getSearchInput = (window) -> getHelp(window)?.querySelector('.search-input')
84 createHeader = (window, vimfx) ->
85 $ = utils.createBox.bind(null, window.document)
89 mainHeading = $('heading-main', header)
90 $('logo', mainHeading) # Content is added by CSS.
91 $('title', mainHeading, translate('help.title'))
93 closeButton = $('close-button', header, '×')
94 closeButton.onclick = -> removeHelp(window)
98 createContent = (window, vimfx) ->
99 $ = utils.createBox.bind(null, window.document)
100 extraCommands = getExtraCommands(vimfx)
102 content = $('content')
104 for mode in vimfx.getGroupedCommands({enabledOnly: true})
105 modeHeading = $('heading-mode search-item', null, mode.name)
107 for category, index in mode.categories
108 categoryContainer = $('category', content)
109 utils.setAttributes(categoryContainer, {
110 'data-mode': mode._name
111 'data-category': category._name
114 # Append the mode heading inside the first category container, rather than
115 # before it, for layout purposes.
117 categoryContainer.appendChild(modeHeading)
118 categoryContainer.classList.add('first')
121 $('heading-category search-item', categoryContainer, category.name)
123 for {command, name, enabledSequences} in category.commands
124 commandContainer = $('command has-click search-item', categoryContainer)
125 commandContainer.setAttribute('data-command', name)
126 commandContainer.onclick = goToCommandSetting.bind(
127 null, window, vimfx, command
129 for sequence in enabledSequences
130 keySequence = $('key-sequence', commandContainer)
131 [specialKeys, rest] =
132 splitSequence(sequence, Object.keys(vimfx.SPECIAL_KEYS))
133 $('key-sequence-special-keys', keySequence, specialKeys)
134 $('key-sequence-rest search-text', keySequence, rest)
135 $('description search-text', commandContainer, command.description)
137 categoryExtraCommands = extraCommands[mode._name]?[category._name]
138 if categoryExtraCommands
139 for name, sequences of categoryExtraCommands when sequences.length > 0
140 commandContainer = $('command search-item', categoryContainer)
141 commandContainer.setAttribute('data-command', name)
142 for sequence in sequences
143 keySequence = $('key-sequence', commandContainer)
144 $('key-sequence-rest search-text', keySequence, sequence)
145 description = translate("mode.#{mode._name}.#{name}")
146 $('description search-text', commandContainer, description)
150 getExtraCommands = (vimfx) ->
151 lastHintChar = translate('help.last_hint_char')
156 if vimfx.options['hints.peek_through']
157 [vimfx.options['hints.peek_through']]
161 if vimfx.options['hints.toggle_in_tab']
162 ["#{vimfx.options['hints.toggle_in_tab']}#{lastHintChar}>"]
165 'toggle_in_background':
166 if vimfx.options['hints.toggle_in_background']
167 ["#{vimfx.options['hints.toggle_in_background']}#{lastHintChar}>"]
174 splitSequence = (sequence, specialKeys) ->
175 specialKeyEnds = specialKeys.map((key) ->
176 pos = sequence.lastIndexOf(key)
177 return if pos == -1 then 0 else pos + key.length
179 splitPos = Math.max(specialKeyEnds...)
180 return [sequence[0...splitPos], sequence[splitPos..]]
182 goToCommandSetting = (window, vimfx, command) ->
183 vimfx.goToCommand = command
185 # Randomize URI to force a reload of the Add-ons Manager if it’s already open.
186 uri = "addons://detail/#{vimfx.id}/preferences?#{Math.random()}"
187 utils.nextTick(window, ->
188 window.BrowserOpenAddonsMgr(uri)
191 search = (content, term) ->
192 document = content.ownerDocument
193 ignoreCase = (term == term.toLowerCase())
194 regex = RegExp("(#{utils.regexEscape(term)})", if ignoreCase then 'i' else '')
197 for item in content.querySelectorAll('.search-item')
198 texts = item.querySelectorAll('.search-text')
199 texts = [item] if texts.length == 0
200 className = SEARCH_NON_MATCH_CLASS
203 {textContent} = element
204 # Clear the previous highlighting. This is possible to do for non-matches
205 # as well, but too slow.
206 if item.classList.contains(SEARCH_MATCH_CLASS)
207 element.textContent = textContent
209 continue if clear or not regex.test(textContent)
211 className = SEARCH_MATCH_CLASS
212 element.textContent = '' # Empty the element.
213 for part, index in textContent.split(regex)
214 # Even indices are surrounding text, odd ones are matches.
216 element.appendChild(document.createTextNode(part))
218 utils.createBox(document, SEARCH_HIGHLIGHT_CLASS, element, part)
220 item.classList.remove(SEARCH_MATCH_CLASS, SEARCH_NON_MATCH_CLASS)
221 item.classList.add(className) unless clear