Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div class="min-w-[20rem]" data-controller="multiple-file-input" data-multiple-file-input-supported-input-class="absolute opacity-0 inset-0">
  <label class="text-lg" for="purchase_report_attachments">Attachments</label>
  
  <section class="mb-4" data-controller="checkboxes">
    <div class="flex items-center justify-between flex-wrap sm:flex-nowrap mb-2">
      <h3 class="text-md">
        Saved uploads
      </h3>

      <label for="purchase_report_attachments_controller"
             class="cursor-pointer rounded bg-white py-1 px-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
        <input type="checkbox"
               id="purchase_report_attachments_controller"
               checked="checked"
               class="peer sr-only"
               data-checkboxes-target="controller" />
        <span class="peer-checked:hidden inline">Restore all</span>
        <span class="peer-checked:inline hidden">Delete all</span>
      </label>
    </div>
    <ul role="list" class="grid grid-cols-2 gap-x-2 gap-y-4 xs:grid-cols-3 sm:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
      <li class="relative flex flex-col items-center text-center border rounded select-none overflow-hidden border-red-500 bg-red-100 [&:has(:checked)]:bg-white [&:has(:checked)]:border-gray-200"
          data-multiple-file-input-target="persisted">
        <input type="checkbox"
               checked="checked"
               id="images_attachment_ids_0"
               class="peer sr-only"
               data-checkboxes-target="checkbox"
               data-action="">
        <label for="images_attachment_ids_0" class="group hidden peer-checked:flex items-center gap-1 cursor-pointer hover:text-red-700 text-gray-700 absolute top-0 right-0 z-50 p-1 bg-white rounded-bl focus:outline-none">
          <span class="text-sm opacity-0 w-0 group-hover:w-auto group-hover:opacity-100">Delete</span>
          <svg class="w-4 h-4 bi bi-trash3" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
            <path d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5ZM11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H2.506a.58.58 0 0 0-.01 0H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1h-.995a.59.59 0 0 0-.01 0H11Zm1.958 1-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5h9.916Zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47ZM8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5Z"/>
          </svg>
        </label>
        <label for="images_attachment_ids_0" class="group flex peer-checked:hidden items-center gap-1 cursor-pointer hover:text-blue-700 text-gray-700 absolute top-0 right-0 z-50 p-1 bg-white rounded-bl focus:outline-none">
          <span class="text-sm opacity-0 w-0 group-hover:w-auto group-hover:opacity-100">Restore</span>
          <svg class="w-4 h-4 bi bi-arrow-clockwise" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
            <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
            <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
          </svg>
        </label>
        <div class="aspect-h-7 aspect-w-10 w-full opacity-50 peer-checked:opacity-100">
          <img src="https://images.unsplash.com/photo-1678834907853-509ffb92a047?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80" alt="a computer generated image of a red swirl" class="border-4 border-white pointer-events-none object-contain">
        </div>

        <p class="w-full flex flex-col items-center bg-gray-100 text-gray-900 px-2 py-1 line-through peer-checked:no-underline">
          <span class="w-full font-bold truncate" data-target="name">Red Swirl</span>
          <span class="text-xs" data-target="size">6MB</span>
        </p>
      </li>
      <li class="relative flex flex-col items-center text-center border rounded select-none overflow-hidden border-red-500 bg-red-100 [&:has(:checked)]:bg-white [&:has(:checked)]:border-gray-200"
          data-multiple-file-input-target="persisted">
        <input type="checkbox"
               checked="checked"
               id="images_attachment_ids_1"
               class="peer sr-only"
               data-checkboxes-target="checkbox"
               data-action="">
        <label for="images_attachment_ids_1" class="group hidden peer-checked:flex items-center gap-1 cursor-pointer hover:text-red-700 text-gray-700 absolute top-0 right-0 z-50 p-1 bg-white rounded-bl focus:outline-none">
          <span class="text-sm opacity-0 w-0 group-hover:w-auto group-hover:opacity-100">Delete</span>
          <svg class="w-4 h-4 bi bi-trash3" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
            <path d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5ZM11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H2.506a.58.58 0 0 0-.01 0H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1h-.995a.59.59 0 0 0-.01 0H11Zm1.958 1-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5h9.916Zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47ZM8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5Z"/>
          </svg>
        </label>
        <label for="images_attachment_ids_1" class="group flex peer-checked:hidden items-center gap-1 cursor-pointer hover:text-blue-700 text-gray-700 absolute top-0 right-0 z-50 p-1 bg-white rounded-bl focus:outline-none">
          <span class="text-sm opacity-0 w-0 group-hover:w-auto group-hover:opacity-100">Restore</span>
          <svg class="w-4 h-4 bi bi-arrow-clockwise" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
            <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
            <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
          </svg>
        </label>
        <div class="aspect-h-7 aspect-w-10 w-full opacity-50 peer-checked:opacity-100">
          <img src="https://images.unsplash.com/photo-1678845530054-0268510ebc25?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3028&q=80" alt="a multicolored abstract background with dots" class="border-4 border-white pointer-events-none object-contain">
        </div>

        <p class="w-full flex flex-col items-center bg-gray-100 text-gray-900 px-2 py-1 line-through peer-checked:no-underline">
          <span class="w-full font-bold truncate" data-target="name">Abstract dots</span>
          <span class="text-xs" data-target="size">385KB</span>
        </p>
      </li>
    </ul>
  </section>

  <div class="relative mb-2">
    <label class="flex flex-col justify-center gap-1 text-center text-gray-600 rounded border border-gray-300 border-dashed p-6 mb-2" for="purchase_report_attachments">
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="mx-auto h-12 w-12 fill-current text-gray-400 bi bi-image" viewBox="0 0 16 16">
            <path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
            <path d="M2.002 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2h-12zm12 1a1 1 0 0 1 1 1v6.5l-3.777-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12V3a1 1 0 0 1 1-1h12z"/>
          </svg>
      <p class="">
        Drag and drop your files or
        <span class="underline cursor-pointer hover:text-black">
          Browse
        </span>
      </p>
      <p class="text-sm text-gray-500">
        PNG, JPG, GIF, or MP4 up to 25MB each
      </p>
    </label>
    <input type="file" multiple="multiple" class="appearance-none w-full text-base font-normal text-ellipsis whitespace-pre overflow-hidden bg-gray-100 py-1 px-3 rounded" name="feedback[images][]" id="purchase_report_attachments" data-multiple-file-input-target="input" data-action="multiple-file-input#previewInGallery"  max-file-size="25MB">
  </div>

  <ul class="bg-red-200 text-red-800 rounded px-2 py-1 mb-2 empty:p-0"
      data-multiple-file-input-target="notice"></ul> 

  <section class="hidden" data-multiple-file-input-target="gallerySection">
    <div class="flex items-center justify-between flex-wrap sm:flex-nowrap mb-2">
      <h3 class="text-md leading-6 font-medium text-gray-900">
        Unsaved uploads
      </h3>

      <button type="button" class="cursor-pointer rounded bg-white py-1 px-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" data-action="multiple-file-input#removeAll">
        Remove all
      </button>
    </div>
    <ul role="list" class="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-4 sm:gap-x-6 lg:grid-cols-6 xl:gap-x-8" data-multiple-file-input-target="gallery">
      <template data-multiple-file-input-target="template">
        <li class="relative flex flex-col items-center text-center border rounded select-none overflow-hidden bg-white border-dashed" data-multiple-file-input-target="unpersisted">
          <button type="button" class="group flex items-center gap-1 cursor-pointer hover:text-red-700 text-gray-700 absolute top-0 right-0 z-50 p-1 bg-white rounded-bl" data-action="multiple-file-input#removeFile">
            <span class="text-sm opacity-0 w-0 group-hover:w-auto group-hover:opacity-100">Remove</span>
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-4 h-4 bi bi-x-lg" viewBox="0 0 16 16">
              <path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/>
            </svg>
          </button>

          <div class="aspect-h-7 aspect-w-10 w-full">
            <img alt="" class="border-4 border-white pointer-events-none object-contain">
          </div>

          <p class="w-full flex flex-col items-center bg-gray-100 text-gray-900 px-2 py-1">
            <span class="w-full font-bold truncate" data-target="name">Loading</span>
            <span class="text-xs" data-target="size">&hellip;</span>
          </p>
        </li>
      </template>
    </ul>
  </section>
</div>

              
            
!

CSS

              
                body { margin: 2rem !important }

              
            
!

JS

              
                class CheckboxesController extends Stimulus.Controller {
  static targets = ['controller', 'checkbox']

  initialize () {
    this.toggle = this.toggle.bind(this)
    this.refresh = this.refresh.bind(this)
  }

  connect () {
    if (!this.hasControllerTarget) return

    this.controllerTarget.addEventListener('change', this.toggle)
    this.checkboxTargets.forEach(checkbox => checkbox.addEventListener('change', this.refresh))
    this.refresh()
  }

  disconnect () {
    if (!this.hasCheckboxAllTarget) return

    this.controllerTarget.removeEventListener('change', this.toggle)
    this.checkboxTargets.forEach(checkbox => checkbox.removeEventListener('change', this.refresh))
  }

  toggle (e) {
    e.preventDefault()

    this.checkboxTargets.forEach(checkbox => {
      checkbox.checked = this.controllerTarget.checked
      this.triggerInputEvent(checkbox)
    })
  }

  refresh () {
    const checkboxesCount = this.checkboxTargets.length
    const checkboxesCheckedCount = this.checked.length
    
    this.controllerTarget.indeterminate = (checkboxesCheckedCount > 0 && checkboxesCheckedCount < checkboxesCount)
    this.controllerTarget.checked = (checkboxesCheckedCount == checkboxesCount)
  }

  triggerInputEvent(checkbox) {
    const event = document.createEvent('HTMLEvents')
    event.initEvent('input', false, true)
    checkbox.dispatchEvent(event)
  }

  get checked () {
    return this.checkboxTargets.filter(checkbox => checkbox.checked)
  }

  get unchecked () {
    return this.checkboxTargets.filter(checkbox => !checkbox.checked)
  }
}

class MultipleFileInputController extends Stimulus.Controller {
  static targets = [ "template", "gallery", "input", "persisted", "unpersisted", "notice", "gallerySection" ]
  static classes = [ "supportedInput" ]

  connect() {
    this.fileSizeBase = 1000
    this.inputTarget.classList.add(...this.supportedInputClasses)
  }

  previewInGallery() {
    if (this.inputTarget.files) {
      this.unpersistedTargets.forEach(n => n.remove())
      this.notices = {}
      this.noticeTarget.innerHTML = ''

      Array.from(this.inputTarget.files).forEach(file => {
        if (this._validateFile(file)) {
          this.gallerySectionTarget.classList.remove('hidden')
          this._addFile(this.galleryTarget, file) 
        } else {
          this._showNoticeForFile(file)
        }
      })
    }
  }
  
  removeFile(event){
    const target = this.unpersistedTargets.find(li => li.contains(event.currentTarget))
    const index = this.unpersistedTargets.indexOf(target)
    const attachments = this.inputTarget.files
    let fileBuffer = new DataTransfer()

    // append the file list to an array iteratively
    for (let i = 0; i < attachments.length; i++) {
      if (index !== i)
        fileBuffer.items.add(attachments[i])
    }
    
    // Assign buffer to file input
    this.inputTarget.files = fileBuffer.files
    target.remove()
  }
  
  removeAll() {
    var fileFieldHTML = this.inputTarget.outerHTML
    var parent = this.inputTarget.parentElement
    this.inputTarget.remove()
    parent.insertAdjacentHTML('afterbegin', fileFieldHTML)
    this.unpersistedTargets.forEach(n => n.remove())
    this.gallerySectionTarget.classList.add('hidden')
  }
  
  _validateFile(file) {
    const validFileSize = this._validateFileSize(file)
    const validFileType = this._validateFileType(file)

    return validFileSize && validFileType
  }
  
  _validateFileType(file) {
    const acceptedTypes = this.inputTarget.accept.replace(/\s/g, '').split(',')
    const validations = acceptedTypes.map(type => new RegExp(type.replace('*', '.*')))
    const isValid = validations.some(validation => validation.test(file.type))
    
    if (isValid) {
      return true
    } else {
      this.notices[file.name] ||= []
      this.notices[file.name].push(`"${file.type}" is not an allowed type`)
      return false
    }
  }
  
  _validateFileSize(file) {
    const maxFileSize = this.inputTarget.getAttribute('max-file-size')
    const base = this.fileSizeBase
    const toInt = value => parseInt(value, 10)
    
    let maxFileSizeNatural = maxFileSize.trim()
    let maxFileSizeBytes = null

    if (/MB$/i.test(maxFileSizeNatural)) {
        maxFileSizeNatural = maxFileSizeNatural.replace(/MB$i/, '').trim()
        maxFileSizeBytes = toInt(maxFileSizeNatural) * base * base
    } else if (/KB$/i.test(maxFileSizeNatural)) {
        maxFileSizeNatural = maxFileSizeNatural.replace(/KB$i/, '').trim()
        maxFileSizeBytes = toInt(maxFileSizeNatural) * base
    } else {
        maxFileSizeBytes = toInt(maxFileSizeNatural)
    }
    
    const isValid = maxFileSizeBytes !== null && file.size <= maxFileSizeBytes
    if (isValid) {
      return true
    } else {
      this.notices[file.name] ||= []
      this.notices[file.name].push(`${this._humanFileSize(file.size)} is too large`)
      return false
    }
  }
  
  _validateTotalNumberOfFiles() {}

  _addFile(target, file) {
    const objectURL = URL.createObjectURL(file)
    const clone = this.templateTarget.content.cloneNode(true)
    const nameElement = clone.querySelector("[data-target='name']")
    const sizeElement = clone.querySelector("[data-target='size']")

    nameElement.textContent = file.name
    nameElement.title = file.name
    sizeElement.textContent = this._humanFileSize(file.size)

    Object.assign(clone.querySelector("img"), {
      src: objectURL,
      alt: file.name
    })

    target.append(clone)
  }
  
  _showNoticeForFile(file) {
    const li = document.createElement('li')
    li.innerHTML = `
      <code>${file.name}</code>
      <ul class="list-disc ml-8">
        ${this.notices[file.name].map(notice => `<li>${notice}</li>`).join('')}
      </ul>
    `
    this.noticeTarget.appendChild(li)
  }
  
  _humanFileSize(size) {
    const base = this.fileSizeBase
    const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(base))
    const suffix = ['B', 'KB', 'MB', 'GB'][i]
    const integer = Math.round(size / Math.pow(base, i))
    return integer + ' ' + suffix
  }
}

const application = Stimulus.Application.start()
application.register('multiple-file-input', MultipleFileInputController)
application.register('checkboxes', CheckboxesController)

              
            
!
999px

Console