UNPKG

js-draw

Version:

Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.

175 lines (174 loc) 7.1 kB
import ReactiveValue from '../../../util/ReactiveValue.mjs'; let idCounter = 0; /** * Creates a stylized file input. This file input can either use the system file picker, or a custom * one specified by `customPickerAction`. */ const makeFileInput = (labelText, context, { accepts = '*', allowMultiSelect = false, customPickerAction } = {}) => { const container = document.createElement('div'); const label = document.createElement('label'); const input = document.createElement('input'); const descriptionBox = document.createElement('div'); descriptionBox.classList.add('toolbar--file-input-description'); const descriptionText = document.createElement('span'); container.classList.add('toolbar--file-input-container'); label.appendChild(document.createTextNode(labelText)); input.accept = accepts; input.type = customPickerAction ? 'button' : 'file'; input.classList.add('file-input'); input.multiple = allowMultiSelect; // Associate the label with the input const inputId = `js-draw-file-input-${idCounter++}`; input.setAttribute('id', inputId); label.htmlFor = inputId; const icon = context.icons.makeUploadFileIcon(); icon.classList.add('icon'); descriptionBox.replaceChildren(icon, descriptionText); label.appendChild(descriptionBox); container.replaceChildren(label, input); const selectedFiles = ReactiveValue.fromInitialValue([]); let loading = false; let cancelLoading = null; const updateStatusText = () => { const files = selectedFiles.get(); if (loading) { descriptionText.textContent = context.localization.fileInput__loading; if (cancelLoading) { const cancelText = document.createElement('b'); cancelText.textContent = context.localization.cancel; cancelText.classList.add('cancel-button'); descriptionText.appendChild(cancelText); } icon.style.display = 'none'; } else if (files.length > 0) { const fileNames = files.map((file) => file.name); const maxNames = 5; if (fileNames.length <= maxNames) { descriptionText.textContent = fileNames.join('\n'); } else { const fileNamesToShow = fileNames.slice(0, maxNames - 1); descriptionText.textContent = [ ...fileNamesToShow, context.localization.fileInput__andNMoreFiles(fileNames.length - fileNamesToShow.length), ].join('\n'); } // Only show the icon when there are files icon.style.display = 'none'; } else { // Show the icon icon.style.display = ''; const text = context.localization.dragAndDropHereOrBrowse; // Split into regions surrounded by {{curly braces}} and regions that are // not. // When given a regular expression, `.split` outputs an array. For example, // "a test __of__ split".split(/__(.*)__/) // results in // ['a test ', 'of', ' split']. const segments = text.split(/[{]{2}(.*)[}]{2}/g); descriptionText.replaceChildren(); for (let i = 0; i < segments.length; i++) { // Inside a {{pair of curly braces}}? if (i % 2 === 1) { const boldedText = document.createElement('b'); boldedText.textContent = segments[i]; descriptionText.appendChild(boldedText); } else { descriptionText.appendChild(document.createTextNode(segments[i])); } } } }; const addFileEventListeners = () => { // Support dropping files label.addEventListener('dragover', (event) => { event.preventDefault(); label.classList.add('drag-target'); }); label.addEventListener('dragenter', (event) => { event.preventDefault(); label.classList.add('drag-target'); }); label.addEventListener('dragleave', (event) => { event.preventDefault(); // Ensure the event wasn't targeting a child. // See https://stackoverflow.com/a/54271161 and // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/relatedTarget const enteringElement = event.relatedTarget; if (!enteringElement || !label.contains(enteringElement)) { label.classList.remove('drag-target'); } }); // See https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop#process_the_drop label.addEventListener('drop', (event) => { event.preventDefault(); label.classList.remove('drag-target'); const fileList = []; if (event.dataTransfer) { fileList.push(...event.dataTransfer.files); } selectedFiles.set(fileList); }); input.addEventListener('change', () => { const fileList = input.files ?? []; selectedFiles.set([...fileList]); }); }; addFileEventListeners(); // Support for custom file pickers if (customPickerAction) { const promptForFiles = async () => { if (loading) { cancelLoading?.(); return; } container.classList.add('-loading'); loading = true; updateStatusText(); try { const data = await customPickerAction({ setOnCancelCallback: (onCancel) => { if (!loading) { throw new Error("Task already completed. Can't register cancel handler."); } cancelLoading = () => { cancelLoading = null; updateStatusText(); onCancel(); }; updateStatusText(); }, }); if (data) { selectedFiles.set(data); } } finally { container.classList.remove('-loading'); loading = false; updateStatusText(); } }; input.onclick = promptForFiles; } selectedFiles.onUpdate((files) => { if (files.length === 0 && input.files && input.files.length > 0) { input.value = ''; } cancelLoading?.(); }); // Update the status text and hide/show the icon. selectedFiles.onUpdateAndNow(updateStatusText); return { container, input, selectedFiles, addTo: (parent) => { parent.appendChild(container); }, }; }; export default makeFileInput;