Files: bd411fd1d719442e7658db49d6b8c5d22b10fb25 / router / html / tabs.js
7028 bytesRaw
1 | const { h } = require('mutant') |
2 | const Tabs = require('hypertabs') |
3 | const open = require('open-external') |
4 | const { webFrame, remote, clipboard } = require('electron') || {} |
5 | const nest = require('depnest') |
6 | |
7 | // const keyscroll = require('../../keyscroll') |
8 | |
9 | exports.needs = nest({ |
10 | 'router.html.page': 'first' |
11 | // 'app.html.menu': 'first', |
12 | // helpers: { |
13 | // build_error: 'first', |
14 | // build_scroller: 'first', |
15 | // external_confirm:'first', |
16 | // }, |
17 | // 'app.html.search_box': 'first' |
18 | }) |
19 | |
20 | exports.gives = nest('router.html.tabs') |
21 | |
22 | exports.create = function (api) { |
23 | return function (path) { |
24 | function setSelected (indexes) { |
25 | const ids = indexes.map(index => tabs.get(index).content.id) |
26 | if (search) |
27 | if (ids.length > 1) |
28 | search.input.value = 'split(' + ids.join(',') + ')' |
29 | else |
30 | search.input.value = ids[0] |
31 | } |
32 | |
33 | const tabs = Tabs(setSelected) |
34 | const search = api.search_box((path, change) => { |
35 | if (tabs.has(path)) { |
36 | tabs.select(path) |
37 | return true |
38 | } |
39 | |
40 | const el = api.page(path) |
41 | if (!el) return |
42 | |
43 | if (!el.title) el.title = path |
44 | // el.scroll = keyscroll(el.querySelector('.Scroller .content')) |
45 | tabs.add(el, change) |
46 | // localStorage.openTabs = JSON.stringify(tabs.tabs) |
47 | return change |
48 | }) |
49 | |
50 | // TODO add options to Tabs : e.g. Tabs(setSelected, { append: el }) |
51 | tabs.firstChild.appendChild( |
52 | h('div.extra', [ |
53 | search, |
54 | api.menu() |
55 | ]) |
56 | ) |
57 | |
58 | var saved = [] |
59 | // try { saved = JSON.parse(localStorage.openTabs) } |
60 | // catch (_) { } |
61 | |
62 | if (!saved || saved.length < 3) |
63 | saved = ['/public', '/private', '/notifications'] |
64 | |
65 | saved.forEach(function (path) { |
66 | var el = api.page(path) |
67 | if (!el) return |
68 | el.id = el.id || path |
69 | if (!el) return |
70 | el.scroll = keyscroll(el.querySelector('.Scroller .content')) |
71 | if (el) tabs.add(el, false, false) |
72 | }) |
73 | |
74 | tabs.select(0) |
75 | search.input.value = null // start with an empty field to show placeholder |
76 | |
77 | // handle link clicks |
78 | window.onclick = function (ev) { |
79 | var link = ancestorAnchor(ev.target) |
80 | if (!link) return |
81 | var path = link.hash.substring(1) |
82 | |
83 | ev.preventDefault() |
84 | ev.stopPropagation() |
85 | |
86 | // let the application handle this link |
87 | if (link.getAttribute('href') === '#') return |
88 | |
89 | // open external links. |
90 | // this ought to be made into something more runcible |
91 | if (link.href && open.isExternal(link.href)) return api.helpers.external_confirm(link.href) |
92 | |
93 | if (tabs.has(path)) |
94 | return tabs.select(path, !ev.ctrlKey, !!ev.shiftKey) |
95 | |
96 | var el = api.page(path) |
97 | if (el) { |
98 | el.id = el.id || path |
99 | el.scroll = keyscroll(el.querySelector('.Scroller .content')) |
100 | tabs.add(el, !ev.ctrlKey, !!ev.shiftKey) |
101 | // localStorage.openTabs = JSON.stringify(tabs.tabs) |
102 | } |
103 | |
104 | return false |
105 | } |
106 | |
107 | var gPressed = false |
108 | window.addEventListener('keydown', function (ev) { |
109 | if (ev.target.nodeName === 'INPUT' || ev.target.nodeName === 'TEXTAREA') |
110 | return |
111 | |
112 | // scroll to top |
113 | if (ev.keyCode == 71) { // g |
114 | if (!gPressed) return gPressed = true |
115 | var el = tabs.get(tabs.selected[0]).firstChild.scroll('first') |
116 | gPressed = false |
117 | } else { |
118 | gPressed = false |
119 | } |
120 | |
121 | switch (ev.keyCode) { |
122 | // scroll through tabs |
123 | case 72: // h |
124 | return tabs.selectRelative(-1) |
125 | case 76: // l |
126 | return tabs.selectRelative(1) |
127 | |
128 | // scroll through messages |
129 | case 74: // j |
130 | return tabs.get(tabs.selected[0]).firstChild.scroll(1) |
131 | case 75: // k |
132 | return tabs.get(tabs.selected[0]).firstChild.scroll(-1) |
133 | |
134 | // close a tab |
135 | case 88: // x |
136 | if (tabs.selected) { |
137 | var sel = tabs.selected |
138 | var i = sel.reduce(function (a, b) { return Math.min(a, b) }) |
139 | tabs.remove(sel) |
140 | tabs.select(Math.max(i - 1, 0)) |
141 | } |
142 | return |
143 | |
144 | // activate the search field |
145 | case 191: // / |
146 | if (ev.shiftKey) |
147 | search.activate('?', ev) |
148 | else |
149 | search.activate('/', ev) |
150 | return |
151 | |
152 | // navigate to a feed |
153 | case 50: // 2 |
154 | if (ev.shiftKey) |
155 | search.activate('@', ev) |
156 | return |
157 | |
158 | // navigate to a channel |
159 | case 51: // 3 |
160 | if (ev.shiftKey) |
161 | search.activate('#', ev) |
162 | return |
163 | |
164 | // navigate to a message |
165 | case 53: // 5 |
166 | if (ev.shiftKey) |
167 | search.activate('%', ev) |
168 | return |
169 | } |
170 | }) |
171 | |
172 | // errors tab |
173 | var { |
174 | container: errorsScroller, |
175 | content: errorsContent |
176 | } = api.helpers.build_scroller() |
177 | |
178 | errorsScroller.id = '/errors' |
179 | errorsScroller.classList.add('-errors') |
180 | |
181 | // remove loader error handler (currently disabled) |
182 | // if (window.onError) { |
183 | // window.removeEventListener('error', window.onError) |
184 | // delete window.onError |
185 | // } |
186 | |
187 | // put errors in a tab |
188 | window.addEventListener('error', ev => { |
189 | const err = ev.error || ev |
190 | if (!tabs.has('/errors')) |
191 | tabs.add(errorsScroller, false) |
192 | |
193 | const el = api.helpers.build_error(err) |
194 | if (errorsContent.firstChild) |
195 | errorsContent.insertBefore(el, errorsContent.firstChild) |
196 | else |
197 | errorsContent.appendChild(el) |
198 | }) |
199 | |
200 | if (process.versions.electron) { |
201 | window.addEventListener('mousewheel', ev => { |
202 | const { ctrlKey, deltaY } = ev |
203 | if (ctrlKey) { |
204 | const direction = (deltaY / Math.abs(deltaY)) |
205 | const currentZoom = webFrame.getZoomLevel() |
206 | webFrame.setZoomLevel(currentZoom - direction) |
207 | } |
208 | }) |
209 | |
210 | window.addEventListener('contextmenu', ev => { |
211 | ev.preventDefault() |
212 | const Menu = remote.Menu |
213 | const MenuItem = remote.MenuItem |
214 | const menu = new Menu() |
215 | menu.append(new MenuItem({ |
216 | label: 'Inspect Element', |
217 | click: () => { |
218 | remote.getCurrentWindow().inspectElement(ev.x, ev.y) |
219 | } |
220 | })) |
221 | |
222 | var message = ancestorMessage(ev.target) |
223 | if (message && message.dataset.key) { |
224 | menu.append(new MenuItem({ |
225 | label: 'Copy id', |
226 | click: () => clipboard.writeText(message.dataset.key) |
227 | })) |
228 | } |
229 | if (message && message.dataset.text) { |
230 | menu.append(new MenuItem({ |
231 | label: 'Copy text', |
232 | click: () => clipboard.writeText(message.dataset.text) |
233 | })) |
234 | } |
235 | menu.popup(remote.getCurrentWindow()) |
236 | }) |
237 | } |
238 | |
239 | return tabs |
240 | } |
241 | } |
242 | |
243 | function ancestorAnchor (el) { |
244 | if (!el) return |
245 | if (el.tagName !== 'A') return ancestorAnchor(el.parentElement) |
246 | return el |
247 | } |
248 | |
249 | function ancestorMessage (el) { |
250 | if (!el) return |
251 | if (!el.classList.contains('Message')) { |
252 | if (el.parentElement) |
253 | return ancestorMessage(el.parentElement) |
254 | else |
255 | return null |
256 | } |
257 | return el |
258 | } |
259 | |
260 |
Built with git-ssb-web