]> git.gir.st - VimFx.git/blob - extension/lib/help.coffee
Port settings page to a modern format
[VimFx.git] / extension / lib / help.coffee
1 # This file creates VimFx’s Keyboard Shortcuts help screen.
2
3 translate = require('./translate')
4 utils = require('./utils')
5
6 CONTAINER_ID = 'VimFxHelpDialogContainer'
7 MAX_FONT_SIZE = 20
8 SEARCH_MATCH_CLASS = 'search-match'
9 SEARCH_NON_MATCH_CLASS = 'search-non-match'
10 SEARCH_HIGHLIGHT_CLASS = 'search-highlight'
11
12 injectHelp = (window, vimfx) ->
13 removeHelp(window)
14
15 {document} = window
16
17 container = utils.createBox(document)
18 container.id = CONTAINER_ID
19
20 wrapper = utils.createBox(document, 'wrapper', container)
21
22 header = createHeader(window, vimfx)
23 wrapper.appendChild(header)
24
25 content = createContent(window, vimfx)
26 wrapper.appendChild(content)
27
28 searchInput = document.createElement('textbox')
29 utils.setAttributes(searchInput, {
30 class: 'search-input'
31 placeholder: translate('help.search')
32 })
33 searchInput.oninput = -> search(content, searchInput.value.trimLeft())
34 searchInput.onkeydown = (event) -> searchInput.blur() if event.key == 'Enter'
35 container.appendChild(searchInput)
36
37 window.gBrowser.selectedBrowser.parentNode.appendChild(container)
38
39 # The font size of menu items is used by default, which is usually quite
40 # small. Try to increase it without causing a scrollbar.
41 computedStyle = window.getComputedStyle(container)
42 fontSize = originalFontSize =
43 parseFloat(computedStyle.getPropertyValue('font-size'))
44 while wrapper.scrollTopMax == 0 and fontSize <= MAX_FONT_SIZE
45 fontSize += 1
46 container.style.fontSize = "#{fontSize}px"
47 container.style.fontSize = "#{Math.max(fontSize - 1, originalFontSize)}px"
48
49 # Uncomment this line if you want to use `gulp help.html`!
50 # utils.writeToClipboard(container.outerHTML)
51
52 removeHelp = (window) -> getHelp(window)?.remove()
53
54 toggleHelp = (window, vimfx) ->
55 helpContainer = getHelp(window)
56 if helpContainer
57 helpContainer.remove()
58 else
59 injectHelp(window, vimfx)
60
61 getHelp = (window) -> window.document.getElementById(CONTAINER_ID)
62
63 getSearchInput = (window) -> getHelp(window)?.querySelector('.search-input')
64
65 createHeader = (window, vimfx) ->
66 $ = utils.createBox.bind(null, window.document)
67
68 header = $('header')
69
70 mainHeading = $('heading-main', header)
71 $('logo', mainHeading) # Content is added by CSS.
72 $('title', mainHeading, translate('help.title'))
73
74 closeButton = $('close-button', header, '×')
75 closeButton.onclick = -> removeHelp(window)
76
77 return header
78
79 createContent = (window, vimfx) ->
80 $ = utils.createBox.bind(null, window.document)
81 extraCommands = getExtraCommands(vimfx)
82
83 content = $('content')
84
85 for mode in vimfx.getGroupedCommands({enabledOnly: true})
86 modeHeading = $('heading-mode search-item', null, mode.name)
87
88 for category, index in mode.categories
89 categoryContainer = $('category', content)
90 utils.setAttributes(categoryContainer, {
91 'data-mode': mode._name
92 'data-category': category._name
93 })
94
95 # Append the mode heading inside the first category container, rather than
96 # before it, for layout purposes.
97 if index == 0
98 categoryContainer.appendChild(modeHeading)
99 categoryContainer.classList.add('first')
100
101 if category.name
102 $('heading-category search-item', categoryContainer, category.name)
103
104 for {command, name, enabledSequences} in category.commands
105 commandContainer = $('command has-click search-item', categoryContainer)
106 commandContainer.setAttribute('data-command', name)
107 commandContainer.onclick = goToCommandSetting.bind(
108 null, window, vimfx, command
109 )
110 for sequence in enabledSequences
111 keySequence = $('key-sequence', commandContainer)
112 [specialKeys, rest] =
113 splitSequence(sequence, Object.keys(vimfx.SPECIAL_KEYS))
114 $('key-sequence-special-keys', keySequence, specialKeys)
115 $('key-sequence-rest search-text', keySequence, rest)
116 $('description search-text', commandContainer, command.description)
117
118 categoryExtraCommands = extraCommands[mode._name]?[category._name]
119 if categoryExtraCommands
120 for name, sequences of categoryExtraCommands when sequences.length > 0
121 commandContainer = $('command search-item', categoryContainer)
122 commandContainer.setAttribute('data-command', name)
123 for sequence in sequences
124 keySequence = $('key-sequence', commandContainer)
125 $('key-sequence-rest search-text', keySequence, sequence)
126 description = translate("mode.#{mode._name}.#{name}")
127 $('description search-text', commandContainer, description)
128
129 return content
130
131 getExtraCommands = (vimfx) ->
132 lastHintChar = translate('help.last_hint_char')
133 return {
134 'hints': {
135 '': {
136 'peek_through':
137 if vimfx.options['hints.peek_through']
138 [vimfx.options['hints.peek_through']]
139 else
140 []
141 'toggle_in_tab':
142 if vimfx.options['hints.toggle_in_tab']
143 ["#{vimfx.options['hints.toggle_in_tab']}#{lastHintChar}>"]
144 else
145 []
146 'toggle_in_background':
147 if vimfx.options['hints.toggle_in_background']
148 ["#{vimfx.options['hints.toggle_in_background']}#{lastHintChar}>"]
149 else
150 []
151 }
152 }
153 }
154
155 splitSequence = (sequence, specialKeys) ->
156 specialKeyEnds = specialKeys.map((key) ->
157 pos = sequence.lastIndexOf(key)
158 return if pos == -1 then 0 else pos + key.length
159 )
160 splitPos = Math.max(specialKeyEnds...)
161 return [sequence[0...splitPos], sequence[splitPos..]]
162
163 goToCommandSetting = (window, vimfx, command) ->
164 vimfx.goToCommand = command
165 removeHelp(window)
166 uri = "#{ADDON_PATH}/content/options.xhtml"
167 utils.nextTick(window, ->
168 window.gBrowser.selectedTab = window.gBrowser.addTab(uri, {
169 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal()
170 })
171 )
172
173 search = (content, term) ->
174 document = content.ownerDocument
175 ignoreCase = (term == term.toLowerCase())
176 regex = RegExp("(#{utils.regexEscape(term)})", if ignoreCase then 'i' else '')
177 clear = (term == '')
178
179 for item in content.querySelectorAll('.search-item')
180 texts = item.querySelectorAll('.search-text')
181 texts = [item] if texts.length == 0
182 className = SEARCH_NON_MATCH_CLASS
183
184 for element in texts
185 {textContent} = element
186 # Clear the previous highlighting. This is possible to do for non-matches
187 # as well, but too slow.
188 if item.classList.contains(SEARCH_MATCH_CLASS)
189 element.textContent = textContent
190
191 continue if clear or not regex.test(textContent)
192
193 className = SEARCH_MATCH_CLASS
194 element.textContent = '' # Empty the element.
195 for part, index in textContent.split(regex)
196 # Even indices are surrounding text, odd ones are matches.
197 if index % 2 == 0
198 element.appendChild(document.createTextNode(part))
199 else
200 utils.createBox(document, SEARCH_HIGHLIGHT_CLASS, element, part)
201
202 item.classList.remove(SEARCH_MATCH_CLASS, SEARCH_NON_MATCH_CLASS)
203 item.classList.add(className) unless clear
204
205 return
206
207 module.exports = {
208 injectHelp
209 removeHelp
210 toggleHelp
211 getHelp
212 getSearchInput
213 }
Imprint / Impressum