Skip to content
This repository has been archived by the owner on Apr 27, 2021. It is now read-only.

Commit

Permalink
MAE-165 RTE Customization (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
petrinecp authored and KenticoPavelV committed Dec 2, 2019
1 parent ca3a20b commit f3ad8ed
Show file tree
Hide file tree
Showing 30 changed files with 353 additions and 185 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -28,6 +28,7 @@ Thumbs.db

# Visual Studio cache/options directory
.vs/
.vscode/

# Ignore Administration Kentico site
CMS/
Expand Down
Expand Up @@ -26,6 +26,7 @@ public class RichTextEditorTests : UnitTests
{
private const string PROPERTY_NAME = "Test";
private const string LICENSE_KEY = "license_key";
private const string DEFUALT_CONFIGURATION_NAME = "default";

private HtmlHelper htmlHelperMock;
private TextWriter writerMock;
Expand Down Expand Up @@ -83,12 +84,20 @@ public void RichTextEditor_InstanceIsNull_ThrowsArgumentNullException()
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
public void RichTextEditor_PropertyNameIsInvalid_ThrowsArgumentNullException(string invalidPropertyName)
public void RichTextEditor_PropertyNameIsInvalid_ThrowsArgumentException(string invalidPropertyName)
{
Assert.That(() => htmlHelperMock.Kentico().RichTextEditor(invalidPropertyName), Throws.ArgumentException);
}


[TestCase(null)]
[TestCase("")]
public void RichTextEditor_ConfigurationNameNullOrEmpty_ThrowsArgumentException(string configurationName)
{
Assert.That(() => htmlHelperMock.Kentico().RichTextEditor(PROPERTY_NAME, configurationName), Throws.ArgumentException);
}


[Test]
public void RichTextEditor_PropertyNameIsValidAndLicenseIsEMSAndOnlineMarketingEnabled_WritesToViewContext()
{
Expand All @@ -102,7 +111,7 @@ public void RichTextEditor_PropertyNameIsValidAndLicenseIsEMSAndOnlineMarketingE
Received.InOrder(() =>
{
writerMock.Write($"<div data-inline-editor=\"Kentico.InlineEditor.RichText\" data-property-name=\"{PROPERTY_NAME.ToLower()}\">");
writerMock.Write($"<div class=\"ktc-rich-text-wrapper\" data-context-macros=\"{{&quot;Test&quot;:&quot;TestDisplayName&quot;}}\" data-get-link-metadata-endpoint-url=\"/testApi\" data-rich-text-editor-license=\"{LICENSE_KEY}\"></div>");
writerMock.Write($"<div class=\"ktc-rich-text-wrapper\" data-context-macros=\"{{&quot;Test&quot;:&quot;TestDisplayName&quot;}}\" data-get-link-metadata-endpoint-url=\"/testApi\" data-rich-text-editor-configuration=\"{DEFUALT_CONFIGURATION_NAME}\" data-rich-text-editor-license=\"{LICENSE_KEY}\"></div>");
writerMock.Write("</div>");
});
}
Expand All @@ -118,7 +127,7 @@ public void RichTextEditor_PropertyNameIsValidAndLicenseIsEMSAndOnlineMarketingE
Received.InOrder(() =>
{
writerMock.Write($"<div data-inline-editor=\"Kentico.InlineEditor.RichText\" data-property-name=\"{PROPERTY_NAME.ToLower()}\">");
writerMock.Write($"<div class=\"ktc-rich-text-wrapper\" data-get-link-metadata-endpoint-url=\"/testApi\" data-rich-text-editor-license=\"{LICENSE_KEY}\"></div>");
writerMock.Write($"<div class=\"ktc-rich-text-wrapper\" data-get-link-metadata-endpoint-url=\"/testApi\" data-rich-text-editor-configuration=\"{DEFUALT_CONFIGURATION_NAME}\" data-rich-text-editor-license=\"{LICENSE_KEY}\"></div>");
writerMock.Write("</div>");
});
}
Expand All @@ -134,7 +143,7 @@ public void RichTextEditor_PropertyNameIsValidAndLicenseWithoutFullContactManage
Received.InOrder(() =>
{
writerMock.Write($"<div data-inline-editor=\"Kentico.InlineEditor.RichText\" data-property-name=\"{PROPERTY_NAME.ToLower()}\">");
writerMock.Write($"<div class=\"ktc-rich-text-wrapper\" data-get-link-metadata-endpoint-url=\"/testApi\" data-rich-text-editor-license=\"{LICENSE_KEY}\"></div>");
writerMock.Write($"<div class=\"ktc-rich-text-wrapper\" data-get-link-metadata-endpoint-url=\"/testApi\" data-rich-text-editor-configuration=\"{DEFUALT_CONFIGURATION_NAME}\" data-rich-text-editor-license=\"{LICENSE_KEY}\"></div>");
writerMock.Write("</div>");
});
}
Expand All @@ -150,7 +159,24 @@ public void RichTextEditor_PropertyNameIsValidAndOnlineMarketingDisabled_WritesT
Received.InOrder(() =>
{
writerMock.Write($"<div data-inline-editor=\"Kentico.InlineEditor.RichText\" data-property-name=\"{PROPERTY_NAME.ToLower()}\">");
writerMock.Write($"<div class=\"ktc-rich-text-wrapper\" data-get-link-metadata-endpoint-url=\"/testApi\" data-rich-text-editor-license=\"{LICENSE_KEY}\"></div>");
writerMock.Write($"<div class=\"ktc-rich-text-wrapper\" data-get-link-metadata-endpoint-url=\"/testApi\" data-rich-text-editor-configuration=\"{DEFUALT_CONFIGURATION_NAME}\" data-rich-text-editor-license=\"{LICENSE_KEY}\"></div>");
writerMock.Write("</div>");
});
}


[Test]
public void RichTextEditor_ConfigurationNameProvided_ConfigurationIdentifierWrittenToViewContext()
{
const string CONFIGURATION_NAME = "testConfiguration";
DynamicTextPatternRegister.Instance = new DynamicTextPatternRegister(new List<DynamicTextPattern>());

htmlHelperMock.Kentico().RichTextEditor(PROPERTY_NAME, CONFIGURATION_NAME);

Received.InOrder(() =>
{
writerMock.Write($"<div data-inline-editor=\"Kentico.InlineEditor.RichText\" data-property-name=\"{PROPERTY_NAME.ToLower()}\">");
writerMock.Write($"<div class=\"ktc-rich-text-wrapper\" data-get-link-metadata-endpoint-url=\"/testApi\" data-rich-text-editor-configuration=\"{CONFIGURATION_NAME}\" data-rich-text-editor-license=\"{LICENSE_KEY}\"></div>");
writerMock.Write("</div>");
});
}
Expand Down
@@ -1,6 +1,5 @@
import FroalaEditor, { RegisterCommandParameters } from "froala-editor/js/froala_editor.pkgd.min";
import { getMediaFilesSelector } from "./helpers";
import { ALLOWED_IMAGE_EXTENSIONS } from "../constants";

export const imageReplaceCommand: RegisterCommandParameters = {
title: "Replace",
Expand All @@ -9,15 +8,15 @@ export const imageReplaceCommand: RegisterCommandParameters = {
undo: true,
refreshAfterCallback: false,
callback(this: FroalaEditor) {
const { image } = this;
const { image, opts } = this;
const currentImage = image.get();
const currentImageElement = (currentImage as HTMLImageElement[])[0];
// When image is inserted for the first time froala inserts the image data into "str" prefixed data attributes,
// although when the image is replaced the new values are not prefixed.
const currentImageId = currentImageElement.dataset.id || currentImageElement.dataset.strid;

getMediaFilesSelector().open({
allowedExtensions: ALLOWED_IMAGE_EXTENSIONS,
allowedExtensions: `.${opts.imageAllowedTypes.join(";.")}`,
selectedValues: [{ fileGuid: currentImageId! }],
applyCallback(images: any) {
if (images) {
Expand Down
@@ -1,7 +1,6 @@
import FroalaEditor, { RegisterCommandParameters } from "froala-editor/js/froala_editor.pkgd.min";
import { hideToolbar, showToolbar } from "../toolbar-helpers";
import { getMediaFilesSelector } from "./helpers";
import { ALLOWED_IMAGE_EXTENSIONS } from "../constants";

export const insertImageCommand: RegisterCommandParameters = {
title: "Insert Image",
Expand All @@ -10,15 +9,15 @@ export const insertImageCommand: RegisterCommandParameters = {
undo: true,
refreshAfterCallback: false,
callback(this: FroalaEditor) {
const froala = this;
const { image, opts } = this;
hideToolbar();

getMediaFilesSelector().open({
allowedExtensions: ALLOWED_IMAGE_EXTENSIONS,
allowedExtensions: `.${opts.imageAllowedTypes.join(";.")}`,
applyCallback(images) {
if (images) {
const selectedImage = images[0];
froala.image.insert(selectedImage.url, true, { name: selectedImage.name, id: selectedImage.fileGuid });
image.insert(selectedImage.url, true, { name: selectedImage.name, id: selectedImage.fileGuid });
}
showToolbar();
},
Expand Down

This file was deleted.

@@ -0,0 +1,104 @@
import FroalaEditor, { FroalaOptions, FroalaEvents } from "froala-editor/js/froala_editor.pkgd.min";

import { UPDATE_WIDGET_PROPERTY_EVENT_NAME } from "@/shared/constants";
import { replaceMacrosWithElements, replaceMacroElements, bindMacroClickListener } from "./plugins/macros/macro-services";
import { unwrapElement } from "./helpers";

type FroalaEventsOption = { [event: string]: Function };

interface CodeMirrorElement extends HTMLElement {
readonly CodeMirror: CodeMirror.Editor;
}

export const getEvents = (inlineEditor: HTMLElement, propertyName: string, propertyValue: string, customOptions: Partial<FroalaOptions>): Partial<FroalaEvents> => {
const events: Partial<FroalaEvents> = {
initialized() {
if (propertyValue) {
const editModePropertyValue = replaceMacrosWithElements(propertyValue, this.opts.contextMacros);
this.html.set(editModePropertyValue);
}
},
["html.set"]() {
bindMacroClickListener(this);
},
contentChanged() {
bindMacroClickListener(this);
updatePropertyValue(inlineEditor, propertyName, this.html.get());
},
["commands.after"](cmd: string) {
if (cmd === "html" && this.codeView.isActive()) {
// Update the underlying Froala HTML when code is changed in CodeMirror
const froalaWrapper = unwrapElement(this.$wp);
const codeMirrorInstance = froalaWrapper!.querySelector<CodeMirrorElement>(".CodeMirror");
if (codeMirrorInstance) {
codeMirrorInstance.CodeMirror.on("change", function (instance: CodeMirror.Editor) {
updatePropertyValue(inlineEditor, propertyName, instance.getValue());
});
}

// Temporary, until https://github.com/froala/wysiwyg-editor/issues/3639 is fixed
const editor = unwrapElement(this.$oel);
const codeViewExitButton = editor!.querySelector(".fr-btn.html-switch");
codeViewExitButton!.innerHTML = this.button.build("html");
}
},
};

return mergeWithCustomEvents(events, customOptions);
}

/**
* Marges the default Froala event implementations for RTE with custom ones.
* @param defaultEvents Default Froala event implementations of the RTE.
* @param customOptions Custom Froala options.
*/
const mergeWithCustomEvents = (defaultEvents: Partial<FroalaEvents>, customOptions: Partial<FroalaOptions>) => {

// Check if custom event implementations are defined
if (typeof customOptions.events !== "object") {
return defaultEvents;
}

// Make a copy of the events to keep the function pure...
const events = { ...defaultEvents } as FroalaEventsOption;
const customEvents = { ...customOptions.events } as FroalaEventsOption

// Iterate over default events
for (const eventName in events) {
const customEvent = customEvents[eventName];

// Check if same custom event implementation exists
if (typeof customEvent === "function") {
const defaultEvent = events[eventName];

// Wrap the custom implementation call with the default first
// Use regular function expression instead of arrow function, so that Froala can bind 'this' to the editor instance
events[eventName] = function (this: FroalaEditor, ...args: any[]) {
defaultEvent.call(this, ...args);
customEvent.call(this, ...args);
};

// Delete the custom event once it's wrapped with the default implementation,
// so that it doesn't overwrite itself during the merge...
delete customEvents[eventName];
}
}

// Merge the rest of the events which don't collide with the default's implementation
return {
...events,
...customEvents
};
}

const updatePropertyValue = (inlineEditor: HTMLElement, propertyName: string, newValue: string) => {
const event = new CustomEvent(UPDATE_WIDGET_PROPERTY_EVENT_NAME, {
detail: {
name: propertyName,
value: replaceMacroElements(newValue),
refreshMarkup: false
}
});

inlineEditor.dispatchEvent(event);
}
@@ -0,0 +1,42 @@
import CodeMirror from "codemirror";
import "codemirror/mode/xml/xml";
import "codemirror/lib/codemirror.css";

import { FroalaOptions, FroalaEvents } from "froala-editor/js/froala_editor.pkgd.min";
import { OPEN_LINK_CONFIGURATION_POPUP_COMMAND_NAME, OPEN_INSERT_LINK_POPUP_COMMAND_NAME } from "./plugins/links/link-constants";
import { OPEN_INSERT_MACRO_POPUP_COMMAND_NAME } from "./plugins/macros/macro-constants";

const defaultOptions: Partial<FroalaOptions> = {
toolbarInline: true,
codeMirror: CodeMirror,
pasteDeniedAttrs: ["id", "style"],
imageAllowedTypes: ["gif", "png", "jpg", "jpeg"],
quickInsertButtons: ["image", "video", "table", "ul", "ol", "hr"],
linkEditButtons: ["linkOpen", OPEN_LINK_CONFIGURATION_POPUP_COMMAND_NAME, "linkRemove"],
toolbarButtons: {
moreText: {
buttons: ["bold", "italic", "underline", "strikeThrough", "subscript", "superscript", "fontFamily", "fontSize", "textColor",
"backgroundColor", "inlineClass", "inlineStyle", "clearFormatting"]
},
moreParagraph: {
buttons: ["formatOL", "formatUL", "paragraphFormat", "alignLeft", "alignCenter", "formatOLSimple", "alignRight", "alignJustify",
"paragraphStyle", "lineHeight", "outdent", "indent", "quote"],
buttonsVisible: 2,
},
moreRich: {
buttons: [OPEN_INSERT_LINK_POPUP_COMMAND_NAME, "insertImage", OPEN_INSERT_MACRO_POPUP_COMMAND_NAME, "insertVideo", "insertTable", "emoticons", "specialCharacters", "insertHR"]
},
moreMisc: {
buttons: ["undo", "redo", "selectAll", "html", "help"],
align: "right",
buttonsVisible: 2,
}
},
};

export const getFroalaOptions = (key: string, events: Partial<FroalaEvents>, customOptions: Partial<FroalaOptions>): Partial<FroalaOptions> => ({
...defaultOptions,
...customOptions,
key,
events,
})

0 comments on commit f3ad8ed

Please sign in to comment.