Paste Images, Drag and Drop Images

Hi All

We have been working on a copy and paste feature in our app where an image can be:

  1. Dragged and dropped;
  2. Uploaded; and
  3. Pasted from the desktop clipboard.

One of the items that would be nice is if when navigating to the view we could focus on the html component rather than requiring a user to first click on the same.

Perhaps something to mull over, though our use case might be particularly unique the intention is to make the lives of our users easier when submitting support requests.

Taking a screenshot, pasting it into an app, saving it, uploading it etc is just so unnecessary. So we have made it easier for our users in a way.

If I am missing something completely obvious, please share.

Kind regards
Matt

@matthew That seems like a very useful HTML Bridge implementation.

Perhaps you can share the HTML Bridge codebase with us here?

As for the focus - which aspect of your copy/paste feature only works after the first click?

Hi Tielman

Thank you for your reply.

I am happy to share with the community and if there are any suggestions or feedback, I would be glad to receive the same.

HTML COMPONENT (UPLOADED TO ASSETS IN JOURNEY APP)

<!DOCTYPE html>
<html lang="en">
<head>
    <!-- GENERAL META TAGS -->
    <title>Support Ticket Attachment Generator</title>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8;" />
    <meta name="format-detection" content="telephone=no">
    <meta name="msapplication-tap-highlight" content="no">
    <meta name="viewport" content="viewport-fit=cover, user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
    <!-- REFERENCE TO THE JOURNEY-IFRAME-CLIENT, DO NOT DELETE -->
    <script src="https://cdn.jsdelivr.net/npm/journey-iframe-client@0.2.0/dist/bundle.min.js"></script>
    <style>
        body {font-family: "Arial Narrow", sans-serif; margin:0; font-size: 16px;}
        img {width: 120px;height: auto;}
        .title {color: #18234C; font-weight: bold; font-size: 17px; padding: 0px 0px 8px 0px;}
        .flex-box {display: flex;}
        .flex-box div {align-self: center;}
        .drop-box {border: 1px solid #18234C; height: 157px; justify-content: left; color: #B0BEC5; background-color: #F5F6F7; border-radius: 2px;}
        .upload-svg {fill: #B0BEC5;}
        .drop-box.active .upload_svg{fill: #ffffff;}
        .sub-div {text-align: center;}
        li {font-family: Calibri; padding: 0px 0px 4px 0px; text-align: left; font-size: 18px;}
        .preview-container {width: 157px; height: 157px; border: 1px solid #18234C; position: relative; margin-left: auto; border-radius: 2px;}
        .preview-image {min-width: 100%; height: 157px;}
        .clear {position: absolute; bottom: -1px; left: -1px; text-align: center; width: calc(100% - 16px); color: #ffffff; background-color: #18234C; padding: 8px; border: 1px solid #18234C; border-radius: 2px;}
        .clear:hover {cursor: pointer;}
        .positive {background-color: #16234D;}
        .negative {background-color: #C42030;}
        .success {background-color: green; color: #ffffff;}
        .failure {background-color: red; color: #ffffff;}
        .info-notification {display: none; position: fixed; z-index: 99; width: calc(100% - 48px); top: 0; margin: 8px; padding: 16px; text-align: center;}
    </style>
</head>
<body>
    <!-- VISIBLE ELEMENTS -->
    <div class="title">SMART IMAGE UPLOADER</div>
    <div id="dropbox" class="drop-box flex-box">
        <div id="dropbox_text" style="padding: 16px; align-self: stretch">
            <div class="sub-div">
                <svg xmlns="http://www.w3.org/2000/svg" width="50" height="43" viewBox="0 0 50 43" id="upload_svg" class="upload-svg">
                    <path d="M48.4 26.5c-.9 0-1.7.7-1.7 1.7v11.6h-43.3v-11.6c0-.9-.7-1.7-1.7-1.7s-1.7.7-1.7 1.7v13.2c0 .9.7 1.7 1.7 1.7h46.7c.9 0 1.7-.7 1.7-1.7v-13.2c0-1-.7-1.7-1.7-1.7zm-24.5 6.1c.3.3.8.5 1.2.5.4 0 .9-.2 1.2-.5l10-11.6c.7-.7.7-1.7 0-2.4s-1.7-.7-2.4 0l-7.1 8.3v-25.3c0-.9-.7-1.7-1.7-1.7s-1.7.7-1.7 1.7v25.3l-7.1-8.3c-.7-.7-1.7-.7-2.4 0s-.7 1.7 0 2.4l10 11.6z"></path>
                </svg>
            </div>
            <div class="sub-div">
                <ol>
                    <li>Click and paste (CTRL + V)</li>
                    <li>Drag &amp; drop image here</li>
                    <li>Double-click to upload image</li>
                </ol>
            </div>
        </div>
        <div id="preview_container" class="preview-container">
            <div class="flex-box">
                <img id="preview_image" class="preview-image" src="" />
            </div>
            <div id="clear_button" class="clear"><b>CLEAR</b></div>
        </div>
    </div>
    <div>&nbsp;</div>
    <div id="info_notification" class="info-notification"></div>

    <script type="text/javascript">
    
    // JOURNEY IFRAME CLIENT
    var client = new JourneyIFrameClient();

    // DATA
    var img = null;
    var submitted_blobs = 0;
    var blob_files_created = 0;

    // ELEMENTS
    var dropbox_el = document.getElementById('dropbox');
    var preview_el = document.getElementById('preview');
    var info_notification_el = document.getElementById('info_notification');
    var preview_image = document.getElementById("preview_image");
    var clear_button = document.getElementById("clear_button");
    var preview_container = document.getElementById("preview_container");
    var upload_svg = document.getElementById("upload_svg");
    var dropbox_text = document.getElementById("dropbox_text");

    // PSEUDO INPUT
    var pseudo_input = document.createElement("input");
    pseudo_input.type = "file";
    pseudo_input.accept = "image/*";

    // EVENT LISTENERS
    dropbox_el.addEventListener('dblclick', function() { pseudo_input.click(); });
    pseudo_input.addEventListener('change', function() {
        var files = pseudo_input.files;
        handleFiles(files);
    });
    dropbox_el.addEventListener('dragenter', function(e) { preventDefault(e, 'dragenter') }, false);
    dropbox_el.addEventListener('dragleave', function(e) { preventDefault(e, 'dragleave') }, false);
    dropbox_el.addEventListener('dragover', function(e) { preventDefault(e, 'dragover') }, false);
    dropbox_el.addEventListener('drop', function(e) { preventDefault(e, 'drop') }, false);
    dropbox_el.addEventListener('drop', function(e) { handleDrop(e) }, false);
    window.addEventListener('paste', function(e) {
        pseudo_input.files = e.clipboardData.files;
        if (validateImage(pseudo_input.files[0])) storeImage(pseudo_input.files[0]);
    });
    clear_button.addEventListener('click', function(e) {
        preview_image.src = blank_image;
        pseudo_input.value = null;
    });
    // SHOW USERS HTML COMPONENT IS ACTIVE
    window.addEventListener('focus', function () {
        dropbox_el.style.backgroundColor = '#FFFFFF';
        upload_svg.style.fill = '#18234C';
        dropbox_text.style.color = '#18234C';
    });
    // SHOW USERS HTML COMPONENT IS NOT ACTIVE
    window.addEventListener('blur', function () {
        dropbox_el.style.backgroundColor = '#F5F6F7';
        upload_svg.style.fill = '#B0BEC5';
        dropbox_text.style.color = '#B0BEC5';
    });

    // SIMPLE NOTIFICATION
    function displayNotification(content, color, timeout) {
        info_notification_el.innerHTML = content;
        info_notification_el.classList.add(color);
        info_notification_el.style.display = 'block';
        setTimeout(function() {
            info_notification_el.style.display = 'none';
            info_notification_el.classList.remove(color);
        }, timeout)
    }

    // BROWSER DEFAULT MANAGEMENT
    function preventDefault(e, type) {
        // SETS CSS ON DROP BOX TO INDICATE HOVERING
        if (type == 'drop' || type == 'dragleave') dropbox_el.classList.remove('active');
        if (type == 'dragenter') dropbox_el.classList.add('active');
        // PREVENTS DEFAULT BROWSER ACTION
        e.preventDefault();
        e.stopPropagation();
    }

    // SIMPLE IMAGE VALIDATION
    function validateImage(image) {
        // CHECK THE FILE TYPE
        var valid_types = ['image/png', 'image/jpeg'];
        if (valid_types.indexOf(image.type) === -1) {
            displayNotification('Invalid file type, only ' + valid_types.toString() + ' files allowed', 'failure', 3000);
            return false;
        }
        // CHECK THE FILE SIZE
        var maxSizeInBytes = 10e6; // 10MB
        if (image.size > maxSizeInBytes) {
            displayNotification('File too large, please upload an image less than 10Mb in size', 'failure', 3000);
            return false;
        }
        return true;
    }

    function handleFiles(files) {
        for (var i = 0, len = files.length; i < len; i++) {
            if (validateImage(files[i])) storeImage(files[i]);
        }
    }

    // HANDLES THE FILE DROP ONTO THE HTML COMPONENT
    function handleDrop(e) {
        var dt = e.dataTransfer;
        var files = dt.files;
        if (files.length) handleFiles(files);
        else displayNotification('No image file(s) detected', 'failure', 3000);
    }

    // CONVERTS AND STORES THE IMAGE
    function storeImage(image) {
        // READ THE IMAGE FILE
        var reader = new FileReader();
        reader.onload = function(e) {
            preview_image.src = e.target.result;
        }
        reader.readAsDataURL(image);
        preview_image.onload = function(e) {
            var is_preview = preview_image.src.includes('svg+xml');
            if (preview_image.naturalHeight > 600 && !is_preview) return resizeImage(preview_image);
            else {
                var base64 = is_preview ? null : preview_image.src.replace(`data:${image.type};base64,`, "");
                client.post('datauri', image.type, base64, image.name) //.then(function(result) {console.log('Data URI: ' + result)});
                // return displayNotification('Image successfully uploaded', 'success', 3000);  
            }
        }
    }

    // SET IMAGE PREVIEW "THUMB"
    client.on('datauri', function(base64) {
        if (base64) {
            preview_image.src = "data:image/png;base64," + base64;
        } else {
            preview_image.src = blank_image;
            pseudo_input.value = null;
        }
    });

    // RESIZES IMAGE TO PREFERRED HEIGHT
    function resizeImage(img) {     
        var canvas = document.createElement('canvas');
        var ctx = canvas.getContext('2d');
        var width = (img.naturalWidth * 600 / img.naturalHeight).toFixed(0);
        // SET WIDTH AND HEIGHT
        canvas.width = width;
        canvas.height = 600;
        // DRAW IMAGE ON CANVAS
        ctx.drawImage(img, 0, 0, width, 600);
        var base64_canvas = canvas.toDataURL("image/png");
        //console.log(base64_canvas);
        preview_image.src = base64_canvas;
    }

    // SET IMAGE PREVIEW TO DEFAULT ON LOAD
    var blank_image = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjIwMHB4IiBoZWlnaHQ9IjIwMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMjk5Ljk5OCAyOTkuOTk4IiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxnPg0KCTxwYXRoIGZpbGw9IiNjY2MiIGQ9Ik0xNDkuOTk1LDAuMDAxQzY3LjE1NiwwLjAwMSwwLDY3LjE2LDAsMTQ5Ljk5OXM2Ny4xNTYsMTQ5Ljk5NywxNDkuOTk1LDE0OS45OTdzMTUwLjAwMy02Ny4xNjEsMTUwLjAwMy0xNDkuOTk3DQoJCUMyOTkuOTk3LDY3LjE1NywyMzIuODM0LDAuMDAxLDE0OS45OTUsMC4wMDF6IE0yMjkuMjUsMjI2Ljg0NEg3MC43NWMtMy45ODEsMC03LjIwNS0zLjIyMS03LjIwNS03LjIwMlY4MC4zNTMNCgkJYzAtMy45NzksMy4yMjQtNy4yMDIsNy4yMDUtNy4yMDJoMTU4LjQ5N2MzLjk4MSwwLDcuMjA1LDMuMjI0LDcuMjA1LDcuMjAydjk3LjUzN2gtMTYuOTQxVjg5LjgzNEg4MC4zMjN2OTMuMDI3aDcuOTQ5DQoJCWwzMi4xMTktNTAuMDg3bDIxLjQ2NSwzMy40NjhsMTAuNjYsMTYuNjJoOS4yNDFsLTE1LjI4MS0yMy44MjdsOS45NTktMTUuNTNsMjUuMjQxLDM5LjM1NWgxOS40OTF2NS40MDV2MTYuMzIxdjEwLjM3NGgxMC4zNzQNCgkJaDI0LjkwOXY0LjY4NGgwLjAwNUMyMzYuNDU0LDIyMy42MjQsMjMzLjIzMSwyMjYuODQ0LDIyOS4yNSwyMjYuODQ0eiBNMjA3LjI2NCwxMjEuMDQyYzAsOC4yMzItNi42NzYsMTQuOTA4LTE0LjkwOCwxNC45MDgNCgkJcy0xNC45MDgtNi42NzYtMTQuOTA4LTE0LjkwOHM2LjY3Ni0xNC45MDgsMTQuOTA4LTE0LjkwOFMyMDcuMjY0LDExMi44MTMsMjA3LjI2NCwxMjEuMDQyeiBNMjM2LjQ1NSwyMDQuNTg3aC0yNC45MTF2LTE2LjMyMQ0KCQloMjQuOTA5aDI0LjkwOXYxNi4zMjFIMjM2LjQ1NXoiLz4NCjwvZz4NCjwvc3ZnPg0K';
    preview_image.src = blank_image;

    </script>
</body>
</html>

JOURNEY VIEW XML

<?xml version="1.0" encoding="UTF-8"?>
<view title="paste">
    <var name="image_1" type="photo" />
    <var name="image_2" type="photo" />
    <var name="image_3" type="photo" />
    <var name="image_4" type="photo" />

    <html src="html/index.html" show-fullscreen-button="false" />
    <columns>
        <column show-if="image_1"><capture-photo bind="image_1" source="any" downloadable="false" /></column>
        <column show-if="image_2"><capture-photo bind="image_2" source="any" downloadable="false" /></column>
        <column show-if="image_3"><capture-photo bind="image_3" source="any" downloadable="false" /></column>
        <column show-if="image_4"><capture-photo bind="image_4" source="any" downloadable="false" /></column>
    </columns>
    
</view>

JOURNEY VIEW JS

function init() {
    component.html().on('datauri', function (mediaType, base64) {  
        var attachment = base64 ? Attachment.create({ mediaType: mediaType, base64: base64 }) : null;
        //Add next image
        if (attachment) for (var i = 1; i <= 4; i++){
            if (!view['image_' + i]) {view['image_' + i] = attachment; break;}
            if (i == 4) notification.info('No more than 4 attachments 4 permitted');
        }
        //Remove last image
        if (!attachment) for (var i = 4; i <= 1; i--){
            if (view['image_' + i]) {view['image_' + i] = null; break;}
            notification.info('No attachments to remove');
        }
    });  
}

function updateSmartImage(image){
    var base64 = image ? image.toBase64() : null;
    component.html().post('datauri', base64);  
}

In terms of challenges we experienced / are experiencing, we have been unable to bring the HTML component into focus “automatically”. My understanding is that the component needs to be “focused” for CTRL + V to work, i.e. paste the image / screenshot. This is a requirement of the clipboard API.

The above being said, we simply styled our HTML to show the user that it either is in or out of focus, the typical greyed out / colorful approach.

Interestingly enough dragging and dropping doesn’t require the component to be focused first. You can simply drag and drop the image over the upload box.

Hope this is useful to the community.

Kind regards
Matt

1 Like

Ah, thanks for clarifying the focus requirement. We are busy adding the ability to focus specific components programmatically (i.e. from the JS / TS) and will add the html component to the list of requested components