git ssb

16+

Dominic / patchbay



Commit bd411fd1d719442e7658db49d6b8c5d22b10fb25

working style chain

mix irving committed on 2/22/2017, 3:11:52 AM
Parent: c0bb55d9b169c5943cb5dfabbd5a58a241a238af

Files changed

index.jschanged
package.jsonchanged
styles/css.jschanged
styles/mcss.jschanged
app/html/render.jsdeleted
app/html/render.mcssdeleted
app/mcss/global.jsdeleted
app/mcss/hypertabs.jsdeleted
app/mcss/hypertabs.mcssdeleted
app/mcss/mixins.jsdeleted
app/mcss/render.jsdeleted
main/html/app.jsadded
main/html/app.mcssadded
main/styles/css/global.jsadded
main/styles/mixins.jsadded
page/html/render/all.jsdeleted
page/html/render/private.jsdeleted
page/html/render/tabs.jsdeleted
router/html/page/all.jsadded
router/html/tabs.jsadded
router/html/tabs.mcssadded
index.jsView
@@ -1,5 +1,7 @@
11 const combine = require('depject')
2+const entry = require('depject/entry')
3+const nest = require('depnest')
24 const bulk = require('bulk-require')
35
46 // polyfills
57 require('setimmediate')
@@ -7,21 +9,16 @@
79 // from more specialized to more general
810 const sockets = combine(
911 // require(patchgit)
1012 bulk(__dirname, [
11- 'page/**/*.js',
12- 'app/**/*.js'
13+ 'main/**/*.js',
14+ 'router/html/page/**/*.js',
15+ 'styles/**/*.js'
1316 ]),
1417 require('patchcore')
1518 )
1619
17-const app = entry(sockets)
20+const api = entry(sockets, nest('main.html.app', 'first'))
1821
19-app()
22+const app = api.main.html.app()
23+document.body.appendChild(app)
2024
21-
22-
23-
24-function entry (sockets) {
25- return sockets.app.html.render[0]()
26-}
27-
package.jsonView
@@ -18,9 +18,9 @@
1818 },
1919 "homepage": "https://github.com/ssbc/picknmix#readme",
2020 "dependencies": {
2121 "bulk-require": "^1.0.0",
22- "depject": "^3.1.6",
22+ "depject": "^3.2.0",
2323 "depnest": "^1.0.2",
2424 "electro": "^2.0.3",
2525 "electron": "^1.4.15",
2626 "hypertabs": "^4.1.1",
styles/css.jsView
@@ -1,7 +1,8 @@
1-const { assign } = Object
21 const { each, map } = require('libnested')
32 const nest = require('depnest')
3+const compile = require('micro-css')
4+const { assign } = Object
45
56 exports.gives = nest('styles.css')
67
78 exports.needs = {
@@ -10,23 +11,27 @@
1011 mixins: 'reduce'
1112 }
1213 }
1314
14-exports.create = (api) => (sofar = {}) => {
15- const mcssObj = api.styles.mcss()
16- const mixinObj = api.styles.mixins()
15+exports.create = function (api) {
16+ return nest('styles.css', css)
1717
18- const mcssMixinsStr = mixinsToMcss(mixinObj)
19- const cssObj = mcssToCss(mcssObj, mcssMixinsStr)
20- return assign(sofar, cssObj)
18+ function css (sofar = {}) {
19+ const mcssObj = api.styles.mcss()
20+ const mixinObj = api.styles.mixins()
21+
22+ const mcssMixinsStr = mixinsToMcss(mixinObj)
23+ const cssObj = mcssToCss(mcssObj, mcssMixinsStr)
24+ return assign(sofar, cssObj)
25+ }
2126 }
2227
2328 function mixinsToMcss (mixinsObj) {
2429 var mcss = ''
2530 each(mixinsObj, (mixinStr, [name]) => {
2631 // QUESTION: are mixins mcss specific or should we convert to mcss here?
2732 // as in, mixins are dom style objects and we use something like `inline-style` package
28- mcss += mixinStr +'\n'
33+ mcss += mixinStr + '\n'
2934 })
3035 return mcss
3136 }
3237
@@ -34,4 +39,5 @@
3439 return map(mcssObj, (mcssStr, [name]) => {
3540 return compile(mixinsStr + '\n' + mcssStr)
3641 })
3742 }
43+
styles/mcss.jsView
@@ -1,18 +1,26 @@
1-const { basename } = require('path')
1+const path = require('path')
2+const { basename } = path
23 const readDirectory = require('read-directory')
34 const { each } = require('libnested')
45 const nest = require('depnest')
56
6-const contents = readDirectory.sync(__dirname, {
7+const contents = readDirectory.sync(path.join(__dirname, '..'), {
78 extensions: false,
8- filter: '**/*.mcss'
9+ filter: '**/*.mcss',
10+ ignore: '**/node_modules/**'
911 })
1012
1113 exports.gives = nest('styles.mcss')
12-exports.create = () => (sofar = {}) => {
13- each(contents, (content, [filename]) => {
14- const name = basename(filename)
15- sofar[name] = content
16- })
17- return sofar
14+
15+exports.create = function (api) {
16+ return nest('styles.mcss', mcss)
17+
18+ function mcss (sofar = {}) {
19+ each(contents, (content, [filename]) => {
20+ const name = basename(filename)
21+ sofar[name] = content
22+ })
23+ return sofar
24+ }
1825 }
26+
app/html/render.jsView
@@ -1,39 +1,0 @@
1-const fs = require('fs')
2-const h = require('../h')
3-const { Value } = require('mutant')
4-const insertCss = require('insert-css')
5-
6-exports.gives = nest([
7- 'app.html.render',
8- 'app.mcss.render'
9-])
10-
11-exports.needs = nest({
12- 'page.html.render': 'first',
13- styles: 'first'
14-})
15-
16-exports.create = function (api) {
17- return {
18- app,
19- mcss: () => fs.readFileSync(__filename.replace(/js$/, 'mcss'), 'utf8')
20- }
21-
22- function app () {
23- process.nextTick(() => insertCss(api.styles()))
24-
25- var view = Value(getView())
26- var screen = h('App', view)
27-
28- window.onhashchange = () => view.set(getView())
29- document.body.appendChild(screen)
30-
31- return screen
32- }
33-
34- function getView () {
35- const view = window.location.hash.substring(1) || 'tabs'
36- return api.page(view)
37- }
38-}
39-
app/html/render.mcssView
@@ -1,11 +1,0 @@
1-App {
2- position: absolute
3-
4- top: 0
5- bottom: 0
6- left: 0
7- right: 0
8- overflow: hidden
9- min-height: 0px
10-}
11-
app/mcss/global.jsView
@@ -1,26 +1,0 @@
1-
2-const mixins = `
3- _textPrimary {
4- color: #222
5- }
6-
7- _textSubtle {
8- color: gray
9- }
10-
11- _backgroundPrimary {
12- background-color: #50aadf
13- }
14-`
15-
16-module.exports = {
17- gives: {
18- mcss: true
19- },
20- create: function (api) {
21- return {
22- mcss: () => mixins
23- }
24- }
25-}
26-
app/mcss/hypertabs.jsView
@@ -1,13 +1,0 @@
1-const fs = require('fs')
2-
3-module.exports = {
4- gives: {
5- mcss: true
6- },
7- create: function (api) {
8- return {
9- mcss: () => fs.readFileSync(__filename.replace(/js$/, 'mcss'), 'utf8')
10- }
11- }
12-}
13-
app/mcss/hypertabs.mcssView
@@ -1,94 +1,0 @@
1-Hypertabs {
2- display: flex
3- flex-direction: column
4-
5- height: 100% /* needed to stop scroller blowing out */
6-
7- nav {
8- display: flex
9-
10- background: linear-gradient(to bottom, #efefef, #e5e5e5)
11-
12- section.tabs {
13- flex-grow: 1
14- display: flex
15- min-width: 0
16-
17- div.tab {
18- flex-grow: 1
19-
20- display: flex
21- align-items: center
22- justify-content: space-between
23-
24- min-width: 3.5rem
25- font-size: .9rem
26- background-color: #efefef
27- overflow-x: hidden
28-
29- padding: 0 .4rem
30- margin-left: .6rem
31- border: 1px gainsboro solid
32- border-bottom: none
33-
34- -selected {
35- color: #222
36- background-color: #fff
37-
38- a.close {
39- visibility: visible
40- }
41- }
42-
43- -notify {
44- background-color: orange;
45- }
46-
47-
48- a {
49- color: #666
50-
51- :hover {
52- color: #0088cc
53- text-decoration: none
54- }
55- }
56-
57- a.link {
58- flex-grow: 1
59- flex-shrink: 0
60- overflow-x: hidden
61- min-width: 0
62- max-width: 90%
63- white-space: nowrap
64- text-overflow: ellipsis
65- }
66-
67- a.close {
68- visibility: hidden
69- }
70- }
71-
72- }
73-
74- div.extra {
75- display: flex
76- align-items: center
77- }
78- }
79-
80- section.content {
81- display: flex
82-
83- height: 100% /* needed to stop making nav weird */
84-
85- div.page {
86- flex-grow: 1
87-
88- display: flex /*hack to get give Scroller context it needs */
89-
90- padding-top: .2rem
91- }
92- }
93-}
94-
app/mcss/mixins.jsView
@@ -1,26 +1,0 @@
1-
2-const mixins = `
3- _textPrimary {
4- color: #222
5- }
6-
7- _textSubtle {
8- color: gray
9- }
10-
11- _backgroundPrimary {
12- background-color: #50aadf
13- }
14-`
15-
16-module.exports = {
17- gives: {
18- mcss: true
19- },
20- create: function (api) {
21- return {
22- mcss: () => mixins
23- }
24- }
25-}
26-
app/mcss/render.jsView
@@ -1,30 +1,0 @@
1-const compile = require('micro-css')
2-const fs = require('fs')
3-const Path = require('path')
4-
5-module.exports = {
6- gives: {
7- mcss: true,
8- css: true,
9- styles: true
10- },
11- needs: {
12- mcss: 'map',
13- css: 'map'
14- },
15- create: function (api) {
16- var styles = ''
17- process.nextTick(function () {
18- const mcss = api.mcss().join('\n')
19- const css = api.css().join('\n')
20- styles = coreStyle + compile(mcss) + css
21- })
22-
23- return {
24- styles: () => styles,
25- // export empty styles
26- mcss: () => '',
27- css: () => ''
28- }
29- }
30-}
main/html/app.jsView
@@ -1,0 +1,39 @@
1+const { Value, h } = require('mutant')
2+const nest = require('depnest')
3+const insertCss = require('insert-css')
4+
5+exports.gives = nest('main.html.app')
6+
7+exports.needs = nest({
8+ 'router.html.page': 'first',
9+ 'styles.css': 'reduce'
10+})
11+
12+exports.create = function (api) {
13+ return nest('main.html.app', app)
14+
15+ function app () {
16+ const css = values(api.styles.css()).join('\n')
17+ insertCss(css)
18+
19+ // var view = Value(getView())
20+ var view = 'hello!'
21+ var screen = h('App', view)
22+
23+ // window.onhashchange = () => view.set(getView())
24+ // document.body.appendChild(screen)
25+
26+ return screen
27+ }
28+
29+ // function getView () {
30+ // const view = window.location.hash.substring(1) || 'tabs'
31+ // return api.page(view)
32+ // }
33+}
34+
35+function values (object) {
36+ const keys = Object.keys(object)
37+ return keys.map(k => object[k])
38+}
39+
main/html/app.mcssView
@@ -1,0 +1,11 @@
1+App {
2+ position: absolute
3+
4+ top: 0
5+ bottom: 0
6+ left: 0
7+ right: 0
8+ overflow: hidden
9+ min-height: 0px
10+}
11+
main/styles/css/global.jsView
@@ -1,0 +1,107 @@
1+const nest = require('depnest')
2+const { assign } = Object
3+
4+exports.gives = nest('styles.css')
5+
6+exports.create = function (api) {
7+ return nest('styles.css', (sofar = {}) => {
8+ return assign(sofar, { globalStyles })
9+ })
10+}
11+
12+const globalStyles = `
13+body {
14+ font-family: sans-serif;
15+ color: #222;
16+}
17+
18+h1, h2, h3, h4, h5, h6, p, ul, ol {
19+ margin-top: .35em;
20+}
21+
22+h1 { font-size: 1.2em; }
23+h2 { font-size: 1.18em; }
24+h3 { font-size: 1.15em; }
25+h4 { font-size: 1.12em; }
26+h5 { font-size: 1.1em; }
27+h6 { font-size: 1em; }
28+
29+* {
30+ word-break: break-word;
31+}
32+
33+a:link, a:visited, a:active {
34+ color: #0088cc;
35+ text-decoration: none;
36+}
37+
38+a:hover,
39+a:focus {
40+ color: #005580;
41+ text-decoration: underline;
42+}
43+
44+pre {
45+ white-space: pre-wrap;
46+ word-wrap: break-word;
47+}
48+
49+p {
50+ margin-top: .35ex;
51+}
52+
53+hr {
54+ border: solid #eee;
55+ clear: both;
56+ border-width: 1px 0 0;
57+ height: 0;
58+ margin-bottom: .9em;
59+}
60+
61+input, textarea {
62+ border: none;
63+ border-radius: .2em;
64+ font-family: sans-serif;
65+}
66+
67+input:focus, .compose:focus, button:focus {
68+ outline: none;
69+ border-color: #0088cc;
70+ box-shadow: 0 0 4px #0088cc;
71+}
72+
73+textarea {
74+ padding: .5em;
75+ font-size: 1em;
76+}
77+
78+textarea:focus {
79+ outline: none;
80+ border-color: none;
81+}
82+
83+button {
84+ background: #fff;
85+ color: #666;
86+ border: 1px solid #bbb;
87+ border-radius: .5em;
88+ padding: .7em;
89+ margin: .5em;
90+ cursor: pointer;
91+ text-transform: uppercase;
92+ font-weight: bold;
93+ font-size: .7em;
94+}
95+
96+button:hover {
97+ background: #ccc;
98+ border: 1px solid #bbb;
99+}
100+
101+/* TextNodeSearcher highlights */
102+
103+highlight {
104+ background: #ff8;
105+}
106+`
107+
main/styles/mixins.jsView
@@ -1,0 +1,29 @@
1+const nest = require('depnest')
2+const { assign } = Object
3+
4+// this is either:
5+// - wrong domain
6+// - wrong gives
7+//
8+exports.gives = nest('styles.mixins')
9+
10+exports.create = function (api) {
11+ return nest('styles.mixins', (sofar = {}) => {
12+ return assign(sofar, { mainMixins })
13+ })
14+}
15+
16+const mainMixins = `
17+_textPrimary {
18+ color: #222
19+}
20+
21+_textSubtle {
22+ color: gray
23+}
24+
25+_backgroundPrimary {
26+ background-color: #50aadf
27+}
28+`
29+
page/html/render/all.jsView
page/html/render/private.jsView
page/html/render/tabs.jsView
@@ -1,262 +1,0 @@
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-
router/html/page/all.jsView
@@ -1,0 +1,9 @@
1+const { h } = require('mutant')
2+const nest = require('depnest')
3+
4+exports.gives = nest('router.html.page')
5+
6+exports.create = function (api) {
7+ return nest('router.html.page', () => '')
8+}
9+
router/html/tabs.jsView
@@ -1,0 +1,259 @@
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+
router/html/tabs.mcssView
@@ -1,0 +1,94 @@
1+Hypertabs {
2+ display: flex
3+ flex-direction: column
4+
5+ height: 100% /* needed to stop scroller blowing out */
6+
7+ nav {
8+ display: flex
9+
10+ background: linear-gradient(to bottom, #efefef, #e5e5e5)
11+
12+ section.tabs {
13+ flex-grow: 1
14+ display: flex
15+ min-width: 0
16+
17+ div.tab {
18+ flex-grow: 1
19+
20+ display: flex
21+ align-items: center
22+ justify-content: space-between
23+
24+ min-width: 3.5rem
25+ font-size: .9rem
26+ background-color: #efefef
27+ overflow-x: hidden
28+
29+ padding: 0 .4rem
30+ margin-left: .6rem
31+ border: 1px gainsboro solid
32+ border-bottom: none
33+
34+ -selected {
35+ color: #222
36+ background-color: #fff
37+
38+ a.close {
39+ visibility: visible
40+ }
41+ }
42+
43+ -notify {
44+ background-color: orange;
45+ }
46+
47+
48+ a {
49+ color: #666
50+
51+ :hover {
52+ color: #0088cc
53+ text-decoration: none
54+ }
55+ }
56+
57+ a.link {
58+ flex-grow: 1
59+ flex-shrink: 0
60+ overflow-x: hidden
61+ min-width: 0
62+ max-width: 90%
63+ white-space: nowrap
64+ text-overflow: ellipsis
65+ }
66+
67+ a.close {
68+ visibility: hidden
69+ }
70+ }
71+
72+ }
73+
74+ div.extra {
75+ display: flex
76+ align-items: center
77+ }
78+ }
79+
80+ section.content {
81+ display: flex
82+
83+ height: 100% /* needed to stop making nav weird */
84+
85+ div.page {
86+ flex-grow: 1
87+
88+ display: flex /*hack to get give Scroller context it needs */
89+
90+ padding-top: .2rem
91+ }
92+ }
93+}
94+

Built with git-ssb-web