├── .gitignore ├── CNAME ├── docs ├── logo.png ├── logo.svg └── index.html ├── dist ├── index.js.gz ├── index.js └── index-pretty.js ├── src ├── _isEmpty.js ├── demo │ ├── public │ │ ├── logo.png │ │ └── logo.svg │ ├── screenshot.jpg │ ├── samples │ │ ├── simple.js │ │ └── with-checks.js │ ├── demo.js │ ├── demo.css │ └── index.ejs ├── _cleanVal.js ├── _shift.js ├── _LooseArray.js ├── _defaultCss.js ├── sheetclip.js ├── _shift.test.js └── index.js ├── .babelrc.json ├── .github └── workflows │ └── main.yml ├── README.md ├── LICENSE.md ├── package.json ├── webpack.config.js └── jest.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | node_modules -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | importabular.lecaro.me 2 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renanlecaro/importabular/HEAD/docs/logo.png -------------------------------------------------------------------------------- /dist/index.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renanlecaro/importabular/HEAD/dist/index.js.gz -------------------------------------------------------------------------------- /src/_isEmpty.js: -------------------------------------------------------------------------------- 1 | export function _isEmpty(obj) { 2 | return Object.keys(obj).length === 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/demo/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renanlecaro/importabular/HEAD/src/demo/public/logo.png -------------------------------------------------------------------------------- /src/demo/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renanlecaro/importabular/HEAD/src/demo/screenshot.jpg -------------------------------------------------------------------------------- /src/_cleanVal.js: -------------------------------------------------------------------------------- 1 | export function _cleanVal(val) { 2 | if (val === 0) return "0"; 3 | if (!val) return ""; 4 | return val.toString(); 5 | } 6 | -------------------------------------------------------------------------------- /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | "@babel/plugin-proposal-class-properties" 14 | ] 15 | } -------------------------------------------------------------------------------- /src/demo/samples/simple.js: -------------------------------------------------------------------------------- 1 | import Importabular from "../../index"; 2 | 3 | const sheet = new Importabular({ 4 | node: document.getElementById("editor"), 5 | columns: [ 6 | { 7 | label: "Contact name", 8 | }, 9 | { 10 | label: "Phone number", 11 | }, 12 | { 13 | label: "Email address", 14 | }, 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /src/_shift.js: -------------------------------------------------------------------------------- 1 | export function _shift(x, y, deltaX, xMin, xMax, yMin, yMax) { 2 | x += deltaX; 3 | if (x < xMin) { 4 | if (xMax === Infinity) { 5 | return { x: xMin, y }; 6 | } 7 | x = xMax; 8 | y--; 9 | if (y < yMin) { 10 | if (yMax === Infinity) { 11 | return { x: xMin, y: yMin }; 12 | } 13 | y = yMax; 14 | } 15 | } 16 | if (x > xMax) { 17 | x = xMin; 18 | y++; 19 | if (y > yMax) { 20 | y = yMin; 21 | x = xMin; 22 | } 23 | } 24 | return { x, y }; 25 | } 26 | -------------------------------------------------------------------------------- /src/demo/demo.js: -------------------------------------------------------------------------------- 1 | import "./demo.css"; 2 | 3 | function setCode(code, codeBlockId) { 4 | const pre = document.createElement("pre"); 5 | const formattedCode = code.replace(/ from "[^;]*;/, ' from "importabular";'); 6 | 7 | pre.innerText = formattedCode; 8 | 9 | document 10 | .querySelector('code[data-script="' + codeBlockId + '"]') 11 | .appendChild(pre); 12 | } 13 | 14 | import S from "!!raw-loader!./samples/simple.js"; 15 | setCode(S, "simple"); 16 | 17 | import "./samples/simple"; 18 | 19 | import WC from "!!raw-loader!./samples/with-checks.js"; 20 | setCode(WC, "with-checks"); 21 | 22 | import "./samples/with-checks"; 23 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | on: [push] 6 | 7 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 8 | jobs: 9 | # This workflow contains a single job called "build" 10 | test: 11 | # The type of runner that the job will run on 12 | runs-on: ubuntu-latest 13 | 14 | # Steps represent a sequence of tasks that will be executed as part of the job 15 | steps: 16 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 17 | - uses: actions/checkout@v2 18 | 19 | # Runs a single command using the runners shell 20 | - run: npm install 21 | - run: npm run test 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # importabular 2 | 3 | Lightweight spreadsheet editor for the web, to easily let your users import their data from excel. 4 | 5 | - Lightweight (under 5kb gzipped) 6 | - Mobile friendly 7 | - Copy / paste 8 | - MIT License 9 | 10 | 11 | # Quickstart 12 | 13 | The quick and dirty way : 14 | 15 | ``` 16 |
17 | 18 | 34 | ``` 35 | # Demo and doc 36 | 37 | The website will give you more details : https://importabular.lecaro.me/ 38 | 39 | NPM : https://www.npmjs.com/package/importabular 40 | 41 |  42 | -------------------------------------------------------------------------------- /src/_LooseArray.js: -------------------------------------------------------------------------------- 1 | import { _cleanVal } from "./_cleanVal"; 2 | import { _isEmpty } from "./_isEmpty"; 3 | 4 | export class _LooseArray { 5 | // An 2D array of strings that only stores non "" values 6 | _data = {}; 7 | 8 | _setVal(x, y, val) { 9 | const hash = this._data; 10 | const cleanedVal = _cleanVal(val); 11 | if (cleanedVal) { 12 | if (!hash[x]) hash[x] = {}; 13 | hash[x][y] = cleanedVal; 14 | } else { 15 | // delete item 16 | if (hash[x] && hash[x][y]) { 17 | delete hash[x][y]; 18 | if (_isEmpty(hash[x])) delete hash[x]; 19 | } 20 | } 21 | } 22 | 23 | _clear() { 24 | this._data = {}; 25 | } 26 | 27 | _getVal(x, y) { 28 | const hash = this._data; 29 | return (hash && hash[x] && hash[x][y]) || ""; 30 | } 31 | 32 | _toArr(width, height) { 33 | const result = []; 34 | for (let y = 0; y < height; y++) { 35 | result.push([]); 36 | for (let x = 0; x < width; x++) { 37 | result[y].push(this._getVal(x, y)); 38 | } 39 | } 40 | return result; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Renan LE CARO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/_defaultCss.js: -------------------------------------------------------------------------------- 1 | export const _defaultCss = ` 2 | html{ 3 | -ms-overflow-style: none; 4 | scrollbar-width: none; 5 | } 6 | ::-webkit-scrollbar { 7 | width: 0; 8 | height:0; 9 | } 10 | *{ 11 | box-sizing: border-box; 12 | } 13 | body{ 14 | padding: 0; 15 | margin: 0; 16 | } 17 | table{ 18 | border-spacing: 0; 19 | background: white; 20 | border: 1px solid #ddd; 21 | border-width: 0 1px 1px 0; 22 | font-size: 16px; 23 | font-family: sans-serif; 24 | border-collapse: separate; 25 | min-width:100%; 26 | } 27 | td, th{ 28 | padding:0; 29 | border: 1px solid; 30 | border-color: #ddd transparent transparent #ddd; 31 | } 32 | td.selected.multi:not(.editing){ 33 | background:#d7f2f9; 34 | } 35 | td.focus:not(.editing){ 36 | border-color: black; 37 | } 38 | td>*, th>*{ 39 | border:none; 40 | padding:10px; 41 | min-width:100px; 42 | min-height: 40px; 43 | font:inherit; 44 | line-height: 20px; 45 | color:inherit; 46 | white-space: normal; 47 | } 48 | td>div::selection { 49 | color: none; 50 | background: none; 51 | } 52 | 53 | .placeholder div{ 54 | user-select:none; 55 | color:rgba(0,0,0,0.2); 56 | } 57 | *[title] div{cursor:help;} 58 | th{text-align:left;} 59 | `; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "importabular", 3 | "version": "0.2.13", 4 | "description": "5kb javascript spreadsheet, let your users import their data from excel.", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist/index.js" 8 | ], 9 | "scripts": { 10 | "start": "webpack serve", 11 | "build": "bash ./build.sh", 12 | "pretty": "prettier --write src/**", 13 | "test": "jest" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/renanlecaro/importabular.git" 18 | }, 19 | "keywords": [ 20 | "spreadsheet", 21 | "vanillajs", 22 | "ui", 23 | "lightweight", 24 | "minimalist", 25 | "data", 26 | "import" 27 | ], 28 | "author": "Renan LE CARO", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/renanlecaro/importabular/issues" 32 | }, 33 | "homepage": "https://lecaro.me/importabular/", 34 | "devDependencies": { 35 | "@babel/cli": "^7.11.6", 36 | "@babel/core": "^7.12.9", 37 | "@babel/plugin-proposal-class-properties": "^7.10.4", 38 | "@babel/preset-env": "^7.12.7", 39 | "babel-jest": "^26.3.0", 40 | "babel-loader": "^8.2.2", 41 | "clean-webpack-plugin": "^3.0.0", 42 | "copy-webpack-plugin": "^6.3.2", 43 | "css-loader": "^5.0.1", 44 | "documentation": "*", 45 | "html-inline-css-webpack-plugin": "^1.10.0", 46 | "html-inline-script-webpack-plugin": "^1.0.1", 47 | "html-webpack-plugin": "^4.5.0", 48 | "jest": "^26.4.2", 49 | "mini-css-extract-plugin": "^1.3.1", 50 | "prettier": "2.1.2", 51 | "raw-loader": "^4.0.2", 52 | "style-loader": "^2.0.0", 53 | "terser": "^5.3.2", 54 | "webpack": "^5.9.0", 55 | "webpack-cli": "^4.2.0", 56 | "webpack-dev-server": "^3.11.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/demo/samples/with-checks.js: -------------------------------------------------------------------------------- 1 | import Importabular from "../../index"; 2 | 3 | const sheet = new Importabular({ 4 | node: document.getElementById("with-checks"), 5 | columns: [ 6 | { 7 | label: "Contact name", 8 | title: "Full name", 9 | placeholder: "John Doe", 10 | }, 11 | { 12 | label: "Phone number", 13 | title: "In the international format", 14 | placeholder: "+33XXXXXXX", 15 | }, 16 | { 17 | label: "Email address", 18 | placeholder: "xxxx@yyyy.zzz", 19 | }, 20 | ], 21 | data: [ 22 | ["Name", "+333555555", "email@adress"], 23 | ["Bad data", "33366666", "email@"], 24 | ["", "", "missing@name"], 25 | ], 26 | checks: checkData, 27 | css: ` 28 | td>div{ 29 | border-right:2px solid transparent; 30 | } 31 | td.invalid >div{ 32 | border-right:2px solid red; 33 | background:rgba(255,0,0,0.1); 34 | } 35 | td.valid > div{ 36 | border-right:2px solid green; 37 | 38 | } 39 | 40 | `, 41 | onChange(data) { 42 | console.table(data); 43 | }, 44 | }); 45 | 46 | function checkData(data) { 47 | // Generate tooltip content for each problem 48 | const titles = data.map((line) => [ 49 | checkName(line), 50 | checkPhone(line), 51 | checkEmail(line), 52 | ]); 53 | 54 | // Display the cell as invalid if there's a problem 55 | const classNames = data.map((line, index) => [ 56 | titles[index][0] ? "invalid" : line[0] && "valid", 57 | titles[index][1] ? "invalid" : line[1] && "valid", 58 | titles[index][2] ? "invalid" : line[2] && "valid", 59 | ]); 60 | 61 | return { titles, classNames }; 62 | } 63 | 64 | function checkName([name, phone, email]) { 65 | if (!name && (phone || email)) { 66 | return "Name is required"; 67 | } 68 | } 69 | function checkPhone([name, phone, email]) { 70 | if (phone && !phone.match(/\+[0-9]+/)) return "Invalid phone number"; 71 | } 72 | 73 | function checkEmail([name, phone, email]) { 74 | if (name && !email) return "Email is required"; 75 | 76 | if (!email.match(/[a-z0-9.-]+@[a-z0-9.-]+/gi)) { 77 | return "Invalid email address"; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 5 | const isProd = process.env.NODE_ENV === 'production' 6 | const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default; 7 | const HtmlInlineScriptPlugin = require('html-inline-script-webpack-plugin'); 8 | 9 | 10 | 11 | const CopyPlugin = require("copy-webpack-plugin"); 12 | 13 | const common={ 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.css$/i, 18 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 19 | }, 20 | { 21 | test: /\.js$/, 22 | exclude: /node_modules/, 23 | use: { 24 | loader: 'babel-loader', 25 | options: { 26 | babelrc: true 27 | } 28 | } 29 | } 30 | ], 31 | }, 32 | optimization: { 33 | minimize: isProd, 34 | }, 35 | stats: 'errors-only', 36 | } 37 | 38 | const docExport= { 39 | entry: './src/demo/demo.js', 40 | output: { 41 | filename: 'main.js', 42 | path: path.resolve(__dirname, 'docs'), 43 | }, 44 | devServer: { 45 | contentBase: './docs', 46 | port: 1234, 47 | open: true, 48 | disableHostCheck: true, 49 | }, 50 | plugins: [ 51 | new CleanWebpackPlugin(), 52 | new MiniCssExtractPlugin(), 53 | new HtmlWebpackPlugin({ 54 | template:'src/demo/index.ejs', 55 | 56 | }), 57 | new HTMLInlineCSSWebpackPlugin(), 58 | new HtmlInlineScriptPlugin(), 59 | new CopyPlugin({ 60 | patterns: [ 61 | { from: "src/demo/public", to: "." } 62 | ], 63 | }), 64 | ], 65 | ...common 66 | } 67 | 68 | function libExport({filename, mangle}) { 69 | return { 70 | ...common, 71 | entry:'./src/index.js', 72 | output: { 73 | filename: 'index.js', 74 | path: path.resolve(__dirname, 'dist'), 75 | libraryExport: "default" , 76 | libraryTarget: 'umd', 77 | library: 'Importabular', 78 | }, 79 | plugins: [ 80 | new CleanWebpackPlugin(), 81 | ], 82 | } 83 | } 84 | module.exports = [ 85 | docExport, 86 | isProd && libExport({ 87 | filename:"", 88 | mangle:{} 89 | }) 90 | ].filter(i=>i) -------------------------------------------------------------------------------- /src/sheetclip.js: -------------------------------------------------------------------------------- 1 | // from https://github.com/warpech/sheetclip/blob/master/sheetclip.js 2 | 3 | function countQuotes(str) { 4 | return str.split('"').length - 1; 5 | } 6 | 7 | export function parseArrayString(str) { 8 | var r, 9 | rlen, 10 | rows, 11 | arr = [], 12 | a = 0, 13 | c, 14 | clen, 15 | multiline, 16 | last; 17 | rows = str.split("\n"); 18 | if (rows.length > 1 && rows[rows.length - 1] === "") { 19 | rows.pop(); 20 | } 21 | for (r = 0, rlen = rows.length; r < rlen; r += 1) { 22 | rows[r] = rows[r].split("\t"); 23 | for (c = 0, clen = rows[r].length; c < clen; c += 1) { 24 | if (!arr[a]) { 25 | arr[a] = []; 26 | } 27 | if (multiline && c === 0) { 28 | last = arr[a].length - 1; 29 | arr[a][last] = arr[a][last] + "\n" + rows[r][0]; 30 | if (multiline && countQuotes(rows[r][0]) & 1) { 31 | //& 1 is a bitwise way of performing mod 2 32 | multiline = false; 33 | arr[a][last] = arr[a][last] 34 | .substring(0, arr[a][last].length - 1) 35 | .replace(/""/g, '"'); 36 | } 37 | } else { 38 | if ( 39 | c === clen - 1 && 40 | rows[r][c].indexOf('"') === 0 && 41 | countQuotes(rows[r][c]) & 1 42 | ) { 43 | arr[a].push(rows[r][c].substring(1).replace(/""/g, '"')); 44 | multiline = true; 45 | } else { 46 | arr[a].push(rows[r][c].replace(/""/g, '"')); 47 | multiline = false; 48 | } 49 | } 50 | } 51 | if (!multiline) { 52 | a += 1; 53 | } 54 | } 55 | return arr; 56 | } 57 | 58 | export function stringifyArray(arr) { 59 | var r, 60 | rlen, 61 | c, 62 | clen, 63 | str = "", 64 | val; 65 | for (r = 0, rlen = arr.length; r < rlen; r += 1) { 66 | for (c = 0, clen = arr[r].length; c < clen; c += 1) { 67 | if (c > 0) { 68 | str += "\t"; 69 | } 70 | val = arr[r][c]; 71 | if (typeof val === "string") { 72 | if (val.indexOf("\n") > -1) { 73 | str += '"' + val.replace(/"/g, '""') + '"'; 74 | } else { 75 | str += val; 76 | } 77 | } else if (val === null || val === void 0) { 78 | //void 0 resolves to undefined 79 | str += ""; 80 | } else { 81 | str += val; 82 | } 83 | } 84 | str += "\n"; 85 | } 86 | return str; 87 | } 88 | -------------------------------------------------------------------------------- /src/demo/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Roboto, sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | section { 8 | color: white; 9 | background-attachment: fixed; 10 | } 11 | section a { 12 | color: white; 13 | } 14 | @media (max-width: 570px) { 15 | #editorNode { 16 | margin-left: -10px; 17 | margin-right: -10px; 18 | width: auto; 19 | align-self: stretch; 20 | } 21 | } 22 | @media (max-width: 1400px) { 23 | section { 24 | padding: 20px; 25 | display: flex; 26 | flex-direction: column; 27 | align-items: center; 28 | } 29 | section > * { 30 | width: 100%; 31 | max-width: 580px; 32 | } 33 | section > *:last-child { 34 | margin-top: 20px; 35 | } 36 | #editorNode > table { 37 | min-width: 100%; 38 | } 39 | } 40 | @media (min-width: 1400px) { 41 | section { 42 | padding: 80px; 43 | display: flex; 44 | min-height: 80vh; 45 | align-items: center; 46 | justify-content: center; 47 | font-size: 20px; 48 | } 49 | section > * { 50 | box-sizing: border-box; 51 | max-width: 600px; 52 | margin: 0 40px; 53 | flex-grow: 1; 54 | flex-shrink: 1; 55 | } 56 | } 57 | 58 | ul.checks { 59 | list-style: none; 60 | padding: 0; 61 | } 62 | ul.checks li { 63 | margin-bottom: 10px; 64 | } 65 | ul.checks li:before { 66 | content: "\2611"; 67 | margin-right: 10px; 68 | } 69 | h1, 70 | h2 { 71 | font-size: 44px; 72 | font-weight: 600; 73 | } 74 | 75 | #editorNode { 76 | overflow: auto; 77 | } 78 | pre > code { 79 | background: #2e3548; 80 | color: #e6f7fd; 81 | border-radius: 4px; 82 | display: block; 83 | font-size: 14px; 84 | padding: 20px; 85 | overflow: auto; 86 | max-width: 100%; 87 | max-height: 70vh; 88 | } 89 | pre.auto { 90 | display: block; 91 | white-space: pre; 92 | background: white; 93 | color: black; 94 | padding: 20px; 95 | font-family: monospace; 96 | line-height: 20px; 97 | margin-bottom: 40px; 98 | font-size: 12px; 99 | max-height: 80vh; 100 | overflow: auto; 101 | } 102 | section.sample { 103 | color: rgba(0, 0, 0, 0.8); 104 | } 105 | 106 | section.sample > :first-child { 107 | /*We flip the order of the script and the container so that the script can see the container when it runs*/ 108 | order: 2; 109 | } 110 | .github-corner { 111 | display: block; 112 | text-decoration: none; 113 | background: white; 114 | color: black; 115 | text-align: center; 116 | padding: 20px; 117 | } 118 | @media screen and (min-width: 1200px) { 119 | .github-corner { 120 | position: fixed; 121 | top: 0; 122 | right: 0; 123 | padding: 10px 60px; 124 | transform: translate(50px, 50px) rotate(45deg); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/_shift.test.js: -------------------------------------------------------------------------------- 1 | import { _shift } from "./_shift"; 2 | 3 | describe("moving forward ", () => { 4 | test("moves one cell to the right", () => { 5 | // X0 to 0X 6 | // 00 00 7 | 8 | expect( 9 | _shift( 10 | 0, // x 11 | 0, // y 12 | 1, // deltaX 13 | 0, // xMin 14 | 1, // xMax 15 | 0, // yMin 16 | 1 // yMax 17 | ) 18 | ).toEqual({ x: 1, y: 0 }); 19 | }); 20 | 21 | test("goes to the next row if it is exiting the bounds horizontally", () => { 22 | // 0X to 00 23 | // 00 X0 24 | expect( 25 | _shift( 26 | 1, // x 27 | 0, // y 28 | 1, // deltaX 29 | 0, // xMin 30 | 1, // xMax 31 | 0, // yMin 32 | 1 // yMax 33 | ) 34 | ).toEqual({ x: 0, y: 1 }); 35 | }); 36 | test("goes back to the first row if it is at the last one", () => { 37 | // 00 to X0 38 | // 0X 00 39 | expect( 40 | _shift( 41 | 1, // x 42 | 1, // y 43 | 1, // deltaX 44 | 0, // xMin 45 | 1, // xMax 46 | 0, // yMin 47 | 1 // yMax 48 | ) 49 | ).toEqual({ x: 0, y: 0 }); 50 | }); 51 | }); 52 | describe("moving backward ", () => { 53 | test("moves one cell to the left", () => { 54 | // 00 to X0 55 | // 0X 00 56 | expect( 57 | _shift( 58 | 1, // x 59 | 0, // y 60 | -1, // deltaX 61 | 0, // xMin 62 | 1, // xMax 63 | 0, // yMin 64 | 1 // yMax 65 | ) 66 | ).toEqual({ x: 0, y: 0 }); 67 | }); 68 | 69 | test("goes to the previous row if it is exiting the bounds horizontally", () => { 70 | // 00 to 0X 71 | // X0 00 72 | expect( 73 | _shift( 74 | 0, // x 75 | 1, // y 76 | -1, // deltaX 77 | 0, // xMin 78 | 1, // xMax 79 | 0, // yMin 80 | 1 // yMax 81 | ) 82 | ).toEqual({ x: 1, y: 0 }); 83 | }); 84 | 85 | test("does not do anything if the new X coordinates would be infinite", () => { 86 | // 0000.. to 0000.. 87 | // X000.. X000.. 88 | expect( 89 | _shift( 90 | 0, // x 91 | 1, // y 92 | -1, // deltaX 93 | 0, // xMin 94 | Infinity, // xMax 95 | 0, // yMin 96 | 1 // yMax 97 | ) 98 | ).toEqual({ x: 0, y: 1 }); 99 | }); 100 | test("does not do anything if the new Y coordinates would be infinite", () => { 101 | // X0 to X0 102 | // 00 00 103 | // .. .. 104 | expect( 105 | _shift( 106 | 0, // x 107 | 0, // y 108 | -1, // deltaX 109 | 0, // xMin 110 | 1, // xMax 111 | 0, // yMin 112 | Infinity // yMax 113 | ) 114 | ).toEqual({ x: 0, y: 0 }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/demo/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 |Install it from npm
26 |npm install importabular
27 |
28 | Instantiate it on a dom node
29 | 30 |
31 |
32 | Manipulate the sheet programmatically
33 |sheet.getData();
34 | sheet.setData([['First','line','text']);
35 | sheet.destroy()
36 |
37 |
38 |
54 | 62 | I've created this lib because I was tired of having to remove 90% of 63 | the features offered by the very few open source libs for web 64 | spreadsheets. 65 |
66 |67 | So for this reinventing the wheel to make sense, I should not add any 68 | extra features to this core. 69 |
70 |80 | The lib is fresh and not battle tested, probably has some bugs. The 81 | API is not stable yet. Feel free to 82 | 83 | create an issue 85 | if you find a bug. 86 |
87 |