API Refactor the CMS layouting to provide access to options.

It is now possible to change the threeColumnLayout width options for the
columns via entwine property LayoutOptions and accessor methods.

Thanks @robert-h-curry, @clarkepaul for contributing!
This commit is contained in:
Mateusz Uzdowski 2012-11-26 17:26:06 +13:00 committed by Ingo Schommer
parent 544d2eb6e1
commit d4f13fe532
11 changed files with 332 additions and 102 deletions

View File

@ -1463,10 +1463,6 @@ class LeftAndMain extends Controller implements PermissionProvider {
return $this->CSSClasses('Controller');
}
public function IsPreviewExpanded() {
return ($this->request->getVar('cms-preview-expanded'));
}
/**
* @return String
*/

View File

@ -338,6 +338,8 @@ body.cms { overflow: hidden; }
.cms-edit-form.CMSMain { padding: 0; }
.cms-container .column-hidden { display: none; }
/** -------------------------------------------- Tabs -------------------------------------------- */
.ui-tabs { padding: 0; background: none; }
.ui-tabs .ui-tabs { position: static; }
@ -571,6 +573,8 @@ p.message { margin-bottom: 12px; }
select.preview-dropdown { display: inline; width: auto; padding-right: 20px; }
.cms-preview { background-color: #eceff1; height: 100%; width: 100%; }
<<<<<<< HEAD
<<<<<<< HEAD
.cms-preview.is-collapsed .cms-preview-toggle a, .cms-preview.is-collapsed .cms-preview-toggle .cms .jstree .jstree-wholerow a:hover, .cms .jstree .jstree-wholerow .cms-preview.is-collapsed .cms-preview-toggle a:hover, .cms-preview.is-collapsed .cms-preview-toggle .cms .jstree .jstree-wholerow #vakata-contextmenu li.vakata-hover > a, .cms .jstree .jstree-wholerow #vakata-contextmenu .cms-preview.is-collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.is-collapsed .cms-preview-toggle .cms #vakata-contextmenu .jstree .jstree-wholerow li.vakata-hover > a, .cms #vakata-contextmenu .jstree .jstree-wholerow .cms-preview.is-collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.is-collapsed .cms-preview-toggle .cms .jstree .jstree-wholerow .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li.vakata-hover > a, .cms .jstree .jstree-wholerow .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu .cms-preview.is-collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.is-collapsed .cms-preview-toggle .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu .cms .jstree .jstree-wholerow li.vakata-hover > a, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu .cms .jstree .jstree-wholerow .cms-preview.is-collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.is-collapsed .cms-preview-toggle .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow a:hover, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow .cms-preview.is-collapsed .cms-preview-toggle a:hover, .cms-preview.is-collapsed .cms-preview-toggle .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow .cms #vakata-contextmenu li.vakata-hover > a, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow .cms #vakata-contextmenu .cms-preview.is-collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.is-collapsed .cms-preview-toggle .cms #vakata-contextmenu .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow li.vakata-hover > a, .cms #vakata-contextmenu .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow .cms-preview.is-collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.is-collapsed .cms-preview-toggle .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow #vakata-contextmenu li.vakata-hover > a, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow #vakata-contextmenu .cms-preview.is-collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.is-collapsed .cms-preview-toggle .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu .jstree .jstree-wholerow li.vakata-hover > a, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu .jstree .jstree-wholerow .cms-preview.is-collapsed .cms-preview-toggle li.vakata-hover > a { left: -15px; }
<<<<<<< HEAD
<<<<<<< HEAD
@ -579,6 +583,11 @@ select.preview-dropdown { display: inline; width: auto; padding-right: 20px; }
=======
.cms-preview .preview-scroll { height: 100%; width: 100%; overflow: auto; }
=======
=======
.cms-preview.collapsed .cms-preview-toggle a, .cms-preview.collapsed .cms-preview-toggle .cms .jstree .jstree-wholerow a:hover, .cms .jstree .jstree-wholerow .cms-preview.collapsed .cms-preview-toggle a:hover, .cms-preview.collapsed .cms-preview-toggle .cms .jstree .jstree-wholerow #vakata-contextmenu li.vakata-hover > a, .cms .jstree .jstree-wholerow #vakata-contextmenu .cms-preview.collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.collapsed .cms-preview-toggle .cms #vakata-contextmenu .jstree .jstree-wholerow li.vakata-hover > a, .cms #vakata-contextmenu .jstree .jstree-wholerow .cms-preview.collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.collapsed .cms-preview-toggle .cms .jstree .jstree-wholerow .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li.vakata-hover > a, .cms .jstree .jstree-wholerow .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu .cms-preview.collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.collapsed .cms-preview-toggle .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu .cms .jstree .jstree-wholerow li.vakata-hover > a, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu .cms .jstree .jstree-wholerow .cms-preview.collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.collapsed .cms-preview-toggle .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow a:hover, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow .cms-preview.collapsed .cms-preview-toggle a:hover, .cms-preview.collapsed .cms-preview-toggle .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow .cms #vakata-contextmenu li.vakata-hover > a, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow .cms #vakata-contextmenu .cms-preview.collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.collapsed .cms-preview-toggle .cms #vakata-contextmenu .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow li.vakata-hover > a, .cms #vakata-contextmenu .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow .cms-preview.collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.collapsed .cms-preview-toggle .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow #vakata-contextmenu li.vakata-hover > a, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow #vakata-contextmenu .cms-preview.collapsed .cms-preview-toggle li.vakata-hover > a, .cms-preview.collapsed .cms-preview-toggle .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu .jstree .jstree-wholerow li.vakata-hover > a, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu .jstree .jstree-wholerow .cms-preview.collapsed .cms-preview-toggle li.vakata-hover > a { left: -15px; }
>>>>>>> API Refactor the layout calls to allow for all mode options.
=======
>>>>>>> Hide columns using css classes. The class is now called column-hidden.
.cms-preview .preview-note { font-weight: 18px; display: block; position: absolute; text-align: center; width: 300px; height: 82px; left: 50%; top: 50%; margin-top: -50px; margin-left: -150px; /* half of width */ color: #CDD7DC; font-weight: bold; font-size: 22px; text-shadow: 0 1px 0 #fff; }
.cms-preview .preview-note span { background: url('../images/sprites-64x64-s88957ee578.png') 0 0 no-repeat; width: 50px; height: 41px; margin: 0 auto 20px; display: block; }
.cms-preview .preview-scroll { height: 100%; width: 100%; overflow: auto; position: relative; }

View File

@ -6,20 +6,64 @@
$.fn.layout.defaults.resize = false;
var minMenuWidth = 40;
var maxMenuWidth = 150;
var prefContentWidth = 820;
var prefPreviewWidth = 500;
var minPreviewWidth = 400;
/**
* Acccess the global variable in the same way the plugin does it.
*/
jLayout = (typeof jLayout === 'undefined') ? {} : jLayout;
jLayout.threeColumnCompressor = function (spec) {
var obj = {}, menu = $.jLayoutWrap(spec.menu), content = $.jLayoutWrap(spec.content), preview = $.jLayoutWrap(spec.preview);
/**
* Factory function for generating new type of algorithm for our CMS.
*
* Spec requires a definition of three column elements:
* - `menu` on the left
* - `content` area in the middle (includes the EditForm, side tool panel, actions, breadcrumbs and tabs)
* - `preview` on the right (will be shown if there is enough space)
*
* Required options:
* - `minContentWidth`: minimum size for the content display as long as the preview is visible
* - `minPreviewWidth`: preview will not be displayed below this size
* - `mode`: one of "split", "content" or "preview"
*
* The algorithm first checks which columns are to be visible and which hidden.
*
* In the case where both preview and content should be shown it first tries to assign half of non-menu space to
* preview and the other half to content. Then if there is not enough space for either content or preview, it tries
* to allocate the minimum acceptable space to that column, and the rest to the other one. If the minimum
* requirements are still not met, it falls back to showing content only.
*
* @param spec A structure defining columns and parameters as per above.
*/
jLayout.threeColumnCompressor = function (spec, options) {
// Spec sanity checks.
if (typeof spec.menu==='undefined' ||
typeof spec.content==='undefined' ||
typeof spec.preview==='undefined') {
throw 'Spec is invalid. Please provide "menu", "content" and "preview" elements.';
}
if (typeof options.minContentWidth==='undefined' ||
typeof options.minPreviewWidth==='undefined' ||
typeof options.mode==='undefined') {
throw 'Spec is invalid. Please provide "minContentWidth", "minPreviewWidth", "mode"';
}
if (options.mode!=='split' && options.mode!=='content' && options.mode!=='preview') {
throw 'Spec is invalid. "mode" should be either "split", "content" or "preview"';
}
// Instance of the algorithm being produced.
var obj = {
options: options
};
// Internal column handles, also implementing layout.
var menu = $.jLayoutWrap(spec.menu),
content = $.jLayoutWrap(spec.content),
preview = $.jLayoutWrap(spec.preview);
/**
* Required interface implementations follow.
* Refer to https://github.com/bramstein/jlayout#layout-algorithms for the interface spec.
*/
obj.layout = function (container) {
var contentHidden = (content.item.is('.is-collapsed'));
var size = container.bounds(),
insets = container.insets(),
top = insets.top,
@ -27,55 +71,71 @@
left = insets.left,
right = size.width - insets.right;
var menuWidth = $('#cms-menu.cms-panel').hasClass('collapsed') ? minMenuWidth : maxMenuWidth;
var contentWidth = contentHidden ? 0 : prefContentWidth;
var previewWidth = right - left - (menuWidth + contentWidth);
var menuWidth = spec.menu.width(),
contentWidth = 0,
previewWidth = 0;
if (!contentHidden) {
var previewWidth = right - left - (menuWidth + contentWidth);
var previewUnderlay = false;
if (this.options.mode==='preview') {
// All non-menu space allocated to preview.
contentWidth = 0;
previewWidth = right - left - menuWidth;
} else if (this.options.mode==='content') {
// All non-menu space allocated to content.
contentWidth = right - left - menuWidth;
previewWidth = 0;
} else { // ==='split'
// Split view - first try 50-50 distribution.
contentWidth = (right - left - menuWidth) / 2;
previewWidth = right - left - (menuWidth + contentWidth);
// If preview width is less than the minimum size, take some off the menu
if (previewWidth < prefPreviewWidth) {
if (previewWidth < minPreviewWidth) {
contentWidth = right - left - menuWidth;
previewWidth = right - left - menuWidth;
previewUnderlay = true;
if (contentWidth < prefContentWidth) {
contentWidth = right - left - menuWidth;
previewWidth = right - left - menuWidth;
}
}
// If violating one of the minima, try to readjust towards satisfying it.
if (contentWidth < this.options.minContentWidth) {
contentWidth = this.options.minContentWidth;
previewWidth = right - left - (menuWidth + contentWidth);
} else if (previewWidth < this.options.minPreviewWidth) {
previewWidth = this.options.minPreviewWidth;
contentWidth = right - left - (menuWidth + previewWidth);
}
else if (previewWidth > 500) {
contentWidth = (right - left - menuWidth) / 2;
previewWidth = right - left - (menuWidth + contentWidth);
// If still violating one of the (other) minima, remove the preview and allocate everything to content.
if (contentWidth < this.options.minContentWidth || previewWidth < this.options.minPreviewWidth) {
contentWidth = right - left - menuWidth;
previewWidth = 0;
}
}
// Apply classes for elements that might not be visible at all.
spec.content.toggleClass('column-hidden', contentWidth===0);
spec.preview.toggleClass('column-hidden', previewWidth===0);
// Apply the widths to columns, and call subordinate layouts to arrange the children.
menu.bounds({'x': left, 'y': top, 'height': bottom - top, 'width': menuWidth});
menu.doLayout();
left += menuWidth;
content.bounds({'x': left, 'y': top, 'height': bottom - top, 'width': contentWidth});
content.item.css({display: contentHidden ? 'none' : 'block'});
content.doLayout();
if (!previewUnderlay) left += contentWidth;
left += contentWidth;
preview.bounds({'x': left, 'y': top, 'height': bottom - top, 'width': previewWidth});
preview.doLayout();
return container;
};
/**
* Helper to generate the required `preferred`, `minimum` and `maximum` interface functions.
*/
function typeLayout(type) {
var func = type + 'Size';
return function (container) {
var menuSize = menu[func](), contentSize = content[func](), previewSize = preview[func](), insets = container.insets();
var menuSize = menu[func](),
contentSize = content[func](),
previewSize = preview[func](),
insets = container.insets();
width = menuSize.width + contentSize.width + previewSize.width;
height = Math.max(menuSize.height, contentSize.height, previewSize.height);
@ -87,10 +147,12 @@
};
}
// Generate interface functions.
obj.preferred = typeLayout('preferred');
obj.minimum = typeLayout('minimum');
obj.maximum = typeLayout('maximum');
return obj;
};
}(jQuery));
}(jQuery));

View File

@ -218,14 +218,17 @@
onchange: function(e) {
e.preventDefault();
var content = $('.cms-content');
var container = $('.cms-container');
var state = $(this).val();
if (state == 'split') {
content.removeClass('is-collapsed');
container.splitViewMode();
} else if (state == 'edit') {
container.contentViewMode();
} else {
content.addClass('is-collapsed');
container.previewMode();
}
content.parent().redraw();
this.addIcon(); //run generic addIcon, on select.preview-dropdown
}
});

View File

@ -102,6 +102,17 @@ jQuery.noConflict();
StateChangeCount: 0,
/**
* Options for the threeColumnCompressor layout algorithm.
*
* See LeftAndMain.Layout.js for description of these options.
*/
LayoutOptions: {
minContentWidth: 820,
minPreviewWidth: 400,
mode: 'split'
},
/**
* Constructor: onmatch
*/
@ -120,7 +131,7 @@ jQuery.noConflict();
this._super();
return;
}
// Initialize layouts
this.redraw();
@ -145,14 +156,66 @@ jQuery.noConflict();
onaftersubmitform: function(){ this.redraw(); }
},
/**
* Ensure the user can see the requested section - restore the default view.
*/
'from .cms-menu-list li a': {
onclick: function() {
this.splitViewMode();
}
},
/**
* Change the options of the threeColumnCompressor layout, and trigger layouting. You can provide any or
* all options. The remaining options will not be changed.
*/
updateLayoutOptions: function(newSpec) {
var spec = this.getLayoutOptions();
$.extend(spec, newSpec);
this.redraw();
},
/**
* Enable the split view - with content on the left and preview on the right.
*/
splitViewMode: function() {
this.updateLayoutOptions({
mode: 'split'
});
this.redraw();
},
/**
* Content only.
*/
contentViewMode: function() {
this.updateLayoutOptions({
mode: 'content'
});
this.redraw();
},
/**
* Preview only.
*/
previewMode: function() {
this.updateLayoutOptions({
mode: 'preview'
});
this.redraw();
},
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
// Use the three column compressor layout, which squishes preview, then menu, then content
this.data('jlayout', jLayout.threeColumnCompressor({
menu: this.children('.cms-menu'),
content: this.children('.cms-content'),
preview: this.children('.cms-preview')}
// Reset the algorithm.
this.data('jlayout', jLayout.threeColumnCompressor(
{
menu: this.children('.cms-menu'),
content: this.children('.cms-content'),
preview: this.children('.cms-preview')
},
this.getLayoutOptions()
));
// Trigger layout algorithm once at the top. This also lays out children - we move from outside to
@ -426,10 +489,9 @@ jQuery.noConflict();
// Set loading state and store element state
var origStyle = contentEl.attr('style');
var origVisible = contentEl.is(':visible');
var origParent = contentEl.parent();
var origParentLayoutApplied = (typeof origParent.data('jlayout')!=='undefined');
var layoutClasses = ['east', 'west', 'center', 'north', 'south'];
var layoutClasses = ['east', 'west', 'center', 'north', 'south', 'column-hidden'];
var elemClasses = contentEl.attr('class');
var origLayoutClasses = [];
if(elemClasses) {
@ -443,7 +505,6 @@ jQuery.noConflict();
.removeClass(layoutClasses.join(' '))
.addClass(origLayoutClasses.join(' '));
if(origStyle) newContentEl.attr('style', origStyle);
newContentEl.css('visibility', 'hidden');
// Allow injection of inline styles, as they're not allowed in the document body.
// Not handling this through jQuery.ondemand to avoid parsing the DOM twice.
@ -453,9 +514,6 @@ jQuery.noConflict();
// Replace panel completely (we need to override the "layout" attribute, so can't replace the child instead)
contentEl.replaceWith(newContentEl);
// Unset loading and restore element state (to avoid breaking existing panel visibility, e.g. with preview expanded)
if(origVisible) newContentEl.css('visibility', 'visible');
// Force jlayout to rebuild internal hierarchy to point to the new elements.
// This is only necessary for elements that are at least 3 levels deep. 2nd level elements will
// be taken care of when we lay out the top level element (.cms-container).

View File

@ -137,6 +137,12 @@ body.cms {
padding: 0;
}
// Hide threeColumnCompressor column.
.cms-container {
.column-hidden {
display: none;
}
}
/** --------------------------------------------
* Tabs
@ -917,14 +923,6 @@ select.preview-dropdown {
height: 100%;
width: 100%;
&.is-collapsed {
.cms-preview-toggle {
a {
left: -15px; // point left
}
}
}
.preview-note {
font-weight: 18px;
display: block;

View File

@ -1,6 +1,6 @@
<div class="cms-navigator">
<span class="preview-selector field dropdown">
<span class="preview-selector field dropdown">
<select id="cms-preview-mode-dropdown" class="preview-dropdown dropdown nolabel" autocomplete="off" name="Action">
<option data-icon="icon-split" class="icon-split icon-view first" value="split"><% _t('SilverStripeNavigator.SplitView', 'Split mode') %></option>
<option data-icon="icon-preview" class="icon-preview icon-view" value="preview"><% _t('SilverStripeNavigator.PreviewView', 'Preview mode') %></option>
@ -8,8 +8,8 @@
<option data-icon="icon-window" class="icon-window icon-view last" value="window"><% _t('SilverStripeNavigator.DualWindowView', 'Dual Window') %></option>
</select>
</span>
<span class="preview-selector field dropdown">
<span class="preview-selector field dropdown">
<select id="cms-preview-mode-dropdown" class="preview-dropdown dropdown nolabel" autocomplete="off" name="Action">
<option data-icon="icon-auto" data-description="<% _t('SilverStripeNavigator.Responsive', 'Responsive') %>" class="icon-auto icon-view first" value="split">
<% _t('SilverStripeNavigator.Auto', 'Auto') %>
@ -26,20 +26,17 @@
</select>
</span>
<div class="cms-preview-states switch-states">
<input type="checkbox" name="cms-preview" class="state cms-preview" id="cms-preview-state" checked>
<label for="cms-preview-state">
<span class="switch-options">
<% loop Items %>
$Items.count
<a href="$Link" class="$FirstLast <% if isActive %> active<% end_if %>">
$Title
</a>
<input type="checkbox" name="cms-preview" class="state cms-preview" id="cms-preview-state" checked>
<label for="cms-preview-state">
<span class="switch-options">
<% loop Items %>
$Items.count
<a href="$Link" class="$FirstLast <% if isActive %> active<% end_if %>">$Title</a>
<% end_loop %>
</span>
<span class="switch"></span>
</label>
</div>
</span>
<span class="switch"></span>
</label>
</div>
</div>

View File

@ -13,7 +13,7 @@
$Menu
$Content
<div class="cms-preview east <% if IsPreviewExpanded %>is-expanded<% else %>is-collapsed<% end_if %>" data-layout-type="border">
<div class="cms-preview east" data-layout-type="border">
<div class="preview-note"><span><!-- --></span><% _t('CMSPageHistoryController_versions.ss.PREVIEW','Website preview') %></div>
<div class="preview-scroll">
<div class="preview-device-outer">

View File

@ -21,11 +21,13 @@ the common `Page` object (a new PHP class `MyPage` will look for a `MyPage.ss` t
We can use this to create a different base template with `LeftAndMain.ss`
(which corresponds to the `LeftAndMain` PHP controller class).
Copy the template markup of the base implementation at `framework/admin/templates/LeftAndMain.ss` into `mysite/templates/LeftAndMain.ss`. It will automatically be picked up by the CMS logic. Add a new section after the `$Content` tag:
Copy the template markup of the base implementation at `framework/admin/templates/LeftAndMain.ss` into
`mysite/templates/LeftAndMain.ss`. It will automatically be picked up by the CMS logic. Add a new section after
the `$Content` tag:
:::ss
...
<div class="cms-container" data-layout="{type: 'border'}">
<div class="cms-container" data-layout-type="border">
$Menu
$Content
<div class="cms-bottom-bar south">
@ -45,6 +47,8 @@ The important piece of information is the `south` class in our new `<div>` struc
plus the height value in our CSS. It instructs the existing parent layout how to render the element.
This layout manager ([jLayout](http://www.bramstein.com/projects/jlayout/))
allows us to build complex layouts with minimal JavaScript configuration.
See [layout reference](../reference/layout) for more specific information on CMS layouting.
## Include custom CSS in the CMS
@ -129,5 +133,6 @@ blocks and concepts for more complex extensions as well.
## Related
* [CMS Architecture](../reference/cms-architecture)
* [Reference: CMS Architecture](../reference/cms-architecture)
* [Reference: Layout](../reference/layout)
* [Topics: Rich Text Editing](../topics/rich-text-editing)

View File

@ -85,25 +85,11 @@ So to add or "subclass" a tools panel, simply create this file and it's automati
## Layout and Panels
The CMS markup is structured into "panels", which are the base units containing
interface components (or other panels), as declared by the class `cms-panel`. Panels can be made collapsible, and
get the ability to be resized and aligned with a layout manager, in our case [jLayout](http://www.bramstein.com/projects/jlayout/).
This layout manager applies CSS declarations (mostly dimensions and positioning) via JavaScript,
by extracting additional metadata from the markup in the form of HTML5 data attributes.
We're using a "border layout" which separates the panels into five areas: north, south, east, west and center (all of which are optional).
As layouts can be nested, this allows for some powerful combinations. Our
[Howto: Extend the CMS Interface](../howto/extend-cms-interface) has a practical example on
how to add a bottom panel to the CMS UI.
The various panels and UI components within them are loosely coupled to the layout engine through the `data-layout-type`
attribute. The layout is triggered on the top element and cascades into children, with a `redraw` method defined on
each panel and UI component that needs to update itself as a result of layouting.
The various panels and UI components within them are not tightly coupled
to the layout engine, so any changes in dimension which impact the overall layout
need to be handled manually. In SilverStripe, we've established a convention for a `redraw()`
method on each panel and UI component for this purpose, which is usually invoked
through its parent container. Invocation order is crucial here, generally going from
innermost to outermost elements. For example, the tab panels have be applied in the CMS form
before the form itself is layouted with its sibling panels to avoid incorrect dimensions.
![Layout variations](_images/cms-architecture.png)
Refer to [Layout reference](../reference/layout) for further information.
## Forms
@ -497,4 +483,5 @@ through the `PjaxResponseNegotiator` class (see above).
* [Howto: Extend the CMS Interface](../howto/extend-cms-interface)
* [Howto: Customize the CMS tree](../howto/customize-cms-tree)
* [Reference: ModelAdmin](../reference/modeladmin)
* [Reference: Layout](../reference/layout)
* [Topics: Rich Text Editing](../topics/rich-text-editing)

115
docs/en/reference/layout.md Normal file
View File

@ -0,0 +1,115 @@
# CMS layout
## Overview
The CMS markup is structured into "panels", which are the base units containing interface components (or other panels),
as declared by the class `cms-panel`. Panels can be made collapsible, and get the ability to be resized and aligned with
a layout manager, in our case [jLayout](http://www.bramstein.com/projects/jlayout/). This layout manager applies CSS
declarations (mostly dimensions and positioning) via JavaScript.
We've established a convention for a `redraw` method on each panel and UI component that need to update their content as
a result of changes to their position, size or visibility. This method would usually be invoked by the parent container.
The layout manager does not dynamically track changes to panel sizes - we have to trigger laying out manually each time
we need an update to happen (for example from `window::onresize` event, or panel toggling). It then cascades through the
children setting sizes and positions, which in turn requires redrawing of some of the elements.
The easiest way to update the layout of the CMS is to call `redraw` on the top-level `.cms-container` element.
:::js
$('.cms-container').redraw();
This causes the framework to:
* reset the _threeColumnCompressor_ algorithm with the current layout options (that can be set via
`updateLayoutOptions`)
* trigger `layout` which cascades into all children resizing and positioning subordinate elements (this is internal
to the layout manager)
* trigger `redraw` on children which also cascades deeper into the hierarchy (this is framework activity)
Caveat #1: `layout` is also triggered when a DOM element is replaced with AJAX in `LeftAndMain::handleAjaxResponse`. In
this case it is triggered on the parent of the element being replaced so jLayout has a chance to rebuild its algorithms.
Calling the top level `layout` is not enough as it will wrongly descend down the detached element's hierarchy.
Caveat #2: invocation order of the `redraws` is crucial here, generally going from innermost to outermost elements. For
example, the tab panels have be applied in the CMS form before the form itself is layouted with its sibling panels to
avoid incorrect dimensions.
![Layout variations](_images/cms-architecture.png)
## Layout API
### redraw
Define `redraw` methods on panels that need to adjust themselves after their sizes, positions or visibility have been
changed.
Call `redraw` on `.cms-container` to re-layout the CMS.
### data-layout-type attribute
Layout manager will automatically apply algorithms to the children of `.cms-container` by inspecting the
`data-layout-type` attribute. Let's take the content toolbar as an example of a second-level layout application:
:::html
<div class="cms-content-tools west cms-panel cms-panel-layout"
data-expandOnClick="true"
data-layout-type="border"
id="cms-content-tools-CMSMain">
<%-- content utilising border's north, south, east, west and center classes --%>
</div>
For detailed discussion on available algorithms refer to
[jLayout algorithms](https://github.com/bramstein/jlayout#layout-algorithms).
Our [Howto: Extend the CMS Interface](../howto/extend-cms-interface) has a practical example on how to add a bottom
panel to the CMS UI.
### Methods
The following methods are available as an interface to underlying _threeColumnCompressor_ algorithm on the
`.cms-container` entwine:
* _getLayoutOptions_: get currently used _threeColumnCompressor_ options.
* _updateLayoutOptions_: change specified options and trigger the laying out:
`$('.cms-container').updateLayoutOptions({mode: 'split'});`
* _splitViewMode_: enable side by side editing.
* _contentViewMode_: only menu and content areas are shown.
* _previewMode_: only menu and preview areas are shown.
### CSS classes
If as a result of alogorithm's calculations the column becomes hidden, `column-hidden` class is added to it.
## ThreeColumnCompressor
You might have noticed that the top-level `.cms-container` has the `data-layout-type` set to `custom`. We use an inhouse
_threeColumnCompressor_ algorithm for the layout of the menu, content and preview columns of the CMS. The annotated code
for this algorithm can be found in `LeftAndMain.Layout.js`.
Since the layout-type for the element is set to `custom` and will be ignored by the layout manager, we apply the
_threeColumnCompressor_ explicitly `LeftAndMain::redraw`. This way we also get a chance to provide options expected
by the algorithm that are initially taken from the `LeftAndMain::LayoutOptions` entwine variable.
### Factory method
Use provided factory method to generate algorithm instances.
:::js
jLayout.threeColumnCompressor(<column-spec-object>, <options-object>);
The parameters are as follows:
* **column-spec-object**: object providing the _menu_, _content_ and _preview_ elements (all fields mandatory)
* **options-object**: object providing the configuration (all fields mandatory, see options below)
### Available options
* _minContentWidth_: minimum size for the content display as long as the preview is visible
* _minPreviewWidth_: preview will not be displayed below this size
* _mode_: one of "split", "content" (content-only), "preview" (preview-only)
## Related
* [Reference: CMS Architecture](../reference/cms-architecture)
* [Howto: Extend the CMS Interface](../howto/extend-cms-interface)