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