├── 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 |
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 |
--------------------------------------------------------------------------------
/templates/BlockTemplates/Full width image.ss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/BlockTemplates/Pic2-Text2-Wrap.ss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/BlockTemplates/Text-Pic-Wrap.ss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/BlockTemplates/Pic-Text-Wrap.ss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/BlockTemplates/Text2-Pic2-Wrap.ss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/BlockTemplates/PictureList.ss:
--------------------------------------------------------------------------------
1 |
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 | 
60 | Overview in page editing
61 |
62 | 
63 | View all blocks across pages in a ModelAdmin
64 |
65 | 
66 | 
67 | 
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 |
--------------------------------------------------------------------------------