November Happy Hour will be moved to Thursday December 5th.

Binh Nguyen Thi
Feb 25, 2024
  964
(3 votes)

How to add more Content Area Context Menu Item in Optimizely CMS 12

Hey folks, today I will share something related to Context Menu customization in the Content Area of Optimizely CMS.

As you know, the content area is a crucial property in Optimizely CMS. It enables editors to place blocks onto a page, allowing for flexible rendering based on the chosen blocks.

You can see the Content Area Context Menu when editing a page in on-page editing mode or in properties editing mode. 

With the Content Area Context Menu, you can see the following available actions:

  • Edit: Navigate to the selected block for editing in the current view.
  • Quick Edit: Allows editing of the block within a modal popup in the current view.
  • Personalize: Enables customization of block visibility, specifying who can view the block's content.
  • Move Outside Group: Moves the block out from a personalized group.
  • Display Option: This action is displayed if configurations for display tags are available.
  • Move Up: Shifts the block upwards.
  • Move Down: Shifts the block downwards.
  • Remove: Deletes a block from the Content Area.

So question: Is it possible if you want to add more customized action for block in Content Area as known as menu item in context menu?

My answer is: Yes. We could

Today, I will show you my way to do it based on my experience in Dojo framework. It may be not the best solution but it works with me.

Here are all steps that you can do to add more menu item to Content Area Context Menu.

1. First step, you need to do is creating a content area command because each menu item in the context menu is currently matched to a content area command.

Here is the code example for creating a content area command:

define("alloy/contentediting/command/CustomOption", [
    // General application modules
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dojo/when",

    "epi/dependency",

    "epi-cms/contentediting/command/_ContentAreaCommand",
    "epi-cms/contentediting/viewmodel/ContentBlockViewModel"
], function (declare, lang, when, dependency, _ContentAreaCommand, ContentBlockViewModel) {

    return declare([_ContentAreaCommand], {        
        // label: [public] String
        //      The action text of the command to be used in visual elements.
        label: "Custom action",
        // iconClass: [readonly] String
        //      The icon class of the command to be used in visual elements.
        iconClass: "epi-iconStar",

        constructor: function () {
        },       
        _execute: function () {            
            //Add your logic here when clicking on this action
        },
        _onModelValueChange: function () {
            // summary:
            //      Updates canExecute after the model value has changed.
            // tags:
            //      protected
            var item = this.model;
           
            this.set("isAvailable", true);

            this.set("canExecute", false);

            if (item && item.contentLink) {
                this.set("canExecute", true);
                return;
            }           
        }

    });
});

2. Next step is creating new dojo component one for ContentAreaCommands. This component is used to declare context menu in on-page editting mode.

define("epi-cms/contentediting/command/ContentAreaCommands", [
    "dojo/_base/array",
    "dojo/_base/declare",
    "dojo/Stateful",
    "dojo/when",
    "dijit/Destroyable",
    "epi-cms/ApplicationSettings",
    "epi-cms/contentediting/command/BlockRemove",
    "epi-cms/contentediting/command/BlockEdit",
    "epi-cms/contentediting/command/ContentAreaItemBlockEdit",
    "epi-cms/contentediting/command/BlockInlineEdit",
    "epi-cms/contentediting/command/MoveVisibleToPrevious",
    "epi-cms/contentediting/command/MoveVisibleToNext",
    "epi-cms/contentediting/command/Personalize",
    "epi-cms/contentediting/command/SelectDisplayOption",
    "epi-cms/contentediting/command/MoveOutsideGroup",
    "alloy/contentediting/command/CustomOption"
], function (array, declare, Stateful, when, Destroyable, ApplicationSettings, Remove, Edit, ContentAreaItemBlockEdit, InlineEdit, MoveVisibleToPrevious, MoveVisibleToNext, Personalize, SelectDisplayOption, MoveOutsideGroup, CustomOption) {

    return declare([Stateful, Destroyable], {
        // tags:
        //      internal

        commands: null,

        constructor: function () {
            this._commandSpliter = this._commandSpliter || new Stateful({
                category: "menuWithSeparator"
            });
            this.contentAreaItemBlockEdit = new ContentAreaItemBlockEdit({ category: null });
            this.blockInlineEdit = new InlineEdit();
            this.moveVisibleToPrevious = new MoveVisibleToPrevious();
            this.moveVisibleToNext = new MoveVisibleToNext();
            this.customOption = new CustomOption();
            this.commands = [
                new Edit({ category: null }),
                this.contentAreaItemBlockEdit,
                this.blockInlineEdit,
                this.customOption,
                this._commandSpliter,
                new SelectDisplayOption(),
                this.moveVisibleToPrevious,
                this.moveVisibleToNext,
                new Remove()
            ];
            var sectionsVisibility = Object.assign({}, ApplicationSettings.sectionsVisibility);
            // Only add personalize command if the ui is not limited
            if (!ApplicationSettings.limitUI && (sectionsVisibility.visitorGroups !== false)) {
                this.moveOutsideGroup = new MoveOutsideGroup();
                this.personalize = new Personalize({ category: null });
                this.commands.splice(5, 0, this.personalize, this.moveOutsideGroup);
            }

            this.commands.forEach(function (command) {
                this.own(command);
            }, this);
        },

        handleDoubleClick: function (itemModel) {
            if (itemModel.inlineBlockData) {
                when(this.contentAreaItemBlockEdit.updateModel(itemModel)).then(function () {
                    this.contentAreaItemBlockEdit.execute();
                }.bind(this));
            } else {
                when(this.blockInlineEdit.updateModel(itemModel)).then(function () {
                    this.blockInlineEdit.execute();
                }.bind(this));
            }
        },

        _modelSetter: function (model) {
            this.model = model;

            array.forEach(this.commands, function (command) {
                command.set("model", model);
            });
        }
    });
});

3. Next step is creating new dojo component one for ContentAreaEditor. This component is used to declare the context menu in properties editting mode

require({
    cache: {
        'url:epi-cms/contentediting/editors/templates/ContentAreaEditor.html': "<div class=\"dijitInline\" tabindex=\"-1\" role=\"presentation\">\r\n    <div class=\"epi-content-area-header-block\">\r\n        <div data-dojo-type=\"epi-cms/contentediting/AllowedTypesList\"\r\n            data-dojo-props=\"allowedTypes: this.allowedTypes, restrictedTypes: this.restrictedTypes\"\r\n            data-dojo-attach-point=\"allowedTypesHeader\"></div>\r\n    </div>\r\n    <div class=\"epi-content-area-editor--wide epi-content-area-editor\">\r\n        <div data-dojo-attach-point=\"treeNode\"></div>\r\n        <div data-dojo-attach-point=\"actionsContainer\" class=\"epi-content-area-actionscontainer\"></div>\r\n    </div>\r\n</div>\r\n"
    }
});
define("epi-cms/contentediting/editors/ContentAreaEditor", [
    // Dojo
    "dojo/_base/declare",
    "dojo/aspect",
    "dojo/dom-class",
    "dojo/dom-style",
    "dojo/on",
    "dojo/topic",
    "dojo/when",
    "dojo/Stateful",

    //Dijit
    "dijit/registry",
    "dijit/_WidgetBase",
    "dijit/_TemplatedMixin",
    "dijit/_CssStateMixin",
    "dijit/_WidgetsInTemplateMixin",

    // EPi Framework
    "epi/dependency",
    "epi/shell/dnd/Target",
    "epi/shell/command/_CommandProviderMixin",
    "epi/shell/command/_Command",
    "epi/shell/applicationSettings",

    //EPi CMS
    "epi-cms/contentediting/editors/_ContentAreaTree",
    "epi-cms/contentediting/editors/_ContentAreaTreeModel",
    "epi-cms/contentediting/viewmodel/PersonalizedGroupViewModel",
    "epi-cms/_ContentContextMixin",
    "epi-cms/ApplicationSettings",
    "epi-cms/contentediting/viewmodel/ContentAreaViewModel",
    "epi-cms/core/ContentReference",
    "epi/shell/widget/ContextMenu",
    "epi/shell/widget/_ValueRequiredMixin",
    "epi-cms/widget/overlay/Block",
    "epi-cms/widget/command/CreateContentFromContentArea",
    "epi-cms/widget/command/CreateContentFromSelector",

    "epi-cms/widget/_HasChildDialogMixin",

    "epi-cms/contentediting/command/BlockRemove",
    "epi-cms/contentediting/command/BlockConvert",
    "epi-cms/contentediting/command/BlockEdit",
    "epi-cms/contentediting/command/ContentAreaItemBlockEdit",
    "epi-cms/contentediting/command/BlockInlineEdit",
    "epi-cms/contentediting/command/MoveToPrevious",
    "epi-cms/contentediting/command/MoveToNext",
    "epi-cms/contentediting/command/MoveOutsideGroup",
    "epi-cms/contentediting/command/Personalize",
    "epi-cms/contentediting/command/SelectDisplayOption",
    "alloy/contentediting/command/CustomOption",
    "epi-cms/contentediting/AllowedTypesList",
    "epi-cms/contentediting/editors/_TextWithActionsMixin",

    // Resources
    "dojo/text!epi-cms/contentediting/editors/templates/ContentAreaEditor.html",
    "epi/i18n!epi/cms/nls/episerver.cms.contentediting.editors.contentarea",
    "epi/i18n!epi/cms/nls/episerver.cms.widget.overlay.blockarea"
], function (

    // Dojo
    declare,
    aspect,
    domClass,
    domStyle,
    on,
    topic,
    when,
    Stateful,

    // Dijit
    registry,
    _WidgetBase,
    _TemplatedMixin,
    _CssStateMixin,
    _WidgetsInTemplateMixin,

    // EPi Framework
    dependency,
    Target,
    _CommandProviderMixin,
    _Command,
    shellApplicationSettings,

    // CMS
    _ContentAreaTree,
    _ContentAreaTreeModel,
    PersonalizedGroupViewModel,

    _ContentContextMixin,
    ApplicationSettings,
    ContentAreaViewModel,

    ContentReference,
    ContextMenu,
    _ValueRequiredMixin,
    BlockOverlay,
    CreateContentFromContentArea,
    CreateContentFromSelector,

    _HasChildDialogMixin,

    RemoveCommand,
    BlockConvertCommand,
    EditCommand,
    ContentAreaItemBlockEdit,
    BlockInlineEdit,
    MoveToPrevious,
    MoveToNext,
    MoveOutsideGroup,
    Personalize,
    SelectDisplayOption,
    CustomOption,
    AllowedTypesList, // used in template
    _TextWithActionsMixin,

    // Resources
    template,
    resources,
    blockAreaRes
) {

    return declare([
        _WidgetBase,
        _TemplatedMixin,
        _WidgetsInTemplateMixin,
        _CssStateMixin,
        _ValueRequiredMixin,
        _ContentContextMixin,
        _CommandProviderMixin,
        _HasChildDialogMixin,
        _TextWithActionsMixin
    ], {
        // summary:
        //      Editor for ContentArea to be able to edit my content area property in forms mode
        //      This should be a simple, non WYSIWYG listing of the inner content blocks with possibilities to add, remove and rearrange the content.
        //
        // tags:
        //      internal

        // baseClass: [public] String
        //    The widget's base CSS class.
        baseClass: "epi-content-area-wrapper",

        emptyClass: "epi-content-area-wrapper--empty",

        // res: Json object
        //      Language resource
        res: resources,

        // templateString: String
        //      UI template for content area editor
        templateString: template,

        // value: String
        //      Value of the content area
        value: null,

        // multiple: Boolean
        //  Value must be true, otherwise dijit/Form will trea the value as an object instead of an array
        multiple: true,

        // parent: Object
        //      Editor wrapper object containe the editor
        parent: null,

        // overlayItem: Object
        //      Source overlay of the content area in on page edit mode
        overlayItem: null,

        // model: Object
        //      Content area editor view model
        model: null,

        // intermediateChanges: Boolean
        //      Inherited from editor interface
        intermediateChanges: true,

        // editMode: String
        //      Flags to detect page edit mode (On page edit or Form edit mode or Create content mode)
        editMode: "onpageedit",

        // _preventOnBlur: [private] Boolean
        //      When set, the onBlur event is prevented.
        _preventOnBlur: false,

        _dndTarget: null,

        // allowedTypes: [public] Array
        //      The types which are allowed. i.e used for filtering based on AllowedTypesAttribute
        allowedTypes: null,

        // restrictedTypes: [public] Array
        //      The types which are restricted.
        restrictedTypes: null,

        // actionsResource: [Object]
        //      The resource of actions link
        actionsResource: blockAreaRes,

        // actionsResource: [Object]
        //      Name of constructor function of ContentAreaTree class
        treeClass: _ContentAreaTree,

        // allowMultipleItems: [Boolean]
        //      Allow dnd multiple items at once
        allowMultipleItems: true,

        constructor: function () {
            this.allowedDndTypes = [];
        },

        onChange: function (value) {
            // summary:
            //    Called when the value in the widget changes.
            // tags:
            //    public callback
        },

        onForceChange: function (value) {
            this.onChange(value);
        },

        _handleModelChange: function (value) {
            // summary:
            //    Called when the value in the model changes.
            // tags:
            //    public callback

            this.validate();
            this.onForceChange(value);

            this._toggleEmptyClass();
            if (this.model.selectedItem) {
                this.updateCommandModel(this.model.selectedItem);
            }

            this._toggleActionsContainer();
        },

        _toggleClass: function (node, className, condition) {
            (node || this.domNode).classList[condition ? "add" : "remove"](className);
        },

        _toggleEmptyClass: function () {
            this._toggleClass(this.domNode, this.emptyClass, !this.value || this.value.length <= 0);
        },

        _onBlur: function () {
            // summary:
            //      Override base to prevent the onBlur from being called when the _preventOnBlur flag is set.
            // tags:
            //      protected override

            if (this._preventOnBlur) {
                return;
            }
            this.inherited(arguments);
        },

        focus: function () {
            // summary:
            //    Focus the tree if there is a value, else focus the create block text.
            // tags:
            //    public
            if (this.tree && this.model.get("value").length > 0) {
                this._focusManager.focus(this.tree.domNode);
            } else {
                if (this.textWithLinks) {
                    this.textWithLinks.focus();
                }
            }
        },

        postMixInProperties: function () {

            this.inherited(arguments);

            this._commandSpliter = this._commandSpliter || new Stateful({
                category: "menuWithSeparator"
            });

            // NOTE: Check for this._commands to allow for mocking the commands without breaking _CommandProviderMixin.
            this.contentAreaItemBlockEdit = new ContentAreaItemBlockEdit({ category: null, isContentAreaReadonly: this.get("readOnly") });
            this.blockInlineEdit = new BlockInlineEdit();
            this.movePrevious = new MoveToPrevious();
            this.moveNext = new MoveToNext();
            this.customOption = new CustomOption();

            this.commands = this._commands || [
                new EditCommand({ category: null }),
                this.contentAreaItemBlockEdit,
                this.blockInlineEdit,
                this.customOption,
                this._commandSpliter,
                new SelectDisplayOption(),
                this.movePrevious,
                this.moveNext,
                new RemoveCommand(),
                new BlockConvertCommand()
            ];

            this.own(on(this.contentAreaItemBlockEdit, "save", function (inlineBlockData, name) {
                this._preventOnBlur = false;

                var value = [];
                (this.model.getChildren() || []).forEach(function (child) {
                    if (child instanceof PersonalizedGroupViewModel) {
                        (child.getChildren() || []).forEach(function (child) {
                            if (child.id === this.model.selectedItem.id) {
                                child.inlineBlockData = inlineBlockData;
                                child.name = name;
                            }
                            value.push(child);
                        }.bind(this));
                    } else {
                        if (child.id === this.model.selectedItem.id) {
                            child.inlineBlockData = inlineBlockData;
                            child.name = name;
                        }
                        value.push(child);
                    }
                }.bind(this));
                value = value.map(function (v) {
                    return v.serialize();
                });

                this.set("value", value);

                // In order to be able to add a block when creating it from a floating editor
                // we need to set the editing parameter on the editors parent wrapper to true
                // since it has been set to false while being suspended when switching to
                // the secondaryView.
                this.parent = this.parent || this.getParent();
                this.parent.set("editing", true);
                this.onForceChange(value);

                // Now call onBlur since it's been prevented using the _preventOnBlur flag.
                this.onBlur();
            }.bind(this)));

            // Only add personalize command if the ui is not limited
            if (this._isPersonalizationEnabled()) {
                this.commands.splice(5, 0,
                    new Personalize({ category: null }),
                    new MoveOutsideGroup()
                );
            }
            this.commands.forEach(function (command) {
                this.own(command);
            }, this);

            this.own(
                //Create the view model
                this.model = this.model || new ContentAreaViewModel({
                    maxLength: this.maxLength,
                    minLength: this.minLength
                }),
                this.treeModel = this.treeModel || new _ContentAreaTreeModel({ model: this.model }),
                this.model.watch("selectedItem", function (name, oldValue, newValue) {
                    //Update the commands with the selected block
                    this.updateCommandModel(newValue);
                }.bind(this)),
                on(this.model, "changed", function () {
                    if (!this._started || this._supressValueChanged) {
                        return;
                    }

                    this._set("value", this.model.get("value"));

                    //Call to the handle model change with the new value
                    this._handleModelChange(this.value);
                }.bind(this))
            );
            // personalizationarea isn't an actual type so it needs to be hardcoded like in _ContentAreaTree
            this.allowedDndTypes.push("personalizationarea");
            this.allowedDndTypes.push(this._getInlineBlockDndKey());

            if (!this.contentDataStore) {
                var registry = dependency.resolve("epi.storeregistry");
                this.contentDataStore = registry.get("epi.cms.contentdata");
            }
        },

        buildRendering: function () {
            this.inherited(arguments);

            this.contextMenu = new ContextMenu();
            this.contextMenu.addProvider(this);

            this.own(this.contextMenu);

            this._setupActions(this.actionsContainer);

            this.own(this._dndTarget = new Target(this.actionsContainer, {
                accept: this.allowedDndTypes,
                reject: this.restrictedDndTypes,
                isSource: false,
                alwaysCopy: false,
                allowMultipleItems: this.allowMultipleItems,
                insertNodes: function () { }
            }));

            this.own(aspect.after(this._dndTarget, "onDropData", function (dndData, source, nodes, copy) {
                dndData.forEach(function (dndData) {
                    this.model.modify(function () {
                        this.model.addChild(dndData.data);
                    }.bind(this));
                }, this);

                if (!this.tree) {
                    this._createTree();
                }

            }.bind(this), true));

            // Handle focus after dropping on the tree or the drop area. We set focus to ourselves so that
            // it is not left where the drag originated.
            this.own(aspect.after(this._dndTarget, "onDrop", this.focus.bind(this)));
        },

        postCreate: function () {
            this.inherited(arguments);

            this.set("emptyMessage", resources.emptymessage);
            this._toggleEmptyClass();

            if (this.parent && this.overlayItem) {
                this.own(aspect.after(this.parent, "onStartEdit", function () {
                    this._selectFromOverlay(this.overlayItem.model);
                }.bind(this)));
            }

            this.own(topic.subscribe("/dnd/start", this._startDrag.bind(this)));
        },

        startup: function () {

            if (this._started) {
                return;
            }

            this.inherited(arguments);

            this.tree && this.tree.startup();
            this.contextMenu.startup();
            this._updateStyle();

            this.own(
                this.allowedTypesHeader.watch("hasRestriction", this._updateStyle.bind(this))
            );
        },

        destroy: function () {
            this.tree && this.tree.destroyRecursive();

            this.inherited(arguments);
        },

        isCreateLinkVisible: function () {
            // summary:
            //      Overridden mixin class, depend on currentMode will show/not create link
            // tags:
            //      protected

            return this.model.canCreateBlock(this.allowedTypes, this.restrictedTypes);
        },

        onDialogExecute: function (selectedContent) {
            if (selectedContent) {
                this._saveValueAndFireOnChange({
                    contentLink: new ContentReference(selectedContent.contentLink).createVersionUnspecificReference().toString(),
                    name: selectedContent.name,
                    typeIdentifier: selectedContent.typeIdentifier
                });
            }
        },

        _saveValueAndFireOnChange: function (block) {
            this._preventOnBlur = false;
            var value = Object.assign([], this.model.get("value"), true);
            value.push(block);
            this.set("value", value);

            // In order to be able to add a block when creating it from a floating editor
            // we need to set the editing parameter on the editors parent wrapper to true
            // since it has been set to false while being suspended when switching to
            // the secondaryView.
            this.parent = this.parent || this.getParent();
            this.parent.set("editing", true);
            this.validate();
            this.onForceChange(value);

            // Now call onBlur since it's been prevented using the _preventOnBlur flag.
            this.onBlur();
        },

        executeAction: function (actionName) {
            // summary:
            //      Overridden mixin class executing click actions from textWithLinks widget
            // actionName: [String]
            //      Action name of link on content area
            // tags:
            //      public

            if (actionName === "createnewblock") {
                // HACK: Preventing the onBlur from being executed so the editor wrapper keeps this editor in editing state
                this._preventOnBlur = true;

                // since we're going to create a block, we need to hide all validation tooltips because onBlur is prevented here
                this.validate(false);

                var command = shellApplicationSettings.inlineBlocksInContentAreaEnabled ? new CreateContentFromContentArea({
                    allowedTypes: this.allowedTypes,
                    restrictedTypes: this.restrictedTypes
                }) : new CreateContentFromSelector({
                    creatingTypeIdentifier: "episerver.core.blockdata",
                    createAsLocalAsset: true,
                    isInQuickEditMode: this.isInQuickEditMode,
                    quickEditBlockId: this.quickEditBlockId,
                    autoPublish: true,
                    allowedTypes: this.allowedTypes,
                    restrictedTypes: this.restrictedTypes
                });

                command.set("model", {
                    save: this._saveValueAndFireOnChange.bind(this),
                    cancel: function () {
                        this._preventOnBlur = false;
                        this.onBlur();
                    }.bind(this)
                });
                command.execute();
            }
        },

        isValid: function (isFocused) {
            // summary:
            //    Check if widget's value is valid.
            // isFocused:
            //    Indicate that the widget is being focused.
            // tags:
            //    protected

            // When create block screen is visible, we need to hide all validation messages since onBlur is prevented.
            return (this._preventOnBlur || !this.required || this.model.get("value").length > 0);
        },

        _setReadOnlyAttr: function (readOnly) {
            this._set("readOnly", readOnly);
            this._toggleActionsContainer();

            if (this._source) {
                this._source.isSource = !this.readOnly;
            }

            if (this.model) {
                this.model.set("readOnly", readOnly);
            }

            this.tree && this.tree.set("readOnly", readOnly);
        },

        _toggleActionsContainer: function () {
            // summary:
            //    Hide actions when readonly or editor has reached the items limit
            // tags:
            //    private

            var visible = !this.model.hasReachedItemsLimit() && !this.get("readOnly");
            domStyle.set(this.actionsContainer, "display", visible ? "" : "none");
        },

        _checkAcceptance: function (source, nodes) {
            // summary:
            //      Customize checkAcceptance func
            // source: Object
            //      The source which provides items
            // nodes: Array
            //      The list of transferred items

            return this.readOnly ? false : this._source.defaultCheckAcceptance(source, nodes) && !this.model.hasReachedItemsLimit();
        },

        _createTree: function () {
            // summary:
            //    Creates the tree widget
            // tags:
            //    private

            //Create the tree
            this.tree = new this.treeClass({
                accept: this.allowedDndTypes,
                reject: this.restrictedDndTypes,
                contextMenu: this.contextMenu,
                model: this.treeModel,
                readOnly: this.readOnly,
                inlineBlockDndKey: this._getInlineBlockDndKey(),
                inlineBlockNameProperties: this.inlineBlockNameProperties
            }).placeAt(this.treeNode);

            this.tree.own(on(this.tree, "dblclick", function (itemModel) {
                if (itemModel.inlineBlockData) {
                    when(this.contentAreaItemBlockEdit.updateModel(itemModel)).then(function () {
                        this.contentAreaItemBlockEdit.execute();
                    }.bind(this));
                } else {
                    when(this.blockInlineEdit.updateModel(itemModel)).then(function () {
                        this.blockInlineEdit.execute();
                    }.bind(this));
                }
            }.bind(this)));

            this.tree.own(
                aspect.after(this.tree.dndController, "onDndEnd", this.focus.bind(this))
            );
        },

        _selectFromOverlay: function (overlayModel) {
            var child = overlayModel && overlayModel.selectedItem && overlayModel.selectedItem.serialize(),
                model = this.model,
                path = ["root"];

            // exit if there is no overlay model to select item
            if (!child) {
                return;
            }

            if (child.contentGroup) {
                model = model.getChild({ name: child.contentGroup });
                path.push(model.id);
            }
            model = model.getChild(child);
            if (!model) {
                return;
            }

            path.push(model.id);

            model.set("selected", true);
            model.set("ensurePersonalization", overlayModel.selectedItem.ensurePersonalization);

            // TODO: move this selection into tree instead
            this.tree && this.tree.set("path", path);

        },

        _startDrag: function (source, nodes, copy) {
            var accepted = this._dndTarget.accept && this._dndTarget.checkAcceptance(source, nodes);
            domClass.toggle(this.domNode, "dojoDndTargetDisabled", !accepted);

            // close the editor when user start draging Block from BlockArea
            //TODO: This widget should not call a method on the parent
            var widget = registry.getEnclosingWidget(nodes[0]);
            if (widget && widget.isInstanceOf(BlockOverlay) && this.parent && this.parent.cancel) {
                // We set isModified to false (default value) because always synchronize value
                // between OPE and Editor
                this.parent.set("isModified", false);
                this.parent.cancel();
            }
        },

        _ensureNonBrokenContentAreaItems: function (value) {
            var itemsList = value || [];
            itemsList.forEach(function (item) {
                if (!item || (!item.contentLink && !item.inlineBlockData)) {
                    item.isBrokenLink = true;
                    item.name = resources.brokenlink;
                }
            });
            return itemsList;
        },

        _setValueAttr: function (value) {
            value = this._ensureNonBrokenContentAreaItems(value);
            // Destroy the tree since that is the fastest way to remove all items
            this.tree && this.tree.destroyRecursive();

            this._set("value", value);

            this._supressValueChanged = true;
            this.model.set("value", value);
            this._supressValueChanged = false;

            // Create the tree again after the value has been set so
            // all tree nodes are created in one go
            this._createTree();

            this._toggleEmptyClass();
            this._toggleActionsContainer();
        },

        _updateStyle: function () {
            // summary:
            //      Handle the widget style depending on the allowedTypesList visibility

            if (!this.domNode) {
                return;
            }

            if (this.allowedTypesHeader.get("hasRestriction")) {
                domClass.remove(this.domNode, "allowed-types-list-hidden");
            } else {
                domClass.add(this.domNode, "allowed-types-list-hidden");
            }
        },

        _isPersonalizationEnabled: function () {
            var sectionsVisibility = Object.assign({}, ApplicationSettings.sectionsVisibility);
            if (sectionsVisibility.visitorGroups === false) {
                return false;
            }
            return !ApplicationSettings.limitUI;
        },

        _getInlineBlockDndKey: function () {
            return this.name + "_contentarea-inline-block";
        }
    });
});

4. Last one, add overrided dojo components into module config as epi base resource to load customized resources once loading epi base resource

<?xml version="1.0" encoding="utf-8"?>
<module loadFromBin="false" clientResourceRelativePath="" viewEngine="Razor"  moduleJsonSerializerType="None" preferredUiJsonSerializerType="Net">	
	<dojo>
		<paths>
			<add name="alloy" path="ClientResources/Scripts" />
		</paths>
	</dojo>
	<clientResources>		
		<add name="epi-cms.widgets.base" path="ClientResources/Scripts/contentediting/command/ContentAreaCommands.js" resourceType="Script" />
		<add name="epi-cms.widgets.base" path="ClientResources/Scripts/editors/ContentAreaEditor.js" resourceType="Script" />
	</clientResources>
</module>

I hope this article will help some of you somehow. Enjoy reading!

Feb 25, 2024

Comments

Please login to comment.
Latest blogs
Optimizely SaaS CMS + Coveo Search Page

Short on time but need a listing feature with filters, pagination, and sorting? Create a fully functional Coveo-powered search page driven by data...

Damian Smutek | Nov 21, 2024 | Syndicated blog

Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog