Implementing Drag-and-Drop Image Sorting and Multi-Image Upload in TinyMCE with Vue

Plugin.js Implemantation

tinymce.PluginManager.add('multiImageUpload', function(editor, pluginUrl) {
    const pluginLabel = 'Multi-Image Upload';
    window.multiImageConfig = {};
    
    const dialogUrl = window.location.origin + '/tinymce/multiImageUpload/upload_dialog.html';
    multiImageConfig.uploadHandler = editor.getParam('images_upload_handler', null, 'function');
    multiImageConfig.basePath = editor.getParam('images_upload_base_path', '', 'string');
    multiImageConfig.allowedTypes = editor.getParam('multiImageUpload_filetype', '.png,.gif,.jpg,.jpeg', 'string');
    multiImageConfig.uploadResults = [];
    
    const launchDialog = function() {
        setupMessageListener(true);
        return editor.windowManager.openUrl({
            title: pluginLabel,
            size: 'large',
            url: dialogUrl,
            buttons: [
                {
                    type: 'custom',
                    text: 'Cancel',
                    name: 'cancel',
                },
                {
                    type: 'custom',
                    text: 'Upload',
                    name: 'upload',
                    primary: true
                },
            ],
            onAction: function (dialogApi, buttonDetails) {
                switch (buttonDetails.name) {
                    case 'upload':
                        window.postMessage({ command: 'processImageUpload' }, '*');
                        break;
                    case 'cancel':
                        window.postMessage({ command: 'cancelUploadProcess' }, '*');
                        setupMessageListener(false);
                        break;
                }
            },
        });
    };

    const insertImages = function() {
        let imageMarkup = '';
        const uploadedImages = multiImageConfig.uploadResults;
        const imageCount = uploadedImages.length;
        
        for (let i = 0; i < imageCount; i++) {
            if (uploadedImages[i].url) {
                imageMarkup += '<img src="' + uploadedImages[i].url + '" />';
            }
        }
        editor.insertContent(imageMarkup);
        multiImageConfig.uploadResults = [];
        tinymce.activeEditor.windowManager.close();
    };

    const handleDialogMessages = function(event){
        const messageData = event.data;
        if (messageData.command === 'completeImageUpload') {
            insertImages();
            setupMessageListener(false);
        }
    }

    const setupMessageListener = function(enable){
        if(enable){
            window.addEventListener('message', handleDialogMessages, false);
        } else {
            window.removeEventListener('message', handleDialogMessages, false);
        }
    }

    editor.ui.registry.getAll().icons.multiImageUpload || editor.ui.registry.addIcon('multiImageUpload', '<svg viewBox="0 0 1280 1024" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M1126.2,779.8V87.6c0-24-22.6-86.9-83.5-86.9H83.5C14.7,0.7,0,63.7,0,87.7v692c0,36.2,29.2,89.7,83.5,89.7l959.3-1.3c51.7,0,83.5-42.5,83.5-88.3zm-1044,4V86.3h961.6V783.7H82.2v0.1z" fill="#53565A"/><path d="M603,461.6L521.1,366.3,313,629.8,227.2,546.8,102.4,716.8H972.8v-170L768.2,235.2,603.1,461.6zM284.6,358.4a105.4,105.4,0,0,0,73.5-30c19.5-19.1,30.3-45,30.2-71.8,0-56.8-45.9-103-102.4-103-56.6,0-102.4,46.1-102.4,103C183.4,313.5,228,358.4,284.6,358.4z" fill="#9598A0"/><path d="M1197.7,153.6l-0.3,669.3s13.5,113.9-67.4,113.9H153.6c0,24.1,23.9,87.2,83.5,87.2h959.3c58.3,0,83.6-49.5,83.6-89.9V240.8c-0.1-41.8-44.9-87.2-82.3-87.2z" fill="#53565A"/></svg>');

    editor.ui.registry.addButton('multiImageUpload', {
        icon: 'multiImageUpload',
        tooltip: pluginLabel,
        onAction: function() {
            launchDialog();
        }
    });
    editor.ui.registry.addMenuItem('multiImageUpload', {
        icon: 'multiImageUpload',
        text: 'Upload Multiple Images...',
        onAction: function() {
            launchDialog();
        }
    });
    return {
        getMetadata: function() {
            return  {
                name: pluginLabel,
                url: "https://tinymce.com/plugins/multi-image-upload",
            };
        }
    };
});

Dialog HTMML Strucutre

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Image Upload Dialog</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background-color: #ffffff;
            font-family: sans-serif;
        }
        .container {
            padding: 15px;
        }
        .upload-header {
            padding: 12px 0;
            border-bottom: 1px solid #e0e0e0;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .image-preview-area {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            margin-top: 20px;
            min-height: 200px;
        }
        .preview-item {
            width: 120px;
            height: 120px;
            border: 2px dashed #cccccc;
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
            cursor: move;
        }
        .preview-item.dragging {
            opacity: 0.5;
            border-color: #007bff;
        }
        .preview-item img {
            max-width: 100%;
            max-height: 100%;
            object-fit: contain;
        }
        .upload-controls {
            margin-top: 15px;
            display: flex;
            gap: 10px;
        }
        .drag-handle {
            position: absolute;
            top: 5px;
            right: 5px;
            width: 20px;
            height: 20px;
            background-color: rgba(0,0,0,0.5);
            border-radius: 50%;
            cursor: move;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="upload-header">
            <h3>Upload and Arrange Images</h3>
        </div>
        <div class="image-preview-area" id="imageContainer">
            <!-- Dynamic preview elements will be inserted here -->
        </div>
        <div class="upload-controls">
            <input type="file" id="fileInput" multiple accept=".png,.gif,.jpg,.jpeg">
            <button id="uploadButton">Upload Selected Images</button>
        </div>
    </div>
    <script>
        // Drag-and-drop sorting implementation
        const imageContainer = document.getElementById('imageContainer');
        let draggedElement = null;

        imageContainer.addEventListener('dragstart', function(e) {
            if (e.target.classList.contains('preview-item')) {
                draggedElement = e.target;
                e.target.classList.add('dragging');
            }
        });

        imageContainer.addEventListener('dragover', function(e) {
            e.preventDefault();
            const afterElement = getDragAfterElement(imageContainer, e.clientY);
            if (draggedElement && afterElement) {
                imageContainer.insertBefore(draggedElement, afterElement);
            }
        });

        imageContainer.addEventListener('dragend', function(e) {
            if (draggedElement) {
                draggedElement.classList.remove('dragging');
                draggedElement = null;
            }
        });

        function getDragAfterElement(container, y) {
            const draggableElements = [...container.querySelectorAll('.preview-item:not(.dragging)')];
            return draggableElements.reduce((closest, child) => {
                const box = child.getBoundingClientRect();
                const offset = y - box.top - box.height / 2;
                if (offset < 0 && offset > closest.offset) {
                    return { offset: offset, element: child };
                } else {
                    return closest;
                }
            }, { offset: Number.NEGATIVE_INFINITY }).element;
        }

        // File handling and communication with parent window
        document.getElementById('fileInput').addEventListener('change', handleFileSelection);
        document.getElementById('uploadButton').addEventListener('click', processUpload);

        function handleFileSelection(e) {
            const files = Array.from(e.target.files);
            files.forEach(file => {
                if (file.type.match('image.*')) {
                    const reader = new FileReader();
                    reader.onload = function(event) {
                        createPreviewElement(event.target.result);
                    };
                    reader.readAsDataURL(file);
                }
            });
        }

        function createPreviewElement(imageData) {
            const previewDiv = document.createElement('div');
            previewDiv.className = 'preview-item';
            previewDiv.draggable = true;
            
            const img = document.createElement('img');
            img.src = imageData;
            
            const handle = document.createElement('div');
            handle.className = 'drag-handle';
            
            previewDiv.appendChild(img);
            previewDiv.appendChild(handle);
            imageContainer.appendChild(previewDiv);
        }

        function processUpload() {
            const previews = imageContainer.querySelectorAll('.preview-item img');
            const imageUrls = Array.from(previews).map(img => ({ url: img.src }));
            
            window.parent.postMessage({ 
                command: 'completeImageUpload', 
                data: imageUrls 
            }, '*');
        }

        window.addEventListener('message', function(event) {
            if (event.data.command === 'processImageUpload') {
                processUpload();
            }
            if (event.data.command === 'cancelUploadProcess') {
                window.parent.postMessage({ command: 'dialogClosed' }, '*');
            }
        });
    </script>
</body>
</html>

Tags: TinyMCE Vue.js Image Upload Drag and Drop Rich Text Editor

Posted on Thu, 07 May 2026 23:11:14 +0000 by heybret