瀏覽代碼

Initial Commit

master
Anoop 2 年之前
當前提交
77b0dcde20
共有 44 個文件被更改,包括 14997 次插入0 次删除
  1. +2
    -0
      .eslintignore
  2. +184
    -0
      .eslintrc
  3. +45
    -0
      .gitignore
  4. +21
    -0
      LICENSE
  5. +87
    -0
      README.md
  6. +4
    -0
      cypress.json
  7. +5
    -0
      cypress/fixtures/example.json
  8. +84
    -0
      cypress/integration/cell.js
  9. +70
    -0
      cypress/integration/column.js
  10. +89
    -0
      cypress/integration/inline_filters.js
  11. +20
    -0
      cypress/integration/new_instance.js
  12. +15
    -0
      cypress/integration/row.js
  13. +17
    -0
      cypress/plugins/index.js
  14. +57
    -0
      cypress/support/commands.js
  15. +20
    -0
      cypress/support/index.js
  16. +12
    -0
      cypress/tsconfig.json
  17. +284
    -0
      index.html
  18. +78
    -0
      package.json
  19. +94
    -0
      rollup.config.js
  20. +145
    -0
      src/body-renderer.js
  21. +895
    -0
      src/cellmanager.js
  22. +455
    -0
      src/columnmanager.js
  23. +11
    -0
      src/dark.css
  24. +623
    -0
      src/datamanager.js
  25. +270
    -0
      src/datatable.js
  26. +73
    -0
      src/defaults.js
  27. +235
    -0
      src/dom.js
  28. +204
    -0
      src/filterRows.js
  29. +10
    -0
      src/icons.js
  30. +5
    -0
      src/index.js
  31. +59
    -0
      src/keyboard.js
  32. +363
    -0
      src/rowmanager.js
  33. +292
    -0
      src/style.css
  34. +373
    -0
      src/style.js
  35. +30
    -0
      src/translationmanager.js
  36. +15
    -0
      src/translations/de.json
  37. +15
    -0
      src/translations/en.json
  38. +15
    -0
      src/translations/fr.json
  39. +13
    -0
      src/translations/index.js
  40. +15
    -0
      src/translations/it.json
  41. +151
    -0
      src/utils.js
  42. +148
    -0
      test/datamanager.spec.js
  43. +81
    -0
      test/utils.spec.js
  44. +9313
    -0
      yarn.lock

+ 2
- 0
.eslintignore 查看文件

@@ -0,0 +1,2 @@
dist
docs

+ 184
- 0
.eslintrc 查看文件

@@ -0,0 +1,184 @@
{
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},

"env": {
"browser": true,
"es6": true,
"node": true,
"mocha": true
},

"globals": {
"document": false,
"escape": false,
"navigator": false,
"unescape": false,
"window": false,
"describe": true,
"before": true,
"it": true,
"expect": true,
"sinon": true,
"Clusterize": true,
"cy": true,
"Cypress": true
},

"plugins": [

],

"rules": {
"block-scoped-var": 2,
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
"camelcase": [2, { "properties": "always" }],
"comma-spacing": [2, { "before": false, "after": true }],
"comma-style": [2, "last"],
"complexity": 0,
"consistent-return": 2,
"consistent-this": 0,
"curly": [2, "multi-line"],
"default-case": 0,
"dot-location": [2, "property"],
"dot-notation": 0,
"eol-last": 2,
"eqeqeq": [2, "allow-null"],
"func-names": 0,
"func-style": 0,
"generator-star-spacing": [2, "both"],
"guard-for-in": 0,
"handle-callback-err": [2, "^(err|error|anySpecificError)$" ],
"indent": [2, 4, { "SwitchCase": 1 }],
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
"keyword-spacing": [2, {"before": true, "after": true}],
"linebreak-style": 0,
"max-depth": 0,
"max-len": [2, 120, 4],
"max-nested-callbacks": 0,
"max-params": 0,
"max-statements": 0,
"new-cap": [2, { "newIsCap": true, "capIsNew": false }],
"newline-after-var": [0, "always"],
"new-parens": 2,
"no-alert": 0,
"no-array-constructor": 2,
"no-bitwise": 0,
"no-caller": 2,
"no-catch-shadow": 0,
"no-cond-assign": 2,
"no-console": 0,
"no-constant-condition": 0,
"no-continue": 0,
"no-control-regex": 2,
"no-debugger": 2,
"no-delete-var": 2,
"no-div-regex": 0,
"no-dupe-args": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-else-return": 2,
"no-empty": 0,
"no-empty-character-class": 2,
"no-eq-null": 0,
"no-eval": 2,
"no-ex-assign": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-extra-boolean-cast": 2,
"no-extra-parens": 0,
"no-extra-semi": 0,
"no-extra-strict": 0,
"no-fallthrough": 2,
"no-floating-decimal": 2,
"no-func-assign": 2,
"no-implied-eval": 2,
"no-inline-comments": 0,
"no-inner-declarations": [2, "functions"],
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 0,
"no-lonely-if": 0,
"no-loop-func": 0,
"no-mixed-requires": 0,
"no-mixed-spaces-and-tabs": [2, false],
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-multiple-empty-lines": [2, { "max": 1 }],
"no-native-reassign": 2,
"no-negated-in-lhs": 2,
"no-nested-ternary": 0,
"no-new": 2,
"no-new-func": 2,
"no-new-object": 2,
"no-new-require": 2,
"no-new-wrappers": 2,
"no-obj-calls": 2,
"no-octal": 2,
"no-octal-escape": 2,
"no-path-concat": 0,
"no-plusplus": 0,
"no-process-env": 0,
"no-process-exit": 0,
"no-proto": 2,
"no-redeclare": 2,
"no-regex-spaces": 2,
"no-reserved-keys": 0,
"no-restricted-modules": 0,
"no-return-assign": 2,
"no-script-url": 0,
"no-self-compare": 2,
"no-sequences": 2,
"no-shadow": 0,
"no-shadow-restricted-names": 2,
"no-spaced-func": 2,
"no-sparse-arrays": 2,
"no-sync": 0,
"no-ternary": 0,
"no-throw-literal": 2,
"no-trailing-spaces": 2,
"no-undef": 2,
"no-undef-init": 2,
"no-undefined": 0,
"no-underscore-dangle": 0,
"no-unneeded-ternary": 2,
"no-unreachable": 2,
"no-unused-expressions": 0,
"no-unused-vars": [2, { "vars": "all", "args": "none" }],
"no-use-before-define": 0,
"no-var": 0,
"no-void": 0,
"no-warning-comments": 0,
"no-with": 2,
"one-var": 0,
"operator-assignment": 0,
"operator-linebreak": [2, "after"],
"padded-blocks": 0,
"quote-props": 0,
"quotes": [2, "single", "avoid-escape"],
"radix": 2,
"semi": [2, "always"],
"semi-spacing": 0,
"sort-vars": 0,
"space-before-blocks": [2, "always"],
"space-before-function-paren": [2, {"anonymous": "always", "named": "never"}],
"space-in-brackets": 0,
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"space-unary-ops": [2, { "words": true, "nonwords": false }],
"spaced-comment": [2, "always"],
"strict": 0,
"use-isnan": 2,
"valid-jsdoc": 0,
"valid-typeof": 2,
"vars-on-top": 2,
"wrap-iife": [2, "any"],
"wrap-regex": 0,
"yoda": [2, "never"]
}
}

+ 45
- 0
.gitignore 查看文件

@@ -0,0 +1,45 @@
# Logs
logs
*.log

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules

# Remove some common IDE working directories
.idea
.vscode

# npm debug logs
npm-debug.log.*

.DS_Store

# cypress
cypress/screenshots
cypress/videos

# dist
dist

.env

+ 21
- 0
LICENSE 查看文件

@@ -0,0 +1,21 @@
The MIT License

Copyright (c) 2018 XhiveFramework Technologies Pvt. Ltd. <developers@xhiveframework.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

+ 87
- 0
README.md 查看文件

@@ -0,0 +1,87 @@
<div align="center">
<img src="https://github.com/xhiveframework/design/raw/master/logos/logo-2019/xhiveframework-datatable-logo.png" height="128">
<h2>XhiveFramework DataTable</h2>
<p align="center">
<p>
A modern datatable library for the web
</p>

[![Test and Release](https://github.com/xhiveframework/datatable/workflows/Test%20and%20Release/badge.svg)](https://github.com/xhiveframework/datatable/actions?query=workflow%3A%22Test+and+Release%22)
[![npm version](https://badge.fury.io/js/xhiveframework-datatable.svg)](https://badge.fury.io/js/xhiveframework-datatable)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT)
![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/xhiveframework-datatable.svg)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)


</div>

## Introduction

XhiveFramework DataTable is a simple, modern and interactive datatable library for displaying tabular data. Originally built for [XhiveERP](https://github.com/xhiveframework/xhiveerp), it can be used to render large amount of rows without sacrificing performance and has the basic data grid features like inline editing and keyboard navigation. It does not require jQuery, unlike most data grids out there.

## Demo

![datatable-demo-2](https://user-images.githubusercontent.com/9355208/40740030-5412aa40-6465-11e8-8542-b0247ab1daac.gif)

## Features

### Cell Features

* Custom Formatters
* Inline Editing
* Mouse Selection
* Copy Cells
* Keyboard Navigation
* Custom Cell Editor

### Column Features

* Reorder Columns
* Sort by Column
* Remove / Hide Column
* Custom Actions
* Resize Column
* Flexible Layout

### Row Features

* Row Selection
* Tree Structured Rows
* Inline Filters
* Large Number of Rows
* Dynamic Row Height

## Install

```bash
yarn add xhiveframework-datatable
# or
npm install xhiveframework-datatable
```

> Note: [`sortablejs`](https://github.com/RubaXa/Sortable) is required to be installed as well.

## Usage

```js
const datatable = new DataTable('#datatable', {
columns: [ 'First Name', 'Last Name', 'Position' ],
data: [
[ 'Don', 'Joe', 'Designer' ],
[ 'Mary', 'Jane', 'Software Developer' ]
]
});
```

## Contribution

* `yarn start` - Start dev server
* Open `index.html` located in the root folder, and start development.
* Run `yarn lint` before committing changes
* This project uses [commitizen](https://github.com/commitizen/cz-cli) for conventional commit messages, use `yarn commit` command instead of `git commit`



## License

[MIT](http://opensource.org/licenses/MIT)

+ 4
- 0
cypress.json 查看文件

@@ -0,0 +1,4 @@
{
"baseUrl": "http://localhost:8989",
"projectId": "2nsyux"
}

+ 5
- 0
cypress/fixtures/example.json 查看文件

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

+ 84
- 0
cypress/integration/cell.js 查看文件

@@ -0,0 +1,84 @@
describe('Cell', function () {
before(function () {
cy.visit('/');
});

it('focuses cell on click', function () {
cy.clickCell(2, 0)
.should('have.class', 'dt-cell--focus');
});

it('not focuses cell which are not focusable', function () {
cy.clickCell(1, 0)
.should('not.have.class', 'dt-cell--focus');
});

it('edit cell on enter press', function () {
cy.getCell(4, 0).type('{enter}')
.should('have.class', 'dt-cell--editing')
.type('{enter}')
.should('not.have.class', 'dt-cell--editing');
});

it('edit cell on double click', function () {
cy.getCell(4, 0)
.as('target')
.dblclick({ force: true })
.should('have.class', 'dt-cell--editing');

cy.clickCell(3, 0);

cy.get('@target').should('not.have.class', 'dt-cell--editing');
});

it('edit cell', function () {
cy.getCell(4, 1).dblclick({ force: true });
cy.getCell(4, 1).find('input').click();
cy.focused().type('{selectall}{del}Test{enter}');
cy.getCell(4, 1).contains('Test');
});

it('if editing is false: editing should not activate', function () {
cy.getCell(3, 0).dblclick({ force: true })
.should('not.have.class', 'dt-cell--editing');
});

it('navigation using arrow keys', function () {
cy.clickCell(2, 0)
.type('{rightarrow}');

cy.get('.dt-cell--focus')
.should('have.class', 'dt-cell--3-0')
.click({ force: true })
.type('{downarrow}');

cy.get('.dt-cell--focus')
.should('have.class', 'dt-cell--3-1');
// TODO: test navigation over hidden rows
});

it('navigation using ctrl + arrow keys', function () {
cy.clickCell(2, 0)
.type('{ctrl}{rightarrow}');
cy.get('.dt-cell--focus')
.should('have.class', 'dt-cell--7-0');
});

it('cell selection using shift + arrow keys', function () {
cy.getCell(2, 1)
.type('{shift}{rightarrow}{rightarrow}{downarrow}');

// 6 cells and 2 headers
cy.get('.dt-cell--highlight').should('have.length', 6 + 2);

cy.clickCell(2, 0);
});

it('mouse selection', function () {
// TODO:
// cy.getCell(2, 1)
// .trigger('mousedown', { which: 1, pageX: 331, pageY: 207, force: true })
// .trigger('mousemove', { which: 1, pageX: 489, pageY: 312 })
// .trigger('mouseup');
});
});

+ 70
- 0
cypress/integration/column.js 查看文件

@@ -0,0 +1,70 @@
describe('Column', function () {
before(function () {
cy.visit('/');
});

it('header dropdown toggles on click', function () {
cy.getColumnCell(2)
.find('.dt-dropdown__toggle')
.as('toggle')
.click();
cy.get('.dt-dropdown__list')
.as('dropdown-list')
.should('be.visible');

cy.getColumnCell(2).click();

cy.get('@dropdown-list').should('not.be.visible');
});

it('sort ascending button should work', function () {
cy.clickDropdown(2);
cy.clickDropdownItem(2, 'Sort Ascending');

cy.window().then(win => win.datatable.getColumn(2))
.its('sortOrder')
.should('eq', 'asc');

cy.window().then(win => win.datatable.datamanager)
.its('currentSort.colIndex')
.should('eq', 2);

cy.get('.dt-scrollable .dt-row:first')
.contains('Airi Satou');

cy.clickDropdownItem(2, 'Reset sorting');
});

it('removes column using dropdown action', function () {
cy.get('.dt-cell--header').should('have.length', 8);

cy.clickDropdown(5);
cy.clickDropdownItem(5, 'Remove column');

cy.get('.dt-cell--header').should('have.length', 7);
});

it('resize column with mouse drag', function () {
cy.get('.dt-cell--header-3 .dt-cell__resize-handle').as('resize-handle');
cy
.get('@resize-handle')
.trigger('mousedown')
.trigger('mousemove', { pageX: 510, pageY: 20, which: 1 })
.trigger('mouseup');

cy.getColumnCell(3)
.should('have.css', 'width')
.and('match', /13\dpx/);
cy.getCell(3, 1)
.should('have.css', 'width')
.and('match', /13\dpx/);
});

it('resize column using double click', function () {
cy.get('.dt-cell--header-4 .dt-cell__resize-handle').trigger('dblclick');
cy.getColumnCell(4).should('have.css', 'width')
.and('match', /9\dpx/);
cy.getCell(4, 1).should('have.css', 'width')
.and('match', /9\dpx/);
});
});

+ 89
- 0
cypress/integration/inline_filters.js 查看文件

@@ -0,0 +1,89 @@
describe('Inline Filters', function () {
before(function () {
cy.visit('/');
});

beforeEach(function () {
cy.get('.dt-filter[data-col-index=4]').as('filterInput4');
cy.get('.dt-filter[data-col-index=5]').as('filterInput5');
cy.get('.dt-filter[data-col-index=6]').as('filterInput6');
cy.get('.dt-row[data-row-index=0]').should('be.visible');
});

it('simple text filter', function () {
cy.getCell(4, 0).click().type('{ctrl}f');

cy.get('@filterInput4').type('edin');
cy.get('.dt-row-0').should('be.visible');
cy.get('.dt-row-1').should('not.exist');
cy.get('@filterInput4').clear();
});

it('simple number filter', function () {
cy.get('@filterInput5').type('2360');
cy.get('.dt-row[data-row-index=8]').should('be.visible');
cy.get('.dt-row[data-row-index=15]').should('not.exist');
cy.get('.dt-row[data-row-index=22]').should('not.exist');
cy.get('@filterInput5').clear();
});

it('greater than', function () {
cy.get('@filterInput5').type('> 6000');
cy.get('.dt-row[data-row-index=0]').should('not.exist');
cy.get('.dt-row[data-row-index=3]').should('be.visible');
cy.get('@filterInput5').clear();
});

it('less than', function () {
cy.get('@filterInput5').type('< 2000');
cy.get('.dt-row[data-row-index=0]').should('not.exist');
cy.get('.dt-row[data-row-index=51]').should('be.visible');
cy.get('@filterInput5').clear();
});

it('range', function () {
cy.get('@filterInput5').type(' 2000: 5000');
cy.get('.dt-row[data-row-index=4]').should('not.exist');
cy.get('.dt-row[data-row-index=5]').should('be.visible');
cy.get('@filterInput5').clear();
});

it('equals', function () {
cy.get('@filterInput5').type('=9608');
cy.get('.dt-row-6').should('be.visible');
cy.get('@filterInput5').clear();
});

it('multiple filters', function () {
cy.get('@filterInput4').type('to');
cy.get('@filterInput5').type('54');

cy.get('.dt-row[data-row-index=4]').should('be.visible');
cy.get('.dt-row[data-row-index=1]').should('not.exist');
cy.get('@filterInput4').clear();
cy.get('@filterInput5').clear();
});

it('greater than for string type filters', function () {
cy.get('@filterInput6').type('> 01/07/2011');
cy.get('.dt-row[data-row-index=0]').should('not.exist');
cy.get('.dt-row[data-row-index=1]').should('be.visible');
cy.get('.dt-row[data-row-index=3]').should('be.visible');
cy.get('.dt-row[data-row-index=5]').should('be.visible');
cy.get('@filterInput6').clear();
});

it('filters with sorting', function () {
cy.visit('/');
cy.clickDropdown(7);
cy.clickDropdownItem(7, 'Sort Descending');
cy.get('.dt-filter[data-col-index=5]').as('filterInput5');
cy.getCell(5, 24).click().type('{ctrl}f');
cy.get('@filterInput5').type('>3000', {delay: 100});

cy.get('.dt-scrollable .dt-row:first')
.should('contain', 'Angelica')
.should('have.class', 'dt-row-24');
cy.get('@filterInput5').clear();
});
});

+ 20
- 0
cypress/integration/new_instance.js 查看文件

@@ -0,0 +1,20 @@
describe('DataTable init', function () {
it('instance is created without any errors', () => {
cy.visit('/');

cy.window()
.its('DataTable')
.then(DataTable => {
// eslint-disable-next-line
new DataTable('#datatable2', {
columns: ['Name', 'Position'],
data: [
['Faris', 'Developer']
]
});
});

cy.get('#datatable2 .datatable')
.contains('Faris');
});
});

+ 15
- 0
cypress/integration/row.js 查看文件

@@ -0,0 +1,15 @@
describe('Row', function () {
before(function () {
cy.visit('/');
});

it('check / uncheck row', function () {
cy.get('.dt-scrollable .dt-row:first')
.find('input[type="checkbox"]')
.click();

cy.get('[data-row-index="0"]').should('have.class', 'dt-row--highlight');

cy.get('.dt-toast').contains('1 row selected');
});
});

+ 17
- 0
cypress/plugins/index.js 查看文件

@@ -0,0 +1,17 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************

// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)

module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
};

+ 57
- 0
cypress/support/commands.js 查看文件

@@ -0,0 +1,57 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

Cypress.Commands.add('getCell', (col, row) => {
return cy.get(`.dt-cell--${col}-${row}`);
});

Cypress.Commands.add('clickCell', (col, row) => {
return cy.getCell(col, row).click({ force: true });
});

Cypress.Commands.add('getColumnCell', (col) => {
return cy.get(`.dt-cell--header-${col}`);
});

Cypress.Commands.add('clickDropdown', (col) => {
return cy.getColumnCell(col)
.find('.dt-dropdown__toggle')
.click();
});

Cypress.Commands.add('clickDropdownItem', (col, item) => {
return cy.get(`.dt-dropdown__list-item:contains("${item}")`)
.click({ force: true });
});

Cypress.Commands.add('typeTab', (shiftKey, ctrlKey) => {
cy.focused().trigger('keydown', {
keyCode: 9,
which: 9,
shiftKey: shiftKey,
ctrlKey: ctrlKey
});
});

+ 20
- 0
cypress/support/index.js 查看文件

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands';

// Alternatively you can use CommonJS syntax:
// require('./commands')

+ 12
- 0
cypress/tsconfig.json 查看文件

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"allowJs": true,
"baseUrl": "../node_modules",
"types": [
"cypress"
]
},
"include": [
"**/*.*"
]
}

+ 284
- 0
index.html 查看文件

@@ -0,0 +1,284 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>XhiveFramework DataTable</title>
<style>
body {
font-family: 'Tahoma';
font-weight: normal;
font-size: 12px;
}
</style>
<link rel="stylesheet" href="./dist/xhiveframework-datatable.css" />
</head>

<body>
<h1>XhiveFramework DataTable</h1>
<button onclick="datatable.render()">Render Table</button>
<button onclick="datatable.refresh()">Refresh Data</button>
<button onclick="switchToTreeView()" data-action="treeview">TreeView</button>
<label>
<input type="checkbox" id="input-large-data" />
<span>Large Data</span>
</label>
<label>
<input type="checkbox" id="dark-theme" />
<span>Dark Theme</span>
</label>
<section style="width: 60%; margin: 0 auto;">

</section>

<section id="datatable2" style="width: 60%; ">

</section>

<script src="./node_modules/sortablejs/Sortable.js"></script>
<script src="./dist/xhiveframework-datatable.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {

let data = [];
let columns = [];
let largeData = false;

document.querySelector('#input-large-data').addEventListener('change', (e) => {
const enabled = e.target.value === 'on';
largeData = enabled;
buildData();
makeDatatable();
});

document.querySelector('#dark-theme').addEventListener('change', (e) => {
const enabled = document.querySelector('#dark-theme:checked');
if (enabled) {
const link = document.createElement('link');
link.href = 'src/dark.css';
link.rel = 'stylesheet'
link.id = 'dark-theme-stylesheet'
document.head.appendChild(link);

link.onload = () => datatable.setDimensions()
} else {
document.getElementById('dark-theme-stylesheet').remove();
setTimeout(() => datatable.setDimensions())
}
})

function buildTreeData() {
columns = [
{ name: 'Files', width: 300 },
{ name: 'Size', width: 150, align: 'right' },
{ name: 'Last Updated', width: 200, align: 'right' },
]

data = [
{
'Files': 'All Folders',
'Size': '2M',
'Last Updated': '',
'indent': -1
},
{
'Files': 'Documents',
'Size': '2M',
'Last Updated': '',
'indent': 0
},
{
'Files': 'project.pdf',
'Size': '1M',
'Last Updated': 'Yesterday',
'indent': 1
},
{
'Files': 'my-face.png',
'Size': '500k',
'Last Updated': '2018-04-09',
'indent': 1
},
{
'Files': 'Projects',
'Size': '77M',
'Last Updated': '',
'indent': 0
},
{
'Files': 'xhiveframework-gantt',
'Size': '23M',
'Last Updated': '',
'indent': 1
},
{
'Files': 'dist',
'Size': '50k',
'Last Updated': '2018-06-01',
'indent': 2
},
{
'Files': 'package.json',
'Size': '5k',
'Last Updated': '2018-06-01',
'indent': 2
},
{
'Files': 'xhiveframework-datatable',
'Size': '54M',
'Last Updated': '',
'indent': 1
},
{
'Files': 'src',
'Size': '53k',
'Last Updated': 'A few seconds ago',
'indent': 2
},
{
'Files': 'dist',
'Size': '21k',
'Last Updated': 'A few seconds ago',
'indent': 2
},
]

data.map(d => d.indent++)
}

function buildData() {
columns = [
{ name: "Name" },
{ name: "Position" },
{ name: "Office" },
{ name: "Extn." },
{
name: "Start Date",
format: (value) => (value || '').split('/').reverse().join('/'),
compareValue: (cell, keyword) => {
const keywordValue = keyword.split('/').reverse().join('/')
return [+new Date(cell.content), +new Date(keywordValue)];
}
},
{ name: "Salary", format: value => formatMoney(value) }
];
data = [["Tiger Nixon", { content: "System Architect<br>New line", editable: false }, "Edinburgh", 5421, "2011/04/25", 320800], ["Garrett Winters", "Accountant", "Tokyo", 8422, "2011/07/25", 170750], ["Ashton Cox", "Junior Technical Author", "San Francisco", 1562, "2009/01/12", 86000], ["Cedric Kelly", "Senior Javascript Developer", "Edinburgh", 6224, "2012/03/29", 433060], ["Airi Satou", "Accountant", "Tokyo", 5407, "2008/11/28", 162700], ["Brielle Williamson", "Integration Specialist", "New York", 4804, "2012/12/02", 372000], ["Herrod Chandler", "Sales Assistant", "San Francisco", 9608, "2012/08/06", 137500], ["Rhona Davidson", "Integration Specialist", "Tokyo", 6200, "2010/10/14", 327900], ["Colleen Hurst", "Javascript Developer", "San Francisco", 2360, "2009/09/15", 205500], ["Sonya Frost", "Software Engineer", "Edinburgh", 1667, "2008/12/13", 103600], ["Jena Gaines", "Office Manager", "London", 3814, "2008/12/19", 90560], ["Quinn Flynn", "Support Lead", "Edinburgh", 9497, "2013/03/03", 342000], ["Charde Marshall", "Regional Director", "San Francisco", 6741, "2008/10/16", 470600], ["Haley Kennedy", "Senior Marketing Designer", "London", 3597, "2012/12/18", 313500], ["Tatyana Fitzpatrick", "Regional Director", "London", 1965, "2010/03/17", 385750], ["Michael Silva", "Marketing Designer", "London", 1581, "2012/11/27", 198500], ["Paul Byrd", "Chief Financial Officer (CFO)", "New York", 3059, "2010/06/09", 725000], ["Gloria Little", "Systems Administrator", "New York", 1721, "2009/04/10", 237500], ["Bradley Greer", "Software Engineer", "London", 2558, "2012/10/13", 132000], ["Dai Rios", "Personnel Lead", "Edinburgh", 2290, "2012/09/26", 217500], ["Jenette Caldwell", "Development Lead", "New York", 1937, "2011/09/03", 345000], ["Yuri Berry", "Chief Marketing Officer (CMO)", "New York", 6154, "2009/06/25", 675000], ["Caesar Vance", "Pre-Sales Support", "New York", 8330, "2011/12/12", 106450], ["Doris Wilder", "Sales Assistant", "Sidney", 3023, "2010/09/20", 85600], ["Angelica Ramos", "Chief Executive Officer (CEO)", "London", 5797, "2009/10/09", 1200000], ["Gavin Joyce", "Developer", "Edinburgh", 8822, "2010/12/22", 92575], ["Jennifer Chang", "Regional Director", "Singapore", 9239, "2010/11/14", 357650], ["Brenden Wagner", "Software Engineer", "San Francisco", 1314, "2011/06/07", 206850], ["Fiona Green", "Chief Operating Officer (COO)", "San Francisco", 2947, "2010/03/11", 850000], ["Shou Itou", "Regional Marketing", "Tokyo", 8899, "2011/08/14", 163000], ["Michelle House", "Integration Specialist", "Sidney", 2769, "2011/06/02", 95400], ["Suki Burks", "Developer", "London", 6832, "2009/10/22", 114500], ["Prescott Bartlett", "Technical Author", "London", 3606, "2011/05/07", 145000], ["Gavin Cortez", "Team Leader", "San Francisco", 2860, "2008/10/26", 235500], ["Martena Mccray", "Post-Sales support", "Edinburgh", 8240, "2011/03/09", 324050], ["Unity Butler", "Marketing Designer", "San Francisco", 5384, "2009/12/09", 85675], ["Howard Hatfield", "Office Manager", "San Francisco", 7031, "2008/12/16", 164500], ["Hope Fuentes", "Secretary", "San Francisco", 6318, "2010/02/12", 109850], ["Vivian Harrell", "Financial Controller", "San Francisco", 9422, "2009/02/14", 452500], ["Timothy Mooney", "Office Manager", "London", 7580, "2008/12/11", 136200], ["Jackson Bradshaw", "Director", "New York", 1042, "2008/09/26", 645750], ["Olivia Liang", "Support Engineer", "Singapore", 2120, "2011/02/03", 234500], ["Bruno Nash", "Software Engineer", "London", 6222, "2011/05/03", 163500], ["Sakura Yamamoto", "Support Engineer", "Tokyo", 9383, "2009/08/19", 139575], ["Thor Walton", "Developer", "New York", 8327, "2013/08/11", 98540], ["Finn Camacho", "Support Engineer", "San Francisco", 2927, "2009/07/07", 87500], ["Serge Baldwin", "Data Coordinator", "Singapore", 8352, "2012/04/09", 138575], ["Zenaida Frank", "Software Engineer", "New York", 7439, "2010/01/04", 125250], ["Zorita Serrano", "Software Engineer", "San Francisco", 4389, "2012/06/01", 115000], ["Jennifer Acosta", "Junior Javascript Developer", "Edinburgh", 3431, "2013/02/01", 75650], ["Cara Stevens", "Sales Assistant", "New York", 3990, "2011/12/06", 145600], ["Hermione Butler", "Regional Director", "London", 1016, "2011/03/21", 356250], ["Lael Greer", "Systems Administrator", "London", 6733, "2009/02/27", 103500], ["Jonas Alexander", "Developer", "San Francisco", 8196, "2010/07/14", 86500], ["Shad Decker", "Regional Director", "Edinburgh", 6373, "2008/11/13", 183000], ["Michael Bruce", "Javascript Developer", "Singapore", 5384, "2011/06/27", 183000], ["Donna Snider", "Customer Support", "New York", 4226, "2011/01/25", 112000], ["Donna Snider", "Customer Support", "LOS Angeles", 42.67, "2011/01/25", 112000]];

if (largeData) {
for (let i = 0; i < 10; i++) {
data = data.concat(data);
}
}

// data = data.slice(1, 3)
}

function makeDatatable(treeView = false) {
console.log('No of Rows:', data.length)

const start = performance.now();
var datatable = new DataTable('section', {
checkboxColumn: true,
serialNoColumn: true,
layout: 'fluid',
columns,
data,
inlineFilters: true,
dynamicRowHeight: true,
treeView: treeView,
showTotalRow: true,
// direction: 'rtl',
// language: 'myLang',
// translations: {
// myLang: {
// "Sort Ascending": "Sort low to high",
// "{count} cells copied": {
// "1": "1 cell was copied",
// "2": "2 cells were copied",
// "default": "Many cells were copied"
// }
// }
// },
// filterRows(keyword, cells, colIndex) {
// return cells
// .filter(cell => cell.content.includes(keyword))
// .map(cell => cell.rowIndex);
// },
getEditor(colIndex, rowIndex, value, parent) {
// editing obj only for date field
if (colIndex != 6) return;

const $input = document.createElement('input');
$input.type = 'date';
parent.appendChild($input);

const parse = value => value.replace(/\//g, '-');
const format = value => value.replace(/\-/g, '/');

return {
initValue(value) {
$input.focus();
$input.value = parse(value);
},
setValue(value) {
$input.value = parse(value);
},
getValue() {
return format($input.value);
}
}
},
hooks: {
columnTotal(columnValues, cell) {
if (cell.colIndex === 5) {
// calculated average for 5th column
const sum = columnValues.reduce((acc, value) => {
if (typeof value === 'number') {
return acc + value
}
return acc
}, 0);
return sum / columnValues.length
}
if (cell.colIndex === 2) {
return 'Total'
}
}
}
});
console.log(performance.now() - start);

window.datatable = datatable;
}

window.switchToTreeView = function () {
datatable.destroy();
buildTreeData();
makeDatatable(true);
}

buildData();
makeDatatable();
})

function formatMoney(amount, decimalCount = 2, decimal = ".", thousands = ",") {
try {
decimalCount = Math.abs(decimalCount);
decimalCount = isNaN(decimalCount) ? 2 : decimalCount;

const negativeSign = amount < 0 ? "-" : "";

let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString();
let j = (i.length > 3) ? i.length % 3 : 0;

return negativeSign + (j ? i.substr(0, j) + thousands : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands) + (decimalCount ? decimal + Math.abs(amount - i).toFixed(decimalCount).slice(2) : "");
} catch (e) {
console.log(e)
}
};
</script>
</body>

</html>

+ 78
- 0
package.json 查看文件

@@ -0,0 +1,78 @@
{
"name": "xhiveframework-datatable",
"version": "0.0.0-development",
"description": "A modern datatable library for the web",
"main": "dist/xhiveframework-datatable.cjs.js",
"unpkg": "dist/xhiveframework-datatable.min.js",
"jsdelivr": "dist/xhiveframework-datatable.min.js",
"scripts": {
"start": "yarn run dev",
"build": "rollup -c && NODE_ENV=production rollup -c",
"dev": "rollup -c -w",
"cy:server": "http-server -p 8989",
"cy:open": "cypress open",
"cy:run": "cypress run",
"test": "start-server-and-test cy:server http://localhost:8989 cy:run",
"test-local": "start-server-and-test cy:server http://localhost:8989 cy:open",
"travis-deploy-once": "travis-deploy-once",
"semantic-release": "semantic-release",
"lint": "eslint src",
"lint-and-build": "yarn lint && yarn build",
"commit": "npx git-cz"
},
"files": [
"dist",
"src"
],
"devDependencies": {
"autoprefixer": "^9.0.0",
"chai": "3.5.0",
"cypress": "^9.2.0",
"cz-conventional-changelog": "^2.1.0",
"deepmerge": "^2.0.1",
"eslint": "^5.0.1",
"eslint-config-airbnb": "^16.1.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.11.0",
"http-server": "^0.11.1",
"mocha": "3.3.0",
"postcss-custom-properties": "^7.0.0",
"postcss-nested": "^3.0.0",
"rollup": "^0.59.4",
"rollup-plugin-commonjs": "^8.3.0",
"rollup-plugin-eslint": "^4.0.0",
"rollup-plugin-json": "^2.3.0",
"rollup-plugin-node-resolve": "^3.0.3",
"rollup-plugin-postcss": "^1.2.8",
"rollup-plugin-uglify-es": "^0.0.1",
"semantic-release": "^17.1.1",
"start-server-and-test": "^1.4.1",
"travis-deploy-once": "^5.0.1"
},
"repository": {
"type": "git",
"url": "https://github.com/xhiveframework/datatable.git"
},
"keywords": [
"datatable",
"data",
"grid",
"table"
],
"author": "Faris Ansari",
"license": "MIT",
"bugs": {
"url": "https://github.com/xhiveframework/datatable/issues"
},
"homepage": "https://xhiveframework.com/datatable",
"dependencies": {
"hyperlist": "^1.0.0-beta",
"lodash": "^4.17.5",
"sortablejs": "^1.7.0"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}

+ 94
- 0
rollup.config.js 查看文件

@@ -0,0 +1,94 @@
import json from 'rollup-plugin-json';
import uglify from 'rollup-plugin-uglify-es';
import nodeResolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import postcss from 'rollup-plugin-postcss';
import nested from 'postcss-nested';
import customProperties from 'postcss-custom-properties';
import autoprefixer from 'autoprefixer';
import eslint from 'rollup-plugin-eslint';
import merge from 'deepmerge';

const production = process.env.NODE_ENV === 'production';

const baseJS = {
input: 'src/index.js',
output: {
file: '',
globals: {
sortablejs: 'Sortable',
'clusterize.js': 'Clusterize'
}
},
plugins: [
json(),
eslint({
exclude: '**/*.json'
}),
nodeResolve(),
commonjs()
],
external: ['sortablejs', 'clusterize.js']
};

const baseCSS = {
input: 'src/style.css',
output: {
file: ''
},
plugins: [
postcss({
extract: true,
minimize: production,
plugins: [
customProperties(),
nested(),
autoprefixer()
]
})
]
};

const devIIFE = merge(baseJS, {
output: {
file: 'dist/xhiveframework-datatable.js',
format: 'iife',
name: 'DataTable'
}
});

const devCjs = merge(baseJS, {
output: {
file: 'dist/xhiveframework-datatable.cjs.js',
format: 'cjs'
}
});

const devCSS = merge(baseCSS, {
output: {
file: 'dist/xhiveframework-datatable.css',
format: 'cjs'
}
});

// production
const prodIIFE = merge(devIIFE, {
output: {
file: 'dist/xhiveframework-datatable.min.js'
},
plugins: [
uglify()
]
});

const prodCSS = merge(devCSS, {
output: {
file: 'dist/xhiveframework-datatable.min.css'
}
});

const developmentAssets = [devIIFE, devCjs, devCSS];
const productionAssets = [prodIIFE, prodCSS];
const assets = production ? productionAssets : developmentAssets;

export default assets;

+ 145
- 0
src/body-renderer.js 查看文件

@@ -0,0 +1,145 @@
import HyperList from 'hyperlist';

export default class BodyRenderer {
constructor(instance) {
this.instance = instance;
this.options = instance.options;
this.datamanager = instance.datamanager;
this.rowmanager = instance.rowmanager;
this.cellmanager = instance.cellmanager;
this.bodyScrollable = instance.bodyScrollable;
this.footer = this.instance.footer;
this.log = instance.log;
}

renderRows(rows) {
this.visibleRows = rows;
this.visibleRowIndices = rows.map(row => row.meta.rowIndex);

if (rows.length === 0) {
this.bodyScrollable.innerHTML = this.getNoDataHTML();
return;
}

const rowViewOrder = this.datamanager.rowViewOrder.map(index => {
if (this.visibleRowIndices.includes(index)) {
return index;
}
return null;
}).filter(index => index !== null);

const computedStyle = getComputedStyle(this.bodyScrollable);

let config = {
width: computedStyle.width,
height: computedStyle.height,
itemHeight: this.options.cellHeight,
total: rows.length,
generate: (index) => {
const el = document.createElement('div');
const rowIndex = rowViewOrder[index];
const row = this.datamanager.getRow(rowIndex);
const rowHTML = this.rowmanager.getRowHTML(row, row.meta);
el.innerHTML = rowHTML;
return el.children[0];
},
afterRender: () => {
this.restoreState();
}
};

if (!this.hyperlist) {
this.hyperlist = new HyperList(this.bodyScrollable, config);
} else {
this.hyperlist.refresh(this.bodyScrollable, config);
}

this.renderFooter();
}

render() {
const rows = this.datamanager.getRowsForView();
this.renderRows(rows);
// setDimensions requires atleast 1 row to exist in dom
this.instance.setDimensions();
}

renderFooter() {
if (!this.options.showTotalRow) return;

const totalRow = this.getTotalRow();
let html = this.rowmanager.getRowHTML(totalRow, { isTotalRow: 1, rowIndex: 'totalRow' });

this.footer.innerHTML = html;
}

getTotalRow() {
const columns = this.datamanager.getColumns();
const totalRowTemplate = columns.map(col => {
let content = null;
if (['_rowIndex', '_checkbox'].includes(col.id)) {
content = '';
}
return {
content,
isTotalRow: 1,
colIndex: col.colIndex,
column: col
};
});

const totalRow = totalRowTemplate.map((cell, i) => {
if (cell.content === '') return cell;

if (this.options.hooks.columnTotal) {
const columnValues = this.visibleRows.map(row => row[i].content);
const result = this.options.hooks.columnTotal.call(this.instance, columnValues, cell);
if (result != null) {
cell.content = result;
return cell;
}
}

cell.content = this.visibleRows.reduce((acc, prevRow) => {
const prevCell = prevRow[i];
if (typeof prevCell.content === 'number') {
if (acc == null) acc = 0;
return acc + prevCell.content;
}
return acc;
}, cell.content);

return cell;
});

return totalRow;
}

restoreState() {
this.rowmanager.highlightCheckedRows();
this.cellmanager.selectAreaOnClusterChanged();
this.cellmanager.focusCellOnClusterChanged();
}

showToastMessage(message, hideAfter) {
this.instance.toastMessage.innerHTML = this.getToastMessageHTML(message);

if (hideAfter) {
setTimeout(() => {
this.clearToastMessage();
}, hideAfter * 1000);
}
}

clearToastMessage() {
this.instance.toastMessage.innerHTML = '';
}

getNoDataHTML() {
return `<div class="dt-scrollable__no-data">${this.options.noDataMessage}</div>`;
}

getToastMessageHTML(message) {
return `<span class="dt-toast__message">${message}</span>`;
}
}

+ 895
- 0
src/cellmanager.js 查看文件

@@ -0,0 +1,895 @@
import {
copyTextToClipboard,
makeDataAttributeString,
throttle,
linkProperties
} from './utils';
import $ from './dom';
import icons from './icons';

export default class CellManager {
constructor(instance) {
this.instance = instance;
linkProperties(this, this.instance, [
'wrapper',
'options',
'style',
'header',
'bodyScrollable',
'columnmanager',
'rowmanager',
'datamanager',
'keyboard'
]);

this.bindEvents();
}

bindEvents() {
this.bindFocusCell();
this.bindEditCell();
this.bindKeyboardSelection();
this.bindCopyCellContents();
this.bindMouseEvents();
this.bindTreeEvents();
}

bindFocusCell() {
this.bindKeyboardNav();
}

bindEditCell() {
this.$editingCell = null;

$.on(this.bodyScrollable, 'dblclick', '.dt-cell', (e, cell) => {
this.activateEditing(cell);
});

this.keyboard.on('enter', () => {
if (this.$focusedCell && !this.$editingCell) {
// enter keypress on focused cell
this.activateEditing(this.$focusedCell);
} else if (this.$editingCell) {
// enter keypress on editing cell
this.deactivateEditing();
}
});
}

bindKeyboardNav() {
const focusLastCell = (direction) => {
if (!this.$focusedCell || this.$editingCell) {
return false;
}

let $cell = this.$focusedCell;
const {
rowIndex,
colIndex
} = $.data($cell);

if (direction === 'left') {
$cell = this.getLeftMostCell$(rowIndex);
} else if (direction === 'right') {
$cell = this.getRightMostCell$(rowIndex);
} else if (direction === 'up') {
$cell = this.getTopMostCell$(colIndex);
} else if (direction === 'down') {
$cell = this.getBottomMostCell$(colIndex);
}

this.focusCell($cell);
return true;
};

['left', 'right', 'up', 'down', 'tab', 'shift+tab']
.map(direction => this.keyboard.on(direction, () => this.focusCellInDirection(direction)));

['left', 'right', 'up', 'down']
.map(direction => this.keyboard.on(`ctrl+${direction}`, () => focusLastCell(direction)));

this.keyboard.on('esc', () => {
this.deactivateEditing(false);
this.columnmanager.toggleFilter(false);
});

if (this.options.inlineFilters) {
this.keyboard.on('ctrl+f', (e) => {
const $cell = $.closest('.dt-cell', e.target);
const { colIndex } = $.data($cell);

this.activateFilter(colIndex);
return true;
});

$.on(this.header, 'focusin', '.dt-filter', () => {
this.unfocusCell(this.$focusedCell);
});
}
}

bindKeyboardSelection() {
const getNextSelectionCursor = (direction) => {
let $selectionCursor = this.getSelectionCursor();

if (direction === 'left') {
$selectionCursor = this.getLeftCell$($selectionCursor);
} else if (direction === 'right') {
$selectionCursor = this.getRightCell$($selectionCursor);
} else if (direction === 'up') {
$selectionCursor = this.getAboveCell$($selectionCursor);
} else if (direction === 'down') {
$selectionCursor = this.getBelowCell$($selectionCursor);
}

return $selectionCursor;
};

['left', 'right', 'up', 'down']
.map(direction =>
this.keyboard.on(`shift+${direction}`, () => this.selectArea(getNextSelectionCursor(direction))));
}

bindCopyCellContents() {
this.keyboard.on('ctrl+c', () => {
const noOfCellsCopied = this.copyCellContents(this.$focusedCell, this.$selectionCursor);
const message = this.instance.translate('{count} cells copied', {
count: noOfCellsCopied
});

if (noOfCellsCopied) {
this.instance.showToastMessage(message, 2);
}
});

if (this.options.pasteFromClipboard) {
this.keyboard.on('ctrl+v', (e) => {
// hack
// https://stackoverflow.com/a/2177059/5353542
this.instance.pasteTarget.focus();

setTimeout(() => {
const data = this.instance.pasteTarget.value;
this.instance.pasteTarget.value = '';
this.pasteContentInCell(data);
}, 10);

return false;
});
}
}

bindMouseEvents() {
let mouseDown = null;

$.on(this.bodyScrollable, 'mousedown', '.dt-cell', (e) => {
mouseDown = true;
this.focusCell($(e.delegatedTarget));
});

$.on(this.bodyScrollable, 'mouseup', () => {
mouseDown = false;
});

const selectArea = (e) => {
if (!mouseDown) return;
this.selectArea($(e.delegatedTarget));
};

$.on(this.bodyScrollable, 'mousemove', '.dt-cell', throttle(selectArea, 50));
}

bindTreeEvents() {
$.on(this.bodyScrollable, 'click', '.dt-tree-node__toggle', (e, $toggle) => {
const $cell = $.closest('.dt-cell', $toggle);
const { rowIndex } = $.data($cell);

if ($cell.classList.contains('dt-cell--tree-close')) {
this.rowmanager.openSingleNode(rowIndex);
} else {
this.rowmanager.closeSingleNode(rowIndex);
}
});
}

focusCell($cell, {
skipClearSelection = 0,
skipDOMFocus = 0,
skipScrollToCell = 0
} = {}) {
if (!$cell) return;

// don't focus if already editing cell
if ($cell === this.$editingCell) return;

const {
colIndex,
isHeader
} = $.data($cell);
if (isHeader) {
return;
}

const column = this.columnmanager.getColumn(colIndex);
if (column.focusable === false) {
return;
}

if (!skipScrollToCell) {
this.scrollToCell($cell);
}

this.deactivateEditing();
if (!skipClearSelection) {
this.clearSelection();
}

if (this.$focusedCell) {
this.$focusedCell.classList.remove('dt-cell--focus');
}

this.$focusedCell = $cell;
$cell.classList.add('dt-cell--focus');

if (!skipDOMFocus) {
// so that keyboard nav works
$cell.focus();
}

this.highlightRowColumnHeader($cell);
}

unfocusCell($cell) {
if (!$cell) return;

// remove cell border
$cell.classList.remove('dt-cell--focus');
this.$focusedCell = null;

// reset header background
if (this.lastHeaders) {
this.lastHeaders.forEach(header => header && header.classList.remove('dt-cell--highlight'));
}
}

highlightRowColumnHeader($cell) {
const {
colIndex,
rowIndex
} = $.data($cell);

const srNoColIndex = this.datamanager.getColumnIndexById('_rowIndex');
const colHeaderSelector = `.dt-cell--header-${colIndex}`;
const rowHeaderSelector = `.dt-cell--${srNoColIndex}-${rowIndex}`;

if (this.lastHeaders) {
this.lastHeaders.forEach(header => header && header.classList.remove('dt-cell--highlight'));
}

const colHeader = $(colHeaderSelector, this.wrapper);
const rowHeader = $(rowHeaderSelector, this.wrapper);

this.lastHeaders = [colHeader, rowHeader];
this.lastHeaders.forEach(header => header && header.classList.add('dt-cell--highlight'));
}

selectAreaOnClusterChanged() {
if (!(this.$focusedCell && this.$selectionCursor)) return;
const {
colIndex,
rowIndex
} = $.data(this.$selectionCursor);
const $cell = this.getCell$(colIndex, rowIndex);

if (!$cell || $cell === this.$selectionCursor) return;

// selectArea needs $focusedCell
const fCell = $.data(this.$focusedCell);
this.$focusedCell = this.getCell$(fCell.colIndex, fCell.rowIndex);

this.selectArea($cell);
}

focusCellOnClusterChanged() {
if (!this.$focusedCell) return;

const {
colIndex,
rowIndex
} = $.data(this.$focusedCell);
const $cell = this.getCell$(colIndex, rowIndex);

if (!$cell) return;
// this function is called after hyperlist renders the rows after scroll,
// focusCell calls clearSelection which resets the area selection
// so a flag to skip it
// we also skip DOM focus and scroll to cell
// because it fights with the user scroll
this.focusCell($cell, {
skipClearSelection: 1,
skipDOMFocus: 1,
skipScrollToCell: 1
});
}

selectArea($selectionCursor) {
if (!this.$focusedCell) return;

if (this._selectArea(this.$focusedCell, $selectionCursor)) {
// valid selection
this.$selectionCursor = $selectionCursor;
}
}

_selectArea($cell1, $cell2) {
if ($cell1 === $cell2) return false;

const cells = this.getCellsInRange($cell1, $cell2);
if (!cells) return false;

this.clearSelection();
this._selectedCells = cells.map(index => this.getCell$(...index));
requestAnimationFrame(() => {
this._selectedCells.map($cell => $cell.classList.add('dt-cell--highlight'));
});
return true;
}

getCellsInRange($cell1, $cell2) {
let colIndex1, rowIndex1, colIndex2, rowIndex2;

if (typeof $cell1 === 'number') {
[colIndex1, rowIndex1, colIndex2, rowIndex2] = arguments;
} else
if (typeof $cell1 === 'object') {
if (!($cell1 && $cell2)) {
return false;
}

const cell1 = $.data($cell1);
const cell2 = $.data($cell2);

colIndex1 = +cell1.colIndex;
rowIndex1 = +cell1.rowIndex;
colIndex2 = +cell2.colIndex;
rowIndex2 = +cell2.rowIndex;
}

if (rowIndex1 > rowIndex2) {
[rowIndex1, rowIndex2] = [rowIndex2, rowIndex1];
}

if (colIndex1 > colIndex2) {
[colIndex1, colIndex2] = [colIndex2, colIndex1];
}

if (this.isStandardCell(colIndex1) || this.isStandardCell(colIndex2)) {
return false;
}

const cells = [];
let colIndex = colIndex1;
let rowIndex = rowIndex1;
const rowIndices = [];

while (rowIndex <= rowIndex2) {
rowIndices.push(rowIndex);
rowIndex += 1;
}

rowIndices.map((rowIndex) => {
while (colIndex <= colIndex2) {
cells.push([colIndex, rowIndex]);
colIndex++;
}
colIndex = colIndex1;
});

return cells;
}

clearSelection() {
(this._selectedCells || [])
.forEach($cell => $cell.classList.remove('dt-cell--highlight'));

this._selectedCells = [];
this.$selectionCursor = null;
}

getSelectionCursor() {
return this.$selectionCursor || this.$focusedCell;
}

activateEditing($cell) {
this.focusCell($cell);
const {
rowIndex,
colIndex
} = $.data($cell);

const col = this.columnmanager.getColumn(colIndex);
if (col && (col.editable === false || col.focusable === false)) {
return;
}

const cell = this.getCell(colIndex, rowIndex);
if (cell && cell.editable === false) {
return;
}

if (this.$editingCell) {
const {
_rowIndex,
_colIndex
} = $.data(this.$editingCell);

if (rowIndex === _rowIndex && colIndex === _colIndex) {
// editing the same cell
return;
}
}

this.$editingCell = $cell;
$cell.classList.add('dt-cell--editing');

const $editCell = $('.dt-cell__edit', $cell);
$editCell.innerHTML = '';

const editor = this.getEditor(colIndex, rowIndex, cell.content, $editCell);

if (editor) {
this.currentCellEditor = editor;
// initialize editing input with cell value
editor.initValue(cell.content, rowIndex, col);
}
}

deactivateEditing(submitValue = true) {
if (submitValue) {
this.submitEditing();
}
// keep focus on the cell so that keyboard navigation works
if (this.$focusedCell) this.$focusedCell.focus();

if (!this.$editingCell) return;
this.$editingCell.classList.remove('dt-cell--editing');
this.$editingCell = null;
}

getEditor(colIndex, rowIndex, value, parent) {
const column = this.datamanager.getColumn(colIndex);
const row = this.datamanager.getRow(rowIndex);
const data = this.datamanager.getData(rowIndex);
let editor = this.options.getEditor ?
this.options.getEditor(colIndex, rowIndex, value, parent, column, row, data) :
this.getDefaultEditor(parent);

if (editor === false) {
// explicitly returned false
return false;
}
if (editor === undefined) {
// didn't return editor, fallback to default
editor = this.getDefaultEditor(parent);
}

return editor;
}

getDefaultEditor(parent) {
const $input = $.create('input', {
class: 'dt-input',
type: 'text',
inside: parent
});

return {
initValue(value) {
$input.focus();
$input.value = value;
},
getValue() {
return $input.value;
},
setValue(value) {
$input.value = value;
}
};
}

submitEditing() {
let promise = Promise.resolve();
if (!this.$editingCell) return promise;

const $cell = this.$editingCell;
const {
rowIndex,
colIndex
} = $.data($cell);
const col = this.datamanager.getColumn(colIndex);

if ($cell) {
const editor = this.currentCellEditor;

if (editor) {
let valuePromise = editor.getValue();

// convert to stubbed Promise
if (!valuePromise.then) {
valuePromise = Promise.resolve(valuePromise);
}

promise = valuePromise.then((value) => {
const oldValue = this.getCell(colIndex, rowIndex).content;

if (oldValue === value) return false;

const done = editor.setValue(value, rowIndex, col);

// update cell immediately
this.updateCell(colIndex, rowIndex, value, true);
$cell.focus();

if (done && done.then) {
// revert to oldValue if promise fails
done.catch((e) => {
console.log(e);
this.updateCell(colIndex, rowIndex, oldValue);
});
}
return done;
});
}
}

this.currentCellEditor = null;
return promise;
}

copyCellContents($cell1, $cell2) {
if (!$cell2 && $cell1) {
// copy only focusedCell
const {
colIndex,
rowIndex
} = $.data($cell1);
const cell = this.getCell(colIndex, rowIndex);
copyTextToClipboard(cell.content);
return 1;
}
const cells = this.getCellsInRange($cell1, $cell2);

if (!cells) return 0;

const rows = cells
// get cell objects
.map(index => this.getCell(...index))
// convert to array of rows
.reduce((acc, curr) => {
const rowIndex = curr.rowIndex;

acc[rowIndex] = acc[rowIndex] || [];
acc[rowIndex].push(curr.content);

return acc;
}, []);

const values = rows
// join values by tab
.map(row => row.join('\t'))
// join rows by newline
.join('\n');

copyTextToClipboard(values);

// return no of cells copied
return rows.reduce((total, row) => total + row.length, 0);
}

pasteContentInCell(data) {
if (!this.$focusedCell) return;

const matrix = data
.split('\n')
.map(row => row.split('\t'))
.filter(row => row.length && row.every(it => it));

let { colIndex, rowIndex } = $.data(this.$focusedCell);

let focusedCell = {
colIndex: +colIndex,
rowIndex: +rowIndex
};

matrix.forEach((row, i) => {
let rowIndex = i + focusedCell.rowIndex;
row.forEach((cell, j) => {
let colIndex = j + focusedCell.colIndex;
this.updateCell(colIndex, rowIndex, cell, true);
});
});
}

activateFilter(colIndex) {
this.columnmanager.toggleFilter();
this.columnmanager.focusFilter(colIndex);

if (!this.columnmanager.isFilterShown) {
// put focus back on cell
this.$focusedCell && this.$focusedCell.focus();
}
}

updateCell(colIndex, rowIndex, value, refreshHtml = false) {
const cell = this.datamanager.updateCell(colIndex, rowIndex, {
content: value
});
this.refreshCell(cell, refreshHtml);
}

refreshCell(cell, refreshHtml = false) {
const $cell = $(this.selector(cell.colIndex, cell.rowIndex), this.bodyScrollable);
$cell.innerHTML = this.getCellContent(cell, refreshHtml);
}

toggleTreeButton(rowIndex, flag) {
const colIndex = this.columnmanager.getFirstColumnIndex();
const $cell = this.getCell$(colIndex, rowIndex);
if ($cell) {
$cell.classList[flag ? 'remove' : 'add']('dt-cell--tree-close');
}
}

isStandardCell(colIndex) {
// Standard cells are in Sr. No and Checkbox column
return colIndex < this.columnmanager.getFirstColumnIndex();
}

focusCellInDirection(direction) {
if (!this.$focusedCell || (this.$editingCell && ['left', 'right', 'up', 'down'].includes(direction))) {
return false;
} else if (this.$editingCell && ['tab', 'shift+tab'].includes(direction)) {
this.deactivateEditing();
}

let $cell = this.$focusedCell;

if (direction === 'left' || direction === 'shift+tab') {
$cell = this.getLeftCell$($cell);
} else if (direction === 'right' || direction === 'tab') {
$cell = this.getRightCell$($cell);
} else if (direction === 'up') {
$cell = this.getAboveCell$($cell);
} else if (direction === 'down') {
$cell = this.getBelowCell$($cell);
}

if (!$cell) {
return false;
}

const {
colIndex
} = $.data($cell);
const column = this.columnmanager.getColumn(colIndex);

if (!column.focusable) {
let $prevFocusedCell = this.$focusedCell;
this.unfocusCell($prevFocusedCell);
this.$focusedCell = $cell;
let ret = this.focusCellInDirection(direction);
if (!ret) {
this.focusCell($prevFocusedCell);
}
return ret;
}

this.focusCell($cell);
return true;
}

getCell$(colIndex, rowIndex) {
return $(this.selector(colIndex, rowIndex), this.bodyScrollable);
}

getAboveCell$($cell) {
const {
colIndex
} = $.data($cell);

let $aboveRow = $cell.parentElement.previousElementSibling;
while ($aboveRow && $aboveRow.classList.contains('dt-row--hide')) {
$aboveRow = $aboveRow.previousElementSibling;
}

if (!$aboveRow) return $cell;
return $(`.dt-cell--col-${colIndex}`, $aboveRow);
}

getBelowCell$($cell) {
const {
colIndex
} = $.data($cell);

let $belowRow = $cell.parentElement.nextElementSibling;
while ($belowRow && $belowRow.classList.contains('dt-row--hide')) {
$belowRow = $belowRow.nextElementSibling;
}

if (!$belowRow) return $cell;
return $(`.dt-cell--col-${colIndex}`, $belowRow);
}

getLeftCell$($cell) {
return $cell.previousElementSibling;
}

getRightCell$($cell) {
return $cell.nextElementSibling;
}

getLeftMostCell$(rowIndex) {
return this.getCell$(this.columnmanager.getFirstColumnIndex(), rowIndex);
}

getRightMostCell$(rowIndex) {
return this.getCell$(this.columnmanager.getLastColumnIndex(), rowIndex);
}

getTopMostCell$(colIndex) {
return this.getCell$(colIndex, this.rowmanager.getFirstRowIndex());
}

getBottomMostCell$(colIndex) {
return this.getCell$(colIndex, this.rowmanager.getLastRowIndex());
}

getCell(colIndex, rowIndex) {
return this.instance.datamanager.getCell(colIndex, rowIndex);
}

getRowHeight() {
return $.style($('.dt-row', this.bodyScrollable), 'height');
}

scrollToCell($cell) {
if ($.inViewport($cell, this.bodyScrollable)) return false;

const {
rowIndex
} = $.data($cell);
this.rowmanager.scrollToRow(rowIndex);
return false;
}

getRowCountPerPage() {
return Math.ceil(this.instance.getViewportHeight() / this.getRowHeight());
}

getCellHTML(cell) {
const {
rowIndex,
colIndex,
isHeader,
isFilter,
isTotalRow
} = cell;
const dataAttr = makeDataAttributeString({
rowIndex,
colIndex,
isHeader,
isFilter,
isTotalRow
});

const row = this.datamanager.getRow(rowIndex);

const isBodyCell = !(isHeader || isFilter || isTotalRow);

const className = [
'dt-cell',
'dt-cell--col-' + colIndex,
isBodyCell ? `dt-cell--${colIndex}-${rowIndex}` : '',
isBodyCell ? 'dt-cell--row-' + rowIndex : '',
isHeader ? 'dt-cell--header' : '',
isHeader ? `dt-cell--header-${colIndex}` : '',
isFilter ? 'dt-cell--filter' : '',
isBodyCell && (row && row.meta.isTreeNodeClose) ? 'dt-cell--tree-close' : ''
].join(' ');

return `
<div class="${className}" ${dataAttr} tabindex="0">
${this.getCellContent(cell)}
</div>
`;
}

getCellContent(cell, refreshHtml = false) {
const {
isHeader,
isFilter,
colIndex
} = cell;

const editable = !isHeader && cell.editable !== false;
const editCellHTML = editable ? this.getEditCellHTML(colIndex) : '';

const sortable = isHeader && cell.sortable !== false;
const sortIndicator = sortable ?
`<span class="sort-indicator">
${this.options.sortIndicator[cell.sortOrder]}
</span>` :
'';

const resizable = isHeader && cell.resizable !== false;
const resizeColumn = resizable ? '<span class="dt-cell__resize-handle"></span>' : '';

const hasDropdown = isHeader && cell.dropdown !== false;
const dropdown = hasDropdown ? this.columnmanager.getDropdownHTML() : '';

let customFormatter = CellManager.getCustomCellFormatter(cell);
let contentHTML;
if (isHeader || isFilter || !customFormatter) {
contentHTML = cell.content;
} else {
if (!cell.html || refreshHtml) {
const row = this.datamanager.getRow(cell.rowIndex);
const data = this.datamanager.getData(cell.rowIndex);
contentHTML = customFormatter(cell.content, row, cell.column, data);
} else {
contentHTML = cell.html;
}
}

cell.html = contentHTML;

if (this.options.treeView && !(isHeader || isFilter) && cell.indent !== undefined) {
const nextRow = this.datamanager.getRow(cell.rowIndex + 1);
const addToggle = nextRow && nextRow.meta.indent > cell.indent;
const leftPadding = 20;
const unit = 'px';

// Add toggle and indent in the first column
const firstColumnIndex = this.datamanager.getColumnIndexById('_rowIndex') + 1;
if (firstColumnIndex === cell.colIndex) {
const padding = ((cell.indent || 0)) * leftPadding;
const toggleHTML = addToggle ?
`<span class="dt-tree-node__toggle" style="left: ${padding - leftPadding}${unit}">
<span class="icon-open">${icons.chevronDown}</span>
<span class="icon-close">${icons.chevronRight}</span>
</span>` : '';
contentHTML = `<span class="dt-tree-node" style="padding-left: ${padding}${unit}">
${toggleHTML}
<span>${contentHTML}</span>
</span>`;
}
}

const className = [
'dt-cell__content',
isHeader ? `dt-cell__content--header-${colIndex}` : `dt-cell__content--col-${colIndex}`
].join(' ');

return `
<div class="${className}">
${contentHTML}
${sortIndicator}
${resizeColumn}
${dropdown}
</div>
${editCellHTML}
`;
}

getEditCellHTML(colIndex) {
return `<div class="dt-cell__edit dt-cell__edit--col-${colIndex}"></div>`;
}

selector(colIndex, rowIndex) {
return `.dt-cell--${colIndex}-${rowIndex}`;
}

static getCustomCellFormatter(cell) {
return cell.format || (cell.column && cell.column.format) || null;
}
}

+ 455
- 0
src/columnmanager.js 查看文件

@@ -0,0 +1,455 @@
import $ from './dom';
import Sortable from 'sortablejs';
import {
linkProperties,
debounce
} from './utils';

export default class ColumnManager {
constructor(instance) {
this.instance = instance;

linkProperties(this, this.instance, [
'options',
'fireEvent',
'header',
'datamanager',
'cellmanager',
'style',
'wrapper',
'rowmanager',
'bodyScrollable',
'bodyRenderer'
]);

this.bindEvents();
}

renderHeader() {
this.header.innerHTML = '<div></div>';
this.refreshHeader();
}

refreshHeader() {
const columns = this.datamanager.getColumns();

// refresh html
$('div', this.header).innerHTML = this.getHeaderHTML(columns);

this.$filterRow = $('.dt-row-filter', this.header);
if (this.$filterRow) {
$.style(this.$filterRow, { display: 'none' });
}
// reset columnMap
this.$columnMap = [];
this.bindMoveColumn();
}

getHeaderHTML(columns) {
let html = this.rowmanager.getRowHTML(columns, {
isHeader: 1
});
if (this.options.inlineFilters) {
html += this.rowmanager.getRowHTML(columns, {
isFilter: 1
});
}
return html;
}

bindEvents() {
this.bindDropdown();
this.bindResizeColumn();
this.bindPerfectColumnWidth();
this.bindFilter();
}

bindDropdown() {
let toggleClass = '.dt-dropdown__toggle';
let dropdownClass = '.dt-dropdown__list';

// attach the dropdown list to container
this.instance.dropdownContainer.innerHTML = this.getDropdownListHTML();
this.$dropdownList = this.instance.dropdownContainer.firstElementChild;

$.on(this.header, 'click', toggleClass, e => {
this.openDropdown(e);
});

const deactivateDropdownOnBodyClick = (e) => {
const selector = [
toggleClass, toggleClass + ' *',
dropdownClass, dropdownClass + ' *'
].join(',');
if (e.target.matches(selector)) return;
deactivateDropdown();
};
$.on(document.body, 'click', deactivateDropdownOnBodyClick);
document.addEventListener('scroll', deactivateDropdown, true);

this.instance.on('onDestroy', () => {
$.off(document.body, 'click', deactivateDropdownOnBodyClick);
$.off(document, 'scroll', deactivateDropdown);
});

$.on(this.$dropdownList, 'click', '.dt-dropdown__list-item', (e, $item) => {
if (!this._dropdownActiveColIndex) return;
const dropdownItems = this.options.headerDropdown;
const { index } = $.data($item);
const colIndex = this._dropdownActiveColIndex;
let callback = dropdownItems[index].action;

callback && callback.call(this.instance, this.getColumn(colIndex));
this.hideDropdown();
});

const _this = this;
function deactivateDropdown(e) {
_this.hideDropdown();
}

this.hideDropdown();
}

openDropdown(e) {
if (!this._dropdownWidth) {
$.style(this.$dropdownList, { display: '' });
this._dropdownWidth = $.style(this.$dropdownList, 'width');
}
$.style(this.$dropdownList, {
display: '',
left: (e.clientX - this._dropdownWidth + 4) + 'px',
top: (e.clientY + 4) + 'px'
});
const $cell = $.closest('.dt-cell', e.target);
const { colIndex } = $.data($cell);
this._dropdownActiveColIndex = colIndex;
}

hideDropdown() {
$.style(this.$dropdownList, {
display: 'none'
});
this._dropdownActiveColIndex = null;
}

bindResizeColumn() {
let isDragging = false;
let $resizingCell, startWidth, startX;

$.on(this.header, 'mousedown', '.dt-cell .dt-cell__resize-handle', (e, $handle) => {
document.body.classList.add('dt-resize');
const $cell = $handle.parentNode.parentNode;
$resizingCell = $cell;
const {
colIndex
} = $.data($resizingCell);
const col = this.getColumn(colIndex);

if (col && col.resizable === false) {
return;
}

isDragging = true;
startWidth = $.style($('.dt-cell__content', $resizingCell), 'width');
startX = e.pageX;
});

const onMouseup = (e) => {
document.body.classList.remove('dt-resize');
if (!$resizingCell) return;
isDragging = false;

const {
colIndex
} = $.data($resizingCell);
this.setColumnWidth(colIndex);
this.style.setBodyStyle();
$resizingCell = null;
};
$.on(document.body, 'mouseup', onMouseup);
this.instance.on('onDestroy', () => {
$.off(document.body, 'mouseup', onMouseup);
});

const onMouseMove = (e) => {
if (!isDragging) return;
let delta = e.pageX - startX;
if (this.options.direction === 'rtl') {
delta = -1 * delta;
}
const finalWidth = startWidth + delta;
const {
colIndex
} = $.data($resizingCell);

let columnMinWidth = this.options.minimumColumnWidth;
if (columnMinWidth > finalWidth) {
// don't resize past 30 pixels
return;
}
this.datamanager.updateColumn(colIndex, {
width: finalWidth
});
this.setColumnHeaderWidth(colIndex);
};
$.on(document.body, 'mousemove', onMouseMove);
this.instance.on('onDestroy', () => {
$.off(document.body, 'mousemove', onMouseMove);
});
}

bindPerfectColumnWidth() {
$.on(this.header, 'dblclick', '.dt-cell .dt-cell__resize-handle', (e, $handle) => {
const $cell = $handle.parentNode.parentNode;
const { colIndex } = $.data($cell);

let longestCell = this.bodyRenderer.visibleRows
.map(d => d[colIndex])
.reduce((acc, curr) => acc.content.length > curr.content.length ? acc : curr);

let $longestCellHTML = this.cellmanager.getCellHTML(longestCell);
let $div = document.createElement('div');
$div.innerHTML = $longestCellHTML;
let cellText = $div.querySelector('.dt-cell__content').textContent;

let {
borderLeftWidth,
borderRightWidth,
paddingLeft,
paddingRight
} = $.getStyle(this.bodyScrollable.querySelector('.dt-cell__content'));

let padding = [borderLeftWidth, borderRightWidth, paddingLeft, paddingRight]
.map(parseFloat)
.reduce((sum, val) => sum + val);

let width = $.measureTextWidth(cellText) + padding;
this.datamanager.updateColumn(colIndex, { width });
this.setColumnHeaderWidth(colIndex);
this.setColumnWidth(colIndex);
});
}

bindMoveColumn() {
if (this.options.disableReorderColumn) return;

const $parent = $('.dt-row', this.header);

this.sortable = Sortable.create($parent, {
onEnd: (e) => {
const {
oldIndex,
newIndex
} = e;
const $draggedCell = e.item;
const {
colIndex
} = $.data($draggedCell);
if (+colIndex === newIndex) return;

this.switchColumn(oldIndex, newIndex);
},
preventOnFilter: false,
filter: '.dt-cell__resize-handle, .dt-dropdown',
chosenClass: 'dt-cell--dragging',
animation: 150
});
}

sortColumn(colIndex, nextSortOrder) {
this.instance.freeze();
this.sortRows(colIndex, nextSortOrder)
.then(() => {
this.refreshHeader();
return this.rowmanager.refreshRows();
})
.then(() => this.instance.unfreeze())
.then(() => {
this.fireEvent('onSortColumn', this.getColumn(colIndex));
});
}

removeColumn(colIndex) {
const removedCol = this.getColumn(colIndex);
this.instance.freeze();
this.datamanager.removeColumn(colIndex)
.then(() => {
this.refreshHeader();
return this.rowmanager.refreshRows();
})
.then(() => this.instance.unfreeze())
.then(() => {
this.fireEvent('onRemoveColumn', removedCol);
});
}

switchColumn(oldIndex, newIndex) {
this.instance.freeze();
this.datamanager.switchColumn(oldIndex, newIndex)
.then(() => {
this.refreshHeader();
return this.rowmanager.refreshRows();
})
.then(() => {
this.setColumnWidth(oldIndex);
this.setColumnWidth(newIndex);
this.instance.unfreeze();
})
.then(() => {
this.fireEvent('onSwitchColumn',
this.getColumn(oldIndex), this.getColumn(newIndex)
);
});
}

toggleFilter(flag) {
if (!this.options.inlineFilters) return;

let showFilter;
if (flag === undefined) {
showFilter = !this.isFilterShown;
} else {
showFilter = flag;
}

if (showFilter) {
$.style(this.$filterRow, { display: '' });
} else {
$.style(this.$filterRow, { display: 'none' });
}

this.isFilterShown = showFilter;
this.style.setBodyStyle();
}

focusFilter(colIndex) {
if (!this.isFilterShown) return;

const $filterInput = $(`.dt-cell--col-${colIndex} .dt-filter`, this.$filterRow);
$filterInput.focus();
}

bindFilter() {
if (!this.options.inlineFilters) return;
const handler = e => {
this.applyFilter(this.getAppliedFilters());
};
$.on(this.header, 'keydown', '.dt-filter', debounce(handler, 300));
}

applyFilter(filters) {
this.datamanager.filterRows(filters)
.then(({
rowsToShow
}) => {
this.rowmanager.showRows(rowsToShow);
});
}

getAppliedFilters() {
const filters = {};
$.each('.dt-filter', this.header).map((input) => {
const value = input.value;
if (value) {
filters[input.dataset.colIndex] = value;
}
});
return filters;
}

applyDefaultSortOrder() {
// sort rows if any 1 column has a default sortOrder set
const columnsToSort = this.getColumns().filter(col => col.sortOrder !== 'none');

if (columnsToSort.length === 1) {
const column = columnsToSort[0];
this.sortColumn(column.colIndex, column.sortOrder);
}
}

sortRows(colIndex, sortOrder) {
return this.datamanager.sortRows(colIndex, sortOrder);
}

getColumn(colIndex) {
return this.datamanager.getColumn(colIndex);
}

getColumns() {
return this.datamanager.getColumns();
}

setColumnWidth(colIndex, width) {
colIndex = +colIndex;

let columnWidth = width || this.getColumn(colIndex).width;

const selector = [
`.dt-cell__content--col-${colIndex}`,
`.dt-cell__edit--col-${colIndex}`
].join(', ');

const styles = {
width: columnWidth + 'px'
};

this.style.setStyle(selector, styles);
}

setColumnHeaderWidth(colIndex) {
colIndex = +colIndex;
this.$columnMap = this.$columnMap || [];
const selector = `.dt-cell__content--header-${colIndex}`;
const {
width
} = this.getColumn(colIndex);

let $column = this.$columnMap[colIndex];
if (!$column) {
$column = this.header.querySelector(selector);
this.$columnMap[colIndex] = $column;
}

$column.style.width = width + 'px';
}

getColumnMinWidth(colIndex) {
colIndex = +colIndex;
return this.getColumn(colIndex).minWidth || 24;
}

getFirstColumnIndex() {
return this.datamanager.getColumnIndexById('_rowIndex') + 1;
}

getHeaderCell$(colIndex) {
return $(`.dt-cell--header-${colIndex}`, this.header);
}

getLastColumnIndex() {
return this.datamanager.getColumnCount() - 1;
}

getDropdownHTML() {
const { dropdownButton } = this.options;

return `
<div class="dt-dropdown">
<div class="dt-dropdown__toggle">${dropdownButton}</div>
</div>
`;
}

getDropdownListHTML() {
const { headerDropdown: dropdownItems } = this.options;

return `
<div class="dt-dropdown__list">
${dropdownItems.map((d, i) => `
<div class="dt-dropdown__list-item" data-index="${i}">${d.label}</div>
`).join('')}
</div>
`;
}
}

+ 11
- 0
src/dark.css 查看文件

@@ -0,0 +1,11 @@
.datatable {
--dt-border-color: #424242;
--dt-light-bg: #2e3538;
--dt-text-color: #dfe2e5;
--dt-text-light: #dfe2e5;
--dt-cell-bg: #1c1f20;
--dt-focus-border-width: 1px;
--dt-selection-highlight-color: var(--dt-light-bg);
--dt-toast-message-border: 1px solid var(--dt-border-color);
--dt-header-cell-bg: #262c2e;
}

+ 623
- 0
src/datamanager.js 查看文件

@@ -0,0 +1,623 @@
import {
isNumeric,
nextTick,
isNumber,
notSet
} from './utils';

export default class DataManager {
constructor(options) {
this.options = options;
this.sortRows = nextTick(this.sortRows, this);
this.switchColumn = nextTick(this.switchColumn, this);
this.removeColumn = nextTick(this.removeColumn, this);
this.options.filterRows = nextTick(this.options.filterRows, this);
}

init(data, columns) {
if (!data) {
data = this.options.data;
}
if (columns) {
this.options.columns = columns;
}

this.data = data;

this.rowCount = 0;
this.columns = [];
this.rows = [];

this.prepareColumns();
this.prepareRows();
this.prepareTreeRows();
this.prepareRowView();
this.prepareNumericColumns();
}

// computed property
get currentSort() {
const col = this.columns.find(col => col.sortOrder !== 'none');
return col || {
colIndex: -1,
sortOrder: 'none'
};
}

prepareColumns() {
this.columns = [];
this.validateColumns();
this.prepareDefaultColumns();
this.prepareHeader();
}

prepareDefaultColumns() {
if (this.options.checkboxColumn && !this.hasColumnById('_checkbox')) {
const cell = {
id: '_checkbox',
content: this.getCheckboxHTML(),
editable: false,
resizable: false,
sortable: false,
focusable: false,
dropdown: false,
width: 32
};
this.columns.push(cell);
}

if (this.options.serialNoColumn && !this.hasColumnById('_rowIndex')) {
let cell = {
id: '_rowIndex',
content: '',
align: 'center',
editable: false,
resizable: false,
focusable: false,
dropdown: false
};

this.columns.push(cell);
}
}

prepareHeader() {
let columns = this.columns.concat(this.options.columns);
const baseCell = {
isHeader: 1,
editable: true,
sortable: true,
resizable: true,
focusable: true,
dropdown: true,
width: null,
format: (value) => {
if (value === null || value === undefined) {
return '';
}
return value + '';
}
};

this.columns = columns
.map((cell, i) => this.prepareCell(cell, i))
.map(col => Object.assign({}, baseCell, col))
.map(col => {
col.content = col.content || col.name || '';
col.id = col.id || col.content;
return col;
});
}

prepareCell(content, i) {
const cell = {
content: '',
sortOrder: 'none',
colIndex: i,
column: this.columns[i]
};

if (content !== null && typeof content === 'object') {
// passed as column/header
Object.assign(cell, content);
} else {
cell.content = content;
}

return cell;
}

prepareNumericColumns() {
const row0 = this.getRow(0);
if (!row0) return;
this.columns = this.columns.map((column, i) => {

const cellValue = row0[i].content;
if (!column.align && isNumeric(cellValue)) {
column.align = 'right';
}

return column;
});
}

prepareRows() {
this.validateData(this.data);

this.rows = this.data.map((d, i) => {
const index = this._getNextRowCount();

let row = [];
let meta = {
rowIndex: index
};

if (Array.isArray(d)) {
// row is an array
if (this.options.checkboxColumn) {
row.push(this.getCheckboxHTML());
}
if (this.options.serialNoColumn) {
row.push((index + 1) + '');
}
row = row.concat(d);

while (row.length < this.columns.length) {
row.push('');
}

} else {
// row is an object
for (let col of this.columns) {
if (col.id === '_checkbox') {
row.push(this.getCheckboxHTML());
} else if (col.id === '_rowIndex') {
row.push((index + 1) + '');
} else {
row.push(d[col.id]);
}
}

meta.indent = d.indent || 0;
}

return this.prepareRow(row, meta);
});
}

prepareTreeRows() {
this.rows.forEach((row, i) => {
if (isNumber(row.meta.indent)) {
// if (i === 36) debugger;
const nextRow = this.getRow(i + 1);
row.meta.isLeaf = !nextRow ||
notSet(nextRow.meta.indent) ||
nextRow.meta.indent <= row.meta.indent;
row.meta.isTreeNodeClose = false;
}
});
}

prepareRowView() {
// This is order in which rows will be rendered in the table.
// When sorting happens, only this.rowViewOrder will change
// and not the original this.rows
this.rowViewOrder = this.rows.map(row => row.meta.rowIndex);
}

prepareRow(row, meta) {
const baseRowCell = {
rowIndex: meta.rowIndex,
indent: meta.indent
};

row = row
.map((cell, i) => this.prepareCell(cell, i))
.map(cell => Object.assign({}, baseRowCell, cell));

// monkey patched in array object
row.meta = meta;
return row;
}

validateColumns() {
const columns = this.options.columns;
if (!Array.isArray(columns)) {
throw new DataError('`columns` must be an array');
}

columns.forEach((column, i) => {
if (typeof column !== 'string' && typeof column !== 'object') {
throw new DataError(`column "${i}" must be a string or an object`);
}
});
}

validateData(data) {
if (Array.isArray(data) &&
(data.length === 0 || Array.isArray(data[0]) || typeof data[0] === 'object')) {
return true;
}
throw new DataError('`data` must be an array of arrays or objects');
}

appendRows(rows) {
this.validateData(rows);

this.rows.push(...this.prepareRows(rows));
}

sortRows(colIndex, sortOrder = 'none') {
colIndex = +colIndex;

// reset sortOrder and update for colIndex
this.getColumns()
.map(col => {
if (col.colIndex === colIndex) {
col.sortOrder = sortOrder;
} else {
col.sortOrder = 'none';
}
});

this._sortRows(colIndex, sortOrder);
}

_sortRows(colIndex, sortOrder) {

if (this.currentSort.colIndex === colIndex) {
// reverse the array if only sortOrder changed
if (
(this.currentSort.sortOrder === 'asc' && sortOrder === 'desc') ||
(this.currentSort.sortOrder === 'desc' && sortOrder === 'asc')
) {
this.reverseArray(this.rowViewOrder);
this.currentSort.sortOrder = sortOrder;
return;
}
}

this.rowViewOrder.sort((a, b) => {
const aIndex = a;
const bIndex = b;

let aContent = this.getCell(colIndex, a).content;
let bContent = this.getCell(colIndex, b).content;
aContent = aContent == null ? '' : aContent;
bContent = bContent == null ? '' : bContent;

if (sortOrder === 'none') {
return aIndex - bIndex;
} else if (sortOrder === 'asc') {
if (aContent < bContent) return -1;
if (aContent > bContent) return 1;
if (aContent === bContent) return 0;
} else if (sortOrder === 'desc') {
if (aContent < bContent) return 1;
if (aContent > bContent) return -1;
if (aContent === bContent) return 0;
}
return 0;
});

if (this.hasColumnById('_rowIndex')) {
// update row index
const srNoColIndex = this.getColumnIndexById('_rowIndex');
this.rows.forEach((row, index) => {
const viewIndex = this.rowViewOrder.indexOf(index);
const cell = row[srNoColIndex];
cell.content = (viewIndex + 1) + '';
});
}
}

reverseArray(array) {
let left = null;
let right = null;
let length = array.length;

for (left = 0, right = length - 1; left < right; left += 1, right -= 1) {
const temporary = array[left];

array[left] = array[right];
array[right] = temporary;
}
}

switchColumn(index1, index2) {
// update columns
const temp = this.columns[index1];
this.columns[index1] = this.columns[index2];
this.columns[index2] = temp;

this.columns[index1].colIndex = index1;
this.columns[index2].colIndex = index2;

// update rows
this.rows.forEach(row => {
const newCell1 = Object.assign({}, row[index1], {
colIndex: index2
});
const newCell2 = Object.assign({}, row[index2], {
colIndex: index1
});

row[index2] = newCell1;
row[index1] = newCell2;
});
}

removeColumn(index) {
index = +index;
const filter = cell => cell.colIndex !== index;
const map = (cell, i) => Object.assign({}, cell, {
colIndex: i
});
// update columns
this.columns = this.columns
.filter(filter)
.map(map);

// update rows
this.rows.forEach(row => {
// remove cell
row.splice(index, 1);
// update colIndex
row.forEach((cell, i) => {
cell.colIndex = i;
});
});
}

updateRow(row, rowIndex) {
if (row.length < this.columns.length) {
if (this.hasColumnById('_rowIndex')) {
const val = (rowIndex + 1) + '';

row = [val].concat(row);
}

if (this.hasColumnById('_checkbox')) {
const val = '<input type="checkbox" />';

row = [val].concat(row);
}
}

const _row = this.prepareRow(row, {rowIndex});
const index = this.rows.findIndex(row => row[0].rowIndex === rowIndex);
this.rows[index] = _row;

return _row;
}

updateCell(colIndex, rowIndex, options) {
let cell;
if (typeof colIndex === 'object') {
// cell object was passed,
// must have colIndex, rowIndex
cell = colIndex;
colIndex = cell.colIndex;
rowIndex = cell.rowIndex;
// the object passed must be merged with original cell
options = cell;
}
cell = this.getCell(colIndex, rowIndex);

// mutate object directly
for (let key in options) {
const newVal = options[key];
if (newVal !== undefined) {
cell[key] = newVal;
}
}

return cell;
}

updateColumn(colIndex, keyValPairs) {
const column = this.getColumn(colIndex);
for (let key in keyValPairs) {
const newVal = keyValPairs[key];
if (newVal !== undefined) {
column[key] = newVal;
}
}
return column;
}

filterRows(filters) {
return this.options.filterRows(this.rows, filters)
.then(result => {
if (!result) {
result = this.getAllRowIndices();
}

if (!result.then) {
result = Promise.resolve(result);
}

return result.then(rowsToShow => {
this._filteredRows = rowsToShow;

const rowsToHide = this.getAllRowIndices()
.filter(index => !rowsToShow.includes(index));

return {
rowsToHide,
rowsToShow
};
});
});
}

getFilteredRowIndices() {
return this._filteredRows || this.getAllRowIndices();
}

getAllRowIndices() {
return this.rows.map(row => row.meta.rowIndex);
}

getRowCount() {
return this.rowCount;
}

_getNextRowCount() {
const val = this.rowCount;

this.rowCount++;
return val;
}

getRows(start, end) {
return this.rows.slice(start, end);
}

getRowsForView(start, end) {
const rows = this.rowViewOrder.map(i => this.rows[i]);
return rows.slice(start, end);
}

getColumns(skipStandardColumns) {
let columns = this.columns;

if (skipStandardColumns) {
columns = columns.slice(this.getStandardColumnCount());
}

return columns;
}

getStandardColumnCount() {
if (this.options.checkboxColumn && this.options.serialNoColumn) {
return 2;
}

if (this.options.checkboxColumn || this.options.serialNoColumn) {
return 1;
}

return 0;
}

getColumnCount(skipStandardColumns) {
let val = this.columns.length;

if (skipStandardColumns) {
val = val - this.getStandardColumnCount();
}

return val;
}

getColumn(colIndex) {
colIndex = +colIndex;

if (colIndex < 0) {
// negative indexes
colIndex = this.columns.length + colIndex;
}

return this.columns.find(col => col.colIndex === colIndex);
}

getColumnById(id) {
return this.columns.find(col => col.id === id);
}

getRow(rowIndex) {
rowIndex = +rowIndex;
return this.rows[rowIndex];
}

getCell(colIndex, rowIndex) {
rowIndex = +rowIndex;
colIndex = +colIndex;
return this.getRow(rowIndex)[colIndex];
}

getChildren(parentRowIndex) {
parentRowIndex = +parentRowIndex;
const parentIndent = this.getRow(parentRowIndex).meta.indent;
const out = [];

for (let i = parentRowIndex + 1; i < this.rowCount; i++) {
const row = this.getRow(i);
if (isNaN(row.meta.indent)) continue;

if (row.meta.indent > parentIndent) {
out.push(i);
}

if (row.meta.indent === parentIndent) {
break;
}
}

return out;
}

getImmediateChildren(parentRowIndex) {
parentRowIndex = +parentRowIndex;
const parentIndent = this.getRow(parentRowIndex).meta.indent;
const out = [];
const childIndent = parentIndent + 1;

for (let i = parentRowIndex + 1; i < this.rowCount; i++) {
const row = this.getRow(i);
if (isNaN(row.meta.indent) || row.meta.indent > childIndent) continue;

if (row.meta.indent === childIndent) {
out.push(i);
}

if (row.meta.indent === parentIndent) {
break;
}
}

return out;
}

get() {
return {
columns: this.columns,
rows: this.rows
};
}

/**
* Returns the original data which was passed
* based on rowIndex
* @param {Number} rowIndex
* @returns Array|Object
* @memberof DataManager
*/
getData(rowIndex) {
return this.data[rowIndex];
}

hasColumn(name) {
return Boolean(this.columns.find(col => col.content === name));
}

hasColumnById(id) {
return Boolean(this.columns.find(col => col.id === id));
}

getColumnIndex(name) {
return this.columns.findIndex(col => col.content === name);
}

getColumnIndexById(id) {
return this.columns.findIndex(col => col.id === id);
}

getCheckboxHTML() {
return '<input type="checkbox" />';
}
}

// Custom Errors
export class DataError extends TypeError {};

+ 270
- 0
src/datatable.js 查看文件

@@ -0,0 +1,270 @@
import $ from './dom';
import DataManager from './datamanager';
import CellManager from './cellmanager';
import ColumnManager from './columnmanager';
import RowManager from './rowmanager';
import BodyRenderer from './body-renderer';
import Style from './style';
import Keyboard from './keyboard';
import TranslationManager from './translationmanager';
import getDefaultOptions from './defaults';

let defaultComponents = {
DataManager,
CellManager,
ColumnManager,
RowManager,
BodyRenderer,
Style,
Keyboard
};

class DataTable {
constructor(wrapper, options) {
DataTable.instances++;

if (typeof wrapper === 'string') {
// css selector
wrapper = document.querySelector(wrapper);
}
this.wrapper = wrapper;
if (!(this.wrapper instanceof HTMLElement)) {
throw new Error('Invalid argument given for `wrapper`');
}

this.initializeTranslations(options);
this.setDefaultOptions();
this.buildOptions(options);
this.prepare();
this.initializeComponents();

if (this.options.data) {
this.refresh();
this.columnmanager.applyDefaultSortOrder();
}
}

initializeTranslations(options) {
this.language = options.language || 'en';
this.translationManager = new TranslationManager(this.language);

if (options.translations) {
this.translationManager.addTranslations(options.translations);
}
}

setDefaultOptions() {
this.DEFAULT_OPTIONS = getDefaultOptions(this);
}

buildOptions(options) {
this.options = this.options || {};

this.options = Object.assign(
{}, this.DEFAULT_OPTIONS,
this.options || {}, options
);

options.headerDropdown = options.headerDropdown || [];
this.options.headerDropdown = [
...this.DEFAULT_OPTIONS.headerDropdown,
...options.headerDropdown
];

// custom user events
this.events = Object.assign(
{}, this.DEFAULT_OPTIONS.events,
this.options.events || {},
options.events || {}
);
this.fireEvent = this.fireEvent.bind(this);
}

prepare() {
this.prepareDom();
this.unfreeze();
}

initializeComponents() {
let components = Object.assign({}, defaultComponents, this.options.overrideComponents);
let {
Style,
Keyboard,
DataManager,
RowManager,
ColumnManager,
CellManager,
BodyRenderer
} = components;

this.style = new Style(this);
this.keyboard = new Keyboard(this.wrapper);
this.datamanager = new DataManager(this.options);
this.rowmanager = new RowManager(this);
this.columnmanager = new ColumnManager(this);
this.cellmanager = new CellManager(this);
this.bodyRenderer = new BodyRenderer(this);
}

prepareDom() {
this.wrapper.innerHTML = `
<div class="datatable" dir="${this.options.direction}">
<div class="dt-header"></div>
<div class="dt-scrollable"></div>
<div class="dt-footer"></div>
<div class="dt-freeze">
<span class="dt-freeze__message">
${this.options.freezeMessage}
</span>
</div>
<div class="dt-toast"></div>
<div class="dt-dropdown-container"></div>
<textarea class="dt-paste-target"></textarea>
</div>
`;

this.datatableWrapper = $('.datatable', this.wrapper);
this.header = $('.dt-header', this.wrapper);
this.footer = $('.dt-footer', this.wrapper);
this.bodyScrollable = $('.dt-scrollable', this.wrapper);
this.freezeContainer = $('.dt-freeze', this.wrapper);
this.toastMessage = $('.dt-toast', this.wrapper);
this.pasteTarget = $('.dt-paste-target', this.wrapper);
this.dropdownContainer = $('.dt-dropdown-container', this.wrapper);
}

refresh(data, columns) {
this.datamanager.init(data, columns);
this.render();
this.setDimensions();
}

destroy() {
this.wrapper.innerHTML = '';
this.style.destroy();
this.fireEvent('onDestroy');
}

appendRows(rows) {
this.datamanager.appendRows(rows);
this.rowmanager.refreshRows();
}

refreshRow(row, rowIndex) {
this.rowmanager.refreshRow(row, rowIndex);
}

render() {
this.renderHeader();
this.renderBody();
}

renderHeader() {
this.columnmanager.renderHeader();
}

renderBody() {
this.bodyRenderer.render();
}

setDimensions() {
this.style.setDimensions();
}

showToastMessage(message, hideAfter) {
this.bodyRenderer.showToastMessage(message, hideAfter);
}

clearToastMessage() {
this.bodyRenderer.clearToastMessage();
}

getColumn(colIndex) {
return this.datamanager.getColumn(colIndex);
}

getColumns() {
return this.datamanager.getColumns();
}

getRows() {
return this.datamanager.getRows();
}

getCell(colIndex, rowIndex) {
return this.datamanager.getCell(colIndex, rowIndex);
}

getColumnHeaderElement(colIndex) {
return this.columnmanager.getColumnHeaderElement(colIndex);
}

getViewportHeight() {
if (!this.viewportHeight) {
this.viewportHeight = $.style(this.bodyScrollable, 'height');
}

return this.viewportHeight;
}

sortColumn(colIndex, sortOrder) {
this.columnmanager.sortColumn(colIndex, sortOrder);
}

removeColumn(colIndex) {
this.columnmanager.removeColumn(colIndex);
}

scrollToLastColumn() {
this.datatableWrapper.scrollLeft = 9999;
}

freeze() {
$.style(this.freezeContainer, {
display: ''
});
}

unfreeze() {
$.style(this.freezeContainer, {
display: 'none'
});
}

updateOptions(options) {
this.buildOptions(options);
}

fireEvent(eventName, ...args) {
// fire internalEventHandlers if any
// and then user events
const handlers = [
...(this._internalEventHandlers[eventName] || []),
this.events[eventName]
].filter(Boolean);

for (let handler of handlers) {
handler.apply(this, args);
}
}

on(event, handler) {
this._internalEventHandlers = this._internalEventHandlers || {};
this._internalEventHandlers[event] = this._internalEventHandlers[event] || [];
this._internalEventHandlers[event].push(handler);
}

log() {
if (this.options.logs) {
console.log.apply(console, arguments);
}
}

translate(str, args) {
return this.translationManager.translate(str, args);
}
}

DataTable.instances = 0;

export default DataTable;

+ 73
- 0
src/defaults.js 查看文件

@@ -0,0 +1,73 @@
import filterRows from './filterRows';
import icons from './icons';

export default function getDefaultOptions(instance) {
return {
columns: [],
data: [],
dropdownButton: icons.chevronDown,
headerDropdown: [
{
label: instance.translate('Sort Ascending'),
action: function (column) {
this.sortColumn(column.colIndex, 'asc');
}
},
{
label: instance.translate('Sort Descending'),
action: function (column) {
this.sortColumn(column.colIndex, 'desc');
}
},
{
label: instance.translate('Reset sorting'),
action: function (column) {
this.sortColumn(column.colIndex, 'none');
}
},
{
label: instance.translate('Remove column'),
action: function (column) {
this.removeColumn(column.colIndex);
}
}
],
events: {
onRemoveColumn(column) {},
onSwitchColumn(column1, column2) {},
onSortColumn(column) {},
onCheckRow(row) {},
onDestroy() {}
},
hooks: {
columnTotal: null
},
sortIndicator: {
asc: '↑',
desc: '↓',
none: ''
},
overrideComponents: {
// ColumnManager: CustomColumnManager
},
filterRows: filterRows,
freezeMessage: '',
getEditor: null,
serialNoColumn: true,
checkboxColumn: false,
clusterize: true,
logs: false,
layout: 'fixed', // fixed, fluid, ratio
noDataMessage: instance.translate('No Data'),
cellHeight: 40,
minimumColumnWidth: 30,
inlineFilters: false,
treeView: false,
checkedRowStatus: true,
dynamicRowHeight: false,
pasteFromClipboard: false,
showTotalRow: false,
direction: 'ltr',
disableReorderColumn: false
};
};

+ 235
- 0
src/dom.js 查看文件

@@ -0,0 +1,235 @@
export default function $(expr, con) {
return typeof expr === 'string' ?
(con || document).querySelector(expr) :
expr || null;
}

$.each = (expr, con) => {
return typeof expr === 'string' ?
Array.from((con || document).querySelectorAll(expr)) :
expr || null;
};

$.create = (tag, o) => {
let element = document.createElement(tag);

for (let i in o) {
let val = o[i];

if (i === 'inside') {
$(val).appendChild(element);
} else
if (i === 'around') {
let ref = $(val);
ref.parentNode.insertBefore(element, ref);
element.appendChild(ref);
} else
if (i === 'styles') {
if (typeof val === 'object') {
Object.keys(val).map(prop => {
element.style[prop] = val[prop];
});
}
} else
if (i in element) {
element[i] = val;
} else {
element.setAttribute(i, val);
}
}

return element;
};

$.on = (element, event, selector, callback) => {
if (!callback) {
callback = selector;
$.bind(element, event, callback);
} else {
$.delegate(element, event, selector, callback);
}
};

$.off = (element, event, handler) => {
element.removeEventListener(event, handler);
};

$.bind = (element, event, callback) => {
event.split(/\s+/).forEach(function (event) {
element.addEventListener(event, callback);
});
};

$.delegate = (element, event, selector, callback) => {
element.addEventListener(event, function (e) {
const delegatedTarget = e.target.closest(selector);
if (delegatedTarget) {
e.delegatedTarget = delegatedTarget;
callback.call(this, e, delegatedTarget);
}
});
};

$.unbind = (element, o) => {
if (element) {
for (let event in o) {
let callback = o[event];

event.split(/\s+/).forEach(function (event) {
element.removeEventListener(event, callback);
});
}
}
};

$.fire = (target, type, properties) => {
let evt = document.createEvent('HTMLEvents');

evt.initEvent(type, true, true);

for (let j in properties) {
evt[j] = properties[j];
}

return target.dispatchEvent(evt);
};

$.data = (element, attrs) => { // eslint-disable-line
if (!attrs) {
return element.dataset;
}

for (const attr in attrs) {
element.dataset[attr] = attrs[attr];
}
};

$.style = (elements, styleMap) => { // eslint-disable-line

if (typeof styleMap === 'string') {
return $.getStyle(elements, styleMap);
}

if (!Array.isArray(elements)) {
elements = [elements];
}

elements.map(element => {
for (const prop in styleMap) {
element.style[prop] = styleMap[prop];
}
});
};

$.removeStyle = (elements, styleProps) => {
if (!Array.isArray(elements)) {
elements = [elements];
}

if (!Array.isArray(styleProps)) {
styleProps = [styleProps];
}

elements.map(element => {
for (const prop of styleProps) {
element.style[prop] = '';
}
});
};

$.getStyle = (element, prop) => {
if (!prop) {
return getComputedStyle(element);
}

let val = getComputedStyle(element)[prop];

if (['width', 'height'].includes(prop)) {
val = parseFloat(val);
}

return val;
};

$.closest = (selector, element) => {
if (!element) return null;

if (element.matches(selector)) {
return element;
}

return $.closest(selector, element.parentNode);
};

$.inViewport = (el, parentEl) => {
const {
top,
left,
bottom,
right
} = el.getBoundingClientRect();
const {
top: pTop,
left: pLeft,
bottom: pBottom,
right: pRight
} = parentEl.getBoundingClientRect();

return top >= pTop && left >= pLeft && bottom <= pBottom && right <= pRight;
};

$.scrollTop = function scrollTop(element, pixels) {
requestAnimationFrame(() => {
element.scrollTop = pixels;
});
};

$.scrollbarSize = function scrollbarSize() {
if (!$.scrollBarSizeValue) {
$.scrollBarSizeValue = getScrollBarSize();
}
return $.scrollBarSizeValue;
};

function getScrollBarSize() {
// assume scrollbar width and height would be the same

// Create the measurement node
const scrollDiv = document.createElement('div');
$.style(scrollDiv, {
width: '100px',
height: '100px',
overflow: 'scroll',
position: 'absolute',
top: '-9999px'
});
document.body.appendChild(scrollDiv);

// Get the scrollbar width
const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;

// Delete the DIV
document.body.removeChild(scrollDiv);

return scrollbarWidth;
}

$.hasVerticalOverflow = function (element) {
return element.scrollHeight > element.offsetHeight + 10;
};

$.hasHorizontalOverflow = function (element) {
return element.scrollWidth > element.offsetWidth + 10;
};

$.measureTextWidth = function (text) {
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.visibility = 'hidden';
div.style.height = 'auto';
div.style.width = 'auto';
div.style.whiteSpace = 'nowrap';
div.innerText = text;
document.body.appendChild(div);
return div.clientWidth + 1;
};

+ 204
- 0
src/filterRows.js 查看文件

@@ -0,0 +1,204 @@
import { isNumber, stripHTML } from './utils';
import CellManager from './cellmanager';

export default function filterRows(rows, filters) {
let filteredRowIndices = [];

if (Object.keys(filters).length === 0) {
return rows.map(row => row.meta.rowIndex);
}

for (let colIndex in filters) {
const keyword = filters[colIndex];

const filteredRows = filteredRowIndices.length ?
filteredRowIndices.map(i => rows[i]) :
rows;

const cells = filteredRows.map(row => row[colIndex]);

let filter = guessFilter(keyword);
let filterMethod = getFilterMethod(rows, filter);

if (filterMethod) {
filteredRowIndices = filterMethod(filter.text, cells);
} else {
filteredRowIndices = cells.map(cell => cell.rowIndex);
}
}

return filteredRowIndices;
};

function getFilterMethod(rows, filter) {
const getFormattedValue = cell => {
let formatter = CellManager.getCustomCellFormatter(cell);
if (formatter && cell.content) {
cell.html = formatter(cell.content, rows[cell.rowIndex], cell.column, rows[cell.rowIndex]);
return stripHTML(cell.html);
}
return cell.content || '';
};

const stringCompareValue = cell =>
String(stripHTML(cell.html || '') || getFormattedValue(cell)).toLowerCase();

const numberCompareValue = cell => parseFloat(cell.content);

const getCompareValues = (cell, keyword) => {
if (cell.column.compareValue) {
const compareValues = cell.column.compareValue(cell, keyword);
if (compareValues && Array.isArray(compareValues)) return compareValues;
}

// check if it can be converted to number
const float = numberCompareValue(cell);
if (!isNaN(float)) {
return [float, keyword];
}

return [stringCompareValue(cell), keyword];
};

let filterMethodMap = {
contains(keyword, cells) {
return cells
.filter(cell => {
const hay = stringCompareValue(cell);
const needle = (keyword || '').toLowerCase();
return !needle || hay.includes(needle);
})
.map(cell => cell.rowIndex);
},

greaterThan(keyword, cells) {
return cells
.filter(cell => {
const [compareValue, keywordValue] = getCompareValues(cell, keyword);
return compareValue > keywordValue;
})
.map(cell => cell.rowIndex);
},

lessThan(keyword, cells) {
return cells
.filter(cell => {
const [compareValue, keywordValue] = getCompareValues(cell, keyword);
return compareValue < keywordValue;
})
.map(cell => cell.rowIndex);
},

equals(keyword, cells) {
return cells
.filter(cell => {
const value = parseFloat(cell.content);
return value === keyword;
})
.map(cell => cell.rowIndex);
},

notEquals(keyword, cells) {
return cells
.filter(cell => {
const value = parseFloat(cell.content);
return value !== keyword;
})
.map(cell => cell.rowIndex);
},

range(rangeValues, cells) {
return cells
.filter(cell => {
const values1 = getCompareValues(cell, rangeValues[0]);
const values2 = getCompareValues(cell, rangeValues[1]);
const value = values1[0];
return value >= values1[1] && value <= values2[1];
})
.map(cell => cell.rowIndex);
},

containsNumber(keyword, cells) {
return cells
.filter(cell => {
let number = parseFloat(keyword, 10);
let string = keyword;
let hayNumber = numberCompareValue(cell);
let hayString = stringCompareValue(cell);

return number === hayNumber || hayString.includes(string);
})
.map(cell => cell.rowIndex);
}
};

return filterMethodMap[filter.type];
}

function guessFilter(keyword = '') {
if (keyword.length === 0) return {};

let compareString = keyword;

if (['>', '<', '='].includes(compareString[0])) {
compareString = keyword.slice(1);
} else if (compareString.startsWith('!=')) {
compareString = keyword.slice(2);
}

if (keyword.startsWith('>')) {
if (compareString) {
return {
type: 'greaterThan',
text: compareString.trim()
};
}
}

if (keyword.startsWith('<')) {
if (compareString) {
return {
type: 'lessThan',
text: compareString.trim()
};
}
}

if (keyword.startsWith('=')) {
if (isNumber(compareString)) {
return {
type: 'equals',
text: Number(keyword.slice(1).trim())
};
}
}

if (isNumber(compareString)) {
return {
type: 'containsNumber',
text: compareString
};
}

if (keyword.startsWith('!=')) {
if (isNumber(compareString)) {
return {
type: 'notEquals',
text: Number(keyword.slice(2).trim())
};
}
}

if (keyword.split(':').length === 2) {
compareString = keyword.split(':');
return {
type: 'range',
text: compareString.map(v => v.trim())
};
}

return {
type: 'contains',
text: compareString.toLowerCase()
};
}

+ 10
- 0
src/icons.js 查看文件

@@ -0,0 +1,10 @@
/* eslint-disable max-len */

// Icons from https://feathericons.com/

let icons = {
chevronDown: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>',
chevronRight: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>'
};

export default icons;

+ 5
- 0
src/index.js 查看文件

@@ -0,0 +1,5 @@
import DataTable from './datatable.js';
import packageJson from '../package.json';
DataTable.__version__ = packageJson.version;

export default DataTable;

+ 59
- 0
src/keyboard.js 查看文件

@@ -0,0 +1,59 @@
import $ from './dom';

const KEYCODES = {
13: 'enter',
91: 'meta',
16: 'shift',
17: 'ctrl',
18: 'alt',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
9: 'tab',
27: 'esc',
67: 'c',
70: 'f',
86: 'v'
};

export default class Keyboard {
constructor(element) {
this.listeners = {};
$.on(element, 'keydown', this.handler.bind(this));
}

handler(e) {
let key = KEYCODES[e.keyCode];

if (e.shiftKey && key !== 'shift') {
key = 'shift+' + key;
}

if ((e.ctrlKey && key !== 'ctrl') || (e.metaKey && key !== 'meta')) {
key = 'ctrl+' + key;
}

const listeners = this.listeners[key];

if (listeners && listeners.length > 0) {
for (let listener of listeners) {
const preventBubbling = listener(e);
if (preventBubbling === undefined || preventBubbling === true) {
e.preventDefault();
}
}
}
}

on(key, listener) {
const keys = key.split(',').map(k => k.trim());

keys.map(key => {
this.listeners[key] = this.listeners[key] || [];
this.listeners[key].push(listener);
});
}
}

export let keyCode = KEYCODES;

+ 363
- 0
src/rowmanager.js 查看文件

@@ -0,0 +1,363 @@
import $ from './dom';
import {
makeDataAttributeString,
nextTick,
ensureArray,
linkProperties,
uniq,
numberSortAsc
} from './utils';

export default class RowManager {
constructor(instance) {
this.instance = instance;
linkProperties(this, this.instance, [
'options',
'fireEvent',
'wrapper',
'bodyScrollable',
'bodyRenderer',
'style'
]);

this.bindEvents();
this.refreshRows = nextTick(this.refreshRows, this);
}

get datamanager() {
return this.instance.datamanager;
}

get cellmanager() {
return this.instance.cellmanager;
}

bindEvents() {
this.bindCheckbox();
}

bindCheckbox() {
if (!this.options.checkboxColumn) return;

// map of checked rows
this.checkMap = [];

$.on(this.wrapper, 'click', '.dt-cell--col-0 [type="checkbox"]', (e, $checkbox) => {
const $cell = $checkbox.closest('.dt-cell');
const {
rowIndex,
isHeader
} = $.data($cell);
const checked = $checkbox.checked;

if (isHeader) {
this.checkAll(checked);
} else {
this.checkRow(rowIndex, checked);
}
});
}

refreshRows() {
this.instance.renderBody();
this.instance.setDimensions();
}

refreshRow(row, rowIndex) {
const _row = this.datamanager.updateRow(row, rowIndex);

_row.forEach(cell => {
this.cellmanager.refreshCell(cell, true);
});
}

getCheckedRows() {
if (!this.checkMap) {
return [];
}

let out = [];
for (let rowIndex in this.checkMap) {
const checked = this.checkMap[rowIndex];
if (checked === 1) {
out.push(rowIndex);
}
}

return out;
}

highlightCheckedRows() {
this.getCheckedRows()
.map(rowIndex => this.checkRow(rowIndex, true));
}

checkRow(rowIndex, toggle) {
const value = toggle ? 1 : 0;
const selector = rowIndex => `.dt-cell--0-${rowIndex} [type="checkbox"]`;
// update internal map
this.checkMap[rowIndex] = value;
// set checkbox value explicitly
$.each(selector(rowIndex), this.bodyScrollable)
.map(input => {
input.checked = toggle;
});
// highlight row
this.highlightRow(rowIndex, toggle);
this.showCheckStatus();
this.fireEvent('onCheckRow', this.datamanager.getRow(rowIndex));
}

checkAll(toggle) {
const value = toggle ? 1 : 0;

// update internal map
if (toggle) {
this.checkMap = Array.from(Array(this.getTotalRows())).map(c => value);
} else {
this.checkMap = [];
}
// set checkbox value
$.each('.dt-cell--col-0 [type="checkbox"]', this.bodyScrollable)
.map(input => {
input.checked = toggle;
});
// highlight all
this.highlightAll(toggle);
this.showCheckStatus();
this.fireEvent('onCheckRow');
}

showCheckStatus() {
if (!this.options.checkedRowStatus) return;
const checkedRows = this.getCheckedRows();
const count = checkedRows.length;
if (count > 0) {
let message = this.instance.translate('{count} rows selected', {
count: count
});
this.bodyRenderer.showToastMessage(message);
} else {
this.bodyRenderer.clearToastMessage();
}
}

highlightRow(rowIndex, toggle = true) {
const $row = this.getRow$(rowIndex);
if (!$row) return;

if (!toggle && this.bodyScrollable.classList.contains('dt-scrollable--highlight-all')) {
$row.classList.add('dt-row--unhighlight');
return;
}

if (toggle && $row.classList.contains('dt-row--unhighlight')) {
$row.classList.remove('dt-row--unhighlight');
}

this._highlightedRows = this._highlightedRows || {};

if (toggle) {
$row.classList.add('dt-row--highlight');
this._highlightedRows[rowIndex] = $row;
} else {
$row.classList.remove('dt-row--highlight');
delete this._highlightedRows[rowIndex];
}
}

highlightAll(toggle = true) {
if (toggle) {
this.bodyScrollable.classList.add('dt-scrollable--highlight-all');
} else {
this.bodyScrollable.classList.remove('dt-scrollable--highlight-all');
for (const rowIndex in this._highlightedRows) {
const $row = this._highlightedRows[rowIndex];
$row.classList.remove('dt-row--highlight');
}
this._highlightedRows = {};
}
}

showRows(rowIndices) {
rowIndices = ensureArray(rowIndices);
const rows = rowIndices.map(rowIndex => this.datamanager.getRow(rowIndex));
this.bodyRenderer.renderRows(rows);
}

showAllRows() {
const rowIndices = this.datamanager.getAllRowIndices();
this.showRows(rowIndices);
}

getChildrenToShowForNode(rowIndex) {
const row = this.datamanager.getRow(rowIndex);
row.meta.isTreeNodeClose = false;

return this.datamanager.getImmediateChildren(rowIndex);
}

openSingleNode(rowIndex) {
const childrenToShow = this.getChildrenToShowForNode(rowIndex);
const visibleRowIndices = this.bodyRenderer.visibleRowIndices;
const rowsToShow = uniq([...childrenToShow, ...visibleRowIndices]).sort(numberSortAsc);

this.showRows(rowsToShow);
}

getChildrenToHideForNode(rowIndex) {
const row = this.datamanager.getRow(rowIndex);
row.meta.isTreeNodeClose = true;

const rowsToHide = this.datamanager.getChildren(rowIndex);
rowsToHide.forEach(rowIndex => {
const row = this.datamanager.getRow(rowIndex);
if (!row.meta.isLeaf) {
row.meta.isTreeNodeClose = true;
}
});

return rowsToHide;
}

closeSingleNode(rowIndex) {
const rowsToHide = this.getChildrenToHideForNode(rowIndex);
const visibleRows = this.bodyRenderer.visibleRowIndices;
const rowsToShow = visibleRows
.filter(rowIndex => !rowsToHide.includes(rowIndex))
.sort(numberSortAsc);

this.showRows(rowsToShow);
}

expandAllNodes() {
let rows = this.datamanager.getRows();
let rootNodes = rows.filter(row => !row.meta.isLeaf);

const childrenToShow = rootNodes.map(row => this.getChildrenToShowForNode(row.meta.rowIndex)).flat();
const visibleRowIndices = this.bodyRenderer.visibleRowIndices;
const rowsToShow = uniq([...childrenToShow, ...visibleRowIndices]).sort(numberSortAsc);

this.showRows(rowsToShow);
}

collapseAllNodes() {
let rows = this.datamanager.getRows();
let rootNodes = rows.filter(row => row.meta.indent === 0);

const rowsToHide = rootNodes.map(row => this.getChildrenToHideForNode(row.meta.rowIndex)).flat();
const visibleRows = this.bodyRenderer.visibleRowIndices;
const rowsToShow = visibleRows
.filter(rowIndex => !rowsToHide.includes(rowIndex))
.sort(numberSortAsc);

this.showRows(rowsToShow);
}

setTreeDepth(depth) {
let rows = this.datamanager.getRows();

const rowsToOpen = rows.filter(row => row.meta.indent < depth);
const rowsToClose = rows.filter(row => row.meta.indent >= depth);
const rowsToHide = rowsToClose.filter(row => row.meta.indent > depth);

rowsToClose.forEach(row => {
if (!row.meta.isLeaf) {
row.meta.isTreeNodeClose = true;
}
});
rowsToOpen.forEach(row => {
if (!row.meta.isLeaf) {
row.meta.isTreeNodeClose = false;
}
});

const rowsToShow = rows
.filter(row => !rowsToHide.includes(row))
.map(row => row.meta.rowIndex)
.sort(numberSortAsc);
this.showRows(rowsToShow);
}

getRow$(rowIndex) {
return $(this.selector(rowIndex), this.bodyScrollable);
}

getTotalRows() {
return this.datamanager.getRowCount();
}

getFirstRowIndex() {
return 0;
}

getLastRowIndex() {
return this.datamanager.getRowCount() - 1;
}

scrollToRow(rowIndex) {
rowIndex = +rowIndex;
this._lastScrollTo = this._lastScrollTo || 0;
const $row = this.getRow$(rowIndex);
if ($.inViewport($row, this.bodyScrollable)) return;

const {
height
} = $row.getBoundingClientRect();
const {
top,
bottom
} = this.bodyScrollable.getBoundingClientRect();
const rowsInView = Math.floor((bottom - top) / height);

let offset = 0;
if (rowIndex > this._lastScrollTo) {
offset = height * ((rowIndex + 1) - rowsInView);
} else {
offset = height * ((rowIndex + 1) - 1);
}

this._lastScrollTo = rowIndex;
$.scrollTop(this.bodyScrollable, offset);
}

getRowHTML(row, props) {
const dataAttr = makeDataAttributeString(props);
let rowIdentifier = props.rowIndex;

if (props.isFilter) {
row = row.map(cell => (Object.assign({}, cell, {
content: this.getFilterInput({
colIndex: cell.colIndex,
name: cell.name
}),
isFilter: 1,
isHeader: undefined,
editable: false
})));

rowIdentifier = 'filter';
}

if (props.isHeader) {
rowIdentifier = 'header';
}

return `
<div class="dt-row dt-row-${rowIdentifier}" ${dataAttr}>
${row.map(cell => this.cellmanager.getCellHTML(cell)).join('')}
</div>
`;
}

getFilterInput(props) {
let title = `title="Filter based on ${props.name || 'Index'}"`;
const dataAttr = makeDataAttributeString(props);
return `<input class="dt-filter dt-input" type="text" ${dataAttr} tabindex="1"
${props.colIndex === 0 ? 'disabled' : title} />`;
}

selector(rowIndex) {
return `.dt-row-${rowIndex}`;
}
}

+ 292
- 0
src/style.css 查看文件

@@ -0,0 +1,292 @@
:root {
--dt-border-color: #d1d8dd;
--dt-primary-color: rgb(82, 146, 247);
--dt-light-bg: #f5f7fa;
--dt-light-red: #FD8B8B;
--dt-light-yellow: #fffce7;
--dt-orange: rgb(255, 160, 10);
--dt-text-color: #000000;
--dt-text-light: #dfe2e5;
--dt-spacer-1: 0.25rem;
--dt-spacer-2: 0.5rem;
--dt-spacer-3: 1rem;
--dt-border-radius: 3px;
--dt-cell-bg: #fff;
--dt-focus-border-width: 2px;
--dt-selection-highlight-color: var(--dt-light-yellow);
--dt-toast-message-border: none;
--dt-header-cell-bg: var(--dt-cell-bg);
}

.datatable {
*, *::after, *::before {
box-sizing: border-box;
}
}

.datatable {
position: relative;
overflow: hidden;
}

.dt-scrollable {
height: 40vw;
overflow: auto;
border-top: 2px solid var(--dt-border-color);

&--highlight-all {
background-color: var(--dt-selection-highlight-color);
}

&__no-data {
text-align: center;
padding: var(--dt-spacer-3);
border-left: 1px solid var(--dt-border-color);
border-right: 1px solid var(--dt-border-color);
}
}

.dt-row {
display: flex;

&--highlight .dt-cell {
background-color: var(--dt-selection-highlight-color);
}

&--unhighlight .dt-cell {
background-color: var(--dt-cell-bg);
}

&--hide {
display: none;
}

&:last-child:not(.dt-row-filter) {
border-bottom: 1px solid var(--dt-border-color);
}
}

.dt-cell {
border: 1px solid var(--dt-border-color);
border-bottom: none;
border-right: none;
position: relative;
outline: none;
padding: 0;
background-color: var(--dt-cell-bg);
color: var(--dt-text-color);
/*
Fix for firefox and Edge
https://stackoverflow.com/a/16337203
firefox paints td background over border
*/
background-clip: padding-box;
user-select: none;

&__content {
padding: var(--dt-spacer-2);
border: var(--dt-focus-border-width) solid transparent;
height: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

&__edit {
display: none;
padding: var(--dt-spacer-2);
background-color: var(--dt-cell-bg);
border: var(--dt-focus-border-width) solid var(--dt-orange);
z-index: 1;
height: 100%;
}

&__resize-handle {
opacity: 0;
position: absolute;
right: -3px;
top: 0;
width: 5px;
height: 100%;
cursor: col-resize;
z-index: 1;
}

&--editing &__content {
display: none;
}

&--editing &__edit {
display: block;
}

&--focus &__content {
border-color: var(--dt-primary-color);
}

&--highlight {
background-color: var(--dt-light-bg);
}

&--dragging {
background-color: var(--dt-light-bg);
}

&--header {
background-color: var(--dt-header-cell-bg);
}

&--header:last-child {
border-right: 1px solid var(--dt-border-color);
}

&--header &__content {
padding-right: var(--dt-spacer-3);
font-weight: bold;
}

&--header:hover .dt-dropdown__toggle {
opacity: 1;
}

&--tree-close {
.icon-open {
display: none;
}

.icon-close {
display: flex;
}
}

&:last-child {
border-right: 1px solid var(--dt-border-color);
}
}

.datatable[dir=rtl] .dt-cell__resize-handle {
right: unset;
left: -3px;
}

.icon-open, .icon-close {
width: 16px;
height: 16px;
}

.icon-open {
display: flex;
}

.icon-close {
display: none;
}

.dt-dropdown {
position: absolute;
right: 10px;
display: inline-flex;
vertical-align: top;
text-align: left;
font-weight: normal;
cursor: pointer;

&__toggle {
opacity: 0;
background-color: var(--dt-header-cell-bg);
}

&__list {
position: fixed;
min-width: 8rem;
z-index: 1;
cursor: pointer;
background-color: var(--dt-cell-bg);
border-radius: var(--dt-border-radius);
padding: var(--dt-spacer-2) 0;
box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
}

&__list-item {
padding: var(--dt-spacer-2) var(--dt-spacer-3);

&:hover {
background-color: var(--dt-light-bg);
}
}

&--active &__list {
display: block;
}
}

.dt-tree-node {
display: flex;
align-items: center;
position: relative;

&__toggle {
display: inline-block;
cursor: pointer;
margin-right: 0.2rem;
}
}

.dt-toast {
position: absolute;
bottom: var(--dt-spacer-3);
left: 50%;
transform: translateX(-50%);

&__message {
display: inline-block;
background-color: rgba(0, 0, 0, 0.8);
color: var(--dt-text-light);
border-radius: var(--dt-border-radius);
padding: var(--dt-spacer-2) var(--dt-spacer-3);
border: var(--dt-toast-message-border);
}
}

.dt-input {
outline: none;
width: 100%;
border: none;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
background-color: inherit;
color: inherit;
margin: 0;
padding: 0;
}

.dt-freeze {
display: flex;
justify-content: center;
align-content: center;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: var(--dt-light-bg);
opacity: 0.5;
font-size: 2em;

&__message {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
}

.dt-paste-target {
position: fixed;
left: -999em;
}

body.dt-resize {
cursor: col-resize;
}

+ 373
- 0
src/style.js 查看文件

@@ -0,0 +1,373 @@
import $ from './dom';
import {
camelCaseToDash,
linkProperties,
throttle
} from './utils';

export default class Style {
constructor(instance) {
this.instance = instance;

linkProperties(this, this.instance, [
'options', 'datamanager', 'columnmanager',
'header', 'footer', 'bodyScrollable', 'datatableWrapper',
'getColumn', 'bodyRenderer'
]);

this.scopeClass = 'dt-instance-' + instance.constructor.instances;
instance.datatableWrapper.classList.add(this.scopeClass);

const styleEl = document.createElement('style');
instance.wrapper.insertBefore(styleEl, instance.datatableWrapper);
this.styleEl = styleEl;

this.bindResizeWindow();
this.bindScrollHeader();
}

get stylesheet() {
return this.styleEl.sheet;
}

bindResizeWindow() {
this.onWindowResize = this.onWindowResize.bind(this);
this.onWindowResize = throttle(this.onWindowResize, 300);

if (this.options.layout === 'fluid') {
$.on(window, 'resize', this.onWindowResize);
}
}

bindScrollHeader() {
this._settingHeaderPosition = false;

$.on(this.bodyScrollable, 'scroll', (e) => {
if (this._settingHeaderPosition) return;

this._settingHeaderPosition = true;

requestAnimationFrame(() => {
const left = -e.target.scrollLeft;

$.style(this.header, {
transform: `translateX(${left}px)`
});
$.style(this.footer, {
transform: `translateX(${left}px)`
});
this._settingHeaderPosition = false;
});
});
}

onWindowResize() {
this.distributeRemainingWidth();
this.refreshColumnWidth();
this.setBodyStyle();
}

destroy() {
this.styleEl.remove();
$.off(window, 'resize', this.onWindowResize);
}

setStyle(selector, styleObject) {
if (selector.includes(',')) {
selector.split(',')
.map(s => s.trim())
.forEach(selector => {
this.setStyle(selector, styleObject);
});
return;
}

selector = selector.trim();
if (!selector) return;

this._styleRulesMap = this._styleRulesMap || {};
const prefixedSelector = this._getPrefixedSelector(selector);

if (this._styleRulesMap[prefixedSelector]) {
this.removeStyle(selector);

// merge with old styleobject
styleObject = Object.assign({}, this._styleRulesMap[prefixedSelector], styleObject);
}

const styleString = this._getRuleString(styleObject);
const ruleString = `${prefixedSelector} { ${styleString} }`;

this._styleRulesMap[prefixedSelector] = styleObject;
this.stylesheet.insertRule(ruleString);
}

removeStyle(selector) {
if (selector.includes(',')) {
selector.split(',')
.map(s => s.trim())
.forEach(selector => {
this.removeStyle(selector);
});
return;
}

selector = selector.trim();
if (!selector) return;

// find and remove
const prefixedSelector = this._getPrefixedSelector(selector);
const index = Array.from(this.stylesheet.cssRules)
.findIndex(rule => rule.selectorText === prefixedSelector);

if (index === -1) return;
this.stylesheet.deleteRule(index);
}

_getPrefixedSelector(selector) {
return `.${this.scopeClass} ${selector}`;
}

_getRuleString(styleObject) {
return Object.keys(styleObject)
.map(prop => {
let dashed = prop;
if (!prop.includes('-')) {
dashed = camelCaseToDash(prop);
}
return `${dashed}:${styleObject[prop]};`;
})
.join('');
}

setDimensions() {
this.setCellHeight();
this.setupMinWidth();
this.setupNaturalColumnWidth();
this.setupColumnWidth();
this.distributeRemainingWidth();
this.setColumnStyle();
this.setBodyStyle();
}

setCellHeight() {
this.setStyle('.dt-cell', {
height: this.options.cellHeight + 'px'
});
}

setupMinWidth() {
$.each('.dt-cell--header', this.header).map(col => {
const { colIndex } = $.data(col);
const column = this.getColumn(colIndex);

if (!column.minWidth) {
const width = $.style($('.dt-cell__content', col), 'width');
// only set this once
column.minWidth = width;
}
});
}

setupNaturalColumnWidth() {
if (!$('.dt-row')) return;

$.each('.dt-row-header .dt-cell', this.header).map($headerCell => {
const { colIndex } = $.data($headerCell);
const column = this.datamanager.getColumn(colIndex);
let width = $.style($('.dt-cell__content', $headerCell), 'width');
if (typeof width === 'number' && width >= this.options.minimumColumnWidth) {
column.naturalWidth = width;
} else {
column.naturalWidth = this.options.minimumColumnWidth;
}
});

// set initial width as naturally calculated by table's first row
$.each('.dt-row-0 .dt-cell', this.bodyScrollable).map($cell => {
const {
colIndex
} = $.data($cell);
const column = this.datamanager.getColumn(colIndex);

let naturalWidth = $.style($('.dt-cell__content', $cell), 'width');

if (typeof naturalWidth === 'number' && naturalWidth >= column.naturalWidth) {
column.naturalWidth = naturalWidth;
} else {
column.naturalWidth = column.naturalWidth;
}
});
}

setupColumnWidth() {
if (this.options.layout === 'ratio') {
let totalWidth = $.style(this.datatableWrapper, 'width');

if (this.options.serialNoColumn) {
const rowIndexColumn = this.datamanager.getColumnById('_rowIndex');
totalWidth = totalWidth - rowIndexColumn.width - 1;
}

if (this.options.checkboxColumn) {
const rowIndexColumn = this.datamanager.getColumnById('_checkbox');
totalWidth = totalWidth - rowIndexColumn.width - 1;
}

const totalParts = this.datamanager.getColumns()
.map(column => {
if (column.id === '_rowIndex' || column.id === '_checkbox') {
return 0;
}
if (!column.width) {
column.width = 1;
}
column.ratioWidth = parseInt(column.width, 10);
return column.ratioWidth;
})
.reduce((a, c) => a + c);

const onePart = totalWidth / totalParts;

this.datamanager.getColumns()
.map(column => {
if (column.id === '_rowIndex' || column.id === '_checkbox') return;
column.width = Math.floor(onePart * column.ratioWidth) - 1;
});
} else {
this.datamanager.getColumns()
.map(column => {
if (!column.width) {
column.width = column.naturalWidth;
}
if (column.id === '_rowIndex') {
column.width = this.getRowIndexColumnWidth();
}
if (column.width < this.options.minimumColumnWidth) {
column.width = this.options.minimumColumnWidth;
}
});
}
}

distributeRemainingWidth() {
if (this.options.layout !== 'fluid') return;

const wrapperWidth = $.style(this.instance.datatableWrapper, 'width');
let firstRow = $('.dt-row', this.bodyScrollable);
let firstRowWidth = wrapperWidth;
if (!firstRow) {
let headerRow = $('.dt-row', this.instance.header);
let cellWidths = Array.from(headerRow.children)
.map(cell => cell.offsetWidth);
firstRowWidth = cellWidths.reduce((sum, a) => sum + a, 0);
} else {
firstRowWidth = $.style(firstRow, 'width');
}
const resizableColumns = this.datamanager.getColumns().filter(col => col.resizable);
const deltaWidth = (wrapperWidth - firstRowWidth) / resizableColumns.length;

resizableColumns.map(col => {
const width = $.style(this.getColumnHeaderElement(col.colIndex), 'width');
let finalWidth = Math.floor(width + deltaWidth) - 2;

this.datamanager.updateColumn(col.colIndex, {
width: finalWidth
});
});
}

setColumnStyle() {
// align columns
this.datamanager.getColumns()
.map(column => {
// alignment
if (!column.align) {
column.align = 'left';
}
if (!['left', 'center', 'right'].includes(column.align)) {
column.align = 'left';
}
this.setStyle(`.dt-cell--col-${column.colIndex}`, {
'text-align': column.align
});

// width
this.columnmanager.setColumnHeaderWidth(column.colIndex);
this.columnmanager.setColumnWidth(column.colIndex);
});
}

refreshColumnWidth() {
this.datamanager.getColumns()
.map(column => {
this.columnmanager.setColumnHeaderWidth(column.colIndex);
this.columnmanager.setColumnWidth(column.colIndex);
});
}

setBodyStyle() {
const bodyWidth = $.style(this.datatableWrapper, 'width');
const firstRow = $('.dt-row', this.bodyScrollable);
if (!firstRow) return;
const rowWidth = $.style(firstRow, 'width');

let width = bodyWidth > rowWidth ? rowWidth : bodyWidth;
$.style(this.bodyScrollable, {
width: width + 'px'
});

// remove the body height, so that it resets to it's original
$.removeStyle(this.bodyScrollable, 'height');

// when there are less rows than the container
// adapt the container height
let bodyHeight = $.getStyle(this.bodyScrollable, 'height');
const scrollHeight = (this.bodyRenderer.hyperlist || {})._scrollHeight || Infinity;
const hasHorizontalOverflow = $.hasHorizontalOverflow(this.bodyScrollable);

let height;

if (scrollHeight < bodyHeight) {
height = scrollHeight;

// account for scrollbar size when
// there is horizontal overflow
if (hasHorizontalOverflow) {
height += $.scrollbarSize();
}

$.style(this.bodyScrollable, {
height: height + 'px'
});
}

const verticalOverflow = this.bodyScrollable.scrollHeight - this.bodyScrollable.offsetHeight;
if (verticalOverflow < $.scrollbarSize()) {
// if verticalOverflow is less than scrollbar size
// then most likely scrollbar is causing the scroll
// which is not needed
$.style(this.bodyScrollable, {
overflowY: 'hidden'
});
}

if (this.options.layout === 'fluid') {
$.style(this.bodyScrollable, {
overflowX: 'hidden'
});
}
}

getColumnHeaderElement(colIndex) {
colIndex = +colIndex;
if (colIndex < 0) return null;
return $(`.dt-cell--col-${colIndex}`, this.header);
}

getRowIndexColumnWidth() {
const rowCount = this.datamanager.getRowCount();
const padding = 22;
return $.measureTextWidth(rowCount + '') + padding;
}
}

+ 30
- 0
src/translationmanager.js 查看文件

@@ -0,0 +1,30 @@
import { format } from './utils';
import getTranslations from './translations';

export default class TranslationManager {
constructor(language) {
this.language = language;
this.translations = getTranslations();
}

addTranslations(translations) {
this.translations = Object.assign(this.translations, translations);
}

translate(sourceText, args) {
let translation = (this.translations[this.language] &&
this.translations[this.language][sourceText]) || sourceText;

if (typeof translation === 'object') {
translation = args && args.count ?
this.getPluralizedTranslation(translation, args.count) :
sourceText;
}

return format(translation, args || {});
}

getPluralizedTranslation(translations, count) {
return translations[count] || translations['default'];
}
};

+ 15
- 0
src/translations/de.json 查看文件

@@ -0,0 +1,15 @@
{
"Sort Ascending": "Aufsteigend sortieren",
"Sort Descending": "Absteigend sortieren",
"Reset sorting": "Sortierung zurücksetzen",
"Remove column": "Spalte entfernen",
"No Data": "Keine Daten",
"{count} cells copied": {
"1": "{count} Zelle kopiert",
"default": "{count} Zellen kopiert"
},
"{count} rows selected": {
"1": "{count} Zeile ausgewählt",
"default": "{count} Zeilen ausgewählt"
}
}

+ 15
- 0
src/translations/en.json 查看文件

@@ -0,0 +1,15 @@
{
"Sort Ascending": "Sort Ascending",
"Sort Descending": "Sort Descending",
"Reset sorting": "Reset sorting",
"Remove column": "Remove column",
"No Data": "No Data",
"{count} cells copied": {
"1": "{count} cell copied",
"default": "{count} cells copied"
},
"{count} rows selected": {
"1": "{count} row selected",
"default": "{count} rows selected"
}
}

+ 15
- 0
src/translations/fr.json 查看文件

@@ -0,0 +1,15 @@
{
"Sort Ascending": "Trier par ordre croissant",
"Sort Descending": "Trier par ordre décroissant",
"Reset sorting": "Réinitialiser le tri",
"Remove column": "Supprimer colonne",
"No Data": "Pas de données",
"{count} cells copied": {
"1": "{count} cellule copiée",
"default": "{count} cellules copiées"
},
"{count} rows selected": {
"1": "{count} ligne sélectionnée",
"default": "{count} lignes sélectionnées"
}
}

+ 13
- 0
src/translations/index.js 查看文件

@@ -0,0 +1,13 @@
import en from './en.json';
import de from './de.json';
import fr from './fr.json';
import it from './it.json';

export default function getTranslations() {
return {
en,
de,
fr,
it,
};
};

+ 15
- 0
src/translations/it.json 查看文件

@@ -0,0 +1,15 @@
{
"Sort Ascending": "Ordinamento ascendente",
"Sort Descending": "Ordinamento decrescente",
"Reset sorting": "Azzeramento ordinamento",
"Remove column": "Rimuovi colonna",
"No Data": "Nessun dato",
"{count} cells copied": {
"1": "Copiato {count} cella",
"default": "{count} celle copiate"
},
"{count} rows selected": {
"1": "{count} linea selezionata",
"default": "{count} linee selezionate"
}
}

+ 151
- 0
src/utils.js 查看文件

@@ -0,0 +1,151 @@
import _throttle from 'lodash/throttle';
import _debounce from 'lodash/debounce';
import _uniq from 'lodash/uniq';

export function camelCaseToDash(str) {
return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`);
}

export function makeDataAttributeString(props) {
const keys = Object.keys(props);

return keys
.map((key) => {
const _key = camelCaseToDash(key);
const val = props[key];

if (val === undefined) return '';
return `data-${_key}="${val}" `;
})
.join('')
.trim();
}

export function copyTextToClipboard(text) {
// https://stackoverflow.com/a/30810322/5353542
var textArea = document.createElement('textarea');

//
// *** This styling is an extra step which is likely not required. ***
//
// Why is it here? To ensure:
// 1. the element is able to have focus and selection.
// 2. if element was to flash render it has minimal visual impact.
// 3. less flakyness with selection and copying which **might** occur if
// the textarea element is not visible.
//
// The likelihood is the element won't even render, not even a flash,
// so some of these are just precautions. However in IE the element
// is visible whilst the popup box asking the user for permission for
// the web page to copy to the clipboard.
//

// Place in top-left corner of screen regardless of scroll position.
textArea.style.position = 'fixed';
textArea.style.top = 0;
textArea.style.left = 0;

// Ensure it has a small width and height. Setting to 1px / 1em
// doesn't work as this gives a negative w/h on some browsers.
textArea.style.width = '2em';
textArea.style.height = '2em';

// We don't need padding, reducing the size if it does flash render.
textArea.style.padding = 0;

// Clean up any borders.
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';

// Avoid flash of white box if rendered for any reason.
textArea.style.background = 'transparent';

textArea.value = text;

document.body.appendChild(textArea);

textArea.select();

try {
document.execCommand('copy');
} catch (err) {
console.log('Oops, unable to copy');
}

document.body.removeChild(textArea);
}

export function isNumeric(val) {
return !isNaN(val);
}

export let throttle = _throttle;

export let debounce = _debounce;

export function nextTick(fn, context = null) {
return (...args) => {
return new Promise(resolve => {
const execute = () => {
const out = fn.apply(context, args);
resolve(out);
};
setTimeout(execute);
});
};
};

export function linkProperties(target, source, properties) {
const props = properties.reduce((acc, prop) => {
acc[prop] = {
get() {
return source[prop];
}
};
return acc;
}, {});
Object.defineProperties(target, props);
};

export function isSet(val) {
return val !== undefined || val !== null;
}

export function notSet(val) {
return !isSet(val);
}

export function isNumber(val) {
return !isNaN(val);
}

export function ensureArray(val) {
if (!Array.isArray(val)) {
return [val];
}
return val;
}

export function uniq(arr) {
return _uniq(arr);
}

export function numberSortAsc(a, b) {
return a - b;
};

export function stripHTML(html) {
return html.replace(/<[^>]*>/g, '');
};

export function format(str, args) {
if (!str) return str;

Object.keys(args).forEach(arg => {
let regex = new RegExp(`{(${arg})}`, 'g');
str = str.replace(regex, args[arg]);
});

return str;
};

+ 148
- 0
test/datamanager.spec.js 查看文件

@@ -0,0 +1,148 @@
/* global describe, it, before */

import chai from 'chai';
import DataManager, {
DataError
} from '../src/datamanager';

chai.expect();
const expect = chai.expect;

describe.only('DataManager instance', () => {

it('should initialize rows and columns given options', () => {
const datamanager = getDataManagerInstance();
expect(datamanager).has.property('rows');
expect(datamanager).has.property('columns');
expect(datamanager.rowCount).to.equal(3);
expect(datamanager._serialNoColumnAdded).to.equal(false);
expect(datamanager._checkboxColumnAdded).to.equal(false);
});

describe('prepareRows', () => {
const datamanager = getDataManagerInstance();

it('should properly build row object when bare minimum options are given', () => {
const firstRow = datamanager.getRow(0);
expect(firstRow).to.deep.equal([
{
colIndex: 0,
content: 'Faris',
rowIndex: 0
},
{
colIndex: 1,
content: 'faris@test.com',
rowIndex: 0
},
{
colIndex: 2,
content: 'Software Developer',
rowIndex: 0
}
]);
});

it('should throw when rows parameter is not an Array', () => {
expect(() => datamanager.init({
columns: ['Name'],
rows: 2
})).to.throw(DataError, '`rows` must be an array');
});

it('should throw when any of the row\'s length doesn\'t match column length', () => {
expect(() => datamanager.init({
columns: ['Name'],
rows: [[]]
})).to.throw(DataError, 'column length');
});

it('should not throw given valid data', () => {
expect(() => datamanager.init({
columns: ['Name'],
rows: [['Faris']]
})).to.not.throw();
});

});

describe('prepareColumns', () => {
const datamanager = getDataManagerInstance();

it('should properly build column object with bare minimum options', () => {
const firstColumn = datamanager.getColumn(0);
expect(firstColumn.colIndex).eq(0);
expect(firstColumn.content).eq('Name');
expect(firstColumn.isHeader).eq(1);
});

it('should throw when columns parameter is not an Array', () => {
expect(() => datamanager.init({
columns: 2
})).to.throw(DataError, 'must be an array');
});

it('should throw when any of the column is not a string or object', () => {
expect(() => datamanager.init({
columns: [2]
})).to.throw(DataError, 'must be a string or an object');
});

it('should not throw given valid params', () => {
expect(() => datamanager.init({
columns: ['Name'],
rows: [['Test']]
})).to.not.throw();
});

it('should properly build column object when editable is false', () => {
const data = {
columns: [
{ content: 'Name', editable: false }
],
rows: [
['Faris']
]
};
datamanager.init(data);
const firstColumn = datamanager.getColumn(0);
expect(firstColumn.colIndex).eq(0);
expect(firstColumn.content).eq('Name');
expect(firstColumn.isHeader).eq(1);
});
});

describe('prepareNumericColumns', () => {
const datamanager = getDataManagerInstance();
it('should assign `align: right` to columns with numeric data', () => {
datamanager.init({
columns: ['Name', 'Number'],
rows: [
['Faris', '123']
]
});

const column0 = datamanager.getColumn(0);
const column1 = datamanager.getColumn(1);
expect(column0.align).to.not.equal('right');
expect(column1.align).to.equal('right');
});
});
});

function getDataManagerInstance(opts = {}) {
const options = Object.assign({}, {
data: {
columns: ['Name', 'Email', 'Occupation'],
rows: [
['Faris', 'faris@test.com', 'Software Developer'],
['Manas', 'manas@test.com', 'Software Engineer'],
['Ameya', 'ameya@test.com', 'Hacker']
]
}
}, opts);

const datamanager = new DataManager(options);
datamanager.init();
return datamanager;
}

+ 81
- 0
test/utils.spec.js 查看文件

@@ -0,0 +1,81 @@
/* global describe, it, before */

import chai from 'chai';
import {
makeDataAttributeString,
getCSSString,
buildCSSRule,
removeCSSRule
} from '../src/utils.js';

chai.expect();
const expect = chai.expect;

describe('#utils', () => {
describe('makeDataAttributeString', () => {
it('should return the correct data-attr string', () => {
const props = {
isHeader: 1,
colIndex: 0,
rowIndex: 4
};

expect(makeDataAttributeString(props))
.to.be.equal('data-is-header="1" data-col-index="0" data-row-index="4"');
});
});

describe('getCSSString', () => {
it('should return CSS key value pairs', () => {
const style = {
width: '2px',
height: '4px',
'margin-top': '3px'
};

expect(getCSSString(style))
.to.be.equal('width: 2px; height: 4px; margin-top: 3px;');
});
});

describe('buildCSSRule', () => {
it('should return CSS rule string with updated properties', () => {
const rule = '.test';
const style = {
width: '2px',
height: '4px',
'margin-top': '3px'
};

const ruleString = buildCSSRule(rule, style);

expect(ruleString)
.to.be.equal('.test { width: 2px; height: 4px; margin-top: 3px; }');

const updatedRuleString = buildCSSRule(rule, { width: '5px' }, ruleString);

expect(updatedRuleString)
.to.be.equal('.test { width: 5px; height: 4px; margin-top: 3px; }');

const updatedRuleString2 = buildCSSRule(rule, { height: '19px' }, updatedRuleString);

expect(updatedRuleString2)
.to.be.equal('.test { width: 5px; height: 19px; margin-top: 3px; }');

const updatedRuleString3 = buildCSSRule('.test2', { height: '45px' }, updatedRuleString2);

expect(updatedRuleString3)
.to.be.equal('.test { width: 5px; height: 19px; margin-top: 3px; }.test2 { height: 45px; }');
});
});

describe('removeCSSRule', () => {
it('should remove the css rule based on the selector', () => {
const rule = '.test';
const cssRuleString = `.test {margin-top: 2px;} .test2 {color: blue;}`;

expect(removeCSSRule(rule, cssRuleString))
.to.be.equal('.test2 {color: blue;}');
})
})
});

+ 9313
- 0
yarn.lock
文件差異過大導致無法顯示
查看文件


Loading…
取消
儲存