├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── README.md ├── bin ├── authorize-push.js └── build.js ├── demo ├── demo.css ├── entry.js ├── index.html └── webpack.config.js ├── navigable-table.js ├── package.json └── test └── navigable-table-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | log/ 4 | demo/bundle.css 5 | demo/bundle.js 6 | demo/*.eot 7 | demo/*.woff2 8 | demo/*.woff2 9 | demo/*.woff 10 | demo/*.ttf 11 | demo/*.svg 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '4' 10 | before_install: 11 | - npm i -g npm 12 | - export DISPLAY=:99.0 13 | - sh -e /etc/init.d/xvfb start 14 | before_script: 15 | - npm prune 16 | after_success: 17 | - npm run semantic-release 18 | - if [[ $TRAVIS_BRANCH == 'master' ]]; then npm run deploy; fi 19 | env: 20 | matrix: 21 | - CLIENT=selenium:firefox 22 | - CLIENT=saucelabs:chrome 23 | branches: 24 | except: 25 | - "/^v\\d+\\.\\d+\\.\\d+$/" 26 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource+coc@martynus.net. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Navigable Table – A jQuery plugin 2 | 3 | > A jQuery plugin for elegant editing of data collections. 4 | 5 | [![Build Status](https://travis-ci.org/gr2m/navigable-table.svg)](https://travis-ci.org/gr2m/navigable-table) 6 | [![Dependency Status](https://david-dm.org/gr2m/navigable-table.svg)](https://david-dm.org/gr2m/navigable-table) 7 | [![devDependency Status](https://david-dm.org/gr2m/navigable-table/dev-status.svg)](https://david-dm.org/gr2m/navigable-table#info=devDependencies) 8 | 9 | ## Download / Installation 10 | 11 | You can download the latest JS & CSS code here: 12 | 13 | - https://npmcdn.com/navigable-table/dist/navigable-table.js 14 | 15 | Or install via [npm](https://www.npmjs.com/) 16 | 17 | ``` 18 | npm install --save navigable-table 19 | ``` 20 | 21 | The JS code can be required with 22 | 23 | ```js 24 | var jQuery = require('jquery') 25 | var navigableTable = require('navigable-table') 26 | 27 | // init 28 | navigableTable(jQuery) 29 | ``` 30 | 31 | ## Usage 32 | 33 | ```html 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
NameE-MailBirthday
67 | ``` 68 | 69 | ## Events 70 | 71 | ``` 72 | // bump event when there is no column / row to jump / move to 73 | $table.on('bump', function(event, direction) {}); 74 | $table.on('bump:up', function(event) {}); 75 | $table.on('bump:down', function(event) {}); 76 | 77 | // move events, when a row is moved up or down 78 | $table.on('move', function(event, direction, index) {}); 79 | $table.on('move:up', function(event, index) {}); 80 | $table.on('move:down', function(event, index) {}); 81 | 82 | // move events, before a row is moved up or down. 83 | // The 'move' events can be preventend by calling cancelMove() 84 | $table.on('before:move', function(event, direction, index, cancelMove) {}); 85 | $table.on('before:move:up', function(event, index, cancelMove) {}); 86 | $table.on('before:move:down', function(event, index, cancelMove) {}); 87 | ``` 88 | 89 | ## Local Setup 90 | 91 | ```bash 92 | git clone git@github.com:gr2m/navigable-table.git 93 | cd navigable-table 94 | npm install 95 | ``` 96 | 97 | ## Test 98 | 99 | You can start a local dev server with 100 | 101 | ```bash 102 | npm start 103 | ``` 104 | 105 | Run tests with 106 | 107 | ```bash 108 | npm test 109 | ``` 110 | 111 | While working on the tests, you can start Selenium / Chrome driver 112 | once, and then tests re-run on each save 113 | 114 | ```bash 115 | npm run test:mocha:watch 116 | ``` 117 | 118 | ## Fine Print 119 | 120 | The Expandable Input Plugin have been authored by [Gregor Martynus](https://github.com/gr2m), 121 | proud member of the [Hoodie Community](http://hood.ie/). 122 | 123 | License: MIT 124 | -------------------------------------------------------------------------------- /bin/authorize-push.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var exec = require('child_process').exec 4 | 5 | var GH_TOKEN = process.env.GH_TOKEN 6 | var repo = require('../package.json').repository.url 7 | 8 | if (!(process.env.CI && GH_TOKEN && repo)) { 9 | console.log('[authorize-push] ignodered because condition not fullfillled (process.env.CI: %s, GH_TOKEN: %s, repo: %s)', !!process.env.CI, !!GH_TOKEN, !!repo) 10 | process.exit(1) 11 | } 12 | 13 | var commands = [ 14 | 'git remote set-url origin ' + repo.replace('https://', 'https://' + GH_TOKEN + '@'), 15 | 'git config user.email "gregor@martynus.net"', 16 | 'git config user.name "gr2m"' 17 | ] 18 | commands.forEach(function (command) { 19 | console.log('[authorize-push] %s', command.replace(GH_TOKEN, '***GH_TOKEN***')) 20 | exec(command) 21 | }) 22 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var resolvePath = require('path').resolve 4 | 5 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 6 | var webpack = require('webpack') 7 | 8 | // returns a Compiler instance 9 | var compiler = webpack({ 10 | entry: [ 11 | './navigable-table.js' 12 | ], 13 | output: { 14 | path: resolvePath(__dirname, '../dist'), 15 | filename: 'navigable-table.js' 16 | }, 17 | module: { 18 | loaders: [ 19 | { 20 | test: /\.css$/, 21 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader') 22 | } 23 | ] 24 | }, 25 | modulesDirectories: [ 26 | 'node_modules' 27 | ] 28 | }) 29 | 30 | compiler.run(function (error, stats) { 31 | if (error) { 32 | throw error 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial; 3 | } 4 | header { 5 | padding: 18px 18px 24px; 6 | background: #222; 7 | color: #fff; 8 | 9 | /* hoodie hoodie hoodie! */ 10 | background-image: linear-gradient(90deg, #2C2A86 20.5%, #078D2D 20.5%, #078D2D 37.5%, #FCB20D 37.5%, #FCB20D 55.5%, #EC5100 55.5%, #EC5100 72.5%, #6A3C06 72.5%, #6A3C06 79.5%, #C01C1B 79.5%); 11 | background-size: 300px 6px; 12 | background-repeat: repeat-x; 13 | background-position: 0 100%; 14 | } 15 | header a { 16 | color: #fff !important; 17 | } 18 | header h1 { 19 | margin: 0; 20 | font-size: 28px; 21 | font-weight: bold; 22 | } 23 | header h6 { 24 | margin: 0; 25 | } 26 | h3 { 27 | padding: 32px 8px 4px; 28 | font-size: 14px; 29 | font-weight: bold; 30 | color: #444; 31 | } 32 | .shortcuts { 33 | font-size: 12px; 34 | } 35 | .shortcuts dt { 36 | width: 80px; 37 | } 38 | .shortcuts dd { 39 | margin-left: 88px; 40 | } 41 | p.info { 42 | padding: 0 8px; 43 | font-size: 10px; 44 | color: #888; 45 | } 46 | p.info a { 47 | color: inherit; 48 | text-decoration: underline; 49 | } 50 | .os-mac .os-other, 51 | .os-other .os-mac { 52 | display: none; 53 | } 54 | -------------------------------------------------------------------------------- /demo/entry.js: -------------------------------------------------------------------------------- 1 | require('bootstrap/dist/css/bootstrap.css') 2 | require('./demo.css') 3 | 4 | var jQuery = require('jquery') 5 | var navigableTable = require('../navigable-table') 6 | var editableTable = require('editable-table') 7 | var expandableInput = require('expandable-input') 8 | 9 | expandableInput(jQuery) 10 | editableTable(jQuery) 11 | navigableTable(jQuery) 12 | 13 | window.$ = window.jQuery = jQuery 14 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Navigable Table – A jQuery plugin 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Navigable Table

13 |
Made by the Hoodie Community.
14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 69 | 70 | 71 | 72 | 78 | 79 | 80 | 84 | 85 | 86 |
NameGenderBirthday
28 | 32 | 36 | newsletter 37 | ? 38 |
43 | 47 | 51 | newsletter 52 | ? 53 |
58 | 62 | 66 | newsletter 67 | ? 68 |
73 | 77 | 81 | newsletter 82 | ? 83 |
87 | 88 |

Supported keyboard shortcuts

89 |
90 |
jump
alt + arrow keys
91 |
move
alt + shift + up/down
92 |
insert
alt + enter (+ shift)
93 |
duplicate
alt + d (+ shift)
94 |
delete
alt + shift + backspace
95 |
96 | 97 |

98 | Note: This demo is using the Editable Table 99 | plugin for auto-adding of new rows, and for styling.
100 | Support keybord shortcuts: 101 |

102 | 103 | 104 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 2 | 3 | module.exports = { 4 | entry: './entry.js', 5 | output: { 6 | path: __dirname, 7 | filename: 'bundle.js' 8 | }, 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.css$/, 13 | // loader: 'style-loader!css-loader' 14 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader') 15 | }, 16 | { 17 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 18 | loader: 'url-loader?limit=100000' 19 | } 20 | ] 21 | }, 22 | modulesDirectories: [ 23 | 'node_modules' 24 | ], 25 | plugins: [ 26 | new ExtractTextPlugin('bundle.css') 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /navigable-table.js: -------------------------------------------------------------------------------- 1 | module.exports = navigableTable 2 | 3 | function navigableTable ($) { 4 | // NAVIGABLE TABLE CLASS DEFINITION 5 | // ================================ 6 | 7 | // 8 | var NavigableTable = function (el) { 9 | var $table, $body 10 | var keyboardShortcutsMetakey 11 | var focusableSelector = '[name]:visible,a,[contenteditable]' 12 | var inputSelector = '[name]:visible,[contenteditable]' 13 | 14 | // 15 | // 16 | // 17 | function initialize () { 18 | $table = $(el) 19 | $body = $table.find('tbody') 20 | 21 | if (window.navigator.appVersion.indexOf('Mac') !== -1) { 22 | keyboardShortcutsMetakey = 'metaKey' 23 | } else { 24 | keyboardShortcutsMetakey = 'altKey' 25 | } 26 | 27 | $body.on('keydown', 'tr', handleKeyDown) 28 | } 29 | 30 | // Event handlers 31 | // -------------- 32 | 33 | // 34 | // 35 | // 36 | function handleKeyDown (event) { 37 | var input = event.target 38 | var shiftKeyPressed = event.shiftKey 39 | var keyCode = event.keyCode 40 | 41 | if (!event[keyboardShortcutsMetakey]) return 42 | 43 | return navigate(input, keyCode, shiftKeyPressed) 44 | } 45 | 46 | // Methods 47 | // ------- 48 | 49 | function navigate (input, keyCode, shiftKeyPressed) { 50 | switch (keyCode) { 51 | case 37: // left 52 | return shiftKeyPressed ? true : jump('left', input) 53 | case 39: // right 54 | return shiftKeyPressed ? true : jump('right', input) 55 | case 38: // up 56 | return shiftKeyPressed ? moveUp(input) : jump('up', input) 57 | case 40: // down 58 | return shiftKeyPressed ? moveDown(input) : jump('down', input) 59 | case 68: // d (duplicate) 60 | return shiftKeyPressed ? duplicateUp(input) : duplicateDown(input) 61 | case 13: // enter (insert) 62 | return shiftKeyPressed ? insertUp(input) : insertDown(input) 63 | case 8: // del 64 | return shiftKeyPressed ? remove(input) : true 65 | } 66 | } 67 | 68 | // 69 | function jump (direction, input) { 70 | var $input = $(input) 71 | var $targetInput = getJumpTargetInput(direction, $input) 72 | 73 | // workaround for: 74 | // https://github.com/gr2m/minutes.io/issues/67 75 | var value = $input.val() 76 | if ($input.is('select')) { 77 | setTimeout(function () { 78 | $input.val(value) 79 | }) 80 | } 81 | 82 | if (!$targetInput.length) { 83 | $input.trigger('bump', [direction]) 84 | $input.trigger('bump:' + direction) 85 | return 86 | } 87 | 88 | $targetInput.focus().select() 89 | return false 90 | } 91 | 92 | // 93 | // moves current row up, by moving the above row down. 94 | // 95 | function moveUp (input) { 96 | var $row = $(input).closest('tr') 97 | var $prev = $row.prev() 98 | var index 99 | var moveRows = true 100 | 101 | if ($prev.length === 0) return false 102 | 103 | function cancelMove () { 104 | moveRows = false 105 | } 106 | index = $row.index() 107 | $row.trigger('before:move', ['up', index, cancelMove]) 108 | $row.trigger('before:move:up', [index, cancelMove]) 109 | 110 | if (!moveRows) return 111 | 112 | $prev.insertAfter($row) 113 | index -= 1 114 | $row.trigger('move', ['up', index]) 115 | $row.trigger('move:up', [index]) 116 | 117 | return false 118 | } 119 | 120 | // 121 | // moves current row down, by moving the below row up. 122 | // 123 | function moveDown (input) { 124 | var $row = $(input).closest('tr') 125 | var $next = $row.next() 126 | var index 127 | var moveRows = true 128 | 129 | if ($next.length === 0) return false 130 | 131 | function cancelMove () { 132 | moveRows = false 133 | } 134 | index = $row.index() 135 | $row.trigger('before:move', ['down', index, cancelMove]) 136 | $row.trigger('before:move:down', [index, cancelMove]) 137 | 138 | if (!moveRows) return 139 | $next.insertBefore($row) 140 | index += 1 141 | $row.trigger('move', ['down', index]) 142 | $row.trigger('move:down', [index]) 143 | 144 | return false 145 | } 146 | 147 | // 148 | function duplicateUp (input) { 149 | var $row = $(input).closest('tr') 150 | var $newRow = $row.clone() 151 | passSelecectValues($row, $newRow) 152 | $row.before($newRow) 153 | $row.trigger('duplicate', ['up', $newRow]) 154 | $row.trigger('duplicate:up', [$newRow]) 155 | jump('up', input) 156 | return false 157 | } 158 | 159 | // 160 | function duplicateDown (input) { 161 | var $row = $(input).closest('tr') 162 | var $newRow = $row.clone() 163 | passSelecectValues($row, $newRow) 164 | $row.after($newRow) 165 | $row.trigger('duplicate', ['down', $newRow]) 166 | $row.trigger('duplicate:down', [$newRow]) 167 | jump('down', input) 168 | return false 169 | } 170 | 171 | // 172 | function insertUp (input) { 173 | var $row = $(input).closest('tr') 174 | var $newRow = $row.clone() 175 | $newRow.find(inputSelector).val('') 176 | $row.before($newRow) 177 | $row.trigger('insert', ['up', $newRow]) 178 | $row.trigger('insert:up', [$newRow]) 179 | jump('up', input) 180 | return false 181 | } 182 | 183 | // 184 | function insertDown (input) { 185 | var $row = $(input).closest('tr') 186 | var $newRow = $row.clone() 187 | $newRow.find(inputSelector).val('') 188 | $row.after($newRow) 189 | $row.trigger('insert', ['down', $newRow]) 190 | $row.trigger('insert:down', [$newRow]) 191 | jump('down', input) 192 | return false 193 | } 194 | 195 | // 196 | function remove (input) { 197 | var $row = $(input).closest('tr') 198 | var $next = $row.next() 199 | // if there is a next row, and it's not the last one ... 200 | if ($next.length && !$next.is(':last-child')) { 201 | jump('down', input) 202 | } else { 203 | jump('up', input) 204 | } 205 | $row.remove() 206 | return false 207 | } 208 | 209 | // 210 | // 211 | // 212 | function getJumpTargetInput (direction, $input) { 213 | if (direction === 'up' || direction === 'down') { 214 | return getJumpTargetRowInput(direction, $input) 215 | } 216 | 217 | return getJumpTargetColumnInput(direction, $input) 218 | } 219 | 220 | // 221 | function getJumpTargetRowInput (direction, $input) { 222 | var $cell = $input.closest('td') 223 | var $inputsInCell = $cell.find(focusableSelector) 224 | var currentInputIndex = $inputsInCell.index($input) 225 | var $row = $cell.parent() 226 | var $targetRow = (direction === 'up') ? $row.prev() : $row.next() 227 | var $targetCell = $targetRow.children('td,th').eq($cell.index()) 228 | 229 | return $targetCell.find(focusableSelector).eq(currentInputIndex) 230 | } 231 | 232 | // 233 | function getJumpTargetColumnInput (direction, $input) { 234 | var $cell = $input.closest('td') 235 | var $inputsInCell = $cell.find(focusableSelector) 236 | var currentInputIndex 237 | var $targetCell 238 | 239 | // if there are more than one inputs in the current cell, 240 | // jump to the one on the left/right, if any 241 | if ($inputsInCell.length > 1) { 242 | currentInputIndex = $inputsInCell.index($input) 243 | if (direction === 'left' && currentInputIndex > 0) { 244 | return $inputsInCell.eq(currentInputIndex - 1) 245 | } 246 | if (direction === 'right' && currentInputIndex < $inputsInCell.length - 1) { 247 | return $inputsInCell.eq(currentInputIndex + 1) 248 | } 249 | } 250 | 251 | $targetCell = (direction === 'left') ? $cell.prev() : $cell.next() 252 | return $targetCell.find(focusableSelector).eq(0) 253 | } 254 | 255 | // 256 | // when cloning a DOM element, values of