diff --git a/CHANGELOG.md b/CHANGELOG.md index e3a4630a2..74c8716a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -* **Build 17x** (2014-12-xx) +* **Build 17x** (2015-01-xx) + - The variable `errors` will be included in a CMS page when redirecting via `Redirect::withErrors($validator)`. + +* **Build 174** (2015-01-05) - Improved asset caching (`cms.enableAssetCache`), when enabled the server will send a *304 Not Modified* header. - Introduced new *Table* widget and *DataTable* form widget. - There is now a simpler way for sending mail via `Mail::sendTo()`. diff --git a/app/config/testing/cms.php b/app/config/testing/cms.php index 7343d99a2..181bada7d 100644 --- a/app/config/testing/cms.php +++ b/app/config/testing/cms.php @@ -96,4 +96,16 @@ return array( */ 'twigNoCache' => true, + + /* + |-------------------------------------------------------------------------- + | Convert Line Endings + |-------------------------------------------------------------------------- + | + | Determines if October should convert line endings from the windows style + | \r\n to the unix style \n. + | + */ + + 'convertLineEndings' => true, ); diff --git a/modules/backend/assets/css/october.css b/modules/backend/assets/css/october.css index 38e3352b5..57520cea4 100644 --- a/modules/backend/assets/css/october.css +++ b/modules/backend/assets/css/october.css @@ -9013,6 +9013,7 @@ label { } .help-block { font-size: 12px; + margin-bottom: 0; } .help-block.before-field { margin-top: 0; diff --git a/modules/backend/assets/js/october.tab.js b/modules/backend/assets/js/october.tab.js index 6c9a8bebe..b10e9ec0f 100644 --- a/modules/backend/assets/js/october.tab.js +++ b/modules/backend/assets/js/october.tab.js @@ -250,8 +250,7 @@ if ($('> li > a', this.$tabsContainer).length == 0) this.$el.trigger('afterAllClosed.oc.tab') - - this.$el.trigger('closed.oc.tab') + this.$el.trigger('closed.oc.tab', [$tab]) $(window).trigger('resize') this.updateClasses() diff --git a/modules/backend/assets/less/controls/forms.less b/modules/backend/assets/less/controls/forms.less index fb34edf33..fee7d02e7 100644 --- a/modules/backend/assets/less/controls/forms.less +++ b/modules/backend/assets/less/controls/forms.less @@ -187,6 +187,7 @@ label { .help-block { font-size: 12px; + margin-bottom: 0; &.before-field { margin-top: 0; } diff --git a/modules/backend/behaviors/ListController.php b/modules/backend/behaviors/ListController.php index cf4896123..44c5005f1 100644 --- a/modules/backend/behaviors/ListController.php +++ b/modules/backend/behaviors/ListController.php @@ -15,7 +15,6 @@ use Backend\Classes\ControllerBehavior; */ class ListController extends ControllerBehavior { - /** * @var array List definitions, keys for alias and value for configuration. */ diff --git a/modules/backend/classes/BackendHelper.php b/modules/backend/classes/BackendHelper.php index e48701bfe..f07637c81 100644 --- a/modules/backend/classes/BackendHelper.php +++ b/modules/backend/classes/BackendHelper.php @@ -13,7 +13,6 @@ use October\Rain\Router\Helper as RouterHelper; */ class BackendHelper { - /** * Returns a URL in context of the Backend */ diff --git a/modules/backend/classes/FormField.php b/modules/backend/classes/FormField.php index 67b2fdacc..c67b6c5c6 100644 --- a/modules/backend/classes/FormField.php +++ b/modules/backend/classes/FormField.php @@ -395,4 +395,54 @@ class FormField return Str::evalHtmlId($id); } + + /** + * Returns this fields value from a supplied data set, which can be + * an array or a model or another generic collection. + * @param mixed $data + * @return mixed + */ + public function getValueFromData($data, $default = null) + { + $fieldName = $this->fieldName; + + /* + * Array field name, eg: field[key][key2][key3] + */ + $keyParts = Str::evalHtmlArray($fieldName); + $lastField = end($keyParts); + $result = $data; + + /* + * Loop the field key parts and build a value. + * To support relations only the last field should return the + * relation value, all others will look up the relation object as normal. + */ + foreach ($keyParts as $key) { + + if ($result instanceof Model && $result->hasRelation($key)) { + if ($key == $lastField) { + $result = $result->getRelationValue($key) ?: $default; + } + else { + $result = $result->{$key}; + } + } + elseif (is_array($result)) { + if (!array_key_exists($key, $result)) { + return $default; + } + $result = $result[$key]; + } + else { + if (!isset($result->{$key})) { + return $default; + } + $result = $result->{$key}; + } + + } + + return $result; + } } diff --git a/modules/backend/classes/FormTabs.php b/modules/backend/classes/FormTabs.php index 3a2e22804..0dab4e06a 100644 --- a/modules/backend/classes/FormTabs.php +++ b/modules/backend/classes/FormTabs.php @@ -16,7 +16,6 @@ use ArrayAccess; */ class FormTabs implements IteratorAggregate, ArrayAccess { - const SECTION_OUTSIDE = 'outside'; const SECTION_PRIMARY = 'primary'; const SECTION_SECONDARY = 'secondary'; @@ -176,5 +175,4 @@ class FormTabs implements IteratorAggregate, ArrayAccess { return isset($this->fields[$offset]) ? $this->fields[$offset] : null; } - -} \ No newline at end of file +} diff --git a/modules/backend/classes/FormWidgetBase.php b/modules/backend/classes/FormWidgetBase.php index 5a5748e1b..da539c723 100644 --- a/modules/backend/classes/FormWidgetBase.php +++ b/modules/backend/classes/FormWidgetBase.php @@ -11,7 +11,6 @@ use Str; */ abstract class FormWidgetBase extends WidgetBase { - /** * @var FormField Object containing general form field information. */ @@ -88,11 +87,11 @@ abstract class FormWidgetBase extends WidgetBase } /** - * Process the postback data for this widget. + * Process the postback value for this widget. * @param $value The existing value for this widget. * @return string The new value for this widget. */ - public function getSaveData($value) + public function getSaveValue($value) { return $value; } @@ -102,25 +101,19 @@ abstract class FormWidgetBase extends WidgetBase * supports nesting via HTML array. * @return string */ - public function getLoadData() + public function getLoadValue() { - list($model, $attribute) = $this->getModelArrayAttribute($this->valueFrom); - - if (!is_null($model)) { - return $model->{$attribute}; - } - - return null; + return $this->formField->getValueFromData($this->model); } /** * Returns the final model and attribute name of * a nested HTML array attribute. - * Eg: list($model, $attribute) = $this->getModelArrayAttribute($this->valueFrom); + * Eg: list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom); * @param string $attribute. * @return array */ - public function getModelArrayAttribute($attribute) + public function resolveModelAttribute($attribute) { $model = $this->model; $parts = Str::evalHtmlArray($attribute); @@ -132,4 +125,5 @@ abstract class FormWidgetBase extends WidgetBase return [$model, $last]; } + } diff --git a/modules/backend/classes/ListColumn.php b/modules/backend/classes/ListColumn.php index 6cf0a32bb..afc44cec5 100644 --- a/modules/backend/classes/ListColumn.php +++ b/modules/backend/classes/ListColumn.php @@ -9,7 +9,6 @@ */ class ListColumn { - /** * @var string List column name. */ diff --git a/modules/backend/classes/WidgetBase.php b/modules/backend/classes/WidgetBase.php index 763f6ed72..40016f78a 100644 --- a/modules/backend/classes/WidgetBase.php +++ b/modules/backend/classes/WidgetBase.php @@ -13,7 +13,6 @@ use Session; */ abstract class WidgetBase { - use \System\Traits\AssetMaker; use \System\Traits\ConfigMaker; use \System\Traits\ViewMaker; diff --git a/modules/backend/classes/WidgetManager.php b/modules/backend/classes/WidgetManager.php index b46cc3623..399c6df7e 100644 --- a/modules/backend/classes/WidgetManager.php +++ b/modules/backend/classes/WidgetManager.php @@ -2,12 +2,10 @@ use Str; use File; -use Lang; use Closure; use October\Rain\Support\Yaml; use Illuminate\Container\Container; use System\Classes\PluginManager; -use System\Classes\SystemException; /** * Widget manager @@ -57,34 +55,6 @@ class WidgetManager $this->pluginManager = PluginManager::instance(); } - /** - * Makes a widget object with configuration set. - * @param string $className A widget class name. - * @param Controller $controller The Backend controller that spawned this widget. - * @param array $configuration Configuration values. - * @return WidgetBase The widget object. - */ - public function makeWidget($className, $controller = null, $configuration = null) - { - /* - * Build configuration - */ - if ($configuration === null) { - $configuration = []; - } - - /* - * Create widget object - */ - if (!class_exists($className)) { - throw new SystemException(Lang::get('backend::lang.widget.not_registered', [ - 'name' => $className - ])); - } - - return new $className($controller, $configuration); - } - // // Form Widgets // diff --git a/modules/backend/controllers/AccessLogs.php b/modules/backend/controllers/AccessLogs.php index 6dd8dc476..1c1bd7617 100644 --- a/modules/backend/controllers/AccessLogs.php +++ b/modules/backend/controllers/AccessLogs.php @@ -21,7 +21,6 @@ use Exception; */ class AccessLogs extends Controller { - public $implement = [ 'Backend.Behaviors.ListController' ]; diff --git a/modules/backend/controllers/EditorPreferences.php b/modules/backend/controllers/EditorPreferences.php index 459aa91fc..b1e0f52ab 100644 --- a/modules/backend/controllers/EditorPreferences.php +++ b/modules/backend/controllers/EditorPreferences.php @@ -14,7 +14,6 @@ use Backend\Models\EditorPreferences as EditorPreferencesModel; */ class EditorPreferences extends Controller { - public $implement = [ 'Backend.Behaviors.FormController', ]; diff --git a/modules/backend/formwidgets/CodeEditor.php b/modules/backend/formwidgets/CodeEditor.php index 605f3d2b5..701f837c3 100644 --- a/modules/backend/formwidgets/CodeEditor.php +++ b/modules/backend/formwidgets/CodeEditor.php @@ -106,7 +106,7 @@ class CodeEditor extends FormWidgetBase $this->vars['stretch'] = $this->formField->stretch; $this->vars['size'] = $this->formField->size; $this->vars['name'] = $this->formField->getName(); - $this->vars['value'] = $this->getLoadData(); + $this->vars['value'] = $this->getLoadValue(); } /** diff --git a/modules/backend/formwidgets/ColorPicker.php b/modules/backend/formwidgets/ColorPicker.php index 463109843..6aeef0682 100644 --- a/modules/backend/formwidgets/ColorPicker.php +++ b/modules/backend/formwidgets/ColorPicker.php @@ -55,7 +55,7 @@ class ColorPicker extends FormWidgetBase public function prepareVars() { $this->vars['name'] = $this->formField->getName(); - $this->vars['value'] = $value = $this->getLoadData(); + $this->vars['value'] = $value = $this->getLoadValue(); $this->vars['availableColors'] = $this->availableColors; $this->vars['isCustomColor'] = !in_array($value, $this->availableColors); } @@ -74,7 +74,7 @@ class ColorPicker extends FormWidgetBase /** * {@inheritDoc} */ - public function getSaveData($value) + public function getSaveValue($value) { return strlen($value) ? $value : null; } diff --git a/modules/backend/formwidgets/DataGrid.php b/modules/backend/formwidgets/DataGrid.php index 8a14211ba..705eae131 100644 --- a/modules/backend/formwidgets/DataGrid.php +++ b/modules/backend/formwidgets/DataGrid.php @@ -69,7 +69,7 @@ class DataGrid extends FormWidgetBase /** * {@inheritDoc} */ - public function getSaveData($value) + public function getSaveValue($value) { return json_decode($value); } diff --git a/modules/backend/formwidgets/DataTable.php b/modules/backend/formwidgets/DataTable.php index 441a60670..705620fae 100644 --- a/modules/backend/formwidgets/DataTable.php +++ b/modules/backend/formwidgets/DataTable.php @@ -69,7 +69,7 @@ class DataTable extends FormWidgetBase /** * {@inheritDoc} */ - public function getSaveData($value) + public function getSaveValue($value) { // TODO: provide a streaming implementation of saving // data to the model. The current implementation returns @@ -96,7 +96,7 @@ class DataTable extends FormWidgetBase // data from the model. The current implementation loads // all records at once. -ab - $records = $this->getLoadData() ?: []; + $records = $this->getLoadValue() ?: []; $dataSource->initRecords((array) $records); } @@ -104,7 +104,13 @@ class DataTable extends FormWidgetBase { $config = $this->makeConfig((array) $this->config); $config->dataSource = 'client'; - $config->alias = $this->alias . 'Table'; + + // It's safe to use the field name as an alias + // as field names do not repeat in forms. This + // approach lets to access the table data by the + // field name in POST requests directly (required + // in some edge cases). + $config->alias = $this->fieldName; $table = new Table($this->controller, $config); diff --git a/modules/backend/formwidgets/DatePicker.php b/modules/backend/formwidgets/DatePicker.php index 1e283aaff..0c1fabf42 100644 --- a/modules/backend/formwidgets/DatePicker.php +++ b/modules/backend/formwidgets/DatePicker.php @@ -62,7 +62,7 @@ class DatePicker extends FormWidgetBase $this->vars['timeName'] = self::TIME_PREFIX.$this->formField->getName(false); $this->vars['timeValue'] = null; - if ($value = $this->getLoadData()) { + if ($value = $this->getLoadValue()) { /* * Date / Time @@ -120,7 +120,7 @@ class DatePicker extends FormWidgetBase /** * {@inheritDoc} */ - public function getSaveData($value) + public function getSaveValue($value) { if (!strlen($value)) { return null; diff --git a/modules/backend/formwidgets/FileUpload.php b/modules/backend/formwidgets/FileUpload.php index 5212de4cf..929d5a757 100644 --- a/modules/backend/formwidgets/FileUpload.php +++ b/modules/backend/formwidgets/FileUpload.php @@ -109,7 +109,7 @@ class FileUpload extends FormWidgetBase */ protected function getRelationObject() { - list($model, $attribute) = $this->getModelArrayAttribute($this->valueFrom); + list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom); return $model->{$attribute}(); } @@ -120,7 +120,7 @@ class FileUpload extends FormWidgetBase */ protected function getRelationType() { - list($model, $attribute) = $this->getModelArrayAttribute($this->valueFrom); + list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom); return $model->getRelationType($attribute); } @@ -195,7 +195,7 @@ class FileUpload extends FormWidgetBase /** * {@inheritDoc} */ - public function getSaveData($value) + public function getSaveValue($value) { return FormField::NO_SAVE_DATA; } diff --git a/modules/backend/formwidgets/RecordFinder.php b/modules/backend/formwidgets/RecordFinder.php index 45db4ff90..3e94b77fb 100644 --- a/modules/backend/formwidgets/RecordFinder.php +++ b/modules/backend/formwidgets/RecordFinder.php @@ -130,7 +130,7 @@ class RecordFinder extends FormWidgetBase public function onRefresh() { - list($model, $attribute) = $this->getModelArrayAttribute($this->valueFrom); + list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom); $model->{$attribute} = post($this->formField->getName()); $this->prepareVars(); @@ -142,8 +142,7 @@ class RecordFinder extends FormWidgetBase */ public function prepareVars() { - // This should be a relation and return a Model - $this->relationModel = $this->getLoadData(); + $this->relationModel = $this->getLoadValue(); $this->vars['value'] = $this->getKeyValue(); $this->vars['field'] = $this->formField; @@ -165,11 +164,25 @@ class RecordFinder extends FormWidgetBase /** * {@inheritDoc} */ - public function getSaveData($value) + public function getSaveValue($value) { return strlen($value) ? $value : null; } + /** + * {@inheritDoc} + */ + public function getLoadValue() + { + list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom); + + if (!is_null($model)) { + return $model->{$attribute}; + } + + return null; + } + public function getKeyValue() { if (!$this->relationModel) { diff --git a/modules/backend/formwidgets/Relation.php b/modules/backend/formwidgets/Relation.php index 16c576ff1..bfd8d97b7 100644 --- a/modules/backend/formwidgets/Relation.php +++ b/modules/backend/formwidgets/Relation.php @@ -104,7 +104,7 @@ class Relation extends FormWidgetBase $field = clone $this->formField; - list($model, $attribute) = $this->getModelArrayAttribute($this->relationName); + list($model, $attribute) = $this->resolveModelAttribute($this->relationName); $relatedObj = $model->makeRelation($attribute); $query = $model->{$attribute}()->newQuery(); @@ -141,7 +141,7 @@ class Relation extends FormWidgetBase /** * {@inheritDoc} */ - public function getSaveData($value) + public function getSaveValue($value) { if (is_string($value) && !strlen($value)) { return null; diff --git a/modules/backend/formwidgets/RichEditor.php b/modules/backend/formwidgets/RichEditor.php index 5e626f7a6..945834214 100644 --- a/modules/backend/formwidgets/RichEditor.php +++ b/modules/backend/formwidgets/RichEditor.php @@ -41,7 +41,7 @@ class RichEditor extends FormWidgetBase $this->vars['stretch'] = $this->formField->stretch; $this->vars['size'] = $this->formField->size; $this->vars['name'] = $this->formField->getName(); - $this->vars['value'] = $this->getLoadData(); + $this->vars['value'] = $this->getLoadValue(); } /** diff --git a/modules/backend/routes.php b/modules/backend/routes.php index 56ef8a3e1..63b93c388 100644 --- a/modules/backend/routes.php +++ b/modules/backend/routes.php @@ -1,10 +1,9 @@ controller) ?: $this; + $controller = property_exists($this, 'controller') && $this->controller + ? $this->controller + : $this; - $manager = WidgetManager::instance(); - $widget = $manager->makeWidget($class, $controller, $configuration); - return $widget; + if (!class_exists($class)) { + throw new SystemException(Lang::get('backend::lang.widget.not_registered', [ + 'name' => $class + ])); + } + + return new $class($controller, $configuration); } } diff --git a/modules/backend/widgets/Form.php b/modules/backend/widgets/Form.php index 3081793bc..b9b388155 100644 --- a/modules/backend/widgets/Form.php +++ b/modules/backend/widgets/Form.php @@ -709,47 +709,11 @@ class Form extends WidgetBase $field = $this->fields[$field]; } - $fieldName = $field->fieldName; - $defaultValue = (!$this->model->exists && $field->defaults !== '') ? $field->defaults : null; + $defaultValue = (!$this->model->exists && $field->defaults !== '') + ? $field->defaults + : null; - /* - * Array field name, eg: field[key][key2][key3] - */ - $keyParts = Str::evalHtmlArray($fieldName); - $lastField = end($keyParts); - $result = $this->data; - - /* - * Loop the field key parts and build a value. - * To support relations only the last field should return the - * relation value, all others will look up the relation object as normal. - */ - foreach ($keyParts as $key) { - - if ($result instanceof Model && $result->hasRelation($key)) { - if ($key == $lastField) { - $result = $result->getRelationValue($key) ?: $defaultValue; - } - else { - $result = $result->{$key}; - } - } - elseif (is_array($result)) { - if (!array_key_exists($key, $result)) { - return $defaultValue; - } - $result = $result[$key]; - } - else { - if (!isset($result->{$key})) { - return $defaultValue; - } - $result = $result->{$key}; - } - - } - - return $result; + return $field->getValueFromData($this->data, $defaultValue); } /** @@ -806,7 +770,16 @@ class Form extends WidgetBase $parts = Str::evalHtmlArray($field); $dotted = implode('.', $parts); - $widgetValue = $widget->getSaveData(array_get($data, $dotted)); + $widgetValue = $widget->getSaveValue(array_get($data, $dotted)); + + /* + * @deprecated Remove if year >= 2016 + */ + if (method_exists($widget, 'getSaveData')) { + traceLog('Method getSaveData() is deprecated, use getSaveValue() instead. Found in: ' . get_class($widget), 'warning'); + $widgetValue = $widget->getSaveData(array_get($data, $dotted)); + } + array_set($data, $dotted, $widgetValue); } diff --git a/modules/backend/widgets/Table.php b/modules/backend/widgets/Table.php index 3a3fe8191..a9f3d740c 100644 --- a/modules/backend/widgets/Table.php +++ b/modules/backend/widgets/Table.php @@ -63,7 +63,10 @@ class Table extends WidgetBase $this->dataSource = new $dataSourceClass($this->recordsKeyFrom); if (Request::method() == 'POST' && $this->isClientDataSource()) { - $requestDataField = $this->alias.'TableData'; + if ( strpos($this->alias, '[') === false ) + $requestDataField = $this->alias.'TableData'; + else + $requestDataField = $this->alias.'[TableData]'; if (Request::exists($requestDataField)) { // Load data into the client memory data source on POST @@ -135,7 +138,7 @@ class Table extends WidgetBase } /** - * Converts the columns associative array to a regular array. + * Converts the columns associative array to a regular array and translates column headers and drop-down options. * Working with regular arrays is much faster in JavaScript. * References: * - http://www.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/ @@ -147,6 +150,15 @@ class Table extends WidgetBase foreach ($this->columns as $key=>$data) { $data['key'] = $key; + + if (isset($data['title'])) + $data['title'] = trans($data['title']); + + if (isset($data['options'])) { + foreach ($data['options'] as &$option) + $option = trans($option); + } + $result[] = $data; } diff --git a/modules/backend/widgets/table/assets/css/table.css b/modules/backend/widgets/table/assets/css/table.css index c872831ae..6a8324843 100644 --- a/modules/backend/widgets/table/assets/css/table.css +++ b/modules/backend/widgets/table/assets/css/table.css @@ -2,13 +2,19 @@ * General control styling */ .control-table .table-container { - border: 1px solid #808c8d; + border: 1px solid #e0e0e0; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; overflow: hidden; margin-bottom: 15px; } +.control-table .table-container:last-child { + margin-bottom: 0; +} +.control-table.active .table-container { + border-color: #808c8d; +} .control-table table { width: 100%; border-collapse: collapse; @@ -35,7 +41,7 @@ left: 1px; right: 1px; margin-top: -1px; - border-bottom: 1px solid #bdc3c7; + border-bottom: 1px solid #e0e0e0; } .control-table table.headers th { padding: 7px 10px; @@ -55,9 +61,11 @@ .control-table table.headers th:last-child { border-right: none; } +.control-table.active table.headers:after { + border-bottom-color: #808c8d; +} .control-table table.data td { border: 1px solid #ecf0f1; - /* TODO: this should be applied only when the control is active */ } .control-table table.data td .content-container { position: relative; @@ -114,7 +122,7 @@ } .control-table .toolbar { background: white; - border-bottom: 1px solid #bdc3c7; + border-bottom: 1px solid #e0e0e0; } .control-table .toolbar a { color: #323e50; @@ -144,6 +152,9 @@ .control-table .toolbar a.delete-table-row:before { background-position: 0 -113px; } +.control-table.active .toolbar { + border-bottom-color: #808c8d; +} .control-table .pagination ul { padding: 0; margin-bottom: 15px; @@ -304,6 +315,7 @@ html.cssanimations .control-table td[data-column-type=dropdown] [data-view-conta border-top: none; padding-top: 1px; overflow: hidden; + z-index: 1000; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; diff --git a/modules/backend/widgets/table/assets/js/table.js b/modules/backend/widgets/table/assets/js/table.js index e83a9d73c..65f1cf37a 100644 --- a/modules/backend/widgets/table/assets/js/table.js +++ b/modules/backend/widgets/table/assets/js/table.js @@ -65,6 +65,7 @@ // Event handlers this.clickHandler = this.onClick.bind(this) this.keydownHandler = this.onKeydown.bind(this) + this.documentClickHandler = this.onDocumentClick.bind(this) this.toolbarClickHandler = this.onToolbarClick.bind(this) if (this.options.postback && this.options.clientDataSourceClass == 'client') @@ -135,6 +136,7 @@ Table.prototype.registerHandlers = function() { this.el.addEventListener('click', this.clickHandler) this.el.addEventListener('keydown', this.keydownHandler) + document.addEventListener('click', this.documentClickHandler) if (this.options.postback && this.options.clientDataSourceClass == 'client') this.$el.closest('form').bind('oc.beforeRequest', this.formSubmitHandler) @@ -146,6 +148,8 @@ Table.prototype.unregisterHandlers = function() { this.el.removeEventListener('click', this.clickHandler); + document.removeEventListener('click', this.documentClickHandler) + this.clickHandler = null this.el.removeEventListener('keydown', this.keydownHandler); @@ -449,6 +453,8 @@ * Removes editor from the currently edited cell and commits the row if needed. */ Table.prototype.unfocusTable = function() { + this.elementRemoveClass(this.el, 'active') + if (this.activeCellProcessor) this.activeCellProcessor.onUnfocus() @@ -461,6 +467,13 @@ this.activeCell = null } + /* + * Makes the table focused in the UI + */ + Table.prototype.focusTable = function() { + this.elementAddClass(this.el, 'active') + } + /* * Calls the onFocus() method for the cell processor responsible for the * newly focused cell. Commit the previous edited row to the data source @@ -471,6 +484,8 @@ if (columnName === null) return + this.focusTable() + var processor = this.getCellProcessor(columnName) if (!processor) throw new Error("Cell processor not found for the column "+columnName) @@ -615,6 +630,8 @@ // ============================ Table.prototype.onClick = function(ev) { + this.focusTable() + if (this.navigation.onClick(ev) === false) return @@ -670,7 +687,12 @@ Table.prototype.onFormSubmit = function(ev, data) { if (data.handler == this.options.postbackHandlerName) { this.unfocusTable() - data.options.data[this.options.alias + 'TableData'] = this.dataSource.getAllData() + + var fieldName = this.options.alias.indexOf('[') > -1 ? + this.options.alias + '[TableData]' : + this.options.alias + 'TableData'; + + data.options.data[fieldName] = this.dataSource.getAllData() } } @@ -693,6 +715,24 @@ this.stopEvent(ev) } + Table.prototype.onDocumentClick = function(ev) { + var target = this.getEventTarget(ev) + + // Determine if the click was inside the table element + // and just exit if so + if (this.parentContainsElement(this.el, target)) + return + + // Request the active cell processor if the clicked + // element belongs to any extra-table element created + // by the processor + + if (this.activeCellProcessor && this.activeCellProcessor.elementBelongsToProcessor(target)) + return + + this.unfocusTable() + } + // PUBLIC METHODS // ============================ @@ -790,6 +830,44 @@ ev.returnValue = false } + Table.prototype.elementHasClass = function(el, className) { + // TODO: refactor to a core library + + if (el.classList) + return el.classList.contains(className); + + return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className); + } + + Table.prototype.elementAddClass = function(el, className) { + // TODO: refactor to a core library + + if (this.elementHasClass(el, className)) + return + + if (el.classList) + el.classList.add(className); + else + el.className += ' ' + className; + } + + Table.prototype.elementRemoveClass = function(el, className) { + // TODO: refactor to a core library + + if (el.classList) + el.classList.remove(className); + else + el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); + } + + Table.prototype.parentContainsElement = function(parent, element) { + while (element && element != parent) { + element = element.parentNode + } + + return element ? true : false + } + Table.prototype.getCellValue = function(cellElement) { return cellElement.querySelector('[data-container]').value } diff --git a/modules/backend/widgets/table/assets/js/table.processor.base.js b/modules/backend/widgets/table/assets/js/table.processor.base.js index 0eb04a4b5..515143acb 100644 --- a/modules/backend/widgets/table/assets/js/table.processor.base.js +++ b/modules/backend/widgets/table/assets/js/table.processor.base.js @@ -167,5 +167,13 @@ return this.getViewContainer(cellElement).textContent = value } + /* + * Determines whether the specified element is some element created by the + * processor. + */ + Base.prototype.elementBelongsToProcessor = function(element) { + return false + } + $.oc.table.processor.base = Base }(window.jQuery); \ No newline at end of file diff --git a/modules/backend/widgets/table/assets/js/table.processor.dropdown.js b/modules/backend/widgets/table/assets/js/table.processor.dropdown.js index ad984e956..42b4b8be7 100644 --- a/modules/backend/widgets/table/assets/js/table.processor.dropdown.js +++ b/modules/backend/widgets/table/assets/js/table.processor.dropdown.js @@ -246,7 +246,7 @@ DropdownProcessor.prototype.getAbsolutePosition = function(element) { // TODO: refactor to a core library - var top = 0, + var top = document.body.scrollTop, left = 0 do { @@ -344,7 +344,7 @@ /* * This method is called when a cell value in the row changes. */ - Base.prototype.onRowValueChanged = function(columnName, cellElement) { + DropdownProcessor.prototype.onRowValueChanged = function(columnName, cellElement) { // Determine if this drop-down depends on the changed column // and update the option list if necessary @@ -382,5 +382,16 @@ }) } + /* + * Determines whether the specified element is some element created by the + * processor. + */ + DropdownProcessor.prototype.elementBelongsToProcessor = function(element) { + if (!this.itemListElement) + return false + + return this.tableObj.parentContainsElement(this.itemListElement, element) + } + $.oc.table.processor.dropdown = DropdownProcessor; }(window.jQuery); \ No newline at end of file diff --git a/modules/backend/widgets/table/assets/less/table.less b/modules/backend/widgets/table/assets/less/table.less index 42ad7ade8..83c040d8e 100644 --- a/modules/backend/widgets/table/assets/less/table.less +++ b/modules/backend/widgets/table/assets/less/table.less @@ -4,12 +4,23 @@ * General control styling */ +@table-active-border: #808c8d; +@table-inactive-border: #e0e0e0; + .control-table { .table-container { - border: 1px solid #808c8d; + border: 1px solid @table-inactive-border; .border-radius(4px); overflow: hidden; margin-bottom: 15px; + + &:last-child { + margin-bottom: 0; + } + } + + &.active .table-container { + border-color: @table-active-border; } table { @@ -41,7 +52,7 @@ left: 1px; right: 1px; margin-top: -1px; - border-bottom: 1px solid #bdc3c7; + border-bottom: 1px solid @table-inactive-border; } th { @@ -66,6 +77,10 @@ } } + &.active table.headers:after { + border-bottom-color: @table-active-border; + } + table.data { td { border: 1px solid #ecf0f1; @@ -75,7 +90,6 @@ padding: 1px; } - /* TODO: this should be applied only when the control is active */ &.active { border-color: @color-focus!important; @@ -146,7 +160,7 @@ .toolbar { background: white; - border-bottom: 1px solid #bdc3c7; + border-bottom: 1px solid @table-inactive-border; a { color: #323e50; @@ -180,6 +194,10 @@ } } + &.active .toolbar { + border-bottom-color: @table-active-border; + } + .pagination { ul { padding: 0; @@ -354,10 +372,11 @@ html.cssanimations { .user-select(none); position: absolute; background: white; - border: 1px solid #808c8d; + border: 1px solid @table-active-border; border-top: none; padding-top: 1px; overflow: hidden; + z-index: 1000; .box-sizing(border-box); .border-bottom-radius(4px); diff --git a/modules/cms/assets/js/october.cmspage.js b/modules/cms/assets/js/october.cmspage.js index f85d9694d..1160462ac 100644 --- a/modules/cms/assets/js/october.cmspage.js +++ b/modules/cms/assets/js/october.cmspage.js @@ -80,7 +80,7 @@ }) /* - * Listen for the closed event + * Listen for the closing events */ $('#cms-master-tabs').on('closed.oc.tab', function(event){ updateModifiedCounter() @@ -89,6 +89,11 @@ setPageTitle('') }) + $('#cms-master-tabs').on('beforeClose.oc.tab', function(event){ + // Dispose data table widgets + $('[data-control=table]', event.relatedTarget).table('dispose') + }) + /* * Listen for the onBeforeRequest event */ diff --git a/modules/cms/classes/CmsException.php b/modules/cms/classes/CmsException.php index ef236fbc6..f85f7cac3 100644 --- a/modules/cms/classes/CmsException.php +++ b/modules/cms/classes/CmsException.php @@ -16,7 +16,6 @@ use Exception; */ class CmsException extends ApplicationException { - /** * @var Cms\Classes\CmsCompoundObject A reference to a CMS object used for masking errors. */ diff --git a/modules/cms/classes/CmsObjectQuery.php b/modules/cms/classes/CmsObjectQuery.php index 2308213a3..a30dd441a 100644 --- a/modules/cms/classes/CmsObjectQuery.php +++ b/modules/cms/classes/CmsObjectQuery.php @@ -10,7 +10,6 @@ use System\Classes\ApplicationException; */ class CmsObjectQuery { - protected $useCache = false; protected $cmsObject; diff --git a/modules/cms/classes/Controller.php b/modules/cms/classes/Controller.php index d29d813dc..dcb14b242 100644 --- a/modules/cms/classes/Controller.php +++ b/modules/cms/classes/Controller.php @@ -8,6 +8,7 @@ use View; use Lang; use Event; use Config; +use Session; use Request; use Response; use Exception; @@ -235,6 +236,13 @@ class Controller extends BaseController 'environment' => App::environment(), ]; + /* + * Check for the presence of validation errors in the session. + */ + $this->vars['errors'] = (Config::get('session.driver') && Session::has('errors')) + ? Session::get('errors') + : new \Illuminate\Support\ViewErrorBag; + /* * Handle AJAX requests and execute the life cycle functions */ diff --git a/modules/cms/classes/SectionParser.php b/modules/cms/classes/SectionParser.php index 74027e3e0..a9e913684 100644 --- a/modules/cms/classes/SectionParser.php +++ b/modules/cms/classes/SectionParser.php @@ -1,5 +1,7 @@ $settings + ]; + + Event::fire('cms.template.processSettingsBeforeSave', [$this, $dataHolder]); + + return $dataHolder->settings; } /** diff --git a/modules/cms/routes.php b/modules/cms/routes.php index 3667a5f22..5e84b32a6 100644 --- a/modules/cms/routes.php +++ b/modules/cms/routes.php @@ -1,14 +1,12 @@ where('slug', '(.*)?'); - }); diff --git a/modules/system/behaviors/SettingsModel.php b/modules/system/behaviors/SettingsModel.php index 8b22a8b4c..095439126 100644 --- a/modules/system/behaviors/SettingsModel.php +++ b/modules/system/behaviors/SettingsModel.php @@ -1,6 +1,7 @@ getSettingsRecord() !== null; + return DbDongle::hasDatabase() && $this->getSettingsRecord() !== null; } /** diff --git a/modules/system/classes/Controller.php b/modules/system/classes/Controller.php index ae68562b3..d1ed59eb5 100644 --- a/modules/system/classes/Controller.php +++ b/modules/system/classes/Controller.php @@ -13,7 +13,6 @@ use Exception; */ class Controller extends BaseController { - /** * Combines JavaScript and StyleSheet assets. * @param string $name Combined file code diff --git a/modules/system/classes/ErrorHandler.php b/modules/system/classes/ErrorHandler.php index 2e572dda7..40912e107 100644 --- a/modules/system/classes/ErrorHandler.php +++ b/modules/system/classes/ErrorHandler.php @@ -19,7 +19,6 @@ use System\Classes\ApplicationException; */ class ErrorHandler { - /** * @var System\Classes\ExceptionBase A prepared mask exception used to mask any exception fired. */ diff --git a/modules/system/classes/ExceptionBase.php b/modules/system/classes/ExceptionBase.php index 09818c6e0..b25a3f834 100644 --- a/modules/system/classes/ExceptionBase.php +++ b/modules/system/classes/ExceptionBase.php @@ -16,7 +16,6 @@ use System\Classes\ApplicationException; */ class ExceptionBase extends Exception { - /** * @var Exception If this exception is acting as a mask, this property stores the face exception. */ diff --git a/modules/system/classes/ModelBehavior.php b/modules/system/classes/ModelBehavior.php index 2959a0b13..60301b5bb 100644 --- a/modules/system/classes/ModelBehavior.php +++ b/modules/system/classes/ModelBehavior.php @@ -12,7 +12,6 @@ use October\Rain\Database\ModelBehavior as ModelBehaviorBase; */ class ModelBehavior extends ModelBehaviorBase { - /** * @var array Properties that must exist in the model using this behavior. */ @@ -29,7 +28,8 @@ class ModelBehavior extends ModelBehaviorBase /* * Validate model properties */ - foreach ($this->requiredProperties as $property) { + foreach ($this->requiredProperties as $property) + { if (!isset($model->{$property})) { throw new ApplicationException(Lang::get('system::lang.behavior.missing_property', [ 'class' => get_class($model), diff --git a/modules/system/classes/PluginManager.php b/modules/system/classes/PluginManager.php index 507d75c3e..491822060 100644 --- a/modules/system/classes/PluginManager.php +++ b/modules/system/classes/PluginManager.php @@ -211,7 +211,6 @@ class PluginManager /** * Returns the directory path to a plugin - * */ public function getPluginPath($id) { @@ -220,7 +219,7 @@ class PluginManager return null; } - return $this->pathMap[$classId]; + return File::normalizePath($this->pathMap[$classId]); } /** diff --git a/modules/system/classes/SystemException.php b/modules/system/classes/SystemException.php index cad2361cd..b7d40c0f5 100644 --- a/modules/system/classes/SystemException.php +++ b/modules/system/classes/SystemException.php @@ -1,5 +1,6 @@ - ./tests + ./tests/unit ./vendor/october/rain/tests diff --git a/tests/phpunit.xml b/tests/functional/phpunit.xml similarity index 89% rename from tests/phpunit.xml rename to tests/functional/phpunit.xml index e29aad30f..a5415e7e5 100644 --- a/tests/phpunit.xml +++ b/tests/functional/phpunit.xml @@ -1,7 +1,7 @@ makeWidget('Backend\Widgets\Search'); - $this->assertTrue($widget instanceof \Backend\Widgets\Search); - - $controller = new Controller; - $widget = $manager->makeWidget('Backend\Widgets\Search', $controller); - $this->assertInstanceOf('Backend\Widgets\Search', $widget); - $this->assertInstanceOf('Backend\Classes\Controller', $widget->getController()); - - $config = ['test' => 'config']; - $widget = $manager->makeWidget('Backend\Widgets\Search', null, $config); - $this->assertInstanceOf('Backend\Widgets\Search', $widget); - $this->assertEquals('config', $widget->getConfig('test')); - } - public function testListFormWidgets() { $manager = WidgetManager::instance(); diff --git a/tests/unit/backend/traits/WidgetMakerTest.php b/tests/unit/backend/traits/WidgetMakerTest.php new file mode 100644 index 000000000..3401fe6df --- /dev/null +++ b/tests/unit/backend/traits/WidgetMakerTest.php @@ -0,0 +1,60 @@ +controller = new Controller; + } +} + +class WidgetMakerTest extends TestCase +{ + /** + * The object under test. + * + * @var object + */ + private $traitObject; + + /** + * Sets up the fixture. + * + * This method is called before a test is executed. + * + * @return void + */ + public function setUp() + { + $traitName = 'Backend\Traits\WidgetMaker'; + $this->traitObject = $this->getObjectForTrait($traitName); + } + + public function testTraitObject() + { + $maker = $this->traitObject; + + $widget = $maker->makeWidget('Backend\Widgets\Search'); + $this->assertTrue($widget instanceof \Backend\Widgets\Search); + } + + public function testMakeWidget() + { + $manager = new ExampleTraitClass; + + $controller = new Controller; + $widget = $manager->makeWidget('Backend\Widgets\Search'); + $this->assertInstanceOf('Backend\Widgets\Search', $widget); + $this->assertInstanceOf('Backend\Classes\Controller', $widget->getController()); + + $config = ['test' => 'config']; + $widget = $manager->makeWidget('Backend\Widgets\Search', $config); + $this->assertInstanceOf('Backend\Widgets\Search', $widget); + $this->assertEquals('config', $widget->getConfig('test')); + } + +} \ No newline at end of file diff --git a/tests/unit/cms/classes/CmsCompoundObjectTest.php b/tests/unit/cms/classes/CmsCompoundObjectTest.php index d5628d798..e5fcaa093 100644 --- a/tests/unit/cms/classes/CmsCompoundObjectTest.php +++ b/tests/unit/cms/classes/CmsCompoundObjectTest.php @@ -162,7 +162,7 @@ class CmsCompoundObjectTest extends TestCase $this->assertFileExists($referenceFilePath); $this->assertFileExists($destFilePath); - $this->assertFileEquals($referenceFilePath, $destFilePath); + $this->assertFileEqualsNormalized($referenceFilePath, $destFilePath); } public function testSaveMarkupAndSettings() @@ -187,7 +187,7 @@ class CmsCompoundObjectTest extends TestCase $this->assertFileExists($referenceFilePath); $this->assertFileExists($destFilePath); - $this->assertFileEquals($referenceFilePath, $destFilePath); + $this->assertFileEqualsNormalized($referenceFilePath, $destFilePath); } public function testSaveFull() @@ -195,8 +195,9 @@ class CmsCompoundObjectTest extends TestCase $theme = Theme::load('apitest'); $destFilePath = $theme->getPath().'/testobjects/compound.htm'; - if (file_exists($destFilePath)) + if (file_exists($destFilePath)) { unlink($destFilePath); + } $this->assertFileNotExists($destFilePath); @@ -213,6 +214,22 @@ class CmsCompoundObjectTest extends TestCase $this->assertFileExists($referenceFilePath); $this->assertFileExists($destFilePath); - $this->assertFileEquals($referenceFilePath, $destFilePath); + $this->assertFileEqualsNormalized($referenceFilePath, $destFilePath); } + + // + // Helpers + // + + protected function assertFileEqualsNormalized($expected, $actual) + { + $expected = file_get_contents($expected); + $expected = preg_replace('~\R~u', PHP_EOL, $expected); // Normalize EOL + + $actual = file_get_contents($actual); + $actual = preg_replace('~\R~u', PHP_EOL, $actual); // Normalize EOL + + $this->assertEquals($expected, $actual); + } + } \ No newline at end of file diff --git a/tests/unit/cms/classes/CmsObjectTest.php b/tests/unit/cms/classes/CmsObjectTest.php index f74193b16..481fc4a33 100644 --- a/tests/unit/cms/classes/CmsObjectTest.php +++ b/tests/unit/cms/classes/CmsObjectTest.php @@ -141,7 +141,7 @@ class CmsObjectTest extends TestCase /** * @expectedException \System\Classes\ApplicationException - * @expectedExceptionMessage The property "something" cannot be set + * @expectedExceptionMessage The property 'something' cannot be set */ public function testFillNotFillable() { diff --git a/tests/unit/cms/classes/CodeParserTest.php b/tests/unit/cms/classes/CodeParserTest.php index e87d79e1d..00799b932 100644 --- a/tests/unit/cms/classes/CodeParserTest.php +++ b/tests/unit/cms/classes/CodeParserTest.php @@ -253,10 +253,22 @@ class CodeParserTest extends TestCase $referenceFilePath = base_path().'/tests/fixtures/cms/reference/namespaces.php'; $this->assertFileExists($referenceFilePath); - $referenceContents = file_get_contents($referenceFilePath); + $referenceContents = $this->getContents($referenceFilePath); $referenceContents = str_replace('{className}', $info['className'], $referenceContents); - $this->assertEquals($referenceContents, file_get_contents($info['filePath'])); + $this->assertEquals($referenceContents, $this->getContents($info['filePath'])); } + + // + // Helpers + // + + protected function getContents($path) + { + $content = file_get_contents($path); + $content = preg_replace('~\R~u', PHP_EOL, $content); // Normalize EOL + return $content; + } + } \ No newline at end of file diff --git a/tests/unit/cms/classes/FileHelperTest.php b/tests/unit/cms/classes/FileHelperTest.php index 2b9075ea1..43f7f6ae4 100644 --- a/tests/unit/cms/classes/FileHelperTest.php +++ b/tests/unit/cms/classes/FileHelperTest.php @@ -28,7 +28,7 @@ class FileHelperTest extends TestCase $str = FileHelper::formatIniString($data); $this->assertNotEmpty($str); - $this->assertEquals(file_get_contents($path), $str); + $this->assertEquals($this->getContents($path), $str); $data = [ 'section' => [ @@ -47,7 +47,7 @@ class FileHelperTest extends TestCase $this->assertFileExists($path); $str = FileHelper::formatIniString($data); - $this->assertEquals(file_get_contents($path), $str); + $this->assertEquals($this->getContents($path), $str); $data = [ 'section' => [ @@ -75,6 +75,18 @@ class FileHelperTest extends TestCase $this->assertFileExists($path); $str = FileHelper::formatIniString($data); - $this->assertEquals(file_get_contents($path), $str); + $this->assertEquals($this->getContents($path), $str); } + + // + // Helpers + // + + protected function getContents($path) + { + $content = file_get_contents($path); + $content = preg_replace('~\R~u', PHP_EOL, $content); // Normalize EOL + return $content; + } + } diff --git a/tests/unit/cms/classes/RouterTest.php b/tests/unit/cms/classes/RouterTest.php index b1d75936c..12c3c402f 100644 --- a/tests/unit/cms/classes/RouterTest.php +++ b/tests/unit/cms/classes/RouterTest.php @@ -6,11 +6,10 @@ use Cms\Classes\Theme; class RouterTest extends TestCase { protected static $theme = null; - + public static function setUpBeforeClass() { - self::$theme = new Theme(); - self::$theme->load('test'); + self::$theme = Theme::load('test'); } protected static function getMethod($name) @@ -28,7 +27,7 @@ class RouterTest extends TestCase $property->setAccessible(true); return $property; } - + public function testLoadUrlMap() { $method = self::getMethod('loadUrlMap'); diff --git a/tests/unit/cms/classes/ThemeTest.php b/tests/unit/cms/classes/ThemeTest.php index 3c5b65c20..f3192aa56 100644 --- a/tests/unit/cms/classes/ThemeTest.php +++ b/tests/unit/cms/classes/ThemeTest.php @@ -2,7 +2,7 @@ use Cms\Classes\Theme; -class ThemeTest extends TestCase +class ThemeTest extends TestCase { public function setUp() { diff --git a/tests/unit/phpunit.xml b/tests/unit/phpunit.xml new file mode 100644 index 000000000..a5415e7e5 --- /dev/null +++ b/tests/unit/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./ + + + \ No newline at end of file diff --git a/tests/unit/system/classes/PluginManagerTest.php b/tests/unit/system/classes/PluginManagerTest.php index ff0a92f5a..dbfcff19a 100644 --- a/tests/unit/system/classes/PluginManagerTest.php +++ b/tests/unit/system/classes/PluginManagerTest.php @@ -72,7 +72,8 @@ class PluginManagerTest extends TestCase { $manager = PluginManager::instance(); $result = $manager->getPluginPath('October\Tester'); - $this->assertEquals(base_path() . '/tests/fixtures/system/plugins/october/tester', $result); + $basePath = str_replace('\\', '/', base_path()); + $this->assertEquals($basePath . '/tests/fixtures/system/plugins/october/tester', $result); } public function testGetPlugins() diff --git a/themes/demo/layouts/default.htm b/themes/demo/layouts/default.htm index 68260361c..3a22acd52 100644 --- a/themes/demo/layouts/default.htm +++ b/themes/demo/layouts/default.htm @@ -3,6 +3,7 @@ description = "Default layout" + October CMS - {{ this.page.title }} @@ -60,4 +61,4 @@ description = "Default layout" {% scripts %} - \ No newline at end of file +