Similar Posts
Añadir editor WYSIWYG al widget Texto de WordPress
Uno de los problemas que se pueden tener a la hora de desarrollar un plugin es que el cliente sepa editar los widgets añadiendo o modificando el HTML. Es por ello que he creado la posibilidad de añadir un pequeño plugin jQuery de edición WYSIWYG a los widgets Texto que vienen en WordPress por defecto, pero lo mismo se puede hacer para cualquier otro widget, incluído los que crees personalmente.
Lo primero será bajarnos el plugin nicEdit, el cual permite de forma muy sencilla añadir un editor WYSIWYG a cualquier elemento. He elegido este plugin y no otro porque en el poco espacio que ofrece el textarea, poner otro plugin más completo, dificultaría su uso, de todas formas, si se quiere usar otro plugin, pues sin problema.
Yo me he creado un directorio /js donde he metido los dos ficheros de nicedit: nicEdit.js y nicEditorIcons.gif.
El siguiente paso es editar nuestro functions.php para añadir el siguiente código:
// El action in_widget_form es el encargado de llamar al método
// form del widget, el que dibuja el formulario
// Comprobaremos si es un WP_Widget_Text y si es así
// añadiremos un checkbox para permitir que el textarea
// sea WYSIWYG
add_action('in_widget_form', 'set_nicedit_form_widget', 10, 3);
function set_nicedit_form_widget($obj, $return, $instance) {
if (is_a($obj, 'WP_Widget_Text')) { ?>
<p><input class="nicedit" id="<?php echo $obj->get_field_id('nicedit'); ?>" name="<?php echo $obj->get_field_name('nicedit'); ?>" type="checkbox" <?php checked(isset($instance['nicedit']) ? $instance['nicedit'] : 0); ?> /> <label for="<?php echo $obj->get_field_id('nicedit'); ?>"><?php _e('Utilizar editor HTML'); ?></label></p>
<?php
if (isset($instance['nicedit']) && !empty($_POST)) {
// Si se ha marcado la opción nicedit en el form y se ha dado GUARDAR
// llamamos la funcion init (explicada más adelante) que se encarga de
// añadir el WYSIWYG al textarea
?>
<script type="text/javascript">
init();
</script>
<?php
}
}
}
// El WP_Widget_Text no tiene el checkbox nicedit en su código
// por lo que al hacer un update lo ignoraría
add_filter('widget_update_callback', 'set_nicedit_update_widget', 10, 4);
function set_nicedit_update_widget($instance, $new_instance, $old_instance, $obj) {
if (is_a($obj, 'WP_Widget_Text')) {
$instance["nicedit"] = isset($new_instance["nicedit"]) && $new_instance["nicedit"] == 'on';
}
return $instance;
}
// Añadimos los scripts y styles necesarios si estamos en la pantalla de widgets
add_action( 'admin_enqueue_scripts', 'add_admin_widget_scripts' );
function add_admin_widget_scripts() {
global $current_screen;
if ($current_screen->base == 'widgets') {
wp_enqueue_script( 'nicedit', get_bloginfo('template_directory') . '/js/nicEdit.js' );
wp_enqueue_script( 'admin', get_bloginfo('template_directory') . '/js/admin.js' );
wp_localize_script( 'admin', 'admin', array('path'=>get_bloginfo('template_directory')) );
wp_enqueue_style( 'admin', get_bloginfo('template_directory') . '/css/admin.css' );
}
}
Ahora deberemos añadir la funcionalidad javascript al código que hemos metido en el widget (admin.js):
// Esta función es la encargada de añadir el WYSIWYG al textarea
function init() {
// Busca todos los checkbox nicedit (le metimos un class)
// que estén seleccionados
jQuery('input:checkbox.nicedit:checked').each(function() {
var $this = jQuery(this);
// Si ya tiene un nicedit nos lo cepillamos
var niceditor = $this.data('nicedit');
var $textearea = $this.parents('form:first').find('textarea');
if (niceditor) niceditor.removeInstance($textearea.attr('id'));
// Añadimos el nicedit al textarea, puedes meterle más botones, mira la doc de nicedit para ello
var area = new nicEditor({buttonList : ['bold','italic', 'link', 'unlink', 'xhtml'], iconsPath: admin.path+ '/js/nicEditorIcons.gif'}).panelInstance($textearea.attr('id'));
$this.data('nicedit', area);
});
}
jQuery(document).ready(function() {
// Si pulsamos el checkbox, añadimos o quitamos el nicedit
// Almacenaremos el objeto nicedit para poder utilizarlo despues
jQuery('.widget-liquid-right').on('click', 'input:checkbox.nicedit', function() {
var $this = jQuery(this);
var $textearea = $this.parents('form:first').find('textarea');
var niceditor = $this.data('nicedit');
if (niceditor) {
niceditor.removeInstance($textearea.attr('id'));
$this.data('nicedit', false);
} else {
var area = new nicEditor({buttonList : ['bold','italic', 'link', 'unlink', 'xhtml'], iconsPath: admin.path+ '/js/nicEditorIcons.gif'}).panelInstance($textearea.attr('id'));
$this.data('nicedit', area);
}
});
// nicEdit controla los submits para actualizar los valores,
// pero en los widgets no funciona, por lo que antes del submit
// forzamos la actualización del valor
jQuery('#widgets-right').on('click', ':submit', function() {
// Quizás se pueda utilizar el objeto nicedit almacenado anteriormente
// pero esto lo hice así en una versión inicial menos genérica y no lo he tocado
for(var i=0; i<nicEditors.editors.length; i++) for(var j=0; j<nicEditors.editors[i].nicInstances.length; j++) nicEditors.editors[i].nicInstances[j].saveContent();
});
// Apaño el orden de los widgets para meter nicedit a los nuevos widgets
// Es necesario si se pusieran los nicedit seleccionados por defecto,
// que en este ejemplo no es el caso, pero si lo añadierais a un widget propio
// no os funcioanría si no hicierais esto.
// Lo que hago es meter un pequeño hack a la función saveOrder
wpWidgets._saveOrder = wpWidgets.saveOrder
wpWidgets.saveOrder = function() {
init();
wpWidgets._saveOrder();
}
// Activo los nicedit en los textareas marcados
// como tal cuando se carga la página al principio
init();
});
Y por último solo nos falta meterle el css necesario, en este caso, el nicedit tiene un pequeño bug y si el textarea tiene un width = 100% no calcula el tamaño real, por eso se lo metemos por css (admin.css).
textarea.widefat {
width: 400px;
height: 100px;
}
Y listo, ya solo falta que el cliente no os dé mucho la lata.
linkNotify: plugin jQuery que indica que se carga un enlace
linkNotify es un plugin jQuery que permite modificar enlaces y mostrar un texto personalizado que indica que el enlace se está cargando. Algo que puede ser muy útil para aplicaciones en la que el usuario es algo básico y pincha y pincha en el enlace eternamente hasta que ve algún resultado sin esperar a que cargue la primera vez.
$('a').linkNotify('Espera mientras carga la pagina...');
Carga asíncrona de scripts en Webkit
Webkit está implementando en la última versión la carga de scripts de forma asíncrona, para ello hace uso de los atributos async y defer. Esta carga de scripts se realiza sin detener el renderizado del HTML y añade el evento onLoad para ejecutar un método cuando acabe de cargarse:
La diferencia entre async y defer es que async se ejecuta a la primera oportunidad después de que finalice la carga y antes de que se ejecute el evento load del objeto window, por lo que con bastante posibilidad el script se ejecute asíncronamente y no en el orden en el que se muestra en al página. Los scripts defer se ejecutarán en el orden en el que se indica en la página, pero empezará despues del parseo completo pero antes de que ocurra el evento DOMContentLoaded del objeto document.
Vía / CSS-Tricks
Javascript para realizar drag y resize
Buen script que nos permite mover nuestras capas mediante drag&drop y cambiarlas de tamaño.
Funciona con posicionamiento relativo y absoluto de los elementos en la página. Además permite personalizar los estilos mediante CSS. No es obtrusivo. Es posible indicar tamaños máximos y mínimos para las cajas y es compatible con la mayoría de los navegadores.
DragResize
Vía / Ajaxline
Selector de hora con Mootools
Cada dÃa son más los scripts que se realizan con frameworks de Javascript, estos, al estar ayudados por las funcionalidades del framework, son más sencillos de desarrollar, lo que aporta más riqueza a la web2.0. En este caso se trata de un selector de horas, que mediante un reloj se puede seleccionar la hora, moviendo las manillas o indicando si queremos AM o PM.
El script es sencillo de usar e implementar, controla independientemente las manillas de las horas y los minutos, permite mover las manillas para indicar la hora y usa CSS sprits para mejorar la velocidad de carga.
NoGray Time Picker
VÃa / WebAppers

Hapi.js + Vue.js llamadas autenticadas al servidor con JWT (login)
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.
/**
* Auth controller
*
* It uses JWT
*/
const jwt2 = require( 'hapi-auth-jwt2' );
const User = require( '../models/users' );
const validate = async function( decoded ) {
const user = await User.findByUUID( decoded.id );
return { isValid: !! user && user.username === decoded.username };
};
exports.plugin = {
name: 'auth',
register: async function( server, options ) {
await server.register( jwt2 );
server.auth.strategy( 'jwt', 'jwt',
{
key: options.jwt.secret,
validate: validate,
verifyOptions: { algorithms: [ 'HS256' ] }, // pick a strong algorithm
}
);
server.auth.default( 'jwt' );
},
};
En el modelo user añadiremos 2 nuevos métodos estáticos, que permitirán encontrar usuarios por username/email y por uuid:
userSchema.static( 'findByUserOrEmail', async function( username, email ) {
const result = await new Promise( ( resolve, reject ) => {
this.model( 'User' )
.findOne( {
$or: [
{ username: username },
{ email: email },
],
} )
.exec( ( error, data ) => {
if ( error ) {
reject( error );
}
resolve( data );
} );
} );
return result;
} );
userSchema.static( 'findByUUID', async function( uuid ) {
const result = await new Promise( ( resolve, reject ) => {
this.model( 'User' )
.findOne( {
uuid,
} )
.exec( ( error, data ) => {
if ( error ) {
reject( error );
}
resolve( data );
} );
} );
return result;
} );
En el manifest de Glue hay que añadir la configuración para CORS, ya que el token viajará usando la cabecera Authorization:
const manifest = {
server: {
port: Config.get( '/server/port' ),
routes: {
cors: {
origin: [ '*' ],
credentials: true,
exposedHeaders: [ 'Authorization' ],
},
},
},
};
Y en la configuración general del servidor hay que añadir una cadena que se usará para encriptar el signature del token JWT.
const config = {
auth: {
jwt: {
secret: process.env.JWT_SECRET,
},
},
};
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:
/**
* Authenticates an user
*/
server.route( {
method: 'POST',
path: '/user/auth',
options: {
tags: [ 'api', 'user', 'auth' ],
description: 'Server authenticate user',
notes: 'Server authenticate user',
auth: false,
validate: {
payload: {
username: Joi.string().alphanum().min( 3 ).max( 20 ).required(),
password: Joi.string().min( 8 ).required(),
},
},
},
/**
* Route handler
*
* @param {object} request
* @param {object} h Hapi object
* @returns {object}
*/
handler: async( request, h ) => { // eslint-disable-line
try {
const user = await User.findByUserOrEmail( request.payload.username, request.payload.email );
if ( ! user ) {
return Boom.badData( 'User or password incorrect' );
}
const isValidPassword = await bcrypt.compare( request.payload.password, user.password );
if ( ! isValidPassword ) {
return Boom.badData( 'User or password incorrect' );
}
const claims = {
id: uuid(),
exp: new Date().getTime() + ( 180 * 24 * 60 * 60 * 1000 ), // 3 months
username: user.username,
};
user.uuid = claims.id;
user.save();
const token = JWT.sign( claims, Config.get( '/auth' ).jwt.secret ); // synchronous
return h.response( {
response: true,
message: 'Check Auth Header for your Token',
} ).header( 'Authorization', token );
} catch ( error ) {
return Boom.badImplementation( 'Error', { error } );
}
},
} );
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á:
import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/js/components/Home';
import Login from '@/js/components/Login';
import Account from '@/js/components/Account';
import Logout from '@/js/components/Logout';
import Storage from '@/js/utils/storage';
Vue.use( Router );
const router = new Router( {
mode: 'history',
routes: [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/login',
name: 'Login',
component: Login,
meta: {
auth: false,
},
},
{
path: '/account',
name: 'Account',
component: Account,
meta: {
auth: true,
},
},
{
path: '/logout',
name: 'Logout',
component: Logout,
meta: {
auth: true,
},
},
],
} );
router.beforeEach( ( to, from, next ) => {
if ( !! to.meta ) {
const storage = new Storage();
const jwtToken = storage.getJWTToken();
// Can't be logged
if ( to.meta.auth === false ) {
if ( jwtToken ) {
next( '/' );
}
// Must be logged
} else if ( to.meta.auth === true ) {
if ( ! jwtToken ) {
next( '/' );
}
}
next();
} else {
next();
}
} );
export default router;
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
*/
class storage {
/**
* 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;
}
return null;
}
/**
* Removes JWT token from session and local storage
*/
removeJWTToken() {
sessionStorage.removeItem( Config.jwt.storageKey );
localStorage.removeItem( Config.jwt.storageKey );
}
}
export default 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
*/
class apiFetch {
/**
* 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() );
}
}
export default 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>
<section class="section">
<div class="container">
<h1 class="title">
Login
</h1>
<div
v-if="error"
class="columns is-centered has-margin-bottom-2"
>
<b-notification
class="column is-7-tablet is-6-desktop is-5-widescreen"
type="is-danger"
has-icon
aria-close-label="Close notification"
role="alert"
size="is-small "
>
{{ error }}
</b-notification>
</div>
</div>
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-4-desktop is-3-widescreen has-background-light login-form">
<b-field label="Username">
<b-input
v-model="username"
value=""
maxlength="30"
icon="account-circle-outline"
/>
</b-field>
<b-field label="Password">
<b-input
v-model="password"
value=""
type="password"
icon="lock-outline"
/>
</b-field>
<div class="field">
<b-checkbox v-model="remember">
Remember me
</b-checkbox>
</div>
<div class="has-text-right">
<b-button
type="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';
export default {
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 updated
this.$router.go( '/' );
} else {
storage.removeJWTToken();
this.error = response.message;
}
} );
},
},
};
</script>
<style lang="scss" scoped>
.login-form {
border-radius: 4px;
}
</style>
Logout borra los datos JWT del navegador y redirige a home:
<template>
<section class="hero is-medium is-primary is-bold">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title">
{{ message }}
</h1>
<h2 class="subtitle">
Miss you 💛
</h2>
</div>
</div>
</section>
</template>
<script>
import User from '@/js/utils/user';
export default {
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>
<div id="account">
<section class="hero is-primary is-bold">
<div class="hero-body">
<div class="has-text-centered">
<h1 class="title">
{{ message }}
</h1>
<h2 class="subtitle">
Your data
</h2>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="tile is-ancestor">
<div class="tile is-parent">
<p class="tile is-child notification">
Some content
</p>
</div>
<div class="tile is-8 is-parent">
<div class="tile is-child notification is-info">
<ul id="data">
<li
v-for="( value, key ) in user"
:key="key"
>
{{ key }} : {{ value }}
</li>
</ul>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
// Dummy component
import ApiFectch from '@/js/utils/api';
import User from '@/js/utils/user';
export default {
name: 'Account',
data() {
return {
message: 'Account',
user: {},
};
},
created() {
const user = new User().getCurrentUser();
new ApiFectch().getUser( user.username )
.then( response => this.user = response );
},
};
</script>
Recuerda que tienes todo el código aquí