├── test ├── mock-css.js ├── jest-pretest.ts └── unit │ ├── value-converters │ └── upcase.spec.ts │ ├── attributes │ └── color.spec.ts │ ├── elements │ └── hello-world.spec.ts │ └── binding-behaviors │ └── primary-click.spec.ts ├── aurelia_project ├── tasks │ ├── jest.ts │ ├── lint.json │ ├── clear-cache.json │ ├── clear-cache.ts │ ├── jest.json │ ├── test.json │ ├── lint.ts │ ├── build.json │ ├── process-json.ts │ ├── test.ts │ ├── run.json │ ├── build.ts │ ├── run.ts │ ├── copy-files.ts │ ├── build-plugin.ts │ ├── process-css.ts │ ├── process-markup.ts │ ├── transpile.ts │ ├── watch.ts │ └── dev-server.ts ├── environments │ ├── dev.ts │ ├── prod.ts │ └── stage.ts ├── generators │ ├── task.json │ ├── attribute.json │ ├── generator.json │ ├── element.json │ ├── value-converter.json │ ├── binding-behavior.json │ ├── value-converter.ts │ ├── binding-behavior.ts │ ├── task.ts │ ├── attribute.ts │ ├── element.ts │ └── generator.ts └── aurelia.json ├── .npmrc ├── favicon.ico ├── src ├── au-datatable.scss ├── components │ ├── pagination │ │ ├── _pagination.scss │ │ ├── pagination.html │ │ └── pagination.ts │ ├── info │ │ ├── info.html │ │ └── info.ts │ ├── search │ │ ├── search.html │ │ └── search.ts │ ├── pagesize │ │ ├── pagesize.html │ │ └── pagesize.ts │ └── filter │ │ ├── filter.html │ │ ├── _filter.scss │ │ └── filter.ts ├── models │ ├── response.ts │ ├── filter.ts │ └── request.ts ├── attributes │ ├── datatable.ts │ └── sort.ts └── index.ts ├── .vscode ├── settings.json ├── extensions.json └── launch.json ├── dev-app ├── app.ts ├── app.html └── main.ts ├── README.md ├── .editorconfig ├── index.html ├── tsconfig.json ├── .eslintrc.json ├── .gitignore └── package.json /test/mock-css.js: -------------------------------------------------------------------------------- 1 | module.exports = ''; 2 | -------------------------------------------------------------------------------- /aurelia_project/tasks/jest.ts: -------------------------------------------------------------------------------- 1 | export {default} from './test'; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # for pnpm, use flat node_modules 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtaalbers-com/au-datatable/HEAD/favicon.ico -------------------------------------------------------------------------------- /aurelia_project/environments/dev.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | debug: true, 3 | testing: true 4 | }; 5 | -------------------------------------------------------------------------------- /aurelia_project/environments/prod.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | debug: false, 3 | testing: false 4 | }; 5 | -------------------------------------------------------------------------------- /aurelia_project/environments/stage.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | debug: true, 3 | testing: false 4 | }; 5 | -------------------------------------------------------------------------------- /aurelia_project/tasks/lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lint", 3 | "description": "Lint source files", 4 | "flags": [] 5 | } -------------------------------------------------------------------------------- /aurelia_project/generators/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task", 3 | "description": "Creates a task and places it in the project tasks folder." 4 | } -------------------------------------------------------------------------------- /src/au-datatable.scss: -------------------------------------------------------------------------------- 1 | /** Import sass components **/ 2 | 3 | @import './components/filter/filter'; 4 | @import './components/pagination/pagination'; 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "html.suggest.angular1": false, 4 | "html.suggest.ionic": false 5 | } -------------------------------------------------------------------------------- /aurelia_project/generators/attribute.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "attribute", 3 | "description": "Creates a custom attribute class and places it in the project resources." 4 | } -------------------------------------------------------------------------------- /aurelia_project/generators/generator.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator", 3 | "description": "Creates a generator class and places it in the project generators folder." 4 | } -------------------------------------------------------------------------------- /aurelia_project/generators/element.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "element", 3 | "description": "Creates a custom element class and template, placing them in the project resources." 4 | } -------------------------------------------------------------------------------- /aurelia_project/generators/value-converter.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "value-converter", 3 | "description": "Creates a value converter class and places it in the project resources." 4 | } -------------------------------------------------------------------------------- /aurelia_project/generators/binding-behavior.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binding-behavior", 3 | "description": "Creates a binding behavior class and places it in the project resources." 4 | } -------------------------------------------------------------------------------- /aurelia_project/tasks/clear-cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clear-cache", 3 | "description": "Clear both transpile cache (only for esnext), and tracing-cache (for CLI built-in tracer)." 4 | } -------------------------------------------------------------------------------- /aurelia_project/tasks/clear-cache.ts: -------------------------------------------------------------------------------- 1 | import * as gulp from 'gulp'; 2 | import {build} from 'aurelia-cli'; 3 | 4 | export default function clearCache() { 5 | return build.clearCache(); 6 | } 7 | -------------------------------------------------------------------------------- /src/components/pagination/_pagination.scss: -------------------------------------------------------------------------------- 1 | .au-pagination { 2 | list-style: none; 3 | 4 | li { 5 | float: left; 6 | 7 | a { 8 | padding: 5px 10px; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /dev-app/app.ts: -------------------------------------------------------------------------------- 1 | export class App { 2 | public message = 'from Aurelia!'; 3 | 4 | clicked(): void { 5 | // eslint-disable-next-line no-alert 6 | alert('A primary button click or a touch'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/info/info.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/jest-pretest.ts: -------------------------------------------------------------------------------- 1 | import 'aurelia-polyfills'; 2 | import {Options} from 'aurelia-loader-nodejs'; 3 | import {globalize} from 'aurelia-pal-nodejs'; 4 | import * as path from 'path'; 5 | Options.relativeToDir = path.join(__dirname, 'unit'); 6 | globalize(); 7 | -------------------------------------------------------------------------------- /src/components/search/search.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aurelia Datatable 2 | A highly customizable html datatable, build for the Aurelia Framework. 3 | 4 | If you have any questions, remarks or anything else, feel free to make a issue on this repo. Let me know what you think of the plugin, I'd love to hear your feedback! 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "AureliaEffect.aurelia", 4 | "msjsdiag.debugger-for-chrome", 5 | "steoates.autoimport", 6 | "EditorConfig.EditorConfig", 7 | "christian-kohler.path-intellisense", 8 | "behzad88.Aurelia" 9 | ] 10 | } -------------------------------------------------------------------------------- /aurelia_project/tasks/jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest", 3 | "description": "Runs Jest and reports the results.", 4 | "flags": [ 5 | { 6 | "name": "watch", 7 | "description": "Watches test files for changes and re-runs the tests automatically.", 8 | "type": "boolean" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /aurelia_project/tasks/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "description": "Runs Jest and reports the results.", 4 | "flags": [ 5 | { 6 | "name": "watch", 7 | "description": "Watches test files for changes and re-runs the tests automatically.", 8 | "type": "boolean" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | # 2 space indentation 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /aurelia_project/tasks/lint.ts: -------------------------------------------------------------------------------- 1 | import * as gulp from 'gulp'; 2 | import * as eslint from 'gulp-eslint'; 3 | import * as project from '../aurelia.json'; 4 | 5 | export default function lint() { 6 | return gulp.src(project.transpiler.source) 7 | .pipe(eslint()) 8 | .pipe(eslint.format()) 9 | .pipe(eslint.failAfterError()); 10 | } 11 | -------------------------------------------------------------------------------- /src/components/pagesize/pagesize.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Aurelia 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/models/response.ts: -------------------------------------------------------------------------------- 1 | export default interface IAuDatatableResponse { 2 | data: any[]; 3 | totalRecords: number; 4 | } 5 | 6 | export class AuDatatableResponse { 7 | public data: any[]; 8 | public totalRecords: number; 9 | 10 | constructor(data: IAuDatatableResponse) { 11 | this.data = data?.data; 12 | this.totalRecords = data?.totalRecords; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /dev-app/app.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /aurelia_project/tasks/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build", 3 | "description": "Builds and processes all application assets.", 4 | "flags": [ 5 | { 6 | "name": "env", 7 | "description": "Sets the build environment.", 8 | "type": "string" 9 | }, 10 | { 11 | "name": "watch", 12 | "description": "Watches source files for changes and refreshes the bundles automatically.", 13 | "type": "boolean" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /aurelia_project/tasks/process-json.ts: -------------------------------------------------------------------------------- 1 | import * as gulp from 'gulp'; 2 | import * as project from '../aurelia.json'; 3 | import {build} from 'aurelia-cli'; 4 | 5 | export default function processJson() { 6 | return gulp.src(project.jsonProcessor.source, {since: gulp.lastRun(processJson)}) 7 | .pipe(build.bundle()); 8 | } 9 | 10 | export function pluginJson(dest) { 11 | return function processPluginJson() { 12 | return gulp.src(project.plugin.source.json) 13 | .pipe(gulp.dest(dest)); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /dev-app/main.ts: -------------------------------------------------------------------------------- 1 | import {Aurelia} from 'aurelia-framework'; 2 | import environment from './environment'; 3 | 4 | export function configure(aurelia: Aurelia): void { 5 | aurelia.use 6 | .standardConfiguration() 7 | // load the plugin ../src 8 | // The "resources" is mapped to "../src" in aurelia.json "paths" 9 | .feature('resources'); 10 | 11 | aurelia.use.developmentLogging(environment.debug ? 'debug' : 'warn'); 12 | 13 | if (environment.testing) { 14 | aurelia.use.plugin('aurelia-testing'); 15 | } 16 | 17 | aurelia.start().then(() => aurelia.setRoot()); 18 | } 19 | -------------------------------------------------------------------------------- /aurelia_project/tasks/test.ts: -------------------------------------------------------------------------------- 1 | import { runCLI } from '@jest/core'; 2 | import * as path from 'path'; 3 | import * as packageJson from '../../package.json'; 4 | 5 | import { CLIOptions } from 'aurelia-cli'; 6 | 7 | export default (cb) => { 8 | let options = packageJson.jest; 9 | 10 | if (CLIOptions.hasFlag('watch')) { 11 | Object.assign(options, { watchAll: true}); 12 | } 13 | 14 | runCLI(options, [path.resolve(__dirname, '../../')]).then(({ results }) => { 15 | if (results.numFailedTests || results.numFailedTestSuites) { 16 | cb('Tests Failed'); 17 | } else { 18 | cb(); 19 | } 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/models/filter.ts: -------------------------------------------------------------------------------- 1 | export interface IAuDatatableFilter { 2 | description?: string; 3 | value?: string | undefined; 4 | selectedColumn?: number; 5 | applyToColumns?: number[]; 6 | } 7 | 8 | export class AuDatatableFilter { 9 | public description: string; 10 | public value: string | undefined; 11 | public selectedColumn: number; 12 | public applyToColumns: number[]; 13 | 14 | constructor(data: IAuDatatableFilter) { 15 | this.description = data?.description; 16 | this.value = data?.value; 17 | this.selectedColumn = data?.selectedColumn; 18 | this.applyToColumns = data?.applyToColumns; 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /test/unit/value-converters/upcase.spec.ts: -------------------------------------------------------------------------------- 1 | import {UpcaseValueConverter} from '../../../src/value-converters/upcase'; 2 | 3 | describe('upcase value converter', () => { 4 | let upcase = new UpcaseValueConverter(); 5 | 6 | it('ignores non string input', () => { 7 | expect(upcase.toView(2)).toBe(''); 8 | expect(upcase.toView(null)).toBe(''); 9 | expect(upcase.toView(undefined)).toBe(''); 10 | expect(upcase.toView({})).toBe(''); 11 | expect(upcase.toView(true)).toBe(''); 12 | }); 13 | 14 | it('convert string to upper case', () => { 15 | expect(upcase.toView('aBc')).toBe('ABC'); 16 | expect(upcase.toView(' x y z ')).toBe(' X Y Z '); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": true, 5 | "typeRoots": [ 6 | "./node_modules/@types" 7 | ], 8 | "removeComments": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "target": "es5", 13 | "lib": [ 14 | "es2015", 15 | "dom" 16 | ], 17 | "moduleResolution": "node", 18 | "baseUrl": "src", 19 | "resolveJsonModule": true, 20 | "paths": { 21 | "resources": [ 22 | "" 23 | ] 24 | }, 25 | "allowJs": false 26 | }, 27 | "include": [ 28 | "src", 29 | "dev-app", 30 | "test" 31 | ], 32 | "atom": { 33 | "rewriteTsconfig": false 34 | } 35 | } -------------------------------------------------------------------------------- /aurelia_project/tasks/run.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run", 3 | "description": "Builds the application and serves up the assets via a local web server, watching files for changes as you work.", 4 | "flags": [ 5 | { 6 | "name": "env", 7 | "description": "Sets the build environment.", 8 | "type": "string" 9 | }, 10 | { 11 | "name": "open", 12 | "description": "Open the default browser at the application location.", 13 | "type": "boolean" 14 | }, 15 | { 16 | "name": "host", 17 | "description": "Set host address of the dev server, the accessible URL", 18 | "type": "string" 19 | }, 20 | { 21 | "name": "port", 22 | "description": "Set port number of the dev server", 23 | "type": "string" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /test/unit/attributes/color.spec.ts: -------------------------------------------------------------------------------- 1 | import {StageComponent} from 'aurelia-testing'; 2 | import {bootstrap} from 'aurelia-bootstrapper'; 3 | 4 | describe('color attribute', () => { 5 | let component; 6 | 7 | afterEach(() => { 8 | if (component) { 9 | component.dispose(); 10 | component = null; 11 | } 12 | }); 13 | 14 | it('sets font color', done => { 15 | let model = {color: 'green'}; 16 | 17 | component = StageComponent 18 | .withResources('attributes/color') 19 | .inView('

') 20 | .boundTo(model); 21 | 22 | component.create(bootstrap).then(() => { 23 | const view = component.element; 24 | expect(view.style.color).toBe('green'); 25 | done(); 26 | }).catch(e => { 27 | fail(e); 28 | done(); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Chrome Debugger", 6 | "type": "chrome", 7 | "request": "launch", 8 | "url": "http://localhost:9000", 9 | "webRoot": "${workspaceRoot}/src", 10 | "userDataDir": "${workspaceRoot}/.chrome", 11 | "sourceMapPathOverrides": { 12 | "../src/*": "${webRoot}/*" 13 | } 14 | }, 15 | { 16 | "type": "chrome", 17 | "request": "attach", 18 | "name": "Attach Karma Chrome", 19 | "address": "localhost", 20 | "port": 9333, 21 | "sourceMaps": true, 22 | "pathMapping": { 23 | "/": "${workspaceRoot}", 24 | "/base/": "${workspaceRoot}/" 25 | }, 26 | "sourceMapPathOverrides": { 27 | "../src/*": "${webRoot}/*" 28 | } 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /test/unit/elements/hello-world.spec.ts: -------------------------------------------------------------------------------- 1 | import {StageComponent} from 'aurelia-testing'; 2 | import {bootstrap} from 'aurelia-bootstrapper'; 3 | 4 | describe('hello-world element', () => { 5 | let component; 6 | 7 | afterEach(() => { 8 | if (component) { 9 | component.dispose(); 10 | component = null; 11 | } 12 | }); 13 | 14 | it('says hello world with message', done => { 15 | let model = {message: 'from me'}; 16 | 17 | component = StageComponent 18 | .withResources('elements/hello-world') 19 | .inView('') 20 | .boundTo(model); 21 | 22 | component.create(bootstrap).then(() => { 23 | const view = component.element; 24 | expect(view.textContent.trim()).toBe('Hello world from me'); 25 | done(); 26 | }).catch(e => { 27 | fail(e); 28 | done(); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "parserOptions": { 13 | "ecmaVersion": 2019, 14 | "sourceType": "module", 15 | "project": "./tsconfig.json", 16 | "tsconfigRootDir": "." 17 | }, 18 | "rules": { 19 | "no-unused-vars": 0, 20 | "@typescript-eslint/no-unused-vars": 0, 21 | "no-prototype-builtins": 0, 22 | "no-console": 0, 23 | "getter-return": 0, 24 | "@typescript-eslint/no-explicit-any": 0, 25 | "@typescript-eslint/no-inferrable-types": 0, 26 | "@typescript-eslint/explicit-module-boundary-types": 0 27 | }, 28 | "env": { 29 | "es6": true, 30 | "browser": true, 31 | "node": true, 32 | "jest": true 33 | } 34 | } -------------------------------------------------------------------------------- /src/components/filter/filter.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aurelia_project/generators/value-converter.ts: -------------------------------------------------------------------------------- 1 | import {inject} from 'aurelia-dependency-injection'; 2 | import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli'; 3 | 4 | @inject(Project, CLIOptions, UI) 5 | export default class ValueConverterGenerator { 6 | constructor(private project: Project, private options: CLIOptions, private ui: UI) { } 7 | 8 | async execute() { 9 | const name = await this.ui.ensureAnswer( 10 | this.options.args[0], 11 | 'What would you like to call the value converter?' 12 | ); 13 | 14 | let fileName = this.project.makeFileName(name); 15 | let className = this.project.makeClassName(name); 16 | 17 | this.project.valueConverters.add( 18 | ProjectItem.text(`${fileName}.ts`, this.generateSource(className)) 19 | ); 20 | 21 | await this.project.commitChanges(); 22 | await this.ui.log(`Created ${fileName}.`); 23 | } 24 | 25 | generateSource(className) { 26 | return `export class ${className}ValueConverter { 27 | toView(value) { 28 | // 29 | } 30 | 31 | fromView(value) { 32 | // 33 | } 34 | } 35 | `; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /aurelia_project/generators/binding-behavior.ts: -------------------------------------------------------------------------------- 1 | import {inject} from 'aurelia-dependency-injection'; 2 | import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli'; 3 | 4 | @inject(Project, CLIOptions, UI) 5 | export default class BindingBehaviorGenerator { 6 | constructor(private project: Project, private options: CLIOptions, private ui: UI) { } 7 | 8 | async execute() { 9 | const name = await this.ui.ensureAnswer( 10 | this.options.args[0], 11 | 'What would you like to call the binding behavior?' 12 | ); 13 | 14 | let fileName = this.project.makeFileName(name); 15 | let className = this.project.makeClassName(name); 16 | 17 | this.project.bindingBehaviors.add( 18 | ProjectItem.text(`${fileName}.ts`, this.generateSource(className)) 19 | ); 20 | 21 | await this.project.commitChanges(); 22 | await this.ui.log(`Created ${fileName}.`); 23 | } 24 | 25 | generateSource(className) { 26 | return `export class ${className}BindingBehavior { 27 | bind(binding, source) { 28 | // 29 | } 30 | 31 | unbind(binding, source) { 32 | // 33 | } 34 | } 35 | ` 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # You may want to customise this file depending on your Operating System 3 | # and the editor that you use. 4 | # 5 | # We recommend that you use a Global Gitignore for files that are not related 6 | # to the project. (https://help.github.com/articles/ignoring-files/#create-a-global-gitignore) 7 | 8 | # OS 9 | # 10 | # Ref: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 11 | # Ref: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 12 | # Ref: https://github.com/github/gitignore/blob/master/Global/Linux.gitignore 13 | .DS_STORE 14 | Thumbs.db 15 | 16 | # Editors 17 | # 18 | # Ref: https://github.com/github/gitignore/blob/master/Global 19 | # Ref: https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore 20 | # Ref: https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore 21 | .idea 22 | .chrome 23 | /*.log 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # Dependencies 31 | node_modules 32 | 33 | # Compiled files 34 | /scripts 35 | /dev-app/environment.ts 36 | /dist 37 | /test/coverage-jest 38 | -------------------------------------------------------------------------------- /aurelia_project/generators/task.ts: -------------------------------------------------------------------------------- 1 | import {inject} from 'aurelia-dependency-injection'; 2 | import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli'; 3 | 4 | @inject(Project, CLIOptions, UI) 5 | export default class TaskGenerator { 6 | constructor(private project: Project, private options: CLIOptions, private ui: UI) { } 7 | 8 | async execute() { 9 | const name = await this.ui.ensureAnswer( 10 | this.options.args[0], 11 | 'What would you like to call the task?' 12 | ); 13 | 14 | let fileName = this.project.makeFileName(name); 15 | let functionName = this.project.makeFunctionName(name); 16 | 17 | this.project.tasks.add( 18 | ProjectItem.text(`${fileName}.ts`, this.generateSource(functionName)) 19 | ); 20 | 21 | await this.project.commitChanges(); 22 | await this.ui.log(`Created ${fileName}.`); 23 | } 24 | 25 | generateSource(functionName) { 26 | return `import * as gulp from 'gulp'; 27 | import * as project from '../aurelia.json'; 28 | 29 | export default function ${functionName}() { 30 | return gulp.src(project.paths.???) 31 | .pipe(gulp.dest(project.paths.output)); 32 | } 33 | `; 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /aurelia_project/generators/attribute.ts: -------------------------------------------------------------------------------- 1 | import {inject} from 'aurelia-dependency-injection'; 2 | import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli'; 3 | 4 | @inject(Project, CLIOptions, UI) 5 | export default class AttributeGenerator { 6 | constructor(private project: Project, private options: CLIOptions, private ui: UI) { } 7 | 8 | async execute() { 9 | const name = await this.ui.ensureAnswer( 10 | this.options.args[0], 11 | 'What would you like to call the custom attribute?' 12 | ); 13 | 14 | let fileName = this.project.makeFileName(name); 15 | let className = this.project.makeClassName(name); 16 | 17 | this.project.attributes.add( 18 | ProjectItem.text(`${fileName}.ts`, this.generateSource(className)) 19 | ); 20 | 21 | await this.project.commitChanges(); 22 | await this.ui.log(`Created ${fileName}.`); 23 | } 24 | 25 | generateSource(className) { 26 | return `import {autoinject} from 'aurelia-framework'; 27 | 28 | @autoinject() 29 | export class ${className}CustomAttribute { 30 | constructor(private element: Element) { } 31 | 32 | valueChanged(newValue, oldValue) { 33 | // 34 | } 35 | } 36 | `; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/models/request.ts: -------------------------------------------------------------------------------- 1 | import { AuDatatableFilter } from './filter'; 2 | 3 | export interface IAuDatatableRequest { 4 | searchQuery?: string; 5 | skip?: number; 6 | pageSize?: number; 7 | totalRecords?: number; 8 | data?: any[]; 9 | currentPage?: number; 10 | sortDirection?: string | undefined; 11 | sortBy?: string; 12 | filters?: AuDatatableFilter[]; 13 | } 14 | 15 | export class AuDatatableRequest { 16 | public searchQuery?: string; 17 | public skip?: number; 18 | public pageSize?: number; 19 | public totalRecords?: number; 20 | public data?: any[]; 21 | public currentPage?: number; 22 | public sortDirection?: string | undefined; 23 | public sortBy?: string; 24 | public filters?: AuDatatableFilter[]; 25 | 26 | constructor(data: IAuDatatableRequest) { 27 | this.searchQuery = data?.searchQuery; 28 | this.skip = data?.skip; 29 | this.pageSize = data?.pageSize; 30 | this.totalRecords = data?.totalRecords; 31 | this.data = data?.data; 32 | this.currentPage = data?.currentPage; 33 | this.sortDirection = data?.sortDirection; 34 | this.sortBy = data?.sortBy; 35 | this.filters = data?.filters; 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /aurelia_project/tasks/build.ts: -------------------------------------------------------------------------------- 1 | import * as gulp from 'gulp'; 2 | import * as del from 'del'; 3 | import * as project from '../aurelia.json'; 4 | import {CLIOptions, build as buildCLI} from 'aurelia-cli'; 5 | import transpile from './transpile'; 6 | import processMarkup from './process-markup'; 7 | import processJson from './process-json'; 8 | import processCSS from './process-css'; 9 | import copyFiles from './copy-files'; 10 | import watch from './watch'; 11 | 12 | function clean() { 13 | return del(project.platform.output); 14 | } 15 | 16 | let build = gulp.series( 17 | readProjectConfiguration, 18 | gulp.parallel( 19 | transpile, 20 | processMarkup, 21 | processJson, 22 | processCSS, 23 | copyFiles 24 | ), 25 | writeBundles 26 | ); 27 | 28 | let main; 29 | 30 | if (CLIOptions.taskName() === 'build' && CLIOptions.hasFlag('watch')) { 31 | main = gulp.series( 32 | clean, 33 | build, 34 | (done) => { watch(); done(); } 35 | ); 36 | } else { 37 | main = gulp.series( 38 | clean, 39 | build 40 | ); 41 | } 42 | 43 | function readProjectConfiguration() { 44 | return buildCLI.src(project); 45 | } 46 | 47 | function writeBundles() { 48 | return buildCLI.dest(); 49 | } 50 | 51 | export { main as default }; 52 | -------------------------------------------------------------------------------- /aurelia_project/tasks/run.ts: -------------------------------------------------------------------------------- 1 | import * as gulp from 'gulp'; 2 | import * as project from '../aurelia.json'; 3 | import * as devServer from './dev-server'; 4 | import {CLIOptions} from 'aurelia-cli'; 5 | import build from './build'; 6 | import watch from './watch'; 7 | 8 | if (!CLIOptions.hasFlag('watch')) { 9 | // "au run" always runs in watch mode 10 | CLIOptions.instance.args.push('--watch'); 11 | } 12 | 13 | let serve = gulp.series( 14 | build, 15 | function startDevServer(done) { 16 | devServer.run({ 17 | open: CLIOptions.hasFlag('open') || project.platform.open, 18 | port: CLIOptions.getFlagValue('port') || project.platform.port, 19 | host: CLIOptions.getFlagValue('host') || project.platform.host || "localhost", 20 | baseDir: project.platform.baseDir 21 | }); 22 | done(); 23 | } 24 | ); 25 | 26 | function log(message) { 27 | console.log(message); //eslint-disable-line no-console 28 | } 29 | 30 | function reload() { 31 | log('Refreshing the browser'); 32 | devServer.reload(); 33 | } 34 | 35 | const run = gulp.series( 36 | serve, 37 | done => { watch(reload); done(); } 38 | ); 39 | 40 | const shutdownDevServer = () => { 41 | devServer.destroy(); 42 | }; 43 | 44 | export { run as default, serve , shutdownDevServer }; 45 | -------------------------------------------------------------------------------- /aurelia_project/tasks/copy-files.ts: -------------------------------------------------------------------------------- 1 | import * as gulp from 'gulp'; 2 | import * as path from 'path'; 3 | import * as minimatch from 'minimatch'; 4 | import * as project from '../aurelia.json'; 5 | 6 | export default function copyFiles(done) { 7 | if (typeof project.build.copyFiles !== 'object') { 8 | done(); 9 | return; 10 | } 11 | 12 | const instruction = getNormalizedInstruction(); 13 | const files = Object.keys(instruction); 14 | 15 | return gulp.src(files, {since: gulp.lastRun(copyFiles)}) 16 | .pipe(gulp.dest(x => { 17 | const filePath = prepareFilePath(x.path); 18 | const key = files.find(f => minimatch(filePath, f)); 19 | return instruction[key]; 20 | })); 21 | } 22 | 23 | function getNormalizedInstruction() { 24 | const files = project.build.copyFiles; 25 | let normalizedInstruction = {}; 26 | 27 | for (let key in files) { 28 | normalizedInstruction[path.posix.normalize(key)] = files[key]; 29 | } 30 | 31 | return normalizedInstruction; 32 | } 33 | 34 | function prepareFilePath(filePath) { 35 | let preparedPath = filePath.replace(process.cwd(), '').slice(1); 36 | 37 | //if we are running on windows we have to fix the path 38 | if (/^win/.test(process.platform)) { 39 | preparedPath = preparedPath.replace(/\\/g, '/'); 40 | } 41 | 42 | return preparedPath; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/search/search.ts: -------------------------------------------------------------------------------- 1 | import { bindable, bindingMode, customElement } from 'aurelia-framework'; 2 | import { AuDatatableRequest } from '../../models/request'; 3 | import { AuDatatableResponse } from '../../models/response'; 4 | 5 | @customElement('au-datatable-search') 6 | export class AuDatatableSearchComponent { 7 | 8 | @bindable({ 9 | defaultBindingMode: bindingMode.twoWay, 10 | }) public request: AuDatatableRequest; 11 | 12 | @bindable() public placeholder: string; 13 | @bindable() public inputClass: string; 14 | @bindable() public debounce: string = '500'; 15 | @bindable() public onSearchChange: (request: AuDatatableRequest) => Promise; 16 | 17 | public async search(): Promise { 18 | if (typeof this.onSearchChange !== 'function') { 19 | throw new Error('[au-table-search:search] No onSearchChange() callback has been set'); 20 | } 21 | this.reset(); 22 | const response = await this.onSearchChange(this.request); 23 | this.request.data = response.data; 24 | this.request.totalRecords = response.totalRecords; 25 | this.reset(); 26 | } 27 | 28 | private reset(): void { 29 | this.request.currentPage = this.request.totalRecords > 0 ? 1 : 0; 30 | this.request.skip = 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /aurelia_project/generators/element.ts: -------------------------------------------------------------------------------- 1 | import {inject} from 'aurelia-dependency-injection'; 2 | import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli'; 3 | 4 | @inject(Project, CLIOptions, UI) 5 | export default class ElementGenerator { 6 | constructor(private project: Project, private options: CLIOptions, private ui: UI) { } 7 | 8 | async execute() { 9 | const name = await this.ui.ensureAnswer( 10 | this.options.args[0], 11 | 'What would you like to call the custom element?' 12 | ); 13 | 14 | let fileName = this.project.makeFileName(name); 15 | let className = this.project.makeClassName(name); 16 | 17 | this.project.elements.add( 18 | ProjectItem.text(`${fileName}.ts`, this.generateJSSource(className)), 19 | ProjectItem.text(`${fileName}.html`, this.generateHTMLSource(className)) 20 | ); 21 | 22 | await this.project.commitChanges(); 23 | await this.ui.log(`Created ${fileName}.`); 24 | } 25 | 26 | generateJSSource(className) { 27 | return `import {bindable} from 'aurelia-framework'; 28 | 29 | export class ${className} { 30 | @bindable value; 31 | 32 | valueChanged(newValue, oldValue) { 33 | // 34 | } 35 | } 36 | `; 37 | } 38 | 39 | generateHTMLSource(className) { 40 | return ` 43 | `; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /aurelia_project/tasks/build-plugin.ts: -------------------------------------------------------------------------------- 1 | import * as gulp from 'gulp'; 2 | import * as del from 'del'; 3 | import { pluginMarkup } from './process-markup'; 4 | import { pluginCSS } from './process-css'; 5 | import { pluginJson } from './process-json'; 6 | import { buildPluginJavaScript } from './transpile'; 7 | import { CLIOptions } from 'aurelia-cli'; 8 | 9 | function clean() { 10 | return del('dist'); 11 | } 12 | 13 | let build = gulp.series( 14 | gulp.parallel( 15 | // package.json "module" field pointing to dist/native-modules/index.js 16 | pluginMarkup('dist/native-modules'), 17 | pluginCSS('dist/native-modules'), 18 | pluginJson('dist/native-modules'), 19 | buildPluginJavaScript('dist/native-modules', 'es2015'), 20 | 21 | // package.json "main" field pointing to dist/native-modules/index.js 22 | pluginMarkup('dist/commonjs'), 23 | pluginCSS('dist/commonjs'), 24 | pluginJson('dist/commonjs'), 25 | buildPluginJavaScript('dist/commonjs', 'commonjs'), 26 | ), (done) => { 27 | console.log('Finish building Aurelia plugin to dist/commonjs and dist/native-modules.'); 28 | done(); 29 | } 30 | ); 31 | 32 | let main; 33 | if (CLIOptions.hasFlag('watch')) { 34 | main = gulp.series( 35 | clean, 36 | () => { 37 | console.log('Watching plugin sources for changes ...'); 38 | return gulp.watch('src/**/*', { ignoreInitial: false }, build); 39 | } 40 | ); 41 | } else { 42 | main = gulp.series( 43 | clean, 44 | build 45 | ); 46 | } 47 | 48 | export { main as default }; 49 | -------------------------------------------------------------------------------- /test/unit/binding-behaviors/primary-click.spec.ts: -------------------------------------------------------------------------------- 1 | import {StageComponent} from 'aurelia-testing'; 2 | import {bootstrap} from 'aurelia-bootstrapper'; 3 | 4 | function fireEvent(el, type, options) { 5 | var o = options || {}; 6 | var e = document.createEvent('Event'); 7 | e.initEvent(type, true, true); 8 | Object.keys(o).forEach(apply); 9 | el.dispatchEvent(e); 10 | function apply(key) { 11 | e[key] = o[key]; 12 | } 13 | } 14 | 15 | function delay() { 16 | return new Promise(resolve => { 17 | setTimeout(resolve, 20); 18 | }); 19 | } 20 | 21 | describe('primaryClick binding behavior', () => { 22 | let component; 23 | 24 | afterEach(() => { 25 | if (component) { 26 | component.dispose(); 27 | component = null; 28 | } 29 | }); 30 | 31 | it('sets font color', done => { 32 | let hitted = false; 33 | function hit() { hitted = true; } 34 | 35 | let model = {hit}; 36 | 37 | component = StageComponent 38 | .withResources('binding-behaviors/primary-click') 39 | .inView('') 40 | .boundTo(model); 41 | 42 | let view; 43 | component.create(bootstrap).then(() => { 44 | view = component.element; 45 | fireEvent(view, 'click', {button: 0}); 46 | }).then(delay).then(() => { 47 | expect(hitted).toBe(true); 48 | hitted = false; 49 | fireEvent(view, 'click', {button: 1}); 50 | }).then(delay).then(() => { 51 | expect(hitted).toBe(false); 52 | done(); 53 | }).catch(e => { 54 | fail(e); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /aurelia_project/tasks/process-css.ts: -------------------------------------------------------------------------------- 1 | import {CLIOptions, build} from 'aurelia-cli'; 2 | import * as gulp from 'gulp'; 3 | import * as project from '../aurelia.json'; 4 | import * as sass from 'gulp-dart-sass'; 5 | import * as sassPackageImporter from 'node-sass-package-importer'; 6 | import * as postcss from 'gulp-postcss'; 7 | import * as autoprefixer from 'autoprefixer'; 8 | import * as cssnano from 'cssnano'; 9 | import * as postcssUrl from 'postcss-url'; 10 | 11 | export default function processCSS() { 12 | return gulp.src(project.cssProcessor.source, {sourcemaps: true}) 13 | .pipe( 14 | // sassPackageImporter handles @import "~bootstrap" 15 | // https://github.com/maoberlehner/node-sass-magic-importer/tree/master/packages/node-sass-package-importer 16 | CLIOptions.hasFlag('watch') ? 17 | sass.sync({importer: sassPackageImporter()}).on('error', sass.logError) : 18 | sass.sync({importer: sassPackageImporter()}) 19 | ) 20 | .pipe(postcss([ 21 | autoprefixer(), 22 | // Inline images and fonts 23 | postcssUrl({url: 'inline', encodeType: 'base64'}), 24 | cssnano() 25 | ])) 26 | .pipe(build.bundle()); 27 | } 28 | 29 | export function pluginCSS(dest) { 30 | return function processPluginCSS() { 31 | return gulp.src(project.plugin.source.css) 32 | .pipe(CLIOptions.hasFlag('watch') ? sass.sync().on('error', sass.logError) : sass.sync()) 33 | .pipe(postcss([ 34 | autoprefixer(), 35 | // Inline images and fonts 36 | postcssUrl({url: 'inline', encodeType: 'base64'}), 37 | cssnano() 38 | ])) 39 | .pipe(gulp.dest(dest)); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/components/filter/_filter.scss: -------------------------------------------------------------------------------- 1 | .au-table-filter { 2 | .au-filter-input { 3 | width: calc(100% - 35px); 4 | transition: 1s; 5 | outline: none; 6 | height: 30px; 7 | border: 1px #ddd solid; 8 | } 9 | 10 | .au-filter-btn { 11 | width: 30px; 12 | margin-left: 5px; 13 | height: 30px; 14 | float: right; 15 | outline: none; 16 | padding: 0; 17 | background-color: #333; 18 | color: white; 19 | opacity: 0.8; 20 | border-radius: 0; 21 | } 22 | 23 | .au-filter-container { 24 | width: 100%; 25 | display: none; 26 | 27 | .au-filters { 28 | list-style: none; 29 | padding: 10px; 30 | border: 1px solid #ddd; 31 | margin-top: 5px; 32 | position: relative; 33 | background-color: white; 34 | top: 0; 35 | bottom: 0; 36 | font-size: 8pt; 37 | right: 0; 38 | margin-bottom: 0; 39 | 40 | .au-clear { 41 | margin-top: 20px; 42 | font-size: 8pt; 43 | padding: 2px 5px; 44 | background: #333; 45 | color: white; 46 | 47 | &:hover { 48 | cursor: pointer; 49 | } 50 | 51 | .au-clear-icon { 52 | color: white; 53 | content: '\2716'; 54 | float: right; 55 | margin-top: 2px; 56 | } 57 | } 58 | 59 | .au-filter { 60 | border-bottom: 1px solid #ddd; 61 | padding: 2px 5px; 62 | 63 | &:active { 64 | background-color: #b9b8b8; 65 | color: white; 66 | } 67 | 68 | &:hover { 69 | cursor: pointer; 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/attributes/datatable.ts: -------------------------------------------------------------------------------- 1 | import { bindable, bindingMode, customAttribute } from 'aurelia-framework'; 2 | import { AuDatatableRequest } from '../models/request'; 3 | import { AuDatatableResponse } from '../models/response'; 4 | 5 | @customAttribute('au-datatable') 6 | export class AuDatatableAttribute { 7 | 8 | @bindable({ 9 | changeHandler: 'init' 10 | }) 11 | public onInit: (request: AuDatatableRequest) => Promise; 12 | 13 | @bindable({ 14 | defaultBindingMode: bindingMode.twoWay, 15 | changeHandler: 'init' 16 | }) 17 | public request: AuDatatableRequest; 18 | 19 | public async refresh(request?: AuDatatableRequest): Promise { 20 | if (request) this.request = request; 21 | else { 22 | this.request.data = []; 23 | this.request.currentPage = 1; 24 | this.request.skip = 0; 25 | this.request.searchQuery = null; 26 | this.request.filters = []; 27 | } 28 | 29 | const response = await this.onInit(this.request); 30 | this.request.data = response.data; 31 | this.request.totalRecords = response.totalRecords; 32 | } 33 | 34 | private async init(): Promise { 35 | if (!this.request || !this.onInit) return; 36 | if (!this.request.pageSize) this.request.pageSize = 10; 37 | 38 | this.request.skip = 0; 39 | const response = await this.onInit(this.request); 40 | this.request.data = response.data; 41 | this.request.totalRecords = response.totalRecords; 42 | if (!this.request.currentPage) { 43 | this.request.currentPage = 1; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { FrameworkConfiguration } from 'aurelia-framework'; 2 | import { PLATFORM } from 'aurelia-pal'; 3 | import { AuDatatableAttribute } from './attributes/datatable'; 4 | import { AuDatatableSortAttribute } from './attributes/sort'; 5 | import { AuDatatableFilterComponent } from './components/filter/filter'; 6 | import { AuDatatableInfoComponent } from './components/info/info'; 7 | import { AuDatatablePagesizeComponent } from './components/pagesize/pagesize'; 8 | import { AuDatatablePaginationComponent } from './components/pagination/pagination'; 9 | import { AuDatatableSearchComponent } from './components/search/search'; 10 | import { AuDatatableFilter } from './models/filter'; 11 | import { AuDatatableRequest } from './models/request'; 12 | import { AuDatatableResponse } from './models/response'; 13 | 14 | export { 15 | AuDatatableAttribute, 16 | AuDatatablePaginationComponent, 17 | AuDatatableInfoComponent, 18 | AuDatatablePagesizeComponent, 19 | AuDatatableSearchComponent, 20 | AuDatatableSortAttribute, 21 | AuDatatableFilterComponent, 22 | AuDatatableResponse, 23 | AuDatatableRequest, 24 | AuDatatableFilter 25 | }; 26 | 27 | export function configure(config: FrameworkConfiguration): void { 28 | config.globalResources([ 29 | PLATFORM.moduleName('./attributes/datatable'), 30 | PLATFORM.moduleName('./attributes/sort'), 31 | PLATFORM.moduleName('./components/pagination/pagination'), 32 | PLATFORM.moduleName('./components/search/search'), 33 | PLATFORM.moduleName('./components/pagesize/pagesize'), 34 | PLATFORM.moduleName('./components/filter/filter'), 35 | PLATFORM.moduleName('./components/info/info') 36 | ]); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/pagesize/pagesize.ts: -------------------------------------------------------------------------------- 1 | import { bindable, customElement } from 'aurelia-framework'; 2 | import { AuDatatableRequest } from '../../models/request'; 3 | import { AuDatatableResponse } from '../../models/response'; 4 | 5 | @customElement('au-datatable-pagesize') 6 | export class AuDatatablePagesizeComponent { 7 | 8 | @bindable() public pageSizes: number[]; 9 | @bindable() public classes: string; 10 | @bindable() public onPageSizeChange: (request: AuDatatableRequest) => Promise; 11 | @bindable() public request: AuDatatableRequest; 12 | 13 | private selectedPageSize: number; 14 | 15 | public bind(): void { 16 | if (!this.pageSizes || this.pageSizes.length === 0) { 17 | throw new Error('[au-table-pagesize:bind] No page sizes has been bound.'); 18 | } 19 | if (!this.request.pageSize) { 20 | this.request.pageSize = this.pageSizes[0]; 21 | } 22 | } 23 | 24 | public setSelected = (option: number): boolean => { 25 | return option === this.request.pageSize; 26 | } 27 | 28 | public async pageSizeChange(): Promise { 29 | if (typeof this.onPageSizeChange !== 'function') { 30 | throw new Error('[au-table-pagesize:pageSizeChange] No onPageSizeChange() callback has been set'); 31 | } 32 | this.request.pageSize = this.selectedPageSize; 33 | this.reset(); 34 | const response = await this.onPageSizeChange(this.request); 35 | this.request.totalRecords = response.totalRecords; 36 | this.request.data = response.data; 37 | } 38 | 39 | private reset(): void { 40 | this.request.currentPage = this.request.totalRecords > 0 ? 1 : 0; 41 | this.request.skip = 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /aurelia_project/tasks/process-markup.ts: -------------------------------------------------------------------------------- 1 | import {CLIOptions, build} from 'aurelia-cli'; 2 | import * as gulp from 'gulp'; 3 | import * as project from '../aurelia.json'; 4 | import * as htmlmin from 'gulp-htmlmin'; 5 | import * as gulpIf from 'gulp-if'; 6 | import * as plumber from 'gulp-plumber'; 7 | 8 | export default function processMarkup() { 9 | return gulp.src(project.markupProcessor.source, {sourcemaps: true, since: gulp.lastRun(processMarkup)}) 10 | .pipe(gulpIf(CLIOptions.hasFlag('watch'), plumber())) 11 | .pipe(htmlmin({ 12 | // collapseInlineTagWhitespace: true, 13 | // collapseBooleanAttributes: true, 14 | // removeAttributeQuotes: true, 15 | // removeScriptTypeAttributes: true, 16 | // removeStyleLinkTypeAttributes: true, 17 | removeComments: true, 18 | collapseWhitespace: true, 19 | minifyCSS: true, 20 | minifyJS: true, 21 | ignoreCustomFragments: [/\${.*?}/g] // ignore interpolation expressions 22 | })) 23 | .pipe(build.bundle()); 24 | } 25 | 26 | export function pluginMarkup(dest) { 27 | return function processPluginMarkup() { 28 | return gulp.src(project.plugin.source.html) 29 | .pipe(gulpIf(CLIOptions.hasFlag('watch'), plumber())) 30 | .pipe(htmlmin({ 31 | // collapseInlineTagWhitespace: true, 32 | // collapseBooleanAttributes: true, 33 | // removeAttributeQuotes: true, 34 | // removeScriptTypeAttributes: true, 35 | // removeStyleLinkTypeAttributes: true, 36 | removeComments: true, 37 | collapseWhitespace: true, 38 | minifyCSS: true, 39 | minifyJS: true, 40 | ignoreCustomFragments: [/\${.*?}/g] // ignore interpolation expressions 41 | })) 42 | .pipe(gulp.dest(dest)); 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/components/pagination/pagination.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aurelia_project/generators/generator.ts: -------------------------------------------------------------------------------- 1 | import {inject} from 'aurelia-dependency-injection'; 2 | import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli'; 3 | 4 | @inject(Project, CLIOptions, UI) 5 | export default class GeneratorGenerator { 6 | constructor(private project: Project, private options: CLIOptions, private ui: UI) { } 7 | 8 | async execute() { 9 | const name = await this.ui.ensureAnswer( 10 | this.options.args[0], 11 | 'What would you like to call the generator?' 12 | ); 13 | 14 | let fileName = this.project.makeFileName(name); 15 | let className = this.project.makeClassName(name); 16 | 17 | this.project.generators.add( 18 | ProjectItem.text(`${fileName}.ts`, this.generateSource(className)) 19 | ); 20 | 21 | await this.project.commitChanges() 22 | await this.ui.log(`Created ${fileName}.`); 23 | } 24 | 25 | generateSource(className) { 26 | return `import {inject} from 'aurelia-dependency-injection'; 27 | import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli'; 28 | 29 | @inject(Project, CLIOptions, UI) 30 | export default class ${className}Generator { 31 | constructor(project, options, ui) { 32 | this.project = project; 33 | this.options = options; 34 | this.ui = ui; 35 | } 36 | 37 | execute() { 38 | return this.ui 39 | .ensureAnswer(this.options.args[0], 'What would you like to call the new item?') 40 | .then(name => { 41 | let fileName = this.project.makeFileName(name); 42 | let className = this.project.makeClassName(name); 43 | 44 | this.project.elements.add( 45 | ProjectItem.text(\`\${fileName}.ts\`, this.generateSource(className)) 46 | ); 47 | 48 | return this.project.commitChanges() 49 | .then(() => this.ui.log(\`Created \${fileName}.\`)); 50 | }); 51 | } 52 | 53 | generateSource(className) { 54 | return \`import {bindable} from 'aurelia-framework'; 55 | 56 | export class \${className} { 57 | @bindable value; 58 | 59 | valueChanged(newValue, oldValue) { 60 | // 61 | } 62 | } 63 | \` 64 | } 65 | } 66 | `; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /aurelia_project/tasks/transpile.ts: -------------------------------------------------------------------------------- 1 | import * as gulp from 'gulp'; 2 | import * as gulpIf from 'gulp-if'; 3 | import * as plumber from 'gulp-plumber'; 4 | import * as rename from 'gulp-rename'; 5 | import * as ts from 'gulp-typescript'; 6 | import * as project from '../aurelia.json'; 7 | import * as fs from 'fs'; 8 | import { Transform } from 'stream'; 9 | import { CLIOptions, build, Configuration } from 'aurelia-cli'; 10 | import * as gulpSourcemaps from 'gulp-sourcemaps'; 11 | 12 | function configureEnvironment() { 13 | let env = CLIOptions.getEnvironment(); 14 | 15 | return gulp.src(`aurelia_project/environments/${env}.ts`, { since: gulp.lastRun(configureEnvironment) }) 16 | .pipe(rename('environment.ts')) 17 | .pipe(new Transform({ 18 | objectMode: true, 19 | transform: function (file, _, cb) { 20 | // https://github.com/aurelia/cli/issues/1031 21 | fs.unlink(`${project.paths.root}/${file.relative}`, function () { cb(null, file); }); 22 | } 23 | })) 24 | .pipe(gulp.dest(project.paths.root)); 25 | } 26 | 27 | function buildTypeScript() { 28 | const typescriptCompiler = ts.createProject('tsconfig.json', { 29 | typescript: require('typescript'), 30 | noEmitOnError: true 31 | }); 32 | 33 | return gulp.src(project.transpiler.dtsSource) 34 | .pipe(gulp.src(project.transpiler.source, { 35 | sourcemaps: true, 36 | since: gulp.lastRun(buildTypeScript) 37 | })) 38 | .pipe(gulpIf(CLIOptions.hasFlag('watch'), plumber())) 39 | .pipe(typescriptCompiler()) 40 | .pipe(build.bundle()); 41 | } 42 | 43 | export default gulp.series( 44 | configureEnvironment, 45 | buildTypeScript 46 | ); 47 | 48 | export function buildPluginJavaScript(dest, format) { 49 | // when format is missing, default is ESM as we turned off "modules": false in .babelrc.js 50 | return function processPluginJavaScript() { 51 | const typescriptCompiler = ts.createProject('tsconfig.json', { 52 | typescript: require('typescript'), 53 | module: format 54 | }); 55 | 56 | return gulp.src(project.transpiler.dtsSource) 57 | .pipe(gulp.src(project.plugin.source.js)) 58 | .pipe(gulpIf(CLIOptions.hasFlag('watch'), plumber())) 59 | .pipe(gulpSourcemaps.init()) 60 | .pipe(typescriptCompiler()) 61 | .pipe(gulpSourcemaps.write('.', { includeContent: false, sourceRoot: '../../src/' })) 62 | .pipe(gulp.dest(dest)); 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "au-datatable", 3 | "version": "1.2.3", 4 | "description": "A highly customizable aurelia html datatable plugin", 5 | "keywords": [ 6 | "aurelia", 7 | "table", 8 | "customizable", 9 | "plugin", 10 | "html", 11 | "datatable" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/dtaalbers/au-datatable.git" 16 | }, 17 | "author": "Tom Aalbers ", 18 | "license": "MIT", 19 | "homepage": "https://github.com/dtaalbers/au-datatable", 20 | "bugs": { 21 | "url": "https://github.com/dtaalbers/au-datatable/issues" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^18.11.18", 25 | "@typescript-eslint/eslint-plugin": "^5.47.1", 26 | "@typescript-eslint/parser": "^5.47.1", 27 | "aurelia-animator-css": "^1.0.4", 28 | "aurelia-bootstrapper": "^2.4.0", 29 | "aurelia-cli": "^3.0.1", 30 | "aurelia-testing": "^1.1.0", 31 | "autoprefixer": "^10.4.13", 32 | "connect": "^3.7.0", 33 | "connect-history-api-fallback": "^2.0.0", 34 | "connect-injector": "^0.4.4", 35 | "cssnano": "^5.1.14", 36 | "debounce": "^1.2.1", 37 | "del": "^6.1.0", 38 | "eslint": "^8.30.0", 39 | "gulp": "^4.0.2", 40 | "gulp-dart-sass": "^1.0.2", 41 | "gulp-eslint": "^6.0.0", 42 | "gulp-htmlmin": "^5.0.1", 43 | "gulp-if": "^3.0.0", 44 | "gulp-plumber": "^1.2.1", 45 | "gulp-postcss": "^9.0.1", 46 | "gulp-rename": "^2.0.0", 47 | "gulp-sourcemaps": "^3.0.0", 48 | "gulp-typescript": "^6.0.0-alpha.1", 49 | "gulp-watch": "^5.0.1", 50 | "minimatch": "^5.1.2", 51 | "node-sass-package-importer": "^5.3.2", 52 | "open": "^8.4.0", 53 | "postcss": "^8.4.20", 54 | "postcss-url": "^10.1.3", 55 | "promise-polyfill": "^8.2.3", 56 | "requirejs": "^2.3.6", 57 | "serve-static": "^1.15.0", 58 | "server-destroy": "^1.0.1", 59 | "socket.io": "^4.5.4", 60 | "text": "requirejs/text", 61 | "tslib": "^2.4.1", 62 | "typescript": "^4.9.4" 63 | }, 64 | "browserslist": [ 65 | "defaults" 66 | ], 67 | "scripts": { 68 | "build": "au build-plugin --env prod", 69 | "start": "au run", 70 | "watch": "au build-plugin --watch", 71 | "prepare": "npm run build" 72 | }, 73 | "engines": { 74 | "node": ">=14.15.0" 75 | }, 76 | "overrides": { 77 | "chokidar": "^3.0.0", 78 | "glob-stream": "^7.0.0", 79 | "glob-parent": "^6.0.0", 80 | "micromatch": "^4.0.0" 81 | }, 82 | "main": "dist/commonjs/index.js", 83 | "module": "dist/native-modules/index.js", 84 | "files": [ 85 | "dist" 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /aurelia_project/aurelia.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "au-datatable", 3 | "type": "project:plugin", 4 | "paths": { 5 | "root": "dev-app", 6 | "resources": "../src", 7 | "elements": "../src/elements", 8 | "attributes": "../src/attributes", 9 | "valueConverters": "../src/value-converters", 10 | "bindingBehaviors": "../src/binding-behaviors" 11 | }, 12 | "transpiler": { 13 | "id": "typescript", 14 | "fileExtension": ".ts", 15 | "dtsSource": [ 16 | "./types/**/*.d.ts" 17 | ], 18 | "source": [ 19 | "dev-app/**/*.ts", 20 | "src/**/*.ts" 21 | ] 22 | }, 23 | "plugin": { 24 | "source": { 25 | "js": "src/**/*.ts", 26 | "css": "src/**/*.scss", 27 | "html": "src/**/*.html", 28 | "json": "src/**/*.json" 29 | } 30 | }, 31 | "markupProcessor": { 32 | "source": [ 33 | "dev-app/**/*.html", 34 | "src/**/*.html" 35 | ] 36 | }, 37 | "cssProcessor": { 38 | "source": [ 39 | "dev-app/**/*.scss", 40 | "src/**/*.scss" 41 | ] 42 | }, 43 | "jsonProcessor": { 44 | "source": [ 45 | "dev-app/**/*.json", 46 | "src/**/*.json" 47 | ] 48 | }, 49 | "platform": { 50 | "port": 9000, 51 | "host": "localhost", 52 | "open": false, 53 | "index": "index.html", 54 | "baseDir": ".", 55 | "output": "scripts" 56 | }, 57 | "build": { 58 | "targets": [ 59 | { 60 | "port": 9000, 61 | "index": "index.html", 62 | "baseDir": ".", 63 | "output": "scripts" 64 | } 65 | ], 66 | "options": { 67 | "minify": "stage & prod", 68 | "sourcemaps": "dev & stage", 69 | "rev": "prod", 70 | "cache": "dev & stage" 71 | }, 72 | "bundles": [ 73 | { 74 | "name": "app-bundle.js", 75 | "source": [ 76 | "**/*" 77 | ] 78 | }, 79 | { 80 | "name": "vendor-bundle.js", 81 | "prepend": [ 82 | "node_modules/promise-polyfill/dist/polyfill.min.js", 83 | "node_modules/requirejs/require.js" 84 | ], 85 | "dependencies": [ 86 | "aurelia-bootstrapper", 87 | "aurelia-loader-default", 88 | "aurelia-pal-browser", 89 | { 90 | "name": "aurelia-testing", 91 | "env": "dev" 92 | }, 93 | "text" 94 | ] 95 | } 96 | ], 97 | "loader": { 98 | "type": "require", 99 | "configTarget": "vendor-bundle.js", 100 | "includeBundleMetadataInConfig": "auto", 101 | "plugins": [ 102 | { 103 | "name": "text", 104 | "extensions": [ 105 | ".html", 106 | ".css" 107 | ], 108 | "stub": false 109 | } 110 | ] 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /src/components/info/info.ts: -------------------------------------------------------------------------------- 1 | import { bindable, BindingEngine, customElement, Disposable, inject } from 'aurelia-framework'; 2 | import { AuDatatableRequest } from '../../models/request'; 3 | 4 | @customElement('au-datatable-info') 5 | @inject(BindingEngine) 6 | export class AuDatatableInfoComponent { 7 | 8 | @bindable() public message: string; 9 | @bindable() public labelFiltered: string; 10 | @bindable() public request: AuDatatableRequest; 11 | 12 | public info: string; 13 | private startRecord: number; 14 | private endRecord: number; 15 | private currentPageCopy: any; 16 | private subscriptions: Disposable[] = []; 17 | 18 | constructor( 19 | private bindingEngine: BindingEngine 20 | ) { } 21 | 22 | public attached(): void { 23 | if (!this.message) { 24 | this.message = 'START_RECORD to END_RECORD of total TOTAL_RECORDS records'; 25 | } 26 | if (!this.labelFiltered) { 27 | this.labelFiltered = 'filtered'; 28 | } 29 | this.subscriptions.push(this.bindingEngine 30 | .propertyObserver(this.request, 'data') 31 | .subscribe(() => this.updateRecordInfo())); 32 | this.subscriptions.push(this.bindingEngine 33 | .propertyObserver(this.request, 'pageSize') 34 | .subscribe(() => this.reset())); 35 | } 36 | 37 | public detached(): void { 38 | this.subscriptions.forEach((x) => x.dispose()); 39 | } 40 | 41 | public updateRecordInfo(): void { 42 | if (!this.startRecord && !this.endRecord) { 43 | this.startRecord = (this.request.pageSize * this.request.currentPage) - (this.request.pageSize - 1); 44 | this.endRecord = this.request.pageSize; 45 | } else { 46 | if (this.currentPageCopy + 1 === this.request.currentPage) { 47 | this.nextPage(); 48 | } else if (this.currentPageCopy - 1 === this.request.currentPage) { 49 | this.previousPage(); 50 | } else { 51 | this.pageChanged(); 52 | } 53 | } 54 | this.currentPageCopy = this.request.currentPage; 55 | this.translateInfo(); 56 | } 57 | 58 | private translateInfo(): void { 59 | if (this.request.totalRecords === undefined 60 | || this.request.pageSize === undefined 61 | || this.startRecord === undefined 62 | || this.endRecord === undefined) { 63 | return; 64 | } 65 | this.info = this.message 66 | .replace('START_RECORD', this.request.data.length === 0 67 | ? '0' 68 | : this.startRecord.toString()) 69 | .replace('END_RECORD', this.request.data.length < this.request.pageSize 70 | ? this.request.totalRecords.toString() 71 | : (this.request.data.length * this.request.currentPage).toString()) 72 | .replace('TOTAL_RECORDS', this.request.totalRecords.toString()); 73 | } 74 | 75 | private nextPage(): void { 76 | this.startRecord += this.request.pageSize; 77 | this.endRecord = (this.endRecord + this.request.pageSize) > this.request.totalRecords 78 | ? this.request.totalRecords 79 | : this.endRecord + this.request.pageSize; 80 | } 81 | 82 | private previousPage(): void { 83 | this.startRecord -= this.request.pageSize; 84 | this.endRecord = this.request.pageSize * this.request.currentPage; 85 | } 86 | 87 | private pageChanged(): void { 88 | const page = this.request.currentPage - 1; 89 | this.startRecord = (page * this.request.pageSize) + 1; 90 | const next = (page + 1) * this.request.pageSize; 91 | this.endRecord = next > this.request.totalRecords ? this.request.totalRecords : next; 92 | } 93 | 94 | private reset(): void { 95 | this.request.currentPage = 1; 96 | this.currentPageCopy = 1; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /aurelia_project/tasks/watch.ts: -------------------------------------------------------------------------------- 1 | import * as gulp from 'gulp'; 2 | import * as minimatch from 'minimatch'; 3 | import * as gulpWatch from 'gulp-watch'; 4 | import * as debounce from 'debounce'; 5 | import * as project from '../aurelia.json'; 6 | import transpile from './transpile'; 7 | import processMarkup from './process-markup'; 8 | import processCSS from './process-css'; 9 | import processJson from './process-json'; 10 | import copyFiles from './copy-files'; 11 | import { build } from 'aurelia-cli'; 12 | 13 | const debounceWaitTime = 100; 14 | let isBuilding = false; 15 | let pendingRefreshPaths = []; 16 | let watchCallback = () => { }; 17 | let watches = [ 18 | { name: 'transpile', callback: transpile, source: project.transpiler.source }, 19 | { name: 'markup', callback: processMarkup, source: project.markupProcessor.source }, 20 | { name: 'CSS', callback: processCSS, source: project.cssProcessor.source }, 21 | { name: 'JSON', callback: processJson, source: project.jsonProcessor.source } 22 | ]; 23 | 24 | if (typeof project.build.copyFiles === 'object') { 25 | for (let src of Object.keys(project.build.copyFiles)) { 26 | watches.push({ name: 'file copy', callback: copyFiles, source: src }); 27 | } 28 | } 29 | 30 | let watch = (callback) => { 31 | watchCallback = callback || watchCallback; 32 | 33 | // watch every glob individually 34 | for(let watcher of watches) { 35 | if (Array.isArray(watcher.source)) { 36 | for(let glob of watcher.source) { 37 | watchPath(glob); 38 | } 39 | } else { 40 | watchPath(watcher.source); 41 | } 42 | } 43 | }; 44 | 45 | let watchPath = (p) => { 46 | gulpWatch( 47 | p, 48 | { 49 | read: false, // performance optimization: do not read actual file contents 50 | verbose: true 51 | }, 52 | (vinyl) => processChange(vinyl)); 53 | }; 54 | 55 | let processChange = (vinyl) => { 56 | if (vinyl.path && vinyl.cwd && vinyl.path.startsWith(vinyl.cwd)) { 57 | let pathToAdd = vinyl.path.slice(vinyl.cwd.length + 1); 58 | log(`Watcher: Adding path ${pathToAdd} to pending build changes...`); 59 | pendingRefreshPaths.push(pathToAdd); 60 | refresh(); 61 | } 62 | } 63 | 64 | let refresh = debounce(() => { 65 | if (isBuilding) { 66 | log('Watcher: A build is already in progress, deferring change detection...'); 67 | return; 68 | } 69 | 70 | isBuilding = true; 71 | 72 | let paths = pendingRefreshPaths.splice(0); 73 | let refreshTasks = []; 74 | 75 | // determine which tasks need to be executed 76 | // based on the files that have changed 77 | for (let watcher of watches) { 78 | if (Array.isArray(watcher.source)) { 79 | for(let source of watcher.source) { 80 | if (paths.find(path => minimatch(path, source))) { 81 | refreshTasks.push(watcher); 82 | } 83 | } 84 | } 85 | else { 86 | if (paths.find(path => minimatch(path, watcher.source))) { 87 | refreshTasks.push(watcher); 88 | } 89 | } 90 | } 91 | 92 | if (refreshTasks.length === 0) { 93 | log('Watcher: No relevant changes found, skipping next build.'); 94 | isBuilding = false; 95 | return; 96 | } 97 | 98 | log(`Watcher: Running ${refreshTasks.map(x => x.name).join(', ')} tasks on next build...`); 99 | 100 | let toExecute = gulp.series( 101 | readProjectConfiguration, 102 | gulp.parallel(refreshTasks.map(x => x.callback)), 103 | writeBundles, 104 | (done) => { 105 | isBuilding = false; 106 | watchCallback(); 107 | done(); 108 | if (pendingRefreshPaths.length > 0) { 109 | log('Watcher: Found more pending changes after finishing build, triggering next one...'); 110 | refresh(); 111 | } 112 | } 113 | ); 114 | 115 | toExecute(); 116 | }, debounceWaitTime); 117 | 118 | function log(message: string) { 119 | console.log(message); 120 | } 121 | 122 | function readProjectConfiguration() { 123 | return build.src(project); 124 | } 125 | 126 | function writeBundles() { 127 | return build.dest(); 128 | } 129 | 130 | export default watch; -------------------------------------------------------------------------------- /src/attributes/sort.ts: -------------------------------------------------------------------------------- 1 | import { bindable, bindingMode, customAttribute, inject } from 'aurelia-framework'; 2 | import { AuDatatableRequest } from '../models/request'; 3 | import { AuDatatableResponse } from '../models/response'; 4 | 5 | @customAttribute('au-datatable-sort') 6 | @inject(Element) 7 | export class AuDatatableSortAttribute { 8 | 9 | @bindable({ 10 | defaultBindingMode: bindingMode.twoWay, 11 | }) public request: AuDatatableRequest; 12 | 13 | @bindable() public onSort: (request: AuDatatableRequest) => Promise; 14 | @bindable() public activeColor: string = '#f44336'; 15 | @bindable() public inactiveColor: string = '#000'; 16 | 17 | private headers: HTMLTableHeaderCellElement[]; 18 | private template: string = ` 19 | 20 | 21 | 22 | 23 | `; 24 | 25 | constructor( 26 | private element: Element 27 | ) { } 28 | 29 | private attached(): void { 30 | if (this.element.nodeName !== 'THEAD') 31 | throw new Error('[au-table-sort:attached] au-table-sort needs to be bound to a THEAD node'); 32 | 33 | this.headers = Array.from(this.element.getElementsByTagName('th')); 34 | this.headers 35 | // Filter out columns without a data name property 36 | .filter((header) => { 37 | const name = this.getName(header); 38 | return name !== null && name !== undefined; 39 | }) 40 | // Generate our sort icons for each header 41 | .forEach((header) => { 42 | const name = this.getName(header); 43 | header.style.cursor = 'pointer'; 44 | header.addEventListener('click', (event) => this.sort(event)); 45 | header.innerHTML = header.innerHTML + this.template; 46 | if (this.request.sortBy === name) { 47 | this.setActive(header, this.request.sortDirection); 48 | } 49 | }); 50 | } 51 | 52 | private async sort(event: any): Promise { 53 | if (typeof this.onSort !== 'function') { 54 | throw new Error('[au-table-sort:sort] No onSort() callback has been set'); 55 | } 56 | 57 | const name = this.getName(event.target); 58 | if (this.request.sortBy === name) { 59 | switch (this.request.sortDirection) { 60 | case 'asc': 61 | this.request.sortDirection = 'desc'; 62 | break; 63 | case 'desc': 64 | this.request.sortDirection = undefined; 65 | break; 66 | default: 67 | this.request.sortDirection = 'asc'; 68 | break; 69 | } 70 | } else { 71 | this.request.sortBy = name; 72 | this.request.sortDirection = 'asc'; 73 | } 74 | 75 | this.setActive(event.target, this.request.sortDirection); 76 | const response = await this.onSort(this.request); 77 | this.request.data = response.data; 78 | this.request.totalRecords = response.totalRecords; 79 | } 80 | 81 | private setActive(target: any, direction: string | undefined): void { 82 | this.reset(); 83 | if (target.nodeName === 'SPAN') { 84 | target = target.parentNode.closest('th'); 85 | } 86 | const sortContainer = target.getElementsByClassName('sorting')[0]; 87 | const sort = sortContainer.getElementsByClassName(direction)[0]; 88 | if (sort) { 89 | sort.style.color = this.activeColor; 90 | } 91 | } 92 | 93 | private reset(): void { 94 | this.headers.forEach((x) => { 95 | const sorts = x.getElementsByClassName('sorting'); 96 | if (sorts.length === 0) { 97 | return; 98 | } 99 | Array.from(sorts[0].getElementsByTagName('span')) 100 | .forEach((span) => span.style.color = this.inactiveColor); 101 | }); 102 | } 103 | 104 | private getName(target: any): string { 105 | if (target.nodeName === 'SPAN') { 106 | target = target.parentNode.closest('th'); 107 | } 108 | return target.getAttribute('data-name'); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/components/pagination/pagination.ts: -------------------------------------------------------------------------------- 1 | import { bindable, BindingEngine, bindingMode, customElement, Disposable, inject } from 'aurelia-framework'; 2 | import { AuDatatableRequest, IAuDatatableRequest } from '../../models/request'; 3 | import { AuDatatableResponse } from '../../models/response'; 4 | 5 | @customElement('au-datatable-pagination') 6 | @inject(BindingEngine) 7 | export class AuDatatablePaginationComponent { 8 | 9 | @bindable({ 10 | defaultBindingMode: bindingMode.twoWay, 11 | changeHandler: 'dataChange' 12 | }) public request: AuDatatableRequest; 13 | 14 | @bindable() public amountOfPages: number = 2; 15 | @bindable() public onNextPage: (request: IAuDatatableRequest) => Promise; 16 | @bindable() public onPreviousPage: (request: IAuDatatableRequest) => Promise; 17 | @bindable() public onPageChange: (request: IAuDatatableRequest) => Promise; 18 | 19 | public followingPages: number; 20 | public previousPages: number; 21 | public refreshing: boolean = false; 22 | private totalPages: number; 23 | private subscriptions: Disposable[] = []; 24 | 25 | constructor( 26 | private bindingEngine: BindingEngine 27 | ) { } 28 | 29 | public attached(): void { 30 | this.subscriptions.push(this.bindingEngine 31 | .propertyObserver(this.request, 'currentPage') 32 | .subscribe(() => this.dataChange())); 33 | this.subscriptions.push(this.bindingEngine 34 | .propertyObserver(this.request, 'totalRecords') 35 | .subscribe(() => this.dataChange())); 36 | this.subscriptions.push(this.bindingEngine 37 | .propertyObserver(this.request, 'pageSize') 38 | .subscribe(() => this.dataChange())); 39 | } 40 | 41 | public async nextPage(): Promise { 42 | if (typeof this.onNextPage !== 'function') { 43 | throw new Error('[au-table-pagination:nextPage] No onNextPage() callback has been set'); 44 | } 45 | if (this.request.currentPage === this.totalPages) { 46 | return; 47 | } 48 | this.refreshing = true; 49 | this.request.skip += this.request.pageSize; 50 | this.request.currentPage++; 51 | const response = await this.onNextPage(this.request); 52 | this.request.totalRecords = response.totalRecords; 53 | this.request.data = response.data; 54 | this.refreshing = false; 55 | } 56 | 57 | public async previousPage(): Promise { 58 | if (typeof this.onPreviousPage !== 'function') { 59 | throw new Error('[au-table-pagination:previousPage] No onPreviousPage() callback has been set'); 60 | } 61 | if (this.request.currentPage === 1) { 62 | return; 63 | } 64 | this.refreshing = true; 65 | this.request.skip -= this.request.pageSize; 66 | this.request.currentPage--; 67 | const response = await this.onPreviousPage(this.request); 68 | this.request.totalRecords = response.totalRecords; 69 | this.request.data = response.data; 70 | this.refreshing = false; 71 | } 72 | 73 | public async changePage(page: number): Promise { 74 | if (typeof this.onPageChange !== 'function') { 75 | throw new Error('[au-table-pagination:changePage] No onChangePage() callback has been set'); 76 | } 77 | if (page + 1 === this.request.currentPage) { 78 | return; 79 | } 80 | this.refreshing = true; 81 | if (page < 0) { 82 | page = 0; 83 | } 84 | this.request.skip = page * this.request.pageSize; 85 | this.request.currentPage = page + 1; 86 | const response = await this.onPageChange(this.request); 87 | this.request.totalRecords = response.totalRecords; 88 | this.request.data = response.data; 89 | this.refreshing = false; 90 | } 91 | 92 | public calculatePreviousPageNumber(index: number): number { 93 | const result = (this.request.currentPage + index) - this.amountOfPages; 94 | return result === 0 ? 1 : result; 95 | } 96 | 97 | public detached(): void { 98 | this.subscriptions.forEach(x => x.dispose()); 99 | } 100 | 101 | private dataChange(): void { 102 | if (this.request.currentPage === undefined || this.request.totalRecords === undefined) { 103 | return; 104 | } 105 | this.refreshing = true; 106 | this.totalPages = Math.ceil(parseInt(this.request.totalRecords.toString(), 10) / this.request.pageSize); 107 | this.previousPages = this.request.currentPage - this.amountOfPages <= 0 108 | ? this.request.currentPage - 1 109 | : this.amountOfPages; 110 | this.followingPages = this.request.currentPage + this.amountOfPages > this.totalPages 111 | ? this.request.currentPage === this.totalPages ? 0 : this.totalPages - this.request.currentPage 112 | : this.amountOfPages; 113 | this.refreshing = false; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /aurelia_project/tasks/dev-server.ts: -------------------------------------------------------------------------------- 1 | // A mini dev server. 2 | // Uses standard Nodejs http or https server. 3 | // Uses "connect" for various middlewares. 4 | // Uses "socket.io" for live-reload in watch mode. 5 | // Uses "open" to automatically open user browser. 6 | import * as connect from 'connect'; 7 | import * as _open from 'open'; 8 | import * as serveStatic from 'serve-static'; 9 | import * as http from 'http'; 10 | import * as _https from 'https'; 11 | import * as historyApiFallback from 'connect-history-api-fallback'; 12 | import * as injector from 'connect-injector'; 13 | import * as socketIO from 'socket.io'; 14 | import * as enableDestroy from 'server-destroy'; 15 | 16 | // Use dedicated path for the dev server socket.io. 17 | // In order to avoid possible conflict with user app socket.io. 18 | const socketIOPath = '/__dev_socket.io'; 19 | // Tell user browser to reload. 20 | const socketIOSnippet = ` 21 | 22 | 29 | `; 30 | let server, io; 31 | 32 | export function run({ 33 | baseDir = '.', 34 | host = 'localhost', 35 | port = 9000, 36 | https = false, 37 | open = false // automatically open a browser window 38 | } = {}) { 39 | const app = connect() 40 | // Inject socket.io snippet for live-reload. 41 | // Note connect-injector is a special middleware, 42 | // has to be applied before all other middlewares. 43 | .use(injector( 44 | (req, res) => { 45 | const contentType = res.getHeader('content-type'); 46 | return contentType && (contentType.toLowerCase().indexOf('text/html') >= 0); 47 | }, 48 | (content, req, res, callback) => { 49 | const injected = content.toString().replace(/<\/head>/i, socketIOSnippet + '\n'); 50 | callback(null, injected); 51 | } 52 | )) 53 | // connect-history-api-fallback is a tool to help SPA dev. 54 | // So in dev mode, http://localhost:port/some/route will get 55 | // the same /index.html as content, instead of 404 at /some/route.html 56 | .use(historyApiFallback()) 57 | .use((req, res, next) => { 58 | res.setHeader('Access-Control-Allow-Origin', '*'); 59 | next(); 60 | }) 61 | .use(serveStatic(baseDir)); 62 | 63 | server = https ? 64 | _https.createServer({key: localKey, cert: localCert}, app) : 65 | http.createServer(app); 66 | io = socketIO(server, {path: socketIOPath}); 67 | server.listen(port); 68 | enableDestroy(server); 69 | const url = `http${https ? 's' : ''}://${host}:${port}`; 70 | console.log(`\x1b[36m\nDev server is started at: ${url}\n\x1b[0m`); 71 | if (open) _open(url); 72 | } 73 | 74 | export function reload() { 75 | io && io.emit('reload'); 76 | } 77 | 78 | export function destroy() { 79 | server && server.destroy(); 80 | } 81 | 82 | // An example of self-signed certificate. 83 | // Expires: Tuesday, 29 January 2030 84 | const localCert = `-----BEGIN CERTIFICATE----- 85 | MIICpDCCAYwCCQDn7uXANbZ/wzANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls 86 | b2NhbGhvc3QwHhcNMjAwMjAxMDA1ODQ3WhcNMzAwMTI5MDA1ODQ3WjAUMRIwEAYD 87 | VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+ 88 | X/5JbY8atUrS+0nfWA/tNHHaf0W9HKwyjC74Fv2xawQOFamSUELfjHhsauVfrPqb 89 | ONxZFAMC8GP4eRrJ8pMapN6HrIJRKMjHgImC4mOyCr7up9MqAVjvVRrrcP//XLdf 90 | ksw70LI0In7tzu7tazKP9Ix7VBbaPBV7YKY/0b7t/m8/0elRrrnUA9xWgFadYYGy 91 | ecgR7mvDDOIH9jzvbVr6tkhxnizlyRdpUuHcdVYGoJ97gJkm7d41Z3ICt9miKgCQ 92 | QyhOKDsDRiXMCUH4EEOYqljJqSnMi5tGgGdB11Pz64f0oIzdTPWOONLtYlzC+h7u 93 | gK199HtIReK7J/AZpdHpAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAF5gqaldwOBj 94 | eD23Xy0r87O4ddxIhlSYlfVFdx+9w4YXNg0rnAbBv2BEZtSIyoH9E3Uc+OKT/63b 95 | GsICR/21Rwz40SIBSTEutWlcp7G1cpqkel3X7fE3n/t5heJjG5QjCsu8R3FOMm/r 96 | JDwsJvooflTpHEYKERYOvmMpxWpwqA0uSRSJnFkD7w/PCNO0nwXFRZSUEyf80sa/ 97 | 4kW3wgcPbsFfjKhR+dMNExHCvXojQW5DIQB1+SVXPH+ovmVWBtuqfFRhg6ZpGZIz 98 | U6CkaUFtibCZy/PkE0b9NxfTZzbwGE6FuJIvxFuiFw+h2585aAe3/NvLE14jezTM 99 | YeaOL55+27w= 100 | -----END CERTIFICATE----- 101 | `; 102 | 103 | const localKey = `-----BEGIN RSA PRIVATE KEY----- 104 | MIIEpAIBAAKCAQEAvl/+SW2PGrVK0vtJ31gP7TRx2n9FvRysMowu+Bb9sWsEDhWp 105 | klBC34x4bGrlX6z6mzjcWRQDAvBj+HkayfKTGqTeh6yCUSjIx4CJguJjsgq+7qfT 106 | KgFY71Ua63D//1y3X5LMO9CyNCJ+7c7u7Wsyj/SMe1QW2jwVe2CmP9G+7f5vP9Hp 107 | Ua651APcVoBWnWGBsnnIEe5rwwziB/Y8721a+rZIcZ4s5ckXaVLh3HVWBqCfe4CZ 108 | Ju3eNWdyArfZoioAkEMoTig7A0YlzAlB+BBDmKpYyakpzIubRoBnQddT8+uH9KCM 109 | 3Uz1jjjS7WJcwvoe7oCtffR7SEXiuyfwGaXR6QIDAQABAoIBADMVqzSk84ulLkjj 110 | KXWHOe7a7dpF4L7YXNTLjScBdF4Ra2skIPakFu3J0d616Ir97dmNLoOwvQYi35Cj 111 | Xq7mKtcxeo1Jm0aP/SCbu0ql2T7DZ2y/GAjjh6vhWHHpRqiNhp9c0vUSEV+wCgNi 112 | TfbjlxPN+Yx2ihNRoCoVS0dAz00pTLBo0W63bPf1WMB9K2TYRpVG+4X/HtFYL77x 113 | isQjAEF7Av2tLPwgp3RX7eeV1ojXdTPrCMEsoyZBaEUjdtuGkP/J5vaF75IWCgIN 114 | 4sdcYKemVeZB7JHie1/LxjNLnXFDDSL6Qb6ps15nMW73+/IoHPZ+KJK42gWcoOGF 115 | AgnUUUkCgYEA53Qjjw/zwP5CWIXgoNgYkg2XpxZ9Hx6GNhfaMnMOKIrA5m9MAkEH 116 | Z7WIwxHFfQNLvtBNGuMPBgNyWKBoF1rsP/WH8aEUGvRJHnqSiBIvhPO6uJ2hQaJg 117 | cvPIDoit5Vu4O4tYUbZI2bn2yH13oUyJ6OK58+87+Nb2jT9c4TiEZ4MCgYEA0pCW 118 | MLBLCHL3oKtb57N4ztbKiOe5FTPl8jPtM13dHBIa/cnWVbajTB4fghtdoLnCqoaS 119 | e43yDhn1liE+1/uj72Kz5R8TgzEOuoOSFM2UdzKtkom493PP1mRgKP1g+I2nZksS 120 | EYGCua8hHXm1LZ3qiDnn3sgVdb38pI4i/THquSMCgYEAjaahgJPlvW6e0iiVMjsu 121 | xmw1LRhxWRNIVmDAtHF/78YDisQAw7xiuND8M050DC9xMwWuus7NygNf/uek7O5D 122 | el1dZr8LW/e3rESd21Mt6/NyijxGjbG/z3ptLJ/vtVgt55s/YTrrWP0cENXg2kHK 123 | gVIJNkZq8L82w3lM8bWyKtsCgYEAv3A1HI8rqMLd4HXrWP0TGPqvqUkEPQKyTUJo 124 | pgrwvFS5tYOMGuGyFcJNYzz+IuLA2cj/5NVo/OkdHyGawUNICJz0cZuPYfd4LJry 125 | dXdzQ+wPYutT/6aLj6AyzRGQ2GnxiE84XjIhaDCRKvs8ffzU/oWnCiVfXW0eBX40 126 | 0X5QqYECgYA9rTtNkcPSc/DQ2MAYqY+gQFT1KJ5iu3xpsJCsda1iA3H+CJgy6ewp 127 | T5wCqfxtcUza62Pa+hwhP4DewAbCosedAhNb7UOqwYXjMR5262ecNqhL3biguD0i 128 | YaFo2iRA3JVA7Nd6a/Q4JbDXJWeKxR+LD35etO20vrqz2jj61pfClw== 129 | -----END RSA PRIVATE KEY----- 130 | `; 131 | -------------------------------------------------------------------------------- /src/components/filter/filter.ts: -------------------------------------------------------------------------------- 1 | import { bindable, bindingMode, containerless, customElement } from 'aurelia-framework'; 2 | import { AuDatatableFilter } from '../../models/filter'; 3 | import { AuDatatableRequest } from '../../models/request'; 4 | import { AuDatatableResponse } from '../../models/response'; 5 | 6 | @containerless() 7 | @customElement('au-datatable-filter') 8 | export class AuDatatableFilterComponent { 9 | 10 | @bindable({ 11 | defaultBindingMode: bindingMode.twoWay, 12 | }) public request: AuDatatableRequest; 13 | 14 | @bindable() public onFilter: (request: AuDatatableRequest) => Promise; 15 | @bindable() public btnClasses: string; 16 | @bindable() public filters: AuDatatableFilter[]; 17 | @bindable() public labelClearFilter: string = 'clear filter'; 18 | 19 | private amountOfColumns: number; 20 | private auTableFilter: any; 21 | private filterElements: any[]; 22 | private activeFilterBtn: any; 23 | private filterValues: string[] = []; 24 | 25 | private attached(): void { 26 | if (!this.request.filters) { 27 | this.request.filters = []; 28 | } 29 | this.getColumnsCount(); 30 | document.getElementsByTagName('html')[0].addEventListener('click', (e) => this.hideFilterDropdowns(e)); 31 | this.request.filters.map((x) => this.filterValues[x.selectedColumn] = x.value); 32 | } 33 | 34 | public detached(): void { 35 | document.getElementsByTagName('html')[0].removeEventListener('click', (e) => this.hideFilterDropdowns(e)); 36 | } 37 | 38 | public shouldGenerateContent(column: number): boolean { 39 | const filter = this.filters.find((fltr) => fltr.applyToColumns.some((c) => c === column)); 40 | return filter !== null && filter !== undefined; 41 | } 42 | 43 | public shouldAddFilter(filter: AuDatatableFilter, column: number): boolean { 44 | return filter.applyToColumns.some((x) => x === column); 45 | } 46 | 47 | public async selectFilter(event: any, filter: AuDatatableFilter, column: number): Promise { 48 | if (typeof this.onFilter !== 'function') { 49 | throw new Error('[au-table-filter:selectFilter] No onFilter() callback has been set'); 50 | } 51 | const value = this.filterValues[column]; 52 | if (value) { 53 | this.removeFiltersForColumn(column); 54 | this.request.filters.push({ 55 | value, 56 | description: filter.description, 57 | selectedColumn: column, 58 | applyToColumns: [] 59 | }); 60 | this.setActiveLabelFilter(event); 61 | const response = await this.onFilter(this.request); 62 | this.request.totalRecords = response.totalRecords; 63 | this.request.data = response.data; 64 | this.reset(); 65 | } else { 66 | this.showInputWarning(event); 67 | } 68 | } 69 | 70 | public isSelectedFilter(filter: AuDatatableFilter, column: number): boolean { 71 | return this.request.filters 72 | .some((x) => x.description === filter.description && x.selectedColumn === column); 73 | } 74 | 75 | public showFilters(event: any): void { 76 | this.activeFilterBtn = event.target; 77 | const parent = event.target.closest('div'); 78 | const filter = parent.getElementsByClassName('au-filter-container')[0]; 79 | filter.style.display = filter.style.display === 'block' ? 'none' : 'block'; 80 | } 81 | 82 | public async inputChanged(column: number): Promise { 83 | if (!this.filterValues[column]) { 84 | this.removeFiltersForColumn(column); 85 | const response = await this.onFilter(this.request); 86 | this.request.totalRecords = response.totalRecords; 87 | this.request.data = response.data; 88 | this.reset(); 89 | } else { 90 | if (this.request.filters.some((x) => x.selectedColumn === column)) { 91 | const filter = this.request.filters.find((x) => x.selectedColumn === column); 92 | filter.value = this.filterValues[column]; 93 | const response = await this.onFilter(this.request); 94 | this.request.totalRecords = response.totalRecords; 95 | this.request.data = response.data; 96 | this.reset(); 97 | } 98 | } 99 | } 100 | 101 | public async clearFilter(event: any, column: number): Promise { 102 | const parent = event.target.closest('td'); 103 | const input = parent.getElementsByClassName('au-filter-input')[0]; 104 | this.removeFiltersForColumn(column); 105 | input.value = ''; 106 | this.filterValues[column] = undefined; 107 | const response = await this.onFilter(this.request); 108 | this.request.totalRecords = response.totalRecords; 109 | this.request.data = response.data; 110 | this.reset(); 111 | } 112 | 113 | private getColumnsCount(): void { 114 | this.auTableFilter = document.getElementsByClassName('au-table-filter')[0]; 115 | const thead = this.auTableFilter.closest('thead'); 116 | const headers = thead.getElementsByTagName('tr')[0]; 117 | this.amountOfColumns = headers.getElementsByTagName('th').length; 118 | } 119 | 120 | private hideFilterDropdowns(event: any): void { 121 | if (this.activeFilterBtn === event.target) { 122 | return; 123 | } 124 | const ignoreElements = ['au-filter', 'au-filter-cell', 'au-filter-input', 'au-clear', 'au-clear-icon']; 125 | if (Array.from(event.target.classList).some(x => ignoreElements.some(y => y === x))) { 126 | return; 127 | } 128 | if (!this.filterElements) { 129 | this.filterElements = this.auTableFilter.getElementsByClassName('au-filter-container'); 130 | } 131 | Array.from(this.filterElements).forEach((x) => x.style.display = 'none'); 132 | } 133 | 134 | private showInputWarning(event: any): void { 135 | const parent = event.target.closest('td'); 136 | const input = parent.getElementsByClassName('au-filter-input')[0]; 137 | input.style.border = '1px red solid'; 138 | setTimeout(() => input.style.border = '1px #ddd solid', 500); 139 | } 140 | 141 | private setActiveLabelFilter(event: any): void { 142 | event.target.classList.add('active'); 143 | } 144 | 145 | private removeFiltersForColumn(column: number): void { 146 | this.removeActiveLabelsForColumn(column); 147 | this.request.filters = this.request.filters 148 | .filter((x) => x.selectedColumn !== column); 149 | } 150 | 151 | private removeActiveLabelsForColumn(column: number): void { 152 | const filters = this.auTableFilter.getElementsByClassName('au-filter'); 153 | Array.from(filters).forEach((element: HTMLElement) => { 154 | if (element.getAttribute('data-column') === column.toString()) { 155 | element.classList.remove('active') 156 | } 157 | }); 158 | } 159 | 160 | private reset(): void { 161 | this.request.currentPage = this.request.totalRecords > 0 ? 1 : 0; 162 | this.request.skip = 0; 163 | } 164 | } 165 | --------------------------------------------------------------------------------