← Браузер/Clipboard API и Drag and Drop#190 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: async и сетьТермин: Event LoopТермин: Core Web Vitals

Clipboard API и Drag and Drop

Две мощные браузерные возможности для работы с данными: Clipboard API позволяет читать и писать в буфер обмена, а Drag and Drop API — перетаскивать элементы интерфейса и файлы.

Clipboard API

Современный асинхронный API для работы с буфером обмена. Требует разрешения пользователя.

Запись текста:

await navigator.clipboard.writeText('Привет!')
console.log('Скопировано в буфер')

Чтение текста:

const text = await navigator.clipboard.readText()
console.log('Из буфера:', text)

Запись произвольных данных:

const item = new ClipboardItem({
  'text/plain': new Blob(['Текст'], { type: 'text/plain' }),
  'text/html': new Blob(['<b>Жирный</b>'], { type: 'text/html' }),
})
await navigator.clipboard.write([item])

Чтение произвольных данных:

const items = await navigator.clipboard.read()
for (const item of items) {
  for (const type of item.types) {
    const blob = await item.getType(type)
    const text = await blob.text()
    console.log(`${type}: ${text}`)
  }
}

Модель разрешений Clipboard

  • clipboard-write — запись в буфер (разрешено автоматически для активных вкладок)
  • clipboard-read — чтение из буфера (требует явного разрешения пользователя)
  • Проверить разрешение:

    const { state } = await navigator.permissions.query({ name: 'clipboard-read' })
    // 'granted', 'denied', 'prompt'

    Drag and Drop API

    Делаем элемент перетаскиваемым:

    <div draggable="true" id="item">Перетащи меня</div>

    Основные события:

    element.addEventListener('dragstart', (e) => {
      e.dataTransfer.setData('text/plain', 'payload data')
      e.dataTransfer.effectAllowed = 'move'
    })
    
    dropZone.addEventListener('dragover', (e) => {
      e.preventDefault()  // обязательно! иначе drop не сработает
      e.dataTransfer.dropEffect = 'move'
    })
    
    dropZone.addEventListener('drop', (e) => {
      e.preventDefault()
      const data = e.dataTransfer.getData('text/plain')
      console.log('Получено:', data)
    })

    DataTransfer объект

    // Установка данных разных типов
    e.dataTransfer.setData('text/plain', 'текст')
    e.dataTransfer.setData('text/html', '<b>html</b>')
    e.dataTransfer.setData('application/json', JSON.stringify({ id: 1 }))
    
    // Чтение данных
    const text = e.dataTransfer.getData('text/plain')
    const types = e.dataTransfer.types  // ['text/plain', 'text/html', ...]
    
    // Для файлов
    const files = e.dataTransfer.files  // FileList

    Перетаскивание файлов

    dropZone.addEventListener('drop', async (e) => {
      e.preventDefault()
      const files = [...e.dataTransfer.files]
      for (const file of files) {
        console.log(file.name, file.size, file.type)
        const text = await file.text()
      }
    })

    Визуальная обратная связь

    dropZone.addEventListener('dragenter', () => dropZone.classList.add('drag-over'))
    dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'))
    dropZone.addEventListener('drop', () => dropZone.classList.remove('drag-over'))

    Copy/Paste события

    document.addEventListener('copy', (e) => {
      e.clipboardData.setData('text/plain', 'перехвачено!')
      e.preventDefault()  // отменяем стандартное копирование
    })
    
    document.addEventListener('paste', (e) => {
      const text = e.clipboardData.getData('text/plain')
      console.log('Вставлено:', text)
    })

    Примеры

    Симуляция операций с буфером обмена и drag-and-drop передачи данных между зонами

    // Симуляция Clipboard и Drag & Drop без браузера
    
    class SimulatedClipboard {
      constructor() {
        this._buffer = null
        this._permissions = { read: 'granted', write: 'granted' }
      }
    
      async writeText(text) {
        if (this._permissions.write !== 'granted') throw new Error('Нет разрешения')
        this._buffer = { type: 'text/plain', data: text }
        console.log(`[Clipboard] Записано: "${text}"`)
      }
    
      async readText() {
        if (this._permissions.read !== 'granted') throw new Error('Нет разрешения')
        if (!this._buffer) return ''
        return this._buffer.data
      }
    
      async write(items) {
        this._buffer = items[0]
        console.log(`[Clipboard] Записан объект типа: ${items[0].type}`)
      }
    }
    
    class SimulatedDataTransfer {
      constructor() {
        this._data = new Map()
        this.effectAllowed = 'all'
        this.dropEffect = 'none'
      }
    
      setData(type, data) { this._data.set(type, data) }
      getData(type) { return this._data.get(type) || '' }
      get types() { return [...this._data.keys()] }
    }
    
    class SimulatedDragDropBoard {
      constructor() {
        this._dropZones = new Map()
        this._currentDrag = null
        this._history = []
      }
    
      registerDropZone(id) {
        this._dropZones.set(id, { id, items: [] })
        console.log(`[DnD] Зона зарегистрирована: #${id}`)
      }
    
      startDrag(sourceId, data) {
        if (!this._dropZones.has(sourceId)) {
          console.log(`[DnD] Источник #${sourceId} не найден`)
          return
        }
        const transfer = new SimulatedDataTransfer()
        transfer.setData('text/plain', typeof data === 'string' ? data : JSON.stringify(data))
        transfer.setData('application/json', JSON.stringify({ source: sourceId, data }))
        this._currentDrag = { sourceId, transfer, data }
        console.log(`[DnD] Начало перетаскивания из #${sourceId}: ${JSON.stringify(data)}`)
      }
    
      drop(targetId) {
        if (!this._currentDrag) {
          console.log('[DnD] Нет активного перетаскивания')
          return null
        }
        if (!this._dropZones.has(targetId)) {
          console.log(`[DnD] Зона #${targetId} не найдена`)
          return null
        }
    
        const { sourceId, data, transfer } = this._currentDrag
        const zone = this._dropZones.get(targetId)
        zone.items.push(data)
    
        this._history.push({ from: sourceId, to: targetId, data })
        this._currentDrag = null
    
        console.log(`[DnD] Сброшено в #${targetId}: ${JSON.stringify(data)}`)
        return transfer.getData('text/plain')
      }
    
      getHistory() { return [...this._history] }
      getZoneItems(id) { return this._dropZones.get(id)?.items || [] }
    }
    
    // Демо
    const clipboard = new SimulatedClipboard()
    const board = new SimulatedDragDropBoard()
    
    // Clipboard
    await clipboard.writeText('Привет, буфер обмена!')
    const text = await clipboard.readText()
    console.log('Прочитано из буфера:', text)
    
    // Drag & Drop
    board.registerDropZone('list-a')
    board.registerDropZone('list-b')
    board.registerDropZone('trash')
    
    console.log('\n--- Перетаскивание элементов ---')
    board.startDrag('list-a', { id: 1, label: 'Задача 1' })
    board.drop('list-b')
    
    board.startDrag('list-a', { id: 2, label: 'Задача 2' })
    board.drop('trash')
    
    board.startDrag('list-b', { id: 3, label: 'Задача 3' })
    board.drop('list-a')
    
    console.log('\n--- История перемещений ---')
    board.getHistory().forEach(({ from, to, data }) => {
      console.log(`  ${from} → ${to}: ${JSON.stringify(data)}`)
    })
    
    console.log('\nЭлементы в list-b:', board.getZoneItems('list-b'))

    Clipboard API и Drag and Drop

    Две мощные браузерные возможности для работы с данными: Clipboard API позволяет читать и писать в буфер обмена, а Drag and Drop API — перетаскивать элементы интерфейса и файлы.

    Clipboard API

    Современный асинхронный API для работы с буфером обмена. Требует разрешения пользователя.

    Запись текста:

    await navigator.clipboard.writeText('Привет!')
    console.log('Скопировано в буфер')

    Чтение текста:

    const text = await navigator.clipboard.readText()
    console.log('Из буфера:', text)

    Запись произвольных данных:

    const item = new ClipboardItem({
      'text/plain': new Blob(['Текст'], { type: 'text/plain' }),
      'text/html': new Blob(['<b>Жирный</b>'], { type: 'text/html' }),
    })
    await navigator.clipboard.write([item])

    Чтение произвольных данных:

    const items = await navigator.clipboard.read()
    for (const item of items) {
      for (const type of item.types) {
        const blob = await item.getType(type)
        const text = await blob.text()
        console.log(`${type}: ${text}`)
      }
    }

    Модель разрешений Clipboard

  • clipboard-write — запись в буфер (разрешено автоматически для активных вкладок)
  • clipboard-read — чтение из буфера (требует явного разрешения пользователя)
  • Проверить разрешение:

    const { state } = await navigator.permissions.query({ name: 'clipboard-read' })
    // 'granted', 'denied', 'prompt'

    Drag and Drop API

    Делаем элемент перетаскиваемым:

    <div draggable="true" id="item">Перетащи меня</div>

    Основные события:

    element.addEventListener('dragstart', (e) => {
      e.dataTransfer.setData('text/plain', 'payload data')
      e.dataTransfer.effectAllowed = 'move'
    })
    
    dropZone.addEventListener('dragover', (e) => {
      e.preventDefault()  // обязательно! иначе drop не сработает
      e.dataTransfer.dropEffect = 'move'
    })
    
    dropZone.addEventListener('drop', (e) => {
      e.preventDefault()
      const data = e.dataTransfer.getData('text/plain')
      console.log('Получено:', data)
    })

    DataTransfer объект

    // Установка данных разных типов
    e.dataTransfer.setData('text/plain', 'текст')
    e.dataTransfer.setData('text/html', '<b>html</b>')
    e.dataTransfer.setData('application/json', JSON.stringify({ id: 1 }))
    
    // Чтение данных
    const text = e.dataTransfer.getData('text/plain')
    const types = e.dataTransfer.types  // ['text/plain', 'text/html', ...]
    
    // Для файлов
    const files = e.dataTransfer.files  // FileList

    Перетаскивание файлов

    dropZone.addEventListener('drop', async (e) => {
      e.preventDefault()
      const files = [...e.dataTransfer.files]
      for (const file of files) {
        console.log(file.name, file.size, file.type)
        const text = await file.text()
      }
    })

    Визуальная обратная связь

    dropZone.addEventListener('dragenter', () => dropZone.classList.add('drag-over'))
    dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'))
    dropZone.addEventListener('drop', () => dropZone.classList.remove('drag-over'))

    Copy/Paste события

    document.addEventListener('copy', (e) => {
      e.clipboardData.setData('text/plain', 'перехвачено!')
      e.preventDefault()  // отменяем стандартное копирование
    })
    
    document.addEventListener('paste', (e) => {
      const text = e.clipboardData.getData('text/plain')
      console.log('Вставлено:', text)
    })

    Примеры

    Симуляция операций с буфером обмена и drag-and-drop передачи данных между зонами

    // Симуляция Clipboard и Drag & Drop без браузера
    
    class SimulatedClipboard {
      constructor() {
        this._buffer = null
        this._permissions = { read: 'granted', write: 'granted' }
      }
    
      async writeText(text) {
        if (this._permissions.write !== 'granted') throw new Error('Нет разрешения')
        this._buffer = { type: 'text/plain', data: text }
        console.log(`[Clipboard] Записано: "${text}"`)
      }
    
      async readText() {
        if (this._permissions.read !== 'granted') throw new Error('Нет разрешения')
        if (!this._buffer) return ''
        return this._buffer.data
      }
    
      async write(items) {
        this._buffer = items[0]
        console.log(`[Clipboard] Записан объект типа: ${items[0].type}`)
      }
    }
    
    class SimulatedDataTransfer {
      constructor() {
        this._data = new Map()
        this.effectAllowed = 'all'
        this.dropEffect = 'none'
      }
    
      setData(type, data) { this._data.set(type, data) }
      getData(type) { return this._data.get(type) || '' }
      get types() { return [...this._data.keys()] }
    }
    
    class SimulatedDragDropBoard {
      constructor() {
        this._dropZones = new Map()
        this._currentDrag = null
        this._history = []
      }
    
      registerDropZone(id) {
        this._dropZones.set(id, { id, items: [] })
        console.log(`[DnD] Зона зарегистрирована: #${id}`)
      }
    
      startDrag(sourceId, data) {
        if (!this._dropZones.has(sourceId)) {
          console.log(`[DnD] Источник #${sourceId} не найден`)
          return
        }
        const transfer = new SimulatedDataTransfer()
        transfer.setData('text/plain', typeof data === 'string' ? data : JSON.stringify(data))
        transfer.setData('application/json', JSON.stringify({ source: sourceId, data }))
        this._currentDrag = { sourceId, transfer, data }
        console.log(`[DnD] Начало перетаскивания из #${sourceId}: ${JSON.stringify(data)}`)
      }
    
      drop(targetId) {
        if (!this._currentDrag) {
          console.log('[DnD] Нет активного перетаскивания')
          return null
        }
        if (!this._dropZones.has(targetId)) {
          console.log(`[DnD] Зона #${targetId} не найдена`)
          return null
        }
    
        const { sourceId, data, transfer } = this._currentDrag
        const zone = this._dropZones.get(targetId)
        zone.items.push(data)
    
        this._history.push({ from: sourceId, to: targetId, data })
        this._currentDrag = null
    
        console.log(`[DnD] Сброшено в #${targetId}: ${JSON.stringify(data)}`)
        return transfer.getData('text/plain')
      }
    
      getHistory() { return [...this._history] }
      getZoneItems(id) { return this._dropZones.get(id)?.items || [] }
    }
    
    // Демо
    const clipboard = new SimulatedClipboard()
    const board = new SimulatedDragDropBoard()
    
    // Clipboard
    await clipboard.writeText('Привет, буфер обмена!')
    const text = await clipboard.readText()
    console.log('Прочитано из буфера:', text)
    
    // Drag & Drop
    board.registerDropZone('list-a')
    board.registerDropZone('list-b')
    board.registerDropZone('trash')
    
    console.log('\n--- Перетаскивание элементов ---')
    board.startDrag('list-a', { id: 1, label: 'Задача 1' })
    board.drop('list-b')
    
    board.startDrag('list-a', { id: 2, label: 'Задача 2' })
    board.drop('trash')
    
    board.startDrag('list-b', { id: 3, label: 'Задача 3' })
    board.drop('list-a')
    
    console.log('\n--- История перемещений ---')
    board.getHistory().forEach(({ from, to, data }) => {
      console.log(`  ${from} → ${to}: ${JSON.stringify(data)}`)
    })
    
    console.log('\nЭлементы в list-b:', board.getZoneItems('list-b'))

    Задание

    Реализуй createDragDropBoard() с методами: registerDropZone(id) регистрирует зону для сброса, startDrag(data) начинает перетаскивание с данными (запоминает текущие данные), drop(zoneId) завершает перетаскивание и возвращает данные (или null если нет активного перетаскивания), getHistory() возвращает массив объектов { from: undefined, to: zoneId, data }.

    Подсказка

    activeDrag хранит объект { data }. В drop() проверяй !activeDrag для возврата null. После успешного drop сохраняй в history и устанавливай activeDrag = null. getHistory() возвращает копию [...history]. zones.has(zoneId) проверяет что зона зарегистрирована.

    Загружаем среду выполнения...
    Загружаем AI-помощника...