├── _config.php ├── lang └── sk.yml ├── .editorconfig ├── composer.json ├── LICENSE ├── javascript └── GridFieldCheckboxSelectComponent.js ├── code ├── GridFieldMultiDeleteButton.php ├── GridFieldCheckboxSelectComponent.php └── GridFieldApplyToMultipleRows.php ├── README.md └── .scrutinizer.yml /_config.php: -------------------------------------------------------------------------------- 1 | =3.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Mark Guinn 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to use, 6 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 7 | Software, and to permit persons to whom the Software is furnished to do so, subject 8 | to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies 11 | or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 15 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 16 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 17 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 18 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /javascript/GridFieldCheckboxSelectComponent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handles selection and check-all as well as confirmations. 3 | * 4 | * @author Mark Guinn 5 | * @date 12.22.2014 6 | * @package gridfieldmultiselect 7 | * @subpackage javascript 8 | */ 9 | (function($){ 10 | $.entwine('ss', function($) { 11 | 12 | $('.ss-gridfield .multiselect').entwine({ 13 | onclick: function (e) { 14 | e.stopPropagation(); 15 | } 16 | }); 17 | 18 | $('.ss-gridfield .multiselect-all').entwine({ 19 | onclick: function () { 20 | this.closest('table').find('.multiselect').prop('checked', this.prop('checked')); 21 | } 22 | }); 23 | 24 | $('.ss-gridfield .multiselect-button').entwine({ 25 | onclick: function(e) { 26 | if (this.closest('.ss-gridfield').find('.multiselect:checked').length == 0) { 27 | alert('Please check one or more rows.'); 28 | return false; 29 | } 30 | 31 | if (this.data('confirm') && !confirm(this.data('confirm'))) { 32 | e.preventDefault(); 33 | e.stopPropagation(); 34 | return false; 35 | } 36 | 37 | this._super(e); 38 | } 39 | }); 40 | 41 | }); 42 | })(jQuery); 43 | -------------------------------------------------------------------------------- /code/GridFieldMultiDeleteButton.php: -------------------------------------------------------------------------------- 1 | 7 | * @date 12.22.2014 8 | * @package gridfieldmultiselect 9 | */ 10 | class GridFieldMultiDeleteButton extends GridFieldApplyToMultipleRows 11 | { 12 | /** 13 | * Shortcut to create a button that deletes all selected entries 14 | */ 15 | public function __construct($targetFragment = 'after') 16 | { 17 | parent::__construct('deleteselected', _t('GridFieldMultiDeleteButton.ButtonText', 'Delete Selected'), array($this, 'deleteRecord'), $targetFragment, array( 18 | 'icon' => 'delete', 19 | 'class' => 'deleteSelected', 20 | 'confirm' => _t('GridFieldMultiDeleteButton.Confirm', 'Are you sure you want to delete all selected items?'), 21 | )); 22 | } 23 | 24 | 25 | /** 26 | * @param DataObject $record 27 | * @param int $index 28 | */ 29 | public function deleteRecord($record, $index) 30 | { 31 | if ($record->hasExtension('Versioned')) { 32 | $record->deleteFromStage('Stage'); 33 | $record->deleteFromStage('Live'); 34 | } else { 35 | $record->delete(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Multi-Select Checkboxes for SilverStripe's GridField 2 | ==================================================== 3 | 4 | GridField components that add checkboxes to each row in a grid and make 5 | it easy to perform actions on multiple rows. 6 | 7 | Requirements 8 | ------------ 9 | - Silverstripe 3.1+ 10 | 11 | 12 | Example: Delete 13 | --------------- 14 | ``` 15 | $gridFieldConfig->addComponents( 16 | new GridFieldCheckboxSelectComponent(), 17 | new GridFieldMultiDeleteButton() 18 | ); 19 | ``` 20 | 21 | Example: Custom Action 22 | ---------------------- 23 | ``` 24 | $gridFieldConfig->addComponents( 25 | new GridFieldCheckboxSelectComponent(), 26 | new GridFieldApplyToMultipleRows( 27 | 'emailClients', 28 | 'Email Selected Clients', 29 | function($record, $index){ 30 | $record->sendEmailToClient(); 31 | } 32 | ) 33 | ); 34 | ``` 35 | 36 | 37 | Developer(s) 38 | ------------ 39 | - Mark Guinn 40 | 41 | Contributions welcome by pull request and/or bug report. 42 | Please follow Silverstripe code standards (tests would be nice). 43 | 44 | 45 | License (MIT) 46 | ------------- 47 | Copyright (c) 2014 Mark Guinn 48 | 49 | Permission is hereby granted, free of charge, to any person obtaining a copy of 50 | this software and associated documentation files (the "Software"), to deal in 51 | the Software without restriction, including without limitation the rights to use, 52 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 53 | Software, and to permit persons to whom the Software is furnished to do so, subject 54 | to the following conditions: 55 | 56 | The above copyright notice and this permission notice shall be included in all copies 57 | or substantial portions of the Software. 58 | 59 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 60 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 61 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 62 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 63 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 64 | DEALINGS IN THE SOFTWARE. 65 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | inherit: true 2 | 3 | checks: 4 | php: 5 | verify_property_names: true 6 | verify_argument_usable_as_reference: true 7 | verify_access_scope_valid: true 8 | useless_calls: true 9 | use_statement_alias_conflict: true 10 | variable_existence: true 11 | unused_variables: true 12 | unused_properties: true 13 | unused_parameters: true 14 | unused_methods: true 15 | unreachable_code: true 16 | too_many_arguments: true 17 | sql_injection_vulnerabilities: true 18 | simplify_boolean_return: true 19 | side_effects_or_types: true 20 | security_vulnerabilities: true 21 | return_doc_comments: true 22 | return_doc_comment_if_not_inferrable: true 23 | require_scope_for_properties: true 24 | require_scope_for_methods: true 25 | require_php_tag_first: true 26 | psr2_switch_declaration: true 27 | psr2_class_declaration: true 28 | property_assignments: true 29 | prefer_while_loop_over_for_loop: true 30 | precedence_mistakes: true 31 | precedence_in_conditions: true 32 | phpunit_assertions: true 33 | php5_style_constructor: true 34 | parse_doc_comments: true 35 | parameter_non_unique: true 36 | parameter_doc_comments: true 37 | param_doc_comment_if_not_inferrable: true 38 | optional_parameters_at_the_end: true 39 | one_class_per_file: true 40 | no_unnecessary_if: true 41 | no_trailing_whitespace: true 42 | no_property_on_interface: true 43 | no_non_implemented_abstract_methods: true 44 | no_error_suppression: true 45 | no_duplicate_arguments: true 46 | no_commented_out_code: true 47 | newline_at_end_of_file: true 48 | missing_arguments: true 49 | method_calls_on_non_object: true 50 | instanceof_class_exists: true 51 | foreach_traversable: true 52 | fix_line_ending: true 53 | fix_doc_comments: true 54 | duplication: true 55 | deprecated_code_usage: true 56 | deadlock_detection_in_loops: true 57 | code_rating: true 58 | closure_use_not_conflicting: true 59 | catch_class_exists: true 60 | blank_line_after_namespace_declaration: false 61 | avoid_multiple_statements_on_same_line: true 62 | avoid_duplicate_types: true 63 | avoid_conflicting_incrementers: true 64 | avoid_closing_tag: true 65 | assignment_of_null_return: true 66 | argument_type_checks: true 67 | 68 | filter: 69 | paths: [code/*, tests/*] 70 | -------------------------------------------------------------------------------- /code/GridFieldCheckboxSelectComponent.php: -------------------------------------------------------------------------------- 1 | 8 | * @date 12.22.2014 9 | * @package apluswhs.com 10 | * @subpackage 11 | */ 12 | class GridFieldCheckboxSelectComponent implements GridField_ColumnProvider 13 | { 14 | const CHECKBOX_COLUMN = 'SelectCheckbox'; 15 | 16 | /** 17 | * Config flag to determine whether checkbox column should be prepended 18 | * or appended to the table. 19 | * 20 | * @var bool 21 | */ 22 | private static $prepend_column = false; 23 | 24 | /** 25 | * Adds some javascript 26 | */ 27 | public function __construct() 28 | { 29 | Requirements::javascript('gridfieldmultiselect/javascript/GridFieldCheckboxSelectComponent.js'); 30 | } 31 | 32 | 33 | /** 34 | * Modify the list of columns displayed in the table. 35 | * 36 | * @see {@link GridFieldDataColumns->getDisplayFields()} 37 | * @see {@link GridFieldDataColumns}. 38 | * 39 | * @param GridField $gridField 40 | * @param array - List reference of all column names. 41 | */ 42 | public function augmentColumns($gridField, &$columns) 43 | { 44 | if (Config::inst()->get(__CLASS__, 'prepend_column')) { 45 | array_unshift($columns, self::CHECKBOX_COLUMN); 46 | } else { 47 | $columns[] = self::CHECKBOX_COLUMN; 48 | } 49 | } 50 | 51 | 52 | /** 53 | * Names of all columns which are affected by this component. 54 | * 55 | * @param GridField $gridField 56 | * @return array 57 | */ 58 | public function getColumnsHandled($gridField) 59 | { 60 | return array(self::CHECKBOX_COLUMN); 61 | } 62 | 63 | 64 | /** 65 | * HTML for the column, content of the element. 66 | * 67 | * @param GridField $gridField 68 | * @param DataObject $record - Record displayed in this row 69 | * @param string $columnName 70 | * @return string - HTML for the column. Return NULL to skip. 71 | */ 72 | public function getColumnContent($gridField, $record, $columnName) 73 | { 74 | if ($columnName === self::CHECKBOX_COLUMN) { 75 | return ''; 78 | } else { 79 | return null; 80 | } 81 | } 82 | 83 | 84 | /** 85 | * Additional metadata about the column which can be used by other components, 86 | * e.g. to set a title for a search column header. 87 | * 88 | * @param GridField $gridField 89 | * @param string $column 90 | * @return array - Map of arbitrary metadata identifiers to their values. 91 | */ 92 | public function getColumnMetadata($gridField, $column) 93 | { 94 | if ($column === self::CHECKBOX_COLUMN) { 95 | $title = _t('GridFieldMultiSelect.SelectAllVisibleRows', 'Select all visible rows'); 96 | return array( 97 | 'title' => '', 99 | ); 100 | } 101 | } 102 | 103 | 104 | /** 105 | * Attributes for the element containing the content returned by {@link getColumnContent()}. 106 | * 107 | * @param GridField $gridField 108 | * @param DataObject $record displayed in this row 109 | * @param string $columnName 110 | * @return array 111 | */ 112 | public function getColumnAttributes($gridField, $record, $columnName) 113 | { 114 | return array('class' => 'col-checkbox'); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /code/GridFieldApplyToMultipleRows.php: -------------------------------------------------------------------------------- 1 | 6 | * @date 12.22.2014 7 | * @package gridfieldmultiselect 8 | */ 9 | class GridFieldApplyToMultipleRows implements GridField_HTMLProvider, GridField_ActionProvider, GridField_URLHandler 10 | { 11 | /** @var callable - this will be called for every row */ 12 | protected $rowHandler; 13 | 14 | /** @var string */ 15 | protected $targetFragment; 16 | 17 | /** @var string */ 18 | protected $buttonText; 19 | 20 | /** @var string */ 21 | protected $actionName; 22 | 23 | /** @var array */ 24 | protected $buttonConfig; 25 | 26 | 27 | /** 28 | * @param string $actionName 29 | * @param string $buttonText 30 | * @param callable $rowHandler 31 | * @param string $targetFragment 32 | * @param array $buttonConfig - icon, class, possibly others 33 | */ 34 | public function __construct($actionName, $buttonText, $rowHandler, $targetFragment = 'after', $buttonConfig = array()) 35 | { 36 | $this->actionName = $actionName; 37 | $this->buttonText = $buttonText; 38 | $this->rowHandler = $rowHandler; 39 | $this->targetFragment = $targetFragment; 40 | $this->buttonConfig = $buttonConfig; 41 | } 42 | 43 | 44 | /** 45 | * Returns a map where the keys are fragment names and the values are 46 | * pieces of HTML to add to these fragments. 47 | * 48 | * Here are 4 built-in fragments: 'header', 'footer', 'before', and 49 | * 'after', but components may also specify fragments of their own. 50 | * 51 | * To specify a new fragment, specify a new fragment by including the 52 | * text "$DefineFragment(fragmentname)" in the HTML that you return. 53 | * 54 | * Fragment names should only contain alphanumerics, -, and _. 55 | * 56 | * If you attempt to return HTML for a fragment that doesn't exist, an 57 | * exception will be thrown when the {@link GridField} is rendered. 58 | * 59 | * @param GridField $gridField 60 | * @return array 61 | */ 62 | public function getHTMLFragments($gridField) 63 | { 64 | $button = new GridField_FormAction($gridField, $this->actionName, $this->buttonText, $this->actionName, null); 65 | $button->addExtraClass('multiselect-button'); 66 | 67 | if (!empty($this->buttonConfig['icon'])) { 68 | $button->setAttribute('data-icon', $this->buttonConfig['icon']); 69 | } 70 | 71 | if (!empty($this->buttonConfig['class'])) { 72 | $button->addExtraClass($this->buttonConfig['class']); 73 | } 74 | 75 | if (!empty($this->buttonConfig['confirm'])) { 76 | $button->setAttribute('data-confirm', $this->buttonConfig['confirm']); 77 | } 78 | 79 | return array( 80 | $this->targetFragment => $button->Field(), 81 | ); 82 | } 83 | 84 | 85 | /** 86 | * Return a list of the actions handled by this action provider. 87 | * 88 | * Used to identify the action later on through the $actionName parameter 89 | * in {@link handleAction}. 90 | * 91 | * There is no namespacing on these actions, so you need to ensure that 92 | * they don't conflict with other components. 93 | * 94 | * @param GridField 95 | * @return Array with action identifier strings. 96 | */ 97 | public function getActions($gridField) 98 | { 99 | return array($this->actionName); 100 | } 101 | 102 | 103 | /** 104 | * Handle an action on the given {@link GridField}. 105 | * 106 | * Calls ALL components for every action handled, so the component needs 107 | * to ensure it only accepts actions it is actually supposed to handle. 108 | * 109 | * @param GridField 110 | * @param String Action identifier, see {@link getActions()}. 111 | * @param Array Arguments relevant for this 112 | * @param Array All form data 113 | * @return array 114 | */ 115 | public function handleAction(GridField $gridField, $actionName, $arguments, $data) 116 | { 117 | if ($actionName === $this->actionName) { 118 | return $this->handleIt($gridField, $data); 119 | } 120 | } 121 | 122 | 123 | /** 124 | * Return URLs to be handled by this grid field, in an array the same form 125 | * as $url_handlers. 126 | * 127 | * Handler methods will be called on the component, rather than the 128 | * {@link GridField}. 129 | */ 130 | public function getURLHandlers($gridField) 131 | { 132 | return array( 133 | $this->actionName => 'handleIt' 134 | ); 135 | } 136 | 137 | 138 | /** 139 | * @param GridField $gridField 140 | * @param array|SS_HTTPRequest $data 141 | * @return array 142 | */ 143 | public function handleIt($gridField, $data = array()) 144 | { 145 | if ($data instanceof SS_HTTPRequest) { 146 | $data = $data->requestVars(); 147 | } 148 | 149 | // Separate out the ID list from the checkboxes 150 | $fieldName = GridFieldCheckboxSelectComponent::CHECKBOX_COLUMN; 151 | $ids = isset($data[$fieldName]) && is_array($data[$fieldName]) ? $data[$fieldName] : array(); 152 | $class = $gridField->getModelClass(); 153 | if (!$class) { 154 | user_error('No model class is defined!'); 155 | } 156 | 157 | $response = array(); 158 | 159 | // Hook for subclasses 160 | $this->onBeforeList($gridField, $data, $ids); 161 | 162 | if (empty($ids)) { 163 | $this->onEmptyList($gridField, $response, $data, $ids); 164 | $records = new ArrayList(); 165 | } else { 166 | $records = DataObject::get($class)->filter('ID', $ids); 167 | foreach ($records as $index => $record) { 168 | call_user_func($this->rowHandler, $record, $index); 169 | } 170 | } 171 | 172 | $this->onAfterList($gridField, $response, $records, $data, $ids); 173 | return $response; 174 | } 175 | 176 | 177 | /** 178 | * Hook for subclasses 179 | * @param GridField $gridField 180 | * @param array $data 181 | * @param array $idList 182 | */ 183 | protected function onBeforeList($gridField, $data, $idList) 184 | { 185 | } 186 | 187 | 188 | /** 189 | * This allows subclasses to have a hook at the end of running through 190 | * all the items. Response will usually be an array on the way in 191 | * but it can be changed to whatever and will be returned as is. 192 | * @param GridField $gridField 193 | * @param array|SS_HTTPResponse $response 194 | * @param SS_List $records 195 | * @param array $data 196 | * @param array $idList 197 | */ 198 | protected function onAfterList($gridField, &$response, $records, $data, $idList) 199 | { 200 | } 201 | 202 | 203 | /** 204 | * @param GridField $gridField 205 | * @param array|SS_HTTPResponse $response 206 | * @param array $data 207 | * @param array $idList 208 | */ 209 | protected function onEmptyList($gridField, &$response, $data, $idList) 210 | { 211 | } 212 | } 213 | --------------------------------------------------------------------------------