From a4067bfc48d995e9320844c109ebfb5b10ca001e Mon Sep 17 00:00:00 2001 From: Ruslan Kabalin Date: Thu, 7 Feb 2013 14:13:36 +0000 Subject: [PATCH] MDL-30637 Simplify moodle forms Make the forms easier to use and navigate by adding functionality to collapse and extend form sections (headers). The logic is as follows: If form contains 2 and less sections (headers): * Display the form as non-collapsible at all. * The point above can be overridden if developer marks the section as expanded in form definition (e.g. $mform->setExpanded('foo')); If form contains 3 and more sections (headers): * always expanding the first section and closing all others by default; * always expanding a section containing at least one "required" element; * expanding any section which contains validation errors after submission; * expanding any section which was previously open on previous submit (e.g. when adding new choices); * expanding the section which is marked as expanded in form definition (e.g. $mform->setExpanded('foo'); --- lib/form/yui/shortforms/shortforms.js | 96 ++++++++++++++ lib/formslib.php | 178 +++++++++++++++++++++++--- theme/base/style/core.css | 4 + 3 files changed, 259 insertions(+), 19 deletions(-) create mode 100644 lib/form/yui/shortforms/shortforms.js diff --git a/lib/form/yui/shortforms/shortforms.js b/lib/form/yui/shortforms/shortforms.js new file mode 100644 index 00000000000..0a81d14a6c7 --- /dev/null +++ b/lib/form/yui/shortforms/shortforms.js @@ -0,0 +1,96 @@ +YUI.add('moodle-form-shortforms', function(Y) { + /** + * Provides the form shortforms class + * + * @module moodle-form-shortforms + */ + + /** + * A class for a shortforms + * + * @param {Object} config Object literal specifying shortforms configuration properties. + * @class M.form.shortforms + * @constructor + * @extends Y.Base + */ + function SHORTFORMS(config) { + SHORTFORMS.superclass.constructor.apply(this, [config]); + } + + var SELECTORS = { + FIELDSETCOLLAPSIBLE : 'fieldset.collapsible', + LEGENDFTOGGLER : 'legend.ftoggler' + }, + CSS = { + COLLAPSED : 'collapsed', + FHEADER : 'fheader', + JSPROCESSED : 'jsprocessed' + }, + ATTRS = {}; + + /** + * Static property provides a string to identify the JavaScript class. + * + * @property NAME + * @type String + * @static + */ + SHORTFORMS.NAME = 'moodle-form-shortforms'; + + /** + * Static property used to define the default attribute configuration for the Shortform. + * + * @property ATTRS + * @type String + * @static + */ + SHORTFORMS.ATTRS = ATTRS; + + /** + * The form ID attribute definition + * + * @attribute formid + * @type String + * @default '' + * @writeOnce + */ + ATTRS.formid = { + value : null + }; + + Y.extend(SHORTFORMS, Y.Base, { + initializer : function() { + var fieldlist = Y.Node.all('#'+this.get('formid')+' '+SELECTORS.FIELDSETCOLLAPSIBLE); + // Look through collapsible fieldset divs + fieldlist.each(this.process_fieldset, this); + // Subscribe collapsible fieldsets to click event + Y.one('#'+this.get('formid')).delegate('click', this.switch_state, SELECTORS.FIELDSETCOLLAPSIBLE+' .'+CSS.FHEADER); + }, + process_fieldset : function(fieldset) { + fieldset.addClass(CSS.JSPROCESSED); + // Get legend element + var legendelement = fieldset.one(SELECTORS.LEGENDFTOGGLER); + + // Turn headers to links for accessibility + var headerlink = Y.Node.create(''); + headerlink.addClass(CSS.FHEADER); + headerlink.appendChild(legendelement.get('firstChild')); + legendelement.prepend(headerlink); + }, + switch_state : function(e) { + e.preventDefault(); + var fieldset = this.ancestor(SELECTORS.FIELDSETCOLLAPSIBLE); + // toggle collapsed class + fieldset.toggleClass(CSS.COLLAPSED); + // get corresponding hidden variable + var statuselement = new Y.one('input[name=mform_isexpanded_'+fieldset.get('id')+']'); + // and invert it + statuselement.set('value', Math.abs(Number(statuselement.get('value'))-1)); + } + }); + + M.form = M.form || {}; + M.form.shortforms = M.form.shortforms || function(params) { + return new SHORTFORMS(params); + }; +}, '@VERSION@', {requires:['base', 'node', 'selector-css3']}); diff --git a/lib/formslib.php b/lib/formslib.php index 03c8c334e83..430045aea7e 100644 --- a/lib/formslib.php +++ b/lib/formslib.php @@ -1069,6 +1069,9 @@ abstract class moodleform { $mform->setType($elementname, $params); } break; + case 'expanded' : + $mform->setExpanded($realelementname, $params); + break; } } } @@ -1260,7 +1263,25 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless { /** @var array Array whose keys are element names. If the key exists this is a advanced element */ var $_advancedElements = array(); - /** @var bool Whether to display advanced elements (on page load) */ + /** + * Array whose keys are element names and the the boolean values reflect the current state. If the key exists this is a collapsible element. + * + * @var array + */ + var $_collapsibleElements = array(); + + /** + * Whether to enable shortforms for this form + * + * @var boolean + */ + var $_disableShortforms = false; + + /** + * Whether to display advanced elements (on page load) + * + * @var boolean + */ var $_showAdvanced = null; /** @var bool whether to automatically initialise M.formchangechecker for this form. */ @@ -1280,6 +1301,14 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless { */ var $_pageparams = ''; + /** + * The maximum number of headers the form should contain in order not to be + * defined as collapsible. + * + * @var int + */ + var $_non_collapsible_headers = 2; + /** * Class constructor - same parameters as HTML_QuickForm_DHTMLRulesTableless * @@ -1348,6 +1377,39 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless { $this->setType('mform_showadvanced_last', PARAM_INT); } } + + /** + * Use this method to indicate that the fieldset should be shown as expanded. + * The method is applicable to header elements only. + * + * @param string $headerName header element name + * @param boolean $expanded default true sets the element to expanded. False makes the element collapsed. + */ + function setExpanded($headerName, $expanded=true){ + if ($this->getElementType('mform_isexpanded_'.$headerName)===false) { + // see if we the form has been submitted already + $formexpanded = optional_param('mform_isexpanded_'.$headerName, -1, PARAM_INT); + if (!$expanded && $formexpanded != -1) { + // override expanded state with the form variable + $expanded = $formexpanded; + } + // create the form element for storing expanded state + $this->addElement('hidden', 'mform_isexpanded_'.$headerName); + $this->setType('mform_isexpanded_'.$headerName, PARAM_INT); + $this->setConstant('mform_isexpanded_' . $headerName, (int)$expanded); + } + $this->_collapsibleElements[$headerName] = !$expanded; + } + + /** + * Use this method to indicate that the form will not be using shortforms. + * + * @param boolean $disable default true, controls if the shortforms are disabled. + */ + function setDisableShortforms ($disable = true) { + $this->_disableShortforms = $disable; + } + /** * Set whether to show advanced elements in the form on first displaying form. Default is not to * display advanced elements in the form until 'Show Advanced' is pressed. @@ -1427,9 +1489,9 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless { */ function accept(&$renderer) { if (method_exists($renderer, 'setAdvancedElements')){ - //check for visible fieldsets where all elements are advanced + //Check for visible fieldsets where all elements are advanced //and mark these headers as advanced as well. - //And mark all elements in a advanced header as advanced + //Also mark all elements in a advanced header as advanced. $stopFields = $renderer->getStopFieldSetElements(); $lastHeader = null; $lastHeaderAdvanced = false; @@ -1462,7 +1524,58 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless { $this->setAdvanced($lastHeader->getName()); } $renderer->setAdvancedElements($this->_advancedElements); - + } + if (method_exists($renderer, 'setCollapsibleElements') && !$this->_disableShortforms){ + // Check how many headers we have in total, if less than $_non_collapsible_headers, + // the form should not be collapsible at all (unless overidden in the form definition). + $headercounter = 0; + foreach (array_keys($this->_elements) as $elementIndex){ + $element =& $this->_elements[$elementIndex]; + if ($element->getType()=='header') { + $headercounter++; + } + } + if ($headercounter > $this->_non_collapsible_headers) { + // So, we have more than $_non_collapsible_headers headers + // add all headers to collapsible elements array (if they have not been added yet). + unset($lastHeader); + $lastHeader = null; + $anyRequiredOrError = false; + $headercounter = 0; + foreach (array_keys($this->_elements) as $elementIndex){ + $element =& $this->_elements[$elementIndex]; + if ($element->getType()=='header') { + if (!is_null($lastHeader)) { + // Check if we had any required elements or + // we are at the top header that should be expanded by default. + if ($anyRequiredOrError || $headercounter === 1) { + $this->setExpanded($lastHeader->getName()); + } elseif (!isset($this->_collapsibleElements[$lastHeader->getName()])) { + // Define element as collapsed by default. + $this->setExpanded($lastHeader->getName(), false); + } + } + $headercounter++; + $lastHeader =& $element; + $anyRequiredOrError = false; + } elseif (in_array($element->getName(), $this->_required) || isset($this->_errors[$element->getName()])) { + $anyRequiredOrError = true; + } + } + // Process very last header. + if (!is_null($lastHeader)){ + // Check if we had any required elements or + // we are at the top header that should be expanded by default. + if ($anyRequiredOrError || $headercounter === 1) { + $this->setExpanded($lastHeader->getName()); + } elseif (!isset($this->_collapsibleElements[$lastHeader->getName()])) { + // Define element as collapsed by default. + $this->setExpanded($lastHeader->getName(), false); + } + } + } + // Pass the array to renderer object. + $renderer->setCollapsibleElements($this->_collapsibleElements, $this->getAttribute('id')); } parent::accept($renderer); } @@ -2228,7 +2341,7 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{ "\n\t\t{header}\n\t\t
{advancedimg}{button}
\n\t\t"; /** @var string Template used when opening a fieldset */ - var $_openFieldsetTemplate = "\n\t
"; + var $_openFieldsetTemplate = "\n\t
"; /** @var string Template used when closing a fieldset */ var $_closeFieldsetTemplate = "\n\t\t
"; @@ -2236,11 +2349,25 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{ /** @var string Required Note template string */ var $_requiredNoteTemplate = "\n\t\t
{requiredNote}
"; - - /** @var array list of elements which are marked as advance and will be grouped together */ + /** + * Array whose keys are element names. If the key exists this is a advanced element + * + * @var array + */ var $_advancedElements = array(); - /** @var int Whether to display advanced elements (on page load) 1 => show, 0 => hide */ + /** + * Array whose keys are element names and the the boolean values reflect the current state. If the key exists this is a collapsible element. + * + * @var array + */ + var $_collapsibleElements = array(); + + /** + * Whether to display advanced elements (on page load) + * + * @var integer 1 means show 0 means hide + */ var $_showAdvanced; /** @@ -2274,6 +2401,14 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{ $this->_advancedElements = $elements; } + /** + * Setting collapsible elements + * + * @param array $elements + */ + function setCollapsibleElements($elements) { + $this->_collapsibleElements = $elements; + } /** * What to do when starting the form * @@ -2285,6 +2420,7 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{ $this->_elementTemplates = str_replace('{req}', $this->_reqHTML, $this->_elementTemplates); $this->_advancedHTML = $form->getAdvancedHTML(); $this->_showAdvanced = $form->getShowAdvanced(); + $formid = $form->getAttribute('id'); parent::startForm($form); if ($form->isFrozen()){ $this->_formTemplate = "\n
\n{content}\n
"; @@ -2297,11 +2433,14 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{ $PAGE->requires->yui_module('moodle-core-formchangechecker', 'M.core_formchangechecker.init', array(array( - 'formid' => $form->getAttribute('id') + 'formid' => $formid )) ); $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle'); } + if (count($this->_collapsibleElements)) { + $PAGE->requires->yui_module('moodle-form-shortforms', 'M.form.shortforms', array(array('formid' => $formid))); + } } /** @@ -2472,17 +2611,18 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{ $this->_fieldsetsOpen--; } + // Define collapsible classes for fieldsets + $fieldsetclasses = array('clearfix'); + if (isset($this->_collapsibleElements[$name])) { + $fieldsetclasses[] = 'collapsible'; + if ($this->_collapsibleElements[$name]) { + $fieldsetclasses[] = 'collapsed'; + } + } + $openFieldsetTemplate = str_replace('{id}', $id, $this->_openFieldsetTemplate); - if ($this->_showAdvanced){ - $advclass = ' class="advanced"'; - } else { - $advclass = ' class="advanced hide"'; - } - if (isset($this->_advancedElements[$name])){ - $openFieldsetTemplate = str_replace('{advancedclass}', $advclass, $openFieldsetTemplate); - } else { - $openFieldsetTemplate = str_replace('{advancedclass}', '', $openFieldsetTemplate); - } + $openFieldsetTemplate = str_replace('{classes}', join(' ', $fieldsetclasses), $openFieldsetTemplate); + $this->_html .= $openFieldsetTemplate . $header_html; $this->_fieldsetsOpen++; } diff --git a/theme/base/style/core.css b/theme/base/style/core.css index f9bf057c1aa..bf8880682b8 100644 --- a/theme/base/style/core.css +++ b/theme/base/style/core.css @@ -234,6 +234,10 @@ a.skip:active {position: static;display: block;} .mform fieldset.hidden {border-width:0;} .mform fieldset.group {margin-bottom: 0} .mform fieldset.error {border: 1px solid #A00;} +.mform fieldset.collapsible legend a.fheader {padding: 0 5px 0 15px; background: url([[pix:t/expanded]]) 2px center no-repeat;} +.mform fieldset.collapsed legend a.fheader {background: url([[pix:t/collapsed]]) 2px center no-repeat;} +.mform fieldset.collapsed.jsprocessed {border-width: 1px 0 0 0; padding: 0;} +.mform fieldset.collapsed.jsprocessed div.fcontainer {display: none;} .mform .fitem {width:100%;overflow:hidden;margin-top:5px;margin-bottom:1px;clear:right;} .mform .fitem .fitemtitle {width:15%;text-align:right;float:left;} .mform .fitem .fitemtitle div {display: inline;}