diff --git a/app/about.html b/app/about.html index 884976c0a..9c1f96221 100644 --- a/app/about.html +++ b/app/about.html @@ -71,10 +71,6 @@
-
diff --git a/app/index.html b/app/index.html index 3960bf74e..58573db5f 100644 --- a/app/index.html +++ b/app/index.html @@ -71,10 +71,6 @@
-
diff --git a/app/scripts/dialog/help.mjs b/app/scripts/dialog/help.mjs new file mode 100644 index 000000000..62d6f9647 --- /dev/null +++ b/app/scripts/dialog/help.mjs @@ -0,0 +1,20 @@ +export class QRCodeHelpDialog { + + constructor(root) { + const elements = this.elements = {}; + const rootElement = document.getElementById(root); + elements['close'] = rootElement.querySelector(".QRCodeAboutDialog-close"); + + elements['close'].addEventListener("click", function () { + this.closeDialog(); + }.bind(this)); + } + + showDialog() { + root.style.display = 'block'; + } + + closeDialog() { + root.style.display = 'none'; + } +} \ No newline at end of file diff --git a/app/scripts/dialog/success.mjs b/app/scripts/dialog/success.mjs new file mode 100644 index 000000000..806bc9d39 --- /dev/null +++ b/app/scripts/dialog/success.mjs @@ -0,0 +1,69 @@ +export class QRCodeSuccessDialog { + constructor(element) { + const elements = this._elements = {}; + const root = elements['root'] = document.getElementById(element); + elements['qrcodeData'] = root.querySelector(".QRCodeSuccessDialog-data"); + elements['qrcodeNavigate'] = root.querySelector(".QRCodeSuccessDialog-navigate"); + elements['qrcodeIgnore'] = root.querySelector(".QRCodeSuccessDialog-ignore"); + elements['qrcodeShare'] = root.querySelector(".QRCodeSuccessDialog-share"); + elements['qrcodeCopy'] = root.querySelector(".QRCodeSuccessDialog-copy"); + + this.currentUrl = undefined; + + if (navigator.share) { + // Sharing is supported so let's make the UI visible + elements['qrcodeShare'].classList.remove('hidden'); + } + + if (navigator.clipboard && navigator.clipboard.writeText) { + elements['qrcodeCopy'].classList.remove('hidden'); + } + + elements['qrcodeIgnore'].addEventListener("click", function () { + this.closeDialog(); + }.bind(this)); + + elements['qrcodeShare'].addEventListener("click", function () { + if (navigator.share) { + navigator.share({ + title: this.currentUrl, + text: this.currentUrl, + url: this.currentUrl + }).then(function () { + this.closeDialog(); + }).catch(function () { + this.closeDialog(); + }) + } + + }.bind(this)); + + elements['qrcodeCopy'].addEventListener("click", function () { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(this.currentUrl) + .then(this.closeDialog()) + .catch(this.closeDialog()); + } + this.closeDialog(); + }.bind(this)); + + elements['qrcodeNavigate'].addEventListener("click", function () { + if (this.currentUrl.protocol === "javascript:") { + console.log("XSS prevented!"); + return; + } + window.location = this.currentUrl; + this.closeDialog(); + }.bind(this)); + } + + showDialog(normalizedUrl) { + this._elements['root'].style.display = 'block'; + this._elements['qrcodeData'].innerText = normalizedUrl; + } + + closeDialog() { + this._elements['root'].style.display = 'none'; + this._elements['qrcodeData'].innerText = ""; + } +} \ No newline at end of file diff --git a/app/scripts/main.mjs b/app/scripts/main.mjs index 30d6bde29..4125a8fe1 100644 --- a/app/scripts/main.mjs +++ b/app/scripts/main.mjs @@ -17,21 +17,50 @@ * */ -import { decode } from './qrclient.js' +import { decode } from './qrclient.js'; +import { QRCodeHelpDialog as HelpDialog } from './dialog/help.mjs'; +import { QRCodeSuccessDialog as SuccessDialog } from './dialog/success.mjs'; +import { FallbackView } from './view/fallback.mjs'; +import { CameraView } from './view/camera.mjs'; + +var normalizeUrl = function(url) { + // Remove leading/trailing white space from protocol, normalize casing, etc. + var normalized; + try { + normalized = new URL(url); + } catch (exception) { + return; + } + return normalized; +}; + + + (function() { 'use strict'; + + // This is the App. var QRCodeCamera = function(element) { // Controls the Camera and the QRCode Module - + var cameraManager = new CameraManager('camera'); - var qrCodeManager = new QRCodeManager('qrcode'); - var qrCodeHelpManager = new QRCodeHelpManager('about'); + var successDialog = new SuccessDialog('qrcode'); + var helpDialog = new HelpDialog('about'); var helpButton = document.querySelector('.about'); helpButton.onclick = function() { - qrCodeHelpManager.showDialog(); + helpDialog.showDialog(); + }; + + var detectQRCode = async function(context) { + let result = await decode(context); + let normalizedUrl; + if(result !== undefined) { + normalizedUrl = normalizeUrl(result); + } + return normalizedUrl; }; var processingFrame = false; @@ -40,449 +69,22 @@ import { decode } from './qrclient.js' // There is a frame in the camera, what should we do with it? if(processingFrame == false) { processingFrame = true; - let url = await qrCodeManager.detectQRCode(context); + let url = await detectQRCode(context); processingFrame = false; if(url === undefined) return; if('ga' in window) ga('send', 'event', 'urlfound'); if('vibrate' in navigator) navigator.vibrate([200]); - qrCodeManager.showDialog(url); - } - }; - }; - - var QRCodeCallbackController = function(element) { - var callbackName = element.querySelector(".QRCodeSuccessDialogCallback-name"); - var callbackDomain = element.querySelector(".QRCodeSuccessDialogCallback-domain"); - var callbackUrl; - var qrcodeUrl; - var isValidCallbackUrl = false; - - this.setQrCode = function(normalizedUrl) { - qrcodeUrl = normalizedUrl; - }; - - var init = function() { - callbackUrl = getCallbackURL(); - isValidCallbackUrl = validateCallbackURL(callbackUrl); - - if(callbackUrl) { - element.addEventListener('click', function() { - // Maybe we should warn if the callback URL is invalid - callbackUrl.searchParams.set('qrcode', qrcodeUrl); - location = callbackUrl; - }); - - element.classList.remove('hidden'); - if(isValidCallbackUrl == false) { - callbackDomain.classList.add('invalid'); - } - callbackDomain.innerText = callbackUrl.origin; - } - }; - - var validateCallbackURL = function(callbackUrl) { - if(document.referrer === "") return false; - - var referrer = new URL(document.referrer); - - return (callbackUrl !== undefined - && referrer.origin == callbackUrl.origin - && referrer.scheme !== 'https'); - }; - - var getCallbackURL = function() { - var url = new URL(window.location); - if('searchParams' in url && url.searchParams.has('x-callback-url')) { - // If the API is not supported, we should shim it. But right now - // let's just get it working - return new URL(url.searchParams.get('x-callback-url')); - } - }; - - init(); - }; - - var normalizeUrl = function(url) { - // Remove leading/trailing white space from protocol, normalize casing, etc. - var normalized; - try { - normalized = new URL(url); - } catch (exception) { - return; - } - return normalized; - }; - - var QRCodeManager = function(element) { - var root = document.getElementById(element); - var qrcodeData = root.querySelector(".QRCodeSuccessDialog-data"); - var qrcodeNavigate = root.querySelector(".QRCodeSuccessDialog-navigate"); - var qrcodeIgnore = root.querySelector(".QRCodeSuccessDialog-ignore"); - var qrcodeShare = root.querySelector(".QRCodeSuccessDialog-share"); - var qrcodeCopy = root.querySelector(".QRCodeSuccessDialog-copy"); - var qrcodeCallback = root.querySelector(".QRCodeSuccessDialog-callback"); - var callbackController = new QRCodeCallbackController(qrcodeCallback); - - var self = this; - - this.currentUrl = undefined; - - if(navigator.share) { - // Sharing is supported so let's make the UI visible - qrcodeShare.classList.remove('hidden'); - } - - if(navigator.clipboard && navigator.clipboard.writeText) { - qrcodeCopy.classList.remove('hidden'); - } - - this.detectQRCode = async function(context) { - let result = await decode(context); - let normalizedUrl; - if(result !== undefined) { - normalizedUrl = normalizeUrl(result); - self.currentUrl = normalizedUrl; - } - return normalizedUrl; - }; - - this.showDialog = function(normalizedUrl) { - root.style.display = 'block'; - qrcodeData.innerText = normalizedUrl; - callbackController.setQrCode(normalizedUrl); - }; - - this.closeDialog = function() { - root.style.display = 'none'; - qrcodeData.innerText = ""; - }; - - qrcodeIgnore.addEventListener("click", function() { - this.closeDialog(); - }.bind(this)); - - qrcodeShare.addEventListener("click", function() { - if(navigator.share) { - navigator.share({ - title: this.currentUrl, - text: this.currentUrl, - url: this.currentUrl - }).then(function() { - self.closeDialog(); - }).catch(function() { - self.closeDialog(); - }) - } - - }.bind(this)); - - qrcodeCopy.addEventListener("click", function() { - if(navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(this.currentUrl) - .then(self.closeDialog()) - .catch(self.closeDialog()); - } - self.closeDialog(); - }.bind(this)); - - qrcodeNavigate.addEventListener("click", function() { - // I really want this to be a link. - - // Prevent XSS. - // Note: there's no need to check for `jAvAsCrIpT:` etc. as - // `normalizeUrl` already took care of that. - if (this.currentUrl.protocol === "javascript:") { - console.log("XSS prevented!"); - return; - } - window.location = this.currentUrl; - this.closeDialog(); - }.bind(this)); - - }; - - let QRCodeHelpManager = function(element) { - let root = document.getElementById(element); - let qrhelpClose = root.querySelector(".QRCodeAboutDialog-close"); - - this.showDialog = function() { - root.style.display = 'block'; - }; - - this.closeDialog = function() { - root.style.display = 'none'; - }; - - qrhelpClose.addEventListener("click", function() { - this.closeDialog(); - }.bind(this)); - }; - - var WebCamManager = function(cameraRoot) { - var width, height; - var cameraToggleInput = cameraRoot.querySelector('.Camera-toggle-input'); - var cameraToggle = cameraRoot.querySelector('.Camera-toggle'); - var cameraVideo = cameraRoot.querySelector('.Camera-video'); - - this.resize = function(w, h) { - if (w && h) { - height = h; - width = w; - } - - var videoDimensions = this.getDimensions(); - cameraVideo.style.transform = 'translate(-50%, -50%) scale(' + videoDimensions.scaleFactor + ')'; - }.bind(this); - - var source = new CameraSource(cameraVideo); - - this.getDimensions = function() { - var dimensions = source.getDimensions(); - var heightRatio = dimensions.height / height; - var widthRatio = dimensions.width / width; - var scaleFactor = 1 / Math.min(heightRatio, widthRatio); - dimensions.scaleFactor = Number.isFinite(scaleFactor)? scaleFactor : 1; - return dimensions; - }; - - // this method can be overwritten from outside - this.onDimensionsChanged = function(){}; - - source.onDimensionsChanged = function() { - this.onDimensionsChanged(); - this.resize(); - }.bind(this); - - source.getCameras(function(cameras) { - if(cameras.length <= 1) { - cameraToggle.style.display="none"; + successDialog.showDialog(url); } - - // Set the source - source.setCamera(0); - }); - - source.onframeready = function(imageData) { - // The Source has some data, we need to push it the controller. - this.onframeready(imageData); - }.bind(this); - - cameraToggleInput.addEventListener('change', function(e) { - // this is the input element, not the control - var cameraIdx = 0; - - if(e.target.checked === true) { - cameraIdx = 1; - } - source.stop(); - source.setCamera(cameraIdx); - }); - - this.stop = function() { - source.stop(); - }; - - this.start = function() { - var cameraIdx = 0; - if(cameraToggleInput.checked === true) { - cameraIdx = 1; - } - source.setCamera(cameraIdx); - }; - - // When using the web cam, we need to turn it off when we aren't using it - document.addEventListener('visibilitychange', function() { - if(document.visibilityState === 'hidden') { - // Disconnect the camera. - this.stop(); - } - else { - this.start(); - } - }.bind(this)); - - }; - - var CameraFallbackManager = function(element) { - var uploadForm = element.querySelector('.CameraFallback-form'); - var inputElement = element.querySelector('.CameraFallback-input'); - var image = new Image(); - - // these methods can be overwritten from outside - this.onframeready = function() {}; - this.onDimensionsChanged = function(){}; - - // these methods are noop for the fallback - this.resize = function() {}; - - // We don't need to upload anything. - uploadForm.addEventListener('submit', function(e) { - e.preventDefault(); - return false; - }); - - inputElement.addEventListener('change', function(e) { - var objectURL = URL.createObjectURL(e.target.files[0]); - image.onload = function() { - this.onDimensionsChanged(); - this.onframeready(image); - URL.revokeObjectURL(objectURL); - }.bind(this); - - image.src = objectURL; - - }.bind(this)); - - this.getDimensions = function() { - return { - width: image.naturalWidth, - height: image.naturalHeight, - scaleFactor: 1 - }; }; }; - var CameraSource = function(videoElement) { - var stream; - var animationFrameId; - var cameras = null; - var self = this; - var useMediaDevices = ('mediaDevices' in navigator - && 'enumerateDevices' in navigator.mediaDevices - && 'getUserMedia' in navigator.mediaDevices); - var gUM = (navigator.getUserMedia || - navigator.webkitGetUserMedia || - navigator.mozGetUserMedia || - navigator.msGetUserMedia || null); - var currentCamera = -1; - - this.stop = function() { - currentCamera = -1; - if(stream) { - stream.getTracks().forEach(function(t) { t.stop(); } ); - } - }; - - this.getDimensions = function() { - return { - width: videoElement.videoWidth, - height: videoElement.videoHeight - }; - }; - - // this method can be overwritten from outside - this.onDimensionsChanged = function(){}; - - this.getCameras = function(cb) { - cb = cb || function() {}; - - if('enumerateDevices' in navigator.mediaDevices) { - navigator.mediaDevices.enumerateDevices() - .then(function(sources) { - return sources.filter(function(source) { - return source.kind == 'videoinput' - }); - }) - .then(function(sources) { - cameras = []; - sources.forEach(function(source) { - if(source.label.indexOf('facing back') >= 0) { - // move front facing to the front. - cameras.unshift(source); - } - else { - cameras.push(source); - } - }); - - cb(cameras); - }) - .catch(error => { - console.error("Enumeration Error", error); - }); - } - else if('getSources' in MediaStreamTrack) { - MediaStreamTrack.getSources(function(sources) { - cameras = []; - for(var i = 0; i < sources.length; i++) { - var source = sources[i]; - if(source.kind === 'video') { - - if(source.facing === 'environment') { - // cameras facing the environment are pushed to the front of the page - cameras.unshift(source); - } - else { - cameras.push(source); - } - } - } - cb(cameras); - }); - } - else { - // We can't pick the correct camera because the API doesn't support it. - cameras = []; - cb(cameras); - } - }; - - this.setCamera = function(idx) { - if (currentCamera === idx || cameras === null) { - return; - } - currentCamera = idx; - var params; - var videoSource = cameras[idx]; - - //Cancel any pending frame analysis - cancelAnimationFrame(animationFrameId); - - if(videoSource === undefined && cameras.length == 0) { - // Because we have no source information, have to assume it user facing. - params = { video: true, audio: false }; - } - else { - params = { video: { deviceId: { exact: videoSource.deviceId || videoSource.id } }, audio: false }; - } - - let selectStream = function(cameraStream) { - stream = cameraStream; - - videoElement.addEventListener('loadeddata', function(e) { - var onframe = function() { - if(videoElement.videoWidth > 0) self.onframeready(videoElement); - if (currentCamera !== -1) { - // if the camera is still running - animationFrameId = requestAnimationFrame(onframe); - } - }; - - self.onDimensionsChanged(); - - animationFrameId = requestAnimationFrame(onframe); - }); - - videoElement.srcObject = stream; - videoElement.load(); - videoElement.play() - .catch(error => { - console.error("Auto Play Error", error); - }); - } - - if (useMediaDevices) { - navigator.mediaDevices.getUserMedia(params) - .then(selectStream) - .catch(console.error); - } - else { - gUM.call(navigator, params, selectStream, console.error); - } - }; - }; + + + + // This is really the Controller. var CameraManager = function(element) { // The camera gets a video stream, and adds it to a canvas. // The canvas is analysed but also displayed to the user. @@ -498,24 +100,23 @@ import { decode } from './qrclient.js' if(location.hash == "#canvasdebug") debug = true; var root = document.getElementById(element); - var cameraRoot; var sourceManager; + let onframeready = function(frameData) { + // Work out which part of the video to capture and apply to canvas. + context.drawImage(frameData, sx, sy, sWidth, sHeight, 0, 0, dWidth, dHeight); + if(self.onframe) self.onframe(context); + }; + // Where are we getting the data from if(gUMPresent === false) { - cameraRoot = root.querySelector('.CameraFallback'); - sourceManager = new CameraFallbackManager(cameraRoot); + sourceManager = new FallbackView('.CameraFallback', onframeready); } else { - cameraRoot = root.querySelector('.CameraRealtime'); - sourceManager = new WebCamManager(cameraRoot); - } - - if(debug) { - root.classList.add('debug'); + sourceManager = new CameraView('.CameraRealtime', onframeready); } - cameraRoot.classList.remove('hidden'); + sourceManager.show(); var cameraCanvas = root.querySelector('.Camera-display'); var cameraOverlay = root.querySelector('.Camera-overlay'); @@ -530,14 +131,7 @@ import { decode } from './qrclient.js' var sHeight; var sWidth; - var cameras = []; - var prevCoordinates = 0; - - sourceManager.onframeready = function(frameData) { - // Work out which part of the video to capture and apply to canvas. - context.drawImage(frameData, sx, sy, sWidth, sHeight, 0, 0, dWidth, dHeight); - if(self.onframe) self.onframe(context); - }; + // Abstract the Overlay as a UI component var getOverlayDimensions = function(width, height) { var minLength = Math.min(width, height); diff --git a/app/scripts/view/base.mjs b/app/scripts/view/base.mjs new file mode 100644 index 000000000..37050cdab --- /dev/null +++ b/app/scripts/view/base.mjs @@ -0,0 +1,20 @@ +export class BaseView { + + constructor(root, onframeready) { + this.root = document.querySelector(root); + this.onframeready = onframeready; + this.elements = {}; + } + + show() { + this.root.classList.remove('hidden'); + } + + resize() { } + + onframeready(frameData) { + this.onframeready(frameData) + } + + onDimensionsChanged() {} +} \ No newline at end of file diff --git a/app/scripts/view/camera.mjs b/app/scripts/view/camera.mjs new file mode 100644 index 000000000..b1d262a6d --- /dev/null +++ b/app/scripts/view/camera.mjs @@ -0,0 +1,231 @@ +import { BaseView } from './base.mjs'; + + +var CameraSource = function (videoElement) { + var stream; + var animationFrameId; + var cameras = null; + var self = this; + var useMediaDevices = ('mediaDevices' in navigator + && 'enumerateDevices' in navigator.mediaDevices + && 'getUserMedia' in navigator.mediaDevices); + var gUM = (navigator.getUserMedia || + navigator.webkitGetUserMedia || + navigator.mozGetUserMedia || + navigator.msGetUserMedia || null); + var currentCamera = -1; + + this.stop = function () { + currentCamera = -1; + if (stream) { + stream.getTracks().forEach(function (t) { t.stop(); }); + } + }; + + this.getDimensions = function () { + return { + width: videoElement.videoWidth, + height: videoElement.videoHeight + }; + }; + + // this method can be overwritten from outside + this.onDimensionsChanged = function () { }; + + this.getCameras = function (cb) { + cb = cb || function () { }; + + if ('enumerateDevices' in navigator.mediaDevices) { + navigator.mediaDevices.enumerateDevices() + .then(function (sources) { + return sources.filter(function (source) { + return source.kind == 'videoinput' + }); + }) + .then(function (sources) { + cameras = []; + sources.forEach(function (source) { + if (source.label.indexOf('facing back') >= 0) { + // move front facing to the front. + cameras.unshift(source); + } + else { + cameras.push(source); + } + }); + + cb(cameras); + }) + .catch(error => { + console.error("Enumeration Error", error); + }); + } + else if ('getSources' in MediaStreamTrack) { + MediaStreamTrack.getSources(function (sources) { + cameras = []; + for (var i = 0; i < sources.length; i++) { + var source = sources[i]; + if (source.kind === 'video') { + + if (source.facing === 'environment') { + // cameras facing the environment are pushed to the front of the page + cameras.unshift(source); + } + else { + cameras.push(source); + } + } + } + cb(cameras); + }); + } + else { + // We can't pick the correct camera because the API doesn't support it. + cameras = []; + cb(cameras); + } + }; + + this.setCamera = function (idx) { + if (currentCamera === idx || cameras === null) { + return; + } + currentCamera = idx; + var params; + var videoSource = cameras[idx]; + + //Cancel any pending frame analysis + cancelAnimationFrame(animationFrameId); + + if (videoSource === undefined && cameras.length == 0) { + // Because we have no source information, have to assume it user facing. + params = { video: true, audio: false }; + } + else { + params = { video: { deviceId: { exact: videoSource.deviceId || videoSource.id } }, audio: false }; + } + + let selectStream = function (cameraStream) { + stream = cameraStream; + + videoElement.addEventListener('loadeddata', function (e) { + var onframe = function () { + if (videoElement.videoWidth > 0) self.onframeready(videoElement); + if (currentCamera !== -1) { + // if the camera is still running + animationFrameId = requestAnimationFrame(onframe); + } + }; + + self.onDimensionsChanged(); + + animationFrameId = requestAnimationFrame(onframe); + }); + + videoElement.srcObject = stream; + videoElement.load(); + videoElement.play() + .catch(error => { + console.error("Auto Play Error", error); + }); + } + + if (useMediaDevices) { + navigator.mediaDevices.getUserMedia(params) + .then(selectStream) + .catch(console.error); + } + else { + gUM.call(navigator, params, selectStream, console.error); + } + }; +}; + +export class CameraView extends BaseView { + + constructor(element, onframeready) { + super(element, onframeready); + + this.width = undefined; + this.height = undefined; + + this.elements['cameraToggleInput'] = this.root.querySelector('.Camera-toggle-input'); + this.elements['cameraToggle'] = this.root.querySelector('.Camera-toggle'); + this.elements['cameraVideo'] = this.root.querySelector('.Camera-video'); + + this.source = new CameraSource(this.elements['cameraVideo']); + + this.source.onDimensionsChanged = function () { + this.onDimensionsChanged(); + this.resize(); + }.bind(this); + + this.source.getCameras(function (cameras) { + if (cameras.length <= 1) { + this.elements['cameraToggle'].style.display = "none"; + } + + // Set the source + this.source.setCamera(0); + }.bind(this)); + + this.source.onframeready = function (imageData) { + // The Source has some data, we need to push it the controller. + this.onframeready(imageData); + }.bind(this); + + this.elements['cameraToggleInput'].addEventListener('change', function (e) { + // this is the input element, not the control + var cameraIdx = 0; + + if (e.target.checked === true) { + cameraIdx = 1; + } + this.source.stop(); + this.source.setCamera(cameraIdx); + }); + + this.stop = function () { + this.source.stop(); + }; + + this.start = function () { + var cameraIdx = 0; + if (this.elements['cameraToggleInput'].checked === true) { + cameraIdx = 1; + } + this.source.setCamera(cameraIdx); + }; + + // When using the web cam, we need to turn it off when we aren't using it + document.addEventListener('visibilitychange', function () { + if (document.visibilityState === 'hidden') { + // Disconnect the camera. + this.stop(); + } + else { + this.start(); + } + }.bind(this)); + + } + + resize(w, h) { + if (w && h) { + this.height = h; + this.width = w; + } + + const videoDimensions = this.getDimensions(); + this.elements['cameraVideo'].style.transform = 'translate(-50%, -50%) scale(' + videoDimensions.scaleFactor + ')'; + } + + getDimensions() { + var dimensions = this.source.getDimensions(); + var heightRatio = dimensions.height / this.height; + var widthRatio = dimensions.width / this.width; + var scaleFactor = 1 / Math.min(heightRatio, widthRatio); + dimensions.scaleFactor = Number.isFinite(scaleFactor) ? scaleFactor : 1; + return dimensions; + } +} diff --git a/app/scripts/view/fallback.mjs b/app/scripts/view/fallback.mjs new file mode 100644 index 000000000..488fe7d07 --- /dev/null +++ b/app/scripts/view/fallback.mjs @@ -0,0 +1,37 @@ +import {BaseView} from './base.mjs'; + +export class FallbackView extends BaseView { + constructor(element, onframeready) { + super(element, onframeready); + + var uploadForm = this.root.querySelector('.CameraFallback-form'); + var inputElement = this.root.querySelector('.CameraFallback-input'); + var image = new Image(); + + // We don't need to upload anything. + uploadForm.addEventListener('submit', function(e) { + e.preventDefault(); + return false; + }); + + inputElement.addEventListener('change', function(e) { + var objectURL = URL.createObjectURL(e.target.files[0]); + image.onload = function() { + this.onDimensionsChanged(); + this.onframeready(image); + URL.revokeObjectURL(objectURL); + }.bind(this); + + image.src = objectURL; + + }.bind(this)); + + this.getDimensions = function() { + return { + width: image.naturalWidth, + height: image.naturalHeight, + scaleFactor: 1 + }; + }; + } +} diff --git a/package-lock.json b/package-lock.json index ece1c1a06..6f8a9dce2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2392,9 +2392,9 @@ }, "dependencies": { "mime-db": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.39.0.tgz", - "integrity": "sha512-DTsrw/iWVvwHH+9Otxccdyy0Tgiil6TWK/xhfARJZF/QFhwOgZgOIvA2/VIGpM8U7Q8z5nDmdDWC6tuVMJNibw==" + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" } } }, @@ -2921,24 +2921,47 @@ "dev": true }, "del": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", - "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.0.tgz", + "integrity": "sha512-C4kvKNlYrwXhKxz97BuohF8YoGgQ23Xm9lvoHmgT7JaPGprSEjk3+XFled74Yt/x0ZABUHg2D67covzAPUKx5Q==", "dev": true, "requires": { "globby": "^6.1.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "p-map": "^1.1.1", - "pify": "^3.0.0", - "rimraf": "^2.2.8" + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" }, "dependencies": { + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } } } }, @@ -3925,14 +3948,14 @@ "dev": true }, "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.8.tgz", + "integrity": "sha512-tPvHgPGB7m40CZ68xqFGkKuzN+RnpGmSV+hgeKxhRpbxdqKXUFJGC3yonBOLzQBcJyGpdZFDfCsdOC2KFsXzeA==", "dev": true, "optional": true, "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" }, "dependencies": { "abbrev": { @@ -3954,7 +3977,7 @@ "optional": true }, "are-we-there-yet": { - "version": "1.1.4", + "version": "1.1.5", "bundled": true, "dev": true, "optional": true, @@ -3980,7 +4003,7 @@ } }, "chownr": { - "version": "1.0.1", + "version": "1.1.1", "bundled": true, "dev": true, "optional": true @@ -4010,16 +4033,16 @@ "optional": true }, "debug": { - "version": "2.6.9", + "version": "4.1.1", "bundled": true, "dev": true, "optional": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "deep-extend": { - "version": "0.5.1", + "version": "0.6.0", "bundled": true, "dev": true, "optional": true @@ -4068,7 +4091,7 @@ } }, "glob": { - "version": "7.1.2", + "version": "7.1.3", "bundled": true, "dev": true, "optional": true, @@ -4088,12 +4111,12 @@ "optional": true }, "iconv-lite": { - "version": "0.4.21", + "version": "0.4.24", "bundled": true, "dev": true, "optional": true, "requires": { - "safer-buffer": "^2.1.0" + "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { @@ -4158,17 +4181,17 @@ "optional": true }, "minipass": { - "version": "2.2.4", + "version": "2.3.5", "bundled": true, "dev": true, "optional": true, "requires": { - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "minizlib": { - "version": "1.1.0", + "version": "1.2.1", "bundled": true, "dev": true, "optional": true, @@ -4186,35 +4209,35 @@ } }, "ms": { - "version": "2.0.0", + "version": "2.1.1", "bundled": true, "dev": true, "optional": true }, "needle": { - "version": "2.2.0", + "version": "2.3.0", "bundled": true, "dev": true, "optional": true, "requires": { - "debug": "^2.1.2", + "debug": "^4.1.0", "iconv-lite": "^0.4.4", "sax": "^1.2.4" } }, "node-pre-gyp": { - "version": "0.10.0", + "version": "0.12.0", "bundled": true, "dev": true, "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", - "needle": "^2.2.0", + "needle": "^2.2.1", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", - "rc": "^1.1.7", + "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4" @@ -4231,13 +4254,13 @@ } }, "npm-bundled": { - "version": "1.0.3", + "version": "1.0.6", "bundled": true, "dev": true, "optional": true }, "npm-packlist": { - "version": "1.1.10", + "version": "1.4.1", "bundled": true, "dev": true, "optional": true, @@ -4314,12 +4337,12 @@ "optional": true }, "rc": { - "version": "1.2.7", + "version": "1.2.8", "bundled": true, "dev": true, "optional": true, "requires": { - "deep-extend": "^0.5.1", + "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -4349,16 +4372,16 @@ } }, "rimraf": { - "version": "2.6.2", + "version": "2.6.3", "bundled": true, "dev": true, "optional": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "safe-buffer": { - "version": "5.1.1", + "version": "5.1.2", "bundled": true, "dev": true, "optional": true @@ -4376,7 +4399,7 @@ "optional": true }, "semver": { - "version": "5.5.0", + "version": "5.7.0", "bundled": true, "dev": true, "optional": true @@ -4429,17 +4452,17 @@ "optional": true }, "tar": { - "version": "4.4.1", + "version": "4.4.8", "bundled": true, "dev": true, "optional": true, "requires": { - "chownr": "^1.0.1", + "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.2" } }, @@ -4450,12 +4473,12 @@ "optional": true }, "wide-align": { - "version": "1.1.2", + "version": "1.1.3", "bundled": true, "dev": true, "optional": true, "requires": { - "string-width": "^1.0.2" + "string-width": "^1.0.2 || 2" } }, "wrappy": { @@ -4465,7 +4488,7 @@ "optional": true }, "yallist": { - "version": "3.0.2", + "version": "3.0.3", "bundled": true, "dev": true, "optional": true @@ -6131,9 +6154,9 @@ } }, "js-yaml": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", - "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "dev": true, "optional": true, "requires": { @@ -6489,27 +6512,27 @@ "optional": true }, "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.1.0.tgz", + "integrity": "sha512-Sc5j3/YnM8tDeyCsVeKlm/0p95075DyLmDEIkSgQ7mXkrOX+uTCtmQFm0CYzVyJwcCCmO3k8qfJt17SxQwB5Zw==", "dev": true }, "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", "dev": true, "requires": { - "is-path-inside": "^1.0.0" + "is-path-inside": "^2.1.0" } }, "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", "dev": true, "requires": { - "path-is-inside": "^1.0.1" + "path-is-inside": "^1.0.2" } }, "is-plain-obj": { @@ -7217,17 +7240,17 @@ "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" }, "mime-types": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", - "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", "requires": { - "mime-db": "~1.38.0" + "mime-db": "1.40.0" }, "dependencies": { "mime-db": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", - "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" } } }, @@ -7292,9 +7315,9 @@ "dev": true }, "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", "dev": true, "optional": true }, @@ -7406,9 +7429,9 @@ } }, "now": { - "version": "13.1.3", - "resolved": "https://registry.npmjs.org/now/-/now-13.1.3.tgz", - "integrity": "sha512-DYRaOqYblnyXF8Awzy/1+mCWjXESxVe7H9phXRGOUm6Vu2dXuJ07yOU37BYESmRue+Aw4sc51W2duzkhwBuCOA==" + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/now/-/now-15.0.6.tgz", + "integrity": "sha512-kX58WG6ioe8guduIUFW+9yTvQt5XcYQsuSQJL1J1XLpWeJIAy951Bkfvli/tLdMXB/lNwlOQUnMyL4zobBSqlA==" }, "now-and-later": { "version": "2.0.0", @@ -7769,9 +7792,9 @@ } }, "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", "dev": true }, "p-map-series": { @@ -9020,9 +9043,9 @@ "integrity": "sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw==" }, "serve": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/serve/-/serve-10.1.2.tgz", - "integrity": "sha512-TVH35uwndRlCqSeX3grR3Ntrjx2aBTeu6sx+zTD2CzN2N/rHuEDTvxiBwWbrellJNyWiQFz2xZmoW+UxV+Zahg==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/serve/-/serve-11.0.0.tgz", + "integrity": "sha512-Gnyyp3JAtRUo0dRH1/YWPKbnaXHfzQBiVh9+qSUi6tyVcVA8twUP2c+GnOwsoe9Ss7dfOHJUTSA4fdWP//Y4gQ==", "requires": { "@zeit/schemas": "2.6.0", "ajv": "6.5.3", @@ -9031,7 +9054,7 @@ "chalk": "2.4.1", "clipboardy": "1.2.3", "compression": "1.7.3", - "serve-handler": "5.0.8", + "serve-handler": "6.0.0", "update-check": "1.5.2" }, "dependencies": { @@ -9069,9 +9092,9 @@ } }, "serve-handler": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-5.0.8.tgz", - "integrity": "sha512-pqk0SChbBLLHfMIxQ55czjdiW7tj2cFy53svvP8e5VqEN/uB/QpfiTJ8k1uIYeFTDVoi+FGi5aqXScuu88bymg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.0.0.tgz", + "integrity": "sha512-2/e0+N1abV1HAN+YN8uCOPi1B0bIYaR6kRcSfzezRwszak5Yzr6QhT34XJk2Bw89rhXenqwLNJb4NnF2/krnGQ==", "requires": { "bytes": "3.0.0", "content-disposition": "0.5.2", diff --git a/package.json b/package.json index 9b7b1b9b1..2e3bcd1fb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "devDependencies": { "@babel/core": "^7.4.3", "@babel/preset-env": "^7.4.3", - "del": "^3.0.0", + "del": "^4.0.0", "gulp": "^4.0.0", "gulp-autoprefixer": "^6.0.0", "gulp-babel": "^8.0.0", @@ -30,10 +30,10 @@ "@babel/register": "^7.4.0", "comlink": "^3.2.0", "gulp-rollup": "^2.16.2", - "now": "^13.1.3", - "rollup": "^1.9.0", + "now": "^15.0.0", + "rollup": "^1.0.0", "rollup-plugin-terser": "^4.0.4", - "serve": "^10.1.2" + "serve": "^11.0.0" }, "babel": { "presets": [