Injecting Resources into Experience Editor in Powerful Ways

I recently got a ping back from Eric Stafford on an old blog article of mine, the first one I had ever posted! He was working on some code and needed to inject in some custom CSS into the Experience Editor. We had several conversations on Slack, and I thought I’d post up some powerful ways in which to achieve this. Be sure to check out Eric’s posts, he’s done a fair amount of research into different ways of achieving this as well.

Javascript

1. Check PageMode

One solution is to check the PageMode, and when IsExperienceEditor include whatever resources you want:

@if (Sitecore.Context.PageMode.IsExperienceEditor)
{
    <script src="/path/to-my/experience-editor.js"></script>
    <link rel="stylesheet" href="/path/to-my/experience-editor.css" />
}

2. Add CSS class to the body element

One fairly common approach is to add a CSS class to the html/body element when in experience editor mode:

<body class="@(Sitecore.Context.PageMode.IsExperienceEditor ? "experience-editor" : string.Empty)">

Then in your own CSS files you can use a descendant selector so the styles only apply when this class is present:

.my-custom-style {
    /* Element styled in normal mode */
}

.experience-editor .my-custom-style {
    /* Let's style some content when in the Experience Editor */
}

And for your JavaScript you can check for the same descendant selector or check for the existence of the parent selector:

$(".experience-editor .my-custom-style").on("click", function() {
    alert("Hey, you're in Page, I mean Experience Editor mode!");
})

function doSomething(a, b) {
    if ($("body").hasClass("experience-editor")) {
        alert("hey, you're in experience editor mode, did you find the elephant?");
    } else {
        alert("hey, you're in normal mode");
    }
}

You don’t strictly need to add the CSS class from C#/Razor since you can detect Sitecore.PageModes object in JavaScript and could then add the CSS class when the page is loaded (note that you don’t even need to add the CSS class if you don’t need to style differently, fo JS-only code you just check the PageMode):

$(document).ready(function(){
    var editorMode = (typeof Sitecore !== 'undefined' && typeof Sitecore.PageModes !== 'undefined');
    if (editorMode) {
        $("body").addClass("experience-editor");

        // elsewhere in your code you could check for the CSS class existence like we did earlier
        // or maybe the var was declared as global...
        if ($("body").hasClass("experience-editor")) {
            alert("Hey, you're in experience editor mode, but why can't you be my friend?");
        }
    }
})

There is a great article from Martina Welander on the above, take a read.

In all these cases, the styles and scripts can be included as part of the CSS/JS files of your regular site, there is no need for separate files. This means that styles and code for your regular site and EE mode can be kept together, next to each other in the physical. Should you make a change in your regular site code the EE mode code is very close by, so maybe it makes your maintenance easier. You may or may not care about this, the downside is that your regular site users will be downloading unnecessary code and a few extra kb.

3. Override Sitecore CSS files from config

There are a couple of CSS files that Sitecore itself injects into the Experience Editor. Fortunately there are settings for these, you could patch these and set it to your own file instead:

<!--
    WEB EDIT EXPERIENCE EDITOR STYLESHEET
    The stylesheet to include in the experience editor.
    Default value: /shell/client/Sitecore/ExperienceEditor/Ribbon.css
-->
<setting name="WebEdit.ExperienceEditorStylesheet" value="/sitecore/shell/client/Sitecore/ExperienceEditor/Ribbon.css" />

<!--
    WEB EDIT CONTENT EDITOR STYLESHEET
    The stylesheet to include in the content editor in WebEdit mode.
    Default value: /webedit.css  
-->
<setting name="WebEdit.ContentEditorStylesheet" value="/webedit.css" />

The problem with doing that however would be that, you know, you won’t include the default styles that Sitecore needs. You could copy+paste them into your own file or extend one of the default files with your own styles. All options equally bad.

Or you could just change the setting to point to your own file, and in your file import in the default Sitecore one:

@import url('/webedit.css');

/* Add in your own custom CSS styles for the Experience Editor mode */
.my-custom-style {
    ...
}

Downside of the above is that you can only include CSS, if you need to include any Experience-Editor-only-Javascript then it is not possible with this method.

I’ve never used this solution myself, but it’s an option.

4. Use Custom Page Extender

In all the cases so far, everything is very implementation specific and it’s all tied to your own project. None of them allow you to modularize the code for a marketplace module. Even the previous option can only be set once, but you can’t guarantee that that someone else has not already patched the setting for their own use.

Whilst carrying out some research for a couple of my previous modules that I needed to inject some resources for (the Environment Styler and Rendering Chrome modules) I carried out some research into Page Extenders. I couldn’t get this to work, and I wrongly assumed it was due to some changes that were made in the Ribbon (sorry Eric, I know I left a comment on your post stating this, but looks like we both missed this one). Eric also alerted me to a post on Brainjocks site, which I had somehow managed to miss. I still didn’t see the mistake though… After chatting to Eric I spent a little time looking back at this and I think I know where I am made the mistake…

using System.IO;
using Sitecore.Diagnostics;
using Sitecore.Mvc.ExperienceEditor.Pipelines.RenderPageExtenders;
using Sitecore.Mvc.Pipelines;

namespace MyProject.CMS.Custom.Pipelines
{
    public class RenderCustomPageExtender : MvcPipelineProcessor<RenderPageExtendersArgs>
    {
        public override void Process(RenderPageExtendersArgs args)
        {
            Assert.ArgumentNotNull((object)args, "args");
            args.Writer.Write("<link href=\"{0}\" rel=\"stylesheet\" />", "/assets/stylesheet/custom.css");
            args.Writer.Write("<script src=\"{0}\"></script>", "/assets/scripts/custom.js");
        }
    }
}

And patch the extender in:

<mvc.renderPageExtenders>
    <processor type="MyProject.CMS.Custom.Pipelines.RenderCustomPageExtender, MyProject.CMS.Custom" />
</mvc.renderPageExtenders>

This works and will inject the resource after the opening body tag. If you’re going this route, follow the code in the Brainjocks article and inject the resources in via config though. Please don’t hardcode like I have here 🙂

Where did I go wrong before? I used dotPeek and looked at an existing processor, inherited from RenderPageExtendersProcessor and implemented the Render() abstract class that Resharper told me to. I really should have dug a little deeper and inspected the base class:

public override void Process(RenderPageExtendersArgs args)
{
    Assert.ArgumentNotNull((object) args, "args");
    if (args.IsRendered)
        return;
    args.IsRendered = this.Render(args.Writer);
}

Essentially the processor would never run since the previous processor set the args.IsRendered to true and the base class checks/breaks out early. The Brainjocks article also correctly overrides the Process() method so this check is not done (but could have just inherited from MvcPipelineProcessor like above). Don’t know how/why I missed the above, so RTFM next time. Ultimately it was not the solution I was looking for anyway but is a very good solution.

5. Inject in the files from SPEAK ribbon

The final option for injecting resources into the Experience Editor really depends on what your module is doing. I used the following technique myself for the Rendering Chrome module which utilises SPEAK.

Rendering Chrome Checkbox

In this particular instance I added a checkbox into the Ribbon, which defines a PageCodeScriptFileName setting pointing to the JavaScript file (see Line #42 of the button definition).

... PageCodeScriptFileName=%2fsitecore+modules%2fRenderingChrome%2fShowRenderingChrome.js& ...

The script in turn loads as many CSS files into the main document as you wish. Need to load some extra JavaScript files as well? Sure, no problem, Sitecore uses require.js for SPEAK so you can just define as many of those as your module needs and it will load them in:

define(["sitecore", "/-/speak/v1/ExperienceEditor/ExperienceEditor.js"], function (Sitecore, ExperienceEditor) {
  Sitecore.Commands.ShowRenderingChrome =
  {
    commandContext: null,
    isEnabled: true,

    canExecute: function (context) {
      if (!ExperienceEditor.isInMode("edit")
        || !context
        || !context.button
        || context.currentContext.isFallback) {
        return false;
      }

      ExperienceEditor.Common.registerDocumentStyles(["/sitecore modules/RenderingChrome/chrome-styles.css"], window.parent.document);
      toggleRenderingChromeHighlight(context.button.get("isChecked") == "1");
      if (!Sitecore.Commands.ShowRenderingChrome.commandContext) {
        this.commandContext = ExperienceEditor.getContext().instance.clone(context);
      }

      return true;
    },

    execute: function (context) {
      ExperienceEditor.PipelinesUtil.generateRequestProcessor("ExperienceEditor.ToggleRegistryKey.Toggle", function (response) {
        response.context.button.set("isChecked", response.responseValue.value ? "1" : "0");
        toggleRenderingChromeHighlight(response.context.button.get("isChecked") == "1");
      }, { value: context.button.get("registryKey") }).execute(context);
    }
  };
  
  var toggleRenderingChromeHighlight = function(enabled) {
      var className = "chromeRenderingHighlight";
	  if (enabled) {
		window.top.document.documentElement.classList.add(className);
	  } else {
		window.top.document.documentElement.classList.remove(className);  
	  }
  };
});

When the ribbon and buttons are loaded, the canExecute() method is called. The check on Line #8 ensures we are in edit mode. We can then inject in our CSS files using a Sitecore JS helper method line on Line #15, which is quite handy really! Add as many as you need for your requirement (but it would be strange to load more than one when you could combine them). When the checkbox is toggled the execute() if fired, which in turn calls by custom function which simple adds or removes a CSS class to the document element (html tag) and we’re back to being able to define our CSS using a descendent selector like in method #2. We’re almost back full circle 🙂

You can see the above at work in this Codepen example. I also suggested to Eric that he follow as similar approach for his “Mister Rogers” post which would then allow the field info to be toggled from the ribbon, and turning it off means it would not get in the way of the WYSIWYG-ness of the Experience Editor. You can see a demo in the style of his post in this Codepen example.

Final Thoughts

Hopefully this gives you a good overview of different options available. Pick whichever works best for you, including from a maintenance perspective.

Be aware that you may need different resources per site/layout and you should cater for that if required with whatever method you follow, but each option has it’s uses and sometimes it’s better to go for the simpler option than a more extensible option. For example, with my own module I did not follow #4 since it’s my code so I define what resources are required. This is not an extension point for anyone else to use and code/resources/files for my module lives and dies together. Even if I added the configurable pipeline in method #4, everyone else would create their own for their modules since this is not a standard Sitecore pipelie. Don’t get me wrong, I’ve clearly used similar myself previously but I don’t have an issue with it being hardcoded in this particular instance. Want to add your own scripts in? Well add your own damn button then :p

Did I miss something? Do you add or inject the resources in differently into the Experience Editor? Can you think of a cleaner option? Answers on a postcard… or just leave a comment or find me on Slack/Twitter.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s