├── app ├── .gitkeep ├── utils │ └── array-utils.js ├── components │ └── sortable-js.js ├── services │ └── drag-store.js └── tailwind │ └── config.js ├── addon ├── .gitkeep ├── services │ └── drag-store.js ├── components │ ├── sortable-js.hbs │ └── sortable-js.js └── utils │ └── array-utils.js ├── vendor └── .gitkeep ├── tests ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ ├── .gitkeep │ │ │ └── cancelable.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ ├── clone.js │ │ │ ├── application.js │ │ │ ├── thresholds.js │ │ │ ├── simple.js │ │ │ ├── cancelable.js │ │ │ └── shared.js │ │ ├── app.js │ │ ├── router.js │ │ ├── templates │ │ │ ├── simple.hbs │ │ │ ├── clone.hbs │ │ │ ├── disable.hbs │ │ │ ├── filter.hbs │ │ │ ├── cancelable.hbs │ │ │ ├── thresholds.hbs │ │ │ ├── handle.hbs │ │ │ ├── shared.hbs │ │ │ └── application.hbs │ │ ├── styles │ │ │ └── app.css │ │ └── index.html │ ├── public │ │ └── robots.txt │ └── config │ │ ├── optional-features.json │ │ ├── targets.js │ │ └── environment.js ├── test-helper.js ├── unit │ ├── services │ │ └── drag-store-test.js │ └── utils │ │ └── array-utils-test.js ├── index.html └── integration │ └── components │ └── sortable-js-test.js ├── cypress.json ├── .watchmanconfig ├── .template-lintrc.js ├── ember-sortablejs.png ├── index.js ├── config ├── environment.js └── ember-try.js ├── jsconfig.json ├── .dependabot └── config.yml ├── cypress ├── .eslintrc.js ├── support │ ├── index.js │ └── commands.js ├── plugins │ └── index.js └── integration │ └── drag_drop.spec.js ├── .ember-cli ├── .eslintignore ├── blueprints └── ember-sortablejs │ └── index.js ├── .editorconfig ├── .gitignore ├── .npmignore ├── testem.js ├── CONTRIBUTING.md ├── LICENSE.md ├── .eslintrc.js ├── ember-cli-build.js ├── .travis.yml ├── package.json ├── addon-test-support └── dndsimulator.js └── README.md /app/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "video": false 3 | } 4 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /app/utils/array-utils.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-sortablejs/utils/array-utils'; 2 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'octane' 5 | }; 6 | -------------------------------------------------------------------------------- /app/components/sortable-js.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-sortablejs/components/sortable-js'; -------------------------------------------------------------------------------- /app/services/drag-store.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-sortablejs/services/drag-store'; 2 | -------------------------------------------------------------------------------- /ember-sortablejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SortableJS/ember-sortablejs/HEAD/ember-sortablejs.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: require('./package').name 5 | }; 6 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/cancelable.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default class CancelableRoute extends Route { 4 | } 5 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | {"compilerOptions":{"target":"es6","experimentalDecorators":true},"exclude":["node_modules","bower_components","tmp","vendor",".git","dist"]} -------------------------------------------------------------------------------- /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | update_configs: 3 | - package_manager: "javascript" 4 | directory: "/" 5 | update_schedule: "live" 6 | default_labels: 7 | - "dependabot" 8 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/clone.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | 3 | export default class CloneController extends Controller { 4 | onClone() { 5 | console.log('*** Action *** - onClone'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cypress/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | ecmaVersion: 2018, 4 | sourceType: 'module', 5 | }, 6 | env: { 7 | 'cypress/globals': true, 8 | }, 9 | extends: ['plugin:cypress/recommended'] 10 | }; 11 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /addon/services/drag-store.js: -------------------------------------------------------------------------------- 1 | import Service from '@ember/service'; 2 | 3 | export default class DragStoreService extends Service { 4 | dragStartInstance = null; 5 | dragAddInstance = null; 6 | 7 | reset() { 8 | this.dragStartInstance = null; 9 | this.dragAddInstance = null; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | 17 | # ember-try 18 | /.node_modules.ember-try/ 19 | /bower.json.ember-try 20 | /package.json.ember-try 21 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { inject as service } from '@ember/service'; 3 | 4 | export default class ApplicationController extends Controller { 5 | @service router; 6 | 7 | get inHomeRoute() { 8 | return this.router.currentRouteName === 'index'; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /addon/components/sortable-js.hbs: -------------------------------------------------------------------------------- 1 | {{#let (element this.element) as |Element|}} 2 | 9 | {{yield this.mappedList}} 10 | 11 | {{/let}} 12 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /tests/unit/services/drag-store-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | module('Unit | Service | drag-store', function(hooks) { 5 | setupTest(hooks); 6 | 7 | // Replace this with your real tests. 8 | test('it exists', function(assert) { 9 | let service = this.owner.lookup('service:drag-store'); 10 | assert.ok(service); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /blueprints/ember-sortablejs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | description: 'adds sortablejs to application', 5 | 6 | normalizeEntityName: function() { 7 | // this prevents an error when the entityName is 8 | // not specified (since that doesn't actually matter 9 | // to us 10 | }, 11 | 12 | afterInstall() { 13 | return this.addPackageToProject('sortablejs', '^1.10.x') 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /connect.lock 16 | /coverage/ 17 | /libpeerconnection.log 18 | /npm-debug.log* 19 | /testem.log 20 | /yarn-error.log 21 | 22 | # ember-try 23 | /.node_modules.ember-try/ 24 | /bower.json.ember-try 25 | /package.json.ember-try 26 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function() { 10 | this.route('simple'); 11 | this.route('shared'); 12 | this.route('clone'); 13 | this.route('disable'); 14 | this.route('handle'); 15 | this.route('filter'); 16 | this.route('thresholds'); 17 | this.route('cancelable'); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/thresholds.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { action } from '@ember/object'; 3 | import { tracked } from '@glimmer/tracking'; 4 | 5 | export default class ThresholdsController extends Controller { 6 | @tracked rangeValue = 0.25; 7 | 8 | get thresholdHeight() { 9 | return this.rangeValue * 100; 10 | } 11 | 12 | @action 13 | setRange(sortable, evt) { 14 | this.set('rangeValue', evt.target.value); 15 | sortable.option('swapThreshold', evt.target.value); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintignore 14 | /.eslintrc.js 15 | /.git/ 16 | /.gitignore 17 | /.template-lintrc.js 18 | /.travis.yml 19 | /.watchmanconfig 20 | /bower.json 21 | /config/ember-try.js 22 | /CONTRIBUTING.md 23 | /ember-cli-build.js 24 | /testem.js 25 | /tests/ 26 | /yarn.lock 27 | .gitkeep 28 | 29 | # ember-try 30 | /.node_modules.ember-try/ 31 | /bower.json.ember-try 32 | /package.json.ember-try 33 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: [ 5 | 'Chrome' 6 | ], 7 | launch_in_dev: [ 8 | 'Chrome' 9 | ], 10 | browser_args: { 11 | Chrome: { 12 | ci: [ 13 | // --no-sandbox is needed when running Chrome inside a container 14 | process.env.CI ? '--no-sandbox' : null, 15 | '--headless', 16 | '--disable-dev-shm-usage', 17 | '--disable-software-rasterizer', 18 | '--mute-audio', 19 | '--remote-debugging-port=0', 20 | '--window-size=1440,900' 21 | ].filter(Boolean) 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd ember-sortablejs` 7 | * `npm install` 8 | 9 | ## Linting 10 | 11 | * `npm run lint:hbs` 12 | * `npm run lint:js` 13 | * `npm run lint:js -- --fix` 14 | 15 | ## Running tests 16 | 17 | * `ember test` – Runs the test suite on the current Ember version 18 | * `ember test --server` – Runs the test suite in "watch mode" 19 | * `ember try:each` – Runs the test suite against multiple Ember versions 20 | 21 | ## Running the dummy application 22 | 23 | * `ember serve` 24 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 25 | 26 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | } 22 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/simple.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Simple list

4 |
5 |
6 |
7 |
8 |
9 | 25 | {{#each list as |item index| }} 26 |
{{item.value.title}}
27 | {{/each}} 28 |
29 |
30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /addon/utils/array-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Moves an array element from one index to another 3 | * @param {Array} arr 4 | * @param {Number} from 5 | * @param {Number} to 6 | * @returns {Array} [ ] 7 | */ 8 | export function move(arr, from, to) { 9 | const clone = [...arr]; 10 | clone.splice(to, 0, clone.splice(from, 1)[0]); 11 | return clone; 12 | } 13 | 14 | /** 15 | * Insert an item to an array in the specified index 16 | * @param {Array} arr 17 | * @param {Number} index 18 | * @param {any} item 19 | * @returns {Array} [ ] 20 | */ 21 | export function insertAt(arr, index, item) { 22 | const clone = [...arr]; 23 | clone.splice(index, 0, item) 24 | return clone; 25 | } 26 | 27 | /** 28 | * Insert an item to an array in the specified index 29 | * @param {Array} arr 30 | * @param {Number} index 31 | * @returns {Array} [ ] 32 | */ 33 | export function removeFrom(arr, index) { 34 | const clone = [...arr]; 35 | clone.splice(index, 1); 36 | return clone; 37 | } 38 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | 27 | require('@4tw/cypress-drag-drop') 28 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | 3 | @import "tailwindcss/components"; 4 | /* @import "components.css"; */ 5 | 6 | @import "tailwindcss/utilities"; 7 | /* @import "utilities.css"; */ 8 | 9 | .ghost-class { 10 | background-color: #e04e39; 11 | color: #fff; 12 | } 13 | 14 | .bg-yellow { 15 | background-color: rgb(253, 255, 143); 16 | } 17 | 18 | .shared-list div { 19 | width: 100% 20 | } 21 | 22 | .handle { 23 | cursor: grab; 24 | } 25 | 26 | .filter { 27 | background-color: #e04e39; 28 | } 29 | 30 | .ember-sortable-js { 31 | width: 200px; 32 | } 33 | 34 | .pretty-list .item { 35 | background-color: #A10808; 36 | display: flex; 37 | flex-direction: column; 38 | justify-content: center; 39 | align-items: center; 40 | text-align: center; 41 | height: 150px; 42 | border: 1px solid grey; 43 | margin: 20px; 44 | width: 150px; 45 | } 46 | 47 | .threshold-indicator { 48 | display: flex; 49 | flex-direction: column ; 50 | text-align: center; 51 | justify-content: center; 52 | align-items: center; 53 | background-color: #cf2b2b; 54 | width: 100%; 55 | } 56 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/simple.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { tracked } from '@glimmer/tracking'; 3 | 4 | export default class SimpleController extends Controller { 5 | @tracked list = [ 6 | { title: 'one' }, 7 | { title: 'two' }, 8 | { title: 'three' }, 9 | { title: 'four' }, 10 | { title: 'five' }, 11 | ]; 12 | onChoose() { 13 | console.log('*** Action *** - onChoose'); 14 | } 15 | onUnchoose() { 16 | console.log('*** Action *** - onUnchoose'); 17 | } 18 | onStart() { 19 | console.log('*** Action *** - onStart'); 20 | } 21 | onEnd() { 22 | console.log('*** Action *** - onEnd'); 23 | } 24 | onMove() { 25 | console.log('*** Action *** - onMove'); 26 | } 27 | onUpdate() { 28 | console.log('*** Action *** - onUpdate'); 29 | } 30 | onAdd() { 31 | console.log('*** Action *** - onAdd'); 32 | } 33 | onRemove() { 34 | console.log('*** Action *** - onAdd'); 35 | } 36 | onClone() { 37 | //_onDragStart 38 | console.log('*** Action *** - onClone'); 39 | } 40 | onChange() { 41 | console.log('*** Action *** - onChange'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 All contributors to ember-sortablejs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | 18 | 19 | {{content-for "body"}} 20 | 21 | 22 | 23 | 24 | 25 | {{content-for "body-footer"}} 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/clone.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Cloning

4 |
5 |
6 |
7 |
8 |
9 | 20 | {{#each list as |item index| }} 21 |
{{item.item}}
22 | {{/each}} 23 |
24 |
25 | 36 | {{#each list as |item index| }} 37 |
{{item.item}}
38 | {{/each}} 39 |
40 |
41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/disable.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Disable Sorting

4 |
5 |
6 |
7 |
8 |
9 | 20 | {{#each list as |item index| }} 21 |
{{item.item}}
22 | {{/each}} 23 |
24 |
25 |
26 |
27 | 38 | {{#each list as |item index| }} 39 |
{{item.item}}
40 | {{/each}} 41 |
42 |
43 |
44 | 45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | ecmaVersion: 2018, 6 | sourceType: 'module', 7 | ecmaFeatures: { 8 | legacyDecorators: true 9 | } 10 | }, 11 | plugins: [ 12 | 'ember' 13 | ], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:ember/recommended' 17 | ], 18 | env: { 19 | browser: true 20 | }, 21 | rules: { 22 | 'ember/order-in-components': [2], 23 | 'ember/no-jquery': 'error' 24 | }, 25 | overrides: [ 26 | // node files 27 | { 28 | files: [ 29 | '.eslintrc.js', 30 | '.template-lintrc.js', 31 | 'ember-cli-build.js', 32 | 'index.js', 33 | 'testem.js', 34 | 'blueprints/*/index.js', 35 | 'config/**/*.js', 36 | 'tests/dummy/config/**/*.js' 37 | ], 38 | excludedFiles: [ 39 | 'addon/**', 40 | 'addon-test-support/**', 41 | 'app/**', 42 | 'tests/dummy/app/**', 43 | ], 44 | parserOptions: { 45 | sourceType: 'script' 46 | }, 47 | env: { 48 | browser: false, 49 | node: true 50 | }, 51 | plugins: ['node'], 52 | rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { 53 | // add your custom rules and overrides for node files here 54 | }) 55 | } 56 | ] 57 | }; 58 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/filter.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Filtering draggable objects

4 |
5 |
6 |
7 |
8 |
9 | 13 |
Item 1
14 |
Item 2
15 |
Item 3
16 |
Item 4
17 |
Item 5
18 |
19 |
20 |
21 |
22 |       
23 |         <SortableJs
24 |           class="list-group"
25 |           @options={{hash animation=150 handle=".handle"}}
26 |         >
27 |           <div class="list-group-item"></span>Item 1</div>
28 |           <div class="list-group-item"></span>Item 2</div>
29 |           <div class="list-group-item filter"></span>Item 3</div>
30 |           <div class="list-group-item"></span>Item 4</div>
31 |           <div class="list-group-item"></span>Item 5</div>
32 |         </SortableJs>
33 |       
34 |     
35 |
36 |
37 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | const isProduction = EmberAddon.env() === 'production'; 5 | 6 | const purgeCSS = { 7 | module: require('@fullhuman/postcss-purgecss'), 8 | options: { 9 | content: [ 10 | // add extra paths here for components/controllers which include tailwind classes 11 | './app/index.html', 12 | './app/templates/**/*.hbs', 13 | './app/components/**/*.hbs', 14 | ], 15 | defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || [] 16 | } 17 | }; 18 | 19 | module.exports = function(defaults) { 20 | let app = new EmberAddon(defaults, { 21 | // Add options here 22 | postcssOptions: { 23 | compile: { 24 | plugins: [ 25 | { 26 | module: require('postcss-import'), 27 | options: { 28 | path: ['node_modules'] 29 | } 30 | }, 31 | require('tailwindcss')('./app/tailwind/config.js'), 32 | ...isProduction ? [purgeCSS] : [] 33 | ] 34 | } 35 | } 36 | }); 37 | 38 | /* 39 | This build file specifies the options for the dummy test app of this 40 | addon, located in `/tests/dummy` 41 | This build file does *not* influence how the addon or the app using it 42 | behave. You most likely want to be modifying `./index.js` or app's build file 43 | */ 44 | 45 | return app.toTree(); 46 | }; 47 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | } 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /tests/unit/utils/array-utils-test.js: -------------------------------------------------------------------------------- 1 | import { move, insertAt, removeFrom } from 'ember-sortablejs/utils/array-utils'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Utility | array-utils', function() { 5 | 6 | // Replace this with your real tests. 7 | module('#move', function() { 8 | test('it moves an array element from one position to another ', function(assert) { 9 | const testArray = ['a', 'b', 'c', 'd', 'e']; 10 | const expected = ['b', 'c', 'd', 'a', 'e']; 11 | const actual = move(testArray, 0, 3); 12 | 13 | assert.ok(actual !== testArray); 14 | assert.deepEqual(actual, expected); 15 | }); 16 | }); 17 | 18 | module('#insertAt', function () { 19 | test('it inserts an element to an array in specified index', function(assert) { 20 | const testArray = ['a', 'b', 'c', 'd', 'e']; 21 | const expected = ['a', 'b', 'f', 'c', 'd', 'e']; 22 | const actual = insertAt(testArray, 2, 'f'); 23 | 24 | assert.ok(actual !== testArray); 25 | assert.deepEqual(actual, expected); 26 | }); 27 | }); 28 | 29 | module('#removeFrom', function () { 30 | test('it removes an array element from a specified index', function(assert) { 31 | const testArray = ['a', 'b', 'c', 'd', 'e']; 32 | const expected = ['a', 'b', 'c', 'e']; 33 | const actual = removeFrom(testArray, 3); 34 | 35 | assert.ok(actual !== testArray); 36 | assert.deepEqual(actual, expected); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/cancelable.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Shared list

4 |
5 |
6 |
7 |
8 |
9 | 18 | {{#each list as |item| }} 19 |
{{item.value.name}}
20 | {{/each}} 21 |
22 |
23 |
24 | 33 | {{#each list as |item| }} 34 |
{{item.value.name}}
35 | {{/each}} 36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 | {{#if this.cancelCB}} 44 | 45 | {{/if}} 46 |
47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | # Node 10.3+ includes npm@6 which has good "npm ci" command 4 | - "10.9" 5 | 6 | addons: 7 | chrome: stable 8 | apt: 9 | packages: 10 | - libgconf-2-4 11 | 12 | cache: 13 | npm: true 14 | directories: 15 | - $HOME/.cache 16 | 17 | env: 18 | global: 19 | # See https://git.io/vdao3 for details. 20 | - JOBS=1 21 | 22 | branches: 23 | only: 24 | - master 25 | # npm version tags 26 | - /^v\d+\.\d+\.\d+/ 27 | 28 | jobs: 29 | fail_fast: true 30 | allow_failures: 31 | - env: EMBER_TRY_SCENARIO=ember-canary 32 | 33 | include: 34 | # runs linting and tests with current locked deps 35 | - stage: "Tests" 36 | name: "Tests" 37 | script: 38 | # - npm run lint:hbs 39 | # - npm run lint:js 40 | - npm test 41 | - npm start & 42 | - wait-on http://localhost:4200 43 | - cypress run 44 | 45 | - stage: "Additional Tests" 46 | name: "Floating Dependencies" 47 | install: 48 | - npm ci 49 | script: 50 | - npm test 51 | 52 | # we recommend new addons test the current and previous LTS 53 | # as well as latest stable release (bonus points to beta/canary) 54 | - env: EMBER_TRY_SCENARIO=ember-3.13 55 | - env: EMBER_TRY_SCENARIO=ember-3.14 56 | - env: EMBER_TRY_SCENARIO=ember-release 57 | - env: EMBER_TRY_SCENARIO=ember-beta 58 | - env: EMBER_TRY_SCENARIO=ember-canary 59 | - env: EMBER_TRY_SCENARIO=ember-default-with-jquery 60 | 61 | script: 62 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO 63 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/thresholds.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Filtering draggable objects

4 |
5 |
6 |
7 |
8 |
9 | 18 |
19 | {{! template-lint-disable no-inline-styles }} 20 | {{! template-lint-disable style-concatenation }} 21 |
Item 1
22 |
23 |
24 | {{! template-lint-disable no-inline-styles }} 25 | {{! template-lint-disable style-concatenation }} 26 |
Item 2
27 |
28 |
29 | Threshold:  30 |
31 |
32 |
33 |
34 |
35 |       
36 |         <SortableJs
37 |           class="pretty-list"
38 |           @options={{hash animation=150 swapThreshold={{this.rangeValue}}}}
39 |         >
40 |           <div class="list-group-item"></span>Item 1</div>
41 |         </SortableJs>
42 |       
43 |     
44 |
45 |
46 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/handle.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Using drag handles

4 |
5 |
6 |
7 |
8 |
9 | 13 |
 Item 1
14 |
 Item 2
15 |
 Item 3
16 |
 Item 4
17 |
 Item 5
18 |
19 |
20 |
21 |
22 |       
23 |         <SortableJs
24 |           class="list-group"
25 |           @options={{hash animation=150 handle=".handle"}}
26 |         >
27 |           <div class="list-group-item"><span class="handle">☰</span>Item 1</div>
28 |           <div class="list-group-item"><span class="handle">☰</span>Item 2</div>
29 |           <div class="list-group-item"><span class="handle">☰</span>Item 3</div>
30 |           <div class="list-group-item"><span class="handle">☰</span>Item 4</div>
31 |           <div class="list-group-item"><span class="handle">☰</span>Item 5</div>
32 |         </SortableJs>
33 |       
34 |     
35 |
36 |
37 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/shared.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Shared list

4 |
5 |
6 |
7 |
8 |
9 | 19 | {{#each list as |item index| }} 20 |
21 | {{item.value.name}} 22 | {{item.value.name}} 23 |
24 | {{/each}} 25 |
26 |
27 |
28 | 38 | {{#each list as |item index|}} 39 |
40 | {{item.value.name}} 41 | {{item.value.name}} 42 |
43 | {{/each}} 44 |
45 |
46 |
47 | 48 |
49 |
50 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = async function() { 6 | return { 7 | scenarios: [ 8 | { 9 | name: 'ember-3.13', 10 | npm: { 11 | devDependencies: { 12 | 'ember-source': '~3.13.0' 13 | } 14 | } 15 | }, 16 | { 17 | name: 'ember-3.14', 18 | npm: { 19 | devDependencies: { 20 | 'ember-source': '~3.14.0' 21 | } 22 | } 23 | }, 24 | { 25 | name: 'ember-release', 26 | npm: { 27 | devDependencies: { 28 | 'ember-source': await getChannelURL('release') 29 | } 30 | } 31 | }, 32 | { 33 | name: 'ember-beta', 34 | npm: { 35 | devDependencies: { 36 | 'ember-source': await getChannelURL('beta') 37 | } 38 | } 39 | }, 40 | { 41 | name: 'ember-canary', 42 | npm: { 43 | devDependencies: { 44 | 'ember-source': await getChannelURL('canary') 45 | } 46 | } 47 | }, 48 | // The default `.travis.yml` runs this scenario via `npm test`, 49 | // not via `ember try`. It's still included here so that running 50 | // `ember try:each` manually or from a customized CI config will run it 51 | // along with all the other scenarios. 52 | { 53 | name: 'ember-default', 54 | npm: { 55 | devDependencies: {} 56 | } 57 | }, 58 | { 59 | name: 'ember-default-with-jquery', 60 | env: { 61 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 62 | 'jquery-integration': true 63 | }) 64 | }, 65 | npm: { 66 | devDependencies: { 67 | '@ember/jquery': '^0.5.1' 68 | } 69 | } 70 | } 71 | ] 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | 32 |
33 | {{#if this.inHomeRoute}} 34 |
35 |
36 |

Ember-SortableJS

37 |

Drag and Drop with sort for ambitious applications

38 | 39 | Get Started 40 | 41 |
42 |
43 |
44 |
45 | {{else}} 46 | {{outlet}} 47 | {{/if}} 48 |
49 | -------------------------------------------------------------------------------- /cypress/integration/drag_drop.spec.js: -------------------------------------------------------------------------------- 1 | describe('Ember SortableJS ', function() { 2 | it('renders a list', function() { 3 | const initial = ['one', 'two', 'three', 'four', 'five']; 4 | 5 | cy.visit('http://localhost:4200/simple'); 6 | 7 | cy 8 | .get('div[data-list-item]') 9 | .should(($divs) => { 10 | expect($divs).to.have.length(5); 11 | 12 | const renderedList = $divs.map((i, el) => el.innerText); 13 | 14 | expect(renderedList.get()).to.deep.eq(initial); 15 | }); 16 | }); 17 | 18 | it('reorders as list from top to bottom', function() { 19 | const sorted = ['two', 'three', 'four', 'five', 'one']; 20 | 21 | cy.visit('http://localhost:4200/simple'); 22 | cy.get('div[data-list-item="0"').drag('div[data-list-item="4"', { force: true, position: 'bottom' }) 23 | 24 | cy 25 | .get('div[data-list-item]') 26 | .should(($divs) => { 27 | expect($divs).to.have.length(5); 28 | 29 | const renderedList = $divs.map((i, el) => el.innerText); 30 | 31 | expect(renderedList.get()).to.deep.eq(sorted); 32 | }); 33 | }); 34 | 35 | it('reorders a list bottom to top', function() { 36 | const sorted = ['five', 'one', 'two', 'three', 'four']; 37 | 38 | cy.visit('http://localhost:4200/simple'); 39 | cy.get('div[data-list-item="4"').drag('div[data-list-item="0"', { force: true, position: 'center' }) 40 | 41 | cy 42 | .get('div[data-list-item]') 43 | .should(($divs) => { 44 | expect($divs).to.have.length(5); 45 | 46 | const renderedList = $divs.map((i, el) => el.innerText); 47 | 48 | expect(renderedList.get()).to.deep.eq(sorted); 49 | }); 50 | }); 51 | 52 | it('adds an item from one list to another', function() { 53 | cy.visit('http://localhost:4200/shared'); 54 | cy.get('.list-a > div[data-list-item="0"').drag('.list-b > div[data-list-item="1"', { force: true, position: 'top' }); 55 | 56 | const listA = ['Jaden', 'Gustavo']; 57 | const listB = ['Lance', 'Luis', 'Britni', 'Kelly']; 58 | 59 | cy 60 | .get('.list-a > div[data-list-item]') 61 | .should(($divs) => { 62 | expect($divs).to.have.length(2); 63 | 64 | const renderedList = $divs.map((i, el) => el.innerText); 65 | 66 | expect(renderedList.get()).to.deep.eq(listA); 67 | }); 68 | 69 | cy 70 | .get('.list-b > div[data-list-item]') 71 | .should(($divs) => { 72 | expect($divs).to.have.length(4); 73 | 74 | const renderedList = $divs.map((i, el) => el.innerText); 75 | 76 | expect(renderedList.get()).to.deep.eq(listB); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/cancelable.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { tracked } from '@glimmer/tracking'; 3 | import { action } from '@ember/object'; 4 | 5 | class Employee { 6 | @tracked sort; 7 | @tracked name; 8 | @tracked type; 9 | @tracked img; 10 | id; 11 | 12 | constructor(sort, { name, type, id, img}) { 13 | this.id = id; 14 | this.sort = sort; 15 | this.name = name; 16 | this.type = type; 17 | this.img = img; 18 | } 19 | } 20 | 21 | export default class CancelableController extends Controller { 22 | @tracked list = []; 23 | @tracked devSort = [1, 2, 3]; 24 | @tracked pmSort = [1, 2, 3]; 25 | @tracked cancelCB = null; 26 | 27 | constructor() { 28 | super(...arguments); 29 | setTimeout(() => { 30 | const all = [ 31 | { id: 1, name: 'Luis', type: 'dev', img: 'https://api.adorable.io/avatars/25/luis' }, 32 | { id: 2, name: 'Jaden', type: 'dev', img: 'https://api.adorable.io/avatars/25/jaden' }, 33 | { id: 3, name: 'Gustavo', type: 'dev', img: 'https://api.adorable.io/avatars/25/gustavo' }, 34 | { id: 4 ,name: 'Lance', type: 'pm', img: 'https://api.adorable.io/avatars/25/lance' }, 35 | { id: 5, name: 'Britni', type: 'pm', img: 'https://api.adorable.io/avatars/25/britni' }, 36 | { id: 6, name: 'Kelly', type: 'pm', img: 'https://api.adorable.io/avatars/25/kelly' } 37 | ]; 38 | this.list = all.map((person, i) => new Employee(i += 1, person)); 39 | }, 5000); 40 | } 41 | 42 | get devList() { 43 | return this.list.filter(employee => employee.type === 'dev'); 44 | } 45 | 46 | get sortedDevList() { 47 | return [...this.devList].sort((a, b) => a.id - b.id); 48 | } 49 | 50 | get pmList() { 51 | return this.list.filter(employee => employee.type === 'pm'); 52 | } 53 | 54 | get sortedPmList() { 55 | return [...this.pmList].sort((a, b) => a.id - b.id); 56 | } 57 | 58 | onSort(list, evt) { 59 | console.log(`*** Action *** - onSort list ${list}`, evt); 60 | } 61 | 62 | onAdd(list, evt) { 63 | console.log(`*** Action *** - onAdd list ${list}`, evt); 64 | } 65 | 66 | onRemove(list, evt) { 67 | console.log(`*** Action *** - onRemove list ${list}`, evt); 68 | } 69 | 70 | @action 71 | onEnd(list, evt, cancelCB) { 72 | console.log('args log', arguments[0]); 73 | console.log('args log', arguments[1]); 74 | console.log('args log', arguments[2]); 75 | this.cancelCB = cancelCB; 76 | console.log(`*** Action *** - onEnd list ${list}`, evt); 77 | } 78 | 79 | @action 80 | cancelDrag() { 81 | this.cancelCB?.(); 82 | this.cancelCB = null; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/shared.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { tracked } from '@glimmer/tracking'; 3 | import { action } from '@ember/object'; 4 | 5 | class Employee { 6 | @tracked sort; 7 | @tracked name; 8 | @tracked type; 9 | @tracked img; 10 | id; 11 | 12 | constructor(sort, { name, type, id, img}) { 13 | this.id = id; 14 | this.sort = sort; 15 | this.name = name; 16 | this.type = type; 17 | this.img = img; 18 | } 19 | } 20 | 21 | export default class SharedController extends Controller { 22 | @tracked list = []; 23 | @tracked devSort = [1, 2, 3]; 24 | @tracked pmSort = [1, 2, 3]; 25 | 26 | constructor() { 27 | super(...arguments); 28 | const all = [ 29 | { id: 1, name: 'Luis', type: 'dev', img: 'https://api.adorable.io/avatars/25/luis' }, 30 | { id: 2, name: 'Jaden', type: 'dev', img: 'https://api.adorable.io/avatars/25/jaden' }, 31 | { id: 3, name: 'Gustavo', type: 'dev', img: 'https://api.adorable.io/avatars/25/gustavo' }, 32 | { id: 4 ,name: 'Lance', type: 'pm', img: 'https://api.adorable.io/avatars/25/lance' }, 33 | { id: 5, name: 'Britni', type: 'pm', img: 'https://api.adorable.io/avatars/25/britni' }, 34 | { id: 6, name: 'Kelly', type: 'pm', img: 'https://api.adorable.io/avatars/25/kelly' } 35 | ]; 36 | this.list = all.map((person, i) => new Employee(i += 1, person)); 37 | } 38 | 39 | get devList() { 40 | return this.list.filter(employee => employee.type === 'dev'); 41 | } 42 | 43 | get sortedDevList() { 44 | return this.devList.sort((a, b) => a.sort - b.sort); 45 | } 46 | 47 | get pmList() { 48 | return this.list.filter(employee => employee.type === 'pm'); 49 | } 50 | 51 | get sortedPmList() { 52 | return this.pmList.sort((a, b) => a.sort - b.sort); 53 | } 54 | 55 | onSort(evt, list) { 56 | // console.log(`*** Action *** - onSort list ${list}`, evt); 57 | } 58 | 59 | onAdd(list, evt) { 60 | console.log(`*** Action *** - onAdd list ${list}`, evt); 61 | } 62 | 63 | onRemove(evt, list) { 64 | // console.log(`*** Action *** - onRemove list ${list}`, evt); 65 | } 66 | 67 | @action 68 | onEnd(list, evt) { 69 | // this.list = [...this.list]; 70 | // console.log(`*** Action *** - onEnd list ${list}`, evt); 71 | // const { 72 | // from: source, 73 | // to: target, 74 | // newIndex, 75 | // item, 76 | // } = evt; 77 | 78 | // const type = target.dataset.groupType; 79 | // const record = this.list.find(r => r.id === Number(item.dataset.recordId)); 80 | 81 | // console.log('type', type); 82 | // console.log('recordid', record); 83 | 84 | // record.type = type; 85 | // record.sort = newIndex + 1; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-sortablejs", 3 | "version": "2.0.0-beta.6", 4 | "description": "EmberJS wrapper for SortableJS", 5 | "keywords": [ 6 | "ember-addon", 7 | "ember", 8 | "ember-cli", 9 | "sortable", 10 | "sortablejs", 11 | "drag", 12 | "drop", 13 | "drag and drop" 14 | ], 15 | "repository": "https://github.com/SortableJS/ember-sortablejs", 16 | "license": "MIT", 17 | "author": "Luis Vegerano", 18 | "directories": { 19 | "doc": "doc", 20 | "test": "tests" 21 | }, 22 | "scripts": { 23 | "build": "ember build", 24 | "lint:hbs": "ember-template-lint .", 25 | "lint:js": "eslint .", 26 | "start": "ember serve", 27 | "test": "ember test", 28 | "test:all": "ember try:each", 29 | "cypress:run": "cypress run", 30 | "cypress:open": "cypress open" 31 | }, 32 | "dependencies": { 33 | "@ember/render-modifiers": "^1.0.2", 34 | "@glimmer/component": "^1.0.0", 35 | "@glimmer/tracking": "^1.0.0", 36 | "ember-auto-import": "^1.5.3", 37 | "ember-cli-babel": "^7.18.0", 38 | "ember-cli-htmlbars": "^4.2.0", 39 | "ember-element-helper": "^0.2.0" 40 | }, 41 | "devDependencies": { 42 | "@4tw/cypress-drag-drop": "^1.3.1", 43 | "@ember/optional-features": "^1.3.0", 44 | "@fullhuman/postcss-purgecss": "^2.1.0", 45 | "@types/sortablejs": "^1.10.2", 46 | "babel-eslint": "^10.0.3", 47 | "broccoli-asset-rev": "^3.0.0", 48 | "cypress": "^4.2.0", 49 | "ember-cli": "~3.15.0", 50 | "ember-cli-dependency-checker": "^3.2.0", 51 | "ember-cli-inject-live-reload": "^2.0.1", 52 | "ember-cli-postcss": "^5.0.0", 53 | "ember-cli-sri": "^2.1.1", 54 | "ember-cli-template-lint": "^1.0.0-beta.3", 55 | "ember-cli-uglify": "^3.0.0", 56 | "ember-disable-prototype-extensions": "^1.1.3", 57 | "ember-export-application-global": "^2.0.1", 58 | "ember-load-initializers": "^2.1.1", 59 | "ember-maybe-import-regenerator": "^0.1.6", 60 | "ember-qunit": "^4.6.0", 61 | "ember-resolver": "^7.0.0", 62 | "ember-source": "~3.15.0", 63 | "ember-source-channel-url": "^2.0.1", 64 | "ember-try": "^1.4.0", 65 | "eslint": "^6.8.0", 66 | "eslint-plugin-cypress": "^2.10.3", 67 | "eslint-plugin-ember": "^7.7.1", 68 | "eslint-plugin-node": "^10.0.0", 69 | "loader.js": "^4.7.0", 70 | "postcss-import": "^12.0.1", 71 | "qunit-dom": "^0.9.2", 72 | "sortablejs": "^1.10.2", 73 | "tailwindcss": "^1.2.0", 74 | "wait-on": "^4.0.1" 75 | }, 76 | "peerDependencies": { 77 | "sortablejs": ">= 1.10.2 < 2" 78 | }, 79 | "engines": { 80 | "node": "10.* || >= 12" 81 | }, 82 | "ember": { 83 | "edition": "octane" 84 | }, 85 | "ember-addon": { 86 | "configPath": "tests/dummy/config" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /addon-test-support/dndsimulator.js: -------------------------------------------------------------------------------- 1 | function getPositionAtCenter(element) { 2 | const {top, left, width, height} = element.getBoundingClientRect(); 3 | return { 4 | x: left + width / 2, 5 | y: top + height / 2 6 | }; 7 | } 8 | 9 | function getDistanceBetweenElements(a, b) { 10 | const aPosition = getPositionAtCenter(a); 11 | const bPosition = getPositionAtCenter(b); 12 | 13 | return Math.hypot(aPosition.x - bPosition.x, aPosition.y - bPosition.y); 14 | } 15 | 16 | function createEvent (eventName, options) { 17 | let event = {}; 18 | event.cancelable = true; 19 | event.bubbles = true; 20 | 21 | /* if the clientX and clientY options are specified, 22 | also calculated the desired screenX and screenY values */ 23 | if (options.x && options.y) { 24 | event.screenX = window.screenX + options.x; 25 | event.screenY = window.screenY + options.y; 26 | event.clientX = options.x; 27 | event.clientY = options.y; 28 | delete options.x; 29 | delete options.y; 30 | } 31 | 32 | event = Object.assign(event, options) 33 | 34 | switch (true) { 35 | case eventName.includes('pointer'): 36 | return new PointerEvent(eventName, event); 37 | case eventName.includes('mouse'): 38 | return new MouseEvent(eventName, event) 39 | case eventName.includes('drag'): 40 | case eventName.includes('drop'): 41 | return new DragEvent(eventName, event); 42 | default: 43 | return new CustomEvent(eventName, event); 44 | } 45 | } 46 | 47 | /** 48 | * @public 49 | * @param {HTMLElement} sourceElement - Element to be dragged 50 | * @param {HTMLElement} targetElement - Element where source element is being dropped 51 | * @returns {Promise} 52 | */ 53 | export async function simulateDrag (sourceElement, targetElement) { 54 | 55 | /* get the coordinates of both elements, note that 56 | left refers to X, and top to Y */ 57 | const sourceCoordinates = sourceElement.getBoundingClientRect(); 58 | const targetCoordinates = targetElement.getBoundingClientRect(); 59 | 60 | const distance = getDistanceBetweenElements(sourceElement, targetElement) + 5; 61 | 62 | /* simulate a mouse down event on the coordinates 63 | of the source element */ 64 | const mouseDownEvent = createEvent( 65 | "pointerdown", 66 | { 67 | x: sourceCoordinates.left, 68 | y: sourceCoordinates.top 69 | } 70 | ); 71 | 72 | sourceElement.dispatchEvent(mouseDownEvent); 73 | 74 | /* simulate a drag start event on the source element */ 75 | const dragStartEvent = createEvent( 76 | "dragstart", 77 | { 78 | x: sourceCoordinates.left, 79 | y: sourceCoordinates.top, 80 | dataTransfer: new DataTransfer() 81 | } 82 | ); 83 | 84 | sourceElement.dispatchEvent(dragStartEvent); 85 | 86 | await new Promise((resolve) => setTimeout(() => resolve(), 1000)); 87 | 88 | /* simulate a drag event on the source element */ 89 | const dragEvent = createEvent( 90 | "drag", 91 | { 92 | x: sourceCoordinates.left, 93 | y: sourceCoordinates.top 94 | } 95 | ); 96 | 97 | sourceElement.dispatchEvent(dragEvent); 98 | 99 | /* simulate a drag enter event on the target element */ 100 | const dragEnterEvent = createEvent( 101 | "dragenter", 102 | { 103 | x: targetCoordinates.left, 104 | y: sourceCoordinates.top + distance, 105 | dataTransfer: dragStartEvent.dataTransfer 106 | } 107 | ); 108 | 109 | targetElement.dispatchEvent(dragEnterEvent); 110 | 111 | /* simulate a drag over event on the target element */ 112 | const dragOverEvent = createEvent( 113 | "dragover", 114 | { 115 | x: targetCoordinates.left, 116 | y: sourceCoordinates.top + distance, 117 | dataTransfer: dragStartEvent.dataTransfer 118 | } 119 | ); 120 | 121 | targetElement.dispatchEvent(dragOverEvent); 122 | 123 | /* simulate a drop event on the target element */ 124 | const dropEvent = createEvent( 125 | "drop", 126 | { 127 | x: targetCoordinates.left, 128 | y: sourceCoordinates.top + distance, 129 | dataTransfer: dragStartEvent.dataTransfer 130 | } 131 | ); 132 | 133 | targetElement.dispatchEvent(dropEvent); 134 | 135 | /* simulate a drag end event on the source element */ 136 | const dragEndEvent = createEvent( 137 | "dragend", 138 | { 139 | x: targetCoordinates.left, 140 | y: sourceCoordinates.top + distance, 141 | dataTransfer: dragStartEvent.dataTransfer 142 | } 143 | ); 144 | 145 | sourceElement.dispatchEvent(dragEndEvent); 146 | 147 | /* simulate a mouseup event on the target element */ 148 | const mouseUpEvent = createEvent( 149 | "mouseup", 150 | { 151 | x: targetCoordinates.left, 152 | y: sourceCoordinates.top + distance 153 | } 154 | ); 155 | 156 | targetElement.dispatchEvent(mouseUpEvent); 157 | } 158 | -------------------------------------------------------------------------------- /addon/components/sortable-js.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import Sortable from 'sortablejs'; 3 | import { bind, next } from '@ember/runloop'; 4 | import { action } from '@ember/object'; 5 | import { tracked } from '@glimmer/tracking'; 6 | import { move, insertAt, removeFrom } from 'ember-sortablejs/utils/array-utils'; 7 | import { inject as service } from '@ember/service'; 8 | 9 | export default class SortableJsComponent extends Component { 10 | @service dragStore; 11 | 12 | @tracked list = []; 13 | 14 | cachedIdentity = new WeakMap(); 15 | 16 | cachedList = null; 17 | hasUpdatedList = false; // Used to prevent unwanted renders. Probably there's a better way to do this. 18 | #sortableContainer = null; 19 | sortableInstance = null; 20 | internalEvents = [ 21 | 'onStart', 22 | 'onAdd', 23 | 'onUpdate', 24 | 'onRemove', 25 | 'onEnd', 26 | ]; 27 | #events = [ 28 | 'onMove', 29 | 'onChange', 30 | 'onChoose', 31 | 'onUnchoose', 32 | 'onSort', 33 | 'onClone', 34 | 'scrollFn', 35 | 'setData', 36 | 'onFilter', 37 | 'onSpill', 38 | ]; 39 | 40 | get element() { 41 | return this.args.tag || 'div'; 42 | } 43 | 44 | get mappedList() { 45 | return this.list.map((item) => this.cachedIdentity.get(item)); 46 | } 47 | 48 | @action 49 | setOptions() { 50 | for (let [key, value] of Object.entries(this.args.options)) { 51 | this.setOption(key, value); 52 | } 53 | } 54 | 55 | /** 56 | * 57 | * @param {HTMLElement} element 58 | */ 59 | @action 60 | didInsert(element) { 61 | const defaults = {}; 62 | const options = Object.assign({}, defaults, this.args.options); 63 | 64 | this.#sortableContainer = element; 65 | 66 | next(this, () => { 67 | this.sortableInstance = Sortable.create(element, options); 68 | this.setupEventHandlers(); 69 | this.setupInternalEventHandlers(); 70 | this.setList(); 71 | }); 72 | } 73 | 74 | @action 75 | setList() { 76 | this.args.items?.forEach((item) => { 77 | const isObject = item && (typeof item === 'object'); 78 | 79 | if (!isObject) throw new TypeError('Item is not an Object'); 80 | 81 | if (!this.cachedIdentity.has(item)) { 82 | this.setIdentity(item); 83 | } 84 | }); 85 | this.list = [...(this.args.items || [])]; 86 | } 87 | 88 | @action 89 | cancelDnD() { 90 | if (this.cachedList) { 91 | this.list = [...this.cachedList]; 92 | this.cachedList = null; 93 | this.dragStore.dragAddInstance?.cancelDnD(); 94 | } 95 | this.dragStore.reset(); 96 | } 97 | 98 | willDestroy() { 99 | if (this.isDestroying || this.isDestroyed) return; 100 | this.sortableInstance.destroy(); 101 | this.dragStore.reset(); 102 | } 103 | 104 | onUpdate(evt) { 105 | const { 106 | newIndex, 107 | oldIndex, 108 | } = evt; 109 | 110 | [this.list[oldIndex], this.list[newIndex]].forEach((item) => this.setIdentity(item)); 111 | 112 | this.sync(evt, move(this.list, oldIndex, newIndex)); 113 | this.hasUpdatedList = true; 114 | this.args?.onUpdate?.(evt); 115 | } 116 | 117 | onRemove(evt) { 118 | const { 119 | oldIndex, 120 | } = evt; 121 | 122 | if (evt.pullMode !== 'clone') { 123 | this.sync(evt, removeFrom(this.list, oldIndex)); 124 | this.hasUpdatedList = true; 125 | } 126 | 127 | this.args?.onRemove?.(evt); 128 | } 129 | 130 | onAdd(evt) { 131 | evt.item.remove(); 132 | this.cachedList = [...this.list]; 133 | this.dragStore.dragAddInstance = this; 134 | const { 135 | oldIndex, 136 | newIndex, 137 | } = evt; 138 | 139 | const oldItem = this.dragStore.dragStartInstance.list[oldIndex]; 140 | 141 | this.setIdentity(oldItem); 142 | 143 | this.sync(evt, insertAt(this.list, newIndex, oldItem)); 144 | this.args?.onAdd?.(evt); 145 | } 146 | 147 | onStart(evt) { 148 | this.cachedList = [...this.list]; 149 | this.dragStore.dragStartInstance = this; 150 | this.args?.onStart?.(evt); 151 | } 152 | 153 | onEnd(evt) { 154 | if (!this.hasUpdatedList) { 155 | evt.item.remove(); 156 | this.list = this.list.map((item) => { 157 | this.setIdentity(item); 158 | return item; 159 | }); 160 | 161 | this.sync(evt, this.list); 162 | } 163 | 164 | this.args?.onEnd?.(evt, this.cancelDnD); 165 | this.hasUpdatedList = false; 166 | } 167 | 168 | setIdentity(obj) { 169 | if (obj && (typeof obj === 'object')) { 170 | this.cachedIdentity.set(obj, { value: obj }); 171 | } 172 | } 173 | 174 | sync({ item }, changedArray) { 175 | item.remove(); 176 | this.list = [...changedArray]; 177 | } 178 | 179 | setupEventHandlers() { 180 | this.#events.forEach(eventName => { 181 | const action = this.args[eventName]; 182 | if (typeof action === 'function') { 183 | this.sortableInstance.option(eventName, bind(this, 'performExternalAction', eventName)); 184 | } 185 | }); 186 | } 187 | 188 | setupInternalEventHandlers() { 189 | this.internalEvents.forEach(eventName => { 190 | this.sortableInstance.option(eventName, bind(this, this[eventName])); 191 | }); 192 | } 193 | 194 | performExternalAction(actionName, ...args) { 195 | this.args[actionName]?.(...args) 196 | } 197 | 198 | setOption(option, value) { 199 | this.sortableInstance.option(option, value); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ember-sortablejs 2 | ============================================================================== 3 | [![Build Status](https://travis-ci.org/SortableJS/ember-sortablejs.svg?branch=master)](https://travis-ci.org/SortableJS/ember-sortablejs) 4 | [![Ember Observer Score](https://emberobserver.com/badges/ember-sortablejs.svg)](https://emberobserver.com/addons/ember-sortablejs) 5 | 6 | This addon allows you to use drag and drop in your ember application using [SortableJS/Sortable](https://github.com/SortableJS/Sortable) 7 | 8 | Compatibility 9 | ------------------------------------------------------------------------------ 10 | 11 | * Ember.js v3.13 or above 12 | * Ember CLI v3.13 or above 13 | * Node.js v10 or above 14 | 15 | Installation 16 | ------------------------------------------------------------------------------ 17 | > **NOTE**: The beta version is out. Please give me a hand and test it out. 18 | ``` 19 | ember install ember-sortablejs@beta 20 | ``` 21 | 22 | This addon has a peer dependency on `sortablejs` that will be installed with the addon 23 | 24 | Still to do 25 | ------------------------------------------------------------------------------ 26 | Refer to the upcoming [project](https://github.com/SortableJS/ember-sortablejs/projects/2) 27 | 28 | Library support 29 | ------------------------------------------------------------------------------ 30 | Currently supported: 31 | - [x] Drag from one list to another 32 | - [x] Sort 33 | - [ ] Clone 34 | - [ ] Swap 35 | - [ ] Multi Drag 36 | - [ ] Nested List 37 | 38 | Usage 39 | ------------------------------------------------------------------------------ 40 | 41 | ```hbs 42 | {{!-- this.list = [{ name: 'item one' }, { name: 'item two' },..] --}} 43 | 50 | {{#each list as |item|}} 51 |
{{item.value.name}}
52 | {{/each}} 53 |
54 | ``` 55 | 56 | How it works 57 | ------------------------------------------------------------------------------ 58 | SortableJs works by manipulating the DOM directly this is NOT compatible with 59 | the Glimmer VM. To mitigate this we need tu use SortableJs as a middle man and use 60 | the events it emits to update state and prevent the DOM manipulation the library does. 61 | 62 | This is accomplished by maintaining an internal list. This list is a copy of the 63 | array supplied via `@items`. The events `onStart`, `onEnd`, `onUpdate`, `onAdd`, 64 | `onRemove` are intercepted to prevent DOM manipulation and maintaining the internal 65 | list. 66 | 67 | You HAVE to provide an object. As the addon uses a WeakMap to cache the items supplied. 68 | When SortableJs emits we update the list and the cache to make changes that will update 69 | the DOM. The addon will ***yield*** an array of objects. Each object contains the key `value`, 70 | which is the original object supplied via `@items`. 71 | 72 | I you have ideas on how to approach this better. Please open an issue 😄 73 | 74 | Caveats 75 | ------------------------------------------------------------------------------ 76 | - Not all SortableJS plugins work... yet. 77 | - While you could bypass `@items` I highly discourage since the library manipulates the DOM directly. 78 | 79 | Options 80 | ------------------------------------------------------------------------------ 81 | The addon supports all the options that sortable accepts, see: https://github.com/SortableJS/Sortable#options 82 | 83 | Component API 84 | ------------------------------------------------------------------------------ 85 | |arg|type|description| 86 | |:---|:---:|:---| 87 | | `@items` | Array | A list of objecs to be managed by the addon | 88 | | `@options` | Object | A hash options supported by SortableJs| 89 | | `@tag` | String | The element to be used to render the list (default: "div")| 90 | | `@onChoose` | Function | (SortablejsEvent) => {...} | 91 | | `@onUnchoose` | Function | (SortablejsEvent) => {...} | 92 | | `@onStart` | Function | (SortablejsEvent) => {...} | 93 | | `@onEnd` | Function | (SortablejsEvent, cancelDnD) => {...} | 94 | | `@onAdd` | Function | (SortablejsEvent) => {...} | 95 | | `@onUpdate` | Function | (SortablejsEvent) => {...} | 96 | | `@onSort` | Function | (SortablejsEvent) => {...} | 97 | | `@onRemove` | Function | (SortablejsEvent) => {...} | 98 | | `@onMove` | Function | (SortablejsMoveEvent) => {...} | 99 | | `@onClone` | Function | (SortablejsEvent) => {...} | 100 | | `@onChange` | Function | (SortablejsEvent) => {...} | 101 | | `@scrollFn` | Function | (SortablejsEvent) => {...} | 102 | | `@setData` | Function | (SortablejsEvent) => {...} | 103 | | `@onFilter` | Function | (SortablejsEvent) => {...} | 104 | | `@onSpill` | Function | (SortablejsEvent) => {...} | 105 | 106 | `SortablejsEvent` - A [`CustomEvent`](https://github.com/SortableJS/Sortable#event-object-demo) provided by SortableJS 107 | 108 | `SortablejsMoveEvent` - A [`CustomEvent`](https://github.com/SortableJS/Sortable#move-event-object) provided by SortableJS 109 | 110 | `cancelDnD` - A callback provided by the ember addon to basically undo you last drag and drop or sort; 111 | 112 | `{{yield}}` - An array of objects with the key `value` where its value is the object supplied. `{ value: }` 113 | 114 | Migrating from 1.x 115 | ------------------------------------------------------------------------------ 116 | - `onSetData` is no longer suported. Rename argument to `setData`. 117 | - `` no longer expects a wrapped list. Instead the addon itself will act as the sortable list container. 118 | 119 | v1 120 | ```hbs 121 | 124 |
    125 |
  • Item 1
  • 126 |
  • Item 2
  • 127 |
  • Item 3
  • 128 |
  • Item 4
  • 129 |
  • Item 5
  • 130 |
131 |
132 | ``` 133 | 134 | v2 135 | ```hbs 136 | {{!-- this.list = [{ name: 'item one' }, { name: 'item two' },..] --}} 137 | 143 | {{#each list as |item|}} 144 |
{{item.value.name}}
145 | {{/each}} 146 |
147 | ``` 148 | License 149 | ------------------------------------------------------------------------------ 150 | 151 | This project is licensed under the [GPL-3.0 License](LICENSE.md). 152 | -------------------------------------------------------------------------------- /tests/integration/components/sortable-js-test.js: -------------------------------------------------------------------------------- 1 | import { module, test, skip } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, find, findAll } from '@ember/test-helpers'; 4 | import { simulateDrag } from 'ember-sortablejs/test-support/dndsimulator'; 5 | import hbs from 'htmlbars-inline-precompile'; 6 | 7 | module('Integration | Component | sortable-js', function(hooks) { 8 | setupRenderingTest(hooks); 9 | 10 | hooks.before(function() { 11 | this.list = [ 12 | { title: 'item one' }, 13 | { title: 'item two' }, 14 | { title: 'item three' }, 15 | { title: 'item four' }, 16 | { title: 'item five' }, 17 | ]; 18 | }); 19 | 20 | test('it renders with a dynamic tag supplied', async function(assert) { 21 | await render(hbs` 22 | 23 | `); 24 | 25 | const list = find('.rendered-list'); 26 | assert.equal(list.nodeName, 'DIV') 27 | 28 | 29 | await render(hbs` 30 | 31 | `); 32 | 33 | const anotherList = find('.another-list'); 34 | assert.equal(anotherList.nodeName, 'UL') 35 | }); 36 | 37 | test('it renders a list', async function(assert) { 38 | await render(hbs` 39 | 44 | {{#each list as |item| }} 45 |
{{item.value.title}}
46 | {{/each}} 47 |
48 | `); 49 | 50 | const listItem = findAll('.list-item'); 51 | assert.equal(listItem.length, 5); 52 | }); 53 | 54 | test('it reorders a list', async function(assert) { 55 | assert.expect(9); 56 | const done = assert.async(); 57 | 58 | this.onChoose = (event) => { 59 | assert.ok(event); 60 | }; 61 | this.onStart = (event) => { 62 | assert.ok(event); 63 | }; 64 | this.onClone = (event) => { 65 | assert.ok(event); 66 | }; 67 | this.onMove = (event) => { 68 | assert.ok(event); 69 | }; 70 | this.onChange = (event) => { 71 | assert.ok(event); 72 | }; 73 | this.onUnchoose = (event) => { 74 | assert.ok(event); 75 | }; 76 | this.onUpdate = (event) => { 77 | assert.ok(event); 78 | }; 79 | this.onEnd = (event) => { 80 | assert.ok(event); 81 | done(); 82 | }; 83 | 84 | await render(hbs` 85 | 98 | {{#each list as |item index| }} 99 |
{{item.value.title}}
100 | {{/each}} 101 |
102 | `); 103 | 104 | const listItemZero = find('div[data-list-item="0"]'); 105 | const listItemThree = find('div[data-list-item="3"]'); 106 | 107 | const expectedOrder = [ 108 | 'item two', 109 | 'item three', 110 | 'item four', 111 | 'item one', 112 | 'item five', 113 | ]; 114 | 115 | await simulateDrag(listItemZero, listItemThree); 116 | 117 | const actualOrder = Array.from(findAll('div[data-list-item]')).map(node => node.innerText); 118 | 119 | assert.deepEqual(actualOrder, expectedOrder); 120 | }); 121 | 122 | skip('it moves and element from one list to another', async function (assert) { 123 | const done = assert.async(); 124 | this.onChoose = (event) => assert.ok(event, 'onChoose'); 125 | this.onStart = (event) => assert.ok(event, 'onStart'); 126 | this.onClone = (event) => assert.ok(event, 'onClone'); 127 | this.onMove = (event) => assert.ok(event, 'onMove'); 128 | this.onChange = (event) => assert.ok(event, 'onChange'); 129 | this.onUnchoose = (event) => assert.ok(event, 'onUnchoose'); 130 | this.onUpdate = (event) => assert.ok(event, 'onUpdate'); 131 | this.onRemove = (event) => assert.ok(event, 'onRemove'); 132 | this.onAdd = (event) => assert.ok(event, 'onAdd'); 133 | this.onEnd = (event) => { 134 | assert.ok(event, 'onEnd'); 135 | done(); 136 | }; 137 | 138 | await render(hbs` 139 | 150 | {{#each list as |item index| }} 151 |
{{item.value.title}}
152 | {{/each}} 153 |
154 |
155 | 162 | {{#each list as |item index| }} 163 |
{{item.value.title}}
164 | {{/each}} 165 |
166 | `); 167 | 168 | const listAItemFive = find('.list-a > div[data-list-item="2"]'); 169 | const listBItemOne = find('.list-b > div[data-list-item="2"]'); 170 | 171 | await simulateDrag(listAItemFive, listBItemOne); 172 | 173 | const listAOrder = Array.from(findAll('.list-a > div[data-list-item]')).map(node => node.innerText); 174 | const listBOrder = Array.from(findAll('.list-b > div[data-list-item]')).map(node => node.innerText); 175 | 176 | const expectedListAOrder = [ 177 | 'item one', 178 | 'item two', 179 | 'item three', 180 | 'item four', 181 | ]; 182 | const expectedListBOrder = [ 183 | 'item five', 184 | 'item one', 185 | 'item two', 186 | 'item three', 187 | 'item four', 188 | 'item five', 189 | ]; 190 | 191 | assert.deepEqual(listAOrder, expectedListAOrder); 192 | assert.deepEqual(listBOrder, expectedListBOrder); 193 | }); 194 | 195 | test('it dynamically sets options', async function(assert) { 196 | let options = { 197 | animation: 150, 198 | ghostClass: 'ghost-class', 199 | }; 200 | 201 | this.set('options', options); 202 | 203 | // Template block usage: 204 | await render(hbs` 205 | 211 | {{#each list as |item| }} 212 |
{{item.value.title}}
213 | {{/each}} 214 |
215 | `); 216 | 217 | const element = find('.list-group'); 218 | const sortableKey = Object.keys(element).pop(); 219 | const sortableInstance = element[sortableKey]; 220 | 221 | assert.equal(sortableInstance.option('ghostClass'), 'ghost-class'); 222 | assert.equal(sortableInstance.option('animation'), 150); 223 | 224 | options = { 225 | animation: 100, 226 | ghostClass: 'foo', 227 | }; 228 | 229 | this.set('options', options); 230 | 231 | assert.equal(sortableInstance.option('ghostClass'), 'foo'); 232 | assert.equal(sortableInstance.option('animation'), 100); 233 | }); 234 | 235 | 236 | 237 | // test('it reorders a list', async function(assert) { 238 | // const done = assert.async(); 239 | // const onChoose = () => assert.ok(true, 'onChosse was called'); 240 | // const onStart = () => assert.ok(true, 'onStart was called'); 241 | // const onMove = () => assert.ok(true, 'onMove was called'); 242 | // const onEnd = () => { 243 | // assert.ok(true, 'onEnd was called'); 244 | // done(); 245 | // } 246 | 247 | // this.set('onChoose', onChoose); 248 | // this.set('onStart', onStart); 249 | // this.set('onMove', onMove); 250 | // this.set('onEnd', onEnd); 251 | 252 | // // Template block usage: 253 | // await render(hbs` 254 | // 262 | //
Item 1
263 | //
Item 2
264 | //
Item 3
265 | //
Item 4
266 | //
Item 5
267 | //
268 | // `); 269 | 270 | // const listItemOne = find('div[data-testid="one"]'); 271 | // const listItemFour = find('div[data-testid="four"]'); 272 | 273 | // await simulateDrag(listItemOne, listItemFour); 274 | 275 | // const listItems = document.querySelector('.list-group').children; 276 | // assert.equal(listItemOne, listItems[3], 'list item was moved'); 277 | // }); 278 | 279 | // test('it moves and element from one list to another', async function (assert) { 280 | // const onAdd = () => assert.ok(true, 'onAdd was called'); 281 | // const onRemove = () => assert.ok(true, 'onRemove was called'); 282 | 283 | // this.set('onAdd', onAdd); 284 | // this.set('onRemove', onRemove); 285 | 286 | // await render(hbs` 287 | // 292 | //
Item 1
293 | //
Item 2
294 | //
Item 3
295 | //
Item 4
296 | //
Item 5
297 | //
298 | 299 | // 304 | //
Item 1
305 | //
Item 2
306 | //
Item 3
307 | //
Item 4
308 | //
Item 5
309 | //
310 | // `); 311 | 312 | // const itemA = find('div[data-testid="one-a"]'); 313 | // const itemB = find('div[data-testid="four-b"]'); 314 | 315 | // await simulateDrag(itemA, itemB); 316 | 317 | // const aItems = document.querySelector('.list-group-a'); 318 | // const bItems = document.querySelector('.list-group-b'); 319 | 320 | // assert.equal(aItems.children.length, 4, 'list a has one less item'); 321 | // assert.equal(bItems.children.length, 6, 'list b has one less item'); 322 | // }); 323 | 324 | // test('it clones an element from one list to another', async function (assert) { 325 | // const onClone = () => assert.ok(true, 'onClone was called'); 326 | 327 | // this.set('onClone', onClone); 328 | 329 | // await render(hbs` 330 | // 338 | //
Item 1
339 | //
Item 2
340 | //
Item 3
341 | //
Item 4
342 | //
Item 5
343 | //
344 | 345 | // 354 | //
Item 1
355 | //
Item 2
356 | //
Item 3
357 | //
Item 4
358 | //
Item 5
359 | //
360 | // `); 361 | 362 | // const itemA = find('div[data-testid="one-a"]'); 363 | // const itemB = find('div[data-testid="four-b"]'); 364 | 365 | // await simulateDrag(itemA, itemB); 366 | 367 | // const aItems = document.querySelector('.list-group-a'); 368 | // const bItems = document.querySelector('.list-group-b'); 369 | 370 | // assert.equal(aItems.children.length, 5, 'list a has all its elements'); 371 | // assert.equal(bItems.children.length, 6, 'list b has a cloned item'); 372 | // }); 373 | 374 | // test('calls onSpill when dropped outside the sortable regioin', async function(assert) { 375 | // this.onSpill = function() { 376 | // assert.ok(true, 'onSpill was invoked'); 377 | // done(); 378 | // }; 379 | // this.options = { 380 | // animation: 100, 381 | // ghostClass: 'foo', 382 | // revertOnSpill: true, 383 | // }; 384 | 385 | // const done = assert.async(); 386 | 387 | // await render(hbs` 388 | //
389 | //
390 | // 395 | //
Item 1
396 | //
Item 2
397 | //
Item 3
398 | //
Item 4
399 | //
Item 5
400 | //
401 | // `); 402 | 403 | // const draggableElement = find('div[data-testid="four"]'); 404 | // const spillElement = find('.spill-element'); 405 | 406 | // await simulateDrag(draggableElement, spillElement); 407 | // }); 408 | }); 409 | -------------------------------------------------------------------------------- /app/tailwind/config.js: -------------------------------------------------------------------------------- 1 | /*global module*/ 2 | module.exports = { 3 | prefix: '', 4 | important: false, 5 | separator: ':', 6 | theme: { 7 | screens: { 8 | sm: '640px', 9 | md: '768px', 10 | lg: '1024px', 11 | xl: '1280px', 12 | }, 13 | colors: { 14 | transparent: 'transparent', 15 | 16 | black: '#000', 17 | white: '#fff', 18 | 19 | gray: { 20 | 100: '#f7fafc', 21 | 200: '#edf2f7', 22 | 300: '#e2e8f0', 23 | 400: '#cbd5e0', 24 | 500: '#a0aec0', 25 | 600: '#718096', 26 | 700: '#4a5568', 27 | 800: '#2d3748', 28 | 900: '#1a202c', 29 | }, 30 | red: { 31 | 100: '#fff5f5', 32 | 200: '#fed7d7', 33 | 300: '#feb2b2', 34 | 400: '#fc8181', 35 | 500: '#f56565', 36 | 600: '#e53e3e', 37 | 700: '#c53030', 38 | 800: '#9b2c2c', 39 | 900: '#742a2a', 40 | }, 41 | orange: { 42 | 100: '#fffaf0', 43 | 200: '#feebc8', 44 | 300: '#fbd38d', 45 | 400: '#f6ad55', 46 | 500: '#ed8936', 47 | 600: '#dd6b20', 48 | 700: '#c05621', 49 | 800: '#9c4221', 50 | 900: '#7b341e', 51 | }, 52 | yellow: { 53 | 100: '#fffff0', 54 | 200: '#fefcbf', 55 | 300: '#faf089', 56 | 400: '#f6e05e', 57 | 500: '#ecc94b', 58 | 600: '#d69e2e', 59 | 700: '#b7791f', 60 | 800: '#975a16', 61 | 900: '#744210', 62 | }, 63 | green: { 64 | 100: '#f0fff4', 65 | 200: '#c6f6d5', 66 | 300: '#9ae6b4', 67 | 400: '#68d391', 68 | 500: '#48bb78', 69 | 600: '#38a169', 70 | 700: '#2f855a', 71 | 800: '#276749', 72 | 900: '#22543d', 73 | }, 74 | teal: { 75 | 100: '#e6fffa', 76 | 200: '#b2f5ea', 77 | 300: '#81e6d9', 78 | 400: '#4fd1c5', 79 | 500: '#38b2ac', 80 | 600: '#319795', 81 | 700: '#2c7a7b', 82 | 800: '#285e61', 83 | 900: '#234e52', 84 | }, 85 | blue: { 86 | 100: '#ebf8ff', 87 | 200: '#bee3f8', 88 | 300: '#90cdf4', 89 | 400: '#63b3ed', 90 | 500: '#4299e1', 91 | 600: '#3182ce', 92 | 700: '#2b6cb0', 93 | 800: '#2c5282', 94 | 900: '#2a4365', 95 | }, 96 | indigo: { 97 | 100: '#ebf4ff', 98 | 200: '#c3dafe', 99 | 300: '#a3bffa', 100 | 400: '#7f9cf5', 101 | 500: '#667eea', 102 | 600: '#5a67d8', 103 | 700: '#4c51bf', 104 | 800: '#434190', 105 | 900: '#3c366b', 106 | }, 107 | purple: { 108 | 100: '#faf5ff', 109 | 200: '#e9d8fd', 110 | 300: '#d6bcfa', 111 | 400: '#b794f4', 112 | 500: '#9f7aea', 113 | 600: '#805ad5', 114 | 700: '#6b46c1', 115 | 800: '#553c9a', 116 | 900: '#44337a', 117 | }, 118 | pink: { 119 | 100: '#fff5f7', 120 | 200: '#fed7e2', 121 | 300: '#fbb6ce', 122 | 400: '#f687b3', 123 | 500: '#ed64a6', 124 | 600: '#d53f8c', 125 | 700: '#b83280', 126 | 800: '#97266d', 127 | 900: '#702459', 128 | }, 129 | }, 130 | spacing: { 131 | px: '1px', 132 | '0': '0', 133 | '1': '0.25rem', 134 | '2': '0.5rem', 135 | '3': '0.75rem', 136 | '4': '1rem', 137 | '5': '1.25rem', 138 | '6': '1.5rem', 139 | '8': '2rem', 140 | '10': '2.5rem', 141 | '12': '3rem', 142 | '16': '4rem', 143 | '20': '5rem', 144 | '24': '6rem', 145 | '32': '8rem', 146 | '40': '10rem', 147 | '48': '12rem', 148 | '56': '14rem', 149 | '64': '16rem', 150 | }, 151 | backgroundColor: theme => theme('colors'), 152 | backgroundPosition: { 153 | bottom: 'bottom', 154 | center: 'center', 155 | left: 'left', 156 | 'left-bottom': 'left bottom', 157 | 'left-top': 'left top', 158 | right: 'right', 159 | 'right-bottom': 'right bottom', 160 | 'right-top': 'right top', 161 | top: 'top', 162 | }, 163 | backgroundSize: { 164 | auto: 'auto', 165 | cover: 'cover', 166 | contain: 'contain', 167 | }, 168 | borderColor: theme => ({ 169 | ...theme('colors'), 170 | default: theme('colors.gray.300', 'currentColor'), 171 | }), 172 | borderRadius: { 173 | none: '0', 174 | sm: '0.125rem', 175 | default: '0.25rem', 176 | md: '0.375rem', 177 | lg: '0.5rem', 178 | full: '9999px', 179 | }, 180 | borderWidth: { 181 | default: '1px', 182 | '0': '0', 183 | '2': '2px', 184 | '4': '4px', 185 | '8': '8px', 186 | }, 187 | boxShadow: { 188 | xs: '0 0 0 1px rgba(0, 0, 0, 0.05)', 189 | sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', 190 | default: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', 191 | md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', 192 | lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', 193 | xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', 194 | '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)', 195 | inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)', 196 | outline: '0 0 0 3px rgba(66, 153, 225, 0.5)', 197 | none: 'none', 198 | }, 199 | container: {}, 200 | cursor: { 201 | auto: 'auto', 202 | default: 'default', 203 | pointer: 'pointer', 204 | wait: 'wait', 205 | text: 'text', 206 | move: 'move', 207 | 'not-allowed': 'not-allowed', 208 | }, 209 | fill: { 210 | current: 'currentColor', 211 | }, 212 | flex: { 213 | '1': '1 1 0%', 214 | auto: '1 1 auto', 215 | initial: '0 1 auto', 216 | none: 'none', 217 | }, 218 | flexGrow: { 219 | '0': '0', 220 | default: '1', 221 | }, 222 | flexShrink: { 223 | '0': '0', 224 | default: '1', 225 | }, 226 | fontFamily: { 227 | sans: [ 228 | 'system-ui', 229 | '-apple-system', 230 | 'BlinkMacSystemFont', 231 | '"Segoe UI"', 232 | 'Roboto', 233 | '"Helvetica Neue"', 234 | 'Arial', 235 | '"Noto Sans"', 236 | 'sans-serif', 237 | '"Apple Color Emoji"', 238 | '"Segoe UI Emoji"', 239 | '"Segoe UI Symbol"', 240 | '"Noto Color Emoji"', 241 | ], 242 | serif: ['Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'], 243 | mono: ['Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace'], 244 | }, 245 | fontSize: { 246 | xs: '0.75rem', 247 | sm: '0.875rem', 248 | base: '1rem', 249 | lg: '1.125rem', 250 | xl: '1.25rem', 251 | '2xl': '1.5rem', 252 | '3xl': '1.875rem', 253 | '4xl': '2.25rem', 254 | '5xl': '3rem', 255 | '6xl': '4rem', 256 | }, 257 | fontWeight: { 258 | hairline: '100', 259 | thin: '200', 260 | light: '300', 261 | normal: '400', 262 | medium: '500', 263 | semibold: '600', 264 | bold: '700', 265 | extrabold: '800', 266 | black: '900', 267 | }, 268 | height: theme => ({ 269 | auto: 'auto', 270 | ...theme('spacing'), 271 | full: '100%', 272 | screen: '100vh', 273 | }), 274 | inset: { 275 | '0': '0', 276 | auto: 'auto', 277 | }, 278 | letterSpacing: { 279 | tighter: '-0.05em', 280 | tight: '-0.025em', 281 | normal: '0', 282 | wide: '0.025em', 283 | wider: '0.05em', 284 | widest: '0.1em', 285 | }, 286 | lineHeight: { 287 | none: '1', 288 | tight: '1.25', 289 | snug: '1.375', 290 | normal: '1.5', 291 | relaxed: '1.625', 292 | loose: '2', 293 | '3': '.75rem', 294 | '4': '1rem', 295 | '5': '1.25rem', 296 | '6': '1.5rem', 297 | '7': '1.75rem', 298 | '8': '2rem', 299 | '9': '2.25rem', 300 | '10': '2.5rem', 301 | }, 302 | listStyleType: { 303 | none: 'none', 304 | disc: 'disc', 305 | decimal: 'decimal', 306 | }, 307 | margin: (theme, { negative }) => ({ 308 | auto: 'auto', 309 | ...theme('spacing'), 310 | ...negative(theme('spacing')), 311 | }), 312 | maxHeight: { 313 | full: '100%', 314 | screen: '100vh', 315 | }, 316 | maxWidth: (theme, { breakpoints }) => ({ 317 | none: 'none', 318 | xs: '20rem', 319 | sm: '24rem', 320 | md: '28rem', 321 | lg: '32rem', 322 | xl: '36rem', 323 | '2xl': '42rem', 324 | '3xl': '48rem', 325 | '4xl': '56rem', 326 | '5xl': '64rem', 327 | '6xl': '72rem', 328 | full: '100%', 329 | ...breakpoints(theme('screens')), 330 | }), 331 | minHeight: { 332 | '0': '0', 333 | full: '100%', 334 | screen: '100vh', 335 | }, 336 | minWidth: { 337 | '0': '0', 338 | full: '100%', 339 | }, 340 | objectPosition: { 341 | bottom: 'bottom', 342 | center: 'center', 343 | left: 'left', 344 | 'left-bottom': 'left bottom', 345 | 'left-top': 'left top', 346 | right: 'right', 347 | 'right-bottom': 'right bottom', 348 | 'right-top': 'right top', 349 | top: 'top', 350 | }, 351 | opacity: { 352 | '0': '0', 353 | '25': '0.25', 354 | '50': '0.5', 355 | '75': '0.75', 356 | '100': '1', 357 | }, 358 | order: { 359 | first: '-9999', 360 | last: '9999', 361 | none: '0', 362 | '1': '1', 363 | '2': '2', 364 | '3': '3', 365 | '4': '4', 366 | '5': '5', 367 | '6': '6', 368 | '7': '7', 369 | '8': '8', 370 | '9': '9', 371 | '10': '10', 372 | '11': '11', 373 | '12': '12', 374 | }, 375 | padding: theme => theme('spacing'), 376 | placeholderColor: theme => theme('colors'), 377 | stroke: { 378 | current: 'currentColor', 379 | }, 380 | strokeWidth: { 381 | '0': '0', 382 | '1': '1', 383 | '2': '2', 384 | }, 385 | textColor: theme => theme('colors'), 386 | width: theme => ({ 387 | auto: 'auto', 388 | ...theme('spacing'), 389 | '1/2': '50%', 390 | '1/3': '33.333333%', 391 | '2/3': '66.666667%', 392 | '1/4': '25%', 393 | '2/4': '50%', 394 | '3/4': '75%', 395 | '1/5': '20%', 396 | '2/5': '40%', 397 | '3/5': '60%', 398 | '4/5': '80%', 399 | '1/6': '16.666667%', 400 | '2/6': '33.333333%', 401 | '3/6': '50%', 402 | '4/6': '66.666667%', 403 | '5/6': '83.333333%', 404 | '1/12': '8.333333%', 405 | '2/12': '16.666667%', 406 | '3/12': '25%', 407 | '4/12': '33.333333%', 408 | '5/12': '41.666667%', 409 | '6/12': '50%', 410 | '7/12': '58.333333%', 411 | '8/12': '66.666667%', 412 | '9/12': '75%', 413 | '10/12': '83.333333%', 414 | '11/12': '91.666667%', 415 | full: '100%', 416 | screen: '100vw', 417 | }), 418 | zIndex: { 419 | auto: 'auto', 420 | '0': '0', 421 | '10': '10', 422 | '20': '20', 423 | '30': '30', 424 | '40': '40', 425 | '50': '50', 426 | }, 427 | gap: theme => theme('spacing'), 428 | gridTemplateColumns: { 429 | none: 'none', 430 | '1': 'repeat(1, minmax(0, 1fr))', 431 | '2': 'repeat(2, minmax(0, 1fr))', 432 | '3': 'repeat(3, minmax(0, 1fr))', 433 | '4': 'repeat(4, minmax(0, 1fr))', 434 | '5': 'repeat(5, minmax(0, 1fr))', 435 | '6': 'repeat(6, minmax(0, 1fr))', 436 | '7': 'repeat(7, minmax(0, 1fr))', 437 | '8': 'repeat(8, minmax(0, 1fr))', 438 | '9': 'repeat(9, minmax(0, 1fr))', 439 | '10': 'repeat(10, minmax(0, 1fr))', 440 | '11': 'repeat(11, minmax(0, 1fr))', 441 | '12': 'repeat(12, minmax(0, 1fr))', 442 | }, 443 | gridColumn: { 444 | auto: 'auto', 445 | 'span-1': 'span 1 / span 1', 446 | 'span-2': 'span 2 / span 2', 447 | 'span-3': 'span 3 / span 3', 448 | 'span-4': 'span 4 / span 4', 449 | 'span-5': 'span 5 / span 5', 450 | 'span-6': 'span 6 / span 6', 451 | 'span-7': 'span 7 / span 7', 452 | 'span-8': 'span 8 / span 8', 453 | 'span-9': 'span 9 / span 9', 454 | 'span-10': 'span 10 / span 10', 455 | 'span-11': 'span 11 / span 11', 456 | 'span-12': 'span 12 / span 12', 457 | }, 458 | gridColumnStart: { 459 | auto: 'auto', 460 | '1': '1', 461 | '2': '2', 462 | '3': '3', 463 | '4': '4', 464 | '5': '5', 465 | '6': '6', 466 | '7': '7', 467 | '8': '8', 468 | '9': '9', 469 | '10': '10', 470 | '11': '11', 471 | '12': '12', 472 | '13': '13', 473 | }, 474 | gridColumnEnd: { 475 | auto: 'auto', 476 | '1': '1', 477 | '2': '2', 478 | '3': '3', 479 | '4': '4', 480 | '5': '5', 481 | '6': '6', 482 | '7': '7', 483 | '8': '8', 484 | '9': '9', 485 | '10': '10', 486 | '11': '11', 487 | '12': '12', 488 | '13': '13', 489 | }, 490 | gridTemplateRows: { 491 | none: 'none', 492 | '1': 'repeat(1, minmax(0, 1fr))', 493 | '2': 'repeat(2, minmax(0, 1fr))', 494 | '3': 'repeat(3, minmax(0, 1fr))', 495 | '4': 'repeat(4, minmax(0, 1fr))', 496 | '5': 'repeat(5, minmax(0, 1fr))', 497 | '6': 'repeat(6, minmax(0, 1fr))', 498 | }, 499 | gridRow: { 500 | auto: 'auto', 501 | 'span-1': 'span 1 / span 1', 502 | 'span-2': 'span 2 / span 2', 503 | 'span-3': 'span 3 / span 3', 504 | 'span-4': 'span 4 / span 4', 505 | 'span-5': 'span 5 / span 5', 506 | 'span-6': 'span 6 / span 6', 507 | }, 508 | gridRowStart: { 509 | auto: 'auto', 510 | '1': '1', 511 | '2': '2', 512 | '3': '3', 513 | '4': '4', 514 | '5': '5', 515 | '6': '6', 516 | '7': '7', 517 | }, 518 | gridRowEnd: { 519 | auto: 'auto', 520 | '1': '1', 521 | '2': '2', 522 | '3': '3', 523 | '4': '4', 524 | '5': '5', 525 | '6': '6', 526 | '7': '7', 527 | }, 528 | transformOrigin: { 529 | center: 'center', 530 | top: 'top', 531 | 'top-right': 'top right', 532 | right: 'right', 533 | 'bottom-right': 'bottom right', 534 | bottom: 'bottom', 535 | 'bottom-left': 'bottom left', 536 | left: 'left', 537 | 'top-left': 'top left', 538 | }, 539 | scale: { 540 | '0': '0', 541 | '50': '.5', 542 | '75': '.75', 543 | '90': '.9', 544 | '95': '.95', 545 | '100': '1', 546 | '105': '1.05', 547 | '110': '1.1', 548 | '125': '1.25', 549 | '150': '1.5', 550 | }, 551 | rotate: { 552 | '-180': '-180deg', 553 | '-90': '-90deg', 554 | '-45': '-45deg', 555 | '0': '0', 556 | '45': '45deg', 557 | '90': '90deg', 558 | '180': '180deg', 559 | }, 560 | translate: (theme, { negative }) => ({ 561 | ...theme('spacing'), 562 | ...negative(theme('spacing')), 563 | '-full': '-100%', 564 | '-1/2': '-50%', 565 | '1/2': '50%', 566 | full: '100%', 567 | }), 568 | skew: { 569 | '-12': '-12deg', 570 | '-6': '-6deg', 571 | '-3': '-3deg', 572 | '0': '0', 573 | '3': '3deg', 574 | '6': '6deg', 575 | '12': '12deg', 576 | }, 577 | transitionProperty: { 578 | none: 'none', 579 | all: 'all', 580 | default: 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform', 581 | colors: 'background-color, border-color, color, fill, stroke', 582 | opacity: 'opacity', 583 | shadow: 'box-shadow', 584 | transform: 'transform', 585 | }, 586 | transitionTimingFunction: { 587 | linear: 'linear', 588 | in: 'cubic-bezier(0.4, 0, 1, 1)', 589 | out: 'cubic-bezier(0, 0, 0.2, 1)', 590 | 'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)', 591 | }, 592 | transitionDuration: { 593 | '75': '75ms', 594 | '100': '100ms', 595 | '150': '150ms', 596 | '200': '200ms', 597 | '300': '300ms', 598 | '500': '500ms', 599 | '700': '700ms', 600 | '1000': '1000ms', 601 | }, 602 | }, 603 | variants: { 604 | accessibility: ['responsive', 'focus'], 605 | alignContent: ['responsive'], 606 | alignItems: ['responsive'], 607 | alignSelf: ['responsive'], 608 | appearance: ['responsive'], 609 | backgroundAttachment: ['responsive'], 610 | backgroundColor: ['responsive', 'hover', 'focus'], 611 | backgroundPosition: ['responsive'], 612 | backgroundRepeat: ['responsive'], 613 | backgroundSize: ['responsive'], 614 | borderCollapse: ['responsive'], 615 | borderColor: ['responsive', 'hover', 'focus'], 616 | borderRadius: ['responsive'], 617 | borderStyle: ['responsive'], 618 | borderWidth: ['responsive'], 619 | boxShadow: ['responsive', 'hover', 'focus'], 620 | boxSizing: ['responsive'], 621 | cursor: ['responsive'], 622 | display: ['responsive'], 623 | fill: ['responsive'], 624 | flex: ['responsive'], 625 | flexDirection: ['responsive'], 626 | flexGrow: ['responsive'], 627 | flexShrink: ['responsive'], 628 | flexWrap: ['responsive'], 629 | float: ['responsive'], 630 | clear: ['responsive'], 631 | fontFamily: ['responsive'], 632 | fontSize: ['responsive'], 633 | fontSmoothing: ['responsive'], 634 | fontStyle: ['responsive'], 635 | fontWeight: ['responsive', 'hover', 'focus'], 636 | height: ['responsive'], 637 | inset: ['responsive'], 638 | justifyContent: ['responsive'], 639 | letterSpacing: ['responsive'], 640 | lineHeight: ['responsive'], 641 | listStylePosition: ['responsive'], 642 | listStyleType: ['responsive'], 643 | margin: ['responsive'], 644 | maxHeight: ['responsive'], 645 | maxWidth: ['responsive'], 646 | minHeight: ['responsive'], 647 | minWidth: ['responsive'], 648 | objectFit: ['responsive'], 649 | objectPosition: ['responsive'], 650 | opacity: ['responsive', 'hover', 'focus'], 651 | order: ['responsive'], 652 | outline: ['responsive', 'focus'], 653 | overflow: ['responsive'], 654 | padding: ['responsive'], 655 | placeholderColor: ['responsive', 'focus'], 656 | pointerEvents: ['responsive'], 657 | position: ['responsive'], 658 | resize: ['responsive'], 659 | stroke: ['responsive'], 660 | strokeWidth: ['responsive'], 661 | tableLayout: ['responsive'], 662 | textAlign: ['responsive'], 663 | textColor: ['responsive', 'hover', 'focus'], 664 | textDecoration: ['responsive', 'hover', 'focus'], 665 | textTransform: ['responsive'], 666 | userSelect: ['responsive'], 667 | verticalAlign: ['responsive'], 668 | visibility: ['responsive'], 669 | whitespace: ['responsive'], 670 | width: ['responsive'], 671 | wordBreak: ['responsive'], 672 | zIndex: ['responsive'], 673 | gap: ['responsive'], 674 | gridAutoFlow: ['responsive'], 675 | gridTemplateColumns: ['responsive'], 676 | gridColumn: ['responsive'], 677 | gridColumnStart: ['responsive'], 678 | gridColumnEnd: ['responsive'], 679 | gridTemplateRows: ['responsive'], 680 | gridRow: ['responsive'], 681 | gridRowStart: ['responsive'], 682 | gridRowEnd: ['responsive'], 683 | transform: ['responsive'], 684 | transformOrigin: ['responsive'], 685 | scale: ['responsive', 'hover', 'focus'], 686 | rotate: ['responsive', 'hover', 'focus'], 687 | translate: ['responsive', 'hover', 'focus'], 688 | skew: ['responsive', 'hover', 'focus'], 689 | transitionProperty: ['responsive'], 690 | transitionTimingFunction: ['responsive'], 691 | transitionDuration: ['responsive'], 692 | }, 693 | corePlugins: {}, 694 | plugins: [], 695 | } 696 | --------------------------------------------------------------------------------