RealGrid2 Guide
Tip 🆕
Image Drag and Drop

Image Drag and Drop

This example shows how to attach image files to RealGrid using drag and drop and display previews.
Drag files onto a cell in the desired row to display them in the first column, Image Attachments.

Attached images appear as thumbnails. Click a thumbnail to download the image.
Click the X button next to a file to remove the attachment from the grid. Dropping more images onto the same row keeps the existing images and adds the new images below them.

This example encodes files in Base64 and stores them in the grid's data.
In your application, send the stored file information to the server or upload files when they are dropped.

※ Does not work in IE.
※ You must implement server-side processing yourself.

Loading RealGrid...

The following code covers image rendering, adding and removing attachments, and connecting drag-and-drop events in the current example.

function escapeHtml(value) {
  return String(value ?? '').replace(/[&<>"']/g, function(ch) {
    return {
      '&': '&amp;', '<': '&lt;', '>': '&gt;',
      '"': '&quot;', "'": '&#39;'
    }[ch];
  });
}
 
function getFiles(grid, dataRow) {
  const files = grid.getDataSource().getValue(dataRow, 'Files');
  return Array.isArray(files) ? files : [];
}
 
function getImageUrl(file) {
  return file && typeof file.dataURL === 'string' &&
    file.dataURL.startsWith('data:image/') ? file.dataURL : '';
}
 
// Show attachment thumbnails with download links and delete buttons.
const htmlRenderer = {
  type: 'html',
  callback: function(grid, cell, w, h) {
    const dataRow = cell.index.dataRow;
    const files = getFiles(grid, dataRow);
    let str = '';
 
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      const fileName = escapeHtml(file.fileName);
      const imageUrl = getImageUrl(file);
      const downloadUrl = 'data:application/octet-stream;base64,' +
        (file.base64Data || '');
      const content = imageUrl
        ? '<img class="file-preview" src="' + escapeHtml(imageUrl) +
          '" alt="' + fileName + '">'
        : fileName;
 
      str += '<span class="file-item">' +
        '<a class="file-download" download="' + fileName +
        '" href="' + downloadUrl + '">' + content + '</a>' +
        '<button type="button" class="file-delete" data-row="' +
        dataRow + '" data-index="' + i +
        '" onclick="fileDelete(this)">X</button>' +
        '</span>';
    }
    return str;
  }
};
 
function deleteFile(el, dataProvider, gridView) {
  const dataRow = Number(el.dataset.row);
  const fileIndex = Number(el.dataset.index);
  if (!Number.isInteger(dataRow) || dataRow < 0 ||
      !Number.isInteger(fileIndex) || fileIndex < 0) return;
 
  const fileValue = dataProvider.getValue(dataRow, 'Files');
  const files = Array.isArray(fileValue) ? fileValue : [];
  dataProvider.setValue(dataRow, 'Files',
    files.filter((_, index) => index !== fileIndex));
}
 
// Convert new files to Base64 in order and append them while keeping existing files.
function fileTobase64(index, dataRow, files, outputs, dataProvider, gridView) {
  if (index >= files.length) {
    dataProvider.setValue(dataRow, 'Files', outputs);
    return;
  }
 
  const file = files[index];
  const reader = new FileReader();
  reader.onload = function() {
    const dataURL = reader.result;
    outputs.push({
      fileName: file.name,
      dataURL: dataURL,
      base64Data: dataURL.split(',')[1]
    });
    fileTobase64(index + 1, dataRow, files, outputs, dataProvider, gridView);
  };
  reader.onerror = function() {
    console.error('Failed to read file:', file.name);
    fileTobase64(index + 1, dataRow, files, outputs, dataProvider, gridView);
  };
  reader.readAsDataURL(file);
}
 
function dragEnter(gridContainer) {
  gridContainer.querySelectorAll('.rg-data-cell').forEach(element => {
    element.ondragenter = () => element.classList.add('drag-over-cell');
    element.ondragleave = (event) => {
      const relatedTarget = event.relatedTarget;
      if (!relatedTarget || !element.contains(relatedTarget)) {
        element.classList.remove('drag-over-cell');
      }
    };
    element.ondrop = element.ondragover = (event) => event.preventDefault();
  });
}
 
function dragLeave(ev, gridContainer) {
  if (!ev.relatedTarget || !gridContainer.contains(ev.relatedTarget)) {
    gridContainer.querySelectorAll('.rg-data-cell').forEach(element => {
      element.classList.remove('drag-over-cell');
    });
  }
}
 
function dropFiles(ev, gridView, dataProvider) {
  ev.preventDefault();
  const gridContainer = gridView.getContainer();
  gridContainer.querySelectorAll('.rg-data-cell').forEach(element => {
    element.ondrop = null;
    element.ondragenter = null;
    element.ondragleave = null;
    element.ondragover = null;
    element.classList.remove('drag-over-cell');
  });
 
  const cellIndex = gridView.mouseToIndex(ev.clientX, ev.clientY, true);
  if (!cellIndex || cellIndex.dataRow < 0) {
    alert('Please drop the file onto a data row.');
    return;
  }
 
  const files = Array.from(ev.dataTransfer.files || []);
  if (files.length === 0) return;
 
  const dataRow = cellIndex.dataRow;
  const existingFiles = dataProvider.getValue(dataRow, 'Files');
  const outputs = Array.isArray(existingFiles) ? [...existingFiles] : [];
  fileTobase64(0, dataRow, files, outputs, dataProvider, gridView);
}
 
// Use this configuration for the first column when creating the grid.
const fileDownloadColumn = {
  name: 'FileDownload',
  fieldName: 'FileDownload',
  width: 180,
  header: { text: 'Image Attachments' },
  editable: false,
  styleName: 'left-column',
  styleCallback: function(grid, cell) {
    return { renderer: htmlRenderer };
  }
};
 
// Connect the renderer to the existing FileDownload column.
gridView.columnByName('FileDownload').styleCallback = fileDownloadColumn.styleCallback;
 
const gridContainer = gridView.getContainer();
 
function fileDelete(el) {
  deleteFile(el, dataProvider, gridView);
}
window.fileDelete = fileDelete;
 
function dragEnterHandler(ev) {
  dragEnter(gridContainer);
}
 
function dragLeaveHandler(ev) {
  dragLeave(ev, gridContainer);
}
 
function dragOverHandler(ev) {
  ev.preventDefault();
}
 
function dropHandler(ev) {
  dropFiles(ev, gridView, dataProvider);
}
 
const realgridDiv = document.getElementById('realgrid');
realgridDiv.ondragenter = dragEnterHandler;
realgridDiv.ondragleave = dragLeaveHandler;
realgridDiv.ondrop = dropHandler;
realgridDiv.ondragover = dragOverHandler;

Use the following CSS for image previews and attachment layout.

.drag-over-cell { background-color: lightblue; }
.right-column { text-align: right; }
.left-column { text-align: left; }
 
.file-item {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 5px;
  margin: 3px;
}
.file-download { display: inline-flex; align-items: center; min-width: 0; overflow-wrap: anywhere; }
.file-preview { width: 55px; height: 55px; object-fit: contain; }
.file-delete {
  flex: 0 0 10px;
  box-sizing: border-box;
  width: 10px;
  min-width: 10px;
  height: 10px;
  padding: 0;
  border: 0;
  background-color: #222;
  color: #fff;
  text-align: center;
  font-size: 10px;
  line-height: 10px;
  cursor: pointer;
}