J3.x

Desarrollo de un Componente MVC/Agregar Niveles

From Joomla! Documentation

< J3.x:Developing an MVC Component
This page is a translated version of the page J3.x:Developing an MVC Component/Adding Levels and the translation is 100% complete.
Other languages:
Deutsch • ‎English • ‎español • ‎français • ‎中文(台灣)‎
Joomla! 
3.x
Tutorial
Desarrollo de un Componente MVC

Agregar una variable de petición en el tipo de menú

Utilizando la base de datos

Lado servidor básico

Agregar gestión de idioma

Agregar acciones del lado servidor

Agregar decoraciones del lado servidor

Agregar verificaciones

Agregar categorías

Agregar configuración

  1. Agregar ACL

Agregar un archivo de secuencia de comandos instalar-desinstalar-actualizar

Agregar un formulario del lado cliente

  1. Agregar una imagen
  2. Agregar un mapa
  3. Agregar AJAX
  4. Agregar un alias

Usar la facilidad filtro de idioma

  1. Agregar una Modal
  2. Agregar Asociaciones
  3. Agregar Comprobación
  4. Agregar Ordenamiento
  5. Agregar Niveles
  6. Agregar Control de Versiones
  7. Agregar Etiquetas
  8. Agregar Accesos
  9. Agregar procesos por lote
  10. Agregar Caché
  11. Agregar un Canal de Noticias

Agregar un servidor de actualización

  1. Agregar campos personalizados
  2. Upgrading to Joomla4



Esta es una serie multi-artículos de tutoriales sobre cómo desarrollar un Componente Modelo-Vista-Controlador para Joomla! VersiónJoomla 3.x.

Comenzar con la Introducción, y navegar por los artículos de esta serie usando el botón de navegación en la parte inferior o en el cuadro de la derecha (los "Artículos de esta serie").



Este artículo es parte del tutorial Desarrollo de un Componente MVC para Joomla! 3.2. Te invitamos a leer las partes anteriores del tutorial antes de leer esto.

En este paso, agregamos niveles a nuestro componente helloworld, lo que básicamente significa que estamos implementando una estructura de árbol en nuestros registros de helloworld.

Puede ver un vídeo (en inglés) complementario en Agregar niveles.

Introducción

Joomla implementa elementos del menú y categorías como estructuras arbóreas. Esto significa, por ejemplo, que los elementos del menú en un menú pueden tener submenús fuera de ellos (como se puede ver en el menú de administración de Joomla) y esos subelementos del menú a su vez pueden tener submenús fuera de ellos, y así sucesivamente. Los submenús están ordenados, por lo que puede definir en qué orden aparecen los subelementos del menú. Alguna terminología:

  1. cada uno de nuestros registros es un nodo dentro de la estructura de árbol
  2. hay un nodo raíz que ocupa la posición más alta en la jerarquía; dentro de Joomla definimos este nodo raíz como nivel 0
  3. el nodo raíz tiene varios hijos, y estos estarán en el nivel 1. Cada uno de los padres de estos hijos es el nodo raíz
  4. estos niños a su vez pueden tener sus propios hijos, en el nivel 2, y así sucesivamente.
Nested Set Model
Node Left Right
Clothing 1 22
Men's 2 9
Suits 3 8
Slacks 4 5
Jackets 6 7
Women's 10 21
Dresses 11 16
Evening Gowns 12 13
Sun Dresses 14 15
Skirts 17 18
Blouses 19 20

Para implementar esta estructura de árbol dentro de una base de datos relacional, Joomla usa el Nested Set Model (texto en inglés). Esto implica asignar a cada registro un campo izquierdo (en Joomla lft) y derecho (en Joomla rgt), como se muestra en el diagrama y la tabla asociada. Deberías poder ver cómo, al usar los valores lft y rgt, se puede determinar para cualquier nodo su nivel en el árbol, su padre y para los nodos con el mismo padre, el orden de esos nodos.

Si bien los valores de lft y rgt pueden ser suficientes para definir un árbol completamente en teoría, en la práctica, agregar campos adicionales brinda más oportunidades para navegar por el árbol de manera eficiente, y en Joomla hay 5 campos asociados con una estructura de árbol:

  • campos lft y rgt
  • parent ID: el ID del nodo padre
  • level: el nivel en la jerarquía, el nivel 0 (Clothing en el ejemplo anterior) es el más alto
  • path; ruta

Path es como la ruta en una estructura de directorio, y es una combinación de los valores de alias que van desde el nodo raíz al nodo en cuestión, con una barra que separa los alias. Por ejemplo, refiriéndose al diagrama anterior, si Clothing es el nodo raíz, entonces la ruta para Jackets sería Men-s/Suits/Jackets.

Observa cómo los campos adicionales nos proporcionan mecanismos eficientes para navegar por el árbol. Por ejemplo, podemos hacer una consulta seleccionando registros DONDE los padres son Suits y ORDENAR por lft, y eso nos dará los Slacks y Jackets en orden. Este tipo de operaciones generalmente se realizan en el lado cliente, donde queremos que el sitio funcione bien. Por el contrario, las operaciones de administración pueden implicar muchas actualizaciones. Por ejemplo, si movemos Blouses para que sea el primer hijo bajo Men's, entonces esto implicará actualizar los valores lft y rgt de casi todos los registros en la tabla de la base de datos.

Como es de esperar, Joomla proporciona funciones de biblioteca para ayudar a ubicar un nuevo nodo en el árbol, reparar un elemento, eliminar un elemento (y todos sus elementos secundarios), reordenar elementos, etc.

Functionalidad

Primero, deseamos que nuestra instalación de este paso construya la estructura de árbol en nuestra tabla de helloworld.

En nuestra vista helloworlds de administración, mostraremos el nivel del registro con sangría del título, como se hace para las categorías y elementos del menú. También mostraremos los valores de nuestros nuevos campos, realmente para nuestros propios fines de diagnóstico, ya que no lo haría en una aplicación real.

En el paso anterior, introdujimos una funcionalidad que permitía al administrador arrastrar los registros para reordenarlos. Continuaremos usando este mecanismo, pero la reordenación se limitará a los registros que tengan el mismo padre (es decir, hermanos), y los registros deberán ordenarse en orden ascendente primero (la clasificación por "Orden descendente" no funcionará).

Si eliminamos un elemento, todos sus hijos se moverán hacia la jerarquía, para tener como padre el padre del registro eliminado.

En la vista de edición de administrador, permitiremos al administrador:

  • Especificar un nuevo padre para el artículo
  • Especificar el posicionamiento de este artículo dentro del orden de sus hermanos.

(aunque, si cambiamos el padre, los nuevos hermanos aparecerán solo después de Guardar).

También nos aseguraremos de que la funcionalidad Nuevo y Guardar como copia funcione como se espera.

En el lado cliente, cambiaremos la página que muestra un registro de HelloWorld para presentar también a los padres y los descendientes de ese registro.

También actualizaremos el formulario de lado cliente para que al crear un nuevo registro de helloworld también solicite al padre.

Enfoque

Inicialmente necesitaremos configurar nuestra base de datos como un árbol de conjunto anidado. Queremos que el registro con id=1 sea la raíz del árbol, por lo tanto, si hay un registro existente con id=1, le asignaremos un nuevo número y cambiaremos los registros asociados en la tabla de Activos y Asociaciones. Una vez que el registro raíz esté en su lugar, estableceremos los otros registros de helloworld como elementos secundarios directos de la raíz y estableceremos los nuevos campos en consecuencia. Como esto es demasiado complejo para hacer en el script SQL, codificaremos esto dentro del archivo de instalación script.php.

El código para realizar operaciones en los campos de la base de datos del conjunto anidado está dentro de JTableNested, por lo que cambiamos nuestra clase de table helloworld para heredarla de ella en lugar de JTable. Usaremos una serie de métodos dentro de esta clase para aplicar los cambios en la base de datos que surgen de las operaciones de administración:

  • setLocation($referenceId, $position) para definir cómo se posiciona el registro actual (para ordenar) con respecto a sus hermanos: usaremos esto cuando el administrador use el formulario de edición de helloworld y cambie el padre o el orden del registro con respecto a sus hermanos.
  • saveorder($idArray, $lft_array) para almacenar un orden revisado de hermanos: usaremos esto siempre que el administrador use la función de arrastrar registro para mover un registro
  • store($updateNulls) para almacenar un registro nuevo o actualizado: el método save() lo llamará en JModelAdmin
  • delete($pk, $children) para eliminar un registro y (opcionalmente) todos sus descendientes: usaremos esto cada vez que el administrador elimine uno o más registros de helloworld, y usaremos la opción de NO borrar a los descendientes.
  • rebuild(...) para recalcular los valores de lft, rgt y path para los registros de helloworld: usaremos esto después de reposicionar un registro cambiando su padre o reordenado en el formulario de edición.

En nuestro archivo de diseño de Helloworlds, la visualización de los nuevos campos es sencilla. Los otros cambios implican lo siguiente.

  1. Para sangrar los registros según el nivel (como se hace para los menús y categorías) usaremos el diseño estándar de joomla en layouts/joomla/html/treeprefix.php
  2. Para admitir el arrastre de las filas a reordenar, el código javascript en sortablelist.js que encontramos en el paso anterior Agregar Ordenamiento se usará nuevamente, pero para entregar el conjunto anidado especificamos diferentes parámetros a la función, y necesitamos agregar atributos diferentes a los elementos .

En nuestro diseño (editar) de Helloworld mostraremos los elementos de entrada a capturar

  • el registro padre: esta será una lista de todos los registros de hellowworld, pero excluyendo el propio registro (ya que un registro no puede tener su propio padre) y sus descendientes (ya que esto formaría un bucle en el árbol), y
  • su posición con respecto a los hermanos del registro - para dar salida a los hermanos en orden.

Debido a los requisitos específicos de estas listas de selección, ninguno de los dos puede implementarse utilizando un campo de formulario estándar de Joomla y XML, y definiremos un campo de formulario personalizado para cada uno y definiremos la lista de valores posibles de forma dinámica.

En el lado cliente, usaremos otra función de tabla anidada getTree() para encontrar los descendientes del registro que estamos mostrando. Y en el formulario del lado cliente para crear un nuevo registro, reutilizaremos el campo de formulario personalizado de administración para presentar la lista de registros para elegir un padre.

Donde en el paso del tutorial anterior usamos el campo de base de datos ordering para ordenar, en este paso usaremos el campo de base de datos lft. Así que eso es un cambio global tanto en nuestro código de administrador como en el de nuestro sitio.

Base de datos e instalación

Archivo de instalación de SQL actualizado:

admin/sql/install.mysql.utf8.sql

DROP TABLE IF EXISTS `#__helloworld`;

CREATE TABLE `#__helloworld` (
	`id`       INT(11)     NOT NULL AUTO_INCREMENT,
	`asset_id` INT(10)     NOT NULL DEFAULT '0',
	`created`  DATETIME    NOT NULL DEFAULT '0000-00-00 00:00:00',
	`created_by`  INT(10) UNSIGNED NOT NULL DEFAULT '0',
	`checked_out` INT(10) NOT NULL DEFAULT '0',
	`checked_out_time` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
	`greeting` VARCHAR(25) NOT NULL,
	`alias`  VARCHAR(40)  NOT NULL DEFAULT '',
	`language`  CHAR(7)  NOT NULL DEFAULT '*',
	`parent_id`	int(10)    NOT NULL DEFAULT '1',
	`level`	int(10)    NOT NULL DEFAULT '0',
	`path`	VARCHAR(400)    NOT NULL DEFAULT '',
	`lft`	int(11)    NOT NULL DEFAULT '0',
	`rgt`	int(11)    NOT NULL DEFAULT '0',
	`published` tinyint(4) NOT NULL DEFAULT '1',
	`catid`	    int(11)    NOT NULL DEFAULT '0',
	`params`   VARCHAR(1024) NOT NULL DEFAULT '',
	`image`   VARCHAR(1024) NOT NULL DEFAULT '',
	`latitude` DECIMAL(9,7) NOT NULL DEFAULT 0.0,
	`longitude` DECIMAL(10,7) NOT NULL DEFAULT 0.0,
	PRIMARY KEY (`id`)
)
	ENGINE =MyISAM
	AUTO_INCREMENT =0
	DEFAULT CHARSET =utf8;

CREATE UNIQUE INDEX `aliasindex` ON `#__helloworld` (`alias`, `catid`);

/*We added the published column to the INTO statement in order to set the published column, in the helloworld root record, to 1 otherwise you won't be able to publish items.*/

INSERT INTO `#__helloworld` (`greeting`,`alias`,`language`, `parent_id`, `level`, `path`, `lft`, `rgt`, `published`) VALUES
('helloworld root','helloworld-root-alias','en-GB', 0, 0, '', 0, 5, 1),
('Hello World!','hello-world','en-GB', 1, 1, 'hello-world', 1, 2, 0),
('Goodbye World!','goodbye-world','en-GB', 1, 1, 'goodbye-world', 3, 4, 0);

Para la actualización definimos la nueva estructura de la base de datos en el siguiente archivo ...

/admin/sql/updates/mysql/0.0.26.sql

ALTER TABLE `#__helloworld` DROP COLUMN `ordering`;
ALTER TABLE `#__helloworld` ADD COLUMN `parent_id` INT(10) NOT NULL DEFAULT '1' AFTER `language`;
ALTER TABLE `#__helloworld` ADD COLUMN `level`	int(10)    NOT NULL DEFAULT '0' AFTER `parent_id`;
ALTER TABLE `#__helloworld` ADD COLUMN `path`	varchar(400)    NOT NULL DEFAULT '' AFTER `level`;
ALTER TABLE `#__helloworld` ADD COLUMN `lft`	int(11)    NOT NULL DEFAULT '0' AFTER `path`;
ALTER TABLE `#__helloworld` ADD COLUMN `rgt`	int(11)    NOT NULL DEFAULT '0' AFTER `lft`;
UPDATE `#__helloworld` SET `path` = `alias`;

Pero para construir el árbol usamos un archivo de instalación script.php. Este código hace lo siguiente:

  1. comprueba si ya hay un registro raíz con id=1. Si es así, asume que el árbol se ha construido y sale sin cambiar nada.
  2. de lo contrario, si hay un registro ordinario con id=1, entonces cambia su id a uno más grande que el id máximo en la tabla. Luego cambia cualquier registro asociado en la tabla de Activos y en la tabla de Asociaciones. (Observamos cómo se creó la clave en la tabla de Asociaciones usando un hash md5 en Agregando Asociaciones. Si el registro de helloworld con id=1 que tuviera esta nueva identificación fuera un id original, entonces la clave de asociaciones formada por el hash md5 habría sido diferente, pero en realidad eso no importa. De hecho, cuando se realiza el hash md5, si las asociaciones en la matriz están en un orden diferente, entonces produciría una clave diferente de todos modos).
  3. crea el registro raíz, con id=1, y establece los valores de lft y rgt para él (según el número total de registros en la tabla).
  4. actualiza los valores lft y rgt para todos los registros de helloworld existentes en la tabla.

script.php

<?php
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * Script file of HelloWorld component.
 *
 * The name of this class is dependent on the component being installed.
 * The class name should have the component's name, directly followed by
 * the text InstallerScript (ex:. com_helloWorldInstallerScript).
 *
 * This class will be called by Joomla!'s installer, if specified in your component's
 * manifest file, and is used for custom automation actions in its installation process.
 *
 * In order to use this automation script, you should reference it in your component's
 * manifest file as follows:
 * <scriptfile>script.php</scriptfile>
 *
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
class com_helloWorldInstallerScript
{
    /**
     * This method is called after a component is installed.
     *
     * @param  \stdClass $parent - Parent object calling this method.
     *
     * @return void
     */
    public function install($parent) 
    {
        $parent->getParent()->setRedirectURL('index.php?option=com_helloworld');
    }

    /**
     * This method is called after a component is uninstalled.
     *
     * @param  \stdClass $parent - Parent object calling this method.
     *
     * @return void
     */
    public function uninstall($parent) 
    {
        echo '<p>' . JText::_('COM_HELLOWORLD_UNINSTALL_TEXT') . '</p>';
    }

    /**
     * This method is called after a component is updated.
     *
     * @param  \stdClass $parent - Parent object calling object.
     *
     * @return void
     */
    public function update($parent) 
    {
        echo '<p>' . JText::sprintf('COM_HELLOWORLD_UPDATE_TEXT', $parent->get('manifest')->version) . '</p>';
    }

    /**
     * Runs just before any installation action is preformed on the component.
     * Verifications and pre-requisites should run in this function.
     *
     * @param  string    $type   - Type of PreFlight action. Possible values are:
     *                           - * install
     *                           - * update
     *                           - * discover_install
     * @param  \stdClass $parent - Parent object calling object.
     *
     * @return void
     */
    public function preflight($type, $parent) 
    {
        echo '<p>' . JText::_('COM_HELLOWORLD_PREFLIGHT_' . $type . '_TEXT') . '</p>';
    }

    /**
     * Runs right after any installation action is preformed on the component.
     *
     * @param  string    $type   - Type of PostFlight action. Possible values are:
     *                           - * install
     *                           - * update
     *                           - * discover_install
     * @param  \stdClass $parent - Parent object calling object.
     *
     * @return void
     */
    function postflight($type, $parent) 
    {
		$db = JFactory::getDbo();
		
		echo '<p>Checking if the root record is already present ...</p>';
		
		$query = $db->getQuery(true);
		$query->select('id');
		$query->from('#__helloworld');
		$query->where('id = 1');
		$query->where('alias = "helloworld-root-alias"');
		$db->setQuery($query);
		$id = $db->loadResult();
		
		if ($id == '1')
		{   // assume tree structure already built
			echo '<p>Root record already present, install program exiting ...</p>';
			return;
		}

		echo '<p>Checking if there is a record with id = 1 ...</p>';
		
		$query = $db->getQuery(true);
		$query->select('id');
		$query->from('#__helloworld');
		$query->where('id = 1');
		$db->setQuery($query);
		$id = $db->loadResult();
			
		if ($id)
		{
			echo '<p>Record with id = 1 found</p>';
			
			// get new id
			$query = $db->getQuery(true)
				->select('max(id) + 1')
				->from('#__helloworld');
			$db->setQuery($query);
			$newid = $db->loadResult(); 
			echo "<p>Changing id to $newid</p>";
			
			// update id in helloworld table
			$query = $db->getQuery(true)
				->update('#__helloworld')
				->set("id = $newid")
				->where("id = $id");
			$db->setQuery($query);
			$result = $db->execute();
			if ($result)
			{
				$nrows = $db->getAffectedRows();
				echo "<p>Id in helloworld table changed, records updated: $nrows</p>";
			}
			else
			{
				echo "<p>Error: Id in helloworld table not changed</p>";
				var_dump($result);
			}
			
			// update id in the associations table
			$query = $db->getQuery(true)
				->update('#__associations')
				->set("id = $newid")
				->where("id = $id")
				->where('context = "com_helloworld.item"');
			$db->setQuery($query);
			$result = $db->execute();
			if ($result)
			{
				$nrows = $db->getAffectedRows();
				echo "<p>Id in associations table changed, records updated: $nrows</p>";
			}
			else
			{
				echo "<p>Error: Id in associations table not changed</p>";
				var_dump($result);
			}
			
			// update id in the assets table
			$query = $db->getQuery(true)
				->update('#__assets')
				->set('name = "com_helloworld.helloworld.' . $newid . '"')
				->where('name = "com_helloworld.helloworld.' . $id . '"');
			$db->setQuery($query);
			$result = $db->execute();
			if ($result)
			{
				$nrows = $db->getAffectedRows();
				echo "<p>Id in assets table changed, records updated: $nrows</p>";
			}
			else
			{
				echo "<p>Error: Id in assets table not changed</p>";
				var_dump($result);
			}
		}
		else 
		{
			echo '<p>No record with id = 1 found</p>';
		}
		
		// find number of records in helloworld table
		$query = $db->getQuery(true)
			->select('count(*)')
			->from('#__helloworld');
		$db->setQuery($query);
		$total = $db->loadResult(); 
		
		// insert root record
		$columns = array('id','greeting','alias','parent_id','rgt');
		$values = array(1, 'helloworld root','helloworld-root-alias',0, 2 * (int)$total + 1);

		$query = $db->getQuery(true)
			->insert('#__helloworld')
			->columns($db->quoteName($columns))
			->values(implode(',', $db->quote($values)));
		$db->setQuery($query);
		$result = $db->execute();
		if ($result)
		{
			$nrows = $db->getAffectedRows();
			echo "<p>$nrows inserted into helloworld table</p>";
		}
		else
		{
			echo "<p>Error creating root record</p>";
			var_dump($result);
		}
		
		// update lft and rgt for each of the other records (ie not root)
		$query = $db->getQuery(true)
			->select('id')
			->from('#__helloworld')
			->where('id > 1');
		$db->setQuery($query);
		$ids = $db->loadColumn(); 
		for ($i = 0; $i < $total; $i++)
		{
			$lft = 2 * (int)$i + 1;
			$rgt = 2 * (int)$i + 2;
			$query = $db->getQuery(true)
				->update('#__helloworld')
				->set("lft = {$lft}")
				->set("rgt = {$rgt}")
				->where("id = {$ids[$i]}");
			$db->setQuery($query);
			$result = $db->execute();
			if ($result)
			{
				$nrows = $db->getAffectedRows();
				echo "<p>$nrows updated in helloworld table, for id = {$ids[$i]}</p>";
			}
			else
			{
				echo "<p>Error updating record</p>";
				var_dump($result);
			}
		}
    }
}

MVC Helloworlds

Archivo de modelo actualizado para incluir los nuevos campos. El orden predeterminado se ha establecido para que se base en la estructura del árbol, ya que posiblemente sea más apropiado que basarse en el saludo.

admin/models/helloworlds.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorldList Model
 *
 * @since  0.0.1
 */
class HelloWorldModelHelloWorlds extends JModelList
{
        /**
         * Constructor.
         *
         * @param   array  $config  An optional associative array of configuration settings.
         *
         * @see     JController
         * @since   1.6
         */
        public function __construct($config = array())
        {
                if (empty($config['filter_fields']))
                {
                        $config['filter_fields'] = array(
                                'id',
                                'greeting',
                                'author',
                                'created',
                                'language',
                                'lft',
                                'category_id',
                                'association',
                                'published'
                        );
                }

                parent::__construct($config);
        }

        protected function populateState($ordering = 'lft', $direction = 'asc')
        {
                $app = JFactory::getApplication();

                // Adjust the context to support modal layouts.
                if ($layout = $app->input->get('layout'))
                {
                        $this->context .= '.' . $layout;
                }

                // Adjust the context to support forced languages.
                $forcedLanguage = $app->input->get('forcedLanguage', '', 'CMD');
                if ($forcedLanguage)
                {
                        $this->context .= '.' . $forcedLanguage;
                }

                parent::populateState($ordering, $direction);
        
                // If there's a forced language then define that filter for the query where clause
                if (!empty($forcedLanguage))
                {
                        $this->setState('filter.language', $forcedLanguage);
                }
        }

        /**
         * Method to build an SQL query to load the list data.
         *
         * @return      string  An SQL query
         */
        protected function getListQuery()
        {
                // Initialize variables.
                $db    = JFactory::getDbo();
                $query = $db->getQuery(true);

                // Create the base select statement.
                $query->select('a.id as id, a.greeting as greeting, a.published as published, a.created as created, 
                          a.checked_out as checked_out, a.checked_out_time as checked_out_time, a.catid as catid,
                          a.lft as lft, a.rgt as rgt, a.parent_id as parent_id, a.level as level, a.path as path,
                          a.image as imageInfo, a.latitude as latitude, a.longitude as longitude, a.alias as alias, a.language as language')
                          ->from($db->quoteName('#__helloworld', 'a'));

                // Join over the categories.
                $query->select($db->quoteName('c.title', 'category_title'))
                        ->join('LEFT', $db->quoteName('#__categories', 'c') . ' ON c.id = a.catid');
        
                // Join with users table to get the username of the author
                $query->select($db->quoteName('u.username', 'author'))
                        ->join('LEFT', $db->quoteName('#__users', 'u') . ' ON u.id = a.created_by');

                // Join with users table to get the username of the person who checked the record out
                $query->select($db->quoteName('u2.username', 'editor'))
                        ->join('LEFT', $db->quoteName('#__users', 'u2') . ' ON u2.id = a.checked_out');

                // Join with languages table to get the language title and image to display
                // Put these into fields called language_title and language_image so that 
                // we can use the little com_content layout to display the map symbol
                $query->select($db->quoteName('l.title', 'language_title') . "," .$db->quoteName('l.image', 'language_image'))
                        ->join('LEFT', $db->quoteName('#__languages', 'l') . ' ON l.lang_code = a.language');

                // Join over the associations - we just want to know if there are any, at this stage
                if (JLanguageAssociations::isEnabled())
                {
                        $query->select('COUNT(asso2.id)>1 as association')
                                ->join('LEFT', '#__associations AS asso ON asso.id = a.id AND asso.context=' . $db->quote('com_helloworld.item'))
                                ->join('LEFT', '#__associations AS asso2 ON asso2.key = asso.key')
                                ->group('a.id');
                }

                // Filter: like / search
                $search = $this->getState('filter.search');

                if (!empty($search))
                {
                        $like = $db->quote('%' . $search . '%');
                        $query->where('greeting LIKE ' . $like);
                }

                // Filter by published state
                $published = $this->getState('filter.published');

                if (is_numeric($published))
                {
                        $query->where('a.published = ' . (int) $published);
                }
                elseif ($published === '')
                {
                        $query->where('(a.published IN (0, 1))');
                }

                // Filter by language, if the user has set that in the filter field
                $language = $this->getState('filter.language');
                if ($language)
                {
                        $query->where('a.language = ' . $db->quote($language));
                }

                // Filter by categories
                $catid = $this->getState('filter.category_id');
                if ($catid)
                {
                        $query->where("a.catid = " . $db->quote($db->escape($catid)));
                }

                // exclude root helloworld record
                $query->where('a.id > 1');

                // Add the list ordering clause.
                $orderCol       = $this->state->get('list.ordering', 'lft');
                $orderDirn      = $this->state->get('list.direction', 'asc');

                $query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));

                return $query;
        }
}

En nuestro archivo de vista creamos un mapeo de ID de padres a los ID de sus hijos. Esto nos permite encontrar más fácilmente los padres sucesivos de un registro de la jerarquía, lo que haremos en el archivo de diseño.

admin/views/helloworlds/view.html.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorlds View
 *
 * @since  0.0.1
 */
class HelloWorldViewHelloWorlds extends JViewLegacy
{
        /**
         * Display the Hello World view
         *
         * @param   string  $tpl  The name of the template file to parse; automatically searches through the template paths.
         *
         * @return  void
         */
        function display($tpl = null)
        {
                // Get application
                $app = JFactory::getApplication();

                // Get data from the model
                $this->items                    = $this->get('Items');
                $this->pagination               = $this->get('Pagination');
                $this->state                    = $this->get('State');
                $this->filterForm       = $this->get('FilterForm');
                $this->activeFilters    = $this->get('ActiveFilters');
        
                // What Access Permissions does this user have? What can (s)he do?
                $this->canDo = JHelperContent::getActions('com_helloworld');

                // Check for errors.
                if (count($errors = $this->get('Errors')))
                {
                        JError::raiseError(500, implode('<br />', $errors));

                        return false;
                }
        
                // Set the sidebar submenu and toolbar, but not on the modal window
                if ($this->getLayout() !== 'modal')
                {
                        HelloWorldHelper::addSubmenu('helloworlds');
                        $this->addToolBar();
                }
                else
                {
                        // If it's being displayed to select a record as an association, then forcedLanguage is set
                        if ($forcedLanguage = $app->input->get('forcedLanguage', '', 'CMD'))
                        {
                                // Transform the language selector filter into an hidden field, so it can't be set
                                $languageXml = new SimpleXMLElement('<field name="language" type="hidden" default="' . $forcedLanguage . '" />');
                                $this->filterForm->setField($languageXml, 'filter', true);

                                // Also, unset the active language filter so the search tools is not open by default with this filter.
                                unset($this->activeFilters['language']);
                        }
                }

                // Prepare a mapping from parent id to the ids of its children
                $this->ordering = array();
                foreach ($this->items as $item)
                {
                        $this->ordering[$item->parent_id][] = $item->id;
                }

                // Display the template
                parent::display($tpl);

                // Set the document
                $this->setDocument();
        }

        /**
         * Add the page title and toolbar.
         *
         * @return  void
         *
         * @since   1.6
         */
        protected function addToolBar()
        {
                $title = JText::_('COM_HELLOWORLD_MANAGER_HELLOWORLDS');

                if ($this->pagination->total)
                {
                        $title .= "<span style='font-size: 0.5em; vertical-align: middle;'>(" . $this->pagination->total . ")</span>";
                }

                JToolBarHelper::title($title, 'helloworld');
                if ($this->canDo->get('core.create')) 
                {
                        JToolBarHelper::addNew('helloworld.add', 'JTOOLBAR_NEW');
                }
                if ($this->canDo->get('core.edit')) 
                {
                        JToolBarHelper::editList('helloworld.edit', 'JTOOLBAR_EDIT');
                }
                if ($this->canDo->get('core.delete')) 
                {
                        JToolBarHelper::deleteList('', 'helloworlds.delete', 'JTOOLBAR_DELETE');
                }
                if ($this->canDo->get('core.edit') || JFactory::getUser()->authorise('core.manage', 'com_checkin'))
                {
                        JToolBarHelper::checkin('helloworlds.checkin');
                }
                if ($this->canDo->get('core.admin')) 
                {
                        JToolBarHelper::divider();
                        JToolBarHelper::preferences('com_helloworld');
                }
        }
        /**
         * Method to set up the document properties
         *
         * @return void
         */
        protected function setDocument() 
        {
                $document = JFactory::getDocument();
                $document->setTitle(JText::_('COM_HELLOWORLD_ADMINISTRATION'));
        }
}

En nuestro archivo de diseño, incluimos nuevas columnas para los campos lft, rgt, level y parent (aunque con encabezados que son solo los nombres de los campos de la base de datos en lugar de cadenas traducidas), e incluimos la ruta en letras pequeñas debajo del alias debajo del saludo. También cambiamos los parámetros para el javascript en sortablelist.js que permite arrastrar filas para reordenar las filas que tienen el mismo padre.

Este código javascript oculta a todos los descendientes (no solo a los hijos inmediatos) cuando se realiza el arrastre, por lo que para cada fila hay un poco de trabajo para crear una lista de los padres sucesivos de esa fila, subiendo la jerarquía de árboles, que luego se coloca en el atributo parent del elemento <tr>. Cuando se arrastra una fila con id=xxx, el javascript oculta cualquier fila en la que aparezca el id xxx en este atributo de los padres.

admin/views/helloworlds/tmpl/default.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted Access');

use Joomla\Registry\Registry;

JHtml::_('formbehavior.chosen', 'select');

$listOrder     = $this->escape($this->state->get('list.ordering'));
$listDirn      = $this->escape($this->state->get('list.direction'));
$user = JFactory::getUser();
$userId = $user->get('id');
$saveOrder = ($listOrder == 'lft' && strtolower($listDirn) == 'asc');
if ($saveOrder)
{
        $saveOrderingUrl = 'index.php?option=com_helloworld&task=helloworlds.saveOrderAjax&tmpl=component';
        // pass true as parameter 7 to indicate that we have a nested set
        JHtml::_('sortablelist.sortable', 'helloworldList', 'adminForm', strtolower($listDirn), $saveOrderingUrl, false, true);
}
$assoc = JLanguageAssociations::isEnabled();
$authorFieldwidth = $assoc ? "10%" : "25%";
JLoader::register('JHtmlHelloworlds', JPATH_ADMINISTRATOR . '/components/com_helloworld/helpers/html/helloworlds.php');
?>
<form action="index.php?option=com_helloworld&view=helloworlds" method="post" id="adminForm" name="adminForm">
        <div id="j-sidebar-container" class="span2">
                <?php echo JHtmlSidebar::render(); ?>
        </div>
        <div id="j-main-container" class="span10">
        <div class="row-fluid">
            <div class="span6">
                <?php echo JText::_('COM_HELLOWORLD_HELLOWORLDS_FILTER'); ?>
                <?php
                    echo JLayoutHelper::render(
                        'joomla.searchtools.default',
                        array('view' => $this)
                    );
                ?>
            </div>
        </div>
        <table class="table table-striped table-hover" id="helloworldList">
            <thead>
            <tr>
                <th width="1%">
                    <?php echo JHtml::_('searchtools.sort', '', 'lft', $listDirn, $listOrder, null, 'asc', 'JGRID_HEADING_ORDERING', 'icon-menu-2'); ?>
                </th>
                <th width="1%"><?php echo JText::_('COM_HELLOWORLD_NUM'); ?></th>
                <th width="1%">
                    <?php echo JHtml::_('grid.checkall'); ?>
                </th>
                <th width="10%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_HELLOWORLDS_NAME', 'greeting', $listDirn, $listOrder); ?>
                </th>
                <th width="10%">
                    <?php echo JText::_('COM_HELLOWORLD_HELLOWORLDS_POSITION'); ?>
                </th>
                <th width="10%">
                    <?php echo JText::_('COM_HELLOWORLD_HELLOWORLDS_IMAGE'); ?>
                </th>
                <th width="5%">
                    <?php echo "lft"; ?>
                </th>
                <th width="5%">
                    <?php echo "rgt"; ?>
                </th>
                <th width="5%">
                    <?php echo "level"; ?>
                </th>
                <th width="5%">
                    <?php echo "parent"; ?>
                </th>
                <?php if ($assoc) : ?>
                    <th width="10%">
                        <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_HELLOWORLDS_ASSOCIATIONS', 'association', $listDirn, $listOrder); ?>
                    </th>
                <?php endif; ?>
                <th width="<?php echo $authorFieldwidth; ?>">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_AUTHOR', 'author', $listDirn, $listOrder); ?>
                </th>
                <th width="10%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_LANGUAGE', 'language', $listDirn, $listOrder); ?>
                </th>
                <th width="10%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_CREATED_DATE', 'created', $listDirn, $listOrder); ?>
                </th>
                <th width="5%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_PUBLISHED', 'published', $listDirn, $listOrder); ?>
                </th>
                <th width="2%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_ID', 'id', $listDirn, $listOrder); ?>
                </th>
            </tr>
            </thead>
            <tfoot>
                <tr>
                    <td colspan="5">
                        <?php echo $this->pagination->getListFooter(); ?>
                    </td>
                </tr>
            </tfoot>
            <tbody>
                <?php if (!empty($this->items)) : ?>
                    <?php foreach ($this->items as $i => $row) :
                        $link = JRoute::_('index.php?option=com_helloworld&task=helloworld.edit&id=' . $row->id);
                        $row->image = new Registry;
                        $row->image->loadString($row->imageInfo);
                        // create a list of the parents up the hierarchy to the root 
                        if ($row->level > 1)
                        {
                            $parentsStr = '';
                            $_currentParentId = $row->parent_id;
                            $parentsStr = ' ' . $_currentParentId;
                            for ($j = 0; $j < $row->level; $j++)
                            {
                                foreach ($this->ordering as $k => $v)
                                {
                                    $v = implode('-', $v);
                                    $v = '-' . $v . '-';
                                    if (strpos($v, '-' . $_currentParentId . '-') !== false)
                                    {
                                        $parentsStr .= ' ' . $k;
                                        $_currentParentId = $k;
                                        break;
                                    }
                                }
                            }
                        }
                        else
                        {
                            $parentsStr = '';
                        }
                    ?>
                        <tr class="row<?php echo $i % 2; ?>" sortable-group-id="<?php echo $row->parent_id; ?>" item-id="<?php echo $row->id; ?>" parents="<?php echo $parentsStr; ?>" level="<?php echo $row->level; ?>">
                            <td><?php
                                $iconClass = '';
                                $canReorder  = $user->authorise('core.edit.state', 'com_helloworld.helloworld.' . $row->id);
                                if (!$canReorder)
                                {
                                    $iconClass = ' inactive';
                                }
                                elseif (!$saveOrder)
                                {
                                    $iconClass = ' inactive tip-top hasTooltip" title="' . JHtml::_('tooltipText', 'JORDERINGDISABLED');
                                }
                                ?>
                                <span class="sortable-handler<?php echo $iconClass ?>">
                                    <span class="icon-menu" aria-hidden="true"></span>
                                </span>
                                <?php if ($canReorder && $saveOrder) : ?>
                                    <input type="text" style="display:none" name="order[]" size="5" value="<?php echo $row->lft; ?>" class="width-20 text-area-order" />
                                <?php endif; ?>
                            </td>
                            <td><?php echo $this->pagination->getRowOffset($i); ?></td>
                            <td>
                                <?php echo JHtml::_('grid.id', $i, $row->id); ?>
                            </td>
                            <td>
                                <?php $prefix = JLayoutHelper::render('joomla.html.treeprefix', array('level' => $row->level)); ?>
                                <?php echo $prefix; ?>
                                <?php if ($row->checked_out) : ?>
                                    <?php $canCheckin = $user->authorise('core.manage', 'com_checkin') || $row->checked_out == $userId; ?>
                                    <?php echo JHtml::_('jgrid.checkedout', $i, $row->editor, $row->checked_out_time, 'helloworlds.', $canCheckin); ?>
                                <?php endif; ?>
                                <a href="<?php echo $link; ?>" title="<?php echo JText::_('COM_HELLOWORLD_EDIT_HELLOWORLD'); ?>">
                                    <?php echo $row->greeting; ?>
                                </a>
                                <span class="small break-word">
                                        <?php echo JText::sprintf('JGLOBAL_LIST_ALIAS', $this->escape($row->alias)); ?>
                                </span>
                                <div class="small">
                                    <?php echo JText::_('JCATEGORY') . ': ' . $this->escape($row->category_title); ?>
                                </div>
                                <div class="small">
                                    <?php echo 'Path: ' . $this->escape($row->path); ?>
                                </div>
                            </td>
                            <td align="center">
                                <?php echo "[" . $row->latitude . ", " . $row->longitude . "]"; ?>
                            </td>
                            <td align="center">
                                <?php
                                    $caption = $row->image->get('caption') ? : '' ;
                                    $src = JURI::root() . ($row->image->get('image') ? : '' );
                                    $html = '<p class="hasTooltip" style="display: inline-block" data-html="true" data-toggle="tooltip" data-placement="right" title="<img width=\'100px\' height=\'100px\' src=\'%s\'>">%s</p>';
                                    echo sprintf($html, $src, $caption);  ?>
                            </td>
                            <td align="center">
                                <?php echo $row->lft; ?>
                            </td>
                            <td align="center">
                                <?php echo $row->rgt; ?>
                            </td>
                            <td align="center">
                                <?php echo $row->level; ?>
                            </td>
                            <td align="center">
                                <?php echo $row->parent_id; ?>
                            </td>
                            <?php if ($assoc) : ?>
                                <td align="center">
                                    <?php if ($row->association) : ?>
                                        <?php echo JHtml::_('helloworlds.association', $row->id); ?>
                                    <?php endif; ?>
                                </td>
                            <?php endif; ?>
                            <td align="center">
                                <?php echo $row->author; ?>
                            </td>
                            <td align="center">
                                <?php echo JLayoutHelper::render('joomla.content.language', $row); ?>
                            </td>
                            <td align="center">
                                <?php echo substr($row->created, 0, 10); ?>
                            </td>
                            <td align="center">
                                <?php echo JHtml::_('jgrid.published', $row->published, $i, 'helloworlds.', true, 'cb'); ?>
                            </td>
                            <td align="center">
                                <?php echo $row->id; ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                <?php endif; ?>
            </tbody>
        </table>
        <input type="hidden" name="task" value=""/>
        <input type="hidden" name="boxchecked" value="0"/>
        <?php echo JHtml::_('form.token'); ?>
    </div>
</form>

También debemos cambiar la opción "ordenar" dentro de los campos de filtro para usar el campo de base de datos lft en lugar del campo ordenar que teníamos en el paso anterior, y establecer el valor predeterminado ordenando por lft.

admin/models/forms/filter_helloworlds.xml

<?xml version="1.0" encoding="utf-8"?>
<form>
	<fields name="filter">
		<field
			name="search"
			type="text"
			label="COM_BANNERS_SEARCH_IN_TITLE"
			hint="JSEARCH_FILTER"
			class="js-stools-search-string"
		/>
		<field
			name="published"
			type="status"
			label="JOPTION_SELECT_PUBLISHED"
			description="JOPTION_SELECT_PUBLISHED_DESC"
			onchange="this.form.submit();"
			>
			<option value="">JOPTION_SELECT_PUBLISHED</option>
		</field>
		<field
			name="language"
			type="contentlanguage"
			label="JOPTION_FILTER_LANGUAGE"
			description="JOPTION_FILTER_LANGUAGE_DESC"
			onchange="this.form.submit();"
			>
			<option value="">JOPTION_SELECT_LANGUAGE</option>
			<option value="*">JALL</option>
		</field>
		<field
			name="category_id"
			type="category"
			label="JOPTION_FILTER_CATEGORY"
			extension="com_helloworld"
			onchange="this.form.submit();"
			published="0,1,2"
			>
			<option value="">JOPTION_SELECT_CATEGORY</option>
		</field>
	</fields>
	<fields name="list">
		<field
			name="fullordering"
			type="list"
			label="COM_HELLOWORLD_LIST_FULL_ORDERING"
			description="COM_HELLOWORLD_LIST_FULL_ORDERING_DESC"
			onchange="this.form.submit();"
			default="lft ASC"
			>
			<option value="">JGLOBAL_SORT_BY</option>
			<option value="lft ASC">COM_HELLOWORLD_ORDERING_ASC</option>
			<option value="lft DESC">COM_HELLOWORLD_ORDERING_DESC</option>
			<option value="greeting ASC">COM_HELLOWORLD_GREETING_ASC</option>
			<option value="greeting DESC">COM_HELLOWORLD_GREETING_DESC</option>
			<option value="id ASC">JGRID_HEADING_ID_ASC</option>
			<option value="id DESC">JGRID_HEADING_ID_DESC</option>
			<option value="published ASC">COM_HELLOWORLD_PUBLISHED_ASC</option>
			<option value="published DESC">COM_HELLOWORLD_PUBLISHED_DESC</option>
			<option value="author ASC">COM_HELLOWORLD_AUTHOR_ASC</option>
			<option value="author DESC">COM_HELLOWORLD_AUTHOR_DESC</option>
			<option value="created ASC">COM_HELLOWORLD_CREATED_ASC</option>
			<option value="created DESC">COM_HELLOWORLD_CREATED_DESC</option>
			<option value="language ASC">COM_HELLOWORLD_LANGUAGE_ASC</option>
			<option value="language DESC">COM_HELLOWORLD_LANGUAGE_DESC</option>
			<option value="association ASC">COM_HELLOWORLD_ASSOCIATION_ASC</option>
			<option value="association DESC">COM_HELLOWORLD_ASSOCIATION_DESC</option>
		</field>
		<field
			name="limit"
			type="limitbox"
			class="input-mini"
			default="25"
			label="COM_CONTENT_LIST_LIMIT"
			description="COM_HELLOWORLD_LIST_LIMIT_DESC"
			onchange="this.form.submit();"
		/>
	</fields>
</form>

Formulario de Edición de Helloworld

El formulario se actualiza para incluir los 2 nuevos elementos de entrada para capturar el padre y el orden.

El campo helloworldparent dará lugar a que se pase un parámetro jform['parent_id'] en el HTTP POST desde el formulario, y esto se asignará al campo parent_id del registro .

El campo helloworldordering dará como resultado un parámetro jform['helloworldordering'] que contiene el id del hermano por debajo del cual se debe colocar este registro, o un valor especial de -1 (para establecerlo como el primer hijo ) o -2 (para configurarlo como el último hijo). Este campo no se asigna a un campo en la base de datos, pero se usará para ubicar la posición del registro editado.

admin/models/forms/helloworld.xml

<?xml version="1.0" encoding="utf-8"?>
<form
				addrulepath="/administrator/components/com_helloworld/models/rules"
>
	<fieldset
				name="details"
				label="COM_HELLOWORLD_HELLOWORLD_DETAILS"
	>
		<field
				name="id"
				type="hidden"
				/>
		<field
				name="greeting"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_GREETING_DESC"
				size="40"
				class="inputbox validate-greeting"
				validate="greeting"
				required="true"
				default=""
				/>
		<field 
				name="alias" 
				type="text" 
				label="JFIELD_ALIAS_LABEL"
				description="JFIELD_ALIAS_DESC"
				hint="JFIELD_ALIAS_PLACEHOLDER"
				size="40" 
				/>
		<field
				name="catid"
				type="category"
				extension="com_helloworld"
				class="inputbox"
				default=""
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC"
				required="true"
		>
				<option value="0">JOPTION_SELECT_CATEGORY</option>
		</field>
		<field
				name="latitude"
				type="number"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_LATITUDE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_LATITUDE_DESC"
				min="-90.0"
				max="90.0"
				class="inputbox"
				required="true"
				default="0.0"
				/>
		<field
				name="longitude"
				type="number"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_LONGITUDE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_LONGITUDE_DESC"
				min="-180.0"
				max="180.0"
				class="inputbox"
				required="true"
				default="0.0"
				/>
		<field
				name="language" 
				type="contentlanguage" 
				label="JFIELD_LANGUAGE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_LANGUAGE_DESC"
				>
				<option value="*">JALL</option>
		</field>
		<field
				name="parent_id"
				type="helloworldparent"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_PARENT_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_PARENT_DESC"
				default="1"
				filter="int">
		</field>
		<field
				name="helloworldordering"
				type="helloworldordering"
				label="JFIELD_ORDERING_LABEL"
				description="JFIELD_ORDERING_DESC"
				filter="int"
				size="1">
		</field>
	</fieldset>
	<fields name="imageinfo">
		<fieldset
			name="image-info"
			label="COM_HELLOWORLD_IMAGE_FIELDS"
		>
			<field
				name="image"
				type="media"
				preview="tooltip"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_IMAGE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_IMAGE_DESC" />
			<field name="alt"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_ALT_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_ALT_DESC"
				size="30"/>
			<field name="caption"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTION_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTION_DESC"
				size="30"/>
		</fieldset>
	</fields>
	<fields name="params">
		<fieldset
				name="params"
				label="JGLOBAL_FIELDSET_DISPLAY_OPTIONS"
		>
			<field
					name="show_category"
					type="list"
					label="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL"
					description="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC"
					default=""
			>
				<option value="">JGLOBAL_USE_GLOBAL</option>
				<option value="0">JHIDE</option>
				<option value="1">JSHOW</option>
			</field>
		</fieldset>
	</fields>
	<fieldset
			name="accesscontrol"
			label="COM_HELLOWORLD_FIELDSET_RULES"
	>
    	<field
				name="asset_id"
				type="hidden"
				filter="unset"
				/>
    	<field
				name="rules"
				type="rules"
				label="COM_HELLOWORLD_FIELD_RULES_LABEL"
				filter="rules"
				validate="rules"
				class="inputbox"
				component="com_helloworld"
				section="message"
				/>
    </fieldset>
</form>

Los dos nuevos elementos de entrada son campos personalizados, por lo que tendremos que proporcionar definiciones de ellos en dos archivos nuevos dentro del directorio admin/models/fields. En primer lugar el padre:

admin/models/fields/helloworldparent.php

<?php
/**
 * Class associated with displaying an input field to capture the parent of a helloworld record
 */

defined('JPATH_BASE') or die;

JFormHelper::loadFieldClass('list');

class JFormFieldHelloworldParent extends JFormFieldList
{
	protected $type = 'HelloworldParent';

	/**
	 * Method to return the field options for the parent
	 *
	 */
	protected function getOptions()
	{
		$options = array();

		$db = JFactory::getDbo();
		$query = $db->getQuery(true)
			->select('DISTINCT(a.id) AS value, a.greeting AS text, a.level, a.lft')
			->from('#__helloworld AS a');
		
		// Prevent parenting to children of this record, or to itself
		// If this record has lft = x and rgt = y, then its children have lft > x and rgt < y
		if ($id = $this->form->getValue('id'))
		{
			$query->join('LEFT', $db->quoteName('#__helloworld') . ' AS h ON h.id = ' . (int) $id)
				->where('NOT(a.lft >= h.lft AND a.rgt <= h.rgt)');
		}
		
		$query->order('a.lft ASC');
		
		$db->setQuery($query);

		try
		{
			$options = $db->loadObjectList();
		}
		catch (RuntimeException $e)
		{
			JError::raiseWarning(500, $e->getMessage());
		}
		
		// Pad the option text with spaces using depth level as a multiplier.
		for ($i = 0; $i < count($options); $i++)
		{
			$options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->text;
		}

		// Merge any additional options in the XML definition.
		$options = array_merge(parent::getOptions(), $options);

		return $options;

	}
}

Y en segundo lugar, el ordenamiento.

admin/models/fields/helloworldordering.php

<?php
/**
 * Class for displaying the Ordering field in the helloworld edit layout
 */

defined('JPATH_BASE') or die;

JFormHelper::loadFieldClass('list');

class JFormFieldHelloworldOrdering extends JFormFieldList
{
	protected $type = 'HelloworldOrdering';

	/**
	 * Method to return the options for ordering the helloworld record
	 * This is the list of siblings the record's siblings - ie those records with the same parent.
	 * The method requires that parent id be set.
	 */
	protected function getOptions()
	{
		$options = array();

		// Get the parent
		$parent_id = $this->form->getValue('parent_id', 0);

		if (empty($parent_id))
		{
			return false;
		}

		$db = JFactory::getDbo();
		$query = $db->getQuery(true)
			->select('a.id AS value, a.greeting AS text')
			->from('#__helloworld AS a')
			->where('a.parent_id =' . (int) $parent_id);

		$query->order('a.lft ASC');

		// Get the options.
		$db->setQuery($query);

		try
		{
			$options = $db->loadObjectList();
		}
		catch (RuntimeException $e)
		{
			JError::raiseWarning(500, $e->getMessage());
		}

		$options = array_merge(
			array(array('value' => '-1', 'text' => JText::_('COM_HELLOWORLD_ITEM_FIELD_ORDERING_VALUE_FIRST'))),
			$options,
			array(array('value' => '-2', 'text' => JText::_('COM_HELLOWORLD_ITEM_FIELD_ORDERING_VALUE_LAST')))
		);

		// Merge any additional options in the XML definition.
		$options = array_merge(parent::getOptions(), $options);

		return $options;
	}

	/**
	 * Method to get the field input markup.
	 *
	 * @return  string  The field input markup.
	 *
	 * This method returns the input element except if a new record is being created, in which case a text string is output
	 */
	protected function getInput()
	{
		if ($this->form->getValue('id', 0) == 0)
		{
			return '<span class="readonly">' . JText::_('COM_HELLOWORLD_ITEM_FIELD_ORDERING_TEXT') . '</span>';
		}
		else
		{
			return parent::getInput();
		}
	}
}

Gestión de actualizaciones de registros del administrador

Nuestro modelo de Helloworld y el código de la tabla de Helloworld manejan juntos las actualizaciones que surgen de las operaciones de administración en el formulario de edición de helloworld y en la vista de helloworlds. Con referencia a los dos archivos actualizados a continuación, aquí se explica cómo se procesan estas actualizaciones.

Arrastrando una fila para reordenar

  • El arrastre se maneja dentro del código javascript en sortablelist.js, y el orden actualizado se envía al servidor a través de una llamada Ajax a task=helloworlds.saveOrderAjax.
  • El código dentro del método saveOrderAjax() del Controlador Admin llama a nuestra función de modelo saveorder().
  • Nuestro modelo saveorder() a su vez llama a saveorder() dentro de JTableNested
  • JTableNested::saveorder () ejecuta las actualizaciones de la base de datos.

Eliminar registros

  • Al hacer clic en el botón Eliminar en la vista helloworlds, se genera un POST HTTP con task=helloworlds.delete y la lista de identificadores de registros que se eliminan (de las casillas de verificación que se han marcado)
  • El delete() controlador del administrador llama a la función delete() en el modelo, pasando la matriz de identificadores que se eliminarán.
  • Como no tenemos una función específica delete() en nuestro modelo de helloworld, esto hace que se llame al modelo de administración principal delete(). Esta función obtiene la instancia de la table del modelo y para cada ID de la matriz llama al método delete() de table, pasando la ID como único parámetro.
  • El método delete() de table que se invoca es delete() en nuestra clase Table de Helloworld. Hemos anulado el método delete() de JNestedTable porque si solo se pasa un solo parámetro a JNestedTable::delete(), se eliminan los elementos hijos del registro. Lo anulamos para que el valor predeterminado NO sea eliminar los elementos hijos.
  • JNestedTable::delete() borra el registro y mueve a cada uno de sus hijos en la jerarquía, para que se convierta en el padre del registro eliminado.

Editar registro para cambiar el padre

  • Al hacer clic en Guardar, se genera un post HTTP con task=helloworld.save que maneja JControllerForm::save(). Esta función llama a save() del modelo.
  • Esto invoca a nuestro save() del modelo helloworld que llama a parent::save() que está en JModelAdmin.
    • JModelAdmin::save() llama a load() de table para cargar los valores existentes, luego a bind() de table para enlazar los nuevos valores
      • Esto invoca nuestra función bind() de table de Helloworld, que verifica si el nuevo padre es el mismo que el padre existente, y cuando lo encuentra, no llama a setLocation() de table para establecer la posición como el último hijo de ese padre
      • JTableNested::setLocation() almacena el padre y la posición (es decir, 'último hijo') en variables de instancia
    • De vuelta en JModelAdmin::save() llama a store() de table.
    • Esto invoca a JTableNested::store() que mueve el registro editado y todos sus descendientes bajo el nuevo registro principal, basado en las variables de instancia previamente almacenadas. Esto corrige los valores lft y rgt de todos los registros en la tabla de base de datos de helloworld. Sin embargo, no corrige el campo de ruta de los registros que se han movido.
  • De vuelta en save() de nuestro modelo de helloworld, llamamos al método rebuild() de JTableNested para reconstruir la tabla de la base de datos completa. Esto es más bien un método básico para arreglar los campos de ruta de los registros movidos. Sin embargo, si no usamos en nuestro código el campo de ruta, podemos omitir este paso.

Editar registro para cambiar el orden (pero no el padre)

  • Al hacer clic en Guardar se crea una publicación HTTP con task=helloworld.save y jform['helloworldordering'] configurada con el id del campo debajo del cual estamos ubicando este registro (o establecido en -1 (primer hijo) o -2 (último hijo ) como casos especiales). Este POST es manejado por JControllerForm::save() que llama a save() del modelo.
  • Esto invoca a nuestro save() del modelo helloworld que llama a parent::save() que está en JModelAdmin.
    • JModelAdmin::save() llama a load() de table para cargar los valores existentes, luego a bind() de table para enlazar los nuevos valores
      • Esto invoca nuestra función bind() de table Helloworld, que verifica si el nuevo padre es el mismo que el padre existente, y cuando lo encuentra, llama a setLocation() de table para establecer la posición posterior de la identificación del registro en el elemento del array 'helloworldordering' (y manejo de los casos especiales de -1 o -2).
      • JTableNested::setLocation() almacena el padre (sin cambios) y la posición en variables de instancia
    • De vuelta en JModelAdmin::save() llama a store() de table.
    • Esto invoca a JTableNested::store() que mueve el registro editado en función de la nueva posición almacenada en su variable de instancia. Esto corrige los valores lft y rgt de todos los registros en la tabla de helloworld de la base de datos.
  • De vuelta en nuestro save() del modelo de helloworld, llamamos al método rebuild() de JTableNested para reconstruir la tabla de la base de datos completa. ¡Esto es muy ineficiente ya que no hay necesidad de reconstruir las rutas en este caso!

Nuevo y Guardar como Copia

  • Ambos resultados dan como resultado un POST HTTP que se maneja con JControllerForm::save() que llama a save () del modelo. En el array de datos para el nuevo registro, el ID no está establecido o está configurado en 0.
  • Esto invoca a nuestro save() del modelo de helloworld que llama a parent::save() que está en JModelAdmin.
    • JModelAdmin::save() llama a la tabla load() para cargar los valores existentes, luego a bind() de table para enlazar los nuevos valores
      • Esto invoca a nuestra función bind() table en Helloworld, que verifica el elemento 'id' de la matriz para ver si es un nuevo registro. Cuando lo encuentra, llama a setLocation() de table para establecer la posición como el último elemento secundario del registro definido en el elemento del array 'parent_id'.
      • JTableNested::setLocation() almacena la posición principal y (la última secundaria) en variables de instancia
    • De vuelta en JModelAdmin::save() se llama a store() de table.
    • Esto invoca a JTableNested::store() que actualiza los campos lft y rgt en la tabla para dejar espacio para que se inserte el nuevo registro en la posición especificada por las variables de instancia establecidas en la llamada a setLocation(). Esto corrige los valores lft y rgt de todos los registros en la tabla de helloworld de la base de datos, pero no establece la ruta de nuestro nuevo registro.
  • De vuelta en nuestro save() del modelo en helloworld, llamamos al método rebuild() de JTableNested para reconstruir la tabla de la base de datos completa. De nuevo, esto es muy ineficiente, ya que, en realidad, ¡solo deberíamos estar llamando al método rebuildPath() de JTableNested para establecer la ruta del nuevo registro!

Abordar el problema de reconstrucción Las ineficiencias asociadas con tener que llamar al método rebuild() son significativas, particularmente porque hace que toda la tabla de helloworld se procese para todas las actualizaciones de registros, incluso si la actualización no se preocupa por ajustar la posición de un registro en el árbol. Hay varias alternativas para abordar este problema:

  • Si no usa el campo de ruta en su código, entonces puede ignorarlo y no molestarse en llamar a rebuild() para solucionarlo.
  • Como se describe en el vídeo que acompaña a este paso del tutorial, se puede seguir el ejemplo de com_content y colocar el código de save() en el modelo de helloworld, en lugar de llamar al modelo de administración padre save() para hacer el trabajo.
  • En el modelo de helloworld, save() para una actualización de registro, puede cargar el registro existente desde la base de datos, comparar su id. Principal con los datos del formulario y llamar a rebuild() solo si la id padre es diferente.

Nuestro modelo actualizado está abajo. Ten en cuenta que el código ha sido eliminado de prepareTable() ya que la definición del orden de los registros ahora se realiza en el método bind() de nuestra clase table de helloworld.

admin/models/helloworld.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

use Joomla\Registry\Registry;

/**
 * HelloWorld Model
 *
 * @since  0.0.1
 */
class HelloWorldModelHelloWorld extends JModelAdmin
{
	// JModelAdmin needs to know this for storing the associations 
	protected $associationsContext = 'com_helloworld.item';

	/**
	 * Method to override getItem to allow us to convert the JSON-encoded image information
	 * in the database record into an array for subsequent prefilling of the edit form
	 * We also use this method to prefill the associations
	 */
	public function getItem($pk = null)
	{
		$item = parent::getItem($pk);
		if ($item AND property_exists($item, 'image'))
		{
			$registry = new Registry($item->image);
			$item->imageinfo = $registry->toArray();
		}

		// Load associated items
		if (JLanguageAssociations::isEnabled())
		{
			$item->associations = array();

			if ($item->id != null)
			{
				$associations = JLanguageAssociations::getAssociations('com_helloworld', '#__helloworld', 'com_helloworld.item', (int)$item->id);

				foreach ($associations as $tag => $association)
				{
					$item->associations[$tag] = $association->id;
				}
			}
		}
		return $item; 
	}

	/**
	 * Method to get a table object, load it if necessary.
	 *
	 * @param   string  $type    The table name. Optional.
	 * @param   string  $prefix  The class prefix. Optional.
	 * @param   array   $config  Configuration array for model. Optional.
	 *
	 * @return  JTable  A JTable object
	 *
	 * @since   1.6
	 */
	public function getTable($type = 'HelloWorld', $prefix = 'HelloWorldTable', $config = array())
	{
		return JTable::getInstance($type, $prefix, $config);
	}

	/**
	 * Method to get the record form.
	 *
	 * @param   array    $data      Data for the form.
	 * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
	 *
	 * @return  mixed    A JForm object on success, false on failure
	 *
	 * @since   1.6
	 */
	public function getForm($data = array(), $loadData = true)
	{
		// Get the form.
		$form = $this->loadForm(
			'com_helloworld.helloworld',
			'helloworld',
			array(
				'control' => 'jform',
				'load_data' => $loadData
			)
		);

		if (empty($form))
		{
			return false;
		}

		return $form;
	}

	/**
	 * Method to preprocess the form to add the association fields dynamically
	 *
	 * @return     none
	 */
	protected function preprocessForm(JForm $form, $data, $group = 'helloworld')
	{
		// Association content items
		if (JLanguageAssociations::isEnabled())
		{
			$languages = JLanguageHelper::getContentLanguages(false, true, null, 'ordering', 'asc');

			if (count($languages) > 1)
			{
				$addform = new SimpleXMLElement('<form />');
				$fields = $addform->addChild('fields');
				$fields->addAttribute('name', 'associations');
				$fieldset = $fields->addChild('fieldset');
				$fieldset->addAttribute('name', 'item_associations');

				foreach ($languages as $language)
				{
					$field = $fieldset->addChild('field');
					$field->addAttribute('name', $language->lang_code);
					$field->addAttribute('type', 'modal_helloworld');
					$field->addAttribute('language', $language->lang_code);
					$field->addAttribute('label', $language->title);
					$field->addAttribute('translate_label', 'false');
				}

				$form->load($addform, false);
			}
		}
		parent::preprocessForm($form, $data, $group);
	}

	/**
	 * Method to get the script to be included on the form
	 *
	 * @return string	Script files
	 */
	public function getScript() 
	{
		return 'administrator/components/com_helloworld/models/forms/helloworld.js';
	}

	/**
	 * Method to get the data that should be injected in the form.
	 *
	 * @return  mixed  The data for the form.
	 *
	 * @since   1.6
	 */
	protected function loadFormData()
	{
		// Check the session for previously entered form data.
		$data = JFactory::getApplication()->getUserState(
			'com_helloworld.edit.helloworld.data',
			array()
		);

		if (empty($data))
		{
			$data = $this->getItem();
		}

		return $data;
	}
	/**
	 * Method to override the JModelAdmin save() function to handle Save as Copy correctly
	 *
	 * @param   The helloworld record data submitted from the form.
	 *
	 * @return  parent::save() return value
	 */
	public function save($data)
	{
		$input = JFactory::getApplication()->input;

		JLoader::register('CategoriesHelper', JPATH_ADMINISTRATOR . '/components/com_categories/helpers/categories.php');

		// Validate the category id
		// validateCategoryId() returns 0 if the catid can't be found
		if ((int) $data['catid'] > 0)
		{
			$data['catid'] = CategoriesHelper::validateCategoryId($data['catid'], 'com_helloworld');
		}

		// Alter the greeting and alias for save as copy
		if ($input->get('task') == 'save2copy')
		{
			$origTable = clone $this->getTable();
			$origTable->load($input->getInt('id'));

			if ($data['greeting'] == $origTable->greeting)
			{
				list($greeting, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['greeting']);
				$data['greeting'] = $greeting;
				$data['alias'] = $alias;
			}
			else
			{
				if ($data['alias'] == $origTable->alias)
				{
					$data['alias'] = '';
				}
			}
			// standard Joomla practice is to set the new record as unpublished
			$data['published'] = 0;
		}

		$result = parent::save($data);
		if ($result)
		{
			$this->getTable()->rebuild(1);
		}
		return $result;
	}
	/**
	 * Method to check if it's OK to delete a message. Overrides JModelAdmin::canDelete
	 */
	protected function canDelete($record)
	{
		if( !empty( $record->id ) )
		{
			return JFactory::getUser()->authorise( "core.delete", "com_helloworld.helloworld." . $record->id );
		}
	}
	/**
	 * Prepare a helloworld record for saving in the database
	 */
	protected function prepareTable($table)
	{
	}

	/**
	 * Save the record reordering after a record is dragged to a new position in the helloworlds view
	 */
	public function saveorder($idArray = null, $lft_array = null)
	{
		// Get an instance of the table object.
		$table = $this->getTable();

		if (!$table->saveorder($idArray, $lft_array))
		{
			$this->setError($table->getError());

			return false;
		}

		return true;
	}
}

admin/tables/helloworld.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
// No direct access
defined('_JEXEC') or die('Restricted access');

/**
 * Hello Table class
 *
 * @since  0.0.1
 */
class HelloWorldTableHelloWorld extends JTableNested
{
	/**
	 * Constructor
	 *
	 * @param   JDatabaseDriver  &$db  A database connector object
	 */
	function __construct(&$db)
	{
		parent::__construct('#__helloworld', 'id', $db);
	}
	/**
	 * Overloaded bind function
	 *
	 * @param       array           named array
	 * @return      null|string     null is operation was satisfactory, otherwise returns an error
	 * @see JTable:bind
	 * @since 1.5
	 */
	public function bind($array, $ignore = '')
	{
		if (isset($array['params']) && is_array($array['params']))
		{
			// Convert the params field to a string.
			$parameter = new JRegistry;
			$parameter->loadArray($array['params']);
			$array['params'] = (string)$parameter;
		}

		if (isset($array['imageinfo']) && is_array($array['imageinfo']))
		{
			// Convert the imageinfo array to a string.
			$parameter = new JRegistry;
			$parameter->loadArray($array['imageinfo']);
			$array['image'] = (string)$parameter;
		}

		// Bind the rules.
		if (isset($array['rules']) && is_array($array['rules']))
		{
			$rules = new JAccessRules($array['rules']);
			$this->setRules($rules);
		}

		if (isset($array['parent_id']))
		{
			if (!isset($array['id']) || $array['id'] == 0)
			{   // new record
				$this->setLocation($array['parent_id'], 'last-child');
			}
			elseif (isset($array['helloworldordering']))
			{
				// when saving a record load() is called before bind() so the table instance will have properties which are the existing field values
				if ($this->parent_id == $array['parent_id'])
				{
					// If first is chosen make the item the first child of the selected parent.
					if ($array['helloworldordering'] == -1)
					{
						$this->setLocation($array['parent_id'], 'first-child');
					}
					// If last is chosen make it the last child of the selected parent.
					elseif ($array['helloworldordering'] == -2)
					{
						$this->setLocation($array['parent_id'], 'last-child');
					}
					// Don't try to put an item after itself. All other ones put after the selected item.
					elseif ($array['helloworldordering'] && $this->id != $array['helloworldordering'])
					{
						$this->setLocation($array['helloworldordering'], 'after');
					}
					// Just leave it where it is if no change is made.
					elseif ($array['helloworldordering'] && $this->id == $array['helloworldordering'])
					{
						unset($array['helloworldordering']);
					}
				}
				// Set the new parent id if parent id not matched and put in last position
				else
				{
					$this->setLocation($array['parent_id'], 'last-child');
				}
			}
		}

		return parent::bind($array, $ignore);
	}

	/**
	 * Method to compute the default name of the asset.
	 * The default name is in the form `table_name.id`
	 * where id is the value of the primary key of the table.
	 *
	 * @return	string
	 * @since	2.5
	 */
	protected function _getAssetName()
	{
		$k = $this->_tbl_key;
		return 'com_helloworld.helloworld.'.(int) $this->$k;
	}
	/**
	 * Method to return the title to use for the asset table.
	 *
	 * @return	string
	 * @since	2.5
	 */
	protected function _getAssetTitle()
	{
		return $this->greeting;
	}
	/**
	 * Method to get the asset-parent-id of the item
	 *
	 * @return	int
	 */
	protected function _getAssetParentId(JTable $table = NULL, $id = NULL)
	{
		// We will retrieve the parent-asset from the Asset-table
		$assetParent = JTable::getInstance('Asset');
		// Default: if no asset-parent can be found we take the global asset
		$assetParentId = $assetParent->getRootId();

		// Find the parent-asset
		if (($this->catid)&& !empty($this->catid))
		{
			// The item has a category as asset-parent
			$assetParent->loadByName('com_helloworld.category.' . (int) $this->catid);
		}
		else
		{
			// The item has the component as asset-parent
			$assetParent->loadByName('com_helloworld');
		}

		// Return the found asset-parent-id
		if ($assetParent->id)
		{
			$assetParentId=$assetParent->id;
		}
		return $assetParentId;
	}

	public function check()
	{
		$this->alias = trim($this->alias);
		if (empty($this->alias))
		{
			$this->alias = $this->greeting;
		}
		$this->alias = JFilterOutput::stringURLSafe($this->alias);
		return true;
	}

	public function delete($pk = null, $children = false)
	{
		return parent::delete($pk, $children);
	}
}

Pantalla del lado cliente de Helloworld

Para mostrar los padres e hijos en la página de helloworld, se requieren 3 cambios:

  1. En la vista, obtener los datos para el padre y para los hijos mediante llamadas a la función del modelo
  2. En el modelo proporcionar los datos para estas llamadas de la función.
  3. En la salida de diseño, el html que incluya los datos de padre e hijo.

Ten en cuenta que hemos cambiado el código del modelo para permitir un parámetro a getItem(). Esto tiene el efecto secundario de cambiar la variable this->item dentro de la instancia del modelo para apuntar al último elemento recuperado. Como la funcionalidad dentro de getMapParams() (llamada desde addMap()) usa this->item para obtener la latitud y longitud, etc. para el actual registro, debemos asegurarnos de que la llamada a addMap() se realice antes del nuevo código que estamos introduciendo.

Nota. Si tienes un elemento que apunta a su registro de hellowworld con id=1, entonces deberás editar ese elemento para apuntar a la nueva identificación.

site/views/helloworld/view.html.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HTML View class for the HelloWorld Component
 *
 * @since  0.0.1
 */
class HelloWorldViewHelloWorld extends JViewLegacy
{
	/**
	 * Display the Hello World view
	 *
	 * @param   string  $tpl  The name of the template file to parse; automatically searches through the template paths.
	 *
	 * @return  void
	 */
	function display($tpl = null)
	{
		// Assign data to the view
		$this->item = $this->get('Item');

		// Check for errors.
		if (count($errors = $this->get('Errors')))
		{
			JLog::add(implode('<br />', $errors), JLog::WARNING, 'jerror');

			return false;
		}

		$this->addMap();

		$model = $this->getModel();
		$this->parentItem = $model->getItem($this->item->parent_id);
		$this->children = $model->getChildren($this->item->id);
		// getChildren includes the record itself (as well as the children) so remove this record
		unset($this->children[0]);

		// Display the view
		parent::display($tpl);
	}

	function addMap() 
	{
		$document = JFactory::getDocument();

		// everything's dependent upon JQuery
		JHtml::_('jquery.framework');

		// we need the Openlayers JS and CSS libraries
		$document->addScript("https://cdnjs.cloudflare.com/ajax/libs/openlayers/4.6.4/ol.js");
		$document->addStyleSheet("https://cdnjs.cloudflare.com/ajax/libs/openlayers/4.6.4/ol.css");

		// ... and our own JS and CSS
		$document->addScript(JURI::root() . "media/com_helloworld/js/openstreetmap.js");
		$document->addStyleSheet(JURI::root() . "media/com_helloworld/css/openstreetmap.css");

		// get the data to pass to our JS code
		$params = $this->get("mapParams");
		$document->addScriptOptions('params', $params);
	}
}

site/models/helloworld.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

JLoader::register('HelloworldHelperRoute', JPATH_ROOT . '/components/com_helloworld/helpers/route.php');

/**
 * HelloWorld Model
 *
 * @since  0.0.1
 */
class HelloWorldModelHelloWorld extends JModelItem
{
	/**
	 * @var object item
	 */
	protected $item;

	/**
	 * Method to auto-populate the model state.
	 *
	 * This method should only be called once per instantiation and is designed
	 * to be called on the first call to the getState() method unless the model
	 * configuration flag to ignore the request is set.
	 *
	 * Note. Calling getState in this method will result in recursion.
	 *
	 * @return	void
	 * @since	2.5
	 */
	protected function populateState()
	{
		// Get the message id
		$jinput = JFactory::getApplication()->input;
		$id     = $jinput->get('id', 1, 'INT');
		$this->setState('message.id', $id);

		// Load the parameters.
		$this->setState('params', JFactory::getApplication()->getParams());
		parent::populateState();
	}

	/**
	 * Method to get a table object, load it if necessary.
	 *
	 * @param   string  $type    The table name. Optional.
	 * @param   string  $prefix  The class prefix. Optional.
	 * @param   array   $config  Configuration array for model. Optional.
	 *
	 * @return  JTable  A JTable object
	 *
	 * @since   1.6
	 */
	public function getTable($type = 'HelloWorld', $prefix = 'HelloWorldTable', $config = array())
	{
		return JTable::getInstance($type, $prefix, $config);
	}

	/**
	 * Get the message
	 * @return object The message to be displayed to the user
	 */
	public function getItem($id = null)
	{
		if (!isset($this->item) || !is_null($id)) 
		{
			$id    = is_null($id) ? $this->getState('message.id') : $id;
			$db    = JFactory::getDbo();
			$query = $db->getQuery(true);
			$query->select('h.greeting, h.params, h.image as image, c.title as category, h.latitude as latitude, h.longitude as longitude,
						h.id as id, h.alias as alias, h.catid as catid, h.parent_id as parent_id, h.level as level')
				  ->from('#__helloworld as h')
				  ->leftJoin('#__categories as c ON h.catid=c.id')
				  ->where('h.id=' . (int)$id);

			if (JLanguageMultilang::isEnabled())
			{
				$lang = JFactory::getLanguage()->getTag();
				$query->where('h.language IN ("*","' . $lang . '")');
			}

			$db->setQuery((string)$query);
		
			if ($this->item = $db->loadObject()) 
			{
				// Load the JSON string
				$params = new JRegistry;
				$params->loadString($this->item->params, 'JSON');
				$this->item->params = $params;

				// Merge global params with item params
				$params = clone $this->getState('params');
				$params->merge($this->item->params);
				$this->item->params = $params;

				// Convert the JSON-encoded image info into an array
				$image = new JRegistry;
				$image->loadString($this->item->image, 'JSON');
				$this->item->imageDetails = $image;
			}
			else
			{
				throw new Exception('Helloworld id not found', 404);
			}
		}
		return $this->item;
	}

	public function getMapParams()
	{
		if ($this->item) 
		{
			$url = HelloworldHelperRoute::getAjaxURL();
			$this->mapParams = array(
				'latitude' => $this->item->latitude,
				'longitude' => $this->item->longitude,
				'zoom' => 10,
				'greeting' => $this->item->greeting,
				'ajaxurl' => $url
			);
			return $this->mapParams; 
		}
		else
		{
			throw new Exception('No helloworld details available for map', 500);
		}
	}

	public function getMapSearchResults($mapbounds)
	{
		try 
		{
			$db    = JFactory::getDbo();
			$query = $db->getQuery(true);
			$query->select('h.id, h.alias, h.catid, h.greeting, h.latitude, h.longitude')
			   ->from('#__helloworld as h')
			   ->where('h.latitude > ' . $mapbounds['minlat'] . 
				' AND h.latitude < ' . $mapbounds['maxlat'] .
				' AND h.longitude > ' . $mapbounds['minlng'] .
				' AND h.longitude < ' . $mapbounds['maxlng']);

			if (JLanguageMultilang::isEnabled())
			{
				$lang = JFactory::getLanguage()->getTag();
				$query->where('h.language IN ("*","' . $lang . '")');
			}

			$db->setQuery($query);
			$results = $db->loadObjectList(); 
		}
		catch (Exception $e)
		{
			$msg = $e->getMessage();
			JFactory::getApplication()->enqueueMessage($msg, 'error'); 
			$results = null;
		}

		if (JLanguageMultilang::isEnabled())
		{
			$query_lang = "&lang={$lang}";
		}
		else
		{
			$query_lang = "";
		}

		for ($i = 0; $i < count($results); $i++) 
		{
			$results[$i]->url = JRoute::_('index.php?option=com_helloworld&view=helloworld&id=' . $results[$i]->id . 
				":" . $results[$i]->alias . '&catid=' . $results[$i]->catid . $query_lang);
		}

		return $results; 
	}

	public function getChildren($id)
	{
		$table = $this->getTable();
		$children = $table->getTree($id);
		return $children;
	}
}

site/views/helloworld/tmpl/default.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');
$lang = JFactory::getLanguage()->getTag();
if (JLanguageMultilang::isEnabled() && $lang)
{
    $query_lang = "&lang={$lang}";
}
else
{
    $query_lang = "";
}
?>
<h1><?php echo $this->item->greeting.(($this->item->category and $this->item->params->get('show_category'))
                                      ? (' ('.$this->item->category.')') : ''); ?>
</h1>
<?php
    $src = $this->item->imageDetails['image'];
    if ($src)
    {
        $html = '<figure>
                    <img src="%s" alt="%s" >
                    <figcaption>%s</figcaption>
                </figure>';
        $alt = $this->item->imageDetails['alt'];
        $caption = $this->item->imageDetails['caption'];
        echo sprintf($html, $src, $alt, $caption);
    } ?>

<?php if ($this->parentItem->id > 1) : ?>
	<h1><?php echo JText::_('COM_HELLOWORLD_PARENT') ?>
	</h1>
	<h3>
		<?php $url = JRoute::_('index.php?option=com_helloworld&view=helloworld&id=' . $this->parentItem->id . ':' . $this->parentItem->alias . '&catid=' . $this->parentItem->catid . $query_lang); ?>
		<a href="<?php echo $url; ?>"><?php echo $this->parentItem->greeting; ?></a>
	</h3>
<?php endif; ?>

<?php if ($this->children) : 
		$baseLevel = $this->item->level; ?>
		<h1><?php echo JText::_('COM_HELLOWORLD_CHILDREN') ?>
		</h1>
		<?php foreach ($this->children as $i => $child) : ?>
			<h3>
				<?php $prefix = JLayoutHelper::render('joomla.html.treeprefix', array('level' => $child->level - $baseLevel)); ?>
				<?php echo $prefix; ?>
				<?php $url = JRoute::_('index.php?option=com_helloworld&view=helloworld&id=' . $child->id . ':' . $child->alias . '&catid=' . $child->catid . $query_lang); ?>
				<a href="<?php echo $url; ?>"><?php echo $child->greeting; ?></a>
			</h3>
	<?php endforeach; ?>
<?php endif; ?>

<div id="map" class="map"></div>
<div class="map-callout map-callout-bottom" id="greeting-container"></div>
<div id="searchmap">
    <?php echo '<input id="token" type="hidden" name="' . JSession::getFormToken() . '" value="1" />'; ?>
    <button type="button" class="btn btn-primary" onclick="searchHere();">
        <?php echo JText::_('COM_HELLOWORLD_SEARCH_HERE_BUTTON') ?>
    </button>
    <div id="searchresults">
    </div>
</div>

Formulario del lado cliente de Helloworld

Para admitir la captura del elemento primario en el formulario de inicio de sesión, reutilizamos el campo de formulario personalizado del administrador en la definición xml de formulario adicional.

Ten en cuenta que en la funcionalidad para crear un nuevo registro de helloworld en la interfaz no hemos incluido un rebuild() de la tabla, por lo que los registros nuevos no pueden obtener el campo path escrito en el base de datos. Sin embargo, el nuevo registro se insertará en el árbol correctamente porque el lado cliente usa el código de table de la administración del lado servidor, por lo que recogerá la herencia de la Tabla Anidada, con su reemplazo bind() etc. Esto también significa que debemos eliminar el código prepareTable() en el modelo que se definió previamente en ordenamiento del nuevo registro.

site/models/forms/add-form.xml

<?xml version="1.0" encoding="utf-8"?>
<form
    addrulepath="/administrator/components/com_helloworld/models/rules"
    addfieldpath="/administrator/components/com_helloworld/models/fields"
    >
    <fieldset
				name="details"
				label="COM_HELLOWORLD_HELLOWORLD_DETAILS"
	>
		<field
				name="id"
				type="hidden"
				/>
		<field
				name="greeting"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_GREETING_DESC"
				size="40"
				class="inputbox"
				validate="greeting"
				required="true"
				hint="COM_HELLOWORLD_HELLOWORLD_GREETING_HINT"
				default=""
				/>
		<field 
				name="alias" 
				type="text" 
				label="JFIELD_ALIAS_LABEL"
				description="JFIELD_ALIAS_DESC"
				hint="JFIELD_ALIAS_PLACEHOLDER"
				size="40" 
				/>
		<field
				name="catid"
				type="category"
				extension="com_helloworld"
				class="inputbox"
				default=""
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC"
				required="true"
				>
				<option value="0">JOPTION_SELECT_CATEGORY</option>
		</field>
		<fields name="imageinfo" label="COM_HELLOWORLD_HELLOWORLD_IMAGE_LABEL">
			<field
				name="image"
				type="file"
				label="COM_HELLOWORLD_HELLOWORLD_PICTURE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_PICTURE_DESC" 
				accept="image/*"
				>
			</field>
			<field
 				name="caption"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_CAPTION_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_CAPTION_DESC"
				size="40"
				class="inputbox"
				>
			</field>
			<field
				name="alt"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_ALTTEXT_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_ALTTEXT_DESC"
				size="40"
				class="inputbox"
				>
			</field>
		</fields>
		<field
				name="language"
				type="contentlanguage"
				label="JFIELD_LANGUAGE_LABEL"
				description="JFIELD_LANGUAGE_DESC"
				class="inputbox">
		</field>
		<field
				name="message"
				type="textarea"
				rows="5"
				cols="80"
				label="COM_HELLOWORLD_HELLOWORLD_MESSAGE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_MESSAGE_DESC"
				hint="COM_HELLOWORLD_HELLOWORLD_MESSAGE_HINT"
				required="true"
				>
        </field>
        <field
				name="captcha"
				type="captcha"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC"
				validate="captcha"
                >
		</field>
        <fields name="params">
            <field
                    name="show_category"
                    type="list"
                    label="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL"
                    description="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC"
                    default=""
                    useglobal="true"
            >
                <option value="0">JHIDE</option>
                <option value="1">JSHOW</option>
            </field>
        </fields>
		<field
				name="parent_id"
				type="helloworldparent"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_PARENT_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_PARENT_DESC"
				default="1"
				filter="int">
		</field>
    </fieldset>
</form>

site/models/form.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorld Model
 *
 * @since  0.0.1
 */
class HelloWorldModelForm extends JModelAdmin
{

	/**
	 * Method to get a table object, load it if necessary.
	 *
	 * @param   string  $type    The table name. Optional.
	 * @param   string  $prefix  The class prefix. Optional.
	 * @param   array   $config  Configuration array for model. Optional.
	 *
	 * @return  JTable  A JTable object
	 *
	 * @since   1.6
	 */
	public function getTable($type = 'HelloWorld', $prefix = 'HelloWorldTable', $config = array())
	{
		return JTable::getInstance($type, $prefix, $config);
	}

    /**
	 * Method to get the record form.
	 *
	 * @param   array    $data      Data for the form.
	 * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
	 *
	 * @return  mixed    A JForm object on success, false on failure
	 *
	 * @since   1.6
	 */
	public function getForm($data = array(), $loadData = true)
	{
		// Get the form.
		$form = $this->loadForm(
			'com_helloworld.form',
			'add-form',
			array(
				'control' => 'jform',
				'load_data' => $loadData
			)
		);

		if (empty($form))
		{
			$errors = $this->getErrors();
			throw new Exception(implode("\n", $errors), 500);
		}

		return $form;
	}

	/**
	 * Method to get the data that should be injected in the form.
	 * As this form is for add, we're not prefilling the form with an existing record
	 * But if the user has previously hit submit and the validation has found an error,
	 *   then we inject what was previously entered.
	 *
	 * @return  mixed  The data for the form.
	 *
	 * @since   1.6
	 */
	protected function loadFormData()
	{
		// Check the session for previously entered form data.
		$data = JFactory::getApplication()->getUserState(
			'com_helloworld.edit.helloworld.data',
			array()
		);

		return $data;
	}
    
	/**
	 * Method to get the script that have to be included on the form
	 * This returns the script associated with helloworld field greeting validation
	 *
	 * @return string	Script files
	 */
	public function getScript() 
	{
		return 'administrator/components/com_helloworld/models/forms/helloworld.js';
	}

	/**
	 * Prepare a helloworld record for saving in the database
	 */
	protected function prepareTable($table)
	{
	}
}

Vista del lado cliente de la categoría

Solo tenemos que cambiar el orden para usar el campo de base de datos lft en lugar del campo ordering.

site/models/category.php

<?php
/**
 * Model for displaying the helloworld messages in a given category
 */

defined('_JEXEC') or die;

class HelloworldModelCategory extends JModelList
{
	public function __construct($config = array())
	{
		if (empty($config['filter_fields']))
		{
			$config['filter_fields'] = array(
				'id',
				'greeting',
				'alias',
				'lft',
			);
		}

		parent::__construct($config);
	}
    
	protected function populateState($ordering = null, $direction = null)
	{
		parent::populateState($ordering, $direction);
        
		$app = JFactory::getApplication('site');
		$catid = $app->input->getInt('id');

		$this->setState('category.id', $catid);
	}
    
	protected function getListQuery()
	{
		$db    = JFactory::getDbo();
		$query = $db->getQuery(true);

		$catid = $this->getState('category.id'); 
		$query->select('id, greeting, alias, catid')
			->from($db->quoteName('#__helloworld'))
			->where('catid = ' . $catid);

		if (JLanguageMultilang::isEnabled())
		{
			$lang = JFactory::getLanguage()->getTag();
			$query->where('language IN ("*","' . $lang . '")');
		}

		$orderCol	= $this->state->get('list.ordering', 'lft');
		$orderDirn 	= $this->state->get('list.direction', 'asc');

		$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));

		return $query;	
	}

	public function getCategoryName()
	{
		$catid = $this->getState('category.id'); 
		$categories = JCategories::getInstance('Helloworld', array());
		$categoryNode = $categories->get($catid);   
		return $categoryNode->title; 
	}
    
	public function getSubcategories()
	{
		$catid = $this->getState('category.id'); 
		$categories = JCategories::getInstance('Helloworld', array());
		$categoryNode = $categories->get($catid);
		$subcats = $categoryNode->getChildren(); 
        
		$lang = JFactory::getLanguage()->getTag();
		if (JLanguageMultilang::isEnabled() && $lang)
		{
			$query_lang = "&lang={$lang}";
		}
		else
		{
			$query_lang = '';
		}
        
		foreach ($subcats as $subcat)
		{
			$subcat->url = JRoute::_("index.php?view=category&id=" . $subcat->id . $query_lang);
		}
		return $subcats;
	}
}

site/models/forms/filter_category.xml

<?xml version="1.0" encoding="utf-8"?>
<form>
	<fields name="list">
        <field
			name="fullordering"
			type="list"
			onchange="this.form.submit();"
			default="lft ASC"
			>
			<option value="">COM_HELLOWORLD_SORT_BY</option>
			<option value="lft ASC">COM_HELLOWORLD_ORDERING_ASC</option>
			<option value="lft DESC">COM_HELLOWORLD_ORDERING_DESC</option>
			<option value="greeting ASC">COM_HELLOWORLD_GREETING_ASC</option>
			<option value="greeting DESC">COM_HELLOWORLD_GREETING_DESC</option>
			<option value="id ASC">COM_HELLOWORLD_ID_ASC</option>
			<option value="id DESC">COM_HELLOWORLD_ID_DESC</option>
			<option value="alias ASC">COM_HELLOWORLD_ALIAS_ASC</option>
			<option value="alias DESC">COM_HELLOWORLD_ALIAS_DESC</option>
		</field>
		<field
			name="limit"
			type="limitbox"
			class="input-mini"
			default="10"
			onchange="this.form.submit();"
		/>
	</fields>
</form>

Actualización cadenas de idioma

site/language/en-GB/en-GB.com_helloworld.ini

; add new message form
COM_HELLOWORLD_LEGEND_DETAILS="New Helloworld Message Details"
COM_HELLOWORLD_HELLOWORLD_CREATING="Add message"
COM_HELLOWORLD_HELLOWORLD_ERROR_UNACCEPTABLE="Sorry, you have an error"
COM_HELLOWORLD_HELLOWORLD_DETAILS="Message details"
COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL="Greeting"
COM_HELLOWORLD_HELLOWORLD_GREETING_DESC="Please specify the greeting to add"
COM_HELLOWORLD_HELLOWORLD_GREETING_HINT="Any characters allowed"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL="Category"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC="Please select the associated category"
COM_HELLOWORLD_HELLOWORLD_MESSAGE_LABEL="Reason"
COM_HELLOWORLD_HELLOWORLD_MESSAGE_DESC="Please say why you're adding this greeting"
COM_HELLOWORLD_HELLOWORLD_MESSAGE_HINT="No HTML tags!"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL="Spam protection"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC="Prove you're a real person!"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL="Display category or not?"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC="Select if you want the category displayed too"
COM_HELLOWORLD_HELLOWORLD_IMAGE_LABEL="Image information"
COM_HELLOWORLD_HELLOWORLD_PICTURE_LABEL="Image file to upload"
COM_HELLOWORLD_HELLOWORLD_PICTURE_DESC="Select the file with the image to upload"
COM_HELLOWORLD_HELLOWORLD_CAPTION_LABEL="Caption"
COM_HELLOWORLD_HELLOWORLD_CAPTION_DESC="Text to use as a caption for the image"
COM_HELLOWORLD_HELLOWORLD_ALTTEXT_LABEL="Alt text"
COM_HELLOWORLD_HELLOWORLD_ALTTEXT_DESC="Text to display if image cannot be shown"
COM_HELLOWORLD_HELLOWORLD_FIELD_PARENT_LABEL="Parent"
COM_HELLOWORLD_HELLOWORLD_FIELD_PARENT_DESC="Select the record which is to be the parent"
; save and cancel confirmation messages
COM_HELLOWORLD_ADD_SUCCESSFUL="New greeting successfully saved"
COM_HELLOWORLD_ADD_CANCELLED="New greeting cancelled ok"
; file upload error conditions
COM_HELLOWORLD_ERROR_FILEUPLOAD="PHP Error %s encountered when uploading file"
COM_HELLOWORLD_ERROR_FILETOOLARGE="Upload file exceeds max size configured in Joomla"
COM_HELLOWORLD_ERROR_BADFILENAME="Upload file has an invalid filename"
COM_HELLOWORLD_ERROR_FILE_EXISTS="Upload file already exists"
COM_HELLOWORLD_ERROR_UNABLE_TO_UPLOAD_FILE="Error creating uploaded file"
; helloworld greeting page
COM_HELLOWORLD_PARENT="Parent"
COM_HELLOWORLD_CHILDREN="Children"
COM_HELLOWORLD_SEARCH_HERE_BUTTON="Search here"
; Ajax handling errors
COM_HELLOWORLD_ERROR_NO_RECORDS="Didn't get any records"
COM_HELLOWORLD_ERROR_NO_MAP_BOUNDS="Error: no map bounds"
; category view, search and ordering fields and headings
COM_HELLOWORLD_SORT_BY="Sort by ..."
COM_HELLOWORLD_ORDERING_ASC="Ordering asc"
COM_HELLOWORLD_ORDERING_DESC="Ordering desc"
COM_HELLOWORLD_GREETING_ASC="Greeting asc"
COM_HELLOWORLD_GREETING_DESC="Greeting desc"
COM_HELLOWORLD_ID_ASC="id asc"
COM_HELLOWORLD_ID_DESC="id desc"
COM_HELLOWORLD_ALIAS_ASC="alias asc"
COM_HELLOWORLD_ALIAS_DESC="alias desc"
COM_HELLOWORLD_HELLOWORLD_ALIAS_LABEL="Alias"
COM_HELLOWORLD_HELLOWORLD_FIELD_URL_LABEL="URL"
COM_HELLOWORLD_HEADER_SUBCATEGORIES="Subcategories"

admin/language/en-GB/en-GB.com_helloworld.ini

; Joomla! Project
; Copyright (C) 2005 - 2018 Open Source Matters. All rights reserved.
; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
; Note : All ini files need to be saved as UTF-8

COM_HELLOWORLD_ADMINISTRATION="HelloWorld - Administration"
COM_HELLOWORLD_ADMINISTRATION_CATEGORIES="HelloWorld - Categories"
COM_HELLOWORLD_NUM="#"
COM_HELLOWORLD_HELLOWORLDS_FILTER="Filters"
COM_HELLOWORLD_AUTHOR="Author"
COM_HELLOWORLD_LANGUAGE="Language"
COM_HELLOWORLD_CREATED_DATE="Created"
COM_HELLOWORLD_PUBLISHED="Published"
COM_HELLOWORLD_HELLOWORLDS_NAME="Name"
COM_HELLOWORLD_HELLOWORLDS_POSITION="Position"
COM_HELLOWORLD_HELLOWORLDS_IMAGE="Image"
COM_HELLOWORLD_HELLOWORLDS_ASSOCIATIONS="Associations"
COM_HELLOWORLD_ID="Id"

COM_HELLOWORLD_HELLOWORLD_CREATING="HelloWorld - Creating"
COM_HELLOWORLD_HELLOWORLD_DETAILS="Details"
COM_HELLOWORLD_HELLOWORLD_EDITING="HelloWorld - Editing"
COM_HELLOWORLD_HELLOWORLD_ERROR_UNACCEPTABLE="Some values are unacceptable"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC="The category the messages belongs to"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL="Category"
COM_HELLOWORLD_HELLOWORLD_FIELD_GREETING_DESC="This message will be displayed"
COM_HELLOWORLD_HELLOWORLD_FIELD_GREETING_LABEL="Message"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL="Show category"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC="If set to Show, the title of the message&rsquo;s category will show."
COM_HELLOWORLD_HELLOWORLD_FIELD_LATITUDE_LABEL="Latitude"
COM_HELLOWORLD_HELLOWORLD_FIELD_LATITUDE_DESC="Enter the position latitude, between -90 and +90 degrees"
COM_HELLOWORLD_HELLOWORLD_FIELD_LONGITUDE_LABEL="Longitude"
COM_HELLOWORLD_HELLOWORLD_FIELD_LONGITUDE_DESC="Enter the position longitude, between -180 and +180 degrees"
COM_HELLOWORLD_HELLOWORLD_FIELD_PARENT_LABEL="Parent"
COM_HELLOWORLD_HELLOWORLD_FIELD_PARENT_DESC="Select the parent record"
COM_HELLOWORLD_ITEM_FIELD_ORDERING_VALUE_FIRST="-- First record"
COM_HELLOWORLD_ITEM_FIELD_ORDERING_VALUE_LAST="-- Last record"
COM_HELLOWORLD_ITEM_FIELD_ORDERING_TEXT="Ordering will be available after saving."
COM_HELLOWORLD_HELLOWORLD_FIELD_LANGUAGE_DESC="Select the appropriate language"
COM_HELLOWORLD_IMAGE_FIELDS="Image details"
COM_HELLOWORLD_HELLOWORLD_FIELD_IMAGE_LABEL="Select image"
COM_HELLOWORLD_HELLOWORLD_FIELD_IMAGE_DESC="Select an image from the library, or upload a new one"
COM_HELLOWORLD_HELLOWORLD_FIELD_ALT_LABEL="Alt text"
COM_HELLOWORLD_HELLOWORLD_FIELD_ALT_DESC="Alternative text (if image cannot be displayed)"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTION_LABEL="Caption"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTION_DESC="Provide a caption for the image"
COM_HELLOWORLD_HELLOWORLD_HEADING_GREETING="Greeting"
COM_HELLOWORLD_HELLOWORLD_HEADING_ID="Id"
COM_HELLOWORLD_MANAGER_HELLOWORLD_EDIT="HelloWorld manager: Edit Message"
COM_HELLOWORLD_MANAGER_HELLOWORLD_NEW="HelloWorld manager: New Message"
COM_HELLOWORLD_MANAGER_HELLOWORLDS="HelloWorld manager"
COM_HELLOWORLD_EDIT_HELLOWORLD="Edit message"
COM_HELLOWORLD_N_ITEMS_DELETED_1="One message deleted"
COM_HELLOWORLD_N_ITEMS_DELETED_MORE="%d messages deleted"
COM_HELLOWORLD_N_ITEMS_PUBLISHED="%d message(s) published"
COM_HELLOWORLD_N_ITEMS_UNPUBLISHED="%d message(s) unpublished"
COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL="Greeting"
COM_HELLOWORLD_HELLOWORLD_GREETING_DESC="Add Hello World Greeting"
COM_HELLOWORLD_SUBMENU_MESSAGES="Messages"
COM_HELLOWORLD_SUBMENU_CATEGORIES="Categories"
COM_HELLOWORLD_CONFIGURATION="HelloWorld Configuration"
COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_LABEL="Messages settings"
COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_DESC="Settings that will be applied to all messages by default"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL="Captcha"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC="Select Captcha to use on front end form"
COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_LABEL="User to email"
COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_DESC="Select user to email when a new message is entered on front end"
COM_HELLOWORLD_FIELDSET_RULES="Message Permissions"
COM_HELLOWORLD_FIELD_RULES_LABEL="Permissions"
COM_HELLOWORLD_ACCESS_DELETE_DESC="Is this group allowed to edit this message?"
COM_HELLOWORLD_ACCESS_DELETE_DESC="Is this group allowed to delete this message?"
COM_HELLOWORLD_TAB_NEW_MESSAGE="New Message"
COM_HELLOWORLD_TAB_EDIT_MESSAGE="Message Details"
COM_HELLOWORLD_TAB_PARAMS="Parameters"
COM_HELLOWORLD_TAB_ASSOCIATIONS="Associations"
COM_HELLOWORLD_TAB_PERMISSIONS="Permissions"
COM_HELLOWORLD_TAB_IMAGE="Image"
COM_HELLOWORLD_LEGEND_DETAILS="Message Details"
COM_HELLOWORLD_LEGEND_PARAMS="Message Parameters"
COM_HELLOWORLD_LEGEND_ASSOCIATIONS="Message Associations"
COM_HELLOWORLD_LEGEND_PERMISSIONS="Message Permissions"
COM_HELLOWORLD_LEGEND_IMAGE="Image info"
; Column ordering in the Helloworlds view
COM_HELLOWORLD_ORDERING_ASC="Ordering ascending"
COM_HELLOWORLD_ORDERING_DESC="Ordering descending"
COM_HELLOWORLD_GREETING_ASC="Greeting ascending"
COM_HELLOWORLD_GREETING_DESC="Greeting descending"
COM_HELLOWORLD_AUTHOR_ASC="Author ascending"
COM_HELLOWORLD_AUTHOR_DESC="Author descending"
COM_HELLOWORLD_CREATED_ASC="Creation date ascending"
COM_HELLOWORLD_CREATED_DESC="Creation date descending"
COM_HELLOWORLD_PUBLISHED_ASC="Unpublished first"
COM_HELLOWORLD_PUBLISHED_DESC="Published first"
COM_HELLOWORLD_LANGUAGE_ASC="Language ascending"
COM_HELLOWORLD_LANGUAGE_DESC="Language descending"
COM_HELLOWORLD_ASSOCIATION_ASC="Association ascending"
COM_HELLOWORLD_ASSOCIATION_DESC="Association descending"
; Helloworld menuitem - selecting a greeting via modal
COM_HELLOWORLD_MENUITEM_SELECT_MODAL_TITLE="Select greeting"
COM_HELLOWORLD_MENUITEM_SELECT_HELLOWORLD="Select"
COM_HELLOWORLD_MENUITEM_SELECT_BUTTON_TOOLTIP="Select a helloworld greeting"
; Checking in records
COM_HELLOWORLD_N_ITEMS_CHECKED_IN_0="No records checked in."
COM_HELLOWORLD_N_ITEMS_CHECKED_IN_1="%d record successfully checked in."
COM_HELLOWORLD_N_ITEMS_CHECKED_IN_MORE="%d records successfully checked in."

Empaquetado del componente

Contenido de su directorio de código. Cada enlace a un archivo a continuación te lleva al paso en el tutorial que tiene la última versión de ese archivo de código fuente.

helloworld.xml

<?xml version="1.0" encoding="utf-8"?>
<extension type="component" version="3.0" method="upgrade">

	<name>COM_HELLOWORLD</name>
	<!-- The following elements are optional and free of formatting constraints -->
	<creationDate>January 2018</creationDate>
	<author>John Doe</author>
	<authorEmail>john.doe@example.org</authorEmail>
	<authorUrl>http://www.example.org</authorUrl>
	<copyright>Copyright Info</copyright>
	<license>License Info</license>
	<!--  The version string is recorded in the components table -->
	<version>0.0.26</version>
	<!-- The description is optional and defaults to the name -->
	<description>COM_HELLOWORLD_DESCRIPTION</description>

	<!-- Runs on install/uninstall/update; New in 2.5 -->
	<scriptfile>script.php</scriptfile>

	<install> <!-- Runs on install -->
		<sql>
			<file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file>
		</sql>
	</install>
	<uninstall> <!-- Runs on uninstall -->
		<sql>
			<file driver="mysql" charset="utf8">sql/uninstall.mysql.utf8.sql</file>
		</sql>
	</uninstall>
	<update> <!-- Runs on update; New since J2.5 -->
		<schemas>
			<schemapath type="mysql">sql/updates/mysql</schemapath>
		</schemas>
	</update>

	<!-- Site Main File Copy Section -->
	<!-- Note the folder attribute: This attribute describes the folder
		to copy FROM in the package to install therefore files copied
		in this section are copied from /site/ in the package -->
	<files folder="site">
		<filename>index.html</filename>
		<filename>helloworld.php</filename>
		<filename>controller.php</filename>
		<filename>router.php</filename>
		<folder>controllers</folder>
		<folder>views</folder>
		<folder>models</folder>
		<folder>helpers</folder>
	</files>

		<languages folder="site/language">
			<language tag="en-GB">en-GB/en-GB.com_helloworld.ini</language>
			<language tag="fr-FR">fr-FR/fr-FR.com_helloworld.ini</language>
		</languages>

	<media destination="com_helloworld" folder="media">
		<filename>index.html</filename>
		<folder>images</folder>
		<folder>js</folder>
		<folder>css</folder>
	</media>

	<administration>
		<!-- Administration Menu Section -->
		<menu link='index.php?option=com_helloworld' img="../media/com_helloworld/images/tux-16x16.png">COM_HELLOWORLD_MENU</menu>
		<!-- Administration Main File Copy Section -->
		<!-- Note the folder attribute: This attribute describes the folder
			to copy FROM in the package to install therefore files copied
			in this section are copied from /admin/ in the package -->
		<files folder="admin">
			<!-- Admin Main File Copy Section -->
			<filename>index.html</filename>
			<filename>config.xml</filename>
			<filename>helloworld.php</filename>
			<filename>controller.php</filename>
			<filename>access.xml</filename>
			<!-- SQL files section -->
			<folder>sql</folder>
			<!-- tables files section -->
			<folder>tables</folder>
			<!-- models files section -->
			<folder>models</folder>
			<!-- views files section -->
			<folder>views</folder>
			<!-- controllers files section -->
			<folder>controllers</folder>
			<!-- helpers files section -->
			<folder>helpers</folder>
		</files>
		<languages folder="admin/language">
			<language tag="en-GB">en-GB/en-GB.com_helloworld.ini</language>
			<language tag="en-GB">en-GB/en-GB.com_helloworld.sys.ini</language>
			<language tag="fr-FR">fr-FR/fr-FR.com_helloworld.ini</language>
			<language tag="fr-FR">fr-FR/fr-FR.com_helloworld.sys.ini</language>
		</languages>
	</administration>

</extension>

Colaboradores