diff --git a/src/UI/Components/Button.js b/src/UI/Components/Button.js
new file mode 100644
index 00000000..662652db
--- /dev/null
+++ b/src/UI/Components/Button.js
@@ -0,0 +1,18 @@
+export default class Button extends Component {
+ static PROPERTIES = {
+ label_key: {
+ description: 'The key to use to look up the label for the button',
+ },
+ click: {
+ description: 'The function to call when the button is clicked',
+ },
+ }
+
+ create_template ({ template }) {
+ $(template).html(`
+
+ `);
+ }
+}
\ No newline at end of file
diff --git a/src/UI/Components/CodeEntryView.js b/src/UI/Components/CodeEntryView.js
new file mode 100644
index 00000000..d016de1f
--- /dev/null
+++ b/src/UI/Components/CodeEntryView.js
@@ -0,0 +1,183 @@
+import { Component } from "../../util/Component.js";
+
+export default class CodeEntryView extends Component {
+ static PROPERTIES = {
+ value: {},
+ is_checking_code: {},
+ }
+
+ static RENDER_MODE = Component.NO_SHADOW;
+
+ static CSS = /*css*/`
+ .wrapper {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ color: #3e5362;
+ }
+
+ .digit-input {
+ box-sizing: border-box;
+ width: 12.89%;
+ height: 50px;
+ font-size: 25px;
+ text-align: center;
+ border-radius: 0.5rem;
+ -moz-appearance: textfield;
+ border: 2px solid #9b9b9b;
+ color: #485660;
+ }
+
+ .digit-input::-webkit-outer-spin-button,
+ .digit-input::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ .confirm-code-hyphen {
+ display: inline-block;
+ width: 14%;
+ text-align: center;
+ font-size: 40px;
+ font-weight: 300;
+ }
+ `
+
+ create_template ({ template }) {
+ // TODO: static member for strings
+ const submit_btn_txt = i18n('confirm_code_generic_submit');
+
+ $(template).html(/*html*/`
+
+ `);
+ }
+
+ on_ready ({ listen }) {
+ let is_checking_code = false;
+
+ $(this.dom_).find('.digit-input').first().focus();
+
+ $(this.dom_).find('.code-confirm-btn').on('click submit', function(e){
+ e.preventDefault();
+ e.stopPropagation();
+
+ $(this.dom_).find('.code-confirm-btn').prop('disabled', true);
+ $(this.dom_).find('.error').hide();
+
+ // Check if already checking code to prevent multiple requests
+ if(is_checking_code)
+ return;
+ // Confirm button
+ is_checking_code = true;
+
+ // set animation
+ $(this.dom_).find('.code-confirm-btn').html(``);
+ })
+
+ // Elements
+ const numberCodeForm = this.dom_.querySelector('[data-number-code-form]');
+ const numberCodeInputs = [...numberCodeForm.querySelectorAll('[data-number-code-input]')];
+
+ // Event listeners
+ numberCodeForm.addEventListener('input', ({ target }) => {
+ if(!target.value.length) { return target.value = null; }
+ const inputLength = target.value.length;
+ let currentIndex = Number(target.dataset.numberCodeInput);
+ if(inputLength === 2){
+ const inputValues = target.value.split('');
+ target.value = inputValues[0];
+ }
+ else if (inputLength > 1) {
+ const inputValues = target.value.split('');
+
+ inputValues.forEach((value, valueIndex) => {
+ const nextValueIndex = currentIndex + valueIndex;
+
+ if (nextValueIndex >= numberCodeInputs.length) { return; }
+
+ numberCodeInputs[nextValueIndex].value = value;
+ });
+ currentIndex += inputValues.length - 2;
+ }
+
+ const nextIndex = currentIndex + 1;
+
+ if (nextIndex < numberCodeInputs.length) {
+ numberCodeInputs[nextIndex].focus();
+ }
+
+ // Concatenate all inputs into one string to create the final code
+ let current_code = '';
+ for(let i=0; i< numberCodeInputs.length; i++){
+ current_code += numberCodeInputs[i].value;
+ }
+ this.set('value', current_code);
+
+ // Automatically submit if 6 digits entered
+ if(current_code.length === 6){
+ $(this.dom_).find('.code-confirm-btn').prop('disabled', false);
+ $(this.dom_).find('.code-confirm-btn').trigger('click');
+ }
+ });
+
+ numberCodeForm.addEventListener('keydown', (e) => {
+ const { code, target } = e;
+
+ const currentIndex = Number(target.dataset.numberCodeInput);
+ const previousIndex = currentIndex - 1;
+ const nextIndex = currentIndex + 1;
+
+ const hasPreviousIndex = previousIndex >= 0;
+ const hasNextIndex = nextIndex <= numberCodeInputs.length - 1
+
+ switch (code) {
+ case 'ArrowLeft':
+ case 'ArrowUp':
+ if (hasPreviousIndex) {
+ numberCodeInputs[previousIndex].focus();
+ }
+ e.preventDefault();
+ break;
+
+ case 'ArrowRight':
+ case 'ArrowDown':
+ if (hasNextIndex) {
+ numberCodeInputs[nextIndex].focus();
+ }
+ e.preventDefault();
+ break;
+ case 'Backspace':
+ if (!e.target.value.length && hasPreviousIndex) {
+ numberCodeInputs[previousIndex].value = null;
+ numberCodeInputs[previousIndex].focus();
+ }
+ break;
+ default:
+ break;
+ }
+ });
+ }
+}
+
+// TODO: This is necessary because files can be loaded from
+// both `/src/UI` and `/UI` in the URL; we need to fix that
+if ( ! window.__component_codeEntryView ) {
+ window.__component_codeEntryView = true;
+
+ customElements.define('c-code-entry-view', CodeEntryView);
+}
diff --git a/src/UI/UIWindow2FASetup.js b/src/UI/UIWindow2FASetup.js
index a9630106..45286b22 100644
--- a/src/UI/UIWindow2FASetup.js
+++ b/src/UI/UIWindow2FASetup.js
@@ -19,6 +19,7 @@
*/
+import CodeEntryView from "./Components/CodeEntryView.js";
import Flexer from "./Components/Flexer.js";
import QRCodeView from "./Components/QRCode.js";
import UIComponentWindow from "./UIComponentWindow.js";
@@ -38,7 +39,12 @@ const UIWindow2FASetup = async function UIWindow2FASetup () {
children: [
new QRCodeView({
value: data.url,
- })
+ }),
+ new CodeEntryView({
+ on_update () {
+ // NEXT
+ }
+ }),
]
});
diff --git a/src/util/Component.js b/src/util/Component.js
index eae4833a..77579610 100644
--- a/src/util/Component.js
+++ b/src/util/Component.js
@@ -1,10 +1,17 @@
import ValueHolder from "./ValueHolder.js";
export class Component extends HTMLElement {
+ // Render modes
+ static NO_SHADOW = Symbol('no-shadow');
+
constructor (property_values) {
super();
- this.dom_ = this.attachShadow({ mode: 'open' });
+ if ( this.constructor.RENDER_MODE === Component.NO_SHADOW ) {
+ this.dom_ = this;
+ } else {
+ this.dom_ = this.attachShadow({ mode: 'open' });
+ }
this.values_ = {};
@@ -26,6 +33,10 @@ export class Component extends HTMLElement {
return this.values_[key].get();
}
+ set (key, value) {
+ this.values_[key].set(value);
+ }
+
connectedCallback () {
console.log('connectedCallback called')
this.on_ready && this.on_ready(this.get_api_());