Create Simple Custom Source Plugin With Filestack

Comprehensive guide to creating and configuring a Custom Source in the HTML File Picker, enhancing flexibility with Filestack's tools..

Overview

In this tutorial we will create simple Custom Source for the File Picker from the static list of files. In more complex use cases you can build Custom Source based on your dynamically generated list or API response.

You can find full Custom Source API reference here.

Github

Source code is available at Github Repository

Create a Plugin with basic information

First, we need to set up basic information about our custom source. Let’s name it My new Custom Source and give it a unique key, myCustomSource. All new lines will be commended in code.

const myNewCustomSource = {
    label: 'My new Custom Source',
    name: 'myCustomSource',
    icon: '<svg height="36" viewBox="0 0 36 36" width="36" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="18" cy="18" fill="#eee" r="18"/><path d="m9.9 18c0-1.71 1.39-3.1 3.1-3.1h4v-1.9h-4c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9h-4c-1.71 0-3.1-1.39-3.1-3.1zm4.1 1h8v-2h-8zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4v1.9h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" fill="#000" fill-opacity=".7"/></g></svg>',
    mounted: (element, actions) => {

    },
    unmounted: (element) => {

    }
}

Create an HTML list for example

Let’s create some HTML list that will be used as our sample file list

const sourceUrlsArray = [{
    url: 'https://picsum.photos/200/200',
    name: 'Sample image 200x200',
},{
    url: 'https://picsum.photos/300/300',
    name: 'Sample image 300x300',
},{
    url: 'https://picsum.photos/400/400',
    name: 'Sample image 400x400',
},{
    url: 'https://picsum.photos/500/500',
    name: 'Sample image 500x500',
},{
    url: 'https://picsum.photos/600/600',
    name: 'Sample image 600x600',
}] // this can be obtained form any place, for tutorial purposes we will set it static

const myNewCustomSource = {
    label: 'My new Custom Source',
    name: 'myCustomSource',
    icon: '<svg height="36" viewBox="0 0 36 36" width="36" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="18" cy="18" fill="#eee" r="18"/><path d="m9.9 18c0-1.71 1.39-3.1 3.1-3.1h4v-1.9h-4c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9h-4c-1.71 0-3.1-1.39-3.1-3.1zm4.1 1h8v-2h-8zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4v1.9h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" fill="#000" fill-opacity=".7"/></g></svg>',
    mounted: (element, actions) => {
        // create new list for plugin view
        const list = document.createElement('ul');
        list.setAttribute('class', 'fsp-picker--custom-source');

        sourceUrlsArray.forEach((element, idx) => {
            // add list element
            const li = document.createElement("li");
            const span = document.createElement("span");

            // create thumbnail view
            const thumb = document.createElement("img");
            thumb.setAttribute("src", element.url);
            thumb.width = "50";
            thumb.height = "50";

            // append elements to list
            li.appendChild(thumb);
            span.innerHTML = `${element.name}`;

            li.appendChild(span);
            list.appendChild(li);
        });

        // append list to picker placeholder
        element.appendChild(list);
    },
    unmounted: (element) => {

    }
}

Handle click event

Now we have to handle user click event on our file list so we will know what files where selected for upload:

const sourceUrlsArray = [{
    url: 'https://picsum.photos/200/200',
    name: 'Sample image 200x200',
},{
    url: 'https://picsum.photos/300/300',
    name: 'Sample image 300x300',
},{
    url: 'https://picsum.photos/400/400',
    name: 'Sample image 400x400',
},{
    url: 'https://picsum.photos/500/500',
    name: 'Sample image 500x500',
},{
    url: 'https://picsum.photos/600/600',
    name: 'Sample image 600x600',
}] // this can be obtained form any place, for tutorial purposes we will set it static

const myNewCustomSource = {
    label: 'My new Custom Source',
    name: 'myCustomSource',
    icon: '<svg height="36" viewBox="0 0 36 36" width="36" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="18" cy="18" fill="#eee" r="18"/><path d="m9.9 18c0-1.71 1.39-3.1 3.1-3.1h4v-1.9h-4c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9h-4c-1.71 0-3.1-1.39-3.1-3.1zm4.1 1h8v-2h-8zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4v1.9h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" fill="#000" fill-opacity=".7"/></g></svg>',
    mounted: (element, actions) => {
        const list = document.createElement('ul');
        list.setAttribute('class', 'fsp-picker--custom-source');

        // handle click on element
        const handleClick = (e) => {
            const idx = parseInt(e.currentTarget.dataset.idx);
            console.log('clicked index', idx);
        };

        sourceUrlsArray.forEach((element, idx) => {
            const li = document.createElement("li");
            const span = document.createElement("span");

            // lets set idx as dataset attribute
            li.dataset.idx = idx;
            // and set id attr, so we can have easy access to element
            li.setAttribute('id', `file-${idx}`)

            const thumb = document.createElement("img");
            thumb.setAttribute("src", element.url);
            thumb.width = "20";
            thumb.height = "20";

            li.appendChild(thumb);
            span.innerHTML = `${element.name}`;

            li.appendChild(span);

            li.addEventListener('click', handleClick);
            list.appendChild(li);
        });

        element.appendChild(list);
    },
    unmounted: (element) => {

    }
}

Add file to upload list after click

Now that we can handle user click, let’s handle adding files to the File Picker upload list:

const sourceUrlsArray = [{
    url: 'https://picsum.photos/200/200',
    name: 'Sample image 200x200',
},{
    url: 'https://picsum.photos/300/300',
    name: 'Sample image 300x300',
},{
    url: 'https://picsum.photos/400/400',
    name: 'Sample image 400x400',
},{
    url: 'https://picsum.photos/500/500',
    name: 'Sample image 500x500',
},{
    url: 'https://picsum.photos/600/600',
    name: 'Sample image 600x600',
}] // this can be obtained form any place, for tutorial purposes we will set it static

const myNewCustomSource = {
    label: 'My new Custom Source',
    name: 'myCustomSource',
    icon: '<svg height="36" viewBox="0 0 36 36" width="36" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="18" cy="18" fill="#eee" r="18"/><path d="m9.9 18c0-1.71 1.39-3.1 3.1-3.1h4v-1.9h-4c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9h-4c-1.71 0-3.1-1.39-3.1-3.1zm4.1 1h8v-2h-8zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4v1.9h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" fill="#000" fill-opacity=".7"/></g></svg>',
    mounted: (element, actions) => {
        const list = document.createElement('ul');
        list.setAttribute('class', 'fsp-picker--custom-source');
        const toUpload = [];

          // handle click on element
        const handleClick = (e) => {
            e.stopPropagation();

            const element = e.currentTarget;
            const idx = parseInt(element.dataset.idx);

            // selected file
            const file = sourceUrlsArray[idx];

            // if file is selected for upload mark it and add to toUpload list if not deselect it
            if (toUpload[idx]) {
                // remove selected class from element
                element.classList.remove('file-selected');

                // remove element from toupload list
                delete toUpload[idx];
            } else {
                // add selected class to element
                element.classList.add('file-selected');

                // set element to toDupload list
                toUpload[idx] = file;
            }

            return false;
        };

        sourceUrlsArray.forEach((element, idx) => {
            const li = document.createElement("li");
            const span = document.createElement("span");

            li.dataset.idx = idx;
            li.setAttribute('id', `file-${idx}`)

            const thumb = document.createElement("img");
            thumb.setAttribute("src", element.url);
            thumb.width = "20";
            thumb.height = "20";

            li.appendChild(thumb);
            span.innerHTML = `${element.name}`;

            li.appendChild(span);

            li.addEventListener('click', handleClick);
            list.appendChild(li);
        });

        element.appendChild(list);
    },
    unmounted: (element) => {

    }
}

Upload files and display summary

Now, that we can select files, let’s learn how we can upload them and display a summary view using actions.fetchAndAddURL and actions.showSummaryView. You can find more actions in Custom Source API reference here.:

const sourceUrlsArray = [{
    url: 'https://picsum.photos/200/200',
    name: 'Sample image 200x200',
},{
    url: 'https://picsum.photos/300/300',
    name: 'Sample image 300x300',
},{
    url: 'https://picsum.photos/400/400',
    name: 'Sample image 400x400',
},{
    url: 'https://picsum.photos/500/500',
    name: 'Sample image 500x500',
},{
    url: 'https://picsum.photos/600/600',
    name: 'Sample image 600x600',
}];

const myNewCustomSource = {
    label: 'My new Custom Source',
    name: 'myCustomSource',
    icon: '<svg height="36" viewBox="0 0 36 36" width="36" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="18" cy="18" fill="#eee" r="18"/><path d="m9.9 18c0-1.71 1.39-3.1 3.1-3.1h4v-1.9h-4c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9h-4c-1.71 0-3.1-1.39-3.1-3.1zm4.1 1h8v-2h-8zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4v1.9h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" fill="#000" fill-opacity=".7"/></g></svg>',
    mounted: (element, actions) => {
        const list = document.createElement('ul');
        list.setAttribute('class', 'fsp-picker--custom-source');
        
        let toUpload = {};

        const handleClick = (e) => {
            e.stopPropagation();

            const element = e.currentTarget;
            const idx = parseInt(element.dataset.idx);
            const file = sourceUrlsArray[idx];

            if (toUpload[idx]) {
                element.classList.remove('file-selected');
                delete toUpload[idx];
            } else {
                element.classList.add('file-selected');
                toUpload[idx] = file;
            }

            // we can add some state to upload btn if files array is empty
            const finishBtn = document.querySelector('.fsp-picker--custom-source-footer button');
            if (toUpload.length === 0) {
                finishBtn.classList.add('btn-disabled');
            } else {
                finishBtn.classList.remove('btn-disabled');
            }

            return false;
        };

        const handleFinishClick = (e) => {
            e.stopPropagation();

            // if there is no files to upload just do nothing
            if (!toUpload || Object.keys(toUpload).length === 0) {
                return;
            }

            const todo = [];
            // lets take all selected files list and add it to upload queue in picker
            Object.values(toUpload).forEach((file) => todo.push(actions.fetchAndAddUrl(file.url)));

            Promise.all(todo).then(() => {
                console.log('All files has been added to queue');

                // after adding all files to queue we can switch view to upload summary
                actions.showSummaryView();

                // cleanup toUpload object
                toUpload = {};
            });
            return false;
        };

        sourceUrlsArray.forEach((element, idx) => {
            const li = document.createElement("li");
            const span = document.createElement("span");

            li.dataset.idx = idx;
            li.setAttribute('id', `file-${idx}`)

            const thumb = document.createElement("img");
            thumb.setAttribute("src", element.url);
            thumb.width = "20";
            thumb.height = "20";

            li.appendChild(thumb);
            span.innerHTML = `${element.name}`;

            li.appendChild(span);

            li.addEventListener('click', handleClick);
            list.appendChild(li);
        });

        // lets add button that will handle finish selecting file process
       
          // header
          const divHeader = document.createElement('div');
          divHeader.setAttribute('class', 'fsp-picker--custom-source-header');

          let buttonViewType = document.createElement('span');
          buttonViewType.classList.add('view-type');

          divHeader.appendChild(buttonViewType);
        
          // View type change handler
          const handleViewTypeClick = (e) => {
            e.stopPropagation();
            if (element.classList.contains('grid')) {
              element.classList.remove('grid');
            } else {
              element.classList.add('grid');
            }
          };

          buttonViewType.addEventListener('click', handleViewTypeClick);

          // lets add button that will handle finish selecting file process
          const divFooter = document.createElement('div');
          divFooter.setAttribute('class', 'fsp-picker--custom-source-footer');

          const finishBtn = document.createElement('button');
          finishBtn.innerText = 'View/Edit selected';
          finishBtn.classList.add('btn-disabled');

          if (actions.filesList && actions.filesList.length > 0) {
            finishBtn.classList.remove('btn-disabled');
          }

          const footerSpan = document.createElement('span');
          footerSpan.innerHTML = 'Selected Files: ' + actions.count;

          divFooter.appendChild(footerSpan);
          divFooter.appendChild(finishBtn);

          finishBtn.addEventListener('click', handleFinishClick);

          element.appendChild(divHeader);
          element.appendChild(list);
          element.appendChild(divFooter);
    },
    unmounted: (element) => {

    }
}

Add Custom Source to the File Picker

const apikey = YOUR_APIKEY;
const client = filestack.init(apikey);
const options = {
    fromSources: [ 'local_file_system', myNewCustomSource, 'googledrive', 'unsplash', 'facebook', 'instagram']
    maxFiles: 20,
    uploadInBackground: false,
    onUploadDone: (res) => console.log(res),
};

const picker = client.picker(options);
picker.open();

Have questions? We’re here to help! Contact Support