├── 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 |
2 |
3 | ${ info }
4 | (${ labelFiltered })
5 |
6 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
6 |
7 |
--------------------------------------------------------------------------------
/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 |
2 | Hello, this is the dev app for plugin au-datatable
3 | This dev app is bundled to scripts/ folder (ignored in .gitignore). If you commit the bundle files to github, this app can serve as a github page!
4 |
5 | Please read the README file in your project for more information.
6 |
7 |
8 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | - ${ filter.description }
10 | - ${ labelClearFilter }
11 |
12 |
13 |
14 | |
15 |
16 |
--------------------------------------------------------------------------------
/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 `
41 | \${value}
42 |
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 |
2 |
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------