[BIGUPDATE] [lib.field-validator] added generic field validator with error message generation + is_valid() checker [popup] refactored all

This commit is contained in:
xdrm-brackets 2018-03-29 01:26:18 +02:00
parent a494f2a45e
commit 236b03775e
15 changed files with 348 additions and 276 deletions

View File

@ -41,10 +41,8 @@ export default class ChannelController{
/* (4) Update buffer */
this._buffer = {};
for( let c of this.list )
if( c.id === this.current ){
console.warn(c);
if( c.id === this.current )
this._buffer = c;
}
/* (5) Load rooms */
gs.get.room.fetch();
@ -203,11 +201,11 @@ export default class ChannelController{
create(name=null, link=null){
/* (1) Manage invalid @link */
if( typeof link !== 'string' || /^[a-z0-9\._-]$/i.test(link) )
if( typeof link !== 'string' || /^[a-z0-9-]$/i.test(link) )
return false;
/* (2) Manage invalid @name */
if( typeof name !== 'string' || /^[a-z0-9\._-]$/i.test(name) )
if( typeof name !== 'string' || !/^[a-z0-9\/_-]{3,}$/i.test(name) )
return false;
/* (3) Try to create room in API */

View File

@ -0,0 +1,36 @@
/* (1) Init all
---------------------------------------------------------*/
/* (1) Import field validator */
const fv = require('./field-validator.js');
/* (2) Create class easy access */
window.FieldValidator = fv.Class;
/* (1) Create basic validators
---------------------------------------------------------*/
/* (1) BYPASS */
FieldValidator.pushFormat('bypass', () => true);
/* (2) Basic name */
FieldValidator.pushFormat('basic-name', (input) => {
return typeof input === 'string' && /^[a-z0-9 _-]{3,20}$/i.test(input);
}, '3 characters required: letters, numbers, spaces, dots, hyphens');
/* (3) URL name */
FieldValidator.pushFormat('url-name', (input) => {
return typeof input === 'string' && /^[a-z0-9_-]{3,20}$/i.test(input);
}, '3 characters required: letters, numbers, dots, hyphens');
/* (4) Password */
FieldValidator.pushFormat('password', (input) => {
return typeof input === 'string' && /^[^<>\/\\]{8,50}$/.test(input);
}, '8 characters required');
/* (5) Room type */
FieldValidator.pushFormat('room.type', (input) => {
return typeof input === 'string' && ['text', 'voice'].indexOf(input) > -1;
});

View File

@ -0,0 +1,116 @@
export const validatorStack = {};
export const inputStack = new WeakMap();
export class Class{
static get _validators(){ return validatorStack; }
/* (1) Push a new Format
*
* @_format<String> Format name
* @_validator<Function> Format callback
* @_error<String> Error message
*
* @return pushed<bool> Whether the validator has been pushed
*
---------------------------------------------------------*/
static pushFormat(_format=null, _validator=null, _error='error'){
/* (1) Error: invalid _format */
if( typeof _format !== 'string' )
return false;
/* (2) Error: invalid _validator */
if( !(_validator instanceof Function) )
return false;
/* (3) Store validator */
Class._validators[_format] = { error: _error, validator: _validator };
return true;
}
/* (1) Pop an existing Format
*
* @_format<String> Format name
*
* @return popped<bool> Whether the validator has been popped
*
---------------------------------------------------------*/
static popFormat(_format=null){
/* (1) Error: invalid _format */
if( typeof _format !== 'string' )
return false;
/* (2) Error: _validator not found */
if( Class._validators[_format] == null )
return false;
/* (3) Remove validator */
delete Class._validators[_format];
return true;
}
/* (2) Builds an validat-able input
*
* @_format<String> Existing validator name
* @_default<mixed> [OPT] Mutable input default value (default to NULL)
*
---------------------------------------------------------*/
constructor(_format, _default=null){
/* (1) Store fields (default value) */
this.mutable = _default;
/* (2) Store _format in STATIC inputStack */
inputStack.set(this, { format: _format });
}
/* (3) Checks if the _mutable has a current valid value
*
* @return valid<bool> Whether the _mutable is valid or not
*
---------------------------------------------------------*/
is_valid(){
// 1. Extract validator name
let format = inputStack.get(this).format;
// 2. Dispatch validation
return validatorStack[format].validator( this.mutable );
}
/* (3) Get current error message
*
* @return NULL if is_valid()
*
---------------------------------------------------------*/
get error(){
// 1. NULL if valid
if( this.is_valid() )
return '';
// 2. Extract validator name
let format = inputStack.get(this).format;
// 3. Dispatch validation
return validatorStack[format].error;
}
}

View File

@ -22,58 +22,115 @@ export default class PopupController{
---------------------------------------------------------*/
/* (1) Create a new Room */
this.register('room.create', {
data: {
type: 'text',
name: ''
type: new FieldValidator('room.type', 'text'),
name: new FieldValidator('basic-name', ''),
reset(){
this.type.mutable = 'text';
this.name.mutable = '';
},
reset(){ this.data.type = 'text'; this.data.name = ''; },
submit(){ gs.get.room.create(this.data.type, this.data.name) && this.parent.hide(); }
submit(){
// validators
if( !this.name.is_valid() )
return;
if( gs.get.room.create(this.type.mutable, this.name.mutable) )
return this.parent.hide();
}
});
/* (2) Create a new Channel */
this.register('channel.create', {
data: {
name: '',
link: ''
name: new FieldValidator('basic-name', ''),
link: new FieldValidator('url-name', ''),
reset(){
this.link.mutable = '',
this.name.mutable = '';
},
reset(){ this.data.link = '', this.data.name = ''; },
submit(){ gs.get.channel.create(this.data.name, this.data.link) && this.parent.hide(); }
submit(){
// validators
if( !this.name.is_valid() )
return false;
if( !this.link.is_valid() )
return false;
if( gs.get.channel.create(this.name.mutable, this.link.mutable) )
return this.parent.hide();
}
});
/* (3) Change nickname */
this.register('nickname.change', {
data: {
value: ''
},
reset(){ this.data.value = ''; },
submit(){ gs.get.content.change_username(this.data.value) && this.parent.hide(); }
/* (3) Change username */
this.register('username.change', {
username: new FieldValidator('basic-name', ''),
reset(){ this.username.mutable = ''; },
submit(){
// validators
if( !this.username.is_valid() )
return false;
if( gs.get.content.change_username(this.username.mutable) )
this.parent.hide();
}
});
/* (4) Invite to channel */
this.register('channel.invite', {
data: {
username: ''
},
reset(){ this.data.username = ''; },
submit(){ gs.get.channel.invite(this.data.username) && this.parent.hide(); }
username: new FieldValidator('basic-name', ''),
reset(){ this.username.mutable = ''; },
submit(){
// validators
if( !this.username.is_valid() )
return false;
if( gs.get.channel.invite(this.username.mutable) )
return this.parent.hide();
}
});
/* (5) Remove channel */
this.register('channel.remove', {
data: {},
reset(){ },
submit(){ gs.get.channel.remove() && this.parent.hide(); }
});
/* (6) Leave channel */
this.register('channel.leave', {
data: {},
reset(){ },
submit(){ gs.get.channel.remove() && this.parent.hide(); }
});
/* (6) Change password */
this.register('password.change', {
data: {
password: '',
confirm: ''
},
reset(){ this.data.password = ''; this.data.confirm = ''; },
submit(){ this.data.password === this.data.confirm && gs.get.content.change_password(this.data.password) && this.parent.hide(); }
password: new FieldValidator('password', ''),
confirm: new FieldValidator('password', ''),
matches: true,
reset(){ this.password.mutable = ''; this.confirm.mutable = ''; },
submit(){
this.matches = this.password.mutable === this.confirm.mutable;
// check passwords matches
if( !this.matches )
return false;
// field validator
if( !this.password.is_valid || !this.confirm.is_valid() )
return false;
if( gs.get.content.change_password(this.data.password.mutable) )
return this.parent.hide();
}
});

View File

@ -223,7 +223,7 @@ export default class RoomController{
return false;
/* (2) Manage invalid @name */
if( typeof name !== 'string' )
if( typeof name !== 'string' || !/^[a-z0-9\/_-]{3,}$/i.test(name) )
return false;
/* (3) Try to create room in API */

View File

@ -3,23 +3,15 @@
/* (1) Default data structure */
gs.set('login', {
// fields
username: {
model: '',
timeout: '',
error: '',
validate: (_username) => /^[a-z0-9_-]{3,20}$/i.test(_username)
},
username: new FieldValidator('basic-name', ''),
password: new FieldValidator('password', ''),
// login failed
failed: false,
password: {
model: '',
timeout: '',
error: '',
validate: (_password) => /^[^<>\/\\]{8,50}$/.test(_password)
},
// functions
func: {
print_err(_field, _message, _duration){},
login(){},
forgot_pass(){},
press_enter(){}
@ -30,93 +22,28 @@ gs.set('login', {
/* (2) Manage error messages
*
* @_field<String> Field to print errors to
* @_message<String> Error message to print
* @_duration<int> Durations (sec) for the message to be displayed
*
---------------------------------------------------------*/
gs.get.login.func.print_err = function(_field, _message='error', _duration=null){
/* (1) Fail: invalid _field */
if( typeof _field !== 'string' || this[_field] == null )
return false;
/* (2) Fail: invalid _message */
if( typeof _message !== 'string' )
return false;
/* (3) Fail: invalid _message */
if( isNaN(_duration) )
return false;
/* (4) Clear timeout if exists */
!isNaN(this[_field].err_to) && clearTimeout(this[_field].err_to);
/* (5) Display error */
this[_field].error = _message;
/* (6) No timeout if _duration if null */
if( _duration === null )
return true;
/* (7) Setup Timeout */
this[_field].err_to = setTimeout( function(){
this.error = '';
}.bind(this[_field]), _duration*1000);
return true;
}.bind(gs.get.login);
/* (3) Login attempt
/* (2) Login attempt
*
---------------------------------------------------------*/
gs.get.login.func.login = function(){
/* (1) Cache fields' values */
let username = this.username.model;
let password = this.password.model;
let username = this.username.mutable;
let password = this.password.mutable;
/* (2) Manage errors */
let errors = false;
if( !this.username.is_valid() )
return false;
// username error
if( !this.username.validate(username) ){
errors = true;
this.func.print_err('username', '3 characters are required: letters, numbers, dot');
}else
this.func.print_err('username', '', 0);
// password error
if( !this.password.validate(password) ){
errors = true;
this.func.print_err('password', '8 characters are required');
}else
this.func.print_err('password', '', 0);
// if errors -> fail
if( errors )
if( !this.password.is_valid() )
return false;
/* (3) API bindings */
api.call('GET /user/token', {}, function(rs){
// manage error
if( rs.error !== 0 || rs.token == null ){
this.func.print_err('username', 'Invalid combination');
this.func.print_err('password', 'Invalid combination');
return;
}
if( rs.error !== 0 || rs.token == null )
return this.failed = true;
// store TOKEN + user data
auth.token = rs.token;

View File

@ -3,31 +3,12 @@
/* (1) Default data structure */
gs.set('register', {
// fields
mail: {
model: '',
timeout: '',
error: '',
validate: (_mail) => true
// validate: (_mail) => /^[\w\.]+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$/i.test(_mail)
},
username: {
model: '',
timeout: '',
error: '',
validate: (_username) => /^[a-z0-9_-]{3,20}$/i.test(_username)
},
password: {
model: '',
timeout: '',
error: '',
validate: (_password) => /^[^<>\/\\]{8,50}$/.test(_password)
},
mail: new FieldValidator('bypass', ''),
username: new FieldValidator('basic-name', ''),
password: new FieldValidator('password', ''),
// functions
func: {
print_err(_field, _message, _duration){},
register(){},
press_enter(){}
}
@ -37,90 +18,30 @@ gs.set('register', {
/* (2) Manage error messages
*
* @_field<String> Field to print errors to
* @_message<String> Error message to print
* @_duration<int> Durations (sec) for the message to be displayed
*
---------------------------------------------------------*/
gs.get.register.func.print_err = function(_field, _message='error', _duration=null){
/* (1) Fail: invalid _field */
if( typeof _field !== 'string' || this[_field] == null )
return false;
/* (2) Fail: invalid _message */
if( typeof _message !== 'string' )
return false;
/* (3) Fail: invalid _message */
if( isNaN(_duration) )
return false;
/* (4) Clear timeout if exists */
!isNaN(this[_field].err_to) && clearTimeout(this[_field].err_to);
/* (5) Display error */
this[_field].error = _message;
/* (6) No timeout if _duration if null */
if( _duration === null )
return true;
/* (7) Setup Timeout */
this[_field].err_to = setTimeout( function(){
this.error = '';
}.bind(this[_field]), _duration*1000);
return true;
}.bind(gs.get.register);
/* (3) Login attempt
/* (2) Login attempt
*
---------------------------------------------------------*/
gs.get.register.func.register = function(){
/* (1) Cache fields' values */
let mail = this.mail.model;
let username = this.username.model;
let password = this.password.model;
let mail = this.mail.mutable;
let username = this.username.mutable;
let password = this.password.mutable;
/* (2) Manage errors */
let errors = false;
// mail error
if( !this.mail.validate(mail) ){
errors = true;
this.func.print_err('mail', 'This field is required');
}else
this.func.print_err('mail', '', 0);
if( !this.mail.is_valid() )
return false;
// username error
if( !this.username.validate(username) ){
errors = true;
this.func.print_err('username', '3 characters are required: letters, numbers, dot');
}else
this.func.print_err('username', '', 0);
if( !this.username.is_valid() )
return false;
// password error
if( !this.password.validate(password) ){
errors = true;
this.func.print_err('password', '8 characters are required');
}else
this.func.print_err('password', '', 0);
// if errors -> fail
if( errors )
if( !this.password.is_valid() )
return false;
/* (3) API bindings */

View File

@ -14,6 +14,25 @@
font-size: .7em;
text-transform: uppercase;
letter-spacing: 1px;
& > span{
color: #f04747;
text-transform: none;
&:before{ content: '(' attr(data-err) ')'; }
&:not([data-err]),
&[data-err='']{
color: inherit;
&:before{ content: ''; }
}
}
}
// input
@ -148,7 +167,7 @@
&:active{ background-color: darken($main, 10%);}
&.invalid{
$main: #e65835;
$main: #f04747;
background-color: $main;
&:hover{ background-color: darken($main, 5%);}
&:active{ background-color: darken($main, 10%);}

View File

@ -139,23 +139,8 @@ body > #WRAPPER.login{
margin-top: 1.2em;
letter-spacing: .2em;
& > span{
color: inherit;
text-transform: none;
&:before{ content: ''; }
&:after{ content: ''; }
}
&.err{
&.err > span{
color: #f04747;
& > span{
&:before{ content: '('; }
&:after{ content: ')'; }
}
}
&.link{

View File

@ -72,6 +72,11 @@
text-transform: uppercase;
letter-spacing: 1px;
strong{
color: $main;
text-transform: none;
}
}
@ -106,6 +111,10 @@
}
}
strong{
color: darken($main,5%);
}
}

View File

@ -9,22 +9,23 @@ import XHRClientDriver from './lib/client/xhr.js'
import WebSocketClientDriver from './lib/client/ws.js'
import APIClient from './lib/api-client.js'
/* (1) Custom lib accessors
---------------------------------------------------------*/
/* (1) Global Store for Vue */
/* (1) Field validation */
require('./lib/field-manager.js');
/* (2) Global Store for Vue */
window.gs = new GlobalStore();
/* (2) Authentication token management */
/* (3) Authentication token management */
window.auth = new Authentication();
gs.set('auth', auth);
/* (3) XHR / WebSocket drivers */
/* (4) XHR / WebSocket drivers */
window.xhrcd = XHRClientDriver;
window.wscd = WebSocketClientDriver;
/* (4) ClientDriver instances */
/* (5) ClientDriver instances */
window.api = new APIClient('api.douscord.xdrm.io');
window.ws = new WebSocketClientDriver('ws.douscord.xdrm.io');

View File

@ -104,7 +104,7 @@
<span data-icon='create' @click='gs.popup.show(`channel.create`); minipop=false'>Create channel</span>
<span class ='invalid-h' data-icon='remove' @click='gs.popup.show(`channel.remove`); minipop=false'>Remove channel</span>
<span data-icon='category' @click='gs.popup.show(`room.create`); minipop=false'>Create room</span>
<span data-icon='edit' @click='gs.popup.show(`nickname.change`); minipop=false'>Change nickname</span>
<span data-icon='edit' @click='gs.popup.show(`username.change`); minipop=false'>Change nickname</span>
<span data-icon='password' @click='gs.popup.show(`password.change`); minipop=false'>Change password</span>
<span class='sb invalid-h' data-icon='leave' @click='gs.popup.show(`channel.leave`); minipop=false'>Leave channel</span>
<span class='sb invalid' data-icon='logout' @click='gs.auth.token=null; gs.refresh()'>Logout</span>

View File

@ -18,15 +18,15 @@
<!-- Pop-up ROOM CREATE -->
<div class='popup' v-show='gs.popup.get(`room.create`).active'>
<span class='header'>Create {{ gs.popup.get(`room.create`).data.type }} room</span>
<span class='header'>Create {{ gs.popup.get(`room.create`).type.mutable }} room</span>
<span class='body form'>
<label for='channel_name'>Room Name</label>
<input type='text' name='channel_name' v-model='gs.popup.get(`room.create`).data.name'>
<label>Room Name <span :data-err='gs.popup.get(`room.create`).name.error'></span></label>
<input type='text' v-model='gs.popup.get(`room.create`).name.mutable'>
<label for='channel_name'>Room Type</label>
<span class='select-box' @click='gs.popup.get(`room.create`).data.type=`text`' :data-selected='gs.popup.get(`room.create`).data.type==`text`?1:0' data-type='text'>Text Room</span>
<span class='select-box' @click='gs.popup.get(`room.create`).data.type=`voice`' :data-selected='gs.popup.get(`room.create`).data.type==`voice`?1:0' data-type='voice'>Voice Room</span>
<label>Room Type</label>
<span class='select-box' @click='gs.popup.get(`room.create`).type.mutable=`text`' :data-selected='gs.popup.get(`room.create`).type.mutable==`text`?1:0' data-type='text'>Text Room</span>
<span class='select-box' @click='gs.popup.get(`room.create`).type.mutable=`voice`' :data-selected='gs.popup.get(`room.create`).type.mutable==`voice`?1:0' data-type='voice'>Voice Room</span>
</span>
<span class='footer form'>
@ -37,13 +37,14 @@
<!-- Pop-up CHANNEL CREATE -->
<div class='popup' v-show='gs.popup.get(`channel.create`).active'>
<span class='header'>Create {{ gs.popup.get(`channel.create`).data.type }} channel</span>
<span class='header'>Create channel #<strong>{{ gs.popup.get(`channel.create`).name.mutable }}</strong><br>url: <strong>/{{gs.popup.get(`channel.create`).link.mutable}}</strong></span>
<span class='body form'>
<label for='channel_name'>Channel Name</label>
<input type='text' name='channel_name' v-model='gs.popup.get(`channel.create`).data.name'>
<label for='channel_link'>Channel Link</label>
<input type='text' name='channel_link' v-model='gs.popup.get(`channel.create`).data.link'>
<label>Channel Name <span :data-err='gs.popup.get(`channel.create`).name.error'></span></label>
<input type='text' v-model='gs.popup.get(`channel.create`).name.mutable'>
<label>Channel Link <span :data-err='gs.popup.get(`channel.create`).link.error'></span></label>
<input type='text' v-model='gs.popup.get(`channel.create`).link.mutable'>
</span>
<span class='footer form'>
@ -53,14 +54,14 @@
</div>
<!-- Pop-up NICKNAME CHANGE -->
<div class='popup' v-show='gs.popup.get(`nickname.change`).active'>
<div class='popup' v-show='gs.popup.get(`username.change`).active'>
<span class='header'>Change nickname</span>
<span class='body form'>
<label for='nickname'>Nickname</label>
<input type='text' name='nickname' v-model='gs.popup.get(`nickname.change`).data.value' :placeholder='gs.auth.user.username'>
<label for='username'>Nickname <span :data-err='gs.popup.get(`username.change`).username.error'></span></label>
<input type='text' name='username' v-model='gs.popup.get(`username.change`).username.mutable' :placeholder='gs.auth.user.username'>
<a @click='gs.popup.get(`nickname.change`).reset()'>Reset Nickname</a>
<a @click='gs.popup.get(`username.change`).reset()'>Reset Nickname</a>
</span>
<span class='footer form'>
@ -74,8 +75,8 @@
<span class='header'>Invite a friend</span>
<span class='body form'>
<label for='username'>friend's username</label>
<input type='text' name='username' v-model='gs.popup.get(`channel.invite`).data.username'>
<label for='username'>friend's username <span :data-err='gs.popup.get(`channel.invite`).username.error'></span></label>
<input type='text' name='username' v-model='gs.popup.get(`channel.invite`).username.mutable'>
</span>
<span class='footer form'>
@ -89,7 +90,7 @@
<span class='header'>Remove channel</span>
<span class='body form'>
<p>You are about to remove the channel #<b>{{ gs.content.cbuf.label }}</b>, this operation cannot be undone.</p>
<p>You are about to remove the channel #<strong>{{ gs.content.cbuf.label }}</strong>, this operation cannot be undone.</p>
</span>
<span class='footer form'>
@ -103,7 +104,7 @@
<span class='header'>Leave channel</span>
<span class='body form'>
<p>You are about to leave the channel #<b>{{ gs.content.cbuf.label }}</b>, this operation cannot be undone.</p>
<p>You are about to leave the channel #<strong>{{ gs.content.cbuf.label }}</strong>, this operation cannot be undone.</p>
</span>
<span class='footer form'>
@ -117,11 +118,12 @@
<span class='header'>Change password</span>
<span class='body form'>
<label for='password'>New password</label>
<input type='password' name='password' v-model='gs.popup.get(`password.change`).data.password'>
<label v-show='!gs.popup.get(`password.change`).matches'><span data-err='passwords does not match'></span></label>
<label>New password <span :data-err='gs.popup.get(`password.change`).password.error'></span></label>
<input type='password' v-model='gs.popup.get(`password.change`).password.mutable'>
<label for='password'>Confirmation</label>
<input type='password' name='password' v-model='gs.popup.get(`password.change`).data.confirm'>
<label>Confirmation <span :data-err='gs.popup.get(`password.change`).confirm.error'></span></label>
<input type='password' v-model='gs.popup.get(`password.change`).confirm.mutable'>
</span>
<span class='footer form'>

View File

@ -6,10 +6,11 @@
<div class='form'>
<h3>Welcome back!</h3>
<label for='username' :class='gs.login.username.error.length<1?``:`err`'>USERNAME <span>{{ gs.login.username.error }}</span></label>
<input @keyup='gs.login.func.press_enter' type='text' name='username' v-model='gs.login.username.model' class='flat' autofocus>
<label for='password' :class='gs.login.password.error.length<1?``:`err`'>PASSWORD <span>{{ gs.login.password.error }}</span></label>
<input @keyup='gs.login.func.press_enter' type='password' name='password' v-model='gs.login.password.model' class='flat'>
<label v-show='gs.login.failed' class='err'><span>Invalid Combination</span></label>
<label>USERNAME <span :data-err='gs.login.username.error'></span></label>
<input @keyup='gs.login.func.press_enter' type='text' v-model='gs.login.username.mutable' class='flat' autofocus>
<label>PASSWORD <span :data-err='gs.login.password.error'></span></label>
<input @keyup='gs.login.func.press_enter' type='password' v-model='gs.login.password.mutable' class='flat'>
<!-- <label for='fpass' class='link' @click='gs.login.func.forgot_pass()'>FORGOT YOUR PASSWORD ?</label> -->
<button class='submit' @click='gs.login.func.login()'>Login</button>

View File

@ -6,12 +6,12 @@
<div class='form'>
<h3>Create an account</h3>
<label for='mail' :class='gs.register.mail.error.length<1?``:`err`'>EMAIL <span>{{ gs.register.mail.error }}</span></label>
<input @keyup='gs.register.func.press_enter' type='email' name='mail' v-model='gs.register.mail.model' class='flat' autofocus>
<label for='username' :class='gs.register.username.error.length<1?``:`err`'>USERNAME <span>{{ gs.register.username.error }}</span></label>
<input @keyup='gs.register.func.press_enter' type='text' name='username' v-model='gs.register.username.model' class='flat'>
<label for='password' :class='gs.register.password.error.length<1?``:`err`'>PASSWORD <span>{{ gs.register.password.error }}</span></label>
<input @keyup='gs.register.func.press_enter' type='password' name='password' v-model='gs.register.password.model' class='flat'>
<label>EMAIL <span :data-err='gs.register.mail.error'></span></label>
<input @keyup='gs.register.func.press_enter' type='email' v-model='gs.register.mail.mutable' class='flat' autofocus>
<label>USERNAME <span :data-err='gs.register.username.error'></span></label>
<input @keyup='gs.register.func.press_enter' type='text' v-model='gs.register.username.mutable' class='flat'>
<label>PASSWORD <span :data-err='gs.register.password.error'></span></label>
<input @keyup='gs.register.func.press_enter' type='password' v-model='gs.register.password.mutable' class='flat'>
<button class='submit' @click='gs.register.func.register()'>Continue</button>
<span>By registering, you agree to Discord's <a href='https://discordapp.com/terms'>Terms of Service</a> and <a href='https://discordapp.com/privacy'>Privacy Policy</a></span>