├── images ├── ContentBlock.png ├── ContentBlockTwo.png ├── blockedit.svg └── BlockIcons.svg ├── _config └── config.yml ├── templates └── GridField_EditableBlockRow.ss ├── composer.json ├── code ├── extensions │ ├── BlockEnhancements_SiteTreeExt.php │ └── UploadFieldExtension.php ├── controllers │ └── LeftAndMain_BlockActions.php └── forms │ ├── GF_BlockEnhancements.php │ ├── GridfieldConfig_BlockManager_bu.txt │ ├── GridfieldConfig_BlockManager_buv2.txt │ ├── GFConf_BlockManagerEnhanced.php │ └── EditableBlockRow.php ├── LICENSE.txt ├── README.md ├── css ├── BlockEnhancements.css └── EditableBlockRow.css ├── _config.php └── js ├── BlockEnhancements.js ├── display_logic_editablerow-fixes.js └── EditableBlockRow.js /images/ContentBlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restruct/silverstripe-block_enhancements/HEAD/images/ContentBlock.png -------------------------------------------------------------------------------- /images/ContentBlockTwo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restruct/silverstripe-block_enhancements/HEAD/images/ContentBlockTwo.png -------------------------------------------------------------------------------- /_config/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: 'blockenhancements' 3 | After: 4 | - 'blocks' 5 | - 'gridfieldextensions' 6 | --- 7 | GridFieldAddNewMultiClass: 8 | showEmptyString: false 9 | 10 | Injector: 11 | GridFieldConfig_BlockManager: 12 | class: GFConf_BlockManagerEnhanced 13 | 14 | UploadField: 15 | extensions: 16 | - 'Milkyway\SS\Core\Extensions\UploadFieldExtension' 17 | 18 | # add 'publish with blocks' action to page 19 | Page: 20 | extensions: 21 | - BlockEnhancements_SiteTreeExt 22 | LeftAndMain: 23 | extensions: 24 | - LeftAndMain_BlockActions -------------------------------------------------------------------------------- /templates/GridField_EditableBlockRow.ss: -------------------------------------------------------------------------------- 1 | 2 | <% if $PrevColumnsCount %> 3 | 4 | 5 | <% end_if %> 6 | 7 | 8 |
9 | <% loop $Form.Fields %> 10 | $FieldHolder 11 | <% end_loop %> 12 |
13 | 14 | -------------------------------------------------------------------------------- /images/blockedit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micschk/silverstripe-block_enhancements", 3 | "description": "Enhancement of the silverstripe-blocks module", 4 | "type": "silverstripe-module", 5 | "keywords": [ 6 | "silverstripe", 7 | "blocks", 8 | "widgets" 9 | ], 10 | "homepage": "http://github.com/micschk/silverstripe-block_enhancements", 11 | "authors": [ 12 | { 13 | "name": "Restruct web & apps", 14 | "email": "dev@restruct.nl" 15 | } 16 | ], 17 | "license": "MIT", 18 | "require": { 19 | "sheadawson/silverstripe-blocks": "~1.0", 20 | "select2/select2": "~4.0", 21 | "silverstripe-australia/gridfieldextensions": "~1.0", 22 | "micschk/silverstripe-groupable-gridfield": "~0.1" 23 | }, 24 | "extra": { 25 | "installer-name": "block_enhancements" 26 | } 27 | } -------------------------------------------------------------------------------- /code/extensions/BlockEnhancements_SiteTreeExt.php: -------------------------------------------------------------------------------- 1 | owner->Blocks()->count()) return; 8 | 9 | $actions->fieldByName('MajorActions')->push( 10 | $publish = FormAction::create('publishPageAndBlocks', 'Published (+Blocks)') 11 | ->setAttribute('data-icon', 'accept') 12 | ->setAttribute('data-icon-alternate', 'disk') 13 | ->setAttribute('data-text-alternate', 'Save & publish (+Blocks)') 14 | ); 15 | 16 | // Set up the initial state of the button to reflect the state of the blocks 17 | foreach ($this->owner->Blocks() as $block) { 18 | if ($block->stagesDiffer('Stage', 'Live')) { 19 | $publish->addExtraClass('ss-ui-alternate'); 20 | break; 21 | } 22 | } 23 | 24 | } 25 | 26 | 27 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Michael van Schaik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SilverStripe Blocks Enhancements (WIP) 2 | 3 | *Work in progress!* 4 | 5 | This module adds enhancements to the Silverstripe Blocks module, namely: 6 | * Visual display/preview of block layouts 7 | * Assign blocks to available block-areas by drag & drop 8 | * Inline editing of blocks, blocktype & content 9 | * Adds a 'Publish + blocks' button to publish all blocks along with the page in one go 10 | * Some other small Ux improvements 11 | 12 | ![image](https://cloud.githubusercontent.com/assets/1005986/13769387/69ccd7d2-ea7f-11e5-833e-24b1102f0bc3.png) 13 | Drag to assign block area, block-preview images, inline-editing 14 | 15 | ## Usage: 16 | * Install as usual (remember to run a 'composer update' as well) 17 | * There's a 'images/BlockIcons.svg' file containing examples of block layout previews. 18 | * Create a BlockClassName.png image for each BlockClassName in mysite/block_images/ 19 | 20 | ## Installation 21 | 22 | #### Composer 23 | 24 | composer require micschk/silverstripe-block_enhancements 25 | 26 | Install via composer, *+ run 'composer update'* (seems needed for the select2 module to create web-accessible 'components' dir). 27 | 28 | Then run dev/build and see the README of sheadawson/silverstripe-blocks for further instructions 29 | 30 | ### Requirements (all pulled in by composer) 31 | 32 | * SilverStripe CMS ~3.1 33 | * Silverstripe Blocks module + requirements 34 | * (This module contains a slightly modified copie of EditableRow by Milkyway Multimedia, thanks) 35 | -------------------------------------------------------------------------------- /css/BlockEnhancements.css: -------------------------------------------------------------------------------- 1 | /* blocktype/classname select2 dropdowns */ 2 | .select2-selection__rendered { 3 | padding: 4px 4px 4px 0; 4 | } 5 | .select2-container .selection .select2-selection { 6 | padding-left: 1px !important; 7 | } 8 | .block-type .select2, 9 | .select2-container .selection .select2-selection.select2-selection--single { 10 | width: auto; 11 | height: auto; 12 | } 13 | .select2-container .select2-selection--single .select2-selection__rendered { 14 | padding-left: 4px; 15 | } 16 | .select2-selection__rendered img, 17 | .select2-results img { 18 | vertical-align: middle; 19 | margin-right: 6px; 20 | } 21 | select + .select2-container--default .select2-selection--single .select2-selection__arrow { 22 | top: 50%; 23 | margin-top: -13px; 24 | } 25 | .ss-gridfield-blockenhancements .ss-gridfield-add-new-multi-class .select2-selection { 26 | min-height: 32px; 27 | min-width: 320px; 28 | } 29 | .ss-gridfield-blockenhancements .ss-gridfield-add-new-multi-class { 30 | margin-top: 8px; 31 | } 32 | #pages-controller-cms-content .ss-gridfield-blockenhancements .ss-gridfield-add-new-multi-class { 33 | //margin-left: 32px; 34 | margin-left: 8px; 35 | } 36 | .ss-gridfield-blockenhancements .ss-gridfield-add-new-multi-class .select2-selection { min-height: 60px; } 37 | .ss-gridfield-blockenhancements .ss-gridfield-add-new-multi-class a.ui-button { margin-top: 16px; } 38 | .cms .ss-gridfield.ss-gridfield-blockenhancements > div.ss-gridfield-buttonrow-after .action.add-existing-search { 39 | margin-top: 24px; 40 | } -------------------------------------------------------------------------------- /css/EditableBlockRow.css: -------------------------------------------------------------------------------- 1 | /*ss-gridfield-editable-row--toggle_loaded ss-gridfield-editable-row--toggle_open*/ 2 | 3 | .cms table.ss-gridfield-table tbody td.ss-gridfield-editable-row--icon-holder { 4 | width: 24px; 5 | } 6 | /* Image by http://uxrepo.com/icon/edit-by-elusive */ 7 | .ss-gridfield-editable-row--toggle { 8 | cursor: pointer; 9 | display: block; 10 | width: 24px; 11 | height: 24px; 12 | /*background-image: url('../images/blockedit.svg');*/ 13 | /*background-size: 30px 30px;*/ 14 | background-color: #b3b3b3; 15 | -webkit-mask-image: url('../images/blockedit.svg'); 16 | -webkit-mask-size: 100% 100%; 17 | mask-image: url('../images/blockedit.svg'); 18 | mask-size: 100% 100%; 19 | } 20 | .ss-gridfield-editable-row--toggle:hover { background-color: black; } 21 | .ss-gridfield-editable-row--toggle.ss-gridfield-editable-row--toggle_open { background-color: gray; } 22 | 23 | .ss-gridfield-editable-row--row_hide { display: none; } 24 | 25 | .ss-gridfield-editable-row--fields .middleColumn { 26 | max-width: 600px; 27 | } 28 | .field.ss-gridfield .ss-gridfield-editable-row--fields .description, 29 | .ss-gridfield-editable-row--fields .htmleditor .middleColumn, 30 | .ss-gridfield-editable-row--fields .ss-uploadfield .middleColumn { 31 | /* margin-left: 0px; */ 32 | /* clear: left; */ 33 | margin-left: 184px; 34 | clear: none; 35 | } 36 | .cms .ss-gridfield-editable-rows table.ss-gridfield-table tbody tr { 37 | cursor: auto !important; 38 | } 39 | .cms table.ss-gridfield-table .ss-gridfield-editable-row--fields .mceLayout tr td { padding: 0; } 40 | .cms table.ss-gridfield-table .ss-gridfield-editable-row--fields .mceLayout tbody { background: transparent; } 41 | 42 | .ss-gridfield-editable-row--reference, 43 | .ss-gridfield-editable-row--reference:hover, 44 | .ss-gridfield-editable-row--row, 45 | .ss-gridfield-editable-row--row:hover 46 | { background-color: #f6f7f8 !important; } 47 | /*.ss-gridfield-editable-row--reference td { border-top: 1px solid rgba(0, 0, 0, 0.1); } 48 | .ss-gridfield-editable-row--row td { border-bottom: 1px solid rgba(0, 0, 0, 0.1); }*/ 49 | .ss-gridfield-blockenhancements .ss-gridfield-item td, 50 | .ss-gridfield-blockenhancements .ss-gridfield-editable-row--row td 51 | { border-bottom: 1px solid rgba(0, 0, 0, 0.1); } -------------------------------------------------------------------------------- /code/extensions/UploadFieldExtension.php: -------------------------------------------------------------------------------- 1 | 8 | * @credit micschk 9 | */ 10 | 11 | use SS_HTTPRequest as SS_HTTPRequest; 12 | use Extension as Extension; 13 | use Folder as Folder; 14 | 15 | class UploadFieldExtension extends Extension { 16 | private static $allowed_actions = [ 17 | 'index', 18 | ]; 19 | 20 | public function beforeCallActionHandler($request, &$action) { 21 | if($this->owner->hasClass('ss-upload-to-folder')) 22 | $this->setFolderFromRequest($request); 23 | 24 | if($action == 'upload') 25 | $action = 'fixedUpload'; 26 | } 27 | 28 | public function index($request) { 29 | return $this->owner->FieldHolder(); 30 | } 31 | 32 | protected function setFolderFromRequest($request) { 33 | if(($folder = $this->getFolderFromRequest($request)) && $folder->canView()) { 34 | $path = strpos($folder->RelativePath, ASSETS_DIR . '/') === 0 ? substr($folder->RelativePath, strlen(ASSETS_DIR . '/')) : $folder->RelativePath; 35 | $this->owner->FolderName = $path; 36 | } 37 | } 38 | 39 | protected function getFolderFromRequest($request) { 40 | $folderId = $request->getVar('folder'); 41 | return $folderId ? Folder::get()->byID($folderId) : null; 42 | } 43 | 44 | public function fixedUpload(SS_HTTPRequest $request) { 45 | // Use a new request that fixes the postVars to use the $_FILES passed in correct order 46 | if(strpos(trim($this->owner->Name, '[]'), '[') !== false) { 47 | $request = $this->fixRequestForArrayFields($request); 48 | } 49 | 50 | return $this->owner->upload($request); 51 | } 52 | 53 | protected function fixRequestForArrayFields(SS_HTTPRequest $request) { 54 | $postVars = $request->postVars(); 55 | $fileVars = array_intersect_key($postVars, $_FILES); 56 | $fieldName = $this->owner->Name; 57 | 58 | foreach($fileVars as $name => $attributes) { 59 | $nameParts = explode('][', trim(substr($fieldName, strlen($name) + 1), ']')); 60 | $newAttributes = []; 61 | $newValue = []; 62 | $values = null; 63 | 64 | foreach($attributes as $attributeName => $attributeValues) { 65 | $values = array_get($attributeValues, implode('.', $nameParts)); 66 | 67 | if(!$values || (is_array($values) && empty($values))) 68 | break; 69 | 70 | array_set($newAttributes, implode('.', $nameParts).'.'.$attributeName, $values); 71 | $newValue[$attributeName] = $values; 72 | } 73 | 74 | if(!$values) 75 | continue; 76 | 77 | $postVars[$name] = $newAttributes; 78 | $postVars[$fieldName] = $newValue; 79 | } 80 | 81 | return new SS_HTTPRequest( 82 | $request->httpMethod(), 83 | $request->getURL(true), 84 | $request->getVars(), 85 | $postVars, 86 | $request->getBody() 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | remove('Block', 'extensions', Config::anything(), "Versioned('Stage', 'Live')"); 10 | // OR something like: Block::remove_extension('Versioned'); 11 | 12 | // OR: Improve the isPublished status representation in your own Base Block class; 13 | 14 | //public function isPublishedNice() 15 | //{ 16 | // if ($this->isPublished() && $this->stagesDiffer('Stage', 'Live')) { return '✔ (edited)'; } 17 | // if ($this->isPublished()) { return '✔'; } 18 | // return "✘"; 19 | //} 20 | 21 | // Include these here already so the below js requirements dont choke 22 | Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); 23 | Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); 24 | // Block some crappy tagfield requirements 25 | Requirements::block('tagfield/css/select2.min.css'); 26 | Requirements::block('tagfield/css/TagField.css'); 27 | Requirements::block('tagfield/js/select2.js'); 28 | //Requirements::javascript(TAG_FIELD_DIR . '/js/TagField.js'); 29 | 30 | // include select2 from CDN 31 | //Requirements::javascript('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.1/js/select2.min.js'); 32 | //Requirements::css('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.1/css/select2.min.css'); 33 | Requirements::javascript('components/select2/dist/js/select2.js'); 34 | Requirements::css('components/select2/dist/css/select2.css'); 35 | 36 | // Parts of this module have been isolated from Milkyway ss-mwm and ss-gridfield-utils 37 | // This was done in order to use just the EditableRows without extending/replacing half the framework 38 | 39 | 40 | // These functions are required by some mwm code: 41 | if (!function_exists('array_set')) { 42 | /** 43 | * Set an array item to a given value using "dot" notation. 44 | * 45 | * If no key is given to the method, the entire array will be replaced. 46 | * 47 | * @param array $array 48 | * @param string $key 49 | * @param mixed $value 50 | * @return array 51 | */ 52 | function array_set(&$array, $key, $value) 53 | { 54 | if (is_null($key)) { 55 | return $array = $value; 56 | } 57 | $keys = explode('.', $key); 58 | while (count($keys) > 1) { 59 | $key = array_shift($keys); 60 | // If the key doesn't exist at this depth, we will just create an empty array 61 | // to hold the next value, allowing us to create the arrays to hold final 62 | // values at the correct depth. Then we'll keep digging into the array. 63 | if (!isset($array[$key]) || !is_array($array[$key])) { 64 | $array[$key] = []; 65 | } 66 | $array =& $array[$key]; 67 | } 68 | $array[array_shift($keys)] = $value; 69 | return $array; 70 | } 71 | } 72 | if (!function_exists('array_get')) { 73 | /** 74 | * Get an item from an array using "dot" notation. 75 | * 76 | * @param array $array 77 | * @param string $key 78 | * @param mixed $default 79 | * @return mixed 80 | */ 81 | function array_get($array, $key, $default = null) 82 | { 83 | if (is_null($key)) { 84 | return $array; 85 | } 86 | if (isset($array[$key])) { 87 | return $array[$key]; 88 | } 89 | foreach (explode('.', $key) as $segment) { 90 | if (!is_array($array) || !array_key_exists($segment, $array)) { 91 | return value($default); 92 | } 93 | $array = $array[$segment]; 94 | } 95 | return $array; 96 | } 97 | } -------------------------------------------------------------------------------- /js/BlockEnhancements.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | $.entwine("ss", function($) { 4 | 5 | // Haven't been able to 'arrive' at the select before the Leftandmain script does 6 | // and applies chozen, despite having way more specific selectors... 7 | // $('#Form_EditForm_Blocks .ss-gridfield-add-new-multi-class select').addClass('no-chzn'); 8 | // Just remove it instead... 9 | $('#Form_EditForm_Blocks #GridFieldAddNewMultiClass_ClassName_chzn, ' 10 | + 'select.block-type + .select2 + .chzn-container').entwine({ 11 | onmatch: function() { 12 | //console.log('match'); 13 | this.remove(); 14 | } 15 | }); 16 | 17 | // initialize select2 on blocktype dropdowns 18 | //+'.cms .field.dropdown select#Form_ItemEditForm_ClassName.block-type' 19 | $('select.select2blocktype, .block-type select, ' 20 | +'.cms .ss-gridfield-blockenhancements .field.dropdown select#GridFieldAddNewMultiClass_ClassName' 21 | ).entwine({ 22 | 23 | //onmatch: function(){ // attempt to override chzn, doesnt work consistently hence above remove() hack 24 | // //this.addClass('no-chzn'); 25 | //}, // override LeftAndMain.js line ~1266 (Chosen) 26 | 27 | onadd: function(){ 28 | // console.log($.cookie('js-project-dir')); 29 | var projectdir = this.data('project-dir'); 30 | if(!projectdir) projectdir = $.cookie('js-project-dir'); // try & get from cookie 31 | if(!projectdir) projectdir = 'mysite'; // guess default... 32 | 33 | var optionHtmlTemplate = function(data, container) { 34 | if (!data.id) { return data.text; } 35 | var $state = $( // $ThemeDir 36 | ' ' + data.text + '' 38 | ); 39 | return $state; 40 | }; 41 | // apply select2 (set inline width if in gridfield to get consistens results) 42 | if(this.parents('.ss-gridfield').length){ 43 | this.css('width','100%'); 44 | } 45 | this.select2({ 46 | templateSelection: optionHtmlTemplate, 47 | templateResult: optionHtmlTemplate, 48 | minimumResultsForSearch: 'Infinity' 49 | }); 50 | //this.select2(); 51 | } 52 | 53 | }); 54 | 55 | $('.ss-gridfield select.select2blocktype').entwine({ 56 | 57 | // update blocks on change (in Gridfield) 58 | onchange: function(){ 59 | 60 | // duplicated from GridFieldOrderableRows.onadd.update: 61 | var grid = this.getGridField(); 62 | var data = []; 63 | data.push({ 64 | name: 'block_id', 65 | value: this.closest('tr').data("id") 66 | }); 67 | data.push({ 68 | name: 'block_type', 69 | value: this.val() 70 | }); 71 | 72 | // area-assignment forwards the request to gridfieldextensions::reorder server side 73 | grid.reload({ 74 | //url: grid.data("url-reorder"), 75 | url: grid.data("url-blocktype-assignment"), 76 | data: data 77 | }); 78 | } 79 | 80 | }); 81 | 82 | }); 83 | })(jQuery); 84 | -------------------------------------------------------------------------------- /js/display_logic_editablerow-fixes.js: -------------------------------------------------------------------------------- 1 | // This patch applies some fixes to displaylogic to make it work with EditableRows, 2 | // probably also fixes deep updates with dot notation in fieldnames. 3 | // These edits could probably included in the general display_logic, as they mainly make the selectors 4 | // less rigid, because we have no way of knowing the exact IDs & names on beforehand for every situation 5 | (function($) { 6 | 7 | // make selectors more specific to override default display logic behaviour 8 | $('.ss-gridfield-blockenhancements div.display-logic, .ss-gridfield-blockenhancements div.display-logic-master') 9 | .entwine({ 10 | 11 | findHolder: function(name) { 12 | // Again, don't search exact ID as we have way of knowing that, check for ends with instead 13 | return this.closest('form,fieldset').find('[id$='+name+'_Holder]'); 14 | }, 15 | 16 | getFormField: function() { 17 | // displaylogicwrappers conain the eval & masters on themselves instead of the child fields 18 | if(this.hasClass('displaylogicwrapper')){ 19 | return this; 20 | } 21 | // leave out the actual name, just return the first element with a name attribute CONTAINING the name, 22 | // as we have no way of knowing the exact name because of deep editing 23 | return this.find('[name*='+name+']'); 24 | //return this.find('[name]'); 25 | //return this.find('[name='+name+']'); 26 | }, 27 | 28 | onmatch: function () { 29 | 30 | var allReadonly = true; 31 | var masters = []; 32 | var field = this.getFormField(); 33 | if(field.data('display-logic-eval') && field.data('display-logic-masters')) { 34 | this.data('display-logic-eval', field.data('display-logic-eval')) 35 | .data('display-logic-masters', field.data('display-logic-masters')); 36 | } 37 | 38 | masters = this.getMasters(); 39 | 40 | for(m in masters) { 41 | var holderName = this.nameToHolder(masters[m]); 42 | 43 | // again, search for field ending with MasterName_Holder, and limit to current fieldset as 44 | // multiple identical fieldsets may be included on the same form only with different IDs 45 | //var master = this.closest('form').find(this.escapeSelector('#'+holderName)); 46 | master = this.closest('form,fieldset').find('[id$='+masters[m]+'_Holder]'); 47 | // Continue with regular code: 48 | 49 | if(!master.is('.readonly')) allReadonly = false; 50 | 51 | master.addClass("display-logic-master"); 52 | if(master.find('input[type=radio]').length) { 53 | master.addClass('optionset'); 54 | } 55 | if(master.find("input[type=checkbox]").length > 1) { 56 | master.addClass('checkboxset'); 57 | } 58 | } 59 | 60 | // If all the masters are readonly fields, the field has no way of displaying. 61 | if(masters.length && allReadonly) { 62 | this.show(); 63 | } 64 | 65 | // Bubble up to super methods if both master & listener (needed for ao Switch field) 66 | if(this.hasClass('display-logic-hidden') && this.hasClass('display-logic-master')) { 67 | this._super(); 68 | } 69 | 70 | } 71 | 72 | }); 73 | 74 | // make selectors more specific to override default display logic behaviour 75 | $('.ss-gridfield-blockenhancements div.display-logic-master').entwine({ 76 | Listeners: null, 77 | 78 | getListeners: function() { 79 | if(l = this._super()) { 80 | return l; 81 | } 82 | var self = this; 83 | var listeners = []; 84 | // EDIT 85 | this.closest("form,fieldset").find('.display-logic').each(function() { 86 | masters = $(this).getMasters(); 87 | for(m in masters) { 88 | //if(self.nameToHolder(masters[m]) == self.attr('id')) { 89 | var endswith = new RegExp(masters[m]+'_Holder$'); 90 | if (self.attr('id').match(endswith)!=null){ 91 | // END:EDIT 92 | listeners.push($(this)[0]); 93 | break; 94 | } 95 | } 96 | }); 97 | this.setListeners(listeners); 98 | return this.getListeners(); 99 | } 100 | }); 101 | 102 | })(jQuery); 103 | -------------------------------------------------------------------------------- /code/controllers/LeftAndMain_BlockActions.php: -------------------------------------------------------------------------------- 1 | owner->getResponseNegotiator()->respond($this->owner->request); 17 | // } 18 | 19 | // Example from leftandmain.php: 20 | // public function save($data, $form) { 21 | // $className = $this->stat('tree_class'); 22 | // 23 | // // Existing or new record? 24 | // $id = $data['ID']; 25 | // if(substr($id,0,3) != 'new') { 26 | // $record = DataObject::get_by_id($className, $id); 27 | // if($record && !$record->canEdit()) return Security::permissionFailure($this); 28 | // if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id); 29 | // } else { 30 | // if(!singleton($this->stat('tree_class'))->canCreate()) return Security::permissionFailure($this); 31 | // $record = $this->getNewItem($id, false); 32 | // } 33 | // 34 | // // save form data into record 35 | // $form->saveInto($record, true); 36 | // $record->write(); 37 | // $this->extend('onAfterSave', $record); 38 | // $this->setCurrentPageID($record->ID); 39 | // 40 | // $this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.SAVEDUP', 'Saved.'))); 41 | // return $this->getResponseNegotiator()->respond($this->getRequest()); 42 | // } 43 | 44 | public function publishPageAndBlocks($data, $form) 45 | { 46 | 47 | // regular save 48 | $this->owner->save($data, $form); 49 | 50 | // Now publish the whole bunch 51 | if ( $page = SiteTree::get()->byID($data['ID']) ) { 52 | if (!$page->canPublish()) { 53 | throw new SS_HTTPResponse_Exception("Publish page not allowed", 403); 54 | } 55 | // else: publish page (also triggers editable columns/rows write()) 56 | $page->doPublish(); 57 | 58 | // and publish any blocks which the user's allowed to publish (have already been written) 59 | if (is_callable(array($page, 'Blocks'))) { 60 | foreach ($page->Blocks() as $block){ 61 | // skip any blocks that we cannot publish 62 | if (!$block->canPublish()) continue; 63 | // publish 64 | $block->invokeWithExtensions('onBeforePublish', $block); 65 | $block->publish('Stage', 'Live'); 66 | $block->invokeWithExtensions('onAfterPublish', $block); 67 | } 68 | } 69 | } else { 70 | throw new SS_HTTPResponse_Exception( 71 | "Bad page ID #" . (int)$data['ID'], 404); 72 | } 73 | 74 | // this generates a message that will show up in the CMS 75 | $this->owner->response->addHeader( 76 | 'X-Status', 77 | rawurlencode("Page + blocks published") 78 | ); 79 | 80 | return $this->owner->getResponseNegotiator()->respond($this->owner->request); 81 | } 82 | 83 | // public function doAction($data, $form){ 84 | // $className = $this->owner->stat('tree_class'); 85 | // $SQL_id = Convert::raw2sql($data['ID']); 86 | // 87 | // $record = DataObject::get_by_id($className, $SQL_id); 88 | // 89 | // if(!$record || !$record->ID){ 90 | // throw new SS_HTTPResponse_Exception( 91 | // "Bad record ID #" . (int)$data['ID'], 404); 92 | // } 93 | // 94 | // // at this point you have a $record, 95 | // // which is your page you can work with! 96 | // 97 | // // this generates a message that will show up in the CMS 98 | // $this->owner->response->addHeader( 99 | // 'X-Status', 100 | // rawurlencode('Success message!') 101 | // ); 102 | // 103 | // return $this->owner->getResponseNegotiator() 104 | // ->respond($this->owner->request); 105 | // } 106 | 107 | } -------------------------------------------------------------------------------- /code/forms/GF_BlockEnhancements.php: -------------------------------------------------------------------------------- 1 | ViewableData::create()->ThemeDir(), 21 | // "ThemeDir" => SSViewer::get_theme_folder(), 22 | // "ProjectDir" => project(), 23 | // "AreaNoneTitle" => Config::inst()->get(get_class(), 'unassigned_area_description'), 24 | //// "BlockAreas" => json_encode( $blockAreas ) 25 | // ); 26 | //->setAttribute('data-project-dir', project()); 27 | // Requirements::javascriptTemplate($moduleDir.'/js/BlockEnhancements.js', $jsVars, 'BlockEnhancements'); 28 | Requirements::javascript($moduleDir.'/js/BlockEnhancements.js'); 29 | Requirements::css($moduleDir.'/css/BlockEnhancements.css'); 30 | 31 | Requirements::javascript($moduleDir.'/js/EditableBlockRow.js'); 32 | Requirements::css($moduleDir.'/css/EditableBlockRow.css'); 33 | 34 | Requirements::javascript($moduleDir.'/js/display_logic_editablerow-fixes.js'); 35 | } 36 | 37 | public function getURLHandlers($grid) { 38 | return array( 39 | // 'POST area_assignment' => 'handleAreaAssignment', 40 | 'POST blocktype_assignment' => 'handleBlockTypeAssignment', 41 | ); 42 | } 43 | 44 | // public function __construct() 45 | // { 46 | // parent::__construct(); 47 | // self::include_requirements(); 48 | // } 49 | 50 | /** 51 | * @param GridField $field 52 | */ 53 | public function getHTMLFragments($field) { 54 | 55 | self::include_requirements(); 56 | 57 | // set ajax urls / vars 58 | $field->addExtraClass('ss-gridfield-blockenhancements'); 59 | // $field->setAttribute('data-url-area-assignment', $field->Link('area_assignment')); 60 | $field->setAttribute('data-url-blocktype-assignment', $field->Link('blocktype_assignment')); 61 | // $field->setAttribute('data-block-area-none-title', Config::inst()->get(get_class(), 'unassigned_area_description')); 62 | 63 | // add no-chozen to dropdown 64 | // $field->getConfig()->getComponentByType('GridFieldAddNewMultiClass')-> 65 | // $field->getConfig()->getComponentByType('GridFieldDetailForm')->setAttribute('data-project-dir', project()); 66 | 67 | } 68 | 69 | /** 70 | * Handles requests to assign a new block area to a block item 71 | * 72 | * @param GridField $grid 73 | * @param SS_HTTPRequest $request 74 | * @return SS_HTTPResponse 75 | */ 76 | // public function handleAreaAssignment($grid, $request) { 77 | // $list = $grid->getList(); 78 | // 79 | // // @TODO: do we need this? (copied from GridFieldOrderableRows::handleReorder) 80 | //// $modelClass = $grid->getModelClass(); 81 | //// if ($list instanceof ManyManyList && !singleton($modelClass)->canView()) { 82 | //// $this->httpError(403); 83 | //// } else if(!($list instanceof ManyManyList) && !singleton($modelClass)->canEdit()) { 84 | //// $this->httpError(403); 85 | //// } 86 | // 87 | // $blockid = $request->postVar('blockarea_block_id'); 88 | // $blockarea = $request->postVar('blockarea_area'); 89 | // if($blockarea=='none') $blockarea = ''; 90 | // $block = $list->byID($blockid); 91 | // 92 | // // Update item with correct Area assigned (custom query required to write m_m_extraField) 93 | //// $block->BlockArea = $blockarea; 94 | //// $block->write(); 95 | // // @TODO: improve this custom query to be more robust? 96 | // DB::query(sprintf( 97 | // "UPDATE `%s` SET `%s` = '%s' WHERE `BlockID` = %d", 98 | // 'SiteTree_Blocks', 99 | // 'BlockArea', 100 | // $blockarea, 101 | // $blockid 102 | // )); 103 | // 104 | // // Forward the request to GridFieldOrderableRows::handleReorder 105 | // return $grid->getConfig() 106 | // ->getComponentByType('GridFieldOrderableRows') 107 | // ->handleReorder($grid, $request); 108 | // } 109 | 110 | /** 111 | * Handles requests to assign a new block area to a block item 112 | * 113 | * @param GridField $grid 114 | * @param SS_HTTPRequest $request 115 | * @return SS_HTTPResponse 116 | */ 117 | public function handleBlockTypeAssignment($grid, $request) { 118 | $list = $grid->getList(); 119 | 120 | // @TODO: do we need this? (copied from GridFieldOrderableRows::handleReorder) 121 | // $modelClass = $grid->getModelClass(); 122 | // if ($list instanceof ManyManyList && !singleton($modelClass)->canView()) { 123 | // $this->httpError(403); 124 | // } else if(!($list instanceof ManyManyList) && !singleton($modelClass)->canEdit()) { 125 | // $this->httpError(403); 126 | // } 127 | 128 | $blockid = $request->postVar('block_id'); 129 | $blocktype = $request->postVar('block_type'); 130 | $block = $list->byID($blockid); 131 | 132 | // Update item with correct Area assigned (custom query required to write m_m_extraField) 133 | $block->ClassName = $blocktype; 134 | $block->write(); 135 | //print_r($block->record); 136 | // // @TODO: improve this custom query to be more robust? 137 | // DB::query(sprintf( 138 | // "UPDATE `%s` SET `%s` = '%s' WHERE `BlockID` = %d", 139 | // 'SiteTree_Blocks', 140 | // 'BlockArea', 141 | // $blockarea, 142 | // $blockid 143 | // )); 144 | return $grid->FieldHolder(); 145 | 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /code/forms/GridfieldConfig_BlockManager_bu.txt: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class GridFieldConfig_BlockManager extends GridFieldConfig 9 | { 10 | public $blockManager; 11 | 12 | public function __construct($canAdd = true, $canEdit = true, $canDelete = true, $editableRows = false, $aboveOrBelow = false) 13 | { 14 | parent::__construct(); 15 | 16 | $this->blockManager = Injector::inst()->get('BlockManager'); 17 | $controllerClass = Controller::curr()->class; 18 | 19 | // Get available Areas (for page) or all in case of ModelAdmin 20 | if ($controllerClass == 'CMSPageEditController') { 21 | $currentPage = Controller::curr()->currentPage(); 22 | $areasFieldSource = $this->blockManager->getAreasForPageType($currentPage->ClassName); 23 | } else { 24 | $areasFieldSource = $this->blockManager->getAreasForTheme(); 25 | } 26 | $blockTypeArray = $this->blockManager->getBlockClasses(); 27 | 28 | // EditableColumns only makes sense on Saveable parenst (eg Page), or inline changes won't be saved 29 | if ($editableRows) { 30 | $displayfields = array( 31 | //'singular_name' => array('title' => _t('Block.BlockType', 'Block Type'), 'field' => 'ReadonlyField'), 32 | 'ClassName' => array( 33 | 'title' => _t('Block.BlockType', 'Block Type').' 34 |             ', 35 | // the  s prevent wrapping of dropdowns 36 | 'callback' => function () use ($blockTypeArray) { 37 | return DropdownField::create('ClassName', 'Block Type', $blockTypeArray) 38 | // ->setHasEmptyDefault(true) 39 | ->addExtraClass('select2blocktype'); 40 | }, 41 | ), 42 | 'Title' => array( 43 | 'title' => _t('Block.TitleName', 'Block Name'), 44 | // 'field' => 'ReadonlyField' 45 | 'field' => 'TextField' 46 | ), 47 | 'BlockArea' => array( 48 | 'title' => _t('Block.BlockArea', 'Block Area').' 49 |             ', 50 | // the  s prevent wrapping of dropdowns 51 | 'callback' => function () use ($areasFieldSource) { 52 | return DropdownField::create('BlockArea', 'Block Area', $areasFieldSource) 53 | ->setHasEmptyDefault(true); 54 | }, 55 | ), 56 | 'isPublishedNice' => array('title' => _t('Block.IsPublishedField', 'Published'), 'field' => 'ReadonlyField'), 57 | 'UsageListAsString' => array('title' => _t('Block.UsageListAsString', 'Used on'), 'field' => 'ReadonlyField'), 58 | ); 59 | 60 | if ($aboveOrBelow) { 61 | $displayfields['AboveOrBelow'] = array( 62 | 'title' => _t('GridFieldConfigBlockManager.AboveOrBelow', 'Above or Below'), 63 | 'callback' => function () { 64 | return DropdownField::create('AboveOrBelow', _t('GridFieldConfigBlockManager.AboveOrBelow', 'Above or Below'), BlockSet::config()->get('above_or_below_options')); 65 | }, 66 | ); 67 | } 68 | $this->addComponent($editable = new GridFieldEditableColumns()); 69 | $editable->setDisplayFields($displayfields); 70 | $this->addComponent($erow = new EditableBlockRow()); 71 | // $erow->setFields('getEditableBlockRowFields'); 72 | } else { // BlockManager 73 | $this->addComponent($dcols = new GridFieldDataColumns()); 74 | 75 | $displayfields = array( 76 | 'singular_name' => _t('Block.BlockType', 'Block Type'), 77 | // 'Title' => _t('Block.Title', 'Description'), 78 | 'Title' => _t('Block.TitleName', 'Block Name'), 79 | 'BlockArea' => _t('Block.BlockArea', 'Block Area'), 80 | 'isPublishedNice' => _t('Block.IsPublishedField', 'Published'), 81 | 'UsageListAsString' => _t('Block.UsageListAsString', 'Used on'), 82 | ); 83 | $dcols->setDisplayFields($displayfields); 84 | $dcols->setFieldCasting(array('UsageListAsString' => 'HTMLText->Raw')); 85 | 86 | // optionally add copybutton 87 | if(class_exists('GridFieldCopyButton')){ 88 | $this->addComponent(new GridFieldCopyButton()); 89 | } 90 | } 91 | 92 | $this->addComponent(new GridFieldButtonRow('before')); 93 | $this->addComponent(new GridFieldButtonRow('after')); 94 | $this->addComponent(new GridFieldToolbarHeader()); 95 | $this->addComponent(new GridFieldDetailForm()); 96 | $this->addComponent(new GridFieldDetailForm()); 97 | 98 | 99 | // load enhancements module 100 | if(class_exists('GF_BlockEnhancements')){ 101 | $this->addComponent(new GF_BlockEnhancements()); 102 | } 103 | 104 | // stuff only for BlockAdmin 105 | if ($controllerClass == 'BlockAdmin') { 106 | $this->addComponent($sort = new GridFieldSortableHeader()); 107 | $sort->setThrowExceptionOnBadDataType(false); 108 | $this->addComponent($filter = new GridFieldFilterHeader()); 109 | $filter->setThrowExceptionOnBadDataType(false); 110 | } else { 111 | // only for GF on SiteTree 112 | $this->addComponent(new GridFieldTitleHeader()); 113 | $this->addComponent(new GridFieldFooter()); 114 | } 115 | 116 | if ($canAdd) { 117 | $multiClass = new GridFieldAddNewMultiClass('after'); 118 | $classes = $this->blockManager->getBlockClasses(); 119 | $multiClass->setClasses($classes); 120 | $this->addComponent($multiClass); 121 | //$this->addComponent(new GridFieldAddNewButton()); 122 | } 123 | 124 | if ($canEdit) { 125 | $this->addComponent(new GridFieldEditButton()); 126 | } 127 | 128 | if ($canDelete) { 129 | $this->addComponent(new GridFieldDeleteAction(true)); 130 | } 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * Add the GridFieldAddExistingSearchButton component to this grid config. 137 | * 138 | * @return $this 139 | **/ 140 | public function addExisting() 141 | { 142 | $this->addComponent($add = new GridFieldAddExistingSearchButton('buttons-after-right')); 143 | $add->setSearchList(Block::get()); 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * Add the GridFieldBulkManager component to this grid config. 150 | * 151 | * @return $this 152 | **/ 153 | public function addBulkEditing() 154 | { 155 | if (class_exists('GridFieldBulkManager')) { 156 | $this->addComponent(new GridFieldBulkManager()); 157 | } 158 | 159 | return $this; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /code/forms/GridfieldConfig_BlockManager_buv2.txt: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class GridFieldConfig_BlockManager extends GridFieldConfig 9 | { 10 | public $blockManager; 11 | 12 | public function __construct($canAdd = true, $canEdit = true, $canDelete = true, $editableRows = false, $aboveOrBelow = false) 13 | { 14 | parent::__construct(); 15 | 16 | $this->blockManager = Injector::inst()->get('BlockManager'); 17 | $controllerClass = Controller::curr()->class; 18 | // Get available Areas (for page) or all in case of ModelAdmin 19 | if ($controllerClass == 'CMSPageEditController') { 20 | $currentPage = Controller::curr()->currentPage(); 21 | $areasFieldSource = $this->blockManager->getAreasForPageType($currentPage->ClassName); 22 | } else { 23 | $areasFieldSource = $this->blockManager->getAreasForTheme(); 24 | } 25 | // EDIT 26 | $blockTypeArray = $this->blockManager->getBlockClasses(); 27 | // /EDIT 28 | 29 | // EditableColumns only makes sense on Saveable parenst (eg Page), or inline changes won't be saved 30 | if ($editableRows) { 31 | $this->addComponent($editable = new GridFieldEditableColumns()); 32 | $displayfields = array( 33 | // EDIT 34 | //'singular_name' => array('title' => _t('Block.BlockType', 'Block Type'), 'field' => 'ReadonlyField'), 35 | 'ClassName' => array( 36 | 'title' => _t('Block.BlockType', 'Block Type').' 37 |             ', 38 | // the  s prevent wrapping of dropdowns 39 | 'callback' => function () use ($blockTypeArray) { 40 | return DropdownField::create('ClassName', 'Block Type', $blockTypeArray) 41 | // ->setHasEmptyDefault(true) 42 | ->addExtraClass('select2blocktype'); 43 | }, 44 | ), 45 | 'Title' => array( 46 | 'title' => _t('Block.TitleName', 'Block Name'), 47 | // 'field' => 'ReadonlyField' 48 | 'field' => 'TextField' 49 | ), 50 | // /EDIT 51 | 'BlockArea' => array( 52 | 'title' => _t('Block.BlockArea', 'Block Area').' 53 |             ', 54 | // the  s prevent wrapping of dropdowns 55 | 'callback' => function () use ($areasFieldSource) { 56 | return DropdownField::create('BlockArea', 'Block Area', $areasFieldSource) 57 | ->setHasEmptyDefault(true); 58 | }, 59 | ), 60 | 'isPublishedNice' => array('title' => _t('Block.IsPublishedField', 'Published'), 'field' => 'ReadonlyField'), 61 | 'UsageListAsString' => array('title' => _t('Block.UsageListAsString', 'Used on'), 'field' => 'ReadonlyField'), 62 | ); 63 | 64 | if ($aboveOrBelow) { 65 | $displayfields['AboveOrBelow'] = array( 66 | 'title' => _t('GridFieldConfigBlockManager.AboveOrBelow', 'Above or Below'), 67 | 'callback' => function () { 68 | return DropdownField::create('AboveOrBelow', _t('GridFieldConfigBlockManager.AboveOrBelow', 'Above or Below'), BlockSet::config()->get('above_or_below_options')); 69 | }, 70 | ); 71 | } 72 | $editable->setDisplayFields($displayfields); 73 | // EDIT 74 | $this->addComponent($erow = new EditableBlockRow()); 75 | // /EDIT 76 | } else { 77 | $this->addComponent($dcols = new GridFieldDataColumns()); 78 | 79 | $displayfields = array( 80 | 'singular_name' => _t('Block.BlockType', 'Block Type'), 81 | // EDIT 82 | // 'Title' => _t('Block.Title', 'Description'), 83 | 'Title' => _t('Block.TitleName', 'Block Name'), 84 | // /EDIT 85 | 'BlockArea' => _t('Block.BlockArea', 'Block Area'), 86 | 'isPublishedNice' => _t('Block.IsPublishedField', 'Published'), 87 | 'UsageListAsString' => _t('Block.UsageListAsString', 'Used on'), 88 | ); 89 | $dcols->setDisplayFields($displayfields); 90 | $dcols->setFieldCasting(array('UsageListAsString' => 'HTMLText->Raw')); 91 | } 92 | 93 | $this->addComponent(new GridFieldButtonRow('before')); 94 | // EDIT 95 | $this->addComponent(new GridFieldButtonRow('after')); 96 | // /EDIT 97 | $this->addComponent(new GridFieldToolbarHeader()); 98 | $this->addComponent(new GridFieldDetailForm()); 99 | // EDIT 100 | //$this->addComponent($sort = new GridFieldSortableHeader()); 101 | //$this->addComponent($filter = new GridFieldFilterHeader()); 102 | //$this->addComponent(new GridFieldDetailForm()); 103 | 104 | //$filter->setThrowExceptionOnBadDataType(false); 105 | //$sort->setThrowExceptionOnBadDataType(false); 106 | 107 | // load enhancements module 108 | if(class_exists('GF_BlockEnhancements')){ 109 | $this->addComponent(new GF_BlockEnhancements()); 110 | } 111 | 112 | // stuff only for BlockAdmin 113 | if ($controllerClass == 'BlockAdmin') { 114 | $this->addComponent($sort = new GridFieldSortableHeader()); 115 | $sort->setThrowExceptionOnBadDataType(false); 116 | $this->addComponent($filter = new GridFieldFilterHeader()); 117 | $filter->setThrowExceptionOnBadDataType(false); 118 | } else { 119 | // only for GF on SiteTree 120 | $this->addComponent(new GridFieldTitleHeader()); 121 | $this->addComponent(new GridFieldFooter()); 122 | } 123 | 124 | if ($canAdd) { 125 | $multiClass = new GridFieldAddNewMultiClass('after'); 126 | $classes = $this->blockManager->getBlockClasses(); 127 | $multiClass->setClasses($classes); 128 | $this->addComponent($multiClass); 129 | //$this->addComponent(new GridFieldAddNewButton()); 130 | } 131 | // /EDIT 132 | 133 | if ($controllerClass == 'BlockAdmin' && class_exists('GridFieldCopyButton')) { 134 | $this->addComponent(new GridFieldCopyButton()); 135 | } 136 | 137 | if ($canEdit) { 138 | $this->addComponent(new GridFieldEditButton()); 139 | } 140 | 141 | if ($canDelete) { 142 | $this->addComponent(new GridFieldDeleteAction(true)); 143 | } 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * Add the GridFieldAddExistingSearchButton component to this grid config. 150 | * 151 | * @return $this 152 | **/ 153 | public function addExisting() 154 | { 155 | // EDIT 156 | //$this->addComponent($add = new GridFieldAddExistingSearchButton()); 157 | $this->addComponent($add = new GridFieldAddExistingSearchButton('buttons-after-right')); 158 | // /EDIT 159 | $add->setSearchList(Block::get()); 160 | 161 | return $this; 162 | } 163 | 164 | /** 165 | * Add the GridFieldBulkManager component to this grid config. 166 | * 167 | * @return $this 168 | **/ 169 | public function addBulkEditing() 170 | { 171 | if (class_exists('GridFieldBulkManager')) { 172 | $this->addComponent(new GridFieldBulkManager()); 173 | } 174 | 175 | return $this; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /images/BlockIcons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 63 | 73 | 80 | 85 | 90 | 95 | 100 | 105 | 106 | 112 | 120 | 125 | 130 | 135 | 140 | 145 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /code/forms/GFConf_BlockManagerEnhanced.php: -------------------------------------------------------------------------------- 1 | blockManager = Injector::inst()->get('BlockManager'); 16 | $controllerClass = Controller::curr()->class; 17 | // Get available Areas (for page) or all in case of ModelAdmin 18 | if ($controllerClass == 'CMSPageEditController') { 19 | $currentPage = Controller::curr()->currentPage(); 20 | $areasFieldSource = $this->blockManager->getAreasForPageType($currentPage->ClassName); 21 | } else { 22 | $areasFieldSource = $this->blockManager->getAreasForTheme(); 23 | } 24 | // EDIT 25 | $blockTypeArray = $this->blockManager->getBlockClasses(); 26 | // /EDIT 27 | 28 | // EditableColumns only makes sense on Saveable parenst (eg Page), or inline changes won't be saved 29 | if ($editableRows) { 30 | 31 | // set project-dir in cookie to be accessible as fallback from js 32 | Cookie::set('js-project-dir',project(),90,null,null,false,false); 33 | 34 | $this->addComponent($editable = new GridFieldEditableColumns()); 35 | $displayfields = array( 36 | // EDIT 37 | //'singular_name' => array('title' => _t('Block.BlockType', 'Block Type'), 'field' => 'ReadonlyField'), 38 | 'ClassName' => array( 39 | 'title' => _t('Block.BlockType', 'Block Type').' 40 |             ', 41 | // the  s prevent wrapping of dropdowns 42 | 'callback' => function () use ($blockTypeArray) { 43 | 44 | return DropdownField::create('ClassName', 'Block Type', $blockTypeArray) 45 | // ->setHasEmptyDefault(true) 46 | ->addExtraClass('select2blocktype') 47 | ->setAttribute('data-project-dir', project()); 48 | }, 49 | ), 50 | 'Title' => array( 51 | 'title' => _t('Block.TitleName', 'Block Name'), 52 | // 'field' => 'ReadonlyField' 53 | 'field' => 'TextField' 54 | ), 55 | // /EDIT 56 | 'BlockArea' => array( 57 | 'title' => _t('Block.BlockArea', 'Block Area').' 58 |             ', 59 | // the  s prevent wrapping of dropdowns 60 | 'callback' => function () use ($areasFieldSource) { 61 | return DropdownField::create('BlockArea', 'Block Area', $areasFieldSource) 62 | ->setHasEmptyDefault(true); 63 | }, 64 | ), 65 | 'isPublishedNice' => array('title' => _t('Block.IsPublishedField', 'Published'), 'field' => 'ReadonlyField'), 66 | 'UsageListAsString' => array('title' => _t('Block.UsageListAsString', 'Used on'), 'field' => 'ReadonlyField'), 67 | ); 68 | 69 | if ($aboveOrBelow) { 70 | $displayfields['AboveOrBelow'] = array( 71 | 'title' => _t('GridFieldConfigBlockManager.AboveOrBelow', 'Above or Below'), 72 | 'callback' => function () { 73 | return DropdownField::create('AboveOrBelow', _t('GridFieldConfigBlockManager.AboveOrBelow', 'Above or Below'), BlockSet::config()->get('above_or_below_options')); 74 | }, 75 | ); 76 | } 77 | $editable->setDisplayFields($displayfields); 78 | // EDIT 79 | $this->addComponent($erow = new EditableBlockRow()); 80 | // /EDIT 81 | } else { 82 | $this->addComponent($dcols = new GridFieldDataColumns()); 83 | 84 | $displayfields = array( 85 | 'singular_name' => _t('Block.BlockType', 'Block Type'), 86 | // EDIT 87 | // 'Title' => _t('Block.Title', 'Description'), 88 | 'Title' => _t('Block.TitleName', 'Block Name'), 89 | // /EDIT 90 | 'BlockArea' => _t('Block.BlockArea', 'Block Area'), 91 | 'isPublishedNice' => _t('Block.IsPublishedField', 'Published'), 92 | 'UsageListAsString' => _t('Block.UsageListAsString', 'Used on'), 93 | ); 94 | $dcols->setDisplayFields($displayfields); 95 | $dcols->setFieldCasting(array('UsageListAsString' => 'HTMLText->Raw')); 96 | } 97 | 98 | $this->addComponent(new GridFieldButtonRow('before')); 99 | // EDIT 100 | $this->addComponent(new GridFieldButtonRow('after')); 101 | // /EDIT 102 | $this->addComponent(new GridFieldToolbarHeader()); 103 | $this->addComponent(new GridFieldDetailForm()); 104 | // EDIT 105 | //$this->addComponent($sort = new GridFieldSortableHeader()); 106 | //$this->addComponent($filter = new GridFieldFilterHeader()); 107 | //$this->addComponent(new GridFieldDetailForm()); 108 | 109 | //$filter->setThrowExceptionOnBadDataType(false); 110 | //$sort->setThrowExceptionOnBadDataType(false); 111 | 112 | // load enhancements module (eg inline editing etc, needs save action @TODO: move to SiteTree only? 113 | if(class_exists('GF_BlockEnhancements')){ 114 | $this->addComponent(new GF_BlockEnhancements()); 115 | } 116 | 117 | // stuff only for BlockAdmin 118 | if ($controllerClass == 'BlockAdmin') { 119 | $this->addComponent($sort = new GridFieldSortableHeader()); 120 | $sort->setThrowExceptionOnBadDataType(false); 121 | $this->addComponent($filter = new GridFieldFilterHeader()); 122 | $filter->setThrowExceptionOnBadDataType(false); 123 | } else { 124 | // only for GF on SiteTree 125 | $this->addComponent(new GridFieldTitleHeader()); 126 | $this->addComponent(new GridFieldFooter()); 127 | // groupable 128 | $this->addComponent(new GridFieldGroupable( 129 | 'BlockArea', 130 | 'Area', 131 | 'none', 132 | $areasFieldSource 133 | )); 134 | // var_dump($areasFieldSource); 135 | 136 | // // Get available Areas (for page) enhancements inactive when in ModelAdmin/BlockAdmin 137 | // if (Controller::curr() && Controller::curr()->class == 'CMSPageEditController') { 138 | // // Provide defined blockAreas to JS 139 | // $blockManager = Injector::inst()->get('BlockManager'); 140 | //// $blockAreas = $blockManager->getAreasForPageType( Controller::curr()->currentPage()->ClassName ); 141 | // $blockAreas = $blockManager->getAreasForPageType( Controller::curr()->currentPage()->ClassName ); 142 | // } 143 | } 144 | 145 | if ($canAdd) { 146 | $multiClass = new GridFieldAddNewMultiClass('after'); 147 | $classes = $this->blockManager->getBlockClasses(); 148 | $multiClass->setClasses($classes); 149 | $this->addComponent($multiClass); 150 | //$this->addComponent(new GridFieldAddNewButton()); 151 | } 152 | // /EDIT 153 | 154 | if ($controllerClass == 'BlockAdmin' && class_exists('GridFieldCopyButton')) { 155 | $this->addComponent(new GridFieldCopyButton()); 156 | } 157 | 158 | if ($canEdit) { 159 | $this->addComponent(new GridFieldEditButton()); 160 | } 161 | 162 | if ($canDelete) { 163 | $this->addComponent(new GridFieldDeleteAction(true)); 164 | } 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * Add the GridFieldAddExistingSearchButton component to this grid config. 171 | * 172 | * @return $this 173 | **/ 174 | public function addExisting() 175 | { 176 | // EDIT 177 | //$this->addComponent($add = new GridFieldAddExistingSearchButton()); 178 | $this->addComponent($add = new GridFieldAddExistingSearchButton('buttons-after-right')); 179 | // /EDIT 180 | $add->setSearchList(Block::get()); 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * Add the GridFieldBulkManager component to this grid config. 187 | * 188 | * @return $this 189 | **/ 190 | public function addBulkEditing() 191 | { 192 | if (class_exists('GridFieldBulkManager')) { 193 | $this->addComponent(new GridFieldBulkManager()); 194 | } 195 | 196 | return $this; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /js/EditableBlockRow.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | $.entwine("ss", function ($) { 3 | 4 | // A global method on grid fields to display the no items message if no rows are found 5 | $(".ss-gridfield").entwine({ 6 | showNoItemsMessage: function () { 7 | if (this.find('.ss-gridfield-items:first').children().not('.ss-gridfield-no-items').length === 0) { 8 | this.find('.ss-gridfield-no-items').show(); 9 | } 10 | } 11 | }); 12 | 13 | // Milkyway\SS\GridFieldUtils\EditableRow 14 | $('.cms-container').entwine({ 15 | OpenGridFieldToggles: {}, 16 | saveTabState: function () { 17 | var $that = this, 18 | OpenGridFieldToggles = $that.getOpenGridFieldToggles(); 19 | 20 | $that._super(); 21 | 22 | $that.find('.ss-gridfield.ss-gridfield-editable-rows').each(function () { 23 | var $this = $(this), 24 | openToggles = $this.getOpenToggles(); 25 | 26 | if (openToggles.length) { 27 | OpenGridFieldToggles[$this.attr('id')] = $this.getOpenToggles(); 28 | } 29 | }); 30 | }, 31 | restoreTabState: function (overrideStates) { 32 | var $that = this, 33 | OpenGridFieldToggles = $that.getOpenGridFieldToggles(); 34 | 35 | $that._super(overrideStates); 36 | 37 | $.each(OpenGridFieldToggles, function (id, openToggles) { 38 | $that.find('#' + id + '.ss-gridfield.ss-gridfield-editable-rows').reopenToggles(openToggles); 39 | }); 40 | 41 | $that.find('.ss-gridfield-editable-row--toggle_start').click(); 42 | 43 | $that.setOpenGridFieldToggles({}); 44 | } 45 | }); 46 | 47 | $(".ss-gridfield.ss-gridfield-editable-rows").entwine({ 48 | reload: function (opts, success) { 49 | var $grid = this, 50 | openToggles = $grid.getOpenToggles(), 51 | args = arguments; 52 | 53 | this._super(opts, function () { 54 | $grid.reopenToggles(openToggles); 55 | $grid.find('.ss-gridfield-editable-row--toggle_start').click(); 56 | 57 | if (success) { 58 | success.apply($grid, args); 59 | } 60 | }); 61 | }, 62 | getOpenToggles: function () { 63 | var $grid = this, 64 | openToggles = []; 65 | 66 | if ($grid.hasClass('ss-gridfield-editable-rows_disableToggleState')) { 67 | return openToggles; 68 | } 69 | 70 | $grid.find(".ss-gridfield-editable-row--toggle_open").each(function (key) { 71 | var $this = $(this), 72 | $holder = $this.parents('td:first'), 73 | $parent = $this.parents('tr:first'), 74 | $currentGrid = $parent.parents('.ss-gridfield:first'), 75 | $editable = $parent.next(); 76 | 77 | if (!$editable.hasClass('ss-gridfield-editable-row--row') 78 | || $editable.data('id') != $parent.data('id') 79 | || $editable.data('class') != $parent.data('class')) { 80 | $editable = null; 81 | } 82 | else if ($currentGrid.hasClass('ss-gridfield-editable-rows_disableToggleState')) { 83 | return true; 84 | } 85 | 86 | openToggles[key] = { 87 | link: $holder.data('link') 88 | }; 89 | 90 | if ($editable) { 91 | $editable.find('.ss-tabset.ui-tabs').each(function () { 92 | if (!openToggles[key].tabs) { 93 | openToggles[key].tabs = {}; 94 | } 95 | 96 | openToggles[key].tabs[this.id] = $(this).tabs('option', 'selected'); 97 | }); 98 | 99 | if ($currentGrid.hasClass('ss-gridfield-editable-rows_allowCachedToggles')) { 100 | openToggles[key].row = $editable.detach(); 101 | } 102 | } 103 | }); 104 | 105 | return openToggles; 106 | }, 107 | reopenToggles: function (openToggles) { 108 | var $grid = this, 109 | openTabsInToggle = function (currentToggle, $row) { 110 | if (currentToggle.hasOwnProperty('tabs') && currentToggle.tabs) { 111 | $.each(currentToggle.tabs, function (key, value) { 112 | $row.find('#' + key + '.ss-tabset.ui-tabs').tabs({ 113 | active: value 114 | }); 115 | }); 116 | } 117 | }; 118 | 119 | if ($grid.hasClass('ss-gridfield-editable-rows_disableToggleState')) { 120 | return; 121 | } 122 | 123 | $.each(openToggles, function (key) { 124 | if (openToggles[key].hasOwnProperty('link') && openToggles[key].link) { 125 | var $toggleHolder = $grid.find("td.ss-gridfield-editable-row--icon-holder[data-link='" 126 | + openToggles[key].link + "']"); 127 | 128 | if (!$toggleHolder.length) { 129 | return true; 130 | } 131 | } 132 | else { 133 | return true; 134 | } 135 | 136 | if (openToggles[key].hasOwnProperty('row') && openToggles[key].row) { 137 | var $parent = $toggleHolder.parents('tr:first'); 138 | 139 | $toggleHolder 140 | .find(".ss-gridfield-editable-row--toggle") 141 | .addClass('ss-gridfield-editable-row--toggle_loaded ss-gridfield-editable-row--toggle_open'); 142 | 143 | if (!$parent.next().hasClass('ss-gridfield-editable-row--row')) { 144 | $parent.after(openToggles[key].row); 145 | } 146 | } 147 | else if (openToggles[key].hasOwnProperty('link') && openToggles[key].link) { 148 | $toggleHolder.find(".ss-gridfield-editable-row--toggle").trigger('click', function ($newRow) { 149 | $grid.find('.ss-gridfield.ss-gridfield-editable-rows').reopenToggles(openToggles); 150 | openTabsInToggle(openToggles[key], $newRow); 151 | }, false); 152 | } 153 | }); 154 | } 155 | }); 156 | 157 | $(".ss-gridfield-editable-row--toggle").entwine({ 158 | onclick: function (e, callback, noFocus) { 159 | var $this = this, 160 | $holder = $this.parents('td:first'), 161 | link = $holder.data('link'), 162 | $parent = $this.parents('tr:first'); 163 | 164 | $this.removeClass('ss-gridfield-editable-row--toggle_start'); 165 | 166 | if ($parent.hasClass('ss-gridfield-editable-row--loading')) { 167 | return false; 168 | } 169 | 170 | if (link && !$this.hasClass('ss-gridfield-editable-row--toggle_loaded')) { 171 | $parent.addClass('ss-gridfield-editable-row--loading'); 172 | 173 | $.ajax({ 174 | url: link, 175 | dataType: 'html', 176 | success: function (data) { 177 | var $data = $(data); 178 | $this.addClass('ss-gridfield-editable-row--toggle_loaded ss-gridfield-editable-row--toggle_open'); 179 | $parent.addClass('ss-gridfield-editable-row--reference') 180 | .removeClass('ss-gridfield-editable-row--loading'); 181 | $parent.after($data); 182 | 183 | $data.find('.ss-gridfield-editable-row--toggle_start').click(); 184 | 185 | if (noFocus !== false) { 186 | $data.find("input:first").focus(); 187 | } 188 | 189 | if (typeof callback === 'function') { 190 | callback($data, $this, $parent); 191 | } 192 | }, 193 | error: function (e) { 194 | alert(ss.i18n._t('GRIDFIELD.ERRORINTRANSACTION')); 195 | $parent.removeClass('ss-gridfield-editable-row--loading'); 196 | } 197 | }); 198 | } 199 | else if (link) { 200 | var $editable = $parent.next(); 201 | 202 | if ($editable.hasClass('ss-gridfield-editable-row--row') 203 | && $editable.data('id') == $parent.data('id') 204 | && $editable.data('class') == $parent.data('class')) { 205 | $this.toggleClass('ss-gridfield-editable-row--toggle_open'); 206 | 207 | if ($this.hasClass('ss-gridfield-editable-row--toggle_open')) { 208 | $editable.removeClass('ss-gridfield-editable-row--row_hide') 209 | .find("input:first").focus(); 210 | } 211 | else { 212 | $editable.addClass('ss-gridfield-editable-row--row_hide'); 213 | } 214 | } 215 | } 216 | 217 | return false; 218 | } 219 | }); 220 | 221 | }); 222 | })(jQuery); -------------------------------------------------------------------------------- /code/forms/EditableBlockRow.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | 11 | use RequestHandler as RequestHandler; 12 | use GridField_HTMLProvider as GridField_HTMLProvider; 13 | use GridField_SaveHandler as GridField_SaveHandler; 14 | use GridField_URLHandler as GridField_URLHandler; 15 | use GridField_ColumnProvider as GridField_ColumnProvider; 16 | use Validator as Validator; 17 | use FieldList as FieldList; 18 | use Session as Session; 19 | 20 | class EditableBlockRow extends RequestHandler implements GridField_HTMLProvider, GridField_SaveHandler, GridField_URLHandler, GridField_ColumnProvider 21 | { 22 | public $column = '_OpenRowForEditing'; 23 | public $urlSegment = 'editableRow'; 24 | public $setWorkingParentOnRecordTo = 'Parent'; 25 | public $disableToggleStateSave = false; 26 | public $cacheToggleStateSave = false; 27 | public $openNewTogglesOnCreate = true; 28 | 29 | protected $permissionCallback; 30 | 31 | protected $itemEditFormCallback; 32 | 33 | protected $fields; 34 | 35 | protected $template; 36 | 37 | protected $validator; 38 | 39 | private $workingGrid; 40 | 41 | private static $allowed_actions = [ 42 | 'loadItem', 43 | 'handleForm', 44 | ]; 45 | 46 | /** 47 | * @param FieldList|callable|array $fields the fields to display in inline form 48 | */ 49 | public function __construct($fields = null) 50 | { 51 | $this->fields = $fields; 52 | parent::__construct(); 53 | } 54 | 55 | /** 56 | * Gets the fields for this class 57 | * 58 | * @return FieldList|callable|array 59 | */ 60 | public function getFields() 61 | { 62 | return $this->fields; 63 | } 64 | 65 | /** 66 | * Sets the fields that will be displayed in this component 67 | * 68 | * @param FieldList|callable|array $fields 69 | * @return static $this 70 | */ 71 | public function setFields($fields) 72 | { 73 | $this->fields = $fields; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Gets the validator 80 | * 81 | * @return Validator|callable|array 82 | */ 83 | public function getValidator() 84 | { 85 | return $this->validator; 86 | } 87 | 88 | /** 89 | * Sets the validator that will be displayed in this component 90 | * 91 | * @param \Validator|Callable|array $validator 92 | * @return static $this 93 | */ 94 | public function setValidator($validator) 95 | { 96 | $this->validator = $validator; 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * Get the permission callback for this column 103 | * 104 | * @return callable 105 | */ 106 | public function getPermissionCallback() 107 | { 108 | return $this->permissionCallback; 109 | } 110 | 111 | /** 112 | * Sets the permission callback for this column 113 | * 114 | * @param callable $permissionCallback 115 | * @return static $this 116 | */ 117 | public function setPermissionCallback($permissionCallback) 118 | { 119 | $this->permissionCallback = $permissionCallback; 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * Get the callback for changes on the edit form after constructing it 126 | * 127 | * @return callable 128 | */ 129 | public function getItemEditFormCallback() 130 | { 131 | return $this->itemEditFormCallback; 132 | } 133 | 134 | /** 135 | * Make changes on the edit form after constructing it. 136 | * 137 | * @param callable $itemEditFormCallback 138 | * @return static $this 139 | */ 140 | public function setItemEditFormCallback($itemEditFormCallback) 141 | { 142 | $this->itemEditFormCallback = $itemEditFormCallback; 143 | 144 | return $this; 145 | } 146 | 147 | public function getURLHandlers($gridField) 148 | { 149 | return [ 150 | $this->urlSegment . '/load/$ID' => 'loadItem', 151 | $this->urlSegment . '/form/$ID' => 'handleForm', 152 | ]; 153 | } 154 | 155 | /** 156 | * Modify the list of columns displayed in the table. 157 | * 158 | * @see {@link GridFieldDataColumns->getDisplayFields()} 159 | * @see {@link GridFieldDataColumns}. 160 | * 161 | * @param \GridField $gridField 162 | * @param array - List reference of all column names. 163 | */ 164 | public function augmentColumns($gridField, &$columns) 165 | { 166 | if (!in_array($this->column, $columns)) { 167 | array_unshift($columns, $this->column); 168 | } 169 | } 170 | 171 | /** 172 | * Names of all columns which are affected by this component. 173 | * 174 | * @param \GridField $gridField 175 | * 176 | * @return array 177 | */ 178 | public function getColumnsHandled($gridField) 179 | { 180 | return [$this->column]; 181 | } 182 | 183 | /** 184 | * HTML for the column, content of the element. 185 | * 186 | * @param \GridField $gridField 187 | * @param \DataObject $record - Record displayed in this row 188 | * @param string $columnName 189 | * 190 | * @return string - HTML for the column. Return NULL to skip. 191 | */ 192 | public function getColumnContent($gridField, $record, $columnName) 193 | { 194 | if (!$this->checkPermission($gridField, $record, $columnName)) { 195 | return ''; 196 | } 197 | 198 | $classes = 'ss-gridfield-editable-row--icon'; 199 | 200 | if ($record) { 201 | $classes .= ' ss-gridfield-editable-row--toggle'; 202 | } 203 | 204 | $openToggleId = 'EditableRowToggles.' . $gridField->ID() . '.' . get_class($record) . '_' . $record->ID; 205 | 206 | if ($this->openNewTogglesOnCreate && Session::get($openToggleId)) { 207 | $classes .= ' ss-gridfield-editable-row--toggle_start'; 208 | } 209 | 210 | Session::clear($openToggleId); 211 | 212 | return sprintf('', $classes); 213 | } 214 | 215 | /** 216 | * Attributes for the element containing the content returned by {@link getColumnContent()}. 217 | * 218 | * @param \GridField $gridField 219 | * @param \DataObject $record displayed in this row 220 | * @param string $columnName 221 | * 222 | * @return array 223 | */ 224 | public function getColumnAttributes($gridField, $record, $columnName) 225 | { 226 | $this->workingGrid = $gridField; 227 | 228 | if (!$this->checkPermission($gridField, $record, $columnName)) { 229 | return []; 230 | } 231 | 232 | return [ 233 | 'data-link' => $this->Link('load', $record->ID), 234 | 'class' => 'ss-gridfield-editable-row--icon-holder', 235 | ]; 236 | } 237 | 238 | /** 239 | * Additional metadata about the column which can be used by other components, 240 | * e.g. to set a title for a search column header. 241 | * 242 | * @param \GridField $gridField 243 | * @param string $columnName 244 | * 245 | * @return array - Map of arbitrary metadata identifiers to their values. 246 | */ 247 | public function getColumnMetadata($gridField, $columnName) 248 | { 249 | if ($columnName == $this->column) { 250 | return [ 251 | 'title' => '', 252 | ]; 253 | } 254 | } 255 | 256 | public function getHTMLFragments($grid) 257 | { 258 | //singleton('require')->css(SS_MWM_DIR . '/thirdparty/font-awesome/font-awesome.min.css'); 259 | $grid->addExtraClass('ss-gridfield-editable-rows'); 260 | //Utilities::include_requirements(); 261 | 262 | if ($this->disableToggleStateSave) { 263 | $grid->addExtraClass('ss-gridfield-editable-rows_disableToggleState'); 264 | } 265 | 266 | if ($this->cacheToggleStateSave) { 267 | $grid->addExtraClass('ss-gridfield-editable-rows_allowCachedToggles'); 268 | } 269 | 270 | $this->workingGrid = $grid; 271 | } 272 | 273 | public function handleSave(\GridField $grid, \DataObjectInterface $record) 274 | { 275 | $list = $grid->getList(); 276 | $value = $grid->Value(); 277 | $className = $this->getComponentName(); 278 | 279 | if (!isset($value[$className]) || !is_array($value[$className])) { 280 | return; 281 | } 282 | 283 | foreach ($value[$className] as $id => $fields) { 284 | if (!is_numeric($id) || !is_array($fields)) { 285 | continue; 286 | } 287 | 288 | $item = $list->byID($id); 289 | 290 | if (!$item || !$item->canEdit()) { 291 | continue; 292 | } 293 | 294 | $form = $this->getForm($grid, $item, false); 295 | $form->loadDataFrom($fields); 296 | $form->saveInto($item); 297 | $extra = method_exists($list, 'getExtraFields') ? array_intersect_key($form->Data, 298 | (array)$list->getExtraFields()) : []; 299 | 300 | // FIX: nonsaving boolean/checkboxfields (unchecked will not be posted) 301 | foreach ($form->Fields()->saveableFields() as $formfield) { 302 | // if boolean field is included in form and unset in values, unset on object 303 | if($formfield instanceof CheckboxField && !array_key_exists($formfield->name,$fields)){ 304 | $item->{$formfield->name} = 0; 305 | } 306 | } 307 | 308 | $item->write(); 309 | $list->add($item, $extra); 310 | } 311 | } 312 | 313 | public function getForm($grid, $record, $removeEditableColumnFields = true) 314 | { 315 | $this->workingGrid = $grid; 316 | $form = \Form::create($this, 317 | $grid->ID() . '-EditableRow-' . $record->ID, 318 | //\FieldList::create(), 319 | $this->getFieldList($record, $grid, $removeEditableColumnFields), 320 | \FieldList::create(), 321 | $this->getValidatorForForm($record, $grid) 322 | ) 323 | ->loadDataFrom($record) 324 | ->setFormAction($this->Link('form',$record->ID)) 325 | ->disableSecurityToken() 326 | ; 327 | 328 | if ($form->Fields()->hasTabSet() && ($root = $form->Fields()->findOrMakeTab('Root')) && $root->Template == 'CMSTabSet') { 329 | $root->setTemplate(''); 330 | $form->removeExtraClass('cms-tabset'); 331 | } 332 | 333 | $callback = $this->getItemEditFormCallback(); 334 | 335 | if ($callback) { 336 | call_user_func($callback, $form, $this, $grid, $record, $removeEditableColumnFields); 337 | } 338 | 339 | return $form; 340 | } 341 | 342 | protected function getFieldList($record, $grid = null, $removeEditableColumnFields = true) 343 | { 344 | $fields = null; 345 | if ($this->fields) { 346 | if ($this->fields instanceof \FieldList) { 347 | $fields = $this->fields; 348 | } elseif (is_callable($this->fields)) { 349 | $fields = call_user_func_array($this->fields, [$record, $grid, $this]); 350 | } else { 351 | $fields = \FieldList::create($this->fields); 352 | } 353 | } 354 | 355 | if (!$fields && $record->hasMethod('getEditableRowFields')) { 356 | $fields = $record->getEditableRowFields($grid); 357 | } 358 | 359 | if (!$fields && $grid) { 360 | if ($editable = $grid->getConfig()->getComponentByType('GridFieldDetailForm')) { 361 | if ($editable->getFields()) { 362 | $fields = $editable->getFields(); 363 | } else { 364 | $fields = \Object::create($editable->getItemRequestClass(), $grid, $editable, $record, 365 | $grid->getForm()->getController(), $editable->getName())->ItemEditForm()->Fields(); 366 | } 367 | } 368 | } 369 | 370 | if ($removeEditableColumnFields && $grid && $editable = $grid->getConfig()->getComponentByType('GridFieldEditableColumns')) { 371 | $editableColumns = $editable->getFields($grid, $record); 372 | 373 | foreach ($editableColumns as $column) { 374 | $fields->removeByName($column->Name); 375 | } 376 | } 377 | //print_r($fields->saveableFields()); 378 | return $fields; 379 | } 380 | 381 | protected function getValidatorForForm($record, $grid = null) 382 | { 383 | if ($this->validator) { 384 | if ($this->validator instanceof \Validator) { 385 | return $this->validator; 386 | } elseif (is_callable($this->validator)) { 387 | return call_user_func_array($this->validator, [$record, $grid, $this]); 388 | } else { 389 | return \Validator::create($this->validator); 390 | } 391 | } 392 | 393 | if ($grid) { 394 | if ($editable = $grid->getConfig()->getComponentByType('GridFieldDetailForm')) { 395 | return $editable->getValidator(); 396 | } 397 | } 398 | 399 | if ($record->hasMethod('getEditableRowValidator')) { 400 | return $record->getEditableRowValidator($grid); 401 | } 402 | 403 | return $record->hasMethod('getCMSValidator') ? $record->getCMSValidator() : null; 404 | } 405 | 406 | public function handleForm($grid, $request) 407 | { 408 | $id = $request->param('ID'); 409 | $record = $this->getRecordFromRequest($grid, $request); 410 | $form = $this->getForm($grid, $record); 411 | $class = $this->getComponentName(); 412 | 413 | foreach ($form->Fields()->dataFields() as $field) { 414 | $field->setName(sprintf( 415 | '%s[%s][%s][%s]', $grid->getName(), $class, $id, $field->getName() 416 | )); 417 | } 418 | 419 | $form->setController($grid->getForm()->getController()); 420 | 421 | // if(!$request->isGET() && $request->remaining() && ($newGrid = $form->handleRequest($request, \DataModel::inst())) && ($newGrid instanceof $grid) && ($row = $newGrid->getConfig()->getComponentByType(__CLASS__))) { 422 | // $form = $row->handleForm($newGrid, $request); 423 | // } 424 | 425 | return $form; 426 | } 427 | 428 | public function loadItem($grid, $request) 429 | { 430 | $record = $this->getRecordFromRequest($grid, $request); 431 | $form = $this->getForm($grid, $record); 432 | //$form = new \Form($this, 'something', $record->getCMSFields(), FieldList::create()); 433 | $this->renameFieldsInCompositeField($form->Fields(), $grid, $record); 434 | 435 | $canEdit = $record->canEdit(); 436 | $canView = $record->canView(); 437 | 438 | if (!$canEdit && !$canView) { 439 | throw new \LogicException('You do not have permission to view this record'); 440 | } 441 | 442 | if (!$canEdit) { 443 | $form->makeReadonly(); 444 | } 445 | 446 | $countUntilThisColumn = 0; 447 | foreach ($grid->getColumns() as $column) { 448 | $countUntilThisColumn++; 449 | 450 | if ($column == $this->column) { 451 | break; 452 | } 453 | } 454 | 455 | if ($countUntilThisColumn == count($grid->getColumns())) { 456 | $countUntilThisColumn = 0; 457 | } 458 | 459 | return $record->customise([ 460 | 'Form' => $form, 461 | 'ColumnCount' => count($grid->getColumns()), 462 | 'PrevColumnsCount' => $countUntilThisColumn, 463 | 'OtherColumnsCount' => count($grid->getColumns()) - $countUntilThisColumn, 464 | ])->renderWith(array_merge((array)$this->template, ['GridField_EditableBlockRow'])); 465 | } 466 | 467 | protected function getRecordFromRequest($grid, $request) 468 | { 469 | $id = $request->param('ID'); 470 | $list = $grid->getList(); 471 | 472 | if (!ctype_digit($id)) { 473 | throw new \SS_HTTPResponse_Exception(null, 400); 474 | } 475 | 476 | if (!$record = $list->byID($id)) { 477 | throw new \SS_HTTPResponse_Exception(null, 404); 478 | } 479 | 480 | if ($this->setWorkingParentOnRecordTo) { 481 | if ($grid->List && ($grid->List instanceof \ManyManyList) && $grid->Form && $grid->Form->Record) { 482 | $record->{$this->setWorkingParentOnRecordTo} = $grid->Form->Record; 483 | } 484 | } 485 | 486 | return $record; 487 | } 488 | 489 | public function Link($action = null, $id = null) 490 | { 491 | return $this->workingGrid ? \Controller::join_links($this->workingGrid->Link($this->urlSegment), $action, 492 | $id) : null; 493 | } 494 | 495 | protected function renameFieldsInCompositeField($fields, $grid, $record) 496 | { 497 | $class = $this->getComponentName(); 498 | 499 | foreach ($fields as $field) { 500 | $field->setName(sprintf( 501 | '%s[%s][%s][%s]', $grid->getName(), $class, $record->ID, $field->getName() 502 | )); 503 | 504 | if ($field->isComposite()) { 505 | $this->renameFieldsInCompositeField($field->FieldList(), $grid, $record); 506 | } 507 | } 508 | } 509 | 510 | private $canView = []; 511 | 512 | protected function checkPermission($gridField, $record, $columnName) 513 | { 514 | if (isset($this->canView[$record->ID])) { 515 | return $this->canView[$record->ID]; 516 | } 517 | 518 | $this->canView[$record->ID] = 519 | ($this->permissionCallback && call_user_func($this->permissionCallback, $gridField, $record, 520 | $columnName)) || 521 | (!$this->permissionCallback && ($record->canView() || $record->canEdit())); 522 | 523 | return $this->canView[$record->ID]; 524 | } 525 | 526 | protected function getComponentName() 527 | { 528 | return str_replace(['\\', '-'], '_', __CLASS__ . '_' . $this->urlSegment); 529 | } 530 | } 531 | --------------------------------------------------------------------------------