diff --git a/LICENSE b/LICENSE index 947ddef..11b7684 100755 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2017, FLX Labs +Copyright (c) 2017 - 2018, FLX Labs All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/composer.json b/composer.json index 72bfafe..234bea9 100755 --- a/composer.json +++ b/composer.json @@ -1,27 +1,28 @@ { - "name": "flxlabs/silverstripe-pagesections", - "version": "0.1.2", - "description": "Adds configurable page sections and elements to your SilverStripe project.", - "type": "silverstripe-module", - "homepage": "http://github.com/flxlabs/silverstripe-pagesections", - "keywords": ["silverstripe", "sections", "elements", "page sections", "page elements"], - "license": "MIT", - "authors": [{ - "name": "Marco Crespi", - "email": "mrc@flxlabs.com" - }], - "support": { - "issues": "http://github.com/flxlabs/silverstripe-pagesections/issues" - }, - "require": { - "silverstripe/framework": "^4.0.1", - "symbiote/silverstripe-gridfieldextensions": "^3" - }, - "extra": { - "installer-name": "pagesections", - "expose": [ - "css", - "javascript" - ] - } + "name": "flxlabs/silverstripe-pagesections", + "version": "0.1.2", + "description": "Adds configurable page sections and elements to your SilverStripe project.", + "type": "silverstripe-module", + "homepage": "http://github.com/flxlabs/silverstripe-pagesections", + "keywords": ["silverstripe", "sections", "elements", "page sections", "page elements"], + "license": "MIT", + "authors": [{ + "name": "Marco Crespi", + "email": "mrc@flxlabs.com" + }], + "support": { + "issues": "http://github.com/flxlabs/silverstripe-pagesections/issues" + }, + "require": { + "silverstripe/framework": "^4.0.1", + "symbiote/silverstripe-gridfieldextensions": "^3", + "unclecheese/betterbuttons": "dev-feature/ss4-upgrade" + }, + "extra": { + "installer-name": "pagesections", + "expose": [ + "css", + "javascript" + ] + } } diff --git a/css/GridFieldPageSectionsExtension.css b/css/GridFieldPageSectionsExtension.css index 6ba66ea..acfd1d1 100755 --- a/css/GridFieldPageSectionsExtension.css +++ b/css/GridFieldPageSectionsExtension.css @@ -22,6 +22,7 @@ list-style-type: none; transition: all .3s ease; } + .treenav-menu li:hover { background-color: #FEFAD5; } @@ -32,6 +33,7 @@ border-bottom: 1px solid #CCC; padding-left: 8px; } + .treenav-menu li.header:hover { background: none; } @@ -40,96 +42,161 @@ border-top: 1px solid #CCC; } +/* hierarchical gridfield */ -.ss-gridfield-pagesections .col-treenav { +.cms table.ss-gridfield-table tr { height: 100%; - padding-left: 0; - /*padding: 0 !important;*/ } -.ss-gridfield-pagesections .col-treenav > button { - /*width: 100% !important;*/ - height: 100% !important; - font-weight: bold !important; - /*font-size: 150% !important;*/ - color: black !important; - border: none; - background: transparent; - padding-right: 0; +.cms table.ss-gridfield-table tbody td.col-treenav { + padding: 0; + height: 100%; + vertical-align: middle; } -.ss-gridfield-pagesections .col-treenav > button, .ss-gridfield-pagesections .col-treenav > button > span { - padding: 0 !important; - margin: 0 !important; - text-align: left !important; +.cms table.ss-gridfield-table tbody td.col-treenav .col-treenav__inner { + display: flex; + height: 100%; + flex-flow: row nowrap; + justify-content: flex-start; + align-items: center; } -.ss-gridfield-pagesections .col-treenav > button:focus { - background: none !important; - box-shadow: none!important; - border: none !important; - background-color: none !important; +.cms table.ss-gridfield-table tbody td.col-treenav .col-treenav__text { + padding: 8px 8px 8px 0; } -.ss-gridfield-pagesections .col-treenav > button > span > span.is-closed{ +.cms table.ss-gridfield-table tbody td.col-treenav .col-treenav__classname { + font-size: 87%; +} - font-size: 100%; +.cms table.ss-gridfield-table tbody td.col-treenav .col-treenav__title { + font-weight: bold; } -.ss-gridfield-pagesections .col-treenav > button > span > span.is-open{ - font-size: 96%; - margin-left: -0.1em; +.cms table.ss-gridfield-table tbody td.col-treenav button { + width: 2em; + height: 100%; + display: block; + float: left; + margin: 0 0.5em 0 0; + padding: 0; + border-radius: 0; + position: relative; } -.ss-gridfield-pagesections .col-treenav > button > span > span.is-end{ - font-size: 110%; - margin-left: -0.1em; +.cms table.ss-gridfield-table tbody td.col-treenav button.ui-state-disabled { + opacity: 1; + filter: Alpha(Opacity=100); + background-image: none; + background: none; + box-shadow: none; } -.ss-gridfield-pagesections .col-treenav > button > span > span { - margin-right: 1em; - text-transform: capitalize; - vertical-align: middle; +.cms table.ss-gridfield-table tbody td.col-treenav button.ui-state-disabled:active { + border: none; + background: none; + box-shadow: none; } -.ss-gridfield-pagesections .col-treenav > button > span { - margin-right: 30px !important; - text-overflow: ellipsis; - white-space: nowrap; - overflow-x: hidden; +.cms table.ss-gridfield-table tbody td.col-treenav button svg { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); } -.ss-gridfield-pagesections .col-treenav > button.level0 > span { - margin-left: 1em !important; +.cms table.ss-gridfield-table tbody td.col-treenav button.level1 { + margin-left: 2em; } -.ss-gridfield-pagesections .col-treenav > button.level1 > span { - margin-left: 2em !important; + +.cms table.ss-gridfield-table tbody td.col-treenav button.level2 { + margin-left: 4em; } -.ss-gridfield-pagesections .col-treenav > button.level2 > span { - margin-left: 4em !important; + +.cms table.ss-gridfield-table tbody td.col-treenav button.level3 { + margin-left: 6em; +} + +.cms table.ss-gridfield-table tbody td.col-treenav button.level4 { + margin-left: 8em; } -.ss-gridfield-pagesections .col-treenav > button.level3 > span { - margin-left: 6em !important; + +.cms table.ss-gridfield-table tbody td.col-treenav button.level5 { + margin-left: 10em; } -.ss-gridfield-pagesections .col-treenav > button.level4 > span { - margin-left: 8em !important; +.cms table.ss-gridfield-table tbody td.col-treenav button.level6 { + margin-left: 12em; } +/** + * action col + */ + +.cms .ss-gridfield-pagesections table.ss-gridfield-table tr td.col-actions { + width: 68px; +} + +.cms .ss-gridfield-pagesections table.ss-gridfield-table tbody td.col-actions a.view-link, +.cms .ss-gridfield-pagesections table.ss-gridfield-table tbody td.col-actions a.edit-link { + margin-top: -5px; +} + +.cms .ss-gridfield-pagesections table.ss-gridfield-table tbody td button.col-actions__button { + margin: 2px; +} + +.cms .ss-gridfield-pagesections table.ss-gridfield-table tbody td button.col-actions__button .ui-button-text { + line-height: 0; + padding: 0; +} + +.cms .ss-gridfield-pagesections table.ss-gridfield-table tbody td button.col-actions__button .col-actions__button__icon svg { + display: block; +} /** * Orderable rows */ +.col-treenav__draggable { + z-index: 300; + background: #f6f7f8; + padding: 2px 4px; + border-radius: 2px; + box-shadow: 0 0 1px #ccc; +} + +.col-treenav__draggable.state-active { + border: 1px solid #417505; +} + +.col-treenav__draggable:before { + position: absolute; + content: ""; + left: -19px; + top: -19px; + width: 8px; + height: 8px; + border: 3px solid #4a4a4a; + border-radius: 50%; + box-sizing: border-box; +} + .ss-gridfield-pagesections thead tr th.col-Reorder span { padding: 0 !important; margin-left: 8px; } +.cms .ss-gridfield-pagesections table.ss-gridfield-table tr td.col-reorder { + position: relative; + padding: 0; + width: 16px; +} + .ss-gridfield-pagesections .col-reorder { position: relative; - padding: 0 !important; - width: 16px !important; } .ss-gridfield-pagesections .col-reorder .handle { @@ -154,26 +221,60 @@ .ss-gridfield-pagesections .col-reorder .ui-droppable { position: absolute; - left: 0; - width: 100%; + left: 100%; + width: 100vw; z-index: 100; display: none; } +.ss-gridfield-pagesections .col-reorder .ui-droppable svg { + position: absolute; + left: 0; + top: 50%; + z-index: 100; + transform: translate(0, -50%); +} + +.ss-gridfield-pagesections .col-reorder .ui-droppable svg path { + fill: #A8CB7F; +} + .ss-gridfield-pagesections .col-reorder .ui-droppable.before { top: 0; - height: 25%; - background-color: rgba(0, 100, 0, 0.4); + height: 50%; +} + +.ss-gridfield-pagesections .col-reorder .ui-droppable.before svg { + top: 0; } .ss-gridfield-pagesections .col-reorder .ui-droppable.middle { - top: 25%; - height: 50%; - background-color: rgba(0, 0, 100, 0.4); + left: auto; + right: 0; + top: 0; + height: 100%; +} + +.ss-gridfield-pagesections .col-reorder .ui-droppable.middle svg { + left: auto; + right: 0; + transform: scale(-1) translate(0, 50%); } .ss-gridfield-pagesections .col-reorder .ui-droppable.after { - bottom: 0; - height: 25%; - background-color: rgba(0, 100, 0, 0.4); + bottom: -1px; + height: 50%; +} + +.ss-gridfield-pagesections .col-reorder .ui-droppable.after svg { + top: 100%; + margin-top: -1px; +} + +.ss-gridfield-pagesections .col-reorder .ui-droppable.state-active { + z-index: 200; +} + +.ss-gridfield-pagesections .col-reorder .ui-droppable.state-active svg path { + fill: #417505; } diff --git a/examples/ImageElement.php_ b/examples/ImageElement.php_ index d355b48..1e0cf13 100755 --- a/examples/ImageElement.php_ +++ b/examples/ImageElement.php_ @@ -1,10 +1,15 @@ 'Image', + 'Image' => Image::class, ); public function getCMSFields(){ diff --git a/examples/TextElement.php_ b/examples/TextElement.php_ index 33cf7d6..3ceaecf 100755 --- a/examples/TextElement.php_ +++ b/examples/TextElement.php_ @@ -1,7 +1,10 @@ 'HTMLText', diff --git a/javascript/GridFieldPageSectionsExtension.js b/javascript/GridFieldPageSectionsExtension.js index dbf35aa..fdd9234 100755 --- a/javascript/GridFieldPageSectionsExtension.js +++ b/javascript/GridFieldPageSectionsExtension.js @@ -1,10 +1,10 @@ -(function($) { - $.entwine("ss", function($) { +(function ($) { + $.entwine("ss", function ($) { // Recursively hide a data-grid row and it's children - var hideRow = function($row) { + var hideRow = function ($row) { var id = $row.data("id"); - $("tr.ss-gridfield-item > .col-treenav[data-parent=" + id + "]").each(function() { + $("tr.ss-gridfield-item > .col-treenav[data-parent=" + id + "]").each(function () { hideRow($(this).parent()); }); $row.hide(); @@ -18,128 +18,134 @@ } }); - // Context menu click - $(document).on("click", ".treenav-menu li", function(event) { - var $this = $(this); - var $menu = $this.parents(".treenav-menu"); - var $gridfield = $(".ss-gridfield-pagesections[data-id='" + $menu.data("grid-id") + "']").find("tbody"); - var newType = $this.data("type"); - - // If we don't have a type then the user clicked a header or some random thing - if (!newType) return; - - if (newType === "__REMOVE__") { - $gridfield.removeElement($menu.data("row-id"), $menu.data("parent-id")); - } else if (newType === "__DELETE__") { - if (!confirm("Are you sure you want to remove this element? All children will be orphans!")) - return; - - $gridfield.deleteElement($menu.data("row-id")); - } else { - $gridfield.addElement($menu.data("row-id"), newType); - } - - $this.parents(".treenav-menu").hide(); - }); - // Show context menu $(".ss-gridfield-pagesections tbody").entwine({ - oncontextmenu: function(event) { - $target = $(event.target); - - var grid = this.getGridField(); - var id = grid.data("id"); - var rowId = $target.parents(".ss-gridfield-item").data("id"); - var $treeNav = $target.hasClass("col-treenav") ? $target : - $target.parents(".col-treenav").first(); - - // If we don't have a col-treenav the user clicked on another column - if ($treeNav.length <= 0) return; - event.preventDefault(); - - var parentId = null; - var parentName = null; - var level = $treeNav.data("level"); - if (level > 0) { - // Go up through the rows and find the first row with lower level (=parent) - $parent = $treeNav.parents(".ss-gridfield-item").prev(); - while ($parent.length > 0 && - $parent.find(".col-treenav").data("level") >= level) { - $parent = $parent.prev(); - } - if ($parent != null) { - parentId = $parent.data("id"); - parentName = $parent.find(".col-treenav > span").html(); - } - } - - var elems = $treeNav.data("allowed-elements"); - $menu = $(""); - $menu.css({ - top: event.pageY + "px", - left: event.pageX + "px" - }); - $(document.body).append($menu); - - $menu.data({ - gridId: id, - rowId: rowId, - parentId: parentId, - }); - $menu.append("
  • Add a child
  • "); - $.each(elems, function(key, value) { - $menu.append("
  • " + value + "
  • "); - }); - $menu.append("
  • Options
  • "); - $menu.append("
  • Remove from " + - (parentId ? parentName : "page") + "
  • "); - $menu.append("
  • Delete
  • "); - $menu.show(); - }, - addElement: function(id, elemType) { + addElement: function (id, elemType) { var grid = this.getGridField(); grid.reload({ url: grid.data("url-add"), - data: [ - { name: "id", value: id }, - { name: "type", value: elemType }, + data: [{ + name: "id", + value: id + }, + { + name: "type", + value: elemType + }, ] }); }, - removeElement: function(id, parentId) { + removeElement: function (id, parentId) { var grid = this.getGridField(); grid.reload({ url: grid.data("url-remove"), - data: [ - { name: "id", value: id }, - { name: "parentId", value: parentId }, + data: [{ + name: "id", + value: id + }, + { + name: "parentId", + value: parentId + }, ] }); }, - deleteElement: function(id) { + deleteElement: function (id) { var grid = this.getGridField(); grid.reload({ url: grid.data("url-delete"), - data: [ - { name: "id", value: id }, - ] + data: [{ + name: "id", + value: id + }, ] }); }, - onadd: function() { + onadd: function () { var grid = this.getGridField(); + var thisGrid = this; - $("tr.ss-gridfield-item").each(function() { + $("tr.ss-gridfield-item").each(function () { var $this = $(this); + // actions + $this.find(".col-actions .add-button").click(function (event) { + event.preventDefault(); + event.stopImmediatePropagation(); + $target = $(event.target); + var elems = $target.data("allowed-elements"); + + var id = grid.data("id"); + var rowId = $target.parents(".ss-gridfield-item").data("id"); + + + var $menu = $(""); + $menu.css({ + top: event.pageY + "px", + left: event.pageX + "px" + }); + $(document.body).append($menu); + + $menu.append("
  • " + ss.i18n._t('PageSections.GridField.AddAChild', 'Add a child') + "
  • "); + $.each(elems, function (key, value) { + var $li = $("
  • " + value + "
  • ") + $li.click(function () { + thisGrid.addElement(rowId, key); + $menu.remove(); + }) + $menu.append($li); + }); + $menu.show(); + }); + + $this.find(".col-actions .delete-button").click(function (event) { + event.preventDefault(); + event.stopImmediatePropagation(); + + $target = $(event.target); + + var id = grid.data("id"); + var rowId = $target.parents(".ss-gridfield-item").data("id"); + var parentId = $target.data("parent-id"); + + var $menu = $(""); + $menu.css({ + top: event.pageY + "px", + left: event.pageX + "px" + }); + $(document.body).append($menu); + + $menu.append("
  • " + ss.i18n._t('PageSections.GridField.Delete', 'Delete') + "
  • "); + + var $li = $("
  • " + ss.i18n._t('PageSections.GridField.RemoveAChild', 'Remove') + "
  • ") + $li.click(function () { + thisGrid.removeElement(rowId, parentId); + $menu.remove(); + }) + $menu.append($li); + if ($target.data("used-count") < 2) { + var $li = $("
  • " + ss.i18n._t('PageSections.GridField.DeleteAChild', 'Finally delete') + "
  • ") + $li.click(function () { + thisGrid.deleteElement(rowId, $menu.data("parent-id")); + $menu.remove(); + }) + $menu.append($li); + } + + $menu.show(); + }); + + // reorder + var icon = "" $col = $this.find(".col-reorder"); - $col.append("
    "); - $col.find("div").each(function() { + $col.append("
    " + icon + "
    " + icon + "
    " + icon + "
    "); + $col.find("div").each(function () { $(this).droppable({ + hoverClass: "state-active", tolerance: "pointer", - drop: function(event, ui) { + drop: function (event, ui) { $drop = $(this); var type = "before"; @@ -187,36 +193,85 @@ value: sort, }], }); + // we alter the state of the published / saved buttons + $('.cms-edit-form .Actions #Form_EditForm_action_publish').button({ + showingAlternate: true + }); + $('.cms-preview').entwine('.ss.preview').changeState('StageLink'); + }, }); }); - - $this.draggable({ revert: "invalid", - helper: function() { - var clone = $this.clone().css("z-index", 200).find(".ui-droppable").remove().end(); - // Timeout is needed otherwise the draggable position is messed up - setTimeout(function() { - hideRow($this); - }, 1); - return clone; + cursor: "crosshair", + cursorAt: { + top: -15, + left: -15 + }, + activeClass: "state-active", + hoverClass: "state-active", + tolerance: "pointer", + greedy: true, + helper: function () { + var $tr = $this.parents("tr.ss-gridfield-item"); + var $helper = $( + "
    " + + $this.find(".col-treenav__title").text() + + "
    " + ) + $this.css("opacity", 0.6) + + return $helper; }, - start: function() { + start: function () { var element = $this.data("class"); - $(".ui-droppable").each(function() { + $(".ui-droppable").each(function () { var $drop = $(this); var $treenav = $drop.parent().siblings(".col-treenav"); + var isOpen = $treenav.find("button").hasClass("is-open"); + var $tr = $drop.parents("tr.ss-gridfield-item"); // Check if we're allowed to drop the element on the specified drop point. + // dont enable dropping on itself + if ($tr.data("id") == $this.data("id")) return + + // dont enable dropping on .before of itself + if ($drop.hasClass("before") && $tr.prev().data("id") == $this.data("id")) return // Depending on where we drop it (before, middle or after) we have to either + // don't show middle if open + if ( + $drop.hasClass("middle") && + isOpen + ) { + return; + } + // let's handle level 0 if not open + else if ( + $treenav.data("level") == 0 && + ( + $drop.hasClass("before") || + ( + $drop.hasClass("after") && + !isOpen + ) + ) + ) { + var allowed = $treenav.data("allowed-parent-elements"); + if (!allowed[element]) return; + } // check our allowed children, or the allowed children of our parent row. - if ($drop.hasClass("before") || - ($drop.hasClass("after") && !$treenav.find("button").hasClass("is-open"))) { - - var $parent = $treenav.parent().siblings("[data-id=" + - $treenav.data("parent") + "]").first(); + else if ( + $drop.hasClass("before") || + ( + $drop.hasClass("after") && + !isOpen + ) + ) { + var $parent = $treenav.parent().siblings( + "[data-id=" + $treenav.data("parent") + "]" + ).first(); var allowed = $parent.find(".col-treenav").data("allowed-elements"); if (allowed && !allowed[element]) return; @@ -228,17 +283,17 @@ $(this).show(); }); }, - stop: function(event, ui) { + stop: function (event, ui) { $(".ui-droppable").hide(); // Show the previous elements. If the user made an invalid movement then // we want this to show anyways. If he did something valid the grid will // refresh so we don't care if it's visible behind the loading icon. - $("tr.ss-gridfield-item").show(); + $("tr.ss-gridfield-item").css("opacity", "") }, }); }); }, - onremove: function() { + onremove: function () { if (this.data('sortable')) { this.sortable("destroy"); } diff --git a/javascript/lang/de.js b/javascript/lang/de.js new file mode 100644 index 0000000..89b4d9a --- /dev/null +++ b/javascript/lang/de.js @@ -0,0 +1,12 @@ +if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') { + if (typeof(console) !== 'undefined') { // eslint-disable-line no-console + console.error('Class ss.i18n not defined'); // eslint-disable-line no-console + } +} else { + ss.i18n.addDictionary('en', { + "PageSections.GridField.AddAChild": "Unterelement hinzufügen", + "PageSections.GridField.Delete": "Löschen", + "PageSections.GridField.DeleteAChild": "Endgültig löschen", + "PageSections.GridField.RemoveAChild": "Entfernen", + }); +} diff --git a/javascript/lang/en.js b/javascript/lang/en.js new file mode 100644 index 0000000..615f3ee --- /dev/null +++ b/javascript/lang/en.js @@ -0,0 +1,12 @@ +if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') { + if (typeof(console) !== 'undefined') { // eslint-disable-line no-console + console.error('Class ss.i18n not defined'); // eslint-disable-line no-console + } +} else { + ss.i18n.addDictionary('en', { + "PageSections.GridField.AddAChild": "Add a child", + "PageSections.GridField.Delete": "Delete", + "PageSections.GridField.DeleteAChild": "Finally delete", + "PageSections.GridField.RemoveAChild": "Remove", + }); +} diff --git a/src/GridFieldPageSectionsExtension.php b/src/GridFieldPageSectionsExtension.php index 0a849c2..65739d6 100755 --- a/src/GridFieldPageSectionsExtension.php +++ b/src/GridFieldPageSectionsExtension.php @@ -13,6 +13,7 @@ use SilverStripe\Forms\GridField\GridField; use SilverStripe\ORM\SS_List; use SilverStripe\ORM\ArrayList; +use SilverStripe\View\ArrayData; use SilverStripe\ORM\DataObject; use SilverStripe\View\Requirements; use SilverStripe\View\ViewableData; @@ -32,8 +33,7 @@ class GridFieldPageSectionsExtension implements "handleAdd", "handleRemove", "handleDelete", - "handleReorder", - "handleMoveToPage" + "handleReorder" ]; @@ -54,8 +54,7 @@ public function getURLHandlers($grid) { "POST add" => "handleAdd", "POST remove" => "handleRemove", "POST delete" => "handleDelete", - "POST reorder" => "handleReorder", - "POST movetopage" => "handleMoveToPage" + "POST reorder" => "handleReorder" ]; } @@ -67,6 +66,7 @@ public function getHTMLFragments($field) { $moduleDir = self::getModuleDir(); Requirements::css($moduleDir . "/css/GridFieldPageSectionsExtension.css"); Requirements::javascript($moduleDir . "/javascript/GridFieldPageSectionsExtension.js"); + Requirements::add_i18n_javascript($moduleDir . '/javascript/lang', false, true); $id = rand(1000000, 9999999); $field->addExtraClass("ss-gridfield-pagesections"); @@ -75,7 +75,6 @@ public function getHTMLFragments($field) { $field->setAttribute("data-url-remove", $field->Link("remove")); $field->setAttribute("data-url-delete", $field->Link("delete")); $field->setAttribute("data-url-reorder", $field->Link("reorder")); - $field->setAttribute("data-url-movetopage", $field->Link("movetopage")); return []; } @@ -90,21 +89,34 @@ public function augmentColumns($gridField, &$columns) { } if (!in_array("Actions", $columns)) { - array_push($columns, "Actions"); + array_splice($columns, 2, 0, "Actions"); } // Insert grid state initial data $state = $gridField->getState(); if (!isset($state->open)) { $state->open = []; + + // Open all elements by default if has children + $list = []; + $newList = $gridField->getManipulatedList(); + while (count($list) < count($newList)) { + foreach ($newList as $item) { + if ($item->isOpenByDefault() && $item->Children()->Count()) { + $this->openElement($state, $item); + } + } + $list = $newList; + $newList = $gridField->getManipulatedList(); + } } } public function getColumnsHandled($gridField) { return [ "Reorder", - "TreeNav", "Actions", + "TreeNav", ]; } @@ -129,12 +141,23 @@ public function getColumnAttributes($gridField, $record, $columnName) { $elems[$class] = $class::getSingularName(); } + // if element has no parent we need to + // know the allowed elements of the page + if (!$record->_Parent) { + $parentClasses = $this->page->getAllowedPageElements(); + $parentElems = []; + foreach ($parentClasses as $class) { + $parentElems[$class] = $class::getSingularName(); + } + } + return [ - "class" => "col-treenav", - "data-class" => $record->ClassName, - "data-level" => strval($record->_Level), - "data-parent" => $record->_Parent ? strval($record->_Parent->ID) : "", - "data-allowed-elements" => json_encode($elems, JSON_UNESCAPED_UNICODE), + "class" => "col-treenav", + "data-class" => $record->ClassName, + "data-level" => strval($record->_Level), + "data-parent" => $record->_Parent ? strval($record->_Parent->ID) : "", + "data-allowed-parent-elements" => !$record->_Parent ? json_encode($parentElems, JSON_UNESCAPED_UNICODE) : "", + "data-allowed-elements" => json_encode($elems, JSON_UNESCAPED_UNICODE), ]; } @@ -162,10 +185,10 @@ public function getColumnContent($gridField, $record, $columnName) { $field = null; if ($record->Children() && $record->Children()->Count() > 0) { - $icon = ($open === true ? '' - : ''); + $icon = ($open === true ? '' + : ''); } else { - $icon = ''; + $icon = ''; } $field = GridField_FormAction::create( @@ -176,12 +199,19 @@ public function getColumnContent($gridField, $record, $columnName) { ["element" => $record] ); $field->addExtraClass("level".$level . ($open ? " is-open" : " is-closed")); + if (!$record->Children()->Count()) { + $field->addExtraClass(" is-end"); + $field->setDisabled(true); + } $field->setButtonContent($icon); $field->setForm($gridField->getForm()); return ViewableData::create()->customise([ "ButtonField" => $field, - "Title" => $record->i18n_singular_name(), + "ID" => $record->ID, + "UsedCount" => $record->Parents()->Count() + $record->getAllPages()->Count(), + "ClassName" => $record->i18n_singular_name(), + "Title" => $record->Title, ])->renderWith("GridFieldPageElement"); } @@ -195,7 +225,66 @@ public function getColumnContent($gridField, $record, $columnName) { ); } $link = Controller::join_links($gridField->link(), $link); - return "Edit"; + $data = new ArrayData([ + 'Link' => $link + ]); + $editButton = $data->renderWith('SilverStripe\Forms\GridField\GridFieldEditButton'); + + $classes = $record->getAllowedPageElements(); + $elems = []; + foreach ($classes as $class) { + $elems[$class] = $class::getSingularName(); + } + $addButton = GridField_FormAction::create( + $gridField, + "AddAction".$record->ID, + null, + null, + null + ); + $addButton->setAttribute("data-allowed-elements", json_encode($elems, JSON_UNESCAPED_UNICODE)); + $addButton->addExtraClass("col-actions__button add-button"); + if (!count($elems)) { + $addButton->setDisabled(true); + } + $addButton->setButtonContent(' + + + + + '); + + $deleteButton = GridField_FormAction::create( + $gridField, + "DeleteAction".$record->ID, + null, + null, + null + ); + $deleteButton->setAttribute( + "data-used-count", + $record->Parents()->Count() + $record->getAllPages()->Count() + ); + $deleteButton->setAttribute( + "data-parent-id", + $record->_Parent ? $record->_Parent->ID : $this->page->ID + ); + $deleteButton->addExtraClass("col-actions__button delete-button"); + + $deleteButton->setButtonContent(' + + + + + '); + + return ViewableData::create()->customise([ + "EditButton" => $editButton, + "AddButton" => $addButton, + "DeleteButton" => $deleteButton, + ])->renderWith("GridFieldPageSectionsActionColumn"); + + return $ret; } } @@ -412,7 +501,7 @@ public function handleRemove(GridField $gridField, HTTPRequest $request) { $parentId = intval($request->postVar("parentId")); $parentObj = DataObject::get_by_id(PageElement::class, $parentId); - // Detach it from this parent (from the page if we're top level) + // Detach it from this parent (from the page section if we're top level) if (!$parentObj) { $gridField->getList()->Remove($obj); } else { diff --git a/src/PageElement.php b/src/PageElement.php index a46adc7..1ed8066 100755 --- a/src/PageElement.php +++ b/src/PageElement.php @@ -4,23 +4,28 @@ use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Core\ClassInfo; +use SilverStripe\Forms\FieldList; +use SilverStripe\Forms\ReadonlyField; use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter; use SilverStripe\Forms\GridField\GridFieldConfig; +use SilverStripe\Forms\GridField\GridFieldConfig_Base; use SilverStripe\Forms\GridField\GridFieldButtonRow; use SilverStripe\Forms\GridField\GridFieldToolbarHeader; use SilverStripe\Forms\GridField\GridFieldDataColumns; use SilverStripe\Forms\GridField\GridFieldDetailForm; +use SilverStripe\Forms\GridField\GridFieldEditButton; +use SilverStripe\Forms\GridField\GridFieldFooter; use SilverStripe\Forms\GridField\GridField; -use SilverStripe\Forms\FieldList; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataObject; use SilverStripe\Security\Member; use SilverStripe\Versioned\Versioned; use Symbiote\GridFieldExtensions\GridFieldAddNewMultiClass; -use UncleCheese\BetterButtons\Actions\BetterButtonPrevNextAction; -use UncleCheese\BetterButtons\Buttons\BetterButton_SaveAndClose; -use UncleCheese\BetterButtons\Buttons\BetterButton_Save; +use UncleCheese\BetterButtons\Actions\PrevNext; +use UncleCheese\BetterButtons\Actions\CustomAction; +use UncleCheese\BetterButtons\Buttons\Save; +use UncleCheese\BetterButtons\Buttons\SaveAndClose; class PageElement extends DataObject { @@ -28,6 +33,7 @@ class PageElement extends DataObject { protected static $singularName = "Element"; protected static $pluralName = "Elements"; + protected static $defaultIsOpen = true; public static function getSingularName() { return static::$singularName; @@ -35,6 +41,9 @@ public static function getSingularName() { public static function getPluralName() { return static::$pluralName; } + public static function isOpenByDefault() { + return static::$defaultIsOpen; + } function canView($member = null) { return true; } function canEdit($member = null) { return true; } @@ -87,6 +96,10 @@ function canCreate($member = null, $context = []) { return true; } "ID", ]; + private static $better_buttons_actions = array ( + 'publishOnAllPages', + ); + public static function getAllowedPageElements() { $classes = array_values(ClassInfo::subclassesFor(PageElement::class)); // remove @@ -141,7 +154,8 @@ public function getChildrenGridField() { ->addComponent($autoCompl) ->addComponent($addNewButton) ->addComponent(new GridFieldPageSectionsExtension($this->owner)) - ->addComponent(new GridFieldDetailForm()); + ->addComponent(new GridFieldDetailForm()) + ->addComponent(new GridFieldFooter()); $dataColumns->setFieldCasting(["GridFieldPreview" => "HTMLText->RAW"]); return new GridField("Children", "Children", $this->Children(), $config); @@ -151,6 +165,27 @@ public function getGridFieldPreview() { return $this->Name; } + // Gets all the pages that this page element is on, plus adds a __PageSection + // attribute to the page object so we know which section this element is in. + public function getAllPages() { + $pages = ArrayList::create(); + + foreach ($this->PageSections() as $section) { + $page = $section->Page(); + $stage = Versioned::get_stage(); + Versioned::set_stage(Versioned::LIVE); + $publishedElem = DataObject::get_by_id($section->ClassName, $section->ID) + ->Elements()->filter("ID", $this->ID)->First(); + $page->__PageSection = $section; + $page->__PageElementVersion = $section->Elements()->filter("ID", $this->ID)->First()->Version; + $page->__PageElementPublishedVersion = $publishedElem ? $publishedElem->Version : "Not published"; + Versioned::set_stage($stage); + $pages->add($page); + } + + return $pages; + } + public function getCMSFields() { $fields = parent::getCMSFields(); $fields->removeByName('Pages'); @@ -162,6 +197,38 @@ public function getCMSFields() { $fields->addFieldToTab('Root.PageSections', $this->getChildrenGridField()); } + // Add our newest version as a readonly field + $fields->addFieldsToTab( + "Root.Main", + ReadonlyField::create("Version", "Version", $this->Version), + "Title" + ); + + // Create an array of all the pages this element is on + $pages = $this->getAllPages(); + + if ($pages->Count() > 0) { + // Remove default field + $fields->removeByName("PageSections"); + + $config = GridFieldConfig_Base::create() + ->removeComponentsByType(GridFieldDataColumns::class) + ->addComponent($dataColumns = new GridFieldDataColumns()) + ->addComponent(new GridFieldDetailForm()) + ->addComponent(new GridFieldEditButton()); + $dataColumns->setDisplayFields([ + "ID" => "ID", + "ClassName" => "Type", + "Title" => "Title", + "__PageSection.Name" => "PageSection", + "__PageElementVersion" => "Element version", + "__PageElementPublishedVersion" => "Published element version", + "getPublishState" => "Page state", + ]); + $gridField = GridField::create("Pages", "Pages", $pages, $config); + $fields->addFieldToTab("Root.Pages", $gridField); + } + return $fields; } @@ -194,4 +261,27 @@ public function forTemplate($parentList = "") { ["ParentList" => $parentList, "Parents" => $parents, "Page" => $page] ); } + + public function getBetterButtonsUtils() { + $fieldList = FieldList::create([ + PrevNext::create(), + ]); + return $fieldList; + } + + public function getBetterButtonsActions() { + $actions = FieldList::create([ + SaveAndClose::create(), + CustomAction::create('publishOnAllPages', 'Publish on all pages') + ->setRedirectType(CustomAction::REFRESH) + ]); + return $actions; + } + + public function publishOnAllPages() { + foreach ($this->getAllPages() as $page) { + $page->publish(Versioned::get_stage(), Versioned::LIVE); + } + return 'Published on all pages'; + } } diff --git a/src/PageSection.php b/src/PageSection.php index dd3e489..e47e718 100644 --- a/src/PageSection.php +++ b/src/PageSection.php @@ -58,7 +58,7 @@ public function onBeforeWrite() { public function onAfterWrite() { parent::onAfterWrite(); - if (Versioned::get_stage() == Versioned::DRAFT) { + if (!$this->__isNew && Versioned::get_stage() == Versioned::DRAFT) { $this->Page()->__PageSectionCounter++; $this->Page()->write(); } @@ -85,4 +85,15 @@ public function forTemplate() { return $form->forTemplate(); } + + // Gets the name of this section from the page it is on + public function getName() { + $page = $this->Page(); + foreach ($page->getPageSectionNames() as $sectionName) { + if ($page->{"PageSection" . $sectionName . "ID"} === $this->ID) { + return $sectionName; + } + } + return null; + } } diff --git a/src/PageSectionsExtension.php b/src/PageSectionsExtension.php index ecf3c46..82734ab 100755 --- a/src/PageSectionsExtension.php +++ b/src/PageSectionsExtension.php @@ -14,6 +14,7 @@ use SilverStripe\Forms\GridField\GridField; use SilverStripe\ORM\SiteTree; use SilverStripe\ORM\DataExtension; +use SilverStripe\ORM\FieldType\DBField; use SilverStripe\Versioned\Versioned; use Symbiote\GridFieldExtensions\GridFieldAddNewMultiClass; @@ -38,6 +39,11 @@ public static function get_extra_config($class = null, $extensionClass = null) { $owns[] = $name; $cascade_deletes[] = $name; + + // Add the inverse relation to the PageElement class + /*Config::inst()->update(PageElement::class, "versioned_belongs_many_many", array( + $class . "_" . $name => $class . "." . $name + ));*/ } // Create the relations for our sections @@ -55,6 +61,14 @@ public static function getAllowedPageElements() { return $classes; } + public function getPageSectionNames() { + $sections = Config::inst()->get($this->owner->ClassName, "page_sections", Config::EXCLUDE_EXTRA_SOURCES); + if (!$sections) { + $sections = ["Main"]; + } + return $sections; + } + public function onBeforeWrite() { parent::onBeforeWrite(); @@ -71,6 +85,7 @@ public function onBeforeWrite() { if (!$this->owner->$name()->ID) { $section = PageSection::create(); $section->PageID = $this->owner->ID; + $section->__isNew = true; $section->write(); $this->owner->$name = $section; } @@ -119,4 +134,8 @@ public function RenderPageSection($name = "Main") { ["Elements" => $elements, "ParentList" => strval($this->owner->ID)] ); } + + public function getPublishState() { + return DBField::create_field("HTMLText", $this->owner->latestPublished() ? "Published" : "Draft"); + } } diff --git a/templates/GridFieldPageElement.ss b/templates/GridFieldPageElement.ss index 471c3b8..1b87b4c 100755 --- a/templates/GridFieldPageElement.ss +++ b/templates/GridFieldPageElement.ss @@ -1 +1,11 @@ -$ButtonField $Title +
    + $ButtonField +
    +
    + $ClassName (ID: {$ID}, {$UsedCount}x) +
    +
    + $Title +
    +
    +
    diff --git a/templates/GridFieldPageSectionsActionColumn.ss b/templates/GridFieldPageSectionsActionColumn.ss new file mode 100644 index 0000000..75edf74 --- /dev/null +++ b/templates/GridFieldPageSectionsActionColumn.ss @@ -0,0 +1,5 @@ +
    + $AddButton + $DeleteButton + $EditButton +
    diff --git a/templates/PageElement.ss b/templates/PageElement.ss new file mode 100644 index 0000000..d7a3c0d --- /dev/null +++ b/templates/PageElement.ss @@ -0,0 +1,7 @@ +
    +

    $Title (v$Version)

    + $Layout +
    + $RenderChildren($ParentList) +
    +