Skip to content

Instantly share code, notes, and snippets.

@ktwrd
Created September 30, 2025 13:43
Show Gist options
  • Save ktwrd/5f5ece79fedf980f1455d0d214f4d962 to your computer and use it in GitHub Desktop.
Save ktwrd/5f5ece79fedf980f1455d0d214f4d962 to your computer and use it in GitHub Desktop.
function cloneStyleSheets(element: any): CSSStyleSheet[] {
const sheets = [...(element.styleSheets || [])]
const styleSheets = sheets.map((styleSheet): CSSStyleSheet|null => {
try {
const rulesText = [...styleSheet.cssRules].map(rule => rule.cssText).join("")
let res = new CSSStyleSheet()
res.replaceSync(rulesText)
return res
} catch (e) {
console.debug(e);
return null;
}
}).filter((item) => item != null);
if (element === document) return styleSheets;
if (!element.parentElement) return cloneStyleSheets(document).concat(styleSheets)
return cloneStyleSheets(element.parentElement).concat(styleSheets)
}
export default class CustomFileInput extends HTMLElement {
internals: ElementInternals;
_inputElement: HTMLInputElement;
constructor() {
super();
this.internals = this.attachInternals();
}
static formAssociated = true;
connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });
if (!this.shadowRoot) {
console.error('[CustomFileInput] Somehow the "shadowRoot" property is null?', this);
throw new Error('Somehow the "shadowRoot" property is null?');
}
const attrs = {
name: this.getAttribute('name') || '',
form: this.getAttribute('form') || '',
}
this.shadowRoot.adoptedStyleSheets = cloneStyleSheets(this);
const wrapper = document.createElement('div');
const self = this;
const currentFilesContainer = document.createElement('ul');
currentFilesContainer.classList.add('list-group', 'mt-3');
currentFilesContainer.style.maxWidth = 'fit-content';
const inputElement = document.createElement('input');
this._inputElement = inputElement;
inputElement.type = 'file';
inputElement.className = 'form-control';
inputElement.setAttribute('name', attrs.name);
inputElement.multiple = this.hasAttribute('multiple');
if (attrs.form.length > 0) inputElement.setAttribute('form', attrs.form);
let inputElementChanged = () => {};
inputElementChanged = function () {
const files: File[] = [...(inputElement.files || [])];
console.log('[CustomFileInput.inputElementChanged]', self.internals);
const elements = [];
for (let index = 0; index < files.length; index++) {
const thisFileIndex = parseInt(index.toString());
const file = files[index];
const item = document.createElement('li');
item.className = 'list-group-item';
const c = document.createElement('div');
c.classList.add('d-flex');
const icon = document.createElement('i');
icon.classList.add('bi', 'bi-file-earmark');
c.appendChild(icon);
const span = document.createElement('span');
span.setAttribute('data-bs-toggle', 'popover');
span.setAttribute('data-bs-placement', 'bottom');
span.setAttribute('data-bs-trigger', 'hover focus');
span.setAttribute('data-bs-content', file.name);
span.classList.add('ps-1', 'pe-2');
span.innerText = file.name;
c.appendChild(span);
const removeBtn = document.createElement('button');
removeBtn.classList.add('btn', 'btn-danger', 'btn-sm', 'ms-auto');
removeBtn.innerHTML = `<i class="bi bi-trash"></i>`;
removeBtn.onclick = function () {
const dt = new DataTransfer();
for (let x = 0; x < files.length; x++) {
if (x !== thisFileIndex) {
dt.items.add(files[x]);
}
}
inputElement.files = dt.files;
inputElement.dispatchEvent(new Event('filesChanged'));
};
c.appendChild(removeBtn);
item.appendChild(c);
elements.push(item);
}
if (elements.length > 0 && !currentFilesContainer.classList.contains('mt-3')) {
currentFilesContainer.classList.add('mt-3');
} else if (elements.length === 0 && currentFilesContainer.classList.contains('mt-3')) {
currentFilesContainer.classList.remove('mt-3');
}
currentFilesContainer.replaceChildren(...elements);
};
inputElement.addEventListener('change', function() {
inputElementChanged();
});
inputElement.addEventListener('filesChanged', function() {
inputElementChanged();
});
shadow.appendChild(wrapper);
wrapper.appendChild(inputElement);
wrapper.appendChild(currentFilesContainer);
}
get files() {
return this._inputElement.files;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment