├── docs └── images │ ├── overview.png │ ├── detail_main.png │ ├── modeladmin.png │ ├── detail_media.png │ └── detail_template.png ├── templates ├── BlockTemplates │ ├── Blank.png │ ├── Block.png │ ├── MultiColumn.png │ ├── PictureList.png │ ├── Pic-Text-Wrap.png │ ├── Text-Pic-Wrap.png │ ├── Full width image.png │ ├── Pic2-Text2-Wrap.png │ ├── Text2-Pic2-Wrap.png │ ├── MultiColumn.ss │ ├── Block.ss │ ├── Full width image.ss │ ├── Pic2-Text2-Wrap.ss │ ├── Text-Pic-Wrap.ss │ ├── Pic-Text-Wrap.ss │ ├── Text2-Pic2-Wrap.ss │ └── PictureList.ss └── Layout │ └── Page.ss ├── _config.php ├── _config ├── routes.yml ├── content-blocks.yml └── betterbuttons.yml ├── code ├── modeladmin │ ├── _notes │ │ └── dwsync.xml │ └── BlockAdmin.php ├── controllers │ └── BlockController.php ├── dataobjects │ ├── _Employee.php │ ├── _FormBlock.php │ ├── MultiColumn.php │ └── Block.php └── ContentBlocksModule.php ├── .editorconfig ├── .gitignore ├── js └── main.js ├── composer.json ├── css ├── ContentBlocksModule.css └── block.css └── README.md /docs/images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasbnielsen/Silverstripe-Content-Blocks/HEAD/docs/images/overview.png -------------------------------------------------------------------------------- /docs/images/detail_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasbnielsen/Silverstripe-Content-Blocks/HEAD/docs/images/detail_main.png -------------------------------------------------------------------------------- /docs/images/modeladmin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasbnielsen/Silverstripe-Content-Blocks/HEAD/docs/images/modeladmin.png -------------------------------------------------------------------------------- /docs/images/detail_media.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasbnielsen/Silverstripe-Content-Blocks/HEAD/docs/images/detail_media.png -------------------------------------------------------------------------------- /docs/images/detail_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasbnielsen/Silverstripe-Content-Blocks/HEAD/docs/images/detail_template.png -------------------------------------------------------------------------------- /templates/BlockTemplates/Blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasbnielsen/Silverstripe-Content-Blocks/HEAD/templates/BlockTemplates/Blank.png -------------------------------------------------------------------------------- /templates/BlockTemplates/Block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasbnielsen/Silverstripe-Content-Blocks/HEAD/templates/BlockTemplates/Block.png -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/modeladmin/BlockAdmin.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

$Title

5 |
$Content
6 |
<% loop $ActiveBlocks %>$Me<% end_loop %> 7 |
8 |
9 | $Form 10 | $PageComments 11 |
-------------------------------------------------------------------------------- /templates/BlockTemplates/MultiColumn.ss: -------------------------------------------------------------------------------- 1 | 2 |
3 | <% if $Header != "None" %> 4 |
5 | <{$Header}>$Name 6 |
7 | <% end_if %> 8 | 9 | <% loop $Blocks.Sort('SortOrder') %> 10 | 11 |
12 | $Me 13 |
14 | 15 | <% end_loop %> 16 | 17 |
18 | -------------------------------------------------------------------------------- /_config/content-blocks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ContentBlocksModule 3 | --- 4 | Page: 5 | extensions: 6 | - ContentBlocksModule 7 | LeftAndMain: 8 | extra_requirements_css: 9 | - content-blocks/css/ContentBlocksModule.css 10 | extra_requirements_javascript: 11 | - content-blocks/js/main.js 12 | GridFieldAddNewMultiClass: 13 | showEmptyString: false 14 | ContentBlocksModule: 15 | copy_css_to_theme: true 16 | -------------------------------------------------------------------------------- /code/controllers/BlockController.php: -------------------------------------------------------------------------------- 1 | "; 13 | echo print_r($request); 14 | echo "
"; 15 | echo print_r($_POST); 16 | echo ""; 17 | echo $_SERVER['HTTP_REFERER']; 18 | 19 | //$this->redirect($_SERVER['HTTP_REFERER'] . "?status=success"); 20 | 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.yml,package.json}] 14 | indent_size = 2 15 | 16 | # The indent size used in the package.json file cannot be changed: 17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 18 | -------------------------------------------------------------------------------- /templates/BlockTemplates/Block.ss: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <% if $Header != "None" %><{$Header}>$Name<% end_if %> 5 |
6 | $Content 7 |
8 |
9 | <% if Images %> 10 | <% loop Images.Sort('SortOrder') %> 11 | 12 | $Me.SetWidth(1000) 13 | 14 | <% end_loop %> 15 | <% end_if %> 16 |
17 | 18 |
-------------------------------------------------------------------------------- /templates/BlockTemplates/Full width image.ss: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% if $Header != "None" %><{$Header}>$Name<% end_if %> 4 | 5 | <% if Images %> 6 | <% loop Images.Sort('SortOrder') %> 7 | 8 | $Me.SetWidth(1000) 9 | 10 | <% end_loop %> 11 | <% end_if %> 12 |
13 |
14 | $Content 15 |
16 |
17 |
18 |
-------------------------------------------------------------------------------- /templates/BlockTemplates/Pic2-Text2-Wrap.ss: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% if Images %> 4 | <% loop Images.Sort('SortOrder') %> 5 | 6 | $Top.FormattedBlockImage($ID, 600, 600) 7 | 8 | <% end_loop %> 9 | <% end_if %> 10 |
11 | <% if $Header != "None" %><{$Header}>$Name<% end_if %> 12 |
13 | $Content 14 |
15 |
16 |
17 |
-------------------------------------------------------------------------------- /templates/BlockTemplates/Text-Pic-Wrap.ss: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | <% if Images %> 5 | <% loop Images.Sort('SortOrder') %> 6 | 7 | $Top.FormattedBlockImage($ID, 600, 600) 8 | 9 | <% end_loop %> 10 | <% end_if %> 11 |
12 | <% if $Header != "None" %><{$Header}>$Name<% end_if %> 13 |
14 | $Content 15 |
16 |
17 |
18 |
-------------------------------------------------------------------------------- /templates/BlockTemplates/Pic-Text-Wrap.ss: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | <% if Images %> 5 | <% loop Images.Sort('SortOrder') %> 6 | 7 | $Top.FormattedBlockImage($ID, 600, 600) 8 | 9 | <% end_loop %> 10 | <% end_if %> 11 |
12 | <% if $Header != "None" %><{$Header}>$Name<% end_if %> 13 |
14 | $Content 15 |
16 |
17 | 18 |
19 |
-------------------------------------------------------------------------------- /templates/BlockTemplates/Text2-Pic2-Wrap.ss: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | <% if $Images %> 5 | <% loop $Images.Sort('SortOrder') %> 6 | 7 | $Top.FormattedBlockImage($ID, 600, 600) 8 | 9 | <% end_loop %> 10 | <% end_if %> 11 |
12 | <% if $Header != "None" %><{$Header}>$Name<% end_if %> 13 |
14 | $Content 15 |
16 |
17 |
18 |
-------------------------------------------------------------------------------- /templates/BlockTemplates/PictureList.ss: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | <% if $Header != "None" %><{$Header}>$Name<% end_if %> 6 |
7 | $Content 8 |
9 |
10 | 21 |
22 |
23 | 24 | -------------------------------------------------------------------------------- /_config/betterbuttons.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: content-blocks-betterbuttons 3 | After: 4 | - 'betterbuttons/*' 5 | Only: 6 | classexists: BetterButton 7 | --- 8 | BetterButtonsUtils: 9 | create: 10 | BetterButton_New: false 11 | 12 | BetterButtonsActions: 13 | create: 14 | BetterButton_Save: true 15 | Group_SaveAnd: true 16 | BetterButton_SaveAndClose: false 17 | BetterButton_SaveAndNext: false 18 | BetterButtonFrontendLinksAction: true 19 | BetterButton_Delete: false 20 | #BetterButtonCancelAction: true #Does not work with add multi class 21 | 22 | edit: 23 | BetterButton_Save: true 24 | Group_SaveAnd: true 25 | BetterButton_SaveAndClose: false 26 | BetterButton_SaveAndNext: false 27 | BetterButton_Delete: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | Icon? 37 | ehthumbs.db 38 | Thumbs.db 39 | # Ignore scattered _notes directories that Dreamweaver uses to keep track of its edits and concurrency 40 | */_notes 41 | 42 | *.mno 43 | /.idea/ -------------------------------------------------------------------------------- /code/dataobjects/_Employee.php: -------------------------------------------------------------------------------- 1 | 'Varchar', 9 | 'Phone' => 'Varchar', 10 | 'Email' => 'Varchar' 11 | ); 12 | 13 | private static $has_one = array( 14 | ); 15 | 16 | private static $many_many = array( 17 | ); 18 | 19 | public function getCMSFields() { 20 | 21 | $fields = parent::getCMSFields(); 22 | $fields->addFieldsToTab("Root.Employee", new TextField('Name', 'Name')); 23 | $fields->addFieldsToTab("Root.Employee", new TextField('Phone', 'Phone')); 24 | $fields->addFieldsToTab("Root.Employee", new TextField('Email', 'Email')); 25 | 26 | return $fields; 27 | } 28 | } -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | // run the function for doc ready 4 | $(document).ready(function(){ 5 | CurrentTemplateBlue(); 6 | }); 7 | // run the function after ajax request (for example after pressing the save button) 8 | $(document).ajaxComplete(function(){ 9 | CurrentTemplateBlue(); 10 | }); 11 | 12 | 13 | function CurrentTemplateBlue(){ 14 | // make the current template blue when page is loaded 15 | $('#Root #Template ul li input:radio[name="Template"]:checked').parent().addClass('selectedBlock'); 16 | 17 | // When template is selected, background blue 18 | $('#Root #Template ul li input:radio[name="Template"]').change(function(){ 19 | if ($(this).is(':checked')) { 20 | // reset all 21 | $('#Root #Template ul li').removeClass('selectedBlock'); 22 | // apply background 23 | $(this).addClass('checked').parent().addClass('selectedBlock'); 24 | } 25 | }); 26 | } 27 | 28 | 29 | })(jQuery); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nobrainerweb/silverstripe-content-blocks", 3 | "description": "Split your page content into manageable blocks/sections of content, each with their own template", 4 | "type": "silverstripe-module", 5 | "homepage": "https://github.com/NobrainerWeb/Silverstripe-Content-Blocks", 6 | "keywords": ["silverstripe", "section", "sections", "blocks", "content"], 7 | "license": "BSD-3-Clause", 8 | "authors": [ 9 | { 10 | "name": "Thomas B. Nielsen - Nobrainer Web", 11 | "email": "nobrainerweb@gmail.com", 12 | "homepage": "http://www.nobrainer.dk" 13 | } 14 | ], 15 | "support": { 16 | "issues": "https://github.com/NobrainerWeb/Silverstripe-Content-Blocks/issues" 17 | }, 18 | "require": { 19 | "silverstripe/framework": "3.*", 20 | "silverstripe/cms": "3.*", 21 | "bummzack/sortablefile": "~1.2" 22 | }, 23 | "suggest": { 24 | "unisolutions/silverstripe-copybutton": "Adds the option to duplicate dataobjects and their relations", 25 | "jonom/focuspoint": "Crop an image based on a focus point", 26 | "silverstripe-australia/gridfieldextensions": "Sort content blocks" 27 | }, 28 | "extra": { 29 | "installer-name": "content-blocks" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /code/dataobjects/_FormBlock.php: -------------------------------------------------------------------------------- 1 | 'Varchar', 9 | //'Phone' => 'Varchar', 10 | //'Email' => 'Varchar' 11 | ); 12 | 13 | static $has_one = array( 14 | ); 15 | 16 | static $many_many = array( 17 | ); 18 | 19 | public function getCMSFields() { 20 | 21 | $fields = parent::getCMSFields(); 22 | //$fields->addFieldsToTab("Root.Employee", new TextField('Name', 'Name')); 23 | //$fields->addFieldsToTab("Root.Employee", new TextField('Phone', 'Phone')); 24 | //$fields->addFieldsToTab("Root.Employee", new TextField('Email', 'Email')); 25 | 26 | return $fields; 27 | } 28 | 29 | public function Form() { 30 | 31 | $fields = new FieldList( 32 | new TextField('Name'), 33 | new EmailField('Email'), 34 | new TextareaField('Message') 35 | ); 36 | 37 | $actions = new FieldList( 38 | new FormAction('submit', 'Submit') 39 | ); 40 | 41 | $form = new Form($this, 'form', $fields, $actions); 42 | $form->setFormAction('/blocks/form'); 43 | $form->setFormMethod('POST'); 44 | 45 | return $form; 46 | } 47 | 48 | /* public function Link () { 49 | return "/blocks/"; 50 | } 51 | */ 52 | public function status() { 53 | return isset($_REQUEST['status']) ? $_REQUEST['status'] : ""; 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /code/dataobjects/MultiColumn.php: -------------------------------------------------------------------------------- 1 | 'Block' 13 | ); 14 | 15 | private static $many_many_extraFields=array( 16 | 'Blocks'=>array('SortOrder'=>'Int') 17 | ); 18 | 19 | private static $defaults = array( 20 | 'Template' => 'MultiColumn', 21 | ); 22 | 23 | /* Clean the relation table when deleting a Block */ 24 | public function onBeforeDelete() { 25 | parent::onBeforeDelete(); 26 | $this->Blocks()->removeAll(); 27 | } 28 | 29 | public function getCMSFields() { 30 | 31 | $fields = parent::getCMSFields(); 32 | 33 | $fields->removeByName(array('PageID','SortOrder', 'Active', 'Title', 'Content', 'Blocks', 'YoutubeVideoID', 'Images', 'Media', 'Files', 'Videos')); 34 | 35 | 36 | if ($this->ID) { 37 | $BlockConfig = GridFieldConfig_RelationEditor::create(20); 38 | $BlockConfig->addComponent(new GridFieldOrderableRows('SortOrder')); 39 | 40 | $BlockGF = new GridField('Blocks', 'Blocks', $this->Blocks(), $BlockConfig); 41 | 42 | $classes = array_values(ClassInfo::subclassesFor($BlockGF->getModelClass())); 43 | 44 | if (count($classes) > 1 && class_exists('GridFieldAddNewMultiClass')) { 45 | $BlockConfig->removeComponentsByType('GridFieldAddNewButton'); 46 | $BlockConfig->addComponent(new GridFieldAddNewMultiClass()); 47 | } 48 | 49 | $fields->addFieldToTab("Root.Main", $BlockGF); 50 | } 51 | $this->extend('updateCMSFields', $fields); 52 | return $fields; 53 | } 54 | } -------------------------------------------------------------------------------- /css/ContentBlocksModule.css: -------------------------------------------------------------------------------- 1 | #Template ul { } 2 | #Template ul li { 3 | position: relative; 4 | float: left; 5 | width: auto; 6 | height: auto; 7 | padding: 25px 10px 10px 10px; 8 | margin: 5px; 9 | overflow: hidden; 10 | border: 2px groove rgba(255, 255, 255, 0.8); 11 | -webkit-border-image: url(../images/textures/bg_fieldset_elements_border.png) 2 stretch stretch; 12 | border-image: url(../images/textures/bg_fieldset_elements_border.png) 2 stretch stretch; 13 | -moz-border-radius: 5px; 14 | -webkit-border-radius: 5px; 15 | border-radius: 5px; 16 | transition: all 500ms; 17 | -webkit-transition: all 500ms; 18 | } 19 | 20 | #Template ul.optionset li { 21 | width: 110px; 22 | height: 90px; 23 | } 24 | 25 | #Template ul li img { 26 | width: 70px; 27 | margin: auto; 28 | } 29 | 30 | #Template ul li:hover, #Template ul li input.changed { 31 | background-color: blue; 32 | background-color: rgba(85, 164, 210, 0.3); 33 | border: 2px groove rgba(85, 164, 210, 0.3); 34 | } 35 | 36 | #Template ul li:hover > label div { 37 | filter:Alpha(opacity=20); 38 | opacity: 0.2; 39 | } 40 | 41 | #Template ul li:hover > label .title { 42 | display: block; 43 | } 44 | 45 | #Template ul .selectedBlock { 46 | background-color: blue; 47 | background-color: rgba(85, 164, 210, 0.3); 48 | border: 2px groove rgba(85, 164, 210, 0.3); 49 | } 50 | 51 | #Template ul li input { 52 | position: absolute; 53 | visibility: hidden; 54 | } 55 | 56 | #Template ul li label { 57 | width: 100%; 58 | height: 100%; 59 | padding: 0; 60 | margin: 0; 61 | } 62 | 63 | 64 | #Template ul li .blockThumbnail { 65 | width: 70px; 66 | margin: 0 auto; 67 | border-radius: 4px; 68 | box-shadow: 0 0 1px rgba(0,0,0,0.3); 69 | background: white; 70 | } 71 | 72 | #Template ul li .blockThumbnail img { 73 | border-radius: 4px; 74 | } 75 | 76 | #Template ul li label .title { 77 | display: none; 78 | position: relative; 79 | top: -50px; 80 | left: 0; 81 | width: 100%; 82 | margin: 0; 83 | padding: 10px 0 0 0; 84 | font-weight: bold; 85 | font-size: 12px; 86 | line-height: 14px; 87 | color: white; 88 | text-align: center; 89 | text-shadow: 1px 1px 10px #333; 90 | word-break:break-word; 91 | } 92 | #Template ul li .description { 93 | display: block; 94 | margin: 0; 95 | font-style: italic; 96 | } 97 | 98 | #Template, #Template li label:after { 99 | content: ""; 100 | display: table; 101 | clear: both; 102 | } 103 | 104 | .cms table.ss-gridfield-table tbody td.col-buttons { 105 | text-align: left; 106 | } -------------------------------------------------------------------------------- /css/block.css: -------------------------------------------------------------------------------- 1 | /* Styling for simple Theme */ 2 | /* Just remove the line Requirements::themedCSS('block'); from ContentBlocksModule.php if you are using your own, or delete what you dont need here */ 3 | 4 | /* Zurb Foundation minimum for grid */ 5 | img{display:block;max-width:100%}*, *:before, *:after {-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;}.row{width:100%;margin:0 auto;max-width:62.5rem}.row:after,.row:before{content:" ";display:table}.row:after{clear:both}.row.collapse>.column,.row.collapse>.columns{padding-left:0;padding-right:0}.row.collapse .row{margin-left:0;margin-right:0}.row .row{width:auto;margin:0 -.9375rem;max-width:none}.row .row:after,.row .row:before{content:" ";display:table}.row .row:after{clear:both}.row .row.collapse{width:auto;margin:0;max-width:none}.row .row.collapse:after,.row .row.collapse:before{content:" ";display:table}.row .row.collapse:after{clear:both}.column,.columns{padding-left:.9375rem;padding-right:.9375rem;width:100%;float:left}@media only screen{.column,.columns{position:relative;padding-left:.9375rem;padding-right:.9375rem;float:left}.small-1{width:8.33333%}.small-2{width:16.66667%}.small-3{width:25%}.small-4{width:33.33333%}.small-5{width:41.66667%}.small-6{width:50%}.small-7{width:58.33333%}.small-8{width:66.66667%}.small-9{width:75%}.small-10{width:83.33333%}.small-11{width:91.66667%}.small-12{width:100%}[class*=column]+[class*=column]:last-child{float:right}[class*=column]+[class*=column].end{float:left}}@media only screen and (min-width:40.063em){.column,.columns{position:relative;padding-left:.9375rem;padding-right:.9375rem;float:left}.medium-1{width:8.33333%}.medium-2{width:16.66667%}.medium-3{width:25%}.medium-4{width:33.33333%}.medium-5{width:41.66667%}.medium-6{width:50%}.medium-7{width:58.33333%}.medium-8{width:66.66667%}.medium-9{width:75%}.medium-10{width:83.33333%}.medium-11{width:91.66667%}.medium-12{width:100%}[class*=column]+[class*=column]:last-child{float:right}[class*=column]+[class*=column].end{float:left}}@media only screen and (min-width:64.063em){.column,.columns{position:relative;padding-left:.9375rem;padding-right:.9375rem;float:left}.large-1{width:8.33333%}.large-2{width:16.66667%}.large-3{width:25%}.large-4{width:33.33333%}.large-5{width:41.66667%}.large-6{width:50%}.large-7{width:58.33333%}.large-8{width:66.66667%}.large-9{width:75%}.large-10{width:83.33333%}.large-11{width:91.66667%}.large-12{width:100%}[class*=column]+[class*=column]:last-child{float:right}[class*=column]+[class*=column].end{float:left}} 6 | 7 | /**/ 8 | .row { margin-bottom: 0.9375rem; } 9 | 10 | /* CSS for wrapping text around images */ 11 | .block-wrap { display: block; } 12 | .block-wrap.block-left { float: left; padding-left: 0; } 13 | .block-wrap.block-right { float: right; padding-right: 0; } 14 | 15 | @media only screen and (max-width: 40em) { 16 | .block-wrap.block-left { 17 | float: none; 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | .block-wrap.block-right { 23 | float: none; 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | } /* max-width 640px, mobile-only styles, use when QAing mobile issues */ 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Silverstripe-Content-Blocks (previous: Silverstripe-Section-Module) 2 | =========================== 3 | 4 | Split your page content into manageable sections/blocks of content, each with their own template. 5 | 6 | ## Create compelling and unique pages ## 7 | This module gives you the option to create your content, in little blocks, instead of just one big content area. 8 | 9 | When the module is installed, a "Blocks" tab will be added to all pages. The blocks tab holds a GridField, that allows you to create as many blocks of content as you would like. 10 | Each block of content can have it's own template assigned. The module commes with a set of standard templates. 11 | 12 | You can easily create your own block templates and even your own block DataObjects with unique fields. This makes it very easy for content editors to create pages with lots of variation, without having to know HTML, fiddle around with tables and so on. 13 | Create your own block templates and/or extend the Block DataObject to create: 14 | - Image lists (simple gallery) 15 | - Employee listings 16 | - Product listings 17 | - and much more 18 | 19 | ### Version compatibility ### 20 | Tested on Silverstripe 3.1.2 21 | 22 | ### Installation instructions ### 23 | 24 | - Put this module under the root folder of site, named content-blocks. 25 | - Add the following code to your themes/your_design/templates/Layout/Page.ss where you want the content blocks to be rendered: 26 | ``` 27 |
<% loop ActiveBlocks %>$Me<% end_loop %>
28 | ``` 29 | 30 | Or you can ask for a single Block to render via it's ID (replace 5 with your ID): 31 | ``` 32 | $OneBlock(5) 33 | ``` 34 | 35 | - install the following dependent module(s) 36 | - GridField Extensions 37 | https://github.com/ajshort/silverstripe-gridfieldextensions/ 38 | 39 | - Better buttons for GridField by unclecheese 40 | https://github.com/unclecheese/silverstripe-gridfield-betterbuttons 41 | 42 | Or use Composer: 43 | ``` 44 | "nobrainerweb/silverstripe-content-blocks": "dev-master" 45 | ``` 46 | 47 | - run sitename.com/dev/build?flush=all 48 | 49 | - The module will copy content-blocks/templates/BlockTemplates to themes/your_design/templates/BlockTemplates, should this fail, please copy the files manually. 50 | - The module will copy content-blocks/css/sections.css to themes/your_design/block.css, should this fail, please copy the file manually. 51 | 52 | ### Usage and customization: ### 53 | - add your own templates to themes/your_design/templates/BLcokTemplates, they need to have the extension .ss and delete any unwanted templates (there is full example set of fixed width and fluid width templates included in the module) 54 | - allways run dev/build?flush=1 after adding templates 55 | - remember to ?flush=1 after modification of templates 56 | 57 | ### Screenshots 58 | 59 | ![Overview](docs/images/overview.png) 60 | Overview in page editing 61 | 62 | ![modeladmin](docs/images/modeladmin.png) 63 | View all blocks across pages in a ModelAdmin 64 | 65 | ![](docs/images/detail_main.png) 66 | ![](docs/images/detail_media.png) 67 | ![](docs/images/detail_template.png) 68 | Detail views 69 | 70 | ### TODO: ### 71 | - Option to add more content placeholders without coding - site config? 72 | - Handle search 73 | - Versioning 74 | 75 | ### IDEAS ### 76 | - Save available templates in database (enum field) - create on dev/build or use template manifest 77 | - Build in template generator 78 | - Form blocks 79 | - Better previews (C/P from design) 80 | - Perhaps add foundation templates as a suggested composer requirement (and other CSS frameworks) 81 | -------------------------------------------------------------------------------- /code/ContentBlocksModule.php: -------------------------------------------------------------------------------- 1 | 'Block' 13 | ); 14 | 15 | private static $many_many_extraFields = array( 16 | 'Blocks' => array( 17 | 'SortOrder'=>'Int' 18 | ) 19 | ); 20 | 21 | public function updateCMSFields(FieldList $fields) { 22 | 23 | // Relation handler for Blocks 24 | $SConfig = GridFieldConfig_RelationEditor::create(25); 25 | if (class_exists('GridFieldOrderableRows')) { 26 | $SConfig->addComponent(new GridFieldOrderableRows('SortOrder')); 27 | } 28 | $SConfig->addComponent(new GridFieldDeleteAction()); 29 | 30 | // If the copy button module is installed, add copy as option 31 | if (class_exists('GridFieldCopyButton')) { 32 | $SConfig->addComponent(new GridFieldCopyButton(), 'GridFieldDeleteAction'); 33 | } 34 | 35 | $gridField = new GridField("Blocks", "Content blocks", $this->owner->Blocks(), $SConfig); 36 | 37 | $classes = array_values(ClassInfo::subclassesFor($gridField->getModelClass())); 38 | 39 | if (count($classes) > 1 && class_exists('GridFieldAddNewMultiClass')) { 40 | $SConfig->removeComponentsByType('GridFieldAddNewButton'); 41 | $SConfig->addComponent(new GridFieldAddNewMultiClass()); 42 | } 43 | 44 | if (self::$create_block_tab) { 45 | $fields->addFieldToTab("Root.Blocks", $gridField); 46 | } else { 47 | // Downsize the content field 48 | $fields->removeByName('Content'); 49 | $fields->addFieldToTab('Root.Main', HTMLEditorField::create('Content')->setRows(self::$contentarea_rows), 'Metadata'); 50 | 51 | $fields->addFieldToTab("Root.Main", $gridField, 'Metadata'); 52 | } 53 | 54 | return $fields; 55 | } 56 | 57 | public function ActiveBlocks() { 58 | return $this->owner->Blocks()->filter(array('Active' => '1'))->sort('SortOrder'); 59 | } 60 | 61 | public function OneBlock($id) { 62 | return Block::get()->byID($id); 63 | } 64 | 65 | // Run on dev buld 66 | function requireDefaultRecords() { 67 | parent::requireDefaultRecords(); 68 | 69 | if (!Config::inst()->get('ContentBlocksModule', 'copy_css_to_theme')) { 70 | return; 71 | } 72 | 73 | // If css file does not exist on current theme, copy from module 74 | $copyfrom = BASE_PATH . "/".CONTENTBLOCKS_MODULE_DIR."/css/block.css"; 75 | $theme = SSViewer::current_theme(); 76 | $copyto = BASE_PATH . "/themes/".$theme."/css/block.css"; 77 | 78 | if(!file_exists($copyto)) { 79 | if(file_exists($copyfrom)) { 80 | copy($copyfrom,$copyto); 81 | echo '
  • block.css copied to: ' . $copyto . '
  • '; 82 | } else { 83 | echo '
  • The default css file was not found: ' . $copyfrom . '
  • '; 84 | } 85 | } 86 | } 87 | 88 | public function contentcontrollerInit($controller) { 89 | if($this->owner->Blocks()->exists()){ 90 | Requirements::themedCSS('block'); 91 | } 92 | } 93 | 94 | /** 95 | * Simple support for Translatable, when a page is translated, copy all content blocks and relate to translated page 96 | * TODO: This is not working as intended, for some reason an image is added to the duplicated block 97 | * All blocks are added to translated page - or something else ... 98 | */ 99 | public function onTranslatableCreate() { 100 | 101 | $translatedPage = $this->owner; 102 | // Getting the parent translation 103 | //$originalPage = $translatedPage->getTranslation('en_US'); 104 | $originalPage = $this->owner->getTranslation($this->owner->default_locale()); 105 | foreach($originalPage->Blocks() as $originalBlock) { 106 | $block = $originalBlock->duplicate(true); 107 | $translatedPage->Blocks()->add($block); 108 | } 109 | 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /code/dataobjects/Block.php: -------------------------------------------------------------------------------- 1 | 'Varchar(255)', 14 | 'Header' => "Enum('None, h1, h2, h3, h4, h5, h6')", 15 | 'Content' => 'HTMLText', 16 | 'Link' => 'Varchar', 17 | 'VideoURL' => 'Varchar', 18 | 'Template' => 'Varchar', 19 | 'Active' => 'Boolean(1)', 20 | 'ImageCropMethod' => 'Enum("CroppedFocusedImage, Fit, FitMax, Fill, FillMax, ScaleWidth, ScaleMaxWidth, ScaleHeight, ScaleMaxHeight, Pad", "CroppedFocusedImage")', 21 | 'ContentAsColumns' => 'Boolean(0)', 22 | 'ExtraCssClasses' => 'Varchar', 23 | 24 | "RedirectionType" => "Enum('Internal,External','Internal')", 25 | "ExternalURL" => "Varchar(2083)" // 2083 is the maximum length of a URL in Internet Explorer. 26 | ); 27 | 28 | private static $many_many = array( 29 | 'Images' => 'Image', 30 | 'Files' => 'File' 31 | ); 32 | 33 | /** 34 | * List of one-to-one relationships. {@link DataObject::$has_one} 35 | * 36 | * @var array 37 | */ 38 | private static $has_one = array( 39 | "LinkTo" => "SiteTree" 40 | ); 41 | 42 | private static $many_many_extraFields = array( 43 | 'Images' => array('SortOrder' => 'Int'), 44 | 'Files' => array('SortOrder' => 'Int') 45 | ); 46 | 47 | private static $belongs_many_many = array( 48 | 'Pages' => 'Page' 49 | ); 50 | 51 | private static $defaults = array( 52 | 'Active' => 1, 53 | 'Page_Blocks[SortOrder]' => 999, // TODO: Fix sorting, new blocks should be added to the bottom of the list/gridfield 54 | "RedirectionType" => "Internal" 55 | ); 56 | 57 | private static $casting = array( 58 | 'createStringAsHTML' => 'HTMLText' 59 | ); 60 | 61 | public function createStringAsHTML($html) 62 | { 63 | $casted = HTMLText::create(); 64 | $casted->setValue($html); 65 | 66 | return $casted; 67 | } 68 | 69 | public function populateDefaults() 70 | { 71 | $this->Template = $this->class; 72 | 73 | parent::populateDefaults(); 74 | } 75 | 76 | public function canView($member = null) 77 | { 78 | return Permission::check('ADMIN') || Permission::check('CMS_ACCESS_BlockAdmin') || Permission::check('CMS_ACCESS_LeftAndMain'); 79 | } 80 | 81 | public function canEdit($member = null) 82 | { 83 | return Permission::check('ADMIN') || Permission::check('CMS_ACCESS_BlockAdmin') || Permission::check('CMS_ACCESS_LeftAndMain'); 84 | } 85 | 86 | public function canCreate($member = null) 87 | { 88 | return Permission::check('ADMIN') || Permission::check('CMS_ACCESS_BlockAdmin') || Permission::check('CMS_ACCESS_LeftAndMain'); 89 | } 90 | 91 | public function canPublish($member = null) 92 | { 93 | return Permission::check('ADMIN') || Permission::check('CMS_ACCESS_BlockAdmin') || Permission::check('CMS_ACCESS_LeftAndMain'); 94 | } 95 | 96 | private static $summary_fields = array( 97 | 'ID' => 'ID', 98 | 'Thumbnail' => 'Thumbnail', 99 | 'Name' => 'Name', 100 | 'Template' => 'Template', 101 | 'ClassName' => 'Type', 102 | 'getIsActive' => 'Active' 103 | ); 104 | 105 | private static $searchable_fields = array( 106 | 'ID' => 'PartialMatchFilter', 107 | 'Name' => 'PartialMatchFilter', 108 | 'Header' => 'PartialMatchFilter', 109 | 'Active' 110 | ); 111 | 112 | public function validate() 113 | { 114 | $result = parent::validate(); 115 | if ($this->Name == '') { 116 | $result->error('A block must have a name'); 117 | } 118 | 119 | return $result; 120 | } 121 | 122 | public function getIsActive() 123 | { 124 | return $this->Active ? 'Yes' : 'No'; 125 | } 126 | 127 | public function getCMSFields() 128 | { 129 | $fields = parent::getCMSFields(); 130 | 131 | $fields->removeByName('SortOrder'); 132 | $fields->removeByName('Pages'); 133 | $fields->removeByName('Active'); 134 | $fields->removeByName('Header'); 135 | $fields->removeByName('Images'); 136 | $fields->removeByName('Files'); 137 | 138 | // Media tab 139 | $fields->addFieldToTab('Root', new TabSet('Media')); 140 | 141 | // If this Block belongs to more than one page, show a warning 142 | // TODO: This is not working when a block is added under another block 143 | $pcount = $this->Pages()->Count(); 144 | if ($pcount > 1) { 145 | $globalwarningfield = new LiteralField("IsGlobalBlockWarning", '

    This block is in use on ' . $pcount . ' pages - any changes made will also affect the block on these pages

    '); 146 | $fields->addFieldToTab("Root.Main", $globalwarningfield, 'Name'); 147 | $fields->addFieldToTab("Root.Media.Images", $globalwarningfield); 148 | $fields->addFieldToTab("Root.Media.Files", $globalwarningfield); 149 | $fields->addFieldToTab("Root.Media.Video", $globalwarningfield); 150 | $fields->addFieldToTab("Root.Template", $globalwarningfield); 151 | $fields->addFieldToTab("Root.Settings", $globalwarningfield); 152 | } 153 | 154 | $fields->addFieldToTab("Root.Main", new TextField('Name', 'Name')); 155 | $fields->addFieldToTab("Root.Main", new DropdownField('Header', 'Use name as header', $this->dbObject('Header')->enumValues()), 'Content'); 156 | $fields->addFieldToTab("Root.Main", new HTMLEditorField('Content', 'Content')); 157 | $fields->addFieldToTab('Root.Main', CheckboxField::create('ContentAsColumns')); 158 | 159 | $imgField = new SortableUploadField('Images', 'Images'); 160 | $imgField->allowedExtensions = array('jpg', 'gif', 'png'); 161 | 162 | $croppingmethodfield = DropdownField::create('ImageCropMethod', 'Image cropping method', singleton('Block')->dbObject('ImageCropMethod')->enumValues())->setDescription('Does not work on all blocks'); 163 | 164 | $fields->addFieldToTab('Root.Media.Images', $imgField); 165 | $fields->addFieldToTab('Root.Media.Images', $croppingmethodfield); 166 | 167 | $fileField = new SortableUploadField('Files', 'Files'); 168 | 169 | $fields->addFieldToTab('Root.Media.Files', $fileField); 170 | $fields->addFieldToTab('Root.Media.Video', new TextField('VideoURL', 'Video URL')); 171 | 172 | // Template tab 173 | $optionset = array(); 174 | $theme = Config::inst()->get('SSViewer', 'theme'); 175 | $src = BASE_PATH . "/themes/" . $theme . "/templates/" . CONTENTBLOCKS_TEMPLATE_DIR . '/'; 176 | $theme_imgsrc = "/themes/" . $theme . "/templates/" . CONTENTBLOCKS_TEMPLATE_DIR . '/'; 177 | $module_imgsrc = '/' . CONTENTBLOCKS_MODULE_DIR . '/templates/' . CONTENTBLOCKS_TEMPLATE_DIR . '/'; 178 | if (!file_exists($src)) { 179 | $src = BASE_PATH . '/' . CONTENTBLOCKS_MODULE_DIR . '/templates/' . CONTENTBLOCKS_TEMPLATE_DIR . '/'; 180 | } 181 | 182 | if (file_exists($src)) { 183 | foreach (glob($src . "*.ss") as $filename) { 184 | $name = $this->file_ext_strip(basename($filename)); 185 | // Is there a template thumbnail, check first in theme, then in module 186 | $img_final_path = $module_imgsrc . $name; 187 | // appearently file_exists requires BASE_PATH infront of it........ 188 | if (file_exists(BASE_PATH . $theme_imgsrc . $name . '.png')) { 189 | $img_final_path = $theme_imgsrc . $name; 190 | } 191 | if (!file_exists(BASE_PATH . $img_final_path . '.png')) { 192 | $img_final_path = $module_imgsrc . 'Blank'; 193 | } 194 | $thumbnail = ''; 195 | $html = '
    ' . $thumbnail . '
    ' . $name . ''; 196 | $optionset[$name] = $this->createStringAsHTML($html); 197 | } 198 | 199 | $tplField = OptionsetField::create( 200 | "Template", 201 | "Choose a template", 202 | $optionset, 203 | $this->Template 204 | )->addExtraClass('stacked'); 205 | $fields->addFieldsToTab("Root.Template", $tplField); 206 | 207 | } else { 208 | $fields->addFieldsToTab("Root.Template", new LiteralField ($name = "literalfield", $content = '

    Warning: The folder ' . $src . ' was not found.')); 209 | } 210 | 211 | // Settings tab 212 | $fields->addFieldToTab("Root.Settings", new CheckboxField('Active', 'Active')); 213 | $fields->addFieldToTab("Root.Settings", new TextField('Link', 'Link')); 214 | $fields->addFieldToTab("Root.Settings", TextField::create('ExtraCssClasses')); 215 | 216 | // taken from RedirectorPage 217 | Requirements::javascript(CMS_DIR . '/javascript/RedirectorPage.js'); 218 | $fields->addFieldsToTab('Root.Settings', 219 | array( 220 | new HeaderField('RedirectorDescHeader', "Set an external or internal link"), 221 | new OptionsetField( 222 | "RedirectionType", 223 | _t('RedirectorPage.REDIRECTTO', "Redirect to"), 224 | array( 225 | "Internal" => _t('RedirectorPage.REDIRECTTOPAGE', "A page on your website"), 226 | "External" => _t('RedirectorPage.REDIRECTTOEXTERNAL', "Another website"), 227 | ), 228 | "Internal" 229 | ), 230 | new TreeDropdownField( 231 | "LinkToID", 232 | _t('RedirectorPage.YOURPAGE', "Page on your website"), 233 | "SiteTree" 234 | ), 235 | new TextField("ExternalURL", _t('RedirectorPage.OTHERURL', "Other website URL")) 236 | ) 237 | ); 238 | 239 | $PagesConfig = GridFieldConfig_RelationEditor::create(10); 240 | $PagesConfig->removeComponentsByType('GridFieldAddNewButton'); 241 | $gridField = new GridField("Pages", "Related pages (This block is used on the following pages)", $this->Pages(), $PagesConfig); 242 | 243 | $fields->addFieldToTab("Root.Settings", $gridField); 244 | 245 | $this->extend('updateCMSFields', $fields); 246 | 247 | return $fields; 248 | } 249 | 250 | /** 251 | * Return the link that we should redirect to. 252 | * Only return a value if there is a legal redirection destination. 253 | */ 254 | public function getInternalExternalLink() 255 | { 256 | if ($this->RedirectionType == 'External') { 257 | if ($this->ExternalURL) { 258 | return $this->ExternalURL; 259 | } 260 | 261 | } else { 262 | $linkTo = $this->LinkToID ? DataObject::get_by_id("SiteTree", $this->LinkToID) : null; 263 | 264 | if ($linkTo) { 265 | return $linkTo->Link(); 266 | } 267 | } 268 | } 269 | 270 | function onBeforeWrite() 271 | { 272 | parent::onBeforeWrite(); 273 | 274 | if (!$this->ID) { 275 | $this->first_write = true; 276 | } 277 | 278 | } 279 | 280 | function onAfterWrite() 281 | { 282 | parent::onAfterWrite(); 283 | 284 | } 285 | 286 | /* Clean the relation table when deleting a Block */ 287 | public function onBeforeDelete() 288 | { 289 | parent::onBeforeDelete(); 290 | $this->Pages()->removeAll(); 291 | $this->Files()->removeAll(); 292 | $this->Images()->removeAll(); 293 | } 294 | 295 | // Should only unlink if a block is on more than one page 296 | public function canDelete($member = null) 297 | { 298 | if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser(); 299 | 300 | // extended access checks 301 | $results = $this->extend('canDelete', $member); 302 | 303 | if ($results && is_array($results)) { 304 | if (!min($results)) return false; 305 | else return true; 306 | } 307 | 308 | // No member found 309 | if (!($member && $member->exists())) return false; 310 | 311 | $pcount = $this->Pages()->Count(); 312 | if ($pcount > 1) { 313 | return false; 314 | } else { 315 | return true; 316 | } 317 | 318 | 319 | return $this->canEdit($member); 320 | } 321 | 322 | function recurse_copy($src, $dst) 323 | { 324 | $dir = opendir($src); 325 | @mkdir($dst); 326 | while (false !== ($file = readdir($dir))) { 327 | if (($file != '.') && ($file != '..')) { 328 | if (is_dir($src . '/' . $file)) { 329 | $this->recurse_copy($src . '/' . $file, $dst . '/' . $file); 330 | } else { 331 | copy($src . '/' . $file, $dst . '/' . $file); 332 | } 333 | } 334 | } 335 | closedir($dir); 336 | } 337 | 338 | /* TODO: add function to calculate image widths based on columns? */ 339 | public function ColumnClass($totalitems) 340 | { 341 | $totalcolumns = 12; // should be configurable 342 | $columns = $totalcolumns / $totalitems; 343 | 344 | return $columns; 345 | } 346 | 347 | public function getThumbnail() 348 | { 349 | if ($this->Images()->Count() >= 1) { 350 | return $this->Images()->First()->croppedImage(50, 40); 351 | } 352 | } 353 | 354 | function forTemplate() 355 | { 356 | 357 | // can we include the Parent page for rendering? Perhaps use a checkbox in the CMS on the block if we should include the Page data. 358 | // $page = Controller::curr(); 359 | // return $this->customise(array('Page' => $page))->renderwith($this->Template); 360 | return $this->renderWith(array($this->Template, 'Block')); // Fall back to Block if selected does not exist 361 | } 362 | 363 | // Returns only the file extension (without the period). 364 | function file_ext($filename) 365 | { 366 | if (!preg_match('/\./', $filename)) return ''; 367 | 368 | return preg_replace('/^.*\./', '', $filename); 369 | } 370 | 371 | // Returns the file name, without the extension. 372 | function file_ext_strip($filename) 373 | { 374 | return preg_replace('/\.[^.]*$/', '', $filename); 375 | } 376 | 377 | /** 378 | * @return string 379 | */ 380 | public function getExtraClasses() 381 | { 382 | $classes = $this->ExtraCssClasses; 383 | if ($this->ContentAsColumns) { 384 | $classes .= ' css-columns'; 385 | } 386 | 387 | $this->extend('updateExtraClasses', $classes); 388 | 389 | return $classes; 390 | } 391 | 392 | /** 393 | * @param int $img_id 394 | * @param $width 395 | * @param $height 396 | * @return Image 397 | */ 398 | public function FormattedBlockImage($img_id, $width, $height) 399 | { 400 | $method = $this->ImageCropMethod; 401 | $img = $this->Images()->filter('ID', $img_id)->first(); 402 | 403 | return $img->$method($width, $height); 404 | } 405 | 406 | /** 407 | * Returns the page object (SiteTree) that we are currently on 408 | * Allow us to loop on children of the page and other page related data 409 | * 410 | * @return SiteTree 411 | */ 412 | public function CurrentPage() 413 | { 414 | return Director::get_current_page(); 415 | } 416 | } 417 | --------------------------------------------------------------------------------