web-request-rpc
Advanced tools
| /*! | ||
| * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. | ||
| */ | ||
| export class WebAppWindowDialog { | ||
| constructor() { | ||
| this._closeEventListeners = new Set(); | ||
| } | ||
| addEventListener(name, listener) { | ||
| if(name !== 'close') { | ||
| throw new Error(`Unknown event "${name}".`); | ||
| } | ||
| if(typeof listener !== 'function') { | ||
| throw new TypeError('"listener" must be a function.'); | ||
| } | ||
| this._closeEventListeners.add(listener); | ||
| } | ||
| removeEventListener(name, listener) { | ||
| if(name !== 'close') { | ||
| throw new Error(`Unknown event "${name}".`); | ||
| } | ||
| if(typeof listener !== 'function') { | ||
| throw new TypeError('"listener" must be a function.'); | ||
| } | ||
| this._closeEventListeners.delete(listener); | ||
| } | ||
| show() {} | ||
| close() { | ||
| // emit event to all `close` event listeners | ||
| for(const listener of this._closeEventListeners) { | ||
| listener({}); | ||
| } | ||
| } | ||
| destroy() { | ||
| this._closeEventListeners.clear(); | ||
| } | ||
| } |
| /*! | ||
| * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. | ||
| */ | ||
| import {WebAppWindowDialog} from './WebAppWindowDialog.js'; | ||
| export class WebAppWindowInlineDialog extends WebAppWindowDialog { | ||
| constructor({url, handle, className}) { | ||
| super(); | ||
| this.url = url; | ||
| this.handle = handle; | ||
| // create a top-level dialog overlay | ||
| this.dialog = document.createElement('dialog'); | ||
| applyStyle(this.dialog, { | ||
| position: 'fixed', | ||
| top: 0, | ||
| left: 0, | ||
| width: '100%', | ||
| height: '100%', | ||
| 'max-width': '100%', | ||
| 'max-height': '100%', | ||
| display: 'none', | ||
| margin: 0, | ||
| padding: 0, | ||
| border: 'none', | ||
| background: 'transparent', | ||
| color: 'black', | ||
| 'box-sizing': 'border-box', | ||
| overflow: 'hidden', | ||
| 'z-index': 1000000 | ||
| }); | ||
| this.dialog.className = 'web-app-window'; | ||
| if(typeof className === 'string') { | ||
| this.dialog.className = this.dialog.className + ' ' + className; | ||
| } | ||
| // ensure backdrop is transparent by default | ||
| const style = document.createElement('style'); | ||
| style.appendChild( | ||
| document.createTextNode(`dialog.web-app-window::backdrop { | ||
| background-color: transparent; | ||
| }`)); | ||
| // create flex container for iframe | ||
| this.container = document.createElement('div'); | ||
| applyStyle(this.container, { | ||
| position: 'relative', | ||
| width: '100%', | ||
| height: '100%', | ||
| margin: 0, | ||
| padding: 0, | ||
| display: 'flex', | ||
| 'flex-direction': 'column' | ||
| }); | ||
| this.container.className = 'web-app-window-backdrop'; | ||
| // create iframe | ||
| this.iframe = document.createElement('iframe'); | ||
| this.iframe.src = url; | ||
| this.iframe.scrolling = 'auto'; | ||
| applyStyle(this.iframe, { | ||
| position: 'fixed', | ||
| top: 0, | ||
| left: 0, | ||
| width: '100%', | ||
| height: '100%', | ||
| border: 'none', | ||
| background: 'transparent', | ||
| overflow: 'hidden', | ||
| margin: 0, | ||
| padding: 0, | ||
| 'flex-grow': 1 | ||
| }); | ||
| // assemble dialog | ||
| this.dialog.appendChild(style); | ||
| this.container.appendChild(this.iframe); | ||
| this.dialog.appendChild(this.container); | ||
| // a.document.appendChild(this.iframe); | ||
| // handle cancel (user pressed escape) | ||
| this.dialog.addEventListener('cancel', e => { | ||
| e.preventDefault(); | ||
| this.hide(); | ||
| }); | ||
| // attach to DOM | ||
| document.body.appendChild(this.dialog); | ||
| this.handle = this.iframe.contentWindow; | ||
| } | ||
| show() { | ||
| this.dialog.style.display = 'block'; | ||
| if(this.dialog.showModal) { | ||
| this.dialog.showModal(); | ||
| } | ||
| } | ||
| close() { | ||
| this.dialog.style.display = 'none'; | ||
| if(this.dialog.close) { | ||
| try { | ||
| this.dialog.close(); | ||
| } catch(e) { | ||
| console.error(e); | ||
| } | ||
| } | ||
| super.close(); | ||
| } | ||
| destroy() { | ||
| this.dialog.parentNode.removeChild(this.dialog); | ||
| super.destroy(); | ||
| } | ||
| } | ||
| function applyStyle(element, style) { | ||
| for(const name in style) { | ||
| element.style[name] = style[name]; | ||
| } | ||
| } |
| /*! | ||
| * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. | ||
| */ | ||
| import {WebAppWindowDialog} from './WebAppWindowDialog.js'; | ||
| export class WebAppWindowPopupDialog extends WebAppWindowDialog { | ||
| constructor({url, handle, bounds = {width: 500, height: 400}}) { | ||
| super(); | ||
| this.url = url; | ||
| this.handle = handle; | ||
| this._locationChanging = false; | ||
| if(!handle) { | ||
| this._openWindow({url, name: 'web-app-window', bounds}); | ||
| } | ||
| this.destroyed = false; | ||
| this._removeListeners = () => {}; | ||
| } | ||
| show() {} | ||
| close() { | ||
| this.destroy(); | ||
| } | ||
| destroy() { | ||
| if(this.handle && !this.destroyed) { | ||
| this.handle.close(); | ||
| super.close(); | ||
| this.handle = null; | ||
| this.destroyed = true; | ||
| this._removeListeners(); | ||
| super.destroy(); | ||
| } | ||
| } | ||
| isClosed() { | ||
| return !this.handle || this.handle.closed; | ||
| } | ||
| _openWindow({url, name, bounds}) { | ||
| const {x, y} = bounds; | ||
| let {width = 500, height = 400} = bounds; | ||
| width = Math.min(width, window.innerWidth); | ||
| height = Math.min(height, window.innerHeight); | ||
| const left = x !== undefined ? | ||
| x : window.screenX + (window.innerWidth - width) / 2; | ||
| const top = y !== undefined ? | ||
| y : window.screenY + (window.innerHeight - height) / 2; | ||
| const features = | ||
| 'popup=yes,menubar=no,location=no,resizable=no,scrollbars=no,status=no,' + | ||
| `width=${width},height=${height},left=${left},top=${top}`; | ||
| this._locationChanging = true; | ||
| this.handle = window.open(url, name, features); | ||
| this._addListeners(); | ||
| } | ||
| setLocation(url) { | ||
| this.url = url; | ||
| this._locationChanging = true; | ||
| this.handle.location.replace(url); | ||
| } | ||
| _addListeners() { | ||
| const destroyDialog = () => this.destroy(); | ||
| // when a new URL loads in the dialog, clear the location changing flag | ||
| const loadDialog = () => { | ||
| this._locationChanging = false; | ||
| }; | ||
| // when the dialog URL changes... | ||
| const unloadDialog = () => { | ||
| if(this._locationChanging) { | ||
| // a location change was expected, return | ||
| return; | ||
| } | ||
| // a location change was NOT expected, destroy the dialog | ||
| this.destroy(); | ||
| }; | ||
| this.handle.addEventListener('unload', unloadDialog); | ||
| this.handle.addEventListener('load', loadDialog); | ||
| // before the current window unloads, destroy the child dialog | ||
| window.addEventListener('beforeUnload', destroyDialog, {once: true}); | ||
| // poll to check for closed window handle; necessary because cross domain | ||
| // windows will not emit any close-related events we can use here | ||
| const intervalId = setInterval(() => { | ||
| if(this.isClosed()) { | ||
| this.destroy(); | ||
| clearInterval(intervalId); | ||
| } | ||
| }, 250); | ||
| // create listener clean up function | ||
| this._removeListeners = () => { | ||
| clearInterval(intervalId); | ||
| this.handle.removeListener('unload', unloadDialog); | ||
| this.handle.removeListener('load', loadDialog); | ||
| window.removeEventListener('beforeUnload', destroyDialog); | ||
| } | ||
| } | ||
| } |
+6
-0
| # web-request-rpc ChangeLog | ||
| ## 2.0.0 - 2022-06-13 | ||
| ### Changed | ||
| - **BREAKING**: Enable the use of 1p popup dialogs or iframes when creating | ||
| new web app windows. | ||
| ## 1.1.7 - 2021-01-22 | ||
@@ -4,0 +10,0 @@ |
+2
-2
@@ -30,3 +30,3 @@ /*! | ||
| * a handle) to send messages to | ||
| * (defaults to `window.parent || window.opener`). | ||
| * (defaults to `window.opener || window.parent`). | ||
| * | ||
@@ -45,3 +45,3 @@ * @return a Promise that resolves to an RPC injector once connected. | ||
| self.origin = utils.parseUrl(origin).origin; | ||
| self._handle = options.handle || window.parent || window.opener; | ||
| self._handle = options.handle || window.opener || window.parent; | ||
@@ -48,0 +48,0 @@ const pending = self._pending; |
+1
-1
| { | ||
| "name": "web-request-rpc", | ||
| "version": "1.1.7", | ||
| "version": "2.0.0", | ||
| "description": "Web Request RPC", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
+2
-2
@@ -53,3 +53,3 @@ /*! | ||
| * a handle) to listen for messages from | ||
| * (defaults to `window.parent || window.opener`). | ||
| * (defaults to `window.opener || window.parent`). | ||
| * [ignoreUnknownApi] `true` to ignore unknown API messages. | ||
@@ -67,3 +67,3 @@ */ | ||
| self.origin = utils.parseUrl(origin).origin; | ||
| self._handle = options.handle || window.parent || window.opener; | ||
| self._handle = options.handle || window.opener || window.parent; | ||
@@ -70,0 +70,0 @@ const ignoreUnknownApi = (options.ignoreUnknownApi === 'true') || false; |
+15
-5
| /*! | ||
| * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. | ||
| * Copyright (c) 2017-2022 Digital Bazaar, Inc. All rights reserved. | ||
| */ | ||
| 'use strict'; | ||
| import {Client} from './Client.js'; | ||
@@ -44,2 +42,4 @@ import {Server} from './Server.js'; | ||
| * loads the window after its construction. | ||
| * [bounds] a bounding rectangle (top, left, width, height) to | ||
| * use when creating a popup window. | ||
| * | ||
@@ -53,6 +53,10 @@ * @return a Promise that resolves to an RPC injector once the window is | ||
| iframe, | ||
| dialog = null, | ||
| popup = false, | ||
| handle, | ||
| windowControl, | ||
| className, | ||
| customize | ||
| customize, | ||
| // top, left, width, height | ||
| bounds | ||
| } = {}) { | ||
@@ -68,9 +72,15 @@ // disallow loading the same WebAppContext more than once | ||
| timeout, | ||
| dialog, | ||
| iframe, | ||
| popup, | ||
| handle, | ||
| windowControl, | ||
| className, | ||
| customize | ||
| customize, | ||
| bounds | ||
| }); | ||
| // if the local window closes, close the control window as well | ||
| window.addEventListener('pagehide', () => this.close(), {once: true}); | ||
| // define control class; this enables the WebApp that is running in the | ||
@@ -77,0 +87,0 @@ // WebAppContext to control its UI or close itself down |
+42
-140
| /*! | ||
| * Copyright (c) 2017-2018 Digital Bazaar, Inc. All rights reserved. | ||
| * Copyright (c) 2017-2022 Digital Bazaar, Inc. All rights reserved. | ||
| */ | ||
| 'use strict'; | ||
| import {WebAppWindowInlineDialog} from './WebAppWindowInlineDialog.js'; | ||
| import {WebAppWindowPopupDialog} from './WebAppWindowPopupDialog.js'; | ||
@@ -18,13 +19,16 @@ // default timeout is 60 seconds | ||
| timeout = LOAD_WINDOW_TIMEOUT, | ||
| dialog = null, | ||
| handle, | ||
| iframe, | ||
| windowControl, | ||
| popup = false, | ||
| className = null, | ||
| customize = null | ||
| customize = null, | ||
| // top, left, width, height | ||
| bounds | ||
| } = {}) { | ||
| this.visible = false; | ||
| this.dialog = null; | ||
| this.iframe = null; | ||
| this.dialog = dialog; | ||
| this.handle = null; | ||
| this.popup = popup; | ||
| this.windowControl = null; | ||
| this._destroyed = false; | ||
| this._ready = false; | ||
@@ -34,2 +38,5 @@ this._private = {}; | ||
| if(handle && handle._dialog) { | ||
| this.dialog = dialog = handle._dialog; | ||
| } | ||
| // private to allow caller to track readiness | ||
@@ -64,40 +71,9 @@ this._private._readyPromise = new Promise((resolve, reject) => { | ||
| } | ||
| if(this.dialog) { | ||
| this.dialog.parentNode.removeChild(this.dialog); | ||
| if(!this._destroyed) { | ||
| this.dialog.destroy(); | ||
| this.dialog = null; | ||
| this._destroyed = true; | ||
| } | ||
| }; | ||
| if(iframe) { | ||
| // TODO: validate `iframe` option as much as possible | ||
| if(!(typeof iframe === 'object' && iframe.contentWindow)) { | ||
| throw new TypeError('`options.iframe` must be an iframe element.'); | ||
| } | ||
| this.windowControl = { | ||
| handle: iframe.contentWindow, | ||
| show() { | ||
| iframe.style.visibility = 'visible'; | ||
| }, | ||
| hide() { | ||
| iframe.style.visibility = 'hidden'; | ||
| } | ||
| }; | ||
| this.iframe = iframe; | ||
| this.handle = this.iframe.contentWindow; | ||
| return; | ||
| } | ||
| if(windowControl) { | ||
| // TODO: validate `windowControl` | ||
| this.windowControl = windowControl; | ||
| this.handle = this.windowControl.handle; | ||
| return; | ||
| } | ||
| if(handle) { | ||
| // TODO: validate `handle` | ||
| this.handle = handle; | ||
| return; | ||
| } | ||
| if(customize) { | ||
@@ -109,86 +85,28 @@ if(!typeof customize === 'function') { | ||
| // create a top-level dialog overlay | ||
| this.dialog = document.createElement('dialog'); | ||
| applyStyle(this.dialog, { | ||
| position: 'fixed', | ||
| top: 0, | ||
| left: 0, | ||
| width: '100%', | ||
| height: '100%', | ||
| 'max-width': '100%', | ||
| 'max-height': '100%', | ||
| display: 'none', | ||
| margin: 0, | ||
| padding: 0, | ||
| border: 'none', | ||
| background: 'transparent', | ||
| color: 'black', | ||
| 'box-sizing': 'border-box', | ||
| overflow: 'hidden', | ||
| 'z-index': 1000000 | ||
| }); | ||
| this.dialog.className = 'web-app-window'; | ||
| if(typeof className === 'string') { | ||
| this.dialog.className = this.dialog.className + ' ' + className; | ||
| if(!this.dialog) { | ||
| if(this.popup) { | ||
| this.dialog = new WebAppWindowPopupDialog({url, handle, bounds}); | ||
| } else { | ||
| this.dialog = new WebAppWindowInlineDialog({url, handle, className}); | ||
| } | ||
| } else if(this.popup && bounds) { | ||
| // resize / re-position popup window as requested | ||
| if(bounds) { | ||
| const {top: y, left: x, width, height} = bounds; | ||
| if(x !== undefined && y !== undefined) { | ||
| this.dialog.handle.moveTo(x, y); | ||
| } | ||
| if(width !== undefined && height !== undefined) { | ||
| this.dialog.handle.resizeTo(width, height); | ||
| } | ||
| } | ||
| } | ||
| // ensure backdrop is transparent by default | ||
| const style = document.createElement('style'); | ||
| style.appendChild( | ||
| document.createTextNode(`dialog.web-app-window::backdrop { | ||
| background-color: transparent; | ||
| }`)); | ||
| // create flex container for iframe | ||
| this.container = document.createElement('div'); | ||
| applyStyle(this.container, { | ||
| position: 'relative', | ||
| width: '100%', | ||
| height: '100%', | ||
| margin: 0, | ||
| padding: 0, | ||
| display: 'flex', | ||
| 'flex-direction': 'column' | ||
| }); | ||
| this.container.className = 'web-app-window-backdrop'; | ||
| // create iframe | ||
| this.iframe = document.createElement('iframe'); | ||
| this.iframe.src = url; | ||
| this.iframe.scrolling = 'auto'; | ||
| applyStyle(this.iframe, { | ||
| position: 'fixed', | ||
| top: 0, | ||
| left: 0, | ||
| width: '100%', | ||
| height: '100%', | ||
| border: 'none', | ||
| background: 'transparent', | ||
| overflow: 'hidden', | ||
| margin: 0, | ||
| padding: 0, | ||
| 'flex-grow': 1 | ||
| }); | ||
| // assemble dialog | ||
| this.dialog.appendChild(style); | ||
| this.container.appendChild(this.iframe); | ||
| this.dialog.appendChild(this.container); | ||
| // handle cancel (user pressed escape) | ||
| this.dialog.addEventListener('cancel', e => { | ||
| e.preventDefault(); | ||
| this.hide(); | ||
| }); | ||
| // attach to DOM | ||
| document.body.appendChild(this.dialog); | ||
| this.handle = this.iframe.contentWindow; | ||
| this.handle = this.dialog.handle; | ||
| if(customize) { | ||
| try { | ||
| customize({ | ||
| dialog: this.dialog, | ||
| container: this.container, | ||
| iframe: this.iframe, | ||
| dialog: this.dialog.dialog, | ||
| container: this.dialog.container, | ||
| iframe: this.dialog.iframe, | ||
| webAppWindow: this | ||
@@ -220,7 +138,4 @@ }); | ||
| body.style.overflow = 'hidden'; | ||
| if(this.dialog) { | ||
| this.dialog.style.display = 'block'; | ||
| if(this.dialog.showModal) { | ||
| this.dialog.showModal(); | ||
| } | ||
| if(!this._destroyed) { | ||
| this.dialog.show(); | ||
| } else if(this.windowControl.show) { | ||
@@ -245,11 +160,4 @@ this.windowControl.show(); | ||
| } | ||
| if(this.dialog) { | ||
| this.dialog.style.display = 'none'; | ||
| if(this.dialog.close) { | ||
| try { | ||
| this.dialog.close(); | ||
| } catch(e) { | ||
| console.error(e); | ||
| } | ||
| } | ||
| if(!this._destroyed) { | ||
| this.dialog.close(); | ||
| } else if(this.windowControl.hide) { | ||
@@ -261,7 +169,1 @@ this.windowControl.hide(); | ||
| } | ||
| function applyStyle(element, style) { | ||
| for(const name in style) { | ||
| element.style[name] = style[name]; | ||
| } | ||
| } |
42771
13.81%16
23.08%1133
16.09%