UNPKG

vue-csv-processor

Version:

Vue 3 component library for CSV file processing with encoding detection and preview

1,283 lines (1,282 loc) 44.1 kB
import { defineComponent, ref, reactive, computed, watch, provide, openBlock, createElementBlock, renderSlot, inject, onMounted, createElementVNode, normalizeClass, withModifiers, toDisplayString, Fragment, renderList, createCommentVNode, withDirectives, vModelSelect, mergeProps, createTextVNode } from "vue"; function parseCSV(content, options = {}) { const { hasHeaders = true, delimiter = "", trimFields = true, encoding = "UTF-8", skipEmptyLines = true } = options; let csvText; if (content instanceof ArrayBuffer) { csvText = new TextDecoder(encoding).decode(content); } else { csvText = content; } if (csvText.charCodeAt(0) === 65279) { csvText = csvText.slice(1); } const lines = []; let currentLine = []; let currentField = ""; let inQuotes = false; for (let i = 0; i < csvText.length; i++) { const char = csvText.charAt(i); const nextChar = i < csvText.length - 1 ? csvText.charAt(i + 1) : ""; if (char === '"') { if (inQuotes && nextChar === '"') { currentField += '"'; i++; } else { inQuotes = !inQuotes; } } else if ((char === delimiter || delimiter === "" && char === ",") && !inQuotes) { if (trimFields) { currentLine.push(currentField.trim()); } else { currentLine.push(currentField); } currentField = ""; } else if ((char === "\n" || char === "\r" && nextChar === "\n") && !inQuotes) { if (char === "\r") i++; if (trimFields) { currentLine.push(currentField.trim()); } else { currentLine.push(currentField); } if (!skipEmptyLines || currentLine.some((field) => field.length > 0)) { lines.push(currentLine); } currentLine = []; currentField = ""; } else { currentField += char; } } if (currentField.length > 0 || currentLine.length > 0) { if (trimFields) { currentLine.push(currentField.trim()); } else { currentLine.push(currentField); } if (!skipEmptyLines || currentLine.some((field) => field.length > 0)) { lines.push(currentLine); } } if (lines.length === 0) { return { data: [], headers: [], errors: ["Empty CSV content"] }; } let headers = []; let data = []; let errors = []; if (hasHeaders && lines.length > 0) { headers = lines[0]; lines.shift(); } else if (!hasHeaders && lines.length > 0) { headers = lines[0].map((_, index) => `Column ${index + 1}`); } data = lines.map((line, lineIndex) => { const row = {}; if (line.length !== headers.length) { errors.push(`Line ${lineIndex + 1} has ${line.length} fields, expected ${headers.length}`); if (line.length < headers.length) { line = [...line, ...Array(headers.length - line.length).fill("")]; } else { line = line.slice(0, headers.length); } } headers.forEach((header, index) => { if (index < line.length) { row[header] = line[index]; } else { row[header] = ""; } }); return row; }); return { data, headers, errors }; } function detectDelimiter(csvContent) { const sampleLines = csvContent.split(/\r?\n/).slice(0, 5).join("\n"); const counts = { ",": (sampleLines.match(/,/g) || []).length, ";": (sampleLines.match(/;/g) || []).length, " ": (sampleLines.match(/\t/g) || []).length, "|": (sampleLines.match(/\|/g) || []).length }; let maxCount = 0; let detectedDelimiter = ","; Object.entries(counts).forEach(([delimiter, count]) => { if (count > maxCount) { maxCount = count; detectedDelimiter = delimiter; } }); return detectedDelimiter; } const SUPPORTED_ENCODINGS = [ { value: "UTF-8", label: "UTF-8 (Standard)" }, { value: "ISO-8859-1", label: "ISO-8859-1 (Latin-1)" }, { value: "windows-1252", label: "Windows-1252 (Western European)" }, { value: "ISO-8859-15", label: "ISO-8859-15 (Latin-9)" }, { value: "macintosh", label: "Mac Roman" }, { value: "windows-1251", label: "Windows-1251 (Cyrillic)" }, { value: "ISO-8859-2", label: "ISO-8859-2 (Central European)" }, { value: "ISO-8859-5", label: "ISO-8859-5 (Cyrillic)" } ]; function detectBOM(buffer) { const uint8Array = new Uint8Array(buffer.slice(0, 4)); if (uint8Array[0] === 239 && uint8Array[1] === 187 && uint8Array[2] === 191) { return "UTF-8"; } if (uint8Array[0] === 255 && uint8Array[1] === 254) { return "UTF-16LE"; } if (uint8Array[0] === 254 && uint8Array[1] === 255) { return "UTF-16BE"; } if (uint8Array[0] === 255 && uint8Array[1] === 254 && uint8Array[2] === 0 && uint8Array[3] === 0) { return "UTF-32LE"; } if (uint8Array[0] === 0 && uint8Array[1] === 0 && uint8Array[2] === 254 && uint8Array[3] === 255) { return "UTF-32BE"; } return null; } function detectEncoding(buffer) { const bomEncoding = detectBOM(buffer); if (bomEncoding) { return bomEncoding; } const uint8Array = new Uint8Array(buffer); let isUtf8 = true; let hasHighAscii = false; for (let i = 0; i < uint8Array.length; i++) { const byte = uint8Array[i]; if (byte > 127) { hasHighAscii = true; if (byte >= 192 && byte <= 223) { if (i + 1 >= uint8Array.length || (uint8Array[i + 1] & 192) !== 128) { isUtf8 = false; break; } i += 1; } else if (byte >= 224 && byte <= 239) { if (i + 2 >= uint8Array.length || (uint8Array[i + 1] & 192) !== 128 || (uint8Array[i + 2] & 192) !== 128) { isUtf8 = false; break; } i += 2; } else if (byte >= 240 && byte <= 247) { if (i + 3 >= uint8Array.length || (uint8Array[i + 1] & 192) !== 128 || (uint8Array[i + 2] & 192) !== 128 || (uint8Array[i + 3] & 192) !== 128) { isUtf8 = false; break; } i += 3; } else { isUtf8 = false; break; } } } if (isUtf8 && hasHighAscii) { return "UTF-8"; } return "windows-1252"; } async function readWithEncoding(file, encoding = "UTF-8") { if (file instanceof ArrayBuffer) { return new TextDecoder(encoding).decode(file); } return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const buffer = e.target.result; const text = new TextDecoder(encoding).decode(buffer); resolve(text); }; reader.onerror = (e) => { reject(new Error(`Error reading file: ${e.target.error}`)); }; reader.readAsArrayBuffer(file); }); } const VueCsvProcessor_vue_vue_type_style_index_0_scoped_67071360_lang = ""; const _export_sfc = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) { target[key] = val; } return target; }; const _sfc_main$5 = defineComponent({ name: "VueCsvProcessor", props: { /** * v-model binding for the processed CSV data */ modelValue: { type: Array, default: () => [] }, /** * Field definitions to map CSV columns to * Format: { fieldName: { required: true|false, label: 'Display Label' } } */ fields: { type: Object, required: true }, /** * Custom text overrides */ text: { type: Object, default: () => ({ errors: { fileRequired: "A file is required", invalidMimeType: "Invalid file type", encodingError: "Error processing file with selected encoding" }, toggleHeaders: "File has headers", submitBtn: "Submit", fieldColumn: "Field", csvColumn: "Column", encoding: "Text Encoding" }) }, /** * Auto map CSV columns to fields by name */ autoMatch: { type: Boolean, default: true }, /** * Default encoding to use */ defaultEncoding: { type: String, default: "UTF-8" } }, emits: ["update:modelValue", "file-loaded", "encoding-changed", "headers-toggled", "data-updated", "mapping-updated"], setup(props, { emit }) { const file = ref(null); const fileBuffer = ref(null); const rawContent = ref(""); const hasHeaders = ref(true); const parsedData = ref([]); const parsedHeaders = ref([]); const errors = ref([]); const encoding = ref(props.defaultEncoding); const supportedEncodings = ref(SUPPORTED_ENCODINGS); const mapping = reactive({}); const processedData = computed(() => { if (!parsedData.value || !parsedData.value.length) return []; return parsedData.value.map((row) => { const mappedRow = {}; Object.keys(mapping).forEach((field) => { const mappedColumn = mapping[field]; if (mappedColumn && mappedColumn in row) { mappedRow[field] = row[mappedColumn]; } else { mappedRow[field] = ""; } }); return mappedRow; }); }); watch(processedData, (newValue) => { emit("update:modelValue", newValue); emit("data-updated", newValue); }, { deep: true }); watch(mapping, (newValue) => { emit("mapping-updated", newValue); }, { deep: true }); const setFile = async (newFile) => { file.value = newFile; errors.value = []; if (!newFile) { parsedData.value = []; parsedHeaders.value = []; fileBuffer.value = null; rawContent.value = ""; return; } try { fileBuffer.value = await readAsArrayBuffer(newFile); if (encoding.value === props.defaultEncoding) { encoding.value = detectEncoding(fileBuffer.value) || props.defaultEncoding; } await processFile(); emit("file-loaded", file.value); } catch (err) { errors.value.push(`Error loading file: ${err.message}`); } }; const readAsArrayBuffer = (file2) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = (e) => reject(new Error("Error reading file")); reader.readAsArrayBuffer(file2); }); }; const processFile = async () => { if (!file.value || !fileBuffer.value) { return; } try { rawContent.value = await readWithEncoding(fileBuffer.value, encoding.value); const delimiter = detectDelimiter(rawContent.value); const result = parseCSV(rawContent.value, { hasHeaders: hasHeaders.value, delimiter, trimFields: true, encoding: encoding.value, skipEmptyLines: true }); parsedData.value = result.data; parsedHeaders.value = result.headers; if (result.errors && result.errors.length) { errors.value = [...errors.value, ...result.errors]; } if (props.autoMatch) { autoMapColumns(); } } catch (err) { errors.value.push(`${props.text.errors.encodingError}: ${err.message}`); } }; const toggleHeaders = () => { hasHeaders.value = !hasHeaders.value; emit("headers-toggled", hasHeaders.value); if (file.value) { processFile(); } }; const changeEncoding = async (newEncoding) => { encoding.value = newEncoding; emit("encoding-changed", newEncoding); if (file.value && fileBuffer.value) { await processFile(); } }; const autoMapColumns = () => { if (!parsedHeaders.value.length) return; const fieldKeys = Object.keys(props.fields); Object.keys(mapping).forEach((key) => { delete mapping[key]; }); fieldKeys.forEach((fieldKey) => { const fieldLabel = props.fields[fieldKey].label || fieldKey; let matchIndex = parsedHeaders.value.findIndex( (header) => header.toLowerCase() === fieldLabel.toLowerCase() ); if (matchIndex === -1) { matchIndex = parsedHeaders.value.findIndex( (header) => header.toLowerCase().includes(fieldLabel.toLowerCase()) || fieldLabel.toLowerCase().includes(header.toLowerCase()) ); } if (matchIndex !== -1) { mapping[fieldKey] = parsedHeaders.value[matchIndex]; } }); }; const mapField = (field, column) => { mapping[field] = column; }; const getSampleData = (rowCount = 5) => { return parsedData.value.slice(0, rowCount); }; provide("csvProcessor", { // State file, hasHeaders, parsedData, parsedHeaders, errors, encoding, supportedEncodings, mapping, rawContent, fileBuffer, text: props.text, fields: props.fields, // Methods setFile, toggleHeaders, changeEncoding, mapField, getSampleData, processFile, autoMapColumns }); return { // State file, hasHeaders, parsedData, parsedHeaders, errors, encoding, supportedEncodings, mapping, rawContent, // Methods setFile, toggleHeaders, changeEncoding, mapField, getSampleData }; } }); const _hoisted_1$5 = { class: "vue-csv-processor" }; function _sfc_render$5(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createElementBlock("div", _hoisted_1$5, [ renderSlot(_ctx.$slots, "default", { file: _ctx.file, errors: _ctx.errors, fields: _ctx.fields, mapping: _ctx.mapping, hasHeaders: _ctx.hasHeaders, csvData: _ctx.parsedData, rawContent: _ctx.rawContent, encoding: _ctx.encoding }, void 0, true) ]); } const VueCsvProcessor = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["render", _sfc_render$5], ["__scopeId", "data-v-67071360"]]); const VueCsvInput_vue_vue_type_style_index_0_scoped_218ab60c_lang = ""; const _sfc_main$4 = defineComponent({ name: "VueCsvInput", props: { /** * Input field name */ name: { type: String, default: "csv-file" }, /** * Accepted file types */ accept: { type: String, default: ".csv,text/csv,application/vnd.ms-excel,text/plain" }, /** * Whether to perform validation */ validation: { type: Boolean, default: true }, /** * Allowed file MIME types */ fileMimeTypes: { type: Array, default: () => [ "text/csv", "text/x-csv", "application/vnd.ms-excel", "text/plain" ] }, /** * Maximum file size in bytes */ maxSize: { type: Number, default: 5 * 1024 * 1024 // 5MB }, /** * Whether the input is disabled */ disabled: { type: Boolean, default: false } }, emits: ["change", "error"], setup(props, { emit }) { const csvProcessor = inject("csvProcessor"); const fileInputRef = ref(null); const isDragging = ref(false); const fileErrors = ref([]); const file = computed(() => csvProcessor.file.value); const handleChange = async (event) => { var _a; const selectedFile = ((_a = event.target.files) == null ? void 0 : _a[0]) || null; if (!selectedFile) { return; } if (props.validation) { fileErrors.value = validateFile(selectedFile); if (fileErrors.value.length > 0) { emit("error", fileErrors.value); return; } } try { await csvProcessor.setFile(selectedFile); emit("change", selectedFile); } catch (error) { fileErrors.value.push(`Error processing file: ${error.message}`); emit("error", fileErrors.value); } }; const removeFile = () => { if (fileInputRef.value) { fileInputRef.value.value = ""; } csvProcessor.setFile(null); fileErrors.value = []; emit("change", null); }; const validateFile = (file2) => { const errors = []; const fileExtension = file2.name.split(".").pop().toLowerCase(); const fileMimeType = file2.type; if (!props.fileMimeTypes.includes(fileMimeType) && fileExtension !== "csv") { errors.push(csvProcessor.text.errors.invalidMimeType); } if (file2.size > props.maxSize) { errors.push(`File is too large. Maximum size is ${formatFileSize(props.maxSize)}.`); } return errors; }; const formatFileSize = (bytes) => { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; const onDragOver = (event) => { isDragging.value = true; }; const onDragLeave = (event) => { isDragging.value = false; }; const onDrop = (event) => { isDragging.value = false; const droppedFile = event.dataTransfer.files[0]; if (!droppedFile) return; if (fileInputRef.value) { const dataTransfer = new DataTransfer(); dataTransfer.items.add(droppedFile); fileInputRef.value.files = dataTransfer.files; handleChange({ target: fileInputRef.value }); } }; onMounted(() => { if (fileInputRef.value && csvProcessor) { csvProcessor.fileInputRef = fileInputRef; } }); watch(() => csvProcessor.errors.value, (newErrors) => { if (newErrors && newErrors.length > 0) { fileErrors.value = newErrors; } }); return { fileInputRef, file, isDragging, fileErrors, handleChange, removeFile, formatFileSize, onDragOver, onDragLeave, onDrop }; } }); const _hoisted_1$4 = { class: "vue-csv-input" }; const _hoisted_2$4 = { key: 0 }; const _hoisted_3$4 = ["name", "accept", "disabled"]; const _hoisted_4$4 = { class: "file-content" }; const _hoisted_5$3 = { key: 0, class: "file-placeholder" }; const _hoisted_6$2 = { key: 1, class: "file-info" }; const _hoisted_7$2 = { class: "file-name" }; const _hoisted_8$2 = { class: "file-size" }; const _hoisted_9$2 = { key: 0, class: "file-errors" }; function _sfc_render$4(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createElementBlock("div", _hoisted_1$4, [ !_ctx.$slots.default ? (openBlock(), createElementBlock("div", _hoisted_2$4, [ createElementVNode("div", { class: normalizeClass(["file-drop-area", { "file-drop-active": _ctx.isDragging, "has-file": !!_ctx.file }]), onDragover: _cache[2] || (_cache[2] = withModifiers((...args) => _ctx.onDragOver && _ctx.onDragOver(...args), ["prevent"])), onDragleave: _cache[3] || (_cache[3] = withModifiers((...args) => _ctx.onDragLeave && _ctx.onDragLeave(...args), ["prevent"])), onDrop: _cache[4] || (_cache[4] = withModifiers((...args) => _ctx.onDrop && _ctx.onDrop(...args), ["prevent"])) }, [ createElementVNode("input", { ref: "fileInputRef", type: "file", name: _ctx.name, accept: _ctx.accept, class: "file-input", onChange: _cache[0] || (_cache[0] = (...args) => _ctx.handleChange && _ctx.handleChange(...args)), disabled: _ctx.disabled }, null, 40, _hoisted_3$4), createElementVNode("div", _hoisted_4$4, [ !_ctx.file ? (openBlock(), createElementBlock("div", _hoisted_5$3, _cache[5] || (_cache[5] = [ createElementVNode("div", { class: "file-icon" }, [ createElementVNode("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "24", height: "24" }, [ createElementVNode("path", { fill: "none", d: "M0 0h24v24H0z" }), createElementVNode("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM6 20V4h7v5h5v11H6z" }) ]) ], -1), createElementVNode("span", null, "Drop CSV file here or click to browse", -1) ]))) : (openBlock(), createElementBlock("div", _hoisted_6$2, [ createElementVNode("span", _hoisted_7$2, toDisplayString(_ctx.file.name), 1), createElementVNode("span", _hoisted_8$2, "(" + toDisplayString(_ctx.formatFileSize(_ctx.file.size)) + ")", 1), createElementVNode("button", { class: "remove-file", onClick: _cache[1] || (_cache[1] = withModifiers((...args) => _ctx.removeFile && _ctx.removeFile(...args), ["prevent"])) }, "✕") ])) ]) ], 34), _ctx.fileErrors.length ? (openBlock(), createElementBlock("div", _hoisted_9$2, [ (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.fileErrors, (error, index) => { return openBlock(), createElementBlock("p", { key: index, class: "error-message" }, toDisplayString(error), 1); }), 128)) ])) : createCommentVNode("", true) ])) : renderSlot(_ctx.$slots, "default", { key: 1, file: _ctx.file, change: _ctx.handleChange, remove: _ctx.removeFile, errors: _ctx.fileErrors }, void 0, true) ]); } const VueCsvInput = /* @__PURE__ */ _export_sfc(_sfc_main$4, [["render", _sfc_render$4], ["__scopeId", "data-v-218ab60c"]]); const VueCsvPreview_vue_vue_type_style_index_0_scoped_f1c12314_lang = ""; const _sfc_main$3 = defineComponent({ name: "VueCsvPreview", props: { /** * Selected encoding (v-model:encoding) */ encoding: { type: String, default: "UTF-8" }, /** * List of encodings to show in the dropdown */ encodings: { type: Array, default: () => SUPPORTED_ENCODINGS.map((e) => e.value) }, /** * Number of preview rows to show */ rowCount: { type: Number, default: 5 }, /** * Whether to show row numbers */ showRowNumbers: { type: Boolean, default: true }, /** * Unique ID for the encoding select */ encodingSelectId: { type: String, default: "csv-encoding-select" } }, emits: ["update:encoding", "encoding-change"], setup(props, { emit }) { const csvProcessor = inject("csvProcessor"); const selectedEncoding = ref(props.encoding); const previewRowCount = ref(props.rowCount); const parsedData = computed(() => csvProcessor.parsedData.value || []); const parsedHeaders = computed(() => csvProcessor.parsedHeaders.value || []); const hasFile = computed(() => !!csvProcessor.file.value); const text = computed(() => csvProcessor.text); const supportedEncodings = computed(() => { return SUPPORTED_ENCODINGS.filter( (encoding) => props.encodings.includes(encoding.value) ); }); const previewData = computed(() => { return parsedData.value.slice(0, previewRowCount.value); }); const totalRows = computed(() => parsedData.value.length); const hasEncodingIssues = computed(() => { if (!previewData.value.length) return false; for (const row of previewData.value) { for (const header of parsedHeaders.value) { if (hasEncodingIssue(row[header])) { return true; } } } return false; }); const hasEncodingIssue = (value) => { if (typeof value !== "string") return false; return value.includes("�") || value.includes("Ã") || /[\u{D800}-\u{DFFF}]/u.test(value); }; const onEncodingChange = () => { emit("update:encoding", selectedEncoding.value); emit("encoding-change", selectedEncoding.value); csvProcessor.changeEncoding(selectedEncoding.value); }; watch(() => props.encoding, (newEncoding) => { selectedEncoding.value = newEncoding; }); watch(() => csvProcessor.encoding.value, (newEncoding) => { selectedEncoding.value = newEncoding; }); return { selectedEncoding, previewRowCount, previewData, parsedHeaders, totalRows, hasFile, text, supportedEncodings, hasEncodingIssues, hasEncodingIssue, onEncodingChange }; } }); const _hoisted_1$3 = { class: "vue-csv-preview" }; const _hoisted_2$3 = { key: 0 }; const _hoisted_3$3 = { key: 0, class: "preview-empty" }; const _hoisted_4$3 = { key: 1, class: "preview-container" }; const _hoisted_5$2 = { class: "encoding-selector" }; const _hoisted_6$1 = ["for"]; const _hoisted_7$1 = ["id"]; const _hoisted_8$1 = ["value"]; const _hoisted_9$1 = { class: "preview-table-wrapper" }; const _hoisted_10$1 = { class: "preview-table" }; const _hoisted_11$1 = { key: 0, class: "row-number-cell" }; const _hoisted_12$1 = { key: 0, class: "row-number-cell" }; const _hoisted_13$1 = { class: "preview-controls" }; const _hoisted_14$1 = { class: "preview-info" }; const _hoisted_15$1 = { class: "preview-buttons" }; const _hoisted_16 = ["disabled"]; const _hoisted_17 = ["disabled"]; const _hoisted_18 = { key: 0, class: "encoding-note" }; function _sfc_render$3(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createElementBlock("div", _hoisted_1$3, [ !_ctx.$slots.default ? (openBlock(), createElementBlock("div", _hoisted_2$3, [ !_ctx.hasFile ? (openBlock(), createElementBlock("div", _hoisted_3$3, _cache[4] || (_cache[4] = [ createElementVNode("p", null, "Upload a CSV file to preview data", -1) ]))) : (openBlock(), createElementBlock("div", _hoisted_4$3, [ createElementVNode("div", _hoisted_5$2, [ createElementVNode("label", { for: _ctx.encodingSelectId, class: "encoding-label" }, toDisplayString(_ctx.text.encoding) + ":", 9, _hoisted_6$1), withDirectives(createElementVNode("select", { id: _ctx.encodingSelectId, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => _ctx.selectedEncoding = $event), class: "encoding-select", onChange: _cache[1] || (_cache[1] = (...args) => _ctx.onEncodingChange && _ctx.onEncodingChange(...args)) }, [ (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.supportedEncodings, (encodingOption) => { return openBlock(), createElementBlock("option", { key: encodingOption.value, value: encodingOption.value }, toDisplayString(encodingOption.label), 9, _hoisted_8$1); }), 128)) ], 40, _hoisted_7$1), [ [vModelSelect, _ctx.selectedEncoding] ]) ]), createElementVNode("div", _hoisted_9$1, [ createElementVNode("table", _hoisted_10$1, [ createElementVNode("thead", null, [ createElementVNode("tr", null, [ _ctx.showRowNumbers ? (openBlock(), createElementBlock("th", _hoisted_11$1, "#")) : createCommentVNode("", true), (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.parsedHeaders, (header, index) => { return openBlock(), createElementBlock("th", { key: index, class: "header-cell" }, toDisplayString(header), 1); }), 128)) ]) ]), createElementVNode("tbody", null, [ (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.previewData, (row, rowIndex) => { return openBlock(), createElementBlock("tr", { key: rowIndex, class: "data-row" }, [ _ctx.showRowNumbers ? (openBlock(), createElementBlock("td", _hoisted_12$1, toDisplayString(rowIndex + 1), 1)) : createCommentVNode("", true), (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.parsedHeaders, (header, colIndex) => { return openBlock(), createElementBlock("td", { key: colIndex, class: normalizeClass(["data-cell", { "encoding-issue": _ctx.hasEncodingIssue(row[header]) }]) }, toDisplayString(row[header]), 3); }), 128)) ]); }), 128)) ]) ]) ]), createElementVNode("div", _hoisted_13$1, [ createElementVNode("div", _hoisted_14$1, [ createElementVNode("span", null, "Showing " + toDisplayString(_ctx.previewData.length) + " of " + toDisplayString(_ctx.totalRows) + " rows", 1) ]), createElementVNode("div", _hoisted_15$1, [ createElementVNode("button", { class: "preview-button", onClick: _cache[2] || (_cache[2] = ($event) => _ctx.previewRowCount = Math.max(5, _ctx.previewRowCount - 5)), disabled: _ctx.previewRowCount <= 5 }, " Show less ", 8, _hoisted_16), createElementVNode("button", { class: "preview-button", onClick: _cache[3] || (_cache[3] = ($event) => _ctx.previewRowCount = Math.min(_ctx.totalRows, _ctx.previewRowCount + 5)), disabled: _ctx.previewRowCount >= _ctx.totalRows }, " Show more ", 8, _hoisted_17) ]) ]), _ctx.hasEncodingIssues ? (openBlock(), createElementBlock("div", _hoisted_18, _cache[5] || (_cache[5] = [ createElementVNode("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "20", height: "20", class: "warning-icon" }, [ createElementVNode("path", { fill: "none", d: "M0 0h24v24H0z" }), createElementVNode("path", { d: "M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-1-5h2v2h-2v-2zm0-8h2v6h-2V7z" }) ], -1), createElementVNode("span", null, " Some characters may not display correctly. Try a different encoding. ", -1) ]))) : createCommentVNode("", true) ])) ])) : renderSlot(_ctx.$slots, "default", { key: 1, previewData: _ctx.previewData, parsedHeaders: _ctx.parsedHeaders, totalRows: _ctx.totalRows, encoding: _ctx.selectedEncoding, supportedEncodings: _ctx.supportedEncodings, changeEncoding: _ctx.onEncodingChange, hasEncodingIssues: _ctx.hasEncodingIssues }, void 0, true) ]); } const VueCsvPreview = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["render", _sfc_render$3], ["__scopeId", "data-v-f1c12314"]]); const VueCsvToggleHeaders_vue_vue_type_style_index_0_scoped_bcaca94b_lang = ""; const _sfc_main$2 = defineComponent({ name: "VueCsvToggleHeaders", props: { /** * HTML ID for the checkbox */ toggleId: { type: String, default: "csv-has-headers" }, /** * Additional attributes to bind to the checkbox */ checkboxAttributes: { type: Object, default: () => ({}) }, /** * Additional attributes to bind to the label */ labelAttributes: { type: Object, default: () => ({}) } }, emits: ["toggle"], setup(props, { emit }) { const csvProcessor = inject("csvProcessor"); const hasHeaders = computed(() => csvProcessor.hasHeaders.value); const text = computed(() => csvProcessor.text); const toggle = () => { csvProcessor.toggleHeaders(); emit("toggle", !hasHeaders.value); }; return { hasHeaders, text, toggle }; } }); const _hoisted_1$2 = { class: "vue-csv-toggle-headers" }; const _hoisted_2$2 = { key: 0, class: "toggle-container" }; const _hoisted_3$2 = ["id", "checked"]; const _hoisted_4$2 = ["for"]; function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createElementBlock("div", _hoisted_1$2, [ !_ctx.$slots.default ? (openBlock(), createElementBlock("div", _hoisted_2$2, [ createElementVNode("input", mergeProps({ id: _ctx.toggleId, type: "checkbox", checked: _ctx.hasHeaders, onChange: _cache[0] || (_cache[0] = (...args) => _ctx.toggle && _ctx.toggle(...args)), class: "toggle-checkbox" }, _ctx.checkboxAttributes), null, 16, _hoisted_3$2), createElementVNode("label", mergeProps({ for: _ctx.toggleId, class: "toggle-label" }, _ctx.labelAttributes), toDisplayString(_ctx.text.toggleHeaders), 17, _hoisted_4$2) ])) : renderSlot(_ctx.$slots, "default", { key: 1, hasHeaders: _ctx.hasHeaders, toggle: _ctx.toggle }, void 0, true) ]); } const VueCsvToggleHeaders = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["render", _sfc_render$2], ["__scopeId", "data-v-bcaca94b"]]); const VueCsvMap_vue_vue_type_style_index_0_scoped_48273534_lang = ""; const _sfc_main$1 = defineComponent({ name: "VueCsvMap", props: { /** * Hide table header */ noThead: { type: Boolean, default: false }, /** * Additional attributes to bind to the select inputs */ selectAttributes: { type: Object, default: () => ({}) }, /** * Auto-match fields to columns when they share the same name */ autoMatch: { type: Boolean, default: true }, /** * Ignore case when auto-matching */ autoMatchIgnoreCase: { type: Boolean, default: true } }, emits: ["update:mapping", "mapping-change"], setup(props, { emit }) { const csvProcessor = inject("csvProcessor"); const fields = computed(() => csvProcessor.fields); const parsedHeaders = computed(() => csvProcessor.parsedHeaders.value || []); const parsedData = computed(() => csvProcessor.parsedData.value || []); const mapping = computed(() => csvProcessor.mapping); const text = computed(() => csvProcessor.text); const hasData = computed(() => parsedData.value.length > 0 && parsedHeaders.value.length > 0); const sampleData = computed(() => parsedData.value.slice(0, 1)); const hasMissingRequiredFields = computed(() => { return Object.entries(fields.value).some(([fieldName, fieldConfig]) => { return fieldConfig.required && !mapping.value[fieldName]; }); }); const updateMapping = (field, column) => { csvProcessor.mapField(field, column); emit("update:mapping", { ...mapping.value }); emit("mapping-change", { field, column }); }; const formatSampleValue = (value) => { if (value === null || value === void 0) return ""; if (typeof value === "string" && value.length > 20) { return value.substring(0, 20) + "..."; } return value.toString(); }; const autoMatchFields = () => { if (!props.autoMatch || !hasData.value) return; Object.keys(fields.value).forEach((fieldName) => { if (mapping.value[fieldName]) return; const fieldLabel = fields.value[fieldName].label || fieldName; let matchIndex = -1; if (props.autoMatchIgnoreCase) { matchIndex = parsedHeaders.value.findIndex( (header) => header.toLowerCase() === fieldLabel.toLowerCase() || header.toLowerCase() === fieldName.toLowerCase() ); } else { matchIndex = parsedHeaders.value.findIndex( (header) => header === fieldLabel || header === fieldName ); } if (matchIndex !== -1) { updateMapping(fieldName, parsedHeaders.value[matchIndex]); } }); }; onMounted(() => { if (hasData.value) { autoMatchFields(); } }); watch(() => parsedHeaders.value, (newHeaders, oldHeaders) => { if (newHeaders.length > 0 && (!oldHeaders || oldHeaders.length === 0)) { autoMatchFields(); } }); return { fields, parsedHeaders, mapping, text, hasData, sampleData, hasMissingRequiredFields, updateMapping, formatSampleValue }; } }); const _hoisted_1$1 = { class: "vue-csv-map" }; const _hoisted_2$1 = { key: 0, class: "map-empty" }; const _hoisted_3$1 = { key: 1 }; const _hoisted_4$1 = { key: 0 }; const _hoisted_5$1 = { key: 0 }; const _hoisted_6 = { class: "field-column" }; const _hoisted_7 = { class: "csv-column" }; const _hoisted_8 = { class: "field-column" }; const _hoisted_9 = { class: "field-info" }; const _hoisted_10 = { class: "field-name" }; const _hoisted_11 = { key: 0, class: "required-indicator" }; const _hoisted_12 = { class: "csv-column" }; const _hoisted_13 = ["value", "onChange"]; const _hoisted_14 = ["value"]; const _hoisted_15 = { key: 0, class: "required-reminder" }; function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createElementBlock("div", _hoisted_1$1, [ !_ctx.hasData ? (openBlock(), createElementBlock("div", _hoisted_2$1, _cache[0] || (_cache[0] = [ createElementVNode("p", null, "Upload a CSV file to map columns", -1) ]))) : (openBlock(), createElementBlock("div", _hoisted_3$1, [ !_ctx.$slots.default ? (openBlock(), createElementBlock("div", _hoisted_4$1, [ createElementVNode("table", { class: normalizeClass(["mapping-table", { "no-thead": _ctx.noThead }]) }, [ !_ctx.noThead ? (openBlock(), createElementBlock("thead", _hoisted_5$1, [ createElementVNode("tr", null, [ createElementVNode("th", _hoisted_6, toDisplayString(_ctx.text.fieldColumn), 1), createElementVNode("th", _hoisted_7, toDisplayString(_ctx.text.csvColumn), 1) ]) ])) : createCommentVNode("", true), createElementVNode("tbody", null, [ (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.fields, (field, fieldName) => { return openBlock(), createElementBlock("tr", { key: fieldName, class: "mapping-row" }, [ createElementVNode("td", _hoisted_8, [ createElementVNode("div", _hoisted_9, [ createElementVNode("span", _hoisted_10, toDisplayString(field.label || fieldName), 1), field.required ? (openBlock(), createElementBlock("span", _hoisted_11, "*")) : createCommentVNode("", true) ]) ]), createElementVNode("td", _hoisted_12, [ createElementVNode("select", mergeProps({ value: _ctx.mapping[fieldName], onChange: ($event) => _ctx.updateMapping(fieldName, $event.target.value), class: "mapping-select", ref_for: true }, _ctx.selectAttributes), [ _cache[1] || (_cache[1] = createElementVNode("option", { value: "" }, "-- Select Column --", -1)), (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.parsedHeaders, (header, index) => { return openBlock(), createElementBlock("option", { key: index, value: header }, [ createTextVNode(toDisplayString(header) + " ", 1), _ctx.sampleData.length && _ctx.sampleData[0][header] ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [ createTextVNode(" (e.g. " + toDisplayString(_ctx.formatSampleValue(_ctx.sampleData[0][header])) + ") ", 1) ], 64)) : createCommentVNode("", true) ], 8, _hoisted_14); }), 128)) ], 16, _hoisted_13) ]) ]); }), 128)) ]) ], 2), _ctx.hasMissingRequiredFields ? (openBlock(), createElementBlock("div", _hoisted_15, _cache[2] || (_cache[2] = [ createElementVNode("p", null, "* Required fields must be mapped", -1) ]))) : createCommentVNode("", true) ])) : renderSlot(_ctx.$slots, "default", { key: 1, sample: _ctx.sampleData, mapping: _ctx.mapping, fields: _ctx.fields, parsedHeaders: _ctx.parsedHeaders, updateMapping: _ctx.updateMapping }, void 0, true) ])) ]); } const VueCsvMap = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["render", _sfc_render$1], ["__scopeId", "data-v-48273534"]]); const VueCsvErrors_vue_vue_type_style_index_0_scoped_3335c9f9_lang = ""; const _sfc_main = defineComponent({ name: "VueCsvErrors", setup() { const csvProcessor = inject("csvProcessor"); const errors = computed(() => csvProcessor.errors.value || []); const hasErrors = computed(() => errors.value.length > 0); const errorCount = computed(() => errors.value.length); return { errors, hasErrors, errorCount }; } }); const _hoisted_1 = { class: "vue-csv-errors" }; const _hoisted_2 = { key: 0 }; const _hoisted_3 = { key: 0, class: "errors-container", role: "alert" }; const _hoisted_4 = { class: "errors-header" }; const _hoisted_5 = { class: "errors-list" }; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createElementBlock("div", _hoisted_1, [ !_ctx.$slots.default ? (openBlock(), createElementBlock("div", _hoisted_2, [ _ctx.hasErrors ? (openBlock(), createElementBlock("div", _hoisted_3, [ createElementVNode("div", _hoisted_4, [ _cache[0] || (_cache[0] = createElementVNode("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "20", height: "20", class: "error-icon" }, [ createElementVNode("path", { fill: "none", d: "M0 0h24v24H0z" }), createElementVNode("path", { d: "M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z" }) ], -1)), createElementVNode("span", null, toDisplayString(_ctx.errorCount) + " " + toDisplayString(_ctx.errorCount === 1 ? "error" : "errors") + " found", 1) ]), createElementVNode("ul", _hoisted_5, [ (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.errors, (error, index) => { return openBlock(), createElementBlock("li", { key: index, class: "error-item" }, toDisplayString(error), 1); }), 128)) ]) ])) : createCommentVNode("", true) ])) : renderSlot(_ctx.$slots, "default", { key: 1, errors: _ctx.errors }, void 0, true) ]); } const VueCsvErrors = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-3335c9f9"]]); const VueCsvProcessorPlugin = { install: (app, options = {}) => { app.component("VueCsvProcessor", VueCsvProcessor); app.component("VueCsvInput", VueCsvInput); app.component("VueCsvPreview", VueCsvPreview); app.component("VueCsvToggleHeaders", VueCsvToggleHeaders); app.component("VueCsvMap", VueCsvMap); app.component("VueCsvErrors", VueCsvErrors); if (options.globalProperties) { Object.keys(options.globalProperties).forEach((key) => { app.config.globalProperties[key] = options.globalProperties[key]; }); } } }; export { SUPPORTED_ENCODINGS, VueCsvErrors, VueCsvInput, VueCsvMap, VueCsvPreview, VueCsvProcessor, VueCsvProcessorPlugin, VueCsvToggleHeaders, VueCsvProcessorPlugin as default, detectEncoding, parseCSV }; //# sourceMappingURL=vue-csv-processor.es.js.map