I'm developing a plugin for Summernote WYSIWYG editor (version 0.8.1) to insert iframe elements into the code.
Working with the samples provided, I managed to get the plugin button in the menu, which opens a dialog where I can enter an URL and a title. It's no problem to add an iframe Tag to the source, but this is not what I want.
I want to insert a placeholder into the code, with a markup like (or similar to) this:
<div class="ext-iframe-subst" data-src="http://www.test.example" data-title="iframe title"><span>iframe URL: http://www.test.example</span></div>
Now, summernote lets me edit the contents of the span, but I'd like to have a placeholder instead, that can't be modified in the editor.
How can I insert a placeholder into the editor, that has the following properties:
This is what I have so far:
// Extends plugins for adding iframes.
// - plugin is external module for customizing.
$.extend($.summernote.plugins, {
/**
* @param {Object} context - context object has status of editor.
*/
'iframe': function (context) {
var self = this;
// ui has renders to build ui elements.
// - you can create a button with `ui.button`
var ui = $.summernote.ui;
var $editor = context.layoutInfo.editor;
var options = context.options;
var lang = options.langInfo;
// add context menu button
context.memo('button.iframe', function () {
return ui.button({
contents: '<i class="fa fa-newspaper-o"/>',
tooltip: lang.iframe.iframe,
click: context.createInvokeHandler('iframe.show')
}).render();
});
// This events will be attached when editor is initialized.
this.events = {
// This will be called after modules are initialized.
'summernote.init': function (we, e) {
console.log('IFRAME initialized', we, e);
},
// This will be called when user releases a key on editable.
'summernote.keyup': function (we, e) {
console.log('IFRAME keyup', we, e);
}
};
// This method will be called when editor is initialized by $('..').summernote();
// You can create elements for plugin
this.initialize = function () {
var $container = options.dialogsInBody ? $(document.body) : $editor;
var body = '<div class="form-group row-fluid">' +
'<label>' + lang.iframe.url + '</label>' +
'<input class="ext-iframe-url form-control" type="text" />' +
'<label>' + lang.iframe.title + '</label>' +
'<input class="ext-iframe-title form-control" type="text" />' +
'<label>' + lang.iframe.alt + '</label>' +
'<textarea class="ext-iframe-alt form-control" placeholder="' + lang.iframe.alttext + '" rows=""10""></textarea>' +
'</div>';
var footer = '<button href="#" class="btn btn-primary ext-iframe-btn disabled" disabled>' + lang.iframe.insert + '</button>';
this.$dialog = ui.dialog({
title: lang.iframe.insert,
fade: options.dialogsFade,
body: body,
footer: footer
}).render().appendTo($container);
};
// This methods will be called when editor is destroyed by $('..').summernote('destroy');
// You should remove elements on `initialize`.
this.destroy = function () {
this.$dialog.remove();
this.$dialog = null;
};
this.bindEnterKey = function ($input, $btn) {
$input.on('keypress', function (event) {
if (event.keyCode === 13) { //key.code.ENTER) {
$btn.trigger('click');
}
});
};
this.createIframeNode = function (data) {
var $iframeSubst = $('<div class="ext-iframe-subst"><span>' + lang.iframe.iframe + '</span></div>');
$iframeSubst.attr("data-src", data.url).attr("data-title", data.title);
return $iframeSubst[0];
};
this.show = function () {
var text = context.invoke('editor.getSelectedText');
context.invoke('editor.saveRange');
console.log("iframe.getInfo: " + text);
this
.showIframeDialog(text)
.then(function (data) {
// [workaround] hide dialog before restore range for IE range focus
ui.hideDialog(self.$dialog);
context.invoke('editor.restoreRange');
// build node
var $node = self.createIframeNode(data);
if ($node) {
// insert iframe node
context.invoke('editor.insertNode', $node);
}
})
.fail(function () {
context.invoke('editor.restoreRange');
});
};
this.showIframeDialog = function (text) {
return $.Deferred(function (deferred) {
var $iframeUrl = self.$dialog.find('.ext-iframe-url');
var $iframeTitle = self.$dialog.find('.ext-iframe-title');
var $iframeBtn = self.$dialog.find('.ext-iframe-btn');
ui.onDialogShown(self.$dialog, function () {
context.triggerEvent('dialog.shown');
$iframeUrl.val(text).on('input', function () {
ui.toggleBtn($iframeBtn, $iframeUrl.val());
}).trigger('focus');
$iframeBtn.click(function (event) {
event.preventDefault();
deferred.resolve({ url: $iframeUrl.val(), title: $iframeTitle.val() });
});
self.bindEnterKey($iframeUrl, $iframeBtn);
});
ui.onDialogHidden(self.$dialog, function () {
$iframeUrl.off('input');
$iframeBtn.off('click');
if (deferred.state() === 'pending') {
deferred.reject();
}
});
ui.showDialog(self.$dialog);
});
};
}
});
// add localization texts
$.extend($.summernote.lang['en-US'], {
iframe: {
iframe: 'iframe',
url: 'iframe URL',
title: 'title',
insert: 'insert iframe',
alt: 'Text alternative',
alttext: 'you should provide a text alternative for the content in this iframe.',
test: 'Test'
}
});
You can use contenteditable
attribute on your span
element and it will work and keep the iframe
plugin HTML in the editor and it will delete the whole block when hitting Del or Backspace keys.
There are some demo plugins in the Github repository and there is one that demontrates usage of dialog and popover editing and you can check the logic and code here.
createIframeNode
we create the element and set the data attributesthis.createIframeNode = function (data) {
var $iframeSubst = $(
'<div class="ext-iframe-subst"><span contenteditable="false">' +
lang.iframe.url + ': ' + data.url +
'</span></div>'
);
$iframeSubst
.attr("data-src", data.url)
.attr("data-title", data.title);
return $iframeSubst[0];
};
We also create a currentEditing
variable to save the element under cursor when the popover menu popups so the opoup dialog will know that we're editing an element and not create a new one.
updateIframeNode
we're using the currentEditing
element to updateHere we re-create only the span
element because the currentEditing
is the actual div.ext-iframe-subst
and then we update the data attributes:
this.updateIframeNode = function (data) {
$(currentEditing).html(
'<span contenteditable="false">' +
lang.iframe.url + ': ' + data.url +
'</span>'
)
$(currentEditing)
.attr("data-src", data.url)
.attr("data-title", data.title);
}
Run the code snippet and try to insert iframes using the button with the square icon. You can edit existing iFrame elements and the block deletes all together.
/**
* @param {Object} context - context object has status of editor.
*/
var iframePlugin = function (context) {
var self = this;
// ui has renders to build ui elements.
// - you can create a button with `ui.button`
var ui = $.summernote.ui;
var dom = $.summernote.dom;
var $editor = context.layoutInfo.editor;
var currentEditing = null;
var options = context.options;
var lang = options.langInfo;
// add context menu button
context.memo('button.iframe', function () {
return ui.button({
contents: '<i class="note-icon-frame"/>',
tooltip: lang.iframe.iframe,
click: (event) => {
currentEditing = null;
context.createInvokeHandler('iframe.show')(event);
}
}).render();
});
context.memo('button.iframeDialog', function () {
return ui.button({
contents: '<i class="note-icon-frame"/>',
tooltip: lang.iframe.iframe,
click: (event) => {
context.createInvokeHandler('iframe.show')(event);
// currentEditing
}
}).render();
});
// This events will be attached when editor is initialized.
this.events = {
// This will be called after modules are initialized.
'summernote.init': function (we, e) {
$('data.ext-iframe', e.editable).each(function() { self.setContent($(this)); });
},
// This will be called when user releases a key on editable.
'summernote.keyup summernote.mouseup summernote.change summernote.scroll': function() {
self.update();
},
'summernote.dialog.shown': function() {
self.hidePopover();
},
};
// This method will be called when editor is initialized by $('..').summernote();
// You can create elements for plugin
this.initialize = function () {
var $container = options.dialogsInBody ? $(document.body) : $editor;
var body = '<div class="form-group row-fluid">' +
'<label>' + lang.iframe.url + '</label>' +
'<input class="ext-iframe-url form-control" type="text" />' +
'<label>' + lang.iframe.title + '</label>' +
'<input class="ext-iframe-title form-control" type="text" />' +
// '<label>' + lang.iframe.alt + '</label>' +
// '<textarea class="ext-iframe-alt form-control" placeholder="' + lang.iframe.alttext + '" rows=""10""></textarea>' +
'</div>';
var footer = '<button href="#" class="btn btn-primary ext-iframe-btn disabled" disabled>' + lang.iframe.insertOrUpdate + '</button>';
this.$dialog = ui.dialog({
title: lang.iframe.insert,
fade: options.dialogsFade,
body: body,
footer: footer
}).render().appendTo($container);
// create popover
this.$popover = ui.popover({
className: 'ext-iframe-popover',
}).render().appendTo('body');
var $content = self.$popover.find('.popover-content');
context.invoke('buttons.build', $content, options.popover.iframe);
};
// This methods will be called when editor is destroyed by $('..').summernote('destroy');
// You should remove elements on `initialize`.
this.destroy = function () {
self.$popover.remove();
self.$popover = null;
self.$dialog.remove();
self.$dialog = null;
};
this.bindEnterKey = function ($input, $btn) {
$input.on('keypress', function (event) {
if (event.keyCode === 13) { //key.code.ENTER) {
$btn.trigger('click');
}
});
};
self.update = function() {
// Prevent focusing on editable when invoke('code') is executed
if (!context.invoke('editor.hasFocus')) {
self.hidePopover();
return;
}
var rng = context.invoke('editor.createRange');
var visible = false;
var $data = $(rng.sc).closest('div.ext-iframe-subst');
if ($data.length) {
currentEditing = $data[0];
var pos = dom.posFromPlaceholder(currentEditing);
const containerOffset = $(options.container).offset();
pos.top -= containerOffset.top;
pos.left -= containerOffset.left;
self.$popover.css({
display: 'block',
left: pos.left,
top: pos.top,
});
// save editor target to let size buttons resize the container
context.invoke('editor.saveTarget', currentEditing);
visible = true;
}
// hide if not visible
if (!visible) {
self.hidePopover();
}
};
self.hidePopover = function() {
self.$popover.hide();
};
this.createIframeNode = function (data) {
var $iframeSubst = $(
'<div class="ext-iframe-subst"><span contenteditable="false">' +
lang.iframe.url + ': ' + data.url +
'</span></div>'
);
$iframeSubst.attr("data-src", data.url).attr("data-title", data.title);
return $iframeSubst[0];
};
this.updateIframeNode = function (data) {
$(currentEditing).html(
'<span contenteditable="false">' +
lang.iframe.url + ': ' + data.url +
'</span>'
)
$(currentEditing).attr("data-src", data.url).attr("data-title", data.title);
}
this.show = function () {
var text = context.invoke('editor.getSelectedText');
context.invoke('editor.saveRange');
this
.showIframeDialog(text)
.then(function (data) {
// [workaround] hide dialog before restore range for IE range focus
ui.hideDialog(self.$dialog);
context.invoke('editor.restoreRange');
if (currentEditing) {
self.updateIframeNode(data);
} else {
// build node
var $node = self.createIframeNode(data);
if ($node) {
// insert iframe node
context.invoke('editor.insertNode', $node);
}
}
})
.fail(function () {
context.invoke('editor.restoreRange');
});
};
this.showIframeDialog = function (text) {
return $.Deferred(function (deferred) {
var $iframeUrl = self.$dialog.find('.ext-iframe-url');
var $iframeTitle = self.$dialog.find('.ext-iframe-title');
var $iframeBtn = self.$dialog.find('.ext-iframe-btn');
ui.onDialogShown(self.$dialog, function () {
context.triggerEvent('dialog.shown');
var dataSrc = currentEditing ? $(currentEditing).attr('data-src') : '';
var dataTitle = currentEditing ? $(currentEditing).attr('data-title') : '';
$iframeTitle.val(dataTitle);
$iframeUrl.val(dataSrc).on('input', function () {
ui.toggleBtn($iframeBtn, $iframeUrl.val());
}).trigger('focus');
$iframeBtn.click(function (event) {
event.preventDefault();
deferred.resolve({ url: $iframeUrl.val(), title: $iframeTitle.val() });
});
self.bindEnterKey($iframeUrl, $iframeBtn);
});
ui.onDialogHidden(self.$dialog, function () {
$iframeUrl.off('input');
$iframeBtn.off('click');
if (deferred.state() === 'pending') {
deferred.reject();
}
});
ui.showDialog(self.$dialog);
});
};
}
// Extends plugins for adding iframes.
// - plugin is external module for customizing.
$.extend(true, $.summernote, {
plugins: {
iframe: iframePlugin,
},
options: {
popover: {
iframe: [
['iframe', ['iframeDialog']],
],
},
},
lang: {
'en-US': {
iframe: {
iframe: 'iframe',
url: 'iframe URL',
title: 'title',
insertOrUpdate: 'insert/update iframe',
alt: 'Text alternative',
alttext: 'you should provide a text alternative for the content in this iframe.',
test: 'Test',
},
},
},
});
$(document).ready(function() {
$('#editor').summernote({
height: 200,
toolbar: [
['operation', ['undo', 'redo']],
['style', ['bold', 'italic', 'underline']],
['color', ['color']],
['insert', ['iframe', 'link','picture', 'hr']],
['view', ['codeview']],
],
});
});
<!-- include libraries(jQuery, bootstrap) -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<!-- include summernote css/js -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/summernote.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/summernote.min.js"></script>
<div id="editor">Hello Summernote</div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With