Skip to content
This repository was archived by the owner on Dec 27, 2022. It is now read-only.

Commit d29e79b

Browse files
committed
Added shell-window modals
1 parent 1714c89 commit d29e79b

File tree

16 files changed

+263
-19
lines changed

16 files changed

+263
-19
lines changed

app/background-process.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import * as adblocker from './background-process/adblocker'
1414
import * as analytics from './background-process/analytics'
1515

1616
import * as windows from './background-process/ui/windows'
17+
import * as modals from './background-process/ui/modals'
1718
import * as windowMenu from './background-process/ui/window-menu'
1819
import registerContextMenu from './background-process/ui/context-menu'
1920
import * as downloads from './background-process/ui/downloads'
@@ -82,6 +83,7 @@ app.on('ready', async function () {
8283
windowMenu.setup()
8384
registerContextMenu()
8485
windows.setup()
86+
modals.setup()
8587
downloads.setup()
8688
permissions.setup()
8789
basicAuth.setup()

app/background-process/browser.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const exec = require('util').promisify(require('child_process').exec)
1111
var debug = require('debug')('beaker')
1212
import * as settingsDb from './dbs/settings'
1313
import {open as openUrl} from './open-url'
14-
import {showModal, closeModal} from './ui/modals'
14+
import {showModal, showShellModal, closeModal} from './ui/modals'
1515
import {
1616
INVALID_SAVE_FOLDER_CHAR_REGEX
1717
} from '../lib/const'
@@ -117,6 +117,7 @@ export const WEBAPI = {
117117
openUrl: url => { openUrl(url) }, // dont return anything
118118
openFolder,
119119
doWebcontentsCmd,
120+
doTest,
120121
closeModal
121122
}
122123

@@ -398,6 +399,12 @@ async function doWebcontentsCmd (method, wcId, ...args) {
398399
return wc[method](...args)
399400
}
400401

402+
async function doTest (test) {
403+
if (test === 'modal') {
404+
return showShellModal(this.sender, 'example', {i: 5})
405+
}
406+
}
407+
401408
// internal methods
402409
// =
403410

app/background-process/ui/modals.js

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {app, BrowserWindow} from 'electron'
1+
import {app, BrowserWindow, ipcMain} from 'electron'
22
import {ModalActiveError} from 'beaker-error-constants'
33
import path from 'path'
44

@@ -11,14 +11,21 @@ const SIZES = {
1111
install: {width: 500, height: 250}
1212
}
1313

14-
// state
14+
// globals
1515
// =
1616

1717
var modalWindow
18+
var reqIdCounter = 0
19+
var activeRequests = []
1820

1921
// exported apis
2022
// =
2123

24+
export function setup () {
25+
// wire up handlers
26+
ipcMain.on('modal-response', onModalResponse)
27+
}
28+
2229
export function showModal (parentWindow, modalName, opts = {}) {
2330
if (modalWindow) {
2431
return Promise.reject(new ModalActiveError())
@@ -81,3 +88,37 @@ export function closeModal (err, res) {
8188
w.close()
8289
return true
8390
}
91+
92+
export function showShellModal (webContents, modalName, opts = {}) {
93+
return new Promise((resolve, reject) => {
94+
// sanity check
95+
if (!webContents.hostWebContents) {
96+
// abort, must be given the webContents of a page
97+
console.error('Warning: showShellModal() was passed the webContents of a non page')
98+
return reject('Invalid shell modal target')
99+
}
100+
101+
// track the new request
102+
var req = {id: ++reqIdCounter, resolve, reject}
103+
activeRequests.push(req)
104+
105+
// send message to create the UI
106+
webContents.hostWebContents.send('command', 'show-modal', req.id, webContents.id, modalName, opts)
107+
})
108+
}
109+
110+
// internal methods
111+
// =
112+
113+
async function onModalResponse (e, reqId, err, res) {
114+
// lookup the request
115+
var req = activeRequests.find(req => req.id == reqId)
116+
if (!req) { return console.error('Warning: failed to find modal request for response #' + reqId) }
117+
118+
// untrack
119+
activeRequests.splice(activeRequests.indexOf(req), 1)
120+
121+
// finish
122+
if (err) req.reject(new Error(err))
123+
else req.resolve(res)
124+
}

app/lib/api-manifests/internal/browser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ export default {
2727
openUrl: 'promise',
2828
openFolder: 'promise',
2929
doWebcontentsCmd: 'promise',
30+
doTest: 'promise',
3031
closeModal: 'sync'
3132
}

app/lib/web-apis/beaker.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ if (window.location.protocol === 'beaker:') {
157157
beaker.browser.openUrl = beakerBrowserRPC.openUrl
158158
beaker.browser.openFolder = beakerBrowserRPC.openFolder
159159
beaker.browser.doWebcontentsCmd = beakerBrowserRPC.doWebcontentsCmd
160+
beaker.browser.doTest = beakerBrowserRPC.doTest
160161
beaker.browser.closeModal = beakerBrowserRPC.closeModal
161162
}
162163

app/shell-window.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
</header>
1515
<div id="prompts"></div>
1616
<div id="toasts"></div>
17+
<div id="modals"></div>
1718
<div id="win32-titlebar"></div>
1819
<div id="left-swipe-arrow"><span class="icon icon-left-bold"></span></div>
1920
<div id="right-swipe-arrow"><span class="icon icon-right-bold"></span></div>

app/shell-window/command-handlers.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ipcRenderer } from 'electron'
22
import * as pages from './pages'
33
import * as zoom from './pages/zoom'
44
import * as navbar from './ui/navbar'
5+
import * as modal from './ui/modal'
56
import permsPrompt from './ui/prompts/permission'
67

78
export function setup () {
@@ -35,6 +36,7 @@ export function setup () {
3536
case 'set-tab': return pages.changeActiveTo(arg1)
3637
case 'load-pinned-tabs': return pages.loadPinnedFromDB()
3738
case 'perms:prompt': return permsPrompt(arg1, arg2, arg3, arg4)
39+
case 'show-modal': return modal.createFromBackgroundProcess(arg1, arg2, arg3, arg4)
3840
}
3941
})
4042
}

app/shell-window/pages.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import parseDatURL from 'parse-dat-url'
88
import * as zoom from './pages/zoom'
99
import * as navbar from './ui/navbar'
1010
import * as prompt from './ui/prompt'
11+
import * as modal from './ui/modal'
1112
import * as statusBar from './ui/statusbar'
1213
import * as toast from './ui/toast.js'
1314
import { SiteInfoNavbarBtn } from './ui/navbar/site-info'
@@ -109,7 +110,8 @@ export function create (opts) {
109110
wcID: null, // the id of the webcontents
110111
webviewEl: createWebviewEl(id, url),
111112
navbarEl: navbar.createEl(id),
112-
promptEl: prompt.createEl(id),
113+
promptEl: prompt.createContainer(id),
114+
modalEl: modal.createContainer(id),
113115
siteInfoNavbarBtn: null, // set after object is created
114116

115117
// page state
@@ -151,8 +153,9 @@ export function create (opts) {
151153
_canGoBack: false, // cached to avoid sync calls to the main process
152154
_canGoForward: false, // cached to avoid sync calls to the main process
153155

154-
// prompts
156+
// UI elements
155157
prompts: [], // list of active prompts (used with perms)
158+
modals: [], // list of active modals
156159

157160
// tab state
158161
isPinned: opts.isPinned, // is this page pinned?
@@ -377,7 +380,8 @@ export async function remove (page) {
377380
pages.splice(i, 1)
378381
webviewsDiv.removeChild(page.webviewEl)
379382
navbar.destroyEl(page.id)
380-
prompt.destroyEl(page.id)
383+
prompt.destroyContainer(page.id)
384+
modal.destroyContainer(page.id)
381385

382386
// persist pins w/o this one, if that was
383387
if (page.isPinned) { savePinnedToDB() }
@@ -418,6 +422,7 @@ export function setActive (page) {
418422
navbar.closeMenus()
419423
navbar.update()
420424
prompt.update()
425+
modal.update()
421426

422427
events.emit('set-active', page)
423428
ipcRenderer.send('shell-window:set-current-location', page.getIntendedURL())
@@ -626,8 +631,9 @@ function onLoadCommit (e) {
626631
zoom.setZoomFromSitedata(page, parseURL(page.getIntendedURL()).origin)
627632
// stop autocompleting
628633
navbar.clearAutocomplete()
629-
// close any prompts
634+
// close any prompts and modals
630635
prompt.forceRemoveAll(page)
636+
modal.forceRemoveAll(page)
631637
// set title in tabs
632638
page.title = e.target.getTitle() // NOTE sync operation
633639
navbar.update(page)
@@ -1026,13 +1032,15 @@ function show (page) {
10261032
page.webviewEl.classList.remove('hidden')
10271033
page.navbarEl.classList.remove('hidden')
10281034
page.promptEl.classList.remove('hidden')
1035+
page.modalEl.classList.remove('hidden')
10291036
events.emit('show', page)
10301037
}
10311038

10321039
function hide (page) {
10331040
page.webviewEl.classList.add('hidden')
10341041
page.navbarEl.classList.add('hidden')
10351042
page.promptEl.classList.add('hidden')
1043+
page.modalEl.classList.add('hidden')
10361044
events.emit('hide', page)
10371045
}
10381046

app/shell-window/ui.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as navbar from './ui/navbar'
44
import * as statusbar from './ui/statusbar'
55
import * as win32Titlebar from './ui/win32-titlebar'
66
import * as pages from './pages'
7+
import * as modal from './ui/modal'
78
import * as commandHandlers from './command-handlers'
89
import * as swipeHandlers from './swipe-handlers'
910

@@ -45,6 +46,7 @@ export function setup (cb) {
4546
commandHandlers.setup()
4647
swipeHandlers.setup()
4748
pages.setup()
49+
modal.setup()
4850
ipcRenderer.send('shell-window:pages-ready')
4951
pages.on('first-page', cb)
5052
}

app/shell-window/ui/modal.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import {ipcRenderer} from 'electron'
2+
import yo from 'yo-yo'
3+
import * as pages from '../pages'
4+
import modalFns from './modals/index'
5+
6+
// globals
7+
// =
8+
9+
var modalsDiv = document.querySelector('#modals')
10+
11+
// exported functions
12+
// =
13+
14+
export function setup () {
15+
document.body.addEventListener('keydown', onGlobalKeydown)
16+
}
17+
18+
export function createContainer (id) {
19+
// render
20+
var el = render(id, null)
21+
modalsDiv.append(el)
22+
return el
23+
}
24+
25+
export function destroyContainer (id) {
26+
var el = document.querySelector(`#modals [data-id="${id}"]`)
27+
if (el) el.remove()
28+
}
29+
30+
export async function createFromBackgroundProcess (reqId, webContentsId, modalId, opts) {
31+
var cb = (err, res) => ipcRenderer.send('modal-response', reqId, err, res)
32+
33+
// look up the page
34+
var page = pages.getByWebContentsID(webContentsId)
35+
if (!page) return cb('Page not available')
36+
37+
// lookup the modal
38+
var modalFn = modalFns[modalId]
39+
if (!modalFn) return cb('Modal not available')
40+
41+
// run the modal
42+
try {
43+
var res = await modalFn(page, opts)
44+
cb(null, res)
45+
} catch (err) {
46+
cb(err.toString())
47+
}
48+
}
49+
50+
export function add (page, { render, onForceClose }) {
51+
// add the modal
52+
var modal = {
53+
render,
54+
onForceClose
55+
}
56+
page.modals.push(modal)
57+
update(page)
58+
59+
return true
60+
}
61+
62+
export function remove (page, modal) {
63+
if (!page || !page.modals) { return } // page no longer exists
64+
65+
// find and remove
66+
var i = page.modals.indexOf(modal)
67+
if (i !== -1) {
68+
page.modals.splice(i, 1)
69+
update(page)
70+
}
71+
}
72+
73+
export function forceRemoveAll (page) {
74+
if (!page || !page.modals) { return } // page no longer exists
75+
76+
// find and remove
77+
page.modals.forEach(p => {
78+
if (typeof p.onForceClose == 'function') { p.onForceClose() }
79+
})
80+
page.modals = []
81+
update(page)
82+
}
83+
84+
export function update (page) {
85+
// fetch current page, if not given
86+
page = page || pages.getActive()
87+
if (!page.webviewEl) return
88+
89+
// render
90+
yo.update(page.modalEl, render(page.id, page))
91+
}
92+
93+
// internal methods
94+
// =
95+
96+
function render (id, page) {
97+
if (!page) { return yo`<div data-id=${id} class="hidden"></div>` }
98+
99+
return yo`<div data-id=${id} class=${page.isActive ? '' : 'hidden'}>
100+
${page.modals.map(modal => {
101+
return yo`<div class="modal">
102+
${modal.render({
103+
rerender: () => update(page),
104+
remove: () => remove(page, modal)
105+
})}
106+
</div>`
107+
})}
108+
</div>`
109+
}
110+
111+
function onGlobalKeydown (e) {
112+
if (e.key === 'Escape') {
113+
// close any active modals
114+
forceRemoveAll(pages.getActive())
115+
}
116+
}

app/shell-window/ui/modals/example.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as yo from 'yo-yo'
2+
import * as modal from '../modal'
3+
4+
// exported api
5+
// =
6+
7+
export default function (page, opts = {}) {
8+
return new Promise((resolve, reject) => {
9+
// create the modal
10+
let i = opts.i ? opts.i : 0
11+
modal.add(page, {
12+
render: ({rerender, remove}) => {
13+
function oninc () {
14+
i++
15+
rerender()
16+
}
17+
18+
return yo`
19+
<div style="padding: 10px 20px 6px; width: 500px;">
20+
<h3>Test Modal</h3>
21+
<p>
22+
Hello world! <a class="link" onclick=${oninc}>Click me (${i} clicks)</a>
23+
</p>
24+
<p>
25+
<a class="btn primary" onclick=${() => { remove(); resolve(i) }}>OK</a>
26+
<a class="btn" onclick=${() => { remove(); reject(new Error('Canceled')) }}>Cancel</a>
27+
</p>
28+
</div>
29+
`
30+
},
31+
onForceClose: () => {
32+
reject(new Error('Closed'))
33+
}
34+
})
35+
})
36+
}

0 commit comments

Comments
 (0)