├── .github └── release-drafter.yml ├── .gitignore ├── .jshintrc ├── README.md ├── build ├── widget.js └── widget.js.map ├── demo ├── assets ├── index.html ├── remotestorage.js ├── remotestorage.js.map ├── widget.js └── widget.js.map ├── package-lock.json ├── package.json ├── src ├── assets │ ├── circle-open.svg │ ├── styles.css │ └── widget.html ├── remotestorage.js └── widget.js └── webpack.config.js /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## Changes 3 | 4 | $CHANGES 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | app/assets/js/vendor/* 4 | tmp 5 | issues 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "global", 4 | "Promise", 5 | "require", 6 | "process", 7 | "module", 8 | "exports", 9 | "document", 10 | "setInterval", 11 | "setTimeout", 12 | "clearInterval" 13 | ], 14 | "browser": false, 15 | "boss": true, 16 | "curly": true, 17 | "debug": false, 18 | "devel": true, 19 | "eqeqeq": true, 20 | "evil": true, 21 | "forin": false, 22 | "immed": false, 23 | "laxbreak": false, 24 | "newcap": true, 25 | "noarg": true, 26 | "noempty": false, 27 | "nonew": false, 28 | "nomen": false, 29 | "onevar": false, 30 | "plusplus": false, 31 | "regexp": false, 32 | "undef": true, 33 | "sub": true, 34 | "strict": false, 35 | "white": false, 36 | "eqnull": true, 37 | "esnext": true, 38 | "unused": true 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # remotestorage-widget 2 | 3 | [![npm](https://img.shields.io/npm/v/remotestorage-widget.svg)](https://www.npmjs.com/package/remotestorage-widget) 4 | 5 | A ready-to-use connect/sync widget, as add-on library for 6 | [remoteStorage.js](https://github.com/remotestorage/remotestorage.js/). 7 | 8 | ## Usage 9 | 10 | ```js 11 | import RemoteStorage from 'remotestoragejs'; 12 | import Widget from 'remotestorage-widget'; 13 | 14 | // ... 15 | 16 | const remoteStorage = new RemoteStorage(/* options */); 17 | 18 | remoteStorage.access.claim('bookmarks', 'rw'); 19 | 20 | const widget = new Widget(remoteStorage); 21 | widget.attach(); 22 | 23 | // ... 24 | ``` 25 | 26 | ## Configuration 27 | 28 | The widget has some configuration options to customize the behavior: 29 | 30 | | Option | Description | Type | Default | 31 | |---|---|---|---| 32 | | `leaveOpen` | Keep the widget open when user clicks outside of it | Boolean | `false` | 33 | | `autoCloseAfter` | Timeout after which the widget closes automatically (in milliseconds). The widget only closes when a storage is connected. | Number | `1500` | 34 | | `skipInitial` | Don't show the initial connect hint, but show sign-in screen directly instead | Boolean | `false` | 35 | | `logging` | Enable logging for debugging purposes | Boolean | `false` | 36 | | `modalBackdrop` | Show a dark, transparent backdrop when opening the widget for connecting an account. `true` shows backdrop everywhere, `false` turns it off everywhere. Default is to only show it on small screens. | Boolean, String | `"onlySmallScreens"` | 37 | 38 | Example: 39 | 40 | ```js 41 | const widget = new Widget(remoteStorage, { autoCloseAfter: 2000 }); 42 | ``` 43 | 44 | ## Available Functions 45 | 46 | `attach(elementID)` - Attach the widget to the DOM and display it. You can 47 | use an optional element ID that the widget should be attached to. 48 | Otherwise it will be attached to the body. 49 | 50 | While the `attach()` method is required for the widget to be actually 51 | shown, the following functions are usually not needed. They allow for 52 | fine-tuning the experience. 53 | 54 | `close()` - Close/minimize the widget to only show the icon. 55 | 56 | `open()` - Open the widget when it's minimized. 57 | 58 | `toggle()` - Switch between open and closed state. 59 | 60 | ## Development / Customization 61 | 62 | Install deps: 63 | 64 | npm install 65 | 66 | Build, run and watch demo/test app: 67 | 68 | npm start 69 | 70 | The demo app will then be served at http://localhost:8008 71 | -------------------------------------------------------------------------------- /build/widget.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Widget=e():t.Widget=e()}(self,(function(){return(()=>{"use strict";var t={d:(e,n)=>{for(var l in n)t.o(n,l)&&!t.o(e,l)&&Object.defineProperty(e,l,{enumerable:!0,get:n[l]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},e={};function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function l(t,e){for(var n=0;ns});var i=function(){function t(e){var l=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(n(this,t),this.rs=e,this.leaveOpen=!!l.leaveOpen&&l.leaveOpen,this.autoCloseAfter=l.autoCloseAfter?l.autoCloseAfter:1500,this.skipInitial=!!l.skipInitial&&l.skipInitial,this.logging=!!l.logging&&l.logging,l.hasOwnProperty("modalBackdrop")){if("boolean"!=typeof l.modalBackdrop&&"onlySmallScreens"!==l.modalBackdrop)throw'options.modalBackdrop has to be true/false or "onlySmallScreens"';this.modalBackdrop=l.modalBackdrop}else this.modalBackdrop="onlySmallScreens";this.active=!1,this.online=!1,this.closed=!1,this.lastSynced=null,this.lastSyncedUpdateLoop=null}var e,i;return e=t,i=[{key:"log",value:function(){if(this.logging){for(var t,e=arguments.length,n=new Array(e),l=0;l

Connect your storage

To sync data with your account
';var e=document.createElement("style");return e.innerHTML='#remotestorage-widget {\n z-index: 21000000;\n}\n\n.rs-widget {\n box-sizing: border-box;\n overflow: hidden;\n max-width: 350px;\n padding: 10px;\n margin: 10px;\n border-radius: 3px;\n background-color: #fff;\n box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1), 0 3px 8px 0 rgba(0,0,0,0.2);\n font-family: arial, sans-serif;\n font-size: 16px;\n color: #333;\n will-change: max-height, height, width, opacity, max-width, background, box-shadow;\n transition-property: width, height, opacity, max-width, max-height, background, box-shadow;\n transition-duration: 300ms;\n}\n\n.rs-widget * {\n box-sizing: border-box;\n}\n\n.rs-widget .rs-hidden {\n display: none;\n}\n\n.rs-box {\n overflow: hidden;\n will-change: height;\n transition-property: height, width, max-height;\n transition-duration: 300ms;\n transition-timing-function: ease-in;\n opacity: 0;\n max-height: 0px;\n}\n\n.rs-box.rs-selected:not([aria-hidden=true]) {\n opacity: 1;\n max-height: 420px;\n}\n\n/* Main logo */\n.rs-main-logo {\n float: left;\n height: 36px;\n width: 36px;\n margin-top: 1px;\n margin-right: 0.625em;\n transition: margin-left 300ms ease-out, transform 300ms ease-out;\n cursor: pointer;\n}\n.rs-widget .rs-backend-remotestorage svg#rs-main-logo-remotestorage {\n display: block;\n}\n.rs-widget[class*="rs-backend-"]:not(.rs-backend-remotestorage) svg#rs-main-logo-remotestorage {\n display: none;\n}\n.rs-widget.rs-backend-dropbox svg#rs-main-logo-dropbox {\n display: block;\n}\n.rs-widget:not(.rs-backend-dropbox) svg#rs-main-logo-dropbox {\n display: none;\n}\n.rs-widget.rs-backend-googledrive svg#rs-main-logo-googledrive {\n display: block;\n}\n.rs-widget:not(.rs-backend-googledrive) svg#rs-main-logo-googledrive {\n display: none;\n}\n\npolygon.rs-logo-shape {\n fill: #FF4B03;\n}\n\npolygon.rs-logo-shape,\n#rs-main-logo-dropbox path,\n#rs-main-logo-googledrive path {\n transition-property: fill;\n transition-duration: 0.5s;\n}\n\n.rs-offline polygon.rs-logo-shape,\n.rs-offline #rs-main-logo-dropbox path,\n.rs-offline #rs-main-logo-googledrive path {\n fill: #888;\n transition-property: fill;\n transition-duration: 0.5s;\n}\n\n/* Hide everything except logo when connected and clicked outside of box */\n.rs-closed {\n max-width: 56px;\n background-color: transparent;\n box-shadow: none;\n opacity: 0.5;\n\n transition: max-height 100ms ease-out 0ms, max-width 300ms ease-out 300ms, background 300ms ease-in 200ms, opacity 300ms ease 200ms;\n}\n\n.rs-closed:hover {\n cursor: pointer;\n opacity: 1;\n}\n\n.rs-box-initial {\n transition-duration: 0ms;\n}\n\n.rs-box-initial:hover {\n cursor: pointer;\n}\n\n.rs-widget a {\n color: #0093cc;\n}\n\n/* HEADLINE */\n.rs-small-headline {\n font-size: 1em;\n font-weight: bold;\n margin: 0;\n margin-bottom: 2px;\n height: 1.2em;\n word-break: break-all;\n overflow: hidden;\n line-height: 1em;\n}\n\n.rs-sub-headline {\n word-break: break-all;\n overflow: hidden;\n color: #666;\n font-size: 0.92em;\n height: 1.2em;\n}\n.rs-big-headline {\n font-size: 1.625em;\n font-weight: normal;\n text-align: center;\n margin-top: 20px;\n margin-bottom: 20px;\n}\n\n/* BUTTONS */\n.rs-button {\n font: inherit;\n color: inherit;\n background-color: transparent;\n border: 1px solid #dcdcdc;\n border-radius: 3px;\n cursor: pointer;\n}\n.rs-button-small {\n padding: 0.6em 0.7em;\n margin-left: 0.2em;\n transition: border-color 300ms ease-out;\n}\n.rs-button-small svg {\n vertical-align: top;\n}\n.rs-button-wrap {\n margin-top: 10px;\n}\n\n.rs-button-wrap img,\n.rs-button-wrap svg {\n float: left;\n margin-right: 0.6em;\n width: 40px;\n height: 40px;\n}\n\n.rs-button-big {\n padding: 15px 10px;\n margin-bottom: 10px;\n display: block;\n width: 100%;\n text-align: left;\n transition: box-shadow 200ms;\n}\n.rs-button-big > div {\n font-size: 1.125em;\n padding: 10px 0;\n}\n.rs-button-big:hover {\n box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1), 0 3px 8px 0 rgba(0,0,0,0.2);\n}\n.rs-button-big:active {\n background-color: #eee;\n box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1), 0 3px 8px 0 rgba(0,0,0,0.2);\n}\n.rs-button-big:last-child {\n margin-bottom: 0;\n}\n\n.rs-content {\n padding: 0 10px 10px 10px;\n}\n\n\n.rs-state-choose .rs-main-logo,\n.rs-state-sign-in .rs-main-logo {\n margin-left: 45%;\n float: none;\n}\n\n.rs-sign-in-form input[type=text] {\n padding: 15px 10px;\n display: block;\n width: 100%;\n font: inherit;\n height: 52px;\n border: 1px solid #aaa;\n border-radius: 0;\n box-shadow: none;\n}\n.rs-sign-in-form button.rs-connect {\n padding: 15px 10px;\n margin-top: 20px;\n margin-bottom: 15px;\n display: block;\n width: 100%;\n border: none;\n border-radius: 3px;\n background-color: #3fb34f;\n font: inherit;\n color: #fff;\n transition: box-shadow 200ms, background-color 200ms;\n}\n\n.rs-sign-in-form button.rs-connect:hover {\n cursor: pointer;\n background-color: #4BCB5D;\n box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1), 0 3px 8px 0 rgba(0,0,0,0.2);\n}\n\n.rs-sign-in-form button.rs-connect:active {\n background-color: #3fb34f;\n}\n\n.rs-sign-in-form button.rs-connect:disabled,\n.rs-sign-in-form button.rs-connect:disabled:hover {\n background-color: #aaa;\n}\n\n.rs-sign-in-form button.rs-connecting svg {\n height: 1em;\n width: auto;\n vertical-align: middle;\n margin-left: 0.5em;\n animation: rs-spin 1s linear infinite;\n}\n\n.rs-sign-in-error.rs-hidden,\n.rs-box-error.rs-hidden {\n height: 0;\n}\n\n.rs-sign-in-error.rs-visible,\n.rs-box-error.rs-visible {\n height: auto;\n border-radius: 3px;\n padding: 0.5em 0.5em;\n margin-top: 0.5em;\n text-align: center;\n background-color: rgba(255,0,0,0.1);\n color: darkred;\n}\n\n.rs-box-error {\n display: flex;\n flex-direction: row;\n}\n\n.rs-box-error .rs-error-message {\n flex: auto;\n}\n\n /*Choose provider box */\n.rs-box-choose {\n text-align: center;\n overflow: hidden;\n}\n\n.rs-box-choose p {\n margin-top: 0;\n margin-bottom: 20px;\n line-height: 1.4em;\n}\n\n/*Connected box */\n.rs-box-connected {\n display: flex;\n flex-direction: row;\n height: 40px;\n transition: height 0s;\n}\n.rs-connected-text {\n flex: auto;\n min-width: 0;\n}\n.rs-box-connected .rs-user {\n font-weight: bold;\n text-overflow: ellipsis;\n overflow: hidden;\n word-break: keep-all;\n}\n.rs-connected-buttons, .rs-error-buttons {\n flex: none;\n}\n.rs-disconnect:hover {\n border-color: #FF2D2D;\n}\n.rs-disconnect:hover .rs-icon{\n fill: #FF2D2D;\n}\n.rs-sync:hover {\n border-color: #FFBB0C;\n}\n.rs-sync:hover .rs-icon {\n fill: #FFBB0C;\n}\n.rs-sync.rs-rotate {\n border-color: #FFBB0C;\n}\n.rs-sync.rs-rotate .rs-icon {\n fill: #FFBB0C;\n animation: rs-spin 1s linear infinite;\n}\n\n/* Floating widget styles (top right corner) */\n.rs-floating {\n position: fixed;\n top: 0;\n right: 0;\n}\n\n@keyframes rs-spin {\n 100% {\n transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n/* Small/mobile screens */\n@media screen and (max-width: 420px) {\n .rs-widget {\n font-size: 100%;\n transition: all 300ms ease-out;\n max-width: 400px;\n }\n .rs-floating {\n position: relative;\n top: auto;\n right: auto\n }\n .rs-closed {\n max-width: 56px;\n }\n .rs-state-choose,\n .rs-state-sign-in {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n max-width: 100%;\n }\n}\n\n/* remove dotted outline border on Firefox */\n.rs-widget a:focus,\n.rs-widget a:active,\n.rs-widget button:focus,\n.rs-widget input:focus {\n outline:none;\n}\n.rs-widget button::-moz-focus-inner,\n.rs-widget input[type="button"]::-moz-focus-inner {\n border:0;\n}\n\n/* prevent rounded buttons on mobile Safari */\n.rs-widget button,\n.rs-widget input[type="button"] {\n -webkit-appearance: none;\n}\n\n.remotestorage-widget-modal-backdrop {\n display: none;\n position: fixed;\n top: 0;\n bottom: 0;\n left: 0;\n right: 0;\n background-color: rgba(0, 0, 0, 0.5);\n opacity: 0;\n transition: opacity 0.3s linear;\n}\n\n.remotestorage-widget-modal-backdrop.visible {\n opacity: 1;\n transition: opacity 0.3s linear;\n}\n',t.appendChild(e),t}},{key:"setModalClass",value:function(){if(this.modalBackdrop){if("onlySmallScreens"===this.modalBackdrop&&!this.isSmallScreen())return;this.rsWidget.classList.add("rs-modal")}}},{key:"setupElements",value:function(){this.rsWidget=document.querySelector(".rs-widget"),this.rsBackdrop=document.querySelector(".remotestorage-widget-modal-backdrop"),this.rsInitial=document.querySelector(".rs-box-initial"),this.rsChoose=document.querySelector(".rs-box-choose"),this.rsConnected=document.querySelector(".rs-box-connected"),this.rsSignIn=document.querySelector(".rs-box-sign-in"),this.rsConnectedLabel=document.querySelector(".rs-box-connected .rs-sub-headline"),this.rsChooseRemoteStorageButton=document.querySelector("button.rs-choose-rs"),this.rsChooseDropboxButton=document.querySelector("button.rs-choose-dropbox"),this.rsChooseGoogleDriveButton=document.querySelector("button.rs-choose-googledrive"),this.rsErrorBox=document.querySelector(".rs-box-error .rs-error-message"),this.rs.apiKeys.hasOwnProperty("googledrive")||this.rsChooseGoogleDriveButton.parentNode.removeChild(this.rsChooseGoogleDriveButton),this.rs.apiKeys.hasOwnProperty("dropbox")||this.rsChooseDropboxButton.parentNode.removeChild(this.rsChooseDropboxButton),this.rsSignInForm=document.querySelector(".rs-sign-in-form"),this.rsAddressInput=this.rsSignInForm.querySelector("input[name=rs-user-address]"),this.rsConnectButton=document.querySelector(".rs-connect"),this.rsDisconnectButton=document.querySelector(".rs-disconnect"),this.rsSyncButton=document.querySelector(".rs-sync"),this.rsLogo=document.querySelector(".rs-widget-icon"),this.rsErrorReconnectLink=document.querySelector(".rs-box-error a.rs-reconnect"),this.rsErrorDisconnectButton=document.querySelector(".rs-box-error button.rs-disconnect"),this.rsConnectedUser=document.querySelector(".rs-connected-text h1.rs-user")}},{key:"setupHandlers",value:function(){var t=this;this.rs.on("connected",(function(){return t.eventHandler("connected")})),this.rs.on("ready",(function(){return t.eventHandler("ready")})),this.rs.on("disconnected",(function(){return t.eventHandler("disconnected")})),this.rs.on("network-online",(function(){return t.eventHandler("network-online")})),this.rs.on("network-offline",(function(){return t.eventHandler("network-offline")})),this.rs.on("error",(function(e){return t.eventHandler("error",e)})),this.setEventListeners(),this.setClickHandlers()}},{key:"attach",value:function(t){var e,n=this.createHtmlTemplate();if(t){if(e=document.getElementById(t),!parent)throw'Failed to find target DOM element with id="'+t+'"'}else e=document.body;e.appendChild(n),this.setupElements(),this.setupHandlers(),this.setInitialState(),this.setModalClass()}},{key:"setEventListeners",value:function(){var t=this;this.rsSignInForm.addEventListener("submit",(function(e){e.preventDefault();var n=document.querySelector("input[name=rs-user-address]").value;t.disableConnectButton(),t.rs.connect(n)}))}},{key:"showChooseOrSignIn",value:function(){this.rsWidget.classList.contains("rs-modal")&&(this.rsBackdrop.style.display="block",this.rsBackdrop.classList.add("visible")),this.rs.apiKeys&&Object.keys(this.rs.apiKeys).length>0?this.setState("choose"):this.setState("sign-in")}},{key:"setClickHandlers",value:function(){var t=this;this.rsInitial.addEventListener("click",(function(){return t.showChooseOrSignIn()})),this.rsChooseRemoteStorageButton.addEventListener("click",(function(){t.setState("sign-in"),t.rsAddressInput.focus()})),this.rsChooseDropboxButton.addEventListener("click",(function(){return t.rs.dropbox.connect()})),this.rsChooseGoogleDriveButton.addEventListener("click",(function(){return t.rs.googledrive.connect()})),this.rsDisconnectButton.addEventListener("click",(function(){return t.rs.disconnect()})),this.rsErrorReconnectLink.addEventListener("click",(function(){return t.rs.reconnect()})),this.rsErrorDisconnectButton.addEventListener("click",(function(){return t.rs.disconnect()})),this.rs.hasFeature("Sync")&&this.rsSyncButton.addEventListener("click",(function(){t.rsSyncButton.classList.contains("rs-rotate")?(t.rs.stopSync(),t.rsSyncButton.classList.remove("rs-rotate")):(t.rsConnectedLabel.textContent="Synchronizing",t.rs.startSync(),t.rsSyncButton.classList.add("rs-rotate"))})),document.addEventListener("click",(function(){return t.close()})),this.rsWidget.addEventListener("click",(function(t){return t.stopPropagation()})),this.rsLogo.addEventListener("click",(function(){return t.toggle()}))}},{key:"toggle",value:function(){this.closed?this.open():"initial"===this.state?this.showChooseOrSignIn():this.close()}},{key:"open",value:function(){this.closed=!1,this.rsWidget.classList.remove("rs-closed"),this.shouldCloseWhenSyncDone=!1;var t=document.querySelector(".rs-box.rs-selected");t&&t.setAttribute("aria-hidden","false")}},{key:"close",value:function(){var t=this;if("error"!==this.state){if(!this.leaveOpen&&this.active){this.closed=!0,this.rsWidget.classList.add("rs-closed");var e=document.querySelector(".rs-box.rs-selected");e&&e.setAttribute("aria-hidden","true")}else this.active?this.setState("connected"):this.setInitialState();this.rsWidget.classList.contains("rs-modal")&&(this.rsBackdrop.classList.remove("visible"),setTimeout((function(){t.rsBackdrop.style.display="none"}),300))}}},{key:"disableConnectButton",value:function(){this.rsConnectButton.disabled=!0,this.rsConnectButton.classList.add("rs-connecting"),this.rsConnectButton.innerHTML="Connecting ".concat('\n \n \n \n \n \n \n\n')}},{key:"enableConnectButton",value:function(){this.rsConnectButton.disabled=!1,this.rsConnectButton.textContent="Connect",this.rsConnectButton.classList.remove("rs-connecting")}},{key:"setOffline",value:function(){this.online&&(this.rsWidget.classList.add("rs-offline"),this.rsConnectedLabel.textContent="Offline",this.online=!1)}},{key:"setOnline",value:function(){this.online||(this.rsWidget.classList.remove("rs-offline"),this.active&&(this.rsConnectedLabel.textContent="Connected")),this.online=!0}},{key:"setBackendClass",value:function(t){this.rsWidget.classList.remove("rs-backend-remotestorage"),this.rsWidget.classList.remove("rs-backend-dropbox"),this.rsWidget.classList.remove("rs-backend-googledrive"),t&&this.rsWidget.classList.add("rs-backend-".concat(t))}},{key:"showErrorBox",value:function(t){this.rsErrorBox.innerHTML=t,this.setState("error")}},{key:"hideErrorBox",value:function(){this.rsErrorBox.innerHTML="",this.close()}},{key:"handleDiscoveryError",value:function(t){var e=document.querySelector(".rs-sign-in-error");e.innerHTML=t.message,e.classList.remove("rs-hidden"),e.classList.add("rs-visible"),this.enableConnectButton()}},{key:"handleSyncError",value:function(t){console.debug('Encountered SyncError: "'.concat(t.message,'"')),this.setOffline()}},{key:"handleUnauthorized",value:function(t){t.code&&"access_denied"===t.code?this.rs.disconnect():(this.open(),this.showErrorBox(t.message+" "),this.rsErrorBox.appendChild(this.rsErrorReconnectLink),this.rsErrorReconnectLink.classList.remove("rs-hidden"))}},{key:"updateLastSyncedStatus",value:function(){var t=new Date;if(this.online)return this.lastSynced=t,void(this.rsConnectedLabel.textContent="Synced just now");if(this.lastSynced){var e=Math.round((t.getTime()-this.lastSynced.getTime())/1e3);this.rsConnectedLabel.textContent="Synced ".concat(e," seconds ago")}else this.rsWidget.classList.contains("rs-state-unauthorized")||(this.rsConnectedLabel.textContent="Offline")}},{key:"isSmallScreen",value:function(){return window.innerWidth<421}}],i&&l(e.prototype,i),Object.defineProperty(e,"prototype",{writable:!1}),t}();const s=i;return e.default})()})); 2 | //# sourceMappingURL=widget.js.map -------------------------------------------------------------------------------- /demo/assets: -------------------------------------------------------------------------------- 1 | ../src/assets/ -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RS Widget Test 5 | 14 | 15 | 16 | 17 | 18 | 38 | 39 | -------------------------------------------------------------------------------- /demo/remotestorage.js: -------------------------------------------------------------------------------- 1 | ../node_modules/remotestoragejs/release/remotestorage.js -------------------------------------------------------------------------------- /demo/remotestorage.js.map: -------------------------------------------------------------------------------- 1 | ../node_modules/remotestoragejs/release/remotestorage.js.map -------------------------------------------------------------------------------- /demo/widget.js: -------------------------------------------------------------------------------- 1 | ../build/widget.js -------------------------------------------------------------------------------- /demo/widget.js.map: -------------------------------------------------------------------------------- 1 | ../build/widget.js.map -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remotestorage-widget", 3 | "version": "1.6.0", 4 | "description": "remoteStorage.js connect widget", 5 | "main": "build/widget.js", 6 | "scripts": { 7 | "start": "npm run dev", 8 | "clean": "rimraf build/*", 9 | "prebuild": "npm run clean -s", 10 | "build": "NODE_ENV=production webpack", 11 | "dev": "webpack serve", 12 | "open": "opener http://localhost:8008", 13 | "version": "npm run build && git add build/", 14 | "update-bower-version": "./scripts/update-bower-version.sh" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/remotestorage/remotestorage-widget.git" 19 | }, 20 | "keywords": [ 21 | "remotestorage", 22 | "remotestoragejs", 23 | "unhosted", 24 | "no-backend", 25 | "offline-first" 26 | ], 27 | "author": "RS Contributors", 28 | "contributors": [], 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/remotestorage/remotestorage-widget/issues" 32 | }, 33 | "homepage": "https://github.com/remotestorage/remotestorage-widget#readme", 34 | "devDependencies": { 35 | "@babel/cli": "^7.17.6", 36 | "@babel/core": "^7.17.5", 37 | "@babel/preset-env": "^7.16.11", 38 | "babel-loader": "^8.2.3", 39 | "file-loader": "^6.2.0", 40 | "html-loader": "^3.1.0", 41 | "http-server": "^14.1.0", 42 | "inline-loader": "^0.1.1", 43 | "opener": "^1.5.2", 44 | "raw-loader": "^4.0.2", 45 | "remotestoragejs": "2.0.0-beta.7", 46 | "rimraf": "^3.0.2", 47 | "svg-inline-loader": "^0.8.2", 48 | "url-loader": "^4.1.1", 49 | "webpack": "^5.69.1", 50 | "webpack-cli": "^4.9.2", 51 | "webpack-dev-server": "^4.7.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/assets/circle-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/styles.css: -------------------------------------------------------------------------------- 1 | #remotestorage-widget { 2 | z-index: 21000000; 3 | } 4 | 5 | .rs-widget { 6 | box-sizing: border-box; 7 | overflow: hidden; 8 | max-width: 350px; 9 | padding: 10px; 10 | margin: 10px; 11 | border-radius: 3px; 12 | background-color: #fff; 13 | box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1), 0 3px 8px 0 rgba(0,0,0,0.2); 14 | font-family: arial, sans-serif; 15 | font-size: 16px; 16 | color: #333; 17 | will-change: max-height, height, width, opacity, max-width, background, box-shadow; 18 | transition-property: width, height, opacity, max-width, max-height, background, box-shadow; 19 | transition-duration: 300ms; 20 | } 21 | 22 | .rs-widget * { 23 | box-sizing: border-box; 24 | } 25 | 26 | .rs-widget .rs-hidden { 27 | display: none; 28 | } 29 | 30 | .rs-box { 31 | overflow: hidden; 32 | will-change: height; 33 | transition-property: height, width, max-height; 34 | transition-duration: 300ms; 35 | transition-timing-function: ease-in; 36 | opacity: 0; 37 | max-height: 0px; 38 | } 39 | 40 | .rs-box.rs-selected:not([aria-hidden=true]) { 41 | opacity: 1; 42 | max-height: 420px; 43 | } 44 | 45 | /* Main logo */ 46 | .rs-main-logo { 47 | float: left; 48 | height: 36px; 49 | width: 36px; 50 | margin-top: 1px; 51 | margin-right: 0.625em; 52 | transition: margin-left 300ms ease-out, transform 300ms ease-out; 53 | cursor: pointer; 54 | } 55 | .rs-widget .rs-backend-remotestorage svg#rs-main-logo-remotestorage { 56 | display: block; 57 | } 58 | .rs-widget[class*="rs-backend-"]:not(.rs-backend-remotestorage) svg#rs-main-logo-remotestorage { 59 | display: none; 60 | } 61 | .rs-widget.rs-backend-dropbox svg#rs-main-logo-dropbox { 62 | display: block; 63 | } 64 | .rs-widget:not(.rs-backend-dropbox) svg#rs-main-logo-dropbox { 65 | display: none; 66 | } 67 | .rs-widget.rs-backend-googledrive svg#rs-main-logo-googledrive { 68 | display: block; 69 | } 70 | .rs-widget:not(.rs-backend-googledrive) svg#rs-main-logo-googledrive { 71 | display: none; 72 | } 73 | 74 | polygon.rs-logo-shape { 75 | fill: #FF4B03; 76 | } 77 | 78 | polygon.rs-logo-shape, 79 | #rs-main-logo-dropbox path, 80 | #rs-main-logo-googledrive path { 81 | transition-property: fill; 82 | transition-duration: 0.5s; 83 | } 84 | 85 | .rs-offline polygon.rs-logo-shape, 86 | .rs-offline #rs-main-logo-dropbox path, 87 | .rs-offline #rs-main-logo-googledrive path { 88 | fill: #888; 89 | transition-property: fill; 90 | transition-duration: 0.5s; 91 | } 92 | 93 | /* Hide everything except logo when connected and clicked outside of box */ 94 | .rs-closed { 95 | max-width: 56px; 96 | background-color: transparent; 97 | box-shadow: none; 98 | opacity: 0.5; 99 | 100 | transition: max-height 100ms ease-out 0ms, max-width 300ms ease-out 300ms, background 300ms ease-in 200ms, opacity 300ms ease 200ms; 101 | } 102 | 103 | .rs-closed:hover { 104 | cursor: pointer; 105 | opacity: 1; 106 | } 107 | 108 | .rs-box-initial { 109 | transition-duration: 0ms; 110 | } 111 | 112 | .rs-box-initial:hover { 113 | cursor: pointer; 114 | } 115 | 116 | .rs-widget a { 117 | color: #0093cc; 118 | } 119 | 120 | /* HEADLINE */ 121 | .rs-small-headline { 122 | font-size: 1em; 123 | font-weight: bold; 124 | margin: 0; 125 | margin-bottom: 2px; 126 | height: 1.2em; 127 | word-break: break-all; 128 | overflow: hidden; 129 | line-height: 1em; 130 | } 131 | 132 | .rs-sub-headline { 133 | word-break: break-all; 134 | overflow: hidden; 135 | color: #666; 136 | font-size: 0.92em; 137 | height: 1.2em; 138 | } 139 | .rs-big-headline { 140 | font-size: 1.625em; 141 | font-weight: normal; 142 | text-align: center; 143 | margin-top: 20px; 144 | margin-bottom: 20px; 145 | } 146 | 147 | /* BUTTONS */ 148 | .rs-button { 149 | font: inherit; 150 | color: inherit; 151 | background-color: transparent; 152 | border: 1px solid #dcdcdc; 153 | border-radius: 3px; 154 | cursor: pointer; 155 | } 156 | .rs-button-small { 157 | padding: 0.6em 0.7em; 158 | margin-left: 0.2em; 159 | transition: border-color 300ms ease-out; 160 | } 161 | .rs-button-small svg { 162 | vertical-align: top; 163 | } 164 | .rs-button-wrap { 165 | margin-top: 10px; 166 | } 167 | 168 | .rs-button-wrap img, 169 | .rs-button-wrap svg { 170 | float: left; 171 | margin-right: 0.6em; 172 | width: 40px; 173 | height: 40px; 174 | } 175 | 176 | .rs-button-big { 177 | padding: 15px 10px; 178 | margin-bottom: 10px; 179 | display: block; 180 | width: 100%; 181 | text-align: left; 182 | transition: box-shadow 200ms; 183 | } 184 | .rs-button-big > div { 185 | font-size: 1.125em; 186 | padding: 10px 0; 187 | } 188 | .rs-button-big:hover { 189 | box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1), 0 3px 8px 0 rgba(0,0,0,0.2); 190 | } 191 | .rs-button-big:active { 192 | background-color: #eee; 193 | box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1), 0 3px 8px 0 rgba(0,0,0,0.2); 194 | } 195 | .rs-button-big:last-child { 196 | margin-bottom: 0; 197 | } 198 | 199 | .rs-content { 200 | padding: 0 10px 10px 10px; 201 | } 202 | 203 | 204 | .rs-state-choose .rs-main-logo, 205 | .rs-state-sign-in .rs-main-logo { 206 | margin-left: 45%; 207 | float: none; 208 | } 209 | 210 | .rs-sign-in-form input[type=text] { 211 | padding: 15px 10px; 212 | display: block; 213 | width: 100%; 214 | font: inherit; 215 | height: 52px; 216 | border: 1px solid #aaa; 217 | border-radius: 0; 218 | box-shadow: none; 219 | } 220 | .rs-sign-in-form button.rs-connect { 221 | padding: 15px 10px; 222 | margin-top: 20px; 223 | margin-bottom: 15px; 224 | display: block; 225 | width: 100%; 226 | border: none; 227 | border-radius: 3px; 228 | background-color: #3fb34f; 229 | font: inherit; 230 | color: #fff; 231 | transition: box-shadow 200ms, background-color 200ms; 232 | } 233 | 234 | .rs-sign-in-form button.rs-connect:hover { 235 | cursor: pointer; 236 | background-color: #4BCB5D; 237 | box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1), 0 3px 8px 0 rgba(0,0,0,0.2); 238 | } 239 | 240 | .rs-sign-in-form button.rs-connect:active { 241 | background-color: #3fb34f; 242 | } 243 | 244 | .rs-sign-in-form button.rs-connect:disabled, 245 | .rs-sign-in-form button.rs-connect:disabled:hover { 246 | background-color: #aaa; 247 | } 248 | 249 | .rs-sign-in-form button.rs-connecting svg { 250 | height: 1em; 251 | width: auto; 252 | vertical-align: middle; 253 | margin-left: 0.5em; 254 | animation: rs-spin 1s linear infinite; 255 | } 256 | 257 | .rs-sign-in-error.rs-hidden, 258 | .rs-box-error.rs-hidden { 259 | height: 0; 260 | } 261 | 262 | .rs-sign-in-error.rs-visible, 263 | .rs-box-error.rs-visible { 264 | height: auto; 265 | border-radius: 3px; 266 | padding: 0.5em 0.5em; 267 | margin-top: 0.5em; 268 | text-align: center; 269 | background-color: rgba(255,0,0,0.1); 270 | color: darkred; 271 | } 272 | 273 | .rs-box-error { 274 | display: flex; 275 | flex-direction: row; 276 | } 277 | 278 | .rs-box-error .rs-error-message { 279 | flex: auto; 280 | } 281 | 282 | /*Choose provider box */ 283 | .rs-box-choose { 284 | text-align: center; 285 | overflow: hidden; 286 | } 287 | 288 | .rs-box-choose p { 289 | margin-top: 0; 290 | margin-bottom: 20px; 291 | line-height: 1.4em; 292 | } 293 | 294 | /*Connected box */ 295 | .rs-box-connected { 296 | display: flex; 297 | flex-direction: row; 298 | height: 40px; 299 | transition: height 0s; 300 | } 301 | .rs-connected-text { 302 | flex: auto; 303 | min-width: 0; 304 | } 305 | .rs-box-connected .rs-user { 306 | font-weight: bold; 307 | text-overflow: ellipsis; 308 | overflow: hidden; 309 | word-break: keep-all; 310 | } 311 | .rs-connected-buttons, .rs-error-buttons { 312 | flex: none; 313 | } 314 | .rs-disconnect:hover { 315 | border-color: #FF2D2D; 316 | } 317 | .rs-disconnect:hover .rs-icon{ 318 | fill: #FF2D2D; 319 | } 320 | .rs-sync:hover { 321 | border-color: #FFBB0C; 322 | } 323 | .rs-sync:hover .rs-icon { 324 | fill: #FFBB0C; 325 | } 326 | .rs-sync.rs-rotate { 327 | border-color: #FFBB0C; 328 | } 329 | .rs-sync.rs-rotate .rs-icon { 330 | fill: #FFBB0C; 331 | animation: rs-spin 1s linear infinite; 332 | } 333 | 334 | /* Floating widget styles (top right corner) */ 335 | .rs-floating { 336 | position: fixed; 337 | top: 0; 338 | right: 0; 339 | } 340 | 341 | @keyframes rs-spin { 342 | 100% { 343 | transform: rotate(360deg); 344 | transform: rotate(360deg); 345 | } 346 | } 347 | 348 | /* Small/mobile screens */ 349 | @media screen and (max-width: 420px) { 350 | .rs-widget { 351 | font-size: 100%; 352 | transition: all 300ms ease-out; 353 | max-width: 400px; 354 | } 355 | .rs-floating { 356 | position: relative; 357 | top: auto; 358 | right: auto 359 | } 360 | .rs-closed { 361 | max-width: 56px; 362 | } 363 | .rs-state-choose, 364 | .rs-state-sign-in { 365 | position: fixed; 366 | top: 0; 367 | left: 0; 368 | right: 0; 369 | max-width: 100%; 370 | } 371 | } 372 | 373 | /* remove dotted outline border on Firefox */ 374 | .rs-widget a:focus, 375 | .rs-widget a:active, 376 | .rs-widget button:focus, 377 | .rs-widget input:focus { 378 | outline:none; 379 | } 380 | .rs-widget button::-moz-focus-inner, 381 | .rs-widget input[type="button"]::-moz-focus-inner { 382 | border:0; 383 | } 384 | 385 | /* prevent rounded buttons on mobile Safari */ 386 | .rs-widget button, 387 | .rs-widget input[type="button"] { 388 | -webkit-appearance: none; 389 | } 390 | 391 | .remotestorage-widget-modal-backdrop { 392 | display: none; 393 | position: fixed; 394 | top: 0; 395 | bottom: 0; 396 | left: 0; 397 | right: 0; 398 | background-color: rgba(0, 0, 0, 0.5); 399 | opacity: 0; 400 | transition: opacity 0.3s linear; 401 | } 402 | 403 | .remotestorage-widget-modal-backdrop.visible { 404 | opacity: 1; 405 | transition: opacity 0.3s linear; 406 | } 407 | -------------------------------------------------------------------------------- /src/assets/widget.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 15 | 16 | 17 |
18 | 19 |
20 | 28 | 38 | 213 |
214 | 215 |
216 |

Connect your storage

217 | To sync data with your account 218 |
219 | 220 | 244 | 245 | 259 | 260 | 472 | 473 | 484 |
485 | -------------------------------------------------------------------------------- /src/remotestorage.js: -------------------------------------------------------------------------------- 1 | ../node_modules/remotestoragejs/release/remotestorage.js -------------------------------------------------------------------------------- /src/widget.js: -------------------------------------------------------------------------------- 1 | import widgetHtml from './assets/widget.html'; 2 | import widgetCss from './assets/styles.css'; 3 | import circleOpenSvg from './assets/circle-open.svg'; 4 | 5 | /** 6 | * RemoteStorage connect widget 7 | * @constructor 8 | * 9 | * @param {object} remoteStorage - remoteStorage instance 10 | * @param {object} options - Widget options 11 | * @param {boolean} options.leaveOpen - Do not minimize widget when user clicks outside of it (default: false) 12 | * @param {number} options.autoCloseAfter - Time after which the widget closes automatically in ms (default: 1500) 13 | * @param {boolean} options.skipInitial - Don't show the initial connect hint, but show sign-in screen directly instead (default: false) 14 | * @param {boolean} options.logging - Enable logging (default: false) 15 | * @param {boolean,string} options.modalBackdrop - Show a dark, transparent backdrop when opening the widget for connecting an account. (default 'onlySmallScreens') 16 | */ 17 | class Widget { 18 | constructor (remoteStorage, options={}) { 19 | this.rs = remoteStorage; 20 | 21 | this.leaveOpen = options.leaveOpen ? options.leaveOpen : false; 22 | this.autoCloseAfter = options.autoCloseAfter ? options.autoCloseAfter : 1500; 23 | this.skipInitial = options.skipInitial ? options.skipInitial : false; 24 | this.logging = options.logging ? options.logging : false; 25 | 26 | if (options.hasOwnProperty('modalBackdrop')) { 27 | if (typeof options.modalBackdrop !== 'boolean' && options.modalBackdrop !== 'onlySmallScreens') { 28 | throw 'options.modalBackdrop has to be true/false or "onlySmallScreens"' 29 | } 30 | this.modalBackdrop = options.modalBackdrop; 31 | } else { 32 | this.modalBackdrop = 'onlySmallScreens'; 33 | } 34 | 35 | // true if we have remoteStorage connection's info 36 | this.active = false; 37 | 38 | // remoteStorage is connected! 39 | this.online = false; 40 | 41 | // widget is minimized ? 42 | this.closed = false; 43 | 44 | this.lastSynced = null; 45 | this.lastSyncedUpdateLoop = null; 46 | } 47 | 48 | log (...msg) { 49 | if (this.logging) { 50 | console.debug('[RS-WIDGET] ', ...msg); 51 | } 52 | } 53 | 54 | // handle events ! 55 | eventHandler (event, msg) { 56 | this.log('EVENT: ', event); 57 | switch (event) { 58 | case 'ready': 59 | this.setState(this.state); 60 | break; 61 | case 'sync-req-done': 62 | this.syncInProgress = true; 63 | this.rsSyncButton.classList.add("rs-rotate"); 64 | setTimeout(() => { 65 | if (!this.syncInProgress) return; 66 | this.rsConnectedLabel.textContent = 'Synchronizing'; 67 | }, 1000); 68 | break; 69 | case 'sync-done': 70 | if (this.online && !msg.completed) return; 71 | this.syncInProgress = false; 72 | this.rsSyncButton.classList.remove("rs-rotate"); 73 | this.updateLastSyncedStatus(); 74 | if (!this.closed && this.shouldCloseWhenSyncDone) { 75 | setTimeout(this.close.bind(this), this.autoCloseAfter); 76 | } 77 | break; 78 | case 'disconnected': 79 | this.active = false; 80 | this.setOnline(); 81 | this.setBackendClass(); // removes all backend CSS classes 82 | this.open(); 83 | this.setInitialState(); 84 | break; 85 | case 'connected': 86 | this.active = true; 87 | this.online = true; 88 | if (this.rs.hasFeature('Sync')) { 89 | this.shouldCloseWhenSyncDone = true; 90 | this.rs.on('sync-req-done', msg => this.eventHandler('sync-req-done', msg)); 91 | this.rs.on('sync-done', msg => this.eventHandler('sync-done', msg)); 92 | } else { 93 | this.rsSyncButton.classList.add('rs-hidden'); 94 | setTimeout(this.close.bind(this), this.autoCloseAfter); 95 | } 96 | let connectedUser = this.rs.remote.userAddress; 97 | this.rsConnectedUser.innerHTML = connectedUser; 98 | this.setBackendClass(this.rs.backend); 99 | this.rsConnectedLabel.textContent = 'Connected'; 100 | this.setState('connected'); 101 | break; 102 | case 'network-offline': 103 | this.setOffline(); 104 | break; 105 | case 'network-online': 106 | this.setOnline(); 107 | break; 108 | case 'error': 109 | this.setBackendClass(this.rs.backend); 110 | 111 | if (msg.name === 'DiscoveryError') { 112 | this.handleDiscoveryError(msg); 113 | } else if (msg.name === 'SyncError') { 114 | this.handleSyncError(msg); 115 | } else if (msg.name === 'Unauthorized') { 116 | this.handleUnauthorized(msg); 117 | } else { 118 | console.debug(`Encountered unhandled error: "${msg}"`); 119 | } 120 | break; 121 | } 122 | } 123 | 124 | setState (state) { 125 | if (!state) return; 126 | this.log('Setting state ', state); 127 | 128 | let lastSelected = document.querySelector('.rs-box.rs-selected'); 129 | if (lastSelected) { 130 | lastSelected.classList.remove('rs-selected'); 131 | lastSelected.setAttribute('aria-hidden', 'true'); 132 | } 133 | 134 | let toSelect = document.querySelector('.rs-box.rs-box-'+state); 135 | if (toSelect) { 136 | toSelect.classList.add('rs-selected'); 137 | toSelect.setAttribute('aria-hidden', 'false'); 138 | } 139 | 140 | let currentStateClass = this.rsWidget.className.match(/rs-state-\S+/g)[0]; 141 | this.rsWidget.classList.remove(currentStateClass); 142 | this.rsWidget.classList.add(`rs-state-${state || this.state}`); 143 | 144 | this.state = state; 145 | } 146 | 147 | /** 148 | * Set widget to its inital state 149 | * 150 | * @private 151 | */ 152 | setInitialState () { 153 | if (this.skipInitial) { 154 | this.showChooseOrSignIn(); 155 | } else { 156 | this.setState('initial'); 157 | } 158 | } 159 | 160 | /** 161 | * Create the widget element and add styling. 162 | * 163 | * @returns {object} The widget's DOM element 164 | * 165 | * @private 166 | */ 167 | createHtmlTemplate () { 168 | const element = document.createElement('div'); 169 | element.id = "remotestorage-widget"; 170 | element.innerHTML = widgetHtml; 171 | 172 | const style = document.createElement('style'); 173 | style.innerHTML = widgetCss; 174 | element.appendChild(style); 175 | 176 | return element; 177 | } 178 | 179 | /** 180 | * Sets the `rs-modal` class on the widget element. 181 | * Done by default for small screens (max-width 420px). 182 | * 183 | * @private 184 | */ 185 | setModalClass () { 186 | if (this.modalBackdrop) { 187 | if (this.modalBackdrop === 'onlySmallScreens' 188 | && !this.isSmallScreen()) { 189 | return; 190 | } 191 | this.rsWidget.classList.add('rs-modal'); 192 | } 193 | } 194 | 195 | /** 196 | * Save all interactive DOM elements as variables for later access. 197 | * 198 | * @private 199 | */ 200 | setupElements () { 201 | this.rsWidget = document.querySelector('.rs-widget'); 202 | this.rsBackdrop = document.querySelector('.remotestorage-widget-modal-backdrop'); 203 | this.rsInitial = document.querySelector('.rs-box-initial'); 204 | this.rsChoose = document.querySelector('.rs-box-choose'); 205 | this.rsConnected = document.querySelector('.rs-box-connected'); 206 | this.rsSignIn = document.querySelector('.rs-box-sign-in'); 207 | 208 | this.rsConnectedLabel = document.querySelector('.rs-box-connected .rs-sub-headline'); 209 | this.rsChooseRemoteStorageButton = document.querySelector('button.rs-choose-rs'); 210 | this.rsChooseDropboxButton = document.querySelector('button.rs-choose-dropbox'); 211 | this.rsChooseGoogleDriveButton = document.querySelector('button.rs-choose-googledrive'); 212 | this.rsErrorBox = document.querySelector('.rs-box-error .rs-error-message'); 213 | 214 | // check if apiKeys is set for Dropbox or Google [googledrive, dropbox] 215 | // to show/hide relative buttons only if needed 216 | if (! this.rs.apiKeys.hasOwnProperty('googledrive')) { 217 | this.rsChooseGoogleDriveButton.parentNode.removeChild(this.rsChooseGoogleDriveButton); 218 | } 219 | 220 | if (! this.rs.apiKeys.hasOwnProperty('dropbox')) { 221 | this.rsChooseDropboxButton.parentNode.removeChild(this.rsChooseDropboxButton); 222 | } 223 | 224 | this.rsSignInForm = document.querySelector('.rs-sign-in-form'); 225 | this.rsAddressInput = this.rsSignInForm.querySelector('input[name=rs-user-address]'); 226 | this.rsConnectButton = document.querySelector('.rs-connect'); 227 | 228 | this.rsDisconnectButton = document.querySelector('.rs-disconnect'); 229 | this.rsSyncButton = document.querySelector('.rs-sync'); 230 | this.rsLogo = document.querySelector('.rs-widget-icon'); 231 | 232 | this.rsErrorReconnectLink = document.querySelector('.rs-box-error a.rs-reconnect'); 233 | this.rsErrorDisconnectButton = document.querySelector('.rs-box-error button.rs-disconnect'); 234 | 235 | this.rsConnectedUser = document.querySelector('.rs-connected-text h1.rs-user'); 236 | } 237 | 238 | /** 239 | * Setup all event handlers 240 | * 241 | * @private 242 | */ 243 | setupHandlers () { 244 | this.rs.on('connected', () => this.eventHandler('connected')); 245 | this.rs.on('ready', () => this.eventHandler('ready')); 246 | this.rs.on('disconnected', () => this.eventHandler('disconnected')); 247 | this.rs.on('network-online', () => this.eventHandler('network-online')); 248 | this.rs.on('network-offline', () => this.eventHandler('network-offline')); 249 | this.rs.on('error', (error) => this.eventHandler('error', error)); 250 | 251 | this.setEventListeners(); 252 | this.setClickHandlers(); 253 | } 254 | 255 | /** 256 | * Append widget to the DOM. 257 | * 258 | * If an elementId is specified, it will be appended to that element, 259 | * otherwise it will be appended to the document's body. 260 | * 261 | * @param {String} [elementId] - Widget's parent 262 | */ 263 | attach (elementId) { 264 | const domElement = this.createHtmlTemplate(); 265 | 266 | let parentContainerEl; 267 | 268 | if (elementId) { 269 | parentContainerEl = document.getElementById(elementId); 270 | if (!parent) { 271 | throw "Failed to find target DOM element with id=\"" + elementId + "\""; 272 | } 273 | } else { 274 | parentContainerEl = document.body; 275 | } 276 | parentContainerEl.appendChild(domElement); 277 | 278 | this.setupElements(); 279 | this.setupHandlers(); 280 | this.setInitialState(); 281 | this.setModalClass(); 282 | } 283 | 284 | setEventListeners () { 285 | this.rsSignInForm.addEventListener('submit', (e) => { 286 | e.preventDefault(); 287 | let userAddress = document.querySelector('input[name=rs-user-address]').value; 288 | this.disableConnectButton(); 289 | this.rs.connect(userAddress); 290 | }); 291 | } 292 | 293 | /** 294 | * Show the screen for choosing a backend if there is more than one backend 295 | * to choose from. Otherwise it directly shows the remoteStorage connect 296 | * screen. 297 | * 298 | * @private 299 | */ 300 | showChooseOrSignIn () { 301 | if (this.rsWidget.classList.contains('rs-modal')) { 302 | this.rsBackdrop.style.display = 'block'; 303 | this.rsBackdrop.classList.add('visible'); 304 | } 305 | // choose backend only if some providers are declared 306 | if (this.rs.apiKeys && Object.keys(this.rs.apiKeys).length > 0) { 307 | this.setState('choose'); 308 | } else { 309 | this.setState('sign-in'); 310 | } 311 | } 312 | 313 | setClickHandlers () { 314 | // Initial button 315 | this.rsInitial.addEventListener('click', () => this.showChooseOrSignIn() ); 316 | 317 | // Choose RS button 318 | this.rsChooseRemoteStorageButton.addEventListener('click', () => { 319 | this.setState('sign-in'); 320 | this.rsAddressInput.focus(); 321 | }); 322 | 323 | // Choose Dropbox button 324 | this.rsChooseDropboxButton.addEventListener('click', () => this.rs["dropbox"].connect() ); 325 | 326 | // Choose Google Drive button 327 | this.rsChooseGoogleDriveButton.addEventListener('click', () => this.rs["googledrive"].connect() ); 328 | 329 | // Disconnect button 330 | this.rsDisconnectButton.addEventListener('click', () => this.rs.disconnect() ); 331 | 332 | this.rsErrorReconnectLink.addEventListener('click', () => this.rs.reconnect() ); 333 | this.rsErrorDisconnectButton.addEventListener('click', () => this.rs.disconnect() ); 334 | 335 | // Sync button 336 | if (this.rs.hasFeature('Sync')) { 337 | this.rsSyncButton.addEventListener('click', () => { 338 | if (this.rsSyncButton.classList.contains('rs-rotate')) { 339 | this.rs.stopSync(); 340 | this.rsSyncButton.classList.remove("rs-rotate"); 341 | } else { 342 | this.rsConnectedLabel.textContent = 'Synchronizing'; 343 | this.rs.startSync(); 344 | this.rsSyncButton.classList.add("rs-rotate"); 345 | } 346 | }); 347 | } 348 | 349 | // Reduce to icon only if connected and clicked outside of widget 350 | document.addEventListener('click', () => this.close() ); 351 | 352 | // Clicks on the widget stop the above event 353 | this.rsWidget.addEventListener('click', e => e.stopPropagation() ); 354 | 355 | // Click on the logo to toggle the widget's open/close state 356 | this.rsLogo.addEventListener('click', () => this.toggle() ); 357 | } 358 | 359 | /** 360 | * Toggle between the widget's open/close state. 361 | * 362 | * When then widget is open and in initial state, it will show the backend 363 | * chooser screen. 364 | */ 365 | toggle () { 366 | if (this.closed) { 367 | this.open(); 368 | } else { 369 | if (this.state === 'initial') { 370 | this.showChooseOrSignIn(); 371 | } else { 372 | this.close(); 373 | } 374 | } 375 | } 376 | 377 | /** 378 | * Open the widget. 379 | */ 380 | open () { 381 | this.closed = false; 382 | this.rsWidget.classList.remove('rs-closed'); 383 | this.shouldCloseWhenSyncDone = false; // prevent auto-closing when user opened the widget 384 | 385 | let selected = document.querySelector('.rs-box.rs-selected'); 386 | if (selected) { 387 | selected.setAttribute('aria-hidden', 'false'); 388 | } 389 | } 390 | 391 | /** 392 | * Close the widget to only show the icon. 393 | * 394 | * If the ``leaveOpen`` config is true or there is no storage connected, 395 | * the widget will not close. 396 | */ 397 | close () { 398 | // don't do anything when we have an error 399 | if (this.state === 'error') { return; } 400 | 401 | if (!this.leaveOpen && this.active) { 402 | this.closed = true; 403 | this.rsWidget.classList.add('rs-closed'); 404 | let selected = document.querySelector('.rs-box.rs-selected'); 405 | if (selected) { 406 | selected.setAttribute('aria-hidden', 'true'); 407 | } 408 | } else if (this.active) { 409 | this.setState('connected'); 410 | } else { 411 | this.setInitialState(); 412 | } 413 | 414 | if (this.rsWidget.classList.contains('rs-modal')) { 415 | this.rsBackdrop.classList.remove('visible'); 416 | setTimeout(() => { 417 | this.rsBackdrop.style.display = 'none'; 418 | }, 300); 419 | } 420 | } 421 | 422 | /** 423 | * Disable the connect button and indicate connect activity 424 | * 425 | * @private 426 | */ 427 | disableConnectButton () { 428 | this.rsConnectButton.disabled = true; 429 | this.rsConnectButton.classList.add('rs-connecting'); 430 | const circleSpinner = circleOpenSvg; 431 | this.rsConnectButton.innerHTML = `Connecting ${circleSpinner}`; 432 | } 433 | 434 | /** 435 | * (Re)enable the connect button and reset to original state 436 | * 437 | * @private 438 | */ 439 | enableConnectButton () { 440 | this.rsConnectButton.disabled = false; 441 | this.rsConnectButton.textContent = 'Connect'; 442 | this.rsConnectButton.classList.remove('rs-connecting'); 443 | } 444 | 445 | /** 446 | * Mark the widget as offline. 447 | * 448 | * This will not do anything when no account is connected. 449 | * 450 | * @private 451 | */ 452 | setOffline () { 453 | if (this.online) { 454 | this.rsWidget.classList.add('rs-offline'); 455 | this.rsConnectedLabel.textContent = 'Offline'; 456 | this.online = false; 457 | } 458 | } 459 | 460 | /** 461 | * Mark the widget as online. 462 | * 463 | * @private 464 | */ 465 | setOnline () { 466 | if (!this.online) { 467 | this.rsWidget.classList.remove('rs-offline'); 468 | if (this.active) { 469 | this.rsConnectedLabel.textContent = 'Connected'; 470 | } 471 | } 472 | this.online = true; 473 | } 474 | 475 | /** 476 | * Set the remoteStorage backend type to show the appropriate icon. 477 | * If no backend is given, all existing backend CSS classes will be removed. 478 | * 479 | * @param {string} [backend] 480 | * 481 | * @private 482 | */ 483 | setBackendClass (backend) { 484 | this.rsWidget.classList.remove('rs-backend-remotestorage'); 485 | this.rsWidget.classList.remove('rs-backend-dropbox'); 486 | this.rsWidget.classList.remove('rs-backend-googledrive'); 487 | 488 | if (backend) { 489 | this.rsWidget.classList.add(`rs-backend-${backend}`); 490 | } 491 | } 492 | 493 | showErrorBox (errorMsg) { 494 | this.rsErrorBox.innerHTML = errorMsg; 495 | this.setState('error'); 496 | } 497 | 498 | hideErrorBox () { 499 | this.rsErrorBox.innerHTML = ''; 500 | this.close(); 501 | } 502 | 503 | handleDiscoveryError (error) { 504 | let msgContainer = document.querySelector('.rs-sign-in-error'); 505 | msgContainer.innerHTML = error.message; 506 | msgContainer.classList.remove('rs-hidden'); 507 | msgContainer.classList.add('rs-visible'); 508 | this.enableConnectButton(); 509 | } 510 | 511 | handleSyncError (error) { 512 | this.setOffline(); 513 | } 514 | 515 | handleUnauthorized (error) { 516 | if (error.code && error.code === 'access_denied') { 517 | this.rs.disconnect(); 518 | } else { 519 | this.open(); 520 | this.showErrorBox(error.message + " "); 521 | this.rsErrorBox.appendChild(this.rsErrorReconnectLink); 522 | this.rsErrorReconnectLink.classList.remove('rs-hidden'); 523 | } 524 | } 525 | 526 | updateLastSyncedStatus () { 527 | const now = new Date(); 528 | if (this.online) { 529 | this.lastSynced = now; 530 | this.rsConnectedLabel.textContent = 'Synced just now'; 531 | return; 532 | } 533 | if (!this.lastSynced) { 534 | if (!this.rsWidget.classList.contains('rs-state-unauthorized')) { 535 | this.rsConnectedLabel.textContent = 'Offline'; 536 | } 537 | return; 538 | } 539 | const secondsSinceLastSync = Math.round((now.getTime() - this.lastSynced.getTime())/1000); 540 | this.rsConnectedLabel.textContent = `Synced ${secondsSinceLastSync} seconds ago`; 541 | } 542 | 543 | isSmallScreen () { 544 | return window.innerWidth < 421; 545 | } 546 | } 547 | 548 | export default Widget; 549 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* global __dirname */ 2 | const isProd = (process.env.NODE_ENV === 'production'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: ["./src/widget.js"], 7 | output: { 8 | path: path.resolve(__dirname, 'build'), 9 | filename: 'widget.js', 10 | library: 'Widget', 11 | libraryTarget: 'umd', 12 | libraryExport: 'default' 13 | }, 14 | mode: isProd ? 'production' : 'development', 15 | devtool: isProd ? 'source-map' : 'eval-source-map', 16 | externals: { 17 | // require("remotestoragejs") is external and available 18 | // on the global var RemoteStorage 19 | // this is how peer dependencies are specified 20 | // in webpack (we need RemoteStorage but we do not include in bundle) 21 | "remotestoragejs": { 22 | root: "RemoteStorage", // in browser