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(`circle anim`); + }) + + // 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_());