Creando un sistema de notas – Parte 2

En este tutorial aprenderemos a crear un sistema de notas utilizando tecnologías web. En esta segunda parte, veremos todo lo referente al lado del cliente.

Esta entrada es la número 2 en una serie de entradas de 2 partes. A continuación puedes ver las demás entradas:

Para llevar a cabo este tutorial, es necesario un conocimiento avanzado de jQuery (creación de plugins, AJAX y jQuery UI), asumiendo que se tenga conocimiento de las demás tecnologías empleadas.

La maquetación

Comenzaremos esta segunda parte escribiendo el HTML de nuestra página. Dado que la página cambiará dinámicamente utilizando JavaScript, no necesitaremos más que un archivo HTML. Incluiremos en el todos los elementos que utilizaremos posteriormente con JavaScript utilizando el plugin jQuery TMPL.

index.html

El contenido de nuestro archivo será el siguiente:

<!DOCTYPE html>
<html>
	<head>
		<title>Sistema Simple de Notas</title>
		<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">

		<!--[if lt IE 9]>
		<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
		<![endif]-->

		<link rel="stylesheet" type="text/css" href="css/normalize.css" media="all">
		<link rel="stylesheet" type="text/css" href="css/style.css" media="all">
	</head>
	<body>
		<div class="alert loading">Cargando…</div>
		<div class="wrapper">
			<section id="note-list"></section>
			<hr>
			<a id="add-note">Añadir Nota</a>
		</div>

		<script id="actions-default-template" type="text/x-jquery-tmpl">
			<a id="edit-note" title="Editar la nota."></a>
			<a id="delete-note" title="Eliminar la nota."></a>
			<span id="sort-handle" title="Mueve la nota para ordenarla."></span>
		</script>
		<script id="actions-active-template" type="text/x-jquery-tmpl">
			<a id="save-changes" title="Confirmar la acción."></a>
			<a id="discard-changes" title="Volver atrás."></a>
		</script>
		<script id="note-template" type="text/x-jquery-tmpl">
			<div class="note" id="note-${id}" data-note-id="${id}">
				<div class="note-wrapper">
					<div class="note-text"></div>
					<div class="note-date">${date}</div>
					<div class="clear"></div>
				</div>
				<div class="note-actions"></div>
				<div class="clear"></div>
			</div>
		</script>

		<script type="text/javascript" src="js/jquery.js"></script>
		<script type="text/javascript" src="js/jquery-ui.js"></script>
		<script type="text/javascript" src="js/jquery.tmpl.js"></script>
		<script type="text/javascript" src="js/script.js"></script>
	</body>
</html>

Analizemos la estructura del archivo:

  • Primero tenemos el script HTML5Shim que habilitará el uso de etiquetas HTML5 en IE

  • A continuación añadimos nuestros estilos, un reset y los estilos de la página.

  • Dentro del body tenemos los siguiente:

    • Un mensaje de carga que mostraremos y ocultaremos con JavaScript.
    • div.wrapper el cual nos ayudará a centrar el contenido y servirá de esqueleto para la página.
    • section#note-list donde añadiremos las notas dinámicamente.
    • a#add-note para cuando queramos añadir una nota nueva.
  • Más abajo tenemos los elementos HTML que utilizaremos más adelante, tal como se muestra en la documentación del plugin jQuery TMPL.

  • Para finalizar cargaremos nuestro JavaScript. Recordar que se escriben al final del body página porque así se cargarán después de que la estructura y los estilos estén listos.

Dando color a la página

Vista Previa
Está será nuestra página una vez acabada.

style.css

Una vez tenemos la maquetación, será hora de ir aplicando estilos para conseguir el resultado que tenemos arriba.

Estilos generales

* {
	outline: none;
}

html,
body {
	background: #f3f3f3 url('../img/body-back.png');
	text-align: center;
}

.wrapper {
	position: relative;
	margin: 50px auto;
	max-width: 840px;
	width: 90%;
	color: #555;
	font: 400 13px 'Helvetica Neue', Arial, Helvetica, Geneva, sans-serif;
}

.clear {
	clear: both;
}

hr {
	margin-top: 40px;
	margin-bottom: 40px;
	border: 0;
	border-top: 3px dashed white;
}
hr:before {
	display: block;
	margin-top: -4px;
	border-top: 3px dashed #d7d7d7;
	content: '';
}

.placeholder {
	border: 1px solid rgba(255, 246, 17, .5);
	background: rgba(255, 246, 17, .1);
}

::selection {
	background: rgba(255, 246, 17, .25);
}
::-moz-selection {
	background: rgba(255, 246, 17, .25);
}

Estilos de las notas

.note {
	margin-bottom: 25px;
	margin-left: 3%;
	width: 94%;
	border-width: 1px 0px;
	border-style: solid;
	border-color: transparent;
}
.note:not(:first-child) {
	margin-top: 25px;
}

	.note-wrapper {
		float: right;
		padding-bottom: 15px;
		width: 91%;
		border: 1px solid rgba(0, 0, 0, .12);
		background: rgba(255, 255, 255, .8) url('../img/noteWrapper-back.png') repeat-y 25px;
		-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .08);
		-moz-box-shadow: 0 1px 2px rgba(0, 0, 0, .08);
		box-shadow: 0 1px 2px rgba(0, 0, 0, .08);
		text-align: justify;
		line-height: 26px;
	}
	.note-wrapper.editable {
		-webkit-box-shadow: 0 0 2px #fff611;
		-moz-box-shadow: 0 0 2px #fff611;
		box-shadow: 0 0 2px #fff611;
	}

		.note-text {
			padding-top: 26px;
			padding-right: 26px;
			padding-left: 50px;
			min-height: 65px;
			background: url('../img/noteText-back.png');
		}

		.note-date {
			float: right;
			padding-top: 10px;
			padding-right: 25px;
			color: #777;
			font-size: 0.85em;
		}

	.note-actions {
		float: left;
		width: 8%;
	}

		.note-actions a,
		.note-actions span {
			display: block;
			margin-bottom: 14px;
			width: 33px;
			height: 33px;
			opacity: 0.6;
			cursor: pointer;
		}
		.note-actions a:hover,
		.note-actions span:hover {
			opacity: 1;
		}

		#edit-note {
			background: url(../img/edit.png);
		}

		#delete-note {
			background: url(../img/delete.png);
		}

		#save-changes {
			background: url(../img/save-changes.png);
		}

		#discard-changes {
			background: url(../img/discard-changes.png);
		}

		#sort-handle {
			background: url(../img/sort-handle.png);
		}

Estilos de las alertas

.alert {
	position: fixed;
	top: 0;
	left: 0;
	z-index: 1000;
	margin-top: -33px;
	margin-bottom: 50px;
	padding: 8px 15px;
	width: 100%;
	-webkit-box-shadow: 0 1px white;
	-moz-box-shadow: 0 1px white;
	box-shadow: 0 1px white;
	color: #f9f9f9;
	font-weight: bold;
	font-size: 13px;
}
.alert.loading {
	margin-top: 0;
	border-bottom: 1px solid black;
	background: rgba(0, 0, 0, 0.6);
	text-shadow: 0 1px black;
}
.alert.error {
	border-bottom: 1px solid #b0352f;
	background: rgba(216, 77, 71, 0.85);
	text-shadow: 0 1px #b0352f;
}
.alert.success {
	border: 1px solid #317a27;
	background: rgba(56, 157, 42, .85);
	text-shadow: 0 1px #317a27;
	font-weight: bold;
}

Botón de añadir nota

#add-note {
	display: inline-block;
	padding: 6px 15px;
	border: 1px solid #e4aa3e;
	background-image: -webkit-linear-gradient(90deg, #fdc459 0%, #fdcd5f 100%);
	background-image: -moz-linear-gradient(90deg, #fdc459 0%, #fdcd5f 100%);
	background-image: -o-linear-gradient(90deg, #fdc459 0%, #fdcd5f 100%);
	background-image: -ms-linear-gradient(90deg, #fdc459 0%, #fdcd5f 100%);
	background-image: linear-gradient(90deg, #fdc459 0%, #fdcd5f 100%);
	color: #9a662d;
	text-decoration: none;
	text-shadow: 0 1px rgba(255, 255, 255, 0.4);
	letter-spacing: 0.4px;
	font-weight: bold;
	font-size: 11px;
}
#add-note:hover {
	background: #ffcf63;
	cursor: pointer;
}

Haciendo nuestra página funcional

script.js

Ya que todas las peticiones que realicemos al archivo ajax.php contendrán partes de código en común (variables, funciones…) crearemos un plugin de jQuery con el fin de no repetir código. Este plugin realizará una petición al archivo con unas variables que serán comunes en todas las peticiones y otras individuales.

// Plugin que nos ayudará a manejar las preticiones.
$.customRequest = function(action, data, events) {
	// Creamos los eventos por defecto.
	var events = $.extend({
		onSuccess:	function() {}
	}, events);

	// Realizamos la petición.
	$.ajax({
		url:		'ajax.php?action=' + action,
		dataType:	'JSON',
		type:		'POST',
		data:		data,
		beforeSend:	function() {
			// Eliminamos todos los mensajes activos y activamos el mensaje de carga.
			$('.alert.error, .alert.success').remove();
			$('.loading').show();
		},
		error:		function(jqXHR) {
			// Eliminamos todos los mensajes y mostramos el error.
			$('.alert.error, .alert.success').remove();
			$('<div class="alert error">').html(jqXHR.responseText)
										  .prependTo('body')
										  .animate({ 'margin-top': '0px' })
										  .delay(2000)
										  .animate({ 'margin-top': '-33px' });
		},
		success:	function(result) {
			$('.alert.error, .alert.success').remove();
			// La petición se completó y el archivo respondió con un mensaje de éxito.
			if (result.success) {
				$('<div class="alert success">').html(result.success)
												.prependTo('body')
												.animate({ 'margin-top': '0px' })
												.delay(2000)
												.animate({ 'margin-top': '-33px' });
			// La petición se completó y el archivo devolvió una cadena con los datos de las notas.
			} else {
				// El plugin de templating creará un bucle automáticamente al pasarle el vector de datos.
				$('#note-template').tmpl(result).hide()
								   .appendTo('#note-list')
								   .slideDown();

				// Actualizamos el ordenado ya que hemos añadido nuevas notas.
				$('#note-list').sortable('refresh');
			}
			// Llamamos a nuestra función opcional.
			events.onSuccess();
		},
		complete:	function() {
			// Ocultamos el mensaje de carga.
			$('.loading').hide();
		}
	});
}

Como podeis ver la función acepta tres parámetros:

  • action: acción que se quiere realizar, corresponde a la variable $_GET['action'] utilizada en nuestro archivo ajax.php.
  • data: datos a enviar mediante POST, así como el ID de una nota, su texto…
  • events: funciones que se ejecutarán en el caso de que algún evento se complete, justo después del código común de todas las peticiones.

Nota: no olvideís repasar el archivo ajax.php en la primera parte de tutorial para saber que parámetros se necesitarán en cada caso.

Una vez tenemos el plugin, ejecutaremos la petición dependiendo de la acción que deseemos realizar. Recordad que todo nuestro código a partir de ahora debe ejecutarse tan solo cuando la página haya terminado de cargar, por lo tanto escribiremos lo siguiente:

$(document).ready(function() {
	// Código.
});

Ahora solo falta que al pinchar sobre los diferentes botones de la página se envien las peticiones. Muy sencillo:

/** Habilitamos el ordenado. **/
$('#note-list').sortable({
	axis:					'y',
	handle:					'#sort-handle',
	placeholder:			'placeholder',
	forcePlaceholderSize:	true,
	update:					function() {
		var positions = $('#note-list').sortable('toArray');
		// Eliminar "nota-" del ID.
		positions = $.map(positions, function(value) {
			return value.replace('note-', '');
		});

		$.customRequest('rearrange', {
			positions: positions
		});
	}
});

/** Cargamos todas las notas. **/
$.customRequest('getAll');

/** Añadir una nueva nota. **/
$('#add-note').click(function() {
	$.customRequest('add');
});

/** Eliminar una nota. **/
$('#delete-note').live('click', function(event) {
	event.preventDefault();

	var $note = $(this).parents('.note');

	$.customRequest(
		'delete',
		{ id: $note.data('note-id') },
		{ onSuccess: function() { $note.slideUp(function() { $(this).remove() }); } }
	);
});

/** Editar una nota. **/
$('#edit-note').live('click', function(event) {
	event.preventDefault();

	var $note_wrapper	= $(this).parent().siblings('.note-wrapper'),
		$note_text		= $note_wrapper.find('.note-text'),
		$note_actions	= $note_wrapper.siblings('.note-actions');

	// Cancelar las demás ediciones.
	$('.note #discard-changes').trigger('click');

	// Hacer la nota editable, guardar el texto actual y cambiar los botones.
	$note_wrapper.addClass('editable');
	$note_text.attr('contentEditable', 'true')
			  .focus();

	if (!$note_text.data('original-text')) {
		$note_text.attr('data-original-text', $note_text.html())
	}
	
	$note_actions.html($('#actions-active-template').tmpl());
});

/** Cancelar la edición. **/
$('#discard-changes').live('click', function(event) {
	event.preventDefault();

	var $note_wrapper	= $(this).parent().siblings('.note-wrapper'),
		$note_text		= $note_wrapper.find('.note-text'),
		$note_actions	= $note_wrapper.siblings('.note-actions');

	// Hacer la nota no editable, restaurar el texto y los botones.
	$note_wrapper.removeClass('editable');
	$note_text.attr('contentEditable', 'false')
			  .html($note_text.data('original-text'))
			  .removeAttr('data-original-text');
	$note_actions.html($('#actions-default-template').tmpl());
});

/** Confirmar la edición. **/
$('#save-changes').live('click', function(event) {
	event.preventDefault();

	var $note			= $(this).parents('.note'),
		$note_wrapper	= $note.find('.note-wrapper'),
		$note_text		= $note.find('.note-text'),
		$note_actions	= $note.find('.note-actions');

	// Guardamos la nota.
	$.customRequest(
		'edit',
		{ id: $note.data('note-id'), text: $note_text.text() },
		{
			onSuccess:	function() {
				$note_text.removeAttr('data-original-text');
				// Hacer la nota no editable y restaurar los botones.
				$note_wrapper.removeClass('editable');
				$note_text.attr('contentEditable', 'false');
				$note_actions.html($('#actions-default-template').tmpl());
			}
		}
	);
});

Resumiendo

Aquí termina el tutorial. El código presentado no es ni mucho menos la única opción de hacer algo así, ni siquiera la opción más óptima, y mucho menos la más rápida. Está hecho de esta forma para intentar enseñar (incluso a mí) algún truco que pueda resultar interesante. Por ejemplo, tenemos el uso del plugin jQuery TMPL, el cual he citado ya muchas veces a lo largo del tutorial pero que me parece indispensable para cualquier proyecto en AJAX, ya que nos da una simpleza y legibilidad que creando el elemento con JavaScript no tendriamos.

Podeis encontrar todos los archivos de este tutorial el GitHub. Si deseais probarlo, por ahora no teneis más remedio que descargarlo y ejecutarlo en vuestro servidor local.

Y hasta aquí todo, espero no haberme enrollado demasiado. No olvideis comentar, ¡nos vemos!

comments powered by Disqus