Create an animating slider with content area
The content area is a is a list of multiple content references. That should work well for creating a slider, slideshow or whatever you call it. A list of items where not all of them is visible at the same time. There are too many different JavaScript implementations for this. Depending on which you use, you might need to tweak the content area a little. Usually should be a list of items, but some also wants extra containers.
About the overlays
Before we start, let's mention the overlays. Edit mode uses overlays for the editable and visible properties on a page. The overlays are the clickable areas and the drop targets. When a page has loaded in the iframe, the editing base finds all nodes that has a data-epi-property-name attribute. These nodes are passed to a overlayfactory which will create overlay items for the nodes. The overlays are absolutely positioned to cover the corresponding node in the template page. This means that all nodes in the template that should be clickable to edit, must at least occupy some space. Floated elements in the template page can make the overlay get incorrect position and size. As the content area adds container element around each block, it's easy to get the overlay misaligned by put floating styles on the block itself, but not on the block container. A container node with no styling and floated child elements will get zero height. The overlay doesn't take overflow: hidden into the positioning either, which is a common issue when dealing with sliders. The content area uses a special overlay which has child block overlays. This is for sorting in on page edit, actions through context menu and also the message to add blocks or pages when the area is empty.
First attempt to create a slider with a content area
I'll use AnythingSlider as the example and use it in the Alloy templates. This script will create wrap the list in additional container Download it and put it in the Static folder and name it "anythingslider". The examples are for WebForms, Joel wrote about custom rendering in MVC: Custom rendering of content areas.
Add a new content area called SliderContentArea to the start page model (EPiServer.Templates.Alloy.Models.Pages.StartPage)
[Display(
GroupName = SystemTabNames.Content,
Order = 330)]
[CultureSpecific]
public virtual ContentArea SliderContentArea { get; set; }
Add the content area to the page template (EPiServer.Templates.Alloy.Views.Pages.StartPageTemplate)
To simplify these examples I put the scripts and styles in the start page template.
<alloy:stylesheetlink runat="server" href="~/Static/anythingslider/css/anythingslider.css" />
<alloy:scriptlink runat="server" src="~/Static/anythingslider/js/jquery.anythingslider.js" />
<style>
.slider {
width: 400px;
height: 240px;
}
.slider .mediaimg img {
width: 100%;
}
.slider .border {
width: 398px;
height: 238px;
}
</style>
<!-- row and spanX is for Bootstrap framework -->
<div class="row">
<div class="span3">
<!-- property to illustrate problem with overlay -->
<episerver:property id="Property3" runat="server" propertyname="TeaserText"></episerver:property>
</div>
<div class="span9">
<!-- render content area as a list -->
<episerver:property id="Property1" runat="server" propertyname="SliderContentArea">
<rendersettings customtagname="ul" cssclass="slider" childrencustomtag="li" /> </episerver:property>
</div>
</div>
<!-- AnythingSlider initialization -->
<script>
// DOM Ready
$(function(){
// Create the slider
$('.slider').anythingSlider({
autoPlay:true,
buildStartStop:false,
buildNavigation:false,
enableNavigation:false,
enableStartStop:false
});
});
</script>
Add 5 blocks to content area, use the start page blocks (except the jumbotron block).
Notice that all blocks are shown in a column, no slider yet. The content area overlay is also too big. This is because the slider script is only initialized when page is loaded the first time. When the property is updated with new items, only the dom nodes inside the content area are replaced.
Publish the page and when the page reloads there will be two slider on the page and looks better, except there are two sliders and extra block overlays that appears when the mouse cursor is beside the content area overlay. The teaser text property gets a little less accessible, even though there still are some pixels left to click it. The overlay positioning the blocks overlays over where the actual node is. The double sliders are due to the custom content area in Alloy templates, with max three items per row. Read more about the templates in Ted's post: Templates for EPiServer 7 CMS.
Fix the slider script initialization
Add it full page refresh to get the script render whenever we add a block. This is essential to have the slider be recreated when we add, remove or rearrange blocks.
public partial class StartPageTemplate : SiteTemplatePage
{
protected override void OnLoad(EventArgs e)
{
// Trigger full page reload when block is updated in edit mode
// (requires FullRefreshPropertiesMetaData control in Root.Master)
var page = Page as PageBase;
if (page != null)
{
if (!page.EditHints.Contains("SliderContentArea"))
{
page.EditHints.Add("SliderContentArea");
}
}
}
}
Fix the rendering
We can implement a custom content area that fixes the rendering as the normal content area, but it will also support extra rendering options. The RenderSettings is a dictionary and can included not known keys for a more generic content area. This implementation adds support for two extra keys innerCustomTagName and innerCssClass. The downside with custom keys are that you get no intellisense for them. So if you use a slider script that want an extra container around the list, this will do the job for you. Otherwise it'll work as the default content area implementation.
Create new class in EPiServer.Templates.Alloy.Business.WebControls.
Custom content area
using System;
using System.Web.UI.HtmlControls;
using EPiServer.Core;
using EPiServer.Web.PropertyControls;
using EPiServer.Web.WebControls;
using EPiServer.Framework.DataAnnotations;
using EPiServer.Web;
namespace EPiServer.Templates.Alloy.Business.WebControls
{
///
/// Provides custom rendering of content areas to enable one more level of markup
///
[TemplateDescriptor(TagString = "SliderContentAreaTag")]
public class SliderContentAreaControl : PropertyContentAreaControl, IRenderTemplate
{
///
/// Creates block controls for the blocks in the block area. Used when the property is in view mode
/// or in "on page edit" mode and the PropertyDataControl does not support on page editing.
///
public override void CreateDefaultControls()
{
CreateContentAreaControls(false);
}
///
/// Creates the "on page edit" controls with the blocks.
/// If no block exist, this method will do nothing.
///
public override void CreateOnPageEditControls()
{
CreateContentAreaControls(PropertyIsEditableForCurrentLanguage());
}
private void CreateContentAreaControls(bool enableEditFeatures)
{
// Get list of block renderers
var controlList = GetContentRenderers(EnableEditFeaturesForChildren || enableEditFeatures);
// get outer container tag name
var containerTagName = !String.IsNullOrWhiteSpace(CustomTagName) ? CustomTagName : "div";
// get inner container tag name
var innerContainerTagName = "div";
// use custom render settings key
if (RenderSettings.ContainsKey("innerCustomTagName"))
{
var tag = (string)RenderSettings["innerCustomTagName"];
if (!String.IsNullOrWhiteSpace(tag))
{
innerContainerTagName = tag;
}
}
// Create containers
HtmlGenericControl container = CreateMainContainer(enableEditFeatures, containerTagName);
HtmlGenericControl innerContainer = new HtmlGenericControl(innerContainerTagName);
// add class for inner
// use custom render settings key for classs
if (RenderSettings.ContainsKey("innerCssClass"))
{
var cssClass = (string)RenderSettings["innerCssClass"];
if (!String.IsNullOrWhiteSpace(cssClass))
{
innerContainer.Attributes.Add("class", cssClass);
}
}
// Add controls
this.Controls.Add(container);
container.Controls.Add(innerContainer);
foreach (ContentRenderer block in controlList)
{
innerContainer.Controls.Add(block);
block.EnsureChildControlsCreated();
}
}
/// The default implementation simply checks the IsNull property
protected override bool ShouldCreateDefaultControls()
{
return PropertyData != null && !PropertyData.IsNull;
}
}
}
Use the custom content area for slider property
Add Tag to render settings on Change the property name to get the correct rendering, now we don't get the extra classes used for the Alloy content areas and the blocks aren't wrapped on multiple rows. I also added support for setting an innerContainer to the inner class. Note the use of Tag to make sure we get SliderContentAreaControl and not the SitePropertyContentAreaControl (which is the default ).
Reload edit and there's only one slider, but the overlay for the content area is now even worse. If look at the page on the site the structure this for the slider:
AnythingSlider wraps the slider in two divs, the inner is a viewport with overflow hidden. Let's look at the rendered slider i edit mode. I usually hold down control/command and click on a page in the top navigation inside the preview frame. The template page will be opened in a new tab with the needed query string parameters ( .../,,4/?id=4&epieditmode=true ). Inspect the slider and we'll see that the data-epi-attributes are attached to the slider UL. This causes the overlay to be too big because it will position itself according to the UL. We want the epi attributes to be on the anythingSlider div.
Move the edit attributes
Let's add some script to fix this. The script in the template page will execute before the edit UI finds the editable nodes, so we can move the attributes without any problem.
// DOM Ready
$(function () {
// Create the slider
$('.slider').anythingSlider({
autoPlay: true,
buildStartStop: false,
buildNavigation: false,
enableNavigation: false,
enableStartStop: false
}).each(function (index, item) {
// move all data-epi-attributes to the injected parent slider node
var i;
var epiAttributes = {};
var attr;
var name;
for (i = item.attributes.length - 1; i >= 0; i--) {
attr = item.attributes[i];
name = attr.nodeName;
// starts with "data-epi"?
if (name.indexOf("data-epi-") === 0) {
epiAttributes[name] = attr.nodeValue;
item.removeAttribute(name);
attr = null;
}
}
// set attributes to parent node
$(item).parents(".anythingSlider").attr(epiAttributes);
});
});
Now we'll get a better sized overlay for the content area. The overlay is now only calculated for the outer property. But there's still the block overlays that are in the way. We can use the default property overlay to remove the block overlays.
Use default overlay
Create an editor descriptor
Create a new class called SliderPropertyContentAreaEditorDescriptor.cs. In the editor descriptor we can specify a custom overlay, or use default by not specify any at all. The default overlay can't handle list items and we must remove drag and drop support.
using EPiServer.Core;
using EPiServer.Shell.ObjectEditing;
using EPiServer.Shell.ObjectEditing.EditorDescriptors;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace EPiServer.Templates.Alloy.Business.EditorDescriptors
{
[EditorDescriptorRegistration(TargetType = typeof(ContentArea), UIHint = "SliderPropertyContentArea")]
public class SliderPropertyContentAreaEditorDescriptor : EditorDescriptor
{
public SliderPropertyContentAreaEditorDescriptor()
{
ClientEditingClass = "epi-cms.contentediting.editors.ContentAreaEditor";
}
public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable attributes)
{
base.ModifyMetadata(metadata, attributes);
// cancel DnD
metadata.AdditionalValues["DropTargetType"] = new string[] { };
}
}
}
Use the editor descriptor for the slider property
Annotate the model with UIHint to get the custom overlay in edit mode. It's the key to be able to use this for a specific property in the model.
[UIHint("SliderPropertyContentArea")]
public virtual ContentArea SliderContentArea { get; set; }
The final result
Reload edit to get the final result. Items can be added, removed and reordered in the flyout editor.
Hi Per. This looks very interesting. Would it be possible for you to provide an Add-on, Nuget package, or package of the final code with any necessary instructions?
Was this ever made into a Nuget package?
Hi Per, why i can't seem to make this work...