Having correctly implemented spatial navigation in your custom forms is more important, than ever these days. Unfortunately Joomla's core JForm does not support adding the required info easily (for example by creating an XML file which will output the correct code.
Essentially one would need to simply add to field definitions two extra parameters, one for tabindex and another for spatial navigation styling, like here:
spNavStyle="nav-left:#jform_city; nav-right:#jform_zip"tabindex="9"
and to have the output looking like this:
<input name="jform[state]" id="jform_state" value="" class="required" tabindex="9" style="nav-left:#jform_city; nav-right:#jform_zip" required="required" aria-required="true" type="text">
Fortunately, if you aren't afraid to making your hands dirty with some PHP, this can be done, relatively easily.
Let's take a look to 2 base scenarios:
1. Use one of core Joomla field types
Let's take the field type "text" - the most common filed for the start. We will work with the Joomla 3.*, but with a little ingenuity, you can adapt the solution for any version of Joomla.
As a first step we will create a local override of the core Joomla file. The file responsible for handling the XML entries for the field type "text" is located in "WEBROOT/libraries/joomla/form/fields" and is named text.php. Let's copy this file to "WEBROOT/com_mycomponent/models/fields" and edit it a bit:
<?php
/**
* @package Joomla.Platform
* @subpackage Form
*
* @copyright Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
defined('JPATH_PLATFORM') or die;
/**
* Form Field class for the Joomla Platform.
* Supports a one line text field.
*
* @link http://www.w3.org/TR/html-markup/input.text.html#input.text
* @since 11.1
*/
class JFormFieldText extends JFormField
{
/**
* The form field type.
*
* @var string
*
* @since 11.1
*/
protected $type = 'Text';
protected $tabIndex;
protected $spNavStyle;
/**
* The allowable maxlength of the field.
*
* @var integer
* @since 3.2
*/
protected $maxLength;
/**
* The mode of input associated with the field.
*
* @var mixed
* @since 3.2
*/
protected $inputmode;
/**
* The name of the form field direction (ltr or rtl).
*
* @var string
* @since 3.2
*/
protected $dirname;
/**
* Method to get certain otherwise inaccessible properties from the form field object.
*
* @param string $name The property name for which to the the value.
*
* @return mixed The property value or null.
*
* @since 3.2
*/
public function __get($name)
{
switch ($name)
{
case 'maxLength':
case 'dirname':
case 'inputmode':
return $this->$name;
}
return parent::__get($name);
}
/**
* Method to set certain otherwise inaccessible properties of the form field object.
*
* @param string $name The property name for which to the the value.
* @param mixed $value The value of the property.
*
* @return void
*
* @since 3.2
*/
public function __set($name, $value)
{
switch ($name)
{
case 'maxLength':
$this->maxLength = (int) $value;
break;
case 'dirname':
$value = (string) $value;
$value = ($value == $name || $value == 'true' || $value == '1');
case 'inputmode':
$this->name = (string) $value;
break;
default:
parent::__set($name, $value);
}
}
/**
* Method to attach a JForm object to the field.
*
* @param SimpleXMLElement $element The SimpleXMLElement object representing the <field /> tag for the form field object.
* @param mixed $value The form field value to validate.
* @param string $group The field name group control value. This acts as as an array container for the field.
* For example if the field has name="foo" and the group value is set to "bar" then the
* full field name would end up being "bar[foo]".
*
* @return boolean True on success.
*
* @see JFormField::setup()
* @since 3.2
*/
public function setup(SimpleXMLElement $element, $value, $group = null)
{
$result = parent::setup($element, $value, $group);
if ($result == true)
{
$inputmode = (string) $this->element['inputmode'];
$dirname = (string) $this->element['dirname'];
$this->inputmode = '';
$inputmode = preg_replace('/\s+/', ' ', trim($inputmode));
$inputmode = explode(' ', $inputmode);
if (!empty($inputmode))
{
$defaultInputmode = in_array('default', $inputmode) ? JText::_("JLIB_FORM_INPUTMODE") . ' ' : '';
foreach (array_keys($inputmode, 'default') as $key)
{
unset($inputmode[$key]);
}
$this->inputmode = $defaultInputmode . implode(" ", $inputmode);
}
// Set the dirname.
$dirname = ((string) $dirname == 'dirname' || $dirname == 'true' || $dirname == '1');
$this->dirname = $dirname ? $this->getName($this->fieldname . '_dir') : false;
$this->tabIndex = (int)$this->element['tabindex'];
$this->spNavStyle = (string)$this->element['spNavStyle'];
$this->maxLength = (int) $this->element['maxlength'];
}
return $result;
}
/**
* Method to get the field input markup.
*
* @return string The field input markup.
*
* @since 11.1
*/
protected function getInput()
{
// Translate placeholder text
$hint = $this->translateHint ? JText::_($this->hint) : $this->hint;
// Initialize some field attributes.
$size = !empty($this->size) ? ' size="' . $this->size . '"' : '';
$maxLength = !empty($this->maxLength) ? ' maxlength="' . $this->maxLength . '"' : '';
$class = !empty($this->class) ? ' class="' . $this->class . '"' : '';
$readonly = $this->readonly ? ' readonly' : '';
$disabled = $this->disabled ? ' disabled' : '';
$required = $this->required ? ' required aria-required="true"' : '';
$hint = $hint ? ' placeholder="' . $hint . '"' : '';
$autocomplete = !$this->autocomplete ? ' autocomplete="off"' : ' autocomplete="' . $this->autocomplete . '"';
$autocomplete = $autocomplete == ' autocomplete="on"' ? '' : $autocomplete;
$autofocus = $this->autofocus ? ' autofocus' : '';
$spellcheck = $this->spellcheck ? '' : ' spellcheck="false"';
$pattern = !empty($this->pattern) ? ' pattern="' . $this->pattern . '"' : '';
$inputmode = !empty($this->inputmode) ? ' inputmode="' . $this->inputmode . '"' : '';
$dirname = !empty($this->dirname) ? ' dirname="' . $this->dirname . '"' : '';
$tabindex = !empty($this->tabIndex) ? ' tabindex="' . $this->tabIndex . '"' : '';
$spNavStyle = !empty($this->spNavStyle) ? ' style="' . $this->spNavStyle . '"' : '';
// Initialize JavaScript field attributes.
$onchange = !empty($this->onchange) ? ' onchange="' . $this->onchange . '"' : '';
// Including fallback code for HTML5 non supported browsers.
JHtml::_('jquery.framework');
JHtml::_('script', 'system/html5fallback.js', false, true);
$datalist = '';
$list = '';
/* Get the field options for the datalist.
Note: getSuggestions() is deprecated and will be changed to getOptions() with 4.0. */
$options = (array) $this->getSuggestions();
if ($options)
{
$datalist = '<datalist id="' . $this->id . '_datalist">';
foreach ($options as $option)
{
if (!$option->value)
{
continue;
}
$datalist .= '<option value="' . $option->value . '">' . $option->text . '</option>';
}
$datalist .= '</datalist>';
$list = ' list="' . $this->id . '_datalist"';
}
$html[] = '<input type="text" name="' . $this->name . '" id="' . $this->id . '"' . $dirname . ' value="'
. htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8') . '"' . $class . $tabindex . $spNavStyle .$size . $disabled . $readonly . $list
. $hint . $onchange . $maxLength . $required . $autocomplete . $autofocus . $spellcheck . $inputmode . $pattern . ' />';
$html[] = $datalist;
return implode($html);
}
/**
* Method to get the field options.
*
* @return array The field option objects.
*
* @since 3.4
*/
protected function getOptions()
{
$options = array();
foreach ($this->element->children() as $option)
{
// Only add <option /> elements.
if ($option->getName() != 'option')
{
continue;
}
// Create a new option object based on the <option /> element.
$options[] = JHtml::_(
'select.option', (string) $option['value'],
JText::alt(trim((string) $option), preg_replace('/[^a-zA-Z0-9_\-]/', '_', $this->fieldname)), 'value', 'text'
);
}
return $options;
}
/**
* Method to get the field suggestions.
*
* @return array The field option objects.
*
* @since 3.2
* @deprecated 4.0 Use getOptions instead
*/
protected function getSuggestions()
{
return $this->getOptions();
}
}
The lines/words highlighted with RED are the newly added ones. This, combined with the proper entries in the form definition XML file:
<field name="state" type="text"
description="COM_VIBRANTVIDEOS_REGISTER_STATE_DESC"
filter="string"
label="COM_VIBRANTVIDEOS_REGISTER_STATE_LABEL"
required="true"
spNavStyle="nav-left:#jform_city; nav-right:#jform_zip"
tabindex="9"
/>
will give us the desired output.
2. The case of a custom field
This can be trickier - or simpler - depending on your custom field code. Let's presume, that you have a custom list field, wich generates an option list from a database table - in the example below, the available subscription plans - once again, the EXTRA code needed to add the spatial navigation code and the tabindex is highlighted with RED:
<?php
/**
* @version 1.0.0
* @package com_mycomponent
* @copyright Copyright (C) 2014. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
* @author Székely Dénes <
*/
defined('JPATH_BASE') or die;
jimport('joomla.form.formfield');
/**
* Supports a value from an external table
*/
class JFormFieldPlan extends JFormField {
/**
* The form field type.
*
* @var string
* @since 1.6
*/
protected $type = 'plan';
protected $tabIndex;
protected $spNavStyle;
/**
* Method to get the field input markup for a generic list.
* Use the multiple attribute to enable multiselect.
*
* @return string The field input markup.
*
* @since 11.1
*/
protected function getInput()
{
$html = array();
$attr = '';
// Initialize some field attributes.
$attr .= $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : '';
// To avoid user's confusion, readonly="true" should imply disabled="true".
if ((string) $this->element['readonly'] == 'true' || (string) $this->element['disabled'] == 'true')
{
$attr .= ' disabled="disabled"';
}
$attr .= $this->element['size'] ? ' size="' . (int) $this->element['size'] . '"' : '';
$attr .= $this->multiple ? ' multiple="multiple"' : '';
$attr .= $this->required ? ' required="required" aria-required="true"' : '';
$attr .= $this->element['tabindex'] ? ' tabindex="' . (int)$this->element['tabindex'] . '"' : '';
$attr .= $this->element['spNavStyle'] ? ' style="' . (string)$this->element['spNavStyle'] . '"' : '';
// Initialize JavaScript field attributes.
$attr .= $this->element['onchange'] ? ' onchange="' . (string) $this->element['onchange'] . '"' : '';
$attr .= $this->element['data-bind'] ? ' data-bind="' . (string) $this->element['data-bind'] . '"' : '';
// Get the field options.
$db = JFactory::getDBO();
$query = $db->getQuery(true);
$query
->select($db->quoteName('a.id','id'))
->select($db->quoteName('a.name', 'name'))
->from($db->quoteName('#__mycomponent_plan', 'a'))
->order($db->quoteName('a.id') . ' ASC');
$db->setQuery((string)$query);
$items = $db->loadObjectList();
$options = array();
if($items){
foreach($items as $item){
$options[] = JHtml::_('select.option', $item->id, ucwords($item->name));
};
};
$html[] = JHtml::_('select.genericlist', $options, $this->name, trim($attr), 'value', 'text', $this->value);
return implode($html);
}
}