├── requirements.txt ├── web ├── robots.txt ├── favicon.ico ├── favicon-192x192.png ├── favicon-512x512.png ├── favicon-96x96.png ├── img │ ├── sup │ │ ├── lydia.png │ │ ├── paypal.png │ │ ├── stripe.svg │ │ ├── github.svg │ │ └── paypal.svg │ ├── meme │ │ ├── data-its-all-csv.webp │ │ ├── csv-to-rule-them-all.webp │ │ ├── perfect-csv-standard.webp │ │ ├── csv-data-atlas-pillar.webp │ │ └── opening-a-large-csv-in-excel.webp │ ├── screenshot │ │ ├── install_prompt.webp │ │ ├── bigdata_preview.webp │ │ ├── open_file_prompt.webp │ │ ├── screenshot_dark.webp │ │ ├── screenshot_light.webp │ │ └── screenshot_light_logo.webp │ ├── platform │ │ ├── microsoft.svg │ │ ├── windows.svg │ │ ├── apple.svg │ │ └── linux.svg │ └── nav │ │ ├── shield24.svg │ │ ├── cp.svg │ │ ├── valid24.svg │ │ ├── fast24.svg │ │ └── linkedin.svg ├── favicon.svg ├── manifest.JSON ├── theme.css └── index.html ├── vercel.json ├── app ├── logo │ ├── nanocell.png │ ├── nc_smiley.png │ ├── nanocell_bg.png │ ├── nanocell_bg_xWide2.png │ ├── nanocell.svg │ ├── nanocell - Copy.svg │ ├── nc_smiley.svg │ └── nanocell_logo_builder.svg ├── css │ ├── Inconsolata-Bold.ttf │ ├── Inconsolata-Regular.ttf │ ├── palettes │ │ ├── dark.css │ │ ├── night.css │ │ └── light.css │ ├── themes │ │ ├── dark.css │ │ ├── light.css │ │ └── night.css │ ├── print.css │ ├── Inputs.css │ ├── styles.css │ └── sheet.css ├── icn │ ├── minimize.svg │ ├── shift.svg │ ├── menu.svg │ ├── menu │ │ ├── validate_headers.svg │ │ ├── fixTop.svg │ │ ├── sort.svg │ │ ├── fixLeft.svg │ │ ├── replace.svg │ │ ├── overview.svg │ │ ├── trim.svg │ │ ├── bin.svg │ │ ├── sort_reverse.svg │ │ ├── validate_data.svg │ │ ├── new.svg │ │ ├── save.svg │ │ ├── undo.svg │ │ ├── redo.svg │ │ ├── open.svg │ │ ├── more.svg │ │ ├── fit_width.svg │ │ ├── find.svg │ │ ├── reloadFile.svg │ │ ├── about.svg │ │ ├── shortcuts.svg │ │ ├── zoomOut.svg │ │ ├── zoomIn.svg │ │ ├── date.svg │ │ ├── integer.svg │ │ ├── decimal.svg │ │ ├── transpose.svg │ │ └── settings.svg │ ├── on.svg │ ├── insert.svg │ ├── resize.svg │ ├── off.svg │ ├── exit.svg │ ├── decrement.svg │ ├── theme.svg │ ├── info.svg │ ├── increment.svg │ ├── true.svg │ ├── false.svg │ ├── lock_open.svg │ ├── delta.svg │ ├── lock.svg │ ├── edit.svg │ └── ratio.svg ├── js │ ├── ui │ │ └── input │ │ │ ├── Scroller.js │ │ │ ├── Table.js │ │ │ ├── BoolInput.js │ │ │ ├── TCell.js │ │ │ ├── NumInput.js │ │ │ └── ListInput.js │ ├── Shortcuts.js │ ├── Msg.js │ ├── utils │ │ ├── misc.js │ │ └── DateExt.js │ ├── main.js │ ├── dom.js │ ├── About.js │ ├── CMenu.js │ ├── key.js │ ├── CsvHandle.js │ ├── mouse.js │ ├── Dataframe.js │ ├── Finder.js │ ├── cmd.js │ └── Setting.js ├── sw_pwa_admin.js ├── home.html └── sw_read_write_csv.js ├── .gitignore ├── misc ├── csv_files │ ├── demo_parser.csv │ ├── demo_color_types.csv │ ├── demo_pipeline_config.csv │ └── demo_r100.csv ├── template.html ├── article.css ├── automate.js └── seo-pages.md ├── update_version.py ├── .vscode └── tasks.json ├── README.md ├── article ├── pwa-showcase.md ├── how-to-open-a-csv-file.md ├── lets-fix-csv-files.md └── about-csv-files.md ├── TERMS_OF_USE.md └── LICENSE.md /requirements.txt: -------------------------------------------------------------------------------- 1 | markdown 2 | requests -------------------------------------------------------------------------------- /web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /app/ -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/favicon.ico -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "python3 build.py", 3 | "outputDirectory": "public" 4 | } -------------------------------------------------------------------------------- /app/logo/nanocell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/app/logo/nanocell.png -------------------------------------------------------------------------------- /app/logo/nc_smiley.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/app/logo/nc_smiley.png -------------------------------------------------------------------------------- /web/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/favicon-192x192.png -------------------------------------------------------------------------------- /web/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/favicon-512x512.png -------------------------------------------------------------------------------- /web/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/favicon-96x96.png -------------------------------------------------------------------------------- /web/img/sup/lydia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/sup/lydia.png -------------------------------------------------------------------------------- /web/img/sup/paypal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/sup/paypal.png -------------------------------------------------------------------------------- /app/logo/nanocell_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/app/logo/nanocell_bg.png -------------------------------------------------------------------------------- /app/css/Inconsolata-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/app/css/Inconsolata-Bold.ttf -------------------------------------------------------------------------------- /app/css/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/app/css/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /app/logo/nanocell_bg_xWide2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/app/logo/nanocell_bg_xWide2.png -------------------------------------------------------------------------------- /web/img/meme/data-its-all-csv.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/meme/data-its-all-csv.webp -------------------------------------------------------------------------------- /web/img/meme/csv-to-rule-them-all.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/meme/csv-to-rule-them-all.webp -------------------------------------------------------------------------------- /web/img/meme/perfect-csv-standard.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/meme/perfect-csv-standard.webp -------------------------------------------------------------------------------- /web/img/screenshot/install_prompt.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/screenshot/install_prompt.webp -------------------------------------------------------------------------------- /web/img/meme/csv-data-atlas-pillar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/meme/csv-data-atlas-pillar.webp -------------------------------------------------------------------------------- /web/img/screenshot/bigdata_preview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/screenshot/bigdata_preview.webp -------------------------------------------------------------------------------- /web/img/screenshot/open_file_prompt.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/screenshot/open_file_prompt.webp -------------------------------------------------------------------------------- /web/img/screenshot/screenshot_dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/screenshot/screenshot_dark.webp -------------------------------------------------------------------------------- /web/img/screenshot/screenshot_light.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/screenshot/screenshot_light.webp -------------------------------------------------------------------------------- /web/img/screenshot/screenshot_light_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/screenshot/screenshot_light_logo.webp -------------------------------------------------------------------------------- /web/img/meme/opening-a-large-csv-in-excel.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricBonjour/nanocell-csv/HEAD/web/img/meme/opening-a-large-csv-in-excel.webp -------------------------------------------------------------------------------- /app/icn/minimize.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/test/ 2 | test/csv_files/ 3 | test/ 4 | public/ 5 | private/ 6 | 7 | *TODO.md 8 | 9 | # Ignore all .csv files except demo.csv 10 | *.csv 11 | !demo*.csv 12 | -------------------------------------------------------------------------------- /app/icn/shift.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /misc/csv_files/demo_parser.csv: -------------------------------------------------------------------------------- 1 | A1,B1,C1 2 | A2, " B2 " ,C2 3 | A3, " B3 4 | B3 5 | B3" ,C3 6 | A""4, B"4,C4"" 7 | A5,B5,C5 8 | A6,B6,C6 9 | A7,B7,C7 10 | A8,B8,C8 11 | A9,B9,C9 12 | 13 | -------------------------------------------------------------------------------- /app/icn/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/validate_headers.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/on.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/fixTop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/sort.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/fixLeft.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/replace.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/insert.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/resize.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/img/platform/microsoft.svg: -------------------------------------------------------------------------------- 1 | Microsoft icon -------------------------------------------------------------------------------- /app/icn/menu/overview.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/trim.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/bin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/sort_reverse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/css/palettes/dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --blue: #8fbbf3; 3 | --orange: #fd971f; 4 | --green: #a6e22e; 5 | --red: #f92672; 6 | --purple: #ae81ff; 7 | --yellow: #ffd866; 8 | --white: #bbb; 9 | --grey: grey; 10 | --black: #000; 11 | } -------------------------------------------------------------------------------- /app/css/palettes/night.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --blue: #8fbbf3; 3 | --orange: #fd971f; 4 | --green: #a6e22e; 5 | --red: #f92672; 6 | --purple: #ae81ff; 7 | --yellow: #ffd866; 8 | --white: #bbb; 9 | --grey: grey; 10 | --black: #000; 11 | } -------------------------------------------------------------------------------- /app/icn/off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/img/platform/windows.svg: -------------------------------------------------------------------------------- 1 | Windows icon -------------------------------------------------------------------------------- /app/css/palettes/light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --blue: #005cc5; 3 | --orange: #e95f00; 4 | --green: #28a745; 5 | --red: #d73a49; 6 | --purple: #6f42c1; 7 | --yellow: #f1e05a; 8 | --white: #bbb; 9 | --grey: #6a737d; 10 | --black: #000; 11 | } -------------------------------------------------------------------------------- /app/icn/exit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/validate_data.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/new.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/save.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/decrement.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/undo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/img/nav/shield24.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/increment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/more.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/true.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/img/nav/cp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/fit_width.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/false.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/lock_open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/find.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/reloadFile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/about.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/shortcuts.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /misc/csv_files/demo_color_types.csv: -------------------------------------------------------------------------------- 1 | Numbers, Text,Not csv compliant, Date, Important , 2 | 3.14159, null,not csv, compliant, 2024-11-29, !cheers! , 3 | 1, true, 1,2 , 4 | +1, false, not \csv\ compliant , 5 | , 1e , 6 | 0, e2 , 7 | +0, inf , 8 | -0, +inf , 9 | 1e2, undefined , 10 | Infinity, NA , 11 | +Infinity, na , 12 | 0x765A, NaN , 13 | , - , 14 | , -- , 15 | , - - , 16 | , =2 , -------------------------------------------------------------------------------- /app/icn/menu/zoomOut.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/delta.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/zoomIn.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/date.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/logo/nanocell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/logo/nanocell - Copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/css/themes/dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --th-bg: #0d0d0d; 3 | --th-txt: #888888; 4 | --txt: #ccc; 5 | --body-bg: #272822; 6 | --slct-bg: #3b3b3b; 7 | --fh-bg: #1e1e1e; 8 | --fh-txt: #888888; 9 | --table-borders: #5a5a5a; 10 | --input-txt: #000; 11 | --input-bg: #aaa; 12 | --input-bg-out: #555; 13 | --num-txt: var(--orange); 14 | --filter: invert(40%); 15 | --dots: var(--orange); 16 | --scrollBar: #5a5a5a; 17 | --cmenuHover: #222; 18 | } -------------------------------------------------------------------------------- /app/css/themes/light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --th-bg: #ddd; 3 | --th-txt: #616161; 4 | --txt: #444; 5 | --body-bg: #fff; 6 | --slct-bg: #d4e3f1; 7 | --fh-bg: #e7e7e7; 8 | --fh-txt: #616161; 9 | --table-borders: #b1b1b1; 10 | --input-txt: #000; 11 | --input-bg: #edf6ff; 12 | --input-bg-out: #f5f5f5; 13 | --num-txt: var(--orange); 14 | --filter: invert(40%); 15 | --dots: var(--blue); 16 | --scrollBar: grey; 17 | --cmenuHover: #fafafa; 18 | } -------------------------------------------------------------------------------- /app/css/themes/night.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --th-bg: #0d0d0d; 3 | --th-txt: #888888; 4 | --txt: #ccc; 5 | --body-bg: #242424; 6 | --slct-bg: #191c1e; 7 | --fh-bg: #0d0d0d; 8 | --fh-txt: #888888; 9 | --table-borders: #5a5a5a; 10 | --input-txt: #ccc; 11 | --input-bg: #070707; 12 | --input-bg-out: #555; 13 | --num-txt: var(--orange); 14 | --filter: invert(40%); 15 | --dots: var(--orange); 16 | --scrollBar: #5a5a5a; 17 | --cmenuHover: #222; 18 | } -------------------------------------------------------------------------------- /app/icn/menu/integer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/decimal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/img/nav/valid24.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icn/menu/transpose.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/img/nav/fast24.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /misc/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {title} 7 | 8 | 9 | 10 | 11 | {tag_img_og} 12 | 13 | 14 | {tag_img_banner} 15 |
16 | {article_metadata} 17 | 18 | {html_body} 19 |
20 | {style} 21 | 22 | -------------------------------------------------------------------------------- /app/icn/menu/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/img/sup/stripe.svg: -------------------------------------------------------------------------------- 1 | Stripe icon -------------------------------------------------------------------------------- /web/img/nav/linkedin.svg: -------------------------------------------------------------------------------- 1 | LinkedIn icon -------------------------------------------------------------------------------- /app/logo/nc_smiley.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /misc/csv_files/demo_pipeline_config.csv: -------------------------------------------------------------------------------- 1 | pipe_id,client_name,file_name_regex,n_cols,data_quality_rating,,comment 2 | 001,Bank of utopia,/.*utopia.*transaction.*/,6,0.34,, 3 | ,,,,,, 4 | 005,Bank of utopia,/.*utopia.*customer.*/,5,.5,,"customer data from ""bank of utopia""" 5 | 006,Bank of utopia,/.*utopia.*accounts.*/,5,1.000,, 6 | 003,The people'sbank,/.*pplb.*client.*/,10,"0,5",,"Info about client 7 | (id, phone,age etc...)" 8 | 004,The people'sbank,/.*pplb.*accounts.*/,10,0.3,, 9 | 007,The people'sbank,/.*pplb.*transactions.*/,18,0.69,,transacttion data 10 | 008,,,,,, 11 | ,,,,,, 12 | 009,,,,,, 13 | 010,,,,,, 14 | 011,,,,,, 15 | 012,,,,,, 16 | 013,,,,,, 17 | 014,,,,,, 18 | 015,,,,,, 19 | 016,,,,,, -------------------------------------------------------------------------------- /web/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/js/ui/input/Scroller.js: -------------------------------------------------------------------------------- 1 | class Scroller extends HTMLElement { 2 | constructor(vertical = true) { 3 | super(); 4 | this.style.display = "block"; 5 | this.style.backgroundColor = "var(--scrollBar)"; 6 | this.style.borderRadius = ".4em"; 7 | this.style.opacity = ".5"; 8 | this.style.zIndex = "90"; 9 | this.style.position = "absolute"; 10 | if (vertical) { 11 | this.style.top = "0"; 12 | this.style.right = "0"; 13 | this.style.width = ".7em"; 14 | this.style.height = "4em"; 15 | } else { 16 | this.style.bottom = "0"; 17 | this.style.left = "0"; 18 | this.style.width = "4em"; 19 | this.style.height = ".7em"; 20 | 21 | } 22 | } 23 | } 24 | customElements.define('ui-scroller', Scroller); 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /update_version.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def get_file_lines (fpath): 4 | with open(fpath, 'r') as file: 5 | lines = file.readlines() 6 | return [line.strip() for line in lines] 7 | 8 | def increment_version(txt): 9 | match = re.search(r"(\d+)\.(\d+)\.(\d+)", txt) 10 | major, minor, patch = map(int, match.groups()) 11 | patch += 1 12 | new_version = f"{major}.{minor}.{patch}" 13 | updated_txt = re.sub(r"(\d+\.\d+\.\d+)", new_version, txt) 14 | print( "updating to : ", updated_txt.split('=')[1]) 15 | return updated_txt 16 | 17 | 18 | path = "app/sw_pwa_admin.js" 19 | 20 | lines = get_file_lines (path) 21 | lines[0] = increment_version(lines[0]) 22 | 23 | with open(path, 'w', encoding='utf-8') as file: 24 | file.write("\n".join(lines)) 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /web/img/sup/github.svg: -------------------------------------------------------------------------------- 1 | GitHub icon -------------------------------------------------------------------------------- /app/css/print.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | 3 | html, 4 | body, 5 | #main, 6 | #body, 7 | table { 8 | page-break-inside: auto; 9 | display: block; 10 | flex-flow: inherit; 11 | overflow: visible; 12 | flex: none; 13 | width: auto; 14 | } 15 | 16 | 17 | #tabs div:not(.activeTab) { 18 | display: none; 19 | 20 | } 21 | 22 | .activeTab { 23 | display: block; 24 | font-weight: bold; 25 | color: black; 26 | } 27 | 28 | 29 | footer, 30 | header { 31 | display: none; 32 | } 33 | 34 | table, 35 | td, 36 | th { 37 | border: 1px solid black; 38 | } 39 | 40 | table { 41 | border-collapse: collapse; 42 | break-inside: auto 43 | } 44 | 45 | tr { 46 | break-inside: avoid !important; 47 | break-after: always !important; 48 | 49 | } 50 | 51 | 52 | } -------------------------------------------------------------------------------- /app/js/ui/input/Table.js: -------------------------------------------------------------------------------- 1 | class Table extends HTMLTableElement { 2 | constructor() { 3 | super(); this.row = undefined; 4 | } 5 | 6 | br() { 7 | this.row = document.createElement("tr"); 8 | this.appendChild(this.row); 9 | } 10 | 11 | push(ele = "", eleClass = undefined) { 12 | 13 | var td = document.createElement("td") 14 | if (eleClass) td.classList.add(eleClass); 15 | if (Array.isArray(ele)) for (var e of ele) try { td.appendChild(e) } catch (err) { td.innerHTML = e } 16 | else try { td.appendChild(ele) } catch (err) { td.innerHTML = ele } 17 | if (this.row == undefined) this.br(); 18 | this.row.appendChild(td); 19 | } 20 | 21 | activeRow() { 22 | return this.row 23 | } 24 | pushRow(array) { 25 | this.br(); 26 | for (var a of array) this.push(a); 27 | } 28 | clear() { while (this.children.length > 0) this.children[0].remove(); } 29 | 30 | } 31 | 32 | customElements.define('ui-table', Table, { extends: 'table' }); 33 | -------------------------------------------------------------------------------- /app/js/ui/input/BoolInput.js: -------------------------------------------------------------------------------- 1 | class BoolInput extends HTMLElement { 2 | constructor(start = false) { 3 | super(); 4 | this.b = true; 5 | this.setAttribute('tabindex', '0'); 6 | this.value = start; 7 | this.style.cursor = "pointer"; 8 | this.style.display = "flex"; 9 | this.style.justifyContent = "center"; 10 | this.addEventListener("click", e => { this.focus(); this.toggle() }); 11 | this.addEventListener("keydown", e => { 12 | var k = e.key.toUpperCase(); 13 | if (k.includes("ARROW") || k === "ENTER") this.toggle() 14 | }); 15 | } 16 | 17 | toggle() { this.value = !this.value } 18 | 19 | get value() { return this.b } 20 | set value(b) { 21 | this.b = b; 22 | this.innerHTML = b ? "🗸" : "🗙"; 23 | var e = new Event("change") 24 | Object.defineProperty(e, 'target', { writable: false, value: this }); 25 | if (this.onchange) this.onchange(e); 26 | } 27 | 28 | } 29 | 30 | customElements.define('ui-bool', BoolInput); 31 | -------------------------------------------------------------------------------- /web/img/sup/paypal.svg: -------------------------------------------------------------------------------- 1 | PayPal icon -------------------------------------------------------------------------------- /app/js/ui/input/TCell.js: -------------------------------------------------------------------------------- 1 | class TCell extends HTMLTableCellElement { 2 | constructor() { 3 | super(); 4 | this.tx; 5 | this.ty; 6 | this.th; 7 | this.addEventListener("pointerenter", e => { 8 | if (LBT == TargetType.cell) { 9 | sheet.rangeEnd = { x: this.tx + sheet.baseX, y: this.ty + sheet.baseY }; 10 | sheet.slctRefresh(false); 11 | } 12 | if (LBT == TargetType.colH && this.tx >= 0) 13 | sheet.slctCol(sheet.x, this.tx + sheet.baseX); 14 | if (LBT == TargetType.rowH && this.ty >= 0) 15 | sheet.slctRow(sheet.y, this.ty + sheet.baseY); 16 | 17 | }); 18 | 19 | this.addEventListener("dblclick", e => { 20 | if (this.tx >= 0 && this.ty >= 0) sheet.input(); 21 | if (this.ty < 0) this.style.width = "auto" 22 | }); 23 | } 24 | 25 | setPosition(x, y) { 26 | this.tx = x; 27 | this.ty = y; 28 | this.th = (x < 0 || y < 0) 29 | if (this.th) this.classList.add("tHeader") 30 | if (y < 0) this.classList.add("tColHeader") 31 | if (x < 0) this.classList.add("tRowHeader") 32 | 33 | 34 | } 35 | } 36 | 37 | customElements.define("ui-cell", TCell, { extends: "td" }); -------------------------------------------------------------------------------- /app/js/Shortcuts.js: -------------------------------------------------------------------------------- 1 | class Shortcuts extends HTMLElement { 2 | constructor() { 3 | super(); 4 | var title = document.createElement("h1") 5 | title.innerHTML = "Keyboard Shortcuts"; 6 | this.appendChild(title); 7 | this.table = new Table(); 8 | this.build(); 9 | this.appendChild(this.table) 10 | dom.dialog.push(this, true); 11 | this.style.textAlign = "left"; 12 | this.style.margin = "2em"; 13 | this.table.style.borderSpacing = "1em 0"; 14 | } 15 | 16 | build() { 17 | for (var c of Object.values(cmd)) { 18 | var k = c.k; 19 | k = k.replace("ENTER", "⮐").replace("BACKSPACE", "⌫").replace("TAB", "⭲").replace("SPACE", "␣") 20 | k = k.replace("ARROWUP", '↑').replace("ARROWRIGHT", '→').replace("ARROWDOWN", '↓').replace("ARROWLEFT", '←') 21 | 22 | this.table.br(); 23 | this.table.push((c.ctrl) ? 'Ctrl' : ''); 24 | this.table.push((c.alt) ? 'Alt' : ''); 25 | this.table.push((c.shift) ? '⇧' : ''); 26 | this.table.push(k); 27 | this.table.push(c.description); 28 | } 29 | } 30 | } 31 | 32 | customElements.define('ui-shortcuts', Shortcuts); 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/icn/ratio.svg: -------------------------------------------------------------------------------- 1 | discountCreated with Sketch. -------------------------------------------------------------------------------- /web/img/platform/apple.svg: -------------------------------------------------------------------------------- 1 | Apple icon -------------------------------------------------------------------------------- /app/sw_pwa_admin.js: -------------------------------------------------------------------------------- 1 | const versionName = 'Beta_v0.0.21'; 2 | const filesToCache = [ 3 | // auto input 4 | ]; 5 | 6 | 7 | self.addEventListener('install', e => { 8 | e.waitUntil(caches.open(versionName).then(cache => { return cache.addAll(filesToCache) })) 9 | // log_to_db("install"); 10 | }); 11 | 12 | self.addEventListener('activate', e => { 13 | console.log('Service worker activating...'); 14 | // e.waitUntil(self.registration?.navigationPreload.enable()); 15 | // e.waitUntil(clients.claim()); 16 | e.waitUntil(caches.open(versionName).then(cache => { return cache.addAll(filesToCache) })) 17 | 18 | e.waitUntil(deleteOldCaches()); 19 | console.log('Service worker activation done'); 20 | }); 21 | 22 | 23 | self.skipWaiting(); 24 | 25 | 26 | 27 | self.addEventListener('fetch', event => { 28 | event.respondWith( 29 | caches.match(event.request) 30 | .then(response => { return response || fetch(event.request) }) 31 | .catch(error => { console.error("Couldn't fetch: " + event.request.url, error) }) 32 | ); 33 | }); 34 | 35 | const deleteCache = async (key) => { 36 | await caches.delete(key); 37 | }; 38 | 39 | const deleteOldCaches = async () => { 40 | const cacheKeepList = [versionName]; 41 | const keyList = await caches.keys(); 42 | const cachesToDelete = keyList.filter((key) => !cacheKeepList.includes(key)); 43 | await Promise.all(cachesToDelete.map(deleteCache)); 44 | }; -------------------------------------------------------------------------------- /misc/article.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, sans-serif; 3 | line-height: 1.5; 4 | margin: 0; 5 | display: flex; 6 | justify-content: center; 7 | flex-direction: column; 8 | align-items: center; 9 | } 10 | 11 | p { 12 | text-align: justify; 13 | } 14 | 15 | section { 16 | margin: 0 1em; 17 | margin-bottom: 25vh; 18 | max-width: 40em; 19 | } 20 | 21 | #banner { 22 | width: 100%; 23 | max-width: 40em !important; 24 | margin: 0; 25 | } 26 | 27 | img { 28 | max-width: 100%; 29 | display: block; 30 | margin: 2em auto; 31 | } 32 | 33 | li p { 34 | margin: 0; 35 | } 36 | 37 | h1 { 38 | font-size: 2em; 39 | margin: 1em 0; 40 | } 41 | 42 | h2 { 43 | margin-top: 3em; 44 | border-bottom: 1px solid black; 45 | } 46 | 47 | h3 { 48 | font-size: inherit; 49 | } 50 | 51 | code, 52 | em { 53 | background-color: #eeeeee; 54 | padding: 3px .4em; 55 | border-radius: .2em; 56 | margin: .2em; 57 | } 58 | 59 | code { 60 | background: black; 61 | color: #ffffff; 62 | } 63 | 64 | pre, 65 | blockquote { 66 | padding: .5em 1em; 67 | } 68 | 69 | blockquote { 70 | background-color: #eeeeee; 71 | margin: 0; 72 | border-left: .5em solid green; 73 | } 74 | 75 | pre { 76 | background: black; 77 | color: #ffffff; 78 | line-height: 1em; 79 | } 80 | 81 | pre code { 82 | padding: 0; 83 | background-color: unset; 84 | } 85 | 86 | #article_metadata { 87 | font-style: italic; 88 | font-size: .8em; 89 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Start HTTP Server", 6 | "type": "shell", 7 | "command": "python", 8 | "args": [ 9 | "-m", 10 | "http.server", 11 | "8000", 12 | "--directory", 13 | "public" 14 | ], 15 | "isBackground": true, 16 | "problemMatcher": [] 17 | }, 18 | { 19 | "label": "Nanocell : Run", 20 | "type": "shell", 21 | "command": "start", 22 | "args": [ 23 | "chrome", 24 | "http://localhost:8000/app/home.html" 25 | ], 26 | "problemMatcher": [], 27 | "detail": "Remember to close all nanocell windows and tabs for changes to take effect" 28 | }, 29 | { 30 | "label": "Nanocell : Build ", 31 | "type": "shell", 32 | "command": "python", 33 | "args": [ 34 | "./build.py" 35 | ], 36 | "problemMatcher": [] 37 | }, 38 | { 39 | "label": "Nanocell : increment Version ", 40 | "type": "shell", 41 | "command": "python", 42 | "args": [ 43 | "./update_version.py" 44 | ], 45 | "problemMatcher": [] 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /app/js/Msg.js: -------------------------------------------------------------------------------- 1 | class Msg extends HTMLElement { 2 | constructor(txt = "Empty message", opt = {}) { 3 | super(); 4 | this.content = document.createElement("div"); 5 | this.ok = document.createElement("button"); 6 | this.cancel = document.createElement("button"); 7 | this.content.innerHTML = txt; 8 | this.ok.innerHTML = "Ok"; 9 | this.cancel.innerHTML = "Cancel"; 10 | this.appendChild(this.content); 11 | if (opt.id === 3) this.appendChild(this.cancel); 12 | if (!opt.t) this.appendChild(this.ok); 13 | dom.dialog.push(this); 14 | this.ok.onclick = () => { if (opt.cbt) opt.cbt(); dom.dialog.clear() } 15 | this.cancel.onclick = () => { if (opt.cbf) opt.cbf(); dom.dialog.clear() } 16 | this.ok.focus(); 17 | this.ok.addEventListener('keydown', (e) => { 18 | var k = e.key.toUpperCase(); 19 | if (k === "TAB") this.cancel.focus(); 20 | if (k === "ARROWLEFT") this.cancel.focus(); 21 | }); 22 | this.cancel.addEventListener('keydown', (e) => { 23 | var k = e.key.toUpperCase(); 24 | if (k === "TAB") this.ok.focus(); 25 | if (k === "ARROWRIGHT") this.ok.focus(); 26 | }); 27 | if (opt.t) setTimeout(() => { dom.dialog.clear() }, opt.t); 28 | } 29 | static quick(txt) { new Msg(txt, { id: 0, t: 1000 }) } 30 | static long(txt) { new Msg(txt, { id: 1, t: 3000 }) } 31 | static confirm(txt) { new Msg(txt, { id: 2 }) } 32 | static choice(txt, cbTrue, cbFalse) { new Msg(txt, { id: 3, cbt: cbTrue, cbf: cbFalse }) } 33 | } 34 | 35 | customElements.define('ui-msg', Msg); 36 | -------------------------------------------------------------------------------- /app/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | csv 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 33 | 34 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/js/ui/input/NumInput.js: -------------------------------------------------------------------------------- 1 | class NumInput extends HTMLElement { 2 | constructor(start = 0, min = 0, max = 999) { 3 | super(); 4 | this.n = start 5 | this.min = min; 6 | this.max = max; 7 | this.left = document.createElement("span") 8 | this.center = document.createElement("span") 9 | this.right = document.createElement("span") 10 | this.left.innerHTML = "-"; 11 | this.center.innerHTML = this.n; 12 | this.right.innerHTML = "+" 13 | this.appendChild(this.left) 14 | this.appendChild(this.center) 15 | this.appendChild(this.right) 16 | this.left.classList.add("slctLeft") 17 | this.right.classList.add("slctRight") 18 | this.style.display = "flex" 19 | this.center.style.flexGrow = "2" 20 | this.left.addEventListener("click", e => { this.value = this.value - 1 }) 21 | this.right.addEventListener("click", e => { this.value = this.value + 1 }) 22 | this.setAttribute('tabindex', '0'); 23 | this.addEventListener("click", e => { this.focus() }) 24 | this.addEventListener("keydown", e => { 25 | var k = e.key.toUpperCase(); 26 | if (k === "ARROWRIGHT" || k === "ARROWUP") { this.value = this.value + 1 } 27 | else if (k === "ARROWLEFT" || k === "ARROWDOWN") { this.value = this.value - 1 } 28 | }) 29 | } 30 | get value() { return this.n } 31 | set value(n) { 32 | n = Number(n); 33 | if (n < this.min) n = this.min; 34 | if (n > this.max) n = this.max; 35 | this.n = n; 36 | this.center.innerHTML = this.n; 37 | var e = new Event("change") 38 | Object.defineProperty(e, 'target', { writable: false, value: this }); 39 | if (this.onchange) this.onchange(e); 40 | } 41 | } 42 | customElements.define('ui-num', NumInput); 43 | 44 | -------------------------------------------------------------------------------- /app/css/Inputs.css: -------------------------------------------------------------------------------- 1 | ::selection { 2 | background-color: #809ecb; 3 | color: black; 4 | } 5 | 6 | :focus { 7 | opacity: 1; 8 | outline: 0; 9 | color: var(--orange); 10 | } 11 | 12 | input { 13 | background-color: var(--input-bg-out); 14 | color: var(--input-txt); 15 | border: 0; 16 | padding: 0; 17 | text-align: inherit; 18 | font-family: inherit; 19 | border-radius: 1em; 20 | height: 1.2em; 21 | margin: .3em; 22 | font-size: inherit; 23 | } 24 | 25 | input:focus { 26 | color: var(--input-txt); 27 | background-color: var(--input-bg); 28 | 29 | } 30 | 31 | button { 32 | cursor: pointer; 33 | background-color: inherit; 34 | border-radius: .7em; 35 | font-size: inherit; 36 | font-family: inherit; 37 | margin: 0 .5em 0 .5em; 38 | padding: 0.2em 2em; 39 | border: none; 40 | box-shadow: inset 0 0 2px; 41 | opacity: .7; 42 | color: inherit; 43 | margin: .3em; 44 | 45 | } 46 | 47 | button:focus { 48 | box-shadow: inset 0 0 3px; 49 | } 50 | 51 | .slctLeft, 52 | .slctRight { 53 | width: 1em; 54 | cursor: pointer; 55 | box-shadow: 0 0 2px var(--fh-txt); 56 | margin: 0 2em; 57 | } 58 | 59 | 60 | 61 | ui-list span { 62 | padding: 0 .5em 0 .5em; 63 | } 64 | 65 | ui-list:focus [selected="true"] { 66 | color: var(--orange) 67 | } 68 | 69 | ui-list[hide="true"] [selected="false"] { 70 | display: none 71 | } 72 | 73 | ui-msg { 74 | width: 100%; 75 | padding: 1em; 76 | } 77 | 78 | ui-calendar table:focus { 79 | color: inherit; 80 | } 81 | 82 | ui-calendar td div { 83 | min-width: 3em; 84 | cursor: pointer; 85 | } 86 | 87 | ui-calendar .picked { 88 | border-radius: 1em; 89 | box-shadow: inset 0 0 2px; 90 | } 91 | 92 | ui-calendar table:focus .picked { 93 | color: var(--orange) 94 | } -------------------------------------------------------------------------------- /app/js/utils/misc.js: -------------------------------------------------------------------------------- 1 | 2 | function signOf(value) { if (value >= 0) return 1; return -1; } 3 | 4 | function isAlphanumeric(char) { 5 | return /^[a-zA-Z0-9_]$/.test(char); 6 | } 7 | 8 | function Timer(name) { 9 | this.init = new Date().getTime(); 10 | this.name = name; 11 | } 12 | // Timer.prototype.log = function (){ 13 | // var time = new Date().getTime()-this.init; 14 | // console.log(this.name+" time: "+ String(time) + " ms"); 15 | // } 16 | 17 | Node.prototype.empty = function () { while (this.firstChild) { this.removeChild(this.firstChild); } }; 18 | Node.prototype.previous = function () { if (this.previousSibling) return this.previousSibling; else return this.parentNode.lastChild; } 19 | Node.prototype.next = function () { if (this.nextSibling) return this.nextSibling; else return this.parentNode.firstChild } 20 | Node.prototype.position = function () { var e = this; var i = 0; while ((e = e.previousSibling) !== null) ++i; return i; } 21 | Node.prototype.addSpan = function (data, c) { 22 | var s = document.createElement("span"); 23 | s.innerHTML = data; 24 | if (c) s.classList.add(c); 25 | this.appendChild(s); 26 | } 27 | 28 | function rndStr(n = 2) { 29 | var r = ''; 30 | var abc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 31 | var len = abc.length; 32 | for (var i = 0; i < n; i++) r += abc.charAt(Math.floor(Math.random() * len)); 33 | return r; 34 | } 35 | 36 | function isValidUrl(txt) { 37 | if (typeof txt !== "string") return false; 38 | let url = txt.toLowerCase(); 39 | return url.startsWith("https://")||url.startsWith("http://") 40 | } 41 | 42 | 43 | round = function (n, integer = true) { 44 | if (isNaN(n) || n === '') return n; 45 | n = Number(n); 46 | if (!integer) n *= 100; 47 | n = Math.round(n + Number.EPSILON); 48 | if (!integer) { 49 | n /= 100; 50 | n += 0.001; 51 | n = Math.round(n * 1000) / 1000; 52 | n = String(n).slice(0, -1); 53 | } 54 | return n; 55 | } 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/js/main.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('beforeunload', function (e) { if (!sheet.df.isSaved) e.preventDefault() }); 2 | 3 | let sampleData = [ 4 | ["Hello World", ""] 5 | // , "a", "a", "a_éC", "dsiuh IUZASH", "siudch", "a", "", "b"], 6 | // ["Hello World", "", "a", "a", "a_éC", "dsiuh IUZASH", "siudch", "a", "", "b"], 7 | // ["Hello World", "" , "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], 8 | 9 | // [ "Numbers", "Text","Not csv compliant", "Date", "Important"] , 10 | // [ "3.14159", "null","not csv, compliant", "2024-11-29", "!cheers!"] , 11 | // [ "1", "true", "1,2"] , 12 | // [ "+1", "false", "not \"csv\" compliant"] , 13 | // [ "", "1e"] , 14 | // [ "0", "e2"] , 15 | // [ "+0", "inf"] , 16 | // [ "-0", "+inf"] , 17 | // [ "1e2", "undefined"] , 18 | // [ "Infinity", "NA"] , 19 | // [ "+Infinity", "na"] , 20 | // [ "0x765A", "NaN"] , 21 | // [ "", "-"] , 22 | // [ "", "--"] , 23 | // [ "", "- -"] , 24 | // [ "", "=2"] , 25 | 26 | ] 27 | 28 | // console.log (navigator.userAgent) 29 | let isOSX = navigator.userAgent.includes('Macintosh') 30 | 31 | // localStorage.clear(); 32 | let is_installed = window.matchMedia('(display-mode: standalone)').matches 33 | let sheet = undefined; 34 | let overview = undefined; 35 | let csvHandle = new CsvHandle() 36 | 37 | launchFileOnInitDone = function () { 38 | window.launchQueue.setConsumer(async (params) => { 39 | console.log(params) 40 | const [handle] = params.files; 41 | if (handle) csvHandle.launchFile(handle) 42 | }); 43 | 44 | window.addEventListener('message', (event) => { 45 | // console.log(event) 46 | if (event.data && event.data.fileHandle) csvHandle.launchFile(event.data.fileHandle) 47 | }); 48 | } 49 | 50 | nanocell_cleanStart = function () { 51 | Setting.log() 52 | Setting.init(); 53 | build_dom() 54 | buildCommands(); 55 | buildMenu(); 56 | buildKeys(); 57 | sheet = new Sheet(new Dataframe(sampleData)); 58 | Setting.runAll(); 59 | launchFileOnInitDone(); 60 | } -------------------------------------------------------------------------------- /app/logo/nanocell_logo_builder.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 40 | 45 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/js/dom.js: -------------------------------------------------------------------------------- 1 | var dom = undefined; 2 | 3 | build_dom = function () { 4 | dom = { 5 | palette: document.getElementById("palette"), 6 | theme: document.getElementById("theme"), 7 | header: document.getElementById("header"), 8 | body: document.getElementById("body"), 9 | content: document.getElementById("content"), 10 | dialog: document.getElementById("dialog"), 11 | footer: document.getElementById("footer"), 12 | cmenu: new CMenu(), 13 | footerDiv: { 14 | left: document.getElementById("footerLeft"), 15 | center: document.getElementById("footerCenter"), 16 | right: document.getElementById("footerRight"), 17 | lock: document.getElementById("lock"), 18 | }, 19 | 20 | } 21 | 22 | 23 | dom.dialog.clear = function (e) { while (this.children.length > 0) this.children[0].remove(); dom.dialog.className = ''; sheet.scrollbarRefresh(); } 24 | dom.dialog.push = function (e, fullscreen = false, closeButton = true) { 25 | this.clear(); 26 | if (fullscreen) dom.dialog.classList.add("dialog_large"); 27 | else dom.dialog.classList.add("dialog_small"); 28 | dom.dialog.classList.add("scroll"); 29 | dom.dialog.appendChild(e); 30 | 31 | if (closeButton) { 32 | var img = document.createElement("img"); 33 | img.src = "icn/off.svg"; 34 | img.style.position = (fullscreen) ? "fixed" : "absolute"; 35 | img.setAttribute("title", "close"); 36 | img.setAttribute("id", "closeDialog"); 37 | img.addEventListener("click", function () { dom.dialog.clear() }) 38 | img.style.cursor = "pointer" 39 | if (!fullscreen) { 40 | img.style.height = "1.3em"; 41 | img.style.marginTop = ".5em"; 42 | } 43 | dom.dialog.appendChild(img) 44 | } 45 | } 46 | Object.defineProperty(dom.dialog, 'isBusy', { get: function () { return dom.dialog.children.length > 0 } }); 47 | Object.defineProperty(dom.dialog, 'isLarge', { get: function () { return dom.dialog.classList.contains("dialog_large") } }); 48 | 49 | dom.content.scrollerY = new Scroller(); 50 | dom.content.scrollerX = new Scroller(false); 51 | dom.body.appendChild(dom.cmenu); 52 | return dom; 53 | 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /web/manifest.JSON: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nanocell-csv", 3 | "short_name": "Nanocell-csv", 4 | "theme_color": "#e7e7e7", 5 | "background_color": "#e7e7e7", 6 | "orientation": "portrait", 7 | "start_url": "/app/home.html", 8 | "scope": "/app/", 9 | "id": "/app/", 10 | "prefer_related_applications": false, 11 | "display": "standalone", 12 | "description": "Nanocell - CSV file viewer & editor : free, fast, simple, lightweight, offline, cross platform, data accurate, PWA (Progressive Web App)", 13 | "lang": "en", 14 | "dir": "ltr", 15 | "categories": [ 16 | "productivity", 17 | "utilities", 18 | "business" 19 | ], 20 | "launch_handler": { 21 | "client_mode": "auto" 22 | }, 23 | "file_handlers": [ 24 | { 25 | "action": "/app/home.html", 26 | "accept": { 27 | "text/csv": [ 28 | ".csv", 29 | ".tsv" 30 | ] 31 | } 32 | } 33 | ], 34 | "scope_extensions": [ 35 | { 36 | "origin": "https://www.nanocell-csv.com/" 37 | }, 38 | { 39 | "origin": "https://nanocell-csv.com/" 40 | }, 41 | { 42 | "origin": "https://nanocell-csv.vercel.app/" 43 | } 44 | ], 45 | "icons": [ 46 | { 47 | "src": "favicon.svg", 48 | "sizes": "any", 49 | "type": "image/svg+xml", 50 | "purpose": "any" 51 | }, 52 | { 53 | "src": "favicon.svg", 54 | "sizes": "512x512", 55 | "type": "image/svg+xml" 56 | }, 57 | { 58 | "src": "favicon-96x96.png", 59 | "sizes": "96x96", 60 | "type": "image/png" 61 | }, 62 | { 63 | "src": "favicon-192x192.png", 64 | "sizes": "192x192", 65 | "type": "image/png" 66 | }, 67 | { 68 | "src": "favicon-512x512.png", 69 | "sizes": "512x512", 70 | "type": "image/png" 71 | } 72 | ], 73 | "screenshots": [ 74 | { 75 | "src": "web/img/screenshot_light.png", 76 | "sizes": "1080x1920", 77 | "type": "image/png", 78 | "label": "App spreadsheet in light mode" 79 | }, 80 | { 81 | "src": "web/img/screenshot_dark.png", 82 | "sizes": "1080x1920", 83 | "type": "image/png", 84 | "label": "App spreadsheet in dark mode" 85 | } 86 | ] 87 | } -------------------------------------------------------------------------------- /app/js/ui/input/ListInput.js: -------------------------------------------------------------------------------- 1 | class ListInput extends HTMLElement { 2 | constructor(list, hide = false) { 3 | super(); 4 | this.list = list; 5 | this.idx = 0; 6 | this.setAttribute('tabindex', 0); 7 | this.style.display = "flex" 8 | 9 | this.left = document.createElement("div") 10 | this.center = document.createElement("div") 11 | this.right = document.createElement("div") 12 | this.left.innerHTML = "<"; 13 | this.right.innerHTML = ">"; 14 | this.appendChild(this.left) 15 | this.appendChild(this.center) 16 | this.appendChild(this.right) 17 | this.left.classList.add("slctLeft") 18 | this.right.classList.add("slctRight") 19 | this.center.style.flexGrow = "2"; 20 | this.left.addEventListener("click", e => { this.prev() }) 21 | this.right.addEventListener("click", e => { this.next() }) 22 | this.setAttribute('hide', hide); 23 | this.addEventListener("click", e => { this.focus() }) 24 | this.addEventListener("keydown", e => { 25 | var k = e.key.toUpperCase(); 26 | if (k === "ARROWRIGHT" || k === "ARROWDOWN") { this.next() } 27 | else if (k === "ARROWLEFT" || k === "ARROWUP") { this.prev() } 28 | }) 29 | for (var ele of list) { 30 | var td = document.createElement("span"); 31 | td.innerHTML = ele; 32 | td.addEventListener("click", e => { this.value = e.target.innerHTML }); 33 | this.center.appendChild(td); 34 | } 35 | 36 | } 37 | 38 | next() { this.idx = (this.idx + 1) % this.list.length; this.value = this.list[this.idx] } 39 | prev() { this.idx = (this.idx + this.list.length - 1) % this.list.length; this.value = this.list[this.idx] } 40 | 41 | get value() { return this.center.children[this.idx].innerHTML } 42 | set value(txt) { 43 | for (var i = 0; i < this.list.length; i++) { 44 | if (this.list[i] === txt) { 45 | this.idx = i; 46 | for (var child of this.center.children) child.setAttribute('selected', "false"); 47 | this.center.children[i].setAttribute('selected', "true"); 48 | var e = new Event("change") 49 | Object.defineProperty(e, 'target', { writable: false, value: this }); 50 | if (this.onchange) this.onchange(e); 51 | return; 52 | } 53 | } 54 | } 55 | } 56 | 57 | customElements.define('ui-list', ListInput); 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Nanocell - CSV 3 | 4 | A free csv file viewer and editor. 5 | 6 | App download availabale on the website : [https://nanocell-csv.com](https://nanocell-csv.com) 7 | 8 | - fast 9 | - simple 10 | - lightweight 11 | - cross platform 12 | - PWA (Progressive Web App) 13 | - quick-view large files 14 | - works 100% offline 15 | - custommizable 16 | - free 17 | 18 | 19 | 20 | ![screenshot](web/img/screenshot/screenshot_light.webp) 21 | 22 | 23 | 24 | ## Built for speed and simplicity 25 | Nanocell-csv lets you edit and visualize CSV files instantly, from massive datasets to small configuration tables. It guarantees your data stays safe and accurate by avoiding to interpret data types. Designed by and for data experts, Nanocell-csv delivers precision and performance you can trust. 26 | 27 | Nanocell-csv aims to be the go-to CSV editing tool for software engineers and data experts worldwide. 28 | 29 | 30 | 31 | ## Key features 32 | 33 | 34 | **Data privacy** - Nanocell-csv works 100% off-line, your data is never leaving your computer. Nanocell-csv.com runs on a static server which, by design, only sends data on request but cannot register any data. This is very easy for the anyone to validate as the source code is available [here](https://github.com/CedricBonjour/nanocell-csv). 35 | 36 | **Data accuracy** - CSV data is text and Nanocell-csv makes sure values are being handled as such. Leading zeros and '+' signs are kept. No more data corruption of phone numbers, zipcodes, etc. Pasting data also finally works as you would expect, no more paste reformatting or column split action to perform ! 37 | 38 | **Instant view large files** - O(1) 😉. This is achieved by sampling the header, the footer and a few rows at regular intervals without parsing the entire file. The goal here is for data experts to quickly understand what they are dealing with when first opening a file. That is before they start using heavier Big-Data tools like pandas, pyspark, powerBI, R etc... 39 | 40 | 41 | 42 | ## Contribute 43 | 44 | **Grow the community** - Star the github repo and talk about Nanocell-csv to people around you! Link it on relevant reddit posts or show how useful its been to you on social media! 45 | 46 | **Give feedback: missing features & bugs** - Nanocell-csv still has a bit to go before being stable and mature. Help us get there faster by reporting on the github issue tracker. 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/js/About.js: -------------------------------------------------------------------------------- 1 | class About extends HTMLElement { 2 | constructor() { 3 | super(); 4 | var title = document.createElement("h1") 5 | var version = document.createElement("h3") 6 | var logo = document.createElement("img") 7 | var homeLink = document.createElement("a") 8 | var bugLink = document.createElement("a") 9 | var buttonBugReport = document.createElement("button") 10 | var aboutFooter = document.createElement("div") 11 | title.innerHTML = "Nanocell CSV Editor"; 12 | buttonBugReport.innerHTML = "Bug Report" 13 | logo.src = "./logo/nanocell.svg" 14 | homeLink.href = "https://nanocell-csv.com/" 15 | homeLink.innerHTML = "https://nanocell-csv.com/" 16 | homeLink.target = "_blank" 17 | bugLink.href = "https://github.com/CedricBonjour/nanocell-csv/issues/new" 18 | bugLink.target = "_blank" 19 | this.style.display = "flex" 20 | this.style.flexDirection = "column" 21 | this.style.height = "100vh" 22 | this.style.justifyContent = "center" 23 | this.style.alignItems = "center" 24 | logo.style.filter = "none"; 25 | logo.style.height = "auto"; 26 | logo.style.width = "10em"; 27 | logo.style.borderRadius = "0"; 28 | aboutFooter.style.position = "absolute" 29 | aboutFooter.style.bottom = "3em" 30 | aboutFooter.style.left = "0" 31 | aboutFooter.style.width = "100%" 32 | aboutFooter.style.display = "flex" 33 | aboutFooter.style.flexDirection = "column" 34 | aboutFooter.style.height = "7vh" 35 | aboutFooter.style.justifyContent = "space-between" 36 | 37 | homeLink.style.textDecoration = "none" 38 | homeLink.style.color = "royalblue" 39 | buttonBugReport.style.color = "royalblue" 40 | buttonBugReport.style.opacity = 1 41 | buttonBugReport.style.setProperty("box-shadow", "none", "important"); 42 | this.getVersion(e => { version.innerHTML = e }); 43 | this.appendChild(logo) 44 | this.appendChild(title); 45 | this.appendChild(version) 46 | bugLink.appendChild(buttonBugReport) 47 | aboutFooter.appendChild(bugLink) 48 | aboutFooter.appendChild(homeLink) 49 | this.appendChild(aboutFooter) 50 | dom.dialog.push(this, true); 51 | } 52 | getVersion(cb) { caches.keys().then(cache => { cb(cache.join('
')) }).catch(() => { cb("version error") }) } 53 | } 54 | customElements.define('ui-about', About); 55 | 56 | // position: absolute; 57 | // bottom: 3em; 58 | // left: 0px; 59 | // width: 100%; 60 | // flex-direction: column; 61 | // display: flex 62 | // ; 63 | // height: 7vh; 64 | // align-content: space-between; 65 | // justify-content: space-between; -------------------------------------------------------------------------------- /misc/automate.js: -------------------------------------------------------------------------------- 1 | function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } 2 | function qid(id) { return document.querySelector(id).click(); } 3 | 4 | var time_start = Date.now(); // Point A 5 | 6 | 7 | 8 | csvHandle.reloadFile(true); 9 | await sleep(200); 10 | stg.theme = "light"; 11 | sheet.fixTop = false; 12 | sheet.x = 0; 13 | sheet.y = 0; 14 | stg.rows = 15; 15 | stg.cols = 10; 16 | sheet.nViewCols = stg.cols; 17 | sheet.nViewRows = stg.rows; 18 | sheet.colWidthList = []; 19 | sheet.reload(); 20 | sheet.slctRefresh(); 21 | 22 | 23 | await sleep(2000); 24 | for (i = 0; i < 10; i++) { sheet.nViewRows++; sheet.reload(); await sleep(7); } 25 | await sleep(260); 26 | for (i = 0; i < 6; i++) { sheet.nViewRows++; sheet.reload(); await sleep(7); } 27 | await sleep(260); 28 | sheet.fixTop = true; 29 | sheet.reload(); 30 | await sleep(440); 31 | 32 | await sleep(440); 33 | sheet.fitWidth(); 34 | await sleep(440); 35 | 36 | 37 | for (i = 0; i < 20; i++) { sheet.baseY ++;sheet.slctRefresh(false); await sleep(7); } 38 | await sleep(260); 39 | for (i = 0; i < 30; i++) { sheet.baseY --;sheet.slctRefresh(false); await sleep(7); } 40 | 41 | 42 | sheet.x = 0; 43 | sheet.y = 0; 44 | sheet.refresh(); 45 | await sleep(260); 46 | sheet.insert(3); //left 47 | await sleep(600); 48 | sheet.x = 0; 49 | sheet.y = 0; 50 | sheet.slctRefresh(); 51 | await sleep(260); 52 | sheet.input("i"); 53 | await sleep(200); 54 | sheet.inputField.value += "d"; 55 | await sleep(200); 56 | sheet.inputField.blur(); 57 | sheet.y++; 58 | sheet.slctRefresh(); 59 | sheet.refresh(); 60 | await sleep(440); 61 | 62 | sheet.slctRange = true; 63 | sheet.y = sheet.df.height-1; 64 | sheet.slctRange = false; 65 | sheet.slctRefresh(true); 66 | await sleep(440); 67 | for (i = 0; i < 7; i++) { sheet.baseY ++;sheet.slctRefresh(false); await sleep(7); } 68 | await sleep(260); 69 | sheet.expand(); 70 | sheet.refresh(); 71 | await sleep(440); 72 | sheet.y=0; 73 | sheet.slctRefresh(true); 74 | await sleep(440); 75 | for (i = 0; i < 3; i++) { sheet.x++;sheet.slctRefresh(true); await sleep(40); } 76 | await sleep(440); 77 | sheet.sort(sheet.x, true); 78 | await sleep(600); 79 | for (i = 0; i < 3; i++) { sheet.x++;sheet.slctRefresh(true); await sleep(40); } 80 | await sleep(440); 81 | sheet.slctCol(sheet.x); 82 | await sleep(440); 83 | cmd.integer.run(); 84 | await sleep(440); 85 | cmd.decimal.run(); 86 | await sleep(440); 87 | for (i = 0; i < 10; i++) { sheet.baseY ++;sheet.slctRefresh(false); await sleep(3); } 88 | await sleep(440); 89 | 90 | qid('img[title="about"]'); 91 | await sleep(3000); 92 | qid('#closeDialog'); 93 | 94 | 95 | 96 | var time_end = Date.now(); // Point B 97 | console.log(`Time elapsed: ${time_end - time_start} ms`); 98 | 99 | -------------------------------------------------------------------------------- /app/css/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "inconsolata"; 3 | src: url("Inconsolata-Regular.ttf"); 4 | } 5 | 6 | hr { 7 | visibility: hidden 8 | } 9 | 10 | b { 11 | color: var(--red); 12 | } 13 | 14 | footer { 15 | padding: .2em; 16 | text-align: center; 17 | flex-wrap: wrap; 18 | } 19 | 20 | .flexMain { 21 | flex-grow: 1; 22 | min-width: 3em; 23 | } 24 | 25 | .flexCol { 26 | display: flex; 27 | flex-flow: column; 28 | font-size: inherit; 29 | } 30 | 31 | .activeTab { 32 | font-weight: bold 33 | } 34 | 35 | #footerCenter { 36 | margin: 0 3em 0 3em; 37 | line-height: 1em; 38 | display: block; 39 | } 40 | 41 | #dragSpace { 42 | min-width: 1em; 43 | -webkit-app-region: drag; 44 | flex-grow: 1; 45 | height: 100%; 46 | } 47 | 48 | #menu { 49 | flex-wrap: wrap; 50 | margin: .5em; 51 | } 52 | 53 | header img:hover, 54 | #menu img:hover { 55 | background-color: #ccc 56 | } 57 | 58 | .dialog_small { 59 | line-height: normal; 60 | max-height: 50vh; 61 | align-items: start; 62 | position: relative; 63 | } 64 | 65 | .dialog_large { 66 | line-height: normal; 67 | align-items: start; 68 | position: fixed; 69 | background-color: var(--body-bg); 70 | top: 0; 71 | left: 0; 72 | width: 100vw; 73 | height: 100vh; 74 | z-index: 99; 75 | } 76 | 77 | #closeDialog { 78 | right: 0; 79 | top: 0; 80 | margin-top: 1em; 81 | height: 2em; 82 | 83 | } 84 | 85 | 86 | /* header, */ 87 | section, 88 | .flexRow { 89 | display: flex; 90 | justify-content: center; 91 | align-items: center; 92 | flex-wrap: nowrap; 93 | } 94 | 95 | header { 96 | display: flex; 97 | justify-content: center; 98 | align-items: center; 99 | flex-wrap: wrap; 100 | /* height: 2em; */ 101 | } 102 | 103 | body { 104 | font-size: 12px; 105 | line-height: 0; 106 | font-family: inconsolata; 107 | background-color: var(--fh-bg); 108 | color: var(--fh-txt); 109 | margin: 0; 110 | position: absolute; 111 | top: 0; 112 | bottom: 0; 113 | width: 100%; 114 | } 115 | 116 | img { 117 | cursor: default; 118 | margin-left: .5em; 119 | height: 1.3em; 120 | padding: .2em; 121 | margin: .2em; 122 | border-radius: 50%; 123 | filter: var(--filter); 124 | } 125 | 126 | 127 | footer img { 128 | height: 1em; 129 | padding: 0em; 130 | } 131 | 132 | 133 | .stg table tr td:first-child { 134 | text-align: left; 135 | } 136 | 137 | 138 | 139 | 140 | #appIcon { 141 | border-radius: 0%; 142 | } 143 | 144 | 145 | 146 | #content { 147 | position: relative; 148 | overflow:hidden; 149 | } 150 | 151 | .scroll { 152 | overflow: scroll; 153 | white-space: nowrap; 154 | } 155 | 156 | .scroll::-webkit-scrollbar { 157 | display: none 158 | } -------------------------------------------------------------------------------- /article/pwa-showcase.md: -------------------------------------------------------------------------------- 1 | # Progressive Web Apps: Revolutionizing the Way We Experience the Web 2 | 3 | > Progressive Web Apps (PWAs) combine the best of web and app experiences—offering speed, offline access, cross-platform compatibility, and seamless updates without downloads. They empower users and businesses with cost-effective, native-app-like functionality. 4 | 5 | Imagine a world where websites behave like apps, offering lightning-fast speed, offline functionality, and seamless user experiences—all without the hassle of downloading from an app store. Enter Progressive Web Apps (PWAs), the future of web technology designed to bridge the gap between the web and native apps. A standout example of this innovation is [Nanocell-csv](https://www.nanocell-csv.com/), a PWA designed to handle CSV files with remarkable precision and ease. 6 | 7 | 8 | 9 | ## What Are Progressive Web Apps? 10 | Progressive Web Apps are web applications that use modern web technologies to deliver app-like experiences. Built with standard web tech like HTML, CSS, and JavaScript, PWAs can be accessed through your browser but feel as polished and responsive as any native app. 11 | 12 | ## What’s in It for Users? 13 | 14 | **Speed and Responsiveness** - PWAs load quickly, even on slower networks. This is thanks to advanced caching techniques powered by service workers, ensuring users experience minimal loading times. 15 | 16 | **Offline Access** - Ever lost a connection while using a traditional website? PWAs have your back. They work offline or in areas with poor connectivity, keeping essential features functional. 17 | 18 | **No Downloads Needed** - Forget app store clutter. PWAs are lightweight and can be installed directly from the browser, saving storage space and time. 19 | 20 | **Cross-Platform Compatibility** - Whether you’re on Android, iOS, Mac, Linux or Windows, PWAs adapt seamlessly, offering a consistent experience across devices. 21 | 22 | **Automatic Updates** - Say goodbye to those constant "update now" notifications. PWAs update in the background, ensuring users always have the latest version. 23 | 24 | ## Why Should Businesses Care? 25 | 26 | For businesses, PWAs offer a golden opportunity to boost engagement and conversion rates. Their speed and reliability reduce bounce rates, while offline functionality ensures users stay connected with your brand. Moreover, PWAs are more cost-effective to develop and maintain compared to traditional apps, making them a win-win for businesses of all sizes. 27 | 28 | ## The Ultimate Goal: Democratizing the Web 29 | At its core, the goal of PWA technology is simple yet profound: to make the web more accessible, reliable, and engaging for everyone. By eliminating the friction between web and app experiences, PWAs empower users and developers alike, creating a more unified digital ecosystem. 30 | 31 | In a world where user expectations are at an all-time high, PWAs are a game-changer, proving that the web can be as dynamic and powerful as any app. Ready to join the revolution? Start exploring the world of Progressive Web Apps today with [Nanocell-csv](https://www.nanocell-csv.com/), a spreadsheet editor PWA! 32 | 33 | -------------------------------------------------------------------------------- /article/how-to-open-a-csv-file.md: -------------------------------------------------------------------------------- 1 | # How to open large CSV files with Nanocell-csv 2 | 3 | > CSV files are a universal data table format, so why is it complicated to open them? Nanocell-csv gives a simple, fast, and elegant solution, especially to preview very large files. 4 | 5 | CSV files are mostly used as a medium to transfer data from one database to another as it is the one format guaranteed to be handled by all systems. You also find them in code repositories as they are the best way to store rows of data in text files. 6 | 7 | However universally used they may be, no tool seems to handle them quite right. CSV files tend to be opened by MS Excel by default. However Excel turns out to be slow, does not detect the columns correctly, and even corrupts data by dropping leading zeros and plus signs (typically in phone numbers or postal codes). 8 | 9 | This is where [Nanocell-csv](https://www.nanocell-csv.com/) comes in. 10 | 11 | 12 | 13 | 14 | ## Installing Nanocell-csv 15 | 16 | This can't get any simpler. 17 | 1. Go to [https://www.nanocell-csv.com/](https://www.nanocell-csv.com/) using a chrome based browser (chrome, edge, chromium, etc...) 18 | 2. Click on install in the top right corner 19 | 3. Confirm the install prompt. 20 | 4. You're done! 21 | 22 | > No install `.exe` file that prompts for admin permission 23 | > No download time 24 | > No hassle. 25 | 26 | 27 | ## Opening a file for the first time 28 | 29 | [Nanocell-csv](https://www.nanocell-csv.com/) should have been detected as the default application for CSV files. 30 | Double clicking on any CSV file should thus open it in Nanocell-csv. 31 | 32 | If that is not the case: 33 | - right click on a csv file 34 | - select : `Open with...` 35 | - select : `Choose another app` 36 | - select : `Nanocell-csv` and click on `Always` 37 | 38 | 39 | ## Setting your preference 40 | 41 | Click on the following icon in the top right corner: 42 | 43 | ![settings-icon](https://www.nanocell-csv.com/app/icn/menu/settings.svg) 44 | 45 | You can change the theme color palette in as the first option. You may also want to change the number of rows, columns or the text size to make [Nanocell-csv](https://www.nanocell-csv.com/) as comfortable as possible on your screen and needs. 46 | 47 | ## What if the file being opened is huge, let's say 20 GB ?! 48 | 49 | [Nanocell-csv](https://www.nanocell-csv.com/) has no limit on file size. It will change to a better suited approach when opening large files. Your 20GB file will still open instantly but in read only mode to give you a quick overview of what the file may contain. This is achieved by sampling the header, the footer and a few rows at regular intervals without parsing the entire file. The goal here is for you to quickly understand what you are dealing with when first opening a file before loading it in more expert tools for transforming big data. 50 | 51 | Nanocell-csv considers editing such large tables outside of database tools, and ETL pipelines as bad practice (Python pandas, R, SQL, Spark ...). Such tools often lack a quick preview feature. 52 | For these reasons, [Nanocell-csv](https://www.nanocell-csv.com/) simply aims to be a fast preview tool. 53 | 54 | > No file size limit ! 55 | > Instant preview ! 56 | 57 | 58 | ![screenshot of big data view](https://www.nanocell-csv.com/img/screenshot/bigdata_preview.webp) 59 | 60 | [Link to Nanocell-csv](https://www.nanocell-csv.com/) -------------------------------------------------------------------------------- /app/js/CMenu.js: -------------------------------------------------------------------------------- 1 | class CMenu extends HTMLElement { 2 | constructor() { 3 | super(); 4 | this.table = new Table(); 5 | this.style.display = "none"; 6 | this.firstBlock = document.createElement("div"); 7 | this.firstBlock.classList.add("cmenu_header") 8 | 9 | this.list = [ 10 | { key: "sa", txt: "Sort", opt: "A
Z", run: cmd.sort.run }, 11 | { key: "sd", txt: "Sort", opt: "Z
A", run: cmd.sort_reverse.run }, 12 | { key: "rn", txt: "Round", opt: "N", run: cmd.integer.run }, 13 | { key: "rf", txt: "Round", opt: "$", run: cmd.decimal.run }, 14 | { key: "ic", txt: "Insert", opt: "|", run: cmd.insertLeft.run }, 15 | { key: "ir", txt: "Insert", opt: "―", run: cmd.insertUp.run }, 16 | { key: "dc", txt: "Delete", opt: "|", run: cmd.deleteCol.run }, 17 | { key: "dr", txt: "Delete", opt: "―", run: cmd.deleteRow.run }, 18 | ] 19 | 20 | this.addEventListener('mouseout', event => { 21 | if (this.contains(event.relatedTarget)) return; 22 | this.style.display = "none"; 23 | }); 24 | 25 | this.buildMenu(); 26 | this.appendChild(this.firstBlock); 27 | this.appendChild(this.table); 28 | 29 | } 30 | 31 | showItems(show_list) { 32 | for (let i = 0; i < this.list.length; i++) { 33 | if (show_list.includes(this.list[i].key)) this.table.rows[i].style.display = "table-row"; 34 | else this.table.rows[i].style.display = "none"; 35 | } 36 | } 37 | 38 | pop(e) { 39 | this.event = e; 40 | this.ttype = getTargetType(e); 41 | if (!this.isValidTarget()) return; 42 | this.x = e.target.tx; 43 | this.y = e.target.ty; 44 | this.reposition(); 45 | if (this.ttype === TargetType.colH) { 46 | sheet.slctCol(this.x + sheet.baseX); 47 | this.firstBlock.innerText = "col : " + e.target.innerText; 48 | this.showItems(["sa", "sd", "rn", "rf", "ic", "dc"]) 49 | } else if (this.ttype === TargetType.rowH) { 50 | sheet.slctRow(this.y + sheet.baseY); 51 | this.firstBlock.innerText = "row : " + e.target.innerText; 52 | this.showItems(["rn", "rf", "ir", "dr"]) 53 | } else if (e.target.classList.contains("slct")) { 54 | this.firstBlock.innerText = "selection"; 55 | this.showItems(["rn", "rf", "dc", "dr"]) 56 | } else { 57 | sheet.x = e.target.tx + sheet.baseX; 58 | sheet.y = e.target.ty + sheet.baseY; 59 | sheet.slctRefresh(); 60 | this.firstBlock.innerText = "cell"; 61 | this.showItems(["rn", "rf", "ic", "ir", "dc", "dr"]) 62 | } 63 | this.style.display = "block" 64 | } 65 | 66 | buildMenu() { 67 | for (var item of this.list) { 68 | this.table.br(); 69 | let div = document.createElement("div"); 70 | div.innerHTML = item.txt; 71 | this.table.push(div); 72 | if (item.opt) { 73 | let optDiv = document.createElement("div"); 74 | optDiv.innerHTML = item.opt; 75 | this.table.push(optDiv); 76 | optDiv.classList.add("cmenu_opt") 77 | } 78 | this.table.activeRow().addEventListener('click', item.run); 79 | } 80 | } 81 | 82 | isValidTarget() { 83 | let okTargets = [ 84 | TargetType.cell, 85 | TargetType.rowH, 86 | TargetType.colH, 87 | ] 88 | return (okTargets.includes(this.ttype)) 89 | } 90 | 91 | reposition() { 92 | let e = this.event; 93 | this.style.left = (e.clientX - 2) + "px"; 94 | this.style.top = (e.clientY - 2) + "px"; 95 | } 96 | } 97 | customElements.define('ui-cmenu', CMenu); -------------------------------------------------------------------------------- /app/css/sheet.css: -------------------------------------------------------------------------------- 1 | .sheet { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | background: var(--table-borders); 6 | border-spacing: 1px; 7 | } 8 | 9 | 10 | .sheet td { 11 | color: var(--txt); 12 | overflow: hidden; 13 | white-space: nowrap; 14 | box-sizing: border-box; 15 | position: relative; 16 | max-width: 0; 17 | background-color: var(--body-bg); 18 | padding: 0; 19 | font-weight: normal; 20 | } 21 | 22 | 23 | .sheet .tHeader { 24 | color: var(--th-txt); 25 | background-color: var(--th-bg); 26 | /* padding: 2px; */ 27 | } 28 | 29 | .sheet .tColHeader { 30 | height: 1.7em; 31 | text-align: center; 32 | position: relative; 33 | /* width: 10%; */ 34 | min-width: 3em !important; 35 | 36 | } 37 | 38 | .headerHandle { 39 | height: 100%; 40 | width: .6em; 41 | /* background: var(--th-bg); */ 42 | position: absolute; 43 | right: 0; 44 | top: 0; 45 | display: flex; 46 | align-items: center; 47 | justify-content: center; 48 | cursor: col-resize; 49 | } 50 | .noclick{ 51 | pointer-events: none; 52 | } 53 | 54 | 55 | 56 | .sheet .tRowHeader { 57 | text-align: end; 58 | width: 1%; 59 | max-width: 1%; 60 | padding: 0 .5em 0 .5em; 61 | white-space: nowrap; 62 | } 63 | 64 | 65 | .sheet td div { 66 | margin: 0 .5em 0 .5em; 67 | line-height: 1em; 68 | height: 0; 69 | display: flex; 70 | align-items: center; 71 | } 72 | 73 | .sheet div { 74 | pointer-events: none; 75 | } 76 | 77 | 78 | 79 | .sheet input, .sheet input:focus { 80 | box-sizing: border-box; 81 | position: absolute; 82 | padding: 0 .5em 0 .5em; 83 | font-size: inherit; 84 | top: 0; 85 | left: 0; 86 | width: 100%; 87 | border-radius: 0; 88 | height: 100%; 89 | color: var(--input-txt); 90 | background-color: var(--input-bg); 91 | z-index: 1; 92 | margin: 0; 93 | } 94 | 95 | 96 | 97 | .slct { 98 | color: inherit; 99 | background-color: var(--slct-bg) !important; 100 | } 101 | 102 | 103 | .date { 104 | color: var(--blue); 105 | text-align: center; 106 | justify-content: center; 107 | } 108 | 109 | .url { 110 | color: var(--blue); 111 | } 112 | 113 | 114 | .num { 115 | text-align: right; 116 | color: var(--num-txt); 117 | justify-content: flex-end; 118 | } 119 | 120 | .error { 121 | color: var(--red); 122 | text-align: center; 123 | /* opacity: 0.8; */ 124 | justify-content: center; 125 | } 126 | 127 | .noComply { 128 | color: var(--purple); 129 | } 130 | 131 | .sheet td div:nth-child(2) { 132 | color: var(--blue); 133 | float: left; 134 | text-align: left; 135 | margin-top: 1em; 136 | } 137 | 138 | .sheet td div:nth-child(3) { 139 | float: right; 140 | /* line-height: normal; */ 141 | /* width: 100%; */ 142 | margin-top: 1em; 143 | } 144 | 145 | 146 | 147 | ui-cmenu { 148 | display: block; 149 | position: fixed; 150 | width: 10em; 151 | top: 0; 152 | left: 0; 153 | background-color: var(--th-bg); 154 | line-height: 1em; 155 | box-shadow: 0 0 0 1px var(--table-borders); 156 | border-radius: 0 1em 1em 1em; 157 | } 158 | 159 | .cmenu_header { 160 | text-align: center; 161 | font-weight: bold; 162 | margin: .5em; 163 | } 164 | 165 | 166 | 167 | 168 | .cmenu_opt { 169 | text-align: right; 170 | } 171 | 172 | ui-cmenu div { 173 | margin: .7em 1em; 174 | } 175 | 176 | 177 | ui-cmenu table { 178 | width: 100%; 179 | border-collapse: collapse; 180 | margin-bottom: 1em; 181 | } 182 | 183 | ui-cmenu tr { 184 | cursor: pointer; 185 | } 186 | 187 | ui-cmenu tr:hover { 188 | background-color: var(--cmenuHover); 189 | 190 | } -------------------------------------------------------------------------------- /app/js/key.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | let buildKeys = function () { 4 | let prevent_dflt_list = ['H', 'N', 'T', 'F', 'O'];// { H: history pop up, N: new window, T: new tab} 5 | document.onkeydown = function (e) { 6 | var k = e.key.toUpperCase(); 7 | // console.log(e) 8 | var ctrlDown = e.metaKey || e.ctrlKey; 9 | var alt = e.altKey; 10 | var shift = e.shiftKey; 11 | var meta = e.metaKey; 12 | var inputting = document.activeElement.tagName == "INPUT"; 13 | sheet.slctRange = shift; 14 | if (ctrlDown && (prevent_dflt_list.includes(k))) { e.preventDefault(); } // prevent : 15 | if (dom.dialog.isBusy && k === "ESCAPE") return dom.dialog.clear(); 16 | if (dom.dialog.isLarge) return; 17 | if (isOSX && k === "S" && meta && shift) return cmd.saveAs.run(); 18 | if (isOSX && k === "S" && meta) return cmd.save.run(); 19 | if (alt && k == "TAB") return; // enable switching window 20 | if (ctrlDown && (k === "C" || k === "V")) return; // enables copy paste events 21 | if (k === "TAB") { e.preventDefault(); } // prevent : all tab events; 22 | if (inputting && !(ctrlDown && (k === "F" || k === 'S' || k === 'O'))) return; // letting finder and save through 23 | if (e.code === "Space") k = "SPACE"; 24 | if (k === "PAGEUP") { k = "ARROWUP"; alt = true } 25 | if (k === "PAGEDOWN") { k = "ARROWDOWN"; alt = true } 26 | 27 | if (ctrlDown) { 28 | switch (k) { 29 | case "ARROWUP": sheet.y = 0; sheet.slctRefresh(); return; 30 | case "ARROWRIGHT": sheet.x = sheet.df.width - 1; sheet.slctRefresh(); return 31 | case "ARROWDOWN": sheet.y = sheet.df.height - 1; sheet.slctRefresh(); return; 32 | case "ARROWLEFT": sheet.x = 0; sheet.slctRefresh(); return; 33 | } 34 | } 35 | 36 | // any char without control keys will start inputing 37 | if (e.key.length === 1 && !ctrlDown && !e.metaKey) { e.preventDefault(); return sheet.input(e.key); } 38 | 39 | // prevents the default from any cmd combo 40 | for (var c of Object.values(cmd)) 41 | if (k === c.k && c.ctrl === ctrlDown && c.shift === shift && c.alt === alt) { c.run(); return e.preventDefault() } 42 | 43 | 44 | switch (k) { 45 | case "ARROWUP": sheet.y--; sheet.slctRefresh(); return; 46 | case "ARROWDOWN": sheet.y++; sheet.slctRefresh(); return; 47 | case "ARROWLEFT": sheet.x--; sheet.slctRefresh(); return; 48 | case "ARROWRIGHT": sheet.x++; sheet.slctRefresh(); return; 49 | case "TAB": sheet.x++; sheet.slctRefresh(); return; 50 | case "ENTER": sheet.input(); return; 51 | case "BACKSPACE": sheet.delete(); return; 52 | } 53 | } 54 | 55 | document.onkeyup = function (e) { 56 | var k = e.key.toUpperCase(); 57 | switch (k) { 58 | case "CONTROL": if (e.shiftKey) sheet.slctRange = true; return; 59 | case "SHIFT": sheet.slctRange = false; return; 60 | } 61 | } 62 | 63 | document.addEventListener('copy', function (e) { 64 | var inputting = document.activeElement.tagName == "INPUT"; 65 | if (inputting) return; 66 | e.preventDefault(); 67 | var clip = sheet.rangeArray().map(r => r.join('\t')).join('\n'); 68 | e.clipboardData.setData('text/plain', clip); 69 | }); 70 | 71 | document.addEventListener('cut', function (e) { 72 | var inputting = document.activeElement.tagName == "INPUT"; 73 | if (inputting) return; 74 | e.preventDefault(); 75 | var clip = sheet.rangeArray().map(r => r.join('\t')).join('\n'); 76 | e.clipboardData.setData('text/plain', clip); 77 | sheet.rangeEdit(''); 78 | sheet.refresh(); 79 | }); 80 | 81 | document.addEventListener('paste', function (e) { 82 | var inputting = document.activeElement.tagName == "INPUT"; 83 | if (inputting) return; 84 | e.preventDefault(); 85 | sheet.paste((e.clipboardData).getData('text').split('\n').map(r => r.split(/[\t,]+/))); 86 | sheet.refresh(); 87 | }); 88 | 89 | } 90 | -------------------------------------------------------------------------------- /app/js/utils/DateExt.js: -------------------------------------------------------------------------------- 1 | Date.prototype.monthList = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; 2 | Date.prototype.week = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; 3 | Date.prototype.parser = { 4 | day: ["day", "Day", "DAY"], 5 | date: ["d1", "dd"], 6 | month: ["mm", "MMM", "Mmm", "mmm", "month", "Month", "MONTH", "month"], 7 | year: ["YY", "yyyy"], 8 | epoch: ["UNIX", "epoch"], 9 | } 10 | 11 | Date.prototype.addDays = function (n) { this.setDate(this.getDate() + n); return this; } 12 | 13 | Date.prototype.build = function (txt, f) { 14 | for (var e of this.parser.epoch) if (f === e) { this.setTime(txt); return this; } 15 | var match = txt.match(/\d+/g); 16 | if (match === null) return undefined; 17 | var nums = match.map(Number); 18 | if (nums.length > 3 || nums.length < 2) return undefined; 19 | 20 | var y = 0, m = 0, d = 0; 21 | var yp = -1, mp = -1, dp = -1; 22 | var fullYear = true; 23 | for (var i = 0; i < this.monthList.length; i++)if (new RegExp(this.monthList[i].substring(0, 3), 'i').test(txt)) m = i + 1; 24 | if (m < 1 && nums.length != 3) return undefined; 25 | if (m > 0 && nums.length != 2) return undefined; 26 | 27 | for (var month of this.parser.month) mp = Math.max(mp, f.search(month)); 28 | for (var date of this.parser.date) dp = Math.max(dp, f.search(date)); 29 | for (var year of this.parser.year) { 30 | var n = f.search(year); 31 | if (n > -1 && year == "YY") fullYear = false; 32 | yp = Math.max(yp, n); 33 | 34 | } 35 | if (m < 1) { 36 | if (mp < yp && mp < dp) m = nums.shift(); 37 | else if (mp > yp && mp > dp) m = nums.pop(); 38 | else { m = nums[1]; nums.splice(1, 1); } 39 | } 40 | 41 | d = (dp < yp) ? nums.shift() : nums.pop(); 42 | y = nums[0]; 43 | if (!fullYear) y = Math.floor(new Date().getFullYear() / 100) * 100 + y; 44 | if (d < 1 || m < 1 || y < 1) return undefined; 45 | this.setMonth(m - 1); 46 | this.setDate(d); 47 | this.setFullYear(y); 48 | if (this.getFormated(f) === txt) return this; 49 | return undefined; 50 | } 51 | 52 | Date.prototype.getFormated = function (f) { 53 | largen = function (n, d) { n = String(n); while (n.length < d) n = "0" + n; return n }; 54 | suffix = function (n) { 55 | if (n % 10 === 1 && n !== 11) return n + "st"; 56 | if (n % 10 === 2 && n !== 12) return n + "nd"; 57 | if (n % 10 === 3 && n !== 13) return n + "rd"; 58 | return n + 'th'; 59 | } 60 | if (isNaN(this.getTime())) return undefined; 61 | f = f.replace("epoch", this.getTime()); 62 | f = f.replace("UNIX", this.getTime()); 63 | 64 | f = f.replace("MONTH", this.monthList[this.getMonth()].toUpperCase()); 65 | f = f.replace("Month", this.monthList[this.getMonth()]); 66 | f = f.replace("month", this.monthList[this.getMonth()].toLowerCase()); 67 | 68 | f = f.replace("MMM", this.monthList[this.getMonth()].substring(0, 3).toUpperCase()); 69 | f = f.replace("Mmm", this.monthList[this.getMonth()].substring(0, 3)); 70 | f = f.replace("mmm", this.monthList[this.getMonth()].substring(0, 3).toLowerCase()); 71 | f = f.replace("mm", largen(this.getMonth() + 1, 2)); 72 | 73 | f = f.replace("yyyy", largen(this.getFullYear(), 4)); 74 | f = f.replace("YY", largen(this.getFullYear() % 100, 2)); 75 | 76 | f = f.replace("DAY", this.week[this.getDay()].toUpperCase()); 77 | f = f.replace("day", this.week[this.getDay()].toLowerCase()); 78 | 79 | f = f.replace("Day", this.week[this.getDay()]); 80 | f = f.replace("dd", largen(this.getDate(), 2)); 81 | f = f.replace("dth", suffix(this.getDate())); 82 | f = f.replace("d1", this.getDate()); 83 | return f; 84 | } 85 | 86 | 87 | Date.prototype.isValidFormat = function (f) { 88 | var d = new Date(1999, 1, 1); 89 | var n = new Date(2222, 2, 2).build(d.getFormated(f), f); 90 | return Boolean(n && d.getTime() === n.getTime()); 91 | } 92 | 93 | 94 | Date.isDate = function (t) { 95 | // const regex = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/; 96 | const regex = /^\d{4}-[01]\d-[0123]\d$/; 97 | return regex.test(t) 98 | } 99 | 100 | -------------------------------------------------------------------------------- /TERMS_OF_USE.md: -------------------------------------------------------------------------------- 1 | # Terms of Use 2 | 3 | Welcome to Nanocell-csv ! These Terms of Use ("Terms") govern your access to and use of our csv editor software ("Software"). By using the Software, you agree to be bound by these Terms. If you do not agree, please refrain from using the Software. 4 | 5 | ### I - License 6 | 7 | ![Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License](https://i.creativecommons.org/l/by-nc-nd/3.0/88x31.png) 8 | 9 | The Software is provided under the **Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License**, which governs your rights to use, modify, and distribute the Software. The full licensing terms are available [here](http://creativecommons.org/licenses/by-nc-nd/3.0/). In the instance of conflict between these Terms of Use and the licensing terms, the Terms of Use shall prevail. Under this license, you are free to Share (copy and redistribute the material in any medium or format) under the following terms: 10 | 11 | - Attribution - You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. 12 | - NonCommercial - You may not use the material for commercial purposes. 13 | - NoDerivatives - If you remix, transform, or build upon the material, you may not distribute the modified material. 14 | - No additional restrictions - You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. 15 | 16 | 17 | 18 | 19 | ### II - Use of the Software 20 | 21 | You may use the Software for personal and educational purposes, but not for commercial purposes, in accordance with the license. 22 | You are not permitted to modify or create derivative works based on the Software. 23 | You are responsible for ensuring that your use of the Software complies with applicable laws and regulations. 24 | 25 | ### III - Contributions 26 | 27 | If you contribute to the Software, you agree that your contributions will be governed by the terms of the Software’s license. 28 | Contributions include but are not limited to code, documentation, bug reports, and feature suggestions. 29 | 30 | ### IV - Disclaimer of Warranties 31 | 32 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. 33 | 34 | AUTHORS AND COPYRIGHT HOLDERS SHALL NOT BE HELD RESPONSIBLE FOR ANY LOSS OF DATA, CORRUPTION OF DATA, OR ANY OTHER INCIDENTS RESULTING FROM THE USE OF THE SOFTWARE. USERS ASSUME FULL RESPONSIBILITY FOR BACKING UP DATA AND ENSURING DATA INTEGRITY WHILE USING THE SOFTWARE 35 | 36 | YOU USE THE SOFTWARE AT YOUR OWN RISK. 37 | 38 | ### V - Limitation of Liability 39 | 40 | UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING AND TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, WE OFFER THE SOFTWARE AND OUR SERVICES AS-IS AND MAKE NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THOSE, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON INFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO THIS EXCLUSION MAY NOT APPLY TO YOU. 41 | 42 | EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL WE BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES, LOSS OR CORRUPTION OF DATA ARISING OUT OF THIS LICENSE OR THE USE OF THE SOFTWARE AND OUR SERVICES, EVEN IF YOU HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 43 | 44 | SAVE FOR ANY CLAIM THAT CANNOT BE LIMITED OR EXCLUDED BY APPLICABLE LAW, DEATH OR PERSONAL INJURY CAUSED BY OUR NEGLIGENCE OR FRAUD, YOU AGREE THAT OUR LIABILITY SHALL AT ALL TIMES BE LIMITED TO A SUM EQUAL TO EUR 1,000. 45 | 46 | Should a clause be deemed invalid, such clause shall be deemed modified to the minimum extent necessary to make it valid whilst keeping its intended meaning. 47 | 48 | ### VI - Privacy 49 | 50 | The Software does not collect or transmit any personal data unless explicitly configured by the user. 51 | 52 | ### VII - Modifications and Updates 53 | 54 | We reserve the right to modify or discontinue the Software at any time, with or without notice. 55 | Updates to the Software may include changes to these Terms, and continued use of the Software after such changes constitutes your acceptance of the updated Terms. 56 | 57 | ### VIII - Governing Law 58 | 59 | These Terms shall be governed by and construed in accordance with the laws of **France**, without regard to its conflict of law provisions. 60 | 61 | ### IX - Contact 62 | 63 | If you have any questions about these Terms, please contact us at **nanocell.csv@gmail.com**. 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /app/js/CsvHandle.js: -------------------------------------------------------------------------------- 1 | class CsvHandle { 2 | constructor() { 3 | this.handle = null; 4 | this.file = null; 5 | this.file_chunks = null; 6 | this.viewOnly = false; 7 | this.sw = new Worker("sw_read_write_csv.js"); 8 | this.sw.addEventListener("message", e => { 9 | let d = e.data 10 | switch (d.cmd) { 11 | case "chunk_loaded": this.file_chunk_loaded(d) 12 | } 13 | }) 14 | } 15 | 16 | async launchFile(handle) { 17 | this.handle = handle; 18 | this.file = await handle.getFile(); 19 | document.title = this.file.name; 20 | console.log("Loading : ", this.file.name) 21 | this.read(this.file) 22 | } 23 | 24 | file_chunk_loaded(d) { 25 | document.getElementById("footerCenter").innerHTML = Math.round(d.status * 100) + "%" 26 | if (d.chunk != null) this.file_chunks.push(d.chunk) 27 | if (d.status >= 1 && d.chunk != null) this.readSuccess(); 28 | } 29 | 30 | readSuccess() { 31 | let matrix = this.file_chunks.flat(1); 32 | sheet = new Sheet(new Dataframe(matrix)) 33 | sheet.df.isSaved = true; 34 | sheet.df.lock = this.viewOnly; 35 | if(stg.trim) sheet.df.trimAll(); 36 | sheet.fixTop = stg.set_headers; 37 | if(stg.fit_col_width) sheet.fitWidth(); 38 | } 39 | 40 | read(file) { 41 | this.file = file; 42 | this.file_chunks = []; 43 | let mbSize = file.size / 1000000 44 | this.viewOnly = mbSize > Number(stg.editMaxFileSize); 45 | // console.log(file) 46 | if(file.size ==0){ 47 | this.file_chunks = [[[]]] 48 | this.readSuccess() 49 | }else{ 50 | this.pipe("read", { file: file, viewOnly: this.viewOnly, n_chunks: stg.vo_n_chunks, n_rows: stg.vo_n_rows }) 51 | } 52 | } 53 | 54 | pipe(cmd, data) { this.sw.postMessage({ cmd: cmd, data: data }) } 55 | 56 | async open() { 57 | try { 58 | 59 | let [fileHandle] = await window.showOpenFilePicker(CsvHandle.pickerOptions); 60 | const newWindow = window.open('./home.html', "_blank", 'width=800,height=600'); // 'newWindow.html' should be the page that will handle the file 61 | const channel = new MessageChannel(); 62 | newWindow.onload = () => { 63 | newWindow.postMessage({ fileHandle }, '*', [channel.port2]); 64 | }; 65 | } catch (err) { 66 | if (err.name === 'AbortError') return; 67 | else console.error('An unexpected error occurred:', err); 68 | 69 | } 70 | } 71 | 72 | reloadFile(force = false) { 73 | if (this.handle === null) return Msg.quick("No file to reload from.") 74 | if (!sheet.df.isSaved && !force) { 75 | Msg.choice("Changes will be lost ? ", () => { this.launchFile(this.handle) }) 76 | }else{ 77 | this.launchFile(this.handle); 78 | } 79 | } 80 | 81 | new() { window.open('./home.html', "_blank", 'width=600,height=400') } 82 | 83 | async saveAs() { 84 | if (this.viewOnly) return; 85 | try { 86 | 87 | this.handle = await window.showSaveFilePicker(CsvHandle.pickerOptions); 88 | this.save(); 89 | } catch (err) { 90 | if (err.name === 'AbortError') return; 91 | else console.error('An unexpected error occurred:', err); 92 | 93 | } 94 | } 95 | async save() { 96 | if (this.viewOnly) return; 97 | if (this.handle === null) this.saveAs(); 98 | else { 99 | const writableStream = await this.handle.createWritable(); 100 | try { 101 | let csvContent = CsvHandle.from2D(sheet.df.data) 102 | await writableStream.write(csvContent); 103 | await writableStream.close(); 104 | sheet.df.isSaved = true; 105 | sheet.refresh(); 106 | this.file = await this.handle.getFile(); 107 | document.title = this.file.name; 108 | } catch (err) { 109 | Msg.confirm(err); 110 | } 111 | } 112 | } 113 | 114 | static from2D(matrix) { 115 | let isStrict = stg.save_strict; 116 | let fw = stg.save_fixed_width_size; 117 | let sep = stg.delimiter; 118 | let spaces = " ".repeat(fw) 119 | if (sep == "TAB") sep = '\t'; 120 | var newMat = []; 121 | for (var row of matrix) { 122 | var newRow = []; 123 | for (var cell of row) { 124 | var data = String(cell); 125 | var quote = false; 126 | for (var i = 0; i < data.length; i++) { 127 | if (data[i] === "," || data[i] === "\n") quote = true; 128 | if (data[i] === '"') { quote = true; data = data.slice(0, i) + '"' + data.slice(i); i++ } 129 | } 130 | if (quote && isStrict) throw "Strict csv format not respected

save aborted"; 131 | if (quote) data = '"' + data + '"'; 132 | if (fw > data.length) data = (spaces + data).slice(-fw); 133 | newRow.push(data); 134 | } 135 | newMat.push(newRow.join(sep)); 136 | } 137 | return newMat.join('\n'); 138 | } 139 | } 140 | 141 | 142 | Object.defineProperty(CsvHandle, 'pickerOptions', { 143 | value: { 144 | types: [ 145 | { 146 | description: "csv (Comma Separated Value) ", 147 | accept: { "text/csv": [".csv", ".tsv"] } 148 | }, 149 | ] 150 | } 151 | }); 152 | 153 | 154 | -------------------------------------------------------------------------------- /app/js/mouse.js: -------------------------------------------------------------------------------- 1 | 2 | let LBT = undefined; 3 | let RBT = undefined; 4 | let mouseX = 0; 5 | let mouseY = 0; 6 | 7 | let mouseXstart = 0; 8 | let mouseYstart = 0; 9 | let mouseTargetStart = 0; 10 | 11 | const TargetType = Object.freeze({ 12 | na: undefined, 13 | cell: 0, 14 | rowH: 1, 15 | colH: 2, 16 | allH: 3, 17 | scrollBarX: 4, 18 | scrollBarY: 5, 19 | headerHandle: 6, 20 | }); 21 | 22 | document.addEventListener('dblclick', (e) => { 23 | if (getTargetType(e) == TargetType.allH) { 24 | sheet.colWidthList = []; 25 | sheet.refresh(); 26 | } 27 | }); 28 | 29 | document.addEventListener('contextmenu', (event) => { 30 | if (!event.ctrlKey) { 31 | event.preventDefault(); 32 | dom.cmenu.pop(event); 33 | } 34 | }); 35 | 36 | document.addEventListener("mouseup", e => { 37 | if (LBT == TargetType.headerHandle) { 38 | var th = mouseTargetStart.parentNode; 39 | var w = th.style.width; 40 | sheet.colWidthList.push({ idx: th.tx + sheet.baseX, width: w }) 41 | } 42 | document.onmousemove = undefined; 43 | if (e.buttons < 1) { 44 | LBT = undefined; 45 | RBT = undefined; 46 | } 47 | }); 48 | 49 | let getTargetType = function (e) { 50 | let t = e.target 51 | if (t.tagName == "TD" && t.parentNode.parentNode === sheet) { 52 | if (t.tx < 0 && t.ty < 0) return TargetType.allH; 53 | if (t.tx < 0) return TargetType.rowH; 54 | if (t.ty < 0) return TargetType.colH; 55 | return TargetType.cell 56 | } 57 | if (t === dom.content.scrollerY) return TargetType.scrollBarY; 58 | if (t === dom.content.scrollerX) return TargetType.scrollBarX; 59 | if (t.classList.contains("headerHandle")) return TargetType.headerHandle; 60 | return TargetType.na 61 | } 62 | 63 | document.addEventListener("mousedown", e => { 64 | mouseXstart = e.clientX; 65 | mouseYstart = e.clientY; 66 | mouseTargetStart = e.target; 67 | if (e.button === 0) LBT = getTargetType(e); 68 | if (e.button === 2) RBT = getTargetType(e); 69 | if (e.target.tagName != "INPUT" && e.target.tagName != "BUTTON") e.preventDefault(); // prevents text selection 70 | if (LBT == TargetType.cell) { 71 | sheet.x = e.target.tx + sheet.baseX; 72 | sheet.y = e.target.ty + sheet.baseY; 73 | if (e.ctrlKey && e.target.firstElementChild.classList.contains("url")) window.open(e.target.innerText, "_blank"); 74 | sheet.slctRefresh(); 75 | check_for_outofbound_scroll(); 76 | } 77 | if (LBT == TargetType.allH) cmd.slctAll.run(); 78 | if (LBT == TargetType.colH) sheet.slctCol(e.target.tx + sheet.baseX); 79 | if (LBT == TargetType.rowH) sheet.slctRow(e.target.ty + sheet.baseY); 80 | if (e.target.tagName != "INPUT" && document.activeElement.tagName == "INPUT") document.activeElement.blur(); 81 | 82 | }); 83 | 84 | document.addEventListener("mousemove", e => { 85 | { 86 | mouseX = e.clientX; 87 | mouseY = e.clientY; 88 | if (e.buttons < 1) { 89 | LBT = undefined; 90 | RBT = undefined; 91 | } 92 | if (LBT === TargetType.scrollBarY) { 93 | let offset = sheet.rows[1].getBoundingClientRect().top; 94 | let theight = sheet.getBoundingClientRect().bottom - offset; 95 | let r = (e.clientY - offset) / theight; 96 | if (r < 0) r = 0; 97 | if (r > 1) r = 1; 98 | sheet.baseY = Math.floor(sheet.df.height * r); 99 | sheet.slctRefresh(false); 100 | } 101 | if (LBT === TargetType.scrollBarX) { 102 | let offset = sheet.rows[0].cells[0].getBoundingClientRect().left; 103 | let tWidth = sheet.getBoundingClientRect().right - offset; 104 | let r = (e.clientX - offset) / tWidth; 105 | if (r < 0) r = 0; 106 | if (r > 1) r = 1; 107 | sheet.baseX = Math.floor(sheet.df.width * r); 108 | sheet.slctRefresh(false); 109 | } 110 | if (LBT == TargetType.headerHandle) { 111 | var th = mouseTargetStart.parentNode; 112 | var startX = th.getBoundingClientRect().left; 113 | var newWidth = 100 * (mouseX - startX) / th.parentNode.offsetWidth; 114 | if (newWidth < 0) newWidth = 0; 115 | th.style.width = `${newWidth}%`; 116 | } 117 | 118 | } 119 | }); 120 | 121 | function check_for_outofbound_scroll() { 122 | let intervalId = setInterval(() => { 123 | if (LBT === undefined) return clearInterval(intervalId); 124 | else { 125 | let change = false; 126 | let rect = sheet.getBoundingClientRect(); 127 | if (mouseY <= rect.top) { 128 | if (sheet.rangeEnd) sheet.rangeEnd.y = sheet.baseY; 129 | sheet.baseY--; 130 | change = true; 131 | } 132 | if (mouseX >= rect.right - 5) { 133 | if (sheet.rangeEnd) sheet.rangeEnd.x = sheet.baseX + sheet.width - 1; 134 | sheet.baseX++; 135 | change = true; 136 | } 137 | if (mouseY >= rect.bottom) { 138 | if (sheet.rangeEnd) sheet.rangeEnd.y = sheet.baseY + sheet.height - 1; 139 | sheet.baseY++; 140 | change = true; 141 | } 142 | if (mouseX <= rect.left + 5) { 143 | if (sheet.rangeEnd) sheet.rangeEnd.x = sheet.baseX; 144 | sheet.baseX--; 145 | change = true; 146 | } 147 | if (change) sheet.slctRefresh(false); 148 | } 149 | 150 | }, 100); 151 | } 152 | -------------------------------------------------------------------------------- /article/lets-fix-csv-files.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # CSV files: A universal format, yet universally frustrating. Let’s fix that! 4 | 5 | > CSV files are universal, yet frustrating. Nanocell-csv fixes this by offering a free, fast and simple tool for editing small tables and previewing large datasets. It prevents data corruption, helps you cleanup your data, and is fully crossplatform. 6 | 7 | CSV files are the backbone of data exchange—simple, universal, and incredibly versatile. To this day, they are the best way to store tabular data in git repositories and probably the most universally used ETL file format (Extract Transform Load). Yet, for something so ubiquitous, they often come with a frustrating reality: no tool seems to handle them quite right. Open a small file in Excel, and you're met with sluggish loading times and a minefield of potential data corruption. Try to preview a massive file, and you’re left twiddling your thumbs. Other specialized apps all seem archaic, are freemium at best, and often single-platform. It is time for change! 8 | 9 | This is where [Nanocell-csv](https://www.nanocell-csv.com/) comes in—a tool born out of necessity, frustration, and a relentless drive to simplify how we work with data. 10 | 11 | Let me take you through the problem, the vision, and how I built a solution that’s fast, privacy-first, and available anywhere you need it. 12 | 13 | ## The Problem I Am Trying to Solve 14 | 15 | Two use cases come to mind when working with CSV files: 16 | 17 | 1. **Small tables** : for which the data is input manually. 18 | 2. **Large tables** : 1+ million rows, which are usually database extracts. 19 | 20 | In the small table case, I want an MS Excel alternative that opens instantly, figures out the separator correctly, and does not corrupt my data. CSV is text and should remain that way. `+01` should stay `+01`, not be interpreted as `1`. I don't care about the graphs, visuals, and endless menus—I want to keep things simple. And, of course, I want to have all editing functions accessible from the keyboard. 21 | 22 | In the large table case, I want to see its content instantly, but I don't really want to edit it. Actually, I would consider editing such tables outside of database tools and ETL pipelines as bad practice. I just want to view a large sample of the data, header and footer included, to get an idea of the file's content before moving on to expert tools to do my job. I have worked many years as a data analyst (even though my background is software engineering), and one of the main problems I came across is corrupted files that people have opened with editors (typically MS Excel) and then overwrote with the editor's standards. How often did I wish the company-wide default CSV file editor was locked in view-only mode for large files? 23 | 24 | In both cases, I want the tool to run locally as data privacy is essential, and I don't want my company to be at risk of a data leak. 25 | 26 | Furthermore, I want the tool to be available anywhere. I work at a big BIG company, and my computer is subject to an admin lock for any `.exe` file I try to run. I don't want to go through the tedious bureaucratic process of asking for permission to install this one app. 27 | 28 | 29 | 30 | ## Adding in a Few Cool Features 31 | 32 | As mentioned previously, one of my major pain points is data corrupted by MS Excel, mainly caused by people with regional settings that use a comma as the decimal separator. For example, `1.5` becoming `1,5`. This does not fit well in a Comma Separated Value file. So I've added a data validation feature that checks that all real numbers are written with a dot and not a comma. 33 | 34 | I also have an issue with financial data that is not saved with two digits after the decimal point. `$1.50` becomes `$1.5`, so I added a feature to round the column to two decimals. And, of course, an equivalent function to round everything up to integers if needed. 35 | 36 | Quotes and commas are, of course, tolerated in most CSV file standards but are considered bad practice. I've added text linting to highlight cell values that contain such unorthodox characters. Another feature also enables the user to replace all `,` with `-` and all `"` with `|`. 37 | 38 | A major use case for [Nanocell-csv](https://www.nanocell-csv.com/) is to quickly identify and resolve any unconventional issues in your data, typically before running a database import pipeline. [Nanocell-csv](https://www.nanocell-csv.com/) makes sure that : headers are limited to alphanumeric characters, encoding is UTF-8, values do not contain line breaks, and much more. 39 | 40 | 41 | ## Roadmap and Future Plans 42 | 43 | As a lightweight editor, I believe [Nanocell-csv](https://www.nanocell-csv.com/) is fairly mature. On the other hand, as a large database extract quick viewer, a lot more can be done. The major enhancements I will be working on in the foreseeable future will be to analyze those big files in the background as the quick view is displayed to offer in-depth analysis of the data. Typically, a correlation heatmap of each column or a histogram of value occurrences, etc. 44 | 45 | Of course, these are just a few ideas. I want this project to be community-driven, so don't hesitate to tell me where to go from here. What started as a one-man engineering project is turning out to be a shared journey of innovation and collaboration with all. Join the ride! 46 | 47 | [Link to Nanocell-csv](https://www.nanocell-csv.com/) 48 | -------------------------------------------------------------------------------- /web/theme.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --green: #48a741; 3 | --grey: #f7f7f7; 4 | } 5 | 6 | 7 | .grow { 8 | flex-grow: 2; 9 | } 10 | 11 | #install { 12 | position: relative; 13 | } 14 | 15 | #install button { 16 | min-width: 10em; 17 | } 18 | 19 | #version { 20 | position: absolute; 21 | bottom: -1.5em; 22 | width: auto; 23 | left: 0; 24 | right: 0; 25 | color: red; 26 | font-size: .6em; 27 | letter-spacing: 4px; 28 | } 29 | 30 | h1, 31 | h2 { 32 | font-size: 2em; 33 | line-height: 1.3em; 34 | margin: .8em; 35 | } 36 | 37 | h1 { 38 | font-size: 3em; 39 | } 40 | 41 | a { 42 | color: inherit; 43 | text-decoration: none; 44 | } 45 | 46 | a:hover { 47 | text-decoration: underline; 48 | text-underline-offset: 5px; 49 | text-decoration-color: var(--green); 50 | text-decoration-thickness: 2px; 51 | } 52 | 53 | p { 54 | width: 40em; 55 | text-align: justify; 56 | max-width: 90%; 57 | } 58 | 59 | body { 60 | display: flex; 61 | flex-direction: column; 62 | overflow-x: hidden; 63 | width: 100%; 64 | height: 100vh; 65 | font-family: sans-serif; 66 | margin: 0; 67 | text-align: center; 68 | line-height: 1.5em; 69 | } 70 | 71 | #container { 72 | overflow-y: scroll; 73 | overflow-x: hidden; 74 | } 75 | 76 | section { 77 | display: flex; 78 | flex-direction: column; 79 | align-items: center; 80 | justify-content: center; 81 | max-width: 100%; 82 | padding: 10vh 0; 83 | } 84 | 85 | section:nth-child(even) { 86 | background-color: var(--grey); 87 | } 88 | 89 | nav { 90 | display: flex; 91 | justify-content: space-evenly; 92 | box-shadow: 0 1px 5px #fff; 93 | z-index: 999; 94 | letter-spacing: 2px; 95 | flex-wrap: wrap; 96 | cursor: pointer; 97 | } 98 | 99 | .nav_element { 100 | min-width: 10em; 101 | display: flex; 102 | justify-content: center; 103 | align-items: center; 104 | margin: 1em 2em; 105 | flex-wrap: wrap; 106 | } 107 | 108 | #navTitle { 109 | font-size: inherit; 110 | font-weight: inherit; 111 | } 112 | 113 | #navLogo { 114 | display: inline-block; 115 | height: 3em; 116 | margin: 0 .5em; 117 | } 118 | 119 | button , .anchorButton{ 120 | position: relative; 121 | cursor: pointer; 122 | margin: 1em; 123 | background-color: white; 124 | color: var(--green); 125 | border: 2px solid var(--green); 126 | padding: 1em; 127 | border-radius: .5em; 128 | text-align: center; 129 | font-weight: bold; 130 | font-size: inherit; 131 | font-family: inherit; 132 | letter-spacing: inherit; 133 | } 134 | 135 | button:hover, .anchorButton:hover { 136 | box-shadow: var(--green) inset 0 0 2px; 137 | text-decoration: none !important; 138 | } 139 | 140 | 141 | header { 142 | display: flex; 143 | min-height: 90vh; 144 | flex-direction: column; 145 | align-items: center; 146 | } 147 | 148 | #screenshots { 149 | margin: 6vh; 150 | position: relative; 151 | width: 60%; 152 | flex-grow: 2; 153 | height: auto; 154 | } 155 | 156 | #lightShot, 157 | #darkShot { 158 | position: absolute; 159 | height: 80%; 160 | box-shadow: 1px 1px 5px 3px #aaa; 161 | } 162 | 163 | #darkShot { 164 | top: 0; 165 | left: 0; 166 | } 167 | 168 | #lightShot { 169 | z-index: 1; 170 | top: 20%; 171 | right: 0; 172 | } 173 | 174 | #keyWords { 175 | flex-wrap: wrap; 176 | margin: 1em; 177 | display: inline-flex; 178 | justify-content: space-evenly; 179 | align-items: center; 180 | min-height: 10vh; 181 | width: 100%; 182 | letter-spacing: 1px; 183 | line-height: 4em; 184 | } 185 | 186 | 187 | #footer { 188 | min-height: 20vh; 189 | display: flex; 190 | text-align: left; 191 | justify-content: flex-start; 192 | align-items: flex-start; 193 | flex-wrap: wrap; 194 | padding: 3em; 195 | flex-direction: row; 196 | align-content: flex-start; 197 | } 198 | 199 | #footer div{ 200 | /* min-height: 20vh; */ 201 | display: inline-flex 202 | ; 203 | text-align: left; 204 | flex-wrap: wrap; 205 | flex-direction: column; 206 | align-content: flex-start; 207 | width: 20em; 208 | align-items: flex-start; 209 | justify-content: flex-start; 210 | } 211 | 212 | #footer a { 213 | margin: 1em; 214 | } 215 | 216 | #footer img { 217 | height: 2em; 218 | } 219 | 220 | 221 | 222 | #email { 223 | cursor: pointer; 224 | letter-spacing: .07em; 225 | position: relative; 226 | } 227 | 228 | #copy_toast { 229 | position: relative; 230 | display: flex; 231 | bottom: -2em; 232 | color: #333; 233 | opacity: 0; 234 | cursor: default; 235 | height: 0; 236 | width: 100%; 237 | transition: opacity cubic-bezier(0.5, -4, 0.7, 0.07) 2s; 238 | align-items: center; 239 | text-decoration: none!important; 240 | font-size: .8em; 241 | justify-content: center; 242 | } 243 | #copy_toast:hover{ 244 | text-decoration: none!important; 245 | 246 | } 247 | 248 | #copy_toast img { 249 | margin: .2em; 250 | } 251 | 252 | @media (max-width: 1000px) { 253 | 254 | .hide_phone, 255 | .nav_link, 256 | .grow { 257 | display: none !important; 258 | } 259 | 260 | nav, 261 | .nav_element, 262 | #keyWords { 263 | flex-direction: column; 264 | margin-bottom: 0; 265 | } 266 | 267 | #navTitle { 268 | margin-bottom: 0; 269 | } 270 | 271 | #header { 272 | min-height: auto; 273 | } 274 | 275 | #install, 276 | .isMobileDnld { 277 | margin: 0; 278 | border: none; 279 | font-size: .8em; 280 | color: gray; 281 | } 282 | 283 | } -------------------------------------------------------------------------------- /web/img/platform/linux.svg: -------------------------------------------------------------------------------- 1 | Linux icon -------------------------------------------------------------------------------- /misc/seo-pages.md: -------------------------------------------------------------------------------- 1 | # CSV file Data Import Tool 2 | Nanocell-csv is your reliable CSV file data import tool, designed to seamlessly handle data integrity and accuracy when importing large or small datasets. With built-in data validation and offline functionality, it ensures your CSV data remains untouched, whether it's for quick inspection or preparation for database import. Experience hassle-free data management with Nanocell-csv’s commitment to clean and accurate CSV handling. 3 | # CSV file Database Preview and Cleanup Tool 4 | When preparing data for database import, Nanocell-csv acts as the ultimate CSV file database preview tool. Instantly visualize your data, identify inconsistencies, and validate headers and encoding to ensure smooth database integration. With no surprises, you can quickly clean up any issues and proceed with your data workflow without interruptions. 5 | # CSV file Big Data Visualization Tool 6 | Nanocell-csv isn't just about editing—it's also a powerful CSV file data visualization tool. Whether you're dealing with complex datasets or simple tables, this tool provides an instant overview without parsing the entire file. You can quickly identify patterns, detect errors, and prepare data for further analysis, all while ensuring accuracy and integrity. 7 | # CSV file Data Exploration Tool 8 | For data professionals looking to explore their CSV files before diving into heavier analysis tools, Nanocell-csv is the perfect data exploration tool. With its ability to instantly display file previews and sample data, it enables you to quickly assess large datasets, understand the structure, and get to work with confidence—all while maintaining complete data accuracy. 9 | # CSV file Viewer and Editor for Windows 10 | Nanocell-csv brings you a fast, lightweight CSV file viewer and editor designed for Windows users. With a user-friendly interface, you can instantly view and edit CSV files of any size without worrying about unwanted type conversions. Whether you’re cleaning up data or making quick edits, Nanocell-csv delivers a seamless experience on your Windows machine. 11 | # Progressive Web Application CSV file Editor 12 | Nanocell-csv is a Progressive Web Application (PWA) CSV file editor that works offline and across all platforms. Say goodbye to installing heavy software or worrying about data security—Nanocell-csv keeps your files safe and accurate, offering an intuitive editing experience from any device with just a browser. 13 | # PWA CSV file Viewer and Editor 14 | As a PWA CSV file viewer and editor, Nanocell-csv offers an incredibly fast and simple solution for CSV file management. No need for installations or system-specific software—access it through any modern browser and start viewing and editing your CSV files instantly, with the guarantee that your data remains accurate and private. 15 | # Simple CSV file Viewer and Editor 16 | Nanocell-csv is the simplest CSV file viewer and editor you’ll ever need. Whether you're dealing with a massive dataset or just making minor tweaks, this tool is designed for quick and efficient CSV file management. With no complex setup or unnecessary features, Nanocell-csv is all about getting the job done with speed and precision. 17 | # Data Accurate CSV file Viewer and Editor 18 | When data accuracy is crucial, Nanocell-csv is the go-to CSV file viewer and editor. With its focus on preserving data integrity, this tool ensures that all values, including leading zeros and special characters, remain intact. Whether you're editing contact details or zip codes, Nanocell-csv guarantees no unwanted data alterations. 19 | # Open Large CSV files Fast with Nanocell-csv 20 | Speed is key when working with large datasets, and Nanocell-csv is the fastest CSV file viewer and editor available. With instant file previews and quick data validation, you can efficiently explore, edit, and visualize CSV files without waiting for long load times. No matter the file size, Nanocell-csv handles it with ease. 21 | # Open Big CSV files Fast with Nanocell-csv 22 | Speed is key when working with big datasets, and Nanocell-csv is the fastest CSV file viewer and editor available. With instant file previews and quick data validation, you can efficiently explore, edit, and visualize CSV files without waiting for long load times. No matter the file size, Nanocell-csv handles it with ease. 23 | # Open Huge CSV files Fast with Nanocell-csv 24 | Speed is key when working with huge datasets, and Nanocell-csv is the fastest CSV file viewer and editor available. With instant file previews and quick data validation, you can efficiently explore, edit, and visualize CSV files without waiting for long load times. No matter the file size, Nanocell-csv handles it with ease. 25 | # Lightweight CSV file Viewer and Editor 26 | Nanocell-csv is a lightweight CSV file viewer and editor that doesn’t weigh down your system. With a minimalistic design and no installation required, you can view and edit your CSV files quickly and efficiently. Whether on Windows, Mac, or Linux, Nanocell-csv ensures that your CSV file management is always smooth and fast. 27 | # Open source CSV file Viewer and Editor 28 | Nanocell-csv is an open-source CSV file viewer and editor that gives you full control over your data management process. Built by and for data experts, it ensures complete transparency, and you can even contribute to its development. Enjoy the benefits of an open-source tool while ensuring data integrity and security with every use. 29 | # Free CSV file Viewer and Editor 30 | Nanocell-csv is a free CSV file viewer and editor that provides powerful features without the price tag. Whether you need to inspect, edit, or validate CSV data, this tool gives you everything you need without the hassle of paid subscriptions. Keep your data safe, accurate, and accessible with this free solution. -------------------------------------------------------------------------------- /app/js/Dataframe.js: -------------------------------------------------------------------------------- 1 | class Dataframe { 2 | constructor(d = [[""]]) { 3 | this.lock = false; 4 | this.isSaved = true; 5 | this.data = d; 6 | this.undoStack = []; 7 | this.redoStack = []; 8 | this.square(); 9 | // this.solver = new Solver(this); 10 | } 11 | 12 | get(x, y) { return (y >= this.height || x >= this.width || y < 0 || x < 0) ? '' : String(this.data[y][x]) } 13 | 14 | 15 | getAll(cb) { 16 | for (var y = 0; y < this.height; y++)for (var x = 0; x < this.width; x++) cb(this.get(x, y), x, y); 17 | } 18 | 19 | trimAll() { 20 | if (this.lock) return; 21 | for (var x = this.width - 1; x >= 0; x--) { 22 | var emptyCol = true; 23 | for (var y = 0; y < this.data.length; y++) if (this.data[y][x].length > 0) { emptyCol = false; break } 24 | if (emptyCol) this.deleteCol(x); 25 | } 26 | for (var y = this.data.length - 1; y >= 0; y--) { 27 | var emptyRow = true; 28 | for (var x = 0; x < this.data[y].length; x++) if (this.data[y][x].length > 0) { emptyRow = false; break } 29 | if (emptyRow) this.deleteRow(y); 30 | } 31 | } 32 | 33 | order(new_order) { 34 | let old_order = new Array(new_order.length); 35 | for (let i = 0; i < new_order.length; i++) old_order[new_order[i]] = i; 36 | var redo = () => { this.data = new_order.map(index => this.data[index]); } 37 | var undo = () => { this.data = old_order.map(index => this.data[index]); } 38 | this.create(redo, undo); 39 | } 40 | 41 | shiftCol(n) { 42 | if (n < 0 || n + 1 > this.width) return; 43 | if (n + 1 === this.width) return this.insertCol(n); 44 | var redo = () => { for (var row of this.data) { var t = row[n]; row[n] = row[n + 1]; row[n + 1] = t } } 45 | var undo = () => { for (var row of this.data) { var t = row[n]; row[n] = row[n + 1]; row[n + 1] = t } } 46 | this.create(redo, undo); 47 | } 48 | shiftRow(n) { 49 | if (n < 0 || n + 1 > this.height) return; 50 | if (n + 1 === this.height) return this.insertRow(n); 51 | var redo = () => { var t = this.data[n]; this.data[n] = this.data[n + 1]; this.data[n + 1] = t } 52 | var undo = () => { var t = this.data[n]; this.data[n] = this.data[n + 1]; this.data[n + 1] = t } 53 | this.create(redo, undo); 54 | } 55 | deleteRow(n) { 56 | 57 | if (this.height < 2 || n < 0 || n >= this.height) return; 58 | for (var x = 0; x < this.width; x++) this.edit(x, n, ''); 59 | var redo = () => { this.data.splice(n, 1); } 60 | var undo = () => { this.data.splice(n, 0, Array(this.width).fill('')) } 61 | this.create(redo, undo); 62 | } 63 | deleteCol(n) { 64 | if (this.width < 2 || n < 0 || n >= this.width) return; 65 | for (var y = 0; y < this.height; y++) this.edit(n, y, ''); 66 | var redo = () => { for (var row of this.data) row.splice(n, 1) } 67 | var undo = () => { for (var row of this.data) row.splice(n, 0, '') } 68 | this.create(redo, undo); 69 | } 70 | insertCol(n) { 71 | if (n > this.width) return; 72 | var redo = () => { for (var row of this.data) row.splice(n, 0, '') } 73 | var undo = () => { for (var row of this.data) row.splice(n, 1) } 74 | this.create(redo, undo); 75 | } 76 | insertRow(n) { 77 | var redo = () => { this.data.splice(n, 0, Array(this.width).fill('')) } 78 | var undo = () => { this.data.splice(n, 1); } 79 | this.create(redo, undo); 80 | } 81 | 82 | pushCol() { 83 | var redo = () => { for (var row of this.data) row.push('') } 84 | var undo = () => { for (var row of this.data) row.pop() } 85 | this.create(redo, undo); 86 | } 87 | pushRow() { 88 | var redo = () => { this.data.push(Array(this.width).fill('')) } 89 | var undo = () => { this.data.pop() } 90 | this.create(redo, undo); 91 | } 92 | edit(x, y, n) { 93 | if (this.lock) return; 94 | var o = this.get(x, y); 95 | if (stg.autoRound) { 96 | try { 97 | var array = String(n).split('='); 98 | array[array.length - 1] = round(array[array.length - 1], false); 99 | n = array.join('='); 100 | } catch (e) { 101 | console.log(e) 102 | throw new Error("edit error n = " + n) 103 | } 104 | } 105 | if (n === o) return; 106 | while (this.width <= x) this.pushCol(); 107 | while (this.height <= y) this.pushRow(); 108 | var redo = () => this.data[y][x] = n; 109 | var undo = () => this.data[y][x] = o; 110 | this.create(redo, undo); 111 | } 112 | 113 | create(r, u) { 114 | if (this.lock) return; 115 | var action = { t: Date.now(), redo: r, undo: u }; r(); 116 | this.undoStack.push(action); 117 | this.redoStack = []; 118 | this.isSaved = false; 119 | } 120 | 121 | undo() { 122 | var prev, action; 123 | do { 124 | if (this.undoStack.length < 1) return; 125 | action = this.undoStack.pop(); 126 | action.undo(); 127 | this.redoStack.push(action); 128 | prev = this.undoStack[this.undoStack.length - 1]; 129 | } while (prev && action.t - prev.t < Dataframe.MS_DELTA); 130 | this.isSaved = false; 131 | } 132 | redo() { 133 | var prev, action; 134 | do { 135 | if (this.redoStack.length < 1) return; 136 | var action = this.redoStack.pop(); 137 | action.redo(); 138 | this.undoStack.push(action); 139 | var next = this.redoStack[this.redoStack.length - 1]; 140 | } while (next && next.t - action.t < Dataframe.MS_DELTA); 141 | this.isSaved = false; 142 | } 143 | square() { 144 | if (this.lock) return; 145 | var m = 1; 146 | for (var row of this.data) m = Math.max(m, row.length); 147 | for (var row of this.data) while (row.length < m) row.push(""); 148 | } 149 | 150 | get width() { return (this.data.length > 0) ? this.data[0].length : 0 } 151 | get height() { return this.data.length } 152 | 153 | } 154 | 155 | Object.defineProperty(Dataframe, 'MS_DELTA', { value: 100 }); 156 | 157 | 158 | -------------------------------------------------------------------------------- /app/js/Finder.js: -------------------------------------------------------------------------------- 1 | class Finder extends HTMLElement { 2 | constructor(sheet) { 3 | super(); 4 | this.sheet = sheet; 5 | this.found = []; 6 | this.search = ""; 7 | 8 | this.lastSearch = ""; 9 | this.idx = 0; 10 | this.advanced = false; 11 | this.table = new Table(); 12 | var img; 13 | 14 | this.caseSensitive = new BoolInput(false); 15 | this.caseSensitive.style.float = "right" 16 | this.caseSensitive.style.marginRight = ".2em" 17 | 18 | this.findIn = document.createElement("input"); 19 | this.foundInfo = document.createElement("span") 20 | this.replaceIn = document.createElement("input"); 21 | this.caseInfo = document.createElement("span"); 22 | 23 | this.foundInfo.style.width = "10em"; 24 | this.foundInfo.style.cursor = "pointer"; 25 | this.foundInfo.style.display = "inline-block"; 26 | this.foundInfo.style.textAlign = "left"; 27 | 28 | 29 | this.table.br(); 30 | this.table.push(this.caseSensitive); 31 | this.table.push(this.caseInfo); 32 | this.table.br(); 33 | 34 | img = document.createElement("img"); 35 | img.addEventListener('click', e => { this.find() }); 36 | img.style.cursor = "pointer"; 37 | 38 | img.src = "icn/menu/find.svg"; 39 | img.style.marginLeft = "9em" 40 | this.table.push(img); 41 | this.table.push(this.findIn); 42 | this.table.push(this.foundInfo); 43 | this.table.br(); 44 | img = document.createElement("img"); 45 | img.src = "icn/menu/replace.svg"; 46 | img.style.marginLeft = "9em" 47 | 48 | this.table.push(img); 49 | this.table.push(this.replaceIn); 50 | 51 | 52 | this.replaceBtn = document.createElement("button"); 53 | this.replaceBtn.innerText = "Replace All" 54 | this.replaceBtn.style.marginBottom = "1.5em" 55 | this.table.br(); 56 | this.table.push(); 57 | this.table.push(this.replaceBtn); 58 | 59 | this.listTable = new Table(); 60 | 61 | this.appendChild(this.listTable); 62 | 63 | this.appendChild(this.table); 64 | 65 | this.findIn.addEventListener('input', e => { this.find() }); 66 | this.foundInfo.addEventListener('click', e => { this.find() }); 67 | this.replaceBtn.addEventListener('click', e => { this.replaceAll() }); 68 | this.findIn.addEventListener("keydown", e => { 69 | switch (e.key.toUpperCase()) { 70 | case "ENTER": this.find(); break; 71 | case "TAB": this.replaceIn.focus(); break; 72 | } 73 | }); 74 | this.replaceIn.addEventListener("keydown", e => { 75 | switch (e.key.toUpperCase()) { 76 | case "ENTER": this.replaceAll(); break; 77 | case "TAB": this.findIn.focus(); break; 78 | } 79 | }); 80 | this.caseSensitive.onchange = e => { 81 | this.caseInfo.innerHTML = this.caseSensitive.value ? "A ≠ a" : "A = a"; 82 | this.find(true) 83 | } 84 | 85 | this.caseInfo.innerHTML = "A ≠ a"; 86 | 87 | this.listTable.style.maxHeight = "20em"; 88 | 89 | this.listTable.classList.add("scroll"); 90 | this.listTable.style.margin = "1em"; 91 | this.listTable.style.display = "inline-block"; 92 | this.table.style.display = "inline-block"; 93 | } 94 | showTable() { 95 | var i = 0; 96 | this.listTable.style.display = "block"; 97 | while (this.listTable.rows.length > 0) this.listTable.rows[0].remove(); 98 | for (var e of this.found) { 99 | i++; 100 | if (i > 500) return; 101 | this.listTable.br(); 102 | this.listTable.push(e.x + 1); 103 | this.listTable.push(e.y + 1); 104 | this.listTable.push(e.v.replace(this.exp, "" + this.search + "")); 105 | 106 | 107 | } 108 | } 109 | find(force = false) { 110 | 111 | this.listTable.style.display = "none"; 112 | this.search = this.findIn.value; 113 | if (this.search.length < 1) { 114 | this.lastSearch = this.search; 115 | this.found = []; 116 | } else if (this.lastSearch === this.search && !force) { 117 | this.idx = (this.idx + 1) % this.found.length; 118 | } else { 119 | this.lastSearch = this.search; 120 | this.idx = 0; 121 | this.found = []; 122 | this.exp = new RegExp(this.search, (this.caseSensitive.value && this.advanced) ? 'g' : 'gi'); 123 | var yStart = 0; 124 | var xStart = 0; 125 | var yEnd = this.sheet.df.height - 1; 126 | var xEnd = this.sheet.df.width - 1; 127 | for (var y = yStart; y <= yEnd; y++)for (var x = xStart; x <= xEnd; x++) { 128 | var v = this.sheet.df.get(x, y); 129 | this.exp.lastIndex = 0; 130 | if (this.exp.test(v)) this.found.push({ x: x, y: y, v: v }); 131 | } 132 | 133 | 134 | } 135 | var info = (this.found.length === 0) ? "No match" : (this.idx + 1) + ' / ' + this.found.length; 136 | this.foundInfo.innerHTML = info; 137 | if (this.found.length > 0) { 138 | sheet.x = this.found[this.idx].x; 139 | sheet.y = this.found[this.idx].y; 140 | sheet.slctRefresh(); 141 | } 142 | 143 | } 144 | 145 | findMenu(prefill = "", adv = false) { 146 | this.listTable.style.display = "none"; 147 | this.advanced = adv; 148 | if (this.advanced) for (var row of this.table.rows) row.style.display = "table-row"; 149 | else for (var row of this.table.rows) if (row.rowIndex !== 1) row.style.display = "none"; 150 | if (prefill.length > 0) this.findIn.value = prefill; 151 | dom.dialog.push(this); 152 | this.findIn.focus(); 153 | if (prefill.length > 0) this.find(false) 154 | } 155 | 156 | replaceAll() { 157 | if (this.search.length < 1) return; 158 | for (var e of this.found) { 159 | this.exp.lastIndex = 0; 160 | this.sheet.df.edit(e.x, e.y, e.v.replace(this.exp, this.replaceIn.value)); 161 | } 162 | this.sheet.refresh(); 163 | this.sheet.slctRefresh(focus = true); 164 | this.find(true); 165 | } 166 | 167 | } 168 | 169 | customElements.define('ui-finder', Finder); 170 | -------------------------------------------------------------------------------- /app/sw_read_write_csv.js: -------------------------------------------------------------------------------- 1 | const CHUNK_SIZE = 500 * 1000; // = 500ko 2 | const n_chars_for_separator_detection = 500; 3 | 4 | 5 | separatorDetection = function (txt) { 6 | if (txt.length > n_chars_for_separator_detection) txt = txt.substring(0, n_chars_for_separator_detection) 7 | d = [',', '\t', ';', ':', '|'] 8 | n = [0, 0, 0, 0] 9 | for (var i = 0; i < txt.length; i++) { 10 | for (var j = 0; j < d.length; j++) { 11 | if (txt[i] == d[j]) n[j]++; 12 | } 13 | } 14 | return d[n.indexOf(Math.max(...n))] 15 | } 16 | 17 | csv_parse = function (s, d = ",") { 18 | var rows = []; 19 | var lr = '\n' 20 | var v = []; //value characters; 21 | var q = '"'; //quote 22 | var f = false; //force 23 | var len = s.length; 24 | var c, j; 25 | for (var i = 0; i < len; i++) { 26 | c = s[i]; 27 | if (c === ' ') continue; 28 | if (c === d) { v.push(""); continue } 29 | if (c === q) { f = true; i++ } 30 | j = i; 31 | if (f) while (j < len && s[j] !== q || s[j] === q && s[j + 1] === q) { 32 | if (s[j] === q && s[j + 1] === q) j++; 33 | j++; 34 | } else { 35 | while (j < len && s[j] !== d && s[j] !== lr) j++; 36 | while (j > i && (s[j - 1] === ' ' || s[j - 1] === '\r' ) ) j--; 37 | } 38 | v.push(s.substring(i, j).replace(/""/g, '"')); 39 | if (f) j++; 40 | i = j; 41 | while (i < len && s[i] !== d && s[i] !== lr) i++; 42 | f = false; 43 | if (s[i] === lr || i === len) { 44 | rows.push(v); 45 | v = [] 46 | } 47 | } 48 | if (s[len-1]=== '\n') rows.push([[""]]) 49 | if (v.length >0) rows.push(v); 50 | return rows; 51 | } 52 | 53 | 54 | csv_parse1 = function (txt, d = ",") { 55 | return txt.split(/[;\r]?\n/).map(s => { 56 | var r = []; //result; 57 | var q = '"'; //quote 58 | var f = false; //force 59 | var len = s.length; 60 | var c, j; 61 | for (var i = 0; i < len; i++) { 62 | c = s[i]; 63 | if (c === ' ') continue; 64 | if (c === d) { r.push(""); continue } 65 | if (c === q) { f = true; i++ } 66 | j = i; 67 | if (f) while (j < len && s[j] !== q || s[j] === q && s[j + 1] === q) { 68 | if (s[j] === q && s[j + 1] === q) j++; 69 | j++; 70 | } else { 71 | while (j < len && s[j] !== d) j++; 72 | while (j > i && s[j - 1] === ' ') j--; 73 | } 74 | r.push(s.substring(i, j).replace(/""/g, '"')); 75 | if (f) j++; 76 | i = j; 77 | while (i < len && s[i] !== d) i++; 78 | f = false; 79 | } return r; 80 | }) 81 | } 82 | 83 | loadcsv = function (data) { 84 | if (data.viewOnly) return load_csv_view_only(data); 85 | let file = data.file; 86 | console.log("reading chunk size : ", CHUNK_SIZE) 87 | console.log("sw loading : ", file.name) 88 | let fileSize = file.size; 89 | let offset = 0 90 | let iteration = 0; 91 | let sep = ';' 92 | let rowCount = 0; 93 | let prepend = ""; 94 | let reader = new FileReader(); 95 | reader.onloadend = e => { 96 | iteration++; 97 | let result = e.target.result; 98 | let status = offset / fileSize; 99 | if (iteration == 1) sep = separatorDetection(result); 100 | else result = prepend + result; 101 | if (status < 1) { 102 | let increment = result.length - 1 103 | while (result[increment] != '\n' && increment > 0) increment--; 104 | if (increment > 0) { 105 | prepend = result.slice(increment + 1) 106 | result = result.slice(0, increment) 107 | } else { 108 | console.log("Warning : case where a row seems longer than the chunk loaded size") 109 | prepend = result; 110 | return seek() 111 | } 112 | } 113 | let matrix = csv_parse(result, sep); 114 | rowCount += matrix.length; 115 | postMessage({ 116 | cmd: "chunk_loaded", 117 | status: status, 118 | chunk: matrix, 119 | chunk_id: iteration, 120 | viewOnly: false, 121 | sep: sep, 122 | rowCount: rowCount 123 | }) 124 | if (offset / fileSize < 1) seek() 125 | }; 126 | seek = function () { 127 | reader.readAsText(file.slice(offset, offset + CHUNK_SIZE), "utf-8"); 128 | offset += CHUNK_SIZE; 129 | } 130 | seek() 131 | } 132 | 133 | load_csv_view_only = function (data) { 134 | let file = data.file; 135 | console.log("reading chunk size : ", CHUNK_SIZE); 136 | console.log("sw loading view only : ", file.name); 137 | let fileSize = file.size; 138 | let sep = ';' 139 | let reader = new FileReader(); 140 | let iteration = 0; 141 | let vo_n_chunks = data.n_chunks; 142 | let vo_n_rows = data.n_rows; 143 | let lastIteration = vo_n_chunks; 144 | reader.onloadend = e => { 145 | let result = e.target.result; 146 | let status = iteration / vo_n_chunks; 147 | if (iteration == 1) sep = separatorDetection(result); 148 | let matrix = csv_parse(result, sep); 149 | if (iteration == 1) matrix = matrix.slice(0, vo_n_rows); 150 | else if (iteration == lastIteration) matrix = matrix.slice(- vo_n_rows); 151 | else matrix = matrix.slice(1, vo_n_rows + 2); 152 | if (iteration != 1) { 153 | matrix[0] = [] 154 | for (var i = 0; i < matrix[1].length; i++) matrix[0].push("! [...] !") 155 | } 156 | 157 | postMessage({ 158 | cmd: "chunk_loaded", 159 | status: status, 160 | chunk: matrix, 161 | chunk_id: iteration, 162 | viewOnly: true, 163 | sep: sep, 164 | rowCount: 0 165 | }) 166 | if (iteration < lastIteration) seek() 167 | }; 168 | 169 | seek = function () { 170 | iteration++; 171 | let offset = (iteration - 1) * (fileSize / vo_n_chunks); 172 | if (iteration == lastIteration) reader.readAsText(file.slice(file.size - CHUNK_SIZE, file.size), "utf-8"); 173 | else reader.readAsText(file.slice(offset, offset + CHUNK_SIZE), "utf-8"); 174 | } 175 | 176 | seek() 177 | } 178 | 179 | addEventListener("message", e => { 180 | switch (e.data.cmd) { 181 | case "read": loadcsv(e.data.data) 182 | } 183 | }) 184 | 185 | -------------------------------------------------------------------------------- /article/about-csv-files.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # Why Does Editing CSV Files Always Feel So Difficult? 6 | 7 | > CSV files are the backbone of data exchange—simple, universal, and incredibly versatile. It's not flashy, it's not trendy, and yet, it is everywhere. But if CSV files are so simple, important and universal then why do they feel so painful to work with? 8 | 9 | ## What Are CSV Files (a quick reminder) ? 10 | 11 | The origins of the CSV file date back to the early days of computing when simplicity was key. Developers needed a lightweight, platform-agnostic way to store and share tabular data, and the CSV was born. 12 | 13 | CSV stands for **Comma-Separated Values**. At its core, it's a simple text file where data is organized into rows and columns, with commas acting as the dividers. For example: 14 | 15 | ```csv 16 | Name, Age, Favorite Food 17 | Alice, 30, Pizza 18 | Bob, 25, Sushi 19 | ``` 20 | 21 | > A text file of tabular data 22 | > Columns of each row are delimited by commas 23 | > Hence the name, CSV: Comma-Separated Values 24 | 25 | ## What Are CSV Files Used for Today ? 26 | 27 | Fast forward to today, and CSV files are everywhere. They are a cornerstone of the digital world, acting as a universal format for data portability across systems. Whether it's accessing massive datasets from the company cloud, or transferring contacts from a phone, CSV files have you covered. Widely used in the ETL (Extract, Transform, Load) field, they enable seamless data transfer between databases with differing proprietary formats. data-engineers take advantage of their simplicity for storing and retrieving tabular data as backups. CSV files integrate well with tools like Excel, Python, or R for data-analists and data-scientists use them as input to machine learning models. Many APIs offer CSV as an exchange format due to its simplicity, and their text-based structure makes them a popular choice on platforms like GitHub, where developers use them to store configurations or small datasets in repositories. Finally, they are the go-to file format when trying to broadcast data publicly. 28 | 29 | > Database import/export, backup & API 30 | > Input format to data analysis/modeling tools 31 | > Configuration tables for code 32 | > ... Just about anything 33 | 34 | ![csv file as atlas god](https://www.nanocell-csv.com/img/meme/csv-data-atlas-pillar.webp) 35 | 36 | 37 | ## Why do CSV files remain truly indispensable? 38 | 39 | What makes the CSV file truly indispensable is its combination of simplicity, age-old reliability, and non-proprietary nature. There's beauty in its straightforwardness. You don't need fancy software to open, read and understand a CSV file—the most lightweight text editors will do (notyepad, fim, emacs...). You can even check their content from the system's command line: `cat ./path-to/filename.csv`. It's the only file format that is guaranteed to interface seamlessly with any existing data handling software, from legacy systems to cutting-edge platforms. Sure, XML and JSON formats have their moments in the spotlight, but when it comes to reliability that will stand the test of time, the CSV is unmatched. 40 | 41 | > Simple 42 | > Reliable 43 | > Timeless 44 | > Human-Readable 45 | > Universal 46 | 47 | ## Common Problems Working with CSV Files 48 | 49 | Working with CSVs isn't always smooth sailing. Here are some common gripes: 50 | 51 | **Encoding Nightmares:** Non-ASCII characters (such as é and ü) can wreak havoc if encoding isn't detected properly. It is worth mentioning that the recommended default encoding for CSV files is *utf-8*. 52 | **Comma Confusion:** What happens when your data values contain commas? (Spoiler: It can get messy if not handled with the right standard.) 53 | **Standard misalignment:** Over time, people have tweaked the CSV standards to better fit their needs. An example of this is in european countries where the commas were already used for in decimal numbers (instead of dots) so they decided to use a semicolon as the column delimiter instead. 54 | **Misuse of Excel:** Microsoft Excel is a great data analysis tool but poorly suited for CSV files. This holds especially true in the fields of ETL and data-engineering. It often misinterprets CSV standards or value data-types. This often ends up in data corruption and even more so when users have the *auto-save* mode activated. Furthermore, CSV files are often database extracts that are too large to be handled by Excel making it feel clunky or even crash upon opening a file. What holds for Excel is also true of other major spreadsheet editors such as Google Sheets, Libre Office Calc, or Mac's Numbers. 55 | 56 | 57 | ## Essential Yet Painful to work with – Why? 58 | 59 | If CSV files are so important and universal then why does it always feels unnecessarily complicated to open them in a any spreadsheet editor? 60 | 61 | Here is a hypothesis: 62 | 1. A spreadsheet editor needs a fancy feature to differentiate itself and be competitive. 63 | 2. Fancy features require complexity that CSV files can't handle. 64 | 3. Editors come up with a proprietary file format to enable this differenciation. 65 | 4. Editors leave out CSV file handling (more or less purposly) to push their user base towards their proprietary format. 66 | 5. Users can't find a descent CSV file editor. 67 | 68 | 69 | ## The solution: An alternative spreadsheet editor dedicated to CSV files 70 | 71 | [Nanocell-csv](https://www.nanocell-csv.com/) is a free, cross platform, spreadsheet editor dedicated to CSV files. Its source code is available on [Github](https://github.com/CedricBonjour/nanocell-csv) to be community driven. [Nanocell-csv](https://www.nanocell-csv.com/), pledges to focus only on CSV files and their real-world use cases. 72 | 73 | > The [Nanocell-csv](https://www.nanocell-csv.com/) file editor strives to embrace CSV core values: 74 | > - Simple 75 | > - Reliable 76 | > - Universal 77 | 78 | ![nanocell-logo](https://www.nanocell-csv.com/img/screenshot/screenshot_light_logo.webp) 79 | 80 | Find out more at [https://www.nanocell-csv.com/](https://www.nanocell-csv.com/) -------------------------------------------------------------------------------- /misc/csv_files/demo_r100.csv: -------------------------------------------------------------------------------- 1 | name,phone,age,company,country,salary,balance, 2 | Alexander,+33600584103,45,Roche,France,10286.06,-2366.48, 3 | Amanda,085937069,57,Alphabet,Armenia,16536.09,3477.84, 4 | Amy,024580073,30,ASML Holding,Kuwait,1792.37,4450.81, 5 | Andrew,+1061073683,27,The Home Depot,Mongolia,903.01,6573.71, 6 | Angela,013912881,20,Broadcom,Iraq,7524.86,-1787.09, 7 | Anna,086078291,68,Shell,Latvia,15945.67,-1370.95, 8 | Anthony,068080263,41,Nestle,Monaco,1215.60,6978.78, 9 | Ashley,038934774,34,Roche,Antigua and Barbuda,12687.12,-4863.50, 10 | Barbara,044406764,67,Walmart,Seychelles,16008.46,-4179.61, 11 | Benjamin,+2095979763,42,The Home Depot,Belarus,7300.27,8113.83, 12 | Betty,+1004483000,21,Tencent Holdings,Albania,19997.16,2743.36, 13 | Brandon,012611801,61,Procter & Gamble,Mali,11453.29,8415.97, 14 | Brenda,099170974,28,Thermo Fisher Scientific,Benin,14707.84,4439.14, 15 | Brian,043819160,44,Broadcom,Eswatini,13711.52,-5894.95, 16 | Carol,074058749,65,Alibaba Group,Iraq,15634.61,4420.61, 17 | Carolyn,074838737,19,Alphabet,Democratic Republic of the Congo,10043.75,-687.50, 18 | Catherine,080722348,27,Meta Platforms,Slovakia,3587.48,5386.23, 19 | Charles,027019217,62,Samsung Electronics,Ukraine,17728.44,3584.56, 20 | Christine,077742928,29,ASML Holding,Paraguay,2488.20,-8093.78, 21 | Christopher,051258271,37,Visa,Thailand,16760.53,-8959.67, 22 | Cynthia,031125044,38,Tencent Holdings,Kyrgyzstan,13443.22,7584.52, 23 | Daniel,084159733,33,LVMH,Egypt,16118.26,6978.73, 24 | David,092193294,23,Reliance Industries,Timor-Leste,8135.46,5880.68, 25 | Deborah,015681667,45,Bank of America,Marshall Islands,9472.73,3682.65, 26 | Debra,023737623,49,Intel,Chad,11485.57,401.03, 27 | Dennis,021274760,47,Chevron,Nicaragua,15398.37,7864.94, 28 | Donald,090425065,33,Visa,Senegal,10670.29,-2530.50, 29 | Donna,016831094,68,TotalEnergies,Mozambique,8969.76,6912.52, 30 | Dorothy,076072034,21,Coca-Cola,Lithuania,8706.08,-6471.84, 31 | Edward,016963163,63,Eli Lilly and Co.,Eritrea,6670.05,3293.30, 32 | Elizabeth,041277390,55,Apple,Libya,3124.08,1849.13, 33 | Emily,080366191,46,Johnson & Johnson,Brunei,9099.62,-5660.31, 34 | Emma,035073478,36,ICBC,Malta,2490.51,6763.29, 35 | Eric,002381126,31,Taiwan Semiconductor Manufacturing Company,United Arab Emirates,18101.91,-8010.29, 36 | Frank,053783338,60,Kweichow Moutai,Spain,14074.90,-3922.18, 37 | Gary,093149052,41,Bank of America,Papua New Guinea,13253.97,-1695.55, 38 | George,034118004,22,L'Oreal,Bangladesh,2166.59,-7850.78, 39 | Gregory,016719453,37,Pfizer,Barbados,13642.40,6210.17, 40 | Helen,020357496,26,Taiwan Semiconductor Manufacturing Company,Lithuania,19972.49,419.54, 41 | Jack,010798833,56,Coca-Cola,Philippines,11413.65,5863.68, 42 | Jacob,056835393,30,Microsoft,Korea,18016.38,8950.16, 43 | James,078417863,69,Taiwan Semiconductor Manufacturing Company,Kenya,5575.92,-3772.14, 44 | Janet,087754462,31,Berkshire Hathaway,Barbados,14497.98,1383.62, 45 | Jason,026582219,60,Meta Platforms,Cyprus,13156.56,996.26, 46 | Jeffrey,015288426,24,TotalEnergies,Peru,2835.42,-684.78, 47 | Jennifer,087731541,30,ASML Holding,Trinidad and Tobago,11201.04,2284.18, 48 | Jerry,042425186,51,Johnson & Johnson,Nigeria,2181.79,-4107.47, 49 | Jessica,097184348,69,The Home Depot,Nauru,11770.63,7934.85, 50 | John,076676554,26,Apple,Eswatini,15518.04,-31.87, 51 | Jonathan,041468920,68,Thermo Fisher Scientific,Seychelles,17013.94,6797.04, 52 | Joseph,093092696,64,Saudi Aramco,Mali,5678.25,-7280.02, 53 | Joshua,096066756,19,AstraZeneca,Uruguay,4494.28,557.99, 54 | Justin,025388710,22,BHP Group,Costa Rica,16128.52,1009.09, 55 | Karen,065382699,54,Roche,Pakistan,18905.72,4307.46, 56 | Katherine,011320279,32,Shell,Jamaica,8604.97,-8654.79, 57 | Kathleen,097091539,56,PepsiCo,Central African Republic,18129.78,-4041.13, 58 | Kenneth,056680546,29,Microsoft,Trinidad and Tobago,2816.52,-5787.97, 59 | Kevin,036283826,58,Walmart,Serbia,3800.38,6476.20, 60 | Kimberly,040058991,35,Shell,Micronesia,12444.52,4690.76, 61 | Larry,055061443,68,L'Oreal,Gabon,1292.69,-1477.81, 62 | Laura,004647147,66,Pfizer,Dominican Republic,18814.28,3921.39, 63 | Linda,047244661,48,ICBC,Indonesia,15399.81,8815.00, 64 | Lisa,009356538,18,Reliance Industries,Netherlands,8121.53,6002.95, 65 | Margaret,061627718,22,TotalEnergies,Panama,2720.13,9891.61, 66 | Mark,013185562,54,Taiwan Semiconductor Manufacturing Company,Panama,1709.05,6279.66, 67 | Mary,017882186,67,Berkshire Hathaway,Panama,6443.92,4586.84, 68 | Matthew,070209651,19,Chevron,Kosovo,3749.81,4421.09, 69 | Melissa,074496434,28,The Home Depot,Brunei,13853.23,-135.86, 70 | Michael,092295803,22,JPMorgan Chase,Burkina Faso,19812.48,-9914.61, 71 | Michelle,065024212,25,Toyota Motor Corp,Saudi Arabia,18734.92,-3584.28, 72 | Nancy,076543096,46,Broadcom,Mauritania,2840.41,660.46, 73 | Nicholas,018057238,39,Mastercard,South Africa,4472.83,5573.89, 74 | Nicole,076582346,51,Samsung Electronics,Mexico,14430.03,2592.15, 75 | Pamela,054100687,50,ICBC,Republic of Korea,2648.02,-5237.95, 76 | Patricia,088854852,66,ICBC,Burundi,11468.05,1813.34, 77 | Patrick,054999858,61,SAP SE,Netherlands,17728.39,6049.95, 78 | Paul,059601374,21,Toyota Motor Corp,Bulgaria,2674.14,-417.14, 79 | Rachel,086184214,20,LVMH,Timor-Leste,19041.84,7372.49, 80 | Raymond,042214409,43,Amazon,Bulgaria,7058.79,5089.08, 81 | Rebecca,045737923,63,Apple,Denmark,11222.07,1077.72, 82 | Richard,024916025,22,Meta Platforms,Vanuatu,10684.35,6325.42, 83 | Robert,031212645,30,Merck & Co.,Jordan,12704.90,-6642.69, 84 | Ronald,000922449,56,Nestle,Sri Lanka,13814.96,-7827.37, 85 | Ruth,067132692,40,Novo Nordisk,Guinea,7649.62,-931.84, 86 | Ryan,009668871,30,Roche,Chad,19398.39,3828.55, 87 | Samantha,059948593,57,Tesla,India,4815.96,-6563.62, 88 | Samuel,050542953,30,Procter & Gamble,Gambia,10651.84,8683.98, 89 | Sandra,007427279,65,Mastercard,Croatia,12147.14,5470.88, 90 | Sarah,067028599,35,TotalEnergies,Japan,17265.98,-9253.97, 91 | Scott,077907959,39,SAP SE,Burma,18830.29,4208.24, 92 | Sharon,055286217,54,Samsung Electronics,Jamaica,5888.97,2996.67, 93 | Shirley,043971073,52,Toyota Motor Corp,Belize,5135.46,-2972.26, 94 | Stephanie,025023315,19,Reliance Industries,Romania,16205.29,3976.65, 95 | Stephen,016073322,29,Bank of America,Slovenia,7361.64,-3019.94, 96 | Steven,051443596,53,Intel,Afghanistan,8085.42,-7125.94, 97 | Susan,077034746,21,LVMH,Cote d’Ivoire,6116.17,9930.62, 98 | Thomas,099796418,65,ASML Holding,South Sudan,8522.37,6242.32, -------------------------------------------------------------------------------- /app/js/cmd.js: -------------------------------------------------------------------------------- 1 | const cmd = { 2 | about :{k:"H" ,ctrl:true, run(){new About()}, description:"About"}, 3 | new :{k:"N" ,ctrl:true, run(){csvHandle.new()}, description:"New sheet"}, 4 | deleteRow :{k:"BACKSPACE",ctrl:true, run(){sheet.deleteRows()}, description:"Delete Row"}, 5 | deleteCol :{k:"BACKSPACE",ctrl:true,shift:true, run(){sheet.deleteCols()}, description:"Delete Col"}, 6 | delete :{k:"BACKSPACE",run(){sheet.rangeEdit('');sheet.refresh() }, description:"Delete Selection"}, 7 | delete2 :{k:"DELETE",run(){sheet.rangeEdit('');sheet.refresh() }, description:"Delete Selection"}, 8 | settings :{k:"G" ,ctrl:true, run(){Setting.show()}, description:"Display Settings"}, 9 | shortcuts :{k:"K" ,ctrl:true, run(){new Shortcuts()}, description:"Display Shortcuts"}, 10 | slctAll :{k:"A" ,ctrl:true, run(){sheet.slctAll()}, description:"Select All"}, 11 | transpose :{k:"T" ,ctrl:true, shift:true, run(){sheet.rangeTranspose();sheet.refresh()}, description:"Transpose Selection"}, 12 | trim :{k:"T" ,ctrl:true, shift:true, run(){sheet.df.trimAll();sheet.refresh()}, description:"Trim : remove all empty rows/cols"}, 13 | integer :{k:"I" ,ctrl:true, run(){sheet.round(true);sheet.refresh()}, description:"Round selection to integer"}, 14 | decimal :{k:"$" ,ctrl:true, run(){sheet.round(false);sheet.refresh()}, description:"Round selection to decimal"}, 15 | fixTop :{k:"B" ,ctrl:true, run(){sheet.fixTop = !sheet.fixTop;sheet.refresh()}, description:"Fix Header Top"}, 16 | fixLeft :{k:"B" ,ctrl:true, shift:true, run(){sheet.fixLeft = !sheet.fixLeft;sheet.refresh()}, description:"Fix Header Left"}, 17 | fit_width :{k:"W" ,ctrl:true, run(){sheet.fitWidth();sheet.refresh()}, description:"Fix Header Left"}, 18 | undo :{k:"Z" ,ctrl:true, run(){sheet.df.undo();sheet.refresh()}, description:"Undo"}, 19 | redo :{k:"Z" ,ctrl:true, shift:true, run(){sheet.df.redo();sheet.refresh()}, description:"Redo"}, 20 | redo2 :{k:"Y" ,ctrl:true, run(){sheet.df.redo();sheet.refresh()}, description:"Redo"}, 21 | date :{k:"T" ,ctrl:true, run(){sheet.rangeEdit( (new Date()).getFormated("yyyy-mm-dd") );sheet.refresh() }, description:"Insert today's date"}, 22 | find :{k:"F" ,ctrl:true, run(){sheet.finder.findMenu(sheet.getSlctFirstValue(),false); sheet.scrollbarRefresh();}, description:"Quick find / match"}, 23 | findAdvanced :{k:"F" ,ctrl:true, shift:true,run(){sheet.finder.findMenu(sheet.getSlctFirstValue(),true)}, description:"Advanced find / replace (work in progress)"}, 24 | menubar :{k:"M" ,ctrl:true, run(){stg.actionBar = !stg.actionBar }, description:"Toggle action bar display"}, 25 | open :{k:"O" ,ctrl:true, run(){csvHandle.open()}, description:"Open a CSV file from the file finder"}, 26 | save :{k:"S" ,ctrl:true, run(){csvHandle.save()}, description:"Save"}, 27 | saveAs :{k:"S" ,ctrl:true, shift:true, run(){csvHandle.saveAs()}, description:"Save As"}, 28 | reloadFile :{k:"R" ,ctrl:true, run(){csvHandle.reloadFile()}, description:"Reload file from last save"}, 29 | expand :{k:"E" ,ctrl:true, run(){sheet.expand()}, description:"Expand first row to selection"}, 30 | validate_data :{k:"P" ,ctrl:true, run(){sheet.validate_data()}, description:"Validate and format data to respect csv standards"}, 31 | validate_headers:{k:"H" ,ctrl:true, shift:true, run(){sheet.validate_headers()}, description:"Validate and format header to respect SQL standards"}, 32 | next_occurance :{k:"D" ,ctrl:true, run(){sheet.go_to_next()}, description:"Go to next occurence of cell value"}, 33 | sort :{k:"L" ,ctrl:true, run(){sheet.sort(sheet.x, true)}, description:"Sort rows based on active column (ascending order)"}, 34 | sort_reverse :{k:"L" ,ctrl:true, shift:true, run(){sheet.sort(sheet.x, false)}, description:"Sort rows based on active column (descending order)"}, 35 | 36 | 37 | shiftUp :{k:"ARROWUP" ,alt:true, run(dir){sheet.shift(0)}, description:"Shift row up"}, 38 | shiftDown :{k:"ARROWDOWN" ,alt:true, run(dir){sheet.shift(2)}, description:"Shift row down"}, 39 | shiftRight :{k:"ARROWRIGHT" ,alt:true, run(dir){sheet.shift(1)}, description:"Shift col right"}, 40 | shiftLeft :{k:"ARROWLEFT" ,alt:true, run(dir){sheet.shift(3)}, description:"Shift col left"}, 41 | 42 | 43 | insertUp :{k:"ARROWUP" ,alt:true, shift:true, run(dir){sheet.insert(0)}, description:"Insert row above"}, 44 | insertDown :{k:"ARROWDOWN" ,alt:true, shift:true, run(dir){sheet.insert(2)}, description:"Insert row below"}, 45 | insertRight :{k:"ARROWRIGHT" ,alt:true, shift:true, run(dir){sheet.insert(1)}, description:"Insert col right"}, 46 | insertLeft :{k:"ARROWLEFT" ,alt:true, shift:true, run(dir){sheet.insert(3)}, description:"Insert col left"}, 47 | 48 | // the following is just for documentation formating but does nothing 49 | scrollLeft :{k:"scroll ARROWUP " ,alt:true, description:"Scroll left"}, 50 | scrollRight :{k:"scroll ARROWDOWN " ,alt:true, description:"Scroll right"}, 51 | 52 | 53 | } 54 | 55 | 56 | function buildCommands() { 57 | for (var c of Object.values(cmd)) { 58 | if (!c.ctrl) c.ctrl = false; 59 | if (!c.shift) c.shift = false; 60 | if (!c.alt) c.alt = false; 61 | } 62 | } 63 | 64 | 65 | function buildMenu() { 66 | var menuItems = [ 67 | "new", "open", "save", "reloadFile", "", 68 | "undo", "redo", "fixLeft", "fixTop","fit_width", "sort", "sort_reverse", "transpose", "trim", "date", "integer", "decimal","validate_headers", "validate_data", 69 | "", "find", "about", "settings", "shortcuts"]; 70 | function buildMenuItem(item) { 71 | if (item === "") return dom.header.appendChild(document.createElement("hr")); 72 | var img = document.createElement("img"); 73 | img.src = "icn/menu/" + item + ".svg"; 74 | img.setAttribute("title", item); 75 | img.addEventListener("click", function () { cmd[item].run() }) 76 | dom.header.appendChild(img); 77 | } 78 | for (var m of menuItems) buildMenuItem(m); 79 | 80 | } 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /app/js/Setting.js: -------------------------------------------------------------------------------- 1 | var stg = {}; 2 | 3 | 4 | class Setting { 5 | constructor(s) { 6 | var stored_val = localStorage.getItem(s.key); 7 | if (!(isNaN(stored_val) || stored_val == null)) stored_val = Number(stored_val); 8 | if (stored_val == "true") stored_val = true; 9 | if (stored_val == "false") stored_val = false; 10 | this.key = s.key; 11 | this.value = (stored_val === null) ? s.dflt : stored_val; 12 | this.cb = s.cb; 13 | Object.defineProperty(stg, this.key, { 14 | get: () => { return this.value }, 15 | set: (e) => { 16 | this.value = e; 17 | localStorage.setItem(this.key, e); 18 | if (this.cb) this.cb(this.value); 19 | } 20 | }); 21 | if(s.key=="theme" && stored_val===null && window.matchMedia('(prefers-color-scheme: dark)').matches) this.value = "night"; 22 | } 23 | 24 | 25 | 26 | static init(cb) { for (var s of Setting.list) if (!s.title) new Setting(s); } 27 | 28 | 29 | static build(setting) { 30 | var row = document.createElement("tr"); 31 | var name = document.createElement("td"); 32 | if (setting.title) { 33 | var title = document.createElement("h3"); 34 | title.innerHTML = setting.title; 35 | name.appendChild(title); 36 | row.appendChild(name); 37 | return row; 38 | } 39 | 40 | 41 | var inputCell = document.createElement("td"); 42 | name.innerHTML = setting.name; 43 | var input = undefined; 44 | if (setting.list) input = new ListInput(setting.list, setting.hide); 45 | else if (setting.max) { 46 | input = new NumInput(setting.dflt, setting.min, setting.max); 47 | } else if (typeof setting.dflt === "boolean") { 48 | input = new BoolInput(); 49 | } 50 | 51 | 52 | if (input === undefined) { 53 | input = document.createElement("span"); 54 | input.innerText = stg[setting.key]; 55 | } else { 56 | input.value = stg[setting.key]; 57 | input.onchange = e => { var c = e.target.value; stg[setting.key] = isNaN(c) ? c : Number(c) } 58 | } 59 | inputCell.appendChild(input); 60 | row.appendChild(name); 61 | row.appendChild(inputCell); 62 | return row; 63 | } 64 | 65 | 66 | static show() { 67 | var content = document.createElement("div"); 68 | var title = document.createElement("h1") 69 | title.innerHTML = "Settings"; 70 | content.appendChild(title); 71 | content.style.margin = "2em"; 72 | content.classList.add("stg"); 73 | var table = document.createElement("table"); 74 | for (var s of Setting.list) table.appendChild(Setting.build(s)); 75 | var b = document.createElement("button"); 76 | b.innerHTML = "Reset to default settings"; 77 | b.style.marginTop = "1em" 78 | b.onclick = Setting.resetDefault; 79 | content.appendChild(table); 80 | content.appendChild(b); 81 | dom.dialog.push(content, true); 82 | } 83 | 84 | 85 | static setTheme() { 86 | dom.theme.href = "css/themes/" + stg.theme + ".css"; 87 | dom.palette.href = "css/palettes/" + stg.theme + ".css"; 88 | } 89 | 90 | static log() { 91 | for (var i = 0; i < localStorage.length; i++) 92 | console.log(localStorage.key(i), " >> ", (localStorage.getItem(localStorage.key(i)))); 93 | } 94 | 95 | static runAll() { 96 | for (var s of Setting.list) if (s.key) stg[s.key] = stg[s.key]; 97 | } 98 | 99 | 100 | static resetDefault() { 101 | localStorage.clear(); 102 | for (var s of Setting.list) if (s.key) stg[s.key] = s.dflt; 103 | cmd.settings.run(); 104 | } 105 | 106 | } 107 | 108 | Object.defineProperty(Setting, 'list', {value: [ 109 | {title:"Appearance"}, 110 | {key:"theme" ,dflt:"light" ,name:"Theme", list:[ "light" , "night", "dark"],hide:true, cb:Setting.setTheme}, 111 | {key:"font" ,dflt:13 ,name:"Font Size", min:7, max:24 ,cb:n=>{dom.body.style.fontSize = n+"px"; } }, 112 | {key:"rows" ,dflt:25 ,name:"Rows", min:10, max:60,cb:n=>{if (sheet)sheet.reload()} }, 113 | {key:"cols" ,dflt:7 ,name:"Cols", min:3, max:30 ,cb:n=>{if (sheet)sheet.reload()} }, 114 | {key:"actionBar" ,dflt:true ,name:"Action Bar", cb:b=>{dom.header.style.display = b? "flex":"none"} }, 115 | {key:"purple" ,dflt:true ,name:"Warning color on line return, comma and double quote values", cb:b=>{sheet.reload()} }, 116 | 117 | {title:"Csv Save"}, 118 | {key:"encoding" ,dflt:"utf-8" ,name:"Encoding"}, 119 | {key:"delimiter" ,dflt:"," ,name:"Delimiter", list:[",", ";" , "TAB"],hide:true}, 120 | {key:"save_fixed_width_size" ,dflt:0 ,name:"Minimum column size", min:0, max: 100 }, 121 | {key:"save_strict" ,dflt:false ,name:"Save-Strict (error on comma or double quotes)"}, 122 | {title:"Csv Open"}, 123 | {key:"fit_col_width" ,dflt:false ,name:"Fit column width" }, 124 | {key:"set_headers" ,dflt:true ,name:"Set headers" }, 125 | {key:"trim" ,dflt:false ,name:"Remove empty rows and columns" }, 126 | 127 | {title:"Data Validation"}, 128 | {key:"dv_comma_num" ,dflt:true ,name:"In numeric values : replace commas by a dot"}, 129 | {key:"dv_comma_txt" ,dflt:true ,name:"In text values : replace commas by a dash "}, 130 | {key:"dv_quotes" ,dflt:true ,name:"Replace double quotes by single quotes"}, 131 | {key:"dv_lr" ,dflt:true ,name:"Replace line returns by a pipe (|)"}, 132 | {key:"dv_lower" ,dflt:false ,name:"Force all text to lower case"}, 133 | 134 | {title:"Csv View Only"}, 135 | {key:"editMaxFileSize" ,dflt: 10 ,name:"Max editable file size (Mo)"}, 136 | {key:"vo_n_chunks" ,dflt: 5 ,name:"Number of chunks loaded", min:5, max:50 }, 137 | {key:"vo_n_rows" ,dflt: 10 ,name:"Number of rows per chunk loaded", min:3, max:50 }, 138 | 139 | {title:"Sort"}, 140 | {key:"sort_header" ,dflt:true ,name:"Ignore 1st row (header row)"}, 141 | {key:"sort_num_first" ,dflt:false ,name:"Numbers are sorted before text"}, 142 | 143 | 144 | ]}); 145 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | NanoCell - CSV file Viewer & Editor 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 28 | 29 | 30 | 31 | 32 | 45 |
46 | 62 |
63 |

Built for speed and simplicity

64 |

65 | Nanocell-csv lets you edit and visualize CSV files instantly, whether you're tackling massive datasets or 66 | fine-tuning small configuration tables. 67 | With a steadfast commitment to data integrity, it keeps your information safe and accurate—no unwanted type 68 | conversions, no surprises. 69 | Designed by and for data experts to simplify your workflow, Nanocell-csv embraces the file type's core values : 70 | simple, reliable, and universal. 71 |

72 |

Nanocell-csv aims to be the go-to CSV editing tool for software engineers and data experts worldwide.

73 | 74 | 75 | Test Nanocell-csv in the browser now 76 | 77 | 78 | 79 |
80 |
81 |

Key features

82 |

Data privacy - Nanocell-csv works 100% off-line, your data is never leaving your computer. 83 | Nanocell-csv.com runs on a static server which, by design, only sends data on request but cannot register any 84 | data. Don't take my word for it, checkout the source code [here].

86 |

Data accuracy - CSV data is text and Nanocell-csv makes sure values are being handled as such. Leading 87 | zeros and '+' signs are 88 | kept. No more data corruption of phone numbers, zipcodes, etc. Pasting data also finally works as 89 | you would expect, no more paste reformatting or column split action to perform !

90 |

Instant view large files - O(1) 😉. This is achieved by sampling the header, 91 | the footer and a few rows at regular intervals without parsing the entire file. The goal here is for data 92 | experts to quickly understand what they are dealing with when first opening a file. That is before they start 93 | using heavier Big-Data tools like pandas, pyspark, powerBI, R etc...

94 |

Install anywhere - Nanocell-csv requires no .exe file for installation and is completely 95 | cross-platform. You can use it anywhere, even at work, for those subject to an admin lock.

96 |

Data validation for database import - quickly identify and resolve any unconventional issues in your 97 | data. Make sure that : headers are limited to alphanumeric characters, encoding is UTF-8, values do not contain 98 | line breaks, and much more. Nanocell-csv helps you streamline your workflow before database import.

99 |
100 | 101 | 114 | 115 | 116 | 117 |
118 |

Contribute

119 | 120 | GitHub stars 121 | 122 |

Grow the community - Star the github repo and talk about Nanocell-csv to people around you! Link it on 123 | relevant reddit posts or show how useful its been to you on social media!

124 |

Give feedback: missing features & bugs - Nanocell-csv still has a bit to go before being stable and 125 | mature. Help us get there faster by reporting on the github issue tracker [here].

127 |
128 | 133 | 157 |
158 | 159 | 160 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Cedric Bonjour 2 | 3 | 4 | Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported 5 | ---------------------------------------------------------------- 6 | 7 | ![Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License](https://i.creativecommons.org/l/by-nc-nd/3.0/88x31.png) 8 | [Link](http://creativecommons.org/licenses/by-nc-nd/3.0/) 9 | 10 | 11 | 12 | 13 | 14 | License 15 | ------- 16 | 17 | THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. 18 | 19 | BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. 20 | 21 | ## 1. Definitions 22 | 23 | a. "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. 24 | 25 | b. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined above) for the purposes of this License. 26 | 27 | c. "Distribute" means to make available to the public the original and copies of the Work through sale or other transfer of ownership. 28 | 29 | d. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. 30 | 31 | e. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. 32 | 33 | f. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. 34 | 35 | g. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. 36 | 37 | h. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. 38 | 39 | i. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. 40 | 41 | ## 2. Fair Dealing Rights. 42 | 43 | Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. 44 | 45 | ## 3. License Grant. 46 | 47 | Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: 48 | 49 | a. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; and, 50 | 51 | b. to Distribute and Publicly Perform the Work including as incorporated in Collections. 52 | 53 | c. The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats, but otherwise you have no rights to make Adaptations. Subject to 8(f), all rights not expressly granted by Licensor are hereby reserved, including but not limited to the rights set forth in Section 4(d). 54 | 55 | ## 4. Restrictions. 56 | 57 | The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: 58 | 59 | a. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(c), as requested. 60 | 61 | b. You may not exercise any of the rights granted to You in Section 3 above in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation. The exchange of the Work for other copyrighted works by means of digital file-sharing or otherwise shall not be considered to be intended for or directed toward commercial advantage or private monetary compensation, provided there is no payment of any monetary compensation in connection with the exchange of copyrighted works. 62 | 63 | c. If You Distribute, or Publicly Perform the Work or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work. The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Collection, at a minimum such credit will appear, if a credit for all contributing authors of Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. 64 | 65 | d. For the avoidance of doubt: 66 | 67 | > i. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; 68 | 69 | > ii. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License if Your exercise of such rights is for a purpose or use which is otherwise than noncommercial as permitted under Section 4(b) and otherwise waives the right to collect royalties through any statutory or compulsory licensing scheme; and, 70 | 71 | > iii. Voluntary License Schemes. The Licensor reserves the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License that is for a purpose or use which is otherwise than noncommercial as permitted under Section 4(b). 72 | 73 | e. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. 74 | 75 | ## 5. Representations, Warranties and Disclaimer 76 | 77 | UNLESS OTHERWISE MUTUALLY AGREED BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. 78 | 79 | ## 6. Limitation on Liability. 80 | 81 | EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 82 | 83 | ## 7. Termination 84 | 85 | a. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. 86 | 87 | b. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. 88 | 89 | 90 | ## 8. Miscellaneous 91 | 92 | a. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. 93 | 94 | b. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. 95 | 96 | c. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. 97 | 98 | d. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. 99 | 100 | e. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. 101 | 102 | --------------------------------------------------------------------------------