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
JavaScript
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