SaaS CMS has officially launched! Learn more now.

Binh Nguyen Thi
Jul 13, 2024
  62
(0 votes)

How to have a link plugin with extra link id attribute in TinyMce

Introduce

Optimizely CMS Editing is using TinyMce for editing rich-text content. We need to use this control a lot in CMS site for kind of WYSWYG content.

I feel quite happy to use it and one of reason that I like to use tinymce is easy to customization. We can add available plug-ins that we want into toolbar only via configuration in server code. We also can build new plug-ins via javascript and register them in server code to use. 

Today, I want to share the way to add a new plug-in as same epi link plug-in with adding more extra id attribute.

Step 1: Create new Link Model in server code with Link Id attribute. Currenty, Link Editor control is using kind of this model type to render to corresponding user interface.

using EPiServer.Cms.Shell.UI.ObjectEditing.InternalMetadata;
using System.ComponentModel;

namespace Sample_Sites.Models
{
    public class CustomLinkModel : LinkModel
    {
        [DisplayName("Link Id")]
        public string LinkId { get; set; }
    }
}

Step 2: Create TinyMce plug-in via javascript named customLink.js under folder "wwwroot/ClientResources/Scripts/tinymce-plugins". In this example, I use Dojo module to do it because I want to re-use code of epi link plug-in. But  you can completely use vanilla javascript to create plug-in only with using  tinymce.PluginManager.add to add new button in TinyMce toolbar

Here is the place that I change to indicate using our custom link model for Link Editor instead of default one

 var linkEditor = new LinkEditor({
     baseClass: "epi-link-item",
     modelType: "Sample_Sites.Models.CustomLinkModel",
     hiddenFields: ["text"] // hide text field from UI
});

Here is the place that I change to read Id value from a element

if (href.length) {
    linkObject.href = href;
    linkObject.targetName = dom.getAttrib(selectedLink, "target");
    linkObject.title = dom.getAttrib(selectedLink, "title");
    linkObject.linkId = dom.getAttrib(selectedLink, "id");
}

Here is the place that I change to set Id attribute for a element

  var callbackMethod = function (value) {
      if (value && value.href) {
          var linkAttributes = {
              href: value.href,
              title: value.title,
              target: value.target ? value.target : null,
              id: value.linkId ? value.linkId : null
          };

Here is full javascript file for custom link plug-in:

define("alloy/tinymce-plugins/customLink", [
    "dojo/_base/lang",
    "dojo/on",
    "epi/shell/widget/dialog/Dialog",
    "epi-cms/ApplicationSettings",
    "epi-cms/widget/LinkEditor",
    "epi-addon-tinymce/tinymce-loader",
    "epi-addon-tinymce/plugins/epi-link/linkViewModel",
    "epi/i18n!epi/cms/nls/episerver.cms.widget.editlink",
    "epi/i18n!epi/cms/nls/episerver.cms.tinymce.plugins.epilink"
], function (lang, on, Dialog, ApplicationSettings, LinkEditor, tinymce, linkViewModel, resource, pluginResource) {

    tinymce.PluginManager.add("custom-link", function (editor) {
        function mceEPiLink() {
            var href = "",
                s = editor.selection,
                dom = editor.dom,
                linkObject = {};

            // CMS-20837: when users use the search function of Chrome (ctrl+f), the highlighted text will be un-highlighted
            // clone the selection here so it will not be affected by Chrome.
            var originalSelection = editor.selection.getRng().cloneRange();

            // When link is at the beginning of a paragraph, then IE (and FF?) returns the paragraph from getNode,
            // the getStart() and getEnd() however returns the anchor.
            var node = s.getStart() === s.getEnd() ? s.getStart() : s.getNode(),
                selectedLink = linkViewModel.getAnchorElement(editor, node);

            // No selection and not in link
            if (s.isCollapsed() && !selectedLink) {
                return;
            }

            if (selectedLink) {
                href = dom.getAttrib(selectedLink, "href");
            }

            if (href.length) {
                linkObject.href = href;
                linkObject.targetName = dom.getAttrib(selectedLink, "target");
                linkObject.title = dom.getAttrib(selectedLink, "title");
                linkObject.linkId = dom.getAttrib(selectedLink, "id");
            }

            var callbackMethod = function (value) {
                if (value && value.href) {
                    var linkAttributes = {
                        href: value.href,
                        title: value.title,
                        target: value.target ? value.target : null,
                        id: value.linkId ? value.linkId : null
                    };

                    // CMS-20837: and set the selection again if selection lost its value.
                    if (!editor.selection.getContent({ format: "html" })) {
                        editor.selection.setRng(originalSelection);
                    }

                    if (selectedLink) {
                        dom.setAttribs(selectedLink, linkAttributes);
                    } else {
                        if (linkViewModel._isImageFigure(node)) {
                            linkViewModel.linkImageFigure(editor, node, linkAttributes);
                        } else {
                            // When opening the link properties dialog in OPE mode an inline iframe is used rather than a popup window.
                            // When using IE clicking in this iframe causes the selection to collapse in the TinyMCE iframe which
                            // breaks the link creation immediately below. The workaround is to store the selection range before
                            // opening, and restoring it before creating the link.
                            s.setRng(s.getRng());
                            // To make sure we dont get nested links and have the same behavior as the default tiny
                            // link dialog we unlink any links in the selection before we create the new link.
                            editor.getDoc().execCommand("unlink", false, null);
                            editor.execCommand("mceInsertLink", false, "#mce_temp_url#", { skip_undo: 1 });

                            var elementArray = tinymce.grep(dom.select("a"), function (n) {
                                return dom.getAttrib(n, "href") === "#mce_temp_url#";
                            });
                            for (var i = 0; i < elementArray.length; i++) {
                                dom.setAttribs(elementArray[i], linkAttributes);
                            }

                            //move selection into the link content to be able to recognize it when looking at selection
                            if (elementArray.length > 0) {
                                var range = editor.dom.createRng();
                                range.selectNodeContents(elementArray[0]);
                                editor.selection.setRng(range);
                            }
                        }
                    }
                } else if (selectedLink) {
                    // pressed delete?
                    dom.setOuterHTML(selectedLink, selectedLink.innerHTML);
                    editor.undoManager.add();
                }
            };

            linkObject.target = linkViewModel.findFrameId(ApplicationSettings.frames, linkObject.targetName);

            var linkEditor = new LinkEditor({
                baseClass: "epi-link-item",
                //TODO: hardcoded for now
                modelType: "Sample_Sites.Models.CustomLinkModel",
                hiddenFields: ["text"] // hide text field from UI
            });

            //Find all Anchors in the document and add them to the Anchor list
            var allLinks = editor.getDoc().querySelectorAll("a[id],a[name]");

            // If the user is using IE 11 or lower we need to convert the
            // nodeList to a regular array
            // HACK: IE11
            if (tinymce.Env.ie && tinymce.Env.ie < 12) {
                allLinks = Array.prototype.slice.call(allLinks);
            }

            var anchors = linkViewModel.findNamedAnchors(allLinks);

            linkEditor.on("fieldCreated", function (fieldname, widget) {
                if (fieldname === "href") {
                    // in this case, widget is HyperLinkSelector
                    var hyperLinkSelector = widget;
                    var anchor = linkViewModel.getFirstAnchorWidget(hyperLinkSelector.get("wrappers"));

                    if (anchor && anchor.inputWidget) {
                        anchor.inputWidget.set("selections", anchors);
                    } else {
                        widget.on("selectorsCreated", function (hyperLinkSelector) { // when all selector have been created
                            var anchorWidget = linkViewModel.getFirstAnchorWidget(hyperLinkSelector.get("wrappers"));

                            if (anchorWidget && anchorWidget.inputWidget) {
                                anchorWidget.inputWidget.set("selections", anchors);
                                anchorWidget.domNode.style.display = "block";
                            }
                        });
                    }

                    if (anchor) {
                        anchor.domNode.style.display = "block";
                    }
                }
            });

            var dialogTitle = lang.replace(selectedLink ? resource.title.template.edit : resource.title.template.create, resource.title.action);

            var dialog = new Dialog({
                title: dialogTitle,
                dialogClass: "epi-dialog-portrait",
                content: linkEditor,
                defaultActionsVisible: false
            });
            dialog.startup();

            //Set the value when the provider/consumer has been initialized
            linkEditor.set("value", linkObject);

            dialog.show();
            editor.fire("OpenWindow", {
                win: null
            });

            dialog.on("execute", function () {

                var value = linkEditor.get("value");
                var linkObject = lang.clone(value);

                if (linkObject && linkObject.target) {
                    // get target frame name, instead of integer value
                    linkObject.target = linkViewModel.findFrameName(ApplicationSettings.frames, linkObject.target);
                }

                //Destroy the editor when the dialog closes
                linkEditor.destroy();
                linkEditor = null;

                callbackMethod(linkObject);
            });

            dialog.on("hide", function () {
                editor.fire("CloseWindow", {
                    win: null
                });
            });
        }

        // Register buttons
        editor.ui.registry.addToggleButton("custom-link", {
            tooltip: pluginResource.title,
            onAction: mceEPiLink,
            icon: "link",
            onSetup: function (buttonApi) {
                function selectionChange(e) {
                    var anchorElement = linkViewModel.getAnchorElement(editor, e.element);
                    var invalidSelection = !linkViewModel.hasValidSelection(editor, e.element);
                    buttonApi.setEnabled(!(invalidSelection && !anchorElement));
                    buttonApi.setActive(!editor.readonly && !!anchorElement);
                }

                editor.on("SelectionChange", selectionChange);

                return function () {
                    editor.off("SelectionChange", selectionChange);
                };
            }
        });

        editor.shortcuts.add("ctrl+k", pluginResource.title, mceEPiLink);

        return {
            getMetadata: function () {
                return {
                    name: "Link (epi)",
                    url: "https://www.optimizely.com"
                };
            }
        };
    });
});

dojo.require("alloy/tinymce-plugins/customLink");

Last step: Add TinyMce configuration with adding your custom plug-in in the toolbar

services.Configure<TinyMceConfiguration>(config =>
{
	config.InheritSettingsFromAncestor = true;
	config.Default()
		 .AddExternalPlugin("custom-link", "/ClientResources/Scripts/tinymce-plugins/customLink.js")
		 .Toolbar("styles | bold italic underline | custom-link anchor | image epi-image-editor epi-personalized-content | bullist numlist outdent indent | epi-dnd-processor | removeformat | fullscreen code")
		 .AddPlugin("code");
});

Finally, check if the plugin is displayed in the TinyMCE editor in Edit Mode. Thankfully, it works! :)

You can see this link https://tedgustaf.com/blog/2022/adding-custom-tinymce-plugin-to-the-html-editor-in-optimizely-cms/ to know how to add a new TinyMce plug-in in general

Jul 13, 2024

Comments

Please login to comment.
Latest blogs
Optimizely SaaS CMS Concepts and Terminologies

Whether you're a new user of Optimizely CMS or a veteran who have been through the evolution of it, the SaaS CMS is bringing some new concepts and...

Patrick Lam | Jul 15, 2024

Create your first demo site with Optimizely SaaS/Visual Builder

Hello everyone, We are very excited about the launch of our SaaS CMS and the new Visual Builder that comes with it. Since it is the first time you'...

Patrick Lam | Jul 11, 2024

Integrate a CMP workflow step with CMS

As you might know Optimizely has an integration where you can create and edit pages in the CMS directly from the CMP. One of the benefits of this i...

Marcus Hoffmann | Jul 10, 2024