Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
202
assets/webapps/mobile-wallet.gaiamobile.org/LICENSE
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2015 Deutsche Telekom, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
364
assets/webapps/mobile-wallet.gaiamobile.org/css/app.css
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
html {
|
||||
font-size: 10px;
|
||||
background-color: #F4F4F4;
|
||||
}
|
||||
|
||||
[role="toolbar"] {
|
||||
position: static;
|
||||
}
|
||||
|
||||
ul {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.gaia-icon::before {
|
||||
font-style: normal;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-settings::before {
|
||||
color: white;
|
||||
top: 2px;
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
.note {
|
||||
font-size: 0.7em;
|
||||
padding-left: 15px;
|
||||
padding-bottom: 5px;
|
||||
background-color: rgb(248, 246, 246);
|
||||
display: none;
|
||||
}
|
||||
|
||||
#main article > header {
|
||||
background-color: rgb(248, 246, 246);
|
||||
color: #F97C17;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px white solid;
|
||||
border-top: 1px white solid;
|
||||
/* box-shadow: 0px 1px 5px 0px rgba(50, 50, 50, 0.1);*/
|
||||
font-size: 12px;
|
||||
box-shadow: 0px 1px 1px 0px rgba(33, 33, 33, 0.1);
|
||||
}
|
||||
|
||||
.bottom-article > header {
|
||||
z-index: 3;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hide, .listview-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#settings p {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child h1:nth-child(2) {
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
#hci-notification {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 300;
|
||||
transition: all 0.6s ease;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
#hci-notification img {
|
||||
width: 240px;
|
||||
box-shadow: 0px 1px 4px 1px #7B7B7B;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#hci-notification.show {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 300;
|
||||
transition: all 0.6s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
article.notification {
|
||||
color: #9C9C9C;
|
||||
font-size: 0.7em;
|
||||
font-style: italic;
|
||||
opacity: 0.95;
|
||||
background: black;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.hci-card {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: white;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hci-container {
|
||||
text-align: center;
|
||||
border-radius: 10px;
|
||||
padding: 0px 20px;
|
||||
color: rgb(77, 78, 83);
|
||||
background: transparent linear-gradient(to bottom, rgb(234, 239, 242), rgb(212, 221, 228));
|
||||
}
|
||||
|
||||
.hci-container p {
|
||||
line-height: 3em;
|
||||
font-weight: 300;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
@keyframes show-card {
|
||||
0% { z-index: 2; transform: rotate(0deg); top: 0px; left: 0px; width: 240px;}
|
||||
50% { z-index: 10; transform: rotate(0deg); top: -30px; left: -2px; width: 244px;}
|
||||
100% { z-index: 10; transform: rotate(0deg); top: -8px; left: 0px; width: 240px;}
|
||||
}
|
||||
|
||||
@keyframes hide-card {
|
||||
0% { z-index: 10; transform: rotate(0deg); top: -8px; left: 0px; width: 240px;}
|
||||
50% { z-index: 10; transform: rotate(0deg); top: -30px; left: -2px; width: 244px;}
|
||||
100% { z-index: 2; transform: rotate(0deg); top: 0px; left: 0px; width: 240px;}
|
||||
}
|
||||
|
||||
ul.swipeable > li.deselected > img {
|
||||
animation-name: hide-card;
|
||||
animation-duration: 0.5s;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
ul.swipeable > li.selected > img {
|
||||
z-index: 10;
|
||||
transform: rotate(0deg);
|
||||
position: relative;
|
||||
top: -8px;
|
||||
animation-name: show-card;
|
||||
animation-duration: 0.5s;
|
||||
}
|
||||
|
||||
ul.details > li {
|
||||
display: block;
|
||||
padding: 15px 10px;
|
||||
}
|
||||
|
||||
ul.details > li > p.label {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.details span.date {
|
||||
display: inline-block;
|
||||
width: 67%;
|
||||
}
|
||||
|
||||
li.details-more {
|
||||
text-align: center;
|
||||
vertical-align: bottom;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
li.details-more > .details-note {
|
||||
font-style: italic;
|
||||
font-size: 0.8em;
|
||||
color: rgb(133, 133, 133);
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
/*.details-oneline p {
|
||||
display: inline-block;
|
||||
}*/
|
||||
|
||||
.active > header {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.slider {
|
||||
background-color: rgb(248, 246, 246);
|
||||
/* transform: translate3d(0, -131px, 0);
|
||||
box-shadow: inset 0px 1px 5px 0px rgba(50, 50, 50, 0.1);
|
||||
box-shadow: 0px 5px 5px 0px rgba(33, 33, 33, 0.1) inset; */
|
||||
padding-top: 1rem;
|
||||
height: 25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bottom-slide .slider {
|
||||
box-shadow: 0px 2px 2px 0px rgba(33, 33, 33, 0.1) inset;
|
||||
}
|
||||
|
||||
.bottom-slide > header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.top-slide {
|
||||
margin-top: -26rem;
|
||||
transition: transform 0.75s, margin-top 0.75s ease;
|
||||
}
|
||||
|
||||
.details-visible .top-slide {
|
||||
margin-top: 0px;
|
||||
overflow: auto;
|
||||
transform: translate3d(0,0,0)
|
||||
}
|
||||
|
||||
.bottom-slide {
|
||||
z-index: 3;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
transition: transform 0.75s, top 0.75s ease;
|
||||
}
|
||||
|
||||
.bottom-slide.details-visible {
|
||||
top: -253px;
|
||||
transform: translate3d(0,0,0)
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child .t-logo {
|
||||
background: url(../img/icons/t_logo_white.png) center center / 5rem auto no-repeat;
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
.pocket, .listview-element-pocket {
|
||||
width: 260px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
top: 150px;
|
||||
left: 0px;
|
||||
background: linear-gradient(to bottom, rgb(200, 200, 200) 0%, #F4F4F4 77%, #F4F4F4 100%) repeat scroll 0% 0% transparent;
|
||||
border-top: 1px solid #808080;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/*.nonpayment*/ .details li {
|
||||
min-height: 0px
|
||||
}
|
||||
|
||||
/*.nonpayment*/ .details p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/*.nonpayment*/ .details p.val {
|
||||
float: right;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.details .price {
|
||||
font-family: "Fira Mono";
|
||||
font-size: 13px;
|
||||
width: 21%;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.details .currency {
|
||||
font-family: "Fira Mono";
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sew, .listview-element-sew {
|
||||
width: 92%;
|
||||
height: 10px;
|
||||
margin: 2px 0px 0px 5px;
|
||||
border-radius: 12px;
|
||||
padding-right: 10px;
|
||||
border-top: 1px dashed #8C8C8C;
|
||||
}
|
||||
|
||||
ul.transaction-list > li.transaction {
|
||||
padding: 4px 0px 1px 10px;
|
||||
color: rgb(133, 133, 133);
|
||||
}
|
||||
|
||||
ul.transaction-list > li.transaction:after {
|
||||
border: 0px none;
|
||||
}
|
||||
|
||||
ul.details > li p {
|
||||
font-size: 1em;
|
||||
margin-left: 10px;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
ul.details > li p.value {
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.error-view {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
margin-top: -2em;
|
||||
}
|
||||
|
||||
.error-view h2.error {
|
||||
font-weight: normal;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.error-view button {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.error-img {
|
||||
width: 5em;
|
||||
height: 5em;
|
||||
background-image: url(../img/errors/error.png);
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.error-api {
|
||||
background-image: url(../img/errors/api-error.png);
|
||||
}
|
||||
|
||||
.error-no-sim {
|
||||
background-image: url(../img/errors/no-sim.png);
|
||||
}
|
||||
|
||||
.error-sim {
|
||||
background-image: url(../img/errors/sim-error.png);
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
margin: 10px 10px 0px 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 360px) {
|
||||
.bottom-slide {
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.bottom-slide.details-visible {
|
||||
top: -26rem;
|
||||
}
|
||||
|
||||
ul.details > li.details-more {
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
131
assets/webapps/mobile-wallet.gaiamobile.org/css/pin.css
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
section#pin {
|
||||
background-color: #F4F4F4;
|
||||
}
|
||||
|
||||
.pin-unblocked {
|
||||
font-weight: 300;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pin-view h2 {
|
||||
font-size: 1.2em;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pin-view:not(hide) {
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.pin-top {
|
||||
/*font-style: italic;*/
|
||||
height: 7.3em;
|
||||
}
|
||||
|
||||
.pin-input {
|
||||
text-align: center;
|
||||
margin: 35px 0px 30px;
|
||||
}
|
||||
|
||||
.pin-input > div {
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 1.5px solid #ccc;
|
||||
vertical-align: baseline;
|
||||
margin: 0px 5px;
|
||||
}
|
||||
|
||||
.pin-input > div.pin-digit-entered {
|
||||
background: linear-gradient(to bottom, #f4f4f4 0%,#333333 100%);
|
||||
border: 1.5px solid #333;
|
||||
}
|
||||
|
||||
.pin-keypad {
|
||||
display: table;
|
||||
text-align: center;
|
||||
border-collapse: collapse;
|
||||
margin: 0px 0px 25px 0px;
|
||||
}
|
||||
|
||||
.pin-keypad-row {
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
/*border-image: linear-gradient(45deg, #f4f4f4, #ccc, #f4f4f4) 1;*/
|
||||
border-image: linear-gradient(to right, #f4f4f4 0%,#cccccc 30%,#cccccc 70%,#f4f4f4 100%) 1;
|
||||
}
|
||||
|
||||
.pin-keypad-row:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.pin-key {
|
||||
display: table-cell;
|
||||
width: 2.7em;
|
||||
height: 2.7em;
|
||||
vertical-align: middle;
|
||||
border-right: 1px solid #ccc;
|
||||
font-size: 1.7em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pin-key.clear-key::after {
|
||||
content: "clear";
|
||||
font-size: 0.5em;
|
||||
vertical-align: 35%;
|
||||
}
|
||||
|
||||
.pin-key:active {
|
||||
color: #F97C17;
|
||||
/*background-color: #ccc;*/
|
||||
}
|
||||
|
||||
.pin-key.gaia-icon::before {
|
||||
position: relative;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.pin-key.icon-clearcancel-cross::before {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
.pin-keypad-row:first-child .pin-key {
|
||||
background: linear-gradient(to bottom, #f4f4f4, #ccc);
|
||||
background-origin: border-box;
|
||||
border-right: 1px solid transparent;
|
||||
box-shadow: inset -999px 0 0 #f4f4f4;
|
||||
}
|
||||
|
||||
.pin-keypad-row:last-child .pin-key {
|
||||
background: linear-gradient(to top, #f4f4f4, #ccc);
|
||||
background-origin: border-box;
|
||||
border-right: 1px solid transparent;
|
||||
box-shadow: inset -999px 0 0 #f4f4f4;
|
||||
}
|
||||
|
||||
.pin-keypad-row .pin-key:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.pin-warning {
|
||||
margin-bottom: -39px;
|
||||
position: relative;
|
||||
top: -13px;
|
||||
font-size: 0.85em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pin-enable-warning {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.error-pin-enable {
|
||||
background-image: url(../img/errors/pin-enable-fail.png);
|
||||
}
|
||||
|
||||
.error-locked {
|
||||
background-image: url(../img/errors/locked.png);
|
||||
}
|
||||
|
||||
172
assets/webapps/mobile-wallet.gaiamobile.org/css/settings.css
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
#settings {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
opacity: 1;
|
||||
transition: all 0.6s ease;
|
||||
background-color: #F4F4F4;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
#settings.edit {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
opacity: 1;
|
||||
transition: all 0.6s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.settings-subpage {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 101;
|
||||
opacity: 1;
|
||||
background-color: #F4F4F4;
|
||||
transition: transform 0.6s ease;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.settings-subpage.slide-in {
|
||||
transition: transform 0.6s ease;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.settings-disabled {
|
||||
pointer-events: none;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.settings-disabled .gaia-icon::before {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.settings-disclaimer {
|
||||
font-size: 0.6em;
|
||||
padding: 0px 15px 10px 15px;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.settings-forward div {
|
||||
width: 100%;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.settings-forward .gaia-icon {
|
||||
text-align: right;
|
||||
color: #868692;
|
||||
}
|
||||
|
||||
.settings-forward .gaia-icon::before {
|
||||
position: relative;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
#settings-status {
|
||||
opacity: 0;
|
||||
transition: opacity 600ms ease 0s;
|
||||
}
|
||||
|
||||
#settings-status.visible {
|
||||
opacity: 1;
|
||||
transition: opacity 1000ms ease 0s;
|
||||
}
|
||||
|
||||
#settings-status p{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#settings-about section > div {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #E7E7E7;
|
||||
padding: 10px;
|
||||
margin: 0px 15px;
|
||||
}
|
||||
|
||||
#settings-about div:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
#settings-about div:first-child {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.about-logo img {
|
||||
max-width: 9em;
|
||||
}
|
||||
|
||||
.about-logo p {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
.about-logo p:first-child {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.about-logo p:nth-child(2) {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.about-logo p:nth-child(4) {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.about-authors p, .about-contributors p {
|
||||
font-size: 0.8em;
|
||||
color: #a6a6a6;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.about-authors strong, .about-contributors strong {
|
||||
color: #333;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.about-authors p {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.about-authors strong {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.about-contributors div {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.about-contributors h3 {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 2px;
|
||||
margin-top: 1px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.about-contributors div p:nth-child(2) {
|
||||
line-height: 1em;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
|
||||
.about-contributors div p:nth-child(3) {
|
||||
line-height: 1em;
|
||||
font-size: 0.6em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* ugly hacks to override header.css, TODO migrate to proper gaia header */
|
||||
section[role="region"].settings-subpage > header:first-child h1:nth-child(2) {
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
section.skin-organic.settings-subpage[role="region"] > header:first-child .gaia-icon {
|
||||
color: #868692;
|
||||
background: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
ul.swipeable {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 0px;
|
||||
}
|
||||
|
||||
ul.swipeable > li {
|
||||
display: inline-block;
|
||||
width: 260px;
|
||||
height: 206px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ul.swipeable > li > img, .listview-element-img {
|
||||
width: 240px;
|
||||
padding: 20px 10px 25px;
|
||||
margin-top: -35px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ul.swipeable > li:first-child {
|
||||
margin-left: 3rem;
|
||||
}
|
||||
|
||||
.swipeable-container {
|
||||
z-index: 2;
|
||||
background-color: #f4f4f4;
|
||||
border-bottom: 1px solid #FBFBFB;
|
||||
box-shadow: 0px 2px 2px 0px rgba(33, 33, 33, 0.1);
|
||||
}
|
||||
|
||||
.swipeable-progress {
|
||||
text-align: center;
|
||||
height: 1.6rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.swipeable-progress div{
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border: 2px solid #aeaeae;
|
||||
vertical-align: baseline;
|
||||
margin: 0px 2px;
|
||||
}
|
||||
|
||||
.swipeable-progress div.swipeable-progress-selected {
|
||||
background: #aeaeae;
|
||||
}
|
||||
|
||||
.swipeable-progress div.swipeable-progress-active {
|
||||
/* background-color: #e20074; */
|
||||
border: 2px solid #F97C17;
|
||||
}
|
||||
|
||||
.swipeable-progress div.swipeable-progress-active.swipeable-progress-selected {
|
||||
background: #F97C17;
|
||||
}
|
||||
|
||||
@media (min-width: 360px) {
|
||||
ul.swipeable > li:first-child {
|
||||
margin-left: 5rem;
|
||||
}
|
||||
|
||||
.swipeable-container {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
BIN
assets/webapps/mobile-wallet.gaiamobile.org/img/errors/error.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/webapps/mobile-wallet.gaiamobile.org/img/t-logo.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
361
assets/webapps/mobile-wallet.gaiamobile.org/index.html
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Mobile Wallet</title>
|
||||
<meta name="theme-color" content="#ec7616">
|
||||
<meta name="description" content="New wallet version targeted for FFOS 2.2">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<link href="shared/style/layout.css" rel="stylesheet" type="text/css">
|
||||
<link href="shared/style/headers.css" rel="stylesheet" type="text/css">
|
||||
<link href="shared/style/buttons.css" rel="stylesheet" type="text/css">
|
||||
<link href="shared/style/input_areas.css" rel="stylesheet" type="text/css">
|
||||
<link href="shared/style/status.css" rel="stylesheet" type="text/css">
|
||||
<link href="shared/style/icons.css" rel="stylesheet" type="text/css">
|
||||
<link href="css/app.css" rel="stylesheet" type="text/css">
|
||||
<link href="css/swipeable.css" rel="stylesheet" type="text/css">
|
||||
<link href="css/settings.css" rel="stylesheet" type="text/css">
|
||||
<link href="css/pin.css" rel="stylesheet" type="text/css">
|
||||
<script src="/shared/js/component_utils.js" defer></script>
|
||||
<script src="/shared/elements/gaia_switch/script.js" defer></script>
|
||||
<script type="text/javascript" src="shared/js/gesture_detector.js" defer></script>
|
||||
<script type="text/javascript" src="js/error/error_consts.js" defer></script>
|
||||
<script type="text/javascript" src="js/error/error_view.js" defer></script>
|
||||
<script type="text/javascript" src="js/common/utils.js" defer></script>
|
||||
<script type="text/javascript" src="js/common/details_view_mixin.js" defer></script>
|
||||
<script type="text/javascript" src="js/common/list_view_mixin.js" defer></script>
|
||||
<script type="text/javascript" src="js/common/card_swipe_mixin.js" defer></script>
|
||||
<script type="text/javascript" src="js/common/swipeable.js" defer></script>
|
||||
<script type="text/javascript" src="js/secure-element/se_utils.js" defer></script>
|
||||
<script type="text/javascript" src="js/secure-element/apdu_helpers.js" defer></script>
|
||||
<script type="text/javascript" src="js/settings/default_settings.js" defer></script>
|
||||
<script type="text/javascript" src="js/secure-element/sim_access_manager.js" defer></script>
|
||||
<script type="text/javascript" src="js/settings/settings_sub_view.js" defer></script>
|
||||
<script type="text/javascript" src="js/settings/config_sub_view.js" defer></script>
|
||||
<script type="text/javascript" src="js/settings/settings_view.js" defer></script>
|
||||
<script type="text/javascript" src="js/settings/settings_controller.js" defer></script>
|
||||
<script type="text/javascript" src="js/pin/pin_controller.js" defer></script>
|
||||
<script type="text/javascript" src="js/pin/pin_view.js" defer></script>
|
||||
<script type="text/javascript" src="js/applets/applet_model.js" defer></script>
|
||||
<script type="text/javascript" src="js/applets/applet_details_view.js" defer></script>
|
||||
<script type="text/javascript" src="js/applets/applet_list_view.js" defer></script>
|
||||
<script type="text/javascript" src="js/applets/applet_list_controller.js" defer></script>
|
||||
<script type="text/javascript" src="js/applets/hci_view.js" defer></script>
|
||||
<script type="text/javascript" src="js/ext_data.js" defer></script>
|
||||
<script type="text/javascript" src="js/app_view.js" defer></script>
|
||||
<script type="text/javascript" src="js/app.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<section role="region" id="global-error" class="vbox fit hide">
|
||||
<header>
|
||||
<h1 class="title">Mobile Wallet</h1>
|
||||
</header>
|
||||
<section class="vbox fit hbox middle center">
|
||||
<article id="unknown-error" class="error-view">
|
||||
<div class="error-img"></div>
|
||||
<h2 class="error">Unknown problem occured</h2>
|
||||
<p>
|
||||
There's something wrong, but we're not sure what that is. Please contact your wallet provider for assistance.
|
||||
</p>
|
||||
</article>
|
||||
<article id="no-sim" class="error-view hide">
|
||||
<div class="error-img error-no-sim"></div>
|
||||
<h2 class="error">Your SIM card is missing</h2>
|
||||
<p>
|
||||
We're not able to detect SIM card in your phone.
|
||||
Please make sure it's inserted properly.
|
||||
If the problem persists, please contact wallet provider for assistance.
|
||||
</p>
|
||||
</article>
|
||||
<article id="no-api" class="error-view hide">
|
||||
<div class="error-img error-api"></div>
|
||||
<h2 class="error">SE API not detected</h2>
|
||||
<p>
|
||||
Most probably your phone has some compatibility issues or not up-to-date software.
|
||||
Please make sure you're running the latest available version of Firefox OS.
|
||||
If the problem persists, please contact your wallet provider for assistance.
|
||||
</p>
|
||||
</article>
|
||||
<article id="sim-error" class="error-view hide">
|
||||
<div class="error-img error-sim"></div>
|
||||
<h2 class="error">SIM problem detected</h2>
|
||||
<p>
|
||||
It seems your SIM card has some technical issues. Please confirm that applet AID values in Settings are vaild and application is authorized to access them.
|
||||
If the problem persists please contact your wallet provider for assistance.
|
||||
</p>
|
||||
<section class="footer">
|
||||
<button class="error-settings-btn">Settings</button>
|
||||
</section>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
<section role="region" id="pin" class="vbox fit pin-view hide">
|
||||
<header>
|
||||
<h1 class="title">Mobile Wallet</h1>
|
||||
</header>
|
||||
<section id="pin-template" class="vbox fit hbox middle center">
|
||||
<article class="pin-unblocked vbox middle center">
|
||||
<div class="pin-top pin-entry">
|
||||
<h2 class="pin-entry-text">Please enter your PIN</h2>
|
||||
<div class="pin-warning error hide">
|
||||
<p><span class="pin-attempts"></span></p>
|
||||
</div>
|
||||
<div class="pin-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="pin-top hide vbox middle pin-incorrect">
|
||||
<h2 class="pin-entry-error-text error">Incorrect PIN</h2>
|
||||
</div>
|
||||
<div class="pin-keypad">
|
||||
<div class="pin-keypad-row">
|
||||
<div class="pin-key" data-value="1">1</div>
|
||||
<div class="pin-key" data-value="2">2</div>
|
||||
<div class="pin-key" data-value="3">3</div>
|
||||
</div>
|
||||
<div class="pin-keypad-row">
|
||||
<div class="pin-key" data-value="4">4</div>
|
||||
<div class="pin-key" data-value="5">5</div>
|
||||
<div class="pin-key" data-value="6">6</div>
|
||||
</div>
|
||||
<div class="pin-keypad-row">
|
||||
<div class="pin-key" data-value="7">7</div>
|
||||
<div class="pin-key" data-value="8">8</div>
|
||||
<div class="pin-key" data-value="9">9</div>
|
||||
</div>
|
||||
<div class="pin-keypad-row">
|
||||
<div class="pin-key clear-key" data-value="clear"></div>
|
||||
<div class="pin-key" data-value="0">0</div>
|
||||
<div class="pin-key gaia-icon icon-dialer-delete" data-value="backspace"></div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="pin-blocked error-view hide">
|
||||
<div class="error-img error-locked"></div>
|
||||
<h2 class="error">Your wallet was locked</h2>
|
||||
<p>
|
||||
We're sorry, but it looks like you entered incorrect PIN too many times.
|
||||
Please unlock your wallet by entering PUK or contact your wallet provider for assistance.
|
||||
</p>
|
||||
<section class="footer">
|
||||
<button class="pin-unblock-btn">Enter PUK</button>
|
||||
</section>
|
||||
</article>
|
||||
<article class="puk-blocked error-view hide">
|
||||
<div class="error-img error-locked"></div>
|
||||
<h2 class="error">Your wallet was locked</h2>
|
||||
<p>
|
||||
We're sorry, but it looks like you entered incorrect PUK too many times.
|
||||
Please contact your wallet provider for assistance.
|
||||
</p>
|
||||
</article>
|
||||
<article class="pin-enable-error error-view hide">
|
||||
<div class="error-img error-pin-enable"></div>
|
||||
<h2 class="error">Enabling PIN lock failed</h2>
|
||||
<p>
|
||||
We're not able to enable PIN lock in your wallet. Most probably you're
|
||||
entering incorrect PIN. If you haven't used wallet PIN lock before,
|
||||
you might want to try the default value, which is
|
||||
<span class="pin-default-val">0000</span>. If the problem persists, please
|
||||
contact your wallet service provider.
|
||||
<div class="pin-enable-warning error hide">
|
||||
<p><span class="pin-enable-attempt">0 attempts</span> remaining</p>
|
||||
</div>
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
<section role="region" id="main" class="vbox fit hide">
|
||||
<header>
|
||||
<menu type="toolbar">
|
||||
<button class="gaia-icon icon-settings" id="settings-edit"></button>
|
||||
</menu>
|
||||
<h1 class="title">Mobile Wallet</h1>
|
||||
</header>
|
||||
<section id="cards-view" class="fit">
|
||||
<article id="payments-view" class="vbox middle active">
|
||||
<header>Your Payment cards</header>
|
||||
<div class="swipeable-container">
|
||||
<div class="swipeable-progress"></div>
|
||||
<ul class="swipeable">
|
||||
<li>
|
||||
<div class="pocket">
|
||||
<div class="sew"></div>
|
||||
</div>
|
||||
<img src="img/cards/no-cards-payment.png">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="payments-view-details" class="slider top-slide gaia-list">
|
||||
<ul class="details">
|
||||
<li><p class="aid">Card number</p><p class="val number"></p></li>
|
||||
<li><p class="date">Valid thru</p><p class="val issued"></p></li>
|
||||
<li class="transactions">
|
||||
<p class="transaction"><span class="date"></span> <span class="price"></span><span class="currency"></span></p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
<article id="nonpayments-view" class="bottom-slide">
|
||||
<header>Your Other Cards</header>
|
||||
<div class="swipeable-container" id="s2">
|
||||
<div id="s2-progress" class="swipeable-progress"></div>
|
||||
<ul class="swipeable">
|
||||
<li>
|
||||
<div class="pocket">
|
||||
<div class="sew"></div>
|
||||
</div>
|
||||
<img src="img/cards/no-cards-other.png">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="nonpayments-view-details" class="slider gaia-list">
|
||||
<ul class="nonpayment details"></ul>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
<section role="region" id="settings" class="vbox fit scroll skin-organic">
|
||||
<header>
|
||||
<menu type="toolbar">
|
||||
<button id="settings-done">Done</button>
|
||||
</menu>
|
||||
<h1 class="title ">Settings</h1>
|
||||
</header>
|
||||
<section class="gaia-list fit scroll">
|
||||
<article>
|
||||
<section>
|
||||
<ul>
|
||||
<li>
|
||||
<gaia-switch id="fast-pay">
|
||||
<label>Fast Pay Mode</label>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<gaia-switch id="pin-checkbox">
|
||||
<label>Enable PIN</label>
|
||||
</gaia-switch>
|
||||
</li>
|
||||
<li id="pin-change-btn" class="settings-disabled settings-forward">
|
||||
<div>Change PIN</div>
|
||||
<aside class="gaia-icon icon-forward"></aside>
|
||||
</li>
|
||||
<li id="about-btn" class="settings-forward">
|
||||
<div>About</div>
|
||||
<aside class="gaia-icon icon-forward"></aside>
|
||||
</li>
|
||||
<li id="adv-settings-btn" class="settings-forward">
|
||||
<div>Advanced Settings</div>
|
||||
<aside class="gaia-icon icon-forward"></aside>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</article>
|
||||
</section>
|
||||
<section role="status" id="settings-status">
|
||||
<p></p>
|
||||
</section>
|
||||
</section>
|
||||
<section role="region" id="settings-about" class="settings-subpage vbox fit skin-organic">
|
||||
<header>
|
||||
<button class="gaia-icon icon-back"></button>
|
||||
<h1 class="title ">About</h1>
|
||||
</header>
|
||||
<section class="gaia-list fit subview-content">
|
||||
<div class="about-logo">
|
||||
<p>Open Source</p>
|
||||
<p>Mobile Wallet</p>
|
||||
<img src="img/t-logo.png">
|
||||
<p>by Deutsche Telekom</p>
|
||||
</div>
|
||||
<div class="about-authors">
|
||||
<p>Code <strong>Krzysztof Mioduszewski</strong></p>
|
||||
<p>Design <strong>Bartosz Ziółkowski</strong></p>
|
||||
</div>
|
||||
<div class="about-contributors">
|
||||
<h3>Contributors</h3>
|
||||
<div>
|
||||
<p>Secure Element API Development</p>
|
||||
<p>
|
||||
<strong>
|
||||
Garner Lee, Krzysztof Mioduszewski, Siddartha Pothapragada, Arno Puder
|
||||
</strong>
|
||||
</p>
|
||||
<p>(Deutsche Telekom Silicon Valley Innovation Center)</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>SIM Integration Support</p>
|
||||
<p>
|
||||
<strong>
|
||||
Andrzej Baraniak, Łukasz Leszek
|
||||
</strong>
|
||||
</p>
|
||||
<p>(T-Mobile Polska)</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Mobile App Consultancy</p>
|
||||
<p>
|
||||
<strong>
|
||||
Adam Paziewski, Radosław Rzepkowski
|
||||
</strong>
|
||||
</p>
|
||||
<p>(T-Mobile Polska)</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<section role="region" id="settings-pin" class="settings-subpage vbox fit scroll skin-organic pin-view">
|
||||
<header>
|
||||
<button class="gaia-icon icon-back"></button>
|
||||
<h1 class="title ">PIN Toggle</h1>
|
||||
</header>
|
||||
<section class="gaia-list fit scroll subview-content">
|
||||
</section>
|
||||
</section>
|
||||
<section role="region" id="settings-configuration" class="settings-subpage vbox fit scroll skin-organic">
|
||||
<header>
|
||||
<button class="gaia-icon icon-back"></button>
|
||||
<menu type="toolbar">
|
||||
<button id="settings-configuration-done">Done</button>
|
||||
</menu>
|
||||
<h1 class="title ">Advanced Settings</h1>
|
||||
</header>
|
||||
<section class="gaia-list fit scroll subview-content">
|
||||
<section>
|
||||
<p class="settings-disclaimer">
|
||||
This menu lets you configure SE-related parameters.
|
||||
Changing these settings can cause the app to stop working.
|
||||
To restore original settings, please reinstall the app.
|
||||
</p>
|
||||
<header>Contactless Registry Service AID</header>
|
||||
<p>
|
||||
<input id="crs-aid" placeholder="CRS AID" required="true" type="text" pattern="[a-fA-F0-9]{10,32}">
|
||||
</p>
|
||||
<header>Universal Integrated Circuit Card AID</header>
|
||||
<p>
|
||||
<input id="uicc-aid" placeholder="UICC AID" required="true" type="text" pattern="[a-fA-F0-9]{10,32}">
|
||||
</p>
|
||||
<header>PIN P2</header>
|
||||
<p>
|
||||
<input id="pinP2" placeholder="PIN P2" required="true" type="text" pattern="[a-fA-F0-9]{2}">
|
||||
</p>
|
||||
<header>Default PIN</header>
|
||||
<p>
|
||||
<input id="default-pin" placeholder="Default PIN" required="true" type="text" pattern="[0-9]{4}">
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
<section role="region" id="hci-notification" class="vbox fit">
|
||||
<div class="hci-card vbox hbox middle center">
|
||||
<div class="hci-container">
|
||||
<p>You paid using:</p>
|
||||
<img id="hci-image" src="img/cards/payment_blue.png">
|
||||
<p id="hci-number"></p>
|
||||
</div>
|
||||
</div>
|
||||
<article class="notification vbox fit hbox bottom center">
|
||||
<p class="bottom-note">Tap to dismiss</p>
|
||||
</article>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
127
assets/webapps/mobile-wallet.gaiamobile.org/js/app.js
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
/* globals AppView, ERRORS, SIMAccess, SettingsController, PinController,
|
||||
AppletListView, AppletListController, HCIView, SEUtils, PAYMENT_IMG_SRC,
|
||||
NONPAYMENT_IMG_SRC, PAYMENT_APPLET_FILTER, NONPAYMENT_APPLET_FILTER,
|
||||
SIM_UPDATE, SIM_REFRESH */
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
'use strict';
|
||||
|
||||
var log = (msg) => console.log('[main-ctrl] ' + msg);
|
||||
var appState = { initialised: false, sentToBackground: false };
|
||||
var appView = new AppView('main', 'global-error');
|
||||
|
||||
if(!window.navigator.seManager) {
|
||||
log('Secure Element API not available. Exiting.');
|
||||
appView.showErrorView(ERRORS.GLOBAL.NO_API);
|
||||
return;
|
||||
}
|
||||
|
||||
// components and views instantiation
|
||||
var pin = new PinController(SIMAccess, 'pin-ctrl', 'pin');
|
||||
|
||||
var settingsPin = new PinController(SIMAccess, 'settings-pin-ctrl',
|
||||
'settings-pin-view', 'pin-template');
|
||||
var settings = new SettingsController('settings-ctrl', 'settings',
|
||||
settingsPin);
|
||||
|
||||
var paymentsView = new AppletListView('payments-view', PAYMENT_IMG_SRC);
|
||||
var hciView = new HCIView('hci-notification', PAYMENT_IMG_SRC);
|
||||
var payments = new AppletListController(SIMAccess,'payments-ctrl',
|
||||
paymentsView, PAYMENT_APPLET_FILTER);
|
||||
|
||||
var nonPaymentsView = new AppletListView('nonpayments-view',
|
||||
NONPAYMENT_IMG_SRC);
|
||||
var nonPayments =
|
||||
new AppletListController(SIMAccess, 'nonpayments-ctrl', nonPaymentsView,
|
||||
NONPAYMENT_APPLET_FILTER, true);
|
||||
|
||||
// updating SIMAccess with settings read from localStorage
|
||||
SIMAccess.crsAid = settings.crsAid;
|
||||
SIMAccess.uiccAid = settings.uiccAid;
|
||||
SIMAccess.pinP2 = settings.pinP2;
|
||||
|
||||
// pin verification
|
||||
var performPinCheck = function performPinCheck() {
|
||||
if(settings.pinEnabled && !pin.pinCheckInProgress) {
|
||||
appView.hideMainView();
|
||||
return pin.verifyPin().then((verified) => {
|
||||
if(!verified) {
|
||||
return Promise.reject('Pin verification failed');
|
||||
}
|
||||
|
||||
appView.showMainView();
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// app starting point
|
||||
var initApp = function intitApp() {
|
||||
appView.showMainView();
|
||||
SIMAccess.start()
|
||||
.then(performPinCheck)
|
||||
.then(() => payments.init())
|
||||
.then(() => nonPayments.init())
|
||||
.then(() => { appState.initialised = true; })
|
||||
.catch((error) => {
|
||||
log('Init promise chain broken. Reason: ' + error);
|
||||
appView.showErrorView(error);
|
||||
SIMAccess.stop();
|
||||
});
|
||||
};
|
||||
|
||||
// global events, system-messages handlers
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
log('Visibility change');
|
||||
if(!document.hidden && appState.initialised) {
|
||||
performPinCheck().catch((e) => appView.showErrorView(e));
|
||||
}
|
||||
|
||||
appState.sentToBackground = document.hidden;
|
||||
});
|
||||
|
||||
window.navigator.mozSetMessageHandler('nfc-hci-event-transaction', (msg) => {
|
||||
var shouldClose = !appState.initialised || appState.sentToBackground;
|
||||
var aid = SEUtils.byteToHexString(msg.aid);
|
||||
log('Got HCI EVT_TRANSACTION from AID ' + aid);
|
||||
|
||||
// SIM card is not accessible for the first 1.5s after HCI
|
||||
setTimeout(() => payments.handleHCIEvt(msg, settings.fastpayEnabled), 2000);
|
||||
hciView.show(aid, shouldClose);
|
||||
});
|
||||
|
||||
// coponent specific event handlers
|
||||
appView.addListener({ onEvent: (id, data) => {
|
||||
if(data.action === 'open-settings') {
|
||||
settings.show();
|
||||
appView.hideErrorView();
|
||||
}
|
||||
}});
|
||||
|
||||
settings.addListener({ onEvent: (id, data) => {
|
||||
log('Settings event: ' + JSON.stringify(data));
|
||||
if(data.error) {
|
||||
appView.showErrorView(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.action === 'settings-updated') {
|
||||
var refreshNeeded = false;
|
||||
Object.keys(data.changes).forEach(key => {
|
||||
refreshNeeded = refreshNeeded || SIM_REFRESH.indexOf(key) !== -1;
|
||||
|
||||
if(SIM_UPDATE.indexOf(key) !== -1) {
|
||||
SIMAccess[key] = data.changes[key];
|
||||
}
|
||||
});
|
||||
|
||||
if(refreshNeeded) {
|
||||
initApp();
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
log('Init access, getting applets from CRS');
|
||||
initApp();
|
||||
});
|
||||
53
assets/webapps/mobile-wallet.gaiamobile.org/js/app_view.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
'use strict';
|
||||
|
||||
/* globals ErrorView, addMixin, ObserverSubjectMixin */
|
||||
/* exported AppView */
|
||||
|
||||
(function(exports) {
|
||||
var AppView = function(mainViewId, errorViewId) {
|
||||
this._mainEl = document.querySelector('#' + mainViewId);
|
||||
|
||||
this._errorView = new ErrorView(errorViewId);
|
||||
this._errorView.addListener(this);
|
||||
|
||||
var settingsBtn = this._mainEl.querySelector('#settings-edit');
|
||||
settingsBtn.addEventListener('click', () => this._settingsClicked());
|
||||
|
||||
addMixin(this, ObserverSubjectMixin);
|
||||
};
|
||||
|
||||
AppView.prototype = {
|
||||
_mainEl: null,
|
||||
_errorView: null,
|
||||
|
||||
showMainView: function showMainView() {
|
||||
this._mainEl.classList.remove('hide');
|
||||
this._errorView.hide();
|
||||
},
|
||||
|
||||
hideMainView: function hideMainView() {
|
||||
this._mainEl.classList.add('hide');
|
||||
},
|
||||
|
||||
showErrorView: function showErrorView(error) {
|
||||
this._errorView.handleError(error);
|
||||
this.hideMainView();
|
||||
},
|
||||
|
||||
hideErrorView: function hideErrorView() {
|
||||
this._errorView.hide();
|
||||
},
|
||||
|
||||
onEvent: function onEvent(id, data) {
|
||||
if(data.action === 'open-settings') {
|
||||
this._settingsClicked();
|
||||
}
|
||||
},
|
||||
|
||||
_settingsClicked: function _settingsClicked() {
|
||||
this._notify({ action: 'open-settings' });
|
||||
},
|
||||
},
|
||||
|
||||
exports.AppView = AppView;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
/* globals addMixin, DetailsViewMixin, DebugMixin */
|
||||
/* exported AppletDetailsView */
|
||||
|
||||
(function(exports) {
|
||||
var AppletDetailsView = function(id, applet) {
|
||||
addMixin(this, DetailsViewMixin);
|
||||
addMixin(this, DebugMixin);
|
||||
|
||||
this.initDetailsView(id, applet);
|
||||
};
|
||||
|
||||
AppletDetailsView.prototype = {
|
||||
_template:
|
||||
'<ul class="details">' +
|
||||
'<li id="aid"><p class="label">AID</p><p class="value"></p></li>' +
|
||||
'<li class="details-more">' +
|
||||
'<p class="details-note">' +
|
||||
'More details read from the applet can be presented here' +
|
||||
'</p>' +
|
||||
'</li>' +
|
||||
'</ul>',
|
||||
|
||||
_render: function(applet) {
|
||||
this._el.querySelector('#aid .value')
|
||||
.textContent = applet.aid;
|
||||
},
|
||||
};
|
||||
|
||||
exports.AppletDetailsView = AppletDetailsView;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
'use strict';
|
||||
|
||||
/* globals AppletListModel, AppletListView, Timer, SEUtils,
|
||||
addMixin, DebugMixin */
|
||||
/* exported AppletListController */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var AppletListController = function(simDataSource, id, view, filter, multipleAppletsActive){
|
||||
this._simDataSource = simDataSource;
|
||||
this._id = id;
|
||||
this._filter = filter;
|
||||
this._multipleAppletsActive = multipleAppletsActive;
|
||||
|
||||
this._view = view;
|
||||
this._view.addListener(this);
|
||||
|
||||
addMixin(this, DebugMixin);
|
||||
};
|
||||
|
||||
AppletListController.prototype = {
|
||||
_id: null,
|
||||
_viewId: null,
|
||||
_multipleAppletsActive: false,
|
||||
|
||||
_simDataSource: null,
|
||||
|
||||
_model: null,
|
||||
_view: null,
|
||||
_filter: null,
|
||||
|
||||
init: function alc_init() {
|
||||
return this.refreshModelView();
|
||||
},
|
||||
|
||||
refreshModelView: function alc_refreshModelView() {
|
||||
return this._refreshModel()
|
||||
.then((simAccessTime) => {
|
||||
var viewUpdateTime = this._refreshView();
|
||||
this.debug('Applets read in ' + simAccessTime + ' ms. ' +
|
||||
'View refreshed in ' + viewUpdateTime + ' ms.');
|
||||
|
||||
return simAccessTime;
|
||||
});
|
||||
},
|
||||
|
||||
// TODO unify AID arguments
|
||||
handleHCIEvt: function alc_handleHCIEvt(hci, fastPayOn) {
|
||||
var aid = SEUtils.byteToHexString(hci.aid);
|
||||
var applet = this._model.getAppletByHexAID(aid);
|
||||
|
||||
if(!applet) {
|
||||
this.debug('Got HCI from uknown aid '+ aid);
|
||||
return;
|
||||
}
|
||||
|
||||
this.debug('HCI EVT_TRANSACTION from ' + aid);
|
||||
if(!fastPayOn) {
|
||||
this._toggleAppletCLF(aid, false)
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
},
|
||||
|
||||
onEvent: function alc_onEvent(source, data) {
|
||||
if(data.action === 'details-hide') {
|
||||
this._view.hideDetails();
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.action === 'details-show') {
|
||||
this._view.showDetails(this._model.getAppletByID(data.id));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO add explicit event
|
||||
var applet = this._model.getAppletByID(data.id);
|
||||
var select = data.action === 'card-selection';
|
||||
|
||||
this._toggleAppletCLF(applet.aid, select);
|
||||
},
|
||||
|
||||
showView: function alc_showView() {
|
||||
this._view.visible = true;
|
||||
},
|
||||
|
||||
hideView: function alc_hideView() {
|
||||
this._view.visible = false;
|
||||
},
|
||||
|
||||
_toggleAppletCLF(aid, toggle) {
|
||||
this._view.disableCardSelection();
|
||||
|
||||
return this._simDataSource.toggleCLF(toggle, aid)
|
||||
// TODO add comment about CREL and auto off
|
||||
//.then(() => this._refreshModel())
|
||||
.catch((e) => this.debug('Toggle fast pay failed', e))
|
||||
.then((success) => {
|
||||
if(success) {
|
||||
this._model.applets.forEach((a) => {
|
||||
if(a.aid === aid) {
|
||||
a.contactlessState = toggle ? 1 : 0;
|
||||
} else if (toggle && !this._multipleAppletsActive) {
|
||||
a.contactlessState = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
this._view.updateSelection(this._model.applets);
|
||||
this._view.enableCardSelection();
|
||||
});
|
||||
},
|
||||
|
||||
_refreshModel: function alc_refreshMode() {
|
||||
var timer = new Timer();
|
||||
return this._simDataSource.getAppletsData()
|
||||
.then((applets) => {
|
||||
var simAccTime = timer.getElapsedTime();
|
||||
|
||||
if(!this._model) {
|
||||
this._model = new AppletListModel();
|
||||
}
|
||||
|
||||
this._model.updateList(applets, this._filter);
|
||||
return simAccTime;
|
||||
});
|
||||
},
|
||||
|
||||
_refreshView: function alc_refreshView() {
|
||||
var timer = new Timer();
|
||||
|
||||
this._view.refreshView(this._model.applets);
|
||||
return timer.getElapsedTime();
|
||||
}
|
||||
};
|
||||
|
||||
exports.AppletListController = AppletListController;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
'use strict';
|
||||
|
||||
/* globals addMixin, ObserverSubjectMixin, CardSwipeMixin, getSamplePaymentImg,
|
||||
ListViewMixin, AppletDetailsView, DebugMixin */
|
||||
/* exported AppletListView */
|
||||
|
||||
(function(exports) {
|
||||
var AppletListView = function(id, imgSrc, data) {
|
||||
this._id = id;
|
||||
|
||||
addMixin(this, ObserverSubjectMixin);
|
||||
addMixin(this, ListViewMixin);
|
||||
addMixin(this, CardSwipeMixin);
|
||||
addMixin(this, DebugMixin);
|
||||
|
||||
this.initListView(id, imgSrc, data);
|
||||
this._detailsView = new AppletDetailsView(id + '-details');
|
||||
};
|
||||
|
||||
AppletListView.prototype = {
|
||||
_detailsVisible: false,
|
||||
_detailsView: null,
|
||||
|
||||
refreshView: function(data) {
|
||||
this.updateList(data);
|
||||
|
||||
if(!Array.isArray(data) || !data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var tapCallback = (id) => {
|
||||
var action = 'details-' + ((this._detailsVisible) ? 'hide' : 'show');
|
||||
this._notify({ id: id, action: action});
|
||||
};
|
||||
|
||||
var swipeCallback = (activeId) => {
|
||||
if(this._detailsVisible) {
|
||||
this.hideDetails();
|
||||
}
|
||||
};
|
||||
|
||||
this.initCardSwipe('li', tapCallback, swipeCallback);
|
||||
this.updateSelection(data);
|
||||
},
|
||||
|
||||
showDetails: function alv_showDetails(applet) {
|
||||
this._el.querySelector('.slider').scrollTop = 0;
|
||||
this._detailsView.refreshView(applet);
|
||||
this._el.classList.add('details-visible');
|
||||
this._detailsVisible = true;
|
||||
},
|
||||
|
||||
hideDetails: function alv_hideDetails() {
|
||||
this._el.classList.remove('details-visible');
|
||||
this._detailsVisible = false;
|
||||
},
|
||||
};
|
||||
|
||||
exports.AppletListView = AppletListView;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
'use strict';
|
||||
|
||||
/* globals SEUtils, TAG */
|
||||
/* exported AppletModel, AppletListModel */
|
||||
|
||||
(function(exports) {
|
||||
// TODO decide how to store data in the app
|
||||
var AppletModel = function(id, aid, state){
|
||||
this.id = id;
|
||||
this.aid = SEUtils.byteToHexString(aid);
|
||||
this.lifecycleState = state[0];
|
||||
this.contactlessState = state[1];
|
||||
};
|
||||
|
||||
AppletModel.prototype = {
|
||||
// id in view
|
||||
id: null,
|
||||
aid: null,
|
||||
lifecycleState: null,
|
||||
contactlessState: null,
|
||||
|
||||
set state(value) {
|
||||
this.lifecycleState = value[0];
|
||||
this.contactlessState = value[1];
|
||||
},
|
||||
|
||||
get selected() {
|
||||
return this.contactlessState === 1;
|
||||
},
|
||||
|
||||
get imgId() {
|
||||
return this.aid;
|
||||
}
|
||||
};
|
||||
|
||||
var AppletListModel = function(appletsData, filter) {
|
||||
if(appletsData) {
|
||||
this.updateList(appletsData, filter);
|
||||
}
|
||||
};
|
||||
|
||||
AppletListModel.prototype = {
|
||||
applets: [],
|
||||
|
||||
updateList: function alm_updateList(appletsData, filter) {
|
||||
var parsedApplets = this._parseAppletList(appletsData);
|
||||
parsedApplets = (!filter) ? parsedApplets : parsedApplets.filter(filter);
|
||||
|
||||
// now we create AppletModel objects and store them in the list
|
||||
var refreshed = [];
|
||||
parsedApplets.forEach((appl, idx) => {
|
||||
var aidHex = SEUtils.byteToHexString(appl.aid);
|
||||
var nAppl = this.getAppletByHexAID(aidHex);
|
||||
if(nAppl) {
|
||||
nAppl.state = appl.state;
|
||||
} else {
|
||||
nAppl = new AppletModel('appl'+idx, appl.aid, appl.state);
|
||||
}
|
||||
|
||||
refreshed.push(nAppl);
|
||||
});
|
||||
|
||||
this.applets = refreshed;
|
||||
},
|
||||
|
||||
getAppletByID: function alm_getAppletByID(id) {
|
||||
return this.applets.find((applet) => applet.id === id);
|
||||
},
|
||||
|
||||
getAppletByHexAID: function alm_getAppletByHexAID(aid) {
|
||||
return this.applets.find((applet) => applet.aid === aid);
|
||||
},
|
||||
|
||||
_parseAppletList: function alm_parseAppletList(data) {
|
||||
var list = [];
|
||||
for(var i = 0, len = data.length; i < len;) {
|
||||
if(data[i] === TAG.CRS.APP_TEMPLATE) {
|
||||
var end = i+2 + data[i+1];
|
||||
var appletData = data.subarray(i+2, end);
|
||||
var applet = this._parseApplet(appletData);
|
||||
|
||||
if(applet) {
|
||||
list.push(applet);
|
||||
}
|
||||
i = end;
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
},
|
||||
|
||||
_parseApplet: function alm_parseApplet(data) {
|
||||
if (data[0] === TAG.CRS.AID) {
|
||||
var applet = {
|
||||
aid: data.subarray(2, 2 + data[1]),
|
||||
state: data.subarray(data[1] + 5)
|
||||
};
|
||||
return applet;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
exports.AppletModel = AppletModel;
|
||||
exports.AppletListModel = AppletListModel;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
'use strict';
|
||||
|
||||
/* exported HCIView */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const CLOSE_TIMEOUT = 1000;
|
||||
|
||||
var HCIView = function(id, imgSrc) {
|
||||
this._el = document.querySelector('#'+id);
|
||||
this._imgSrc = imgSrc;
|
||||
};
|
||||
|
||||
HCIView.prototype = {
|
||||
_el: null,
|
||||
_imgSrc: null,
|
||||
_clickHandler: null,
|
||||
|
||||
show: function show(aid, closeApp) {
|
||||
window.navigator.vibrate([50, 50, 50]);
|
||||
|
||||
this._el.querySelector('#hci-image').src = this._imgSrc(aid);
|
||||
this._el.querySelector('#hci-number').textContent = aid;
|
||||
|
||||
this._clickHandler = () => this._dismiss(closeApp);
|
||||
this._el.addEventListener('click', this._clickHandler);
|
||||
this._el.classList.add('show');
|
||||
},
|
||||
|
||||
_dismiss: function _dismiss(closeApp) {
|
||||
if(closeApp) {
|
||||
setTimeout(() => window.close(), CLOSE_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
|
||||
this._el.classList.remove('show');
|
||||
this._el.removeEventListener('click', this._clickHandler);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
exports.HCIView = HCIView;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
'use strict';
|
||||
|
||||
/* exported CardSwipeMixin */
|
||||
/* globals Swipeable */
|
||||
|
||||
(function(exports) {
|
||||
/**
|
||||
* Needs to be used with ObserverSubjectMixin
|
||||
*/
|
||||
var CardSwipeMixin = {
|
||||
_swipeHandler: null,
|
||||
|
||||
initCardSwipe: function initCardSwipe(selector, tapCb, swipeCb, options) {
|
||||
options = options || {
|
||||
'element-width': 260,
|
||||
'auto-scroll-trigger': 0.25,
|
||||
'min-speed': 0.75
|
||||
};
|
||||
|
||||
if(this._swipeHandler) {
|
||||
this._swipeHandler.disable();
|
||||
this._swipeHandler.onswipe = null;
|
||||
this._swipeHandler.ontap = null;
|
||||
this._swipeHandler.onvertswipe = null;
|
||||
this._swipeHandler = null;
|
||||
}
|
||||
|
||||
this._swipeHandler = new Swipeable(this._el, selector, options);
|
||||
if(tapCb) {
|
||||
this._swipeHandler.ontap = tapCb;
|
||||
}
|
||||
|
||||
if(swipeCb) {
|
||||
this._swipeHandler.onswipe = swipeCb;
|
||||
}
|
||||
|
||||
this._swipeHandler.onvertswipe = (direction, id) => {
|
||||
var itemEl = this._el.querySelector('#' + id);
|
||||
var action = '';
|
||||
if(direction === 'up' && !itemEl.classList.contains('selected')) {
|
||||
action = 'card-selection';
|
||||
this._changeItemSelection(itemEl, true);
|
||||
}
|
||||
|
||||
if(direction === 'down' && itemEl.classList.contains('selected')) {
|
||||
action = 'card-deselection';
|
||||
this._changeItemSelection(itemEl, false);
|
||||
}
|
||||
|
||||
if(action) {
|
||||
this._notify({ id: id, action: action});
|
||||
}
|
||||
};
|
||||
|
||||
this._swipeHandler.enable();
|
||||
},
|
||||
|
||||
updateSelection: function updateSelection(items) {
|
||||
items.forEach((item, idx) => {
|
||||
var itemEl = this._el.querySelector('#'+item.id);
|
||||
|
||||
if(item.selected && !itemEl.classList.contains('selected')) {
|
||||
this._changeItemSelection(itemEl, true);
|
||||
}
|
||||
|
||||
if(!item.selected && itemEl.classList.contains('selected')) {
|
||||
this._changeItemSelection(itemEl, false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
enableCardSelection: function enablCardSelection() {
|
||||
this._swipeHandler.unpauseSwipe();
|
||||
},
|
||||
|
||||
disableCardSelection: function disableCardSelection() {
|
||||
this._swipeHandler.pauseSwipe();
|
||||
},
|
||||
|
||||
_changeItemSelection: function changeItemSelection(itemEl, select) {
|
||||
var elIdx = this._swipeHandler._elts.indexOf(itemEl);
|
||||
if(elIdx === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(select) {
|
||||
itemEl.classList.remove('deselected');
|
||||
itemEl.classList.add('selected');
|
||||
this._swipeHandler.progressSelect(elIdx, true);
|
||||
} else {
|
||||
itemEl.classList.remove('selected');
|
||||
itemEl.classList.add('deselected');
|
||||
this._swipeHandler.progressSelect(elIdx, false);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
exports.CardSwipeMixin = CardSwipeMixin;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
'use strict';
|
||||
|
||||
/* exported DetailsViewMixin */
|
||||
(function(exports) {
|
||||
|
||||
var DetailsViewMixin = {
|
||||
_id: null,
|
||||
_el: null,
|
||||
_visible: false,
|
||||
_template: '',
|
||||
|
||||
initDetailsView: function(id, data) {
|
||||
this._id = id;
|
||||
this._el = document.querySelector('#' + this._id);
|
||||
this._el.innerHTML = this._template;
|
||||
|
||||
if(data) {
|
||||
this._render(data);
|
||||
}
|
||||
},
|
||||
|
||||
isVisible: function() {
|
||||
return this._visible;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this._visible = true;
|
||||
this._el.classList.add(this._id+'-visible');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this._visible = false;
|
||||
this._el.classList.remove(this._id+'-visible');
|
||||
},
|
||||
|
||||
_render: function(data) {},
|
||||
|
||||
refreshView: function(data) {
|
||||
if(data) {
|
||||
this._render(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.DetailsViewMixin = DetailsViewMixin;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
'use strict';
|
||||
|
||||
/* globals CARD_UNKNOWN */
|
||||
/* exported ListViewMixin */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var ListViewMixin = {
|
||||
_id: null,
|
||||
_el: null,
|
||||
_visible: false,
|
||||
|
||||
_imgMap: null,
|
||||
|
||||
_itemTemplate:
|
||||
'<div class="listview-element-pocket">' +
|
||||
'<div class="listview-element-sew"></div>' +
|
||||
'</div>' +
|
||||
'<img class="listview-element-img" src="img/cards/unknown.png">',
|
||||
|
||||
initListView: function(id, imgMap, data) {
|
||||
this._id = id;
|
||||
this._el = document.querySelector('#' + this._id);
|
||||
|
||||
this._imgMap = imgMap;
|
||||
|
||||
if(data && data.length) {
|
||||
this._render(data);
|
||||
}
|
||||
},
|
||||
|
||||
updateList(listItems) {
|
||||
if(!Array.isArray(listItems) || !listItems.length) {
|
||||
this.debug('list without elements', listItems);
|
||||
this._renderEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
this._render(listItems);
|
||||
},
|
||||
|
||||
isVisible: function() {
|
||||
return this._visible;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this._visible = true;
|
||||
this._el.classList.remove('listview-hide');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this._visible = false;
|
||||
this._el.classList.add('listview-hide');
|
||||
},
|
||||
|
||||
_render: function(data){
|
||||
var emptyLi = document.createElement('li');
|
||||
emptyLi.innerHTML = this._itemTemplate;
|
||||
var ulFragment = document.createDocumentFragment();
|
||||
data.forEach((item) => {
|
||||
var li = emptyLi.cloneNode(true);
|
||||
li.id = item.id;
|
||||
|
||||
var src = this._imgMap(item.imgId);
|
||||
// TODO remove CARD_UNKNOW dependency here
|
||||
li.querySelector('img').src = src ? src : CARD_UNKNOWN;
|
||||
|
||||
ulFragment.appendChild(li);
|
||||
});
|
||||
|
||||
var ul = this._el.querySelector('ul');
|
||||
ul.innerHTML = '';
|
||||
ul.appendChild(ulFragment);
|
||||
},
|
||||
|
||||
_renderEmpty: function() {
|
||||
var emptyLi = document.createElement('li');
|
||||
emptyLi.innerHTML = this._itemTemplate;
|
||||
|
||||
var ul = this._el.querySelector('ul');
|
||||
ul.innerHTML = '';
|
||||
ul.appendChild(emptyLi);
|
||||
},
|
||||
};
|
||||
|
||||
exports.ListViewMixin = ListViewMixin;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
'use strict';
|
||||
|
||||
/* globals GestureDetector */
|
||||
/* exported Swipeable */
|
||||
|
||||
(function(exports) {
|
||||
var Swipeable = function(container, eltSelector, options) {
|
||||
this._container = container.querySelector('.swipeable-container');
|
||||
this._eltSelector = eltSelector;
|
||||
|
||||
this.updateElements();
|
||||
this._createProgress();
|
||||
|
||||
this._gestureDetector = new GestureDetector(this._container,
|
||||
{ holdEvents: false });
|
||||
this._panHandler = e => this._handlePan(e);
|
||||
this._swipeHandler = e => this._handleSwipe(e);
|
||||
|
||||
this._container.addEventListener('tap', (e) => {
|
||||
if(this._tapCallback) {
|
||||
this._tapCallback(this._elts[this._activeEltIdx].id);
|
||||
}
|
||||
});
|
||||
|
||||
options = options || {};
|
||||
this._elWidth = options['element-width'] || this._elts[0].offsetWidth;
|
||||
this._triggerPoint = options['auto-scroll-trigger'] || this._triggerPoint;
|
||||
this._minSpeed = options['min-speed'] || this._minSpeed;
|
||||
};
|
||||
|
||||
Swipeable.prototype = {
|
||||
_gestureDetector: null,
|
||||
_container: null,
|
||||
_eltSelector: null,
|
||||
_elts: [],
|
||||
_progressElts: null,
|
||||
|
||||
_elWidth: 0,
|
||||
_triggerPoint: 0.25,
|
||||
_minSpeed: 1.0,
|
||||
|
||||
_inTransition: false,
|
||||
_offset: 0,
|
||||
_activeEltIdx: 0,
|
||||
|
||||
_tapCallback: null,
|
||||
_swipeCallback: null,
|
||||
_vertSwipeCallback: null,
|
||||
|
||||
set ontap(callback) {
|
||||
this._tapCallback = callback;
|
||||
},
|
||||
|
||||
set onswipe(callback) {
|
||||
this._swipeCallback = callback;
|
||||
},
|
||||
|
||||
set onvertswipe(callback) {
|
||||
this._vertSwipeCallback = callback;
|
||||
},
|
||||
|
||||
enable: function() {
|
||||
this._gestureDetector.startDetecting();
|
||||
this.unpauseSwipe();
|
||||
},
|
||||
|
||||
disable: function() {
|
||||
this._gestureDetector.stopDetecting();
|
||||
},
|
||||
|
||||
pauseSwipe: function() {
|
||||
this._container.removeEventListener('pan', this._panHandler);
|
||||
this._container.removeEventListener('swipe', this._swipeHandler);
|
||||
},
|
||||
|
||||
unpauseSwipe: function() {
|
||||
this._container.addEventListener('pan', this._panHandler);
|
||||
this._container.addEventListener('swipe', this._swipeHandler);
|
||||
},
|
||||
|
||||
updateElements: function() {
|
||||
var nodeList = this._container.querySelectorAll('.swipeable > ' + this._eltSelector);
|
||||
this._elts = Array.prototype.slice.call(nodeList);
|
||||
},
|
||||
|
||||
_createProgress: function() {
|
||||
if(!this._elts || !this._elts.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var progressDiv = this._container.querySelector('.swipeable-progress');
|
||||
if(!progressDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
var progressFragment = document.createDocumentFragment();
|
||||
this._progressElts = this._elts.map(() => {
|
||||
var div = document.createElement('div');
|
||||
progressFragment.appendChild(div);
|
||||
return div;
|
||||
});
|
||||
|
||||
this._progressElts[0].classList.add('swipeable-progress-active');
|
||||
progressDiv.innerHTML = '';
|
||||
progressDiv.appendChild(progressFragment);
|
||||
},
|
||||
|
||||
_updateProgress: function() {
|
||||
this._progressElts.forEach((elt, idx) => {
|
||||
if(idx === this._activeEltIdx) {
|
||||
elt.classList.add('swipeable-progress-active');
|
||||
} else if(elt.classList.contains('swipeable-progress-active')) {
|
||||
elt.classList.remove('swipeable-progress-active');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
progressSelect: function progressSelect(idx, select) {
|
||||
var pElt = this._progressElts[idx];
|
||||
if(!pElt) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(select) {
|
||||
pElt.classList.add('swipeable-progress-selected');
|
||||
} else if(pElt.classList.contains('swipeable-progress-selected')) {
|
||||
pElt.classList.remove('swipeable-progress-selected');
|
||||
}
|
||||
},
|
||||
|
||||
_handlePan: function(event) {
|
||||
if(this._inTransition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(event.detail.relative.dx) > Math.abs(event.detail.relative.dy)) {
|
||||
this._offset += event.detail.relative.dx;
|
||||
this._animate();
|
||||
}
|
||||
},
|
||||
|
||||
_handleSwipe: function(event) {
|
||||
if(this._vertSwipeCallback &&
|
||||
['up', 'down'].indexOf(event.detail.direction) !== -1) {
|
||||
var id = this._elts[this._activeEltIdx].id;
|
||||
this._vertSwipeCallback(event.detail.direction, id);
|
||||
}
|
||||
|
||||
var elPos = Math.abs(this._offset)%this._elWidth;
|
||||
var triggerPassed = elPos > this._elWidth * this._triggerPoint;
|
||||
|
||||
var velocity = event.detail.vx;
|
||||
var fastEnough = Math.abs(velocity) > this._minSpeed;
|
||||
|
||||
var direction = (event.detail.direction === 'left') ? 1 : -1;
|
||||
var newIdx = this._computeNewIndex(direction);
|
||||
|
||||
// Make sure that that the speed and pan amount are in the same direction
|
||||
//var samedirection = velocity === 0 || this._offset / velocity >= 0;
|
||||
|
||||
if((triggerPassed || fastEnough) && newIdx !== this._activeEltIdx) {
|
||||
var speed = Math.max(Math.abs(velocity), this._minSpeed);
|
||||
var time = (this._elWidth - Math.abs(this._offset%this._elWidth)) / speed;
|
||||
|
||||
this._activeEltIdx = newIdx;
|
||||
this._performScroll(time);
|
||||
this._updateProgress();
|
||||
if (this._swipeCallback) {
|
||||
this._swipeCallback(this._activeEltIdx);
|
||||
}
|
||||
} else if(this._offset !== 0){
|
||||
var time = Math.abs(this._offset%this._elWidth) / this._minSpeed;
|
||||
this._performScroll(time);
|
||||
if (this._swipeCallback) {
|
||||
this._swipeCallback(this._activeEltIdx);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_animate: function(time) {
|
||||
var handler = (e) => {
|
||||
e.target.style.transition = null;
|
||||
e.target.removeEventListener('transitionend', handler);
|
||||
};
|
||||
this._elts.forEach((el) => {
|
||||
if(time) {
|
||||
el.style.transition = 'transform ' + time + 'ms ease';
|
||||
el.addEventListener('transitionend', handler);
|
||||
}
|
||||
el.style.transform = 'translate3d('+this._offset+'px,0,0)';
|
||||
});
|
||||
},
|
||||
|
||||
_performScroll: function(time) {
|
||||
this._inTransition = true;
|
||||
this._offset = -1 * this._activeEltIdx * this._elWidth;
|
||||
this._animate(time);
|
||||
setTimeout(() => { this._inTransition = false; }, time);
|
||||
},
|
||||
|
||||
_computeNewIndex: function(direction) {
|
||||
var index = this._activeEltIdx + direction;
|
||||
var offBounds = index < 0 || index >= this._elts.length;
|
||||
return offBounds ? this._activeEltIdx : index;
|
||||
}
|
||||
};
|
||||
|
||||
exports.Swipeable = Swipeable;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
'use strict';
|
||||
|
||||
/* exported Timer, addMixin, ObserverSubjectMixin, DebugMixin */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
function addMixin(destination, source) {
|
||||
for (var k in source) {
|
||||
if (source.hasOwnProperty(k) && !(k in destination)) {
|
||||
destination[k] = source[k];
|
||||
}
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
var DebugMixin = {
|
||||
DEBUG: true,
|
||||
|
||||
debug: function(msg, obj) {
|
||||
if(!this.DEBUG) {
|
||||
return;
|
||||
}
|
||||
|
||||
var str = (this._id) ? ('[' + this._id + '] ' + msg) : msg;
|
||||
if(obj) {
|
||||
str += '|' + JSON.stringify(obj);
|
||||
}
|
||||
console.log(str);
|
||||
}
|
||||
};
|
||||
|
||||
var ObserverSubjectMixin = {
|
||||
_notify: function mlv_notify(data) {
|
||||
if(this._observers && Array.isArray(this._observers)) {
|
||||
this._observers.forEach((obs) => obs.onEvent(this._id, data));
|
||||
}
|
||||
},
|
||||
|
||||
addListener: function mlv_addListener(observer) {
|
||||
if('onEvent' in observer) {
|
||||
if(!this._observers) {
|
||||
this._observers = [];
|
||||
}
|
||||
this._observers.push(observer);
|
||||
}
|
||||
},
|
||||
|
||||
removeListener: function mlv_removeListener(observer) {
|
||||
if(!this._observers || !Array.isArray(this._observers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = this._observers.indexOf(observer);
|
||||
if(idx !== -1) {
|
||||
this._observers.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var Timer = function() {
|
||||
this._startTime = new Date();
|
||||
};
|
||||
|
||||
Timer.prototype = {
|
||||
_startTime: null,
|
||||
|
||||
getElapsedTime: function getElapsedTime() {
|
||||
var endTime = new Date();
|
||||
return endTime.getTime() - this._startTime.getTime();
|
||||
},
|
||||
|
||||
reset: function reset() {
|
||||
this._startTime = new Date();
|
||||
}
|
||||
};
|
||||
|
||||
exports.addMixin = addMixin;
|
||||
exports.DebugMixin = DebugMixin;
|
||||
exports.ObserverSubjectMixin = ObserverSubjectMixin;
|
||||
exports.Timer = Timer;
|
||||
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
'use strict';
|
||||
|
||||
/* exported ERRORS */
|
||||
(function(exports) {
|
||||
|
||||
const ERRORS = {
|
||||
SIM: {
|
||||
NOT_IDLE: 'Wrong sim access state.',
|
||||
ACCESS_FAILED: 'Failed to enable SIM access.',
|
||||
NO_READER: 'No reader with SE present.',
|
||||
APPLETS_FAILURE: 'Failed to get applets data.',
|
||||
PIN_FAILURE: 'Failed to issue PIN command.',
|
||||
},
|
||||
|
||||
GLOBAL: {
|
||||
NO_API: 'No Secure Element API present.',
|
||||
NO_SIM: 'No SIM present.',
|
||||
SIM_FAILURE: 'SIM failure.',
|
||||
UNKNOWN: 'Unknown error.'
|
||||
}
|
||||
};
|
||||
|
||||
exports.ERRORS = ERRORS;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
'use strict';
|
||||
|
||||
/* globals ERRORS, addMixin, ObserverSubjectMixin */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const ERROR_VIEW_ID = {};
|
||||
ERROR_VIEW_ID[ERRORS.GLOBAL.NO_API] = 'no-api';
|
||||
ERROR_VIEW_ID[ERRORS.GLOBAL.NO_SIM] = 'no-sim';
|
||||
ERROR_VIEW_ID[ERRORS.GLOBAL.SIM_FAILURE] = 'sim-error';
|
||||
ERROR_VIEW_ID[ERRORS.GLOBAL.Unknown] = 'unknown-error';
|
||||
|
||||
var ErrorView = function(id) {
|
||||
this._el = document.querySelector('#' + id);
|
||||
|
||||
addMixin(this, ObserverSubjectMixin);
|
||||
|
||||
var settingsBtn = this._el.querySelector('.error-settings-btn');
|
||||
settingsBtn.addEventListener('click',
|
||||
() => this._notify({ action: 'open-settings'}));
|
||||
};
|
||||
|
||||
ErrorView.prototype = {
|
||||
_el: null,
|
||||
|
||||
show: function() {
|
||||
this._el.classList.remove('hide');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this._el.classList.add('hide');
|
||||
},
|
||||
|
||||
handleError: function(error) {
|
||||
console.error(error);
|
||||
var globalError = this._mapErrorToGlobal(error);
|
||||
this._showGlobalError(globalError);
|
||||
this.show();
|
||||
},
|
||||
|
||||
_mapErrorToGlobal: function(error) {
|
||||
if(Object.keys(ERRORS.GLOBAL).find(k => ERRORS.GLOBAL[k] === error)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// mapping no reader to no SIM
|
||||
if(ERRORS.SIM.NO_READER === error) {
|
||||
return ERRORS.GLOBAL.NO_SIM;
|
||||
}
|
||||
|
||||
if(Object.keys(ERRORS.SIM).find(k => ERRORS.SIM[k] === error)) {
|
||||
return ERRORS.GLOBAL.SIM_FAILURE;
|
||||
}
|
||||
|
||||
return ERRORS.GLOBAL.UNKNOWN;
|
||||
},
|
||||
|
||||
_showGlobalError: function(globalError) {
|
||||
var errorViewId = ERROR_VIEW_ID[globalError];
|
||||
var nodesList = this._el.querySelectorAll('article');
|
||||
[].slice.call(nodesList).forEach(e => {
|
||||
if(e.id === errorViewId) {
|
||||
e.classList.remove('hide');
|
||||
} else {
|
||||
e.classList.add('hide');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
exports.ErrorView = ErrorView;
|
||||
}(window));
|
||||
66
assets/webapps/mobile-wallet.gaiamobile.org/js/ext_data.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
'use strict';
|
||||
|
||||
/* globals SEUtils */
|
||||
/* exported PAYMENT_IMG_SRC, NONPAYMENT_IMG_SRC, PAYMENT_APPLET_FILTER,
|
||||
NONPAYMENT_APPLET_FILTER, CARD_UNKNOWN */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const CARD_UNKNOWN = 'img/cards/unknown.png';
|
||||
|
||||
const SAMPLE_NONPAYMENT_IMG = [
|
||||
'img/cards/other-1.png',
|
||||
'img/cards/other-2.png',
|
||||
'img/cards/other-3.png'
|
||||
];
|
||||
|
||||
const SAMPLE_PAYMENT_IMG = [
|
||||
'img/cards/payment-1.png',
|
||||
'img/cards/payment-2.png',
|
||||
'img/cards/payment-3.png'
|
||||
];
|
||||
|
||||
var _getSampleImg = function(imageArray) {
|
||||
var counter = 0;
|
||||
var imgMap = {};
|
||||
return function(id) {
|
||||
var imgSrc = imgMap[id];
|
||||
if(imgSrc) {
|
||||
return imgSrc;
|
||||
}
|
||||
|
||||
counter = (imageArray.length - 1) === counter ? 0 : ++counter;
|
||||
if(id) {
|
||||
imgMap[id] = imageArray[counter];
|
||||
}
|
||||
|
||||
return imageArray[counter];
|
||||
};
|
||||
};
|
||||
|
||||
var PAYMENT_IMG_SRC = _getSampleImg(SAMPLE_PAYMENT_IMG);
|
||||
var NONPAYMENT_IMG_SRC = _getSampleImg(SAMPLE_NONPAYMENT_IMG);
|
||||
|
||||
var PAYMENT_APPLET_FILTER = function(appletData) {
|
||||
var hexAid = SEUtils.byteToHexString(appletData.aid);
|
||||
var hexState = SEUtils.byteToHexString(appletData.state);
|
||||
// hex states for payment applets
|
||||
// 1F00 - personalised, not active on Contactless interface (CLF)
|
||||
// 1F01 - personalised, active on CLF
|
||||
// 0708 - not personalised, not possible to use in payment
|
||||
return hexAid.startsWith('A000000004') &&
|
||||
(hexState === '1F00' || hexState === '1F01');
|
||||
};
|
||||
|
||||
var NONPAYMENT_APPLET_FILTER = function(appletData) {
|
||||
var hexAid = SEUtils.byteToHexString(appletData.aid);
|
||||
return hexAid.startsWith('D');
|
||||
};
|
||||
|
||||
exports.CARD_UNKNOWN = CARD_UNKNOWN;
|
||||
exports.PAYMENT_IMG_SRC = PAYMENT_IMG_SRC;
|
||||
exports.NONPAYMENT_IMG_SRC = NONPAYMENT_IMG_SRC;
|
||||
exports.PAYMENT_APPLET_FILTER = PAYMENT_APPLET_FILTER;
|
||||
exports.NONPAYMENT_APPLET_FILTER = NONPAYMENT_APPLET_FILTER;
|
||||
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
'use strict';
|
||||
|
||||
/* globals addMixin, DebugMixin, PinView, DEFAULTS, PIN_DEFAULTS */
|
||||
/* exported PinController */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const PIN_STATE = {
|
||||
UNKNOWN: 'unknown',
|
||||
ENABLED: 'enabled',
|
||||
DISABLED: 'disabled',
|
||||
BLOCKED: 'blocked',
|
||||
};
|
||||
|
||||
var _defaultPin = DEFAULTS.PIN_VALUE;
|
||||
|
||||
var PinController = function(simDataSource, id, viewId, templateId) {
|
||||
this._simDataSource = simDataSource;
|
||||
this._id = id;
|
||||
this._view = new PinView(viewId, PIN_DEFAULTS.PIN_LEN, templateId);
|
||||
|
||||
addMixin(this, DebugMixin);
|
||||
};
|
||||
|
||||
PinController.prototype = {
|
||||
_id: null,
|
||||
_simDataSource: null,
|
||||
_view: null,
|
||||
|
||||
// TODO consider removing
|
||||
_simPinState: PIN_STATE.UNKNOWN,
|
||||
|
||||
_pinCheckInProgress: false,
|
||||
_lastInputedPin: null,
|
||||
|
||||
get pinCheckInProgress() {
|
||||
return this._pinCheckInProgress;
|
||||
},
|
||||
|
||||
set defaultPin(value) {
|
||||
_defaultPin = value;
|
||||
},
|
||||
|
||||
get view() {
|
||||
return this._view._el;
|
||||
},
|
||||
|
||||
hideView: function() {
|
||||
this._view.hide();
|
||||
},
|
||||
|
||||
verifyPin: function() {
|
||||
return this._requestPinVerification().then((result) => {
|
||||
if(result) {
|
||||
this.hideView();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
},
|
||||
|
||||
enablePin: function() {
|
||||
return this._requestPinVerification()
|
||||
.then((verified) => {
|
||||
if(verified) {
|
||||
this.debug('Pin verified');
|
||||
this._simPinState = PIN_STATE.ENABLED;
|
||||
this._lastInputedPin = null;
|
||||
return true;
|
||||
} else {
|
||||
this.debug('Pin verification failed, pin probably disabled');
|
||||
var pin = this._lastInputedPin;
|
||||
this._lastInputedPin = null;
|
||||
return this._enableSimPin(pin);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
disablePin: function() {
|
||||
return this._requestPinVerification()
|
||||
.then((verified) => {
|
||||
if(verified) {
|
||||
this.debug('Pin verified');
|
||||
var pin = this._lastInputedPin;
|
||||
this._lastInputedPin = null;
|
||||
return this._disableSimPin(pin);
|
||||
} else {
|
||||
this.debug('Pin verification failed, pin probably disabled');
|
||||
this._lastInputedPin = null;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
changePin: function() {
|
||||
var pin;
|
||||
return this._requestPinVerification(this._view.REQUEST_BEFORE_CHANGE)
|
||||
.then((verified) => {
|
||||
if(!verified) {
|
||||
this.debug('Pin not verfied, not changing');
|
||||
this._lastInputedPin = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
pin = this._lastInputedPin;
|
||||
this._lastInputedPin = null;
|
||||
return this._getNewPin();
|
||||
})
|
||||
.then((newPin) => {
|
||||
this.debug('New pin value ' + newPin);
|
||||
var pinBytes = this._formatPin(pin);
|
||||
var newPinBytes = this._formatPin(newPin);
|
||||
return this._simDataSource.changePIN(pinBytes, newPinBytes);
|
||||
})
|
||||
.then((result) => this._handlePinChangeResult(result));
|
||||
},
|
||||
|
||||
cancelPinAction: function() {
|
||||
this._view.cancelPinRequest();
|
||||
},
|
||||
|
||||
_handlePinChangeResult: function(result) {
|
||||
this.debug('Pin change result ' + result);
|
||||
if(result === '9000') {
|
||||
return true;
|
||||
} else {
|
||||
this.debug('Unexpected error');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_getNewPin: function(prevAttempFailed) {
|
||||
var newPin;
|
||||
return this._getUserPin(PIN_DEFAULTS.MAX_PUK_ATTEMPTS, prevAttempFailed,
|
||||
this._view.REQUEST_NEW_PIN)
|
||||
.then((pin1) => {
|
||||
newPin = pin1;
|
||||
return this._getUserPin(PIN_DEFAULTS.MAX_PUK_ATTEMPTS, false,
|
||||
this._view.REQUEST_CONFIRM);
|
||||
})
|
||||
.then((pin2) => {
|
||||
if(newPin === pin2) {
|
||||
return newPin;
|
||||
}
|
||||
|
||||
this.debug('Pins do not match');
|
||||
return this._getNewPin(true);
|
||||
});
|
||||
},
|
||||
|
||||
_requestPinVerification: function(reqType) {
|
||||
this.debug('_requestPinVerification, SIM PIN : ' + this._simPinState);
|
||||
// TODO verify if we need this
|
||||
//if(this._pinCheckInProgress) {
|
||||
// return Promise.resolve(false);
|
||||
//}
|
||||
|
||||
this._pinCheckInProgress = true;
|
||||
this._view.show();
|
||||
return this._performVerification(PIN_DEFAULTS.MAX_PIN_ATTEMPTS, false, reqType)
|
||||
.then((verified) => {
|
||||
if(verified) {
|
||||
//this._view.hide();
|
||||
// TODO think this through
|
||||
this._pinCheckInProgress = false;
|
||||
}
|
||||
|
||||
return verified;
|
||||
});
|
||||
},
|
||||
|
||||
_getUserPin: function(attemptsLeft, lastVerFailed, reqType, pinLen) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var listener = {
|
||||
onEvent: (id, data) => {
|
||||
this._view.removeListener(listener);
|
||||
if (data.pin) {
|
||||
this.debug('got PIN: ' + data.pin);
|
||||
resolve(data.pin);
|
||||
} else {
|
||||
reject('pin-cancelled');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this._view.addListener(listener);
|
||||
if(!this._view.isVisible) {
|
||||
this._view.show();
|
||||
}
|
||||
|
||||
this.debug('waiting for PIN input from user');
|
||||
this._view.requestPin(attemptsLeft, lastVerFailed, reqType, pinLen);
|
||||
});
|
||||
},
|
||||
|
||||
_performVerification: function(attemptsLeft, lastVerFailed, reqType) {
|
||||
return this._getUserPin(attemptsLeft, lastVerFailed, reqType)
|
||||
.then((pin) => {
|
||||
this._lastInputedPin = pin;
|
||||
var pinBytes = this._formatPin(pin);
|
||||
this.debug('verifying PIN ' + pinBytes);
|
||||
return this._simDataSource.verifyPIN(pinBytes);
|
||||
})
|
||||
.then((result) => this._handleVerficationResult(result));
|
||||
//TODO should we catch here?
|
||||
},
|
||||
|
||||
_handleVerficationResult: function(result) {
|
||||
this.debug('Verification finished, status word:' + result);
|
||||
if(result === '9000') {
|
||||
return true;
|
||||
} else if(result === '63C0' || result === '6983') {
|
||||
this.debug('Pin blocked');
|
||||
return this._handlePinBlocked();
|
||||
} else if(result.startsWith('63C')) {
|
||||
var attemptsLeft = Number.parseInt(result[3]);
|
||||
this.debug('Verification failed, tries left: ' + attemptsLeft);
|
||||
return this._performVerification(attemptsLeft, true);
|
||||
} else {
|
||||
this.debug('Unknown status word, PIN probably not enabled');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_handlePinBlocked: function() {
|
||||
this._simPinState = PIN_STATE.BLOCKED;
|
||||
|
||||
this.debug('PUK unblock flow start.');
|
||||
var unblockRequested = () => this._performPinUnblock(PIN_DEFAULTS.MAX_PUK_ATTEMPTS);
|
||||
return this._view.blockPin().then(unblockRequested);
|
||||
},
|
||||
|
||||
_performPinUnblock: function(attemptsLeft, lastUnblockFailed) {
|
||||
var puk;
|
||||
return this._getUserPin(attemptsLeft, lastUnblockFailed,
|
||||
this._view.REQUEST_PUK, PIN_DEFAULTS.MAX_SIM_PIN_LEN)
|
||||
.then((inputedPuk) => {
|
||||
this.debug('Inputed PUK ' + inputedPuk);
|
||||
puk = inputedPuk;
|
||||
return this._getNewPin(false);
|
||||
})
|
||||
.then((inputedPin) => {
|
||||
this.debug('Inputed PIN ' + inputedPin);
|
||||
var bytePuk = this._formatPin(puk);
|
||||
var bytePin = this._formatPin(inputedPin);
|
||||
// if we're part of disable/enable flow we need to save pin here
|
||||
this._lastInputedPin = inputedPin;
|
||||
return this._simDataSource.unblockPIN(bytePuk, bytePin);
|
||||
})
|
||||
.then((result) => this._handlePinUnblockResult(result));
|
||||
},
|
||||
|
||||
_handlePinUnblockResult: function(result) {
|
||||
this.debug('PIN unblock result ' + result);
|
||||
if(result === '9000') {
|
||||
return true;
|
||||
} else if(result === '63C0' || !result.startsWith('63C')) {
|
||||
this.debug('Complate block');
|
||||
return this._view.blockPuk();
|
||||
} else {
|
||||
var attemptsLeft = Number.parseInt(result[3]);
|
||||
this.debug('PIN unblock failed, tries left: ' + attemptsLeft);
|
||||
return this._performPinUnblock(attemptsLeft, true);
|
||||
}
|
||||
},
|
||||
|
||||
_enableSimPin: function(pin) {
|
||||
var pinBytes = this._formatPin(pin);
|
||||
return this._simDataSource.enablePIN(pinBytes)
|
||||
.then((result) => this._handlePinEnabledResult(result));
|
||||
},
|
||||
|
||||
_handlePinEnabledResult: function(result) {
|
||||
this.debug('PIN enable result ' + result);
|
||||
if(result === '9000') {
|
||||
this._simPinState = PIN_STATE.ENABLED;
|
||||
//this._view.hide();
|
||||
return true;
|
||||
} else if(result === '63C0' || result === '6983') {
|
||||
this.debug('Pin blocked');
|
||||
return this._handlePinBlocked();
|
||||
} else {
|
||||
var tries = result.startsWith('63C') ? Number.parseInt(result[3]) : 0;
|
||||
this._view.showEnableFailed(tries, _defaultPin);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_disableSimPin: function(pin) {
|
||||
var pinBytes = this._formatPin(pin);
|
||||
return this._simDataSource.disablePIN(pinBytes)
|
||||
.then((result) => this._handlePinDisableResult(result));
|
||||
},
|
||||
|
||||
_handlePinDisableResult: function(result) {
|
||||
this.debug('PIN disable result ' + result);
|
||||
if(result === '9000') {
|
||||
this._simPinState = PIN_STATE.DISABLED;
|
||||
//this._view.hide();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_formatPin: function(pin) {
|
||||
var pinBytes = pin.split('').map((d) => d.charCodeAt());
|
||||
for(var i = pinBytes.length; i < PIN_DEFAULTS.MAX_SIM_PIN_LEN; i++) {
|
||||
pinBytes.push(PIN_DEFAULTS.PIN_PADDING);
|
||||
}
|
||||
|
||||
return pinBytes;
|
||||
}
|
||||
};
|
||||
|
||||
exports.PinController = PinController;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
274
assets/webapps/mobile-wallet.gaiamobile.org/js/pin/pin_view.js
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
'use strict';
|
||||
|
||||
/* globals addMixin, ObserverSubjectMixin, DebugMixin */
|
||||
/* exported PinView */
|
||||
|
||||
(function(exports) {
|
||||
// number of pin input attempts which should
|
||||
// trigger the pin warning display
|
||||
const PIN_ATTEMPTS_WARNING = 2;
|
||||
const PIN_INCORRECT_TIMEOUT = 1000; //ms
|
||||
const PIN_DEFAULT_LEN = 4;
|
||||
|
||||
const PIN_REQ_DEF = 'request-default-pin';
|
||||
const PIN_REQ_BEFORE_CHANGE = 'request-pin-before-change';
|
||||
const PIN_REQ_PUK = 'request-puk';
|
||||
const PIN_REQ_NEW_PIN = 'request-new-pin';
|
||||
const PIN_REQ_REPEAT_NEW_PIN = 'request-repeat-new-pin';
|
||||
|
||||
const PIN_ENTRY_TEXT_MAP = {};
|
||||
|
||||
PIN_ENTRY_TEXT_MAP[PIN_REQ_DEF] = {
|
||||
entry: 'Please enter your PIN',
|
||||
error: 'Incorrect PIN'
|
||||
};
|
||||
|
||||
PIN_ENTRY_TEXT_MAP[PIN_REQ_BEFORE_CHANGE] = {
|
||||
entry: 'Please enter current PIN',
|
||||
error: 'Incorrect PIN'
|
||||
},
|
||||
|
||||
PIN_ENTRY_TEXT_MAP[PIN_REQ_PUK] = {
|
||||
entry: 'Please enter your PUK',
|
||||
error: 'Incorrect PUK'
|
||||
};
|
||||
|
||||
PIN_ENTRY_TEXT_MAP[PIN_REQ_NEW_PIN] = {
|
||||
entry: 'Please enter new PIN',
|
||||
error: 'New PIN mismatch',
|
||||
};
|
||||
|
||||
PIN_ENTRY_TEXT_MAP[PIN_REQ_REPEAT_NEW_PIN] = {
|
||||
entry: 'Please confirm new PIN',
|
||||
error: ''
|
||||
};
|
||||
|
||||
var PinView = function(id, defPinLen, templateId) {
|
||||
this._id = id;
|
||||
this._defaultPinLen = defPinLen || PIN_DEFAULT_LEN;
|
||||
this._maxPinLen = this._defaultPinLen;
|
||||
|
||||
addMixin(this, ObserverSubjectMixin);
|
||||
addMixin(this, DebugMixin);
|
||||
|
||||
if(templateId) {
|
||||
this._el = document.querySelector('#' + templateId).cloneNode(true);
|
||||
this._el.id = this._id;
|
||||
} else {
|
||||
this._el = document.querySelector('#' + this._id);
|
||||
}
|
||||
|
||||
this._init();
|
||||
};
|
||||
|
||||
PinView.prototype = {
|
||||
REQUEST_PIN: PIN_REQ_DEF,
|
||||
REQUEST_BEFORE_CHANGE: PIN_REQ_BEFORE_CHANGE,
|
||||
REQUEST_PUK: PIN_REQ_PUK,
|
||||
REQUEST_NEW_PIN: PIN_REQ_NEW_PIN,
|
||||
REQUEST_CONFIRM: PIN_REQ_REPEAT_NEW_PIN,
|
||||
|
||||
_el: null,
|
||||
_id: null,
|
||||
_visible: false,
|
||||
|
||||
_pinInputEl: null,
|
||||
_inputedPin: '',
|
||||
|
||||
// we have a default PIN length but we can do one time request for PINs
|
||||
// of different length (i.e. PUK)
|
||||
_defaultPinLen: 0,
|
||||
_maxPinLen: 0,
|
||||
|
||||
_inputBlocked: false,
|
||||
|
||||
isVisible() {
|
||||
return this._visible;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this._visible = true;
|
||||
this._el.classList.remove('hide');
|
||||
this._clearPin();
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this._visible = false;
|
||||
this._el.classList.add('hide');
|
||||
},
|
||||
|
||||
_init: function() {
|
||||
this._pinInputEl = this._el.querySelector('.pin-input');
|
||||
|
||||
var keyHandler = this._keyHandler.bind(this);
|
||||
var keypad = this._el.querySelector('.pin-keypad');
|
||||
keypad.addEventListener('touchstart', keyHandler, true);
|
||||
|
||||
this._updatePinInput();
|
||||
},
|
||||
|
||||
_keyHandler: function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
var key = event.target.dataset.value;
|
||||
if(!key || this._inputBlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(key) {
|
||||
case 'clear': this._clearPin(); break;
|
||||
case 'backspace': this._backspacePin(); break;
|
||||
default: this._appendPin(key);
|
||||
}
|
||||
},
|
||||
|
||||
_clearPin: function() {
|
||||
this._inputedPin = '';
|
||||
this._updatePinInput();
|
||||
},
|
||||
|
||||
_backspacePin: function() {
|
||||
if(!this._inputedPin.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._inputedPin = this._inputedPin.slice(0, -1);
|
||||
this._updatePinInput();
|
||||
},
|
||||
|
||||
_appendPin: function(digit) {
|
||||
if(this._inputedPin.length >= this._maxPinLen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._inputedPin += digit;
|
||||
this._updatePinInput();
|
||||
|
||||
if(this._inputedPin.length === this._maxPinLen) {
|
||||
if(this._maxPinLen !== this._defaultPinLen) {
|
||||
this._maxPinLen = this._defaultPinLen;
|
||||
}
|
||||
this._notify({ action: 'pin-inputed', pin: this._inputedPin });
|
||||
}
|
||||
},
|
||||
|
||||
_updatePinInput: function() {
|
||||
var inputedDigits = this._inputedPin.length;
|
||||
var fragment = document.createDocumentFragment();
|
||||
for(var i = 0; i < this._maxPinLen; i++) {
|
||||
var pinDigit = document.createElement('div');
|
||||
if(i < inputedDigits) {
|
||||
pinDigit.classList.add('pin-digit-entered');
|
||||
}
|
||||
fragment.appendChild(pinDigit);
|
||||
}
|
||||
this._pinInputEl.innerHTML = '';
|
||||
this._pinInputEl.appendChild(fragment);
|
||||
},
|
||||
|
||||
_updatePinInputTexts: function _updatePinInputTexts(reqType) {
|
||||
reqType = (reqType in PIN_ENTRY_TEXT_MAP) ? reqType : PIN_REQ_DEF;
|
||||
this._el.querySelector('.pin-entry-text')
|
||||
.textContent = PIN_ENTRY_TEXT_MAP[reqType].entry;
|
||||
this._el.querySelector('.pin-entry-error-text')
|
||||
.textContent = PIN_ENTRY_TEXT_MAP[reqType].error;
|
||||
},
|
||||
|
||||
requestPin: function(attemptsLeft, showPinIncorrect, reqType, pinLen) {
|
||||
this.debug('requestPIN, tries: ' + attemptsLeft + ', type: ' + reqType);
|
||||
this._maxPinLen = pinLen || this._maxPinLen;
|
||||
this._clearPin();
|
||||
|
||||
this._updatePinInputTexts(reqType);
|
||||
this._showByClass('pin-unblocked');
|
||||
|
||||
if(showPinIncorrect) {
|
||||
this._showPinIncorrect();
|
||||
if(reqType !== PIN_REQ_NEW_PIN) {
|
||||
this._showWarning(attemptsLeft);
|
||||
}
|
||||
} else if(attemptsLeft <= PIN_ATTEMPTS_WARNING) {
|
||||
this._showWarning(attemptsLeft);
|
||||
} else {
|
||||
this._clearWarning();
|
||||
}
|
||||
},
|
||||
|
||||
cancelPinRequest: function() {
|
||||
this._maxPinLen = this._defaultPinLen;
|
||||
this._notify({});
|
||||
},
|
||||
|
||||
blockPin: function() {
|
||||
window.navigator.vibrate([200, 100, 200, 100, 200]);
|
||||
this._showByClass('pin-blocked');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
var unblockBtn = this._el.querySelector('.pin-unblock-btn');
|
||||
var onclick = () => {
|
||||
unblockBtn.removeEventListener('click', onclick);
|
||||
resolve();
|
||||
};
|
||||
|
||||
unblockBtn.addEventListener('click', onclick);
|
||||
});
|
||||
},
|
||||
|
||||
blockPuk: function() {
|
||||
window.navigator.vibrate([200, 100, 200, 100, 200]);
|
||||
this._showByClass('puk-blocked');
|
||||
return new Promise(()=>{});
|
||||
},
|
||||
|
||||
showEnableFailed: function(tries, defaultPin) {
|
||||
window.navigator.vibrate([200, 100, 200, 100, 200]);
|
||||
this._showByClass('pin-enable-error');
|
||||
this._el.querySelector('.pin-default-val').textContent = defaultPin;
|
||||
if(tries || tries === 0) {
|
||||
this._el.querySelector('.pin-enable-warning').classList.remove('hide');
|
||||
var att = (tries === 1) ? ' entry attempt' : ' entry attempts';
|
||||
this._el.querySelector('.pin-enable-attempt').textContent = tries + att;
|
||||
} else {
|
||||
this._el.querySelector('.pin-enable-warning').classList.add('hide');
|
||||
}
|
||||
},
|
||||
|
||||
_showByClass: function(clss) {
|
||||
var nodesList = this._el.querySelectorAll('article');
|
||||
Array.prototype.slice.call(nodesList).forEach(el => {
|
||||
if(el.classList.contains(clss)) {
|
||||
el.classList.remove('hide');
|
||||
} else {
|
||||
el.classList.add('hide');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_showWarning: function(attemptsLeft) {
|
||||
this._el.querySelector('.pin-warning').classList.remove('hide');
|
||||
var warning = attemptsLeft + ' attempt';
|
||||
warning = (attemptsLeft !== 1) ? (warning + 's') : warning;
|
||||
warning += ' remaining';
|
||||
this._el.querySelector('.pin-attempts').textContent = warning;
|
||||
},
|
||||
|
||||
_togglePinIncorrect: function(show) {
|
||||
this._inputBlocked = show;
|
||||
this._el.querySelector('.pin-entry').classList.toggle('hide');
|
||||
this._el.querySelector('.pin-incorrect').classList.toggle('hide');
|
||||
},
|
||||
|
||||
_showPinIncorrect: function() {
|
||||
window.navigator.vibrate([200, 100, 200]);
|
||||
this._togglePinIncorrect(true);
|
||||
setTimeout(() => this._togglePinIncorrect(false), PIN_INCORRECT_TIMEOUT);
|
||||
},
|
||||
|
||||
_clearWarning: function() {
|
||||
this._el.querySelector('.pin-warning').classList.add('hide');
|
||||
}
|
||||
};
|
||||
|
||||
exports.PinView = PinView;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
CRS AID, TAGS and APDUs based on:
|
||||
GlobalPlatform Card Technology Contactless Services Card Specification v2.2
|
||||
– Amendment C Version 1.1.1
|
||||
|
||||
UICC APDUs based on:
|
||||
ETSI TS 102 221 V8.2.0 (2009-06)
|
||||
Smart Cards; UICC-Terminal interface; Physical and logical characteristics
|
||||
(Release 8)
|
||||
*/
|
||||
|
||||
/* globals SEUtils */
|
||||
/* exported APDU, AID, TAG */
|
||||
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const AID = {
|
||||
CRS: 'A00000015143525300',
|
||||
UICC: 'A0000000871001FF33FFFF8901010100'
|
||||
};
|
||||
|
||||
const APDU = {
|
||||
// gets the response from SIM, can be used to retireve channel select
|
||||
getResponse: { cla: 0x00, ins: 0xC0, p1: 0x00, p2: 0x00 },
|
||||
readBinary: { cla: 0x00, ins: 0xB0, p1: 0x00, p2: 0x00 },
|
||||
CRS: {
|
||||
// Get applet version number and global update counter
|
||||
getData: { cla: 0x80, ins: 0xCA, p1: 0x00, p2: 0xA5 },
|
||||
// Get status of applets and Security Domains only.
|
||||
// The ISD is ignored. Get first or only occurrence
|
||||
getStatusAll1st: {
|
||||
cla: 0x80, ins: 0xF2, p1: 0x40, p2: 0x00,
|
||||
data: new Uint8Array([
|
||||
0x4F, 0x00, // applet AID, empty here
|
||||
0x5C, 0x03, // tag list to be returned
|
||||
0x4F, // AID
|
||||
0x9F, 0x70 // applet life cycle state - first byte;
|
||||
// contactless activation state - second byte
|
||||
])
|
||||
},
|
||||
// Get status of applets and Security Domains only.
|
||||
// The ISD is ignored. Get next occurrence(s), if SW is 63h10h
|
||||
getStatusAllNext: { cla: 0x80, ins: 0xF2, p1: 0x40, p2: 0x01,
|
||||
data: new Uint8Array([0x4F, 0x00, 0x5C, 0x03, 0x4F, 0x9F, 0x70]) },
|
||||
// Get status of applet identified with AID
|
||||
getStatusAID: (aid) => {
|
||||
return { cla: 0x80, ins: 0xF2, p1: 0x40, p2: 0x00,
|
||||
data: SEUtils.joinUint8Arrays(
|
||||
[0x4F, 0x0F], // applet AID
|
||||
SEUtils.hexStringToByte(aid),
|
||||
[0x5C, 0x04, // tag list
|
||||
0x4F, // AID
|
||||
0x9F, // applet lifecycle state
|
||||
0x70, // contactless activation state
|
||||
0x81 // selection priority
|
||||
]
|
||||
)
|
||||
};
|
||||
},
|
||||
activateCLF: (aid) => {
|
||||
return { cla: 0x80, ins: 0xF0, p1: 0x01, p2: 0x01,
|
||||
data: SEUtils.joinUint8Arrays([0x4F, 0x0F],
|
||||
SEUtils.hexStringToByte(aid))
|
||||
};
|
||||
},
|
||||
deactivateCLF: (aid) => {
|
||||
return { cla: 0x80, ins: 0xF0, p1: 0x01, p2: 0x00,
|
||||
data: SEUtils.joinUint8Arrays([0x4F, 0x0F],
|
||||
SEUtils.hexStringToByte(aid))
|
||||
};
|
||||
},
|
||||
nfcActivate: { cla: 0x80, ins: 0xF0, p1: 0x04, p2: 0x80,
|
||||
data: new Uint8Array([0x80, 0x01, 0x40]) },
|
||||
nfcDeactivate: { cla: 0x80, ins: 0xF0, p1: 0x04, p2: 0x00,
|
||||
data: new Uint8Array([0x80, 0x01, 0x40]) }
|
||||
},
|
||||
// PIN commands
|
||||
UICC: {
|
||||
getAttempts: (p2) => {
|
||||
return { cla: 0x00, ins: 0x20, p1: 0x00, p2: p2 };
|
||||
},
|
||||
verify: (p2, pinBytes) => {
|
||||
return { cla: 0x00, ins: 0x20, p1: 0x00, p2: p2, data: pinBytes };
|
||||
},
|
||||
enable: (p2, defPinBytes) => {
|
||||
return { cla: 0x00, ins: 0x28, p1: 0x00, p2: p2, data: defPinBytes };
|
||||
},
|
||||
disable: (p2, defPinBytes) => {
|
||||
return { cla: 0x00, ins: 0x26, p1: 0x00, p2: p2, data: defPinBytes };
|
||||
},
|
||||
change: (p2, curPinBytes, newPinBytes) => {
|
||||
var data = SEUtils.joinUint8Arrays(curPinBytes, newPinBytes);
|
||||
return { cla: 0x00, ins: 0x24, p1: 0x00, p2: p2, data: data };
|
||||
},
|
||||
unblock: (p2, pukBytes, defPinBytes) => {
|
||||
var data = SEUtils.joinUint8Arrays(pukBytes, defPinBytes);
|
||||
return { cla: 0x00, ins: 0x2C, p1: 0x00, p2: p2, data: data };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const TAG = {
|
||||
CRS: {
|
||||
APP_TEMPLATE: 0x61,
|
||||
AID: 0x4F,
|
||||
STATE: 0x9F70
|
||||
},
|
||||
};
|
||||
|
||||
exports.AID = AID;
|
||||
exports.APDU = APDU;
|
||||
exports.TAG = TAG;
|
||||
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
'use strict';
|
||||
|
||||
/* exported SEUtils */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var SEUtils = {
|
||||
byteToHexString: function byteToHexString(uint8arr) {
|
||||
if (!uint8arr) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var hexStr = '';
|
||||
for (var i = 0; i < uint8arr.length; i++) {
|
||||
var hex = (uint8arr[i] & 0xff).toString(16);
|
||||
hex = (hex.length === 1) ? '0' + hex : hex;
|
||||
hexStr += hex;
|
||||
}
|
||||
return hexStr.toUpperCase();
|
||||
},
|
||||
|
||||
hexStringToByte: function hexStringToByte(str) {
|
||||
var a = [];
|
||||
for(var i = 0, len = str.length; i < len; i+=2) {
|
||||
a.push(parseInt(str.substr(i,2),16));
|
||||
}
|
||||
return new Uint8Array(a);
|
||||
},
|
||||
|
||||
byteToASCIIString: function byteToASCIIString(arr) {
|
||||
var str = '';
|
||||
|
||||
var len = arr && arr.length ? arr.length : 0;
|
||||
for(var i = 0; i < len; i++) {
|
||||
str += arr[i] < 0x20 || arr > 0x7E ? '' : String.fromCharCode(arr[i]);
|
||||
}
|
||||
|
||||
return str;
|
||||
},
|
||||
|
||||
joinUint8Arrays: function joinUint8Arrays() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var length = args.reduce(function(a, b) { return a + b.length; }, 0);
|
||||
var out = new Uint8Array(length);
|
||||
|
||||
args.reduce(function(previousLen, buffer) {
|
||||
out.set(buffer, previousLen);
|
||||
return previousLen + buffer.length;
|
||||
}, 0);
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
// no support for multibyte tag and constructed data object
|
||||
parseSimpleTLV: function parseSimpleTLV(data) {
|
||||
if(!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = {};
|
||||
for(var i = 0, len = data.length; i < len;) {
|
||||
var tag = data[i];
|
||||
var length = data[i+1];
|
||||
var value = data.slice(i+2, i+2 + length);
|
||||
result[tag] = value;
|
||||
i += 2 + length;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
promises: {
|
||||
whilePromise: function(condition, action, init) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var loop = (value) => {
|
||||
if (condition(value)) {
|
||||
return resolve(value);
|
||||
}
|
||||
|
||||
return action(value).then(loop).catch(reject);
|
||||
};
|
||||
loop(init);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
exports.SEUtils = SEUtils;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
'use strict';
|
||||
|
||||
/* globals SEUtils, APDU, ERRORS, DEFAULTS */
|
||||
/* exported simAccessManager */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const SIM_ACCESS_DISABLED = 'disabled';
|
||||
const SIM_ACCESS_IDLE = 'idle';
|
||||
const SIM_ACCESS_BUSY = 'busy';
|
||||
|
||||
var _simState = SIM_ACCESS_DISABLED;
|
||||
var _seSession = null;
|
||||
|
||||
var _crsAid = DEFAULTS.CRS_AID;
|
||||
var _uiccAid = DEFAULTS.UICC_AID;
|
||||
var _pinP2 = DEFAULTS.PIN_P2;
|
||||
|
||||
var SIMAccessManager = {
|
||||
set crsAid(value) {
|
||||
_crsAid = value;
|
||||
},
|
||||
|
||||
get crsAidBytes() {
|
||||
return SEUtils.hexStringToByte(_crsAid);
|
||||
},
|
||||
|
||||
set uiccAid(value) {
|
||||
_uiccAid = value;
|
||||
},
|
||||
|
||||
get uiccAidBytes() {
|
||||
return SEUtils.hexStringToByte(_uiccAid);
|
||||
},
|
||||
|
||||
set pinP2(value) {
|
||||
_pinP2 = value;
|
||||
},
|
||||
|
||||
get pinP2Bytes() {
|
||||
return SEUtils.hexStringToByte(_pinP2)[0];
|
||||
},
|
||||
|
||||
start: function start() {
|
||||
if(!navigator.seManager) {
|
||||
return Promise.reject(ERRORS.GLOBAL.NO_API);
|
||||
}
|
||||
|
||||
return navigator.seManager.getSEReaders()
|
||||
.then((readers) => {
|
||||
var reader = readers.find(r => r.type.toLowerCase() === 'uicc' && r.isSEPresent);
|
||||
if(!reader) {
|
||||
return Promise.reject(ERRORS.SIM.NO_READER);
|
||||
}
|
||||
return reader.openSession();
|
||||
})
|
||||
.then((session) => {
|
||||
_seSession = session;
|
||||
_simState = SIM_ACCESS_IDLE;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
_simState = SIM_ACCESS_DISABLED;
|
||||
|
||||
if(e !== ERRORS.SIM.NO_READER) {
|
||||
e = ERRORS.SIM.ACCESS_FAILED;
|
||||
}
|
||||
return Promise.reject(e);
|
||||
});
|
||||
},
|
||||
|
||||
stop: function stop() {
|
||||
return _seSession.closeAll()
|
||||
.then(() => {
|
||||
_seSession = null;
|
||||
_simState = SIM_ACCESS_DISABLED;
|
||||
})
|
||||
.catch(e => this._cleanup(e, ERRORS.SIM.SIM_FAILURE));
|
||||
},
|
||||
|
||||
_initSIMAccess: function _initSIMAccess() {
|
||||
if(_simState !== SIM_ACCESS_IDLE) {
|
||||
return Promise.reject(ERRORS.SIM.NOT_IDLE);
|
||||
}
|
||||
|
||||
if(!_seSession.reader.isSEPresent) {
|
||||
return Promise.reject(ERRORS.SIM.NO_READER);
|
||||
}
|
||||
|
||||
_simState = SIM_ACCESS_BUSY;
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
_cleanup: function _cleanup(error, message) {
|
||||
console.error(error);
|
||||
_simState = SIM_ACCESS_IDLE;
|
||||
if(error === ERRORS.SIM.NOT_IDLE || error === ERRORS.SIM.NO_READER) {
|
||||
message = error;
|
||||
}
|
||||
return Promise.reject(message);
|
||||
},
|
||||
|
||||
getAppletsData: function getAppletsData() {
|
||||
return this._initSIMAccess()
|
||||
.then(() => _seSession.openLogicalChannel(this.crsAidBytes))
|
||||
.then((channel) => {
|
||||
return channel.transmit(APDU.CRS.getStatusAll1st);
|
||||
})
|
||||
.then((resp) => {
|
||||
var receivedData = new Uint8Array();
|
||||
|
||||
// we can get different responses, also propriatary
|
||||
// using counter to not allow to try to query the SIM indifinately
|
||||
var count = 0;
|
||||
var condition = (resp) => (count++ > 10 || resp.sw1 === 0x90 && resp.sw2 === 0x00);
|
||||
var action = (resp) => {
|
||||
receivedData = SEUtils.joinUint8Arrays(receivedData, resp.data);
|
||||
return resp.channel.transmit(APDU.CRS.getStatusAllNext);
|
||||
};
|
||||
|
||||
return SEUtils.promises.whilePromise(condition, action, resp)
|
||||
.then((lastResp) => {
|
||||
lastResp.channel.close();
|
||||
_simState = SIM_ACCESS_IDLE;
|
||||
return SEUtils.joinUint8Arrays(receivedData, lastResp.data);
|
||||
});
|
||||
})
|
||||
.catch(e => this._cleanup(e, ERRORS.SIM.APPLETS_FAILURE));
|
||||
},
|
||||
|
||||
toggleCLF: function toggleCLF(on, aid) {
|
||||
var apdu = on ? APDU.CRS.activateCLF(aid) : APDU.CRS.deactivateCLF(aid);
|
||||
return this._initSIMAccess()
|
||||
.then(() => _seSession.openLogicalChannel(this.crsAidBytes))
|
||||
.then((channel) => channel.transmit(apdu))
|
||||
.then((resp) => {
|
||||
return resp.channel.close()
|
||||
.then(() => {
|
||||
_simState = SIM_ACCESS_IDLE;
|
||||
return resp.sw1 === 0x90 && resp.sw2 === 0x00;
|
||||
});
|
||||
})
|
||||
.catch(e => this._cleanup(e, ERRORS.SIM.SIM_FAILURE));
|
||||
},
|
||||
|
||||
_execPinCommand: function _execPinCommand(type, pin1, pin2) {
|
||||
return this._initSIMAccess()
|
||||
.then(() => _seSession.openLogicalChannel(this.uiccAidBytes))
|
||||
.then((channel) => {
|
||||
var apdu = APDU.UICC[type](this.pinP2Bytes, pin1, pin2);
|
||||
if(!apdu) {
|
||||
return Promise.reject('PIN APDU type ' + type + ' not found');
|
||||
}
|
||||
return channel.transmit(apdu);
|
||||
})
|
||||
.then((resp) => {
|
||||
return resp.channel.close().then(() => {
|
||||
_simState = SIM_ACCESS_IDLE;
|
||||
return SEUtils.byteToHexString([resp.sw1, resp.sw2]);
|
||||
});
|
||||
})
|
||||
.catch(e => this._cleanup(e, ERRORS.SIM.PIN_FAILURE));
|
||||
},
|
||||
|
||||
verifyPIN: function verifyPIN(pin) {
|
||||
return this._execPinCommand('verify', pin);
|
||||
},
|
||||
|
||||
enablePIN: function enablePIN(pin) {
|
||||
return this._execPinCommand('enable', pin);
|
||||
},
|
||||
|
||||
unblockPIN: function unblockPIN(puk, pin) {
|
||||
return this._execPinCommand('unblock', puk, pin);
|
||||
},
|
||||
|
||||
disablePIN: function disablePIN(pin) {
|
||||
return this._execPinCommand('disable', pin);
|
||||
},
|
||||
|
||||
changePIN: function changePIN(currentPin, newPin) {
|
||||
return this._execPinCommand('change', currentPin, newPin);
|
||||
},
|
||||
};
|
||||
|
||||
exports.SIMAccess = SIMAccessManager;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
'use strict';
|
||||
|
||||
/* exported ConfigSubView */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var ConfigSubView = function(id, crsAid, uiccAid, pinP2, defaultPin) {
|
||||
this._id = id;
|
||||
this._el = document.querySelector('#'+id);
|
||||
|
||||
this.crsInput = this._el.querySelector('#crs-aid');
|
||||
this.crsInput.value = crsAid;
|
||||
|
||||
this.uiccInput = this._el.querySelector('#uicc-aid');
|
||||
this.uiccInput.value = uiccAid;
|
||||
|
||||
this.pinP2Input = this._el.querySelector('#pinP2');
|
||||
this.pinP2Input.value = pinP2;
|
||||
|
||||
this.defaultPinInput = this._el.querySelector('#default-pin');
|
||||
this.defaultPinInput.value = defaultPin;
|
||||
|
||||
this._el.querySelector('header button.icon-back')
|
||||
.addEventListener('click', () => this._handleConfigFinished());
|
||||
this._el.querySelector('#settings-configuration-done')
|
||||
.addEventListener('click', () => this._handleConfigFinished());
|
||||
};
|
||||
|
||||
ConfigSubView.prototype = {
|
||||
_el: null,
|
||||
_id: null,
|
||||
_visible: false,
|
||||
|
||||
crsInput: null,
|
||||
uiccInput: null,
|
||||
pinP2Input: null,
|
||||
defaultPinInput: null,
|
||||
|
||||
get visible() {
|
||||
return this._visible;
|
||||
},
|
||||
|
||||
set visible(value) {
|
||||
if(value) {
|
||||
this._visible = true;
|
||||
this._el.classList.add('slide-in');
|
||||
} else {
|
||||
this._visible = false;
|
||||
this._el.classList.remove('slide-in');
|
||||
}
|
||||
},
|
||||
|
||||
_handleConfigFinished: function() {
|
||||
if(!this.crsInput.validity.valid || !this.uiccInput.validity.valid ||
|
||||
!this.pinP2Input.validity.valid ||
|
||||
!this.defaultPinInput.validity.valid) {
|
||||
this.debug('Inputs not valid, not closing settings');
|
||||
return;
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
exports.ConfigSubView = ConfigSubView;
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
/* globals AID */
|
||||
/* exported DEFAULTS, SIM_REFRESH, SIM_UPDATE */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const DEFAULTS = {
|
||||
PIN: false,
|
||||
FASTPAY: true,
|
||||
CRS_AID: AID.CRS,
|
||||
UICC_AID: AID.UICC,
|
||||
PIN_P2: '0E',
|
||||
PIN_VALUE: '0000'
|
||||
};
|
||||
|
||||
// settings properties which trigger sim data refresh on change
|
||||
const SIM_REFRESH = ['crsAid'];
|
||||
// settings properties which trigger SIMAccessManager update
|
||||
const SIM_UPDATE = ['crsAid', 'uiccAid', 'pinP2'];
|
||||
|
||||
const PIN_DEFAULTS = {
|
||||
MAX_PIN_ATTEMPTS: 3,
|
||||
MAX_PUK_ATTEMPTS: 10,
|
||||
PIN_LEN: 4,
|
||||
MAX_SIM_PIN_LEN: 8,
|
||||
PIN_PADDING: 255
|
||||
};
|
||||
|
||||
exports.DEFAULTS = DEFAULTS;
|
||||
exports.SIM_REFRESH = SIM_REFRESH;
|
||||
exports.SIM_UPDATE = SIM_UPDATE;
|
||||
exports.PIN_DEFAULTS = PIN_DEFAULTS;
|
||||
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
'use strict';
|
||||
|
||||
/* globals addMixin, DebugMixin, ObserverSubjectMixin, SettingsView, DEFAULTS */
|
||||
/* exported SettingsController */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var SettingsController = function(id, viewId, pinCtrl) {
|
||||
this._id = id;
|
||||
this._pinCtrl = pinCtrl;
|
||||
|
||||
addMixin(this, DebugMixin);
|
||||
addMixin(this, ObserverSubjectMixin);
|
||||
|
||||
this._init(viewId);
|
||||
};
|
||||
|
||||
SettingsController.prototype = {
|
||||
_id: null,
|
||||
_view: null,
|
||||
|
||||
_settingsData: {
|
||||
'pin': DEFAULTS.PIN,
|
||||
'fastPay': DEFAULTS.FASTPAY,
|
||||
'crsAid': DEFAULTS.CRS_AID,
|
||||
'uiccAid': DEFAULTS.UICC_AID,
|
||||
'pinP2': DEFAULTS.PIN_P2,
|
||||
'defaultPin': DEFAULTS.PIN_VALUE
|
||||
},
|
||||
|
||||
get pinEnabled() {
|
||||
return this._settingsData.pin;
|
||||
},
|
||||
|
||||
get fastpayEnabled() {
|
||||
return this._settingsData.fastPay;
|
||||
},
|
||||
|
||||
get crsAid() {
|
||||
return this._settingsData.crsAid;
|
||||
},
|
||||
|
||||
get uiccAid() {
|
||||
return this._settingsData.uiccAid;
|
||||
},
|
||||
|
||||
get pinP2() {
|
||||
return this._settingsData.pinP2;
|
||||
},
|
||||
|
||||
get defaultPin() {
|
||||
return this._settingsData.defaultPin;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this._view.visible = true;
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this._view.visible = false;
|
||||
},
|
||||
|
||||
_init: function(viewId) {
|
||||
Object.keys(this._settingsData).forEach(s => {
|
||||
this._settingsData[s] = this._readSetting(s, this._settingsData[s]);
|
||||
});
|
||||
|
||||
this._view = new SettingsView(viewId, Object.create(this._settingsData));
|
||||
this._view.addListener(this);
|
||||
this._view.setSubViewContent('settings-pin', this._pinCtrl.view);
|
||||
},
|
||||
|
||||
_readSetting: function(settingName, defValue) {
|
||||
var settingVal = localStorage.getItem(settingName);
|
||||
if(settingVal === 'true' || settingVal === 'false') {
|
||||
settingVal = (settingVal === 'true');
|
||||
}
|
||||
|
||||
if(settingVal === null && defValue !== undefined) {
|
||||
localStorage.setItem(settingName, defValue);
|
||||
settingVal = defValue;
|
||||
}
|
||||
|
||||
return settingVal;
|
||||
},
|
||||
|
||||
onEvent: function(id, event) {
|
||||
this.debug('Event from: ' + id + ', ' + JSON.stringify(event));
|
||||
switch(event.action) {
|
||||
case 'editing-finished': this._handleInput(event.data); break;
|
||||
case 'pin-toggle': this._handlePinToggle(event.data.checked); break;
|
||||
case 'pin-change': this._handlePinChange(); break;
|
||||
case 'pin-cancel': this._handlePinCancel(); break;
|
||||
}
|
||||
},
|
||||
|
||||
addPinComponent: function(pinCtrl) {
|
||||
this._pinCtrl = pinCtrl;
|
||||
this._view.setSubViewContent('settings-pin', this._pinCtrl.view);
|
||||
},
|
||||
|
||||
_handlePinToggle: function(enable) {
|
||||
this._view.showSubview('settings-pin');
|
||||
if(enable) {
|
||||
this._handlePinEnable();
|
||||
} else {
|
||||
this._handlePinDisable();
|
||||
}
|
||||
},
|
||||
|
||||
_handlePinEnable: function() {
|
||||
this._pinCtrl.enablePin()
|
||||
.then((enabled) => {
|
||||
this._view.pinEnabled = enabled;
|
||||
if(enabled) {
|
||||
this._view.hideSubview('settings-pin');
|
||||
this._pin = true;
|
||||
localStorage.setItem('pin', this._pin);
|
||||
this._view.showStatus('Wallet PIN enabled successfully');
|
||||
} else {
|
||||
this.debug('PIN enable failure');
|
||||
this._view.showStatus('Failed to enable Wallet PIN');
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
if(e !== 'pin-cancelled') {
|
||||
this._view.hideSubview('settings-pin');
|
||||
this._notify({ error: e, msg: 'PIN enable error' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_handlePinDisable: function() {
|
||||
this._pinCtrl.disablePin()
|
||||
.then((disabled) => {
|
||||
this._view.hideSubview('settings-pin');
|
||||
this._view.pinEnabled = !disabled;
|
||||
if(disabled) {
|
||||
this._pin = false;
|
||||
localStorage.setItem('pin', this._pin);
|
||||
this._view.showStatus('Wallet PIN disabled successfully');
|
||||
} else {
|
||||
this.debug('PIN disable failure');
|
||||
this._view.showStatus('Failed to disable Wallet PIN');
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
if(e !== 'pin-cancelled') {
|
||||
this._view.hideSubview('settings-pin');
|
||||
this._notify({ error: e, msg: 'PIN disable error' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_handlePinChange: function() {
|
||||
this._view.showSubview('settings-pin');
|
||||
this._pinCtrl.changePin()
|
||||
.then((success) => {
|
||||
this._view.hideSubview('settings-pin');
|
||||
if(success) {
|
||||
this._view.showStatus('Wallet PIN changed successfully');
|
||||
} else {
|
||||
this.debug('Change failed');
|
||||
this._view.showStatus('Failed to change Wallet PIN');
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
if(e !== 'pin-cancelled') {
|
||||
this._view.hideSubview('settings-pin');
|
||||
this._notify({ error: e, msg: 'PIN chnage error' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_handlePinCancel() {
|
||||
this._pinCtrl.cancelPinAction();
|
||||
},
|
||||
|
||||
_handleInput: function(data) {
|
||||
var changes = {};
|
||||
Object.keys(data).forEach(s => {
|
||||
if(this._settingsData[s] !== data[s]) {
|
||||
this._settingsData[s] = data[s];
|
||||
changes[s] = data[s];
|
||||
localStorage.setItem(s, data[s]);
|
||||
}
|
||||
});
|
||||
|
||||
if(Object.keys(changes).length) {
|
||||
this._notify({ action: 'settings-updated', changes: changes });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
exports.SettingsController = SettingsController;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
'use strict';
|
||||
|
||||
/* exported SettingsSubView */
|
||||
|
||||
(function(exports) {
|
||||
var SettingsSubView = function(id, onCloseCb) {
|
||||
this._id = id;
|
||||
this._onCloseCallback = onCloseCb;
|
||||
this._el = document.querySelector('#'+id);
|
||||
this._el.querySelector('header button.icon-back')
|
||||
.addEventListener('click', () => {
|
||||
this.visible = false;
|
||||
if(this._onCloseCallback) {
|
||||
this._onCloseCallback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SettingsSubView.prototype = {
|
||||
_el: null,
|
||||
_id: null,
|
||||
_visible: false,
|
||||
|
||||
_onCloseCallback: null,
|
||||
|
||||
get visible() {
|
||||
return this._visible;
|
||||
},
|
||||
|
||||
set visible(value) {
|
||||
if(value) {
|
||||
this._visible = true;
|
||||
this._el.classList.add('slide-in');
|
||||
} else {
|
||||
this._visible = false;
|
||||
this._el.classList.remove('slide-in');
|
||||
}
|
||||
},
|
||||
|
||||
set content(el) {
|
||||
var contentEl = this._el.querySelector('.subview-content');
|
||||
this._el.replaceChild(el, contentEl);
|
||||
el.classList.add('subview-content');
|
||||
},
|
||||
};
|
||||
|
||||
exports.SettingsSubView = SettingsSubView;
|
||||
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
'use strict';
|
||||
|
||||
/* globals addMixin, ObserverSubjectMixin, DebugMixin, SettingsSubView,
|
||||
ConfigSubView */
|
||||
/* exported SettingsView */
|
||||
|
||||
(function(exports) {
|
||||
const STATUS_TIMEOUT = 4000; // ms
|
||||
|
||||
var SettingsView = function(id, options) {
|
||||
this._id = id;
|
||||
this._el = document.querySelector('#' + id);
|
||||
|
||||
this._initMainView(options.pin, options.fastPay);
|
||||
this._initSubViews(options);
|
||||
|
||||
addMixin(this, ObserverSubjectMixin);
|
||||
addMixin(this, DebugMixin);
|
||||
};
|
||||
|
||||
SettingsView.prototype = {
|
||||
_el: null,
|
||||
_id: null,
|
||||
_visible: false,
|
||||
_subviews: [],
|
||||
|
||||
_pinCheckbox: null,
|
||||
_fastPayCheckbox: null,
|
||||
|
||||
_pinChangeBtn: null,
|
||||
_pinEnabled: false,
|
||||
|
||||
get visible() {
|
||||
return this._visible;
|
||||
},
|
||||
|
||||
set visible(value) {
|
||||
if(value) {
|
||||
this._visible = true;
|
||||
this._el.classList.remove('hide');
|
||||
this._el.classList.add('edit');
|
||||
} else {
|
||||
this._subviews.forEach((v) => { v.visible = false; });
|
||||
this._visible = false;
|
||||
this._el.classList.remove('edit');
|
||||
}
|
||||
},
|
||||
|
||||
set pinEnabled(value) {
|
||||
this._pinCheckbox.checked = value;
|
||||
if(value) {
|
||||
this._pinChangeBtn.classList.remove('settings-disabled');
|
||||
} else {
|
||||
this._pinChangeBtn.classList.add('settings-disabled');
|
||||
}
|
||||
},
|
||||
|
||||
_initMainView: function(pin, fastPay) {
|
||||
this._pinCheckbox = this._el.querySelector('#pin-checkbox');
|
||||
this._pinCheckbox.addEventListener('change', () => {
|
||||
this._notify({
|
||||
action: 'pin-toggle',
|
||||
data: { checked: this._pinCheckbox.checked }
|
||||
});
|
||||
this.pinEnabled = !this._pinCheckbox.checked;
|
||||
});
|
||||
|
||||
this._pinChangeBtn = this._el.querySelector('#pin-change-btn');
|
||||
this._pinChangeBtn.addEventListener('click', () => {
|
||||
this._notify({ action: 'pin-change' });
|
||||
});
|
||||
|
||||
this.pinEnabled = pin;
|
||||
|
||||
this._fastPayCheckbox = this._el.querySelector('#fast-pay');
|
||||
this._fastPayCheckbox.checked = fastPay;
|
||||
|
||||
this._el.querySelector('#settings-done').addEventListener('click',
|
||||
() => this._handleEditingFinished());
|
||||
},
|
||||
|
||||
_initSubViews: function(cfg){
|
||||
var config = new ConfigSubView('settings-configuration', cfg.crsAid,
|
||||
cfg.uiccAid, cfg.pinP2, cfg.defaultPin);
|
||||
this._el.querySelector('#adv-settings-btn')
|
||||
.addEventListener('click', () => { config.visible = true; });
|
||||
|
||||
var about = new SettingsSubView('settings-about');
|
||||
this._el.querySelector('#about-btn')
|
||||
.addEventListener('click', () => { about.visible = true; });
|
||||
|
||||
var pinCloseHandler = () => this._handlePinViewClosed();
|
||||
var pin = new SettingsSubView('settings-pin', pinCloseHandler);
|
||||
|
||||
this._subviews.push(config);
|
||||
this._subviews.push(about);
|
||||
this._subviews.push(pin);
|
||||
},
|
||||
|
||||
_getSubView: function(id) {
|
||||
return this._subviews.find(v => v._id === id);
|
||||
},
|
||||
|
||||
setSubViewContent: function(id, content) {
|
||||
var subView = this._getSubView(id);
|
||||
if(subView) {
|
||||
subView.content = content;
|
||||
}
|
||||
},
|
||||
|
||||
showSubview: function(id, hide) {
|
||||
var subView = this._getSubView(id);
|
||||
if(subView) {
|
||||
subView.visible = !hide;
|
||||
}
|
||||
},
|
||||
|
||||
hideSubview: function(id) {
|
||||
this.showSubview(id, true);
|
||||
},
|
||||
|
||||
showStatus: function(statusMsg) {
|
||||
this._el.querySelector('#settings-status p').textContent = statusMsg;
|
||||
var status = this._el.querySelector('#settings-status');
|
||||
status.classList.add('visible');
|
||||
setTimeout(() => status.classList.remove('visible'), STATUS_TIMEOUT);
|
||||
},
|
||||
|
||||
_handleEditingFinished: function _handleEditingFinished() {
|
||||
var conf = this._getSubView('settings-configuration');
|
||||
|
||||
this._notify({
|
||||
action: 'editing-finished',
|
||||
data: {
|
||||
pin: this._pinCheckbox.checked,
|
||||
fastPay: this._fastPayCheckbox.checked,
|
||||
crsAid: conf.crsInput.value,
|
||||
uiccAid: conf.uiccInput.value,
|
||||
pinP2: conf.pinP2Input.value,
|
||||
defaultPin: conf.defaultPinInput.value
|
||||
}
|
||||
});
|
||||
this.visible = false;
|
||||
},
|
||||
|
||||
_handlePinViewClosed: function _handlePinViewClosed() {
|
||||
this._notify({ action: 'pin-cancel' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.SettingsView = SettingsView;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
1
assets/webapps/mobile-wallet.gaiamobile.org/manifest.webapp
Executable file
|
|
@ -0,0 +1 @@
|
|||
{"version":"2.5","name":"Mobile Wallet","description":"New wallet version for FFOS 2.2","launch_path":"/index.html","icons":{"84":"/img/icons/default_84.png","126":"/img/icons/default_126.png","142":"/img/icons/default_142.png","189":"/img/icons/default_189.png","284":"/img/icons/default_284.png"},"type":"certified","permissions":{"secureelement-manage":{},"nfc-hci-events":{}},"secure_element_access":["*/A0*"],"messages":[{"nfc-hci-event-transaction":"/index.html"}],"orientation":"portrait","default_locale":"en","origin":"app://mobile-wallet.gaiamobile.org"}
|
||||
|
After Width: | Height: | Size: 755 B |
|
After Width: | Height: | Size: 475 B |
|
After Width: | Height: | Size: 748 B |
|
|
@ -0,0 +1,151 @@
|
|||
'use strict';
|
||||
/* global ComponentUtils */
|
||||
|
||||
window.GaiaSwitch = (function(win) {
|
||||
// Extend from the HTMLElement prototype
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
// Allow baseurl to be overridden (used for demo page)
|
||||
var baseurl = window.GaiaSwitchBaseurl ||
|
||||
'/shared/elements/gaia_switch/';
|
||||
|
||||
proto.createdCallback = function() {
|
||||
var shadow = this.createShadowRoot();
|
||||
this._template = template.content.cloneNode(true);
|
||||
this._input = this._template.querySelector('input[type="checkbox"]');
|
||||
|
||||
var checked = this.getAttribute('checked');
|
||||
if (checked !== null) {
|
||||
this._input.checked = true;
|
||||
}
|
||||
|
||||
var wrapper = this._template.getElementById('switch');
|
||||
wrapper.addEventListener('click', this.handleClick.bind(this));
|
||||
|
||||
shadow.appendChild(this._template);
|
||||
|
||||
ComponentUtils.style.call(this, baseurl);
|
||||
|
||||
// Proxy RTL changes to the shadow root so we can style for RTL.
|
||||
var dirObserver = new MutationObserver(this.updateInternalDir.bind(this));
|
||||
dirObserver.observe(document.documentElement, {
|
||||
attributeFilter: ['dir'],
|
||||
attributes: true
|
||||
});
|
||||
this.updateInternalDir();
|
||||
};
|
||||
|
||||
proto.updateInternalDir = function() {
|
||||
var internal = this.shadowRoot.firstElementChild;
|
||||
if (document.documentElement.dir === 'rtl') {
|
||||
internal.setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
internal.removeAttribute('dir');
|
||||
}
|
||||
};
|
||||
|
||||
proto.handleClick = function(e) {
|
||||
// If user clicked the (optional) link, do not toggle gaia-switch.
|
||||
if (e && e.target.tagName === 'A') {
|
||||
return;
|
||||
}
|
||||
|
||||
e && e.preventDefault();
|
||||
e && e.stopImmediatePropagation();
|
||||
|
||||
// Dispatch a click event.
|
||||
var event = new MouseEvent('click', {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
|
||||
if (!event.defaultPrevented) {
|
||||
this.checked = !this.checked;
|
||||
}
|
||||
|
||||
// Dispatch a change event for the gaia-switch.
|
||||
this.dispatchEvent(new CustomEvent('change', {
|
||||
bubbles: true,
|
||||
cancelable: false
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Allows users to simulate clicking through javascript.
|
||||
*/
|
||||
proto.click = function() {
|
||||
this.handleClick();
|
||||
};
|
||||
|
||||
/**
|
||||
* Proxy the checked property to the input element.
|
||||
*/
|
||||
Object.defineProperty( proto, 'checked', {
|
||||
get: function() {
|
||||
return this._input.checked;
|
||||
},
|
||||
set: function(value) {
|
||||
this._input.checked = value;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Disabled setter
|
||||
*/
|
||||
Object.defineProperty( proto, 'disabled', {
|
||||
get: function() {
|
||||
return this.hasAttribute('disabled');
|
||||
},
|
||||
set: function(value) {
|
||||
if (value) {
|
||||
this.setAttribute('disabled', true);
|
||||
} else {
|
||||
this.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Proxy the name property to the input element.
|
||||
*/
|
||||
Object.defineProperty( proto, 'name', {
|
||||
get: function() {
|
||||
return this.getAttribute('name');
|
||||
},
|
||||
set: function(value) {
|
||||
this.setAttribute('name', value);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Proxy the input type.
|
||||
*/
|
||||
Object.defineProperty( proto, 'type', {
|
||||
get: function() {
|
||||
return 'gaia-switch';
|
||||
}
|
||||
});
|
||||
|
||||
// HACK: Create a <template> in memory at runtime.
|
||||
// When the custom-element is created we clone
|
||||
// this template and inject into the shadow-root.
|
||||
// Prior to this we would have had to copy/paste
|
||||
// the template into the <head> of every app that
|
||||
// wanted to use <gaia-switch>, this would make
|
||||
// markup changes complicated, and could lead to
|
||||
// things getting out of sync. This is a short-term
|
||||
// hack until we can import entire custom-elements
|
||||
// using HTML Imports (bug 877072).
|
||||
var template = document.createElement('template');
|
||||
template.innerHTML = `<span id="switch">
|
||||
<input type="checkbox">
|
||||
<span><content select="label"></content></span>
|
||||
<div class="details"><content select="details"></content></div>
|
||||
<content select="a"></content>
|
||||
</span>`;
|
||||
|
||||
// Register and return the constructor
|
||||
return document.registerElement('gaia-switch', { prototype: proto });
|
||||
})(window);
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
gaia-switch {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#switch {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: none;
|
||||
}
|
||||
|
||||
#switch span {
|
||||
font-size: 1.9rem;
|
||||
color: var(--label-color);
|
||||
padding: 1rem 0;
|
||||
line-height: 3rem;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
/* Override inheriting nowrap from legacy lists building blocks. */
|
||||
white-space: normal;
|
||||
}
|
||||
#switch span:-moz-dir(ltr) { float: left; }
|
||||
#switch span:-moz-dir(rtl) { float: right; }
|
||||
|
||||
#switch span:not(:empty) {
|
||||
padding-inline-end: 6rem;
|
||||
}
|
||||
|
||||
#switch input {
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
offset-inline-start: 0;
|
||||
}
|
||||
|
||||
#switch input:checked ~ span::after {
|
||||
background-position: center bottom;
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
* ON/OFF SWITCHES
|
||||
* ---------------------------------- */
|
||||
|
||||
#switch input ~ span::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
offset-inline-end: 0;
|
||||
top: 2.5rem;
|
||||
width: 6rem;
|
||||
margin: -1.4rem 0 0;
|
||||
height: 2.7rem;
|
||||
border-radius: 1.35rem;
|
||||
overflow: hidden;
|
||||
background-color: #e6e6e6;
|
||||
background-image: url(images/background_off.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 9.2rem 2.7rem;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
#switch input ~ span:-moz-dir(ltr)::after { background-position: -3.2rem 0; }
|
||||
#switch input ~ span:-moz-dir(rtl)::after { background-position: 0 0; }
|
||||
|
||||
/* switch: 'ON' state */
|
||||
#switch input:checked ~ span:-moz-dir(ltr)::after {
|
||||
background-image: url(images/background.png);
|
||||
background-position: 0 0;
|
||||
}
|
||||
#switch input:checked ~ span:-moz-dir(rtl)::after {
|
||||
background-image: url(images/background_rtl.png);
|
||||
background-position: -3.2rem 0;
|
||||
}
|
||||
|
||||
/* switch: disabled state */
|
||||
#switch input:disabled ~ span::after {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
#switch input.uninit ~ span::after {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* A <details> slot may be specified in addition the the <label>.
|
||||
* When provided, it appears under the <label> and can provide additional context.
|
||||
*/
|
||||
#switch div.details {
|
||||
clear: both;
|
||||
position: relative;
|
||||
top: -1.5rem;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding-top: 0.5rem;
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: 8rem;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.6rem;
|
||||
color: #505859;
|
||||
font-style: normal;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* A <a> link slow may be specified in addition to the <label> and <details>.
|
||||
* When provided, it will appear under the <details> section.
|
||||
*/
|
||||
gaia-switch a {
|
||||
position: relative;
|
||||
top: -0.8rem;
|
||||
font-size: 1.7rem;
|
||||
line-height: 2.4rem;
|
||||
font-weight: 400;
|
||||
color: #35679a;
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* ComponentUtils is a utility which allows us to use web components earlier
|
||||
* than we should be able to by polyfilling and fixing platform deficiencies.
|
||||
*/
|
||||
exports.ComponentUtils = {
|
||||
|
||||
/**
|
||||
* Injects a style.css into both the shadow root and outside the shadow
|
||||
* root so we can style projected content. Bug 992249.
|
||||
*/
|
||||
style: function(baseUrl) { if (window.VulpesCompat) { window.VulpesCompat.componentStyle(this, baseUrl).catch(console.error); return; }
|
||||
var style = document.createElement('style');
|
||||
var url = baseUrl + 'style.css';
|
||||
var self = this;
|
||||
|
||||
style.setAttribute('scoped', '');
|
||||
style.innerHTML = '@import url(' + url + ');';
|
||||
this.appendChild(style);
|
||||
|
||||
this.style.visibility = 'hidden';
|
||||
|
||||
// Wait for the stylesheet to load before injecting
|
||||
// it into the shadow-dom. This is to work around
|
||||
// bug 1003294, let's review once landed.
|
||||
style.addEventListener('load', function() {
|
||||
|
||||
// Put a clone of the stylesheet into the shadow-dom.
|
||||
// We have to use two <style> nodes, to work around
|
||||
// the lack of `:host` (bug 992245) and `:content`
|
||||
// (bug 992249) selectors. Once we have those we
|
||||
// can do all our styling from a single style-sheet
|
||||
// within the shadow-dom.
|
||||
if (self.shadowRoot) {
|
||||
self.shadowRoot.appendChild(style.cloneNode(true));
|
||||
}
|
||||
|
||||
self.style.visibility = '';
|
||||
});
|
||||
}
|
||||
};
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,684 @@
|
|||
/* exported GestureDetector */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GestureDetector.js: generate events for one and two finger gestures.
|
||||
*
|
||||
* A GestureDetector object listens for touch events on a specified
|
||||
* element and generates higher-level events that describe one and two finger
|
||||
* gestures on the element.
|
||||
*
|
||||
* Supported events:
|
||||
*
|
||||
* tap like a click event
|
||||
* dbltap like dblclick
|
||||
* pan one finger motion
|
||||
* swipe when a finger is released following pan events
|
||||
* holdstart touch and hold. Must set an option to get these.
|
||||
* holdmove motion after a holdstart event
|
||||
* holdend when the finger goes up after holdstart/holdmove
|
||||
* transform 2-finger pinch and twist gestures for scaling and rotation
|
||||
*
|
||||
* Each of these events is a bubbling CustomEvent with important details in the
|
||||
* event.detail field. The event details are not yet stable and are not yet
|
||||
* documented. See the calls to emitEvent() for details.
|
||||
*
|
||||
* To use this library, create a GestureDetector object by passing an element to
|
||||
* the GestureDetector() constructor and then calling startDetecting() on it.
|
||||
* The element will be the target of all the emitted gesture events. You can
|
||||
* also pass an optional object as the second constructor argument. If you're
|
||||
* interested in holdstart/holdmove/holdend events, pass {holdEvents:true} as
|
||||
* this second argument. Otherwise they will not be generated.
|
||||
* If you want to customize the pan threshold, pass {panThreshold:X}
|
||||
* (X and Y in pixels) in the options argument.
|
||||
*
|
||||
* Implementation note: event processing is done with a simple finite-state
|
||||
* machine. This means that in general, the various kinds of gestures are
|
||||
* mutually exclusive. You won't get pan events until your finger has
|
||||
* moved more than a minimum threshold, for example, but it does, the FSM enters
|
||||
* a new state in which it can emit pan and swipe events and cannot emit hold
|
||||
* events. Similarly, if you've started a 1 finger pan/swipe gesture and
|
||||
* accidentally touch with a second finger, you'll continue to get pan events,
|
||||
* and won't suddenly start getting 2-finger transform events.
|
||||
*
|
||||
* This library never calls preventDefault() or stopPropagation on any of the
|
||||
* events it processes, so the raw touch events should still be
|
||||
* available for other code to process. It is not clear to me whether this is a
|
||||
* feature or a bug.
|
||||
*/
|
||||
|
||||
var GestureDetector = (function() {
|
||||
|
||||
//
|
||||
// Constructor
|
||||
//
|
||||
function GD(e, options) {
|
||||
this.element = e;
|
||||
this.options = options || {};
|
||||
this.options.panThreshold = this.options.panThreshold || GD.PAN_THRESHOLD;
|
||||
this.state = initialState;
|
||||
this.timers = {};
|
||||
}
|
||||
|
||||
//
|
||||
// Public methods
|
||||
//
|
||||
|
||||
GD.prototype.startDetecting = function() {
|
||||
var self = this;
|
||||
eventtypes.forEach(function(t) {
|
||||
self.element.addEventListener(t, self);
|
||||
});
|
||||
};
|
||||
|
||||
GD.prototype.stopDetecting = function() {
|
||||
var self = this;
|
||||
eventtypes.forEach(function(t) {
|
||||
self.element.removeEventListener(t, self);
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Internal methods
|
||||
//
|
||||
|
||||
GD.prototype.handleEvent = function(e) {
|
||||
var handler = this.state[e.type];
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
// If this is a touch event handle each changed touch separately
|
||||
if (e.changedTouches) {
|
||||
for (var i = 0; i < e.changedTouches.length; i++) {
|
||||
handler(this, e, e.changedTouches[i]);
|
||||
// The first changed touch might have changed the state of the FSM.
|
||||
handler = this.state[e.type];
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else { // Otherwise, just dispatch the event to the handler
|
||||
handler(this, e);
|
||||
}
|
||||
};
|
||||
|
||||
GD.prototype.startTimer = function(type, time) {
|
||||
this.clearTimer(type);
|
||||
var self = this;
|
||||
this.timers[type] = setTimeout(function() {
|
||||
self.timers[type] = null;
|
||||
var handler = self.state[type];
|
||||
if (handler) {
|
||||
handler(self, type);
|
||||
}
|
||||
}, time);
|
||||
};
|
||||
|
||||
GD.prototype.clearTimer = function(type) {
|
||||
if (this.timers[type]) {
|
||||
clearTimeout(this.timers[type]);
|
||||
this.timers[type] = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Switch to a new FSM state, and call the init() function of that
|
||||
// state, if it has one. The event and touch arguments are optional
|
||||
// and are just passed through to the state init function.
|
||||
GD.prototype.switchTo = function(state, event, touch) {
|
||||
this.state = state;
|
||||
if (state.init) {
|
||||
state.init(this, event, touch);
|
||||
}
|
||||
};
|
||||
|
||||
GD.prototype.emitEvent = function(type, detail) {
|
||||
if (!this.target) {
|
||||
console.error('Attempt to emit event with no target');
|
||||
return;
|
||||
}
|
||||
|
||||
var event = this.element.ownerDocument.createEvent('CustomEvent');
|
||||
event.initCustomEvent(type, true, true, detail);
|
||||
this.target.dispatchEvent(event);
|
||||
};
|
||||
|
||||
//
|
||||
// Tuneable parameters
|
||||
//
|
||||
GD.HOLD_INTERVAL = 1000; // Hold events after 1000 ms
|
||||
GD.PAN_THRESHOLD = 20; // 20 pixels movement before touch panning
|
||||
GD.DOUBLE_TAP_DISTANCE = 50;
|
||||
GD.DOUBLE_TAP_TIME = 500;
|
||||
GD.VELOCITY_SMOOTHING = 0.5;
|
||||
|
||||
// Don't start sending transform events until the gesture exceeds a threshold
|
||||
GD.SCALE_THRESHOLD = 20; // pixels
|
||||
GD.ROTATE_THRESHOLD = 22.5; // degrees
|
||||
|
||||
// For pans and zooms, we compute new starting coordinates that are part way
|
||||
// between the initial event and the event that crossed the threshold so that
|
||||
// the first event we send doesn't cause a big lurch. This constant must be
|
||||
// between 0 and 1 and says how far along the line between the initial value
|
||||
// and the new value we pick
|
||||
GD.THRESHOLD_SMOOTHING = 0.9;
|
||||
|
||||
//
|
||||
// Helpful shortcuts and utility functions
|
||||
//
|
||||
|
||||
var abs = Math.abs, floor = Math.floor, sqrt = Math.sqrt, atan2 = Math.atan2;
|
||||
var PI = Math.PI;
|
||||
|
||||
// The names of events that we need to register handlers for
|
||||
var eventtypes = [
|
||||
'touchstart',
|
||||
'touchmove',
|
||||
'touchend'
|
||||
];
|
||||
|
||||
// Return the event's timestamp in ms
|
||||
function eventTime(e) {
|
||||
// In gecko, synthetic events seem to be in microseconds rather than ms.
|
||||
// So if the timestamp is much larger than the current time, assue it is
|
||||
// in microseconds and divide by 1000
|
||||
var ts = e.timeStamp;
|
||||
if (ts > 2 * Date.now()) {
|
||||
return Math.floor(ts / 1000);
|
||||
} else {
|
||||
return ts;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Return an object containg the space and time coordinates of
|
||||
// and event and touch. We freeze the object to make it immutable so
|
||||
// we can pass it in events and not worry about values being changed.
|
||||
function coordinates(e, t) {
|
||||
return Object.freeze({
|
||||
screenX: t.screenX,
|
||||
screenY: t.screenY,
|
||||
clientX: t.clientX,
|
||||
clientY: t.clientY,
|
||||
timeStamp: eventTime(e)
|
||||
});
|
||||
}
|
||||
|
||||
// Like coordinates(), but return the midpoint between two touches
|
||||
function midpoints(e, t1, t2) {
|
||||
return Object.freeze({
|
||||
screenX: floor((t1.screenX + t2.screenX) / 2),
|
||||
screenY: floor((t1.screenY + t2.screenY) / 2),
|
||||
clientX: floor((t1.clientX + t2.clientX) / 2),
|
||||
clientY: floor((t1.clientY + t2.clientY) / 2),
|
||||
timeStamp: eventTime(e)
|
||||
});
|
||||
}
|
||||
|
||||
// Given coordinates objects c1 and c2, return a new coordinates object
|
||||
// representing a point and time along the line between those points.
|
||||
// The position of the point is controlled by the THRESHOLD_SMOOTHING constant
|
||||
function between(c1, c2) {
|
||||
var r = GD.THRESHOLD_SMOOTHING;
|
||||
return Object.freeze({
|
||||
screenX: floor(c1.screenX + r * (c2.screenX - c1.screenX)),
|
||||
screenY: floor(c1.screenY + r * (c2.screenY - c1.screenY)),
|
||||
clientX: floor(c1.clientX + r * (c2.clientX - c1.clientX)),
|
||||
clientY: floor(c1.clientY + r * (c2.clientY - c1.clientY)),
|
||||
timeStamp: floor(c1.timeStamp + r * (c2.timeStamp - c1.timeStamp))
|
||||
});
|
||||
}
|
||||
|
||||
// Compute the distance between two touches
|
||||
function touchDistance(t1, t2) {
|
||||
var dx = t2.screenX - t1.screenX;
|
||||
var dy = t2.screenY - t1.screenY;
|
||||
return sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
// Compute the direction (as an angle) of the line between two touches
|
||||
// Returns a number d, -180 < d <= 180
|
||||
function touchDirection(t1, t2) {
|
||||
return atan2(t2.screenY - t1.screenY,
|
||||
t2.screenX - t1.screenX) * 180 / PI;
|
||||
}
|
||||
|
||||
// Compute the clockwise angle between direction d1 and direction d2.
|
||||
// Returns an angle a -180 < a <= 180.
|
||||
function touchRotation(d1, d2) {
|
||||
var angle = d2 - d1;
|
||||
if (angle > 180) {
|
||||
angle -= 360;
|
||||
} else if (angle <= -180) {
|
||||
angle += 360;
|
||||
}
|
||||
return angle;
|
||||
}
|
||||
|
||||
// Determine if two taps are close enough in time and space to
|
||||
// trigger a dbltap event. The arguments are objects returned
|
||||
// by the coordinates() function.
|
||||
function isDoubleTap(lastTap, thisTap) {
|
||||
var dx = abs(thisTap.screenX - lastTap.screenX);
|
||||
var dy = abs(thisTap.screenY - lastTap.screenY);
|
||||
var dt = thisTap.timeStamp - lastTap.timeStamp;
|
||||
return (dx < GD.DOUBLE_TAP_DISTANCE &&
|
||||
dy < GD.DOUBLE_TAP_DISTANCE &&
|
||||
dt < GD.DOUBLE_TAP_TIME);
|
||||
}
|
||||
|
||||
//
|
||||
// The following objects are the states of our Finite State Machine
|
||||
//
|
||||
|
||||
// In this state we're not processing any gestures, just waiting
|
||||
// for an event to start a gesture and ignoring others
|
||||
var initialState = {
|
||||
name: 'initialState',
|
||||
init: function(d, e, t) {
|
||||
// When we enter or return to the initial state, clear
|
||||
// the detector properties that were tracking gestures
|
||||
// Don't clear d.lastTap here, though. We need it for dbltap events
|
||||
d.target = null;
|
||||
d.start = d.last = null;
|
||||
d.touch1 = d.touch2 = null;
|
||||
d.vx = d.vy = null;
|
||||
d.startDistance = d.lastDistance = null;
|
||||
d.startDirection = d.lastDirection = null;
|
||||
d.lastMidpoint = null;
|
||||
d.scaled = d.rotated = null;
|
||||
|
||||
// If we were invoked with an event and touch, then process it.
|
||||
if (e && t && e.type === 'touchstart') {
|
||||
initialState.touchstart(d, e, t);
|
||||
}
|
||||
},
|
||||
|
||||
// Switch to the touchstarted state and process the touch event there
|
||||
touchstart: function(d, e, t) {
|
||||
d.switchTo(touchStartedState, e, t);
|
||||
}
|
||||
};
|
||||
|
||||
// One finger is down but we haven't generated any event yet. We're
|
||||
// waiting to see... If the finger goes up soon, its a tap. If the finger
|
||||
// stays down and still, its a hold. If the finger moves its a pan/swipe.
|
||||
// And if a second finger goes down, its a transform
|
||||
var touchStartedState = {
|
||||
name: 'touchStartedState',
|
||||
init: function(d, e, t) {
|
||||
// Remember the target of the event
|
||||
d.target = e.target;
|
||||
// Remember the id of the touch that started
|
||||
d.touch1 = t.identifier;
|
||||
// Get the coordinates of the touch
|
||||
d.start = d.last = coordinates(e, t);
|
||||
// Start a timer for a hold
|
||||
// If we're doing hold events, start a timer for them
|
||||
if (d.options.holdEvents) {
|
||||
d.startTimer('holdtimeout', GD.HOLD_INTERVAL);
|
||||
}
|
||||
},
|
||||
|
||||
touchstart: function(d, e, t) {
|
||||
// If another finger goes down in this state, then
|
||||
// go to transform state to start 2-finger gestures.
|
||||
d.clearTimer('holdtimeout');
|
||||
|
||||
if (e.touches.length > 1) { // verify that we have 2 fingers
|
||||
d.switchTo(transformState, e, t);
|
||||
}
|
||||
else {
|
||||
// If Gecko fails to deliver a touchend event to us (bug 1162771, e.g.)
|
||||
// then we might get two touchstart events in a row and end up here
|
||||
// when there is actually only one finger currently on the screen.
|
||||
// In that case, we switch back to the initial state and process
|
||||
// this touch from that state. (We don't switch directly to
|
||||
// touchStartedState because we need to reset things in the
|
||||
// initialState init() function.)
|
||||
console.warn('Ignoring missing touchend event. See bug 1162771.');
|
||||
d.switchTo(initialState, e, t);
|
||||
}
|
||||
},
|
||||
touchmove: function(d, e, t) {
|
||||
// Ignore any touches but the initial one
|
||||
// This could happen if there was still a finger down after
|
||||
// the end of a previous 2-finger gesture, e.g.
|
||||
if (t.identifier !== d.touch1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (abs(t.screenX - d.start.screenX) > d.options.panThreshold ||
|
||||
abs(t.screenY - d.start.screenY) > d.options.panThreshold) {
|
||||
d.clearTimer('holdtimeout');
|
||||
d.switchTo(panStartedState, e, t);
|
||||
}
|
||||
},
|
||||
touchend: function(d, e, t) {
|
||||
// Ignore any touches but the initial one
|
||||
if (t.identifier !== d.touch1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there was a previous tap that was close enough in time
|
||||
// and space, then emit a 'dbltap' event
|
||||
if (d.lastTap && isDoubleTap(d.lastTap, d.start)) {
|
||||
d.emitEvent('tap', d.start);
|
||||
d.emitEvent('dbltap', d.start);
|
||||
// clear the lastTap property, so we don't get another one
|
||||
d.lastTap = null;
|
||||
}
|
||||
else {
|
||||
// Emit a 'tap' event using the starting coordinates
|
||||
// as the event details
|
||||
d.emitEvent('tap', d.start);
|
||||
|
||||
// Remember the coordinates of this tap so we can detect double taps
|
||||
d.lastTap = coordinates(e, t);
|
||||
}
|
||||
|
||||
// In either case clear the timer and go back to the initial state
|
||||
d.clearTimer('holdtimeout');
|
||||
d.switchTo(initialState);
|
||||
},
|
||||
|
||||
holdtimeout: function(d) {
|
||||
d.switchTo(holdState);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// A single touch has moved enough to exceed the pan threshold and now
|
||||
// we're going to generate pan events after each move and a swipe event
|
||||
// when the touch ends. We ignore any other touches that occur while this
|
||||
// pan/swipe gesture is in progress.
|
||||
var panStartedState = {
|
||||
name: 'panStartedState',
|
||||
init: function(d, e, t) {
|
||||
// Panning doesn't start until the touch has moved more than a
|
||||
// certain threshold. But we don't want the pan to have a jerky
|
||||
// start where the first event is a big distance. So proceed as
|
||||
// pan actually started at a point along the path between the
|
||||
// first touch and this current touch.
|
||||
d.start = d.last = between(d.start, coordinates(e, t));
|
||||
|
||||
// If we transition into this state with a touchmove event,
|
||||
// then process it with that handler. If we don't do this then
|
||||
// we can end up with swipe events that don't know their velocity
|
||||
if (e.type === 'touchmove') {
|
||||
panStartedState.touchmove(d, e, t);
|
||||
}
|
||||
},
|
||||
|
||||
touchmove: function(d, e, t) {
|
||||
// Ignore any fingers other than the one we're tracking
|
||||
if (t.identifier !== d.touch1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Each time the touch moves, emit a pan event but stay in this state
|
||||
var current = coordinates(e, t);
|
||||
d.emitEvent('pan', {
|
||||
absolute: {
|
||||
dx: current.screenX - d.start.screenX,
|
||||
dy: current.screenY - d.start.screenY
|
||||
},
|
||||
relative: {
|
||||
dx: current.screenX - d.last.screenX,
|
||||
dy: current.screenY - d.last.screenY
|
||||
},
|
||||
position: current
|
||||
});
|
||||
|
||||
// Track the pan velocity so we can report this with the swipe
|
||||
// Use a exponential moving average for a bit of smoothing
|
||||
// on the velocity
|
||||
var dt = current.timeStamp - d.last.timeStamp;
|
||||
var vx = (current.screenX - d.last.screenX) / dt;
|
||||
var vy = (current.screenY - d.last.screenY) / dt;
|
||||
|
||||
if (d.vx == null) { // first time; no average
|
||||
d.vx = vx;
|
||||
d.vy = vy;
|
||||
}
|
||||
else {
|
||||
d.vx = d.vx * GD.VELOCITY_SMOOTHING +
|
||||
vx * (1 - GD.VELOCITY_SMOOTHING);
|
||||
d.vy = d.vy * GD.VELOCITY_SMOOTHING +
|
||||
vy * (1 - GD.VELOCITY_SMOOTHING);
|
||||
}
|
||||
|
||||
d.last = current;
|
||||
},
|
||||
touchend: function(d, e, t) {
|
||||
// Ignore any fingers other than the one we're tracking
|
||||
if (t.identifier !== d.touch1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit a swipe event when the finger goes up.
|
||||
// Report start and end point, dx, dy, dt, velocity and direction
|
||||
var current = coordinates(e, t);
|
||||
var dx = current.screenX - d.start.screenX;
|
||||
var dy = current.screenY - d.start.screenY;
|
||||
// angle is a positive number of degrees, starting at 0 on the
|
||||
// positive x axis and increasing clockwise.
|
||||
var angle = atan2(dy, dx) * 180 / PI;
|
||||
if (angle < 0) {
|
||||
angle += 360;
|
||||
}
|
||||
|
||||
// Direction is 'right', 'down', 'left' or 'up'
|
||||
var direction;
|
||||
if (angle >= 315 || angle < 45) {
|
||||
direction = 'right';
|
||||
} else if (angle >= 45 && angle < 135) {
|
||||
direction = 'down';
|
||||
} else if (angle >= 135 && angle < 225) {
|
||||
direction = 'left';
|
||||
} else if (angle >= 225 && angle < 315) {
|
||||
direction = 'up';
|
||||
}
|
||||
|
||||
d.emitEvent('swipe', {
|
||||
start: d.start,
|
||||
end: current,
|
||||
dx: dx,
|
||||
dy: dy,
|
||||
dt: e.timeStamp - d.start.timeStamp,
|
||||
vx: d.vx,
|
||||
vy: d.vy,
|
||||
direction: direction,
|
||||
angle: angle
|
||||
});
|
||||
|
||||
// Go back to the initial state
|
||||
d.switchTo(initialState);
|
||||
}
|
||||
};
|
||||
|
||||
// We enter this state if the user touches and holds for long enough
|
||||
// without moving much. When we enter we emit a holdstart event. Motion
|
||||
// after the holdstart generates holdmove events. And when the touch ends
|
||||
// we generate a holdend event. holdmove and holdend events can be used
|
||||
// kind of like drag and drop events in a mouse-based UI. Currently,
|
||||
// these events just report the coordinates of the touch. Do we need
|
||||
// other details?
|
||||
var holdState = {
|
||||
name: 'holdState',
|
||||
init: function(d) {
|
||||
d.emitEvent('holdstart', d.start);
|
||||
},
|
||||
|
||||
touchmove: function(d, e, t) {
|
||||
var current = coordinates(e, t);
|
||||
d.emitEvent('holdmove', {
|
||||
absolute: {
|
||||
dx: current.screenX - d.start.screenX,
|
||||
dy: current.screenY - d.start.screenY
|
||||
},
|
||||
relative: {
|
||||
dx: current.screenX - d.last.screenX,
|
||||
dy: current.screenY - d.last.screenY
|
||||
},
|
||||
position: current
|
||||
});
|
||||
|
||||
d.last = current;
|
||||
},
|
||||
|
||||
touchend: function(d, e, t) {
|
||||
var current = coordinates(e, t);
|
||||
d.emitEvent('holdend', {
|
||||
start: d.start,
|
||||
end: current,
|
||||
dx: current.screenX - d.start.screenX,
|
||||
dy: current.screenY - d.start.screenY
|
||||
});
|
||||
d.switchTo(initialState);
|
||||
}
|
||||
};
|
||||
|
||||
// We enter this state if a second touch starts before we start
|
||||
// recoginzing any other gesture. As the touches move we track the
|
||||
// distance and angle between them to report scale and rotation values
|
||||
// in transform events.
|
||||
var transformState = {
|
||||
name: 'transformState',
|
||||
init: function(d, e, t) {
|
||||
// Remember the id of the second touch
|
||||
d.touch2 = t.identifier;
|
||||
|
||||
// Get the two Touch objects
|
||||
var t1 = e.touches.identifiedTouch(d.touch1);
|
||||
var t2 = e.touches.identifiedTouch(d.touch2);
|
||||
|
||||
// Compute and remember the initial distance and angle
|
||||
d.startDistance = d.lastDistance = touchDistance(t1, t2);
|
||||
d.startDirection = d.lastDirection = touchDirection(t1, t2);
|
||||
|
||||
// Don't start emitting events until we're past a threshold
|
||||
d.scaled = d.rotated = false;
|
||||
},
|
||||
|
||||
touchmove: function(d, e, t) {
|
||||
// Ignore touches we're not tracking
|
||||
if (t.identifier !== d.touch1 && t.identifier !== d.touch2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the two Touch objects
|
||||
var t1 = e.touches.identifiedTouch(d.touch1);
|
||||
var t2 = e.touches.identifiedTouch(d.touch2);
|
||||
|
||||
// Compute the new midpoints, distance and direction
|
||||
var midpoint = midpoints(e, t1, t2);
|
||||
var distance = touchDistance(t1, t2);
|
||||
var direction = touchDirection(t1, t2);
|
||||
var rotation = touchRotation(d.startDirection, direction);
|
||||
|
||||
// Check all of these numbers against the thresholds. Otherwise
|
||||
// the transforms are too jittery even when you try to hold your
|
||||
// fingers still.
|
||||
if (!d.scaled) {
|
||||
if (abs(distance - d.startDistance) > GD.SCALE_THRESHOLD) {
|
||||
d.scaled = true;
|
||||
d.startDistance = d.lastDistance =
|
||||
floor(d.startDistance +
|
||||
GD.THRESHOLD_SMOOTHING * (distance - d.startDistance));
|
||||
} else {
|
||||
distance = d.startDistance;
|
||||
}
|
||||
}
|
||||
if (!d.rotated) {
|
||||
if (abs(rotation) > GD.ROTATE_THRESHOLD) {
|
||||
d.rotated = true;
|
||||
} else {
|
||||
direction = d.startDirection;
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing has exceeded the threshold yet, then we
|
||||
// don't even have to fire an event.
|
||||
if (d.scaled || d.rotated) {
|
||||
// The detail field for the transform gesture event includes
|
||||
// 'absolute' transformations against the initial values and
|
||||
// 'relative' transformations against the values from the last
|
||||
// transformgesture event.
|
||||
d.emitEvent('transform', {
|
||||
absolute: { // transform details since gesture start
|
||||
scale: distance / d.startDistance,
|
||||
rotate: touchRotation(d.startDirection, direction)
|
||||
},
|
||||
relative: { // transform since last gesture change
|
||||
scale: distance / d.lastDistance,
|
||||
rotate: touchRotation(d.lastDirection, direction)
|
||||
},
|
||||
midpoint: midpoint
|
||||
});
|
||||
|
||||
d.lastDistance = distance;
|
||||
d.lastDirection = direction;
|
||||
d.lastMidpoint = midpoint;
|
||||
}
|
||||
},
|
||||
|
||||
touchend: function(d, e, t) {
|
||||
// If either finger goes up, we're done with the gesture.
|
||||
// The user might move that finger and put it right back down
|
||||
// again to begin another 2-finger gesture, so we can't go
|
||||
// back to the initial state while one of the fingers remains up.
|
||||
// On the other hand, we can't go back to touchStartedState because
|
||||
// that would mean that the finger left down could cause a tap or
|
||||
// pan event. So we need an afterTransform state that waits for
|
||||
// a finger to come back down or the other finger to go up.
|
||||
if (t.identifier === d.touch2) {
|
||||
d.touch2 = null;
|
||||
} else if (t.identifier === d.touch1) {
|
||||
d.touch1 = d.touch2;
|
||||
d.touch2 = null;
|
||||
} else {
|
||||
return; // It was a touch we weren't tracking
|
||||
}
|
||||
|
||||
// If we emitted any transform events, now we need to emit
|
||||
// a transformend event to end the series. The details of this
|
||||
// event use the values from the last touchmove, and the
|
||||
// relative amounts will 1 and 0, but they are included for
|
||||
// completeness even though they are not useful.
|
||||
if (d.scaled || d.rotated) {
|
||||
d.emitEvent('transformend', {
|
||||
absolute: { // transform details since gesture start
|
||||
scale: d.lastDistance / d.startDistance,
|
||||
rotate: touchRotation(d.startDirection, d.lastDirection)
|
||||
},
|
||||
relative: { // nothing has changed relative to the last touchmove
|
||||
scale: 1,
|
||||
rotate: 0
|
||||
},
|
||||
midpoint: d.lastMidpoint
|
||||
});
|
||||
}
|
||||
|
||||
d.switchTo(afterTransformState);
|
||||
}
|
||||
};
|
||||
|
||||
// We did a tranform and one finger went up. Wait for that finger to
|
||||
// come back down or the other finger to go up too.
|
||||
var afterTransformState = {
|
||||
name: 'afterTransformState',
|
||||
touchstart: function(d, e, t) {
|
||||
d.switchTo(transformState, e, t);
|
||||
},
|
||||
|
||||
touchend: function(d, e, t) {
|
||||
if (t.identifier === d.touch1) {
|
||||
d.switchTo(initialState);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return GD;
|
||||
}());
|
||||
|
|
@ -0,0 +1,428 @@
|
|||
/* ----------------------------------
|
||||
* Buttons
|
||||
* ---------------------------------- */
|
||||
|
||||
.button::-moz-focus-inner,
|
||||
.bb-button::-moz-focus-inner,
|
||||
button::-moz-focus-inner {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button,
|
||||
.bb-button,
|
||||
.button {
|
||||
font-family: sans-serif;
|
||||
font-style: italic;
|
||||
width: 100%;
|
||||
height: 4rem;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0 1.2rem;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
background: #d8d8d8;
|
||||
border: none;
|
||||
border-radius: 2rem;
|
||||
font-weight: normal;
|
||||
font-size: 1.6rem;
|
||||
line-height: 4rem;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Recommend */
|
||||
button.recommend,
|
||||
.bb-button.recommend {
|
||||
background-color: #00caf2;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Danger */
|
||||
button.danger,
|
||||
.danger.bb-button {
|
||||
background-color: #e51e1e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Pressed */
|
||||
button:active,
|
||||
.bb-button:active,
|
||||
.button:active {
|
||||
background-color: #b2f2ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
button[data-icon],
|
||||
.bb-button[data-icon],
|
||||
.button[data-icon] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
button[data-icon]:before,
|
||||
.bb-button[data-icon]:before,
|
||||
.button[data-icon]:before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -1.5rem;
|
||||
offset-inline-end: 1.5rem;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
button[data-icon]:active:before,
|
||||
.bb-button[data-icon]:active:before,
|
||||
.button[data-icon]:active:before {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Pressed with dark skin */
|
||||
.skin-dark button:active,
|
||||
.skin-dark .bb-button:active,
|
||||
.skin-dark .button:active {
|
||||
background-color: #00aacc;
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
button[disabled],
|
||||
.bb-button[aria-disabled="true"],
|
||||
.button[aria-disabled="true"] {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
color: #c7c7c7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
button[disabled].recommend,
|
||||
.bb-button[aria-disabled="true"].recommend,
|
||||
.button[aria-disabled="true"].recommend {
|
||||
background-color: #97cbd5;
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
button[disabled].danger,
|
||||
.bb-button[aria-disabled="true"].danger,
|
||||
.button[aria-disabled="true"].danger {
|
||||
background-color: #eb7474;
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
/* Disabled with dark background */
|
||||
.skin-dark button[disabled],
|
||||
.skin-dark .button[aria-disabled="true"],
|
||||
.skin-dark .bb-button[aria-disabled="true"] {
|
||||
background-color: #565656;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
|
||||
.skin-dark button[disabled].recommend,
|
||||
.skin-dark .button[aria-disabled="true"].recommend,
|
||||
.skin-dark .bb-button[aria-disabled="true"].recommend {
|
||||
background-color: #006579;
|
||||
}
|
||||
|
||||
.skin-dark button[disabled].danger,
|
||||
.skin-dark .button[aria-disabled="true"].danger,
|
||||
.skin-dark .bb-button[aria-disabled="true"].danger {
|
||||
background-color: #730f0f;
|
||||
}
|
||||
|
||||
button[disabled]::-moz-selection,
|
||||
.button[aria-disabled="true"]::-moz-selection,
|
||||
.bb-button[aria-disabled="true"]::-moz-selection {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
* Buttons inside lists
|
||||
* ---------------------------------- */
|
||||
|
||||
li button,
|
||||
li .bb-button,
|
||||
li .button {
|
||||
position: relative;
|
||||
border: 0.1rem solid #c7c7c7;
|
||||
background: rgba(0,0,0,0.05);
|
||||
margin: 0 0 1rem;
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
height: auto;
|
||||
min-height: 4rem;
|
||||
text-overflow: initial;
|
||||
white-space: normal;
|
||||
line-height: 2rem;
|
||||
padding: 1rem 1.2rem;
|
||||
}
|
||||
html[dir="ltr"] li button,
|
||||
html[dir="ltr"] li .bb-button,
|
||||
html[dir="ltr"] li .button {
|
||||
text-align: left;
|
||||
}
|
||||
html[dir="rtl"] li button,
|
||||
html[dir="rtl"] li .bb-button,
|
||||
html[dir="rtl"] li .button {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Press */
|
||||
li .bb-button:active:after,
|
||||
li .button:active:after,
|
||||
li button:active:after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
li button:disabled,
|
||||
li .bb-button[aria-disabled="true"],
|
||||
li .button[aria-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
li button.icon,
|
||||
li .bb-button.icon,
|
||||
li .button.icon {
|
||||
padding-inline-end: 4rem;
|
||||
}
|
||||
|
||||
li button.icon:before,
|
||||
li .bb-button.icon:before,
|
||||
li .button.icon:before {
|
||||
content: "";
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
offset-inline-end: 0.5rem;
|
||||
margin-top: -1.5rem;
|
||||
background: transparent no-repeat center center / 100% auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
li button[data-icon]:before,
|
||||
li .bb-button[data-icon]:before,
|
||||
li .button[data-icon]:before {
|
||||
offset-inline-end: 1rem;
|
||||
}
|
||||
|
||||
li button.icon-view:active,
|
||||
li .bb-button.icon-view:active,
|
||||
li .button.icon-view:active {
|
||||
background-color: #b2f2ff;
|
||||
}
|
||||
|
||||
li button[data-icon="expand"]:before,
|
||||
li .bb-button[data-icon="expand"]:before,
|
||||
li .button[data-icon="expand"]:before {
|
||||
color: #00aac5;
|
||||
top: unset;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
li button[data-icon="expand"][disabled]:before,
|
||||
li .bb-button[data-icon="expand"][aria-disabled="true"]:before,
|
||||
li .button[data-icon="expand"][aria-disabled="true"]:before {
|
||||
color: #A9A9A9;
|
||||
}
|
||||
|
||||
li button[data-icon="forward"]:before,
|
||||
li .bb-button[data-icon="forward"]:before,
|
||||
li .button[data-icon="forward"]:before {
|
||||
color: #686f72;
|
||||
}
|
||||
|
||||
/* Getting more space for text, as icon is shorter */
|
||||
li button.icon-dialog,
|
||||
li .bb-button.icon-dialog,
|
||||
li .button.icon-dialog {
|
||||
padding-inline-end: 3rem;
|
||||
}
|
||||
|
||||
li button.icon-dialog:before,
|
||||
li .bb-button.icon-dialog:before,
|
||||
li .button.icon-dialog:before {
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: auto;
|
||||
offset-inline-end: 0.8rem;
|
||||
bottom: 0.5rem;
|
||||
border-top: 0.7rem solid transparent;
|
||||
border-bottom: 0.7rem solid transparent;
|
||||
border-inline-start: 0.7rem solid #00aac5;
|
||||
z-index: 1;
|
||||
}
|
||||
html[dir="ltr"] li button.icon-dialog:before,
|
||||
html[dir="ltr"] li .bb-button.icon-dialog:before,
|
||||
html[dir="ltr"] li .button.icon-dialog:before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
html[dir="rtl"] li button.icon-dialog:before,
|
||||
html[dir="rtl"] li .bb-button.icon-dialog:before,
|
||||
html[dir="rtl"] li .button.icon-dialog:before {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
li button.icon-dialog:disabled:before,
|
||||
li .bb-button[aria-disabled="true"].icon-dialog:before,
|
||||
li .button[aria-disabled="true"].icon-dialog:before {
|
||||
border-inline-start: 0.7rem solid #a9a9a9;
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
* Theme: Dark
|
||||
* ---------------------------------- */
|
||||
|
||||
.skin-dark li button,
|
||||
.skin-dark li .bb-button,
|
||||
.skin-dark li .button {
|
||||
background-color: #454545;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.skin-dark li button:active,
|
||||
.skin-dark li .button:active,
|
||||
.skin-dark li .bb-button:active {
|
||||
background-color: #00aacc;
|
||||
}
|
||||
|
||||
.skin-dark li button.icon-view:active,
|
||||
.skin-dark li .button.icon-view:active,
|
||||
.skin-dark li .bb-button.icon-view:active {
|
||||
background-color: #00aacc;
|
||||
}
|
||||
|
||||
.skin-dark li button.icon-dialog:active:before,
|
||||
.skin-dark li .bb-button.icon-dialog:active:before,
|
||||
.skin-dark li .button.icon-dialog:active:before {
|
||||
border-inline-start: 0.7rem solid #b2f2ff;
|
||||
}
|
||||
|
||||
.skin-dark li button:disabled,
|
||||
.skin-dark li .bb-button[aria-disabled="true"],
|
||||
.skin-dark li .button[aria-disabled="true"] {
|
||||
background-color: #454545;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Inputs inside of .button */
|
||||
.button input,
|
||||
body[role="application"] .bb-button input,
|
||||
body[role="application"] .button input {
|
||||
border: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* select */
|
||||
select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
font-style: italic;
|
||||
font-size: 1.6rem;
|
||||
color: #333;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0;
|
||||
padding-inline-start: 1.1rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
height: 4rem;
|
||||
border: 0.1rem solid #c7c7c7;
|
||||
background: rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.skin-dark select {
|
||||
background-color: rgba(87, 87, 87, 0.8);
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* remove dotted outline */
|
||||
select:-moz-focusring {
|
||||
color: transparent;
|
||||
text-shadow: 0 0 0 #333;
|
||||
}
|
||||
|
||||
.skin-dark select:-moz-focusring {
|
||||
text-shadow: 0 0 0 #fff;
|
||||
}
|
||||
|
||||
/* Hides dropdown arrow until bug #649849 is fixed */
|
||||
.bb-button.icon select,
|
||||
.button.icon select,
|
||||
.bb-button[data-icon] select,
|
||||
.button[data-icon] select {
|
||||
position: absolute;
|
||||
top: -0.6rem;
|
||||
offset-inline-start: 0;
|
||||
width: calc(100% + 3rem);
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-inline-end: 5rem;
|
||||
text-indent: 0;
|
||||
font-weight: 400;
|
||||
font-size: 1.7rem;
|
||||
color: rgba(0,0,0,0);
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
/**
|
||||
* The select element uses the text color to determine the color of the outline.
|
||||
* We put a text shadow with needed offset and no blur, so that replaces the text.
|
||||
*/
|
||||
html[dir="ltr"] .bb-button.icon select,
|
||||
html[dir="ltr"] .button.icon select,
|
||||
html[dir="ltr"] .bb-button[data-icon] select,
|
||||
html[dir="ltr"] .button[data-icon] select,
|
||||
html[dir="ltr"] .bb-button.icon-dialog select,
|
||||
html[dir="ltr"] .button.icon-dialog select {
|
||||
text-shadow: 1rem 0.5rem 0 #333;
|
||||
}
|
||||
html[dir="rtl"] .bb-button.icon select,
|
||||
html[dir="rtl"] .button.icon select,
|
||||
html[dir="rtl"] .bb-button[data-icon] select,
|
||||
html[dir="rtl"] .button[data-icon] select,
|
||||
html[dir="rtl"] .bb-button.icon-dialog select,
|
||||
html[dir="rtl"] .button.icon-dialog select {
|
||||
text-shadow: -1rem 0.5rem 0 #333;
|
||||
}
|
||||
|
||||
.bb-button.icon select option,
|
||||
.button.icon select option,
|
||||
.bb-button[data-icon] select option,
|
||||
.button[data-icon] select option {
|
||||
color: #000;
|
||||
border: 0;
|
||||
padding: 0.6rem 1.3rem;
|
||||
text-shadow: 0 0 0 rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.bb-button.icon-dialog,
|
||||
.button.icon-dialog {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .skin-dark .bb-button.icon select,
|
||||
html[dir="ltr"] .skin-dark .button.icon select,
|
||||
html[dir="ltr"] .skin-dark .bb-button[data-icon] select,
|
||||
html[dir="ltr"] .skin-dark .button[data-icon] select,
|
||||
html[dir="ltr"] .skin-dark .bb-button.icon-dialog select,
|
||||
html[dir="ltr"] .skin-dark .button.icon-dialog select {
|
||||
text-shadow: 1rem 0.5rem 0 #fff;
|
||||
}
|
||||
html[dir="rtl"] .skin-dark .bb-button.icon select,
|
||||
html[dir="rtl"] .skin-dark .button.icon select,
|
||||
html[dir="rtl"] .skin-dark .bb-button[data-icon] select,
|
||||
html[dir="rtl"] .skin-dark .button[data-icon] select,
|
||||
html[dir="rtl"] .skin-dark .bb-button.icon-dialog select,
|
||||
html[dir="rtl"] .skin-dark .button.icon-dialog select {
|
||||
text-shadow: -1rem 0.5rem 0 #fff;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><path fill="#667174" fill-opacity=".7" d="m 18.2995,19.66 -5.056,-5.057 5.056,-5.056 c -0.31,-0.516 -0.62,-1.032 -0.93,-1.34 l -5.672,5.673 v 1.652 l 5.675,5.676 c 0.31,-0.515 0.722,-0.928 0.93,-1.547 z"/></svg>
|
||||
|
After Width: | Height: | Size: 294 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><path fill="#667174" fill-opacity=".7" d="m 11.698,19.66 5.056,-5.057 -5.056,-5.056 c 0.31,-0.516 0.62,-1.032 0.93,-1.34 L 18.3,13.88 v 1.652 l -5.675,5.676 c -0.31,-0.515 -0.722,-0.928 -0.93,-1.547 z"/></svg>
|
||||
|
After Width: | Height: | Size: 292 B |
|
|
@ -0,0 +1,280 @@
|
|||
<!DOCTYPE html>
|
||||
<html dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Buttons</title>
|
||||
<link rel="stylesheet" href="../buttons.css">
|
||||
<link rel="stylesheet" href="../../elements/gaia-icons/gaia-icons-embedded.css">
|
||||
<link rel="stylesheet" href="../../elements/gaia-icons/bidi-helper.css">
|
||||
|
||||
<!--
|
||||
- These <style> and <script> elements are only used for the example code,
|
||||
- they are not needed for the real case.
|
||||
-->
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 10px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
label.bb-docs {
|
||||
font-family: sans-serif;
|
||||
font-size: 1.2rem;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
h2.bb-docs {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 300;
|
||||
color: #666;
|
||||
margin: -0.1rem 0 0;
|
||||
background-color: #f5f5f5;
|
||||
padding: 0.4rem 3rem;
|
||||
border: solid 0.1rem #e8e8e8;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
div.bb-docs {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
div.bb-docs.skin-organic {
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
div.bb-docs.skin-dark {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
body[role="application"] > [role="region"] {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-icon]:before {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
document.getElementById('dir').checked = false;
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body role="application">
|
||||
<p dir="ltr">
|
||||
<label class="bb-docs">
|
||||
<input id="dir" type="checkbox" checked="false"
|
||||
onchange="document.documentElement.dir = this.checked ? 'rtl' : 'ltr'">
|
||||
RTL mode
|
||||
</label>
|
||||
</p>
|
||||
<h2 class="bb-docs">Special buttons</h2>
|
||||
<div class="bb-docs skin-organic">
|
||||
<button>Default</button>
|
||||
<button class="recommend">Recommend</button>
|
||||
<button class="danger">Danger</button>
|
||||
</div>
|
||||
|
||||
<h2 class="bb-docs">Buttons with icons</h2>
|
||||
<div class="bb-docs skin-organic">
|
||||
<button data-icon="forward" aria-label="forward" data-l10n-id="">Default</button>
|
||||
<button data-icon="forward" aria-label="forward" data-l10n-id="" class="recommend">Recommend</button>
|
||||
<button data-icon="forward" aria-label="forward" data-l10n-id="" class="danger">Danger</button>
|
||||
<button data-icon="forward-light" aria-label="forward-light" data-l10n-id="">Default</button>
|
||||
<button data-icon="back" aria-label="back" data-l10n-id="">Default</button>
|
||||
<button data-icon="back-light" aria-label="back-light" data-l10n-id="">Default</button>
|
||||
</div>
|
||||
|
||||
<h2 class="bb-docs">Disabled</h2>
|
||||
<div class="bb-docs skin-organic">
|
||||
<button disabled>Default</button>
|
||||
<a href="#" role="button" aria-disabled="true" class="recommend bb-button">Recommend</a>
|
||||
<button disabled class="danger">Danger</button>
|
||||
</div>
|
||||
|
||||
<h2 class="bb-docs">Skin dark</h2>
|
||||
<div class="bb-docs skin-dark">
|
||||
<button>Default</button>
|
||||
<button class="recommend">Recommend</button>
|
||||
<button class="danger">Danger</button>
|
||||
</div>
|
||||
|
||||
<h2 class="bb-docs">Disabled</h2>
|
||||
<div class="bb-docs skin-dark">
|
||||
<button disabled>Default</button>
|
||||
<a href="#" role="button" aria-disabled="true" class="recommend bb-button">Recommend</a>
|
||||
<button disabled class="danger">Danger</button>
|
||||
</div>
|
||||
|
||||
<h2 class="bb-docs">List buttons</h2>
|
||||
<div class="bb-docs skin-organic">
|
||||
<ul>
|
||||
<li>
|
||||
<button>Default</button>
|
||||
</li>
|
||||
<li>
|
||||
<button disabled>Disabled</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="icon icon-view">
|
||||
Icon view button
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="icon icon-dialog">
|
||||
Icon dialog button
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<span class="button icon icon-dialog">
|
||||
<select>
|
||||
<option selected>Icon dialog with options</option>
|
||||
<option>option 1</option>
|
||||
<option>option 2</option>
|
||||
<option>option 3</option>
|
||||
</select>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<button class="icon icon-dialog" disabled>
|
||||
Icon dialog button disabled
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button data-icon="forward" aria-label="forward" data-l10n-id="">
|
||||
Side navigation
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button disabled data-icon="forward" aria-label="forward" data-l10n-id="">
|
||||
Side navigation
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a role="button" data-icon="expand" class="bb-button" aria-label="expand" data-l10n-id="">
|
||||
Tuesday Sept. 18, 2012
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a role="button" aria-disabled="true" data-icon="expand" class="bb-button" aria-label="expand" data-l10n-id="">
|
||||
Tuesday Sept. 18, 2012
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 class="bb-docs">Skin Dark</h2>
|
||||
<div class="bb-docs skin-dark">
|
||||
<ul>
|
||||
<li>
|
||||
<button>Default</button>
|
||||
</li>
|
||||
<li>
|
||||
<button disabled>Disabled</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="icon icon-dialog">
|
||||
Dark icon dialog button
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button data-icon="forward" aria-label="forward" data-l10n-id="">
|
||||
Side navigation
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button disabled data-icon="forward" aria-label="forward" data-l10n-id="">
|
||||
Side navigation
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a role="button" data-icon="expand" class="bb-button" aria-label="expand" data-l10n-id="">
|
||||
Tuesday Sept. 18, 2012
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a role="button" aria-disabled="true" data-icon="expand" class="bb-button" aria-label="expand" data-l10n-id="">
|
||||
Tuesday Sept. 18, 2012
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 class="bb-docs">Select</h2>
|
||||
<div class="bb-docs">
|
||||
<select>
|
||||
<option>option 1</option>
|
||||
<option>option 2</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h2 class="bb-docs">Fake select</h2>
|
||||
<div class="bb-docs">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="bb-button" data-icon="expand" aria-label="expand" data-l10n-id="">
|
||||
<select>
|
||||
<option>option 1</option>
|
||||
<option>option 2</option>
|
||||
</select>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 class="bb-docs">Disabled select</h2>
|
||||
<div class="bb-docs">
|
||||
<ul>
|
||||
<li>
|
||||
<span aria-disabled="true" class="bb-button" data-icon="expand" aria-label="expand" data-l10n-id="">
|
||||
<select>
|
||||
<option>option 1</option>
|
||||
<option>option 2</option>
|
||||
</select>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 class="bb-docs">Skin dark</h2>
|
||||
<div class="bb-docs skin-dark">
|
||||
<select>
|
||||
<option>option 1</option>
|
||||
<option>option 2</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h2 class="bb-docs">Fake select</h2>
|
||||
<div class="bb-docs skin-dark">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="bb-button" data-icon="expand" aria-label="expand" data-l10n-id="">
|
||||
<select>
|
||||
<option>option 1</option>
|
||||
<option>option 2</option>
|
||||
</select>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span aria-disabled="true" class="bb-button" data-icon="expand" aria-label="expand" data-l10n-id="">
|
||||
<select>
|
||||
<option>option 1</option>
|
||||
<option>option 2</option>
|
||||
</select>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
/* -------------------------------------------------------------------
|
||||
HEADERS: layout
|
||||
Form factor or Skin dependent styles should NOT be defined here.
|
||||
------------------------------------------------------------------- */
|
||||
|
||||
section[role="region"] > header:first-child {
|
||||
position: relative;
|
||||
display: block;
|
||||
z-index: 10;
|
||||
padding: 0;
|
||||
height: 5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child h1 {
|
||||
font-size: 2.3rem;
|
||||
line-height: 5rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0 1rem;
|
||||
height: 100%;
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child h1 em {
|
||||
font-weight: 400;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1em;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child menu {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
html[dir="ltr"] section[role="region"] > header:first-child menu {
|
||||
float: right;
|
||||
}
|
||||
html[dir="rtl"] section[role="region"] > header:first-child menu {
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
Buttons
|
||||
---------------------------------- */
|
||||
|
||||
section[role="region"] > header:first-child a,
|
||||
section[role="region"] > header:first-child button {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
min-width: 5rem;
|
||||
width: auto;
|
||||
height: 5rem;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0 1rem;
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
font-size: 1.5rem;
|
||||
line-height: 5rem;
|
||||
border-radius: 0;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
font-style: italic;
|
||||
}
|
||||
html[dir="ltr"] section[role="region"] > header:first-child a,
|
||||
html[dir="ltr"] section[role="region"] > header:first-child button {
|
||||
float: left;
|
||||
}
|
||||
html[dir="rtl"] section[role="region"] > header:first-child a,
|
||||
html[dir="rtl"] section[role="region"] > header:first-child button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Pressed state */
|
||||
section[role="region"] > header:first-child a:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child button::-moz-focus-inner {
|
||||
outline: none;
|
||||
border: none;
|
||||
margin-top: -0.2rem; /* To fix line-height bug (697451) */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
section[role="region"] > header:first-child a[aria-disabled="true"],
|
||||
section[role="region"] > header:first-child button[disabled] {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
Icons
|
||||
---------------------------------- */
|
||||
|
||||
section[role="region"] > header:first-child .icon {
|
||||
display: block;
|
||||
width: 3rem;
|
||||
height: 5rem;
|
||||
background: transparent no-repeat center / 3rem auto;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
Subheader
|
||||
---------------------------------- */
|
||||
|
||||
section[role="region"] > header {
|
||||
z-index: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
section[role="region"] header h2 {
|
||||
margin: 0;
|
||||
padding: 0.8rem 3rem;
|
||||
font-weight: 400;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.8rem;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
HEADER SKIN: default
|
||||
Default values in case we are not overriding them using
|
||||
class="skin-*"
|
||||
----------------------------------------------------------------- */
|
||||
|
||||
section[role="region"] > header:first-child {
|
||||
color: #fff;
|
||||
background-color: #f97c17;
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child h1 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child a,
|
||||
section[role="region"] > header:first-child button {
|
||||
color: rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child a:active,
|
||||
section[role="region"] > header:first-child button:active,
|
||||
section[role="region"] > header:first-child a:hover,
|
||||
section[role="region"] > header:first-child button:hover {
|
||||
background-color: rgba(0,0,0,0.07);
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child a:focus,
|
||||
section[role="region"] > header:first-child button:focus {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
Icons
|
||||
---------------------------------- */
|
||||
|
||||
section[role="region"] > header:first-child .icon-add {
|
||||
background-image: url(headers/images/icons/add.png);
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child .icon-compose {
|
||||
background-image: url(headers/images/icons/compose.png);
|
||||
}
|
||||
|
||||
html[dir="ltr"] section[role="region"] > header:first-child .icon-edit {
|
||||
background-image: url(headers/images/icons/edit.png);
|
||||
}
|
||||
html[dir="rtl"] section[role="region"] > header:first-child .icon-edit {
|
||||
background-image: url(headers/images/icons/edit-rtl.png);
|
||||
}
|
||||
|
||||
html[dir="ltr"] section[role="region"] > header:first-child .icon-send {
|
||||
background-image: url(headers/images/icons/send.png);
|
||||
}
|
||||
html[dir="rtl"] section[role="region"] > header:first-child .icon-send {
|
||||
background-image: url(headers/images/icons/send_rtl.png);
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child .icon-close {
|
||||
background-image: url(headers/images/icons/close.png);
|
||||
}
|
||||
|
||||
html[dir="ltr"] section[role="region"] > header:first-child .icon-back {
|
||||
background-image: url(headers/images/icons/left.png);
|
||||
}
|
||||
html[dir="rtl"] section[role="region"] > header:first-child .icon-back {
|
||||
background-image: url(headers/images/icons/right.png);
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child .icon-menu {
|
||||
background-image: url(headers/images/icons/menu.png);
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child .icon-user {
|
||||
background-image: url(headers/images/icons/user.png);
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child .icon-up {
|
||||
background-image: url(headers/images/icons/up.png);
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child .icon-down {
|
||||
background-image: url(headers/images/icons/down.png);
|
||||
}
|
||||
|
||||
section[role="region"] > header:first-child .icon-options {
|
||||
background-image: url(headers/images/icons/options.png);
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
Subheader
|
||||
---------------------------------- */
|
||||
|
||||
section[role="region"] header h2 {
|
||||
color: #424242;
|
||||
border-bottom: solid 0.1rem #e6e6e6;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
HEADER SKIN: comms
|
||||
.skin-comms will override default values
|
||||
----------------------------------------------------------------- */
|
||||
|
||||
.skin-comms section[role="region"] > header:first-child,
|
||||
section[role="region"].skin-comms > header:first-child {
|
||||
background-color: #00adad;
|
||||
}
|
||||
|
||||
.skin-comms section[role="region"] > header:first-child a,
|
||||
.skin-comms section[role="region"] > header:first-child button,
|
||||
section[role="region"].skin-comms > header:first-child a,
|
||||
section[role="region"].skin-comms > header:first-child button {
|
||||
color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.skin-comms section[role="region"] > header:first-child a:active,
|
||||
.skin-comms section[role="region"] > header:first-child button:active,
|
||||
.skin-comms section[role="region"] > header:first-child a:hover,
|
||||
.skin-comms section[role="region"] > header:first-child button:hover,
|
||||
section[role="region"].skin-comms > header:first-child a:active,
|
||||
section[role="region"].skin-comms > header:first-child button:active,
|
||||
section[role="region"].skin-comms > header:first-child a:hover,
|
||||
section[role="region"].skin-comms > header:first-child button:hover {
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.skin-comms section[role="region"] > header:first-child a:focus,
|
||||
.skin-comms section[role="region"] > header:first-child button:focus,
|
||||
section[role="region"].skin-comms > header:first-child a:focus,
|
||||
section[role="region"].skin-comms > header:first-child button:focus {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
HEADER SKIN: dark
|
||||
.skin-dark will override default values
|
||||
----------------------------------------------------------------- */
|
||||
|
||||
section[role="region"].skin-dark > header:first-child,
|
||||
.skin-dark > section[role="region"] > header:first-child {
|
||||
background-color: #242d33;
|
||||
}
|
||||
|
||||
.skin-dark section[role="region"] > header:first-child a,
|
||||
.skin-dark section[role="region"] > header:first-child button,
|
||||
section[role="region"].skin-dark > header:first-child a,
|
||||
section[role="region"].skin-dark > header:first-child button {
|
||||
color: #00aac5;
|
||||
}
|
||||
|
||||
.skin-dark section[role="region"] > header:first-child a:active,
|
||||
.skin-dark section[role="region"] > header:first-child button:active,
|
||||
.skin-dark section[role="region"] > header:first-child a:hover,
|
||||
.skin-dark section[role="region"] > header:first-child button:hover,
|
||||
section[role="region"].skin-dark > header:first-child a:active,
|
||||
section[role="region"].skin-dark > header:first-child button:active,
|
||||
section[role="region"].skin-dark > header:first-child a:hover,
|
||||
section[role="region"].skin-dark > header:first-child button:hover {
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.skin-dark section[role="region"] > header:first-child a:focus,
|
||||
.skin-dark section[role="region"] > header:first-child button:focus,
|
||||
section[role="region"].skin-dark > header:first-child a:focus,
|
||||
section[role="region"].skin-dark > header:first-child button:focus {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
section[role="region"].skin-dark header h2,
|
||||
.skin-dark > section[role="region"] header h2 {
|
||||
background-color: #2c353b;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
HEADER SKIN: organic
|
||||
.skin-organic will override default values
|
||||
----------------------------------------------------------------- */
|
||||
|
||||
section[role="region"].skin-organic > header:first-child,
|
||||
.skin-organic section[role="region"] > header:first-child {
|
||||
color: #868692;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
section[role="region"].skin-organic > header:first-child h1,
|
||||
.skin-organic section[role="region"] > header:first-child h1 {
|
||||
color: #868692;
|
||||
}
|
||||
|
||||
section[role="region"].skin-organic > header:first-child a,
|
||||
section[role="region"].skin-organic > header:first-child button,
|
||||
.skin-organic section[role="region"] > header:first-child a,
|
||||
.skin-organic section[role="region"] > header:first-child button {
|
||||
color: #00aac5;
|
||||
}
|
||||
|
||||
section[role="region"].skin-organic > header:first-child a:focus,
|
||||
section[role="region"].skin-organic > header:first-child button:focus,
|
||||
.skin-organic section[role="region"] > header:first-child a:focus,
|
||||
.skin-organic section[role="region"] > header:first-child button:focus {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
Icons
|
||||
---------------------------------- */
|
||||
|
||||
html[dir="ltr"] section[role="region"].skin-organic > header:first-child .icon-back,
|
||||
html[dir="ltr"] .skin-organic section[role="region"] > header:first-child .icon-back {
|
||||
background-image: url(headers/images/icons/organic/left.png);
|
||||
}
|
||||
html[dir="rtl"] section[role="region"].skin-organic > header:first-child .icon-back,
|
||||
html[dir="rtl"] .skin-organic section[role="region"] > header:first-child .icon-back {
|
||||
background-image: url(headers/images/icons/organic/right.png);
|
||||
}
|
||||
|
||||
section[role="region"].skin-organic > header:first-child .icon-close,
|
||||
.skin-organic section[role="region"] > header:first-child .icon-close {
|
||||
background-image: url(headers/images/icons/organic/close.png);
|
||||
}
|
||||
|
||||
section[role="region"].skin-organic > header:first-child .icon-add,
|
||||
.skin-organic section[role="region"] > header:first-child .icon-add {
|
||||
background-image: url(headers/images/icons/organic/add.png);
|
||||
}
|
||||
|
||||
html[dir="ltr"] section[role="region"].skin-organic > header:first-child .icon-edit,
|
||||
html[dir="ltr"] .skin-organic section[role="region"] > header:first-child .icon-edit {
|
||||
background-image: url(headers/images/icons/organic/edit.png);
|
||||
}
|
||||
html[dir="rtl"] section[role="region"].skin-organic > header:first-child .icon-edit,
|
||||
html[dir="rtl"] .skin-organic section[role="region"] > header:first-child .icon-edit {
|
||||
background-image: url(headers/images/icons/organic/edit-rtl.png);
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
Subheader
|
||||
---------------------------------- */
|
||||
|
||||
section[role="region"].skin-organic header h2,
|
||||
.skin-organic section[role="region"] header h2 {
|
||||
background-color: #e7e7e7;
|
||||
color: #4d4d4d;
|
||||
border: none;
|
||||
}
|
||||
|
After Width: | Height: | Size: 162 B |
|
After Width: | Height: | Size: 188 B |
|
After Width: | Height: | Size: 273 B |
|
After Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 324 B |
|
After Width: | Height: | Size: 344 B |
|
After Width: | Height: | Size: 146 B |
|
After Width: | Height: | Size: 96 B |
|
After Width: | Height: | Size: 152 B |
|
After Width: | Height: | Size: 129 B |
|
After Width: | Height: | Size: 200 B |
|
After Width: | Height: | Size: 409 B |
|
After Width: | Height: | Size: 345 B |
|
After Width: | Height: | Size: 151 B |
|
After Width: | Height: | Size: 179 B |
|
After Width: | Height: | Size: 272 B |
|
After Width: | Height: | Size: 194 B |
|
After Width: | Height: | Size: 179 B |
|
After Width: | Height: | Size: 297 B |
|
After Width: | Height: | Size: 313 B |
|
After Width: | Height: | Size: 223 B |
|
After Width: | Height: | Size: 279 B |
|
|
@ -0,0 +1,180 @@
|
|||
<!DOCTYPE html>
|
||||
<html dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Headers</title>
|
||||
<meta name="description" content="Providing top-level navigation and inputs for the active view">
|
||||
<!--
|
||||
- This <style> and <link> is only used for the example preview,
|
||||
- it isn't needed for the real use case.
|
||||
-->
|
||||
<link rel="stylesheet" href="../headers.css">
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 10px;
|
||||
background-color: #fff;
|
||||
}
|
||||
body {
|
||||
background: none;
|
||||
}
|
||||
|
||||
h2.bb-docs {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 300;
|
||||
color: #666;
|
||||
margin: -0.1rem 0 0;
|
||||
background-color: #f5f5f5;
|
||||
padding: 0.4rem;
|
||||
padding-inline-start: 3rem;
|
||||
border: solid 0.1rem #e8e8e8;
|
||||
margin: 3rem 0 1rem;
|
||||
}
|
||||
|
||||
section[role="region"] {
|
||||
margin-bottom: 1.5rem;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
document.getElementById('dir').checked = false;
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body role="application">
|
||||
<p dir="ltr">
|
||||
<label>
|
||||
<input type="checkbox" id="dir"
|
||||
onchange="document.documentElement.dir = this.checked ? 'rtl' : 'ltr'">
|
||||
RTL mode
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<section role="region">
|
||||
<header>
|
||||
<a href="#"><span class="icon icon-close">close</span></a>
|
||||
<menu type="toolbar">
|
||||
<button>done</button>
|
||||
</menu>
|
||||
<h1>Email</h1>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<section role="region">
|
||||
<header>
|
||||
<a href="#"></a>
|
||||
<menu type="toolbar">
|
||||
<button><span class="icon icon-send">send</span></button>
|
||||
</menu>
|
||||
<h1>Send message to X</h1>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<section role="region">
|
||||
<header>
|
||||
<a href="#"><span class="icon icon-back">back</span></a>
|
||||
<menu type="toolbar">
|
||||
<button><span class="icon icon-edit">edit</span></button>
|
||||
<button><span class="icon icon-add">add</span></button>
|
||||
</menu>
|
||||
<h1>Email</h1>
|
||||
</header>
|
||||
<header>
|
||||
<h2>Subheader text</h2>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<section role="region">
|
||||
<header>
|
||||
<button><span class="icon icon-menu">menu</span></button>
|
||||
<menu type="toolbar">
|
||||
<button><span class="icon icon-edit">edit</span></button>
|
||||
<button><span class="icon icon-add">add</span></button>
|
||||
</menu>
|
||||
<h1>Inbox <em>(2)</em></h1>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<h2 class="bb-docs">Skin: Dark</h2>
|
||||
<section role="region" class="skin-dark">
|
||||
<header>
|
||||
<a href="#"><span class="icon icon-close">close</span></a>
|
||||
<menu type="toolbar">
|
||||
<button>done</button>
|
||||
</menu>
|
||||
<h1>Music</h1>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<section role="region" class="skin-dark">
|
||||
<header>
|
||||
<a href="#"><span class="icon icon-back">back</span></a>
|
||||
<menu type="toolbar">
|
||||
<button><span class="icon icon-edit">edit</span></button>
|
||||
<button><span class="icon icon-add">add</span></button>
|
||||
</menu>
|
||||
<h1>Music</h1>
|
||||
</header>
|
||||
<header>
|
||||
<h2>Subheader text</h2>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<h2 class="bb-docs">Skin: Organic</h2>
|
||||
<section role="region" class="skin-organic">
|
||||
<header>
|
||||
<a href="#"><span class="icon icon-close">close</span></a>
|
||||
<menu type="toolbar">
|
||||
<button>done</button>
|
||||
</menu>
|
||||
<h1>Settings</h1>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<div class="skin-organic">
|
||||
<section role="region">
|
||||
<header>
|
||||
<a href="#"><span class="icon icon-back">back</span></a>
|
||||
<menu type="toolbar">
|
||||
<button><span class="icon icon-edit">edit</span></button>
|
||||
<button><span class="icon icon-add">add</span></button>
|
||||
</menu>
|
||||
<h1>Settings</h1>
|
||||
</header>
|
||||
<header>
|
||||
<h2>Subheader text</h2>
|
||||
</header>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<h2 class="bb-docs">Skin: Comms</h2>
|
||||
<div class="skin-comms">
|
||||
<section role="region">
|
||||
<header>
|
||||
<a href="#"><span class="icon icon-close">close</span></a>
|
||||
<menu type="toolbar">
|
||||
<button>done</button>
|
||||
</menu>
|
||||
<h1>Contacts</h1>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<section role="region">
|
||||
<header>
|
||||
<a href="#"><span class="icon icon-back">back</span></a>
|
||||
<menu type="toolbar">
|
||||
<button><span class="icon icon-edit">edit</span></button>
|
||||
<button><span class="icon icon-add">add</span></button>
|
||||
</menu>
|
||||
<h1>Contacts</h1>
|
||||
</header>
|
||||
<header>
|
||||
<h2>Subheader text</h2>
|
||||
</header>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,575 @@
|
|||
/* ----------------------------------
|
||||
* Input areas
|
||||
* ---------------------------------- */
|
||||
|
||||
/**
|
||||
* BiDi notes:
|
||||
*
|
||||
* - as the <input>, <textarea> and [contentEditable] elements are filled by
|
||||
* the user, they need a `unicode-bidi: -moz-plaintext` attribute for their
|
||||
* content to be displayed properly (e.g. LTR content in an RTL document,
|
||||
* or vice-versa);
|
||||
*
|
||||
* - consequence: *-inline-start/end properties can still be used on such
|
||||
* elements content direction doesn't impact element's direction
|
||||
*
|
||||
* - however, the one-line inputs must still be aligned along with the
|
||||
* *document* direction: to the left in LTR, to the right in RTL
|
||||
* -- no matter what the *content* direction is.
|
||||
*/
|
||||
|
||||
html[dir="ltr"] input { text-align: left; }
|
||||
html[dir="rtl"] input { text-align: right; }
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="email"],
|
||||
input[type="tel"],
|
||||
input[type="search"],
|
||||
input[type="url"],
|
||||
input[type="number"],
|
||||
textarea {
|
||||
font-family: sans-serif;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 4rem;
|
||||
line-height: 4rem;
|
||||
resize: none;
|
||||
padding: 0 1.5rem;
|
||||
font-size: 1.6rem;
|
||||
border: 0.1rem solid #c7c7c7;
|
||||
border-radius: 0;
|
||||
box-shadow: none; /* override the box-shadow from the system (performance issue) */
|
||||
color: #333;
|
||||
background: #fff;
|
||||
margin: 0 0 1rem;
|
||||
unicode-bidi: -moz-plaintext;
|
||||
}
|
||||
|
||||
label:active {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 10rem;
|
||||
max-height: 10rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
input::-moz-placeholder,
|
||||
textarea::-moz-placeholder {
|
||||
color: #a9a9a9;
|
||||
opacity: 1;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
input[disabled],
|
||||
textarea[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.skin-dark input[type="text"],
|
||||
.skin-dark input[type="password"],
|
||||
.skin-dark input[type="email"],
|
||||
.skin-dark input[type="tel"],
|
||||
.skin-dark input[type="search"],
|
||||
.skin-dark input[type="url"],
|
||||
.skin-dark input[type="number"],
|
||||
.skin-dark textarea {
|
||||
color: #fff;
|
||||
background-color: transparent;
|
||||
border-color: #454545;
|
||||
}
|
||||
|
||||
form p {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
form p input + button[type="reset"],
|
||||
form p textarea + button[type="reset"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-size: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
background: url(input_areas/images/clear.png) no-repeat 50% 50% / 2.4rem auto;
|
||||
}
|
||||
html[dir="ltr"] form p input + button[type="reset"],
|
||||
html[dir="ltr"] form p textarea + button[type="reset"] {
|
||||
right: -0.3rem;
|
||||
}
|
||||
html[dir="rtl"] form p input + button[type="reset"],
|
||||
html[dir="rtl"] form p textarea + button[type="reset"] {
|
||||
left: -0.3rem;
|
||||
}
|
||||
|
||||
.skin-dark p input + button[type="reset"],
|
||||
.skin-dark p textarea + button[type="reset"] {
|
||||
background-image: url(input_areas/images/clear_dark.png);
|
||||
}
|
||||
|
||||
/* To avoid colission with BB butons */
|
||||
li input + button[type="reset"]:after,
|
||||
li textarea + button[type="reset"]:after {
|
||||
background: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
padding: 1.2rem;
|
||||
}
|
||||
|
||||
html[dir="ltr"] form p input:focus { padding-right: 3rem; }
|
||||
html[dir="rtl"] form p input:focus { padding-left: 3rem; }
|
||||
|
||||
form p input:focus + button[type="reset"],
|
||||
form p textarea:focus + button[type="reset"] {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* Fieldset */
|
||||
fieldset {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 1.5rem 0 0;
|
||||
padding: 0;
|
||||
font-size: 2rem;
|
||||
line-height: 1em;
|
||||
background: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
fieldset legend,
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime"],
|
||||
input[type="datetime-local"] {
|
||||
border: 0.1rem solid #c7c7c7;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
font-style: italic;
|
||||
font-size: 1.6rem;
|
||||
margin: 0 0 1.5rem;
|
||||
padding: 0 1.5rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
height: 4rem;
|
||||
color: #333;
|
||||
border-radius: 0;
|
||||
background-color: #fff;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
html[dir="ltr"] fieldset legend,
|
||||
html[dir="ltr"] input[type="date"],
|
||||
html[dir="ltr"] input[type="time"],
|
||||
html[dir="ltr"] input[type="datetime"],
|
||||
html[dir="ltr"] input[type="datetime-local"] {
|
||||
background-image: url(input_areas/images/dialog.svg);
|
||||
background-position: right 1rem bottom 1rem;
|
||||
}
|
||||
html[dir="rtl"] fieldset legend,
|
||||
html[dir="rtl"] input[type="date"],
|
||||
html[dir="rtl"] input[type="time"],
|
||||
html[dir="rtl"] input[type="datetime"],
|
||||
html[dir="rtl"] input[type="datetime-local"] {
|
||||
background-image: url(input_areas/images/dialog_rtl.svg);
|
||||
background-position: left 1rem bottom 1rem;
|
||||
}
|
||||
|
||||
fieldset legend {
|
||||
margin: 0 0 1rem;
|
||||
padding: 1rem 1.5rem 0;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
fieldset[disabled] {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.skin-dark fieldset legend,
|
||||
.skin-dark input[type="date"],
|
||||
.skin-dark input[type="time"],
|
||||
.skin-dark input[type="datetime"],
|
||||
.skin-dark input[type="datetime-local"] {
|
||||
color: #fff;
|
||||
background-color: transparent;
|
||||
border-color: #454545;
|
||||
}
|
||||
|
||||
fieldset[disabled] legend,
|
||||
input[type="date"][disabled],
|
||||
input[type="time"][disabled],
|
||||
input[type="datetime"][disabled],
|
||||
input[type="datetime-local"][disabled] {
|
||||
background-color: transparent;
|
||||
}
|
||||
html[dir="ltr"] fieldset[disabled] legend,
|
||||
html[dir="ltr"] input[type="date"][disabled],
|
||||
html[dir="ltr"] input[type="time"][disabled],
|
||||
html[dir="ltr"] input[type="datetime"][disabled],
|
||||
html[dir="ltr"] input[type="datetime-local"][disabled] {
|
||||
background-image: url(input_areas/images/dialog_disabled.svg);
|
||||
}
|
||||
html[dir="rtl"] fieldset[disabled] legend,
|
||||
html[dir="rtl"] input[type="date"][disabled],
|
||||
html[dir="rtl"] input[type="time"][disabled],
|
||||
html[dir="rtl"] input[type="datetime"][disabled],
|
||||
html[dir="rtl"] input[type="datetime-local"][disabled] {
|
||||
background-image: url(input_areas/images/dialog_disabled_rtl.svg);
|
||||
}
|
||||
|
||||
input[type="date"]:focus,
|
||||
input[type="time"]:focus,
|
||||
input[type="datetime"]:focus,
|
||||
input[type="datetime-local"]:focus {
|
||||
box-shadow: none;
|
||||
border-bottom-color: #c7c7c7;
|
||||
}
|
||||
|
||||
.skin-dark input[type="date"]:focus,
|
||||
.skin-dark input[type="time"]:focus,
|
||||
.skin-dark input[type="datetime"]:focus,
|
||||
.skin-dark input[type="datetime-local"]:focus {
|
||||
box-shadow: none;
|
||||
border-bottom-color: #454545;
|
||||
}
|
||||
|
||||
input[type="date"]:active,
|
||||
input[type="time"]:active,
|
||||
input[type="datetime"]:active,
|
||||
input[type="datetime-local"]:active {
|
||||
background-color: #b2f2ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-dark input[type="date"]:active,
|
||||
.skin-dark input[type="time"]:active,
|
||||
.skin-dark input[type="datetime"]:active,
|
||||
.skin-dark input[type="datetime-local"]:active {
|
||||
background-color: #00aacc;
|
||||
background-image: url(input_areas/images/dialog_active.svg);
|
||||
}
|
||||
|
||||
/* fix for required inputs with wrong or empty value e.g. [type=email] */
|
||||
input:invalid,
|
||||
textarea:invalid,
|
||||
.skin-dark input:invalid,
|
||||
.skin-dark textarea:invalid {
|
||||
color: #b90000;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
.skin-dark input:focus,
|
||||
.skin-dark textarea:focus {
|
||||
box-shadow: inset 0 -0.1rem 0 #00caf2;
|
||||
border-bottom-color: #00caf2;
|
||||
}
|
||||
|
||||
input:invalid:focus,
|
||||
textarea:invalid:focus,
|
||||
.skin-dark input:invalid:focus,
|
||||
.skin-dark textarea:invalid:focus {
|
||||
box-shadow: inset 0 -0.1rem 0 #820000;
|
||||
border-bottom-color: #820000;
|
||||
}
|
||||
|
||||
input[type="range"]:focus,
|
||||
.skin-dark input[type="range"]:focus {
|
||||
box-shadow: none;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* Tidy (search/submit) */
|
||||
form[role="search"] {
|
||||
position: relative;
|
||||
height: 3.7rem;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
form[role="search"].skin-dark {
|
||||
background: #202020;
|
||||
}
|
||||
|
||||
form[role="search"] p {
|
||||
padding: 0 1.5rem;
|
||||
padding-inline-start: 3rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
form[role="search"] p input,
|
||||
form[role="search"] p textarea {
|
||||
height: 3.7rem;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
form[role="search"] p textarea {
|
||||
padding: 1rem 0 0;
|
||||
}
|
||||
|
||||
form[role="search"] p input::-moz-placeholder {
|
||||
background-image: url(input_areas/images/search.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 3rem;
|
||||
}
|
||||
html[dir="ltr"] form[role="search"] p input::-moz-placeholder {
|
||||
background-position: right -0.5rem center;
|
||||
}
|
||||
html[dir="rtl"] form[role="search"] p input::-moz-placeholder {
|
||||
background-position: left -0.5rem center;
|
||||
}
|
||||
|
||||
form[role="search"].skin-dark p input::-moz-placeholder {
|
||||
background-image: url(input_areas/images/search_dark.svg);
|
||||
}
|
||||
|
||||
form[role="search"] p input:focus::-moz-placeholder {
|
||||
background: none;
|
||||
}
|
||||
|
||||
form[role="search"] p input:invalid,
|
||||
form[role="search"] p textarea:invalid,
|
||||
form[role="search"] p input:focus,
|
||||
form[role="search"] p textarea:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
form[role="search"].skin-dark p input,
|
||||
form[role="search"].skin-dark p textarea {
|
||||
color: #fff;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
form[role="search"] button[type="submit"] {
|
||||
min-width: 6rem;
|
||||
height: 3.7rem;
|
||||
padding: 0 1.5rem;
|
||||
border: none;
|
||||
color: #00aac5;
|
||||
font-weight: normal;
|
||||
font-size: 1.6rem;
|
||||
line-height: 3.7rem;
|
||||
width: auto;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
background-image: none;
|
||||
background-color: unset;
|
||||
font-style: italic;
|
||||
overflow: visible;
|
||||
}
|
||||
html[dir="ltr"] form[role="search"] button[type="submit"] { float: right; }
|
||||
html[dir="rtl"] form[role="search"] button[type="submit"] { float: left; }
|
||||
|
||||
form[role="search"] button[type="submit"]:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
offset-inline-start: -0.1rem;
|
||||
top: 0.7rem;
|
||||
bottom: 0.7rem;
|
||||
width: 0.1rem;
|
||||
background: #c7c7c7;
|
||||
}
|
||||
|
||||
form[role="search"] button[type="submit"].icon {
|
||||
font-size: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 3rem;
|
||||
}
|
||||
|
||||
form[role="search"] p input + button[type="reset"]{
|
||||
height: 3.7rem;
|
||||
offset-inline-end: 0.5rem;
|
||||
}
|
||||
|
||||
form[role="search"].skin-dark button[type="submit"]:after {
|
||||
background-color: #575757;
|
||||
}
|
||||
|
||||
form[role="search"] button[type="submit"] + p > textarea {
|
||||
height: 3.7rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
form[role="search"] button[type="submit"] + p button {
|
||||
height: 3.7rem;
|
||||
}
|
||||
|
||||
form[role="search"].full button[type="submit"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
form[role="search"] button.icon:active,
|
||||
form[role="search"] button[type="submit"]:active {
|
||||
background-color: #b2f2ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
form[role="search"].skin-dark button.icon:active,
|
||||
form[role="search"].skin-dark button[type="submit"]:active {
|
||||
background: #00aacc;
|
||||
}
|
||||
|
||||
form[role="search"] button[type="submit"][disabled] {
|
||||
color: #adadad;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
form button::-moz-focus-inner {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* .bb-editable - e.g email & SMS recipients */
|
||||
.bb-editable [contenteditable] {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
max-width: calc(100% - 1.4rem);
|
||||
overflow: hidden;
|
||||
padding: 0 1.5rem;
|
||||
margin: 0 0 0.6rem;
|
||||
line-height: 2.8rem;
|
||||
border-radius: 1.4rem;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.6rem;
|
||||
font-style: italic;
|
||||
white-space: nowrap;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .bb-editable [contenteditable].icon {
|
||||
padding-right: 3.7rem;
|
||||
}
|
||||
html[dir="rtl"] .bb-editable [contenteditable].icon {
|
||||
padding-left: 3.7rem;
|
||||
}
|
||||
|
||||
.bb-editable [contenteditable="false"].invalid {
|
||||
background-color: #fedcdc;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
html[dir="ltr"] .bb-editable [contenteditable="false"].invalid { padding-left: 3.3rem; }
|
||||
html[dir="rtl"] .bb-editable [contenteditable="false"].invalid { padding-right: 3.3rem; }
|
||||
|
||||
.bb-editable [contenteditable="false"].invalid:before {
|
||||
content: "!";
|
||||
position: absolute;
|
||||
top: 0.6rem;
|
||||
display: block;
|
||||
width: 1.7rem;
|
||||
height: 1.6rem;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
line-height: 1.6rem;
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
text-indent: -0.1rem;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background-color: #b80404;
|
||||
}
|
||||
html[dir="ltr"] .bb-editable [contenteditable="false"].invalid:before { left: 0.8rem; }
|
||||
html[dir="rtl"] .bb-editable [contenteditable="false"].invalid:before { right: 0.8rem; }
|
||||
|
||||
.bb-editable [contenteditable="false"].invalid:hover:before,
|
||||
.bb-editable [contenteditable="false"].invalid:active:before {
|
||||
color: #b2f2ff;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.bb-editable [contenteditable]:hover,
|
||||
.bb-editable [contenteditable]:active,
|
||||
.bb-editable [contenteditable].invalid:hover,
|
||||
.bb-editable [contenteditable].invalid:active {
|
||||
color: #fff;
|
||||
background-color: #b2f2ff;
|
||||
}
|
||||
|
||||
.bb-editable [contenteditable].icon:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0.7rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 3rem;
|
||||
}
|
||||
html[dir="ltr"] .bb-editable [contenteditable].icon:after {
|
||||
right: 0;
|
||||
background-position: -0.7rem -3.8rem;
|
||||
}
|
||||
html[dir="rtl"] .bb-editable [contenteditable].icon:after {
|
||||
left: 0;
|
||||
background-position: 0.7rem -3.8rem;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .bb-editable [contenteditable].icon:hover:after,
|
||||
html[dir="ltr"] .bb-editable [contenteditable].icon:active:after {
|
||||
background-position: -0.7rem -0.8rem;
|
||||
}
|
||||
html[dir="rtl"] .bb-editable [contenteditable].icon:hover:after,
|
||||
html[dir="rtl"] .bb-editable [contenteditable].icon:active:after {
|
||||
background-position: 0.7rem -0.8rem;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .bb-editable [contenteditable].invalid.icon {
|
||||
padding-right: 3.7rem;
|
||||
}
|
||||
html[dir="rtl"] .bb-editable [contenteditable].invalid.icon {
|
||||
padding-left: 3.7rem;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .bb-editable [contenteditable].invalid.icon:after {
|
||||
background-position: -0.7rem -6.8rem;
|
||||
}
|
||||
html[dir="rtl"] .bb-editable [contenteditable].invalid.icon:after {
|
||||
background-position: 0.7rem -6.8rem;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .bb-editable [contenteditable].invalid.icon:hover:after,
|
||||
html[dir="ltr"] .bb-editable [contenteditable].invalid.icon:active:after {
|
||||
background-position: -0.7rem -0.8rem;
|
||||
}
|
||||
html[dir="rtl"] .bb-editable [contenteditable].invalid.icon:hover:after,
|
||||
html[dir="rtl"] .bb-editable [contenteditable].invalid.icon:active:after {
|
||||
background-position: 0.7rem -0.8rem;
|
||||
}
|
||||
|
||||
.bb-editable [contenteditable="true"],
|
||||
.bb-editable [contenteditable="true"]:hover,
|
||||
.skin-comms .bb-editable [contenteditable="true"]:hover {
|
||||
padding: 0 0.2rem;
|
||||
white-space: normal;
|
||||
font-size: 1.8rem;
|
||||
font-style: normal;
|
||||
color: #000;
|
||||
background: none repeat scroll 0 0 transparent;
|
||||
}
|
||||
|
||||
.skin-comms .bb-editable [contenteditable]:hover,
|
||||
.skin-comms .bb-editable [contenteditable]:active {
|
||||
color: #fff;
|
||||
background-color: #29bbb5;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9"><path fill="#00AAC5" enable-background="new" d="M9 0L9 9 0 9"/></svg>
|
||||
|
After Width: | Height: | Size: 148 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9"><path fill="#B2F2FF" d="M9 0v9h-9"/></svg>
|
||||
|
After Width: | Height: | Size: 121 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9"><path opacity=".5" fill="#A9A9A9" d="M9 0L9 9 0 9"/></svg>
|
||||
|
After Width: | Height: | Size: 137 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9"><path opacity=".5" fill="#A9A9A9" enable-background="new" d="M0 0L0 9 9 9"/></svg>
|
||||
|
After Width: | Height: | Size: 161 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9"><path fill="#00AAC5" enable-background="new" d="M0 0L0 9 9 9"/></svg>
|
||||
|
After Width: | Height: | Size: 148 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><path fill-rule="evenodd" clip-rule="evenodd" fill="#424242" d="M23.385 21.494c.266.266.057.903-.465 1.426s-1.16.729-1.426.465l-4.041-4.039c-1.01.656-2.21 1.047-3.505 1.047-3.561 0-6.448-2.885-6.448-6.446 0-3.56 2.887-6.446 6.448-6.446 3.562 0 6.449 2.887 6.449 6.446 0 1.297-.393 2.499-1.051 3.509l4.041 4.039h-.002zm-9.437-12.459c-2.712 0-4.914 2.198-4.914 4.912s2.202 4.913 4.914 4.913c2.714 0 4.914-2.199 4.914-4.913-.001-2.714-2.2-4.912-4.914-4.912z"/></svg>
|
||||
|
After Width: | Height: | Size: 546 B |