Commit bd411fd1d719442e7658db49d6b8c5d22b10fb25
working style chain
mix irving committed on 2/22/2017, 3:11:52 AMParent: c0bb55d9b169c5943cb5dfabbd5a58a241a238af
Files changed
index.js | changed |
package.json | changed |
styles/css.js | changed |
styles/mcss.js | changed |
app/html/render.js | deleted |
app/html/render.mcss | deleted |
app/mcss/global.js | deleted |
app/mcss/hypertabs.js | deleted |
app/mcss/hypertabs.mcss | deleted |
app/mcss/mixins.js | deleted |
app/mcss/render.js | deleted |
main/html/app.js | added |
main/html/app.mcss | added |
main/styles/css/global.js | added |
main/styles/mixins.js | added |
page/html/render/all.js | deleted |
page/html/render/private.js | deleted |
page/html/render/tabs.js | deleted |
router/html/page/all.js | added |
router/html/tabs.js | added |
router/html/tabs.mcss | added |
index.js | ||
---|---|---|
@@ -1,5 +1,7 @@ | ||
1 | 1 | const combine = require('depject') |
2 | +const entry = require('depject/entry') | |
3 | +const nest = require('depnest') | |
2 | 4 | const bulk = require('bulk-require') |
3 | 5 | |
4 | 6 | // polyfills |
5 | 7 | require('setimmediate') |
@@ -7,21 +9,16 @@ | ||
7 | 9 | // from more specialized to more general |
8 | 10 | const sockets = combine( |
9 | 11 | // require(patchgit) |
10 | 12 | bulk(__dirname, [ |
11 | - 'page/**/*.js', | |
12 | - 'app/**/*.js' | |
13 | + 'main/**/*.js', | |
14 | + 'router/html/page/**/*.js', | |
15 | + 'styles/**/*.js' | |
13 | 16 | ]), |
14 | 17 | require('patchcore') |
15 | 18 | ) |
16 | 19 | |
17 | -const app = entry(sockets) | |
20 | +const api = entry(sockets, nest('main.html.app', 'first')) | |
18 | 21 | |
19 | -app() | |
22 | +const app = api.main.html.app() | |
23 | +document.body.appendChild(app) | |
20 | 24 | |
21 | - | |
22 | - | |
23 | - | |
24 | -function entry (sockets) { | |
25 | - return sockets.app.html.render[0]() | |
26 | -} | |
27 | - |
package.json | ||
---|---|---|
@@ -18,9 +18,9 @@ | ||
18 | 18 | }, |
19 | 19 | "homepage": "https://github.com/ssbc/picknmix#readme", |
20 | 20 | "dependencies": { |
21 | 21 | "bulk-require": "^1.0.0", |
22 | - "depject": "^3.1.6", | |
22 | + "depject": "^3.2.0", | |
23 | 23 | "depnest": "^1.0.2", |
24 | 24 | "electro": "^2.0.3", |
25 | 25 | "electron": "^1.4.15", |
26 | 26 | "hypertabs": "^4.1.1", |
styles/css.js | ||
---|---|---|
@@ -1,7 +1,8 @@ | ||
1 | -const { assign } = Object | |
2 | 1 | const { each, map } = require('libnested') |
3 | 2 | const nest = require('depnest') |
3 | +const compile = require('micro-css') | |
4 | +const { assign } = Object | |
4 | 5 | |
5 | 6 | exports.gives = nest('styles.css') |
6 | 7 | |
7 | 8 | exports.needs = { |
@@ -10,23 +11,27 @@ | ||
10 | 11 | mixins: 'reduce' |
11 | 12 | } |
12 | 13 | } |
13 | 14 | |
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) | |
17 | 17 | |
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 | + } | |
21 | 26 | } |
22 | 27 | |
23 | 28 | function mixinsToMcss (mixinsObj) { |
24 | 29 | var mcss = '' |
25 | 30 | each(mixinsObj, (mixinStr, [name]) => { |
26 | 31 | // QUESTION: are mixins mcss specific or should we convert to mcss here? |
27 | 32 | // as in, mixins are dom style objects and we use something like `inline-style` package |
28 | - mcss += mixinStr +'\n' | |
33 | + mcss += mixinStr + '\n' | |
29 | 34 | }) |
30 | 35 | return mcss |
31 | 36 | } |
32 | 37 | |
@@ -34,4 +39,5 @@ | ||
34 | 39 | return map(mcssObj, (mcssStr, [name]) => { |
35 | 40 | return compile(mixinsStr + '\n' + mcssStr) |
36 | 41 | }) |
37 | 42 | } |
43 | + |
styles/mcss.js | ||
---|---|---|
@@ -1,18 +1,26 @@ | ||
1 | -const { basename } = require('path') | |
1 | +const path = require('path') | |
2 | +const { basename } = path | |
2 | 3 | const readDirectory = require('read-directory') |
3 | 4 | const { each } = require('libnested') |
4 | 5 | const nest = require('depnest') |
5 | 6 | |
6 | -const contents = readDirectory.sync(__dirname, { | |
7 | +const contents = readDirectory.sync(path.join(__dirname, '..'), { | |
7 | 8 | extensions: false, |
8 | - filter: '**/*.mcss' | |
9 | + filter: '**/*.mcss', | |
10 | + ignore: '**/node_modules/**' | |
9 | 11 | }) |
10 | 12 | |
11 | 13 | 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 | + } | |
18 | 25 | } |
26 | + |
app/html/render.js | ||
---|---|---|
@@ -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.mcss | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.mcss | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.mcss | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js |
---|
page/html/render/private.js |
---|
page/html/render/tabs.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.mcss | ||
---|---|---|
@@ -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