Summernote "image upload" doesn't show on toolbar
Hi Sean, can you provide your summernote code here? As well as a screenshot of your summernote text area?
Interesting. Just out of curiosity, what happens if you remove the HTML View button or add the Image Upload to a different group? I'm just grasping at straws here but every summernote text area I have seen had the HTML View button last.
Hmmm, no that's not it. I just tested for myself. Can you paste your summernote code here?
In addition to Brad suggestion of posting the code here, some questions
-
Do you see any error on console when you preview your project?
Asking this because I can see an error on the bottom of the Wappler App:
-
Your Summernote is inside a modal and there is a checkbox for that, is checked?
PS: You're using Wappler 6.5.4 and seems there's no legend for "Wappler update" on the right top corner, strange, but should not be a problem, remember there's a new version that can be downloaded by: right click on the tray system icon -> update
Good eye @franse. I totally missed those. Especially the modal check box.
box is checked
@seanjm did you ever figure this out? It's not showing for me either.
never worked to this date
I just added the file upload, video upload and audio upload as well and none of those show up either. I'm going to see if anything sticks out to me in the summernote-update.js file which appears to be where the function to render the buttons is.
$.extend($.summernote.plugins, {
/**
* @param {Object} context - context object has status of editor.
*/
'file': function (context) {
let self = this,
// ui has renders to build ui elements
// for e.g. you can create a button with 'ui.button'
ui = $.summernote.ui,
$note = context.layoutInfo.note,
// contentEditable element
$editor = context.layoutInfo.editor,
$editable = context.layoutInfo.editable,
$toolbar = context.layoutInfo.toolbar,
// options holds the Options Information from Summernote and what we extended above.
options = context.options,
// lang holds the Language Information from Summernote and what we extended above.
lang = options.langInfo;
context.memo('button.file', function () {
// Here we create a button
let button = ui.button({
// icon for button
contents: options.file.icon,
// tooltip for button
tooltip: lang.file.file,
click: function (e) {
options.file.type = 'file';
context.invoke('file.show');
}
});
return button.render();
});
context.memo('button.imageFile', function () {
let button = ui.button({
contents: options.file.imageIcon,
tooltip: lang.file.image,
click: function (e) {
options.file.type = 'image';
context.invoke('file.show');
}
});
return button.render();
});
context.memo('button.videoFile', function () {
let button = ui.button({
contents: options.file.videoIcon,
tooltip: lang.file.video,
click: function (e) {
options.file.type = 'video';
context.invoke('file.show');
}
});
return button.render();
});
context.memo('button.audioFile', function () {
let button = ui.button({
contents: options.file.audioIcon,
tooltip: lang.file.audio,
click: function (e) {
options.file.type = 'audio';
context.invoke('file.show');
}
});
return button.render();
});
this.initialize = function () {
// This is how we can add a Modal Dialog to allow users to interact with the Plugin.
// get the correct container for the plugin how it's attached to the document DOM.
let $container = options.dialogsInBody ? $(document.body) : $editor;
let fileLimitation = '';
if (options.maximumFileSize) {
let unit = Math.floor(Math.log(options.maximumFileSize) / Math.log(1024));
let readableSize = (options.maximumFileSize / Math.pow(1024, unit)).toFixed(2) * 1 +
' ' + ' KMGTP'[unit] + 'B';
fileLimitation = '<small>' + lang.file.maximumFileSize + ' : ' + readableSize + '</small>';
}
// Build the Body HTML of the Dialog.
let body = [
'<div class="form-group note-form-group note-group-select-from-files">',
'<label class="note-form-label">' + lang.file.selectFromFiles + '</label>',
'<input class="note-file-input form-control-file note-form-control note-input" type="file" name="file" accept="*/*">',
'</div>',
fileLimitation,
'<div class="form-group note-group-image-url" style="overflow:auto;">',
'<label class="note-form-label">' + lang.file.url + '</label>',
'<input class="note-file-url form-control note-form-control note-input" type="text">',
'</div>'
].join('');
// Build the Footer HTML of the Dialog.
let footer = '<button href="#" class="btn btn-primary note-file-btn">' + lang.file.insert + '</button>';
this.$dialog = ui.dialog({
// Set the title for the Dialog. Note: We don't need to build the markup for the Modal
// Header, we only need to set the Title.
title: lang.file.insert,
// Set the Body of the Dialog.
body: body,
// Set the Footer of the Dialog.
footer: footer
// This adds the Modal to the DOM.
}).render().appendTo($container);
};
this.destroy = function () {
ui.hideDialog(this.$dialog);
this.$dialog.remove();
};
this.bindEnterKey = function ($input, $btn) {
$input.on('keypress', function (event) {
if (event.keyCode === 13)
$btn.trigger('click');
});
};
this.bindLabels = function () {
self.$dialog.find('.form-control:first').focus().select();
self.$dialog.find('label').on('click', function () {
$(this).parent().find('.form-control:first').focus();
});
};
/**
* @method readFileAsDataURL
*
* read contents of file as representing URL
*
* @param {File} file
* @return {Promise} - then: dataUrl
*
* @todo this method already exists in summernote.js so we should use that one
*/
this.readFileAsDataURL = function (file) {
return $.Deferred(function (deferred) {
$.extend(new FileReader(), {
onload: function (e) {
var dataURL = e.target.result;
deferred.resolve(dataURL);
},
onerror: function (err) {
deferred.reject(err);
}
}).readAsDataURL(file);
}).promise();
};
this.createFile = function (url) {
// IMG url patterns (jpg, jpeg, png, gif, svg, webp)
var imgRegExp = /^.+.(jpg|jpeg|png|gif|svg|webp)$/;
var imgBase64RegExp = /^data:(image\/jpeg|image\/png|image\/gif|image\/svg|image\/webp).+$/;
// AUDIO url patterns (mp3, ogg, oga)
var audioRegExp = /^.+.(mp3|ogg|oga)$/;
var audioBase64RegExp = /^data:(audio\/mpeg|audio\/ogg).+$/;
// VIDEO url patterns (mp4, ogc, webm)
var videoRegExp = /^.+.(mp4|ogv|webm)$/;
var videoBase64RegExp = /^data:(video\/mpeg|video\/mp4|video\/ogv|video\/webm).+$/;
var $file;
if (url.match(imgRegExp) || url.match(imgBase64RegExp)) {
$file = $('<img>')
.attr('src', url)
;
} else if (url.match(audioRegExp) || url.match(audioBase64RegExp)) {
$file = $('<audio controls>')
.attr('src', url)
;
} else if (url.match(videoRegExp) || url.match(videoBase64RegExp)) {
$file = $('<video controls>')
.attr('src', url)
;
} else {
//We can't use this type of file. You have to implement onFileUpload into your Summernote
console.log('File type not supported. Please define "onFileUpload" callback in Summernote.');
return false;
}
$file.addClass('note-file-clip');
return $file;
};
this.insertFile = function (src, param) {
var $file = self.createFile(src);
if (!$file) {
context.triggerEvent('file.upload.error');
}
context.invoke('editor.beforeCommand');
if (typeof param === 'string') {
$file.attr('data-filename', param);
}
$file.show();
context.invoke('editor.insertNode', $file[0]);
context.invoke('editor.afterCommand');
};
this.insertFilesAsDataURL = function (files) {
$.each(files, function (idx, file) {
var filename = file.name;
if (options.maximumFileSize && options.maximumFileSize < file.size) {
context.triggerEvent('file.upload.error', lang.file.maximumFileSizeError);
} else {
self.readFileAsDataURL(file).then(function (dataURL) {
return self.insertFile(dataURL, filename);
}).fail(function () {
context.triggerEvent('file.upload.error');
});
}
});
};
this.uploadFiles = function(files) {
var file = files[0];
if (options.maximumFileSize && options.maximumFileSize < file.size) {
console.error(lang.file.maximumFileSizeError);
context.triggerEvent('file.upload.error', lang.file.maximumFileSizeError);
return;
}
if (options.file.type != 'file' && file.type.indexOf(options.file.type) == -1) {
console.error(lang.file.acceptError);
context.triggerEvent('file.upload.error', lang.file.acceptError);
return;
}
var url = options.file[options.file.type + 'Url'] || options.file.url;
var data = new FormData();
data.append('file', file);
if (!url) {
console.error('No url is set.');
return;
}
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.onload = function() {
if (this.status == 200) {
try {
var response = JSON.parse(this.response);
var listMimeImg = ['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/svg'];
var listMimeAudio = ['audio/mpeg', 'audio/ogg'];
var listMimeVideo = ['video/mpeg', 'video/mp4', 'video/webm'];
var $file;
if (listMimeImg.indexOf(file.type) > -1) {
$file = $('<img>').attr('src', response.url);
} else if (listMimeAudio.indexOf(file.type) > -1) {
$file = $('<audio controls preload="metadata">').attr('src', reaponse.url);
} else if (listMimeVideo.indexOf(file.type) > -1) {
$file = $('<video controls preload="metadata">').attr('src', response.url);
} else {
$file = $('<a>').attr('title', file.name).attr('href', response.url).text(file.name);
}
$file.addClass('note-file-clip');
context.invoke('editor.beforeCommand');
$file.show();
context.invoke('editor.insertNode', $file[0]);
context.invoke('editor.afterCommand');
} catch(err) {
console.error(err);
context.triggerEvent('file.upload.error');
}
} else {
context.triggerEvent('file.upload.error');
}
};
xhr.send(data);
};
this.show = function (data) {
context.invoke('editor.saveRange');
this.showFileDialog().then(function (data) {
// [workaround] hide dialog before restore range for IE range focus
ui.hideDialog(self.$dialog);
context.invoke('editor.restoreRange');
if (typeof data === 'string') { // file url
// If onFileLinkInsert set
if (options.callbacks.onFileLinkInsert) {
context.triggerEvent('file.link.insert', data);
} else {
self.insertFile(data);
}
} else { // array of files
// If onFileUpload set
if (options.callbacks.onFileUpload) {
context.triggerEvent('file.upload', data);
} else { //if (options.file.url) {
self.uploadFiles(data);
} //else {
// else insert File as dataURL
//self.insertFilesAsDataURL(data);
//}
}
}).fail(function () {
context.invoke('editor.restoreRange');
});
};
this.showFileDialog = function (type) {
return $.Deferred(function (deferred) {
var $fileInput = self.$dialog.find('.note-file-input');
var $fileUrl = self.$dialog.find('.note-file-url');
var $fileBtn = self.$dialog.find('.note-file-btn');
var accept = options.file.type == 'file' ? '*/*' : options.file.type + '/*';
$fileInput.attr('accept', accept);
$fileBtn.text(lang.file.insert + ' ' + lang.file[options.file.type]);
ui.onDialogShown(self.$dialog, function () {
context.triggerEvent('dialog.shown');
// Cloning FileInput to clear element.
$fileInput.replaceWith($fileInput.clone().on('change', function (event) {
deferred.resolve(event.target.files || event.target.value);
}).val(''));
$fileBtn.click(function (e) {
e.preventDefault();
deferred.resolve($fileUrl.val());
});
$fileUrl.on('keyup paste', function () {
var url = $fileUrl.val();
ui.toggleBtn($fileBtn, url);
}).val('');
self.bindEnterKey($fileUrl, $fileBtn);
self.bindLabels();
});
ui.onDialogHidden(self.$dialog, function () {
$fileInput.off('change');
$fileUrl.off('keyup paste keypress');
$fileBtn.off('click');
if (deferred.state() === 'pending')
deferred.reject();
});
ui.showDialog(self.$dialog);
});
};
}
});
The image upload shows in the picture modal. Click the picture icon and you will see it (you can also add it separately).
Remember to add the additional features to a group if you want them displayed in the toolbar.
I can reproduce the error..
It happens on Lite
theme (workaround = change it to bootstrap
)
V1 Lite:

V2 Bootstrap:

Turns out my issue was that Wappler simply hadn't added the upload js file.
I added this and they show up now.
<script src="/dmxAppConnect/dmxSummernoteFileUpload/summernote-upload.js" defer></script>
Yes! And make sure that is placed AFTER summernote on the <head>
tags
Yes I did. I didn't think to look for it until I investigated where the button code was and saw it was in that file and low and behold, the file wasn't included. Sometimes I overthink haha
finally something that works, wappler team needs to look into this
This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.