feat: TinyMCE plugin insert iframe (#427)

This commit is contained in:
Jhon Vente
2024-02-16 14:02:34 -05:00
committed by GitHub
parent f33a3b5521
commit 90d5ac4ffc
5 changed files with 618 additions and 1 deletions

View File

@@ -0,0 +1,205 @@
function tinyMCEEmbedIframePlugin(editor) {
function openInsertIframeModal() {
const defaultConfig = {
title: 'Insert iframe',
body: {
type: 'tabpanel',
tabs: [
{
title: 'General',
items: [
{
type: 'input',
name: 'source',
label: 'Source URL',
multiline: false,
autofocus: true,
required: true,
},
{
type: 'selectbox',
name: 'sizeType',
label: 'Size',
items: [
{ text: 'Inline Value', value: 'inline' },
{ text: 'Big embed', value: 'big' },
{ text: 'Small embed', value: 'small' },
],
},
{
type: 'sizeinput',
name: 'size',
label: 'Dimensions',
},
],
},
{
title: 'Advanced',
items: [
{
type: 'input',
name: 'name',
label: 'Name',
value: '',
},
{
type: 'input',
name: 'title',
label: 'Title',
value: '',
},
{
type: 'input',
name: 'longDescriptionURL',
label: 'Long description URL',
value: '',
},
{
type: 'checkbox',
name: 'border',
label: 'Show iframe border',
text: 'Border',
checked: false,
},
{
type: 'checkbox',
name: 'scrollbar',
label: 'Enable scrollbar',
text: 'Scrollbar',
checked: false,
},
],
},
],
},
buttons: [
{
type: 'cancel',
name: 'cancel',
text: 'Cancel',
},
{
type: 'submit',
name: 'save',
text: 'Save',
primary: true,
},
],
onChange(api, field) {
const { name } = field;
const data = api.getData();
const { sizeType, ...fields } = data;
const isSizeTypeFiled = name === 'sizeType';
const hasCustomSize = sizeType === 'inline';
if (!hasCustomSize && isSizeTypeFiled) {
const {
body: {
tabs: [generalTab],
},
} = defaultConfig;
generalTab.items = generalTab.items.filter(
(item) => item.type !== 'sizeinput',
);
defaultConfig.initialData = { ...fields, sizeType };
api.redial(defaultConfig);
}
if (hasCustomSize && isSizeTypeFiled) {
const {
body: {
tabs: [generalTab],
},
} = defaultConfig;
const hasSizeInput = generalTab.items.some((item) => item.name === 'size');
if (!hasSizeInput) {
generalTab.items = [
...generalTab.items,
{
type: 'sizeinput',
name: 'size',
label: 'Dimensions',
},
];
}
defaultConfig.initialData = { ...fields, sizeType };
api.redial(defaultConfig);
}
},
onSubmit(api) {
const data = api.getData();
const sizeTypes = {
small: {
height: '100px',
width: '100px',
},
big: {
height: '800px',
width: '800px',
},
};
if (data.source) {
const {
size, sizeType, name, title, longDescriptionURL, border, scrollbar,
} = data;
const { width, height } = sizeTypes[sizeType] || { width: size.width, height: size.height };
const pxRegex = /^\d+px$/;
const widthFormat = pxRegex.test(width) ? width : '300px';
const heightFormat = pxRegex.test(height) ? height : '300px';
const hasScroll = scrollbar ? 'yes' : 'no';
let iframeCode = `<iframe src="${data.source}" width="${widthFormat}" height="${heightFormat}" scrolling="${hasScroll}"`;
if (name) {
iframeCode += ` name="${name}"`;
}
if (title) {
iframeCode += ` title="${title}"`;
}
if (longDescriptionURL) {
iframeCode += ` longdesc="${longDescriptionURL}"`;
}
if (!border) {
iframeCode += 'frameborder="0"';
}
iframeCode += '></iframe>';
iframeCode = `<div class="tiny-pageembed" style="width: ${widthFormat}; height: ${heightFormat}">`
+ `${iframeCode}`
+ '</div>';
editor.insertContent(iframeCode);
}
api.close();
},
};
editor.windowManager.open(defaultConfig);
}
// Register the button
editor.ui.registry.addButton('embediframe', {
text: 'Embed iframe',
onAction: openInsertIframeModal,
});
}
((tinymce) => {
if (tinymce) {
tinymce.PluginManager.add('embediframe', tinyMCEEmbedIframePlugin);
}
})(window.tinymce);
export default tinyMCEEmbedIframePlugin;

View File

@@ -0,0 +1,408 @@
import tinyMCEEmbedIframePlugin from './embedIframePlugin';
const editorMock = {
windowManager: {
open: jest.fn(),
},
insertContent: jest.fn(),
ui: {
registry: {
addButton: jest.fn(),
},
},
};
describe('TinyMCE Embed IFrame Plugin', () => {
const pluginConfig = {
title: 'Insert iframe',
body: {
type: 'tabpanel',
tabs: [
{
title: 'General',
items: [
{
type: 'input',
name: 'source',
label: 'Source URL',
multiline: false,
autofocus: true,
required: true,
},
{
type: 'selectbox',
name: 'sizeType',
label: 'Size',
items: [
{ text: 'Inline Value', value: 'inline' },
{ text: 'Big embed', value: 'big' },
{ text: 'Small embed', value: 'small' },
],
},
{
type: 'sizeinput',
name: 'size',
label: 'Dimensions',
},
],
},
{
title: 'Advanced',
items: [
{
type: 'input',
name: 'name',
label: 'Name',
value: '',
},
{
type: 'input',
name: 'title',
label: 'Title',
value: '',
},
{
type: 'input',
name: 'longDescriptionURL',
label: 'Long description URL',
value: '',
},
{
type: 'checkbox',
name: 'border',
label: 'Show iframe border',
text: 'Border',
checked: false,
},
{
type: 'checkbox',
name: 'scrollbar',
label: 'Enable scrollbar',
text: 'Scrollbar',
checked: false,
},
],
},
],
},
buttons: [
{
type: 'cancel',
name: 'cancel',
text: 'Cancel',
},
{
type: 'submit',
name: 'save',
text: 'Save',
primary: true,
},
],
};
beforeEach(() => {
jest.clearAllMocks();
});
test('opens insert iframe modal on button action', () => {
// Invoke the plugin
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
expect(editorMock.windowManager.open).toHaveBeenCalled();
});
test('opens insert iframe modal on button action validate onSubmit and OnChange function', () => {
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
expect(editorMock.windowManager.open).toHaveBeenCalledWith(
expect.objectContaining({
onSubmit: expect.any(Function),
onChange: expect.any(Function),
}),
);
});
test('opens insert iframe modal on button action validate title', () => {
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
expect(editorMock.windowManager.open).toHaveBeenCalled();
expect(editorMock.windowManager.open).toHaveBeenCalledWith(
expect.objectContaining({
title: pluginConfig.title,
}),
);
});
test('opens insert iframe modal on button action validate buttons', () => {
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
expect(editorMock.windowManager.open).toHaveBeenCalled();
expect(editorMock.windowManager.open).toHaveBeenCalledWith(
expect.objectContaining({
buttons: pluginConfig.buttons,
}),
);
});
test('opens insert iframe modal on button action validate tabs', () => {
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
const [generalTab, advancedTab] = pluginConfig.body.tabs;
expect(editorMock.windowManager.open).toHaveBeenCalled();
expect(editorMock.windowManager.open).toHaveBeenCalledWith(
expect.objectContaining({
body: { type: 'tabpanel', tabs: [generalTab, advancedTab] },
}),
);
});
test('tests onChange function in plugin', () => {
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
// Access the onChange function from the opened configuration
const onChangeFunction = editorMock.windowManager.open.mock.calls[0][0].onChange;
// Mock API and field for onChange
const apiMock = {
getData: jest.fn(() => ({ sizeType: 'big' })),
redial: jest.fn(),
};
const field = {
name: 'sizeType',
};
// Simulate calling the onChange function
onChangeFunction(apiMock, field);
expect(apiMock.getData).toHaveBeenCalled();
expect(apiMock.redial).toHaveBeenCalled();
});
test('modifies generalTab items when sizeType is not inline', () => {
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
const onChangeFunction = editorMock.windowManager.open.mock.calls[0][0].onChange;
const apiMock = {
getData: jest.fn(() => ({ sizeType: 'big' })),
redial: jest.fn(),
};
const field = {
name: 'sizeType',
};
onChangeFunction(apiMock, field);
const [generalTab, advancedTab] = pluginConfig.body.tabs;
const generalTabExpected = generalTab.items.filter(
(item) => item.type !== 'sizeinput',
);
const expectedTabs = [
{ title: generalTab.title, items: generalTabExpected, type: generalTab.type },
advancedTab,
];
const expectedBody = {
type: pluginConfig.body.type,
tabs: expectedTabs,
};
expect(apiMock.redial).toHaveBeenCalledWith(expect.objectContaining({
body: expectedBody,
}));
});
test('adds sizeinput to generalTab items when sizeType is inline', () => {
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
const onChangeFunction = editorMock.windowManager.open.mock.calls[0][0].onChange;
const apiMock = {
getData: jest.fn(() => ({ sizeType: 'inline' })),
redial: jest.fn(),
};
const field = {
name: 'sizeType',
};
onChangeFunction(apiMock, field);
const [generalTab, advancedTab] = pluginConfig.body.tabs;
expect(apiMock.redial).toHaveBeenCalledWith(
expect.objectContaining({
body: { type: 'tabpanel', tabs: [generalTab, advancedTab] },
}),
);
});
test('tests onSubmit function in plugin', () => {
const dataMock = {
source: 'https://www.example.com',
sizeType: 'big',
};
const apiMock = {
getData: jest.fn(() => dataMock),
close: jest.fn(),
};
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
const onSubmitFunction = editorMock.windowManager.open.mock.calls[0][0].onSubmit;
onSubmitFunction(apiMock);
expect(apiMock.getData).toHaveBeenCalled();
expect(editorMock.insertContent).toHaveBeenCalled();
expect(apiMock.close).toHaveBeenCalled();
});
test('tests onSubmit function in plugin advanced properties', () => {
const dataMock = {
source: 'https://www.example.com',
sizeType: 'big',
name: 'iframeName',
title: 'iframeTitle',
longDescriptionURL: 'https://example.com/description',
border: true,
scrollbar: true,
};
const apiMock = {
getData: jest.fn(() => dataMock),
close: jest.fn(),
};
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
const onSubmitFunction = editorMock.windowManager.open.mock.calls[0][0].onSubmit;
onSubmitFunction(apiMock);
expect(apiMock.getData).toHaveBeenCalled();
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining('width="800px"'));
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining('height="800px"'));
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining(`name="${dataMock.name}"`));
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining(`title="${dataMock.title}"`));
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining(`longdesc="${dataMock.longDescriptionURL}"`));
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining('scrolling="yes"'));
expect(apiMock.close).toHaveBeenCalled();
});
describe('tests onSubmit function in plugin sizeType', () => {
test('tests onSubmit function in plugin with sizeType big', () => {
const dataMock = {
source: 'https://www.example.com',
sizeType: 'big',
};
const apiMock = {
getData: jest.fn(() => dataMock),
close: jest.fn(),
};
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
const onSubmitFunction = editorMock.windowManager.open.mock.calls[0][0].onSubmit;
onSubmitFunction(apiMock);
expect(apiMock.getData).toHaveBeenCalled();
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining('width="800px"'));
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining('height="800px"'));
expect(apiMock.close).toHaveBeenCalled();
});
test('tests onSubmit function in plugin with sizeType small', () => {
const dataMock = {
source: 'https://www.example.com',
sizeType: 'small',
};
const apiMock = {
getData: jest.fn(() => dataMock),
close: jest.fn(),
};
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
const onSubmitFunction = editorMock.windowManager.open.mock.calls[0][0].onSubmit;
onSubmitFunction(apiMock);
expect(apiMock.getData).toHaveBeenCalled();
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining('width="100px"'));
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining('height="100px"'));
expect(apiMock.close).toHaveBeenCalled();
});
test('tests onSubmit function in plugin with custom sizeType', () => {
const dataMock = {
source: 'https://www.example.com',
sizeType: 'inline',
size: {
width: '500px',
height: '700px',
},
};
const apiMock = {
getData: jest.fn(() => dataMock),
close: jest.fn(),
};
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
const onSubmitFunction = editorMock.windowManager.open.mock.calls[0][0].onSubmit;
onSubmitFunction(apiMock);
expect(apiMock.getData).toHaveBeenCalled();
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining('width="500px"'));
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining('height="700px"'));
expect(apiMock.close).toHaveBeenCalled();
});
test('tests onSubmit function in plugin with custom sizeType invalid values', () => {
const dataMock = {
source: 'https://www.example.com',
sizeType: 'inline',
size: {
width: 'test',
height: 'test',
},
};
const apiMock = {
getData: jest.fn(() => dataMock),
close: jest.fn(),
};
tinyMCEEmbedIframePlugin(editorMock);
editorMock.ui.registry.addButton.mock.calls[0][1].onAction();
const onSubmitFunction = editorMock.windowManager.open.mock.calls[0][0].onSubmit;
onSubmitFunction(apiMock);
expect(apiMock.getData).toHaveBeenCalled();
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining('width="300px"'));
expect(editorMock.insertContent).toHaveBeenCalledWith(expect.stringContaining('height="300px"'));
expect(apiMock.close).toHaveBeenCalled();
});
});
});

View File

@@ -14,6 +14,7 @@ import { selectors } from '../../data/redux';
import ImageUploadModal from '../ImageUploadModal';
import SourceCodeModal from '../SourceCodeModal';
import * as hooks from './hooks';
import './customTinyMcePlugins/embedIframePlugin';
const editorConfigDefaultProps = {
setEditorRef: undefined,

View File

@@ -33,6 +33,7 @@ const pluginConfig = ({ isLibrary, placeholder, editorType }) => {
quickToolbar,
plugins.a11ychecker,
plugins.powerpaste,
plugins.embediframe,
].join(' '),
menubar: false,
toolbar: toolbar ? mapToolbars([
@@ -54,7 +55,7 @@ const pluginConfig = ({ isLibrary, placeholder, editorType }) => {
],
[imageUploadButton, buttons.link, buttons.unlink, buttons.blockQuote, buttons.codeBlock],
[buttons.table, buttons.emoticons, buttons.charmap, buttons.hr],
[buttons.removeFormat, codeButton, buttons.a11ycheck],
[buttons.removeFormat, codeButton, buttons.a11ycheck, buttons.embediframe],
]) : false,
imageToolbar: mapToolbars([
// [buttons.rotate.left, buttons.rotate.right],