Showing posts with label image. Show all posts
Showing posts with label image. Show all posts

Monday, 26 September 2016

Adding a button to Experience Editor to open the image editor app

I ran across a question in Stack Overflow today about how to show "edit image" in Sitecore experience editor? and thought that would be an easy one to tackle.  Originally I figured I could copy the command from the existing link ("Edit Image") in the content editor and it would be a very simple answer, however it turns out that it's not quite so easy.

If you have access to a decompiler, take a look at the existing command Sitecore.Shell.Framework.Commands.Shell.EditImage as well as the existing command for selecting an image in Experience Editor Sitecore.Shell.Applications.WebEdit.Commands.ChooseImage. You'll see that they're similar, but unfortunately they use two different methods of opening a dialog (Windows.RunApplication and SheerResponse.ShowModalDialog), as well as reading the image from the context vs from the URL, so we can't reuse the existing class as-is. Fortunately the two classes are similar enough that you can easily re-use code from both, and make a class which opens the image editor in a modal dialog (as required by the experience editor).  I've posted a gist to the full class below.

Step 1:
Duplicate one of the buttons under /sitecore/system/Field types/Simple Types/Image/WebEdit Buttons
Change the icon and text to something meaningful to your authors.
Make the contents of the Click field: chrome:field:editcontrol({command:"webedit:changeimage"})
(annoyingly there's already a webedit:editimage which opens the image properties, so we can't name it that).

Step 2:
Create your custom command class (eg. EditImage.cs), make it serializable and inherit from WebEditImageCommand like the other EE buttons. I have posted the full class as a gist to make it easier to read and copy.

Step 3:
Open App_Config\Include\Sitecore.ExperienceEditor.config and duplicate the entry for webedit:chooseimage. Change the command name to webedit:changeimage as per step 1, and change the type to the custom command class you created in step 2.

When you select an image in the Experience Editor, you should now have an extra button in the list which opens the image editor in a modal window when clicked!

Extra 'edit image' button second from the left
After clicking the button, the image is opened in the image editor modal

Note: You'll have to refresh the window to see the updated image.  I leave it as an exercise for the reader to determine a way to get the image to refresh after closing the modal :) or maybe I'll write that up in a later post...

Wednesday, 24 February 2016

Extending an Image field (pt 2) - adding a SPEAK dialog

This is the second part of a 2-part post about how to extend the Sitecore image field, and add an image overlaid on top of the main image. If you haven't already, check out part 1 to see how to extend the image field. This post will focus on the SPEAK component which will allow the user to set the top/left coordinates that specify where our overlay will be placed over the main image.

So if you remember back to when we set up the Overlay button on our new field, we updated the Message field to use "overlay". Let's handle this message in our ImageWithOverlay.cs class.
/// The overlay message.
private const string OverlayMessage = "overlay";

/// The overlay application location.
private const string OverlayAppLocation = "/sitecore/client/Your Apps/OverlaySelector";

public override void HandleMessage(Message message)
{
    if (message["id"] != ID)
    {
        return;
    }

    string[] command = message.Name.Split(':');
    Assert.IsTrue(command.Length > 1, "Expected message format is control:message");

    if (command[1] == OverlayMessage)
    {
        Sitecore.Context.ClientPage.Start(this, "Overlay");
        return;
    }

    base.HandleMessage(message);
}

public void Overlay(ClientPipelineArgs args)
{
    if (args.IsPostBack)
    {
        if (!args.HasResult)
        {
            return;
        }

        XmlValue.SetAttribute(Models.Constants.CoordinatesAttribute, args.Result);
        Update();
        SetModified();
        SheerResponse.Refresh(this);
    }
    else
    {
        UrlString urlString = new UrlString(OverlayAppLocation);

        Item selectedImage = GetMediaItem();
        if (selectedImage != null)
        {
            urlString["fo"] = selectedImage.Uri.ToString();
        }

        string coords = XmlValue.GetAttribute(Models.Constants.CoordinatesAttribute);
        if (!string.IsNullOrEmpty(coords))
        {
            urlString["coords"] = coords;
        }

        SheerResponse.ShowModalDialog(new ModalDialogOptions(urlString.ToString()) { Width = "800px", Height = "275px", Response = true, ForceDialogSize = true });
        args.WaitForPostBack();
    }
}


This code handles the message and will open our SPEAK application that we're about to make. We're passing the existing coordinates (if they're set) and the Uri of the main image (so that we can display it in our SPEAK dialog).

Ok let's build our SPEAK app. This will show our main image with our overlay image over top, and allow the user to drag the overlay around to set its coordinates. It'll also have a 'save' and 'cancel' button which sets (or not) the coordinates on the main image field.

In Sitecore Explorer, expand core/sitecore/client/Your Apps. Right click it and create a /sitecore/client/Business Component Library/Templates/Pages/Speak-DialogPage called "OverlaySelector". Set the following properties:

  • Theme: Oxford

  • Subthemes: Dialogs

Open the design layout (right click -> tasks -> Design Layout, or Ctrl+U) and set
  • Layout: /sitecore/client/Speak/Layouts/Layouts/Speak-Layout


Add the following renderings:
TypeIDLocationOther
PageCodePage.CodePageCodeScriptFileName: /Scripts/Speak/Overlay.js
DialogPage.Body
DialogHeaderDialogHeader
DialogFooterDialogFooter
DialogContentMDialogContent
TextHeaderTitleDialogHeader.TitleText: Select a teardrop position
SectionMainImageDialogContent.Main
ImageOverlayImageMainImage.ContentAlt: Overlay, Height: 300, Width: 245, ImageUrl: /Content/Images/overlay.png
TextCoordinatesDialogContent.MainText: 100,100
ButtonSaveButtonDialogFooter.ButtonButtonType: Primary, Text: Select
ButtonCancelButtonDialogFooter.ButtonButtonType: Primary, Text: Cancel
RuleSaveButtonRulePage.BodyField: Rule, RuleItemId: (see below), TargetControl: SaveButton, Trigger: click
RuleCancelButtonRulePage.BodyField: Rule, RuleItemId: (see below), TargetControl: CancelButton, Trigger: click
Under your OverlaySelector item, create a /sitecore/cilent/Speak/Templates/Pages/PageSettings item which will have the settings for our dialog (for now, just the 2 rules for our buttons).
Under the PageSettings item, create: /sitecore/client/Speak/Layouts/Renderings/Resources/Rule Definition called CancelButtonRuleDefinition with rule: where always close the dialog
/sitecore/client/Speak/Layouts/Renderings/Resources/Rule Definition called SaveButtonRuleDefinition with rule: where always the dialog return value to component Coordinates text

The final step is to add our javascript for the component. We'll use jQuery UI to make the overlay draggable.
define(["sitecore", "jquery", "jqueryui"], function (_sc, $, ui) {
    var overlaySelectorDialog = _sc.Definitions.App.extend({
        initialized: function () {
            var app = this;
            var scale = 1;

            var itemUriString = _sc.Helpers.url.getQueryParameters(window.location.href)['fo'];
            var itemPath = null;
            try {
                var itemUri = new URL(itemUriString);
                itemPath = itemUri.pathname;
                if (itemPath == "" || itemPath.indexOf("?") > -1) throw "Invalid URL";
            } catch (e) {
                // Doesn't support URL (IE and pretty much FF as well)
                var slashes = itemUriString.indexOf("//");
                var query = itemUriString.indexOf("?");
                if (slashes > -1) itemPath = itemUriString.substring(slashes, query > -1 ? query : itemUriString.length);
            }

            var mainImage = document.querySelector('[data-sc-id="MainImage"]');
            if (itemPath == null || itemPath == "") {
                alert("Couldn't parse item URL for your background image");
            } else {
                var itemUriSplit = itemPath.substring(2).split("/");
                var database = new _sc.Definitions.Data.Database(new _sc.Definitions.Data.DatabaseUri(itemUriSplit[0]));
                database.getItem(itemUriSplit[1], function (item) {
                    if (item == null) alert("Couldn't find background image item in database for unknown reason");
                    else {
                        var imgWidth = parseInt(item.Width);
                        var imgHeight = parseInt(item.Height);
                        if (imgWidth > imgHeight) {
                            scale = 500 / imgWidth;
                            mainImage.style.height = Math.round(scale * imgHeight) + "px";
                        } else {
                            scale = 500 / imgHeight;
                            mainImage.style.width = Math.round(scale * imgWidth) + "px";
                        }
                        
                        mainImage.style.backgroundImage = "url('" + item.$mediaurl.replace("thn=1", "") + "&w=500')";

                        var coords = _sc.Helpers.url.getQueryParameters(window.location.href)['coords'];
                        if (coords != null && coords != "") {
                            app.Coordinates.set('text', coords);
                            var coordsSplit = coords.split(",");
                            jQuery('[data-sc-id="OverlayImage"]').css({ "left": (parseInt(coordsSplit[0]) * scale) + "px", "top": (parseInt(coordsSplit[1]) * scale) + "px" });
                        }
                    }
                });
            }

            mainImage.style.height = "500px";
            mainImage.style.width = "500px";
            mainImage.style.backgroundSize = "cover";

            jQuery('[data-sc-id="OverlayImage"]').draggable({
                containment: '[data-sc-id="MainImage"]',
                scroll: false,
                start: function (e, ui) {
                    app.Coordinates.set('text', Math.round(ui.position.left / scale) + "," + Math.round(ui.position.top / scale));
                },
                drag: function (e, ui) {
                    app.Coordinates.set('text', Math.round(ui.position.left / scale) + "," + Math.round(ui.position.top / scale));
                },
                stop: function (e, ui) {
                    app.Coordinates.set('text', Math.round(ui.position.left / scale) + "," + Math.round(ui.position.top / scale));
                }
            });
        }
    });
    return overlaySelectorDialog;
});
I've uploaded the full code to Github if you'd like to try it out for yourself.