Algo bastante sencillo y que puede sernos útil es detectar los parámetros que se envían por URL para así modificar el comportamiento de nuestro Javascript, por ejemplo si se envía un parámetro o tiene cierto valor, se podría cargar un objeto o implementar una función.
El script sería el siguiente:
// Obtenemos la URL
var url = document.location.href;
// Nos quedamos con los parámetros
url = url.substring(url.lastIndexOf('?')+1);
// Dividimos los distintos parámetros
url = url.split('&');
// Almacenamos los parámetros en un array(param => valor)
var res = new Array();
for(var i=0; i
Si nuestra URL es amigable, no hay parámetros sino que se indican en la propia URL (http://servidor/metodo/accion/parametro/parametro), deberíamos hacer algo así:
//Obtenemos la Query String (URL - host)
var url = document.location.href;
url = url.substring(url.lastIndexOf(document.location.host)+1);
// Separamos mediante la barra (/)
var res = url.split('/');
Como se puede apreciar es muy sencillo, pero nos puede ser muy útil, sobre todo si queremos ganar en rendimiento y no cargar todo el js, que a veces no está muy optimizado y en todas las páginas se carga cuando a veces no sería necesario.
Ya en varios sitios se ha mostrado como realizar la navegación mediante el teclado, esto significa usar las teclas para acceder a contenidos o para realizar acciones.
Nosotros queremos darle un toque de automatización, que tan solo sea necesario añadir un enlace interno en el documento para acceder a la zona del documento.
Crearemos un script que obtenga todos los enlaces internos del documento y si tienen el atributo rel y su valor tiene el formato “formato:[letra]“, se considerará un objetivo para la navegación por teclado. La letra será la que se usará para acceder y pueden repetirse, por lo que si se repiten las teclas para el shortcut, se irán alternando uno a uno los distintos enlaces.
var __SHORTCUTS__ = {
// Guarda los shortcuts con los enlaces
shortcuts: new Array(),
// Ãndice del enlace actual organizado por shortcuts
idx_shortcuts: new Array(),
// Obtiene todos los enlaces internos y los que corresponden a un shortcut los almacena
leer_shortcuts: function(e) {
var objs = document.getElementsByTagName("A");
for (var i=0; i<objs.length; i++) {
var rel = objs[i].rel;
if (rel && rel.match(/shortcut:[a-z]/i) && objs[i].name) {
var tipo = (rel.substring(rel.indexOf(":")+1)+"").toUpperCase().substring(0, 1);
if (!__SHORTCUTS__.shortcuts[tipo]) {
__SHORTCUTS__.shortcuts[tipo] = new Array();
__SHORTCUTS__.idx_shortcuts[tipo] = 0;
}
__SHORTCUTS__.shortcuts[tipo][__SHORTCUTS__.shortcuts[tipo].length] = objs[i];
}
}
// Capturo el evento de pulsado de teclado en el document
document.onkeydown = __SHORTCUTS__.controlar_keypress;
},
// Captura una tecla pulsada y accede al shortcut que tenga asociado
controlar_keypress: function(e) {
try {
if (!e) e = event;
var key = e.keyCode;
// Obtengo el caracter correspondiente, quizás esto falle, no he hecho pruebas suficientes
var c = String.fromCharCode(key);
// Si es una letra
if (c.match(/^[a-z]$/i)) {
// Obtengo el enlace para el shortcut
var obj = __SHORTCUTS__.shortcuts[c.toUpperCase()][__SHORTCUTS__.idx_shortcuts[c.toUpperCase()]];
// Acceso a esa parte del documento
document.location = "#"+obj.name;
// Incremento el indice para que acceda al siguiente
__SHORTCUTS__.idx_shortcuts[c.toUpperCase()] = (__SHORTCUTS__.idx_shortcuts[c.toUpperCase()]+1)%__SHORTCUTS__.shortcuts[c.toUpperCase()].length;
}
} catch (e) {} // En IE me da un error que no llego a comprender, ¡cómo no!
}
}
// Cargo el proceso en el onload
if (window.addEventListener) {
window.addEventListener("load", function(event) {__SHORTCUTS__.leer_shortcuts(event);}, false);
} else if (window.attachEvent) {
window.attachEvent("onload", function(event) {__SHORTCUTS__.leer_shortcuts(event);});
} else {
document.onload = function(event) {__SHORTCUTS__.leer_shortcuts(event);}
}
Algo que hay que tener en cuenta muchas veces cuando trabajamos con formularios es la longitud del texto que introducimos en los textarea, ya que en muchos casos esa información se almacena en la BD y puede que el campo en dónde se guarda tenga un lÃmite de tamaño.
La barra de progreso funciona de la siguiente manera, el fondo es una imagen con el progreso, inicialmente estará desplazada a la izquierda el tamaño de la barra. Por ejemplo, si la barra tiene un ancho de 300px, la posición izquierda del fondo será -300px. Cuando se pulse una tecla, se recalculará la posición izquierda, según la longitud del texto. Si la longitud supera el máximo, el fondo no será la imagen, sino de color rojo para avisar del error.
var max=250;
var ancho=300;
function progreso_tecla(obj) {
var progreso = document.getElementById("progreso");
if (obj.value.length < max) {
progreso.style.backgroundColor = "#FFFFFF";
progreso.style.backgroundImage = "url(textarea.png)";
progreso.style.color = "#000000";
var pos = ancho-parseInt((ancho*parseInt(obj.value.length))/250);
progreso.style.backgroundPosition = "-"+pos+"px 0px";
} else {
progreso.style.backgroundColor = "#CC0000";
progreso.style.backgroundImage = "url()";
progreso.style.color = "#FFFFFF";
}
progreso.innerHTML = "("+obj.value.length+" / "+max+")";
}
Estoy colaborando junto a @jlantunez y @belelros en WebSlides, un proyecto open source que permite crear presentaciones usando un navegador web de forma increíble.
Para hacer las cosas bien, vamos a incluir pruebas de testing, y para ello he mirado cómo hacerlo con AVA y PhantomJS. El problema con el que me he encontrado ha sido que todo es asíncrono y a veces da un poco de problemas esperar a cargar la página para que AVA empiece a realizar las pruebas.
Además no es plan de cargar la página en cada una de las pruebas, por lo que cargo la página una vez y luego realizo las pruebas necesarias. Esto me obliga a que las pruebas sean secuenciales en vez de en paralelo, pero bueno, tampoco es mucho problema.
Como me he roto la cabeza intentado averiguar cómo hacer, ya que soy un tanto novato en esto, pongo el código para aquel que lo necesite, aunque bueno, en breve estará en GitHub:
// Cargo las librerías
let phantom = require("phantom");
import test from 'ava';
// Para almacenar lo que PhantomJS necesita
let ph_, page_, status_;
// Función que carga la página
const load = async () => {
await phantom.create().then(async ph => {
ph_ = ph;
return await ph_.createPage();
}).then(page => {
page_ = page;
return page_.open('http://webslides.tv/');
}).then(status => {
status_ = status;
return true;
}).catch(e => console.log(e));
}
// Tests
test.serial("Page loaded", async t => {
await load();
t.is(status_, 'success');
});
test.serial('#webslides exits', async t => {
await page_
.evaluate( () => document.querySelector('#webslides') != null )
.then( ws => { t.truthy(ws); } );
});
test.serial('WebSlides object exits', async t => {
await page_
.evaluate( () => window.ws != null )
.then( ws => { t.truthy(ws); } );
});
/**
* Last test
*/
test.serial('Closing', async t => {
await page_.close();
ph_.exit();
t.true(true);
});
LibrerÃa que nos permite usar scrolls personalizados en nuestras páginas web, ya sean verticales u horizontales. Aunque hay que intentar que no aparezcan en nuestra página, en caso de que sea necesario, porque los hay que son totalmente necesarios, quizás se puedan personalizar con este script.
Por lo poco que he visto del código (que no está fácil de seguir porque está ofuscado), veo que fija el tamaño de la capa y con la propiedad overflow muestra los scrolls. Dibuja los scrolls personalizados encima y mediante ellos realiza el scroll de la capa, usando scrollTop y scrollLeft (que creo que antes no funcionaba del todo bien en Opera 8 y en la versión 9 lo han solucionado). Algo que yo no solÃa hacer y que he visto como lo hace aquÃ, es capturar el evento selectstart y hacer que devuelva false, para que cuando se haga el drag del scroll no se seleccione la imagen que usamos para personalizar el control. FleXcroll, Flexible and Accessible Custom Scroll Bars
VÃa / aNieto2K
Hoy toca realizar conexiones con el servidor usando llamadas autenticadas. Para eso usaremos JWT (JSON Web Token), que viene a ser el envío de un token en cada llamada que necesita autenticación. El token está formado por tres cadenas codificadas en base64 y separadas por un punto (header.payload.signature). En el payload se envía toda la info que queramos, pero sin pasarse porque el token no debería ser muy largo y sobre todo sin enviar información sensible, porque el token no está encriptado, es por ello que la comunicación navegador/servidor debe ser mediante HTTPS. Si quieres una explicación más detallada, aquí te lo explican mejor.
En el payload vamos a guardar varios claims, entre ellos el username y un uuid que usaremos para validar que el usuario es correcto. JWT asegura que el token es válido, pero no viene mal añadir cierta seguridad. Cuando el usuario se loguea un nuevo uuid es generado, por lo que se comprobará si coinciden para el username enviado.
Ya tenemos la explicación teórica, ahora vamos con la parte de programación, primero la parte servidor y luego el frontend.
Lo primero que tenemos que hacer es añadir un nuevo plugin a hapi.js que se encargue de la autenticación, registrando un nuevo strategy a server.auth que use JWT. El plugin hapi-auth-jwt2 se encargará de toda la autenticación JWT y añadiremos una capa extra de validación comprobando que el username y el uuid coinciden.
Y por último añadimos una nueva ruta para autenticar (login) el usuario, comprobamos que el usuario y la contraseña coinciden, y si es así, creamos un uuid que guardamos en la bd y generamos el JWT y lo enviamos en la cabecera Authorization:
Vale, ya tenemos la parte del servidor, ahora la parte del frontend. Primero es añadir las rutas para /login, /account y /logout. He añadido un meta que indica si la ruta tiene que ser obligatoriamente autenticada, obligatoriamente no autenticada o como sea. Para ello, para cada ruta comprobará si el JWT token está almacenado o no y según el meta redirigirá a home o continuará:
Para gestionar el almacenamiento en el navegador en vez de cookies vamos a usar sessionStorage y localStorage para guardar el token JWT. Como el formulario de login permite recordar la sesión, vamos a usar ambos storages. Si no se recuerda usaremos sessionStorage, que se borrará cuando se cierra el navegador, en caso contrario usaremos localStorage.
import Config from'@/js/config';
/**
* API methods for sesion/local storage.
* Depending on `this.session` it saves only on `sessionStorage` or also in `localStorage`
*
* @since v0.8.0
*/classstorage{
/**
* Constructor
*
* @param {boolean} session If stored only in session
*/constructor( session = false ) {
this.session = session;
}
/**
* It saves the token in the session (and local storage is this.session === false),
*
* @param {strig} token JWT token
*/
setJWTToken( token ) {
sessionStorage.setItem( Config.jwt.storageKey, token );
if ( ! this.session ) {
localStorage.setItem( Config.jwt.storageKey, token );
}
}
/**
* It gets a value from session storage or in local if session = false
*
* @returns {string}
*/
getJWTToken() {
const sessionValue = sessionStorage.getItem( Config.jwt.storageKey );
if ( sessionValue ) {
return sessionValue;
}
if ( ! this.session ) {
const storedValue = localStorage.getItem( Config.jwt.storageKey );
return storedValue;
}
returnnull;
}
/**
* Removes JWT token from session and local storage
*/
removeJWTToken() {
sessionStorage.removeItem( Config.jwt.storageKey );
localStorage.removeItem( Config.jwt.storageKey );
}
}
exportdefault storage;
También hemos creado una librería para tratar las llamadas a la API. Hay dos métodos, uno para autenticar el usuario (login) que mirará la cabecera Authorization, y otro método que obtiene los datos del usuario actual realizando una llamada autenticada enviando el JWT token en la cabecera Authorization:
import Config from'@/js/config';
import Storage from'@/js/utils/storage';
/**
* API backend methods
*
* @since 0.8.0
*/classapiFetch{
/**
* Authenticate an user, if ok, JWT token is sent by the server in Authorization header
*
* @param {string} username User name
* @param {string} password Password
*
* @returns {Promise}
*/
auth( username, password ) {
return fetch( Config.api.user.auth, {
method: 'POST',
body: JSON.stringify(
{ username, password }
),
mode: 'cors',
} ).then( response => {
const auth = response.headers.get( 'Authorization' );
if ( auth ) {
return {
response: true,
token: auth,
};
}
return {
response: false,
message: 'Username or password not valid',
};
} );
}
getUser( username ) {
return fetch( `${ Config.api.user.get }${ username }`, {
method: 'GET',
headers: {
Authorization: new Storage().getJWTToken(),
},
} ).then( response => response.json() );
}
}
exportdefault apiFetch;
Ahora solo faltan los controladores para login, account y logout. Login realizar la llamada al servidor y si se obtiene el JWT se guarda:
<template><sectionclass="section"><divclass="container"><h1class="title">
Login
</h1><divv-if="error"class="columns is-centered has-margin-bottom-2"
><b-notificationclass="column is-7-tablet is-6-desktop is-5-widescreen"type="is-danger"has-iconaria-close-label="Close notification"role="alert"size="is-small "
>
{{ error }}
</b-notification></div></div><divclass="container"><divclass="columns is-centered"><divclass="column is-5-tablet is-4-desktop is-3-widescreen has-background-light login-form"><b-fieldlabel="Username"><b-inputv-model="username"value=""maxlength="30"icon="account-circle-outline"
/></b-field><b-fieldlabel="Password"><b-inputv-model="password"value=""type="password"icon="lock-outline"
/></b-field><divclass="field"><b-checkboxv-model="remember">
Remember me
</b-checkbox></div><divclass="has-text-right"><b-buttontype="is-primary"
@click="submit"
>
Log in
</b-button></div></div></div></div></section></template><script>import ApiFectch from'@/js/utils/api';
import Storage from'@/js/utils/storage';
exportdefault {
name: 'Login',
// Form data and error messages if login fails
data() {
return {
username: '',
password: '',
remember: false,
error: '',
};
},
methods: {
// It logs in, using the backend API for authenticate the user data.// If user logs in, it saves the JWT token in the browser. If not, shows error message.
submit: function() {
const api = new ApiFectch();
api.auth( this.username, this.password )
.then( response => {
const storage = new Storage( ! this.remember );
if ( !! response.response && !! response.token ) {
storage.setJWTToken( response.token );
this.error = false;
// `go` refreshes the page, so user data is updatedthis.$router.go( '/' );
} else {
storage.removeJWTToken();
this.error = response.message;
}
} );
},
},
};
</script><stylelang="scss"scoped>.login-form {
border-radius: 4px;
}
</style>
Logout borra los datos JWT del navegador y redirige a home:
<template><sectionclass="hero is-medium is-primary is-bold"><divclass="hero-body"><divclass="container has-text-centered"><h1class="title">
{{ message }}
</h1><h2class="subtitle">
Miss you 💛
</h2></div></div></section></template><script>import User from'@/js/utils/user';
exportdefault {
name: 'Logout',
// Dummy data
data() {
return {
message: 'Bye',
};
},
// After being created it logs out and go to home
created() {
new User().logout();
// `go` instead of `push` because refreshing the page updates the user data// Maybe using vuex is a better way to do it, or not...this.$router.go( '/' );
},
};
</script>
Y Account recupera los datos del usuario una vez que ha creado el controlador:
<template><divid="account"><sectionclass="hero is-primary is-bold"><divclass="hero-body"><divclass="has-text-centered"><h1class="title">
{{ message }}
</h1><h2class="subtitle">
Your data
</h2></div></div></section><sectionclass="section"><divclass="container"><divclass="tile is-ancestor"><divclass="tile is-parent"><pclass="tile is-child notification">
Some content
</p></div><divclass="tile is-8 is-parent"><divclass="tile is-child notification is-info"><ulid="data"><liv-for="( value, key ) in user":key="key"
>
{{ key }} : {{ value }}
</li></ul></div></div></div></div></section></div></template><script>// Dummy componentimport ApiFectch from'@/js/utils/api';
import User from'@/js/utils/user';
exportdefault {
name: 'Account',
data() {
return {
message: 'Account',
user: {},
};
},
created() {
const user = new User().getCurrentUser();
new ApiFectch().getUser( user.username )
.then( response =>this.user = response );
},
};
</script>