Commit 4ed25d0653cf7fe85f8e3eb411723ec297f60638
a beginning gesture
mix irving committed on 2/20/2017, 3:02:35 AMFiles changed
.gitignore | added |
app/html/render.js | added |
app/html/render.mcss | added |
app/mcss/global.js | added |
app/mcss/hypertabs.js | added |
app/mcss/hypertabs.mcss | added |
app/mcss/mixins.js | added |
app/mcss/render.js | added |
index.js | added |
package.json | added |
page/html/render/all.js | added |
page/html/render/private.js | added |
page/html/render/tabs.js | added |
.gitignore | ||
---|---|---|
@@ -1,0 +1,1 @@ | ||
1 | +node_modules |
app/html/render.js | ||
---|---|---|
@@ -1,0 +1,39 @@ | ||
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,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 | + |
app/mcss/global.js | ||
---|---|---|
@@ -1,0 +1,26 @@ | ||
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,0 +1,13 @@ | ||
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,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 | + |
app/mcss/mixins.js | ||
---|---|---|
@@ -1,0 +1,26 @@ | ||
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,0 +1,30 @@ | ||
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 | +} |
index.js | ||
---|---|---|
@@ -1,0 +1,27 @@ | ||
1 | +const combine = require('depject') | |
2 | +const bulk = require('bulk-require') | |
3 | + | |
4 | +// polyfills | |
5 | +require('setimmediate') | |
6 | + | |
7 | +// from more specialized to more general | |
8 | +const sockets = combine( | |
9 | + // require(patchgit) | |
10 | + bulk(__dirname, [ | |
11 | + 'page/**/*.js', | |
12 | + 'app/**/*.js' | |
13 | + ]), | |
14 | + require('patchcore') | |
15 | +) | |
16 | + | |
17 | +const app = entry(sockets) | |
18 | + | |
19 | +app() | |
20 | + | |
21 | + | |
22 | + | |
23 | + | |
24 | +function entry (sockets) { | |
25 | + return sockets.app.html.render[0]() | |
26 | +} | |
27 | + |
package.json | ||
---|---|---|
@@ -1,0 +1,33 @@ | ||
1 | +{ | |
2 | + "name": "picknmix", | |
3 | + "version": "0.0.1", | |
4 | + "description": "patchbay 2? building on patchcore", | |
5 | + "main": "index.js", | |
6 | + "scripts": { | |
7 | + "start": "electro index.js", | |
8 | + "test": "echo \"Error: no test specified\" && exit 1" | |
9 | + }, | |
10 | + "repository": { | |
11 | + "type": "git", | |
12 | + "url": "git+https://github.com/ssbc/picknmix.git" | |
13 | + }, | |
14 | + "author": "mixmix", | |
15 | + "license": "GPL-3.0", | |
16 | + "bugs": { | |
17 | + "url": "https://github.com/ssbc/picknmix/issues" | |
18 | + }, | |
19 | + "homepage": "https://github.com/ssbc/picknmix#readme", | |
20 | + "dependencies": { | |
21 | + "bulk-require": "^1.0.0", | |
22 | + "depject": "^3.1.6", | |
23 | + "depnest": "^1.0.2", | |
24 | + "electro": "^2.0.3", | |
25 | + "electron": "^1.4.15", | |
26 | + "hypertabs": "^4.1.1", | |
27 | + "insert-css": "^2.0.0", | |
28 | + "micro-css": "^1.0.0", | |
29 | + "mutant": "^3.14.2", | |
30 | + "open-external": "^0.1.1", | |
31 | + "setimmediate": "^1.0.5" | |
32 | + } | |
33 | +} |
page/html/render/all.js |
---|
page/html/render/private.js |
---|
page/html/render/tabs.js | ||
---|---|---|
@@ -1,0 +1,262 @@ | ||
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 | + |
Built with git-ssb-web