Binh Nguyen Thi
Jul 13, 2024
  238
(2 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
SaaS CMS Tips & Tricks #001 - Blueprints

Do you have a lot of content for your website? Wish you're able to magically copy and paste the structure of a page or component easily? Are you...

Patrick Lam | Aug 19, 2024

Bring Your Own AI - Now Available in Optimizely CMS

We are introducing a new capability in Optimizely CMS: Bring Your Own AI. We call it, CustomAI! This feature, powered by the AI-Assistant for...

Luc Gosso (MVP) | Aug 19, 2024 | Syndicated blog

Jhoose Security V2.3.1 released

The latest release of the Jhoose security module offers significant updates: - Updated frontend framework and improved UX, transitioning from...

Andrew Markham | Aug 18, 2024 | Syndicated blog

Running a multisite solution on localhost in Visual Studio

Running a multisite solution on localhost through Visual Studio is quite easy to set up. A small Visual Studio config file needs a small touch and...

Henning Sjørbotten | Aug 16, 2024 | Syndicated blog