├── tslint.json ├── .vscode ├── extensions.json └── settings.json ├── .npmignore ├── .gitignore ├── src ├── utils.ts ├── types.ts └── index.ts ├── .travis.yml ├── public ├── app.ts ├── app.css └── index.html ├── rollup.config.js ├── tsconfig.json ├── LICENSE.md ├── karma.conf.js ├── package.json ├── test ├── markup.js └── test.spec.ts └── README.md /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard-plus" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "eg2.tslint" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !/*.js 3 | !/*.map 4 | !/*.d.ts 5 | !README.md 6 | !LICENSE.md 7 | !package.json 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | **/.DS_Store 5 | *.log 6 | /*.js 7 | /*.map 8 | /*.d.ts 9 | *cache 10 | 11 | !/*.config.js 12 | !/*.conf.js 13 | !/benchmark.js 14 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function child (element: HTMLElement | null, selector: string): HTMLElement | null { 2 | if (!element || !selector) return null 3 | 4 | return element.querySelector(selector) as HTMLElement 5 | } 6 | 7 | export function children (selector: string, parent?: HTMLElement | null): HTMLElement[] { 8 | if (!parent) { 9 | parent = document.body 10 | } 11 | 12 | return Array.prototype.slice.call(parent.querySelectorAll(selector)) 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.tabSize": 2, 4 | "editor.rulers": [ 5 | 120 6 | ], 7 | "files.trimTrailingWhitespace": true, 8 | "files.insertFinalNewline": true, 9 | "files.exclude": { 10 | "**/.git": true, 11 | "**/.DS_Store": true, 12 | "node_modules": true, 13 | "test-lib": true, 14 | "lib": true, 15 | "coverage": true, 16 | "npm": true 17 | }, 18 | "eslint.enable": false, 19 | "typescript.format.enable": false, 20 | "tslint.enable": true, 21 | "tslint.autoFixOnSave": true, 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - '8' 7 | - '10' 8 | 9 | env: 10 | - NODE_ENV=development 11 | 12 | git: 13 | depth: 1 14 | 15 | cache: 16 | yarn: true 17 | directories: 18 | - node_modules 19 | 20 | notifications: 21 | email: false 22 | 23 | before_install: 24 | - export CHROME_BIN=chromium-browser 25 | - export DISPLAY=:99.0 26 | - sh -e /etc/init.d/xvfb start 27 | 28 | before_script: 29 | - "sudo chown root /usr/lib/chromium-browser/chrome-sandbox" 30 | - "sudo chmod 4755 /usr/lib/chromium-browser/chrome-sandbox" 31 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { EmitusListener as Listener } from 'emitus' 2 | 3 | // Events 4 | export type Event = 'filter' | 'sort' 5 | 6 | export interface Options { 7 | container: HTMLElement | string 8 | selector: string 9 | gutter?: number 10 | resetFilterValue?: string 11 | hiddenClass?: string 12 | matchedClass?: string 13 | } 14 | 15 | export interface Sortboard { 16 | // Methods 17 | filter (pattern: string): void 18 | sort (): void 19 | reset (): void 20 | 21 | // Events 22 | on (eventName: Event, listener: Listener): void 23 | off (eventName: Event, listener?: Listener): void 24 | } 25 | 26 | export interface Elements { 27 | container: HTMLElement 28 | blocks: HTMLElement[] 29 | } 30 | -------------------------------------------------------------------------------- /public/app.ts: -------------------------------------------------------------------------------- 1 | import { sortboard } from '../src' 2 | 3 | const sb = sortboard({ 4 | container: '#sortlist', 5 | selector: 'li' 6 | }) 7 | 8 | const anchors = children('#filters a') 9 | 10 | anchors.forEach((el) => { 11 | el.addEventListener( 12 | 'click', 13 | (event) => { 14 | const an = event.target 15 | 16 | if (!an.classList.contains('active')) { 17 | const data = an.getAttribute('data-filter') 18 | sb.filter(data) 19 | 20 | anchors.forEach((a) => { 21 | a.classList.remove('active') 22 | }) 23 | an.classList.add('active') 24 | } 25 | }, 26 | false 27 | ) 28 | }) 29 | 30 | function children (selector, parent = document) { 31 | return Array.prototype.slice 32 | .call(parent.querySelectorAll(selector)) 33 | } 34 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { name } from './package.json' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import resolve from 'rollup-plugin-node-resolve' 4 | import typescript from 'rollup-plugin-typescript2' 5 | import { terser } from 'rollup-plugin-terser' 6 | 7 | export default { 8 | input: 'src/index.ts', 9 | name, 10 | output: { 11 | file: `./dist/${name}.umd.min.js`, 12 | format: 'umd', 13 | sourcemap: false, 14 | exports: 'named' 15 | }, 16 | plugins: [ 17 | typescript(), 18 | resolve(), 19 | commonjs({ 20 | sourceMap: false, 21 | include: 'node_modules/emitus/index.js' 22 | }), 23 | terser() 24 | ], 25 | onwarn 26 | } 27 | 28 | function onwarn(message) { 29 | const suppressed = ['UNRESOLVED_IMPORT', 'THIS_IS_UNDEFINED'] 30 | 31 | if (!suppressed.find((code) => message.code === code)) { 32 | return console.warn(message.message) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "include": [ 4 | "src" 5 | ], 6 | "exclude": [ 7 | "test", 8 | "dist", 9 | "node_modules" 10 | ], 11 | "compilerOptions": { 12 | "outDir": "./dist", 13 | "strict": true, 14 | "declaration": true, 15 | "sourceMap": false, 16 | "pretty": true, 17 | "noImplicitReturns": true, 18 | "noUnusedParameters": true, 19 | "moduleResolution": "node", 20 | "emitDecoratorMetadata": true, 21 | "experimentalDecorators": true, 22 | "allowSyntheticDefaultImports": true, 23 | "module": "es2015", 24 | "target": "es2015", 25 | "typeRoots": [ 26 | "node_modules/@types" 27 | ], 28 | "types": [ 29 | "@types/jasmine", 30 | "@types/node" 31 | ], 32 | "lib": [ 33 | "es2017", 34 | "dom", 35 | "es2015.generator", 36 | "es2015.iterable", 37 | "es2015.promise", 38 | "es2015.symbol", 39 | "es2015.symbol.wellknown", 40 | "esnext.asynciterable" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present José Luis Quintana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const realBrowser = String(process.env.BROWSER).match(/^(1|true)$/gi) 2 | const travisLaunchers = { 3 | chrome_travis: { 4 | base: 'Chrome', 5 | flags: ['--no-sandbox'] 6 | } 7 | } 8 | 9 | const localBrowsers = realBrowser ? Object.keys(travisLaunchers) : ['Chrome'] 10 | 11 | module.exports = (config) => { 12 | const env = process.env['NODE_ENV'] || 'development' 13 | 14 | config.set({ 15 | frameworks: ['jasmine', 'karma-typescript'], 16 | plugins: [ 17 | 'karma-jasmine', 18 | 'karma-typescript', 19 | 'karma-chrome-launcher', 20 | 'karma-jasmine-html-reporter', 21 | 'karma-spec-reporter' 22 | ], 23 | client: { 24 | clearContext: false // leave Jasmine Spec Runner output visible in browser 25 | }, 26 | files: [{ pattern: 'src/**/*.ts' }, { pattern: 'test/**/*.spec.ts' }], 27 | preprocessors: { 28 | '**/*.ts': ['karma-typescript'], 29 | 'test/**/*.spec.ts': ['karma-typescript'] 30 | }, 31 | reporters: ['progress', 'kjhtml'], 32 | colors: true, 33 | logLevel: env === 'debug' ? config.LOG_DEBUG : config.LOG_INFO, 34 | autoWatch: true, 35 | browsers: localBrowsers, 36 | singleRun: false 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sortboard", 3 | "version": "3.1.0", 4 | "description": "An small library for easy sorting and filtering of elements.", 5 | "repository": "joseluisq/sortboard", 6 | "license": "MIT", 7 | "main": "./dist/index.js", 8 | "typings": "./dist/index.d.ts", 9 | "author": { 10 | "name": "José Luis Quintana", 11 | "url": "http://git.io/joseluisq" 12 | }, 13 | "files": [ 14 | "/dist/*.js", 15 | "/dist/*.d.ts", 16 | "README.md", 17 | "LICENSE.md" 18 | ], 19 | "keywords": [ 20 | "grid", 21 | "sort", 22 | "filter", 23 | "sortlist", 24 | "filterlist", 25 | "responsive", 26 | "block" 27 | ], 28 | "scripts": { 29 | "clean": "rm -rf dist .cache", 30 | "prestart": "yarn clean", 31 | "start": "parcel public/index.html", 32 | "version": "npm run build", 33 | "build": "npm run test && npm run clean && npm run compile", 34 | "compile": "npm run compile:browser && npm run compile:cjs", 35 | "compile:cjs": "tsc --module commonjs --target es5", 36 | "compile:browser": "rollup -c", 37 | "watch": "tsc --watch", 38 | "test": "npm run lint && karma start --single-run --reporters spec", 39 | "test:watch": "env NODE_ENV=debug karma start", 40 | "lint": "tslint --format stylish --project tsconfig.json" 41 | }, 42 | "dependencies": { 43 | "emitus": "^2.3.1" 44 | }, 45 | "devDependencies": { 46 | "@types/jasmine": "^2.5.54", 47 | "@types/node": "^8.0.26", 48 | "git-testing-hook": "^0.3.0", 49 | "jasmine-core": "^3.2.1", 50 | "jasmine-spec-reporter": "^4.2.1", 51 | "karma": "^2.0.5", 52 | "karma-chrome-launcher": "^2.2.0", 53 | "karma-jasmine": "^1.1.2", 54 | "karma-jasmine-html-reporter": "^1.3.0", 55 | "karma-spec-reporter": "^0.0.32", 56 | "karma-typescript": "^3.0.13", 57 | "parcel-bundler": "^1.9.7", 58 | "parcel-plugin-typescript": "^1.0.0", 59 | "rollup": "^0.64.1", 60 | "rollup-plugin-commonjs": "^9.1.5", 61 | "rollup-plugin-node-resolve": "^3.3.0", 62 | "rollup-plugin-terser": "^1.0.1", 63 | "rollup-plugin-typescript2": "^0.16.1", 64 | "tslint": "^5.11.0", 65 | "tslint-config-standard-plus": "^2.0.1", 66 | "typescript": "^2.9.2" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/markup.js: -------------------------------------------------------------------------------- 1 | module.exports.init = () => { 2 | const tmpl = ` 3 | 49 | ` 50 | 51 | document.body.innerHTML = tmpl 52 | } 53 | -------------------------------------------------------------------------------- /public/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Monaco', monospace; 3 | background: rgba(226, 226, 226, 0.3); 4 | cursor: default; 5 | margin: 0; 6 | } 7 | 8 | h1, 9 | p { 10 | text-align: center; 11 | padding: 0; 12 | display: block; 13 | margin: 0; 14 | } 15 | 16 | p { 17 | padding: 0; 18 | color: #B0B0B0; 19 | } 20 | 21 | h1 { 22 | margin-top: 10px; 23 | margin-bottom: 5px; 24 | font-weight: normal; 25 | } 26 | 27 | #chessboard { 28 | margin: 0 auto; 29 | max-width: 800px; 30 | overflow: hidden; 31 | position: relative; 32 | } 33 | 34 | /* Filters */ 35 | #filters { 36 | text-align: center; 37 | color: #b0b0b0; 38 | font-size: 12px; 39 | padding: 15px 0 0 0; 40 | } 41 | 42 | #filters a { 43 | color: #2e3032; 44 | margin-left: 0; 45 | margin-right: 0; 46 | text-decoration: none; 47 | transition: all .2s ease-out; 48 | font-size: 11px; 49 | } 50 | 51 | #filters a:hover, 52 | #filters a.active { 53 | color: #d37070; 54 | } 55 | /* Sortlist */ 56 | 57 | #sortlist { 58 | position: relative; 59 | display: block; 60 | padding: 0; 61 | margin: 1.5em auto; 62 | overflow: hidden; 63 | -webkit-transition: all 0.4s ease-out; 64 | -moz-transition: all 0.4s ease-out; 65 | transition: all 0.4s ease-out; 66 | } 67 | 68 | #sortlist li { 69 | position: absolute; 70 | width: 90px; 71 | height: 90px; 72 | display: block; 73 | text-align: center; 74 | font-size: 3.2em; 75 | line-height: 1.6em; 76 | overflow: hidden; 77 | color: #868686; 78 | background-color: #E9E9E9; 79 | box-shadow: inset 0px 0px 0px 1px #CECECE; 80 | 81 | -webkit-transition: all 0.4s ease-out; 82 | -moz-transition: all 0.4s ease-out; 83 | transition: all 0.4s ease-out; 84 | } 85 | 86 | #sortlist > li:hover { 87 | color: #D17E7E; 88 | } 89 | 90 | /* Creating the chessboard */ 91 | #sortlist li:nth-child(-2n+8), 92 | #sortlist li:nth-child(8) ~ li:nth-child(-2n+15), 93 | #sortlist li:nth-child(16) ~ li:nth-child(-2n+24), 94 | #sortlist li:nth-child(24) ~ li:nth-child(-2n+31), 95 | #sortlist li:nth-child(32) ~ li:nth-child(-2n+40), 96 | #sortlist li:nth-child(40) ~ li:nth-child(-2n+47), 97 | #sortlist li:nth-child(48) ~ li:nth-child(-2n+56), 98 | #sortlist li:nth-child(56) ~ li:nth-child(-2n+63) { 99 | background-color: white; 100 | } 101 | 102 | #sortlist.filtered > li { 103 | color: #D17E7E; 104 | background-color: white; 105 | } 106 | 107 | .link { 108 | font-size: 11px; 109 | display: block; 110 | padding: 2em 0; 111 | } 112 | 113 | .link a { 114 | color: #2e3032; 115 | } 116 | 117 | @media screen and (max-width: 480px) { 118 |  #filters a { 119 |    margin: 20px 5px; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/test.spec.ts: -------------------------------------------------------------------------------- 1 | import markup from './markup' 2 | import { Listener, sortboard, Sortboard } from '../src' 3 | 4 | markup.init() 5 | 6 | describe('Sortboard', () => { 7 | let sb: Sortboard 8 | 9 | beforeEach(() => { 10 | sb = sortboard({ 11 | container: '#sortlist', 12 | selector: 'li' 13 | }) as Sortboard 14 | }) 15 | 16 | describe('api', () => { 17 | it('should be a function', () => { 18 | expect(typeof sortboard).toBe('function') 19 | }) 20 | 21 | it('should return an object', () => { 22 | expect(typeof sb).toBe('object') 23 | }) 24 | 25 | it('should contain an `on` function', () => { 26 | expect(typeof sb.on).toBe('function') 27 | }) 28 | 29 | it('should contain an `off` function', () => { 30 | expect(typeof sb.off).toBe('function') 31 | }) 32 | 33 | it('should contain a `filter` function', () => { 34 | expect(typeof sb.filter).toBe('function') 35 | }) 36 | 37 | it('should contain a `reset` function', () => { 38 | expect(typeof sb.reset).toBe('function') 39 | }) 40 | 41 | it('should contain a `sort` function', () => { 42 | expect(typeof sb.sort).toBe('function') 43 | }) 44 | }) 45 | 46 | describe('on', () => { 47 | let onSpy: jasmine.Spy 48 | const onEvent: Listener = () => console.log('ok!') 49 | 50 | describe('`filter` event', () => { 51 | beforeEach(() => { 52 | onSpy = spyOn(sb, 'on') 53 | sb.on('filter', onEvent) 54 | }) 55 | 56 | it('should be a function', () => { 57 | expect(typeof sb.filter).toBe('function') 58 | }) 59 | 60 | it('should be called', () => { 61 | expect(sb.on).toHaveBeenCalled() 62 | }) 63 | 64 | it('should track all the arguments of its calls', () => { 65 | expect(sb.on).toHaveBeenCalledWith('filter', onEvent) 66 | }) 67 | 68 | it('should contain the given arguments (`filter` event)', () => { 69 | const [ type, args ] = onSpy.calls.argsFor(0) 70 | 71 | expect(type).toBe('filter') 72 | expect(args).toEqual(onEvent) 73 | }) 74 | }) 75 | 76 | describe('`sort` event', () => { 77 | beforeEach(() => { 78 | onSpy = spyOn(sb, 'on') 79 | sb.on('sort', onEvent) 80 | }) 81 | 82 | it('should be a function', () => { 83 | expect(typeof sb.sort).toBe('function') 84 | }) 85 | 86 | it('should be called', () => { 87 | expect(sb.on).toHaveBeenCalled() 88 | }) 89 | 90 | it('should track all the arguments of its calls', () => { 91 | expect(sb.on).toHaveBeenCalledWith('sort', onEvent) 92 | }) 93 | 94 | it('should contain the given arguments (`sort` event)', () => { 95 | const [ type, args ] = onSpy.calls.argsFor(0) 96 | 97 | expect(type).toBe('sort') 98 | expect(args).toEqual(onEvent) 99 | }) 100 | }) 101 | }) 102 | 103 | }) 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sortboard 2 | 3 | [![npm](https://img.shields.io/npm/v/sortboard.svg)](https://www.npmjs.com/package/sortboard) [![npm](https://img.shields.io/npm/dt/sortboard.svg)](https://www.npmjs.com/package/sortboard) [![Build Status](https://travis-ci.org/joseluisq/sortboard.svg?branch=master)](https://travis-ci.org/joseluisq/sortboard) [![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 4 | > An small library for easy sorting and filtering of elements. 5 | 6 | Sortboard is small library for sorting and filtering HTML elements which uses CSS3 [`matrix()` and `scale()`](http://www.w3.org/TR/2011/WD-css3-2d-transforms-20111215/) transform functions. It supports [RegEx](https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions) filters and it's responsive by default. 7 | 8 | :tada: View demo on [Codepen](http://codepen.io/joseluisq/full/IlHzo/) 9 | 10 | ## Install 11 | 12 | [Yarn](https://github.com/yarnpkg/) 13 | 14 | ```sh 15 | yarn add sortboard --dev 16 | ``` 17 | 18 | [NPM](https://www.npmjs.com/) 19 | 20 | ```sh 21 | npm install sortboard --save-dev 22 | ``` 23 | 24 | The [UMD](https://github.com/umdjs/umd) build is also available on [unpkg](https://unpkg.com). 25 | 26 | ```html 27 | 28 | ``` 29 | 30 | You can use the library via `window.sortboard` 31 | 32 | ## Usage 33 | 34 | Define some list. For example an `ul` with `li` child elements, then set your filters in each `li` element with the `data-filter` attribute. It can add several filters by element separated by whitespace. 35 | 36 | __Typescript:__ 37 | 38 | ```ts 39 | import { sortboard, Sortboard, Listener } from 'sortboard' 40 | 41 | const sb = sortboard({ 42 | container: '#mylist', 43 | selector: 'li' 44 | }) 45 | 46 | const onEvent: Listener = () => console.log('ok!') 47 | sb.on('filter', onEvent) 48 | 49 | sb.filter('programing front-end') 50 | 51 | // Or filter using a RegEx 52 | sb.filter(/(webdesign|illustration)/) 53 | ``` 54 | 55 | __Markup:__ 56 | 57 | ```html 58 | 66 | ``` 67 | 68 | ## API 69 | 70 | ### Options 71 | 72 | - __container__ : The container of elements. 73 | - __selector__ : The query selector for each element. 74 | - __gutter__ : The margin for each element defined in pixels. Default is `10` pixels. 75 | - __resetFilterValue__ : The reset filter value used by `reset()` method. Default is `all`. 76 | - __hiddenClass__ : Class name for hidden elements. Default is `.sortboard-hidden` class name. 77 | - __matchedClass__ : Class name for matched elements. Default is `.sortboard-matched` class name. 78 | 79 | ### Methods 80 | 81 | - __filter( string | RegExp )__ : It's string or `RegExp` regular expresion pattern to filter. Which should match to `data-filter` attribute for each element to searching. 82 | - __reset()__ : Reset the elements which uses `resetFilterValue` option for resetting. 83 | - __sort()__ : Sort the element positions in the container. Sort method is called after the `filter()` method and in each trigger of `window.resize` event. 84 | 85 | ### Events 86 | 87 | #### filter 88 | Trigger when filter is completed. 89 | 90 | Passed params: 91 | - __matchedElements:__ An `array` with all matched elements. 92 | - __restElements:__ An `array` with the rest (not matched) elements. 93 | - __filterValue:__ A `string` with the current filter value used. 94 | 95 | ```js 96 | sb.on('filter', (matchedElements, restElements, filterValue) => {}) 97 | ``` 98 | 99 | #### sort 100 | Trigger when sort is completed. 101 | 102 | ```js 103 | sb.on('sort', () => {}) 104 | ``` 105 | 106 | ## Development 107 | 108 | ```sh 109 | yarn start 110 | ``` 111 | 112 | ## Contributions 113 | 114 | Feel free to send some [pull request](https://github.com/joseluisq/sortboard/pulls) or [issue](https://github.com/joseluisq/sortboard/issues). 115 | 116 | ## License 117 | MIT license 118 | 119 | © 2014-present [José Luis Quintana](http://git.io/joseluisq) 120 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { child, children } from './utils' 2 | import { Elements, Event, Options, Sortboard } from './types' 3 | import { emitus, Emitus, EmitusListener as Listener } from 'emitus' 4 | 5 | export const defaults: Options = { 6 | container: '.sortboard', 7 | selector: '.sortboard-block', 8 | gutter: 0, 9 | resetFilterValue: 'all', 10 | hiddenClass: '.sortboard-hidden', 11 | matchedClass: '.sortboard-matched' 12 | } 13 | 14 | const emitter: Emitus = emitus() 15 | 16 | export default function sortboard (options?: Options): Sortboard | null { 17 | const opts: Options = { ...defaults, ...options } as Options 18 | 19 | if (!opts.container) { 20 | return null 21 | } 22 | 23 | let container: HTMLElement 24 | 25 | if (typeof opts.container === 'string') { 26 | const childContainer: HTMLElement | null = child(document.body, opts.container) 27 | 28 | if (!childContainer) { 29 | return null 30 | } 31 | 32 | container = childContainer 33 | } else { 34 | container = opts.container 35 | } 36 | 37 | let api: Sortboard | null = null 38 | 39 | const blocks: HTMLElement[] = children(opts.selector, container) 40 | 41 | if (!blocks.length) { 42 | return null 43 | } 44 | 45 | const els: Elements = { container, blocks } 46 | 47 | api = createSortboard(els, opts) 48 | 49 | return api 50 | } 51 | 52 | export { sortboard, Sortboard, Elements, Options, Listener, Event, Emitus } 53 | 54 | function createSortboard ({ container, blocks }: Elements, opts: Options): Sortboard { 55 | let currentFilter: string | RegExp = '' 56 | 57 | const api: Sortboard = { 58 | // Methods 59 | sort, 60 | reset, 61 | filter, 62 | 63 | // Events 64 | on: emitter.on, 65 | off: emitter.off 66 | } 67 | 68 | init() 69 | 70 | return api 71 | 72 | function init (): void { 73 | events() 74 | sort() 75 | } 76 | 77 | function filter (pattern: string | RegExp = ''): void { 78 | if (!pattern || pattern === currentFilter) return 79 | 80 | currentFilter = pattern 81 | 82 | const notMatchedElements: HTMLElement[] = [] 83 | const matchedElements: HTMLElement[] = blocks.filter((item: any) => { 84 | const cords: string = item.getAttribute('data-cords').trim() 85 | 86 | if (pattern === opts.resetFilterValue) { 87 | translate(item, cords, false) 88 | return true 89 | } 90 | 91 | const data: string = (item.getAttribute('data-filter') || '').toString().trim() 92 | const regx: RegExp = pattern instanceof RegExp ? pattern : RegExp(pattern) 93 | const matchPattern: boolean = !!data && regx.test(data) 94 | 95 | if (!matchPattern) { 96 | notMatchedElements.push(item) 97 | } 98 | 99 | translate(item, cords, !matchPattern) 100 | 101 | return matchPattern 102 | }) 103 | 104 | emitter.emit('filter', [ matchedElements, notMatchedElements, currentFilter ]) 105 | 106 | if (matchedElements.length) { 107 | sort() 108 | } 109 | } 110 | 111 | function reset (): void { 112 | filter(opts.resetFilterValue) 113 | } 114 | 115 | function sort (): void { 116 | let n = 0 117 | let x = 0 118 | let y = 0 119 | let totalW = 0 120 | let totalH = 0 121 | let breakW = 0 122 | 123 | const gutter: number = opts.gutter! 124 | const parentWidth: number = container.parentElement!.offsetWidth 125 | 126 | blocks.forEach((child: HTMLElement) => { 127 | if (child.classList.contains(opts.hiddenClass!)) return 128 | 129 | if (totalW >= parentWidth - child.offsetHeight - gutter) { 130 | totalW = 0 131 | y += child.offsetHeight + gutter 132 | 133 | if (!breakW) { 134 | breakW = n * child.offsetWidth + (n * gutter - gutter) 135 | } 136 | } else { 137 | totalW += n ? gutter : 0 138 | } 139 | 140 | n++ 141 | totalW += child.offsetWidth 142 | x = totalW - child.offsetWidth 143 | totalH = y + child.offsetHeight 144 | 145 | translate(child, `${x},${y}`, false) 146 | }) 147 | 148 | const pNWidth: number = n * parentWidth + (n < 2 ? n : n - 1) * gutter 149 | const pWidth: number = breakW || pNWidth 150 | 151 | container.style.width = `${pWidth}px` 152 | container.style.height = `${totalH}px` 153 | 154 | emitter.emit('sort') 155 | } 156 | 157 | function events (): void { 158 | window.addEventListener('resize', sort, false) 159 | } 160 | 161 | function translate (item: HTMLElement, cords: string, hide: boolean): void { 162 | const matrix = `matrix(1,0,0,1,${cords}) scale(${hide ? '0.001' : '1'})` 163 | 164 | if (hide) { 165 | item.classList.add(opts.hiddenClass!) 166 | item.classList.remove(opts.matchedClass!) 167 | } else { 168 | item.classList.add(opts.matchedClass!) 169 | item.classList.remove(opts.hiddenClass!) 170 | } 171 | 172 | item.setAttribute('data-cords', cords) 173 | item.style.setProperty('opacity', hide ? '0' : '1') 174 | item.style.setProperty('-webkit-transform', matrix) 175 | item.style.setProperty('-moz-transform', matrix) 176 | item.style.setProperty('transform', matrix) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Sortboard 9 | 10 | 11 | 12 | 13 | 14 | 16 | 18 | 20 | 49 | 50 | 51 |

Sortboard

52 |

Easy sorting and filtering of elements.

53 | 54 | 66 | 67 |
68 | 69 | 117 | 118 |
119 | 120 | 121 | 122 | 123 | 124 | --------------------------------------------------------------------------------