Begin migration to Polymer 3.0
This commit is contained in:
parent
a464f8d13f
commit
ce10f86145
|
@ -0,0 +1 @@
|
|||
node_modules
|
|
@ -10,9 +10,10 @@
|
|||
margin: 0;
|
||||
} </style>
|
||||
|
||||
<script src="bower_components/webcomponentsjs/webcomponents-lite.js"></script>
|
||||
<link rel="stylesheet" href="./src/styles/fonts.css">
|
||||
|
||||
<link rel="import" href="src/ui/app/app.html">
|
||||
<script type="module" src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
|
||||
<script type="module" src="./src/ui/app/app.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import './src/ui/app/app.js';
|
||||
import '../@webcomponents/webcomponentsjs/webcomponents-bundle.js';
|
||||
const $_documentContainer = document.createElement('template');
|
||||
$_documentContainer.setAttribute('style', 'display: none;');
|
||||
|
||||
$_documentContainer.innerHTML = `<title>Padlock</title><style> html, body {
|
||||
background: #59c6ff;
|
||||
margin: 0;
|
||||
} </style><pl-app></pl-app>`;
|
||||
|
||||
document.head.appendChild($_documentContainer.content);
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"homepage": "http://padlock.io",
|
||||
"name": "padlock",
|
||||
"version": "3.0.0",
|
||||
"resolutions": {
|
||||
"inherits": "2.0.3",
|
||||
"samsam": "1.1.3",
|
||||
"supports-color": "3.1.2",
|
||||
"type-detect": "1.0.0",
|
||||
"@webcomponents/webcomponentsjs": "2.0.0-beta.2"
|
||||
},
|
||||
"main": "index.js",
|
||||
"author": "Martin Kleinschrodt <martin@maklesoft.com>",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@polymer/polymer": "^3.0.0",
|
||||
"@polymer/iron-list": "^3.0.0-pre.18",
|
||||
"@polymer/paper-spinner": "^3.0.0-pre.18",
|
||||
"@webcomponents/webcomponentsjs": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
<script src="../padlock-lite.js"></script>
|
||||
|
||||
<link rel="import" href="../styles/shared.html">
|
||||
<link rel="import" href="../ui/animation/animation.html">
|
||||
<link rel="import" href="../ui/base/base.html">
|
||||
<link rel="import" href="../ui/dialog/dialog-mixin.html">
|
||||
<link rel="import" href="../ui/icon/icon.html">
|
||||
<link rel="import" href="../ui/input/input.html">
|
||||
<link rel="import" href="../ui/loading-button/loading-button.html">
|
||||
<link rel="import" href="../ui/locale/locale.html">
|
||||
<link rel="import" href="../ui/notification/notification.html">
|
||||
<link rel="import" href="../ui/payment-dialog/payment-dialog.html">
|
||||
<link rel="import" href="../ui/promo/promo.html">
|
||||
<link rel="import" href="../ui/promo/promo-dialog.html">
|
||||
<link rel="import" href="../ui/sync/subinfo.html">
|
||||
<script type="module" src="../styles/shared.js"></script>
|
||||
<script type="module" src="../ui/animation/animation.js"></script>
|
||||
<script type="module" src="../ui/base/base.js"></script>
|
||||
<script type="module" src="../ui/dialog/dialog-mixin.js"></script>
|
||||
<script type="module" src="../ui/icon/icon.js"></script>
|
||||
<script type="module" src="../ui/input/input.js"></script>
|
||||
<script type="module" src="../ui/loading-button/loading-button.js"></script>
|
||||
<script type="module" src="../ui/locale/locale.js"></script>
|
||||
<script type="module" src="../ui/notification/notification.js"></script>
|
||||
<script type="module" src="../ui/payment-dialog/payment-dialog.js"></script>
|
||||
<script type="module" src="../ui/promo/promo.js"></script>
|
||||
<script type="module" src="../ui/promo/promo-dialog.js"></script>
|
||||
<script type="module" src="../ui/sync/subinfo.js"></script>
|
||||
|
||||
<dom-module id="pl-cloud-dashboard">
|
||||
|
||||
|
@ -407,8 +407,20 @@
|
|||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
<script type="module">
|
||||
import '../styles/shared.js';
|
||||
import '../ui/animation/animation.js';
|
||||
import '../ui/base/base.js';
|
||||
import '../ui/dialog/dialog-mixin.js';
|
||||
import '../ui/icon/icon.js';
|
||||
import '../ui/input/input.js';
|
||||
import '../ui/loading-button/loading-button.js';
|
||||
import '../ui/locale/locale.js';
|
||||
import '../ui/notification/notification.js';
|
||||
import '../ui/payment-dialog/payment-dialog.js';
|
||||
import '../ui/promo/promo.js';
|
||||
import '../ui/promo/promo-dialog.js';
|
||||
import '../ui/sync/subinfo.js';
|
||||
|
||||
const { LocaleMixin, DialogMixin, NotificationMixin, AnimationMixin, SubInfoMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
|
@ -683,8 +695,6 @@ class Dashboard extends applyMixins(
|
|||
}
|
||||
|
||||
window.customElements.define(Dashboard.is, Dashboard);
|
||||
|
||||
})();
|
||||
</script>
|
||||
</script>
|
||||
|
||||
</dom-module>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
import '../../../../node_modules/@polymer/polymer/polymer-legacy.js';
|
||||
const $_documentContainer = document.createElement('template');
|
||||
$_documentContainer.setAttribute('style', 'display: none;');
|
||||
|
||||
<custom-style>
|
||||
$_documentContainer.innerHTML = `<custom-style>
|
||||
<style is="custom-style">
|
||||
|
||||
body {
|
||||
|
@ -132,4 +134,6 @@
|
|||
}
|
||||
|
||||
</style>
|
||||
</custom-style>
|
||||
</custom-style>`;
|
||||
|
||||
document.head.appendChild($_documentContainer.content);
|
|
@ -39,3 +39,11 @@
|
|||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'FontAwesome';
|
||||
src: url('../../assets/fonts/fontawesome-webfont.eot?v=4.7.0');
|
||||
src: url('../../assets/fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../../assets/fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../../assets/fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../../assets/fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../../assets/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<link rel="stylesheet" href="fonts.css">
|
||||
<link rel="import" href="config.html">
|
||||
import './config.js';
|
||||
const $_documentContainer = document.createElement('template');
|
||||
$_documentContainer.setAttribute('style', 'display: none;');
|
||||
|
||||
<dom-module id="shared">
|
||||
$_documentContainer.innerHTML = `<dom-module id="shared">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
|
@ -55,7 +56,7 @@
|
|||
|
||||
button.arrow::before, a.button.arrow::before {
|
||||
font-family: "FontAwesome";
|
||||
content: "\f054";
|
||||
content: "\\f054";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -350,4 +351,6 @@
|
|||
|
||||
</style>
|
||||
</template>
|
||||
</dom-module>
|
||||
</dom-module>`;
|
||||
|
||||
document.head.appendChild($_documentContainer.content);
|
|
@ -1,7 +1,4 @@
|
|||
<link rel="import" href="../base/base.html">
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
import '../base/base.js';
|
||||
|
||||
const defaults = {
|
||||
animation: "slideIn",
|
||||
|
@ -56,6 +53,3 @@ padlock.AnimationMixin = (superClass) => {
|
|||
|
||||
};
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
|
@ -1,8 +1,5 @@
|
|||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../data/data.html">
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
import '../base/base.js';
|
||||
import '../data/data.js';
|
||||
|
||||
const startedLoading = new Date().getTime();
|
||||
const { init, track, setTrackingID } = padlock.tracking;
|
||||
|
@ -115,6 +112,3 @@ padlock.AnalyticsMixin = (superClass) => {
|
|||
|
||||
};
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
|
@ -1,29 +1,53 @@
|
|||
<script src="../../padlock.js"></script>
|
||||
import './analytics.js';
|
||||
import '../../styles/shared.js';
|
||||
import '../animation/animation.js';
|
||||
import '../base/base.js';
|
||||
import '../clipboard/clipboard.js';
|
||||
import '../cloud-view/cloud-view.js';
|
||||
import '../data/data.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../icon/icon.js';
|
||||
import '../list-view/list-view.js';
|
||||
import '../locale/locale.js';
|
||||
import '../notification/notification.js';
|
||||
import '../record-view/record-view.js';
|
||||
import '../settings-view/settings-view.js';
|
||||
import '../start-view/start-view.js';
|
||||
import '../sync/sync.js';
|
||||
import '../title-bar/title-bar.js';
|
||||
import './auto-lock.js';
|
||||
import './auto-sync.js';
|
||||
import './hints.js';
|
||||
import './messages.js';
|
||||
|
||||
<link rel="import" href="./analytics.html">
|
||||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../animation/animation.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../clipboard/clipboard.html">
|
||||
<link rel="import" href="../cloud-view/cloud-view.html">
|
||||
<link rel="import" href="../data/data.html">
|
||||
<link rel="import" href="../dialog/dialog-mixin.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
<link rel="import" href="../list-view/list-view.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="../notification/notification.html">
|
||||
<link rel="import" href="../record-view/record-view.html">
|
||||
<link rel="import" href="../settings-view/settings-view.html">
|
||||
<link rel="import" href="../start-view/start-view.html">
|
||||
<link rel="import" href="../sync/sync.html">
|
||||
<link rel="import" href="../title-bar/title-bar.html">
|
||||
<link rel="import" href="./auto-lock.html">
|
||||
<link rel="import" href="./auto-sync.html">
|
||||
<link rel="import" href="./hints.html">
|
||||
<link rel="import" href="./messages.html">
|
||||
/* global cordova, StatusBar */
|
||||
|
||||
<dom-module id="pl-app">
|
||||
<template>
|
||||
const { NotificationMixin, DialogMixin, MessagesMixin, DataMixin, AnimationMixin, ClipboardMixin,
|
||||
SyncMixin, AutoSyncMixin, AutoLockMixin, HintsMixin, AnalyticsMixin, LocaleMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
const { getPlatformName, getDeviceInfo, isTouch } = padlock.platform;
|
||||
|
||||
const cordovaReady = new Promise((resolve) => {
|
||||
document.addEventListener("deviceready", resolve);
|
||||
});
|
||||
|
||||
class App extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
SyncMixin,
|
||||
AutoSyncMixin,
|
||||
AutoLockMixin,
|
||||
DialogMixin,
|
||||
MessagesMixin,
|
||||
NotificationMixin,
|
||||
HintsMixin,
|
||||
AnalyticsMixin,
|
||||
AnimationMixin,
|
||||
ClipboardMixin,
|
||||
LocaleMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
|
||||
@keyframes fadeIn {
|
||||
|
@ -236,7 +260,7 @@
|
|||
.last-sync::before {
|
||||
font-family: "FontAwesome";
|
||||
font-size: 90%;
|
||||
content: "\f017\ ";
|
||||
content: "\\f017\\ ";
|
||||
}
|
||||
|
||||
.menu-wrapper {
|
||||
|
@ -255,7 +279,7 @@
|
|||
.menu-item-hint.warning::before {
|
||||
font-family: "FontAwesome";
|
||||
font-size: 85%;
|
||||
content: "\f071\ ";
|
||||
content: "\\f071\\ ";
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
@ -316,58 +340,58 @@
|
|||
|
||||
<pl-start-view id="startView"></pl-start-view>
|
||||
|
||||
<div id="menuWrapper" class="menu-wrapper" on-click="_menuWrapperClicked" show-tags$="[[ _showingTags ]]">
|
||||
<div id="menuWrapper" class="menu-wrapper" on-click="_menuWrapperClicked" show-tags\$="[[ _showingTags ]]">
|
||||
<div id="menu" class="menu">
|
||||
<div class="spacer"></div>
|
||||
<div class="account menu-item tap" on-click="_openCloudView">
|
||||
<div>
|
||||
<div hidden$="[[ settings.syncConnected ]]">[[ $l("Log In") ]]</div>
|
||||
<div hidden$="[[ !settings.syncConnected ]]">[[ $l("My Account") ]]</div>
|
||||
<div class="menu-item-hint warning" hidden$="[[ !isTrialExpired(settings.syncConnected, settings.syncSubStatus) ]]">[[ $l("Trial Expired") ]]</div>
|
||||
<div class="menu-item-hint warning" hidden$="[[ !isSubUnpaid(settings.syncConnected, settings.syncSubStatus) ]]">[[ $l("Payment Failed") ]]</div>
|
||||
<div class="menu-item-hint warning" hidden$="[[ !isSubCanceled(settings.syncConnected, settings.syncSubStatus) ]]">[[ $l("Subscr. Canceled") ]]</div>
|
||||
<div hidden\$="[[ settings.syncConnected ]]">[[ \$l("Log In") ]]</div>
|
||||
<div hidden\$="[[ !settings.syncConnected ]]">[[ \$l("My Account") ]]</div>
|
||||
<div class="menu-item-hint warning" hidden\$="[[ !isTrialExpired(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Trial Expired") ]]</div>
|
||||
<div class="menu-item-hint warning" hidden\$="[[ !isSubUnpaid(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Payment Failed") ]]</div>
|
||||
<div class="menu-item-hint warning" hidden\$="[[ !isSubCanceled(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Subscr. Canceled") ]]</div>
|
||||
</div>
|
||||
<pl-icon icon="cloud" class="account-icon"></pl-icon>
|
||||
</div>
|
||||
<div class="menu-item tap" on-click="synchronize" disabled$="[[ !isSubValid(settings.syncConnected, settings.syncSubStatus) ]]">
|
||||
<div class="menu-item tap" on-click="synchronize" disabled\$="[[ !isSubValid(settings.syncConnected, settings.syncSubStatus) ]]">
|
||||
<div>
|
||||
<div>[[ $l("Synchronize") ]]</div>
|
||||
<div class="menu-item-hint" hidden$="[[ settings.syncConnected ]]">[[ $l("Log In To Sync") ]]</div>
|
||||
<div class="menu-item-hint last-sync" hidden$="[[ !settings.syncConnected ]]">[[ lastSync ]]</div>
|
||||
<div>[[ \$l("Synchronize") ]]</div>
|
||||
<div class="menu-item-hint" hidden\$="[[ settings.syncConnected ]]">[[ \$l("Log In To Sync") ]]</div>
|
||||
<div class="menu-item-hint last-sync" hidden\$="[[ !settings.syncConnected ]]">[[ lastSync ]]</div>
|
||||
</div>
|
||||
<pl-icon icon="refresh" spin$="[[ isSynching ]]"></pl-icon>
|
||||
<pl-icon icon="refresh" spin\$="[[ isSynching ]]"></pl-icon>
|
||||
</div>
|
||||
<!-- <div class="menu-item tap" on-click="_newRecord"> -->
|
||||
<!-- <div>[[ $l("Add Record") ]]</div> -->
|
||||
<!-- <div>[[ \$l("Add Record") ]]</div> -->
|
||||
<!-- <pl-icon icon="add"></pl-icon> -->
|
||||
<!-- </div> -->
|
||||
<div class="menu-item tap" on-click="_openSettings">
|
||||
<div>[[ $l("Settings") ]]</div>
|
||||
<div>[[ \$l("Settings") ]]</div>
|
||||
<pl-icon icon="settings"></pl-icon>
|
||||
</div>
|
||||
<div class="menu-item tap" on-click="_showTags">
|
||||
<div>[[ $l("Tags") ]]</div>
|
||||
<div>[[ \$l("Tags") ]]</div>
|
||||
<pl-icon icon="tag"></pl-icon>
|
||||
</div>
|
||||
<div class="menu-item tap" on-click="_enableMultiSelect">
|
||||
<div>[[ $l("Multi-Select") ]]</div>
|
||||
<div>[[ \$l("Multi-Select") ]]</div>
|
||||
<pl-icon icon="checked"></pl-icon>
|
||||
</div>
|
||||
<div class="menu-item tap" on-click="_lock">
|
||||
<div>[[ $l("Lock App") ]]</div>
|
||||
<div>[[ \$l("Lock App") ]]</div>
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="menu-info">
|
||||
<div><strong>Padlock {{ settings.version }}</strong></div>
|
||||
<div>Made with ♥ in Germany</div>
|
||||
<div>Made with ♥ in Germany</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tags">
|
||||
<div class="spacer"></div>
|
||||
<div class="menu-item tap" on-click="_closeTags">
|
||||
<div>[[ $l("Tags") ]]</div>
|
||||
<div>[[ \$l("Tags") ]]</div>
|
||||
<pl-icon icon="close"></pl-icon>
|
||||
</div>
|
||||
<template is="dom-repeat" items="[[ collection.tags ]]">
|
||||
|
@ -376,8 +400,8 @@
|
|||
<pl-icon icon="tag"></pl-icon>
|
||||
</div>
|
||||
</template>
|
||||
<div class="no-tags" disabled hidden$="[[ _hasTags(collection.tags) ]]">
|
||||
[[ $l("You don't have any tags yet!") ]]
|
||||
<div class="no-tags" disabled="" hidden\$="[[ _hasTags(collection.tags) ]]">
|
||||
[[ \$l("You don't have any tags yet!") ]]
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
|
@ -385,12 +409,7 @@
|
|||
|
||||
<div id="main">
|
||||
|
||||
<pl-list-view id="listView"
|
||||
selected-record="{{ _selectedRecord }}"
|
||||
on-open-settings="_openSettings"
|
||||
on-open-cloud-view="_openCloudView"
|
||||
on-toggle-menu="_toggleMenu"
|
||||
></pl-list-view>
|
||||
<pl-list-view id="listView" selected-record="{{ _selectedRecord }}" on-open-settings="_openSettings" on-open-cloud-view="_openCloudView" on-toggle-menu="_toggleMenu"></pl-list-view>
|
||||
|
||||
<div id="pages">
|
||||
|
||||
|
@ -398,362 +417,324 @@
|
|||
<pl-icon icon="logo" class="placeholder-icon"></pl-icon>
|
||||
</div>
|
||||
|
||||
<pl-record-view id="recordView" class="view"
|
||||
on-record-close="_closeRecord"></pl-record-view>
|
||||
<pl-record-view id="recordView" class="view" on-record-close="_closeRecord"></pl-record-view>
|
||||
|
||||
<pl-settings-view id="settingsView" class="view"
|
||||
on-settings-back="_settingsBack"></pl-settings-view>
|
||||
<pl-settings-view id="settingsView" class="view" on-settings-back="_settingsBack"></pl-settings-view>
|
||||
|
||||
<pl-cloud-view id="cloudView" class="view"
|
||||
on-cloud-back="_cloudViewBack"></pl-cloud-view>
|
||||
<pl-cloud-view id="cloudView" class="view" on-cloud-back="_cloudViewBack"></pl-cloud-view>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<pl-title-bar></pl-title-bar>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
static get is() { return "pl-app"; }
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
static get properties() { return {
|
||||
locked: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
observer: "_lockedChanged"
|
||||
},
|
||||
_currentView: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer: "_currentViewChanged"
|
||||
},
|
||||
_selectedRecord: {
|
||||
type: Object,
|
||||
observer: "_selectedRecordChanged"
|
||||
},
|
||||
_menuOpen: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: "_menuOpenChanged"
|
||||
},
|
||||
_showingTags: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
|
||||
/* global cordova, StatusBar */
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const { NotificationMixin, DialogMixin, MessagesMixin, DataMixin, AnimationMixin, ClipboardMixin,
|
||||
SyncMixin, AutoSyncMixin, AutoLockMixin, HintsMixin, AnalyticsMixin, LocaleMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
const { getPlatformName, getDeviceInfo, isTouch } = padlock.platform;
|
||||
// If we want to capture all keydown events, we have to add the listener
|
||||
// directly to the document
|
||||
document.addEventListener("keydown", this._keydown.bind(this), false);
|
||||
|
||||
const cordovaReady = new Promise((resolve) => {
|
||||
document.addEventListener("deviceready", resolve);
|
||||
});
|
||||
// Listen for android back button
|
||||
document.addEventListener("backbutton", this._back.bind(this), false);
|
||||
|
||||
class App extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
SyncMixin,
|
||||
AutoSyncMixin,
|
||||
AutoLockMixin,
|
||||
DialogMixin,
|
||||
MessagesMixin,
|
||||
NotificationMixin,
|
||||
HintsMixin,
|
||||
AnalyticsMixin,
|
||||
AnimationMixin,
|
||||
ClipboardMixin,
|
||||
LocaleMixin
|
||||
) {
|
||||
document.addEventListener("dialog-open", () => this.classList.add("dialog-open"));
|
||||
document.addEventListener("dialog-close", () => this.classList.remove("dialog-open"));
|
||||
}
|
||||
|
||||
static get is() { return "pl-app"; }
|
||||
get _isNarrow() {
|
||||
return this.offsetWidth < 600;
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
locked: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
observer: "_lockedChanged"
|
||||
},
|
||||
_currentView: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer: "_currentViewChanged"
|
||||
},
|
||||
_selectedRecord: {
|
||||
type: Object,
|
||||
observer: "_selectedRecordChanged"
|
||||
},
|
||||
_menuOpen: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: "_menuOpenChanged"
|
||||
},
|
||||
_showingTags: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
let isIPhoneX;
|
||||
getDeviceInfo()
|
||||
.then((device) => {
|
||||
isIPhoneX = /iPhone10,3|iPhone10,6/.test(device.model);
|
||||
if (isIPhoneX) {
|
||||
Object.assign(document.body.style, {
|
||||
margin: 0,
|
||||
height: "812px",
|
||||
position: "relative"
|
||||
});
|
||||
}
|
||||
return cordovaReady;
|
||||
})
|
||||
.then(() => {
|
||||
// Replace window.open method with the inappbrowser equivalent
|
||||
window.open = cordova.InAppBrowser.open;
|
||||
if (isIPhoneX) {
|
||||
StatusBar && StatusBar.show();
|
||||
}
|
||||
navigator.splashscreen.hide();
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// If we want to capture all keydown events, we have to add the listener
|
||||
// directly to the document
|
||||
document.addEventListener("keydown", this._keydown.bind(this), false);
|
||||
|
||||
// Listen for android back button
|
||||
document.addEventListener("backbutton", this._back.bind(this), false);
|
||||
|
||||
document.addEventListener("dialog-open", () => this.classList.add("dialog-open"));
|
||||
document.addEventListener("dialog-close", () => this.classList.remove("dialog-open"));
|
||||
}
|
||||
|
||||
get _isNarrow() {
|
||||
return this.offsetWidth < 600;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
let isIPhoneX;
|
||||
getDeviceInfo()
|
||||
.then((device) => {
|
||||
isIPhoneX = /iPhone10,3|iPhone10,6/.test(device.model);
|
||||
if (isIPhoneX) {
|
||||
Object.assign(document.body.style, {
|
||||
margin: 0,
|
||||
height: "812px",
|
||||
position: "relative"
|
||||
});
|
||||
}
|
||||
return cordovaReady;
|
||||
})
|
||||
.then(() => {
|
||||
// Replace window.open method with the inappbrowser equivalent
|
||||
window.open = cordova.InAppBrowser.open;
|
||||
if (isIPhoneX) {
|
||||
StatusBar && StatusBar.show();
|
||||
}
|
||||
navigator.splashscreen.hide();
|
||||
});
|
||||
|
||||
getPlatformName().then((platform) => {
|
||||
const className = platform.toLowerCase().replace(/ /g, "-");
|
||||
getPlatformName().then((platform) => {
|
||||
const className = platform.toLowerCase().replace(/ /g, "-");
|
||||
if (className) {
|
||||
this.classList.add(className);
|
||||
this.root.querySelector("pl-title-bar").classList.add(className);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!isTouch()) {
|
||||
window.addEventListener("focus", () => setTimeout(() => {
|
||||
if (this.locked) {
|
||||
this.$.startView.focus();
|
||||
}
|
||||
}, 100));
|
||||
}
|
||||
}
|
||||
if (!isTouch()) {
|
||||
window.addEventListener("focus", () => setTimeout(() => {
|
||||
if (this.locked) {
|
||||
this.$.startView.focus();
|
||||
}
|
||||
}, 100));
|
||||
}
|
||||
}
|
||||
|
||||
recordDeleted(record) {
|
||||
if (record === this._selectedRecord) {
|
||||
this.$.listView.deselect();
|
||||
}
|
||||
}
|
||||
recordDeleted(record) {
|
||||
if (record === this._selectedRecord) {
|
||||
this.$.listView.deselect();
|
||||
}
|
||||
}
|
||||
|
||||
dataLoaded() {
|
||||
this.locked = false;
|
||||
this.$.startView.open = true;
|
||||
}
|
||||
dataLoaded() {
|
||||
this.locked = false;
|
||||
this.$.startView.open = true;
|
||||
}
|
||||
|
||||
dataUnloaded() {
|
||||
this.clearDialogs();
|
||||
this.$.startView.reset();
|
||||
this.locked = true;
|
||||
this.$.startView.open = false;
|
||||
this.clearClipboard();
|
||||
}
|
||||
dataUnloaded() {
|
||||
this.clearDialogs();
|
||||
this.$.startView.reset();
|
||||
this.locked = true;
|
||||
this.$.startView.open = false;
|
||||
this.clearClipboard();
|
||||
}
|
||||
|
||||
dataReset() {
|
||||
setTimeout(() => this.alert($l("App reset successfully. Off to a fresh start!"), { type: "success" }), 500);
|
||||
}
|
||||
dataReset() {
|
||||
setTimeout(() => this.alert($l("App reset successfully. Off to a fresh start!"), { type: "success" }), 500);
|
||||
}
|
||||
|
||||
_closeRecord() {
|
||||
this.$.listView.deselect();
|
||||
}
|
||||
_closeRecord() {
|
||||
this.$.listView.deselect();
|
||||
}
|
||||
|
||||
_selectedRecordChanged() {
|
||||
clearTimeout(this._selectedRecordChangedTimeout);
|
||||
this._selectedRecordChangedTimeout = setTimeout(() => {
|
||||
if (this._selectedRecord) {
|
||||
this.$.recordView.record = this._selectedRecord;
|
||||
this._currentView = "recordView";
|
||||
this._selectedRecord.lastUsed = new Date();
|
||||
this.saveCollection();
|
||||
} else if (this._currentView == "recordView") {
|
||||
this._currentView = "";
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
_selectedRecordChanged() {
|
||||
clearTimeout(this._selectedRecordChangedTimeout);
|
||||
this._selectedRecordChangedTimeout = setTimeout(() => {
|
||||
if (this._selectedRecord) {
|
||||
this.$.recordView.record = this._selectedRecord;
|
||||
this._currentView = "recordView";
|
||||
this._selectedRecord.lastUsed = new Date();
|
||||
this.saveCollection();
|
||||
} else if (this._currentView == "recordView") {
|
||||
this._currentView = "";
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
_openSettings() {
|
||||
this._currentView = "settingsView";
|
||||
this.$.listView.deselect();
|
||||
}
|
||||
_openSettings() {
|
||||
this._currentView = "settingsView";
|
||||
this.$.listView.deselect();
|
||||
}
|
||||
|
||||
_settingsBack() {
|
||||
this._currentView = "";
|
||||
}
|
||||
_settingsBack() {
|
||||
this._currentView = "";
|
||||
}
|
||||
|
||||
_openCloudView() {
|
||||
this._currentView = "cloudView";
|
||||
this.refreshAccount();
|
||||
this.$.listView.deselect();
|
||||
if (!this.settings.syncConnected && !isTouch()) {
|
||||
setTimeout(() => this.$.cloudView.focusEmailInput(), 500);
|
||||
}
|
||||
}
|
||||
_openCloudView() {
|
||||
this._currentView = "cloudView";
|
||||
this.refreshAccount();
|
||||
this.$.listView.deselect();
|
||||
if (!this.settings.syncConnected && !isTouch()) {
|
||||
setTimeout(() => this.$.cloudView.focusEmailInput(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
_cloudViewBack() {
|
||||
this._currentView = "";
|
||||
}
|
||||
_cloudViewBack() {
|
||||
this._currentView = "";
|
||||
}
|
||||
|
||||
_currentViewChanged(curr, prev) {
|
||||
this.$.main.classList.toggle("showing-pages", !!curr);
|
||||
_currentViewChanged(curr, prev) {
|
||||
this.$.main.classList.toggle("showing-pages", !!curr);
|
||||
|
||||
const currView = this.$[curr];
|
||||
const prevView = this.$[prev];
|
||||
if (currView) {
|
||||
this.animateElement(currView, {
|
||||
animation: "viewIn",
|
||||
duration: 400,
|
||||
easing: "cubic-bezier(0.6, 0, 0.2, 1)",
|
||||
fill: "backwards"
|
||||
});
|
||||
currView.classList.add("showing");
|
||||
currView.animate();
|
||||
}
|
||||
if (prevView) {
|
||||
this.animateElement(prevView, {
|
||||
animation: !curr || this._isNarrow ? "viewOutSide" : "viewOutBack",
|
||||
duration: 400,
|
||||
easing: "cubic-bezier(0.6, 0, 0.2, 1)",
|
||||
fill: "forwards"
|
||||
});
|
||||
setTimeout(() => prevView.classList.remove("showing"), 350);
|
||||
}
|
||||
}
|
||||
const currView = this.$[curr];
|
||||
const prevView = this.$[prev];
|
||||
if (currView) {
|
||||
this.animateElement(currView, {
|
||||
animation: "viewIn",
|
||||
duration: 400,
|
||||
easing: "cubic-bezier(0.6, 0, 0.2, 1)",
|
||||
fill: "backwards"
|
||||
});
|
||||
currView.classList.add("showing");
|
||||
currView.animate();
|
||||
}
|
||||
if (prevView) {
|
||||
this.animateElement(prevView, {
|
||||
animation: !curr || this._isNarrow ? "viewOutSide" : "viewOutBack",
|
||||
duration: 400,
|
||||
easing: "cubic-bezier(0.6, 0, 0.2, 1)",
|
||||
fill: "forwards"
|
||||
});
|
||||
setTimeout(() => prevView.classList.remove("showing"), 350);
|
||||
}
|
||||
}
|
||||
|
||||
//* Keyboard shortcuts
|
||||
_keydown(event) {
|
||||
if (this.locked || padlock.Input.activeInput) {
|
||||
return;
|
||||
}
|
||||
//* Keyboard shortcuts
|
||||
_keydown(event) {
|
||||
if (this.locked || padlock.Input.activeInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shortcut;
|
||||
const control = event.ctrlKey || event.metaKey;
|
||||
let shortcut;
|
||||
const control = event.ctrlKey || event.metaKey;
|
||||
|
||||
// ESCAPE -> Back
|
||||
if (event.key === "Escape") {
|
||||
shortcut = () => this._back();
|
||||
}
|
||||
// CTRL/CMD + F -> Filter
|
||||
else if (control && event.key === "f") {
|
||||
shortcut = () => this.$.listView.search();
|
||||
}
|
||||
// CTRL/CMD + N -> New Record
|
||||
else if (control && event.key === "n") {
|
||||
shortcut = () => this.createRecord();
|
||||
}
|
||||
// ESCAPE -> Back
|
||||
if (event.key === "Escape") {
|
||||
shortcut = () => this._back();
|
||||
}
|
||||
// CTRL/CMD + F -> Filter
|
||||
else if (control && event.key === "f") {
|
||||
shortcut = () => this.$.listView.search();
|
||||
}
|
||||
// CTRL/CMD + N -> New Record
|
||||
else if (control && event.key === "n") {
|
||||
shortcut = () => this.createRecord();
|
||||
}
|
||||
|
||||
// If one of the shortcuts matches, execute it and prevent the default behaviour
|
||||
if (shortcut) {
|
||||
shortcut();
|
||||
event.preventDefault();
|
||||
} else if (event.key.length === 1) {
|
||||
this.$.listView.search();
|
||||
}
|
||||
}
|
||||
// If one of the shortcuts matches, execute it and prevent the default behaviour
|
||||
if (shortcut) {
|
||||
shortcut();
|
||||
event.preventDefault();
|
||||
} else if (event.key.length === 1) {
|
||||
this.$.listView.search();
|
||||
}
|
||||
}
|
||||
|
||||
_back() {
|
||||
switch (this._currentView) {
|
||||
case "recordView":
|
||||
this._closeRecord();
|
||||
break;
|
||||
case "settingsView":
|
||||
this._settingsBack();
|
||||
break;
|
||||
case "cloudView":
|
||||
this._cloudViewBack();
|
||||
break;
|
||||
default:
|
||||
if (this.$.listView.filterActive) {
|
||||
this.$.listView.clearFilter();
|
||||
} else {
|
||||
navigator.Backbutton && navigator.Backbutton.goBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
_back() {
|
||||
switch (this._currentView) {
|
||||
case "recordView":
|
||||
this._closeRecord();
|
||||
break;
|
||||
case "settingsView":
|
||||
this._settingsBack();
|
||||
break;
|
||||
case "cloudView":
|
||||
this._cloudViewBack();
|
||||
break;
|
||||
default:
|
||||
if (this.$.listView.filterActive) {
|
||||
this.$.listView.clearFilter();
|
||||
} else {
|
||||
navigator.Backbutton && navigator.Backbutton.goBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_lockedChanged() {
|
||||
if (this.locked) {
|
||||
this._currentView = "";
|
||||
this.$.main.classList.remove("active");
|
||||
this._menuOpen = false;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.$.main.classList.add("active");
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
_lockedChanged() {
|
||||
if (this.locked) {
|
||||
this._currentView = "";
|
||||
this.$.main.classList.remove("active");
|
||||
this._menuOpen = false;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.$.main.classList.add("active");
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
|
||||
_menuOpenChanged() {
|
||||
this.$.menuWrapper.classList.toggle("show-menu", this._menuOpen);
|
||||
this.$.main.classList.toggle("show-menu", this._menuOpen);
|
||||
if (!this._menuOpen) {
|
||||
setTimeout(() => this._showingTags = false, 300);
|
||||
}
|
||||
this.animateCascade(this.root.querySelectorAll(".menu .menu-item"), {
|
||||
animation: this._menuOpen ? "menuItemIn" : "menuItemOut",
|
||||
duration: 400,
|
||||
fullDuration: 600,
|
||||
initialDelay: 50,
|
||||
fill: "both"
|
||||
});
|
||||
}
|
||||
_menuOpenChanged() {
|
||||
this.$.menuWrapper.classList.toggle("show-menu", this._menuOpen);
|
||||
this.$.main.classList.toggle("show-menu", this._menuOpen);
|
||||
if (!this._menuOpen) {
|
||||
setTimeout(() => this._showingTags = false, 300);
|
||||
}
|
||||
this.animateCascade(this.root.querySelectorAll(".menu .menu-item"), {
|
||||
animation: this._menuOpen ? "menuItemIn" : "menuItemOut",
|
||||
duration: 400,
|
||||
fullDuration: 600,
|
||||
initialDelay: 50,
|
||||
fill: "both"
|
||||
});
|
||||
}
|
||||
|
||||
_toggleMenu() {
|
||||
this._menuOpen = !this._menuOpen;
|
||||
}
|
||||
_toggleMenu() {
|
||||
this._menuOpen = !this._menuOpen;
|
||||
}
|
||||
|
||||
_lock() {
|
||||
if (this.isSynching) {
|
||||
this.alert($l("Cannot lock app while sync is in progress!"));
|
||||
} else {
|
||||
this.unloadData();
|
||||
}
|
||||
}
|
||||
_lock() {
|
||||
if (this.isSynching) {
|
||||
this.alert($l("Cannot lock app while sync is in progress!"));
|
||||
} else {
|
||||
this.unloadData();
|
||||
}
|
||||
}
|
||||
|
||||
_newRecord() {
|
||||
this.createRecord();
|
||||
}
|
||||
_newRecord() {
|
||||
this.createRecord();
|
||||
}
|
||||
|
||||
_enableMultiSelect() {
|
||||
this.$.listView.multiSelect = true;
|
||||
}
|
||||
_enableMultiSelect() {
|
||||
this.$.listView.multiSelect = true;
|
||||
}
|
||||
|
||||
_menuWrapperClicked() {
|
||||
setTimeout(() => this._menuOpen = false, 50);
|
||||
}
|
||||
_menuWrapperClicked() {
|
||||
setTimeout(() => this._menuOpen = false, 50);
|
||||
}
|
||||
|
||||
_showTags(e) {
|
||||
this._menuOpen = true;
|
||||
this._showingTags = true;
|
||||
this.animateCascade(this.root.querySelectorAll(".tags .menu-item, .no-tags"), {
|
||||
animation: "tagIn",
|
||||
duration: 400,
|
||||
fullDuration: 600,
|
||||
fill: "both"
|
||||
});
|
||||
e.stopPropagation();
|
||||
}
|
||||
_showTags(e) {
|
||||
this._menuOpen = true;
|
||||
this._showingTags = true;
|
||||
this.animateCascade(this.root.querySelectorAll(".tags .menu-item, .no-tags"), {
|
||||
animation: "tagIn",
|
||||
duration: 400,
|
||||
fullDuration: 600,
|
||||
fill: "both"
|
||||
});
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
_closeTags(e) {
|
||||
this._showingTags = false;
|
||||
e.stopPropagation();
|
||||
}
|
||||
_closeTags(e) {
|
||||
this._showingTags = false;
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
_selectTag(e) {
|
||||
setTimeout(() => {
|
||||
this.$.listView.filterString = e.model.item;
|
||||
}, 350);
|
||||
}
|
||||
|
||||
_hasTags() {
|
||||
return !!this.collection.tags.length;
|
||||
}
|
||||
_selectTag(e) {
|
||||
setTimeout(() => {
|
||||
this.$.listView.filterString = e.model.item;
|
||||
}, 350);
|
||||
}
|
||||
|
||||
_hasTags() {
|
||||
return !!this.collection.tags.length;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(App.is, App);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script src="../../../bower_components/zxcvbn/lib/zxcvbn.js"></script>
|
||||
</dom-module>
|
|
@ -1,7 +1,4 @@
|
|||
<link rel="import" href="../base/base.html">
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
import '../base/base.js';
|
||||
|
||||
padlock.AutoLockMixin = (superClass) => {
|
||||
|
||||
|
@ -78,6 +75,3 @@ padlock.AutoLockMixin = (superClass) => {
|
|||
};
|
||||
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
|
@ -1,7 +1,4 @@
|
|||
<link rel="import" href="../base/base.html">
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
import '../base/base.js';
|
||||
|
||||
padlock.AutoSyncMixin = (superClass) => {
|
||||
|
||||
|
@ -22,6 +19,3 @@ padlock.AutoSyncMixin = (superClass) => {
|
|||
};
|
||||
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
|
@ -1,8 +1,5 @@
|
|||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
const { wait } = padlock.util;
|
||||
const { isChromeApp, isChromeOS, getReviewLink } = padlock.platform;
|
||||
|
@ -269,6 +266,3 @@ padlock.HintsMixin = (superClass) => {
|
|||
};
|
||||
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
|
@ -1,8 +1,5 @@
|
|||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
const { Messages } = padlock.messages;
|
||||
const { FileSource } = padlock.source;
|
||||
|
@ -45,6 +42,3 @@ padlock.MessagesMixin = (superClass) => {
|
|||
};
|
||||
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
|
@ -1,25 +0,0 @@
|
|||
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
window.padlock = window.padlock || {};
|
||||
|
||||
padlock.BaseElement = class Base extends Polymer.Element {
|
||||
|
||||
truthy(val) {
|
||||
return !!val;
|
||||
}
|
||||
|
||||
equals(val, ...vals) {
|
||||
return vals.some((v) => v === val);
|
||||
}
|
||||
|
||||
identity(val) {
|
||||
return val;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
|
@ -0,0 +1,22 @@
|
|||
import '../../../../../node_modules/@polymer/polymer/polymer-legacy.js';
|
||||
import { PolymerElement, html } from '../../../../../node_modules/@polymer/polymer/polymer-element.js';
|
||||
import "../../padlock.js";
|
||||
|
||||
window.Polymer = { html };
|
||||
window.padlock = window.padlock || {};
|
||||
|
||||
padlock.BaseElement = class Base extends PolymerElement {
|
||||
|
||||
truthy(val) {
|
||||
return !!val;
|
||||
}
|
||||
|
||||
equals(val, ...vals) {
|
||||
return vals.some((v) => v === val);
|
||||
}
|
||||
|
||||
identity(val) {
|
||||
return val;
|
||||
}
|
||||
|
||||
};
|
|
@ -1,11 +1,13 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
<dom-module id="pl-clipboard">
|
||||
|
||||
<template>
|
||||
const { setClipboard } = padlock.platform;
|
||||
const { LocaleMixin } = padlock;
|
||||
|
||||
class Clipboard extends LocaleMixin(padlock.BaseElement) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: flex;
|
||||
|
@ -52,66 +54,59 @@
|
|||
</style>
|
||||
|
||||
<div class="content">
|
||||
<div class="title">[[ $l("Copied To Clipboard:") ]]</div>
|
||||
<div class="title">[[ \$l("Copied To Clipboard:") ]]</div>
|
||||
<div class="name">[[ record.name ]] / [[ field.name ]]</div>
|
||||
</div>
|
||||
|
||||
<button class="tiles-2 tap" on-click="clear">
|
||||
<div><strong>[[ $l("Clear") ]]</strong></div>
|
||||
<div><strong>[[ \$l("Clear") ]]</strong></div>
|
||||
<div class="countdown">[[ _tMinusClear ]]s<div>
|
||||
</button>
|
||||
|
||||
|
||||
</template>
|
||||
</div></div></button>
|
||||
`;
|
||||
}
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
static get is() { return "pl-clipboard"; }
|
||||
|
||||
const { setClipboard } = padlock.platform;
|
||||
const { LocaleMixin } = padlock;
|
||||
static get properties() { return {
|
||||
record: Object,
|
||||
field: Object
|
||||
}; }
|
||||
|
||||
class Clipboard extends LocaleMixin(padlock.BaseElement) {
|
||||
set(record, field, duration = 60) {
|
||||
clearInterval(this._interval);
|
||||
|
||||
static get is() { return "pl-clipboard"; }
|
||||
this.record = record;
|
||||
this.field = field;
|
||||
setClipboard(field.value);
|
||||
|
||||
static get properties() { return {
|
||||
record: Object,
|
||||
field: Object
|
||||
}; }
|
||||
this.classList.add("showing");
|
||||
|
||||
set(record, field, duration = 60) {
|
||||
clearInterval(this._interval);
|
||||
const tStart = Date.now();
|
||||
|
||||
this.record = record;
|
||||
this.field = field;
|
||||
setClipboard(field.value);
|
||||
this._tMinusClear = duration;
|
||||
this._interval = setInterval(() => {
|
||||
const dt = tStart + duration * 1000 - Date.now();
|
||||
if (dt <= 0) {
|
||||
this.clear();
|
||||
} else {
|
||||
this._tMinusClear = Math.floor(dt/1000);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
this.classList.add("showing");
|
||||
|
||||
const tStart = Date.now();
|
||||
|
||||
this._tMinusClear = duration;
|
||||
this._interval = setInterval(() => {
|
||||
const dt = tStart + duration * 1000 - Date.now();
|
||||
if (dt <= 0) {
|
||||
this.clear();
|
||||
} else {
|
||||
this._tMinusClear = Math.floor(dt/1000);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
clearInterval(this._interval);
|
||||
setClipboard(" ");
|
||||
this.classList.remove("showing");
|
||||
typeof this._resolve === "function" && this._resolve();
|
||||
this._resolve = null;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
clearInterval(this._interval);
|
||||
setClipboard(" ");
|
||||
this.classList.remove("showing");
|
||||
typeof this._resolve === "function" && this._resolve();
|
||||
this._resolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Clipboard.is, Clipboard);
|
||||
|
@ -139,9 +134,3 @@ padlock.ClipboardMixin = (baseClass) => {
|
|||
|
||||
};
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
||||
|
|
@ -1,21 +1,31 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../animation/animation.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../data/data.html">
|
||||
<link rel="import" href="../dialog/dialog-mixin.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
<link rel="import" href="../input/input.html">
|
||||
<link rel="import" href="../loading-button/loading-button.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="../notification/notification.html">
|
||||
<link rel="import" href="../promo/promo.html">
|
||||
<link rel="import" href="../sync/sync.html">
|
||||
<link rel="import" href="../toggle/toggle-button.html">
|
||||
import '../../styles/shared.js';
|
||||
import '../animation/animation.js';
|
||||
import '../base/base.js';
|
||||
import '../data/data.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../icon/icon.js';
|
||||
import '../input/input.js';
|
||||
import '../loading-button/loading-button.js';
|
||||
import '../locale/locale.js';
|
||||
import '../notification/notification.js';
|
||||
import '../promo/promo.js';
|
||||
import '../sync/sync.js';
|
||||
import '../toggle/toggle-button.js';
|
||||
|
||||
<dom-module id="pl-cloud-view">
|
||||
|
||||
<template>
|
||||
const { LocaleMixin, DialogMixin, NotificationMixin, DataMixin, SyncMixin, AnimationMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
|
||||
class CloudView extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
SyncMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
NotificationMixin,
|
||||
AnimationMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: flex;
|
||||
|
@ -181,62 +191,62 @@
|
|||
|
||||
<header>
|
||||
<pl-icon icon="close" class="tap" on-click="_back"></pl-icon>
|
||||
<div class="title">[[ $l("My Account") ]]</div>
|
||||
<div class="title">[[ \$l("My Account") ]]</div>
|
||||
<pl-icon icon="logout" class="tap" on-click="_logout"></pl-icon>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<section class="highlight dark" hidden$="[[ !truthy(promo) ]]">
|
||||
<section class="highlight dark" hidden\$="[[ !truthy(promo) ]]">
|
||||
<pl-promo promo="[[ promo ]]" data-source="App - Promo" on-promo-redeem="_buySubscription" on-promo-expired="_promoExpired"></pl-promo>
|
||||
</section>
|
||||
|
||||
<section class="highlight tiles warning" hidden$="[[ !isTrialing(subStatus) ]]">
|
||||
<section class="highlight tiles warning" hidden\$="[[ !isTrialing(subStatus) ]]">
|
||||
<div class="info">
|
||||
<pl-icon class="info-icon" icon="time"></pl-icon>
|
||||
<div class="info-body">
|
||||
<div class="info-title">[[ $l("Trialing ({0} days left)", remainingTrialDays) ]]</div>
|
||||
<div class="info-title">[[ \$l("Trialing ({0} days left)", remainingTrialDays) ]]</div>
|
||||
<div class="info-text">[[ trialingMessage(remainingTrialDays) ]]</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="tap" on-click="_buySubscription" data-source="App - Trialing">[[ $l("Upgrade Now") ]]</button>
|
||||
<button class="tap" on-click="_buySubscription" data-source="App - Trialing">[[ \$l("Upgrade Now") ]]</button>
|
||||
</section>
|
||||
|
||||
<section class="highlight tiles warning" hidden$="[[ !isTrialExpired(subStatus) ]]">
|
||||
<section class="highlight tiles warning" hidden\$="[[ !isTrialExpired(subStatus) ]]">
|
||||
<div class="info">
|
||||
<pl-icon class="info-icon" icon="error"></pl-icon>
|
||||
<div class="info-body">
|
||||
<div class="info-title">[[ $l("Trial Expired") ]]</div>
|
||||
<div class="info-title">[[ \$l("Trial Expired") ]]</div>
|
||||
<div class="info-text">[[ trialExpiredMessage() ]]</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="tap" on-click="_buySubscription" data-source="App - Trial Expired">[[ $l("Upgrade Now") ]]</button>
|
||||
<button class="tap" on-click="_buySubscription" data-source="App - Trial Expired">[[ \$l("Upgrade Now") ]]</button>
|
||||
</section>
|
||||
|
||||
<section class="highlight tiles warning" hidden$="[[ !isSubUnpaid(subStatus) ]]">
|
||||
<section class="highlight tiles warning" hidden\$="[[ !isSubUnpaid(subStatus) ]]">
|
||||
<div class="info">
|
||||
<pl-icon class="info-icon" icon="error"></pl-icon>
|
||||
<div class="info-body">
|
||||
<div class="info-title">[[ $l("Payment Failed") ]]</div>
|
||||
<div class="info-title">[[ \$l("Payment Failed") ]]</div>
|
||||
<div class="info-text">[[ subUnpaidMessage() ]]</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="tap" on-click="_updatePaymentMethod" data-source="App - Payment Failed">[[ $l("Update Payment Method") ]]</button>
|
||||
<button class="tap" on-click="_contactSupport">[[ $l("Contact Support") ]]</button>
|
||||
<button class="tap" on-click="_updatePaymentMethod" data-source="App - Payment Failed">[[ \$l("Update Payment Method") ]]</button>
|
||||
<button class="tap" on-click="_contactSupport">[[ \$l("Contact Support") ]]</button>
|
||||
</section>
|
||||
|
||||
<section class="highlight tiles warning" hidden$="[[ !isSubCanceled(subStatus) ]]">
|
||||
<section class="highlight tiles warning" hidden\$="[[ !isSubCanceled(subStatus) ]]">
|
||||
<div class="info">
|
||||
<pl-icon class="info-icon" icon="error"></pl-icon>
|
||||
<div class="info-body">
|
||||
<div class="info-title">[[ $l("Subscription Canceled") ]]</div>
|
||||
<div class="info-title">[[ \$l("Subscription Canceled") ]]</div>
|
||||
<div class="info-text">[[ subCanceledMessage() ]]</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="tap" on-click="reactivateSubscription">[[ $l("Reactivate Subscription") ]]</button>
|
||||
<button class="tap" on-click="reactivateSubscription">[[ \$l("Reactivate Subscription") ]]</button>
|
||||
</section>
|
||||
|
||||
<section class="highlight hidden$="[[ !settings.syncConnected ]]">
|
||||
<section class="highlight hidden\$=" [[="" !settings.syncconnected="" ]]"="">
|
||||
<div class="account tiles">
|
||||
<div class="account-info">
|
||||
<div class="account-email">[[ settings.syncEmail ]]</div>
|
||||
|
@ -249,47 +259,44 @@
|
|||
<span>[[ lastSync ]]</span>
|
||||
</div>
|
||||
</div>
|
||||
<pl-icon class="account-sync tap" icon="refresh" spin$="[[ isSynching ]]" on-click="synchronize"
|
||||
disabled$="[[ !isSubValid(subStatus) ]]"></pl-icon>
|
||||
<pl-icon class="account-sync tap" icon="refresh" spin\$="[[ isSynching ]]" on-click="synchronize" disabled\$="[[ !isSubValid(subStatus) ]]"></pl-icon>
|
||||
</div>
|
||||
<div class="unlock-feature-hint" hidden$="[[ isSubValid(subStatus) ]]">[[ $l("Upgrade to enable synchronization!") ]]</div>
|
||||
<div class="unlock-feature-hint" hidden\$="[[ isSubValid(subStatus) ]]">[[ \$l("Upgrade to enable synchronization!") ]]</div>
|
||||
</section>
|
||||
|
||||
<section hidden$="[[ !settings.syncConnected ]]">
|
||||
<div class="section-header">[[ $l("{0} Devices Connected", account.devices.length) ]]</div>
|
||||
<section hidden\$="[[ !settings.syncConnected ]]">
|
||||
<div class="section-header">[[ \$l("{0} Devices Connected", account.devices.length) ]]</div>
|
||||
<div class="devices">
|
||||
<template is="dom-repeat" items="[[ account.devices ]]">
|
||||
<div class="section-row">
|
||||
<div class="section-row-label">[[ item.description ]]</div>
|
||||
<pl-icon icon="delete" class="tap" on-click="_revokeDevice" disabled$="[[ _isCurrentDevice(item) ]]"></pl-icon>
|
||||
<pl-icon icon="delete" class="tap" on-click="_revokeDevice" disabled\$="[[ _isCurrentDevice(item) ]]"></pl-icon>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section hidden$="[[ !truthy(account.paymentSource) ]]">
|
||||
<div class="section-header">[[ $l("Billing") ]]</div>
|
||||
<section hidden\$="[[ !truthy(account.paymentSource) ]]">
|
||||
<div class="section-header">[[ \$l("Billing") ]]</div>
|
||||
<button class="tap" on-click="_updatePaymentMethod" data-source="App - Billing">[[ _paymentSourceLabel(account.paymentSource) ]]</button>
|
||||
<button class="tap" on-click="cancelSubscription"
|
||||
hidden$="[[ !isSubActive(subStatus) ]]">[[ $l("Cancel Subscription") ]]</button>
|
||||
<button class="tap" on-click="cancelSubscription" hidden\$="[[ !isSubActive(subStatus) ]]">[[ \$l("Cancel Subscription") ]]</button>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<div class="login" hidden$="[[ settings.syncConnected ]]">
|
||||
<div class="login" hidden\$="[[ settings.syncConnected ]]">
|
||||
|
||||
<pl-icon icon="close" class="back-button tap" on-click="_back"></pl-icon>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div class="title">[[ $l("Padlock Online") ]]</div>
|
||||
<div class="title">[[ \$l("Padlock Online") ]]</div>
|
||||
|
||||
<div class="text">[[ loginInfoText() ]]</div>
|
||||
|
||||
<pl-input id="emailInput" type="email" placeholder="[[ $l('Enter Email Address') ]]" value="[[ settings.syncEmail ]]"
|
||||
select-on-focus required on-enter="_login" class="tap"></pl-input>
|
||||
<pl-input id="emailInput" type="email" placeholder="[[ \$l('Enter Email Address') ]]" value="[[ settings.syncEmail ]]" select-on-focus="" required="" on-enter="_login" class="tap"></pl-input>
|
||||
|
||||
<pl-loading-button id="loginButton" class="tap" on-click="_login">[[ $l("Log In") ]]</pl-loading-button>
|
||||
<pl-loading-button id="loginButton" class="tap" on-click="_login">[[ \$l("Log In") ]]</pl-loading-button>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
|
@ -302,158 +309,136 @@
|
|||
</div>
|
||||
|
||||
<div class="rounded-corners"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
static get is() { return "pl-cloud-view"; }
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
static get properties() {
|
||||
|
||||
const { LocaleMixin, DialogMixin, NotificationMixin, DataMixin, SyncMixin, AnimationMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
}
|
||||
|
||||
class CloudView extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
SyncMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
NotificationMixin,
|
||||
AnimationMixin
|
||||
) {
|
||||
ready() {
|
||||
super.ready();
|
||||
this.listen("data-loaded", () => this.animate());
|
||||
this.listen("sync-connect-start", () => this.animate());
|
||||
this.listen("sync-connect-cancel", () => this.animate());
|
||||
this.listen("sync-connect-success", () => this.animate());
|
||||
this.listen("sync-disconnect", () => this.animate());
|
||||
this.listen("sync-connect-success", () => this.refreshAccount());
|
||||
}
|
||||
|
||||
static get is() { return "pl-cloud-view"; }
|
||||
animate() {
|
||||
if (this.settings.syncConnected) {
|
||||
this.animateCascade(this.root.querySelectorAll("section:not([hidden])"), { initialDelay: 200 });
|
||||
}
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
focusEmailInput() {
|
||||
this.$.emailInput.focus();
|
||||
}
|
||||
|
||||
}
|
||||
_credentialsChanged() {
|
||||
if (this.isActivationPending() && !this._testCredsTimeout) {
|
||||
// Wait 1 minute, then poll every 10 seconds
|
||||
this._testCredsTimeout = setTimeout(() => this.testCredentials(10000), 60000);
|
||||
// Also test on first focus event since there is a chance the user is just returning
|
||||
// from his email client / web browsers
|
||||
window.addEventListener("focus", () => this.testCredentials(), { once: true });
|
||||
} else if (!this.isActivationPending()) {
|
||||
clearTimeout(this._testCredsTimeout);
|
||||
this._testCredsTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.listen("data-loaded", () => this.animate());
|
||||
this.listen("sync-connect-start", () => this.animate());
|
||||
this.listen("sync-connect-cancel", () => this.animate());
|
||||
this.listen("sync-connect-success", () => this.animate());
|
||||
this.listen("sync-disconnect", () => this.animate());
|
||||
this.listen("sync-connect-success", () => this.refreshAccount());
|
||||
}
|
||||
_back() {
|
||||
this.dispatchEvent(new CustomEvent("cloud-back"));
|
||||
}
|
||||
|
||||
animate() {
|
||||
if (this.settings.syncConnected) {
|
||||
this.animateCascade(this.root.querySelectorAll("section:not([hidden])"), { initialDelay: 200 });
|
||||
}
|
||||
}
|
||||
_logout() {
|
||||
this.confirm(
|
||||
$l("Are you sure you want to log out?"),
|
||||
$l("Log Out")
|
||||
).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.disconnectCloud();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
focusEmailInput() {
|
||||
this.$.emailInput.focus();
|
||||
}
|
||||
_login() {
|
||||
if (this._submittingEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
_credentialsChanged() {
|
||||
if (this.isActivationPending() && !this._testCredsTimeout) {
|
||||
// Wait 1 minute, then poll every 10 seconds
|
||||
this._testCredsTimeout = setTimeout(() => this.testCredentials(10000), 60000);
|
||||
// Also test on first focus event since there is a chance the user is just returning
|
||||
// from his email client / web browsers
|
||||
window.addEventListener("focus", () => this.testCredentials(), { once: true });
|
||||
} else if (!this.isActivationPending()) {
|
||||
clearTimeout(this._testCredsTimeout);
|
||||
this._testCredsTimeout = null;
|
||||
}
|
||||
}
|
||||
this.$.loginButton.start();
|
||||
|
||||
_back() {
|
||||
this.dispatchEvent(new CustomEvent("cloud-back"));
|
||||
}
|
||||
if (this.$.emailInput.invalid) {
|
||||
this.alert($l("Please enter a valid email address!"))
|
||||
.then(() => this.$.emailInput.focus());
|
||||
this.$.loginButton.fail();
|
||||
return;
|
||||
}
|
||||
|
||||
_logout() {
|
||||
this.confirm(
|
||||
$l("Are you sure you want to log out?"),
|
||||
$l("Log Out")
|
||||
).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.disconnectCloud();
|
||||
}
|
||||
});
|
||||
}
|
||||
this._submittingEmail = true;
|
||||
|
||||
_login() {
|
||||
if (this._submittingEmail) {
|
||||
return;
|
||||
}
|
||||
this.connectCloud(this.$.emailInput.value)
|
||||
.then(() => {
|
||||
this._submittingEmail = false;
|
||||
this.$.loginButton.success();
|
||||
return this.promptLoginCode();
|
||||
})
|
||||
.then(() => {
|
||||
if (this.settings.syncConnected) {
|
||||
this.synchronize();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this._submittingEmail = false;
|
||||
this.$.loginButton.fail();
|
||||
});
|
||||
|
||||
this.$.loginButton.start();
|
||||
}
|
||||
|
||||
if (this.$.emailInput.invalid) {
|
||||
this.alert($l("Please enter a valid email address!"))
|
||||
.then(() => this.$.emailInput.focus());
|
||||
this.$.loginButton.fail();
|
||||
return;
|
||||
}
|
||||
_isCurrentDevice(device) {
|
||||
return this.settings.syncId === device.tokenId;
|
||||
}
|
||||
|
||||
this._submittingEmail = true;
|
||||
_revokeDevice(e) {
|
||||
const device = e.model.item;
|
||||
this.confirm($l("Do you want to revoke access to for the device \"{0}\"?", device.description))
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.cloudSource.source.revokeAuthToken(device.tokenId)
|
||||
.then(() => {
|
||||
this.refreshAccount();
|
||||
this.alert($l("Access for {0} revoked successfully!", device.description),
|
||||
{ type: "success" });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.connectCloud(this.$.emailInput.value)
|
||||
.then(() => {
|
||||
this._submittingEmail = false;
|
||||
this.$.loginButton.success();
|
||||
return this.promptLoginCode();
|
||||
})
|
||||
.then(() => {
|
||||
if (this.settings.syncConnected) {
|
||||
this.synchronize();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this._submittingEmail = false;
|
||||
this.$.loginButton.fail();
|
||||
});
|
||||
_contactSupport() {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
|
||||
}
|
||||
_paymentSourceLabel() {
|
||||
const s = this.account && this.account.paymentSource;
|
||||
return s && `${s.brand} •••• •••• •••• ${s.lastFour}`;
|
||||
}
|
||||
|
||||
_isCurrentDevice(device) {
|
||||
return this.settings.syncId === device.tokenId;
|
||||
}
|
||||
_buySubscription(e) {
|
||||
this.buySubscription(e.target.dataset.source);
|
||||
}
|
||||
|
||||
_revokeDevice(e) {
|
||||
const device = e.model.item;
|
||||
this.confirm($l("Do you want to revoke access to for the device \"{0}\"?", device.description))
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.cloudSource.source.revokeAuthToken(device.tokenId)
|
||||
.then(() => {
|
||||
this.refreshAccount();
|
||||
this.alert($l("Access for {0} revoked successfully!", device.description),
|
||||
{ type: "success" });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_contactSupport() {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
|
||||
_paymentSourceLabel() {
|
||||
const s = this.account && this.account.paymentSource;
|
||||
return s && `${s.brand} •••• •••• •••• ${s.lastFour}`;
|
||||
}
|
||||
|
||||
_buySubscription(e) {
|
||||
this.buySubscription(e.target.dataset.source);
|
||||
}
|
||||
|
||||
_updatePaymentMethod(e) {
|
||||
this.updatePaymentMethod(e.target.dataset.source);
|
||||
}
|
||||
|
||||
_promoExpired() {
|
||||
this.dispatch("settings-changed");
|
||||
}
|
||||
_updatePaymentMethod(e) {
|
||||
this.updatePaymentMethod(e.target.dataset.source);
|
||||
}
|
||||
|
||||
_promoExpired() {
|
||||
this.dispatch("settings-changed");
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(CloudView.is, CloudView);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -1,14 +1,11 @@
|
|||
<link rel="import" href="../../../bower_components/polymer/lib/mixins/mutable-data.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
import { MutableData } from '../../../../../node_modules/@polymer/polymer/lib/mixins/mutable-data.js';
|
||||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
import { Polymer } from '../../../../../node_modules/@polymer/polymer/lib/legacy/polymer-fn.js';
|
||||
|
||||
const { Collection, Record, Settings } = padlock.data;
|
||||
const { FileSource, EncryptedSource } = padlock.source;
|
||||
const { getDesktopSettings } = padlock.platform;
|
||||
const { MutableData } = Polymer;
|
||||
|
||||
const desktopSettings = getDesktopSettings();
|
||||
const dbPath = desktopSettings ? desktopSettings.get("dbPath") : "data.pls";
|
||||
|
@ -293,6 +290,3 @@ Object.assign(padlock.DataMixin, {
|
|||
collection,
|
||||
settings
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
|
@ -1,134 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="dialog.html">
|
||||
|
||||
<dom-module id="pl-dialog-alert">
|
||||
|
||||
<template>
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
};
|
||||
}
|
||||
|
||||
:host([type="warning"]) {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(180deg, #f49300 0%, #f25b00 100%);
|
||||
};
|
||||
}
|
||||
|
||||
:host([type="plain"]) {
|
||||
--pl-dialog-inner: {
|
||||
background: var(--color-background);
|
||||
};
|
||||
}
|
||||
|
||||
:host([hide-icon]) .info-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([hide-icon]) .info-text,
|
||||
:host([hide-icon]) .info-title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-text:not(.small) {
|
||||
font-size: var(--font-size-default);
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog" open="{{ open }}"
|
||||
prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dialogDismiss">
|
||||
<div class="info" hidden$="[[ _hideInfo(title, message) ]]">
|
||||
<pl-icon class="info-icon" icon="[[ _icon(type) ]]"></pl-icon>
|
||||
<div class="info-body">
|
||||
<div class="info-title">[[ title ]]</div>
|
||||
<div class$="info-text [[ _textClass(title) ]]">[[ message ]]</div>
|
||||
</div>
|
||||
</div>
|
||||
<template is="dom-repeat" items="[[ options ]]">
|
||||
<button on-click="_selectOption" class$="[[ _buttonClass(index) ]]">[[ item ]]</button>
|
||||
</template>
|
||||
</pl-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
const defaultButtonLabel = $l("OK");
|
||||
|
||||
class DialogAlert extends padlock.BaseElement {
|
||||
|
||||
static get is() { return "pl-dialog-alert"; }
|
||||
|
||||
static get properties() { return {
|
||||
buttonLabel: { type: String, value: defaultButtonLabel },
|
||||
title: { type: String, value: ""},
|
||||
message: { type: String, value: "" },
|
||||
options: { type: Array, value: ["OK"] },
|
||||
preventDismiss: { type: Boolean, value: false },
|
||||
type: { type: String, value: "info", reflectToAttribute: true },
|
||||
hideIcon: { type: Boolean, value: false, reflectToAttribute: true },
|
||||
open: { type: Boolean, value: false }
|
||||
}; }
|
||||
|
||||
show(message = "", { title = "", options = ["OK"], type = "info", preventDismiss = false, hideIcon = false } = {}) {
|
||||
this.message = message;
|
||||
this.title = title;
|
||||
this.type = type;
|
||||
this.preventDismiss = preventDismiss;
|
||||
this.options = options;
|
||||
this.hideIcon = hideIcon;
|
||||
|
||||
setTimeout(() => this.open = true, 10);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_icon() {
|
||||
switch (this.type) {
|
||||
case "info":
|
||||
return "info-round";
|
||||
case "warning":
|
||||
return "error";
|
||||
case "success":
|
||||
return "success";
|
||||
case "question":
|
||||
return "question";
|
||||
}
|
||||
}
|
||||
|
||||
_selectOption(e) {
|
||||
this.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(this.options.indexOf(e.model.item));
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_dialogDismiss() {
|
||||
typeof this._resolve === "function" && this._resolve();
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_textClass() {
|
||||
return this.title ? "small" : "";
|
||||
}
|
||||
|
||||
_buttonClass(index) {
|
||||
return "tap tiles-" + (Math.floor((index + 1) % 8) + 1);
|
||||
}
|
||||
|
||||
_hideInfo() {
|
||||
return !this.title && !this.message;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogAlert.is, DialogAlert);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,124 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
import './dialog.js';
|
||||
|
||||
const defaultButtonLabel = $l("OK");
|
||||
|
||||
class DialogAlert extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
};
|
||||
}
|
||||
|
||||
:host([type="warning"]) {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(180deg, #f49300 0%, #f25b00 100%);
|
||||
};
|
||||
}
|
||||
|
||||
:host([type="plain"]) {
|
||||
--pl-dialog-inner: {
|
||||
background: var(--color-background);
|
||||
};
|
||||
}
|
||||
|
||||
:host([hide-icon]) .info-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([hide-icon]) .info-text,
|
||||
:host([hide-icon]) .info-title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-text:not(.small) {
|
||||
font-size: var(--font-size-default);
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog" open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dialogDismiss">
|
||||
<div class="info" hidden\$="[[ _hideInfo(title, message) ]]">
|
||||
<pl-icon class="info-icon" icon="[[ _icon(type) ]]"></pl-icon>
|
||||
<div class="info-body">
|
||||
<div class="info-title">[[ title ]]</div>
|
||||
<div class\$="info-text [[ _textClass(title) ]]">[[ message ]]</div>
|
||||
</div>
|
||||
</div>
|
||||
<template is="dom-repeat" items="[[ options ]]">
|
||||
<button on-click="_selectOption" class\$="[[ _buttonClass(index) ]]">[[ item ]]</button>
|
||||
</template>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-dialog-alert"; }
|
||||
|
||||
static get properties() { return {
|
||||
buttonLabel: { type: String, value: defaultButtonLabel },
|
||||
title: { type: String, value: ""},
|
||||
message: { type: String, value: "" },
|
||||
options: { type: Array, value: ["OK"] },
|
||||
preventDismiss: { type: Boolean, value: false },
|
||||
type: { type: String, value: "info", reflectToAttribute: true },
|
||||
hideIcon: { type: Boolean, value: false, reflectToAttribute: true },
|
||||
open: { type: Boolean, value: false }
|
||||
}; }
|
||||
|
||||
show(message = "", { title = "", options = ["OK"], type = "info", preventDismiss = false, hideIcon = false } = {}) {
|
||||
this.message = message;
|
||||
this.title = title;
|
||||
this.type = type;
|
||||
this.preventDismiss = preventDismiss;
|
||||
this.options = options;
|
||||
this.hideIcon = hideIcon;
|
||||
|
||||
setTimeout(() => this.open = true, 10);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_icon() {
|
||||
switch (this.type) {
|
||||
case "info":
|
||||
return "info-round";
|
||||
case "warning":
|
||||
return "error";
|
||||
case "success":
|
||||
return "success";
|
||||
case "question":
|
||||
return "question";
|
||||
}
|
||||
}
|
||||
|
||||
_selectOption(e) {
|
||||
this.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(this.options.indexOf(e.model.item));
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_dialogDismiss() {
|
||||
typeof this._resolve === "function" && this._resolve();
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_textClass() {
|
||||
return this.title ? "small" : "";
|
||||
}
|
||||
|
||||
_buttonClass(index) {
|
||||
return "tap tiles-" + (Math.floor((index + 1) % 8) + 1);
|
||||
}
|
||||
|
||||
_hideInfo() {
|
||||
return !this.title && !this.message;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogAlert.is, DialogAlert);
|
|
@ -1,67 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="dialog.html">
|
||||
|
||||
<dom-module id="pl-dialog-confirm">
|
||||
|
||||
<template>
|
||||
<style include="shared"></style>
|
||||
|
||||
<pl-dialog open="{{ open }}" prevent-dismiss>
|
||||
<div class="message tiles-1">{{ message }}</div>
|
||||
<button class="tap tiles-2" on-click="_confirm">{{ confirmLabel }}</button>
|
||||
<button class="tap tiles-3" on-click="_cancel">{{ cancelLabel }}</button>
|
||||
</pl-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
const defaultMessage = $l("Are you sure you want to do this?");
|
||||
const defaultConfirmLabel = $l("Confirm");
|
||||
const defaultCancelLabel = $l("Cancel");
|
||||
|
||||
class DialogConfirm extends padlock.BaseElement {
|
||||
|
||||
static get is() { return "pl-dialog-confirm"; }
|
||||
|
||||
static get properties() { return {
|
||||
confirmLabel: { type: String, value: defaultConfirmLabel },
|
||||
cancelLabel: { type: String, value: defaultCancelLabel },
|
||||
message: { type: String, value: defaultMessage },
|
||||
open: { type: Boolean, value: false }
|
||||
}; }
|
||||
|
||||
_confirm() {
|
||||
this.dispatchEvent(new CustomEvent("dialog-confirm", { bubbles: true, composed: true }));
|
||||
this.open = false;
|
||||
typeof (this._resolve === "function") && this._resolve(true);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_cancel() {
|
||||
this.dispatchEvent(new CustomEvent("dialog-cancel", { bubbles: true, composed: true }));
|
||||
this.open = false;
|
||||
typeof (this.resolve === "function") && this._resolve(false);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
confirm(message, confirmLabel, cancelLabel) {
|
||||
this.message = message || defaultMessage;
|
||||
this.confirmLabel = confirmLabel || defaultConfirmLabel;
|
||||
this.cancelLabel = cancelLabel || defaultCancelLabel;
|
||||
this.open = true;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogConfirm.is, DialogConfirm);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,58 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
import './dialog.js';
|
||||
|
||||
const defaultMessage = $l("Are you sure you want to do this?");
|
||||
const defaultConfirmLabel = $l("Confirm");
|
||||
const defaultCancelLabel = $l("Cancel");
|
||||
|
||||
class DialogConfirm extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared"></style>
|
||||
|
||||
<pl-dialog open="{{ open }}" prevent-dismiss="">
|
||||
<div class="message tiles-1">{{ message }}</div>
|
||||
<button class="tap tiles-2" on-click="_confirm">{{ confirmLabel }}</button>
|
||||
<button class="tap tiles-3" on-click="_cancel">{{ cancelLabel }}</button>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-dialog-confirm"; }
|
||||
|
||||
static get properties() { return {
|
||||
confirmLabel: { type: String, value: defaultConfirmLabel },
|
||||
cancelLabel: { type: String, value: defaultCancelLabel },
|
||||
message: { type: String, value: defaultMessage },
|
||||
open: { type: Boolean, value: false }
|
||||
}; }
|
||||
|
||||
_confirm() {
|
||||
this.dispatchEvent(new CustomEvent("dialog-confirm", { bubbles: true, composed: true }));
|
||||
this.open = false;
|
||||
typeof (this._resolve === "function") && this._resolve(true);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_cancel() {
|
||||
this.dispatchEvent(new CustomEvent("dialog-cancel", { bubbles: true, composed: true }));
|
||||
this.open = false;
|
||||
typeof (this.resolve === "function") && this._resolve(false);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
confirm(message, confirmLabel, cancelLabel) {
|
||||
this.message = message || defaultMessage;
|
||||
this.confirmLabel = confirmLabel || defaultConfirmLabel;
|
||||
this.cancelLabel = cancelLabel || defaultCancelLabel;
|
||||
this.open = true;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogConfirm.is, DialogConfirm);
|
|
@ -1,13 +1,10 @@
|
|||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../generator/generator.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="dialog-alert.html">
|
||||
<link rel="import" href="dialog-confirm.html">
|
||||
<link rel="import" href="dialog-prompt.html">
|
||||
<link rel="import" href="dialog-options.html">
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
import '../base/base.js';
|
||||
import '../generator/generator.js';
|
||||
import '../locale/locale.js';
|
||||
import './dialog-alert.js';
|
||||
import './dialog-confirm.js';
|
||||
import './dialog-prompt.js';
|
||||
import './dialog-options.js';
|
||||
|
||||
const dialogElements = {};
|
||||
|
||||
|
@ -107,6 +104,3 @@ padlock.DialogMixin = (superClass) => {
|
|||
|
||||
};
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
|
@ -1,72 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="dialog.html">
|
||||
|
||||
<dom-module id="pl-dialog-options">
|
||||
|
||||
<template>
|
||||
<style include="shared"></style>
|
||||
|
||||
<pl-dialog open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dismiss">
|
||||
<template is="dom-if" if="[[ _hasMessage(message) ]]" restamp>
|
||||
<div class="message tiles-1">[[ message ]]</div>
|
||||
</template>
|
||||
<template is="dom-repeat" items="[[ options ]]">
|
||||
<button class$="[[ _buttonClass(index) ]]" on-click="_selectOption">[[ item ]]</button>
|
||||
</template>
|
||||
</pl-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
class DialogOptions extends padlock.BaseElement {
|
||||
|
||||
static get is() { return "pl-dialog-options"; }
|
||||
|
||||
static get properties() { return {
|
||||
message: { type: String, value: "" },
|
||||
open: { type: Boolean, value: false },
|
||||
options: { type: Array, value: [$l("Dismiss")] },
|
||||
preventDismiss: { type: Boolean, value: false }
|
||||
}; }
|
||||
|
||||
choose(message, options) {
|
||||
this.message = message || "";
|
||||
this.options = options || this.options;
|
||||
|
||||
setTimeout(() => this.open = true, 50);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_selectOption(e) {
|
||||
this.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(this.options.indexOf(e.model.item));
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_buttonClass(index) {
|
||||
return "tap tiles-" + (Math.floor((index + 1) % 8) + 1);
|
||||
}
|
||||
|
||||
_hasMessage(message) {
|
||||
return !!message;
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(-1);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.customElements.define(DialogOptions.is, DialogOptions);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,62 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../locale/locale.js';
|
||||
import './dialog.js';
|
||||
|
||||
class DialogOptions extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared"></style>
|
||||
|
||||
<pl-dialog open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dismiss">
|
||||
<template is="dom-if" if="[[ _hasMessage(message) ]]" restamp="">
|
||||
<div class="message tiles-1">[[ message ]]</div>
|
||||
</template>
|
||||
<template is="dom-repeat" items="[[ options ]]">
|
||||
<button class\$="[[ _buttonClass(index) ]]" on-click="_selectOption">[[ item ]]</button>
|
||||
</template>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-dialog-options"; }
|
||||
|
||||
static get properties() { return {
|
||||
message: { type: String, value: "" },
|
||||
open: { type: Boolean, value: false },
|
||||
options: { type: Array, value: [$l("Dismiss")] },
|
||||
preventDismiss: { type: Boolean, value: false }
|
||||
}; }
|
||||
|
||||
choose(message, options) {
|
||||
this.message = message || "";
|
||||
this.options = options || this.options;
|
||||
|
||||
setTimeout(() => this.open = true, 50);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_selectOption(e) {
|
||||
this.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(this.options.indexOf(e.model.item));
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_buttonClass(index) {
|
||||
return "tap tiles-" + (Math.floor((index + 1) % 8) + 1);
|
||||
}
|
||||
|
||||
_hasMessage(message) {
|
||||
return !!message;
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(-1);
|
||||
this._resolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogOptions.is, DialogOptions);
|
|
@ -1,120 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../input/input.html">
|
||||
<link rel="import" href="../loading-button/loading-button.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="dialog.html">
|
||||
|
||||
<dom-module id="pl-dialog-prompt">
|
||||
|
||||
<template>
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
};
|
||||
}
|
||||
|
||||
pl-input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
position: relative;
|
||||
margin-top: 15px;
|
||||
font-weight: bold;
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-error);
|
||||
text-shadow: none;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog" open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dismiss">
|
||||
<div class="message tiles-1" hidden$="[[ !_hasMessage(message) ]]">[[ message ]]</div>
|
||||
<pl-input class="tiles-2" id="input" type="[[ type ]]" placeholder="[[ placeholder ]]" on-enter="_confirm"></pl-input>
|
||||
<pl-loading-button id="confirmButton" class="tap tiles-3" on-click="_confirm">[[ confirmLabel ]]</pl-loading-button>
|
||||
<button class="tap tiles-4" on-click="_dismiss" hidden$="[[ _hideCancelButton ]]">[[ cancelLabel ]]</button>
|
||||
<div class="validation-message" slot="after">[[ _validationMessage ]]</div>
|
||||
</pl-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
const defaultConfirmLabel = $l("OK");
|
||||
const defaultCancelLabel = $l("Cancel");
|
||||
const defaultType = "text";
|
||||
const defaultPlaceholder = "";
|
||||
|
||||
class DialogPrompt extends padlock.BaseElement {
|
||||
|
||||
static get is() { return "pl-dialog-prompt"; }
|
||||
|
||||
static get properties() { return {
|
||||
confirmLabel: { type: String, value: defaultConfirmLabel },
|
||||
cancelLabel: { type: String, value: defaultCancelLabel },
|
||||
message: { type: String, value: "" },
|
||||
open: { type: Boolean, value: false },
|
||||
placeholder: { type: String, value: ""},
|
||||
preventDismiss: { type: Boolean, value: true},
|
||||
type: { type: String, value: defaultType },
|
||||
validationFn: Function,
|
||||
_validationMessage: { type: String, value: "" }
|
||||
}; }
|
||||
|
||||
_confirm() {
|
||||
this.$.confirmButton.start();
|
||||
const val = this.$.input.value;
|
||||
const p = typeof this.validationFn === "function" ? this.validationFn(val) : Promise.resolve(val);
|
||||
p.then((v) => {
|
||||
this._validationMessage = "";
|
||||
this.$.confirmButton.success();
|
||||
typeof this._resolve === "function" && this._resolve(v);
|
||||
this._resolve = null;
|
||||
this.open = false;
|
||||
}).catch((e) => {
|
||||
this.$.dialog.rumble();
|
||||
this._validationMessage = e;
|
||||
this.$.confirmButton.fail();
|
||||
});
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(null);
|
||||
this._resolve = null;
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
_hasMessage() {
|
||||
return !!this.message;
|
||||
}
|
||||
|
||||
prompt(message, placeholder, type, confirmLabel, cancelLabel, preventDismiss = true, validation) {
|
||||
this.$.confirmButton.stop();
|
||||
this.message = message || "";
|
||||
this.type = type || defaultType;
|
||||
this.placeholder = placeholder || defaultPlaceholder;
|
||||
this.confirmLabel = confirmLabel || defaultConfirmLabel;
|
||||
this.cancelLabel = cancelLabel || defaultCancelLabel;
|
||||
this._hideCancelButton = cancelLabel === false;
|
||||
this.preventDismiss = preventDismiss;
|
||||
this.validationFn = validation;
|
||||
this._validationMessage = "";
|
||||
this.$.input.value = "";
|
||||
this.open = true;
|
||||
|
||||
setTimeout(() => this.$.input.focus(), 100);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogPrompt.is, DialogPrompt);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,111 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../input/input.js';
|
||||
import '../loading-button/loading-button.js';
|
||||
import '../locale/locale.js';
|
||||
import './dialog.js';
|
||||
|
||||
const defaultConfirmLabel = $l("OK");
|
||||
const defaultCancelLabel = $l("Cancel");
|
||||
const defaultType = "text";
|
||||
const defaultPlaceholder = "";
|
||||
|
||||
class DialogPrompt extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
};
|
||||
}
|
||||
|
||||
pl-input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
position: relative;
|
||||
margin-top: 15px;
|
||||
font-weight: bold;
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-error);
|
||||
text-shadow: none;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog" open="{{ open }}" prevent-dismiss="[[ preventDismiss ]]" on-dialog-dismiss="_dismiss">
|
||||
<div class="message tiles-1" hidden\$="[[ !_hasMessage(message) ]]">[[ message ]]</div>
|
||||
<pl-input class="tiles-2" id="input" type="[[ type ]]" placeholder="[[ placeholder ]]" on-enter="_confirm"></pl-input>
|
||||
<pl-loading-button id="confirmButton" class="tap tiles-3" on-click="_confirm">[[ confirmLabel ]]</pl-loading-button>
|
||||
<button class="tap tiles-4" on-click="_dismiss" hidden\$="[[ _hideCancelButton ]]">[[ cancelLabel ]]</button>
|
||||
<div class="validation-message" slot="after">[[ _validationMessage ]]</div>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-dialog-prompt"; }
|
||||
|
||||
static get properties() { return {
|
||||
confirmLabel: { type: String, value: defaultConfirmLabel },
|
||||
cancelLabel: { type: String, value: defaultCancelLabel },
|
||||
message: { type: String, value: "" },
|
||||
open: { type: Boolean, value: false },
|
||||
placeholder: { type: String, value: ""},
|
||||
preventDismiss: { type: Boolean, value: true},
|
||||
type: { type: String, value: defaultType },
|
||||
validationFn: Function,
|
||||
_validationMessage: { type: String, value: "" }
|
||||
}; }
|
||||
|
||||
_confirm() {
|
||||
this.$.confirmButton.start();
|
||||
const val = this.$.input.value;
|
||||
const p = typeof this.validationFn === "function" ? this.validationFn(val) : Promise.resolve(val);
|
||||
p.then((v) => {
|
||||
this._validationMessage = "";
|
||||
this.$.confirmButton.success();
|
||||
typeof this._resolve === "function" && this._resolve(v);
|
||||
this._resolve = null;
|
||||
this.open = false;
|
||||
}).catch((e) => {
|
||||
this.$.dialog.rumble();
|
||||
this._validationMessage = e;
|
||||
this.$.confirmButton.fail();
|
||||
});
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(null);
|
||||
this._resolve = null;
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
_hasMessage() {
|
||||
return !!this.message;
|
||||
}
|
||||
|
||||
prompt(message, placeholder, type, confirmLabel, cancelLabel, preventDismiss = true, validation) {
|
||||
this.$.confirmButton.stop();
|
||||
this.message = message || "";
|
||||
this.type = type || defaultType;
|
||||
this.placeholder = placeholder || defaultPlaceholder;
|
||||
this.confirmLabel = confirmLabel || defaultConfirmLabel;
|
||||
this.cancelLabel = cancelLabel || defaultCancelLabel;
|
||||
this._hideCancelButton = cancelLabel === false;
|
||||
this.preventDismiss = preventDismiss;
|
||||
this.validationFn = validation;
|
||||
this._validationMessage = "";
|
||||
this.$.input.value = "";
|
||||
this.open = true;
|
||||
|
||||
setTimeout(() => this.$.input.focus(), 100);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(DialogPrompt.is, DialogPrompt);
|
|
@ -1,191 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../animation/animation.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../input/input.html">
|
||||
|
||||
<dom-module id="pl-dialog">
|
||||
|
||||
<template>
|
||||
<style include="shared">
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
@apply --fullbleed;
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
@apply --scroll;
|
||||
}
|
||||
|
||||
:host(:not(.open)) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.outer {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scrim {
|
||||
display: block;
|
||||
background: var(--color-background);
|
||||
opacity: 0;
|
||||
transition: opacity 400ms cubic-bezier(0.6, 0, 0.2, 1);
|
||||
transform: translate3d(0, 0, 0);
|
||||
@apply --fullbleed;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
:host(.open) .scrim {
|
||||
opacity: 0.90;
|
||||
}
|
||||
|
||||
.inner {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
max-width: var(--pl-dialog-max-width, 400px);
|
||||
z-index: 1;
|
||||
--color-background: var(--color-primary);
|
||||
--color-foreground: var(--color-tertiary);
|
||||
--color-highlight: var(--color-secondary);
|
||||
background: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
border-radius: var(--border-radius);
|
||||
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
|
||||
box-shadow: rgba(0, 0, 0, 0.25) 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
@apply --pl-dialog-inner;
|
||||
}
|
||||
|
||||
.outer {
|
||||
transform: translate3d(0, 0, 0);
|
||||
/* transition: transform 400ms cubic-bezier(1, -0.3, 0, 1.3), opacity 400ms cubic-bezier(0.6, 0, 0.2, 1); */
|
||||
transition: transform 400ms cubic-bezier(0.6, 0, 0.2, 1), opacity 400ms cubic-bezier(0.6, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
:host(:not(.open)) .outer {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0) scale(0.8);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="scrim"></div>
|
||||
|
||||
<div class="outer" on-click="dismiss">
|
||||
<slot name="before"></slot>
|
||||
<div id="inner" class="inner" on-click="_preventDismiss">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="after"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
class Dialog extends padlock.AnimationMixin(padlock.BaseElement) {
|
||||
|
||||
static get is() { return "pl-dialog"; }
|
||||
|
||||
static get properties() { return {
|
||||
animationOptions: {
|
||||
type: Object,
|
||||
value: {
|
||||
duration: 500,
|
||||
fullDuration: 700
|
||||
}
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
observer: "_openChanged"
|
||||
},
|
||||
isShowing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true
|
||||
},
|
||||
preventDismiss: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
// window.addEventListener("keydown", (e) => {
|
||||
// if (this.open && (e.key === "Enter" || e.key === "Escape")) {
|
||||
// this.dismiss();
|
||||
// // e.preventDefault();
|
||||
// // e.stopPropagation();
|
||||
// }
|
||||
// });
|
||||
window.addEventListener("backbutton", (e) => {
|
||||
if (this.open) {
|
||||
this.dismiss();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rumble() {
|
||||
this.animateElement(this.$.inner, { animation: "rumble", duration: 200, clear: true });
|
||||
}
|
||||
|
||||
//* Changed handler for the _open_ property. Shows/hides the dialog
|
||||
_openChanged() {
|
||||
clearTimeout(this._hideTimeout);
|
||||
|
||||
// Set _display: block_ if we're showing. If we're hiding
|
||||
// we need to wait until the transitions have finished before we
|
||||
// set _display: none_.
|
||||
if (this.open) {
|
||||
if (padlock.Input.activeInput) {
|
||||
padlock.Input.activeInput.blur();
|
||||
}
|
||||
this.style.display = "";
|
||||
this.isShowing = true;
|
||||
} else {
|
||||
this._hideTimeout = window.setTimeout(() => {
|
||||
this.style.display = "none";
|
||||
this.isShowing = false;
|
||||
}, 400);
|
||||
}
|
||||
|
||||
this.offsetLeft;
|
||||
|
||||
this.classList.toggle("open", this.open);
|
||||
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
this.open ? "dialog-open" : "dialog-close",
|
||||
{ bubbles: true, composed: true }
|
||||
));
|
||||
}
|
||||
|
||||
_preventDismiss(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
if (!this.preventDismiss) {
|
||||
this.dispatchEvent(new CustomEvent("dialog-dismiss"));
|
||||
this.open = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.customElements.define(Dialog.is, Dialog);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,181 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../animation/animation.js';
|
||||
import '../base/base.js';
|
||||
import '../input/input.js';
|
||||
|
||||
class Dialog extends padlock.AnimationMixin(padlock.BaseElement) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
@apply --fullbleed;
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
@apply --scroll;
|
||||
}
|
||||
|
||||
:host(:not(.open)) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.outer {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scrim {
|
||||
display: block;
|
||||
background: var(--color-background);
|
||||
opacity: 0;
|
||||
transition: opacity 400ms cubic-bezier(0.6, 0, 0.2, 1);
|
||||
transform: translate3d(0, 0, 0);
|
||||
@apply --fullbleed;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
:host(.open) .scrim {
|
||||
opacity: 0.90;
|
||||
}
|
||||
|
||||
.inner {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
max-width: var(--pl-dialog-max-width, 400px);
|
||||
z-index: 1;
|
||||
--color-background: var(--color-primary);
|
||||
--color-foreground: var(--color-tertiary);
|
||||
--color-highlight: var(--color-secondary);
|
||||
background: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
border-radius: var(--border-radius);
|
||||
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
|
||||
box-shadow: rgba(0, 0, 0, 0.25) 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
@apply --pl-dialog-inner;
|
||||
}
|
||||
|
||||
.outer {
|
||||
transform: translate3d(0, 0, 0);
|
||||
/* transition: transform 400ms cubic-bezier(1, -0.3, 0, 1.3), opacity 400ms cubic-bezier(0.6, 0, 0.2, 1); */
|
||||
transition: transform 400ms cubic-bezier(0.6, 0, 0.2, 1), opacity 400ms cubic-bezier(0.6, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
:host(:not(.open)) .outer {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0) scale(0.8);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="scrim"></div>
|
||||
|
||||
<div class="outer" on-click="dismiss">
|
||||
<slot name="before"></slot>
|
||||
<div id="inner" class="inner" on-click="_preventDismiss">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="after"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-dialog"; }
|
||||
|
||||
static get properties() { return {
|
||||
animationOptions: {
|
||||
type: Object,
|
||||
value: {
|
||||
duration: 500,
|
||||
fullDuration: 700
|
||||
}
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
observer: "_openChanged"
|
||||
},
|
||||
isShowing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true
|
||||
},
|
||||
preventDismiss: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
// window.addEventListener("keydown", (e) => {
|
||||
// if (this.open && (e.key === "Enter" || e.key === "Escape")) {
|
||||
// this.dismiss();
|
||||
// // e.preventDefault();
|
||||
// // e.stopPropagation();
|
||||
// }
|
||||
// });
|
||||
window.addEventListener("backbutton", (e) => {
|
||||
if (this.open) {
|
||||
this.dismiss();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rumble() {
|
||||
this.animateElement(this.$.inner, { animation: "rumble", duration: 200, clear: true });
|
||||
}
|
||||
|
||||
//* Changed handler for the _open_ property. Shows/hides the dialog
|
||||
_openChanged() {
|
||||
clearTimeout(this._hideTimeout);
|
||||
|
||||
// Set _display: block_ if we're showing. If we're hiding
|
||||
// we need to wait until the transitions have finished before we
|
||||
// set _display: none_.
|
||||
if (this.open) {
|
||||
if (padlock.Input.activeInput) {
|
||||
padlock.Input.activeInput.blur();
|
||||
}
|
||||
this.style.display = "";
|
||||
this.isShowing = true;
|
||||
} else {
|
||||
this._hideTimeout = window.setTimeout(() => {
|
||||
this.style.display = "none";
|
||||
this.isShowing = false;
|
||||
}, 400);
|
||||
}
|
||||
|
||||
this.offsetLeft;
|
||||
|
||||
this.classList.toggle("open", this.open);
|
||||
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
this.open ? "dialog-open" : "dialog-close",
|
||||
{ bubbles: true, composed: true }
|
||||
));
|
||||
}
|
||||
|
||||
_preventDismiss(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
if (!this.preventDismiss) {
|
||||
this.dispatchEvent(new CustomEvent("dialog-dismiss"));
|
||||
this.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Dialog.is, Dialog);
|
|
@ -1,57 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../dialog/dialog.html">
|
||||
<link rel="import" href="./export.html">
|
||||
|
||||
<dom-module id="pl-export-dialog">
|
||||
|
||||
<template>
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
};
|
||||
}
|
||||
|
||||
.message {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog">
|
||||
<div class="message">[[ $l("Export {0} Records", records.length) ]]</div>
|
||||
<pl-export export-records="[[ records ]]" on-click="_close" class="tiles-2"></pl-export>
|
||||
</pl-dialog>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
const { BaseElement, LocaleMixin } = padlock;
|
||||
|
||||
class PlExportDialog extends LocaleMixin(BaseElement) {
|
||||
|
||||
static get is() { return "pl-export-dialog"; }
|
||||
|
||||
static get properties() { return {
|
||||
records: Array
|
||||
}; }
|
||||
|
||||
_close() {
|
||||
this.$.dialog.open = false;
|
||||
}
|
||||
|
||||
export(records) {
|
||||
this.records = records;
|
||||
this.$.dialog.open = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.customElements.define(PlExportDialog.is, PlExportDialog);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,46 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../dialog/dialog.js';
|
||||
import './export.js';
|
||||
|
||||
const { BaseElement, LocaleMixin } = padlock;
|
||||
|
||||
class PlExportDialog extends LocaleMixin(BaseElement) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(rgb(89, 198, 255) 0%, rgb(7, 124, 185) 100%);
|
||||
};
|
||||
}
|
||||
|
||||
.message {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog">
|
||||
<div class="message">[[ \$l("Export {0} Records", records.length) ]]</div>
|
||||
<pl-export export-records="[[ records ]]" on-click="_close" class="tiles-2"></pl-export>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-export-dialog"; }
|
||||
|
||||
static get properties() { return {
|
||||
records: Array
|
||||
}; }
|
||||
|
||||
_close() {
|
||||
this.$.dialog.open = false;
|
||||
}
|
||||
|
||||
export(records) {
|
||||
this.records = records;
|
||||
this.$.dialog.open = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(PlExportDialog.is, PlExportDialog);
|
|
@ -1,172 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../data/data.html">
|
||||
<link rel="import" href="../dialog/dialog-mixin.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
|
||||
<dom-module id="pl-export">
|
||||
|
||||
<template>
|
||||
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 15px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
pl-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="tiles tiles-1 row">
|
||||
<div class="label">[[ $l("As CSV") ]]</div>
|
||||
<pl-icon icon="copy" class="tap" on-click="_copyCSV"></pl-icon>
|
||||
<pl-icon icon="download" class="tap" on-click="_downloadCSV" hidden$="[[ _isMobile() ]]"></pl-icon>
|
||||
</div>
|
||||
<div class="tiles tiles-2 row">
|
||||
<div class="label">[[ $l("As Encrypted File") ]]</div>
|
||||
<pl-icon icon="copy" class="tap" on-click="_copyEncrypted"></pl-icon>
|
||||
<pl-icon icon="download" class="tap" on-click="_downloadEncrypted" hidden$="[[ _isMobile() ]]"></pl-icon>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global zxcvbn */
|
||||
(() => {
|
||||
|
||||
const exportCSVWarning = $l(
|
||||
"WARNING: Exporting to CSV format will save your data without encyryption of any " +
|
||||
"kind which means it can be read by anyone. We strongly recommend exporting your data as " +
|
||||
"a secure, encrypted file, instead! Are you sure you want to proceed?"
|
||||
);
|
||||
|
||||
const { LocaleMixin, DialogMixin, DataMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
const { isCordova, setClipboard } = padlock.platform;
|
||||
const { toPadlock, toCSV } = padlock.exp;
|
||||
|
||||
class PlExport extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin
|
||||
) {
|
||||
|
||||
static get is() { return "pl-export"; }
|
||||
|
||||
static get properties() { return {
|
||||
exportRecords: Array
|
||||
}; }
|
||||
|
||||
_downloadCSV() {
|
||||
this.confirm(exportCSVWarning, $l("Download"), $l("Cancel"), { type: "warning" })
|
||||
.then((confirm) => {
|
||||
if (confirm) {
|
||||
setTimeout(() => {
|
||||
const date = new Date().toISOString().substr(0, 10);
|
||||
const fileName = `padlock-export-${date}.csv`;
|
||||
const csv = toCSV(this.exportRecords);
|
||||
const a = document.createElement("a");
|
||||
a.href = `data:application/octet-stream,${encodeURIComponent(csv)}`;
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
this.dispatch("data-exported");
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_copyCSV() {
|
||||
this.confirm(exportCSVWarning, $l("Copy to Clipboard"), $l("Cancel"), { type: "warning" })
|
||||
.then((confirm) => {
|
||||
if (confirm) {
|
||||
setClipboard(toCSV(this.exportRecords))
|
||||
.then(() => this.alert($l("Your data has successfully been copied to the system " +
|
||||
"clipboard. You can now paste it into the spreadsheet program of your choice.")));
|
||||
this.dispatch("data-exported");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getEncryptedData() {
|
||||
return this.prompt($l("Please choose a password to protect your data. This may be the same as " +
|
||||
"your master password or something else, but make sure it is sufficiently strong!"),
|
||||
$l("Enter Password"), "password", $l("Confirm"), $l("Cancel"))
|
||||
.then((pwd) => {
|
||||
if (!pwd) {
|
||||
if (pwd === "") {
|
||||
this.alert($l("Please enter a password!"));
|
||||
}
|
||||
return Promise.reject();
|
||||
}
|
||||
if (zxcvbn(pwd).score < 2) {
|
||||
return this.confirm($l(
|
||||
"WARNING: The password you entered is weak which makes it easier for " +
|
||||
"attackers to break the encryption used to protect your data. Try to use a longer " +
|
||||
"password or include a variation of uppercase, lowercase and special characters as " +
|
||||
"well as numbers."
|
||||
), $l("Use Anyway"), $l("Choose Different Password"), { type: "warning" }).then((confirm) => {
|
||||
if (!confirm) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return toPadlock(this.exportRecords, pwd);
|
||||
});
|
||||
} else {
|
||||
return toPadlock(this.exportRecords, pwd);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_downloadEncrypted() {
|
||||
this._getEncryptedData()
|
||||
.then((data) => {
|
||||
const a = document.createElement("a");
|
||||
const date = new Date().toISOString().substr(0, 10);
|
||||
const fileName = `padlock-export-${date}.pls`;
|
||||
a.href = `data:application/octet-stream,${encodeURIComponent(data)}`;
|
||||
a.download = fileName;
|
||||
setTimeout(() => {
|
||||
a.click();
|
||||
this.dispatch("data-exported");
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
_copyEncrypted() {
|
||||
this._getEncryptedData()
|
||||
.then((data) => {
|
||||
setClipboard(data)
|
||||
.then(() => {
|
||||
this.alert($l("Your data has successfully been copied to the system clipboard."),
|
||||
{ type: "success" });
|
||||
});
|
||||
this.dispatch("data-exported");
|
||||
});
|
||||
}
|
||||
|
||||
_isMobile() {
|
||||
return isCordova();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.customElements.define(PlExport.is, PlExport);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,159 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../data/data.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../icon/icon.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
const exportCSVWarning = $l(
|
||||
"WARNING: Exporting to CSV format will save your data without encyryption of any " +
|
||||
"kind which means it can be read by anyone. We strongly recommend exporting your data as " +
|
||||
"a secure, encrypted file, instead! Are you sure you want to proceed?"
|
||||
);
|
||||
|
||||
const { LocaleMixin, DialogMixin, DataMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
const { isCordova, setClipboard } = padlock.platform;
|
||||
const { toPadlock, toCSV } = padlock.exp;
|
||||
|
||||
class PlExport extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 15px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
pl-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="tiles tiles-1 row">
|
||||
<div class="label">[[ \$l("As CSV") ]]</div>
|
||||
<pl-icon icon="copy" class="tap" on-click="_copyCSV"></pl-icon>
|
||||
<pl-icon icon="download" class="tap" on-click="_downloadCSV" hidden\$="[[ _isMobile() ]]"></pl-icon>
|
||||
</div>
|
||||
<div class="tiles tiles-2 row">
|
||||
<div class="label">[[ \$l("As Encrypted File") ]]</div>
|
||||
<pl-icon icon="copy" class="tap" on-click="_copyEncrypted"></pl-icon>
|
||||
<pl-icon icon="download" class="tap" on-click="_downloadEncrypted" hidden\$="[[ _isMobile() ]]"></pl-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-export"; }
|
||||
|
||||
static get properties() { return {
|
||||
exportRecords: Array
|
||||
}; }
|
||||
|
||||
_downloadCSV() {
|
||||
this.confirm(exportCSVWarning, $l("Download"), $l("Cancel"), { type: "warning" })
|
||||
.then((confirm) => {
|
||||
if (confirm) {
|
||||
setTimeout(() => {
|
||||
const date = new Date().toISOString().substr(0, 10);
|
||||
const fileName = `padlock-export-${date}.csv`;
|
||||
const csv = toCSV(this.exportRecords);
|
||||
const a = document.createElement("a");
|
||||
a.href = `data:application/octet-stream,${encodeURIComponent(csv)}`;
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
this.dispatch("data-exported");
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_copyCSV() {
|
||||
this.confirm(exportCSVWarning, $l("Copy to Clipboard"), $l("Cancel"), { type: "warning" })
|
||||
.then((confirm) => {
|
||||
if (confirm) {
|
||||
setClipboard(toCSV(this.exportRecords))
|
||||
.then(() => this.alert($l("Your data has successfully been copied to the system " +
|
||||
"clipboard. You can now paste it into the spreadsheet program of your choice.")));
|
||||
this.dispatch("data-exported");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getEncryptedData() {
|
||||
return this.prompt($l("Please choose a password to protect your data. This may be the same as " +
|
||||
"your master password or something else, but make sure it is sufficiently strong!"),
|
||||
$l("Enter Password"), "password", $l("Confirm"), $l("Cancel"))
|
||||
.then((pwd) => {
|
||||
if (!pwd) {
|
||||
if (pwd === "") {
|
||||
this.alert($l("Please enter a password!"));
|
||||
}
|
||||
return Promise.reject();
|
||||
}
|
||||
if (zxcvbn(pwd).score < 2) {
|
||||
return this.confirm($l(
|
||||
"WARNING: The password you entered is weak which makes it easier for " +
|
||||
"attackers to break the encryption used to protect your data. Try to use a longer " +
|
||||
"password or include a variation of uppercase, lowercase and special characters as " +
|
||||
"well as numbers."
|
||||
), $l("Use Anyway"), $l("Choose Different Password"), { type: "warning" }).then((confirm) => {
|
||||
if (!confirm) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return toPadlock(this.exportRecords, pwd);
|
||||
});
|
||||
} else {
|
||||
return toPadlock(this.exportRecords, pwd);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_downloadEncrypted() {
|
||||
this._getEncryptedData()
|
||||
.then((data) => {
|
||||
const a = document.createElement("a");
|
||||
const date = new Date().toISOString().substr(0, 10);
|
||||
const fileName = `padlock-export-${date}.pls`;
|
||||
a.href = `data:application/octet-stream,${encodeURIComponent(data)}`;
|
||||
a.download = fileName;
|
||||
setTimeout(() => {
|
||||
a.click();
|
||||
this.dispatch("data-exported");
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
_copyEncrypted() {
|
||||
this._getEncryptedData()
|
||||
.then((data) => {
|
||||
setClipboard(data)
|
||||
.then(() => {
|
||||
this.alert($l("Your data has successfully been copied to the system clipboard."),
|
||||
{ type: "success" });
|
||||
});
|
||||
this.dispatch("data-exported");
|
||||
});
|
||||
}
|
||||
|
||||
_isMobile() {
|
||||
return isCordova();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(PlExport.is, PlExport);
|
|
@ -1,14 +1,16 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../dialog/dialog.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="../slider/slider.html">
|
||||
<link rel="import" href="../toggle/toggle-button.html">
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../dialog/dialog.js';
|
||||
import '../icon/icon.js';
|
||||
import '../locale/locale.js';
|
||||
import '../slider/slider.js';
|
||||
import '../toggle/toggle-button.js';
|
||||
|
||||
<dom-module id="pl-generator">
|
||||
const { BaseElement, LocaleMixin } = padlock;
|
||||
|
||||
<template>
|
||||
class Generator extends LocaleMixin(BaseElement) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
|
@ -45,7 +47,7 @@
|
|||
|
||||
.header::before, .header::after {
|
||||
font-family: "FontAwesome";
|
||||
content: "\ \f0e7\ ";
|
||||
content: "\\ \\f0e7\\ ";
|
||||
}
|
||||
|
||||
.value {
|
||||
|
@ -81,96 +83,83 @@
|
|||
|
||||
<pl-dialog id="dialog" on-dialog-dismiss="_dismiss">
|
||||
<div class="generate-button tap" on-click="_generate">
|
||||
<div class="header">[[ $l("Generate Random Value") ]]</div>
|
||||
<div class="header">[[ \$l("Generate Random Value") ]]</div>
|
||||
<div class="value tiles-1">
|
||||
{{ value }}
|
||||
</div>
|
||||
</div>
|
||||
<pl-toggle-button label="a-z" active="{{ lower }}" class="tap" reverse></pl-toggle-button>
|
||||
<pl-toggle-button label="A-Z" active="{{ upper }}" class="tap" reverse></pl-toggle-button>
|
||||
<pl-toggle-button label="0-9" active="{{ numbers }}" class="tap" reverse></pl-toggle-button>
|
||||
<pl-toggle-button label="?()/%..." active="{{ other }}" class="tap" reverse></pl-toggle-button>
|
||||
<pl-slider label="[[ $l('length') ]]" min="5" max="50" value="{{ length }}"></pl-slider>
|
||||
<button class="confirm-button tap" on-click="_confirm">[[ $l("Apply") ]]</button>
|
||||
<pl-toggle-button label="a-z" active="{{ lower }}" class="tap" reverse=""></pl-toggle-button>
|
||||
<pl-toggle-button label="A-Z" active="{{ upper }}" class="tap" reverse=""></pl-toggle-button>
|
||||
<pl-toggle-button label="0-9" active="{{ numbers }}" class="tap" reverse=""></pl-toggle-button>
|
||||
<pl-toggle-button label="?()/%..." active="{{ other }}" class="tap" reverse=""></pl-toggle-button>
|
||||
<pl-slider label="[[ \$l('length') ]]" min="5" max="50" value="{{ length }}"></pl-slider>
|
||||
<button class="confirm-button tap" on-click="_confirm">[[ \$l("Apply") ]]</button>
|
||||
<pl-icon icon="cancel" class="close-button tap" on-click="_dismiss"></pl-icon>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
static get is() { return "pl-generator"; }
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
static get properties() { return {
|
||||
value: {
|
||||
type: String,
|
||||
value: "",
|
||||
notify: true
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
value: 10
|
||||
},
|
||||
lower: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
upper: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
numbers: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
other: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
|
||||
const { BaseElement, LocaleMixin } = padlock;
|
||||
static get observers() { return [
|
||||
"_generate(length, lower, upper, numbers, other)"
|
||||
]; }
|
||||
|
||||
class Generator extends LocaleMixin(BaseElement) {
|
||||
generate() {
|
||||
this._generate();
|
||||
this.$.dialog.open = true;
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
static get is() { return "pl-generator"; }
|
||||
_generate() {
|
||||
var charSet = "";
|
||||
this.lower && (charSet += padlock.util.chars.lower);
|
||||
this.upper && (charSet += padlock.util.chars.upper);
|
||||
this.numbers && (charSet += padlock.util.chars.numbers);
|
||||
this.other && (charSet += padlock.util.chars.other);
|
||||
|
||||
static get properties() { return {
|
||||
value: {
|
||||
type: String,
|
||||
value: "",
|
||||
notify: true
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
value: 10
|
||||
},
|
||||
lower: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
upper: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
numbers: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
other: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
this.value = charSet ? padlock.util.randomString(this.length, charSet) : "";
|
||||
}
|
||||
|
||||
static get observers() { return [
|
||||
"_generate(length, lower, upper, numbers, other)"
|
||||
]; }
|
||||
|
||||
generate() {
|
||||
this._generate();
|
||||
this.$.dialog.open = true;
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_generate() {
|
||||
var charSet = "";
|
||||
this.lower && (charSet += padlock.util.chars.lower);
|
||||
this.upper && (charSet += padlock.util.chars.upper);
|
||||
this.numbers && (charSet += padlock.util.chars.numbers);
|
||||
this.other && (charSet += padlock.util.chars.other);
|
||||
|
||||
this.value = charSet ? padlock.util.randomString(this.length, charSet) : "";
|
||||
}
|
||||
|
||||
_confirm() {
|
||||
typeof this._resolve === "function" && this._resolve(this.value);
|
||||
this.$.dialog.open = false;
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(undefined);
|
||||
this.$.dialog.open = false;
|
||||
}
|
||||
_confirm() {
|
||||
typeof this._resolve === "function" && this._resolve(this.value);
|
||||
this.$.dialog.open = false;
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(undefined);
|
||||
this.$.dialog.open = false;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Generator.is, Generator);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -1,19 +1,9 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'FontAwesome';
|
||||
src: url('../../../assets/fonts/fontawesome-webfont.eot?v=4.7.0');
|
||||
src: url('../../../assets/fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../../../assets/fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../../../assets/fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../../../assets/fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../../../assets/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
<dom-module id="pl-icon">
|
||||
<template>
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
|
||||
class PlIcon extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: inline-block;
|
||||
|
@ -38,216 +28,203 @@
|
|||
}
|
||||
|
||||
:host([icon="add"]) > div::before {
|
||||
content: "\f067";
|
||||
content: "\\f067";
|
||||
}
|
||||
|
||||
:host([icon="menu"]) > div::before {
|
||||
content: "\f0c9";
|
||||
content: "\\f0c9";
|
||||
}
|
||||
|
||||
:host([icon="close"]) > div::before {
|
||||
content: "\f00d";
|
||||
content: "\\f00d";
|
||||
}
|
||||
|
||||
:host([icon="more"]) > div::before {
|
||||
content: "\f142";
|
||||
content: "\\f142";
|
||||
}
|
||||
|
||||
:host([icon="delete"]) > div::before {
|
||||
content: "\f014";
|
||||
content: "\\f014";
|
||||
}
|
||||
|
||||
:host([icon="copy"]) > div::before {
|
||||
/* content: "\f24d"; */
|
||||
content: "\f0c5";
|
||||
/* content: "\\f24d"; */
|
||||
content: "\\f0c5";
|
||||
}
|
||||
|
||||
:host([icon="edit"]) > div::before {
|
||||
content: "\f040";
|
||||
content: "\\f040";
|
||||
}
|
||||
|
||||
:host([icon="forward"]) > div::before {
|
||||
content: "\f054";
|
||||
content: "\\f054";
|
||||
}
|
||||
|
||||
:host([icon="backward"]) > div::before {
|
||||
content: "\f053";
|
||||
content: "\\f053";
|
||||
}
|
||||
|
||||
:host([icon="check"]) > div::before {
|
||||
content: "\f00c";
|
||||
content: "\\f00c";
|
||||
}
|
||||
|
||||
:host([icon="cancel"]) > div::before {
|
||||
content: "\f00d";
|
||||
content: "\\f00d";
|
||||
}
|
||||
|
||||
:host([icon="generate"]) > div::before {
|
||||
/* content: "\f0d0"; */
|
||||
content: "\f0e7";
|
||||
/* content: "\\f0d0"; */
|
||||
content: "\\f0e7";
|
||||
}
|
||||
|
||||
:host([icon="tag"]) > div::before {
|
||||
content: "\f02b";
|
||||
content: "\\f02b";
|
||||
}
|
||||
|
||||
:host([icon="dropdown"]) > div::before {
|
||||
content: "\f0d7";
|
||||
content: "\\f0d7";
|
||||
}
|
||||
|
||||
:host([icon="dropup"]) > div::before {
|
||||
content: "\f0d8";
|
||||
content: "\\f0d8";
|
||||
}
|
||||
|
||||
:host([icon="settings"]) > div::before {
|
||||
content: "\f013";
|
||||
content: "\\f013";
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
:host([icon="cloud"]) > div::before {
|
||||
content: "\f0c2";
|
||||
content: "\\f0c2";
|
||||
}
|
||||
|
||||
:host([icon="lock"]) > div::before {
|
||||
content: "\f023";
|
||||
content: "\\f023";
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
:host([icon="refresh"]) > div::before {
|
||||
content: "\f021";
|
||||
content: "\\f021";
|
||||
}
|
||||
|
||||
:host([icon="unlock"]) > div::before {
|
||||
content: "\f13e";
|
||||
content: "\\f13e";
|
||||
}
|
||||
|
||||
:host([icon="export"]) > div::before {
|
||||
content: "\f093";
|
||||
content: "\\f093";
|
||||
}
|
||||
|
||||
:host([icon="import"]) > div::before {
|
||||
content: "\f019";
|
||||
content: "\\f019";
|
||||
}
|
||||
|
||||
:host([icon="search"]) > div::before {
|
||||
content: "\f002";
|
||||
content: "\\f002";
|
||||
}
|
||||
|
||||
:host([icon="info"]) > div::before {
|
||||
content: "\f129";
|
||||
content: "\\f129";
|
||||
}
|
||||
|
||||
:host([icon="info-round"]) > div::before {
|
||||
content: "\f05a";
|
||||
content: "\\f05a";
|
||||
}
|
||||
|
||||
:host([icon="download"]) > div::before {
|
||||
content: "\f019";
|
||||
content: "\\f019";
|
||||
}
|
||||
|
||||
:host([icon="upload"]) > div::before {
|
||||
content: "\f093";
|
||||
content: "\\f093";
|
||||
}
|
||||
|
||||
:host([icon="show"]) > div::before {
|
||||
content: "\f06e";
|
||||
content: "\\f06e";
|
||||
}
|
||||
|
||||
:host([icon="hide"]) > div::before {
|
||||
content: "\f070";
|
||||
content: "\\f070";
|
||||
}
|
||||
|
||||
:host([icon="checked"]) > div::before {
|
||||
content: "\f046";
|
||||
content: "\\f046";
|
||||
}
|
||||
|
||||
:host([icon="success"]) > div::before {
|
||||
content: "\f058";
|
||||
content: "\\f058";
|
||||
}
|
||||
|
||||
:host([icon="unchecked"]) > div::before {
|
||||
content: "\f096";
|
||||
content: "\\f096";
|
||||
}
|
||||
|
||||
:host([icon="share"]) > div::before {
|
||||
content: "\f045";
|
||||
content: "\\f045";
|
||||
}
|
||||
|
||||
:host([icon="logout"]) > div::before {
|
||||
content: "\f08b";
|
||||
content: "\\f08b";
|
||||
}
|
||||
|
||||
:host([icon="mail"]) > div::before {
|
||||
content: "\f0e0";
|
||||
content: "\\f0e0";
|
||||
}
|
||||
|
||||
:host([icon="user"]) > div::before {
|
||||
content: "\f2bd";
|
||||
content: "\\f2bd";
|
||||
}
|
||||
|
||||
:host([icon="record"]) > div::before {
|
||||
content: "\f15b";
|
||||
content: "\\f15b";
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
:host([icon="mobile"]) > div::before {
|
||||
content: "\f10b";
|
||||
content: "\\f10b";
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
:host([icon="database"]) > div::before {
|
||||
content: "\f1c0";
|
||||
content: "\\f1c0";
|
||||
}
|
||||
|
||||
:host([icon="time"]) > div::before {
|
||||
content: "\f017";
|
||||
content: "\\f017";
|
||||
}
|
||||
|
||||
:host([icon="error"]) > div::before {
|
||||
content: "\f071";
|
||||
content: "\\f071";
|
||||
}
|
||||
|
||||
:host([icon="question"]) > div::before {
|
||||
content: "\f059";
|
||||
content: "\\f059";
|
||||
}
|
||||
|
||||
:host([icon="desktop"]) > div::before {
|
||||
content: "\f109";
|
||||
content: "\\f109";
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
:host([icon="logo"]) > div::before {
|
||||
font-family: "Padlock";
|
||||
content: "\0041";
|
||||
content: "\\0041";
|
||||
font-size: 110%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div></div>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global Polymer */
|
||||
|
||||
(() => {
|
||||
|
||||
class PlIcon extends padlock.BaseElement {
|
||||
|
||||
static get is() { return "pl-icon"; }
|
||||
|
||||
static get properties() { return {
|
||||
icon: {
|
||||
type: String,
|
||||
reflectToAttribute: true
|
||||
}
|
||||
}; }
|
||||
static get is() { return "pl-icon"; }
|
||||
|
||||
static get properties() { return {
|
||||
icon: {
|
||||
type: String,
|
||||
reflectToAttribute: true
|
||||
}
|
||||
}; }
|
||||
}
|
||||
|
||||
window.customElements.define(PlIcon.is, PlIcon);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -1,275 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
|
||||
<dom-module id="pl-input">
|
||||
<template>
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host(:not([multiline])) {
|
||||
padding: 0 10px;
|
||||
height: var(--row-height);
|
||||
}
|
||||
|
||||
input {
|
||||
box-sizing: border-box;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
text-align: inherit;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
text-shadow: inherit;
|
||||
color: inherit;
|
||||
opacity: 0.5;
|
||||
@apply --pl-input-placeholder;
|
||||
}
|
||||
|
||||
.mask {
|
||||
@apply --fullbleed;
|
||||
pointer-events: none;
|
||||
font-size: 150%;
|
||||
line-height: 22px;
|
||||
letter-spacing: -4.5px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
input[disabled], textarea[disabled] {
|
||||
opacity: 1;
|
||||
-webkit-text-fill-color: currentColor;
|
||||
}
|
||||
|
||||
input[invisible], textarea[invisible] {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template is="dom-if" if="[[ multiline ]]" on-dom-change="_domChange">
|
||||
<textarea id="input" value="{{ value::input }}" placeholder$="[[ placeholder ]]" readonly$="[[ readonly ]]"
|
||||
rows="1" autocomplete="off" spellcheck="false" autocapitalize$="[[ _computeAutoCapitalize(autocapitalize) ]]" autocorrect="off"
|
||||
on-focus="_focused" on-blur="_blurred" on-change="_changeHandler" on-keydown="_keydown"
|
||||
on-touchend="_stopPropagation" tabindex$="[[ _tabIndex(noTab) ]]"
|
||||
invisible$="[[ _showMask(masked, value, focused) ]]" disabled$="[[ disabled ]]"></textarea>
|
||||
<textarea class="mask" value="[[ _mask(value) ]]" tabindex="-1"
|
||||
invisible$="[[ !_showMask(masked, value, focused) ]]" disabled></textarea>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[ !multiline ]]" on-dom-change="_domChange">
|
||||
<input id="input" value="{{ value::input }}" tabindex$="[[ _tabIndex(noTab) ]]"
|
||||
autocomplete="off" spellcheck="false" autocapitalize$="[[ _computeAutoCapitalize(autocapitalize) ]]" autocorrect="off"
|
||||
type$="[[ type ]]" placeholder$="[[ placeholder ]]" readonly$="[[ readonly ]]"
|
||||
required$="[[ required ]]" pattern$="[[ pattern ]]" disabled$="[[ disabled ]]"
|
||||
on-focus="_focused" on-blur="_blurred" on-change="_changeHandler" on-keydown="_keydown"
|
||||
on-touchend="_stopPropagation" invisible$="[[ _showMask(masked, value, focused) ]]">
|
||||
<input class="mask" value="[[ _mask(value) ]]" tabindex="-1"
|
||||
invisible$="[[ !_showMask(masked, value, focused) ]]" disabled>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<script src="../../../bower_components/autosize/dist/autosize.js"></script>
|
||||
<script>
|
||||
/* global autosize */
|
||||
(() => {
|
||||
|
||||
let activeInput = null;
|
||||
|
||||
// On touch devices, blur active input when tapping on a non-input
|
||||
document.addEventListener("touchend", () => {
|
||||
if (activeInput) {
|
||||
activeInput.blur();
|
||||
}
|
||||
});
|
||||
|
||||
class PlInput extends padlock.BaseElement {
|
||||
|
||||
static get is() { return "pl-input"; }
|
||||
|
||||
static get activeInput() { return activeInput; }
|
||||
|
||||
static get properties() { return {
|
||||
autosize: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
autocapitalize: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
focused: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
reflectToAttribute: true,
|
||||
readonly: true
|
||||
},
|
||||
invalid: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notifiy: true,
|
||||
reflectToAttribute: true,
|
||||
readonly: true
|
||||
},
|
||||
masked: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
multiline: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
pattern: {
|
||||
type: String,
|
||||
value: null
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
noTab: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
value: ""
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
value: "text"
|
||||
},
|
||||
selectOnFocus: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
value: "",
|
||||
notify: true,
|
||||
observer: "_valueChanged"
|
||||
}
|
||||
}; }
|
||||
|
||||
get inputElement() {
|
||||
return this.root.querySelector(this.multiline ? "textarea" : "input");
|
||||
}
|
||||
|
||||
_domChange() {
|
||||
if (this.autosize && this.multiline && this.inputElement) {
|
||||
autosize(this.inputElement);
|
||||
}
|
||||
setTimeout(() => this._valueChanged(), 50);
|
||||
}
|
||||
|
||||
_stopPropagation(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
_focused(e) {
|
||||
e.stopPropagation();
|
||||
this.focused = true;
|
||||
activeInput = this;
|
||||
this.dispatchEvent(new CustomEvent("focus"));
|
||||
|
||||
if (this.selectOnFocus) {
|
||||
setTimeout(() => this.selectAll(), 10);
|
||||
}
|
||||
}
|
||||
|
||||
_blurred(e) {
|
||||
e.stopPropagation();
|
||||
this.focused = false;
|
||||
if (activeInput === this) {
|
||||
activeInput = null;
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent("blur"));
|
||||
}
|
||||
|
||||
_changeHandler(e) {
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(new CustomEvent("change"));
|
||||
}
|
||||
|
||||
_keydown(e) {
|
||||
if (e.key === "Enter" && !this.multiline) {
|
||||
this.dispatchEvent(new CustomEvent("enter"));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} else if (e.key === "Escape") {
|
||||
this.dispatchEvent(new CustomEvent("escape"));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
_valueChanged() {
|
||||
this.invalid = this.inputElement && !this.inputElement.checkValidity();
|
||||
if (this.autosize && this.multiline) {
|
||||
autosize.update(this.inputElement);
|
||||
}
|
||||
}
|
||||
|
||||
_tabIndex(noTab) {
|
||||
return noTab ? "-1" : "";
|
||||
}
|
||||
|
||||
_showMask() {
|
||||
return this.masked && !!this.value && !this.focused;
|
||||
}
|
||||
|
||||
_mask(value) {
|
||||
return value && value.replace(/[^\n]/g, "\u2022");
|
||||
}
|
||||
|
||||
_computeAutoCapitalize() {
|
||||
return this.autocapitalize ? "" : "off";
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.inputElement.focus();
|
||||
}
|
||||
|
||||
blur() {
|
||||
this.inputElement.blur();
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
try {
|
||||
this.inputElement.setSelectionRange(0, this.value.length);
|
||||
} catch (e) {
|
||||
this.inputElement.select();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.customElements.define(PlInput.is, PlInput);
|
||||
|
||||
padlock.Input = PlInput;
|
||||
|
||||
})();
|
||||
</script>
|
||||
</dom-module>
|
|
@ -0,0 +1,254 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import autosize from '../../../../../node_modules/autosize/src/autosize.js';
|
||||
|
||||
let activeInput = null;
|
||||
|
||||
// On touch devices, blur active input when tapping on a non-input
|
||||
document.addEventListener("touchend", () => {
|
||||
if (activeInput) {
|
||||
activeInput.blur();
|
||||
}
|
||||
});
|
||||
|
||||
class PlInput extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host(:not([multiline])) {
|
||||
padding: 0 10px;
|
||||
height: var(--row-height);
|
||||
}
|
||||
|
||||
input {
|
||||
box-sizing: border-box;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
text-align: inherit;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
text-shadow: inherit;
|
||||
color: inherit;
|
||||
opacity: 0.5;
|
||||
@apply --pl-input-placeholder;
|
||||
}
|
||||
|
||||
.mask {
|
||||
@apply --fullbleed;
|
||||
pointer-events: none;
|
||||
font-size: 150%;
|
||||
line-height: 22px;
|
||||
letter-spacing: -4.5px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
input[disabled], textarea[disabled] {
|
||||
opacity: 1;
|
||||
-webkit-text-fill-color: currentColor;
|
||||
}
|
||||
|
||||
input[invisible], textarea[invisible] {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template is="dom-if" if="[[ multiline ]]" on-dom-change="_domChange">
|
||||
<textarea id="input" value="{{ value::input }}" placeholder\$="[[ placeholder ]]" readonly\$="[[ readonly ]]" rows="1" autocomplete="off" spellcheck="false" autocapitalize\$="[[ _computeAutoCapitalize(autocapitalize) ]]" autocorrect="off" on-focus="_focused" on-blur="_blurred" on-change="_changeHandler" on-keydown="_keydown" on-touchend="_stopPropagation" tabindex\$="[[ _tabIndex(noTab) ]]" invisible\$="[[ _showMask(masked, value, focused) ]]" disabled\$="[[ disabled ]]"></textarea>
|
||||
<textarea class="mask" value="[[ _mask(value) ]]" tabindex="-1" invisible\$="[[ !_showMask(masked, value, focused) ]]" disabled=""></textarea>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[ !multiline ]]" on-dom-change="_domChange">
|
||||
<input id="input" value="{{ value::input }}" tabindex\$="[[ _tabIndex(noTab) ]]" autocomplete="off" spellcheck="false" autocapitalize\$="[[ _computeAutoCapitalize(autocapitalize) ]]" autocorrect="off" type\$="[[ type ]]" placeholder\$="[[ placeholder ]]" readonly\$="[[ readonly ]]" required\$="[[ required ]]" pattern\$="[[ pattern ]]" disabled\$="[[ disabled ]]" on-focus="_focused" on-blur="_blurred" on-change="_changeHandler" on-keydown="_keydown" on-touchend="_stopPropagation" invisible\$="[[ _showMask(masked, value, focused) ]]">
|
||||
<input class="mask" value="[[ _mask(value) ]]" tabindex="-1" invisible\$="[[ !_showMask(masked, value, focused) ]]" disabled="">
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-input"; }
|
||||
|
||||
static get activeInput() { return activeInput; }
|
||||
|
||||
static get properties() { return {
|
||||
autosize: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
autocapitalize: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
focused: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
reflectToAttribute: true,
|
||||
readonly: true
|
||||
},
|
||||
invalid: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notifiy: true,
|
||||
reflectToAttribute: true,
|
||||
readonly: true
|
||||
},
|
||||
masked: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
multiline: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
pattern: {
|
||||
type: String,
|
||||
value: null
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
noTab: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
value: ""
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
value: "text"
|
||||
},
|
||||
selectOnFocus: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
value: "",
|
||||
notify: true,
|
||||
observer: "_valueChanged"
|
||||
}
|
||||
}; }
|
||||
|
||||
get inputElement() {
|
||||
return this.root.querySelector(this.multiline ? "textarea" : "input");
|
||||
}
|
||||
|
||||
_domChange() {
|
||||
if (this.autosize && this.multiline && this.inputElement) {
|
||||
autosize(this.inputElement);
|
||||
}
|
||||
setTimeout(() => this._valueChanged(), 50);
|
||||
}
|
||||
|
||||
_stopPropagation(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
_focused(e) {
|
||||
e.stopPropagation();
|
||||
this.focused = true;
|
||||
activeInput = this;
|
||||
this.dispatchEvent(new CustomEvent("focus"));
|
||||
|
||||
if (this.selectOnFocus) {
|
||||
setTimeout(() => this.selectAll(), 10);
|
||||
}
|
||||
}
|
||||
|
||||
_blurred(e) {
|
||||
e.stopPropagation();
|
||||
this.focused = false;
|
||||
if (activeInput === this) {
|
||||
activeInput = null;
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent("blur"));
|
||||
}
|
||||
|
||||
_changeHandler(e) {
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(new CustomEvent("change"));
|
||||
}
|
||||
|
||||
_keydown(e) {
|
||||
if (e.key === "Enter" && !this.multiline) {
|
||||
this.dispatchEvent(new CustomEvent("enter"));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} else if (e.key === "Escape") {
|
||||
this.dispatchEvent(new CustomEvent("escape"));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
_valueChanged() {
|
||||
this.invalid = this.inputElement && !this.inputElement.checkValidity();
|
||||
if (this.autosize && this.multiline) {
|
||||
autosize.update(this.inputElement);
|
||||
}
|
||||
}
|
||||
|
||||
_tabIndex(noTab) {
|
||||
return noTab ? "-1" : "";
|
||||
}
|
||||
|
||||
_showMask() {
|
||||
return this.masked && !!this.value && !this.focused;
|
||||
}
|
||||
|
||||
_mask(value) {
|
||||
return value && value.replace(/[^\n]/g, "\u2022");
|
||||
}
|
||||
|
||||
_computeAutoCapitalize() {
|
||||
return this.autocapitalize ? "" : "off";
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.inputElement.focus();
|
||||
}
|
||||
|
||||
blur() {
|
||||
this.inputElement.blur();
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
try {
|
||||
this.inputElement.setSelectionRange(0, this.value.length);
|
||||
} catch (e) {
|
||||
this.inputElement.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(PlInput.is, PlInput);
|
||||
|
||||
padlock.Input = PlInput;
|
|
@ -1,481 +0,0 @@
|
|||
<link rel="import" href="../../../bower_components/iron-list/iron-list.html">
|
||||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../animation/animation.html">
|
||||
<link rel="import" href="../data/data.html">
|
||||
<link rel="import" href="../dialog/dialog-mixin.html">
|
||||
<link rel="import" href="../export/export-dialog.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
<link rel="import" href="../input/input.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="../record-item/record-item.html">
|
||||
<link rel="import" href="../sync/sync.html">
|
||||
|
||||
<dom-module id="pl-list-view">
|
||||
<template>
|
||||
|
||||
<style include="shared">
|
||||
:host {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: var(--color-quaternary);
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
flex: 1;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.filter-input:not([active]) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header {
|
||||
--color-background: var(--color-primary);
|
||||
--color-foreground: var(--color-tertiary);
|
||||
--color-highlight: var(--color-secondary);
|
||||
color: var(--color-foreground);
|
||||
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
|
||||
border-bottom: none;
|
||||
background: linear-gradient(90deg, #59c6ff 0%, #077cb9 100%);
|
||||
}
|
||||
|
||||
header pl-icon[icon=logo] {
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
.empty {
|
||||
@apply --fullbleed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@apply --fullbleed;
|
||||
top: var(--row-height);
|
||||
z-index: 1;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
padding: 15px 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
background: var(--color-background);
|
||||
border-bottom: solid 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.empty-message::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
right: 18px;
|
||||
margin: 0 auto;
|
||||
transform: rotate(45deg);
|
||||
background: var(--color-background);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cloud-icon-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
header pl-icon.syncing-icon {
|
||||
position: absolute;
|
||||
font-size: 55%;
|
||||
top: 1px;
|
||||
left: 0px;
|
||||
color: var(--color-highlight);
|
||||
text-shadow: none;
|
||||
animation: spin 1s infinite;
|
||||
transform-origin: center 49%;
|
||||
}
|
||||
|
||||
.current-section {
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
padding: 0 15px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-size: var(--font-size-tiny);
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: var(--color-foreground);
|
||||
color: var(--color-background);
|
||||
}
|
||||
|
||||
.current-section pl-icon {
|
||||
float: right;
|
||||
height: 35px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.section-separator {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
padding: 0 15px;
|
||||
font-size: var(--font-size-tiny);
|
||||
font-weight: bold;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
iron-list {
|
||||
margin-top: -35px;
|
||||
}
|
||||
|
||||
#sectionSelector {
|
||||
--row-height: 40px;
|
||||
--font-size-default: var(--font-size-small);
|
||||
--pl-dialog-inner: {
|
||||
background: var(--color-secondary);
|
||||
};
|
||||
}
|
||||
|
||||
.multi-select {
|
||||
background: var(--color-background);
|
||||
height: var(--row-height);
|
||||
border-top: solid 1px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.multi-select > pl-icon {
|
||||
width: var(--row-height);
|
||||
height: var(--row-height);
|
||||
}
|
||||
|
||||
.multi-select-count {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<header hidden$="[[ multiSelect ]]">
|
||||
<pl-icon icon="menu" class="tap" on-click="_toggleMenu" hidden$="[[ filterActive ]]"></pl-icon>
|
||||
<pl-input id="filterInput" class="filter-input tap" placeholder="[[ $l('Type To Search') ]]" active$="[[ filterActive ]]"
|
||||
value="{{ filterString }}" on-escape="clearFilter" no-tab></pl-input>
|
||||
<pl-icon icon="add" class="tap" on-click="_newRecord" hidden$="[[ filterActive ]]"></pl-icon>
|
||||
<pl-icon icon="cancel" class="tap" on-click="clearFilter" hidden$="[[ !filterActive ]]"></pl-icon>
|
||||
</header>
|
||||
|
||||
<header hidden$="[[ !multiSelect ]]">
|
||||
<pl-icon icon="cancel" class="tap" on-click="_clearMultiSelection"></pl-icon>
|
||||
<pl-icon icon="checked" class="tap" on-click="_selectAll"></pl-icon>
|
||||
<div class="multi-select-count"><div>[[ _multiSelectLabel(selectedRecords.length) ]]</div></div>
|
||||
<pl-icon icon="delete" class="tap" on-click="_deleteSelected"></pl-icon>
|
||||
<pl-icon icon="share" class="tap" on-click="_shareSelected"></pl-icon>
|
||||
</header>
|
||||
|
||||
<div class="current-section tap" on-click="_selectSection" hidden$="[[ _isEmpty(records.length) ]]">
|
||||
<pl-icon icon="dropdown" class="float-right"></pl-icon>
|
||||
<div>[[ _currentSection ]]</div>
|
||||
</div>
|
||||
|
||||
<main id="main">
|
||||
|
||||
<iron-list id="list"
|
||||
mutable-data
|
||||
scroll-target="main"
|
||||
multi-selection="[[ multiSelect ]]"
|
||||
hidden$="[[ _isEmpty(records.length) ]]"
|
||||
items="[[ records ]]"
|
||||
selection-enabled
|
||||
selected-item="{{ selectedRecord }}"
|
||||
selected-items="{{ selectedRecords }}">
|
||||
<template>
|
||||
<div>
|
||||
<div class="section-header" hidden$="[[ !_firstInSection(records, index) ]]">
|
||||
<div>[[ _sectionHeader(index, records) ]]</div>
|
||||
<div class="spacer"></div>
|
||||
<div>[[ _sectionHeader(index, records) ]]</div>
|
||||
</div>
|
||||
<pl-record-item record="[[ item ]]" selected$="[[ selected ]]" tabindex$="[[ tabIndex ]]"
|
||||
multi-select="[[ multiSelect ]]" on-multi-select="_recordMultiSelect"></pl-record-item>
|
||||
<div class="section-separator" hidden$="[[ !_lastInSection(records, index) ]]"></div>
|
||||
</div>
|
||||
</template>
|
||||
</iron-list>
|
||||
|
||||
</main>
|
||||
|
||||
<div hidden$="[[ !_isEmpty(records.length) ]]" class="empty">
|
||||
<div class="empty-message">
|
||||
[[ $l("You don't have any data yet! Start by creating your first record!") ]]
|
||||
</div>
|
||||
<div class="spacer tiles-2"></div>
|
||||
</div>
|
||||
|
||||
<pl-dialog-options id="sectionSelector" on-dialog-open="_stopPropagation"
|
||||
on-dialog-close="_stopPropagation"></pl-dialog-options>
|
||||
|
||||
<div class="rounded-corners"></div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
const { LocaleMixin, DataMixin, SyncMixin, BaseElement, DialogMixin, AnimationMixin } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
|
||||
class ListView extends applyMixins(
|
||||
BaseElement,
|
||||
LocaleMixin,
|
||||
SyncMixin,
|
||||
DataMixin,
|
||||
DialogMixin,
|
||||
AnimationMixin
|
||||
) {
|
||||
|
||||
static get is() { return "pl-list-view"; }
|
||||
|
||||
static get properties() { return {
|
||||
_currentSection: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
animationOptions: {
|
||||
type: Object,
|
||||
value: { clear: true }
|
||||
},
|
||||
filterActive: {
|
||||
type: Boolean,
|
||||
computed: "_filterActive(filterString)"
|
||||
},
|
||||
multiSelect: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
selectedRecord: {
|
||||
type: Object,
|
||||
notify: true
|
||||
},
|
||||
selectedRecords: {
|
||||
type: Array,
|
||||
value: () => [],
|
||||
notify: true
|
||||
}
|
||||
|
||||
}; }
|
||||
|
||||
static get observers() { return [
|
||||
"_fixScroll(records)",
|
||||
"_scrollToSelected(records, selectedRecord)",
|
||||
"_updateCurrentSection(records)",
|
||||
"_selectedCountChanged(selectedRecords.length)"
|
||||
]; }
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
window.addEventListener("keydown", (e) => {
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
this.$.list.focusItem(this.$.list.firstVisibleIndex);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
this.$.list.focusItem(this.$.list.lastVisibleIndex);
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.$.list.addEventListener("keydown", (e) => e.stopPropagation());
|
||||
this.$.main.addEventListener("scroll", () => this._updateCurrentSection());
|
||||
this.listen("data-loaded", () => { this.animateRecords(600); });
|
||||
this.listen("sync-success", (e) => {
|
||||
if (!e.detail || !e.detail.auto) {
|
||||
this.animateRecords();
|
||||
}
|
||||
});
|
||||
this.listen("data-imported", () => this.animateRecords());
|
||||
}
|
||||
|
||||
dataUnloaded() {
|
||||
this._clearFilter();
|
||||
}
|
||||
|
||||
select(record) {
|
||||
this.$.list.selectItem(record);
|
||||
}
|
||||
|
||||
deselect() {
|
||||
this.$.list.clearSelection();
|
||||
}
|
||||
|
||||
recordCreated(record) {
|
||||
this.select(record);
|
||||
}
|
||||
|
||||
_isEmpty() {
|
||||
return !this.collection.records.filter((r) => !r.removed).length;
|
||||
}
|
||||
|
||||
_openMenu() {
|
||||
this.dispatchEvent(new CustomEvent("open-menu"));
|
||||
}
|
||||
|
||||
_newRecord() {
|
||||
this.createRecord();
|
||||
}
|
||||
|
||||
_filterActive() {
|
||||
return this.filterString !== "";
|
||||
}
|
||||
|
||||
_clearFilter() {
|
||||
this.set("filterString", "");
|
||||
}
|
||||
|
||||
_toggleMenu() {
|
||||
this.dispatchEvent(new CustomEvent("toggle-menu"));
|
||||
}
|
||||
|
||||
_openSettings() {
|
||||
this.dispatchEvent(new CustomEvent("open-settings"));
|
||||
}
|
||||
|
||||
_openCloudView() {
|
||||
this.dispatchEvent(new CustomEvent("open-cloud-view"));
|
||||
}
|
||||
|
||||
_scrollToSelected() {
|
||||
const l = this.$.list;
|
||||
const i = l.items.indexOf(this.selectedRecord);
|
||||
if (i !== -1 && (i < l.firstVisibleIndex || i > l.lastVisibleIndex)) {
|
||||
// Scroll to item before the selected one so that selected
|
||||
// item is more towards the middle of the list
|
||||
l.scrollToIndex(Math.max(i - 1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
_fixScroll() {
|
||||
// Workaround for list losing scrollability on iOS after resetting filter
|
||||
padlock.platform.isIOS().then((yes) => {
|
||||
if (yes) {
|
||||
this.$.main.style.overflow = "hidden";
|
||||
setTimeout(() => this.$.main.style.overflow = "auto", 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_firstInSection(records, index) {
|
||||
return index === 0 || this._sectionHeader(index - 1) !== this._sectionHeader(index);
|
||||
}
|
||||
|
||||
_lastInSection(records, index) {
|
||||
return this._sectionHeader(index + 1) !== this._sectionHeader(index);
|
||||
}
|
||||
|
||||
_sectionHeader(index) {
|
||||
const record = this.records[index];
|
||||
return index < this._recentCount ? $l("Recently Used") :
|
||||
record && record.name[0] && record.name[0].toUpperCase() || $l("No Name");
|
||||
}
|
||||
|
||||
_updateCurrentSection() {
|
||||
this._currentSection = this._sectionHeader(this.$.list.firstVisibleIndex);
|
||||
}
|
||||
|
||||
_selectSection() {
|
||||
const sections = Array.from(this.records.reduce((s, r, i) => s.add(this._sectionHeader(i)), new Set()));
|
||||
if (sections.length > 1) {
|
||||
this.$.sectionSelector.choose("", sections)
|
||||
.then((i) => {
|
||||
const record = this.records.find((r, j) => this._sectionHeader(j) === sections[i]);
|
||||
this.$.list.scrollToItem(record);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
animateRecords(delay = 100) {
|
||||
const m4e = (e) => this.$.list.modelForElement(e);
|
||||
|
||||
this.$.list.style.opacity = 0;
|
||||
setTimeout(() => {
|
||||
const first = this.$.list.firstVisibleIndex;
|
||||
const last = this.$.list.lastVisibleIndex + 1;
|
||||
const elements = Array.from(this.root.querySelectorAll("pl-record-item, .section-header"));
|
||||
const animated = elements
|
||||
.filter((el) => m4e(el).index >= first && m4e(el).index <= last)
|
||||
.sort((a, b) => m4e(a).index - m4e(b).index);
|
||||
|
||||
this.animateCascade(animated);
|
||||
this.$.list.style.opacity = 1;
|
||||
}, delay);
|
||||
}
|
||||
|
||||
_stopPropagation(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
_selectedCountChanged() {
|
||||
const count = this.selectedRecords && this.selectedRecords.length;
|
||||
if (this._lastSelectCount && !count) {
|
||||
this.multiSelect = false;
|
||||
}
|
||||
this._lastSelectCount = count;
|
||||
}
|
||||
|
||||
_recordMultiSelect() {
|
||||
this.multiSelect = true;
|
||||
}
|
||||
|
||||
_clearMultiSelection() {
|
||||
this.$.list.clearSelection();
|
||||
this.multiSelect = false;
|
||||
}
|
||||
|
||||
_selectAll() {
|
||||
this.records.forEach((r) => this.$.list.selectItem(r));
|
||||
}
|
||||
|
||||
_shareSelected() {
|
||||
const exportDialog = this.getSingleton("pl-export-dialog");
|
||||
exportDialog.export(this.selectedRecords);
|
||||
}
|
||||
|
||||
_deleteSelected() {
|
||||
this.confirm($l(
|
||||
"Are you sure you want to delete these records? " +
|
||||
"This action can not be undone!",
|
||||
this.selectedRecords.length
|
||||
), $l("Delete {0} Records", this.selectedRecords.length))
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteRecords(this.selectedRecords);
|
||||
this.multiSelect = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_multiSelectLabel(count) {
|
||||
return count ? $l("{0} records selected", count) : $l("tap to select");
|
||||
}
|
||||
|
||||
search() {
|
||||
this.$.filterInput.focus();
|
||||
}
|
||||
|
||||
clearFilter() {
|
||||
this.$.filterInput.value = "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.customElements.define(ListView.is, ListView);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,459 @@
|
|||
import '../../../../../node_modules/@polymer/iron-list/iron-list.js';
|
||||
import '../../styles/shared.js';
|
||||
import '../animation/animation.js';
|
||||
import '../data/data.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../export/export-dialog.js';
|
||||
import '../base/base.js';
|
||||
import '../icon/icon.js';
|
||||
import '../input/input.js';
|
||||
import '../locale/locale.js';
|
||||
import '../record-item/record-item.js';
|
||||
import '../sync/sync.js';
|
||||
|
||||
const { LocaleMixin, DataMixin, SyncMixin, BaseElement, DialogMixin, AnimationMixin } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
|
||||
class ListView extends applyMixins(
|
||||
BaseElement,
|
||||
LocaleMixin,
|
||||
SyncMixin,
|
||||
DataMixin,
|
||||
DialogMixin,
|
||||
AnimationMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: var(--color-quaternary);
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
flex: 1;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.filter-input:not([active]) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header {
|
||||
--color-background: var(--color-primary);
|
||||
--color-foreground: var(--color-tertiary);
|
||||
--color-highlight: var(--color-secondary);
|
||||
color: var(--color-foreground);
|
||||
text-shadow: rgba(0, 0, 0, 0.2) 0 2px 0;
|
||||
border-bottom: none;
|
||||
background: linear-gradient(90deg, #59c6ff 0%, #077cb9 100%);
|
||||
}
|
||||
|
||||
header pl-icon[icon=logo] {
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
.empty {
|
||||
@apply --fullbleed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@apply --fullbleed;
|
||||
top: var(--row-height);
|
||||
z-index: 1;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
padding: 15px 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
background: var(--color-background);
|
||||
border-bottom: solid 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.empty-message::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
right: 18px;
|
||||
margin: 0 auto;
|
||||
transform: rotate(45deg);
|
||||
background: var(--color-background);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cloud-icon-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
header pl-icon.syncing-icon {
|
||||
position: absolute;
|
||||
font-size: 55%;
|
||||
top: 1px;
|
||||
left: 0px;
|
||||
color: var(--color-highlight);
|
||||
text-shadow: none;
|
||||
animation: spin 1s infinite;
|
||||
transform-origin: center 49%;
|
||||
}
|
||||
|
||||
.current-section {
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
padding: 0 15px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-size: var(--font-size-tiny);
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: var(--color-foreground);
|
||||
color: var(--color-background);
|
||||
}
|
||||
|
||||
.current-section pl-icon {
|
||||
float: right;
|
||||
height: 35px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.section-separator {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
padding: 0 15px;
|
||||
font-size: var(--font-size-tiny);
|
||||
font-weight: bold;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
iron-list {
|
||||
margin-top: -35px;
|
||||
}
|
||||
|
||||
#sectionSelector {
|
||||
--row-height: 40px;
|
||||
--font-size-default: var(--font-size-small);
|
||||
--pl-dialog-inner: {
|
||||
background: var(--color-secondary);
|
||||
};
|
||||
}
|
||||
|
||||
.multi-select {
|
||||
background: var(--color-background);
|
||||
height: var(--row-height);
|
||||
border-top: solid 1px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.multi-select > pl-icon {
|
||||
width: var(--row-height);
|
||||
height: var(--row-height);
|
||||
}
|
||||
|
||||
.multi-select-count {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<header hidden\$="[[ multiSelect ]]">
|
||||
<pl-icon icon="menu" class="tap" on-click="_toggleMenu" hidden\$="[[ filterActive ]]"></pl-icon>
|
||||
<pl-input id="filterInput" class="filter-input tap" placeholder="[[ \$l('Type To Search') ]]" active\$="[[ filterActive ]]" value="{{ filterString }}" on-escape="clearFilter" no-tab=""></pl-input>
|
||||
<pl-icon icon="add" class="tap" on-click="_newRecord" hidden\$="[[ filterActive ]]"></pl-icon>
|
||||
<pl-icon icon="cancel" class="tap" on-click="clearFilter" hidden\$="[[ !filterActive ]]"></pl-icon>
|
||||
</header>
|
||||
|
||||
<header hidden\$="[[ !multiSelect ]]">
|
||||
<pl-icon icon="cancel" class="tap" on-click="_clearMultiSelection"></pl-icon>
|
||||
<pl-icon icon="checked" class="tap" on-click="_selectAll"></pl-icon>
|
||||
<div class="multi-select-count"><div>[[ _multiSelectLabel(selectedRecords.length) ]]</div></div>
|
||||
<pl-icon icon="delete" class="tap" on-click="_deleteSelected"></pl-icon>
|
||||
<pl-icon icon="share" class="tap" on-click="_shareSelected"></pl-icon>
|
||||
</header>
|
||||
|
||||
<div class="current-section tap" on-click="_selectSection" hidden\$="[[ _isEmpty(records.length) ]]">
|
||||
<pl-icon icon="dropdown" class="float-right"></pl-icon>
|
||||
<div>[[ _currentSection ]]</div>
|
||||
</div>
|
||||
|
||||
<main id="main">
|
||||
|
||||
<iron-list id="list" mutable-data="" scroll-target="main" multi-selection="[[ multiSelect ]]" hidden\$="[[ _isEmpty(records.length) ]]" items="[[ records ]]" selection-enabled="" selected-item="{{ selectedRecord }}" selected-items="{{ selectedRecords }}">
|
||||
<template>
|
||||
<div>
|
||||
<div class="section-header" hidden\$="[[ !_firstInSection(records, index) ]]">
|
||||
<div>[[ _sectionHeader(index, records) ]]</div>
|
||||
<div class="spacer"></div>
|
||||
<div>[[ _sectionHeader(index, records) ]]</div>
|
||||
</div>
|
||||
<pl-record-item record="[[ item ]]" selected\$="[[ selected ]]" tabindex\$="[[ tabIndex ]]" multi-select="[[ multiSelect ]]" on-multi-select="_recordMultiSelect"></pl-record-item>
|
||||
<div class="section-separator" hidden\$="[[ !_lastInSection(records, index) ]]"></div>
|
||||
</div>
|
||||
</template>
|
||||
</iron-list>
|
||||
|
||||
</main>
|
||||
|
||||
<div hidden\$="[[ !_isEmpty(records.length) ]]" class="empty">
|
||||
<div class="empty-message">
|
||||
[[ \$l("You don't have any data yet! Start by creating your first record!") ]]
|
||||
</div>
|
||||
<div class="spacer tiles-2"></div>
|
||||
</div>
|
||||
|
||||
<pl-dialog-options id="sectionSelector" on-dialog-open="_stopPropagation" on-dialog-close="_stopPropagation"></pl-dialog-options>
|
||||
|
||||
<div class="rounded-corners"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-list-view"; }
|
||||
|
||||
static get properties() { return {
|
||||
_currentSection: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
animationOptions: {
|
||||
type: Object,
|
||||
value: { clear: true }
|
||||
},
|
||||
filterActive: {
|
||||
type: Boolean,
|
||||
computed: "_filterActive(filterString)"
|
||||
},
|
||||
multiSelect: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
selectedRecord: {
|
||||
type: Object,
|
||||
notify: true
|
||||
},
|
||||
selectedRecords: {
|
||||
type: Array,
|
||||
value: () => [],
|
||||
notify: true
|
||||
}
|
||||
|
||||
}; }
|
||||
|
||||
static get observers() { return [
|
||||
"_fixScroll(records)",
|
||||
"_scrollToSelected(records, selectedRecord)",
|
||||
"_updateCurrentSection(records)",
|
||||
"_selectedCountChanged(selectedRecords.length)"
|
||||
]; }
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
window.addEventListener("keydown", (e) => {
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
this.$.list.focusItem(this.$.list.firstVisibleIndex);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
this.$.list.focusItem(this.$.list.lastVisibleIndex);
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.$.list.addEventListener("keydown", (e) => e.stopPropagation());
|
||||
this.$.main.addEventListener("scroll", () => this._updateCurrentSection());
|
||||
this.listen("data-loaded", () => { this.animateRecords(600); });
|
||||
this.listen("sync-success", (e) => {
|
||||
if (!e.detail || !e.detail.auto) {
|
||||
this.animateRecords();
|
||||
}
|
||||
});
|
||||
this.listen("data-imported", () => this.animateRecords());
|
||||
}
|
||||
|
||||
dataUnloaded() {
|
||||
this._clearFilter();
|
||||
}
|
||||
|
||||
select(record) {
|
||||
this.$.list.selectItem(record);
|
||||
}
|
||||
|
||||
deselect() {
|
||||
this.$.list.clearSelection();
|
||||
}
|
||||
|
||||
recordCreated(record) {
|
||||
this.select(record);
|
||||
}
|
||||
|
||||
_isEmpty() {
|
||||
return !this.collection.records.filter((r) => !r.removed).length;
|
||||
}
|
||||
|
||||
_openMenu() {
|
||||
this.dispatchEvent(new CustomEvent("open-menu"));
|
||||
}
|
||||
|
||||
_newRecord() {
|
||||
this.createRecord();
|
||||
}
|
||||
|
||||
_filterActive() {
|
||||
return this.filterString !== "";
|
||||
}
|
||||
|
||||
_clearFilter() {
|
||||
this.set("filterString", "");
|
||||
}
|
||||
|
||||
_toggleMenu() {
|
||||
this.dispatchEvent(new CustomEvent("toggle-menu"));
|
||||
}
|
||||
|
||||
_openSettings() {
|
||||
this.dispatchEvent(new CustomEvent("open-settings"));
|
||||
}
|
||||
|
||||
_openCloudView() {
|
||||
this.dispatchEvent(new CustomEvent("open-cloud-view"));
|
||||
}
|
||||
|
||||
_scrollToSelected() {
|
||||
const l = this.$.list;
|
||||
const i = l.items.indexOf(this.selectedRecord);
|
||||
if (i !== -1 && (i < l.firstVisibleIndex || i > l.lastVisibleIndex)) {
|
||||
// Scroll to item before the selected one so that selected
|
||||
// item is more towards the middle of the list
|
||||
l.scrollToIndex(Math.max(i - 1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
_fixScroll() {
|
||||
// Workaround for list losing scrollability on iOS after resetting filter
|
||||
padlock.platform.isIOS().then((yes) => {
|
||||
if (yes) {
|
||||
this.$.main.style.overflow = "hidden";
|
||||
setTimeout(() => this.$.main.style.overflow = "auto", 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_firstInSection(records, index) {
|
||||
return index === 0 || this._sectionHeader(index - 1) !== this._sectionHeader(index);
|
||||
}
|
||||
|
||||
_lastInSection(records, index) {
|
||||
return this._sectionHeader(index + 1) !== this._sectionHeader(index);
|
||||
}
|
||||
|
||||
_sectionHeader(index) {
|
||||
const record = this.records[index];
|
||||
return index < this._recentCount ? $l("Recently Used") :
|
||||
record && record.name[0] && record.name[0].toUpperCase() || $l("No Name");
|
||||
}
|
||||
|
||||
_updateCurrentSection() {
|
||||
this._currentSection = this._sectionHeader(this.$.list.firstVisibleIndex);
|
||||
}
|
||||
|
||||
_selectSection() {
|
||||
const sections = Array.from(this.records.reduce((s, r, i) => s.add(this._sectionHeader(i)), new Set()));
|
||||
if (sections.length > 1) {
|
||||
this.$.sectionSelector.choose("", sections)
|
||||
.then((i) => {
|
||||
const record = this.records.find((r, j) => this._sectionHeader(j) === sections[i]);
|
||||
this.$.list.scrollToItem(record);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
animateRecords(delay = 100) {
|
||||
const m4e = (e) => this.$.list.modelForElement(e);
|
||||
|
||||
this.$.list.style.opacity = 0;
|
||||
setTimeout(() => {
|
||||
const first = this.$.list.firstVisibleIndex;
|
||||
const last = this.$.list.lastVisibleIndex + 1;
|
||||
const elements = Array.from(this.root.querySelectorAll("pl-record-item, .section-header"));
|
||||
const animated = elements
|
||||
.filter((el) => m4e(el).index >= first && m4e(el).index <= last)
|
||||
.sort((a, b) => m4e(a).index - m4e(b).index);
|
||||
|
||||
this.animateCascade(animated);
|
||||
this.$.list.style.opacity = 1;
|
||||
}, delay);
|
||||
}
|
||||
|
||||
_stopPropagation(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
_selectedCountChanged() {
|
||||
const count = this.selectedRecords && this.selectedRecords.length;
|
||||
if (this._lastSelectCount && !count) {
|
||||
this.multiSelect = false;
|
||||
}
|
||||
this._lastSelectCount = count;
|
||||
}
|
||||
|
||||
_recordMultiSelect() {
|
||||
this.multiSelect = true;
|
||||
}
|
||||
|
||||
_clearMultiSelection() {
|
||||
this.$.list.clearSelection();
|
||||
this.multiSelect = false;
|
||||
}
|
||||
|
||||
_selectAll() {
|
||||
this.records.forEach((r) => this.$.list.selectItem(r));
|
||||
}
|
||||
|
||||
_shareSelected() {
|
||||
const exportDialog = this.getSingleton("pl-export-dialog");
|
||||
exportDialog.export(this.selectedRecords);
|
||||
}
|
||||
|
||||
_deleteSelected() {
|
||||
this.confirm($l(
|
||||
"Are you sure you want to delete these records? " +
|
||||
"This action can not be undone!",
|
||||
this.selectedRecords.length
|
||||
), $l("Delete {0} Records", this.selectedRecords.length))
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteRecords(this.selectedRecords);
|
||||
this.multiSelect = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_multiSelectLabel(count) {
|
||||
return count ? $l("{0} records selected", count) : $l("tap to select");
|
||||
}
|
||||
|
||||
search() {
|
||||
this.$.filterInput.focus();
|
||||
}
|
||||
|
||||
clearFilter() {
|
||||
this.$.filterInput.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(ListView.is, ListView);
|
|
@ -1,126 +0,0 @@
|
|||
<link rel="import" href="../../../bower_components/paper-spinner/paper-spinner-lite.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
|
||||
<dom-module id="pl-loading-button">
|
||||
|
||||
<template>
|
||||
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button > * {
|
||||
@apply --absolute-center;
|
||||
transition: transform 0.2s cubic-bezier(1, -0.3, 0, 1.3), opacity 0.2s;
|
||||
}
|
||||
|
||||
button > .label {
|
||||
@appy --fullbleed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button.loading .label, button.success .label, button.fail .label,
|
||||
button:not(.loading) .spinner,
|
||||
button:not(.success) .icon-success,
|
||||
button:not(.fail) .icon-fail {
|
||||
opacity: 0.5;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
button pl-icon {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
paper-spinner-lite {
|
||||
line-height: normal;
|
||||
--paper-spinner-color: currentColor;
|
||||
--paper-spinner-stroke-width: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button type="button" class$="[[ _buttonClass(_loading, _success, _fail) ]]"
|
||||
tabindex$="[[ _tabIndex(noTab) ]]">
|
||||
<div class="label"><slot></slot></div>
|
||||
<paper-spinner-lite active="[[ _loading ]]" class="spinner"></paper-spinner-lite>
|
||||
<pl-icon icon="check" class="icon-success"></pl-icon>
|
||||
<pl-icon icon="cancel" class="icon-fail"></pl-icon>
|
||||
</button>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
class LoadingButton extends padlock.BaseElement {
|
||||
|
||||
static get is() { return "pl-loading-button"; }
|
||||
|
||||
static get properties() { return {
|
||||
_fail: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
_loading: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
_success: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
noTab: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
|
||||
start() {
|
||||
clearTimeout(this._stopTimeout);
|
||||
this._success = this._fail = false;
|
||||
this._loading = true;
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._success = this._fail = this._loading = false;
|
||||
}
|
||||
|
||||
success() {
|
||||
this._loading = this._fail = false;
|
||||
this._success = true;
|
||||
this._stopTimeout = setTimeout(() => this.stop(), 1000);
|
||||
}
|
||||
|
||||
fail() {
|
||||
this._loading = this._success = false;
|
||||
this._fail = true;
|
||||
this._stopTimeout = setTimeout(() => this.stop(), 1000);
|
||||
}
|
||||
|
||||
_tabIndex(noTab) {
|
||||
return noTab ? "-1" : "";
|
||||
}
|
||||
|
||||
_buttonClass() {
|
||||
return this._loading ? "loading" : this._success ? "success" : this._fail ? "fail" : "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.customElements.define(LoadingButton.is, LoadingButton);
|
||||
|
||||
})();
|
||||
</script>
|
||||
</dom-module>
|
|
@ -0,0 +1,114 @@
|
|||
import '../../../../../node_modules/@polymer/paper-spinner/paper-spinner-lite.js';
|
||||
import '../base/base.js';
|
||||
import '../icon/icon.js';
|
||||
|
||||
class LoadingButton extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button > * {
|
||||
@apply --absolute-center;
|
||||
transition: transform 0.2s cubic-bezier(1, -0.3, 0, 1.3), opacity 0.2s;
|
||||
}
|
||||
|
||||
button > .label {
|
||||
@appy --fullbleed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button.loading .label, button.success .label, button.fail .label,
|
||||
button:not(.loading) .spinner,
|
||||
button:not(.success) .icon-success,
|
||||
button:not(.fail) .icon-fail {
|
||||
opacity: 0.5;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
button pl-icon {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
paper-spinner-lite {
|
||||
line-height: normal;
|
||||
--paper-spinner-color: currentColor;
|
||||
--paper-spinner-stroke-width: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button type="button" class\$="[[ _buttonClass(_loading, _success, _fail) ]]" tabindex\$="[[ _tabIndex(noTab) ]]">
|
||||
<div class="label"><slot></slot></div>
|
||||
<paper-spinner-lite active="[[ _loading ]]" class="spinner"></paper-spinner-lite>
|
||||
<pl-icon icon="check" class="icon-success"></pl-icon>
|
||||
<pl-icon icon="cancel" class="icon-fail"></pl-icon>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-loading-button"; }
|
||||
|
||||
static get properties() { return {
|
||||
_fail: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
_loading: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
_success: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
noTab: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
|
||||
start() {
|
||||
clearTimeout(this._stopTimeout);
|
||||
this._success = this._fail = false;
|
||||
this._loading = true;
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._success = this._fail = this._loading = false;
|
||||
}
|
||||
|
||||
success() {
|
||||
this._loading = this._fail = false;
|
||||
this._success = true;
|
||||
this._stopTimeout = setTimeout(() => this.stop(), 1000);
|
||||
}
|
||||
|
||||
fail() {
|
||||
this._loading = this._success = false;
|
||||
this._fail = true;
|
||||
this._stopTimeout = setTimeout(() => this.stop(), 1000);
|
||||
}
|
||||
|
||||
_tabIndex(noTab) {
|
||||
return noTab ? "-1" : "";
|
||||
}
|
||||
|
||||
_buttonClass() {
|
||||
return this._loading ? "loading" : this._success ? "success" : this._fail ? "fail" : "";
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(LoadingButton.is, LoadingButton);
|
|
@ -1,46 +0,0 @@
|
|||
<link rel="import" href="../base/base.html">
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
const { resolveLanguage } = padlock.util;
|
||||
const { getLocale } = padlock.platform;
|
||||
|
||||
let translations, language;
|
||||
|
||||
function localize(msg, ...fmtArgs) {
|
||||
const lang = translations[language];
|
||||
let res = lang && lang[msg] || msg;
|
||||
|
||||
for (let i = 0; i < fmtArgs.length; i++) {
|
||||
res = res
|
||||
.replace(new RegExp(`\\{${i}\\}`, "g"), fmtArgs[i]);
|
||||
}
|
||||
|
||||
res = res.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
localize.loadTranslations = (t) => {
|
||||
translations = t;
|
||||
language = resolveLanguage(getLocale(), translations);
|
||||
};
|
||||
|
||||
padlock.LocaleMixin = (superClass) => {
|
||||
|
||||
return class LocaleMixin extends superClass {
|
||||
|
||||
$l() {
|
||||
return localize.apply(null, arguments);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
window.$l = localize;
|
||||
|
||||
})();
|
||||
</script>
|
||||
<script src="translations.js" type="text/javascript" charset="utf-8"></script>
|
File diff suppressed because one or more lines are too long
|
@ -1,10 +1,9 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
|
||||
<dom-module id="pl-notification">
|
||||
|
||||
<template>
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
|
||||
class Notification extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
|
@ -46,59 +45,54 @@
|
|||
<div class="background"></div>
|
||||
|
||||
<div class="text" on-click="_click">{{ message }}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
static get is() { return "pl-notification"; }
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
static get properties() { return {
|
||||
message: String,
|
||||
type: {
|
||||
type: String,
|
||||
value: "info",
|
||||
observer: "_typeChanged"
|
||||
}
|
||||
}; }
|
||||
|
||||
class Notification extends padlock.BaseElement {
|
||||
show(message, type, duration) {
|
||||
if (message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
static get is() { return "pl-notification"; }
|
||||
if (type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
message: String,
|
||||
type: {
|
||||
type: String,
|
||||
value: "info",
|
||||
observer: "_typeChanged"
|
||||
}
|
||||
}; }
|
||||
this.classList.add("showing");
|
||||
|
||||
show(message, type, duration) {
|
||||
if (message) {
|
||||
this.message = message;
|
||||
}
|
||||
if (duration) {
|
||||
setTimeout(() => this.hide(false), duration);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
this.type = type;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
this.classList.add("showing");
|
||||
hide(clicked) {
|
||||
this.classList.remove("showing");
|
||||
typeof this._resolve === "function" && this._resolve(clicked);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
if (duration) {
|
||||
setTimeout(() => this.hide(false), duration);
|
||||
}
|
||||
_typeChanged(newType, oldType) {
|
||||
this.classList.remove(oldType);
|
||||
this.classList.add(newType);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
hide(clicked) {
|
||||
this.classList.remove("showing");
|
||||
typeof this._resolve === "function" && this._resolve(clicked);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_typeChanged(newType, oldType) {
|
||||
this.classList.remove(oldType);
|
||||
this.classList.add(newType);
|
||||
}
|
||||
|
||||
_click() {
|
||||
this.hide(true);
|
||||
}
|
||||
_click() {
|
||||
this.hide(true);
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Notification.is, Notification);
|
||||
|
@ -120,9 +114,3 @@ padlock.NotificationMixin = (baseClass) => {
|
|||
|
||||
};
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
||||
|
|
@ -1,15 +1,31 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../dialog/dialog.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
<link rel="import" href="../input/input.html">
|
||||
<link rel="import" href="../loading-button/loading-button.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../dialog/dialog.js';
|
||||
import '../icon/icon.js';
|
||||
import '../input/input.js';
|
||||
import '../loading-button/loading-button.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
<dom-module id="pl-payment-dialog">
|
||||
const { LocaleMixin, BaseElement } = padlock;
|
||||
const { applyMixins, formatDateUntil } = padlock.util;
|
||||
const { track } = padlock.tracking;
|
||||
|
||||
<template>
|
||||
let stripe;
|
||||
|
||||
const stripeLoaded = new Promise((resolve) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://js.stripe.com/v3/";
|
||||
script.async = true;
|
||||
script.onload = resolve;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
|
||||
class PaymentDialog extends applyMixins(
|
||||
BaseElement,
|
||||
LocaleMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-max-width: 500px;
|
||||
|
@ -177,7 +193,7 @@
|
|||
|
||||
.secure-payment::before {
|
||||
font-family: "FontAwesome";
|
||||
content: "\f023\ ";
|
||||
content: "\\f023\\ ";
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
@ -238,44 +254,43 @@
|
|||
|
||||
<pl-dialog id="cardDialog" open="{{ open }}" on-dialog-dismiss="_dismiss">
|
||||
<form id="paymentForm" method="post" on-submit="_submitCard">
|
||||
<div class="update-title" hidden$="[[ _hasPlan(plan) ]]">[[ $l("Update Payment Method") ]]</div>
|
||||
<div class="plan" hidden$="[[ !_hasPlan(plan) ]]">
|
||||
<div class="plan-name">[[ $l("Upgrade to {0} now!", plan.name) ]]</div>
|
||||
<div class="update-title" hidden\$="[[ _hasPlan(plan) ]]">[[ \$l("Update Payment Method") ]]</div>
|
||||
<div class="plan" hidden\$="[[ !_hasPlan(plan) ]]">
|
||||
<div class="plan-name">[[ \$l("Upgrade to {0} now!", plan.name) ]]</div>
|
||||
<div class="features">
|
||||
<div class="feature sync">
|
||||
<pl-icon icon="mobile"></pl-icon>
|
||||
<pl-icon icon="desktop"></pl-icon>
|
||||
<div class="feature-label">[[ $l("Seamless Synchronization") ]]</div>
|
||||
<div class="feature-label">[[ \$l("Seamless Synchronization") ]]</div>
|
||||
</div>
|
||||
<div class="feature backup">
|
||||
<pl-icon icon="database"></pl-icon>
|
||||
<div>[[ $l("Automatic Backups") ]]</div>
|
||||
<div>[[ \$l("Automatic Backups") ]]</div>
|
||||
</div>
|
||||
<div class="feature encryption">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<div>[[ $l("End-To-End Encryption") ]]</div>
|
||||
<div>[[ \$l("End-To-End Encryption") ]]</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="price">
|
||||
<div class="price-original" hidden$="[[ !truthy(promo) ]]">$[[ _originalPrice.0 ]][[ _originalPrice.1 ]]</div>
|
||||
<div class="price-original" hidden\$="[[ !truthy(promo) ]]">\$[[ _originalPrice.0 ]][[ _originalPrice.1 ]]</div>
|
||||
<div class="price-amount">
|
||||
<span class="price-currency">$</span>[[ _price.0 ]]<span class="price-decimal">[[ _price.1 ]]</span>
|
||||
<span class="price-currency">\$</span>[[ _price.0 ]]<span class="price-decimal">[[ _price.1 ]]</span>
|
||||
</div>
|
||||
<div class="price-period">[[ $l("per month*") ]]</div>
|
||||
<div class="price-period">[[ \$l("per month*") ]]</div>
|
||||
</div>
|
||||
<div class="price-hint">[[ $l("*USD\, billed annually") ]]</div>
|
||||
<div class="price-hint">[[ \$l("*USD\\, billed annually") ]]</div>
|
||||
</div>
|
||||
<div class="promo-desc" hidden$="[[ !truthy(promo) ]]">
|
||||
<div class="promo-desc" hidden\$="[[ !truthy(promo) ]]">
|
||||
<div>[[ promo.title ]]</div>
|
||||
<div class="redeem-within" hidden$="[[ !truthy(promo.redeemWithin) ]]">[[ $l("Expires In") ]] [[ _redeemCountDown ]]</div>
|
||||
<div class="redeem-within" hidden\$="[[ !truthy(promo.redeemWithin) ]]">[[ \$l("Expires In") ]] [[ _redeemCountDown ]]</div>
|
||||
</div>
|
||||
<div class="message card-hint tap" hidden$="[[ _hasError(_cardError) ]]" on-click="_toggleCoupon">
|
||||
<div>[[ $l("Please enter your credit or debit card information:") ]]</div>
|
||||
<div class="message card-hint tap" hidden\$="[[ _hasError(_cardError) ]]" on-click="_toggleCoupon">
|
||||
<div>[[ \$l("Please enter your credit or debit card information:") ]]</div>
|
||||
</div>
|
||||
<div class="message card-hint tap" hidden$="[[ !_hasError(_cardError) ]]" on-click="_openSupport">
|
||||
<div class="message card-hint tap" hidden\$="[[ !_hasError(_cardError) ]]" on-click="_openSupport">
|
||||
<div class="error">[[ _cardError ]]</div>
|
||||
<div class="support-link"
|
||||
hidden$="[[ !_needsSupport ]]">[[ $l("Contact Support") ]]</div>
|
||||
<div class="support-link" hidden\$="[[ !_needsSupport ]]">[[ \$l("Contact Support") ]]</div>
|
||||
</div>
|
||||
<div class="stripe-element-wrapper">
|
||||
<slot>
|
||||
|
@ -284,217 +299,188 @@
|
|||
<pl-loading-button class="tap" id="submitButton" on-click="_submitCard">[[ _submitLabel(plan) ]]</pl-loading-button>
|
||||
</form>
|
||||
<div slot="after" class="secure-payment">
|
||||
<span>[[ $l("Secure Payment - ") ]]</span>
|
||||
<span>[[ \$l("Secure Payment - ") ]]</span>
|
||||
<img src="assets/img/powered_by_stripe.svg">
|
||||
</div>
|
||||
<button class="trial-button" slot="after" hidden$="[[ _trialExpired(remainingTrialDays) ]]">
|
||||
[[ $l("Continue Trial ({0} Days Left)", remainingTrialDays) ]]
|
||||
<button class="trial-button" slot="after" hidden\$="[[ _trialExpired(remainingTrialDays) ]]">
|
||||
[[ \$l("Continue Trial ({0} Days Left)", remainingTrialDays) ]]
|
||||
</button>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
static get is() { return "pl-payment-dialog"; }
|
||||
|
||||
<script>
|
||||
/* global Stripe */
|
||||
(() => {
|
||||
static get properties() { return {
|
||||
open: { type: Boolean },
|
||||
promo: { type: Object, value: null },
|
||||
plan: { type: Object, value: null },
|
||||
source: Object,
|
||||
stripePubKey: { type: String, value: "" },
|
||||
csrfToken: { stype: String, value: ""},
|
||||
remainingTrialDays: { type: Number, value: 0 },
|
||||
_cardError: { type: String, value: "" },
|
||||
_needsSupport: { type: Boolean, value: false },
|
||||
_price: {
|
||||
type: Array,
|
||||
computed: "_calcPrice(plan.amount, promo.coupon.percent_off, promo.coupon.amount_off)"
|
||||
},
|
||||
_originalPrice: {
|
||||
type: Array,
|
||||
computed: "_calcPrice(plan.amount)"
|
||||
}
|
||||
}; }
|
||||
|
||||
const { LocaleMixin, BaseElement } = padlock;
|
||||
const { applyMixins, formatDateUntil } = padlock.util;
|
||||
const { track } = padlock.tracking;
|
||||
static get observers() { return [
|
||||
"_setupCountdown(promo.redeemWithin)"
|
||||
]; }
|
||||
|
||||
let stripe;
|
||||
_setupCountdown() {
|
||||
clearInterval(this._countdown);
|
||||
const p = this.promo;
|
||||
if (p && p.redeemWithin) {
|
||||
this._countdown = setInterval(() => {
|
||||
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
const stripeLoaded = new Promise((resolve) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://js.stripe.com/v3/";
|
||||
script.async = true;
|
||||
script.onload = resolve;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
show(source) {
|
||||
this._needsSupport = false;
|
||||
this._cardError = "";
|
||||
this.open = true;
|
||||
this._source = source;
|
||||
|
||||
class PaymentDialog extends applyMixins(
|
||||
BaseElement,
|
||||
LocaleMixin
|
||||
) {
|
||||
if (!this._cardElement) {
|
||||
this._setupPayment();
|
||||
}
|
||||
|
||||
static get is() { return "pl-payment-dialog"; }
|
||||
track("Payment Dialog: Open", {
|
||||
"Plan": this.plan && this.plan.id,
|
||||
"Source": this._source
|
||||
});
|
||||
|
||||
static get properties() { return {
|
||||
open: { type: Boolean },
|
||||
promo: { type: Object, value: null },
|
||||
plan: { type: Object, value: null },
|
||||
source: Object,
|
||||
stripePubKey: { type: String, value: "" },
|
||||
csrfToken: { stype: String, value: ""},
|
||||
remainingTrialDays: { type: Number, value: 0 },
|
||||
_cardError: { type: String, value: "" },
|
||||
_needsSupport: { type: Boolean, value: false },
|
||||
_price: {
|
||||
type: Array,
|
||||
computed: "_calcPrice(plan.amount, promo.coupon.percent_off, promo.coupon.amount_off)"
|
||||
},
|
||||
_originalPrice: {
|
||||
type: Array,
|
||||
computed: "_calcPrice(plan.amount)"
|
||||
}
|
||||
}; }
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
static get observers() { return [
|
||||
"_setupCountdown(promo.redeemWithin)"
|
||||
]; }
|
||||
_setupPayment() {
|
||||
stripeLoaded.then(() => {
|
||||
stripe = Stripe(this.stripePubKey);
|
||||
const elements = stripe.elements();
|
||||
const card = this._cardElement = elements.create("card", {
|
||||
iconStyle: "solid",
|
||||
style: {
|
||||
base: {
|
||||
fontFamily: '"Clear Sans", "Helvetica Neue", Helvetica, sans-serif',
|
||||
fontSmoothing: "antialiased",
|
||||
fontSize: "18px"
|
||||
},
|
||||
invalid: {
|
||||
textShadow: "none"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_setupCountdown() {
|
||||
clearInterval(this._countdown);
|
||||
const p = this.promo;
|
||||
if (p && p.redeemWithin) {
|
||||
this._countdown = setInterval(() => {
|
||||
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
const cardElement = document.createElement("div");
|
||||
this.appendChild(cardElement);
|
||||
card.mount(cardElement);
|
||||
|
||||
show(source) {
|
||||
this._needsSupport = false;
|
||||
this._cardError = "";
|
||||
this.open = true;
|
||||
this._source = source;
|
||||
card.addEventListener("change", (e) => this._cardError = e.error && e.error.message || "");
|
||||
});
|
||||
}
|
||||
|
||||
if (!this._cardElement) {
|
||||
this._setupPayment();
|
||||
}
|
||||
_submitCard() {
|
||||
if (this._submittingCard) {
|
||||
return;
|
||||
}
|
||||
|
||||
track("Payment Dialog: Open", {
|
||||
"Plan": this.plan && this.plan.id,
|
||||
"Source": this._source
|
||||
});
|
||||
this.$.submitButton.start();
|
||||
this._submittingCard = true;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
const coupon = this.promo && this.promo.coupon.id || "";
|
||||
|
||||
_setupPayment() {
|
||||
stripeLoaded.then(() => {
|
||||
stripe = Stripe(this.stripePubKey);
|
||||
const elements = stripe.elements();
|
||||
const card = this._cardElement = elements.create("card", {
|
||||
iconStyle: "solid",
|
||||
style: {
|
||||
base: {
|
||||
fontFamily: '"Clear Sans", "Helvetica Neue", Helvetica, sans-serif',
|
||||
fontSmoothing: "antialiased",
|
||||
fontSize: "18px"
|
||||
},
|
||||
invalid: {
|
||||
textShadow: "none"
|
||||
}
|
||||
}
|
||||
});
|
||||
stripe.createToken(this._cardElement).then((result) => {
|
||||
const edata = {
|
||||
"Plan": this.plan && this.plan.id,
|
||||
"Source": this._source,
|
||||
"Success": true,
|
||||
"Token Created": true,
|
||||
"Coupon": coupon
|
||||
};
|
||||
|
||||
const cardElement = document.createElement("div");
|
||||
this.appendChild(cardElement);
|
||||
card.mount(cardElement);
|
||||
if (result.error) {
|
||||
this.$.submitButton.fail();
|
||||
this._submittingCard = false;
|
||||
Object.assign(edata, {
|
||||
"Error Code": result.error.code,
|
||||
"Error Type": result.error.type,
|
||||
"Error Message": result.error.message,
|
||||
"Success": false,
|
||||
"Token Created": false
|
||||
});
|
||||
track("Payment Dialog: Submit", edata);
|
||||
} else {
|
||||
this.source.subscribe(result.token.id, coupon, this._source)
|
||||
.then(() => {
|
||||
this.$.submitButton.success();
|
||||
typeof this._resolve === "function" && this._resolve(true);
|
||||
this._submittingCard = false;
|
||||
this._resolve = null;
|
||||
this.open = false;
|
||||
track("Payment Dialog: Submit", edata);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$.submitButton.fail();
|
||||
this._submittingCard = false;
|
||||
this._cardError = e.message;
|
||||
this._needsSupport = true;
|
||||
Object.assign(edata, {
|
||||
"Error Code": e.code,
|
||||
"Error Message": e.message,
|
||||
"Success": false
|
||||
});
|
||||
track("Payment Dialog: Submit", edata);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
card.addEventListener("change", (e) => this._cardError = e.error && e.error.message || "");
|
||||
});
|
||||
}
|
||||
_hasError() {
|
||||
return !!this._cardError;
|
||||
}
|
||||
|
||||
_submitCard() {
|
||||
if (this._submittingCard) {
|
||||
return;
|
||||
}
|
||||
_calcPrice(amount, percentOff = 0, amountOff = 0) {
|
||||
amount = percentOff ? amount * (1 - percentOff / 100) : amount - amountOff;
|
||||
const d = Math.round((amount / 12) % 100);
|
||||
|
||||
this.$.submitButton.start();
|
||||
this._submittingCard = true;
|
||||
return [
|
||||
`${Math.floor(amount / 1200)}`,
|
||||
d < 10 ? `.0${d}` : `.${d}`
|
||||
];
|
||||
}
|
||||
|
||||
const coupon = this.promo && this.promo.coupon.id || "";
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(false);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
stripe.createToken(this._cardElement).then((result) => {
|
||||
const edata = {
|
||||
"Plan": this.plan && this.plan.id,
|
||||
"Source": this._source,
|
||||
"Success": true,
|
||||
"Token Created": true,
|
||||
"Coupon": coupon
|
||||
};
|
||||
_openSupport() {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
this.$.submitButton.fail();
|
||||
this._submittingCard = false;
|
||||
Object.assign(edata, {
|
||||
"Error Code": result.error.code,
|
||||
"Error Type": result.error.type,
|
||||
"Error Message": result.error.message,
|
||||
"Success": false,
|
||||
"Token Created": false
|
||||
});
|
||||
track("Payment Dialog: Submit", edata);
|
||||
} else {
|
||||
this.source.subscribe(result.token.id, coupon, this._source)
|
||||
.then(() => {
|
||||
this.$.submitButton.success();
|
||||
typeof this._resolve === "function" && this._resolve(true);
|
||||
this._submittingCard = false;
|
||||
this._resolve = null;
|
||||
this.open = false;
|
||||
track("Payment Dialog: Submit", edata);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$.submitButton.fail();
|
||||
this._submittingCard = false;
|
||||
this._cardError = e.message;
|
||||
this._needsSupport = true;
|
||||
Object.assign(edata, {
|
||||
"Error Code": e.code,
|
||||
"Error Message": e.message,
|
||||
"Success": false
|
||||
});
|
||||
track("Payment Dialog: Submit", edata);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
_trialExpired() {
|
||||
return !this.remainingTrialDays;
|
||||
}
|
||||
|
||||
_hasError() {
|
||||
return !!this._cardError;
|
||||
}
|
||||
|
||||
_calcPrice(amount, percentOff = 0, amountOff = 0) {
|
||||
amount = percentOff ? amount * (1 - percentOff / 100) : amount - amountOff;
|
||||
const d = Math.round((amount / 12) % 100);
|
||||
|
||||
return [
|
||||
`${Math.floor(amount / 1200)}`,
|
||||
d < 10 ? `.0${d}` : `.${d}`
|
||||
];
|
||||
}
|
||||
|
||||
_dismiss() {
|
||||
typeof this._resolve === "function" && this._resolve(false);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_openSupport() {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
|
||||
_trialExpired() {
|
||||
return !this.remainingTrialDays;
|
||||
}
|
||||
|
||||
_hasPlan() {
|
||||
return !!this.plan;
|
||||
}
|
||||
|
||||
_submitLabel() {
|
||||
return this.plan ? $l("Upgrade Now") : $l("Submit");
|
||||
}
|
||||
_hasPlan() {
|
||||
return !!this.plan;
|
||||
}
|
||||
|
||||
_submitLabel() {
|
||||
return this.plan ? $l("Upgrade Now") : $l("Submit");
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(PaymentDialog.is, PaymentDialog);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -1,63 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../dialog/dialog.html">
|
||||
<link rel="import" href="./promo.html">
|
||||
|
||||
<dom-module id="pl-promo-dialog">
|
||||
|
||||
<template>
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(180deg, #555 0%, #222 100%);
|
||||
};
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog" on-dialog-dismiss="_dismiss">
|
||||
<pl-promo promo="[[ promo ]]" on-promo-expire="_dismiss" on-promo-redeem="_redeem"></pl-promo>
|
||||
</pl-dialog>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
const { BaseElement } = padlock;
|
||||
|
||||
class PlExportDialog extends BaseElement {
|
||||
|
||||
static get is() { return "pl-promo-dialog"; }
|
||||
|
||||
static get properties() { return {
|
||||
promo: Object
|
||||
}; }
|
||||
|
||||
_dismiss() {
|
||||
this.$.dialog.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(false);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_redeem() {
|
||||
this.$.dialog.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(true);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
show() {
|
||||
setTimeout(() => this.$.dialog.open = true, 10);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.customElements.define(PlExportDialog.is, PlExportDialog);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,52 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../dialog/dialog.js';
|
||||
import './promo.js';
|
||||
|
||||
const { BaseElement } = padlock;
|
||||
|
||||
class PlExportDialog extends BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
background: linear-gradient(180deg, #555 0%, #222 100%);
|
||||
};
|
||||
}
|
||||
</style>
|
||||
|
||||
<pl-dialog id="dialog" on-dialog-dismiss="_dismiss">
|
||||
<pl-promo promo="[[ promo ]]" on-promo-expire="_dismiss" on-promo-redeem="_redeem"></pl-promo>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-promo-dialog"; }
|
||||
|
||||
static get properties() { return {
|
||||
promo: Object
|
||||
}; }
|
||||
|
||||
_dismiss() {
|
||||
this.$.dialog.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(false);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
_redeem() {
|
||||
this.$.dialog.open = false;
|
||||
typeof this._resolve === "function" && this._resolve(true);
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
show() {
|
||||
setTimeout(() => this.$.dialog.open = true, 10);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(PlExportDialog.is, PlExportDialog);
|
|
@ -1,12 +1,14 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../icon/icon.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
<dom-module id="pl-promo">
|
||||
|
||||
<template>
|
||||
const { LocaleMixin } = padlock;
|
||||
const { formatDateUntil, isFuture } = padlock.util;
|
||||
|
||||
class Promo extends LocaleMixin(padlock.BaseElement) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
|
||||
button {
|
||||
|
@ -49,53 +51,38 @@
|
|||
</div>
|
||||
|
||||
<button class="tap" on-click="_redeem">
|
||||
<div>[[ $l("Redeem Now") ]]</div>
|
||||
<div class="redeem-within" hidden$="[[ !truthy(promo.redeemWithin) ]]">[[ $l("Expires In") ]] [[ _redeemCountDown ]]</div>
|
||||
<div>[[ \$l("Redeem Now") ]]</div>
|
||||
<div class="redeem-within" hidden\$="[[ !truthy(promo.redeemWithin) ]]">[[ \$l("Expires In") ]] [[ _redeemCountDown ]]</div>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
static get is() { return "pl-promo"; }
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
static get properties() { return {
|
||||
promo: Object
|
||||
}; }
|
||||
|
||||
const { LocaleMixin } = padlock;
|
||||
const { formatDateUntil, isFuture } = padlock.util;
|
||||
static get observers() { return [
|
||||
"_setupCountdown(promo.redeemWithin)"
|
||||
]; }
|
||||
|
||||
class Promo extends LocaleMixin(padlock.BaseElement) {
|
||||
|
||||
static get is() { return "pl-promo"; }
|
||||
|
||||
static get properties() { return {
|
||||
promo: Object
|
||||
}; }
|
||||
|
||||
static get observers() { return [
|
||||
"_setupCountdown(promo.redeemWithin)"
|
||||
]; }
|
||||
|
||||
_setupCountdown() {
|
||||
clearInterval(this._countdown);
|
||||
const p = this.promo;
|
||||
if (p && p.redeemWithin) {
|
||||
this._countdown = setInterval(() => {
|
||||
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
|
||||
if (!isFuture(p.created, p.redeemWithin)) {
|
||||
this.dispatchEvent(new CustomEvent("promo-expired"));
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
_redeem() {
|
||||
this.dispatchEvent(new CustomEvent("promo-redeem"));
|
||||
}
|
||||
_setupCountdown() {
|
||||
clearInterval(this._countdown);
|
||||
const p = this.promo;
|
||||
if (p && p.redeemWithin) {
|
||||
this._countdown = setInterval(() => {
|
||||
this._redeemCountDown = formatDateUntil(p.created, p.redeemWithin);
|
||||
if (!isFuture(p.created, p.redeemWithin)) {
|
||||
this.dispatchEvent(new CustomEvent("promo-expired"));
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
_redeem() {
|
||||
this.dispatchEvent(new CustomEvent("promo-redeem"));
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Promo.is, Promo);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
||||
|
|
@ -1,13 +1,22 @@
|
|||
<link rel="import" href="../../../bower_components/polymer/lib/mixins/mutable-data.html">
|
||||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="../clipboard/clipboard.html">
|
||||
import '../../../../../node_modules/@polymer/polymer/lib/mixins/mutable-data.js';
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../icon/icon.js';
|
||||
import '../locale/locale.js';
|
||||
import '../clipboard/clipboard.js';
|
||||
|
||||
<dom-module id="pl-record-item">
|
||||
<template>
|
||||
const { ClipboardMixin, LocaleMixin, DataMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
const { isTouch } = padlock.platform;
|
||||
|
||||
class RecordItem extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
ClipboardMixin,
|
||||
LocaleMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
|
@ -87,7 +96,7 @@
|
|||
|
||||
.copied-message::before {
|
||||
font-family: "FontAwesome";
|
||||
content: "\f00c\ ";
|
||||
content: "\\f00c\\ ";
|
||||
}
|
||||
|
||||
:host(:not(.touch):not([multi-select])) .field:hover {
|
||||
|
@ -152,11 +161,11 @@
|
|||
<div class="highlight"></div>
|
||||
|
||||
<div class="header">
|
||||
<div class="name" hidden$="[[ !truthy(record.name) ]]">[[ record.name ]]</div>
|
||||
<div class="name" disabled hidden$="[[ truthy(record.name) ]]">[[ $l("No Name") ]]</div>
|
||||
<div class="name" hidden\$="[[ !truthy(record.name) ]]">[[ record.name ]]</div>
|
||||
<div class="name" disabled="" hidden\$="[[ truthy(record.name) ]]">[[ \$l("No Name") ]]</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="tags">
|
||||
<template is="dom-repeat" items="[[ _limitTags(record.tags) ]]" mutable-data>
|
||||
<template is="dom-repeat" items="[[ _limitTags(record.tags) ]]" mutable-data="">
|
||||
<div class="ellipsis tag">[[ item ]]</div>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -164,102 +173,81 @@
|
|||
|
||||
<div class="fields">
|
||||
|
||||
<template is="dom-repeat" items="[[ record.fields ]]" mutable-data>
|
||||
<template is="dom-repeat" items="[[ record.fields ]]" mutable-data="">
|
||||
|
||||
<div class="field" on-click="_copyField">
|
||||
<div class="field-label">[[ item.name ]]</div>
|
||||
<div class="copied-message">[[ $l("copied") ]]</div>
|
||||
<div class="copied-message">[[ \$l("copied") ]]</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<div class="field" disabled hidden$="[[ _hasFields(record.fields.length) ]]">[[ $l("No Fields") ]]</div>
|
||||
<div class="field" disabled="" hidden\$="[[ _hasFields(record.fields.length) ]]">[[ \$l("No Fields") ]]</div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
static get is() { return "pl-record-item"; }
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
static get properties() { return {
|
||||
multiSelect: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
record: Object
|
||||
}; }
|
||||
|
||||
const { ClipboardMixin, LocaleMixin, DataMixin, BaseElement } = padlock;
|
||||
const { MutableData } = Polymer;
|
||||
const { applyMixins } = padlock.util;
|
||||
const { isTouch } = padlock.platform;
|
||||
ready() {
|
||||
super.ready();
|
||||
// For some reason the keydown event doesn't bubble if a record item has focus so we have to
|
||||
// re-dispatch it
|
||||
this.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") {
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", e));
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
class RecordItem extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
MutableData,
|
||||
ClipboardMixin,
|
||||
LocaleMixin
|
||||
) {
|
||||
this.classList.toggle("touch", isTouch());
|
||||
}
|
||||
|
||||
static get is() { return "pl-record-item"; }
|
||||
_copyField(e) {
|
||||
if (!this.multiSelect) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.setClipboard(this.record, e.model.item);
|
||||
const field = this.root.querySelectorAll(".field")[e.model.index];
|
||||
field.classList.add("copied");
|
||||
this.record.lastUsed = new Date();
|
||||
this.saveCollection();
|
||||
setTimeout(() => field.classList.remove("copied"), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
multiSelect: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
record: Object
|
||||
}; }
|
||||
_fieldLabel(value) {
|
||||
return value ? value + ":" : "";
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
// For some reason the keydown event doesn't bubble if a record item has focus so we have to
|
||||
// re-dispatch it
|
||||
this.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") {
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", e));
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
_hasFields() {
|
||||
return !!this.record.fields.length;
|
||||
}
|
||||
|
||||
this.classList.toggle("touch", isTouch());
|
||||
}
|
||||
_selectIconClicked() {
|
||||
this.dispatchEvent(new CustomEvent("multi-select", { detail: this.record }));
|
||||
}
|
||||
|
||||
_copyField(e) {
|
||||
if (!this.multiSelect) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.setClipboard(this.record, e.model.item);
|
||||
const field = this.root.querySelectorAll(".field")[e.model.index];
|
||||
field.classList.add("copied");
|
||||
this.record.lastUsed = new Date();
|
||||
this.saveCollection();
|
||||
setTimeout(() => field.classList.remove("copied"), 1000);
|
||||
}
|
||||
}
|
||||
_limitTags() {
|
||||
const tags = this.record.tags.slice(0, 2);
|
||||
const more = this.record.tags.length - tags.length;
|
||||
|
||||
_fieldLabel(value) {
|
||||
return value ? value + ":" : "";
|
||||
}
|
||||
if (more) {
|
||||
tags.push("+" + more);
|
||||
}
|
||||
|
||||
_hasFields() {
|
||||
return !!this.record.fields.length;
|
||||
}
|
||||
|
||||
_selectIconClicked() {
|
||||
this.dispatchEvent(new CustomEvent("multi-select", { detail: this.record }));
|
||||
}
|
||||
|
||||
_limitTags() {
|
||||
const tags = this.record.tags.slice(0, 2);
|
||||
const more = this.record.tags.length - tags.length;
|
||||
|
||||
if (more) {
|
||||
tags.push("+" + more);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(RecordItem.is, RecordItem);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -1,12 +1,14 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../dialog/dialog.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../dialog/dialog.js';
|
||||
import '../icon/icon.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
<dom-module id="pl-record-field-dialog">
|
||||
const { BaseElement, LocaleMixin } = padlock;
|
||||
|
||||
<template>
|
||||
class RecordFieldDialog extends LocaleMixin(BaseElement) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
--pl-dialog-inner: {
|
||||
|
@ -97,137 +99,122 @@
|
|||
|
||||
<pl-dialog open="{{ open }}" prevent-dismiss="[[ editing ]]" on-dialog-dismiss="_close">
|
||||
<div class="header">
|
||||
<pl-input id="nameInput" placeholder="[[ $l('Enter Field Name') ]]" class="name"
|
||||
readonly="[[ !editing ]]" on-click="_inputClicked" on-enter="_nameInputEnter"></pl-input>
|
||||
<pl-icon icon="cancel" class="tap" on-click="_close" hidden$="[[ editing ]]"></pl-icon>
|
||||
<pl-input id="nameInput" placeholder="[[ \$l('Enter Field Name') ]]" class="name" readonly="[[ !editing ]]" on-click="_inputClicked" on-enter="_nameInputEnter"></pl-input>
|
||||
<pl-icon icon="cancel" class="tap" on-click="_close" hidden\$="[[ editing ]]"></pl-icon>
|
||||
</div>
|
||||
<div class="value-wrapper">
|
||||
<pl-input id="valueInput" multiline class="value" autosize on-click="_inputClicked"
|
||||
placeholder="[[ $l('Enter Content') ]]" readonly="[[ !editing ]]"></pl-input>
|
||||
<button class="generate-button tap" on-click="_generate" hidden$="[[ !editing ]]">
|
||||
<div>[[ $l("Generate") ]]</div>
|
||||
<pl-input id="valueInput" multiline="" class="value" autosize="" on-click="_inputClicked" placeholder="[[ \$l('Enter Content') ]]" readonly="[[ !editing ]]"></pl-input>
|
||||
<button class="generate-button tap" on-click="_generate" hidden\$="[[ !editing ]]">
|
||||
<div>[[ \$l("Generate") ]]</div>
|
||||
<pl-icon icon="generate"></pl-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="action-items tiles-3 tiles" hidden$="[[ editing ]]">
|
||||
<div class="action-items tiles-3 tiles" hidden\$="[[ editing ]]">
|
||||
<pl-icon icon="copy" class="tap" on-click="_copy"></pl-icon>
|
||||
<pl-icon icon="edit" class="tap" on-click="_edit"></pl-icon>
|
||||
<pl-icon icon="generate" class="tap" on-click="_generate"></pl-icon>
|
||||
<pl-icon icon="delete" class="tap" on-click="_delete"></pl-icon>
|
||||
</div>
|
||||
<div class="action-items" hidden$="[[ !editing ]]">
|
||||
<button class="tap" on-click="_discardChanges">[[ $l("Discard") ]]</button>
|
||||
<button class="tap" on-click="_saveChanges">[[ $l("Save") ]]</button>
|
||||
<div class="action-items" hidden\$="[[ !editing ]]">
|
||||
<button class="tap" on-click="_discardChanges">[[ \$l("Discard") ]]</button>
|
||||
<button class="tap" on-click="_saveChanges">[[ \$l("Save") ]]</button>
|
||||
</div>
|
||||
</div>
|
||||
</pl-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
static get is() { return "pl-record-field-dialog"; }
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
static get properties() { return {
|
||||
editing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
field: {
|
||||
type: Object,
|
||||
value: () => { return { name: "", value: "" }; }
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
|
||||
const { BaseElement, LocaleMixin } = padlock;
|
||||
openField(field, edit = false, presets = {}) {
|
||||
this.open = true;
|
||||
this.editing = false;
|
||||
this.field = field;
|
||||
this.$.nameInput.value = presets.name || this.field.name;
|
||||
this.$.valueInput.value = presets.value || this.field.value;
|
||||
if (edit) {
|
||||
this._edit();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
class RecordFieldDialog extends LocaleMixin(BaseElement) {
|
||||
_closeWithAction(action) {
|
||||
this.open = false;
|
||||
this._resolve && this._resolve({
|
||||
action: action,
|
||||
name: this.$.nameInput.value,
|
||||
value: this.$.valueInput.value
|
||||
});
|
||||
this._resolve = null;
|
||||
}
|
||||
|
||||
static get is() { return "pl-record-field-dialog"; }
|
||||
_close() {
|
||||
this._closeWithAction();
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
editing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
field: {
|
||||
type: Object,
|
||||
value: () => { return { name: "", value: "" }; }
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
_delete() {
|
||||
this._closeWithAction("delete");
|
||||
}
|
||||
|
||||
openField(field, edit = false, presets = {}) {
|
||||
this.open = true;
|
||||
this.editing = false;
|
||||
this.field = field;
|
||||
this.$.nameInput.value = presets.name || this.field.name;
|
||||
this.$.valueInput.value = presets.value || this.field.value;
|
||||
if (edit) {
|
||||
this._edit();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
_copy() {
|
||||
this._closeWithAction("copy");
|
||||
}
|
||||
|
||||
_closeWithAction(action) {
|
||||
this.open = false;
|
||||
this._resolve && this._resolve({
|
||||
action: action,
|
||||
name: this.$.nameInput.value,
|
||||
value: this.$.valueInput.value
|
||||
});
|
||||
this._resolve = null;
|
||||
}
|
||||
_generate() {
|
||||
this._closeWithAction("generate");
|
||||
}
|
||||
|
||||
_close() {
|
||||
this._closeWithAction();
|
||||
}
|
||||
_edit() {
|
||||
this.editing = true;
|
||||
setTimeout(() => {
|
||||
if (!this.$.nameInput.value) {
|
||||
this.$.nameInput.focus();
|
||||
} else {
|
||||
this.$.valueInput.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
_delete() {
|
||||
this._closeWithAction("delete");
|
||||
}
|
||||
_discardChanges() {
|
||||
this.$.nameInput.value = this.field.name;
|
||||
this.$.valueInput.value = this.field.value;
|
||||
this._close();
|
||||
}
|
||||
|
||||
_copy() {
|
||||
this._closeWithAction("copy");
|
||||
}
|
||||
_saveChanges() {
|
||||
this.field.name = this.$.nameInput.value;
|
||||
this.field.value = this.$.valueInput.value;
|
||||
this._closeWithAction("edited");
|
||||
}
|
||||
|
||||
_generate() {
|
||||
this._closeWithAction("generate");
|
||||
}
|
||||
|
||||
_edit() {
|
||||
this.editing = true;
|
||||
setTimeout(() => {
|
||||
if (!this.$.nameInput.value) {
|
||||
this.$.nameInput.focus();
|
||||
} else {
|
||||
this.$.valueInput.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
_discardChanges() {
|
||||
this.$.nameInput.value = this.field.name;
|
||||
this.$.valueInput.value = this.field.value;
|
||||
this._close();
|
||||
}
|
||||
|
||||
_saveChanges() {
|
||||
this.field.name = this.$.nameInput.value;
|
||||
this.field.value = this.$.valueInput.value;
|
||||
this._closeWithAction("edited");
|
||||
}
|
||||
|
||||
_inputClicked(e) {
|
||||
if (!e.target.value) {
|
||||
this.editing = true;
|
||||
}
|
||||
}
|
||||
|
||||
_nameInputEnter() {
|
||||
this.$.valueInput.focus();
|
||||
}
|
||||
_inputClicked(e) {
|
||||
if (!e.target.value) {
|
||||
this.editing = true;
|
||||
}
|
||||
}
|
||||
|
||||
_nameInputEnter() {
|
||||
this.$.valueInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(RecordFieldDialog.is, RecordFieldDialog);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -1,200 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../clipboard/clipboard.html">
|
||||
<link rel="import" href="../dialog/dialog-mixin.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
<link rel="import" href="../input/input.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="../notification/notification.html">
|
||||
<link rel="import" href="record-field-dialog.html">
|
||||
|
||||
<dom-module id="pl-record-field">
|
||||
<template>
|
||||
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
color: inherit;
|
||||
font-size: var(--font-size-small);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: var(--font-size-tiny);
|
||||
font-weight: bold;
|
||||
color: var(--color-highlight);
|
||||
margin: 12px 12px 6px 12px;
|
||||
}
|
||||
|
||||
#valueInput {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 130%;
|
||||
line-height: 1.2;
|
||||
flex: 1;
|
||||
margin: 0 12px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#valueInput::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 20px;
|
||||
background: linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
|
||||
}
|
||||
|
||||
.field-buttons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.field-button {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
font-size: 110%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
:host(:not(.touch):not(:hover)) .field-buttons {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.field-button:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container tap" on-click="_fieldClicked">
|
||||
<div class="name">[[ field.name ]]</div>
|
||||
<pl-input multiline id="valueInput" value="[[ field.value ]]" disabled
|
||||
placeholder="[[ $l('No Content') ]]" masked="[[ field.masked ]]"></pl-input>
|
||||
</div>
|
||||
|
||||
<div class="field-buttons">
|
||||
<pl-icon icon="[[ _toggleMaskIcon(field.masked) ]]" class="field-button tap"
|
||||
on-click="_toggleMask" hidden$="[[ !_hasValue(field.value) ]]"></pl-icon>
|
||||
<pl-icon icon="copy" class="field-button tap" on-click="_copy" hidden$="[[ !_hasValue(field.value) ]]"></pl-icon>
|
||||
<pl-icon icon="edit" class="field-button tap" on-click="_edit" hidden$="[[ _hasValue(field.value) ]]"></pl-icon>
|
||||
<pl-icon icon="generate" class="field-button tap" on-click="_showGenerator" hidden$="[[ _hasValue(field.value) ]]"></pl-icon>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
const { applyMixins } = padlock.util;
|
||||
const { isTouch } = padlock.platform;
|
||||
const { BaseElement, NotificationMixin, LocaleMixin, DialogMixin, ClipboardMixin } = padlock;
|
||||
|
||||
class RecordField extends applyMixins(
|
||||
BaseElement,
|
||||
NotificationMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
ClipboardMixin
|
||||
) {
|
||||
|
||||
static get is() { return "pl-record-field"; }
|
||||
|
||||
static get properties() { return {
|
||||
_masked: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
record: Object,
|
||||
field: {
|
||||
type: Object,
|
||||
value: () => { return { name: "", value: "", masked: false }; }
|
||||
}
|
||||
}; }
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.classList.toggle("touch", isTouch());
|
||||
}
|
||||
|
||||
_delete() {
|
||||
this.dispatchEvent(new CustomEvent("field-delete", { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
_showGenerator() {
|
||||
return this.generate()
|
||||
.then((value) => {
|
||||
this.set("field.value", value);
|
||||
this.dispatchEvent(new CustomEvent("field-change"));
|
||||
});
|
||||
}
|
||||
|
||||
_copy() {
|
||||
this.setClipboard(this.record, this.field);
|
||||
}
|
||||
|
||||
_toggleMaskIcon() {
|
||||
return this.field.masked ? "show" : "hide";
|
||||
}
|
||||
|
||||
_toggleMask() {
|
||||
this.set("field.masked", !this.field.masked);
|
||||
this.dispatchEvent(new CustomEvent("field-change"));
|
||||
}
|
||||
|
||||
_openFieldDialog(edit, presets) {
|
||||
this.lineUpDialog("pl-record-field-dialog", (d) => d.openField(this.field, edit, presets))
|
||||
.then((result) => {
|
||||
switch (result.action) {
|
||||
case "copy":
|
||||
this._copy();
|
||||
break;
|
||||
case "generate":
|
||||
this.generate()
|
||||
.then((value) => {
|
||||
this._openFieldDialog(true, { name: result.name, value: value || result.value });
|
||||
});
|
||||
break;
|
||||
case "delete":
|
||||
this._delete();
|
||||
break;
|
||||
case "edited":
|
||||
this.notifyPath("field.name");
|
||||
this.notifyPath("field.value");
|
||||
setTimeout(() => this.dispatchEvent(new CustomEvent("field-change"), 500));
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_hasValue() {
|
||||
return !!this.field.value;
|
||||
}
|
||||
|
||||
_fieldClicked() {
|
||||
this._openFieldDialog();
|
||||
}
|
||||
|
||||
_edit() {
|
||||
this._openFieldDialog(true);
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(RecordField.is, RecordField);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,188 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../clipboard/clipboard.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../icon/icon.js';
|
||||
import '../input/input.js';
|
||||
import '../locale/locale.js';
|
||||
import '../notification/notification.js';
|
||||
import './record-field-dialog.js';
|
||||
|
||||
const { applyMixins } = padlock.util;
|
||||
const { isTouch } = padlock.platform;
|
||||
const { BaseElement, NotificationMixin, LocaleMixin, DialogMixin, ClipboardMixin } = padlock;
|
||||
|
||||
class RecordField extends applyMixins(
|
||||
BaseElement,
|
||||
NotificationMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
ClipboardMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: block;
|
||||
color: inherit;
|
||||
font-size: var(--font-size-small);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: var(--font-size-tiny);
|
||||
font-weight: bold;
|
||||
color: var(--color-highlight);
|
||||
margin: 12px 12px 6px 12px;
|
||||
}
|
||||
|
||||
#valueInput {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 130%;
|
||||
line-height: 1.2;
|
||||
flex: 1;
|
||||
margin: 0 12px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#valueInput::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 20px;
|
||||
background: linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
|
||||
}
|
||||
|
||||
.field-buttons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.field-button {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
font-size: 110%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
:host(:not(.touch):not(:hover)) .field-buttons {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.field-button:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container tap" on-click="_fieldClicked">
|
||||
<div class="name">[[ field.name ]]</div>
|
||||
<pl-input multiline="" id="valueInput" value="[[ field.value ]]" disabled="" placeholder="[[ \$l('No Content') ]]" masked="[[ field.masked ]]"></pl-input>
|
||||
</div>
|
||||
|
||||
<div class="field-buttons">
|
||||
<pl-icon icon="[[ _toggleMaskIcon(field.masked) ]]" class="field-button tap" on-click="_toggleMask" hidden\$="[[ !_hasValue(field.value) ]]"></pl-icon>
|
||||
<pl-icon icon="copy" class="field-button tap" on-click="_copy" hidden\$="[[ !_hasValue(field.value) ]]"></pl-icon>
|
||||
<pl-icon icon="edit" class="field-button tap" on-click="_edit" hidden\$="[[ _hasValue(field.value) ]]"></pl-icon>
|
||||
<pl-icon icon="generate" class="field-button tap" on-click="_showGenerator" hidden\$="[[ _hasValue(field.value) ]]"></pl-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-record-field"; }
|
||||
|
||||
static get properties() { return {
|
||||
_masked: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
record: Object,
|
||||
field: {
|
||||
type: Object,
|
||||
value: () => { return { name: "", value: "", masked: false }; }
|
||||
}
|
||||
}; }
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.classList.toggle("touch", isTouch());
|
||||
}
|
||||
|
||||
_delete() {
|
||||
this.dispatchEvent(new CustomEvent("field-delete", { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
_showGenerator() {
|
||||
return this.generate()
|
||||
.then((value) => {
|
||||
this.set("field.value", value);
|
||||
this.dispatchEvent(new CustomEvent("field-change"));
|
||||
});
|
||||
}
|
||||
|
||||
_copy() {
|
||||
this.setClipboard(this.record, this.field);
|
||||
}
|
||||
|
||||
_toggleMaskIcon() {
|
||||
return this.field.masked ? "show" : "hide";
|
||||
}
|
||||
|
||||
_toggleMask() {
|
||||
this.set("field.masked", !this.field.masked);
|
||||
this.dispatchEvent(new CustomEvent("field-change"));
|
||||
}
|
||||
|
||||
_openFieldDialog(edit, presets) {
|
||||
this.lineUpDialog("pl-record-field-dialog", (d) => d.openField(this.field, edit, presets))
|
||||
.then((result) => {
|
||||
switch (result.action) {
|
||||
case "copy":
|
||||
this._copy();
|
||||
break;
|
||||
case "generate":
|
||||
this.generate()
|
||||
.then((value) => {
|
||||
this._openFieldDialog(true, { name: result.name, value: value || result.value });
|
||||
});
|
||||
break;
|
||||
case "delete":
|
||||
this._delete();
|
||||
break;
|
||||
case "edited":
|
||||
this.notifyPath("field.name");
|
||||
this.notifyPath("field.value");
|
||||
setTimeout(() => this.dispatchEvent(new CustomEvent("field-change"), 500));
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_hasValue() {
|
||||
return !!this.field.value;
|
||||
}
|
||||
|
||||
_fieldClicked() {
|
||||
this._openFieldDialog();
|
||||
}
|
||||
|
||||
_edit() {
|
||||
this._openFieldDialog(true);
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(RecordField.is, RecordField);
|
|
@ -1,303 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../animation/animation.html">
|
||||
<link rel="import" href="../data/data.html">
|
||||
<link rel="import" href="../dialog/dialog-mixin.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
<link rel="import" href="../input/input.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="record-field.html">
|
||||
<link rel="import" href="record-field-dialog.html">
|
||||
|
||||
<dom-module id="pl-record-view">
|
||||
<template>
|
||||
|
||||
<style include="shared">
|
||||
:host {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
@apply --fullbleed;
|
||||
transition: background 0.5s;
|
||||
background: var(--color-quaternary);
|
||||
}
|
||||
|
||||
#background {
|
||||
@apply --fullbleed;
|
||||
}
|
||||
|
||||
header > pl-input {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
pl-record-field, .add-button-wrapper {
|
||||
transform: translate3d(0, 0, 0);
|
||||
margin: 6px;
|
||||
@apply --card;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.add-button pl-icon {
|
||||
width: 30px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
margin: 8px 0;
|
||||
padding: 0 8px;
|
||||
/* align-items: center; */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.tags::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 1px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: var(--color-foreground);
|
||||
color: var(--color-background);
|
||||
font-weight: bold;
|
||||
border-radius: var(--border-radius);
|
||||
margin-right: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-tiny);
|
||||
white-space: nowrap;
|
||||
line-height: 0;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.tags pl-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.tag.add {
|
||||
padding-left: 0;
|
||||
padding-right: 12px;
|
||||
border: dashed 1px;
|
||||
background: transparent;
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
</style>
|
||||
|
||||
<header>
|
||||
<pl-icon icon="close" class="tap" on-click="close"></pl-icon>
|
||||
<pl-input id="nameInput" class="name tap" value="{{ record.name }}"
|
||||
placeholder="[[ $l('Enter Record Name') ]]" select-on-focus autocapitalize
|
||||
on-change="_notifyChange" on-enter="_nameEnter"
|
||||
></pl-input>
|
||||
<pl-icon icon="delete" class="tap" on-click="_deleteRecord"></pl-icon>
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
<div class="tags animate">
|
||||
<template is="dom-repeat" items="[[ record.tags ]]">
|
||||
<div class="tag tap" on-click="_removeTag">
|
||||
<div class="tag-name">[[ item ]]</div>
|
||||
<pl-icon icon="cancel"></pl-icon>
|
||||
</div>
|
||||
</template>
|
||||
<div class="tag add tap" on-click="_addTag">
|
||||
<pl-icon icon="tag"></pl-icon>
|
||||
<div>[[ $l("Add Tag") ]]</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fields">
|
||||
<template is="dom-repeat" items="[[ record.fields ]]" id="fieldList">
|
||||
<div class="animate">
|
||||
<pl-record-field field="[[ item ]]" record="[[ record ]]"
|
||||
on-field-change="_notifyChange"
|
||||
on-field-delete="_deleteField"></pl-record-field>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="add-button-wrapper animate">
|
||||
<button class="add-button tap" on-click="_addField">
|
||||
<pl-icon icon="add"></pl-icon>
|
||||
<div>[[ $l("Add Field") ]]</div>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="rounded-corners"></div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
const { LocaleMixin, DialogMixin, DataMixin, AnimationMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
|
||||
class RecordView extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
AnimationMixin
|
||||
) {
|
||||
|
||||
static get is() { return "pl-record-view"; }
|
||||
|
||||
static get properties() { return {
|
||||
record: {
|
||||
type: Object,
|
||||
notify: true,
|
||||
observer: "_recordChanged"
|
||||
},
|
||||
_edited: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
|
||||
dataUnloaded() {
|
||||
this.record = null;
|
||||
const fieldDialog = this.getSingleton("pl-record-field-dialog");
|
||||
fieldDialog.open = false;
|
||||
fieldDialog.field = null;
|
||||
}
|
||||
|
||||
_recordChanged() {
|
||||
setTimeout(() => {
|
||||
if (this.record && !this.record.name) {
|
||||
this.$.nameInput.focus();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
_notifyChange() {
|
||||
this.dispatch("record-changed", this.record);
|
||||
this.notifyPath("record");
|
||||
}
|
||||
|
||||
_deleteField(e) {
|
||||
this.confirm($l("Are you sure you want to delete this field?"), $l("Delete")).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.splice("record.fields", e.model.index, 1);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_deleteRecord() {
|
||||
this.confirm($l("Are you sure you want to delete this record?"), $l("Delete")).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteRecord(this.record);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_addField(field = { name: "", value: "", masked: false }) {
|
||||
this.lineUpDialog("pl-record-field-dialog", (d) => d.openField(field, true))
|
||||
.then((result) => {
|
||||
switch (result.action) {
|
||||
case "generate":
|
||||
this.generate()
|
||||
.then((value) => {
|
||||
field.value = value;
|
||||
field.name = result.name;
|
||||
this._addField(field);
|
||||
});
|
||||
break;
|
||||
case "edited":
|
||||
this.push("record.fields", field);
|
||||
this._notifyChange();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_fieldButtonClicked() {
|
||||
this._addField();
|
||||
}
|
||||
|
||||
_hasTags() {
|
||||
return !!this.record.tags.length;
|
||||
}
|
||||
|
||||
_removeTag(e) {
|
||||
this.confirm($l("Do you want to remove this tag?"), $l("Remove"), $l("Cancel"), { title: $l("Remove Tag") })
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.record.removeTag(e.model.item);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_createTag() {
|
||||
return this.prompt("", $l("Enter Tag Name"), "text", $l("Add Tag"), false, false)
|
||||
.then((tag) => {
|
||||
if (tag) {
|
||||
this.record.addTag(tag);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_addTag() {
|
||||
const tags = this.collection.tags.filter((tag) => !this.record.hasTag(tag));
|
||||
if (!tags.length) {
|
||||
return this._createTag();
|
||||
}
|
||||
|
||||
return this.choose("", tags.concat([$l("New Tag")]), { preventDismiss: false })
|
||||
.then((choice) => {
|
||||
if (choice == tags.length) {
|
||||
return this._createTag();
|
||||
}
|
||||
|
||||
const tag = tags[choice];
|
||||
if (tag) {
|
||||
this.record.addTag(tag);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_nameEnter() {
|
||||
this.$.nameInput.blur();
|
||||
}
|
||||
|
||||
animate() {
|
||||
setTimeout(() => {
|
||||
this.animateCascade(this.root.querySelectorAll(".animate"), { fullDuration: 800, fill: "both" });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dispatchEvent(new CustomEvent("record-close"));
|
||||
}
|
||||
|
||||
edit() {
|
||||
this.$.nameInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(RecordView.is, RecordView);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,288 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import '../animation/animation.js';
|
||||
import '../data/data.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../icon/icon.js';
|
||||
import '../input/input.js';
|
||||
import '../locale/locale.js';
|
||||
import './record-field.js';
|
||||
import './record-field-dialog.js';
|
||||
|
||||
const { LocaleMixin, DialogMixin, DataMixin, AnimationMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
|
||||
class RecordView extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
AnimationMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
@apply --fullbleed;
|
||||
transition: background 0.5s;
|
||||
background: var(--color-quaternary);
|
||||
}
|
||||
|
||||
#background {
|
||||
@apply --fullbleed;
|
||||
}
|
||||
|
||||
header > pl-input {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
pl-record-field, .add-button-wrapper {
|
||||
transform: translate3d(0, 0, 0);
|
||||
margin: 6px;
|
||||
@apply --card;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.add-button pl-icon {
|
||||
width: 30px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
margin: 8px 0;
|
||||
padding: 0 8px;
|
||||
/* align-items: center; */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.tags::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 1px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: var(--color-foreground);
|
||||
color: var(--color-background);
|
||||
font-weight: bold;
|
||||
border-radius: var(--border-radius);
|
||||
margin-right: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-tiny);
|
||||
white-space: nowrap;
|
||||
line-height: 0;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.tags pl-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.tag.add {
|
||||
padding-left: 0;
|
||||
padding-right: 12px;
|
||||
border: dashed 1px;
|
||||
background: transparent;
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
</style>
|
||||
|
||||
<header>
|
||||
<pl-icon icon="close" class="tap" on-click="close"></pl-icon>
|
||||
<pl-input id="nameInput" class="name tap" value="{{ record.name }}" placeholder="[[ \$l('Enter Record Name') ]]" select-on-focus="" autocapitalize="" on-change="_notifyChange" on-enter="_nameEnter"></pl-input>
|
||||
<pl-icon icon="delete" class="tap" on-click="_deleteRecord"></pl-icon>
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
<div class="tags animate">
|
||||
<template is="dom-repeat" items="[[ record.tags ]]">
|
||||
<div class="tag tap" on-click="_removeTag">
|
||||
<div class="tag-name">[[ item ]]</div>
|
||||
<pl-icon icon="cancel"></pl-icon>
|
||||
</div>
|
||||
</template>
|
||||
<div class="tag add tap" on-click="_addTag">
|
||||
<pl-icon icon="tag"></pl-icon>
|
||||
<div>[[ \$l("Add Tag") ]]</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fields">
|
||||
<template is="dom-repeat" items="[[ record.fields ]]" id="fieldList">
|
||||
<div class="animate">
|
||||
<pl-record-field field="[[ item ]]" record="[[ record ]]" on-field-change="_notifyChange" on-field-delete="_deleteField"></pl-record-field>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="add-button-wrapper animate">
|
||||
<button class="add-button tap" on-click="_addField">
|
||||
<pl-icon icon="add"></pl-icon>
|
||||
<div>[[ \$l("Add Field") ]]</div>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="rounded-corners"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-record-view"; }
|
||||
|
||||
static get properties() { return {
|
||||
record: {
|
||||
type: Object,
|
||||
notify: true,
|
||||
observer: "_recordChanged"
|
||||
},
|
||||
_edited: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
}; }
|
||||
|
||||
dataUnloaded() {
|
||||
this.record = null;
|
||||
const fieldDialog = this.getSingleton("pl-record-field-dialog");
|
||||
fieldDialog.open = false;
|
||||
fieldDialog.field = null;
|
||||
}
|
||||
|
||||
_recordChanged() {
|
||||
setTimeout(() => {
|
||||
if (this.record && !this.record.name) {
|
||||
this.$.nameInput.focus();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
_notifyChange() {
|
||||
this.dispatch("record-changed", this.record);
|
||||
this.notifyPath("record");
|
||||
}
|
||||
|
||||
_deleteField(e) {
|
||||
this.confirm($l("Are you sure you want to delete this field?"), $l("Delete")).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.splice("record.fields", e.model.index, 1);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_deleteRecord() {
|
||||
this.confirm($l("Are you sure you want to delete this record?"), $l("Delete")).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteRecord(this.record);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_addField(field = { name: "", value: "", masked: false }) {
|
||||
this.lineUpDialog("pl-record-field-dialog", (d) => d.openField(field, true))
|
||||
.then((result) => {
|
||||
switch (result.action) {
|
||||
case "generate":
|
||||
this.generate()
|
||||
.then((value) => {
|
||||
field.value = value;
|
||||
field.name = result.name;
|
||||
this._addField(field);
|
||||
});
|
||||
break;
|
||||
case "edited":
|
||||
this.push("record.fields", field);
|
||||
this._notifyChange();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_fieldButtonClicked() {
|
||||
this._addField();
|
||||
}
|
||||
|
||||
_hasTags() {
|
||||
return !!this.record.tags.length;
|
||||
}
|
||||
|
||||
_removeTag(e) {
|
||||
this.confirm($l("Do you want to remove this tag?"), $l("Remove"), $l("Cancel"), { title: $l("Remove Tag") })
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.record.removeTag(e.model.item);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_createTag() {
|
||||
return this.prompt("", $l("Enter Tag Name"), "text", $l("Add Tag"), false, false)
|
||||
.then((tag) => {
|
||||
if (tag) {
|
||||
this.record.addTag(tag);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_addTag() {
|
||||
const tags = this.collection.tags.filter((tag) => !this.record.hasTag(tag));
|
||||
if (!tags.length) {
|
||||
return this._createTag();
|
||||
}
|
||||
|
||||
return this.choose("", tags.concat([$l("New Tag")]), { preventDismiss: false })
|
||||
.then((choice) => {
|
||||
if (choice == tags.length) {
|
||||
return this._createTag();
|
||||
}
|
||||
|
||||
const tag = tags[choice];
|
||||
if (tag) {
|
||||
this.record.addTag(tag);
|
||||
this._notifyChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_nameEnter() {
|
||||
this.$.nameInput.blur();
|
||||
}
|
||||
|
||||
animate() {
|
||||
setTimeout(() => {
|
||||
this.animateCascade(this.root.querySelectorAll(".animate"), { fullDuration: 800, fill: "both" });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dispatchEvent(new CustomEvent("record-close"));
|
||||
}
|
||||
|
||||
edit() {
|
||||
this.$.nameInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(RecordView.is, RecordView);
|
|
@ -1,543 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../animation/animation.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../data/data.html">
|
||||
<link rel="import" href="../dialog/dialog-mixin.html">
|
||||
<link rel="import" href="../export/export.html">
|
||||
<link rel="import" href="../icon/icon.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
<link rel="import" href="../slider/slider.html">
|
||||
<link rel="import" href="../sync/sync.html">
|
||||
<link rel="import" href="../toggle/toggle-button.html">
|
||||
|
||||
<dom-module id="pl-settings-view">
|
||||
|
||||
<template>
|
||||
|
||||
<style include="shared">
|
||||
@keyframes beat {
|
||||
0% { transform: scale(1); }
|
||||
5% { transform: scale(1.4); }
|
||||
15% { transform: scale(1); }
|
||||
}
|
||||
|
||||
:host {
|
||||
@apply --fullbleed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
background: var(--color-quaternary);
|
||||
}
|
||||
|
||||
section {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
section .info {
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
text-align: center;
|
||||
line-height: normal;
|
||||
padding: 15px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button > pl-icon {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
pl-toggle-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.padlock-heart {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
animation: beat 5s infinite;
|
||||
}
|
||||
|
||||
.padlock-heart::before {
|
||||
font-family: "FontAwesome";
|
||||
content: "\f004";
|
||||
}
|
||||
|
||||
.made-in {
|
||||
font-size: var(--font-size-tiny);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.db-path {
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
word-wrap: break-word;
|
||||
font-size: var(--font-size-tiny);
|
||||
}
|
||||
|
||||
#customUrlInput:not([invalid]) + .url-warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.url-warning {
|
||||
font-size: var(--font-size-small);
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
border-top: solid 1px var(--border-color);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.feature-locked {
|
||||
font-size: var(--font-size-tiny);
|
||||
color: var(--color-error);
|
||||
margin: -14px 15px 12px 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<header>
|
||||
<pl-icon icon="close" class="tap" on-click="_back"></pl-icon>
|
||||
<div class="title">[[ $l("Settings") ]]</div>
|
||||
<pl-icon></pl-icon>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<section>
|
||||
<div class="section-header">[[ $l("Auto Lock") ]]</div>
|
||||
<pl-toggle-button active="{{ settings.autoLock }}" label="[[ $l('Lock Automatically') ]]" class="tap"
|
||||
reverse on-change="settingChanged"></pl-toggle-button>
|
||||
<pl-slider min="1" max="10" value="{{ settings.autoLockDelay }}" step="1" unit="[[ $l(' min') ]]"
|
||||
label="[[ $l('After') ]]" hidden$="{{ !settings.autoLock }}" on-change="settingChanged"></pl-slider>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-header">[[ $l("Synchronization") ]]</div>
|
||||
<div disabled$="[[ !isSubValid(settings.syncConnected, settings.syncSubStatus) ]]">
|
||||
<pl-toggle-button active="{{ settings.syncAuto }}" label="[[ $l('Sync Automatically') ]]"
|
||||
reverse class="tap" on-change="settingChanged"></pl-toggle-button>
|
||||
<div class="feature-locked" hidden$="[[ settings.syncConnected ]]">[[ $l("Log in to enable auto sync!") ]]</div>
|
||||
<div class="feature-locked" hidden$="[[ !isTrialExpired(settings.syncConnected, settings.syncSubStatus) ]]">[[ $l("Upgrade to enable auto sync!") ]]</div>
|
||||
<div class="feature-locked" hidden$="[[ !isSubCanceled(settings.syncConnected, settings.syncSubStatus) ]]">[[ $l("Upgrade to enable auto sync!") ]]</div>
|
||||
</div>
|
||||
<pl-toggle-button active="{{ settings.syncCustomHost }}" label="[[ $l('Use Custom Server') ]]" reverse
|
||||
on-change="_customHostChanged" class="tap" disabled$="[[ settings.syncConnected ]]"></pl-toggle-button>
|
||||
<div class="tap" hidden$="[[ !settings.syncCustomHost ]]" disabled$="[[ settings.syncConnected ]]">
|
||||
<pl-input id="customUrlInput" placeholder="[[ $l('Enter Custom URL') ]]"
|
||||
value="{{ settings.syncHostUrl }}" pattern="^https://[^\s/$.?#].[^\s]*$"
|
||||
required on-change="settingChanged"></pl-input>
|
||||
<div class="url-warning">
|
||||
<strong>[[ $l("Invalid URL") ]]</strong> -
|
||||
[[ $l("Make sure that the URL is of the form https://myserver.tld:port. Note that a https connection is required.") ]]
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<button on-click="_changePassword" class="tap">[[ $l("Change Master Password") ]]</button>
|
||||
<button on-click="_resetData" class="tap">[[ $l("Reset App") ]]</button>
|
||||
<button class="tap" on-click="_import">[[ $l("Import...") ]]</button>
|
||||
<button class="tap" on-click="_export">[[ $l("Export...") ]]</div>
|
||||
</section>
|
||||
|
||||
<section hidden$="[[ !_isDesktop() ]]">
|
||||
<div class="section-header">[[ $l("Updates") ]]</div>
|
||||
<pl-toggle-button id="autoUpdatesButton" label="[[ $l('Automatically Install Updates') ]]" class="tap"
|
||||
reverse on-change="_desktopSettingsChanged"></pl-toggle-button>
|
||||
<pl-toggle-button id="betaReleasesButton" label="[[ $l('Install Beta Releases') ]]" class="tap"
|
||||
reverse on-change="_desktopSettingsChanged"></pl-toggle-button>
|
||||
<button on-click="_checkForUpdates" class="tap">[[ $l("Check For Updates...") ]]</button>
|
||||
</section>
|
||||
|
||||
<section hidden$="[[ !_isDesktop() ]]">
|
||||
<div class="section-header">[[ $l("Database") ]]</div>
|
||||
<div class="info">
|
||||
<div>[[ $l("Current Location:") ]]</div>
|
||||
<div class="db-path">[[ _dbPath ]]</div>
|
||||
</div>
|
||||
<button on-click="_saveDBAs" class="tap">[[ $l("Change Location...") ]]</button>
|
||||
<button on-click="_loadDB" class="tap">[[ $l("Load Different Database...") ]]</button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<button class="info tap" on-click="_openSource">
|
||||
<div><strong>Padlock {{ settings.version }}</strong></div>
|
||||
<div class="made-in">Made with ♥ in Germany</div>
|
||||
</button>
|
||||
<button on-click="_openWebsite" class="tap">[[ $l("Website") ]]</button>
|
||||
<button on-click="_sendMail" class="tap">[[ $l("Support") ]]</button>
|
||||
<button on-click="_promptReview" class="tap">
|
||||
<span>[[ $l("I") ]]</span><div class="padlock-heart"></div><span>Padlock</span>
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="rounded-corners"></div>
|
||||
|
||||
<input type="file" name="importFile" id="importFile" on-change="_importFile" accept="text/plain,.csv,.pls,.set" hidden>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
const { LocaleMixin, DialogMixin, DataMixin, AnimationMixin, SyncMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
const { isCordova, getReviewLink, isTouch, getDesktopSettings, checkForUpdates,
|
||||
saveDBAs, loadDB, isElectron } = padlock.platform;
|
||||
|
||||
class SettingsView extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
AnimationMixin,
|
||||
SyncMixin
|
||||
) {
|
||||
|
||||
static get is() { return "pl-settings-view"; }
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (isElectron()) {
|
||||
const desktopSettings = getDesktopSettings().get();
|
||||
this.$.autoUpdatesButton.active = desktopSettings.autoDownloadUpdates;
|
||||
this.$.betaReleasesButton.active = desktopSettings.allowPrerelease;
|
||||
this._dbPath = desktopSettings.dbPath;
|
||||
}
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.animateCascade(this.root.querySelectorAll("section"), { initialDelay: 200 });
|
||||
}
|
||||
|
||||
_back() {
|
||||
this.dispatchEvent(new CustomEvent("settings-back"));
|
||||
}
|
||||
|
||||
//* Opens the change password dialog and resets the corresponding input elements
|
||||
_changePassword() {
|
||||
let newPwd;
|
||||
return this.promptPassword(this.password, $l(
|
||||
"Are you sure you want to change your master password? Enter your " +
|
||||
"current password to continue!"
|
||||
), $l("Confirm"), $l("Cancel"))
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
return this.prompt($l("Now choose a new master password!"),
|
||||
$l("Enter New Password"), "password", $l("Confirm"), $l("Cancel"), false, (val) => {
|
||||
if (val === "") {
|
||||
return Promise.reject($l("Please enter a password!"));
|
||||
}
|
||||
return Promise.resolve(val);
|
||||
});
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
.then((pwd) => {
|
||||
if (pwd === null) {
|
||||
return Promise.reject();
|
||||
}
|
||||
newPwd = pwd;
|
||||
return this.promptPassword(pwd, $l("Confirm your new master password!"),
|
||||
$l("Confirm"), $l("Cancel"));
|
||||
})
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
return this.setPassword(newPwd);
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (this.settings.syncConnected) {
|
||||
return this.confirm(
|
||||
$l("Do you want to update the password for you online account {0} as well?",
|
||||
this.settings.syncEmail),
|
||||
$l("Yes"), $l("No")
|
||||
).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.setRemotePassword(this.password);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.alert($l("Master password changed successfully."), { type: "success" });
|
||||
});
|
||||
}
|
||||
|
||||
_openWebsite() {
|
||||
window.open("https://padlock.io", "_system");
|
||||
}
|
||||
|
||||
_sendMail() {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
|
||||
_openGithub() {
|
||||
window.open("https://github.com/maklesoft/padlock/", "_system");
|
||||
}
|
||||
|
||||
_resetData() {
|
||||
this.promptPassword(this.password,
|
||||
$l(
|
||||
"Are you sure you want to delete all your data and reset the app? Enter your " +
|
||||
"master password to continue!"
|
||||
),
|
||||
$l("Reset App")
|
||||
)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
return this.resetData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_import() {
|
||||
const options = [
|
||||
$l("From Clipboard")
|
||||
];
|
||||
if (!isCordova()) {
|
||||
options.push($l("From File"));
|
||||
}
|
||||
this.choose($l("Please choose an import method!"), options, { preventDismiss: false, type: "question" })
|
||||
.then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this._importFromClipboard();
|
||||
break;
|
||||
case 1:
|
||||
this.$.importFile.click();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_importFile() {
|
||||
const file = this.$.importFile.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this._importString(reader.result)
|
||||
.catch((e) => {
|
||||
switch (e.code) {
|
||||
case "decryption_failed":
|
||||
this.alert($l("Failed to open file. Did you enter the correct password?"),
|
||||
{ type: "warning" });
|
||||
break;
|
||||
case "unsupported_container_version":
|
||||
this.confirm($l(
|
||||
"It seems the data you are trying to import was exported from a " +
|
||||
"newer version of Padlock and can not be opened with the version you are " +
|
||||
"currently running."
|
||||
), $l("Check For Updates"), $l("Cancel"), { type: "info" })
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
padlock.platform.checkForUpdates();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "invalid_csv":
|
||||
this.alert($l("Failed to recognize file format."), { type: "warning" });
|
||||
break;
|
||||
default:
|
||||
this.alert($l("Failed to open file."), { type: "warning" });
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
this.$.importFile.value = "";
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
_importFromClipboard() {
|
||||
padlock.platform.getClipboard()
|
||||
.then((str) => this._importString(str))
|
||||
.catch((e) => {
|
||||
switch (e.code) {
|
||||
case "decryption_failed":
|
||||
this.alert($l("Failed to decrypt data. Did you enter the correct password?"),
|
||||
{ type: "warning" });
|
||||
break;
|
||||
default:
|
||||
this.alert($l("No supported data found in clipboard. Please make sure to copy " +
|
||||
"you data to the clipboard first (e.g. via ctrl + C)."), { type: "warning" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_importString(rawStr) {
|
||||
const imp = padlock.imp;
|
||||
const isPadlock = imp.isFromPadlock(rawStr);
|
||||
const isSecuStore = imp.isFromSecuStore(rawStr);
|
||||
const isLastPass = imp.isFromLastPass(rawStr);
|
||||
const isCSV = imp.isCSV(rawStr);
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (isPadlock || isSecuStore) {
|
||||
return this.prompt($l("This file is protected by a password."),
|
||||
$l("Enter Password"), "password", $l("Confirm"), $l("Cancel"));
|
||||
}
|
||||
})
|
||||
.then((pwd) => {
|
||||
if (pwd === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPadlock) {
|
||||
return imp.fromPadlock(rawStr, pwd);
|
||||
} else if (isSecuStore) {
|
||||
return imp.fromSecuStore(rawStr, pwd);
|
||||
} else if (isLastPass) {
|
||||
return imp.fromLastPass(rawStr);
|
||||
} else if (isCSV) {
|
||||
return this.choose($l(
|
||||
"The data you want to import seems to be in CSV format. Before you continue, " +
|
||||
"please make sure that the data is structured according to Padlocks specific " +
|
||||
"requirements!"
|
||||
), [$l("Review Import Guidelines"), $l("Continue"), $l("Cancel")], { type: "info" })
|
||||
.then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
window.open("https://padlock.io/howto/import/#importing-from-csv", "_system");
|
||||
// Reopen dialog for when the user comes back from the web page
|
||||
return this._importString(rawStr);
|
||||
case 1:
|
||||
return imp.fromCSV(rawStr);
|
||||
case 2:
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
throw new imp.ImportError("invalid_csv");
|
||||
}
|
||||
})
|
||||
.then((records) => {
|
||||
if (records) {
|
||||
this.addRecords(records);
|
||||
this.dispatch("data-imported", { records: records });
|
||||
this.alert($l("Successfully imported {0} records.", records.length), { type: "success" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_isMobile() {
|
||||
return isCordova();
|
||||
}
|
||||
|
||||
_isTouch() {
|
||||
return isTouch();
|
||||
}
|
||||
|
||||
_openSource() {
|
||||
window.open("https://github.com/maklesoft/padlock/", "_system");
|
||||
}
|
||||
|
||||
_autoLockInfo() {
|
||||
return this.$l(
|
||||
"Tell Padlock to automatically lock the app after a certain period of " +
|
||||
"inactivity in case you leave your device unattended for a while."
|
||||
);
|
||||
}
|
||||
|
||||
_peekValuesInfo() {
|
||||
return this.$l(
|
||||
"If enabled allows peeking at field values in the record list " +
|
||||
"by moving the mouse cursor over the corresponding field."
|
||||
);
|
||||
}
|
||||
|
||||
_resetDataInfo() {
|
||||
return this.$l(
|
||||
"Want to start fresh? Reseting Padlock will delete all your locally stored data and settings " +
|
||||
"and will restore the app to the state it was when you first launched it."
|
||||
);
|
||||
}
|
||||
|
||||
_promptReview() {
|
||||
this.choose($l(
|
||||
"So glad to hear you like our app! Would you mind taking a second to " +
|
||||
"let others know what you think about Padlock?"
|
||||
), [$l("Rate Padlock"), $l("No Thanks")])
|
||||
.then((choice) => {
|
||||
if (choice === 0) {
|
||||
getReviewLink(0).then((link) => window.open(link, "_system"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_isDesktop() {
|
||||
return isElectron();
|
||||
}
|
||||
|
||||
_desktopSettingsChanged() {
|
||||
getDesktopSettings().set({
|
||||
autoDownloadUpdates: this.$.autoUpdatesButton.active,
|
||||
allowPrerelease: this.$.betaReleasesButton.active
|
||||
});
|
||||
}
|
||||
|
||||
_checkForUpdates() {
|
||||
checkForUpdates();
|
||||
}
|
||||
|
||||
_saveDBAs() {
|
||||
saveDBAs();
|
||||
}
|
||||
|
||||
_loadDB() {
|
||||
loadDB();
|
||||
}
|
||||
|
||||
_export() {
|
||||
const exportDialog = this.getSingleton("pl-export-dialog");
|
||||
exportDialog.export(this.records);
|
||||
}
|
||||
|
||||
_autoSyncInfoText() {
|
||||
return $l(
|
||||
"Enable Auto Sync to automatically synchronize your data with " +
|
||||
"your Padlock online account every time you make a change!"
|
||||
);
|
||||
}
|
||||
|
||||
_customHostChanged() {
|
||||
if (this.settings.syncCustomHost) {
|
||||
this.confirm(
|
||||
$l("Are you sure you want to use a custom server for synchronization? " +
|
||||
"This option is only recommended for advanced users!"),
|
||||
$l("Continue"))
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.settingChanged();
|
||||
} else {
|
||||
this.set("settings.syncCustomHost", false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.settingChanged();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.customElements.define(SettingsView.is, SettingsView);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,523 @@
|
|||
import '../../styles/shared.js';
|
||||
import '../animation/animation.js';
|
||||
import '../base/base.js';
|
||||
import '../data/data.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../export/export.js';
|
||||
import '../icon/icon.js';
|
||||
import '../locale/locale.js';
|
||||
import '../slider/slider.js';
|
||||
import '../sync/sync.js';
|
||||
import '../toggle/toggle-button.js';
|
||||
|
||||
const { LocaleMixin, DialogMixin, DataMixin, AnimationMixin, SyncMixin, BaseElement } = padlock;
|
||||
const { applyMixins } = padlock.util;
|
||||
const { isCordova, getReviewLink, isTouch, getDesktopSettings, checkForUpdates,
|
||||
saveDBAs, loadDB, isElectron } = padlock.platform;
|
||||
|
||||
class SettingsView extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
AnimationMixin,
|
||||
SyncMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
@keyframes beat {
|
||||
0% { transform: scale(1); }
|
||||
5% { transform: scale(1.4); }
|
||||
15% { transform: scale(1); }
|
||||
}
|
||||
|
||||
:host {
|
||||
@apply --fullbleed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
background: var(--color-quaternary);
|
||||
}
|
||||
|
||||
section {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
section .info {
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
text-align: center;
|
||||
line-height: normal;
|
||||
padding: 15px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button > pl-icon {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
pl-toggle-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.padlock-heart {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
animation: beat 5s infinite;
|
||||
}
|
||||
|
||||
.padlock-heart::before {
|
||||
font-family: "FontAwesome";
|
||||
content: "\\f004";
|
||||
}
|
||||
|
||||
.made-in {
|
||||
font-size: var(--font-size-tiny);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.db-path {
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
word-wrap: break-word;
|
||||
font-size: var(--font-size-tiny);
|
||||
}
|
||||
|
||||
#customUrlInput:not([invalid]) + .url-warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.url-warning {
|
||||
font-size: var(--font-size-small);
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
border-top: solid 1px var(--border-color);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.feature-locked {
|
||||
font-size: var(--font-size-tiny);
|
||||
color: var(--color-error);
|
||||
margin: -14px 15px 12px 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<header>
|
||||
<pl-icon icon="close" class="tap" on-click="_back"></pl-icon>
|
||||
<div class="title">[[ \$l("Settings") ]]</div>
|
||||
<pl-icon></pl-icon>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<section>
|
||||
<div class="section-header">[[ \$l("Auto Lock") ]]</div>
|
||||
<pl-toggle-button active="{{ settings.autoLock }}" label="[[ \$l('Lock Automatically') ]]" class="tap" reverse="" on-change="settingChanged"></pl-toggle-button>
|
||||
<pl-slider min="1" max="10" value="{{ settings.autoLockDelay }}" step="1" unit="[[ \$l(' min') ]]" label="[[ \$l('After') ]]" hidden\$="{{ !settings.autoLock }}" on-change="settingChanged"></pl-slider>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-header">[[ \$l("Synchronization") ]]</div>
|
||||
<div disabled\$="[[ !isSubValid(settings.syncConnected, settings.syncSubStatus) ]]">
|
||||
<pl-toggle-button active="{{ settings.syncAuto }}" label="[[ \$l('Sync Automatically') ]]" reverse="" class="tap" on-change="settingChanged"></pl-toggle-button>
|
||||
<div class="feature-locked" hidden\$="[[ settings.syncConnected ]]">[[ \$l("Log in to enable auto sync!") ]]</div>
|
||||
<div class="feature-locked" hidden\$="[[ !isTrialExpired(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Upgrade to enable auto sync!") ]]</div>
|
||||
<div class="feature-locked" hidden\$="[[ !isSubCanceled(settings.syncConnected, settings.syncSubStatus) ]]">[[ \$l("Upgrade to enable auto sync!") ]]</div>
|
||||
</div>
|
||||
<pl-toggle-button active="{{ settings.syncCustomHost }}" label="[[ \$l('Use Custom Server') ]]" reverse="" on-change="_customHostChanged" class="tap" disabled\$="[[ settings.syncConnected ]]"></pl-toggle-button>
|
||||
<div class="tap" hidden\$="[[ !settings.syncCustomHost ]]" disabled\$="[[ settings.syncConnected ]]">
|
||||
<pl-input id="customUrlInput" placeholder="[[ \$l('Enter Custom URL') ]]" value="{{ settings.syncHostUrl }}" pattern="^https://[^\\s/\$.?#].[^\\s]*\$" required="" on-change="settingChanged"></pl-input>
|
||||
<div class="url-warning">
|
||||
<strong>[[ \$l("Invalid URL") ]]</strong> -
|
||||
[[ \$l("Make sure that the URL is of the form https://myserver.tld:port. Note that a https connection is required.") ]]
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<button on-click="_changePassword" class="tap">[[ \$l("Change Master Password") ]]</button>
|
||||
<button on-click="_resetData" class="tap">[[ \$l("Reset App") ]]</button>
|
||||
<button class="tap" on-click="_import">[[ \$l("Import...") ]]</button>
|
||||
<button class="tap" on-click="_export">[[ \$l("Export...") ]]
|
||||
</button></section>
|
||||
|
||||
<section hidden\$="[[ !_isDesktop() ]]">
|
||||
<div class="section-header">[[ \$l("Updates") ]]</div>
|
||||
<pl-toggle-button id="autoUpdatesButton" label="[[ \$l('Automatically Install Updates') ]]" class="tap" reverse="" on-change="_desktopSettingsChanged"></pl-toggle-button>
|
||||
<pl-toggle-button id="betaReleasesButton" label="[[ \$l('Install Beta Releases') ]]" class="tap" reverse="" on-change="_desktopSettingsChanged"></pl-toggle-button>
|
||||
<button on-click="_checkForUpdates" class="tap">[[ \$l("Check For Updates...") ]]</button>
|
||||
</section>
|
||||
|
||||
<section hidden\$="[[ !_isDesktop() ]]">
|
||||
<div class="section-header">[[ \$l("Database") ]]</div>
|
||||
<div class="info">
|
||||
<div>[[ \$l("Current Location:") ]]</div>
|
||||
<div class="db-path">[[ _dbPath ]]</div>
|
||||
</div>
|
||||
<button on-click="_saveDBAs" class="tap">[[ \$l("Change Location...") ]]</button>
|
||||
<button on-click="_loadDB" class="tap">[[ \$l("Load Different Database...") ]]</button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<button class="info tap" on-click="_openSource">
|
||||
<div><strong>Padlock {{ settings.version }}</strong></div>
|
||||
<div class="made-in">Made with ♥ in Germany</div>
|
||||
</button>
|
||||
<button on-click="_openWebsite" class="tap">[[ \$l("Website") ]]</button>
|
||||
<button on-click="_sendMail" class="tap">[[ \$l("Support") ]]</button>
|
||||
<button on-click="_promptReview" class="tap">
|
||||
<span>[[ \$l("I") ]]</span><div class="padlock-heart"></div><span>Padlock</span>
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="rounded-corners"></div>
|
||||
|
||||
<input type="file" name="importFile" id="importFile" on-change="_importFile" accept="text/plain,.csv,.pls,.set" hidden="">
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-settings-view"; }
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (isElectron()) {
|
||||
const desktopSettings = getDesktopSettings().get();
|
||||
this.$.autoUpdatesButton.active = desktopSettings.autoDownloadUpdates;
|
||||
this.$.betaReleasesButton.active = desktopSettings.allowPrerelease;
|
||||
this._dbPath = desktopSettings.dbPath;
|
||||
}
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.animateCascade(this.root.querySelectorAll("section"), { initialDelay: 200 });
|
||||
}
|
||||
|
||||
_back() {
|
||||
this.dispatchEvent(new CustomEvent("settings-back"));
|
||||
}
|
||||
|
||||
//* Opens the change password dialog and resets the corresponding input elements
|
||||
_changePassword() {
|
||||
let newPwd;
|
||||
return this.promptPassword(this.password, $l(
|
||||
"Are you sure you want to change your master password? Enter your " +
|
||||
"current password to continue!"
|
||||
), $l("Confirm"), $l("Cancel"))
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
return this.prompt($l("Now choose a new master password!"),
|
||||
$l("Enter New Password"), "password", $l("Confirm"), $l("Cancel"), false, (val) => {
|
||||
if (val === "") {
|
||||
return Promise.reject($l("Please enter a password!"));
|
||||
}
|
||||
return Promise.resolve(val);
|
||||
});
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
.then((pwd) => {
|
||||
if (pwd === null) {
|
||||
return Promise.reject();
|
||||
}
|
||||
newPwd = pwd;
|
||||
return this.promptPassword(pwd, $l("Confirm your new master password!"),
|
||||
$l("Confirm"), $l("Cancel"));
|
||||
})
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
return this.setPassword(newPwd);
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (this.settings.syncConnected) {
|
||||
return this.confirm(
|
||||
$l("Do you want to update the password for you online account {0} as well?",
|
||||
this.settings.syncEmail),
|
||||
$l("Yes"), $l("No")
|
||||
).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.setRemotePassword(this.password);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.alert($l("Master password changed successfully."), { type: "success" });
|
||||
});
|
||||
}
|
||||
|
||||
_openWebsite() {
|
||||
window.open("https://padlock.io", "_system");
|
||||
}
|
||||
|
||||
_sendMail() {
|
||||
window.open("mailto:support@padlock.io", "_system");
|
||||
}
|
||||
|
||||
_openGithub() {
|
||||
window.open("https://github.com/maklesoft/padlock/", "_system");
|
||||
}
|
||||
|
||||
_resetData() {
|
||||
this.promptPassword(this.password,
|
||||
$l(
|
||||
"Are you sure you want to delete all your data and reset the app? Enter your " +
|
||||
"master password to continue!"
|
||||
),
|
||||
$l("Reset App")
|
||||
)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
return this.resetData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_import() {
|
||||
const options = [
|
||||
$l("From Clipboard")
|
||||
];
|
||||
if (!isCordova()) {
|
||||
options.push($l("From File"));
|
||||
}
|
||||
this.choose($l("Please choose an import method!"), options, { preventDismiss: false, type: "question" })
|
||||
.then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this._importFromClipboard();
|
||||
break;
|
||||
case 1:
|
||||
this.$.importFile.click();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_importFile() {
|
||||
const file = this.$.importFile.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this._importString(reader.result)
|
||||
.catch((e) => {
|
||||
switch (e.code) {
|
||||
case "decryption_failed":
|
||||
this.alert($l("Failed to open file. Did you enter the correct password?"),
|
||||
{ type: "warning" });
|
||||
break;
|
||||
case "unsupported_container_version":
|
||||
this.confirm($l(
|
||||
"It seems the data you are trying to import was exported from a " +
|
||||
"newer version of Padlock and can not be opened with the version you are " +
|
||||
"currently running."
|
||||
), $l("Check For Updates"), $l("Cancel"), { type: "info" })
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
padlock.platform.checkForUpdates();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "invalid_csv":
|
||||
this.alert($l("Failed to recognize file format."), { type: "warning" });
|
||||
break;
|
||||
default:
|
||||
this.alert($l("Failed to open file."), { type: "warning" });
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
this.$.importFile.value = "";
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
_importFromClipboard() {
|
||||
padlock.platform.getClipboard()
|
||||
.then((str) => this._importString(str))
|
||||
.catch((e) => {
|
||||
switch (e.code) {
|
||||
case "decryption_failed":
|
||||
this.alert($l("Failed to decrypt data. Did you enter the correct password?"),
|
||||
{ type: "warning" });
|
||||
break;
|
||||
default:
|
||||
this.alert($l("No supported data found in clipboard. Please make sure to copy " +
|
||||
"you data to the clipboard first (e.g. via ctrl + C)."), { type: "warning" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_importString(rawStr) {
|
||||
const imp = padlock.imp;
|
||||
const isPadlock = imp.isFromPadlock(rawStr);
|
||||
const isSecuStore = imp.isFromSecuStore(rawStr);
|
||||
const isLastPass = imp.isFromLastPass(rawStr);
|
||||
const isCSV = imp.isCSV(rawStr);
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (isPadlock || isSecuStore) {
|
||||
return this.prompt($l("This file is protected by a password."),
|
||||
$l("Enter Password"), "password", $l("Confirm"), $l("Cancel"));
|
||||
}
|
||||
})
|
||||
.then((pwd) => {
|
||||
if (pwd === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPadlock) {
|
||||
return imp.fromPadlock(rawStr, pwd);
|
||||
} else if (isSecuStore) {
|
||||
return imp.fromSecuStore(rawStr, pwd);
|
||||
} else if (isLastPass) {
|
||||
return imp.fromLastPass(rawStr);
|
||||
} else if (isCSV) {
|
||||
return this.choose($l(
|
||||
"The data you want to import seems to be in CSV format. Before you continue, " +
|
||||
"please make sure that the data is structured according to Padlocks specific " +
|
||||
"requirements!"
|
||||
), [$l("Review Import Guidelines"), $l("Continue"), $l("Cancel")], { type: "info" })
|
||||
.then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
window.open("https://padlock.io/howto/import/#importing-from-csv", "_system");
|
||||
// Reopen dialog for when the user comes back from the web page
|
||||
return this._importString(rawStr);
|
||||
case 1:
|
||||
return imp.fromCSV(rawStr);
|
||||
case 2:
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
throw new imp.ImportError("invalid_csv");
|
||||
}
|
||||
})
|
||||
.then((records) => {
|
||||
if (records) {
|
||||
this.addRecords(records);
|
||||
this.dispatch("data-imported", { records: records });
|
||||
this.alert($l("Successfully imported {0} records.", records.length), { type: "success" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_isMobile() {
|
||||
return isCordova();
|
||||
}
|
||||
|
||||
_isTouch() {
|
||||
return isTouch();
|
||||
}
|
||||
|
||||
_openSource() {
|
||||
window.open("https://github.com/maklesoft/padlock/", "_system");
|
||||
}
|
||||
|
||||
_autoLockInfo() {
|
||||
return this.$l(
|
||||
"Tell Padlock to automatically lock the app after a certain period of " +
|
||||
"inactivity in case you leave your device unattended for a while."
|
||||
);
|
||||
}
|
||||
|
||||
_peekValuesInfo() {
|
||||
return this.$l(
|
||||
"If enabled allows peeking at field values in the record list " +
|
||||
"by moving the mouse cursor over the corresponding field."
|
||||
);
|
||||
}
|
||||
|
||||
_resetDataInfo() {
|
||||
return this.$l(
|
||||
"Want to start fresh? Reseting Padlock will delete all your locally stored data and settings " +
|
||||
"and will restore the app to the state it was when you first launched it."
|
||||
);
|
||||
}
|
||||
|
||||
_promptReview() {
|
||||
this.choose($l(
|
||||
"So glad to hear you like our app! Would you mind taking a second to " +
|
||||
"let others know what you think about Padlock?"
|
||||
), [$l("Rate Padlock"), $l("No Thanks")])
|
||||
.then((choice) => {
|
||||
if (choice === 0) {
|
||||
getReviewLink(0).then((link) => window.open(link, "_system"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_isDesktop() {
|
||||
return isElectron();
|
||||
}
|
||||
|
||||
_desktopSettingsChanged() {
|
||||
getDesktopSettings().set({
|
||||
autoDownloadUpdates: this.$.autoUpdatesButton.active,
|
||||
allowPrerelease: this.$.betaReleasesButton.active
|
||||
});
|
||||
}
|
||||
|
||||
_checkForUpdates() {
|
||||
checkForUpdates();
|
||||
}
|
||||
|
||||
_saveDBAs() {
|
||||
saveDBAs();
|
||||
}
|
||||
|
||||
_loadDB() {
|
||||
loadDB();
|
||||
}
|
||||
|
||||
_export() {
|
||||
const exportDialog = this.getSingleton("pl-export-dialog");
|
||||
exportDialog.export(this.records);
|
||||
}
|
||||
|
||||
_autoSyncInfoText() {
|
||||
return $l(
|
||||
"Enable Auto Sync to automatically synchronize your data with " +
|
||||
"your Padlock online account every time you make a change!"
|
||||
);
|
||||
}
|
||||
|
||||
_customHostChanged() {
|
||||
if (this.settings.syncCustomHost) {
|
||||
this.confirm(
|
||||
$l("Are you sure you want to use a custom server for synchronization? " +
|
||||
"This option is only recommended for advanced users!"),
|
||||
$l("Continue"))
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.settingChanged();
|
||||
} else {
|
||||
this.set("settings.syncCustomHost", false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.settingChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(SettingsView.is, SettingsView);
|
|
@ -1,9 +1,9 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
|
||||
<dom-module id="pl-slider">
|
||||
<template>
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
|
||||
class Slider extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: flex;
|
||||
|
@ -78,68 +78,59 @@
|
|||
<label>{{ label }}</label>
|
||||
<input type="range" value="{{ _strValue::input }}" min="{{ min }}" max="{{ max }}" step="{{ step }}" on-change="_inputChange">
|
||||
<span class="value-display">{{ value }}{{ unit }}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
<script>
|
||||
(() => {
|
||||
static get is() { return "pl-slider"; }
|
||||
|
||||
class Slider extends padlock.BaseElement {
|
||||
static get properties() { return {
|
||||
min: {
|
||||
type: Number,
|
||||
value: 1
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
value: 10
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
observer: "_valueChanged",
|
||||
notify: true
|
||||
},
|
||||
unit: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
value: 1
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
hideValue: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
_strValue: {
|
||||
type: String,
|
||||
observer: "_strValueChanged"
|
||||
}
|
||||
}; }
|
||||
|
||||
static get is() { return "pl-slider"; }
|
||||
_strValueChanged() {
|
||||
this.value = parseFloat(this._strValue, 10);
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
min: {
|
||||
type: Number,
|
||||
value: 1
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
value: 10
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
observer: "_valueChanged",
|
||||
notify: true
|
||||
},
|
||||
unit: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
value: 1
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
hideValue: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
_strValue: {
|
||||
type: String,
|
||||
observer: "_strValueChanged"
|
||||
}
|
||||
}; }
|
||||
|
||||
_strValueChanged() {
|
||||
this.value = parseFloat(this._strValue, 10);
|
||||
}
|
||||
|
||||
_valueChanged() {
|
||||
this._strValue = this.value.toString();
|
||||
}
|
||||
|
||||
_inputChange() {
|
||||
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
|
||||
}
|
||||
_valueChanged() {
|
||||
this._strValue = this.value.toString();
|
||||
}
|
||||
|
||||
_inputChange() {
|
||||
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Slider.is, Slider);
|
||||
|
||||
})();
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,911 +0,0 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../animation/animation.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../dialog/dialog-mixin.html">
|
||||
<link rel="import" href="../data/data.html">
|
||||
<link rel="import" href="../input/input.html">
|
||||
<link rel="import" href="../loading-button/loading-button.html">
|
||||
<link rel="import" href="../locale/locale.html">
|
||||
|
||||
<dom-module id="pl-start-view">
|
||||
|
||||
<template>
|
||||
|
||||
<style include="shared">
|
||||
|
||||
@keyframes reveal {
|
||||
from { transform: translate(0, 30px); opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
to { transform: translate(0, -200px); opacity: 0; }
|
||||
}
|
||||
|
||||
:host {
|
||||
--color-background: var(--color-primary);
|
||||
--color-foreground: var(--color-tertiary);
|
||||
--color-highlight: var(--color-secondary);
|
||||
@apply --fullbleed;
|
||||
@apply --scroll;
|
||||
color: var(--color-foreground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 5;
|
||||
text-align: center;
|
||||
text-shadow: rgba(0, 0, 0, 0.15) 0 2px 0;
|
||||
background: linear-gradient(180deg, #59c6ff 0%, #077cb9 100%);
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform 0.4s cubic-bezier(1, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
main {
|
||||
@apply --fullbleed;
|
||||
background: transparent;
|
||||
min-height: 510px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-box {
|
||||
width: 300px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: block;
|
||||
font-size: 110px;
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
margin-bottom: 30px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.start-button pl-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.form-box pl-input, .form-box .input-wrapper {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-box pl-loading-button {
|
||||
width: var(--row-height);
|
||||
}
|
||||
|
||||
.strength-meter {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
margin-bottom: -15px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: var(--font-size-tiny);
|
||||
width: 305px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.hint pl-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:host(:not([_mode="get-started"])) .get-started,
|
||||
:host(:not([_mode="unlock"])) .unlock {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.get-started-steps {
|
||||
width: 100%;
|
||||
height: 270px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.get-started-step {
|
||||
@apply --fullbleed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.get-started-step:not(.center) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.get-started-step > * {
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform 0.5s cubic-bezier(0.60, 0.2, 0.1, 1.2), opacity 0.3s;
|
||||
}
|
||||
|
||||
.get-started-step > :nth-child(2) {
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
.get-started-step > :nth-child(3) {
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
|
||||
.get-started-step:not(.center) > * {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.get-started-step.left > * {
|
||||
transform: translate3d(-200px, 0, 0);
|
||||
}
|
||||
|
||||
.get-started-step.right > * {
|
||||
transform: translate3d(200px, 0, 0);
|
||||
}
|
||||
|
||||
.get-started-thumbs {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.get-started-thumbs > * {
|
||||
background: var(--color-foreground);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.get-started-thumbs > .right {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
button.skip {
|
||||
background: none;
|
||||
border: none;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
font-weight: bold;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.version {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 20px;
|
||||
margin: auto;
|
||||
font-size: var(--font-size-small);
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
text-shadow: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hint.choose-password {
|
||||
margin-top: 30px;
|
||||
width: 160px;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:host([open]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host([open]) {
|
||||
transition-delay: 0.4s;
|
||||
transform: translate3d(0, -100%, 0);
|
||||
}
|
||||
</style>
|
||||
|
||||
<main class="unlock">
|
||||
|
||||
<pl-icon icon="logo" class="hero animate-in animate-out"></pl-icon>
|
||||
|
||||
<div class="form-box tiles-2 animate-in animate-out">
|
||||
<pl-input id="passwordInput" type="password" class="tap"
|
||||
select-on-focus on-enter="_unlock" no-tab="[[ open ]]"
|
||||
placeholder="[[ $l('Enter Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="unlockButton" on-click="_unlock" class="tap"
|
||||
label="[[ $l('Unlock') ]]" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<main class="get-started">
|
||||
|
||||
<pl-icon icon="logo" class="hero animate-in animate-out"></pl-icon>
|
||||
|
||||
<div class="get-started-steps">
|
||||
|
||||
<div class$="get-started-step [[ _getStartedClass(_getStartedStep, 0) ]]">
|
||||
|
||||
<div class="welcome-title animate-in">[[ $l("Welcome to Padlock!") ]]</div>
|
||||
<div class="welcome-subtitle animate-in">[[ $l("Let's get you set up! This will only take a couple of seconds.") ]]</div>
|
||||
|
||||
<pl-loading-button on-click="_startSetup" class="form-box tiles-2 animate-in tap start-button" no-tab="[[ open ]]">
|
||||
<div>[[ $l("Get Started") ]]</div>
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class$="get-started-step [[ _getStartedClass(_getStartedStep, 1) ]]">
|
||||
|
||||
<div class="form-box tiles-2">
|
||||
<pl-input id="emailInput" type="email" select-on-focus no-tab="[[ open ]]" class="tap"
|
||||
on-enter="_enterEmail" placeholder="[[ $l('Enter Email Address') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="emailButton" on-click="_enterEmail" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="cloud"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ _getStartedHint(0) ]]"></span>
|
||||
</div>
|
||||
|
||||
<button class="skip" on-click="_skipEmail">[[ $l("Use Offline") ]]</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class$="get-started-step [[ _getStartedClass(_getStartedStep, 2) ]]">
|
||||
|
||||
<div class="form-box tiles-2">
|
||||
<pl-input id="codeInput" required select-on-focus no-tab="[[ open ]]" class="tap"
|
||||
on-enter="_enterCode" placeholder="[[ $l('Enter Login Code') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="codeButton" on-click="_enterCode" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="mail"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ $l('Check your inbox! An email was sent to **{0}** containing your login code.', settings.syncEmail) ]]"></span>
|
||||
</div>
|
||||
|
||||
<button class="skip" on-click="_cancelActivation">[[ $l("Cancel") ]]</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class$="get-started-step [[ _getStartedClass(_getStartedStep, 3) ]]" hidden$="[[ !_hasCloudData ]]">
|
||||
|
||||
<div>
|
||||
<div class="form-box tiles-2">
|
||||
|
||||
<pl-input id="cloudPwdInput" class="tap" type="password" select-on-focus no-tab="[[ open ]]"
|
||||
on-enter="_enterCloudPassword" placeholder="[[ $l('Enter Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="cloudPwdButton" on-click="_enterCloudPassword" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ $l('Please enter the master password for the account **{0}**!', settings.syncEmail) ]]"></span>
|
||||
</div>
|
||||
|
||||
<button class="skip" on-click="_forgotCloudPassword">[[ $l("I Forgot My Password") ]]</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class$="get-started-step [[ _getStartedClass(_getStartedStep, 3) ]]" hidden$="[[ _hasCloudData ]]">
|
||||
|
||||
<div>
|
||||
<div class="form-box tiles-2">
|
||||
|
||||
<pl-input id="newPasswordInput" class="tap" type="password" select-on-focus no-tab="[[ open ]]"
|
||||
on-enter="_enterNewPassword" value="{{ newPwd }}" placeholder="[[ $l('Enter Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button on-click="_enterNewPassword" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="strength-meter">[[ _pwdStrength ]]</div>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ _getStartedHint(1) ]]"></span>
|
||||
</div>
|
||||
|
||||
<div on-click="_openPwdHowto" class="hint choose-password">[[ $l("How do I choose a good master password?") ]]</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class$="get-started-step [[ _getStartedClass(_getStartedStep, 4) ]]">
|
||||
|
||||
<div class="form-box tiles-2">
|
||||
|
||||
<pl-input id="confirmPasswordInput" class="tap" type="password" select-on-focus no-tab="[[ open ]]"
|
||||
on-enter="_confirmNewPassword" placeholder="[[ $l('Confirm Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button on-click="_confirmNewPassword" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ _getStartedHint(2) ]]"</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class$="get-started-step [[ _getStartedClass(_getStartedStep, 5) ]]">
|
||||
|
||||
<div class="welcome-title animate-out">[[ $l("All done!") ]]</div>
|
||||
<div class="welcome-subtitle animate-out">[[ $l("You're all set! Enjoy using Padlock!") ]]</div>
|
||||
|
||||
<pl-loading-button id="getStartedButton" on-click="_finishSetup" class="form-box tiles-2 animate-out tap start-button" no-tab="[[ open ]]">
|
||||
<span>[[ $l("Finish Setup") ]]</span>
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="get-started-thumbs animate-in animate-out">
|
||||
<div class$="[[ _getStartedClass(_getStartedStep, 0) ]]" on-click="_goToStep"></div>
|
||||
<div class$="[[ _getStartedClass(_getStartedStep, 1) ]]" on-click="_goToStep"></div>
|
||||
<div class$="[[ _getStartedClass(_getStartedStep, 3) ]]" on-click="_goToStep"></div>
|
||||
<div class$="[[ _getStartedClass(_getStartedStep, 4) ]]" on-click="_goToStep"></div>
|
||||
<div class$="[[ _getStartedClass(_getStartedStep, 5) ]]" on-click="_goToStep"></div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global zxcvbn */
|
||||
(() => {
|
||||
|
||||
const { DataMixin, LocaleMixin, DialogMixin, AnimationMixin, BaseElement, SyncMixin } = padlock;
|
||||
const { applyMixins, wait } = padlock.util;
|
||||
const { isTouch, getAppStoreLink } = padlock.platform;
|
||||
const { track } = padlock.tracking;
|
||||
|
||||
class StartView extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
AnimationMixin,
|
||||
SyncMixin
|
||||
) {
|
||||
|
||||
static get is() { return "pl-start-view"; }
|
||||
|
||||
static get properties() { return {
|
||||
open: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true,
|
||||
observer: "_openChanged"
|
||||
},
|
||||
_getStartedStep: {
|
||||
type: Number,
|
||||
value: 0
|
||||
},
|
||||
_hasData: {
|
||||
type: Boolean
|
||||
},
|
||||
_hasCloudData: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
_mode: {
|
||||
type: String,
|
||||
reflectToAttribute: true,
|
||||
computed: "_computeMode(_hasData)"
|
||||
},
|
||||
_pwdStrength: {
|
||||
type: String,
|
||||
computed: "_passwordStrength(newPwd)"
|
||||
}
|
||||
}; }
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._failCount = 0;
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.dataReady()
|
||||
.then(() => this.reset())
|
||||
.then(() => {
|
||||
if (!isTouch() && this._hasData) {
|
||||
this.$.passwordInput.focus();
|
||||
}
|
||||
this._openChanged();
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.$.passwordInput.value = "";
|
||||
this.$.emailInput.value = "";
|
||||
this.$.newPasswordInput.value = "";
|
||||
this.$.confirmPasswordInput.value = "";
|
||||
this.$.cloudPwdInput.value = "";
|
||||
this.$.unlockButton.stop();
|
||||
this.$.getStartedButton.stop();
|
||||
this._failCount = 0;
|
||||
this._getStartedStep = 0;
|
||||
this._hasCloudData = false;
|
||||
return this._checkHasData();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.$.passwordInput.focus();
|
||||
}
|
||||
|
||||
_openChanged() {
|
||||
if (this.open) {
|
||||
this.animateCascade(this.root.querySelectorAll(`main.${this._mode} .animate-out`), {
|
||||
animation: "fade",
|
||||
duration: 400,
|
||||
fullDuration: 600,
|
||||
initialDelay: 0,
|
||||
fill: "forwards",
|
||||
easing: "cubic-bezier(1, 0, 0.2, 1)",
|
||||
clear: 3000
|
||||
});
|
||||
} else {
|
||||
this.animateCascade(this.root.querySelectorAll(`main.${this._mode} .animate-in`), {
|
||||
animation: "reveal",
|
||||
duration: 1000,
|
||||
fullDuration: 1500,
|
||||
initialDelay: 300,
|
||||
fill: "backwards",
|
||||
clear: 3000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_startSetup() {
|
||||
this._getStartedStep = 1;
|
||||
if (!isTouch()) {
|
||||
this.$.emailInput.focus();
|
||||
}
|
||||
|
||||
track("Setup: Start");
|
||||
}
|
||||
|
||||
_enterEmail() {
|
||||
this.$.emailInput.blur();
|
||||
if (this.$.emailInput.invalid) {
|
||||
this.alert(
|
||||
this.$.emailInput.validationMessage || $l("Please enter a valid email address!"),
|
||||
{ type: "warning" }
|
||||
).then(() => this.$.emailInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.emailButton.start();
|
||||
padlock.stats.set({ pairingSource: "Setup" });
|
||||
|
||||
this.connectCloud(this.$.emailInput.value)
|
||||
.then(() => {
|
||||
this.$.emailButton.success();
|
||||
this._getStartedStep = 2;
|
||||
this.$.codeInput.value = "";
|
||||
if (!isTouch()) {
|
||||
this.$.codeInput.focus();
|
||||
}
|
||||
})
|
||||
.catch(() => this.$.emailButton.fail());
|
||||
|
||||
track("Setup: Email", { "Skipped": false, "Email": this.$.emailInput.value });
|
||||
}
|
||||
|
||||
_enterCode() {
|
||||
if (this._checkingCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$.codeInput.invalid) {
|
||||
this.alert($l("Please enter the login code sent to you via email!"), { type: "warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
this._checkingCode = true;
|
||||
|
||||
this.$.codeButton.start();
|
||||
this.activateToken(this.$.codeInput.value)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
return this.hasCloudData().then((hasData) => {
|
||||
this._checkingCode = false;
|
||||
this.$.codeButton.success();
|
||||
this._hasCloudData = hasData;
|
||||
return this._connected();
|
||||
});
|
||||
} else {
|
||||
this._checkingCode = false;
|
||||
this._rumble();
|
||||
this.$.codeButton.fail();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
this._checkingCode = false;
|
||||
this._rumble();
|
||||
this.$.codeButton.fail();
|
||||
this._handleCloudError(e);
|
||||
});
|
||||
|
||||
track("Setup: Code", { "Email": this.$.emailInput.value });
|
||||
}
|
||||
|
||||
_connected() {
|
||||
setTimeout(() => {
|
||||
this._getStartedStep = 3;
|
||||
if (!isTouch()) {
|
||||
this.$.newPasswordInput.focus();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
_cancelActivation() {
|
||||
this.cancelConnect();
|
||||
this.$.codeInput.value = "";
|
||||
this._getStartedStep = 1;
|
||||
}
|
||||
|
||||
_skipEmail() {
|
||||
this.$.emailInput.value = "";
|
||||
this._getStartedStep = 3;
|
||||
if (!isTouch()) {
|
||||
this.$.newPasswordInput.focus();
|
||||
}
|
||||
|
||||
track("Setup: Email", { "Skipped": true });
|
||||
}
|
||||
|
||||
_enterNewPassword() {
|
||||
this.$.newPasswordInput.blur();
|
||||
const pwd = this.$.newPasswordInput.value;
|
||||
|
||||
if (!pwd) {
|
||||
this.alert("Please enter a master password!").then(() => this.$.newPasswordInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
const next = () => {
|
||||
this._getStartedStep = 4;
|
||||
if (!isTouch()) {
|
||||
this.$.confirmPasswordInput.focus();
|
||||
}
|
||||
|
||||
track("Setup: Choose Password");
|
||||
};
|
||||
|
||||
if (zxcvbn(pwd).score < 2) {
|
||||
this.choose($l(
|
||||
"The password you entered is weak which makes it easier for attackers to break " +
|
||||
"the encryption used to protect your data. Try to use a longer password or include a " +
|
||||
"variation of uppercase, lowercase and special characters as well as numbers!"
|
||||
), [
|
||||
$l("Learn More"),
|
||||
$l("Choose Different Password"),
|
||||
$l("Use Anyway")
|
||||
], {
|
||||
type: "warning",
|
||||
title: $l("WARNING: Weak Password"),
|
||||
hideIcon: true
|
||||
}).then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this._openPwdHowto();
|
||||
break;
|
||||
case 1:
|
||||
this.$.newPasswordInput.focus();
|
||||
break;
|
||||
case 2:
|
||||
next();
|
||||
break;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
_confirmNewPassword() {
|
||||
this.$.confirmPasswordInput.blur();
|
||||
if (this.$.confirmPasswordInput.value !== this.$.newPasswordInput.value) {
|
||||
this.choose($l(
|
||||
"The password you entered does not match the original one!"
|
||||
), [
|
||||
$l("Try Again"),
|
||||
$l("Change Password")
|
||||
], { type: "warning" })
|
||||
.then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this.$.confirmPasswordInput.focus();
|
||||
break;
|
||||
case 1:
|
||||
this._getStartedStep = 3;
|
||||
this.$.newPasswordInput.focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._getStartedStep = 5;
|
||||
|
||||
track("Setup: Confirm Password");
|
||||
}
|
||||
|
||||
_computeMode() {
|
||||
return this._hasData ? "unlock" : "get-started";
|
||||
}
|
||||
|
||||
_checkHasData() {
|
||||
return this.hasData()
|
||||
.then((has) => this._hasData = has);
|
||||
}
|
||||
|
||||
_finishSetup() {
|
||||
this._initializeData();
|
||||
|
||||
track("Setup: Finish");
|
||||
}
|
||||
|
||||
_initializeData() {
|
||||
this.$.getStartedButton.start();
|
||||
if (this._initializing) {
|
||||
return;
|
||||
}
|
||||
this._initializing = true;
|
||||
|
||||
const password = this.cloudSource.password || this.$.newPasswordInput.value;
|
||||
this.cloudSource.password = password;
|
||||
|
||||
const promises = [
|
||||
this.initData(password),
|
||||
wait(1000)
|
||||
];
|
||||
|
||||
if (this.settings.syncConnected && !this._hasCloudData) {
|
||||
promises.push(this.collection.save(this.cloudSource));
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
this.$.getStartedButton.success();
|
||||
this.$.newPasswordInput.blur();
|
||||
this._initializing = false;
|
||||
});
|
||||
}
|
||||
|
||||
_promptResetData(message) {
|
||||
this.prompt(message, $l("Type 'RESET' to confirm"), "text", $l("Reset App"))
|
||||
.then((value) => {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
if (value.toUpperCase() === "RESET") {
|
||||
this.resetData();
|
||||
} else {
|
||||
this.alert($l("You didn't type 'RESET'. Refusing to reset the app."));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_unlock() {
|
||||
const password = this.$.passwordInput.value;
|
||||
|
||||
if (!password) {
|
||||
this.alert($l("Please enter your password!")).then(() => this.$.passwordInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.passwordInput.blur();
|
||||
this.$.unlockButton.start();
|
||||
|
||||
if (this._unlocking) {
|
||||
return;
|
||||
}
|
||||
this._unlocking = true;
|
||||
|
||||
Promise.all([
|
||||
this.loadData(password),
|
||||
wait(1000)
|
||||
])
|
||||
.then(() => {
|
||||
this.$.unlockButton.success();
|
||||
this._unlocking = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$.unlockButton.fail();
|
||||
this._unlocking = false;
|
||||
switch (e.code) {
|
||||
case "decryption_failed":
|
||||
this._rumble();
|
||||
this._failCount++;
|
||||
if (this._failCount > 2) {
|
||||
this.promptForgotPassword()
|
||||
.then((doReset) => {
|
||||
if (doReset) {
|
||||
this.resetData();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.$.passwordInput.focus();
|
||||
}
|
||||
break;
|
||||
case "unsupported_container_version":
|
||||
this.confirm($l(
|
||||
"It seems the data stored on this device was saved with a newer version " +
|
||||
"of Padlock and can not be opened with the version you are currently running. " +
|
||||
"Please install the latest version of Padlock or reset the data to start over!"
|
||||
), $l("Check For Updates"), $l("Reset Data"))
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
padlock.platform.checkForUpdates();
|
||||
} else {
|
||||
this._promptResetData($l(
|
||||
"Are you sure you want to reset the app? " +
|
||||
"WARNING: This will delete all your data!"
|
||||
));
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this._promptResetData($l(
|
||||
"An error occured while loading your data! If the problem persists, please try " +
|
||||
"resetting or reinstalling the app!"
|
||||
));
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_passwordStrength(pwd) {
|
||||
const score = pwd ? zxcvbn(pwd).score : -1;
|
||||
const strength = score === -1 ? "" : score < 2 ? $l("weak") : score < 4 ? $l("medium") : $l("strong");
|
||||
return strength && $l("strength: {0}", strength);
|
||||
}
|
||||
|
||||
_getStartedHint(step) {
|
||||
return [
|
||||
$l(
|
||||
"Logging in will unlock advanced features like automatic backups and seamless " +
|
||||
"synchronization between all your devices!"
|
||||
),
|
||||
$l(
|
||||
"Your **master password** is a single passphrase used to protect your data. " +
|
||||
"Without it, nobody will be able to access your data - not even us!"
|
||||
),
|
||||
$l(
|
||||
"**Don't forget your master password!** For privacy and security reasons we never store your " +
|
||||
"password anywhere which means we won't be able to help you recover your data in case you forget " +
|
||||
"it. We recommend writing it down on a piece of paper and storing it somewhere safe."
|
||||
)
|
||||
][step];
|
||||
}
|
||||
|
||||
_getStartedClass(currStep, step) {
|
||||
return currStep > step ? "left" : currStep < step ? "right" : "center";
|
||||
}
|
||||
|
||||
_goToStep(e) {
|
||||
const s = Array.from(this.root.querySelectorAll(".get-started-thumbs > *")).indexOf(e.target);
|
||||
if (s < this._getStartedStep) {
|
||||
this._getStartedStep = s;
|
||||
}
|
||||
}
|
||||
|
||||
_openProductPage() {
|
||||
getAppStoreLink().then((link) => window.open(link, "_system"));
|
||||
}
|
||||
|
||||
_openPwdHowto() {
|
||||
window.open("https://padlock.io/howto/choose-master-password/", "_system");
|
||||
}
|
||||
|
||||
_rumble() {
|
||||
this.animateElement(this.root.querySelector(`main.${this._mode} .hero`),
|
||||
{ animation: "rumble", duration: 200, clear: true });
|
||||
}
|
||||
|
||||
_enterCloudPassword() {
|
||||
if (this._restoringCloud) {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = this.$.cloudPwdInput.value;
|
||||
|
||||
if (!password) {
|
||||
this.alert($l("Please enter your password!")).then(() => this.$.cloudPwdInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.cloudPwdInput.blur();
|
||||
this.$.cloudPwdButton.start();
|
||||
this._restoringCloud = true;
|
||||
|
||||
this.cloudSource.password = password;
|
||||
this.collection.fetch(this.cloudSource)
|
||||
.then(() => {
|
||||
this.$.cloudPwdButton.success();
|
||||
this._restoringCloud = false;
|
||||
this._getStartedStep = 5;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$.cloudPwdButton.fail();
|
||||
this._restoringCloud = false;
|
||||
|
||||
if (e.code === "decryption_failed") {
|
||||
this._rumble();
|
||||
} else {
|
||||
this._handleCloudError(e);
|
||||
}
|
||||
});
|
||||
|
||||
track("Setup: Remote Password", { Email: this.settings.syncEmail });
|
||||
}
|
||||
|
||||
_forgotCloudPassword() {
|
||||
this.forgotCloudPassword().then(() => {
|
||||
this._hasCloudData = false;
|
||||
this._connected();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.customElements.define(StartView.is, StartView);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -0,0 +1,891 @@
|
|||
import '../../../../../node_modules/zxcvbn/dist/zxcvbn.js';
|
||||
import '../../styles/shared.js';
|
||||
import '../animation/animation.js';
|
||||
import '../base/base.js';
|
||||
import '../dialog/dialog-mixin.js';
|
||||
import '../data/data.js';
|
||||
import '../input/input.js';
|
||||
import '../loading-button/loading-button.js';
|
||||
import '../locale/locale.js';
|
||||
|
||||
const { DataMixin, LocaleMixin, DialogMixin, AnimationMixin, BaseElement, SyncMixin } = padlock;
|
||||
const { applyMixins, wait } = padlock.util;
|
||||
const { isTouch, getAppStoreLink } = padlock.platform;
|
||||
const { track } = padlock.tracking;
|
||||
|
||||
class StartView extends applyMixins(
|
||||
BaseElement,
|
||||
DataMixin,
|
||||
LocaleMixin,
|
||||
DialogMixin,
|
||||
AnimationMixin,
|
||||
SyncMixin
|
||||
) {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
|
||||
@keyframes reveal {
|
||||
from { transform: translate(0, 30px); opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
to { transform: translate(0, -200px); opacity: 0; }
|
||||
}
|
||||
|
||||
:host {
|
||||
--color-background: var(--color-primary);
|
||||
--color-foreground: var(--color-tertiary);
|
||||
--color-highlight: var(--color-secondary);
|
||||
@apply --fullbleed;
|
||||
@apply --scroll;
|
||||
color: var(--color-foreground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 5;
|
||||
text-align: center;
|
||||
text-shadow: rgba(0, 0, 0, 0.15) 0 2px 0;
|
||||
background: linear-gradient(180deg, #59c6ff 0%, #077cb9 100%);
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform 0.4s cubic-bezier(1, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
main {
|
||||
@apply --fullbleed;
|
||||
background: transparent;
|
||||
min-height: 510px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-box {
|
||||
width: 300px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: block;
|
||||
font-size: 110px;
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
margin-bottom: 30px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.start-button pl-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.form-box pl-input, .form-box .input-wrapper {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-box pl-loading-button {
|
||||
width: var(--row-height);
|
||||
}
|
||||
|
||||
.strength-meter {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
margin-bottom: -15px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: var(--font-size-tiny);
|
||||
width: 305px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.hint pl-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:host(:not([_mode="get-started"])) .get-started,
|
||||
:host(:not([_mode="unlock"])) .unlock {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.get-started-steps {
|
||||
width: 100%;
|
||||
height: 270px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.get-started-step {
|
||||
@apply --fullbleed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.get-started-step:not(.center) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.get-started-step > * {
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform 0.5s cubic-bezier(0.60, 0.2, 0.1, 1.2), opacity 0.3s;
|
||||
}
|
||||
|
||||
.get-started-step > :nth-child(2) {
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
.get-started-step > :nth-child(3) {
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
|
||||
.get-started-step:not(.center) > * {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.get-started-step.left > * {
|
||||
transform: translate3d(-200px, 0, 0);
|
||||
}
|
||||
|
||||
.get-started-step.right > * {
|
||||
transform: translate3d(200px, 0, 0);
|
||||
}
|
||||
|
||||
.get-started-thumbs {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.get-started-thumbs > * {
|
||||
background: var(--color-foreground);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.get-started-thumbs > .right {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
button.skip {
|
||||
background: none;
|
||||
border: none;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
font-weight: bold;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.version {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 20px;
|
||||
margin: auto;
|
||||
font-size: var(--font-size-small);
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
text-shadow: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hint.choose-password {
|
||||
margin-top: 30px;
|
||||
width: 160px;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:host([open]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host([open]) {
|
||||
transition-delay: 0.4s;
|
||||
transform: translate3d(0, -100%, 0);
|
||||
}
|
||||
</style>
|
||||
|
||||
<main class="unlock">
|
||||
|
||||
<pl-icon icon="logo" class="hero animate-in animate-out"></pl-icon>
|
||||
|
||||
<div class="form-box tiles-2 animate-in animate-out">
|
||||
<pl-input id="passwordInput" type="password" class="tap" select-on-focus="" on-enter="_unlock" no-tab="[[ open ]]" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="unlockButton" on-click="_unlock" class="tap" label="[[ \$l('Unlock') ]]" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<main class="get-started">
|
||||
|
||||
<pl-icon icon="logo" class="hero animate-in animate-out"></pl-icon>
|
||||
|
||||
<div class="get-started-steps">
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 0) ]]">
|
||||
|
||||
<div class="welcome-title animate-in">[[ \$l("Welcome to Padlock!") ]]</div>
|
||||
<div class="welcome-subtitle animate-in">[[ \$l("Let's get you set up! This will only take a couple of seconds.") ]]</div>
|
||||
|
||||
<pl-loading-button on-click="_startSetup" class="form-box tiles-2 animate-in tap start-button" no-tab="[[ open ]]">
|
||||
<div>[[ \$l("Get Started") ]]</div>
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 1) ]]">
|
||||
|
||||
<div class="form-box tiles-2">
|
||||
<pl-input id="emailInput" type="email" select-on-focus="" no-tab="[[ open ]]" class="tap" on-enter="_enterEmail" placeholder="[[ \$l('Enter Email Address') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="emailButton" on-click="_enterEmail" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="cloud"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ _getStartedHint(0) ]]"></span>
|
||||
</div>
|
||||
|
||||
<button class="skip" on-click="_skipEmail">[[ \$l("Use Offline") ]]</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 2) ]]">
|
||||
|
||||
<div class="form-box tiles-2">
|
||||
<pl-input id="codeInput" required="" select-on-focus="" no-tab="[[ open ]]" class="tap" on-enter="_enterCode" placeholder="[[ \$l('Enter Login Code') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="codeButton" on-click="_enterCode" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="mail"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ \$l('Check your inbox! An email was sent to **{0}** containing your login code.', settings.syncEmail) ]]"></span>
|
||||
</div>
|
||||
|
||||
<button class="skip" on-click="_cancelActivation">[[ \$l("Cancel") ]]</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 3) ]]" hidden\$="[[ !_hasCloudData ]]">
|
||||
|
||||
<div>
|
||||
<div class="form-box tiles-2">
|
||||
|
||||
<pl-input id="cloudPwdInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_enterCloudPassword" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button id="cloudPwdButton" on-click="_enterCloudPassword" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ \$l('Please enter the master password for the account **{0}**!', settings.syncEmail) ]]"></span>
|
||||
</div>
|
||||
|
||||
<button class="skip" on-click="_forgotCloudPassword">[[ \$l("I Forgot My Password") ]]</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 3) ]]" hidden\$="[[ _hasCloudData ]]">
|
||||
|
||||
<div>
|
||||
<div class="form-box tiles-2">
|
||||
|
||||
<pl-input id="newPasswordInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_enterNewPassword" value="{{ newPwd }}" placeholder="[[ \$l('Enter Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button on-click="_enterNewPassword" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="strength-meter">[[ _pwdStrength ]]</div>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ _getStartedHint(1) ]]"></span>
|
||||
</div>
|
||||
|
||||
<div on-click="_openPwdHowto" class="hint choose-password">[[ \$l("How do I choose a good master password?") ]]</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 4) ]]">
|
||||
|
||||
<div class="form-box tiles-2">
|
||||
|
||||
<pl-input id="confirmPasswordInput" class="tap" type="password" select-on-focus="" no-tab="[[ open ]]" on-enter="_confirmNewPassword" placeholder="[[ \$l('Confirm Master Password') ]]"></pl-input>
|
||||
|
||||
<pl-loading-button on-click="_confirmNewPassword" class="tap" no-tab="[[ open ]]">
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<pl-icon icon="lock"></pl-icon>
|
||||
<span inner-h-t-m-l="[[ _getStartedHint(2) ]]" <="" span="">
|
||||
</span></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class\$="get-started-step [[ _getStartedClass(_getStartedStep, 5) ]]">
|
||||
|
||||
<div class="welcome-title animate-out">[[ \$l("All done!") ]]</div>
|
||||
<div class="welcome-subtitle animate-out">[[ \$l("You're all set! Enjoy using Padlock!") ]]</div>
|
||||
|
||||
<pl-loading-button id="getStartedButton" on-click="_finishSetup" class="form-box tiles-2 animate-out tap start-button" no-tab="[[ open ]]">
|
||||
<span>[[ \$l("Finish Setup") ]]</span>
|
||||
<pl-icon icon="forward"></pl-icon>
|
||||
</pl-loading-button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="get-started-thumbs animate-in animate-out">
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 0) ]]" on-click="_goToStep"></div>
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 1) ]]" on-click="_goToStep"></div>
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 3) ]]" on-click="_goToStep"></div>
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 4) ]]" on-click="_goToStep"></div>
|
||||
<div class\$="[[ _getStartedClass(_getStartedStep, 5) ]]" on-click="_goToStep"></div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() { return "pl-start-view"; }
|
||||
|
||||
static get properties() { return {
|
||||
open: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true,
|
||||
observer: "_openChanged"
|
||||
},
|
||||
_getStartedStep: {
|
||||
type: Number,
|
||||
value: 0
|
||||
},
|
||||
_hasData: {
|
||||
type: Boolean
|
||||
},
|
||||
_hasCloudData: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
_mode: {
|
||||
type: String,
|
||||
reflectToAttribute: true,
|
||||
computed: "_computeMode(_hasData)"
|
||||
},
|
||||
_pwdStrength: {
|
||||
type: String,
|
||||
computed: "_passwordStrength(newPwd)"
|
||||
}
|
||||
}; }
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._failCount = 0;
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.dataReady()
|
||||
.then(() => this.reset())
|
||||
.then(() => {
|
||||
if (!isTouch() && this._hasData) {
|
||||
this.$.passwordInput.focus();
|
||||
}
|
||||
this._openChanged();
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.$.passwordInput.value = "";
|
||||
this.$.emailInput.value = "";
|
||||
this.$.newPasswordInput.value = "";
|
||||
this.$.confirmPasswordInput.value = "";
|
||||
this.$.cloudPwdInput.value = "";
|
||||
this.$.unlockButton.stop();
|
||||
this.$.getStartedButton.stop();
|
||||
this._failCount = 0;
|
||||
this._getStartedStep = 0;
|
||||
this._hasCloudData = false;
|
||||
return this._checkHasData();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.$.passwordInput.focus();
|
||||
}
|
||||
|
||||
_openChanged() {
|
||||
if (this.open) {
|
||||
this.animateCascade(this.root.querySelectorAll(`main.${this._mode} .animate-out`), {
|
||||
animation: "fade",
|
||||
duration: 400,
|
||||
fullDuration: 600,
|
||||
initialDelay: 0,
|
||||
fill: "forwards",
|
||||
easing: "cubic-bezier(1, 0, 0.2, 1)",
|
||||
clear: 3000
|
||||
});
|
||||
} else {
|
||||
this.animateCascade(this.root.querySelectorAll(`main.${this._mode} .animate-in`), {
|
||||
animation: "reveal",
|
||||
duration: 1000,
|
||||
fullDuration: 1500,
|
||||
initialDelay: 300,
|
||||
fill: "backwards",
|
||||
clear: 3000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_startSetup() {
|
||||
this._getStartedStep = 1;
|
||||
if (!isTouch()) {
|
||||
this.$.emailInput.focus();
|
||||
}
|
||||
|
||||
track("Setup: Start");
|
||||
}
|
||||
|
||||
_enterEmail() {
|
||||
this.$.emailInput.blur();
|
||||
if (this.$.emailInput.invalid) {
|
||||
this.alert(
|
||||
this.$.emailInput.validationMessage || $l("Please enter a valid email address!"),
|
||||
{ type: "warning" }
|
||||
).then(() => this.$.emailInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.emailButton.start();
|
||||
padlock.stats.set({ pairingSource: "Setup" });
|
||||
|
||||
this.connectCloud(this.$.emailInput.value)
|
||||
.then(() => {
|
||||
this.$.emailButton.success();
|
||||
this._getStartedStep = 2;
|
||||
this.$.codeInput.value = "";
|
||||
if (!isTouch()) {
|
||||
this.$.codeInput.focus();
|
||||
}
|
||||
})
|
||||
.catch(() => this.$.emailButton.fail());
|
||||
|
||||
track("Setup: Email", { "Skipped": false, "Email": this.$.emailInput.value });
|
||||
}
|
||||
|
||||
_enterCode() {
|
||||
if (this._checkingCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$.codeInput.invalid) {
|
||||
this.alert($l("Please enter the login code sent to you via email!"), { type: "warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
this._checkingCode = true;
|
||||
|
||||
this.$.codeButton.start();
|
||||
this.activateToken(this.$.codeInput.value)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
return this.hasCloudData().then((hasData) => {
|
||||
this._checkingCode = false;
|
||||
this.$.codeButton.success();
|
||||
this._hasCloudData = hasData;
|
||||
return this._connected();
|
||||
});
|
||||
} else {
|
||||
this._checkingCode = false;
|
||||
this._rumble();
|
||||
this.$.codeButton.fail();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
this._checkingCode = false;
|
||||
this._rumble();
|
||||
this.$.codeButton.fail();
|
||||
this._handleCloudError(e);
|
||||
});
|
||||
|
||||
track("Setup: Code", { "Email": this.$.emailInput.value });
|
||||
}
|
||||
|
||||
_connected() {
|
||||
setTimeout(() => {
|
||||
this._getStartedStep = 3;
|
||||
if (!isTouch()) {
|
||||
this.$.newPasswordInput.focus();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
_cancelActivation() {
|
||||
this.cancelConnect();
|
||||
this.$.codeInput.value = "";
|
||||
this._getStartedStep = 1;
|
||||
}
|
||||
|
||||
_skipEmail() {
|
||||
this.$.emailInput.value = "";
|
||||
this._getStartedStep = 3;
|
||||
if (!isTouch()) {
|
||||
this.$.newPasswordInput.focus();
|
||||
}
|
||||
|
||||
track("Setup: Email", { "Skipped": true });
|
||||
}
|
||||
|
||||
_enterNewPassword() {
|
||||
this.$.newPasswordInput.blur();
|
||||
const pwd = this.$.newPasswordInput.value;
|
||||
|
||||
if (!pwd) {
|
||||
this.alert("Please enter a master password!").then(() => this.$.newPasswordInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
const next = () => {
|
||||
this._getStartedStep = 4;
|
||||
if (!isTouch()) {
|
||||
this.$.confirmPasswordInput.focus();
|
||||
}
|
||||
|
||||
track("Setup: Choose Password");
|
||||
};
|
||||
|
||||
if (zxcvbn(pwd).score < 2) {
|
||||
this.choose($l(
|
||||
"The password you entered is weak which makes it easier for attackers to break " +
|
||||
"the encryption used to protect your data. Try to use a longer password or include a " +
|
||||
"variation of uppercase, lowercase and special characters as well as numbers!"
|
||||
), [
|
||||
$l("Learn More"),
|
||||
$l("Choose Different Password"),
|
||||
$l("Use Anyway")
|
||||
], {
|
||||
type: "warning",
|
||||
title: $l("WARNING: Weak Password"),
|
||||
hideIcon: true
|
||||
}).then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this._openPwdHowto();
|
||||
break;
|
||||
case 1:
|
||||
this.$.newPasswordInput.focus();
|
||||
break;
|
||||
case 2:
|
||||
next();
|
||||
break;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
_confirmNewPassword() {
|
||||
this.$.confirmPasswordInput.blur();
|
||||
if (this.$.confirmPasswordInput.value !== this.$.newPasswordInput.value) {
|
||||
this.choose($l(
|
||||
"The password you entered does not match the original one!"
|
||||
), [
|
||||
$l("Try Again"),
|
||||
$l("Change Password")
|
||||
], { type: "warning" })
|
||||
.then((choice) => {
|
||||
switch (choice) {
|
||||
case 0:
|
||||
this.$.confirmPasswordInput.focus();
|
||||
break;
|
||||
case 1:
|
||||
this._getStartedStep = 3;
|
||||
this.$.newPasswordInput.focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._getStartedStep = 5;
|
||||
|
||||
track("Setup: Confirm Password");
|
||||
}
|
||||
|
||||
_computeMode() {
|
||||
return this._hasData ? "unlock" : "get-started";
|
||||
}
|
||||
|
||||
_checkHasData() {
|
||||
return this.hasData()
|
||||
.then((has) => this._hasData = has);
|
||||
}
|
||||
|
||||
_finishSetup() {
|
||||
this._initializeData();
|
||||
|
||||
track("Setup: Finish");
|
||||
}
|
||||
|
||||
_initializeData() {
|
||||
this.$.getStartedButton.start();
|
||||
if (this._initializing) {
|
||||
return;
|
||||
}
|
||||
this._initializing = true;
|
||||
|
||||
const password = this.cloudSource.password || this.$.newPasswordInput.value;
|
||||
this.cloudSource.password = password;
|
||||
|
||||
const promises = [
|
||||
this.initData(password),
|
||||
wait(1000)
|
||||
];
|
||||
|
||||
if (this.settings.syncConnected && !this._hasCloudData) {
|
||||
promises.push(this.collection.save(this.cloudSource));
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
this.$.getStartedButton.success();
|
||||
this.$.newPasswordInput.blur();
|
||||
this._initializing = false;
|
||||
});
|
||||
}
|
||||
|
||||
_promptResetData(message) {
|
||||
this.prompt(message, $l("Type 'RESET' to confirm"), "text", $l("Reset App"))
|
||||
.then((value) => {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
if (value.toUpperCase() === "RESET") {
|
||||
this.resetData();
|
||||
} else {
|
||||
this.alert($l("You didn't type 'RESET'. Refusing to reset the app."));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_unlock() {
|
||||
const password = this.$.passwordInput.value;
|
||||
|
||||
if (!password) {
|
||||
this.alert($l("Please enter your password!")).then(() => this.$.passwordInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.passwordInput.blur();
|
||||
this.$.unlockButton.start();
|
||||
|
||||
if (this._unlocking) {
|
||||
return;
|
||||
}
|
||||
this._unlocking = true;
|
||||
|
||||
Promise.all([
|
||||
this.loadData(password),
|
||||
wait(1000)
|
||||
])
|
||||
.then(() => {
|
||||
this.$.unlockButton.success();
|
||||
this._unlocking = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$.unlockButton.fail();
|
||||
this._unlocking = false;
|
||||
switch (e.code) {
|
||||
case "decryption_failed":
|
||||
this._rumble();
|
||||
this._failCount++;
|
||||
if (this._failCount > 2) {
|
||||
this.promptForgotPassword()
|
||||
.then((doReset) => {
|
||||
if (doReset) {
|
||||
this.resetData();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.$.passwordInput.focus();
|
||||
}
|
||||
break;
|
||||
case "unsupported_container_version":
|
||||
this.confirm($l(
|
||||
"It seems the data stored on this device was saved with a newer version " +
|
||||
"of Padlock and can not be opened with the version you are currently running. " +
|
||||
"Please install the latest version of Padlock or reset the data to start over!"
|
||||
), $l("Check For Updates"), $l("Reset Data"))
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
padlock.platform.checkForUpdates();
|
||||
} else {
|
||||
this._promptResetData($l(
|
||||
"Are you sure you want to reset the app? " +
|
||||
"WARNING: This will delete all your data!"
|
||||
));
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this._promptResetData($l(
|
||||
"An error occured while loading your data! If the problem persists, please try " +
|
||||
"resetting or reinstalling the app!"
|
||||
));
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_passwordStrength(pwd) {
|
||||
const score = pwd ? zxcvbn(pwd).score : -1;
|
||||
const strength = score === -1 ? "" : score < 2 ? $l("weak") : score < 4 ? $l("medium") : $l("strong");
|
||||
return strength && $l("strength: {0}", strength);
|
||||
}
|
||||
|
||||
_getStartedHint(step) {
|
||||
return [
|
||||
$l(
|
||||
"Logging in will unlock advanced features like automatic backups and seamless " +
|
||||
"synchronization between all your devices!"
|
||||
),
|
||||
$l(
|
||||
"Your **master password** is a single passphrase used to protect your data. " +
|
||||
"Without it, nobody will be able to access your data - not even us!"
|
||||
),
|
||||
$l(
|
||||
"**Don't forget your master password!** For privacy and security reasons we never store your " +
|
||||
"password anywhere which means we won't be able to help you recover your data in case you forget " +
|
||||
"it. We recommend writing it down on a piece of paper and storing it somewhere safe."
|
||||
)
|
||||
][step];
|
||||
}
|
||||
|
||||
_getStartedClass(currStep, step) {
|
||||
return currStep > step ? "left" : currStep < step ? "right" : "center";
|
||||
}
|
||||
|
||||
_goToStep(e) {
|
||||
const s = Array.from(this.root.querySelectorAll(".get-started-thumbs > *")).indexOf(e.target);
|
||||
if (s < this._getStartedStep) {
|
||||
this._getStartedStep = s;
|
||||
}
|
||||
}
|
||||
|
||||
_openProductPage() {
|
||||
getAppStoreLink().then((link) => window.open(link, "_system"));
|
||||
}
|
||||
|
||||
_openPwdHowto() {
|
||||
window.open("https://padlock.io/howto/choose-master-password/", "_system");
|
||||
}
|
||||
|
||||
_rumble() {
|
||||
this.animateElement(this.root.querySelector(`main.${this._mode} .hero`),
|
||||
{ animation: "rumble", duration: 200, clear: true });
|
||||
}
|
||||
|
||||
_enterCloudPassword() {
|
||||
if (this._restoringCloud) {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = this.$.cloudPwdInput.value;
|
||||
|
||||
if (!password) {
|
||||
this.alert($l("Please enter your password!")).then(() => this.$.cloudPwdInput.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.cloudPwdInput.blur();
|
||||
this.$.cloudPwdButton.start();
|
||||
this._restoringCloud = true;
|
||||
|
||||
this.cloudSource.password = password;
|
||||
this.collection.fetch(this.cloudSource)
|
||||
.then(() => {
|
||||
this.$.cloudPwdButton.success();
|
||||
this._restoringCloud = false;
|
||||
this._getStartedStep = 5;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$.cloudPwdButton.fail();
|
||||
this._restoringCloud = false;
|
||||
|
||||
if (e.code === "decryption_failed") {
|
||||
this._rumble();
|
||||
} else {
|
||||
this._handleCloudError(e);
|
||||
}
|
||||
});
|
||||
|
||||
track("Setup: Remote Password", { Email: this.settings.syncEmail });
|
||||
}
|
||||
|
||||
_forgotCloudPassword() {
|
||||
this.forgotCloudPassword().then(() => {
|
||||
this._hasCloudData = false;
|
||||
this._connected();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(StartView.is, StartView);
|
|
@ -1,7 +1,4 @@
|
|||
<link rel="import" href="../base/base.html">
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
import '../base/base.js';
|
||||
|
||||
const { isFuture } = padlock.util;
|
||||
|
||||
|
@ -98,6 +95,3 @@ padlock.SubInfoMixin = (superClass) => {
|
|||
};
|
||||
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
|
@ -1,11 +1,8 @@
|
|||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="../data/data.html">
|
||||
<link rel="import" href="../payment-dialog/payment-dialog.html">
|
||||
<link rel="import" href="../promo/promo-dialog.html">
|
||||
<link rel="import" href="./subinfo.html">
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
import '../base/base.js';
|
||||
import '../data/data.js';
|
||||
import '../payment-dialog/payment-dialog.js';
|
||||
import '../promo/promo-dialog.js';
|
||||
import './subinfo.js';
|
||||
|
||||
const { EncryptedSource, CloudSource } = padlock.source;
|
||||
const { DataMixin, SubInfoMixin } = padlock;
|
||||
|
@ -571,6 +568,3 @@ padlock.SyncMixin = (superClass) => {
|
|||
};
|
||||
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
|
@ -1,9 +1,9 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
|
||||
<dom-module id="pl-title-bar">
|
||||
<template>
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
|
||||
class TitleBar extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
height: var(--title-bar-height);
|
||||
|
@ -73,7 +73,7 @@
|
|||
}
|
||||
|
||||
.buttons.macos-linux button.close::after {
|
||||
content: "\f00d";
|
||||
content: "\\f00d";
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@
|
|||
}
|
||||
|
||||
.buttons.macos-linux button.minimize::after {
|
||||
content: "\f2d1";
|
||||
content: "\\f2d1";
|
||||
font-size: 6px;
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@
|
|||
}
|
||||
|
||||
.buttons.macos-linux button.maximize::after {
|
||||
content: "\f07d";
|
||||
content: "\\f07d";
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@
|
|||
}
|
||||
|
||||
.buttons.windows button.close::after {
|
||||
content: "\f00d";
|
||||
content: "\\f00d";
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
@ -124,12 +124,12 @@
|
|||
}
|
||||
|
||||
.buttons.windows button.minimize::after {
|
||||
content: "\f2d1";
|
||||
content: "\\f2d1";
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.buttons.windows button.maximize::after {
|
||||
content: "\f2d0";
|
||||
content: "\\f2d0";
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
@ -147,36 +147,23 @@
|
|||
<button class="maximize" on-click="_maximize"></button>
|
||||
<button class="close" on-click="_close"></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
static get is() { return "pl-title-bar"; }
|
||||
|
||||
<script>
|
||||
/* global Polymer */
|
||||
_close() {
|
||||
require("electron").remote.getCurrentWindow().close();
|
||||
}
|
||||
|
||||
(() => {
|
||||
|
||||
class TitleBar extends padlock.BaseElement {
|
||||
|
||||
static get is() { return "pl-title-bar"; }
|
||||
|
||||
_close() {
|
||||
require("electron").remote.getCurrentWindow().close();
|
||||
}
|
||||
|
||||
_minimize() {
|
||||
require("electron").remote.getCurrentWindow().minimize();
|
||||
}
|
||||
|
||||
_maximize() {
|
||||
var win = require("electron").remote.getCurrentWindow();
|
||||
win.setFullScreen(!win.isFullScreen());
|
||||
}
|
||||
_minimize() {
|
||||
require("electron").remote.getCurrentWindow().minimize();
|
||||
}
|
||||
|
||||
_maximize() {
|
||||
var win = require("electron").remote.getCurrentWindow();
|
||||
win.setFullScreen(!win.isFullScreen());
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(TitleBar.is, TitleBar);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
|
@ -1,10 +1,10 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
<link rel="import" href="toggle.html">
|
||||
|
||||
<dom-module id="pl-toggle-button">
|
||||
<template>
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
import './toggle.js';
|
||||
|
||||
class ToggleButton extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared">
|
||||
:host {
|
||||
display: inline-block;
|
||||
|
@ -45,41 +45,32 @@
|
|||
<pl-toggle id="toggle" active="{{ active }}"></pl-toggle>
|
||||
<div>[[ label ]]</div>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
static get is() { return "pl-toggle-button"; }
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
static get properties() { return {
|
||||
active: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
reverse: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
}
|
||||
}; }
|
||||
|
||||
class ToggleButton extends padlock.BaseElement {
|
||||
|
||||
static get is() { return "pl-toggle-button"; }
|
||||
|
||||
static get properties() { return {
|
||||
active: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
reverse: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
reflectToAttribute: true
|
||||
}
|
||||
}; }
|
||||
|
||||
toggle() {
|
||||
this.$.toggle.toggle();
|
||||
}
|
||||
toggle() {
|
||||
this.$.toggle.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(ToggleButton.is, ToggleButton);
|
||||
|
||||
})();
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,10 +1,9 @@
|
|||
<link rel="import" href="../../styles/shared.html">
|
||||
<link rel="import" href="../base/base.html">
|
||||
|
||||
<dom-module id="pl-toggle">
|
||||
|
||||
<template>
|
||||
import '../../styles/shared.js';
|
||||
import '../base/base.js';
|
||||
|
||||
class Toggle extends padlock.BaseElement {
|
||||
static get template() {
|
||||
return Polymer.html`
|
||||
<style include="shared"></style>
|
||||
<style>
|
||||
:host {
|
||||
|
@ -47,47 +46,37 @@
|
|||
</style>
|
||||
|
||||
<div class="knob"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
</template>
|
||||
static get is() { return "pl-toggle"; }
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
static get properties() { return {
|
||||
active: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
notap: Boolean
|
||||
}; }
|
||||
|
||||
class Toggle extends padlock.BaseElement {
|
||||
static get is() { return "pl-toggle"; }
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("click", this._tap.bind(this));
|
||||
}
|
||||
|
||||
static get properties() { return {
|
||||
active: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
notify: true,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
notap: Boolean
|
||||
}; }
|
||||
_tap() {
|
||||
if (!this.notap) {
|
||||
this.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("click", this._tap.bind(this));
|
||||
}
|
||||
|
||||
_tap() {
|
||||
if (!this.notap) {
|
||||
this.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.active = !this.active;
|
||||
|
||||
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
|
||||
}
|
||||
toggle() {
|
||||
this.active = !this.active;
|
||||
|
||||
this.dispatchEvent(new CustomEvent("change", { bubbles: true, composed: true }));
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define(Toggle.is, Toggle);
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</dom-module>
|
File diff suppressed because it is too large
Load Diff
|
@ -50,12 +50,18 @@
|
|||
"watchify": "^3.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@polymer/iron-list": "^3.0.0-pre.18",
|
||||
"@polymer/paper-spinner": "^3.0.0-pre.18",
|
||||
"@polymer/polymer": "^3.0.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.0.0",
|
||||
"autosize": "^4.0.2",
|
||||
"electron-store": "^1.1.0",
|
||||
"electron-updater": "^2.21.0",
|
||||
"fs-extra": "^5.0.0",
|
||||
"moment": "^2.21.0",
|
||||
"uuid": "^3.1.0",
|
||||
"yargs": "^4.8.1"
|
||||
"yargs": "^4.8.1",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"scripts": {
|
||||
"bower-install": "pushd app && bower install && popd",
|
||||
|
|
Loading…
Reference in New Issue