From 3c7c87b33817e69c1ed7f4714d839965c8ebd96c Mon Sep 17 00:00:00 2001 From: Sam Georges Date: Wed, 16 Jul 2014 18:28:15 +1000 Subject: [PATCH 1/4] Fixes #254 add Laravel helper functions to Twig environment --- modules/cms/ServiceProvider.php | 43 ------ modules/cms/classes/CombineAssets.php | 9 +- modules/system/ServiceProvider.php | 39 ++++++ modules/system/classes/MarkupManager.php | 127 ++++++++++++++++++ modules/system/twig/Extension.php | 25 +--- tests/unit/cms/classes/ControllerTest.php | 3 +- .../unit/system/classes/MarkupManagerTest.php | 111 +++++++++++++++ 7 files changed, 293 insertions(+), 64 deletions(-) create mode 100644 tests/unit/system/classes/MarkupManagerTest.php diff --git a/modules/cms/ServiceProvider.php b/modules/cms/ServiceProvider.php index 28f57e5c2..2a528b8b4 100644 --- a/modules/cms/ServiceProvider.php +++ b/modules/cms/ServiceProvider.php @@ -4,7 +4,6 @@ use Lang; use Backend; use BackendMenu; use BackendAuth; -use System\Classes\MarkupManager; use Backend\Classes\WidgetManager; use October\Rain\Support\ModuleServiceProvider; @@ -93,48 +92,6 @@ class ServiceProvider extends ModuleServiceProvider ]); }); - /* - * Register markup tags - */ - MarkupManager::instance()->registerCallback(function($manager){ - $manager->registerFunctions([ - // Global helpers - 'post' => 'post', - - // Form helpers - 'form_ajax' => ['Form', 'ajax'], - 'form_open' => ['Form', 'open'], - 'form_close' => ['Form', 'close'], - 'form_token' => ['Form', 'token'], - 'form_session_key' => ['Form', 'sessionKey'], - 'form_token' => ['Form', 'token'], - 'form_model' => ['Form', 'model'], - 'form_label' => ['Form', 'label'], - 'form_text' => ['Form', 'text'], - 'form_password' => ['Form', 'password'], - 'form_checkbox' => ['Form', 'checkbox'], - 'form_radio' => ['Form', 'radio'], - 'form_file' => ['Form', 'file'], - 'form_select' => ['Form', 'select'], - 'form_select_range' => ['Form', 'selectRange'], - 'form_select_month' => ['Form', 'selectMonth'], - 'form_submit' => ['Form', 'submit'], - 'form_macro' => ['Form', '__call'], - 'form_value' => ['Form', 'value'], - ]); - - $manager->registerFilters([ - // String helpers - 'slug' => ['Str', 'slug'], - 'plural' => ['Str', 'plural'], - 'singular' => ['Str', 'singular'], - 'finish' => ['Str', 'finish'], - 'snake' => ['Str', 'snake'], - 'camel' => ['Str', 'camel'], - 'studly' => ['Str', 'studly'], - ]); - }); - /* * Register widgets */ diff --git a/modules/cms/classes/CombineAssets.php b/modules/cms/classes/CombineAssets.php index f1e41418d..be749c15d 100644 --- a/modules/cms/classes/CombineAssets.php +++ b/modules/cms/classes/CombineAssets.php @@ -4,6 +4,7 @@ use URL; use File; use Lang; use Cache; +use Route; use Config; use Request; use Response; @@ -193,7 +194,13 @@ class CombineAssets */ protected function getCombinedUrl($outputFilename = 'undefined.css') { - return URL::action('Cms\Classes\Controller@combine', [$outputFilename], false); + $combineAction = 'Cms\Classes\Controller@combine'; + $actionExists = Route::getRoutes()->getByAction($combineAction) !== null; + + if ($actionExists) + return URL::action($combineAction, [$outputFilename], false); + else + return Request::getBasePath().'/combine/'.$outputFilename; } /** diff --git a/modules/system/ServiceProvider.php b/modules/system/ServiceProvider.php index dba82c288..430934ab3 100644 --- a/modules/system/ServiceProvider.php +++ b/modules/system/ServiceProvider.php @@ -10,6 +10,7 @@ use BackendAuth; use Twig_Environment; use Twig_Loader_String; use System\Classes\ErrorHandler; +use System\Classes\MarkupManager; use System\Classes\PluginManager; use System\Classes\SettingsManager; use System\Twig\Engine as TwigEngine; @@ -179,6 +180,44 @@ class ServiceProvider extends ModuleServiceProvider ]); }); + /* + * Register markup tags + */ + MarkupManager::instance()->registerCallback(function($manager){ + $manager->registerFunctions([ + // Functions + 'post' => 'post', + 'link_to' => 'link_to', + 'link_to_asset' => 'link_to_asset', + 'link_to_route' => 'link_to_route', + 'link_to_action' => 'link_to_action', + 'asset' => 'asset', + 'action' => 'action', + 'url' => 'url', + 'route' => 'route', + 'secure_url' => 'secure_url', + 'secure_asset' => 'secure_asset', + + // Classes + 'str_*' => ['Str', '*'], + 'url_*' => ['URL', '*'], + 'html_*' => ['HTML', '*'], + 'form_*' => ['Form', '*'], + 'form_macro' => ['Form', '__call'], + ]); + + $manager->registerFilters([ + // Classes + 'slug' => ['Str', 'slug'], + 'plural' => ['Str', 'plural'], + 'singular' => ['Str', 'singular'], + 'finish' => ['Str', 'finish'], + 'snake' => ['Str', 'snake'], + 'camel' => ['Str', 'camel'], + 'studly' => ['Str', 'studly'], + ]); + }); + /* * Register settings */ diff --git a/modules/system/classes/MarkupManager.php b/modules/system/classes/MarkupManager.php index 969e7886f..6a6453900 100644 --- a/modules/system/classes/MarkupManager.php +++ b/modules/system/classes/MarkupManager.php @@ -1,5 +1,10 @@ listExtensions(self::EXTENSION_TOKEN_PARSER); } + /** + * Makes a set of Twig functions for use in a twig extension. + * @param array $functions Current collection + * @return array + */ + public function makeTwigFunctions($functions = []) + { + if (!is_array($functions)) + $functions = []; + + foreach ($this->listFunctions() as $name => $callable) { + + /* + * Handle a wildcard function + */ + if (strpos($name, '*') !== false && $this->isWildCallable($callable)) { + $callable = function($name) use ($callable) { + $arguments = array_slice(func_get_args(), 1); + $method = $this->isWildCallable($callable, Str::camel($name)); + return call_user_func_array($method, $arguments); + }; + } + + if (!is_callable($callable)) + throw new ApplicationException(sprintf('The markup function for %s is not callable.', $name)); + + $functions[] = new Twig_SimpleFunction($name, $callable, ['is_safe' => ['html']]); + } + + return $functions; + } + + /** + * Makes a set of Twig filters for use in a twig extension. + * @param array $filters Current collection + * @return array + */ + public function makeTwigFilters($filters = []) + { + if (!is_array($filters)) + $filters = []; + + foreach ($this->listFilters() as $name => $callable) { + + /* + * Handle a wildcard function + */ + if (strpos($name, '*') !== false && $this->isWildCallable($callable)) { + $callable = function($name) use ($callable) { + $arguments = array_slice(func_get_args(), 1); + $method = $this->isWildCallable($callable, Str::camel($name)); + return call_user_func_array($method, $arguments); + }; + } + + if (!is_callable($callable)) + throw new ApplicationException(sprintf('The markup filter for %s is not callable.', $name)); + + $filters[] = new Twig_SimpleFilter($name, $callable, ['is_safe' => ['html']]); + } + + return $filters; + } + + /** + * Makes a set of Twig token parsers for use in a twig extension. + * @param array $parsers Current collection + * @return array + */ + public function makeTwigTokenParsers($parsers = []) + { + if (!is_array($parsers)) + $parsers = []; + + $extraParsers = $this->listTokenParsers(); + foreach ($extraParsers as $obj) { + if (!$obj instanceof Twig_TokenParser) + continue; + + $parsers[] = $obj; + } + + return $parsers; + } + + /** + * Tests if a callable type contains a wildcard, also acts as a + * utility to replace the wildcard with a string. + * @param callable $callable + * @param string $replaceWith + * @return mixed + */ + protected function isWildCallable($callable, $replaceWith = false) + { + $isWild = false; + + if (is_string($callable) && strpos($callable, '*') !== false) + $isWild = $replaceWith ? str_replace('*', $replaceWith, $callable) : true; + + if (is_array($callable)) { + if (is_string($callable[0]) && strpos($callable[0], '*') !== false) { + if ($replaceWith) { + $isWild = $callable; + $isWild[0] = str_replace('*', $replaceWith, $callable[0]); + } + else + $isWild = true; + } + + if (!empty($callable[1]) && strpos($callable[1], '*') !== false) { + if ($replaceWith) { + $isWild = $isWild ?: $callable; + $isWild[1] = str_replace('*', $replaceWith, $callable[1]); + } + else + $isWild = true; + } + } + + return $isWild; + } + } \ No newline at end of file diff --git a/modules/system/twig/Extension.php b/modules/system/twig/Extension.php index c51a8450b..71483a294 100644 --- a/modules/system/twig/Extension.php +++ b/modules/system/twig/Extension.php @@ -52,12 +52,7 @@ class Extension extends Twig_Extension /* * Include extensions provided by plugins */ - foreach ($this->markupManager->listFunctions() as $name => $callable) { - if (!is_callable($callable)) - throw new ApplicationException(sprintf('The markup function for %s is not callable.', $name)); - - $functions[] = new Twig_SimpleFunction($name, $callable, ['is_safe' => ['html']]); - } + $functions = $this->markupManager->makeTwigFunctions($functions); return $functions; } @@ -76,12 +71,7 @@ class Extension extends Twig_Extension /* * Include extensions provided by plugins */ - foreach ($this->markupManager->listFilters() as $name => $callable) { - if (!is_callable($callable)) - throw new ApplicationException(sprintf('The markup filter for %s is not callable.', $name)); - - $filters[] = new Twig_SimpleFilter($name, $callable, ['is_safe' => ['html']]); - } + $filters = $this->markupManager->makeTwigFilters($filters); return $filters; } @@ -95,13 +85,10 @@ class Extension extends Twig_Extension { $parsers = []; - $extraParsers = $this->markupManager->listTokenParsers(); - foreach ($extraParsers as $obj) { - if (!$obj instanceof Twig_TokenParser) - continue; - - $parsers[] = $obj; - } + /* + * Include extensions provided by plugins + */ + $parsers = $this->markupManager->makeTwigTokenParsers($parsers); return $parsers; } diff --git a/tests/unit/cms/classes/ControllerTest.php b/tests/unit/cms/classes/ControllerTest.php index 4c04e7f2f..cae7f33d3 100644 --- a/tests/unit/cms/classes/ControllerTest.php +++ b/tests/unit/cms/classes/ControllerTest.php @@ -5,7 +5,8 @@ use Cms\Classes\Theme; class ControllerTest extends TestCase { - public function tearDown() { + public function tearDown() + { Mockery::close(); } diff --git a/tests/unit/system/classes/MarkupManagerTest.php b/tests/unit/system/classes/MarkupManagerTest.php new file mode 100644 index 000000000..f22c4c799 --- /dev/null +++ b/tests/unit/system/classes/MarkupManagerTest.php @@ -0,0 +1,111 @@ +getMethod($name); + $method->setAccessible(true); + return $method->invokeArgs($object, $params); + } + + public static function getProtectedProperty($object, $name) + { + $className = get_class($object); + $class = new ReflectionClass($className); + $property = $class->getProperty($name); + $property->setAccessible(true); + return $property->getValue($object); + } + + public static function setProtectedProperty($object, $name, $value) + { + $className = get_class($object); + $class = new ReflectionClass($className); + $property = $class->getProperty($name); + $property->setAccessible(true); + return $property->setValue($object, $value); + } + + // + // Tests + // + + public function testIsWildCallable() + { + $manager = MarkupManager::instance(); + + /* + * Negatives + */ + $callable = 'something'; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertFalse($result); + + $callable = ['Form', 'open']; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertFalse($result); + + $callable = function() { return 'O, Hai!'; }; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertFalse($result); + + /* + * String + */ + $callable = 'something_*'; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertTrue($result); + + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'delicious']); + $this->assertEquals('something_delicious', $result); + + /* + * Array + */ + $callable = ['Class', 'foo_*']; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertTrue($result); + + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'bar']); + $this->assertTrue(isset($result[0])); + $this->assertTrue(isset($result[1])); + $this->assertEquals('Class', $result[0]); + $this->assertEquals('foo_bar', $result[1]); + + $callable = ['My*', 'method']; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertTrue($result); + + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'Class']); + $this->assertTrue(isset($result[0])); + $this->assertTrue(isset($result[1])); + $this->assertEquals('MyClass', $result[0]); + $this->assertEquals('method', $result[1]); + + $callable = ['My*', 'my*']; + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]); + $this->assertTrue($result); + + $result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'Food']); + $this->assertTrue(isset($result[0])); + $this->assertTrue(isset($result[1])); + $this->assertEquals('MyFood', $result[0]); + $this->assertEquals('myFood', $result[1]); + } + +} \ No newline at end of file From f6fffad37d887f17c38edd01122475e69bc1cf62 Mon Sep 17 00:00:00 2001 From: Sam Georges Date: Wed, 16 Jul 2014 18:48:18 +1000 Subject: [PATCH 2/4] Convert DataGrid form widget in to Grid widget (no longer exclusive to forms) --- modules/backend/formwidgets/DataGrid.php | 166 +++---------- .../datagrid/partials/_datagrid.htm | 17 +- .../datagrid/partials/_toolbar.htm | 6 - modules/backend/widgets/Grid.php | 226 ++++++++++++++++++ modules/backend/widgets/Toolbar.php | 2 +- .../grid}/assets/css/datagrid.css | 0 .../grid}/assets/js/datagrid.js | 13 +- .../grid}/assets/less/datagrid.less | 0 .../vendor/handsontable/columnautosize.js | 0 .../assets/vendor/handsontable/columnmove.js | 0 .../vendor/handsontable/columnresize.js | 0 .../vendor/handsontable/columnsorting.js | 0 .../handsontable/jquery.handsontable.css | 0 .../handsontable/jquery.handsontable.js | 0 .../handsontable/jquery.handsontable.less | 0 .../vendor/handsontable/removebutton.js | 0 .../assets/vendor/handsontable/rowmove.js | 0 .../backend/widgets/grid/partials/_grid.htm | 23 ++ .../widgets/grid/partials/_toolbar.htm | 20 ++ 19 files changed, 316 insertions(+), 157 deletions(-) delete mode 100644 modules/backend/formwidgets/datagrid/partials/_toolbar.htm create mode 100644 modules/backend/widgets/Grid.php rename modules/backend/{formwidgets/datagrid => widgets/grid}/assets/css/datagrid.css (100%) rename modules/backend/{formwidgets/datagrid => widgets/grid}/assets/js/datagrid.js (96%) rename modules/backend/{formwidgets/datagrid => widgets/grid}/assets/less/datagrid.less (100%) rename modules/backend/{formwidgets/datagrid => widgets/grid}/assets/vendor/handsontable/columnautosize.js (100%) rename modules/backend/{formwidgets/datagrid => widgets/grid}/assets/vendor/handsontable/columnmove.js (100%) rename modules/backend/{formwidgets/datagrid => widgets/grid}/assets/vendor/handsontable/columnresize.js (100%) rename modules/backend/{formwidgets/datagrid => widgets/grid}/assets/vendor/handsontable/columnsorting.js (100%) rename modules/backend/{formwidgets/datagrid => widgets/grid}/assets/vendor/handsontable/jquery.handsontable.css (100%) rename modules/backend/{formwidgets/datagrid => widgets/grid}/assets/vendor/handsontable/jquery.handsontable.js (100%) rename modules/backend/{formwidgets/datagrid => widgets/grid}/assets/vendor/handsontable/jquery.handsontable.less (100%) rename modules/backend/{formwidgets/datagrid => widgets/grid}/assets/vendor/handsontable/removebutton.js (100%) rename modules/backend/{formwidgets/datagrid => widgets/grid}/assets/vendor/handsontable/rowmove.js (100%) create mode 100644 modules/backend/widgets/grid/partials/_grid.htm create mode 100644 modules/backend/widgets/grid/partials/_toolbar.htm diff --git a/modules/backend/formwidgets/DataGrid.php b/modules/backend/formwidgets/DataGrid.php index 249b7f0e3..1b405ca8c 100644 --- a/modules/backend/formwidgets/DataGrid.php +++ b/modules/backend/formwidgets/DataGrid.php @@ -1,5 +1,6 @@ columns = $this->getConfig('columns', []); $this->size = $this->getConfig('size', $this->size); + $this->grid = $this->makeGridWidget(); + $this->grid->bindToController(); } /** @@ -59,125 +52,10 @@ class DataGrid extends FormWidgetBase */ public function prepareVars() { + $this->vars['grid'] = $this->grid; $this->vars['name'] = $this->formField->getName(); - $this->vars['columnHeaders'] = $this->getColumnHeaders(); - $this->vars['columnDefinitions'] = $this->getColumnDefinitions(); - $this->vars['columnWidths'] = $this->getColumnWidths(); $this->vars['size'] = $this->size; - $this->vars['toolbarWidget'] = $this->makeToolbarWidget(); - $this->vars['value'] = json_encode($this->model->{$this->columnName}); - } - - protected function makeToolbarWidget() - { - $toolbarConfig = $this->makeConfig([ - 'alias' => $this->alias . 'Toolbar', - 'buttons' => $this->getViewPath('_toolbar.htm'), - ]); - - $toolbarWidget = $this->makeWidget('Backend\Widgets\Toolbar', $toolbarConfig); - return $toolbarWidget; - } - - // - // Getters - // - - protected function getColumnHeaders() - { - $headers = []; - foreach ($this->columns as $key => $column) { - $headers[] = isset($column['title']) ? $column['title'] : '???'; - } - return $headers; - } - - protected function getColumnWidths() - { - $widths = []; - foreach ($this->columns as $key => $column) { - $widths[] = isset($column['width']) ? $column['width'] : '0'; - } - return $widths; - } - - protected function getColumnDefinitions() - { - $definitions = []; - foreach ($this->columns as $key => $column) { - $item = []; - $item['data'] = $key; - - if (isset($column['readOnly'])) - $item['readOnly'] = $column['readOnly']; - - $item = $this->evalColumnType($column, $item); - $definitions[] = $item; - } - return $definitions; - } - - protected function evalColumnType($column, $item) - { - if (!isset($column['type'])) - return $item; - - switch ($column['type']) { - case 'number': - $item['type'] = 'numeric'; - break; - - case 'currency': - $item['type'] = 'numeric'; - $item['format'] = '$0,0.00'; - break; - - case 'checkbox': - $item['type'] = 'checkbox'; - break; - - case 'autocomplete': - $item['type'] = 'autocomplete'; - if (isset($column['options'])) $item['source'] = $column['options']; - if (isset($column['strict'])) $item['strict'] = $column['strict']; - break; - } - - return $item; - } - - // - // AJAX - // - - public function onAutocomplete() - { - if (!$this->model->methodExists('getGridAutocompleteValues')) - throw new ApplicationException('Model :model does not contain a method getGridAutocompleteValues()'); - - $field = post('autocomplete_field'); - $value = post('autocomplete_value'); - $data = post('autocomplete_data', []); - $result = $this->model->getGridAutocompleteValues($field, $value, $data); - if (!is_array($result)) - $result = []; - - return ['result' => $result]; - } - - // - // Internals - // - - /** - * {@inheritDoc} - */ - public function loadAssets() - { - $this->addCss('vendor/handsontable/jquery.handsontable.css', 'core'); - $this->addCss('css/datagrid.css', 'core'); - $this->addJs('vendor/handsontable/jquery.handsontable.js', 'core'); - $this->addJs('js/datagrid.js', 'core'); + $this->vars['value'] = json_encode($this->formField->value); } /** @@ -187,4 +65,28 @@ class DataGrid extends FormWidgetBase { return json_decode($value); } + + protected function makeGridWidget() + { + $config = $this->makeConfig((array) $this->config); + $config->dataLocker = '#'.$this->getId('dataLocker'); + + $grid = new Grid($this->controller, $config); + $grid->alias = $this->alias . 'Grid'; + $grid->bindEvent('grid.autocomplete', [$this, 'getAutocompleteValues']); + + return $grid; + } + + public function getAutocompleteValues($field, $value, $data) + { + if (!$this->model->methodExists('getGridAutocompleteValues')) + throw new ApplicationException('Model :model does not contain a method getGridAutocompleteValues()'); + + $result = $this->model->getGridAutocompleteValues($field, $value, $data); + if (!is_array($result)) + $result = []; + + return $result; + } } \ No newline at end of file diff --git a/modules/backend/formwidgets/datagrid/partials/_datagrid.htm b/modules/backend/formwidgets/datagrid/partials/_datagrid.htm index f8cb23918..7ec6fe954 100644 --- a/modules/backend/formwidgets/datagrid/partials/_datagrid.htm +++ b/modules/backend/formwidgets/datagrid/partials/_datagrid.htm @@ -2,16 +2,7 @@ id="getId() ?>" class="field-datagrid size-"> - render() ?> - -
+ render() ?> - \ No newline at end of file diff --git a/modules/backend/formwidgets/datagrid/partials/_toolbar.htm b/modules/backend/formwidgets/datagrid/partials/_toolbar.htm deleted file mode 100644 index ffa217aff..000000000 --- a/modules/backend/formwidgets/datagrid/partials/_toolbar.htm +++ /dev/null @@ -1,6 +0,0 @@ -
- Insert Row - Delete Row - - -
\ No newline at end of file diff --git a/modules/backend/widgets/Grid.php b/modules/backend/widgets/Grid.php new file mode 100644 index 000000000..8105149fd --- /dev/null +++ b/modules/backend/widgets/Grid.php @@ -0,0 +1,226 @@ +columns = $this->getConfig('columns', []); + $this->showHeader = $this->getConfig('showHeader', $this->showHeader); + $this->allowInsert = $this->getConfig('allowInsert', $this->allowInsert); + $this->allowRemove = $this->getConfig('allowRemove', $this->allowRemove); + $this->disableToolbar = $this->getConfig('disableToolbar', $this->disableToolbar); + $this->dataLocker = $this->getConfig('dataLocker', $this->dataLocker); + $this->dataSource = $this->getConfig('dataSource', $this->dataSource); + } + + /** + * Renders the widget. + */ + public function render() + { + $this->prepareVars(); + return $this->makePartial('grid'); + } + + /** + * Prepares the view data + */ + public function prepareVars() + { + $this->vars['columnHeaders'] = $this->getColumnHeaders(); + $this->vars['columnDefinitions'] = $this->getColumnDefinitions(); + $this->vars['columnWidths'] = $this->getColumnWidths(); + $this->vars['toolbarWidget'] = $this->makeToolbarWidget(); + + $this->vars['showHeader'] = $this->showHeader; + $this->vars['allowInsert'] = $this->allowInsert; + $this->vars['allowRemove'] = $this->allowRemove; + $this->vars['disableToolbar'] = $this->disableToolbar; + $this->vars['dataLocker'] = $this->dataLocker; + } + + protected function makeToolbarWidget() + { + if ($this->disableToolbar) + return; + + $toolbarConfig = $this->makeConfig([ + 'alias' => $this->alias . 'Toolbar', + 'buttons' => $this->getViewPath('_toolbar.htm'), + ]); + + $toolbarWidget = $this->makeWidget('Backend\Widgets\Toolbar', $toolbarConfig); + $toolbarWidget->vars['allowInsert'] = $this->allowInsert; + $toolbarWidget->vars['allowRemove'] = $this->allowRemove; + return $toolbarWidget; + } + + // + // AJAX + // + + public function onAutocomplete() + { + $field = post('autocomplete_field'); + $value = post('autocomplete_value'); + $data = post('autocomplete_data', []); + $result = $this->fireEvent('grid.autocomplete', [$field, $value, $data], true); + return ['result' => $result]; + } + + public function onDataSource() + { + if ($this->dataLocker) + return; + + $result = $this->dataSource; + return ['result' => $result]; + } + + // + // Getters + // + + protected function getColumnHeaders() + { + if (!$this->showHeader) + return false; + + $headers = []; + foreach ($this->columns as $key => $column) { + $headers[] = isset($column['title']) ? $column['title'] : '???'; + } + return $headers; + } + + protected function getColumnWidths() + { + $widths = []; + foreach ($this->columns as $key => $column) { + $widths[] = isset($column['width']) ? $column['width'] : '0'; + } + return $widths; + } + + protected function getColumnDefinitions() + { + $definitions = []; + foreach ($this->columns as $key => $column) { + $item = []; + $item['data'] = $key; + + if (isset($column['readOnly'])) + $item['readOnly'] = $column['readOnly']; + + $item = $this->evalColumnType($column, $item); + $definitions[] = $item; + } + return $definitions; + } + + protected function evalColumnType($column, $item) + { + if (!isset($column['type'])) + return $item; + + switch ($column['type']) { + case 'number': + $item['type'] = 'numeric'; + break; + + case 'currency': + $item['type'] = 'numeric'; + $item['format'] = '$0,0.00'; + break; + + case 'checkbox': + $item['type'] = 'checkbox'; + break; + + case 'autocomplete': + $item['type'] = 'autocomplete'; + if (isset($column['options'])) $item['source'] = $column['options']; + if (isset($column['strict'])) $item['strict'] = $column['strict']; + break; + } + + return $item; + } + + // + // Internals + // + + /** + * {@inheritDoc} + */ + public function loadAssets() + { + $this->addCss('vendor/handsontable/jquery.handsontable.css', 'core'); + $this->addCss('css/datagrid.css', 'core'); + $this->addJs('vendor/handsontable/jquery.handsontable.js', 'core'); + $this->addJs('js/datagrid.js', 'core'); + } + +} \ No newline at end of file diff --git a/modules/backend/widgets/Toolbar.php b/modules/backend/widgets/Toolbar.php index 57f75a6fe..e2abfb0b2 100644 --- a/modules/backend/widgets/Toolbar.php +++ b/modules/backend/widgets/Toolbar.php @@ -83,6 +83,6 @@ class Toolbar extends WidgetBase if (!isset($this->config->buttons)) return false; - return $this->controller->makePartial($this->config->buttons); + return $this->controller->makePartial($this->config->buttons, $this->vars); } } \ No newline at end of file diff --git a/modules/backend/formwidgets/datagrid/assets/css/datagrid.css b/modules/backend/widgets/grid/assets/css/datagrid.css similarity index 100% rename from modules/backend/formwidgets/datagrid/assets/css/datagrid.css rename to modules/backend/widgets/grid/assets/css/datagrid.css diff --git a/modules/backend/formwidgets/datagrid/assets/js/datagrid.js b/modules/backend/widgets/grid/assets/js/datagrid.js similarity index 96% rename from modules/backend/formwidgets/datagrid/assets/js/datagrid.js rename to modules/backend/widgets/grid/assets/js/datagrid.js index 83dfd2971..b625d5629 100644 --- a/modules/backend/formwidgets/datagrid/assets/js/datagrid.js +++ b/modules/backend/widgets/grid/assets/js/datagrid.js @@ -37,12 +37,12 @@ startRows: this.options.startRows, minRows: this.options.minRows, currentRowClassName: 'currentRow', - // rowHeaders: true, + // rowHeaders: false, // manualColumnMove: true, // manualRowMove: true, fillHandle: false, multiSelect: false, - removeRowPlugin: true + removeRowPlugin: this.options.allowRemove } if (this.options.autoInsertRows) @@ -68,6 +68,13 @@ delete handsontableOptions.data } } + else if (this.options.sourceHandler) { + $.request(self.options.sourceHandler, { + success: function(data, textStatus, jqXHR){ + self.gridInstance.loadData(data.result) + } + }) + } this.$el.handsontable(handsontableOptions) this.gridInstance = this.$el.handsontable('getInstance') @@ -138,6 +145,8 @@ columnWidths: null, columns: null, autocompleteHandler: null, + sourceHandler: null, + allowRemove: true, confirmMessage: 'Are you sure?' } diff --git a/modules/backend/formwidgets/datagrid/assets/less/datagrid.less b/modules/backend/widgets/grid/assets/less/datagrid.less similarity index 100% rename from modules/backend/formwidgets/datagrid/assets/less/datagrid.less rename to modules/backend/widgets/grid/assets/less/datagrid.less diff --git a/modules/backend/formwidgets/datagrid/assets/vendor/handsontable/columnautosize.js b/modules/backend/widgets/grid/assets/vendor/handsontable/columnautosize.js similarity index 100% rename from modules/backend/formwidgets/datagrid/assets/vendor/handsontable/columnautosize.js rename to modules/backend/widgets/grid/assets/vendor/handsontable/columnautosize.js diff --git a/modules/backend/formwidgets/datagrid/assets/vendor/handsontable/columnmove.js b/modules/backend/widgets/grid/assets/vendor/handsontable/columnmove.js similarity index 100% rename from modules/backend/formwidgets/datagrid/assets/vendor/handsontable/columnmove.js rename to modules/backend/widgets/grid/assets/vendor/handsontable/columnmove.js diff --git a/modules/backend/formwidgets/datagrid/assets/vendor/handsontable/columnresize.js b/modules/backend/widgets/grid/assets/vendor/handsontable/columnresize.js similarity index 100% rename from modules/backend/formwidgets/datagrid/assets/vendor/handsontable/columnresize.js rename to modules/backend/widgets/grid/assets/vendor/handsontable/columnresize.js diff --git a/modules/backend/formwidgets/datagrid/assets/vendor/handsontable/columnsorting.js b/modules/backend/widgets/grid/assets/vendor/handsontable/columnsorting.js similarity index 100% rename from modules/backend/formwidgets/datagrid/assets/vendor/handsontable/columnsorting.js rename to modules/backend/widgets/grid/assets/vendor/handsontable/columnsorting.js diff --git a/modules/backend/formwidgets/datagrid/assets/vendor/handsontable/jquery.handsontable.css b/modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.css similarity index 100% rename from modules/backend/formwidgets/datagrid/assets/vendor/handsontable/jquery.handsontable.css rename to modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.css diff --git a/modules/backend/formwidgets/datagrid/assets/vendor/handsontable/jquery.handsontable.js b/modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.js similarity index 100% rename from modules/backend/formwidgets/datagrid/assets/vendor/handsontable/jquery.handsontable.js rename to modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.js diff --git a/modules/backend/formwidgets/datagrid/assets/vendor/handsontable/jquery.handsontable.less b/modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.less similarity index 100% rename from modules/backend/formwidgets/datagrid/assets/vendor/handsontable/jquery.handsontable.less rename to modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.less diff --git a/modules/backend/formwidgets/datagrid/assets/vendor/handsontable/removebutton.js b/modules/backend/widgets/grid/assets/vendor/handsontable/removebutton.js similarity index 100% rename from modules/backend/formwidgets/datagrid/assets/vendor/handsontable/removebutton.js rename to modules/backend/widgets/grid/assets/vendor/handsontable/removebutton.js diff --git a/modules/backend/formwidgets/datagrid/assets/vendor/handsontable/rowmove.js b/modules/backend/widgets/grid/assets/vendor/handsontable/rowmove.js similarity index 100% rename from modules/backend/formwidgets/datagrid/assets/vendor/handsontable/rowmove.js rename to modules/backend/widgets/grid/assets/vendor/handsontable/rowmove.js diff --git a/modules/backend/widgets/grid/partials/_grid.htm b/modules/backend/widgets/grid/partials/_grid.htm new file mode 100644 index 000000000..149c01f65 --- /dev/null +++ b/modules/backend/widgets/grid/partials/_grid.htm @@ -0,0 +1,23 @@ +
+ + render() ?> + + +
data-data-locker="" + data-autocomplete-handler="getEventHandler('onAutocomplete') ?>" + data-source-handler="getEventHandler('onDataSource') ?>" + >
+ +
+ diff --git a/modules/backend/widgets/grid/partials/_toolbar.htm b/modules/backend/widgets/grid/partials/_toolbar.htm new file mode 100644 index 000000000..88a64a12e --- /dev/null +++ b/modules/backend/widgets/grid/partials/_toolbar.htm @@ -0,0 +1,20 @@ +
+ + + Insert Row + + + + + Delete Row + + + + +
\ No newline at end of file From 1a11f371d8b72c4d89d8257bd4d60637f577fb75 Mon Sep 17 00:00:00 2001 From: Sam Georges Date: Wed, 16 Jul 2014 18:59:56 +1000 Subject: [PATCH 3/4] Remove native scroll from handsontable --- .../handsontable/jquery.handsontable.js | 220 +----------------- 1 file changed, 10 insertions(+), 210 deletions(-) diff --git a/modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.js b/modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.js index 8b7ed63cb..64e2d154d 100644 --- a/modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.js +++ b/modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.js @@ -2797,9 +2797,7 @@ DefaultSettings.prototype = { allowInvalid: true, invalidCellClassName: 'htInvalid', fragmentSelection: false, - readOnly: false, - scrollbarModelV: 'dragdealer', - scrollbarModelH: 'dragdealer' + readOnly: false }; $.fn.handsontable = function (action) { @@ -3021,8 +3019,6 @@ Handsontable.TableView = function (instance) { data: instance.getDataAtCell, totalRows: instance.countRows, totalColumns: instance.countCols, - scrollbarModelV: this.settings.scrollbarModelV, - scrollbarModelH: this.settings.scrollbarModelH, offsetRow: 0, offsetColumn: 0, width: this.getWidth(), @@ -3317,9 +3313,6 @@ Handsontable.TableView.prototype.maximumVisibleElementWidth = function (left) { */ Handsontable.TableView.prototype.maximumVisibleElementHeight = function (top) { var rootHeight = this.wt.wtViewport.getWorkspaceHeight(); - if(this.wt.isNativeScroll) { - return rootHeight; - } return rootHeight - top; }; @@ -8004,25 +7997,6 @@ WalkontableScroll.prototype.scrollViewport = function (coords) { , fixedRowsTop = this.instance.getSetting('fixedRowsTop') , fixedColumnsLeft = this.instance.getSetting('fixedColumnsLeft'); - if (this.instance.isNativeScroll) { - var TD = this.instance.wtTable.getCell(coords); - if (typeof TD === 'object') { - var offset = WalkontableDom.prototype.offset(TD); - var outerHeight = WalkontableDom.prototype.outerHeight(TD); - var scrollY = window.scrollY; - var clientHeight = document.documentElement.clientHeight; - if (outerHeight < clientHeight) { - if (offset.top < scrollY) { - TD.scrollIntoView(true); - } - else if (offset.top + outerHeight > scrollY + clientHeight) { - TD.scrollIntoView(false); - } - } - return; - } - } - if (coords[0] < 0 || coords[0] > totalRows - 1) { throw new Error('row ' + coords[0] + ' does not exist'); } @@ -8437,129 +8411,10 @@ WalkontableScrollbarNative.prototype.destroy = function () { this.$scrollHandler.off('scroll.walkontable'); }; -/// -var WalkontableVerticalScrollbarNative = function (instance) { - this.instance = instance; - this.type = 'vertical'; - this.cellSize = 23; - this.init(); - - var that = this; - WalkontableCellStrategy.prototype.isLastIncomplete = function () { //monkey patch needed. In future get rid of it to improve performance - /* - * this.remainingSize = window viewport reduced by sum of all rendered cells (also those before the visible part) - * that.sumCellSizes(...) = sum of the sizes of cells that are before the visible part + 1 cell that is partially visible on top of the screen - */ - return this.remainingSize > that.sumCellSizes(that.offset, that.offset + that.curOuts + 1); - }; -}; - -WalkontableVerticalScrollbarNative.prototype = new WalkontableScrollbarNative(); - -WalkontableVerticalScrollbarNative.prototype.getLastCell = function () { - return this.instance.getSetting('offsetRow') + this.instance.wtTable.tbodyChildrenLength - 1; -}; - -WalkontableVerticalScrollbarNative.prototype.getTableSize = function () { - return this.instance.wtDom.outerHeight(this.TABLE); -}; - -var partialOffset = 0; - -WalkontableVerticalScrollbarNative.prototype.sumCellSizes = function (from, length) { - var sum = 0; - while (from < length) { - sum += this.instance.getSetting('rowHeight', from); - from++; - } - return sum; -}; - -WalkontableVerticalScrollbarNative.prototype.applyToDOM = function () { - var headerSize = this.instance.wtViewport.getColumnHeaderHeight(); - this.fixedContainer.style.height = headerSize + this.sumCellSizes(0, this.total) + 'px'; - this.fixed.style.top = this.measureBefore + 'px'; - this.fixed.style.bottom = ''; -}; - -WalkontableVerticalScrollbarNative.prototype.scrollTo = function (cell) { - var newY = this.tableParentOffset + cell * this.cellSize; - this.$scrollHandler.scrollTop(newY); - this.onScroll(newY); -}; - -WalkontableVerticalScrollbarNative.prototype.readSettings = function () { - var offset = this.instance.wtDom.offset(this.fixedContainer); - this.tableParentOffset = offset.top; - this.tableParentOtherOffset = offset.left; - this.windowSize = this.$scrollHandler.height(); - this.windowScrollPosition = this.$scrollHandler.scrollTop(); - this.offset = this.instance.getSetting('offsetRow'); - this.total = this.instance.getSetting('totalRows'); -}; - -/// - -var WalkontableHorizontalScrollbarNative = function (instance) { - this.instance = instance; - this.type = 'horizontal'; - this.cellSize = 50; - this.init(); -}; - -WalkontableHorizontalScrollbarNative.prototype = new WalkontableScrollbarNative(); - -WalkontableHorizontalScrollbarNative.prototype.getLastCell = function () { - return this.instance.wtTable.getLastVisibleColumn(); -}; - -WalkontableHorizontalScrollbarNative.prototype.getTableSize = function () { - return this.instance.wtDom.outerWidth(this.TABLE); -}; - -WalkontableHorizontalScrollbarNative.prototype.applyToDOM = function () { - this.fixedContainer.style.paddingLeft = this.measureBefore + 'px'; - this.fixedContainer.style.paddingRight = this.measureAfter + 'px'; -}; - -WalkontableHorizontalScrollbarNative.prototype.scrollTo = function (cell) { - this.$scrollHandler.scrollLeft(this.tableParentOffset + cell * this.cellSize); -}; - -WalkontableHorizontalScrollbarNative.prototype.readSettings = function () { - var offset = this.instance.wtDom.offset(this.fixedContainer); - this.tableParentOffset = offset.left; - this.tableParentOtherOffset = offset.top; - this.windowSize = this.$scrollHandler.width(); - this.windowScrollPosition = this.$scrollHandler.scrollLeft(); - this.offset = this.instance.getSetting('offsetColumn'); - this.total = this.instance.getSetting('totalColumns'); -}; function WalkontableScrollbars(instance) { - if(instance.getSetting('scrollbarModelV') === 'native') { - instance.update('scrollbarModelH', 'none'); - } - - switch (instance.getSetting('scrollbarModelV')) { - case 'dragdealer': - this.vertical = new WalkontableVerticalScrollbar(instance); - break; - - case 'native': - this.vertical = new WalkontableVerticalScrollbarNative(instance); - break; - } - - switch (instance.getSetting('scrollbarModelH')) { - case 'dragdealer': - this.horizontal = new WalkontableHorizontalScrollbar(instance); - break; - - case 'native': - this.horizontal = new WalkontableHorizontalScrollbarNative(instance); - break; - } + this.vertical = new WalkontableVerticalScrollbar(instance); + this.horizontal = new WalkontableHorizontalScrollbar(instance); } WalkontableScrollbars.prototype.destroy = function () { @@ -8677,8 +8532,6 @@ function WalkontableSettings(instance, settings) { //presentation mode scrollH: 'auto', //values: scroll (always show scrollbar), auto (show scrollbar if table does not fit in the container), none (never show scrollbar) scrollV: 'auto', //values: see above - scrollbarModelH: 'dragdealer', //values: dragdealer, native - scrollbarModelV: 'dragdealer', //values: dragdealer, native stretchH: 'hybrid', //values: hybrid, all, last, none currentRowClassName: null, currentColumnClassName: null, @@ -8882,10 +8735,6 @@ function WalkontableTable(instance) { this.columnFilter = new WalkontableColumnFilter(); this.verticalRenderReverse = false; - - if (this.instance.getSetting('scrollbarModelV') === 'native' || this.instance.getSetting('scrollbarModelH') === 'native') { - this.instance.isNativeScroll = true; - } } WalkontableTable.prototype.refreshHiderDimensions = function () { @@ -8894,7 +8743,7 @@ WalkontableTable.prototype.refreshHiderDimensions = function () { var spreaderStyle = this.spreader.style; - if ((height !== Infinity || width !== Infinity) && !this.instance.isNativeScroll) { + if (height !== Infinity || width !== Infinity) { if (height === Infinity) { height = this.instance.wtViewport.getWorkspaceActualHeight(); } @@ -8908,13 +8757,9 @@ WalkontableTable.prototype.refreshHiderDimensions = function () { spreaderStyle.top = '0'; spreaderStyle.left = '0'; - if (this.instance.getSetting('scrollbarModelV') === 'dragdealer') { - spreaderStyle.height = '4000px'; - } - - if (this.instance.getSetting('scrollbarModelH') === 'dragdealer') { - spreaderStyle.width = '4000px'; - } + // For dragdealer + spreaderStyle.height = '4000px'; + spreaderStyle.width = '4000px'; if (height < 0) { //this happens with WalkontableScrollbarNative and causes "Invalid argument" error in IE8 height = 0; @@ -8923,11 +8768,6 @@ WalkontableTable.prototype.refreshHiderDimensions = function () { this.hiderStyle.height = height + 'px'; this.hiderStyle.width = width + 'px'; } - else { - spreaderStyle.position = 'relative'; - spreaderStyle.width = 'auto'; - spreaderStyle.height = 'auto'; - } }; WalkontableTable.prototype.refreshStretching = function () { @@ -8960,9 +8800,6 @@ WalkontableTable.prototype.refreshStretching = function () { } var containerHeightFn = function (cacheHeight) { - if (that.instance.isNativeScroll) { - return 2 * that.instance.wtViewport.getViewportHeight(cacheHeight); - } return that.instance.wtViewport.getViewportHeight(cacheHeight); }; @@ -9075,10 +8912,6 @@ WalkontableTable.prototype.adjustColumns = function (TR, desiredCount) { }; WalkontableTable.prototype.draw = function (selectionsOnly) { - if (this.instance.isNativeScroll) { - this.verticalRenderReverse = false; //this is only supported in dragdealer mode, not in native - } - this.rowFilter.readSettings(this.instance); this.columnFilter.readSettings(this.instance); @@ -9180,7 +9013,6 @@ WalkontableTable.prototype._doDraw = function () { } if (first) { -// if (r === 0) { first = false; this.adjustAvailableNodes(); @@ -9248,9 +9080,7 @@ WalkontableTable.prototype._doDraw = function () { res = this.rowStrategy.add(r, TD, this.verticalRenderReverse); if (res === false) { - if (!this.instance.isNativeScroll) { - this.rowStrategy.removeOutstanding(); - } + this.rowStrategy.removeOutstanding(); } if (this.rowStrategy.isLastIncomplete()) { @@ -9368,10 +9198,6 @@ WalkontableTable.prototype.refreshSelections = function (selectionsOnly) { * */ WalkontableTable.prototype.getCell = function (coords) { - if (this.instance.isNativeScroll) { - return this.instance.wtTable.TBODY.querySelectorAll('[data-row="' + coords[0] + '"][data-column="' + coords[1] + '"]')[0]; - } - if (this.isRowBeforeViewport(coords[0])) { return -1; //row before viewport } @@ -9425,21 +9251,11 @@ WalkontableTable.prototype.isColumnAfterViewport = function (c) { }; WalkontableTable.prototype.isRowInViewport = function (r) { - if (this.instance.isNativeScroll) { - return !!this.instance.wtTable.TBODY.querySelectorAll('[data-row="' + r + '"]')[0]; - } - else { - return (!this.isRowBeforeViewport(r) && !this.isRowAfterViewport(r)); - } + return (!this.isRowBeforeViewport(r) && !this.isRowAfterViewport(r)); }; WalkontableTable.prototype.isColumnInViewport = function (c) { - if (this.instance.isNativeScroll) { - return !!this.instance.wtTable.TBODY.querySelectorAll('[data-column="' + c + '"]')[0]; - } - else { - return (!this.isColumnBeforeViewport(c) && !this.isColumnAfterViewport(c)); - } + return (!this.isColumnBeforeViewport(c) && !this.isColumnAfterViewport(c)); }; WalkontableTable.prototype.isLastRowFullyVisible = function () { @@ -9453,22 +9269,10 @@ WalkontableTable.prototype.isLastColumnFullyVisible = function () { function WalkontableViewport(instance) { this.instance = instance; this.resetSettings(); - - if (this.instance.isNativeScroll) { - var that = this; - that.clientHeight = document.documentElement.clientHeight; //browser viewport height - $(window).on('resize', function () { - that.clientHeight = document.documentElement.clientHeight; - }); - } } // Used by scrollbar WalkontableViewport.prototype.getWorkspaceHeight = function (proposedHeight) { - if (this.instance.isNativeScroll) { - return this.clientHeight; - } - var height = this.instance.getSetting('height'); if (height === Infinity || height === void 0 || height === null || height < 1) { @@ -9584,10 +9388,6 @@ WalkontableViewport.prototype.resetSettings = function () { this.columnHeaderHeight = NaN; }; function WalkontableWheel(instance) { - if (instance.isNativeScroll) { - return; - } - //spreader === instance.wtTable.TABLE.parentNode $(instance.wtTable.spreader).on('mousewheel', function (event, delta, deltaX, deltaY) { if (!deltaX && !deltaY && delta) { //we are in IE8, see https://github.com/brandonaaron/jquery-mousewheel/issues/53 From f4c13092dd4d70530a3d5963b52d09d94ce5c503 Mon Sep 17 00:00:00 2001 From: Sam Georges Date: Wed, 16 Jul 2014 19:36:16 +1000 Subject: [PATCH 4/4] Minor --- modules/backend/widgets/grid/assets/js/datagrid.js | 2 +- .../grid/assets/vendor/handsontable/jquery.handsontable.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/backend/widgets/grid/assets/js/datagrid.js b/modules/backend/widgets/grid/assets/js/datagrid.js index b625d5629..a1609cc3d 100644 --- a/modules/backend/widgets/grid/assets/js/datagrid.js +++ b/modules/backend/widgets/grid/assets/js/datagrid.js @@ -32,7 +32,7 @@ colWidths: function(columnIndex) { return self.staticWidths[columnIndex] }, - height: 400, + // height: 400, columns: this.columns, startRows: this.options.startRows, minRows: this.options.minRows, diff --git a/modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.js b/modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.js index 64e2d154d..42ba7132d 100644 --- a/modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.js +++ b/modules/backend/widgets/grid/assets/vendor/handsontable/jquery.handsontable.js @@ -14,6 +14,7 @@ * - Removed contextMenu plugin * - Use autocomplete plugin instead of typeahead * - Custom checkboxes + * - Removed native scrollbars * */