In AEM 6.3, the responsive Layout feature was introduced, and the Adobe documentation will show you exactly how it works. Though some customizations and how to achieve them are not super clear, this feature works out of the box. It is certainly a great addition that enables authors to create custom grid layouts that were previously achieved via inflexible column control components.
Banner photo by delfi de la Rua on Unsplash
In this post, I will demo a limitation of the layout editor and a way to fix it, purely for experimenting purposes. You might also learn how to build a toolbar action with popovers as a bonus!
Pro tip 1: Adobe has JsDoc documentation for the Editor Core javascript API
Pro tip 2: each gif below has an “elapsed time” in the lower right corner to help you know when the gif starts. Is it /dʒɪf/ like “Jeff”or /ɡɪf/ like “Gift”?!
Sample Layout Authoring Experience
Here, I have two components that I want to show side-by-side:
Next, I’d like to nest another layout container in the one above:
I’ve added a new component and authored it to that nested layout container.
Notice how, in the nested layout container, you only get the remaining columns (5) as opposed to what you’d expect in a standard 12 column grid.
Using a 12 grid container, regardless of how nested the layout containers are
So this was interesting. As an author, I believe I should have the ability to select the grid column count for the nested layout container. Granted, there might not be many use-cases to jamming 12 columns in such a tight space, but it would be nice to have fine control if I so desire.
With that in mind, my coworker Paul and I discussed this and he showed me exactly how the grid width
is stored.
Obligatory shout out to Paul Bjorkstrand!.
Every time an author makes a layout change via the editor, a new node called cq:responsive
is stored under that component. Under that you’d see the breakpoint nodes. Each node contains a property called “width” which dictates the component column grid column count, in the case of the layout container.
Additionally, looking at the JS API, there is a namespace: Granite.authorresponsive.persistence
that has several methods to make layout changes. The one that interested me was: Granite.authorresponsive.persistence.setBreakpointConfig(editable, breakpoint, config)
where:
editable
is the Granite.author.Editablebreakpoint
is the breakpoint name, like “default”, “phone” or whatever is the breakpoint you have defined. we can use Granite.author.responsive#getCurrentBreakpoint to get this at any point.config
is an object with the properties that need to be stored into the relevantcq:responsive
node of theeditable
A ToolbarAction Solution!
With this information, I can now build a ToolbarAction
to make this change for me and set the layout container to any width
I want. Here is a demo:
And here is the code that builds this ToolbarAction:
Add this to a clientlib with category cq.authoring.editor.hook
This code is heavily based on the code in ACS commons for PanelSelector. That code helped me make this a lot faster since the general layout and design is the same (toolbar action with a popover), but my toolbar action does something different.
(function ($, ns, channel, window) { "use strict"; var columnSelector; var NS_COLUMNSELECTOR = ".cmp-columnelector"; var POPOVER_MIN_WIDTH = "4rem"; // looks better /** * @typedef {Object} ColumnSelectorConfig Represents a Column Selector configuration object * @property {Granite.author.Editable} editable The [Editable]{@link Granite.author.Editable} against which to create the column selector * @property {HTMLElement} target The target against which to attach the column selector UI */ var ColumnSelector = ns.util.createClass({ /** * The Column Selector configuration Object * * @member {ColumnSelectorConfig} ColumnSelector#_config */ _config: {}, /** * An Object that is used to cache the internal HTMLElements for this Column Selector * * @member {Object} ColumnSelector#_elements */ _elements: {}, constructor: function ColumnSelector(config) { var that = this; that._handleOutOfAreaClickBound = that._handleOutOfAreaClick.bind(that) that._handleButtonClickBound = that._handleButtonClick.bind(that) that._config = config; that._render(); that._bindEvents(); }, // if the column selector is open isOpen: function () { return (this._elements.popover && this._elements.popover.open); }, /** * Renders the Column Selector, adds its items and attaches it to the DOM * * @private */ _render: function () { this._elements.popover = this._createPopover(); this._elements.buttonList = this._createColumnButtonList(); // append column list to the popover this._elements.popover.content.appendChild(this._elements.buttonList); // append popover to the content frame for re-use. ns.ContentFrame.scrollView[0].appendChild(this._elements.popover); }, /** * Creates a simple [Coral.Popover]{@link Coral.Popover} * * @private * @returns {Coral.Popover} a simple coral popover. */ _createPopover: function () { var that = this; var popover = new Coral.Popover().set({ alignAt: Coral.Overlay.align.LEFT_BOTTOM, alignMy: Coral.Overlay.align.LEFT_TOP, target: that._config.target, interaction: Coral.Popover.interaction.OFF, open: true }); // this is 9.375rem by default.. didnt want to css it popover.style.minWidth = POPOVER_MIN_WIDTH; return popover; }, /** * Creates the [Coral.ButtonList]{@link Coral.ButtonList}; 12 buttons for 12 columns) * * @private * @returns {Coral.ButtonList} List of buttons of all 12 columns. */ _createColumnButtonList: function() { var buttonList = new Coral.ButtonList().set({ interaction: Coral.ButtonList.interaction.OFF }); // columns 1...12 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] .map(function (n) { var btnItem = new Coral.ButtonList.Item() .set({ innerHTML: n, value: n }) btnItem.style.textAlign = "center" btnItem.style.width = "100%"; return btnItem; }) .forEach(function (listItem) { return buttonList.items.add(listItem) }); return buttonList; }, /** * Binds interaction events * * @private */ _bindEvents: function () { var that = this; // escape key $(document).off("keyup" + NS_COLUMNSELECTOR).on("keyup" + NS_COLUMNSELECTOR, function (event) { if (event.keyCode === 27) { that._finish(); } }); // out of area clicks document.removeEventListener("click", that._handleOutOfAreaClickBound); document.addEventListener("click", that._handleOutOfAreaClickBound, true); // click handlers for each button this._elements.buttonList.items.getAll() .forEach(function (buttonItem) { buttonItem.addEventListener("click", that._handleButtonClickBound, true); }); // reposition the popover with overlay change, // as the editable toolbar can jump following navigation to a panel channel.off("cq-overlays-repositioned" + NS_COLUMNSELECTOR).on("cq-overlays-repositioned" + NS_COLUMNSELECTOR, function () { if (that._elements.popover) { that._elements.popover.reposition(); } }); }, /** * Handles click on a clolumn button. * @param {Event} e the button click event. */ _handleButtonClick: function (e) { var that = this; var breakpoint = ns.responsive.getCurrentBreakpoint(); // setBreakpointConfig is not documented in the following url, but works nonetheless: // https://helpx.adobe.com/experience-manager/6-4/sites/developing/using/reference-materials/jsdoc/ui-touch/editor-core/index.html ns.responsive.persistence.setBreakpointConfig(that._config.editable, breakpoint, { width: e.target.value }) .done(function () { // refresh after making the change. ns.edit.EditableActions.REFRESH.execute(that._config.editable).done(function () { that._config.editable.overlay.setSelected(true); }); }); that._finish(); }, /** * Handles clicks outside of the Column Selector popover * * @private * @param {Event} event The click event */ _handleOutOfAreaClick: function (event) { console.log("closing..") var that = this; if (!$(event.target).closest(that._elements.popover).length) { that._finish(); } }, /** * Unbinds event handlers * * @private */ _unbindEvents: function () { var that = this; $(document).off("click" + NS_COLUMNSELECTOR); // unbind click events here this._elements.buttonList.items.getAll().forEach(function (buttonItem) { buttonItem.removeEventListener("click", that._handleButtonClick); }) }, /** * Finishes column selection, hides it and cleans up. * * @private */ _finish: function () { var that = this; if (that._elements.popover && that._elements.popover.parentNode) { that._elements.popover.open = false; that._unbindEvents(); that._elements.popover.parentNode.removeChild(that._elements.popover); } } }); /** * Toolbar action that works on responsivegrid component to allow authors * to chose the grid column width/count */ var columnSelect = new ns.ui.ToolbarAction({ name: "COLUMN_SELECT", text: Granite.I18n.get("Select Column"), icon: "tableEdit", execute: function (editable, param, target) { if (!columnSelector || !columnSelector.isOpen()) { columnSelector = new ColumnSelector({ "editable": editable, "target": target[0] }); } // do not close the toolbar return false; }, // make sure this is responsivegrid and that current component does not have live relationship. condition: function (editable) { if (editable.type !== "wcm/foundation/components/responsivegrid") { return false; } if (editable.config[MSM.MSMCommons.Constants.PROP_LIVE_RELATIONSHIP]) { return MSM.MSMCommons.isInheritanceCancelled(editable) || MSM.MSMCommons.isManuallyCreated(editable); } else { return true } }, isNonMulti: true }); channel.on("cq-layer-activated", function (event) { if (event.layer === "Edit") { ns.EditorFrame.editableToolbar.registerAction("COLUMN_SELECT", columnSelect); } }); }(jQuery, Granite.author, jQuery(document), this));
The code is well documented and should be easy to follow. This was a lot of fun to build and I hope to make more toolbar actions in the future!
I tried this approach in 6.4.2 I created a new JS under http://localhost:4502/crx/de/index.jsp#/libs/granite/contexthub/components/contexthub/authoring-hook/, added the JS to the JS.txt file, rebuild the clientlibs. But when I tried edit the layout container, I am not seeing the new icon. What am I missing?
I’m not sure that adding this js under /libs/granite/contexthub/components/contexthub/authoring-hook would work.
I suggest you add the JS in a new clientlib with the suggested category in the post. Then edit a page and inspect to see the JS code is actually loading.
Yes, the path I provided has the category suggested. in the blog
Hi Ahmed, tried putting the js in a clientlib . When the custom button is clicked author mode flags an error
An error has occured during execution of the selected action: Coral.ButtonList.interaction is undefined -> _createColumnButtonList@http://localhost:4502/libs/cq/gui/components/authoring/editors/clientlibs/sites/page.js:7671:9
Can you please share a package, so i can test it on 6.3 and 6.4
Thanks
Hi,
I’ve built this for fun and it is not mean to be used in an actual production application. Some of the APIs and the approaches I use depend on hidden implementation details of the Layout Editor. Meaning, this can change or break with an upcoming SP. And if you want to build and run this, you’d have to do it at your own risk 🙂
I believe I have built and tested this on AEM 6.5.0