Archive for category Page Editor / Experience Editor

EditFrame vs Experience Editor Buttons in Sitecore

EditFrame and Experience editor buttons are both great ways of adding experience editor support to our Sitecore pages.

  • EditFrame buttons
    • Create item in core database (I usually just duplicate the Default folder at /sitecore/content/Applications/WebEdit/Edit Frame Buttons/Default). You can add the field(s) you want to make editable in the ‘Fields’ field
    • In your view, you need to wrap the html that you want to make editable, with the EditFrame code, where you need to supply it with the path of your edit frame button in core, and the item ID of the item you want to edit. The fields made editable would depend on the fields you include in the EditFrame button item as shown in the above image.
      @using (BeginEditFrame("/sitecore/content/Applications/WebEdit/Edit Frame Buttons/Headline", Model.Id.ToString()))
      {
      <div>@Model.Headline</div>
      }
      

    You will now be able to see additional buttons around the html you enclosed in the EditFrame code

  • Experience Editor Buttons
    • Create button item in Core database – use the template – /sitecore/templates/System/WebEdit/Field Editor Button and create the /sitecore/content/Applications/WebEdit/Custom Experience Buttons
    • In your rendering, select the button you just created in the ‘Experience Editor Buttons’ field

    The additional button you created will now be available when you select the entire rendering. If the datasource is available, that item is considered for editing and the field name mentioned in the experience editor button item in core is searched for in that item, else the context item is considered for the same.

Points to note about these 2 implementations:

EditFrame Buttons Experience Editor Buttons
Providing fields to edit Pipe separated – In the core item for the button Pipe separated – In the core item for the button
Item to be edited Item id passed in, in the view code The datasource item is used if available, else the context item is used. This is irrespective of whether the fields mentioned in the button item exist in the datasource item.
Code change required Yes, code needs to be added in the view as shown above. No
Html to be selected  Only the html around which the EditFrame code is wrapped needs to be selected to see the button. The available buttons show up when the entire rendering is selected.
Flexibility  You can choose to edit any number of items selecting any single html tag as is the requirement. Only 1 item can be edited per rendering (datasource or context item), and individual html elements cannot be selected to see the button(s).

, , ,

Leave a comment

Add workflow notifications for all editable items on a Sitecore page

While editing a page in experience editor mode, when we edit the fields which are a part of the context item, if the item goes into the edit stage in the workflow, we see a notification on saving the item.
This notification tells us that the item needs to go through workflow to be published.

2016-11-09_212657

Now, if a module on the page gets data from a rendering datasource item, and we edit that content, there is no notification on the page on item save, even though the datasource item would infact need to go through the workflow to be able to be published.
We used this excellent module form Sitecore marketplace to resolve this issue: Datasource Workflow Module

Now we get additional notifications when we edit rendering datasource items.

2016-11-09_214522

However this doesn’t address reference items that the datasource item might refer to.

So what we did to resolve this issue, is marry the code of this module with code to collect all items which are editable on the current context page (Thanks to this post!). So while we render the page, we collect all editable items on the page and then run a check on this list, to find all items which are not in the final stage of workflow, and show notifications in the notification area for all these items.

Following are the config updates you will need:

<configuration>
  <sitecore>
      <pipelines>
        <renderField>
          <processor type="MySite.Sitecore.Pipelines.RenderField.CollectItemReferences, MySite.Sitecore"></processor>
        </renderField>
        <getPageEditorNotifications>
          <processor type="MySite.Sitecore.Pipelines.GetPageEditorNotifications.GetReferenceWorkflowNotification, MySite.Sitecore" />
        </getPageEditorNotifications>
        <getContentEditorWarnings>
          <processor type="MySite.Sitecore.Pipelines.GetContentEditorWarnings.GetReferenceWorkflowNotification, MySite.Sitecore" />
        </getContentEditorWarnings>
        <getChromeData>
          <processor type="MySite.Sitecore.Pipelines.GetChromeData.EditFrameCollectItemReferences, MySite.Sitecore"></processor>
        </getChromeData>
      </pipelines>
      <setting name="ReferenceWorkflowNotification.ShowWorkflowNoAccessMessage" value="true" />
      <setting name="ReferenceWorkflowNotification.UseHtmlInMessaging" value="true" />
    </settings>
    <referenceWorkflowNotificationItems>
      <webEdit>
        <itemReferences type="MySite.Sitecore.Models.ItemReferenceCollection, MySite.Sitecore" singleInstance="true"></itemReferences>
      </webEdit>
    </referenceWorkflowNotificationItems>
  </sitecore>
</configuration>

Following is the code in use:

using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Layouts;
using Sitecore.Rules.ConditionalRenderings;
using Sitecore.SecurityModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web;
using MySite.Sitecore.Models;
using Sc = Sitecore;

namespace MySite.Sitecore.Extensions
{
    public static class ItemExtensions
    {	
        private const string ContentEditorUrlFormat = "{0}://{1}/sitecore/shell/Applications/Content Editor?id={2}&amp;sc_content=master&amp;fo={2}&amp;vs={3}&amp;la={4}";

        public static RenderingReference[] GetRenderingReferences(this Item i)
        {
            return i == null ? new RenderingReference[0] : i.Visualization.GetRenderings(Sc.Context.Device, false);
        }

        public static List<Item> GetAllUniqueDataSourceItems(this Item i, bool includePersonalizationDataSourceItems = true, bool includeMultiVariateTestDataSourceItems = true)
        {
            var list = new List<Item>();
            foreach (RenderingReference reference in i.GetRenderingReferences())
            {
                list.AddUnqiueDataSourceItem(reference.GetDataSourceItem());

                if (includePersonalizationDataSourceItems)
                {
                    foreach (var dataSourceItem in reference.GetPersonalizationDataSourceItem())
                    {
                        list.AddUnqiueDataSourceItem(dataSourceItem);
                    }
                }

                if (includeMultiVariateTestDataSourceItems)
                {
                    foreach (Item dataSourceItem in reference.GetMultiVariateTestDataSourceItems())
                    {
                        list.AddUnqiueDataSourceItem(dataSourceItem);
                    }
                }
            }

            list.AddRange(AddDatasourceReferenceItems(i));
            list = list.DistinctBy(l => l.ID).ToList();

            return list;
        }

        private static List<Item> AddDatasourceReferenceItems(Item context)
        {
            return ItemReferenceCollection.FromConfiguration().GetItemReferences(context.ID.Guid)
                .Select(x => context.Database.GetItem(new ID(x))).ToList();
        }

        public static List<Item> GetDataSourceItems(this Item i)
        {
            List<Item> list = new List<Item>();
            foreach (RenderingReference reference in i.GetRenderingReferences())
            {
                Item dataSourceItem = reference.GetDataSourceItem();
                if (dataSourceItem != null)
                {
                    list.Add(dataSourceItem);
                }
            }
            return list;
        }

        public static Item GetDataSourceItem(this RenderingReference reference)
        {
            if (reference != null)
            {
                return GetDataSourceItem(reference.Settings.DataSource, reference.Database);
            }
            return null;
        }

        public static List<Item> GetPersonalizationDataSourceItems(this Item i)
        {
            var list = new List<Item>();
            foreach (var reference in i.GetRenderingReferences())
            {
                list.AddRange(reference.GetPersonalizationDataSourceItem());
            }
            return list;
        }

        private static IEnumerable<Item> GetPersonalizationDataSourceItem(this RenderingReference reference)
        {
            var list = new List<Item>();
            if (reference != null && reference.Settings.Rules != null && reference.Settings.Rules.Count > 0)
            {
                list.AddRange(reference.Settings.Rules.Rules.SelectMany(r => r.Actions).OfType<SetDataSourceAction<ConditionalRenderingsRuleContext>>().Select(setDataSourceAction => GetDataSourceItem(setDataSourceAction.DataSource, reference.Database)).Where(dataSourceItem => dataSourceItem != null));
            }
            return list;
        }

        public static List<Item> GetMultiVariateTestDataSourceItems(this Item i)
        {
            var list = new List<Item>();
            foreach (var reference in i.GetRenderingReferences())
            {
                list.AddRange(reference.GetMultiVariateTestDataSourceItems());
            }
            return list;
        }

        private static IEnumerable<Item> GetMultiVariateTestDataSourceItems(this RenderingReference reference)
        {
            var list = new List<Item>();
            if (reference != null && !string.IsNullOrEmpty(reference.Settings.MultiVariateTest))
            {
                using (new SecurityDisabler())
                {
                    // Sitecore 7 to Sitecore 8:
                    var mvVariateTestForLang = Sc.Analytics.Testing.TestingUtils.TestingUtil.MultiVariateTesting.GetVariableItem(reference);
                    
                    // Sitecore 8.1 and above:
                    //var contentTestStore = new Sitecore.ContentTesting.Data.SitecoreContentTestStore();
                    //var mvVariateTestForLang =  contentTestStore.GetMultivariateTestVariable(reference, reference.Language);
                    
                    var variableItem = (mvVariateTestForLang != null) ? mvVariateTestForLang.InnerItem : null;
                    if (variableItem == null) return list;
                    foreach (Item mvChild in variableItem.Children)
                    {
                        var mvDataSourceItem = mvChild.GetInternalLinkFieldItem("Datasource");
                        if (mvDataSourceItem != null)
                        {
                            list.Add(mvDataSourceItem);
                        }
                    }
                }
            }
            return list;
        }

        private static bool ListContainsItem(IEnumerable<Item> list, Item dataSourceItem)
        {
            var uniqueId = dataSourceItem.GetUniqueId();
            return list.Any(e => e.GetUniqueId() == uniqueId);
        }

        private static void AddUnqiueDataSourceItem(this List<Item> list, Item dataSourceItem)
        {
            if (dataSourceItem != null && !ListContainsItem(list, dataSourceItem))
            {
                list.Add(dataSourceItem);
            }
        }

        private static Item GetDataSourceItem(string id, Database db)
        {
            Guid itemId;
            return Guid.TryParse(id, out itemId)
                                    ? db.GetItem(new ID(itemId))
                                    : db.GetItem(id);
        }

        public static string GetContentEditorUrl(this Item i)
        {
            var requestUri = HttpContext.Current.Request.Url;
            return string.Format(ContentEditorUrlFormat, requestUri.Scheme, requestUri.Host, WebUtility.HtmlEncode(i.ID.ToString()), i.Version.Number, i.Language.CultureInfo.Name);
        }

        public static Item GetInternalLinkFieldItem(this Item i, string internalLinkFieldName)
        {
            if (i == null) return null;
            InternalLinkField ilf = i.Fields[internalLinkFieldName];
            if (ilf != null && ilf.TargetItem != null)
            {
                return ilf.TargetItem;
            }
            return null;
        }

        public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> items, Func<T, TKey> property)
        {
            return items.GroupBy(property).Select(x => x.First());
        }
    }
}
using Sitecore.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using Sc = Sitecore;

namespace MySite.Sitecore.Models
{
    public class ItemReferenceCollection : IItemReferenceCollection
    {
        protected static object SyncRoot = new object();
        protected IDictionary<Guid, IDictionary<Guid, Guid>> ItemReferences = new Dictionary<Guid, IDictionary<Guid, Guid>>();

        ///
<summary>
        /// Adds the item reference.
        /// </summary>

        /// <param name="itemId">The item identifier.</param>
        public void AddItemReference(Guid itemId)
        {
            lock (SyncRoot)
            {
                var contextItem = Sc.Context.Item;

                if (contextItem == null)
                {
                    return;
                }

                if (ItemReferences.ContainsKey(contextItem.ID.Guid))
                {
                    if (!ItemReferences[contextItem.ID.Guid].ContainsKey(itemId))
                    {
                        ItemReferences[contextItem.ID.Guid].Add(itemId, itemId);
                    }
                }
                else
                {
                    ItemReferences.Add(contextItem.ID.Guid, new Dictionary<Guid, Guid> { { itemId, itemId } });
                }
            }
        }

        ///
<summary>
        /// Adds the item references.
        /// </summary>

        /// <param name="itemIds">The item ids.</param>
        public void AddItemReferences(IEnumerable<Guid> itemIds)
        {
            foreach (var itemId in itemIds)
            {
                AddItemReference(itemId);
            }
        }

        ///
<summary>
        /// Gets the item references.
        /// </summary>

        /// <param name="currentItemId">The current item identifier.</param>
        /// <returns></returns>
        public IEnumerable<Guid> GetItemReferences(Guid currentItemId)
        {
            lock (SyncRoot)
            {
                if (ItemReferences.ContainsKey(currentItemId))
                {
                    return ItemReferences[currentItemId].Values.ToArray();
                }
            }

            return new List<Guid>();
        }

        ///
<summary>
        /// Clears the specified item references.
        /// </summary>

        /// <param name="currentItemId">The current item identifier.</param>
        public void Clear(Guid currentItemId)
        {
            lock (SyncRoot)
            {
                if (ItemReferences.ContainsKey(currentItemId))
                {
                    ItemReferences.Remove(currentItemId);
                }
            }
        }

        ///
<summary>
        /// Gets the instance configured in Sitecore config using Sitecore.Configuration.Factory.
        /// </summary>

        /// <returns></returns>
        public static IItemReferenceCollection FromConfiguration()
        {
            return Factory.CreateObject("referenceWorkflowNotificationItems/webEdit/itemReferences", false) as IItemReferenceCollection;
        }
    }

    public interface IItemReferenceCollection
    {
        void AddItemReference(Guid itemId);

        void AddItemReferences(IEnumerable<Guid> itemIds);

        IEnumerable<Guid> GetItemReferences(Guid currentItemId);

        void Clear(Guid currentItemId);
    }
}
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using Sitecore.Security.AccessControl;
using Sitecore.Workflows;
using Sc = Sitecore;

namespace MySite.Sitecore.Models
{
    public class ItemWorkflowModel
    { 
        #region Fields

        private readonly Item _contextItem;
        private readonly Database _database;
        private readonly IWorkflowProvider _workflowProvider;
        private readonly IWorkflow _workflow;
        private readonly WorkflowState _workflowState;
        WorkflowCommand[] _commands;
        private bool? _showWorkflowNoAccessMessage;
        private const string ShowWorkflowNoAccessMessageKey = "ReferenceWorkflowNotification.ShowWorkflowNoAccessMessage";

        #endregion

        #region Properties

        public Item ContextItem
        {
            get
            {
                return _contextItem;
            }
        }

        public Database Database
        {
            get
            {
                return _database;
            }
        }

        public IWorkflowProvider WorkflowProvider
        {
            get
            {
                return _workflowProvider;
            }
        }

        public IWorkflow Workflow
        {
            get
            {
                return _workflow;
            }
        }
        public WorkflowState WorkflowState
        {
            get
            {
                return _workflowState;
            }
        }

        public WorkflowCommand[] Commands
        {
            get
            {
                if (_commands == null)
                {
                    _commands = WorkflowFilterer.FilterVisibleCommands(Workflow.GetCommands(ContextItem));
                    if (_commands == null)
                    {
                        _commands = new WorkflowCommand[0];
                    }
                }
                return _commands;
            }
        }

        public bool ShowWorkflowNoAccessMessage
        {
            get
            {
                if (!_showWorkflowNoAccessMessage.HasValue)
                {
                    _showWorkflowNoAccessMessage = Sc.Configuration.Settings.GetBoolSetting(ShowWorkflowNoAccessMessageKey, false);
                }
                return _showWorkflowNoAccessMessage.Value;
            }
        }

        public bool ShowNotification
        {
            get
            {
                return NoNulls && !WorkflowState.FinalState && (Commands.Length > 0 && HasWriteAccess() || ShowWorkflowNoAccessMessage);
            }
        }

        public bool NoNulls
        {
            get
            {
                return (Database != null && WorkflowProvider != null && Workflow != null && WorkflowState != null);
            }
        }

        private bool UseHtmlMessaging
        {
            get
            {
                return Sc.Configuration.Settings.GetBoolSetting("ReferenceWorkflowNotification.UseHtmlInMessaging", false);
            }
        }

        #endregion

        #region Constructor

        public ItemWorkflowModel(Item i)
        {
            if (i != null)
            {
                _contextItem = i;
                _database = i.Database;
                if (_database != null)
                {
                    _workflowProvider = _database.WorkflowProvider;
                    if (_workflowProvider != null)
                    {
                        _workflow = _workflowProvider.GetWorkflow(ContextItem);
                        if (_workflow != null)
                        {
                            _workflowState = _workflow.GetState(ContextItem);
                        }
                    }
                }
            }
        }

        #endregion

        #region Methods

        public bool HasWriteAccess()
        {
            return AuthorizationManager.IsAllowed(ContextItem, AccessRight.ItemWrite, Sc.Context.User);
        }

        public string GetEditorDescription(bool isPageEditor = true)
        {
            string noAccessMessage = string.Empty;
            string workflowDisplayName = Workflow.Appearance.DisplayName;
            string workflowStateDisplayName = WorkflowState.DisplayName;
            string itemDisplayName = ContextItem.Paths.ContentPath;

            if (UseHtmlMessaging && !isPageEditor)
            {
                workflowDisplayName = BoldValue(workflowDisplayName);
                workflowStateDisplayName = BoldValue(workflowStateDisplayName);
                itemDisplayName = BoldValue(itemDisplayName);
            }
            else
            {
                workflowDisplayName = SingleQuoteValue(workflowDisplayName);
                workflowStateDisplayName = SingleQuoteValue(workflowStateDisplayName);
                itemDisplayName = SingleQuoteValue(itemDisplayName);
            }

            if (!HasWriteAccess())
            {
                noAccessMessage = " You cannot change the workflow because do not have write access to this item.";
            }

            return Translate.Text("The data source item {0} is in the {1} state of the {2} workflow.{3}", itemDisplayName, workflowStateDisplayName, workflowDisplayName, noAccessMessage);
        }

        private static string SingleQuoteValue(string value)
        {
            return string.Format("'{0}'", value);
        }

        private static string BoldValue(string value)
        {
            return string.Format("<b>{0}</b>", value);
        }

        #endregion
    }
}
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetChromeData;
using System;

namespace MySite.Sitecore.Pipelines.GetChromeData
{
    public class EditFrameCollectItemReferences : GetChromeDataProcessor
    {
        public override void Process(GetChromeDataArgs args)
        {
            try
            {
                if (CanCollectReferences(args))
                {
                    Models.ItemReferenceCollection.FromConfiguration().AddItemReference(args.Item.ID.Guid);
                }
            }
            catch (Exception ex)
            {
                Log.Error("Error in EditFrameCollectItemReferences pipeline processor.", ex, this);
            }
        }

        protected virtual bool CanCollectReferences(GetChromeDataArgs args)
        {
            return args != null && args.Item != null && !args.Aborted
                && args.ChromeType != null && args.ChromeData != null
                && "editFrame".Equals(args.ChromeType, StringComparison.OrdinalIgnoreCase)
                && (Context.PageMode.IsPageEditor || Context.PageMode.IsPageEditorEditing);
        }
    }
}
using MySite.Sitecore.Extensions;
using MySite.Sitecore.Models;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetContentEditorWarnings;
using Sitecore.Shell.Framework.CommandBuilders;


namespace MySite.Sitecore.Pipelines.GetContentEditorWarnings
{
    public class GeReferenceWorkflowNotification
    {
        #region Methods

        public void Process(GetContentEditorWarningsArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            if (arguments.Item != null)
            {
                foreach (var ds in arguments.Item.GetAllUniqueDataSourceItems())
                {
                    GetNotifications(arguments, ds);
                }
            }
        }

        public void GetNotifications(GetContentEditorWarningsArgs arguments, Item contextItem)
        {
            if (arguments == null) return;
            var wfModel = new ItemWorkflowModel(contextItem);
            if (wfModel.ShowNotification)
            {
                SetNotification(arguments, wfModel);
            }
        }

        private void SetNotification(GetContentEditorWarningsArgs arguments, ItemWorkflowModel wfModel)
        {
            var editorNotification = arguments.Add();
            editorNotification.Title = "Datasource Item in Workflow";
            editorNotification.Text = wfModel.GetEditorDescription(false);
            editorNotification.Icon = wfModel.WorkflowState.Icon;
            if (wfModel.HasWriteAccess())
            {
                foreach (var command in wfModel.Commands)
                {
                    editorNotification.AddOption(command.DisplayName, new WorkflowCommandBuilder(wfModel.ContextItem, wfModel.Workflow, command).ToString());
                }
            }
        }

        #endregion
    }
}
using MySite.Sitecore.Extensions;
using MySite.Sitecore.Models;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetPageEditorNotifications;
using Sitecore.Shell.Framework.CommandBuilders;
using System.Linq;


namespace MySite.Sitecore.Pipelines.GetPageEditorNotifications
{
    public class GetReferenceWorkflowNotification : GetPageEditorNotificationsProcessor
    {
        #region Methods

        public override void Process(GetPageEditorNotificationsArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            if (arguments.ContextItem == null) return;
            foreach (var dataSourceArguments in arguments.ContextItem.GetAllUniqueDataSourceItems().Select(ds => new GetPageEditorNotificationsArgs(ds)))
            {
                GetNotifications(dataSourceArguments);
                arguments.Notifications.AddRange(dataSourceArguments.Notifications);
            }
        }

        public void GetNotifications(GetPageEditorNotificationsArgs arguments)
        {
            if (arguments == null) return;
            var wfModel = new ItemWorkflowModel(arguments.ContextItem);
            if (wfModel.ShowNotification)
            {
                SetNotification(arguments, wfModel);
            }
        }

        private void SetNotification(GetPageEditorNotificationsArgs arguments, ItemWorkflowModel wfModel)
        {
            var editorNotification = new PageEditorNotification(wfModel.GetEditorDescription(), PageEditorNotificationType.Warning)
            {
                Icon = wfModel.WorkflowState.Icon
            };
            if (wfModel.HasWriteAccess())
            {
                foreach (var notificationOption in wfModel.Commands
                    .Select(command => new PageEditorNotificationOption(command.DisplayName, new WorkflowCommandBuilder(wfModel.ContextItem, wfModel.Workflow, command).ToString())))
                {
                    editorNotification.Options.Add(notificationOption);
                }
            }
            arguments.Notifications.Add(editorNotification);
        }

        #endregion
    }
}
using MySite.Sitecore.Models;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.RenderField;
using System;

namespace MySite.Sitecore.Pipelines.RenderField
{
    public class CollectItemReferences
    {
        public void Process(RenderFieldArgs args)
        {
            try
            {
                if (CanCollectReferences(args))
                {
                    ItemReferenceCollection.FromConfiguration().AddItemReference(args.Item.ID.Guid);
                }
            }
            catch(Exception ex)
            {
                Log.Error("Error in CollectItemReferences pipeline processor.", ex, this);
            }
        }

        protected virtual bool CanCollectReferences(RenderFieldArgs args)
        {
            return args != null && args.Item != null && !args.Aborted 
                && !args.DisableWebEdit && !args.DisableWebEditContentEditing
                && (Context.PageMode.IsPageEditor || Context.PageMode.IsPageEditorEditing);
        }
    }
}
using MySite.Sitecore.Extensions;
using MySite.Sitecore.Models;
using Sitecore.Data.Validators;
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;

namespace MySite.Sitecore.Validators
{
    [Serializable]
    public class ComponentsinFinalWorkflow : StandardValidator
    {
        #region Properties

        public override string Name
        {
            get { return "Component Workflow State"; }
        }

        #endregion

        #region Constructor

        public ComponentsinFinalWorkflow(SerializationInfo info, StreamingContext context) : base(info, context)
        {
        }

        public ComponentsinFinalWorkflow()
        {
        }

        #endregion

        #region Methods

        protected override ValidatorResult Evaluate()
        {
            var item = GetItem();
            var result = ValidatorResult.Valid;
            var paths = new List<string>();
            foreach (var ds in item.GetAllUniqueDataSourceItems())
            {
                var wfModel = new ItemWorkflowModel(ds);
                if (wfModel.NoNulls && !wfModel.WorkflowState.FinalState)
                {
                    paths.Add(ds.Paths.ContentPath);
                    result = GetMaxValidatorResult();
                }
            }

            Text = string.Empty;
            if (paths.Count > 0)
            {
                Text += GetText("The item{0} {1} should be in a final workflow state.", paths.Count > 1 ? "s" : string.Empty, GetPathsString(paths));
            }

            return result;
        }

        private string GetPathsString(List<string> paths)
        {
            string formatString = "\"{0}\"{1}";
            if (paths.Count == 1)
            {
                return string.Format(formatString, paths[0], string.Empty);
            }
            else if (paths.Count == 2)
            {
                return string.Format(formatString, paths[0], " and ") + string.Format(formatString, paths[1], string.Empty);
            }

            string pathsString = string.Empty;
            string separator = ", ";
            for (int i = 0; i < paths.Count; i++)
            {
                if (i == paths.Count - 1)
                {
                    separator = string.Empty;
                }
                if (i == paths.Count - 2)
                {
                    separator = ", and ";
                }
                pathsString += string.Format(formatString, paths[i], separator);
            }
            return pathsString;
        }

        protected override ValidatorResult GetMaxValidatorResult()
        {
            return ValidatorResult.Warning;
        }

        #endregion
    }
}

Once you have these updates in place, you’ll see that there will be a notification for each editable item on the page which is not in the final stage of workflow in content editor and experience editor mode!

2016-11-09_215548

, , , , , , , , , , , , , , , , , ,

Leave a comment

Page editor support in Sitecore for css background images

While displaying images on your site, sometimes it is preferable to use css style background images instead of the img tag.
Some of the reasons I have located for this:

  • It lets you have actual text in the element (offset with CSS so people can’t see it) instead of an alt attribute which is theoretically better food for search engines.
  • It also lets you use sprites (background images containing multiple pictures which are cropped with background-position, height and width) to reduce the numbers of HTTP requests being made for images.
  • Use background-image if you intend to have people print your page and you do not want the image to be included by default.
  • Use background-image if you need for only a portion of the image to be visible, as with CSS sprites.
  • Use background-image with background-size:cover in order to stretch a background image to fill its entire window.
  • Use CSS background images when doing image-replacement of text eg. paragraphs/headers.
  • Use CSS background images if the image is not part of the content.

When we use background images, how do we get page editor support for such images?

One great solution would ofcourse be using Sitecore EditFrames. (EditFrame with Sitecore MVC and Glass.Mapper)

But – this does involve creating additional items in the Core DB, especially if you have different field names for the image fields.
If you wanted to bypass this step, here’s a solution!

For every background image you render – make sure you add a couple of lines for the scenario when this page is loaded in page editor mode:


<figure class="page-banner" style="background-image: url('/en/sitecore/shell/~/media/images/banners/startup.jpg')">
    @if (Sitecore.Context.PageMode.IsPageEditor)
    {
        @RenderImage(m => m.Background_Image, new { @class = "page-editor-editable-image" }, isEditable: true)
    }
</figure>

You just need to make sure that this page editor friendly image (with the special class we define) is on the top of all your content – layer wise (Settings z-index:-1;position:relative to the remaining content will ensure this).

2015-11-01_070436

Once this is done, here’s how your image will appear in page editor mode:

2015-11-01_064908

, , , ,

2 Comments

Allowing highlighted text in simple Sitecore text fields with truncate enabled

In our project, we came across a requirement where the content author wanted to manage text with highlights, and we also needed to be able to truncate this – to show as teasers in certain locations – to maintain the integrity of the design.

2015-07-05_155157

We initially thought of Rich Text fields – but then the business need was to ONLY allow highlighted text as special formats – and nothing else. And this was only for certain fields – so we certainly didn’t want to restrict the rich text editor itself either. Additionally allowing full blown html, made the truncation bit a little dicey.

So here’s the solution we came up with.

We picked a custom tag and had the content authors enter the text with those tags. For example, for the above image, the content managed in the sitecore field (single line or multiline – plain text field) was: [highlight]Test[/highlight] caption with [highlight]highlight[/highlight]

While rendering this field on the view:

2015-07-05_155935

In the above example, we not only transform the input text to include highlights, we are also keeping in mind page editor support, and further – we also limit the output text to a certain character count.

public static string GetHighlightedTextHtml(string textToTransform, int characterLimit = 0, bool appendEllipses = false)
        {
            if (string.IsNullOrWhiteSpace(textToTransform))
            {
                return string.Empty;
            }

            string rawText = textToTransform
               .Replace("[highlight]", string.Empty)
               .Replace("[/highlight]", string.Empty);

            characterLimit = TextUtilityStatic.TruncTextToSpecificNumOfChars(rawText, characterLimit).Length;

            string transformedHtml = textToTransform
                .Replace("[highlight]", "<em class=\"highlight--yellow\">")
                .Replace("[/highlight]", "</em>");

            if ((characterLimit > 0 && characterLimit >= rawText.Length) || characterLimit == 0)
            {
                return transformedHtml;
            }

            bool inTag = false;
            int textCharCounter = 0, htmlCharCounter = 0;

            foreach (Char c in transformedHtml)
            {
                if (c == '<')
                {
                    inTag = true;
                    htmlCharCounter++;
                    continue;
                }

                if (c == '>')
                {
                    inTag = false;
                    htmlCharCounter++;
                    continue;
                }

                if (inTag)
                {
                    htmlCharCounter++;
                    continue;
                }

                // Out of tag
                if (characterLimit <= textCharCounter)
                {
                    break;
                }
                htmlCharCounter++;
                textCharCounter++;
            }

            transformedHtml = transformedHtml.Substring(0, htmlCharCounter);

            if (inTag)
            {
                transformedHtml += "</em>";
            }

            return transformedHtml + (appendEllipses ? "..." : "");
        }

Additionally we have logic in place to truncate text to the given character limit – to the nearest word and optionally append ellipses to the text.

        public static string TruncTextToSpecificNumOfChars(string str, int maxCharCount, bool appendEllipses = false)
        {
            if (string.IsNullOrWhiteSpace(str)) return string.Empty;

            if (str.Length <= maxCharCount || maxCharCount == 0)
                return str;

            var originalStrLen = str.Length;

            if (str.Length > maxCharCount)
                str = str.Substring(0, maxCharCount + 1);

            var ellipsePos = str.LastIndexOfAny(new[] { ' ', '.', ',', ';', '!', '-', ']', '}', ')', '*' });
            if (ellipsePos != -1 && str.Length > ellipsePos && ellipsePos > 0)
                str = str.Substring(0, ellipsePos);

            if (ellipsePos == -1)
            {
                str = str.Substring(0, str.Length - 1);
            }

            if ((str.Length < originalStrLen) && appendEllipses)
            {
                str = str.TrimEnd('.').TrimEnd(',').TrimEnd(';').TrimEnd('-');
            }

            return str + ((str.Length < originalStrLen) && appendEllipses ? "..." : string.Empty);
        }

, , ,

Leave a comment

EditFrame with Sitecore MVC and Glass.Mapper

Ever hand to deal with html which is just not compatible with the tags that glass html helpers render for you?
And in these occasions, it pretty much seems like you are going to have to say bye bye to page editor support!
EditFrame is the answer to your problems.

This is definitely not a new concept and existed in the asp.net version of sitecore libraries too.
In this post, I will demonstrate how to get this set up in your MVC site (with glass mapper).

EditFrame basically lets you make any html page editor friendly. All you need to do, is tell it which item and which field / set of fields you want to associate with the given html.

There are 2 parts to setting this up

  • Create items with the fields you want to add support for specified
  • Add the corresponding code – passing in the item which needs to be made editable, wrapped around the html which will be editable

In this example, consider a simple Product template, with a multilist field ‘Categories’

2015-06-07_013912

Core Items

In core database, under /sitecore/content/Applications/WebEdit/Edit Frame Buttons, add your field, making a duplicate of the /sitecore/content/Applications/WebEdit/Edit Frame Buttons/Default folder for convenience. This will bring over with it the below items.

2015-06-07_014505

The title and tooltip in the above screenshot will correspond to the title and tooltip of the edit frame in page editor mode when you select the editable target html.

You can then specify the fields for which you would want to add page editor support. In this example, I have mentioned only 1, but as mentioned below, you can add a pipe delimited set of multiple fields.

2015-06-07_015230

Another great advantage of this approach, of separating the fields from the items / templates here, is that you can create a single folder for common fields like say Background Image, Products, Featured Links etc, which might be reused over multiple templates!

So now in the code, you need to create the frame tag and pass in the path to this folder we created, along with the item which needs to be editable.

Following is the view code:

@using sc72
@using sc72.tdsmaster.sitecore.templates.User_Defined

@inherits Glass.Mapper.Sc.Web.Mvc.GlassView<Product>

<!-- START: Product Detail -->

<b>Sku:</b> @Editable(p => p.Sku)
<b>Name:</b> @Editable(p => p.Name)
@RenderImage(p => p.Image, isEditable: true)
@using (BeginEditFrame(ItemTree.Content.Applications.WebEdit.Edit_Frame_Buttons.Site.Status.ItemPath, Model.Id.ToString()))
{
    if (Model.Custom_Status != null)
    {
        <span><b>Status:</b> @Model.Custom_Status.Title</span>
    }
}

@using (BeginEditFrame(ItemTree.Content.Applications.WebEdit.Edit_Frame_Buttons.Site.Categories.ItemPath, Model.Id.ToString()))
{
    if (Model.Custom_Categories != null && Model.Custom_Categories.Any())
    {
        <span><b>Categories:</b> </span>
        foreach (Product_Category category in Model.Custom_Categories)
        {
            @:&nbsp;&nbsp;&nbsp;&nbsp; @category.Title

        }
    }
}

@RenderLink(p => p.Purchase_Link, isEditable: true)

<!-- END: Product Detail -->

Here’s the resultant page in pageeditor mode:

2015-06-07_015929

Here are examples of scenarios when this is particularly useful:

  • Background images
  • Page editor support for sitecore reference type fields (droplinks, treelists, multilists etc)
  • Iframes / Audio / Video tags
  • Rendering sitecore data to get plugins on your page
  • Newer html tags – picture tag / canvas – Or really any HTML for which glass might not have page editor support included html helper methods.

You can also configure the above to allow inserting new items in page editor mode!

, , , , , ,

2 Comments

Unsupported iframe urls in Sitecore page editor mode

I was working with Sitecore 6.5 when I ran into this issue with a client application.

If the Html (in a rich text field in this case) had an embedded iframe with a youtube url or a flickr slideshow, trying to perform any action on the page in page editor mode / preview mode would result in a javascript error, as in no save, no publish, nothing!

2014-05-25_155557

I saw the error:

Uncaught SecurityError: Blocked a frame with origin “http://mysite-local.com” from accessing a frame with origin “https://www.youtube.com“. The frame requesting access has a protocol of “http”, the frame being accessed has a protocol of “https”. Protocols must match.

This occurred even though I was accessing my site using http, and i tried accessing the youtube URL

  • specifically with http
  • without any protocol – which ideally means it should match the current browser url protocol.

But the issue persisted.

Here’s how we got around it:

The content author will need to add these potential iframe tags as anchor tags in the rich text field. These anchor tags could mandate a special attribute which would be the indicator for this custom code to trigger and transform the marked anchor tags into iframes only when the page mode is normal. (FYI, This js issue will also interfere with the ‘preview’ mode)

The content author will now enter this:

2014-05-25_155746

  • Add a processor in the <renderField> pipeline (Web.Config)

<processor type="MySite.RenderExternalLinks, MySite" />

  • For the condition when page mode is normal – search for all anchor tags within the rtf and if the special attribute to indicate transform is present, replace this with an iframe tag. You can you ‘data-‘ attributes to allow the content author to add additional attributes which will be applicable to the rendered iframe tag.

public class RenderExternalLinks
 {
 public void Process(RenderFieldArgs args)
 {
 Assert.ArgumentNotNull(args, "args");
 Assert.ArgumentNotNull(args.FieldTypeKey, "args.FieldTypeKey");

 if (args.FieldTypeKey != "rich text"
 || String.IsNullOrEmpty(args.FieldValue)
 || !Context.PageMode.IsNormal)
 return;

 // Load the HTML into the HtmlAgilityPack
 var doc = new HtmlDocument { OptionWriteEmptyNodes = true };
 doc.LoadHtml(args.Result.FirstPart);

 // Search for all links
 var aTag = doc.DocumentNode.SelectNodes("//a");
 if (aTag == null || aTag.Count == 0)
 return;

 foreach (HtmlNode node in aTag)
 {
 // Look for links to YouTube
 if (node.Attributes["href"] != null && node.Attributes["data-transform"] != null && node.ParentNode != null
 && (node.Attributes["href"].Value.Contains("youtube.com") || node.Attributes["href"].Value.Contains("flickr.com")))
 {
 node.ParentNode.InnerHtml
 = node.ParentNode.InnerHtml.Replace(node.OuterHtml,
 "");
 }
 }
 // Replace the Rich Text content with the modified content
 args.Result.FirstPart = doc.DocumentNode.OuterHtml;
 }
 }

So now the content author can made all the settings needed using the anchor tag and this will get rendered as the intended iframe tag in normal mode.

, , , , , ,

Leave a comment