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
Comments