Drag and Drop Image using Client Side Rendering
By Anatoly Mironov
I continue my series about Client Side Rendering (CSR) and jsgrid. Today I want to try a custom field where users can drag and drop images. The inspiration comes from:
- AutoUpload field written by Anton Vishnyakov and
- Base64 Drag and drop written by oroboto
What I want to achieve is:
- A custom field that is rendered with jslink
- Users can drag and drop small pictures (thumbnails) into the field
- A base64 image representation is saved as the field value
- Optionally implement pasting images using Clipboard API
Step 1 Create a field with a custom jslink
Create a field of type Note. I am using the PnP Core Extensions to make it quickier: ’ My jslink file is very simple to begin with: [code language=“javascript”] (function () { ‘use strict’; function view(ctx, field) { return “hello”; } var overrideContext = {}; overrideContext.Templates = overrideContext.Templates || {}; overrideContext.Templates.Fields = { ‘DragAndDropImage’: { ‘View’: view, ‘DisplayForm’: view //‘EditForm’: verySimpleNewAndEdit, //‘NewForm’: verySimpleNewAndEdit } }; SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext); })(); [/code] This will result in the following display form. Just outputting “hello” indicates that my field is jslink are registered correctly:
Step 2. Ensure base64 works in the field
Not all fields will work. I have tried many of them and found that the field type Note with Plain text works for saving base64 images. First download some sample icons at flaticons.com Convert an icon to a base64 image using dataurl.net: Save the string into the Drag And Drop Image field: Now update the view template in the CSR overrides: [code language=“javascript”] //http://stackoverflow.com/a/822486/632117 //in display form we have to strip the html elements that are pasted in by SharePoint function strip(html) { var tmp = document.createElement(“DIV”); tmp.innerHTML = html; return tmp.textContent || tmp.innerText || ‘’; } function renderImage(fieldValue) { if (fieldValue) { return [’’].join(’’); } return ‘’; } function view(ctx, field) { //disable editing in Quick Edit mode if (ctx.inGridMode) { field.AllowGridEditing = false; } var fieldValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name]; return renderImage(fieldValue); } [/code] Well, it works:
Step 3. Implement drag and drop
Let’s start with the edit template: [code language=“javascript” highlight=“6,8”] function renderDropZone(fieldValue) { var img = renderImage(fieldValue); return [’’, img, ‘’].join(’’); } function edit(ctx, field) { var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx); formCtx.registerGetValueCallback(formCtx.fieldName, function () { //will implement the logic later return ‘’; }); var fieldValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name]; return renderDropZone(fieldValue); } var overrideContext = {}; overrideContext.Templates = overrideContext.Templates || {}; overrideContext.Templates.Fields = { ‘DragAndDropImage’: { ‘View’: view, ‘DisplayForm’: view, ‘EditForm’: edit, ‘NewForm’: edit } }; [/code] I have wrapped the image element with a div (id=drop_zone). Now I need some css to show it: [code language=“javascript”] var additionalStyle = [ ‘’, ‘#drop_zone { height: 100px; width:100px; background: #efe}’, ‘’ ].join(’’); document.write(additionalStyle); [/code] Now there is a clear “drop zone”: To make it as simple as possible, let’s follow oroboto’s drag and drop code. Here we go: [code language=“javascript”] var processFiles = function (event) { var element = event.target; event.stopPropagation(); event.preventDefault(); removeDropZoneClass(event); // FileList object of File objects var files = event.dataTransfer.files; for (var i = 0, f; f = files[i]; i++) { var reader = new FileReader(); // closure to capture file info reader.onload = (function (file, index) { return function (e) { var dataUri = e.target.result; var img = renderImage(dataUri); element.innerHTML = img; }; })(f, i); // read file as data URI reader.readAsDataURL(f); } }; var highlightDropZone = function (e) { e.stopPropagation(); e.preventDefault(); document.getElementById(‘drop_zone’).classList.add(‘highlight’); } var removeDropZoneClass = function (e) { document.getElementById(‘drop_zone’).classList.remove(‘highlight’); } function prepareDropZone(ctx) { if (ctx.inGridMode) { return; } // add event listeners if File API is supported var dropZone = document.getElementById(‘drop_zone’); if (window.File && window.FileReader && window.FileList && window.Blob) { dropZone.addEventListener(‘drop’, processFiles, false); dropZone.addEventListener(‘dragover’, highlightDropZone, false); dropZone.addEventListener(‘dragenter’, highlightDropZone, false); dropZone.addEventListener(‘dragleave’, removeDropZoneClass, false); } else { dropZone.innerHTML = ‘The File APIs are not fully supported in this browser.’; dropZone.className = ‘highlight’; } }; // … code omitted for brevity overrideContext.Templates.OnPostRender = prepareDropZone; [/code] That’s not bad, not bad: The last thing in this step is to save the base64 value [code language=“javascript”] formCtx.registerGetValueCallback(formCtx.fieldName, function () { var dropZone = document.getElementById(‘drop_zone’); var img = dropZone.getElementsByTagName(‘img’); return img.length ? img[0].src : ‘’; }); [/code]
Step 4 Support pasting images
Drag And Drop is cool. But what about pasting images. Wouldn’t it be nice? The functionality is called Clipboard API. You can check which browsers support this on caniuse.com. The simplest example I’ve found is on stackoverflow: How does onpaste work The code I use is as follows: [code language=“javascript”] document.onpaste = function (event) { var items = (event.clipboardData || event.originalEvent.clipboardData).items; console.log(JSON.stringify(items)); // will give you the mime types var blob = items[0].getAsFile(); var reader = new FileReader(); reader.onload = function (e) { var dataUri = e.target.result; var img = renderImage(dataUri); dropZone.innerHTML = img; }; // data url! reader.readAsDataURL(blob); } [/code] Here is how pasting works:
Step 5 Error handling (Not done yet)
I need to ensure that
- only images are allowed
- only small images are allowed, otherwise base64 will be to heavy
- proper handling of old browsers, we don’t need to support them, but the users should get good information about what they need to make it work
- a help icon is shown, when clicked a help callout is shown about how it works
Step 6 Support Quick Edit (Not done yet)
We need code that works in Quick Edit:
- need to write code for jsgrid
- need handling of multiple drop zones on the same page (unique ids and smart handling of pasting)
Summary
This post is about discovering what’s possible to do with client side rendering and using today’s technologies like html5 and new javascript apis. I hope you’ve got some inspiration how normal sharepoint fields combined with Client Side Rendering (CSR) api can create more value in your project, in Office 365 and in SharePoint On Premises. Please leave feedback in comments.
Fulll source code
And finally, here comes the most up-to-date full source code for this custom Drag And Drop Image field:
(function () {
'use strict';
var additionalStyle = [ '<style>', '#drop_zone { height: 100px; width:100px; background: #efe}', '#drop_zone.highlight {background:#fee;}', '</style>' ].join('');
document.write(additionalStyle);
var processFiles = function (event) {
var element = event.target;
event.stopPropagation();
event.preventDefault();
removeDropZoneClass(event);
// FileList object of File objects
var files = event.dataTransfer.files;
for (var i = 0, f; f = files[i]; i++) {
var reader = new FileReader();
// closure to capture file info
reader.onload = (function (file, index) {
return function (e) {
var dataUri = e.target.result;
var img = renderImage(dataUri); element.innerHTML = img;
};
})(f, i);
// read file as data URI
reader.readAsDataURL(f);
}
};
var highlightDropZone = function (e) {
e.stopPropagation();
e.preventDefault();
document.getElementById('drop_zone').classList.add('highlight');
}
var removeDropZoneClass = function (e) {
document.getElementById('drop_zone').classList.remove('highlight');
}
function prepareDropZone(ctx) {
if (ctx.inGridMode) { return; }
// add event listeners if File API is supported
var dropZone = document.getElementById('drop_zone');
if (window.File && window.FileReader && window.FileList && window.Blob) {
dropZone.addEventListener('drop', processFiles, false);
dropZone.addEventListener('dragover', highlightDropZone, false);
dropZone.addEventListener('dragenter', highlightDropZone, false);
dropZone.addEventListener('dragleave', removeDropZoneClass, false);
} else {
dropZone.innerHTML = 'The File APIs are not fully supported in this browser.'; dropZone.className = 'highlight';
}
document.onpaste = function (event) {
var items = (event.clipboardData || event.originalEvent.clipboardData).items; console.log(JSON.stringify(items));
// will give you the mime types
var blob = items[0].getAsFile();
var reader = new FileReader();
reader.onload = function (e) {
var dataUri = e.target.result;
var img = renderImage(dataUri);
dropZone.innerHTML = img;
};
// data url!
reader.readAsDataURL(blob);
}
};
//http://stackoverflow.com/a/822486/632117
//in display form we have
//to strip the html elements that are pasted in by SharePoint
function strip(html) {
var tmp = document.createElement('DIV');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
}
function renderImage(fieldValue) {
if (fieldValue) {
return ['<img style="max-width:100px;" src="', strip(fieldValue), '"/>'].join('');
}
return '';
}
function view(ctx, field) {
//disable editing in Quick Edit mode
if (ctx.inGridMode) {
field.AllowGridEditing = false;
}
var fieldValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name];
return renderImage(fieldValue);
}
function renderDropZone(fieldValue) {
var img = renderImage(fieldValue);
return ['Drop files here<div id="drop_zone"', img, '</div>'].join('');
}
function edit(ctx, field) {
var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);
formCtx.registerGetValueCallback(formCtx.fieldName, function () {
var dropZone = document.getElementById('drop_zone');
var img = dropZone.getElementsByTagName('img');
return img.length ? img[0].src : ''; });
var fieldValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name];
return renderDropZone(fieldValue);
} v
ar overrideContext = {};
overrideContext.Templates = overrideContext.Templates || {};
overrideContext.Templates.Fields = { 'DragAndDropImage': { 'View': view, 'DisplayForm': view, 'EditForm': edit, 'NewForm': edit } };
overrideContext.Templates.OnPostRender = prepareDropZone; SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext); })();
Comments from Wordpress.com
Client Side Rendering with Async dependencies - Bool Tech - Dec 1, 2014
[…] Drag and Drop Image using Client Side Rendering […]