Experience Sitecore ! | June 2019

Experience Sitecore !

More than 200 articles about the best DXP by Martin Miles

Enhancing Treelist and Miltilist fields with a lookup feature for productivity. Two approaches, same goal: browser user script and serverside

brThis trick saves me lo-o-o-ot of time, especially working on large Helix/SXA solution.

Typically you have a treelist that looks like this:

Now tell me what actions do you take when there is a necessity to look up what are one or few of these templates? How you achieve that?

I assume you raw copy GUID, paste into search, inspect and then return back. Clumsy, isn't it?

Objective. My desire was to achieve a popup or new tab window by clicking ENTER hotkey once so that I immediately can look up this template for whatever interests me. I ended up not just achieving the goal but doing that in two opposite ways. Let's consider the pros and cons of each approach (they are almost opposite)


LET'S REVIEW THE OPTIONS

1. Browser user script. I recently wrote about implementing browser user script in order to enhance your productivity while working with Sitecore content management. This approach has

  • Pros:

    • leaves server untouched
    • user can easily adjust the flow right in the nice code editor provided by the extension
  • Cons:

    • applies only on a specific browser on your client machine
    • sometimes relies on browser extensions ie. TapmerMonkey


2. Serverside script alteration is an opposite option for achieving the goal. I recently blogged about developing Expand / Collapse all editor sections implementing this approach. In order to do so, I need to modify <web_root>\sitecore\shell\Applications\Content Manager\Content Editor.js file within my sitecore instance.

  • Pros:

    • applies to all users with all browsers using your Sitecore instance
  • Cons:

    • served by a back-end and risks interfering with other serverside code
    • business logic needs to be placed into Sitecore instance, whether by deployment or manually

Anyway, you may pick up either of the solutions depending on your preferences, it's better to have a choice. Let's turn to the implementation.


IMPLEMENTATION

1. Browser user script. The way Sitecore Desktop functions is dynamically loading iframes for specific features, so it is not as easy to use selectors with it due to not just elements but even documents not yet loaded into DOM. 

To address this implementation I picked Mutation. It allows you creating handlers for whatever mutation occurs in your DOM, one may also specify what types of events to handle:

// handing a mutation occured
var mutationObserver = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    console.log(mutation);
  });
});

// starts listening for changes in the root HTML element of the page.
mutationObserver.observe(document.documentElement, {
  attributes: true,
  characterData: true,
  childList: true,
  subtree: true,
  attributeOldValue: true,
  characterDataOldValue: true
});
I use nested MutationObserver approach where outer observer indirectly handles new documents appearing and once it happens - attaches the nested observer within a given iframe. So below is the entire code:
// ==UserScript==
// @name         Multilist and Treelist values lookup
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  When working with multilist / treelist items in Sitecore, simply hit ENTER on right-hand side selected item in order to preview it in new tab
// @author       Martin Miles
// @match        */sitecore/shell/default.aspx*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const settings = { mutation: { attributes: true, characterData: true, childList: true, subtree: true, attributeOldValue: true, characterDataOldValue: true } };

    var mutationObserver = new MutationObserver(function (mutations) {
        mutations.forEach(function (mutation) {

            if (mutation.type == "attributes" && mutation.target.id.startsWith("startbar_application_") && mutation.target.className == "scActiveApplication") {
                var iframe = getIframeFromActiveTab(mutation.target);
                if (iframe) {
                    internalMutationObserver(iframe);
                }
            }
        });
    });

    mutationObserver.observe(document.documentElement, settings.mutation);

    function internalMutationObserver(iframe) {
        var innerObserver = new MutationObserver(function (innerMutations) {
            innerMutations.forEach(function (m) {

                var _el = m.target;
                if (_el.className == 'scEditorFieldMarkerBarCell') {

                    var labels = _el.parentElement.querySelectorAll('.scContentControlMultilistCaption');
                    labels.forEach(function (label) {
                        if (label.innerHTML == 'Selected') {
                            label.innerHTML = label.innerHTML + ' (hit ENTER on selected item for preview in a new tab)';
                        }
                    });

                    var selects = _el.parentElement.querySelectorAll('select');
                    selects.forEach(function (select) {
                        select.addEventListener("keypress", keyPressed);
                    });
                }
            });
        });

        // wire up internal mutation
        innerObserver.observe(iframe.contentDocument, settings.mutation);
    }

    function getIframeFromActiveTab(element) {
        let frameName = element.id.replace('startbar_application_', '');
        let iframe = document.querySelectorAll('iframe#' + frameName);
        if (iframe.length > 0) {
            return iframe[0];
        }

        return null;
    }

    function keyPressed(e) {
        if (e.keyCode == 13) {

            var selsectedValue = e.target.options[e.target.selectedIndex].value;

            var url = getUrl(selsectedValue);
            if (url) {
                openInNewTab(url);
            }

            return false;
        }
    }

    function getUrl(selsectedValue) {

        let guid;
        var parts = selsectedValue.split("|");
        if (parts.length === 2) {
            guid = parts[1];
        }
        else if (isGuid(selsectedValue)) {
            guid = selsectedValue;
        }

        return !guid ? guid : '/sitecore/shell/Applications/Content Editor?id='
            + guid + '&amp;vs=1&amp;la=en&amp;sc_content=master&amp;fo='
            + guid + '&ic=People%2f16x16%2fcubes_blue.png&he=Content+Editor&cl=0';
    }

    function isGuid(stringToTest) {
        if (stringToTest[0] === "{") {
            stringToTest = stringToTest.substring(1, stringToTest.length - 1);
        }
        var regexGuid = /^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$/gi;
        return regexGuid.test(stringToTest);
    }

    function openInNewTab(url) {
        var win = window.open(url, '_blank');
        win.focus();
    }
})();
To run this scrup add a new script from TamperMonkey's dashboard, paste the code, save and enable it. After refreshing Sitecore Desktop you'll be able to preview multiselect field values bu hitting ENTER button, just as on a demo below:

Ready-to-use script is available on GitHub: link

2. The serverside approach considers modifying some serverside component, in a given case, it is quite simple - just modifying <web_root>\sitecore\shell\Applications\Content Manager\Content Editor.js file by adding few functions at the bottom of it:

scContentEditor.prototype.onDomReady = function (evt) {
    this.registerJQueryExtensions(window.jQuery || window.$sc);
    this.addMultilistEnhancer(window.jQuery || window.$sc);
};

scContentEditor.prototype.addMultilistEnhancer = function ($) {
    $ = $ || window.jQuery || window.$sc;
    if (!$) { return; }

    $('#MainPopupIframe').load(function () {
        $(this).show();
        console.log('iframe loaded successfully')
    });

    $(document).on("keypress", "select.scContentControlMultilistBox", function (e) {
        if (e.keyCode == 13) {

            var selsectedValue = e.target.options[e.target.selectedIndex].value;

            var url = getUrl(selsectedValue);
            if (url) {
                openInNewTab(url);
            }

            return false;
        }
    });

    function getUrl(selsectedValue) {

        let guid;
        var parts = selsectedValue.split("|");
        if (parts.length === 2) {
            guid = parts[1];
        }
        else if (isGuid(selsectedValue)) {
            guid = selsectedValue;
        }

        return !guid ? guid : '/sitecore/shell/Applications/Content Editor?id='
            + guid + '&amp;vs=1&amp;la=en&amp;sc_content=master&amp;fo='
            + guid + '&ic=People%2f16x16%2fcubes_blue.png&he=Content+Editor&cl=0';
    }

    function isGuid(stringToTest) {
        if (stringToTest[0] === "{") {
            stringToTest = stringToTest.substring(1, stringToTest.length - 1);
        }
        var regexGuid = /^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$/gi;
        return regexGuid.test(stringToTest);
    }

    function openInNewTab(url) {
        var win = window.open(url, '_blank');
        win.focus();
    }
};

Invalidate browser cache and refresh the page. I am not including a demo record as it works identical to the demo above for the first approach.

Note! if you also have Expand/Collapse script in place from previous blog post, then you need to share once handler for cboth calls since unlike in C# you may not have multi-delegate prototype handler:
// you cannot have multidelegate onDomReady here, so share all calls within one handler
scContentEditor.prototype.onDomReady = function (evt) {
    this.registerJQueryExtensions(window.jQuery || window.$sc);
    this.addMultilistEnhancer(window.jQuery || window.$sc);
    this.addCollapser(window.jQuery || window.$sc);
};

Result. Regardless of the approach taken, now you may look up Treelist selected items by selecting them and hitting the ENTER button. You may include this trick into your own Sitecore productivity pack (you have one already, don't you?) as it indeed helps so much.

Implementing blogs index page with filters and paging: SXA walkthrough

Objective.

Initially I've had a template called Blog, and several pages of it which are actual blog posts. Now I have created a page called Blog Index, implementing template, partial and page designs of the same name. The obvious purpose of given page is to show the list of blog posts, being able to filter this out by certain criteria, including a new custom one - series. Content authors want to group these blogs posts into series, so that's a grouping by a logical criteria. They also want to to display the most recent higher, but give readers an option to select an order, as well as page size.


Implementation plan

  1. Switch to SXA "Live mode" (optional)
  2. Create taxonomy categories
  3. Create Series interface template
  4. Use interface template and update blog posts
  5. Create search scope for page template
  6. Create computed field
  7. Publish, redeploy and re-build indexes
  8. Create facets
  9. Create filter datasources
  10. Make rendering variant for search filters
  11. Make rendering variant for search results
  12. Place component to partial design
  13. Configuring Search Results component
  14. Enjoy result!


IMPLEMENTATION

1. Before start, switch web to master, this can be done at /sitecore/content/Tenant/Platform/Settings/Site Grouping/Platform at Database field by setting it to master (dont't forget to publish that particulat item however). Once done, it will use not only master database at published site, but also master indexes.


2. Firstly, let's create Series taxonomy folder under Taxonomy (/sitecore/content/Tenant/Platform/Data/Taxonomy) and populate it with actual series-categories that will be used for filtering:


3. Now I can create interface template to implement series selection. This template will be later used with not just Blogs but also few other page types, that's why I make it an interface and put into shared - /sitecore/templates/Project/Tenant/Platform/Interfaces/Shared/_Series.

Make sure the Source column has correctly set datasource, so that you later will be able to pick up right category under site's Data/Taxonomy/Series folder, as on example below:

Datasource=query:$site/*[@@name='Data']/Taxonomy/Series&IncludeTemplatesForDisplay=Taxonomy folder,Category&IncludeTemplatesForSelection=Category


4. Once done, add _Series interface template to actual page template (Blog in my case). Then one can go to existing blog posts and assign them into series (best with Sitecore PowerShell):

$rootItem = Get-Item master:/sitecore/content/Tenant/Platform;
$sourceTemplate = Get-Item "/sitecore/templates/Project/Tenant/Platform/Pages/Blog";  
$selectedSeries = "{1072C536-0EC2-4EAB-8D98-DC9BF441F30A}";

Get-ChildItem $rootItem.FullPath -Recurse | Where-Object { $_.TemplateName -eq $sourceTemplate.Name } | ForEach-Object {  
        $_.Editing.BeginEdit()
        $_.Fields["Series"].Value = $selectedSeries;
$_.Editing.EndEdit() }

Now selecting an item displays which series it belongs to:


5. Create scope under /sitecore/content/Tenant/Platform/Settings/Scopes, call it Blogs and set its field Scope Query to filter out by Blog template ID:


6. Create computed field contentseries at you project to store actual name of Series into index that's in addition to another field in index called series so that automatically indexed by template and stores GUID for series. This is how I implemented it in Platform.Website.ContentSearch.config:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <contentSearch>
      <indexConfigurations>
        <defaultSolrIndexConfiguration>
          <fieldMap>
            <fieldNames>
              <field fieldName="contentseries" returnType="stringCollection" patch:after="*[0]" />
            </fieldNames>
          </fieldMap>
          <documentOptions>
            <fields hint="raw:AddComputedIndexField">
              <field fieldId="{ID-of-Series-field-within-_Series_template}" fieldName="contentseries" returnType="stringCollection" patch:after="*[0]">
                Tenant.Site.Website.ComputedFields.CategoriesField,Tenant.Site.Website
              </field>
            </fields>
          </documentOptions>          
        </defaultSolrIndexConfiguration>
      </indexConfigurations>
    </contentSearch>
  </sitecore>
</configuration>


7. Publish this configuration into webfolder, then clean up (you can run PS script below) and rebuild indexes.

stop-service solrServiceName
get-childitem -path c:\PathTo\Solr\server\solr\Platform*\Data -recurse | remove-item -force -recurse
start-service solrServiceName
iisreset /stop
iisreset /start


8. When computed field comes into index, the next step sould be to create Series facets. Please note that facet name should be different from the field name as the one got by template and then facet overwrites it

  • For Series - under /sitecore/content/Tenant/Platform/Settings/Facets. The most important field here is Field Name, the value would be contentseries that matched name of field we've created at the previous step
  • Also create one for Published date that relies on already existing published_date_tdt field, which is a custom date field presenting in all of my content page templates.


9. Create new datasource items:

  • checklist filter called Series under /sitecore/content/Tenant/Platform/Data/Search/Checklist Filter folder and assign its first field to point Series facet created at previous step
  • an item for Publication Date under /sitecore/content/Tenant/Platform/Data/Search/Date Filter/Publication Date.


10. Implement Search Filter rendering variant that will contain actual filters. I create that under my custom component Search Content, make two columns and also component variant field into each of them. Assign Filter (Checklist) into first and Filter (Date) into second. Reference datasource items from previous step for each component correspondingly:


11. Implement Search Result rendering variant that will define presentation for each item shown/found:

Noticed Series reference field? That switches context to the item references by Series field, so that I can get a value of actual category under Taxonomy folder.


12. In partial design for Blog Index, drop the following renderings into the canvas: Search content, Sort Results, Page Size and Search Results.


13. Finally, for Search Results component, go to Edit component properties, under SearchCriteria section assign Search results signature to search-results and also select Search scope to match Blogs.

The result: