CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-lexical--utils

This package provides essential utility functions for the Lexical rich text editor framework, offering a comprehensive set of DOM manipulation helpers, tree traversal algorithms, and editor state management tools.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

file-handling.mddocs/

File Handling

MIME type validation and asynchronous file reading utilities with support for batching and order preservation. These functions provide robust file processing capabilities designed for handling media files and maintaining compatibility with the Lexical editor's history system.

Capabilities

MIME Type Validation

Checks if a file matches one or more acceptable MIME types with case-sensitive comparison.

/**
 * Returns true if the file type matches the types passed within the acceptableMimeTypes array, false otherwise.
 * The types passed must be strings and are CASE-SENSITIVE.
 * eg. if file is of type 'text' and acceptableMimeTypes = ['TEXT', 'IMAGE'] the function will return false.
 * @param file - The file you want to type check.
 * @param acceptableMimeTypes - An array of strings of types which the file is checked against.
 * @returns true if the file is an acceptable mime type, false otherwise.
 */
function isMimeType(
  file: File,
  acceptableMimeTypes: Array<string>
): boolean;

Usage Examples:

import { isMimeType } from "@lexical/utils";

const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const files = Array.from(fileInput.files || []);

// Check for image files
const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
const imageFiles = files.filter(file => isMimeType(file, imageTypes));

// Check for video files
const videoTypes = ['video/mp4', 'video/webm', 'video/quicktime'];
const videoFiles = files.filter(file => isMimeType(file, videoTypes));

// Check for text files (case-sensitive)
const textTypes = ['text/plain', 'text/markdown', 'text/html'];
const textFiles = files.filter(file => isMimeType(file, textTypes));

// Using MIME type prefixes for broader matching
const audioTypes = ['audio/']; // Matches any audio type
const audioFiles = files.filter(file => isMimeType(file, audioTypes));

// Validation with feedback
function validateFileType(file: File): { valid: boolean; message: string } {
  const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
  
  if (isMimeType(file, allowedTypes)) {
    return { valid: true, message: 'File type accepted' };
  } else {
    return { 
      valid: false, 
      message: `File type '${file.type}' not supported. Please upload JPEG, PNG, or PDF files.`
    };
  }
}

Media File Reader

Advanced asynchronous file reader with MIME type filtering, batched results, and order preservation for compatibility with Lexical's history system.

/**
 * Lexical File Reader with:
 *  1. MIME type support
 *  2. batched results (HistoryPlugin compatibility)
 *  3. Order aware (respects the order when multiple Files are passed)
 *
 * const filesResult = await mediaFileReader(files, ['image/']);
 * filesResult.forEach(file => editor.dispatchCommand('INSERT_IMAGE', {
 *   src: file.result,
 * }));
 */
function mediaFileReader(
  files: Array<File>,
  acceptableMimeTypes: Array<string>
): Promise<Array<{file: File; result: string}>>;

Usage Examples:

import { mediaFileReader } from "@lexical/utils";

// Basic image upload handling
async function handleImageUpload(files: File[]) {
  const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
  
  try {
    const results = await mediaFileReader(files, imageTypes);
    
    results.forEach(({ file, result }) => {
      console.log(`Processed ${file.name}: ${result.length} characters`);
      
      // Insert into editor
      editor.dispatchCommand('INSERT_IMAGE', {
        src: result,        // Data URL string
        alt: file.name,
        width: 'auto',
        height: 'auto'
      });
    });
  } catch (error) {
    console.error('Failed to process images:', error);
  }
}

// Multiple file type processing
async function handleMultipleFileTypes(files: File[]) {
  // Process images
  const imageResults = await mediaFileReader(files, ['image/']);
  
  // Process videos  
  const videoResults = await mediaFileReader(files, ['video/']);
  
  // Process audio
  const audioResults = await mediaFileReader(files, ['audio/']);
  
  // Handle each type differently
  imageResults.forEach(({ file, result }) => {
    insertImage(result, file.name);
  });
  
  videoResults.forEach(({ file, result }) => {
    insertVideo(result, file.name);
  });
  
  audioResults.forEach(({ file, result }) => {
    insertAudio(result, file.name);
  });
}

// With progress tracking and error handling
async function handleFileUploadWithProgress(files: File[]) {
  const acceptedTypes = ['image/jpeg', 'image/png', 'image/gif'];
  
  // Filter files first to show immediate feedback
  const validFiles = files.filter(file => isMimeType(file, acceptedTypes));
  const invalidFiles = files.filter(file => !isMimeType(file, acceptedTypes));
  
  if (invalidFiles.length > 0) {
    console.warn(`Skipped ${invalidFiles.length} invalid files:`, 
      invalidFiles.map(f => f.name));
  }
  
  if (validFiles.length === 0) {
    throw new Error('No valid image files to process');
  }
  
  // Show progress
  console.log(`Processing ${validFiles.length} files...`);
  
  const results = await mediaFileReader(validFiles, acceptedTypes);
  
  // Results maintain original file order
  results.forEach(({ file, result }, index) => {
    console.log(`File ${index + 1}/${results.length}: ${file.name} processed`);
    
    // Create image element
    const img = new Image();
    img.onload = () => {
      console.log(`Image loaded: ${img.width}x${img.height}`);
    };
    img.src = result;
  });
  
  return results;
}

// Drag and drop integration
function setupDragAndDrop(editor: LexicalEditor) {
  const dropZone = editor.getRootElement();
  
  dropZone?.addEventListener('drop', async (event) => {
    event.preventDefault();
    
    const files = Array.from(event.dataTransfer?.files || []);
    const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    
    try {
      const results = await mediaFileReader(files, imageTypes);
      
      // Insert images at drop position
      editor.update(() => {
        results.forEach(({ file, result }) => {
          const imageNode = createImageNode({
            src: result,
            alt: file.name
          });
          
          $insertNodeToNearestRoot(imageNode);
        });
      });
    } catch (error) {
      console.error('Drop upload failed:', error);
    }
  });
}

// Batch processing with size limits
async function handleLargeFileSet(files: File[], maxBatchSize: number = 5) {
  const imageTypes = ['image/'];
  const validFiles = files.filter(file => isMimeType(file, imageTypes));
  
  const results: Array<{file: File; result: string}> = [];
  
  // Process in batches to avoid memory issues
  for (let i = 0; i < validFiles.length; i += maxBatchSize) {
    const batch = validFiles.slice(i, i + maxBatchSize);
    console.log(`Processing batch ${Math.floor(i / maxBatchSize) + 1}...`);
    
    const batchResults = await mediaFileReader(batch, imageTypes);
    results.push(...batchResults);
    
    // Optional: Add delay between batches
    if (i + maxBatchSize < validFiles.length) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
  }
  
  return results;
}

Error Handling

Both functions handle errors gracefully:

  • isMimeType returns false for invalid inputs rather than throwing
  • mediaFileReader rejects the promise if file reading fails
  • Files that don't match MIME types are silently skipped by mediaFileReader

Common Error Scenarios:

import { mediaFileReader, isMimeType } from "@lexical/utils";

async function robustFileHandling(files: File[]) {
  const acceptedTypes = ['image/jpeg', 'image/png'];
  
  // Pre-validate files
  const validationResults = files.map(file => ({
    file,
    valid: isMimeType(file, acceptedTypes),
    error: !isMimeType(file, acceptedTypes) ? 
      `Invalid type: ${file.type}` : null
  }));
  
  const validFiles = validationResults
    .filter(result => result.valid)
    .map(result => result.file);
  
  const errors = validationResults
    .filter(result => !result.valid)
    .map(result => result.error);
  
  if (errors.length > 0) {
    console.warn('File validation errors:', errors);
  }
  
  if (validFiles.length === 0) {
    throw new Error('No valid files to process');
  }
  
  try {
    const results = await mediaFileReader(validFiles, acceptedTypes);
    return { results, errors };
  } catch (readError) {
    console.error('File reading failed:', readError);
    throw new Error(`Failed to read files: ${readError.message}`);
  }
}

docs

dom-manipulation.md

editor-state.md

file-handling.md

index.md

specialized-utilities.md

tree-traversal.md

tile.json