From 4a9fe06d50a3e9967cb3bb49dbe9443184e6870e Mon Sep 17 00:00:00 2001 From: Sam Georges Date: Sun, 29 Jun 2014 13:35:47 +1000 Subject: [PATCH] Form fields can now define "depends" to refresh individual fields when they are changed Form field options can now be deferred using a closure --- modules/backend/assets/css/october.css | 11 +- .../assets/js/october.loadindicator.js | 3 +- .../assets/less/controls/loadindicator.less | 10 ++ modules/backend/assets/less/controls/tab.less | 2 +- modules/backend/classes/FormField.php | 26 +++- modules/backend/widgets/Form.php | 88 ++++++++++++- .../backend/widgets/form/assets/js/form.js | 4 - .../widgets/form/assets/js/october.form.js | 116 ++++++++++++++++++ .../backend/widgets/form/partials/_field.htm | 37 +++--- .../form/partials/_field_balloon-selector.htm | 5 +- .../form/partials/_field_checkboxlist.htm | 11 +- .../widgets/form/partials/_field_dropdown.htm | 7 +- .../widgets/form/partials/_field_radio.htm | 7 +- .../backend/widgets/form/partials/_form.htm | 7 +- .../widgets/form/partials/_form_fields.htm | 10 +- 15 files changed, 296 insertions(+), 48 deletions(-) delete mode 100644 modules/backend/widgets/form/assets/js/form.js create mode 100644 modules/backend/widgets/form/assets/js/october.form.js diff --git a/modules/backend/assets/css/october.css b/modules/backend/assets/css/october.css index 24bc97637..6db41eb7f 100644 --- a/modules/backend/assets/css/october.css +++ b/modules/backend/assets/css/october.css @@ -8749,11 +8749,13 @@ html.cssanimations .loading-indicator.transparent > span { margin-top: -10px; background-size: 20px 20px; } +.loading-indicator-container.size-form-field .loading-indicator, .loading-indicator-container.size-input-text .loading-indicator { background-color: transparent; padding: 0; margin: 0; } +.loading-indicator-container.size-form-field .loading-indicator > span, .loading-indicator-container.size-input-text .loading-indicator > span { padding: 0; margin: 0; @@ -8764,6 +8766,13 @@ html.cssanimations .loading-indicator.transparent > span { height: 23px; background-size: 23px 23px; } +.loading-indicator-container.size-form-field .loading-indicator > span { + top: 0; + right: 0; + width: 20px; + height: 20px; + background-size: 20px 20px; +} html.cssanimations .cursor-loading-indicator { background: transparent url(../images/loading-indicator-transparent.svg) no-repeat 50% 50%; -webkit-animation: spin 1s linear infinite; @@ -10179,7 +10188,7 @@ body.dropdown-open .dropdown-overlay { .control-tabs.content-tabs.tabs-inset ul.nav-tabs li { border-top: 1px solid transparent; } -.control-tabs.content-tabs.tabs-inset ul.nav-tabs li .active { +.control-tabs.content-tabs.tabs-inset ul.nav-tabs li.active { border-top: 1px solid #e3e5e7; } .control-tabs.content-tabs.tabs-inset ul.nav-tabs li:first-child { diff --git a/modules/backend/assets/js/october.loadindicator.js b/modules/backend/assets/js/october.loadindicator.js index 42ec07b0c..a5bcc2667 100644 --- a/modules/backend/assets/js/october.loadindicator.js +++ b/modules/backend/assets/js/october.loadindicator.js @@ -11,7 +11,8 @@ * * JavaScript API: * - * $('#buttons').loadIndicator({text: 'Saving...', 'opaque': true}) - display the indicator + * $('#buttons').loadIndicator({text: 'Saving...', opaque: true}) - display the indicator in a solid (opaque) state + * $('#buttons').loadIndicator({text: 'Saving...'}) - display the indicator in a transparent state * $('#buttons').loadIndicator('hide') - display the indicator */ +function ($) { "use strict"; diff --git a/modules/backend/assets/less/controls/loadindicator.less b/modules/backend/assets/less/controls/loadindicator.less index f337ab30a..70dc43e74 100644 --- a/modules/backend/assets/less/controls/loadindicator.less +++ b/modules/backend/assets/less/controls/loadindicator.less @@ -66,6 +66,8 @@ html.cssanimations { } } + +.loading-indicator-container.size-form-field .loading-indicator, .loading-indicator-container.size-input-text .loading-indicator { background-color: transparent; padding: 0; @@ -83,6 +85,14 @@ html.cssanimations { } } +.loading-indicator-container.size-form-field .loading-indicator > span { + top: 0; + right: 0; + width: 20px; + height: 20px; + background-size: 20px 20px; +} + // // Cursor loading indicator // -------------------------------------------------- diff --git a/modules/backend/assets/less/controls/tab.less b/modules/backend/assets/less/controls/tab.less index aff6de4db..208093189 100644 --- a/modules/backend/assets/less/controls/tab.less +++ b/modules/backend/assets/less/controls/tab.less @@ -250,7 +250,7 @@ padding-left: 20px; li { border-top: 1px solid transparent; - .active { + &.active { border-top: 1px solid @color-tab-content-border; } } diff --git a/modules/backend/classes/FormField.php b/modules/backend/classes/FormField.php index 04b4b879a..29b7d5490 100644 --- a/modules/backend/classes/FormField.php +++ b/modules/backend/classes/FormField.php @@ -50,7 +50,7 @@ class FormField /** * @var string Field options. */ - public $options = []; + public $options; /** * @var string Specifies a side. Possible values: auto, left, right, full @@ -127,6 +127,11 @@ class FormField */ public $config; + /** + * @var array Other field names this field depends on, when the other fields are modified, this field will update. + */ + public $depends; + public function __construct($columnName, $label) { $this->columnName = $columnName; @@ -167,9 +172,23 @@ class FormField * @param array $value * @return self */ - public function options($value = []) + public function options($value = null) { - $this->options = $value; + if ($value === null) { + if (is_array($this->options)) { + return $this->options; + } + elseif (is_callable($this->options)) { + $callable = $this->options; + return $callable(); + } + + return []; + } + else { + $this->options = $value; + } + return $this; } @@ -209,6 +228,7 @@ class FormField if (isset($config['default'])) $this->defaults = $config['default']; if (isset($config['cssClass'])) $this->cssClass = $config['cssClass']; if (isset($config['attributes'])) $this->attributes = $config['attributes']; + if (isset($config['depends'])) $this->depends = $config['depends']; if (isset($config['path'])) $this->path = $config['path']; if (array_key_exists('required', $config)) $this->required = $config['required']; diff --git a/modules/backend/widgets/Form.php b/modules/backend/widgets/Form.php index 7d8772b51..e1885b195 100644 --- a/modules/backend/widgets/Form.php +++ b/modules/backend/widgets/Form.php @@ -120,7 +120,7 @@ class Form extends WidgetBase */ public function loadAssets() { - $this->addJs('js/form.js', 'core'); + $this->addJs('js/october.form.js', 'core'); } /** @@ -188,7 +188,7 @@ class Form extends WidgetBase if (!$this->model) throw new ApplicationException(Lang::get('backend::lang.form.missing_model', ['class'=>get_class($this->controller)])); - $this->data = (object)$this->getConfig('data', $this->model); + $this->data = (object) $this->getConfig('data', $this->model); return $this->model; } @@ -205,6 +205,57 @@ class Form extends WidgetBase $this->vars['secondaryTabs'] = $this->secondaryTabs; } + /** + * Sets or resets form field values. + * @param array $data + * @return array + */ + public function setFormValues($data = null) + { + if ($data == null) + $data = $this->getSaveData(); + + $this->model->fill($data); + $this->data = (object) array_merge((array) $this->data, (array) $data); + + foreach ($this->allFields as $field) + $field->value = $this->getFieldValue($field); + + return $data; + } + + /** + * Event handler for refreshing the form. + */ + public function onRender() + { + $this->setFormValues(); + $this->prepareVars(); + $result = []; + + /* + * If an array of fields is supplied, update specified fields individually. + */ + if (($updateFields = post('fields')) && is_array($updateFields)) { + + foreach ($updateFields as $field) { + if (!isset($this->allFields[$field])) + continue; + + $fieldObject = $this->allFields[$field]; + $result['#' . $fieldObject->getId('container')] = $this->renderField($fieldObject); + } + } + + /* + * Update the whole form + */ + if (empty($result)) + $result = ['#'.$this->getId() => $this->makePartial('form')]; + + return $result; + } + /** * Creates a flat array of form fields from the configuration. * Also slots fields in to their respective tabs. @@ -400,14 +451,25 @@ class Form extends WidgetBase */ $optionModelTypes = ['dropdown', 'radio', 'checkboxlist']; if (in_array($field->type, $optionModelTypes)) { - $fieldOptions = (isset($config['options'])) ? $config['options'] : null; - $fieldOptions = $this->getOptionsFromModel($field, $fieldOptions); - $field->options($fieldOptions); + + /* + * Defer the execution of option data collection + */ + $field->options(function() use ($field, $config) { + $fieldOptions = (isset($config['options'])) ? $config['options'] : null; + $fieldOptions = $this->getOptionsFromModel($field, $fieldOptions); + return $fieldOptions; + }); } return $field; } + /** + * Check if a field type is a widget or not + * @param string $fieldType + * @return boolean + */ private function isFormWidget($fieldType) { if ($fieldType === null) @@ -521,6 +583,22 @@ class Form extends WidgetBase return $result; } + /** + * Returns a HTML encoded value containing the other fields this + * field depends on + * @param use Backend\Classes\FormField $field + * @return string + */ + public function getFieldDepends($field) + { + if (!$field->depends) + return; + + $depends = is_array($field->depends) ? $field->depends : [$field->depends]; + $depends = htmlspecialchars(json_encode($depends), ENT_QUOTES, 'UTF-8'); + return $depends; + } + /** * Returns postback data from a submitted form. */ diff --git a/modules/backend/widgets/form/assets/js/form.js b/modules/backend/widgets/form/assets/js/form.js deleted file mode 100644 index 968613cee..000000000 --- a/modules/backend/widgets/form/assets/js/form.js +++ /dev/null @@ -1,4 +0,0 @@ -/* - * Form Behavior - */ - diff --git a/modules/backend/widgets/form/assets/js/october.form.js b/modules/backend/widgets/form/assets/js/october.form.js new file mode 100644 index 000000000..1a9379ba0 --- /dev/null +++ b/modules/backend/widgets/form/assets/js/october.form.js @@ -0,0 +1,116 @@ +/* + * Form Widget + * + * Dependences: + * - Nil + */ ++function ($) { "use strict"; + + var FormWidget = function (element, options) { + + var $el = this.$el = $(element); + + this.options = options || {}; + + this.bindDependants() + } + + FormWidget.DEFAULTS = { + refreshHandler: null + } + + /* + * Bind dependant fields + */ + FormWidget.prototype.bindDependants = function() { + var self = this, + form = this.$el, + formEl = form.closest('form'), + fieldMap = {} + + /* + * Map master and slave field map + */ + form.find('[data-field-depends]').each(function(){ + var name = $(this).data('column-name'), + depends = $(this).data('field-depends') + + $.each(depends, function(index, depend){ + if (!fieldMap[depend]) + fieldMap[depend] = { fields: [] } + + fieldMap[depend].fields.push(name) + }) + }) + + /* + * When a master is updated, refresh its slaves + */ + $.each(fieldMap, function(columnName, toRefresh){ + form.find('[data-column-name="'+columnName+'"]') + .on('change', 'select, input', function(){ + formEl.request(self.options.refreshHandler, { + data: toRefresh + }) + + $.each(toRefresh.fields, function(index, field){ + form.find('[data-column-name="'+field+'"]') + .addClass('loading-indicator-container size-form-field') + .loadIndicator() + }) + }) + }) + + + // dependants.on('change', 'select, input', function(){ + // var depends = $(this).closest('[data-field-depends]').data('field-depends'), + // form = $(this).closest('form') + + // if (!form.length || !self.options.refreshHandler) + // return + + // form.request(self.options.refreshHandler) + // }) + + } + + + // FORM WIDGET PLUGIN DEFINITION + // ============================ + + var old = $.fn.formWidget + + $.fn.formWidget = function (option) { + var args = arguments, + result + + this.each(function () { + var $this = $(this) + var data = $this.data('oc.formwidget') + var options = $.extend({}, FormWidget.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.formwidget', (data = new FormWidget(this, options))) + if (typeof option == 'string') result = data[option].call($this) + if (typeof result != 'undefined') return false + }) + + return result ? result : this + } + + $.fn.formWidget.Constructor = FormWidget + + // FORM WIDGET NO CONFLICT + // ================= + + $.fn.formWidget.noConflict = function () { + $.fn.formWidget = old + return this + } + + // FORM WIDGET DATA-API + // ============== + + $(document).render(function(){ + $('[data-control="formwidget"]').formWidget(); + }) + +}(window.jQuery); \ No newline at end of file diff --git a/modules/backend/widgets/form/partials/_field.htm b/modules/backend/widgets/form/partials/_field.htm index 7bf035430..fd8533002 100644 --- a/modules/backend/widgets/form/partials/_field.htm +++ b/modules/backend/widgets/form/partials/_field.htm @@ -1,24 +1,21 @@ -
+type, ['checkbox', 'switch'])): ?> - type, ['checkbox', 'switch'])): ?> + makePartial('field_'.$field->type, ['field' => $field]) ?> - makePartial('field_'.$field->type, ['field' => $field]) ?> - - - - label): ?> - - - - comment && $field->commentPosition == 'above'): ?> -

comment)) ?>

- - - renderFieldElement($field) ?> - - comment && $field->commentPosition == 'below'): ?> -

comment)) ?>

- + + label): ?> + -
+ + comment && $field->commentPosition == 'above'): ?> +

comment)) ?>

+ + + renderFieldElement($field) ?> + + comment && $field->commentPosition == 'below'): ?> +

comment)) ?>

+ + + \ No newline at end of file diff --git a/modules/backend/widgets/form/partials/_field_balloon-selector.htm b/modules/backend/widgets/form/partials/_field_balloon-selector.htm index ed9ad5c6c..fb38c7f32 100644 --- a/modules/backend/widgets/form/partials/_field_balloon-selector.htm +++ b/modules/backend/widgets/form/partials/_field_balloon-selector.htm @@ -1,7 +1,10 @@ +options(); +?>
attributes) ?>> diff --git a/modules/backend/widgets/form/partials/_field_checkboxlist.htm b/modules/backend/widgets/form/partials/_field_checkboxlist.htm index 719c6f520..51d47f93e 100644 --- a/modules/backend/widgets/form/partials/_field_checkboxlist.htm +++ b/modules/backend/widgets/form/partials/_field_checkboxlist.htm @@ -1,10 +1,11 @@ - options(); $checkedValues = (is_array($field->value)) ? $field->value : [$field->value]; ?> -options)): ?> + + - options) > 10): ?> + 10): ?> : @@ -17,7 +18,7 @@
- options as $value => $option): ?> + $option): ?> columnName.'_'.$index; @@ -42,7 +43,7 @@
- options) > 10): ?> + 10): ?>
diff --git a/modules/backend/widgets/form/partials/_field_dropdown.htm b/modules/backend/widgets/form/partials/_field_dropdown.htm index a194d1fac..fb69faccd 100644 --- a/modules/backend/widgets/form/partials/_field_dropdown.htm +++ b/modules/backend/widgets/form/partials/_field_dropdown.htm @@ -1,6 +1,9 @@ +options(); +?> previewMode): ?> -
options[$field->value])) ? e($field->options[$field->value]) : '' ?>
+
value])) ? e($fieldOptions[$field->value]) : '' ?>