├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | // -*- mode: Javascript -*- 2 | 3 | 4 | // Use this file as a starting point for your project's .eslintrc. 5 | // Copy this file, and add rule overrides as needed. 6 | { 7 | "extends": "airbnb/base", 8 | 9 | /** 10 | * Overrides to the airbnb rules 11 | */ 12 | "rules": { 13 | "func-names": 0, 14 | "comma-dangle": [2, "never"], 15 | "no-console": 0, 16 | "no-param-reassign": 0, 17 | "no-undef": [2], 18 | "no-undefined": 1, 19 | "no-use-before-define": 0, 20 | "no-var": 0, 21 | "no-nested-ternary": 0, 22 | "object-shorthand": 0, 23 | // http://eslint.org/docs/rules/prefer-const 24 | "prefer-const": 1, 25 | // http://eslint.org/docs/rules/no-unused-vars.html 26 | "no-unused-vars": [1, {"vars": "all", "args": "after-used"}], 27 | "padded-blocks": [0, "always"], 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.0" 4 | - "4.1" 5 | script: 6 | - npm run test 7 | - npm run test:lint 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Juho Vepsalainen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build status](https://secure.travis-ci.org/bebraw/segmentize.png)](http://travis-ci.org/bebraw/segmentize) 2 | # segmentize - Simple segmentation useful for pagination 3 | 4 | ```javascript 5 | expect(segmentize({ 6 | page: 4, 7 | pages: 10, 8 | beginPages: 2, // optional 9 | endPages: 1, // optional 10 | sidePages: 1 // defaults to zero 11 | })).to.deep.equal({ 12 | beginPages: [1, 2], // one-indexed 13 | previousPages: [3], 14 | centerPage: [4], // always one page 15 | nextPages: [5], 16 | endPages: [10] 17 | }); 18 | ``` 19 | 20 | See `./test.js` for more examples. 21 | 22 | ## Contributors 23 | 24 | * [Artem Sapegin](https://github.com/sapegin) - `const` -> `var`. Safari needs this to work. 25 | 26 | ## License 27 | 28 | *segmentize* is available under MIT. See LICENSE for more details. 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (o) { 4 | var pages = o.pages; 5 | var page = Math.min(Math.max(o.page, 1), pages); 6 | var previousPages = o.sidePages ? 7 | range(Math.max(page - o.sidePages, 1), page) : []; 8 | var nextPages = o.sidePages ? 9 | range(page + 1, Math.min(page + o.sidePages + 1, pages)) : []; 10 | var beginPages = o.beginPages ? range(1, Math.min(o.beginPages, pages) + 1) : []; 11 | var endPages = o.endPages ? range(Math.max(pages - o.endPages + 1, 0), pages + 1) : []; 12 | 13 | return { 14 | beginPages: difference(beginPages, range(page, Math.max(pages, o.beginPages) + 1)), 15 | previousPages: difference(previousPages, beginPages), 16 | centerPage: [page], 17 | nextPages: difference(nextPages, endPages), 18 | endPages: difference(endPages, range(0, page + 1)) 19 | }; 20 | }; 21 | 22 | function range(a, b) { 23 | var len = b ? b : a; 24 | var ret = []; 25 | var i = b ? a : 0; 26 | 27 | for (; i < len; i++) { 28 | ret.push(i); 29 | } 30 | 31 | return ret; 32 | } 33 | 34 | function difference(a, b) { 35 | return a.filter(function (v) { 36 | return b.indexOf(v) < 0; 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "segmentize", 3 | "description": "Simple segmentation useful for pagination", 4 | "author": "Juho Vepsalainen ", 5 | "version": "0.4.1", 6 | "scripts": { 7 | "test": "mocha ./test", 8 | "test:watch": "mocha ./test --watch", 9 | "test:lint": "eslint .", 10 | "preversion": "npm run test && npm run test:lint" 11 | }, 12 | "main": "./index.js", 13 | "dependencies": {}, 14 | "devDependencies": { 15 | "chai": "^3.4.1", 16 | "eslint": "^1.10.3", 17 | "eslint-config-airbnb": "^3.1.0", 18 | "git-prepush-hook": "^1.0.1", 19 | "intersect": "^1.0.1", 20 | "jsverify": "^0.7.1", 21 | "mocha": "^2.3.4" 22 | }, 23 | "files": [ 24 | "*.md", 25 | "./index.js", 26 | "./LICENSE" 27 | ], 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/bebraw/segmentize.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/bebraw/segmentize/issues" 34 | }, 35 | "keywords": [ 36 | "segment", 37 | "segments", 38 | "pagination" 39 | ], 40 | "license": "MIT", 41 | "pre-push": [ 42 | "test", 43 | "test:lint" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const expect = require('chai').expect; 3 | const jsc = require('jsverify'); 4 | const intersect = require('intersect'); 5 | const segmentize = require('./'); 6 | 7 | describe('segmentize', function () { 8 | it('should show only one page if there is one', function () { 9 | expect(segmentize({ 10 | page: 1, 11 | pages: 1 12 | })).to.deep.equal({ 13 | beginPages: [], 14 | previousPages: [], 15 | centerPage: [1], 16 | nextPages: [], 17 | endPages: [] 18 | }); 19 | }); 20 | 21 | it('should show only one page if only page and pages are set', function () { 22 | const pageAmount = 2; 23 | 24 | expect(segmentize({ 25 | page: 1, 26 | pages: pageAmount 27 | })).to.deep.equal({ 28 | beginPages: [], 29 | previousPages: [], 30 | centerPage: [1], 31 | nextPages: [], 32 | endPages: [] 33 | }); 34 | }); 35 | 36 | it('should show current at start by default', function () { 37 | const pageAmount = 4; 38 | 39 | expect(segmentize({ 40 | page: 1, 41 | pages: pageAmount 42 | })).to.deep.equal({ 43 | beginPages: [], 44 | previousPages: [], 45 | centerPage: [1], 46 | nextPages: [], 47 | endPages: [] 48 | }); 49 | }); 50 | 51 | it('should show only current at end by default', function () { 52 | const pageAmount = 4; 53 | 54 | expect(segmentize({ 55 | page: pageAmount, 56 | pages: pageAmount 57 | })).to.deep.equal({ 58 | beginPages: [], 59 | previousPages: [], 60 | centerPage: [pageAmount], 61 | nextPages: [], 62 | endPages: [] 63 | }); 64 | }); 65 | 66 | it('should show only current if there is only one page', function () { 67 | expect(segmentize({ 68 | page: 1, 69 | pages: 1 70 | })).to.deep.equal({ 71 | beginPages: [], 72 | previousPages: [], 73 | centerPage: [1], 74 | nextPages: [], 75 | endPages: [] 76 | }); 77 | }); 78 | 79 | it('should accept begin pages', function () { 80 | expect(segmentize({ 81 | page: 5, 82 | pages: 10, 83 | beginPages: 2 84 | })).to.deep.equal({ 85 | beginPages: [1, 2], 86 | previousPages: [], 87 | centerPage: [5], 88 | nextPages: [], 89 | endPages: [] 90 | }); 91 | }); 92 | 93 | it('should accept begin pages at end', function () { 94 | const pageAmount = 10; 95 | 96 | expect(segmentize({ 97 | page: pageAmount, 98 | pages: pageAmount, 99 | beginPages: 2 100 | })).to.deep.equal({ 101 | beginPages: [1, 2], 102 | previousPages: [], 103 | centerPage: [pageAmount], 104 | nextPages: [], 105 | endPages: [] 106 | }); 107 | }); 108 | 109 | it('should accept begin pages with overlap', function () { 110 | expect(segmentize({ 111 | page: 2, 112 | pages: 10, 113 | beginPages: 2 114 | })).to.deep.equal({ 115 | beginPages: [1], 116 | previousPages: [], 117 | centerPage: [2], 118 | nextPages: [], 119 | endPages: [] 120 | }); 121 | }); 122 | 123 | it('should accept end pages', function () { 124 | expect(segmentize({ 125 | page: 5, 126 | pages: 10, 127 | endPages: 2 128 | })).to.deep.equal({ 129 | beginPages: [], 130 | previousPages: [], 131 | centerPage: [5], 132 | nextPages: [], 133 | endPages: [9, 10] 134 | }); 135 | }); 136 | 137 | it('should accept first page and end pages', function () { 138 | expect(segmentize({ 139 | page: 1, 140 | pages: 10, 141 | endPages: 2 142 | })).to.deep.equal({ 143 | beginPages: [], 144 | previousPages: [], 145 | centerPage: [1], 146 | nextPages: [], 147 | endPages: [9, 10] 148 | }); 149 | }); 150 | 151 | it('should accept end pages with overlap', function () { 152 | const pageAmount = 10; 153 | 154 | expect(segmentize({ 155 | page: pageAmount - 3, 156 | pages: pageAmount, 157 | endPages: 2 158 | })).to.deep.equal({ 159 | beginPages: [], 160 | previousPages: [], 161 | centerPage: [pageAmount - 3], 162 | nextPages: [], 163 | endPages: [pageAmount - 1, pageAmount] 164 | }); 165 | }); 166 | 167 | it('should accept both begin and end pages', function () { 168 | expect(segmentize({ 169 | page: 5, 170 | pages: 10, 171 | beginPages: 2, 172 | endPages: 2 173 | })).to.deep.equal({ 174 | beginPages: [1, 2], 175 | previousPages: [], 176 | centerPage: [5], 177 | nextPages: [], 178 | endPages: [9, 10] 179 | }); 180 | }); 181 | 182 | it('should show only one page if there is one with both begin and end pages', function () { 183 | expect(segmentize({ 184 | page: 1, 185 | pages: 1, 186 | beginPages: 3, 187 | endPages: 3 188 | })).to.deep.equal({ 189 | beginPages: [], 190 | previousPages: [], 191 | centerPage: [1], 192 | nextPages: [], 193 | endPages: [] 194 | }); 195 | }); 196 | 197 | it('should show only two pages if there are two with both begin and end pages', function () { 198 | expect(segmentize({ 199 | page: 1, 200 | pages: 2, 201 | beginPages: 3, 202 | endPages: 3 203 | })).to.deep.equal({ 204 | beginPages: [], 205 | previousPages: [], 206 | centerPage: [1], 207 | nextPages: [], 208 | endPages: [2] 209 | }); 210 | }); 211 | 212 | it('should show only two pages if there are two with both begin and end pages ' + 213 | 'and latter is selected', function () { 214 | expect(segmentize({ 215 | page: 2, 216 | pages: 2, 217 | beginPages: 3, 218 | endPages: 3 219 | })).to.deep.equal({ 220 | beginPages: [1], 221 | previousPages: [], 222 | centerPage: [2], 223 | nextPages: [], 224 | endPages: [] 225 | }); 226 | }); 227 | 228 | it('should segmentize #1 correctly', function () { 229 | expect(segmentize({ 230 | page: 1, 231 | pages: 3, 232 | beginPages: 3, 233 | endPages: 3 234 | })).to.deep.equal({ 235 | beginPages: [], 236 | previousPages: [], 237 | centerPage: [1], 238 | nextPages: [], 239 | endPages: [2, 3] 240 | }); 241 | }); 242 | 243 | it('should segmentize #2 correctly', function () { 244 | expect(segmentize({ 245 | page: 1, 246 | pages: 3, 247 | beginPages: 3, 248 | endPages: 3 249 | })).to.deep.equal({ 250 | beginPages: [], 251 | previousPages: [], 252 | centerPage: [1], 253 | nextPages: [], 254 | endPages: [2, 3] 255 | }); 256 | }); 257 | 258 | it('should segmentize 6 pages correctly when 4th page is selected', function () { 259 | expect(segmentize({ 260 | page: 4, 261 | pages: 6, 262 | beginPages: 3, 263 | endPages: 3 264 | })).to.deep.equal({ 265 | beginPages: [1, 2, 3], 266 | previousPages: [], 267 | centerPage: [4], 268 | nextPages: [], 269 | endPages: [5, 6] 270 | }); 271 | }); 272 | 273 | it('should segmentize #6 correctly', function () { 274 | expect(segmentize({ 275 | page: 9, 276 | pages: 11, 277 | beginPages: 3, 278 | endPages: 3 279 | })).to.deep.equal({ 280 | beginPages: [1, 2, 3], 281 | previousPages: [], 282 | centerPage: [9], 283 | nextPages: [], 284 | endPages: [10, 11] 285 | }); 286 | }); 287 | 288 | it('should segmentize start correctly', function () { 289 | expect(segmentize({ 290 | page: 2, 291 | pages: 11, 292 | beginPages: 3, 293 | endPages: 3 294 | })).to.deep.equal({ 295 | beginPages: [1], 296 | previousPages: [], 297 | centerPage: [2], 298 | nextPages: [], 299 | endPages: [9, 10, 11] 300 | }); 301 | }); 302 | 303 | it('should segmentize #9 correctly, part 1', function () { 304 | // given there's overlap between begin and end, this should merge 305 | expect(segmentize({ 306 | page: 3, 307 | pages: 7, 308 | beginPages: 3, 309 | endPages: 3 310 | })).to.deep.equal({ 311 | beginPages: [1, 2], 312 | previousPages: [], 313 | centerPage: [3], 314 | nextPages: [], 315 | endPages: [5, 6, 7] 316 | }); 317 | }); 318 | 319 | it('should segmentize #9 correctly, part 2', function () { 320 | // given begin and end are next after each other, this should merge 321 | expect(segmentize({ 322 | page: 4, 323 | pages: 7, 324 | beginPages: 3, 325 | endPages: 3 326 | })).to.deep.equal({ 327 | beginPages: [1, 2, 3], 328 | previousPages: [], 329 | centerPage: [4], 330 | nextPages: [], 331 | endPages: [5, 6, 7] 332 | }); 333 | }); 334 | 335 | it('should segmentize ten items correctly at start', function () { 336 | expect(segmentize({ 337 | page: 4, 338 | pages: 10, 339 | beginPages: 3, 340 | endPages: 3 341 | })).to.deep.equal({ 342 | beginPages: [1, 2, 3], 343 | previousPages: [], 344 | centerPage: [4], 345 | nextPages: [], 346 | endPages: [8, 9, 10] 347 | }); 348 | }); 349 | 350 | it('should segmentize ten items correctly at end', function () { 351 | expect(segmentize({ 352 | page: 5, 353 | pages: 10, 354 | beginPages: 3, 355 | endPages: 3 356 | })).to.deep.equal({ 357 | beginPages: [1, 2, 3], 358 | previousPages: [], 359 | centerPage: [5], 360 | nextPages: [], 361 | endPages: [8, 9, 10] 362 | }); 363 | }); 364 | 365 | it('should accept side pages number', function () { 366 | expect(segmentize({ 367 | page: 5, 368 | pages: 15, 369 | sidePages: 2 370 | })).to.deep.equal({ 371 | beginPages: [], 372 | previousPages: [3, 4], 373 | centerPage: [5], 374 | nextPages: [6, 7], 375 | endPages: [] 376 | }); 377 | }); 378 | 379 | it('should accept side pages number', function () { 380 | expect(segmentize({ 381 | page: 5, 382 | pages: 15, 383 | sidePages: 2, 384 | beginPages: 2, 385 | endPages: 2 386 | })).to.deep.equal({ 387 | beginPages: [1, 2], 388 | previousPages: [3, 4], 389 | centerPage: [5], 390 | nextPages: [6, 7], 391 | endPages: [14, 15] 392 | }); 393 | }); 394 | 395 | it('should not show begin pages', function () { 396 | expect(segmentize({ 397 | page: 1, 398 | pages: 1000, 399 | beginPages: 3, 400 | endPages: 3, 401 | sidePages: 2 402 | })).to.deep.equal({ 403 | beginPages: [], 404 | previousPages: [], 405 | centerPage: [1], 406 | nextPages: [2, 3], 407 | endPages: [998, 999, 1000] 408 | }); 409 | }); 410 | 411 | it('should accept side pages number for the first page', function () { 412 | expect(segmentize({ 413 | page: 1, 414 | pages: 15, 415 | sidePages: 3 416 | })).to.deep.equal({ 417 | beginPages: [], 418 | previousPages: [], 419 | centerPage: [1], 420 | nextPages: [2, 3, 4], 421 | endPages: [] 422 | }); 423 | }); 424 | 425 | it('should clamp to begin', function () { 426 | expect(segmentize({ 427 | page: -1, 428 | pages: 1000, 429 | beginPages: 3, 430 | endPages: 3, 431 | sidePages: 2 432 | })).to.deep.equal({ 433 | beginPages: [], 434 | previousPages: [], 435 | centerPage: [1], 436 | nextPages: [2, 3], 437 | endPages: [998, 999, 1000] 438 | }); 439 | }); 440 | 441 | it('should accept side pages number for the last page', function () { 442 | expect(segmentize({ 443 | page: 15, 444 | pages: 15, 445 | sidePages: 3 446 | })).to.deep.equal({ 447 | beginPages: [], 448 | previousPages: [12, 13, 14], 449 | centerPage: [15], 450 | nextPages: [], 451 | endPages: [] 452 | }); 453 | }); 454 | 455 | it('should clamp to end', function () { 456 | expect(segmentize({ 457 | page: 1001, 458 | pages: 1000, 459 | beginPages: 3, 460 | endPages: 3, 461 | sidePages: 2 462 | })).to.deep.equal({ 463 | beginPages: [1, 2, 3], 464 | previousPages: [998, 999], 465 | centerPage: [1000], 466 | nextPages: [], 467 | endPages: [] 468 | }); 469 | }); 470 | 471 | it('should not intersect', function () { 472 | const property = jsc.forall(jsc.nat(), jsc.nat(), function (a, b) { 473 | const max = Math.max(a, b); 474 | const min = Math.min(a, b); 475 | 476 | return !intersect.apply(null, values(segmentize({ 477 | page: min, 478 | pages: max, 479 | sidePages: min, 480 | beginPages: min, 481 | endPages: min 482 | }))).length; 483 | }); 484 | 485 | jsc.assert(property); 486 | }); 487 | }); 488 | 489 | function values(o) { 490 | return Object.keys(o).map(function (k) { 491 | return Array.isArray(o[k]) ? o[k] : [o[k]]; 492 | }); 493 | } 494 | --------------------------------------------------------------------------------