diff --git a/.eslintrc.json b/.eslintrc.json index c14ac6f7..b3499ad3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,16 +1,21 @@ { - "parser": "babel-eslint", + "parser": "@typescript-eslint/parser", "parserOptions": { - "ecmaVersion": 2017, + "ecmaVersion": 2020, "sourceType": "module", "allowImportExportEverywhere": true }, "env": { "browser": true, - "jasmine": true + "jasmine": true, + "es6": true }, - "plugins": [], - "extends": ["eslint:recommended"], + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], "globals": { "Uint8Array": true, "Promise": true, @@ -33,7 +38,7 @@ "callback-return": "off", "camelcase": "off", "capitalized-comments": "off", - "class-methods-use-this": "error", + "class-methods-use-this": "off", "comma-dangle": "off", "comma-spacing": "off", "comma-style": "off", @@ -179,6 +184,7 @@ "no-underscore-dangle": "off", "no-unmodified-loop-condition": "error", "no-unneeded-ternary": "off", + "@typescript-eslint/no-unused-vars": "off", "no-unused-vars": "off", "no-unused-expressions": "off", "no-use-before-define": "off", diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..67f088fb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 120, + "quoteProps": "preserve", + "singleQuote": true, + "tabWidth": 2 +} diff --git a/CHANGES.md b/CHANGES.md index eccea491..4467b350 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # Changelog +## 2.0.0 (Unreleased) + +Breaking changes: +- Removed the `clone()` method on `Model`. + ## 1.0.0 (Unreleased) - Fix a race condition when setting a localForage driver diff --git a/README.md b/README.md index 4f4586c5..0a0601c5 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ and instead start writing declarative, component-based code that automatically updates only the changed parts of the DOM, similarly to basically all modern JavaScript frameworks. -The original Backbone Views aren't components can't be rendered in a nested and +The original Backbone Views aren't components and can't be rendered in a nested and declarative way. Instead, it's up to you to manually make sure that these views are rendered in the correct place in the DOM. This approach becomes unwieldy, difficult and fragile as your site becomes larger and more complex. @@ -41,6 +41,14 @@ We can cheat a little by letting the existing Views also be web components (more accurately, "custom elements"), this allows us to declaratively render the UI, while we're progressively getting rid of the views. +## Big changes in version 2 + +We've made big, backwards incompatible changes in version 2. + +- Removed the old `View` type +- Removed the `Router` and `History` classes. +- TypeScript type declarations (generated from typed JSDoc comments) +- All other types (`Model`, `Collection`, `ElementView`) are now ES6 classes. ## Sekeletor adds the following changes to Backbone @@ -56,16 +64,17 @@ UI, while we're progressively getting rid of the views. as an instance of HTMLElement and can be used to register a custom element or web-component. -![](https://raw.githubusercontent.com/conversejs/skeletor/master/images/skeletor.jpg) - ### Backwards incompatible changes * Collection.prototype.forEach no longer returns the items being iterated over. If you need that, use `map` instead. -* The `chain` method on Models has been removed. +* The `chain`, `clone` and `escape` methods on Models have been removed. +* The `clone` method has also been removed from Collections * The `inject`, `foldl` and `foldr` methods on Collections has been removed. You can use `reduce` instead. * Removed the `sample`, `take`, `tail` and `initial` method on Collections. * Removed the `without`, `reject` and `select` methods on Collections, use `filter`. +* Removed the `.extend()` method on `Model` and `Collection`. +* Models and Collections should be defined via `class .. extends` syntax. #### Changes due to using Lodash instead of Underscore diff --git a/karma.conf.js b/karma.conf.js index ed5ed43e..a3f789dc 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,8 +1,7 @@ /* global module */ const path = require('path'); - -module.exports = function(config) { +module.exports = function (config) { config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '', @@ -14,22 +13,21 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ - 'node_modules/lodash/lodash.js', - 'node_modules/sinon/pkg/sinon.js', - 'test/vendor/json2.js', - 'dist/skeletor.js', - 'test/indexeddb.test.js', - 'test/localStorage.test.js', - 'test/sessionStorage.test.js', + 'node_modules/lodash/lodash.js', + 'node_modules/sinon/pkg/sinon.js', + 'test/vendor/json2.js', + 'dist/skeletor.js', + 'test/indexeddb.test.js', + 'test/localStorage.test.js', + 'test/sessionStorage.test.js', - 'test/setup/dom-setup.js', - 'test/collection.js', - 'test/events.js', - 'test/model.js', - 'test/noconflict.js', - 'test/router.js', - 'test/sync.js', - 'test/view.js', + 'test/setup/dom-setup.js', + 'test/collection.js', + 'test/events.js', + 'test/model.js', + 'test/noconflict.js', + 'test/router.js', + 'test/sync.js', ], // list of files to exclude @@ -40,37 +38,42 @@ module.exports = function(config) { preprocessors: { 'test/indexeddb.test.js': ['webpack'], 'test/localStorage.test.js': ['webpack'], - 'test/sessionStorage.test.js': ['webpack'] + 'test/sessionStorage.test.js': ['webpack'], }, webpack: { mode: 'development', devtool: 'inline-source-map', module: { - rules: [{ - test: /\.js$/, - use: { - loader: 'babel-loader', - options: { - presets: [ - ["@babel/preset-env", { - "targets": { - "browsers": [">1%", "not ie 11", "not op_mini all", "not dead"] - } - }] - ], - plugins: [ + rules: [ + { + test: /\.js$/, + use: { + loader: 'babel-loader', + options: { + presets: [ + [ + '@babel/preset-env', + { + 'targets': { + 'browsers': ['>1%', 'not ie 11', 'not op_mini all', 'not dead'], + }, + }, + ], + ], + plugins: [ '@babel/plugin-proposal-optional-chaining', - '@babel/plugin-proposal-nullish-coalescing-operator' - ] - } - } - }] + '@babel/plugin-proposal-nullish-coalescing-operator', + ], + }, + }, + }, + ], }, output: { path: path.resolve('test'), filename: '[name].out.js', - chunkFilename: '[id].[chunkHash].js' - } + chunkFilename: '[id].[chunkHash].js', + }, }, // test results reporter to use @@ -80,8 +83,8 @@ module.exports = function(config) { client: { mocha: { reporter: 'html', - ui: 'bdd' - } + ui: 'bdd', + }, }, // web server port @@ -107,6 +110,6 @@ module.exports = function(config) { // Concurrency level // how many browser should be started simultaneous - concurrency: Infinity - }) -} + concurrency: Infinity, + }); +}; diff --git a/package-lock.json b/package-lock.json index 9dde3251..a0a5e1dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@babel/preset-env": "^7.9.5", "@rollup/plugin-babel": "^5.0.3", "@rollup/plugin-node-resolve": "^8.0.1", + "@typescript-eslint/eslint-plugin": "^6.1.0", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.2", "chai": "^4.2.0", @@ -36,11 +37,14 @@ "karma-qunit": "^4.1.1", "karma-webpack": "^4.0.2", "mocha": "^10.2.0", + "prettier": "^3.0.0", "qunit": "^2.10.0", "rollup": "^1.32.1", "rollup-plugin-sourcemaps": "^0.6.2", "rollup-plugin-terser": "^5.3.0", "sinon": "^9.0.2", + "typescript": "^5.1.6", + "typescript-eslint": "^0.0.1-alpha.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "window-or-global": "^1.0.1" @@ -60,12 +64,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -139,12 +143,12 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -183,19 +187,20 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.20.12", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz", - "integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.9.tgz", + "integrity": "sha512-Pwyi89uO4YrGKxL/eNJ8lfEH55DnRloGPOseaA8NFNL6jAUnn+KccaISiFazCj5IolPPDjGSdzQzXVzODVRqUQ==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.20.7", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/helper-split-export-declaration": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -238,9 +243,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", "dev": true, "engines": { "node": ">=6.9.0" @@ -259,13 +264,13 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", "dev": true, "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -284,12 +289,12 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz", - "integrity": "sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", "dev": true, "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -327,21 +332,21 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true, "engines": { "node": ">=6.9.0" @@ -366,20 +371,20 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", - "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", + "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.20.7", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-simple-access": { @@ -395,42 +400,42 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", "dev": true, "dependencies": { - "@babel/types": "^7.20.0" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -475,12 +480,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-validator-identifier": "^7.22.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -489,9 +494,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.15.tgz", - "integrity": "sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==", + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1619,14 +1624,14 @@ } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1654,13 +1659,13 @@ } }, "node_modules/@babel/types": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", - "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1689,6 +1694,42 @@ "resolved": "https://registry.npmjs.org/@converse/openpromise/-/openpromise-0.0.1.tgz", "integrity": "sha512-oA1TKrm6H838isYZJxMWXpXyOUezkD49eMJ6bkI+FfL2MsVuOV3ZbhBV+c07mLSknKXO7pUbWTVa5f7bXJXYjQ==" }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -1791,6 +1832,41 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -1915,9 +1991,9 @@ "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, "node_modules/@types/node": { @@ -1935,11 +2011,328 @@ "@types/node": "*" } }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, "node_modules/@types/trusted-types": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.1.0.tgz", + "integrity": "sha512-qg7Bm5TyP/I7iilGyp6DRqqkt8na00lI6HbjWZObgk3FFSzH5ypRwAHXJhJkwiRtTcfn+xYQIMOR5kJgpo6upw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/type-utils": "6.1.0", + "@typescript-eslint/utils": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.1.0.tgz", + "integrity": "sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw==", + "dev": true, + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/typescript-estree": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.1.0.tgz", + "integrity": "sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.1.0.tgz", + "integrity": "sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.1.0", + "@typescript-eslint/utils": "6.1.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.1.0.tgz", + "integrity": "sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.1.0.tgz", + "integrity": "sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.1.0.tgz", + "integrity": "sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/typescript-estree": "6.1.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.1.0.tgz", + "integrity": "sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.1.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -2280,6 +2673,15 @@ "node": ">=0.10.0" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -3590,6 +3992,18 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4405,6 +4819,35 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4417,6 +4860,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -4825,6 +5277,35 @@ "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", "dev": true }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -4837,6 +5318,12 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -6011,6 +6498,15 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/mergebounce": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/mergebounce/-/mergebounce-0.1.1.tgz", @@ -6616,6 +7112,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -7048,6 +7550,15 @@ "isarray": "0.0.1" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -7176,6 +7687,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -7315,6 +7841,26 @@ "node": ">=0.4.x" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/qunit": { "version": "2.19.4", "resolved": "https://registry.npmjs.org/qunit/-/qunit-2.19.4.tgz", @@ -7692,6 +8238,16 @@ "node": ">=0.12" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", @@ -7791,6 +8347,29 @@ "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", "dev": true }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", @@ -7854,9 +8433,9 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -8033,6 +8612,15 @@ "node": ">=8" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/slice-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", @@ -8945,6 +9533,18 @@ "node": ">=0.6" } }, + "node_modules/ts-api-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", + "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -9008,6 +9608,25 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "dev": true }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "0.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-0.0.1-alpha.0.tgz", + "integrity": "sha512-1hNKM37dAWML/2ltRXupOq2uqcdRQyDFphl+341NTPXFLLLiDhErXx8VtaSLh3xP7SyHZdcCgpt9boYYVb3fQg==", + "dev": true + }, "node_modules/ua-parser-js": { "version": "0.7.33", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz", @@ -10240,12 +10859,12 @@ } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.5" } }, "@babel/compat-data": { @@ -10302,12 +10921,12 @@ } }, "@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-builder-binary-assignment-operator-visitor": { @@ -10334,19 +10953,20 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.20.12", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz", - "integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.9.tgz", + "integrity": "sha512-Pwyi89uO4YrGKxL/eNJ8lfEH55DnRloGPOseaA8NFNL6jAUnn+KccaISiFazCj5IolPPDjGSdzQzXVzODVRqUQ==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.20.7", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/helper-split-export-declaration": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" } }, "@babel/helper-create-regexp-features-plugin": { @@ -10374,9 +10994,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", "dev": true }, "@babel/helper-explode-assignable-expression": { @@ -10389,13 +11009,13 @@ } }, "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", "dev": true, "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" } }, "@babel/helper-hoist-variables": { @@ -10408,12 +11028,12 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz", - "integrity": "sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", "dev": true, "requires": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.22.5" } }, "@babel/helper-module-imports": { @@ -10442,18 +11062,18 @@ } }, "@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true }, "@babel/helper-remap-async-to-generator": { @@ -10469,17 +11089,14 @@ } }, "@babel/helper-replace-supers": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", - "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", + "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.20.7", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5" } }, "@babel/helper-simple-access": { @@ -10492,33 +11109,33 @@ } }, "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", "dev": true, "requires": { - "@babel/types": "^7.20.0" + "@babel/types": "^7.22.5" } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", "dev": true }, "@babel/helper-validator-option": { @@ -10551,20 +11168,20 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-validator-identifier": "^7.22.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.20.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.15.tgz", - "integrity": "sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==", + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { @@ -11317,14 +11934,14 @@ } }, "@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" } }, "@babel/traverse": { @@ -11346,13 +11963,13 @@ } }, "@babel/types": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", - "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", "to-fast-properties": "^2.0.0" } }, @@ -11375,6 +11992,29 @@ "resolved": "https://registry.npmjs.org/@converse/openpromise/-/openpromise-0.0.1.tgz", "integrity": "sha512-oA1TKrm6H838isYZJxMWXpXyOUezkD49eMJ6bkI+FfL2MsVuOV3ZbhBV+c07mLSknKXO7pUbWTVa5f7bXJXYjQ==" }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true + } + } + }, + "@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true + }, "@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -11458,6 +12098,32 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -11557,9 +12223,9 @@ "dev": true }, "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, "@types/node": { @@ -11577,11 +12243,211 @@ "@types/node": "*" } }, + "@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, "@types/trusted-types": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" }, + "@typescript-eslint/eslint-plugin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.1.0.tgz", + "integrity": "sha512-qg7Bm5TyP/I7iilGyp6DRqqkt8na00lI6HbjWZObgk3FFSzH5ypRwAHXJhJkwiRtTcfn+xYQIMOR5kJgpo6upw==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/type-utils": "6.1.0", + "@typescript-eslint/utils": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@typescript-eslint/parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.1.0.tgz", + "integrity": "sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw==", + "dev": true, + "peer": true, + "requires": { + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/typescript-estree": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.1.0.tgz", + "integrity": "sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.1.0.tgz", + "integrity": "sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "6.1.0", + "@typescript-eslint/utils": "6.1.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/types": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.1.0.tgz", + "integrity": "sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.1.0.tgz", + "integrity": "sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@typescript-eslint/utils": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.1.0.tgz", + "integrity": "sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/typescript-estree": "6.1.0", + "semver": "^7.5.4" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.1.0.tgz", + "integrity": "sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.1.0", + "eslint-visitor-keys": "^3.4.1" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true + } + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -11882,6 +12748,12 @@ "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", "dev": true }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -12958,6 +13830,15 @@ } } }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -13609,6 +14490,31 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-glob": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -13621,6 +14527,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -13937,6 +14852,28 @@ "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", "dev": true }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "dependencies": { + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + } + } + }, "globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -13949,6 +14886,12 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -14870,6 +15813,12 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "mergebounce": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/mergebounce/-/mergebounce-0.1.1.tgz", @@ -15350,6 +16299,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -15707,6 +16662,12 @@ "isarray": "0.0.1" } }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, "pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -15801,6 +16762,12 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prettier": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "dev": true + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -15919,6 +16886,12 @@ "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", "dev": true }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, "qunit": { "version": "2.19.4", "resolved": "https://registry.npmjs.org/qunit/-/qunit-2.19.4.tgz", @@ -16225,6 +17198,12 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", @@ -16301,6 +17280,15 @@ } } }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", @@ -16343,9 +17331,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "serialize-javascript": { @@ -16486,6 +17474,12 @@ } } }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "slice-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", @@ -17234,6 +18228,13 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "ts-api-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", + "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", + "dev": true, + "requires": {} + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -17282,6 +18283,18 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "dev": true }, + "typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true + }, + "typescript-eslint": { + "version": "0.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-0.0.1-alpha.0.tgz", + "integrity": "sha512-1hNKM37dAWML/2ltRXupOq2uqcdRQyDFphl+341NTPXFLLLiDhErXx8VtaSLh3xP7SyHZdcCgpt9boYYVb3fQg==", + "dev": true + }, "ua-parser-js": { "version": "0.7.33", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz", diff --git a/package.json b/package.json index 6ea1f70c..412d1e72 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@babel/preset-env": "^7.9.5", "@rollup/plugin-babel": "^5.0.3", "@rollup/plugin-node-resolve": "^8.0.1", + "@typescript-eslint/eslint-plugin": "^6.1.0", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.2", "chai": "^4.2.0", @@ -45,11 +46,14 @@ "karma-qunit": "^4.1.1", "karma-webpack": "^4.0.2", "mocha": "^10.2.0", + "prettier": "^3.0.0", "qunit": "^2.10.0", "rollup": "^1.32.1", "rollup-plugin-sourcemaps": "^0.6.2", "rollup-plugin-terser": "^5.3.0", "sinon": "^9.0.2", + "typescript": "^5.1.6", + "typescript-eslint": "^0.0.1-alpha.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "window-or-global": "^1.0.1" @@ -58,7 +62,9 @@ "test": "karma start && karma start karma.mocha.conf && npm run lint", "dev": "karma start --single-run=false", "build": "rollup -c", - "lint": "eslint src/*.js test/*.js" + "types": "tsc", + "lint": "eslint src/*.js test/*.js", + "prettier": "prettier --write src/**/*.js test/**/*.js" }, "module": "src/main.js", "version": "0.0.8", diff --git a/src/collection.js b/src/collection.js index ccdb7af3..07fb0af6 100644 --- a/src/collection.js +++ b/src/collection.js @@ -1,131 +1,158 @@ -// Backbone.js 1.4.0 -// (c) 2010-2019 Jeremy Ashkenas and DocumentCloud -// Backbone may be freely distributed under the MIT license. - -// Collection -// ---------- - -// If models tend to represent a single row of data, a Collection is -// more analogous to a table full of data ... or a small slice or page of that -// table, or a collection of rows that belong together for a particular reason -// -- all of the messages in this particular folder, all of the documents -// belonging to this particular author, and so on. Collections maintain -// indexes of their models, both in order, and for lookup by `id`. - -import { inherits, getResolveablePromise, getSyncMethod, wrapError } from './helpers.js'; -import { Events } from './events.js'; +import { getResolveablePromise, getSyncMethod, wrapError } from './helpers.js'; import { Model } from './model.js'; -import clone from "lodash-es/clone.js"; +import clone from 'lodash-es/clone.js'; import countBy from 'lodash-es/countBy.js'; -import difference from 'lodash-es/difference.js'; -import every from 'lodash-es/every.js'; -import extend from "lodash-es/extend.js"; -import findIndex from 'lodash-es/findIndex.js'; -import findLastIndex from 'lodash-es/findLastIndex.js'; import groupBy from 'lodash-es/groupBy.js'; -import indexOf from 'lodash-es/indexOf.js'; -import isEmpty from "lodash-es/isEmpty.js"; -import isFunction from "lodash-es/isFunction.js"; +import isFunction from 'lodash-es/isFunction.js'; import isString from 'lodash-es/isString.js'; import keyBy from 'lodash-es/keyBy.js'; -import lastIndexOf from 'lodash-es/lastIndexOf.js'; -import some from 'lodash-es/some.js'; import sortBy from 'lodash-es/sortBy.js'; +import EventEmitter from './eventemitter.js'; const slice = Array.prototype.slice; -// Create a new **Collection**, perhaps to contain a specific type of `model`. -// If a `comparator` is specified, the Collection will maintain -// its models in sort order, as they're added and removed. -export const Collection = function(models, options) { - options || (options = {}); - this.preinitialize.apply(this, arguments); - if (options.model) this.model = options.model; - if (options.comparator !== undefined) this.comparator = options.comparator; - this._reset(); - this.initialize.apply(this, arguments); - if (models) this.reset(models, extend({silent: true}, options)); -}; - -Collection.extend = inherits; - - // Default options for `Collection#set`. -const setOptions = {add: true, remove: true, merge: true}; -const addOptions = {add: true, remove: false}; - -// Splices `insert` into `array` at index `at`. -const splice = function(array, insert, at) { - at = Math.min(Math.max(at, 0), array.length); - const tail = Array(array.length - at); - const length = insert.length; - let i; - for (i = 0; i < tail.length; i++) tail[i] = array[i + at]; - for (i = 0; i < length; i++) array[i + at] = insert[i]; - for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; -}; - -// Define the Collection's inheritable methods. -Object.assign(Collection.prototype, Events, { +const setOptions = { add: true, remove: true, merge: true }; +const addOptions = { add: true, remove: false }; + +/** + * @typedef {Record.} Options + * @typedef {Record.} Attributes + * + * @typedef {Record.} CollectionOptions + * @property {Model} [model] + * @property {Function} [comparator] + */ + +/** + * If models tend to represent a single row of data, a Collection is + * more analogous to a table full of data ... or a small slice or page of that + * table, or a collection of rows that belong together for a particular reason + * -- all of the messages in this particular folder, all of the documents + * belonging to this particular author, and so on. Collections maintain + * indexes of their models, both in order, and for lookup by `id`. + */ +class Collection extends EventEmitter { + /** + * Create a new **Collection**, perhaps to contain a specific type of `model`. + * If a `comparator` is specified, the Collection will maintain + * its models in sort order, as they're added and removed. + * @param {Model[]} models + * @param {CollectionOptions} options + */ + constructor(models, options) { + super(); + options || (options = {}); + this.preinitialize.apply(this, arguments); + if (options.model) this._model = options.model; + if (options.comparator !== undefined) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, Object.assign({ silent: true }, options)); + } - // The default model for a collection is just a **Backbone.Model**. - // This should be overridden in most cases. - model: Model, + get browserStorage() { + return null; + } + /** + * The default model for a collection is just a **Model**. + * This should be overridden in most cases. + * @return {typeof Model} + */ + get model() { + return this._model ?? Model; + } - // preinitialize is an empty function by default. You can override it with a function - // or object. preinitialize will run before any instantiation logic is run in the Collection. - preinitialize: function(){}, + /** + * @param {Model} model + */ + set model(model) { + this._model = model; + } - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, + get length() { + return this.models.length; + } - // The JSON representation of a Collection is an array of the - // models' attributes. - toJSON: function(options) { - return this.map(function(model) { return model.toJSON(options); }); - }, + /** + * preinitialize is an empty function by default. You can override it with a function + * or object. preinitialize will run before any instantiation logic is run in the Collection. + */ + preinitialize() {} + + /** + * Initialize is an empty function by default. Override it with your own + * initialization logic. + */ + initialize() {} + + /** + * The JSON representation of a Collection is an array of the + * models' attributes. + *@param {Options} options + */ + toJSON(options) { + return this.map(function (model) { + return model.toJSON(options); + }); + } - // Proxy `Backbone.sync` by default. - sync: function(method, model, options) { + /** + *@param {string} method + *@param {Model|Collection} model + *@param {Options} options + */ + sync(method, model, options) { return getSyncMethod(this)(method, model, options); - }, - - // Add a model, or list of models to the set. `models` may be Backbone - // Models or raw JavaScript objects to be converted to Models, or any - // combination of the two. - add: function(models, options) { - return this.set(models, extend({merge: false}, options, addOptions)); - }, - - // Remove a model, or a list of models from the set. - remove: function(models, options) { - options = extend({}, options); + } + + /** + * Add a model, or list of models to the set. `models` may be + * Models or raw JavaScript objects to be converted to Models, or any + * combination of the two. + *@param {Model[]|Model|Attributes|Attributes[]} models + *@param {Options} options + */ + add(models, options) { + return this.set(models, Object.assign({ merge: false }, options, addOptions)); + } + + /** + * Remove a model, or a list of models from the set. + * @param {Model|Model[]} models + * @param {Options} options + */ + remove(models, options) { + options = Object.assign({}, options); const singular = !Array.isArray(models); - models = singular ? [models] : models.slice(); - const removed = this._removeModels(models, options); + const modelsArray = singular ? [models] : /** @type {Model[]} */ (models).slice(); + const removed = this._removeModels(modelsArray, options); if (!options.silent && removed.length) { - options.changes = {added: [], merged: [], removed: removed}; + options.changes = { added: [], merged: [], removed: removed }; this.trigger('update', this, options); } return singular ? removed[0] : removed; - }, + } - // Update a collection by `set`-ing a new list of models, adding new ones, - // removing models that are no longer present, and merging models that - // already exist in the collection, as necessary. Similar to **Model#set**, - // the core operation for updating the data contained by the collection. - set: function(models, options) { + /** + * Update a collection by `set`-ing a new list of models, adding new ones, + * removing models that are no longer present, and merging models that + * already exist in the collection, as necessary. Similar to **Model#set**, + * the core operation for updating the data contained by the collection. + *@param {Model[]|Model|Attributes|Attributes[]} models + * @param {Options} options + */ + set(models, options) { if (models == null) return; - options = extend({}, setOptions, options); + options = Object.assign({}, setOptions, options); if (options.parse && !this._isModel(models)) { models = this.parse(models, options) || []; } const singular = !Array.isArray(models); - models = singular ? [models] : models.slice(); + models = singular ? [/** @type {Model} */ (models)] : /** @type {Model[]} */ (models).slice(); let at = options.at; if (at != null) at = +at; @@ -169,7 +196,7 @@ Object.assign(Collection.prototype, Events, { } models[i] = existing; - // If this is a new, valid model, push it to the `toAdd` list. + // If this is a new, valid model, push it to the `toAdd` list. } else if (add) { model = models[i] = this._prepareModel(model, options); if (model) { @@ -193,19 +220,20 @@ Object.assign(Collection.prototype, Events, { // See if sorting is needed, update `length` and splice in new models. let orderChanged = false; const replace = !sortable && add && remove; + if (set.length && replace) { - orderChanged = this.length !== set.length || some(this.models, (m, index) => m !== set[index]); + orderChanged = this.length !== set.length || this.models.some((m, idx) => m !== set[idx]); this.models.length = 0; - splice(this.models, set, 0); - this.length = this.models.length; + this.models.splice(0, 0, ...set); } else if (toAdd.length) { if (sortable) sort = true; - splice(this.models, toAdd, at == null ? this.length : at); - this.length = this.models.length; + let idx = at == null ? this.length : at; + idx = Math.min(Math.max(idx, 0), this.models.length); + this.models.splice(idx, 0, ...toAdd); } // Silently sort the collection if appropriate. - if (sort) this.sort({silent: true}); + if (sort) this.sort({ silent: true }); // Unless silenced, it's time to fire all appropriate add/sort/update events. if (!options.silent) { @@ -219,7 +247,7 @@ Object.assign(Collection.prototype, Events, { options.changes = { added: toAdd, removed: toRemove, - merged: toMerge + merged: toMerge, }; this.trigger('update', this, options); } @@ -227,242 +255,304 @@ Object.assign(Collection.prototype, Events, { // Return the added (or merged) model (or models). return singular ? models[0] : models; - }, - - clearStore: async function(options={}, filter=(o) => o) { - await Promise.all(this.models - .filter(filter) - .map(m => { - return new Promise( - resolve => { - m.destroy(Object.assign(options, { - 'success': resolve, - 'error': (m, e) => { console.error(e); resolve() } - })); - } - ); - }) - ); - await this.browserStorage.clear(); - this.reset(); - }, - - // When you have more items than you want to add or remove individually, - // you can reset the entire set with a new list of models, without firing - // any granular `add` or `remove` events. Fires `reset` when finished. - // Useful for bulk operations and optimizations. - reset: function(models, options) { + } + + async clearStore(options = {}, filter = (o) => o) { + await Promise.all( + this.models.filter(filter).map((m) => { + return new Promise((resolve) => { + m.destroy( + Object.assign(options, { + 'success': resolve, + 'error': (m, e) => { + console.error(e); + resolve(); + }, + }) + ); + }); + }) + ); + await this.browserStorage.clear(); + this.reset(); + } + + /** + * When you have more items than you want to add or remove individually, + * you can reset the entire set with a new list of models, without firing + * any granular `add` or `remove` events. Fires `reset` when finished. + * Useful for bulk operations and optimizations. + * @param {Model|Model[]} [models] + * @param {Options} [options] + */ + reset(models, options) { options = options ? clone(options) : {}; for (let i = 0; i < this.models.length; i++) { this._removeReference(this.models[i], options); } options.previousModels = this.models; this._reset(); - models = this.add(models, extend({silent: true}, options)); + models = this.add(models, Object.assign({ silent: true }, options)); if (!options.silent) this.trigger('reset', this, options); return models; - }, + } - // Add a model to the end of the collection. - push: function(model, options) { - return this.add(model, extend({at: this.length}, options)); - }, + /** + * Add a model to the end of the collection. + * @param {Model} model + * @param {Options} [options] + */ + push(model, options) { + return this.add(model, Object.assign({ at: this.length }, options)); + } - // Remove a model from the end of the collection. - pop: function(options) { + /** + * Remove a model from the end of the collection. + * @param {Options} [options] + */ + pop(options) { const model = this.at(this.length - 1); return this.remove(model, options); - }, + } - // Add a model to the beginning of the collection. - unshift: function(model, options) { - return this.add(model, extend({at: 0}, options)); - }, + /** + * Add a model to the beginning of the collection. + * @param {Model} model + * @param {Options} [options] + */ + unshift(model, options) { + return this.add(model, Object.assign({ at: 0 }, options)); + } - // Remove a model from the beginning of the collection. - shift: function(options) { + /** + * Remove a model from the beginning of the collection. + * @param {Options} [options] + */ + shift(options) { const model = this.at(0); return this.remove(model, options); - }, + } - // Slice out a sub-array of models from the collection. - slice: function() { + /** Slice out a sub-array of models from the collection. */ + slice() { return slice.apply(this.models, arguments); - }, + } - filter: function(callback, thisArg) { - return this.models.filter( - isFunction(callback) ? callback : m => m.matches(callback), - thisArg - ); - }, + /** + * @param {Function|Object} callback + * @param {any} thisArg + */ + filter(callback, thisArg) { + return this.models.filter(isFunction(callback) ? callback : (m) => m.matches(callback), thisArg); + } - every: function(pred) { - return every(this.models.map(m => m.attributes), pred); - }, + /** + * @param {Function} pred + */ + every(pred) { + if (isFunction(pred)) { + return this.models.map((m) => m.attributes).every(pred); + } else { + return this.models.every((m) => m.matches(pred)); + } + } - difference: function(values) { - return difference(this.models, values); - }, + /** + * @param {Model[]} values + */ + difference(values) { + return this.models.filter((m) => !values.includes(m)); + } - max: function() { + max() { return Math.max.apply(Math, this.models); - }, + } - min: function() { + min() { return Math.min.apply(Math, this.models); - }, + } - drop: function(n=1) { + drop(n = 1) { return this.models.slice(n); - }, + } - some: function(pred) { - return some(this.models.map(m => m.attributes), pred); - }, + /** + * @param {Function|Object} pred + */ + some(pred) { + if (isFunction(pred)) { + return this.models.map((m) => m.attributes).some(pred); + } else { + return this.models.some((m) => m.matches(pred)); + } + } - sortBy: function(iteratee) { + sortBy(iteratee) { return sortBy( this.models, - isFunction(iteratee) ? iteratee : m => isString(iteratee) ? m.get(iteratee) : m.matches(iteratee), + isFunction(iteratee) ? iteratee : (m) => (isString(iteratee) ? m.get(iteratee) : m.matches(iteratee)) ); - }, + } - isEmpty: function() { - return isEmpty(this.models); - }, + isEmpty() { + return !this.models.length; + } - keyBy: function(iteratee) { + keyBy(iteratee) { return keyBy(this.models, iteratee); - }, + } - each: function(callback, thisArg) { + each(callback, thisArg) { return this.forEach(callback, thisArg); - }, + } - forEach: function(callback, thisArg) { + forEach(callback, thisArg) { return this.models.forEach(callback, thisArg); - }, + } - includes: function(item) { + includes(item) { return this.models.includes(item); - }, + } - size: function() { + size() { return this.models.length; - }, + } - countBy: function(f) { - return countBy( - this.models, - isFunction(f) ? f : m => isString(f) ? m.get(f) : m.matches(f), - ); - }, + countBy(f) { + return countBy(this.models, isFunction(f) ? f : (m) => (isString(f) ? m.get(f) : m.matches(f))); + } - groupBy: function(pred) { - return groupBy( - this.models, - isFunction(pred) ? pred : m => isString(pred) ? m.get(pred) : m.matches(pred), - ); - }, + groupBy(pred) { + return groupBy(this.models, isFunction(pred) ? pred : (m) => (isString(pred) ? m.get(pred) : m.matches(pred))); + } - indexOf: function(fromIndex) { - return indexOf(this.models, fromIndex); - }, + /** + * @param {number} fromIndex + */ + indexOf(fromIndex) { + return this.models.indexOf(fromIndex); + } - findLastIndex: function(pred, fromIndex) { - return findLastIndex( - this.models, - isFunction(pred) ? pred : m => isString(pred) ? m.get(pred) : m.matches(pred), + /** + * @param {Function|string|RegExp} pred + * @param {number} fromIndex + */ + findLastIndex(pred, fromIndex) { + return this.models.findLastIndex( + isFunction(pred) ? pred : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)), fromIndex ); - }, + } - lastIndexOf: function(fromIndex) { - return lastIndexOf(this.models, fromIndex); - }, + /** + * @param {number} fromIndex + */ + lastIndexOf(fromIndex) { + return this.models.lastIndexOf(fromIndex); + } - findIndex: function(pred) { - return findIndex( - this.models, - isFunction(pred) ? pred : m => isString(pred) ? m.get(pred) : m.matches(pred), - ); - }, + /** + * @param {Function|string|RegExp} pred + */ + findIndex(pred) { + return this.models.findIndex(isFunction(pred) ? pred : (m) => (isString(pred) ? m.get(pred) : m.matches(pred))); + } - last: function() { + last() { const length = this.models == null ? 0 : this.models.length; return length ? this.models[length - 1] : undefined; - }, + } - head: function() { + head() { return this.models[0]; - }, + } - first: function() { + first() { return this.head(); - }, + } - map: function(cb, thisArg) { - return this.models.map( - isFunction(cb) ? cb : m => isString(cb) ? m.get(cb) : m.matches(cb), - thisArg - ); - }, + map(cb, thisArg) { + return this.models.map(isFunction(cb) ? cb : (m) => (isString(cb) ? m.get(cb) : m.matches(cb)), thisArg); + } - reduce: function(callback, initialValue) { + reduce(callback, initialValue) { return this.models.reduce(callback, initialValue || this.models[0]); - }, + } - reduceRight: function(callback, initialValue) { + reduceRight(callback, initialValue) { return this.models.reduceRight(callback, initialValue || this.models[0]); - }, + } - toArray: function() { + toArray() { return Array.from(this.models); - }, + } - // Get a model from the set by id, cid, model object with id or cid - // properties, or an attributes object that is transformed through modelId. - get: function(obj) { + /** + * Get a model from the set by id, cid, model object with id or cid + * properties, or an attributes object that is transformed through modelId. + * @param {string|number|Object|Model} obj + */ + get(obj) { if (obj == null) return undefined; - return this._byId[obj] || + return ( + this._byId[obj] || this._byId[this.modelId(this._isModel(obj) ? obj.attributes : obj)] || - obj.cid && this._byId[obj.cid]; - }, + (obj.cid && this._byId[obj.cid]) + ); + } - // Returns `true` if the model is in the collection. - has: function(obj) { + /** + * Returns `true` if the model is in the collection. + * @param {string|number|Object|Model} obj + */ + has(obj) { return this.get(obj) != null; - }, + } - // Get the model at the given index. - at: function(index) { + /** + * Get the model at the given index. + * @param {number} index + */ + at(index) { if (index < 0) index += this.length; return this.models[index]; - }, + } - // Return models with matching attributes. Useful for simple cases of - // `filter`. - where: function(attrs, first) { + /** + * Return models with matching attributes. Useful for simple cases of + * `filter`. + * @param {Attributes} attrs + * @param {boolean} first + */ + where(attrs, first) { return this[first ? 'find' : 'filter'](attrs); - }, + } - // Return the first model with matching attributes. Useful for simple cases - // of `find`. - findWhere: function(attrs) { + /** + * Return the first model with matching attributes. Useful for simple cases + * of `find`. + * @param {Attributes} attrs + */ + findWhere(attrs) { return this.where(attrs, true); - }, + } - find: function(predicate, fromIndex) { - const pred = isFunction(predicate) ? predicate : m => m.matches(predicate); + /** + * @param {Attributes} predicate + * @param {number} [fromIndex] + */ + find(predicate, fromIndex) { + const pred = isFunction(predicate) ? predicate : (m) => m.matches(predicate); return this.models.find(pred, fromIndex); - }, - + } - // Force the collection to re-sort itself. You don't need to call this under - // normal circumstances, as the set will maintain sort order as each item - // is added. - sort: function(options) { + /** + * Force the collection to re-sort itself. You don't need to call this under + * normal circumstances, as the set will maintain sort order as each item + * is added. + * @param {Options} options + */ + sort(options) { let comparator = this.comparator; if (!comparator) throw new Error('Cannot sort a set without a comparator'); options || (options = {}); @@ -478,22 +568,29 @@ Object.assign(Collection.prototype, Events, { } if (!options.silent) this.trigger('sort', this, options); return this; - }, + } - // Pluck an attribute from each model in the collection. - pluck: function(attr) { + /** + * Pluck an attribute from each model in the collection. + * @param {string} attr + */ + pluck(attr) { return this.map(attr + ''); - }, + } - // Fetch the default set of models for this collection, resetting the - // collection when they arrive. If `reset: true` is passed, the response - // data will be passed through the `reset` method instead of `set`. - fetch: function(options) { - options = extend({parse: true}, options); + /** + * Fetch the default set of models for this collection, resetting the + * collection when they arrive. If `reset: true` is passed, the response + * data will be passed through the `reset` method instead of `set`. + * @param {Options} options + */ + fetch(options) { + options = Object.assign({ parse: true }, options); const success = options.success; + // eslint-disable-next-line @typescript-eslint/no-this-alias const collection = this; const promise = options.promise && getResolveablePromise(); - options.success = function(resp) { + options.success = function (resp) { const method = options.reset ? 'reset' : 'set'; collection[method](resp, options); if (success) success.call(options.context, collection, resp, options); @@ -502,12 +599,16 @@ Object.assign(Collection.prototype, Events, { }; wrapError(this, options); return promise ? promise : this.sync('read', this, options); - }, + } - // Create a new instance of a model in this collection. Add the model to the - // collection immediately, unless `wait: true` is passed, in which case we - // wait for the server to agree. - create: function(model, options) { + /** + * Create a new instance of a model in this collection. Add the model to the + * collection immediately, unless `wait: true` is passed, in which case we + * wait for the server to agree. + * @param {Model|Attributes} model + * @param {Options} [options] + */ + create(model, options) { options = options ? clone(options) : {}; const wait = options.wait; const return_promise = options.promise; @@ -516,10 +617,11 @@ Object.assign(Collection.prototype, Events, { model = this._prepareModel(model, options); if (!model) return false; if (!wait) this.add(model, options); + // eslint-disable-next-line @typescript-eslint/no-this-alias const collection = this; const success = options.success; const error = options.error; - options.success = function(m, resp, callbackOpts) { + options.success = function (m, resp, callbackOpts) { if (wait) { collection.add(m, callbackOpts); } @@ -530,78 +632,96 @@ Object.assign(Collection.prototype, Events, { promise.resolve(m); } }; - options.error = function(model, e, options) { + options.error = function (model, e, options) { error && error.call(options.context, model, e, options); return_promise && promise.reject(e); - } + }; - model.save(null, Object.assign(options, {'promise': false})); + model.save(null, Object.assign(options, { 'promise': false })); if (return_promise) { return promise; } else { return model; } - }, + } - // **parse** converts a response into a list of models to be added to the - // collection. The default implementation is just to pass it through. - parse: function(resp, options) { + /** + * **parse** converts a response into a list of models to be added to the + * collection. The default implementation is just to pass it through. + * @param {Object} resp + * @param {Options} [options] + */ + parse(resp, options) { return resp; - }, - - // Create a new collection with an identical list of models as this one. - clone: function() { - return new this.constructor(this.models, { - model: this.model, - comparator: this.comparator - }); - }, + } - // Define how to uniquely identify models in the collection. - modelId: function(attrs) { + /** + * Define how to uniquely identify models in the collection. + * @param {Attributes} attrs + */ + modelId(attrs) { return attrs[this.model.prototype?.idAttribute || 'id']; - }, + } - // Get an iterator of all models in this collection. - values: function() { + /** Get an iterator of all models in this collection. */ + values() { return new CollectionIterator(this, ITERATOR_VALUES); - }, + } - // Get an iterator of all model IDs in this collection. - keys: function() { + /** Get an iterator of all model IDs in this collection. */ + keys() { return new CollectionIterator(this, ITERATOR_KEYS); - }, + } - // Get an iterator of all [ID, model] tuples in this collection. - entries: function() { + /** Get an iterator of all [ID, model] tuples in this collection. */ + entries() { return new CollectionIterator(this, ITERATOR_KEYSVALUES); - }, + } - // Private method to reset all internal state. Called when the collection - // is first initialized or reset. - _reset: function() { - this.length = 0; + /** + * Private method to reset all internal state. Called when the collection + * is first initialized or reset. + */ + _reset() { this.models = []; - this._byId = {}; - }, + this._byId = {}; + } + + /** + * @param {Attributes} attrs + * @param {Options} [options] + */ + createModel(attrs, options) { + const Klass = this.model; + return new Klass(attrs, options); + } - // Prepare a hash of attributes (or other model) to be added to this - // collection. - _prepareModel: function(attrs, options) { + /** + * Prepare a hash of attributes (or other model) to be added to this + * collection. + * @param {Attributes|Model} attrs + * @param {Options} [options] + * @return {Model} + */ + _prepareModel(attrs, options) { if (this._isModel(attrs)) { if (!attrs.collection) attrs.collection = this; - return attrs; + return /** @type {Model} */(attrs); } options = options ? clone(options) : {}; options.collection = this; - const model = new this.model(attrs, options); + const model = this.createModel(attrs, options); if (!model.validationError) return model; this.trigger('invalid', this, model.validationError, options); - return false; - }, + return null; + } - // Internal method called by both remove and set. - _removeModels: function(models, options) { + /** + * Internal method called by both remove and set. + * @param {Model[]} models + * @param {Options} [options] + */ + _removeModels(models, options) { const removed = []; for (let i = 0; i < models.length; i++) { const model = this.get(models[i]); @@ -609,7 +729,6 @@ Object.assign(Collection.prototype, Events, { const index = this.indexOf(model); this.models.splice(index, 1); - this.length--; // Remove references before triggering 'remove' event to prevent an // infinite loop. #3693 @@ -626,36 +745,55 @@ Object.assign(Collection.prototype, Events, { this._removeReference(model, options); } return removed; - }, + } - // Method for checking whether an object should be considered a model for - // the purposes of adding to the collection. - _isModel: function(model) { + /** + * Method for checking whether an object should be considered a model for + * the purposes of adding to the collection. + * @param {any} model + */ + _isModel(model) { return model instanceof Model; - }, + } - // Internal method to create a model's ties to a collection. - _addReference: function(model, options) { + /** + * Internal method to create a model's ties to a collection. + * @param {Model} model + * @param {Options} [options] + */ + _addReference(model, options) { this._byId[model.cid] = model; const id = this.modelId(model.attributes); if (id != null) this._byId[id] = model; model.on('all', this._onModelEvent, this); - }, + } - // Internal method to sever a model's ties to a collection. - _removeReference: function(model, options) { + /** + * Internal method to sever a model's ties to a collection. + * @private + * @param {Model} model + * @param {Options} [options] + */ + _removeReference(model, options) { delete this._byId[model.cid]; const id = this.modelId(model.attributes); if (id != null) delete this._byId[id]; if (this === model.collection) delete model.collection; model.off('all', this._onModelEvent, this); - }, + } - // Internal method called every time a model in the set fires an event. - // Sets need to update their indexes when models change ids. All other - // events simply proxy through. "add" and "remove" events that originate - // in other collections are ignored. - _onModelEvent: function(event, model, collection, options) { + /** + * Internal method called every time a model in the set fires an event. + * Sets need to update their indexes when models change ids. All other + * events simply proxy through. "add" and "remove" events that originate + * in other collections are ignored. + * @private + * @param {any} event + * @param {Model} model + * @param {Collection} collection + * @param {Options} [options] + */ + _onModelEvent(event, model, collection, options) { if (model) { if ((event === 'add' || event === 'remove') && collection !== this) return; if (event === 'destroy') this.remove(model, options); @@ -670,12 +808,10 @@ Object.assign(Collection.prototype, Events, { } this.trigger.apply(this, arguments); } - -}); +} // Defining an @@iterator method implements JavaScript's Iterable protocol. // In modern ES2015 browsers, this value is found at Symbol.iterator. -/* global Symbol */ const $$iterator = typeof Symbol === 'function' && Symbol.iterator; if ($$iterator) { Collection.prototype[$$iterator] = Collection.prototype.values; @@ -688,7 +824,7 @@ if ($$iterator) { // use of `for of` loops in modern browsers and interoperation between // Collection and other JavaScript functions and third-party libraries // which can operate on Iterables. -const CollectionIterator = function(collection, kind) { +const CollectionIterator = function (collection, kind) { this._collection = collection; this._kind = kind; this._index = 0; @@ -703,14 +839,13 @@ const ITERATOR_KEYSVALUES = 3; // All Iterators should themselves be Iterable. if ($$iterator) { - CollectionIterator.prototype[$$iterator] = function() { + CollectionIterator.prototype[$$iterator] = function () { return this; }; } -CollectionIterator.prototype.next = function() { +CollectionIterator.prototype.next = function () { if (this._collection) { - // Only continue iterating if the iterated collection is long enough. if (this._index < this._collection.length) { const model = this._collection.at(this._index); @@ -724,11 +859,12 @@ CollectionIterator.prototype.next = function() { const id = this._collection.modelId(model.attributes); if (this._kind === ITERATOR_KEYS) { value = id; - } else { // ITERATOR_KEYSVALUES + } else { + // ITERATOR_KEYSVALUES value = [id, model]; } } - return {value: value, done: false}; + return { value: value, done: false }; } // Once exhausted, remove the reference to the collection so future @@ -736,5 +872,7 @@ CollectionIterator.prototype.next = function() { this._collection = undefined; } - return {value: undefined, done: true}; + return { value: undefined, done: true }; }; + +export { Collection }; diff --git a/src/drivers/sessionStorage.js b/src/drivers/sessionStorage.js index 2f45fdaf..524507df 100644 --- a/src/drivers/sessionStorage.js +++ b/src/drivers/sessionStorage.js @@ -19,202 +19,199 @@ import getCallback from 'localforage/src/utils/getCallback'; import normalizeKey from 'localforage/src/utils/normalizeKey'; import serializer from 'localforage/src/utils/serializer'; -const serialize = serializer["serialize"]; -const deserialize = serializer["deserialize"]; - - -function isSessionStorageValid () { - // If the app is running inside a Google Chrome packaged webapp, or some - // other context where sessionStorage isn't available, we don't use - // sessionStorage. This feature detection is preferred over the old - // `if (window.chrome && window.chrome.runtime)` code. - // See: https://github.com/mozilla/localForage/issues/68 - try { - // If sessionStorage isn't available, we get outta here! - // This should be inside a try catch - if (sessionStorage && ('setItem' in sessionStorage)) { - return true; - } - } catch (e) { - console.log(e); +const serialize = serializer['serialize']; +const deserialize = serializer['deserialize']; + +function isSessionStorageValid() { + // If the app is running inside a Google Chrome packaged webapp, or some + // other context where sessionStorage isn't available, we don't use + // sessionStorage. This feature detection is preferred over the old + // `if (window.chrome && window.chrome.runtime)` code. + // See: https://github.com/mozilla/localForage/issues/68 + try { + // If sessionStorage isn't available, we get outta here! + // This should be inside a try catch + if (sessionStorage && 'setItem' in sessionStorage) { + return true; } - return false; + } catch (e) { + console.log(e); + } + return false; } function _getKeyPrefix(options, defaultConfig) { - let keyPrefix = options.name + '/'; + let keyPrefix = options.name + '/'; - if (options.storeName !== defaultConfig.storeName) { - keyPrefix += options.storeName + '/'; - } - return keyPrefix; + if (options.storeName !== defaultConfig.storeName) { + keyPrefix += options.storeName + '/'; + } + return keyPrefix; } const dbInfo = { - 'serializer': { - 'serialize': serialize, - 'deserialize': deserialize - } + 'serializer': { + 'serialize': serialize, + 'deserialize': deserialize, + }, }; function _initStorage(options) { - dbInfo.keyPrefix = _getKeyPrefix(options, this._defaultConfig); - if (options) { - for (const i in options) { // eslint-disable-line guard-for-in - dbInfo[i] = options[i]; - } + dbInfo.keyPrefix = _getKeyPrefix(options, this._defaultConfig); + if (options) { + for (const i in options) { + // eslint-disable-line guard-for-in + dbInfo[i] = options[i]; } + } } // Remove all keys from the datastore, effectively destroying all data in // the app's key/value store! function clear(callback) { - const promise = this.ready().then(function() { - const keyPrefix = dbInfo.keyPrefix; + const promise = this.ready().then(function () { + const keyPrefix = dbInfo.keyPrefix; - for (let i = sessionStorage.length - 1; i >= 0; i--) { - const key = sessionStorage.key(i); + for (let i = sessionStorage.length - 1; i >= 0; i--) { + const key = sessionStorage.key(i); - if (key.indexOf(keyPrefix) === 0) { - sessionStorage.removeItem(key); - } - } - }); + if (key.indexOf(keyPrefix) === 0) { + sessionStorage.removeItem(key); + } + } + }); - executeCallback(promise, callback); - return promise; + executeCallback(promise, callback); + return promise; } // Retrieve an item from the store. Unlike the original async_storage // library in Gaia, we don't modify return values at all. If a key's value // is `undefined`, we pass that value to the callback function. function getItem(key, callback) { - key = normalizeKey(key); - - const promise = this.ready().then(function() { - let result = sessionStorage.getItem(dbInfo.keyPrefix + key); - // If a result was found, parse it from the serialized - // string into a JS object. If result isn't truthy, the key - // is likely undefined and we'll pass it straight to the - // callback. - if (result) { - result = dbInfo.serializer.deserialize(result); - } - return result; - }); - executeCallback(promise, callback); - return promise; + key = normalizeKey(key); + + const promise = this.ready().then(function () { + let result = sessionStorage.getItem(dbInfo.keyPrefix + key); + // If a result was found, parse it from the serialized + // string into a JS object. If result isn't truthy, the key + // is likely undefined and we'll pass it straight to the + // callback. + if (result) { + result = dbInfo.serializer.deserialize(result); + } + return result; + }); + executeCallback(promise, callback); + return promise; } // Iterate over all items in the store. function iterate(iterator, callback) { - const self = this; - - const promise = self.ready().then(function() { - const keyPrefix = dbInfo.keyPrefix; - const keyPrefixLength = keyPrefix.length; - const length = sessionStorage.length; - - // We use a dedicated iterator instead of the `i` variable below - // so other keys we fetch in sessionStorage aren't counted in - // the `iterationNumber` argument passed to the `iterate()` - // callback. - // - // See: github.com/mozilla/localForage/pull/435#discussion_r38061530 - let iterationNumber = 1; - - for (let i = 0; i < length; i++) { - const key = sessionStorage.key(i); - if (key.indexOf(keyPrefix) !== 0) { - continue; - } - let value = sessionStorage.getItem(key); - - // If a result was found, parse it from the serialized - // string into a JS object. If result isn't truthy, the - // key is likely undefined and we'll pass it straight - // to the iterator. - if (value) { - value = dbInfo.serializer.deserialize(value); - } - - value = iterator( - value, - key.substring(keyPrefixLength), - iterationNumber++ - ); - - if (value !== void 0) { // eslint-disable-line no-void - return value; - } - } - }); + const self = this; + + const promise = self.ready().then(function () { + const keyPrefix = dbInfo.keyPrefix; + const keyPrefixLength = keyPrefix.length; + const length = sessionStorage.length; + + // We use a dedicated iterator instead of the `i` variable below + // so other keys we fetch in sessionStorage aren't counted in + // the `iterationNumber` argument passed to the `iterate()` + // callback. + // + // See: github.com/mozilla/localForage/pull/435#discussion_r38061530 + let iterationNumber = 1; + + for (let i = 0; i < length; i++) { + const key = sessionStorage.key(i); + if (key.indexOf(keyPrefix) !== 0) { + continue; + } + let value = sessionStorage.getItem(key); + + // If a result was found, parse it from the serialized + // string into a JS object. If result isn't truthy, the + // key is likely undefined and we'll pass it straight + // to the iterator. + if (value) { + value = dbInfo.serializer.deserialize(value); + } + + value = iterator(value, key.substring(keyPrefixLength), iterationNumber++); + + if (value !== void 0) { + // eslint-disable-line no-void + return value; + } + } + }); - executeCallback(promise, callback); - return promise; + executeCallback(promise, callback); + return promise; } // Same as sessionStorage's key() method, except takes a callback. function key(n, callback) { - const self = this; - const promise = self.ready().then(function() { - let result; - try { - result = sessionStorage.key(n); - } catch (error) { - result = null; - } + const self = this; + const promise = self.ready().then(function () { + let result; + try { + result = sessionStorage.key(n); + } catch (error) { + result = null; + } - // Remove the prefix from the key, if a key is found. - if (result) { - result = result.substring(dbInfo.keyPrefix.length); - } + // Remove the prefix from the key, if a key is found. + if (result) { + result = result.substring(dbInfo.keyPrefix.length); + } - return result; - }); + return result; + }); - executeCallback(promise, callback); - return promise; + executeCallback(promise, callback); + return promise; } function keys(callback) { - const self = this; - const promise = self.ready().then(function() { - const length = sessionStorage.length; - const keys = []; - - for (let i = 0; i < length; i++) { - const itemKey = sessionStorage.key(i); - if (itemKey.indexOf(dbInfo.keyPrefix) === 0) { - keys.push(itemKey.substring(dbInfo.keyPrefix.length)); - } - } - return keys; - }); + const self = this; + const promise = self.ready().then(function () { + const length = sessionStorage.length; + const keys = []; + + for (let i = 0; i < length; i++) { + const itemKey = sessionStorage.key(i); + if (itemKey.indexOf(dbInfo.keyPrefix) === 0) { + keys.push(itemKey.substring(dbInfo.keyPrefix.length)); + } + } + return keys; + }); - executeCallback(promise, callback); - return promise; + executeCallback(promise, callback); + return promise; } // Supply the number of keys in the datastore to the callback function. function length(callback) { - const self = this; - const promise = self.keys().then(function(keys) { - return keys.length; - }); + const self = this; + const promise = self.keys().then(function (keys) { + return keys.length; + }); - executeCallback(promise, callback); - return promise; + executeCallback(promise, callback); + return promise; } // Remove an item from the store, nice and simple. function removeItem(key, callback) { - key = normalizeKey(key); - const promise = this.ready().then(function() { - sessionStorage.removeItem(dbInfo.keyPrefix + key); - }); - executeCallback(promise, callback); - return promise; + key = normalizeKey(key); + const promise = this.ready().then(function () { + sessionStorage.removeItem(dbInfo.keyPrefix + key); + }); + executeCallback(promise, callback); + return promise; } // Set a key's value and run an optional callback once the value is set. @@ -222,85 +219,82 @@ function removeItem(key, callback) { // in case you want to operate on that value only after you're sure it // saved, or something like that. async function setItem(key, value, callback) { - key = normalizeKey(key); - await this.ready(); - - // Convert undefined values to null. - // https://github.com/mozilla/localForage/pull/42 - value = value ?? null; - - // Save the original value to pass to the callback. - const originalValue = value; - - dbInfo.serializer.serialize(value, (value, error) => { - if (error) { - throw error; - } else { - try { - sessionStorage.setItem(dbInfo.keyPrefix + key, value); - executeCallback(Promise.resolve(originalValue), callback); - } catch (e) { - if ( - e.name === 'QuotaExceededError' || - e.name === 'NS_ERROR_DOM_QUOTA_REACHED' - ) { - console.error("Your sesionStorage capacity is used up."); - throw e; - } - throw e; - } - } - }); -} + key = normalizeKey(key); + await this.ready(); -function dropInstance(options, callback) { - callback = getCallback.apply(this, arguments); + // Convert undefined values to null. + // https://github.com/mozilla/localForage/pull/42 + value = value ?? null; - options = (typeof options !== 'function' && options) || {}; - if (!options.name) { - const currentConfig = this.config(); - options.name = options.name || currentConfig.name; - options.storeName = options.storeName || currentConfig.storeName; - } + // Save the original value to pass to the callback. + const originalValue = value; - const self = this; - let promise; - if (!options.name) { - promise = Promise.reject(new Error('Invalid arguments')); + dbInfo.serializer.serialize(value, (value, error) => { + if (error) { + throw error; } else { - promise = new Promise(function(resolve) { - if (!options.storeName) { - resolve(`${options.name}/`); - } else { - resolve(_getKeyPrefix(options, self._defaultConfig)); - } - }).then(function(keyPrefix) { - for (let i = sessionStorage.length - 1; i >= 0; i--) { - const key = sessionStorage.key(i); - if (key.indexOf(keyPrefix) === 0) { - sessionStorage.removeItem(key); - } - } - }); + try { + sessionStorage.setItem(dbInfo.keyPrefix + key, value); + executeCallback(Promise.resolve(originalValue), callback); + } catch (e) { + if (e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') { + console.error('Your sesionStorage capacity is used up.'); + throw e; + } + throw e; + } } + }); +} + +function dropInstance(options, callback) { + callback = getCallback.apply(this, arguments); + + options = (typeof options !== 'function' && options) || {}; + if (!options.name) { + const currentConfig = this.config(); + options.name = options.name || currentConfig.name; + options.storeName = options.storeName || currentConfig.storeName; + } + + const self = this; + let promise; + if (!options.name) { + promise = Promise.reject(new Error('Invalid arguments')); + } else { + promise = new Promise(function (resolve) { + if (!options.storeName) { + resolve(`${options.name}/`); + } else { + resolve(_getKeyPrefix(options, self._defaultConfig)); + } + }).then(function (keyPrefix) { + for (let i = sessionStorage.length - 1; i >= 0; i--) { + const key = sessionStorage.key(i); + if (key.indexOf(keyPrefix) === 0) { + sessionStorage.removeItem(key); + } + } + }); + } - executeCallback(promise, callback); - return promise; + executeCallback(promise, callback); + return promise; } const sessionStorageWrapper = { - _driver: 'sessionStorageWrapper', - _initStorage: _initStorage, - _support: isSessionStorageValid(), - iterate: iterate, - getItem: getItem, - setItem: setItem, - removeItem: removeItem, - clear: clear, - length: length, - key: key, - keys: keys, - dropInstance: dropInstance + _driver: 'sessionStorageWrapper', + _initStorage: _initStorage, + _support: isSessionStorageValid(), + iterate: iterate, + getItem: getItem, + setItem: setItem, + removeItem: removeItem, + clear: clear, + length: length, + key: key, + keys: keys, + dropInstance: dropInstance, }; export default sessionStorageWrapper; diff --git a/src/element.js b/src/element.js index 85fde106..4b5115c8 100644 --- a/src/element.js +++ b/src/element.js @@ -1,45 +1,40 @@ -import extend from "lodash-es/extend.js"; -import isElement from "lodash-es/isElement.js"; -import isFunction from "lodash-es/isFunction.js"; -import pick from "lodash-es/pick.js"; -import result from "lodash-es/result.js"; -import uniqueId from "lodash-es/uniqueId.js"; -import { Events } from './events.js'; -import { inherits, NotImplementedError } from './helpers.js'; +import uniqueId from 'lodash-es/uniqueId.js'; import { render } from 'lit-html'; - - -const paddedLt = /^\s*} options + */ constructor(options) { super(); + + // Will be assigned to from Events + this.stopListening = null; + // Creating a View creates its initial element outside of the DOM, // if an existing element is not provided... this.cid = uniqueId('view'); this._domEvents = []; - extend(this, pick(options, viewOptions)); + + const { model, collection, events } = options; + + Object.assign(this, { model, collection, events }); } - createRenderRoot () { + createRenderRoot() { // Render without the shadow DOM return this; } - connectedCallback () { + connectedCallback() { if (!this._initialized) { this.preinitialize.apply(this, arguments); this.initialize.apply(this, arguments); @@ -48,43 +43,58 @@ export class ElementView extends HTMLElement { this.delegateEvents(); } - disconnectedCallback () { + disconnectedCallback() { this.undelegateEvents(); - this.stopListening(); + this.stopListening?.(); } - // preinitialize is an empty function by default. You can override it with a function - // or object. preinitialize will run before any instantiation logic is run in the View - preinitialize () { // eslint-disable-line class-methods-use-this - } - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize() {} // eslint-disable-line class-methods-use-this - - // **render** is the core function that your view should override, in order - // to populate its element (`this.el`), with the appropriate HTML. The - // convention is for **render** to always return `this`. + /** + * preinitialize is an empty function by default. You can override it with a function + * or object. preinitialize will run before any instantiation logic is run in the View + * eslint-disable-next-line class-methods-use-this + */ + preinitialize() {} + + /** + * Initialize is an empty function by default. Override it with your own + * initialization logic. + */ + initialize() {} + + beforeRender() {} + afterRender() {} + + /** + * **render** is the core function that your view should override, in order + * to populate its element (`this.el`), with the appropriate HTML. The + * convention is for **render** to always return `this`. + */ render() { - isFunction(this.beforeRender) && this.beforeRender(); - isFunction(this.toHTML) && render(this.toHTML(), this); - isFunction(this.afterRender) && this.afterRender(); + this.beforeRender(); + render(this.toHTML(), this); + this.afterRender(); return this; } - // Set callbacks, where `this.events` is a hash of - // - // *{"event selector": "callback"}* - // - // { - // 'mousedown .title': 'edit', - // 'click .button': 'save', - // 'click .open': function(e) { ... } - // } - // - // pairs. Callbacks will be bound to the view, with `this` set properly. - // Uses event delegation for efficiency. - // Omitting the selector binds the event to `this.el`. + toHTML() { + return ''; + } + + /** + * Set callbacks, where `this.events` is a hash of + * + * *{"event selector": "callback"}* + * + * { + * 'mousedown .title': 'edit', + * 'click .button': 'save', + * 'click .open': function(e) { ... } + * } + * + * pairs. Callbacks will be bound to the view, with `this` set properly. + * Uses event delegation for efficiency. + * Omitting the selector binds the event to `this.el`. + */ delegateEvents() { if (!this.events) { return this; @@ -92,7 +102,7 @@ export class ElementView extends HTMLElement { this.undelegateEvents(); for (const key in this.events) { let method = this.events[key]; - if (!isFunction(method)) method = this[method]; + if (typeof method !== 'function') method = this[method]; if (!method) continue; const match = key.match(delegateEventSplitter); this.delegate(match[1], match[2], method.bind(this)); @@ -100,15 +110,21 @@ export class ElementView extends HTMLElement { return this; } - // Make a event delegation handler for the given `eventName` and `selector` - // and attach it to `this.el`. - // If selector is empty, the listener will be bound to `this.el`. If not, a - // new handler that will recursively traverse up the event target's DOM - // hierarchy looking for a node that matches the selector. If one is found, - // the event's `delegateTarget` property is set to it and the return the - // result of calling bound `listener` with the parameters given to the - // handler. + /** + * Make a event delegation handler for the given `eventName` and `selector` + * and attach it to `this.el`. + * If selector is empty, the listener will be bound to `this.el`. If not, a + * new handler that will recursively traverse up the event target's DOM + * hierarchy looking for a node that matches the selector. If one is found, + * the event's `delegateTarget` property is set to it and the return the + * result of calling bound `listener` with the parameters given to the + * handler. + * @param {string} eventName + * @param {string} selector + * @param {(ev: Event) => any} listener + */ delegate(eventName, selector, listener) { + // eslint-disable-next-line @typescript-eslint/no-this-alias const root = this; if (!root) { return this; @@ -123,29 +139,33 @@ export class ElementView extends HTMLElement { for (let i = 0, len = els.length; i < len; i++) { const item = els[i]; item.addEventListener(eventName, listener, false); - this._domEvents.push({el: item, eventName: eventName, handler: listener}); + this._domEvents.push({ el: item, eventName: eventName, handler: listener }); } return listener; } - const handler = selector ? function (e) { - let node = e.target || e.srcElement; - for (; node && node != root; node = node.parentNode) { - if (node.matches(selector)) { - e.delegateTarget = node; - listener(e); + const handler = selector + ? function (e) { + let node = e.target || e.srcElement; + for (; node && node != root; node = node.parentNode) { + if (node.matches(selector)) { + e.delegateTarget = node; + listener(e); + } + } } - } - } : listener; + : listener; this.addEventListener(eventName, handler, false); - this._domEvents.push({el: this, eventName: eventName, handler: handler, listener: listener, selector: selector}); + this._domEvents.push({ el: this, eventName: eventName, handler: handler, listener: listener, selector: selector }); return this; } - // Clears all callbacks previously bound to the view by `delegateEvents`. - // You usually don't need to use this, but may wish to if you have multiple - // Backbone views attached to the same DOM element. + /** + * Clears all callbacks previously bound to the view by `delegateEvents`. + * You usually don't need to use this, but may wish to if you have multiple + * Backbone views attached to the same DOM element. + */ undelegateEvents() { if (this) { for (let i = 0, len = this._domEvents.length; i < len; i++) { @@ -157,8 +177,13 @@ export class ElementView extends HTMLElement { return this; } - // A finer-grained `undelegateEvents` for removing a single delegated event. - // `selector` and `listener` are both optional. + /** + * A finer-grained `undelegateEvents` for removing a single delegated event. + * `selector` and `listener` are both optional. + * @param {string} eventName + * @param {string} selector + * @param {(ev: Event) => any} listener + */ undelegate(eventName, selector, listener) { if (typeof selector === 'function') { listener = selector; @@ -169,9 +194,10 @@ export class ElementView extends HTMLElement { let i = handlers.length; while (i--) { const item = handlers[i]; - const match = item.eventName === eventName && - (listener ? item.listener === listener : true) && - (selector ? item.selector === selector : true); + const match = + item.eventName === eventName && + (listener ? item.listener === listener : true) && + (selector ? item.selector === selector : true); if (!match) { continue; @@ -184,5 +210,6 @@ export class ElementView extends HTMLElement { } } -// Set up all inheritable **View** properties and methods. -Object.assign(ElementView.prototype, Events); +Object.assign(ElementView.prototype, EventEmitter.prototype); + +export default ElementView; diff --git a/src/eventemitter.js b/src/eventemitter.js new file mode 100644 index 00000000..facb0459 --- /dev/null +++ b/src/eventemitter.js @@ -0,0 +1,180 @@ +/** + * @copyright 2010-2019 Jeremy Ashkenas and DocumentCloud + * @copyright 2023 JC Brand + */ +import Listening from './listening.js'; +import isEmpty from 'lodash-es/isEmpty.js'; +import keys from 'lodash-es/keys.js'; +import uniqueId from 'lodash-es/uniqueId.js'; +import { eventsApi, onApi, offApi, onceMap, tryCatchOn, triggerApi } from './utils/events.js'; + +// A private global variable to share between listeners and listenees. +let _listening; + +class EventEmitter { + /** + * @typedef {import('./model.js').Model} Model + * @typedef {import('./collection.js').Collection} Collection + * @typedef {Record.} Options + * + * @callback EventCallback + * @param {any} event + * @param {Model} model + * @param {Collection} collection + * @param {Options} [options] + */ + + /** + * Bind an event to a `callback` function. Passing `"all"` will bind + * the callback to all events fired. + * @param {string} name + * @param {EventCallback} callback + * @param {any} context + * @return {EventEmitter} + */ + on(name, callback, context) { + this._events = eventsApi(onApi, this._events || {}, name, callback, { + context: context, + ctx: this, + listening: _listening, + }); + + if (_listening) { + const listeners = this._listeners || (this._listeners = {}); + listeners[_listening.id] = _listening; + // Allow the listening to use a counter, instead of tracking + // callbacks for library interop + _listening.interop = false; + } + + return this; + } + + /** + * Inversion-of-control versions of `on`. Tell *this* object to listen to + * an event in another object... keeping track of what it's listening to + * for easier unbinding later. + * @param {any} obj + * @param {string} name + * @param {EventCallback} [callback] + * @return {EventEmitter} + */ + listenTo(obj, name, callback) { + if (!obj) return this; + const id = obj._listenId || (obj._listenId = uniqueId('l')); + const listeningTo = this._listeningTo || (this._listeningTo = {}); + let listening = (_listening = listeningTo[id]); + + // This object is not listening to any other events on `obj` yet. + // Setup the necessary references to track the listening callbacks. + if (!listening) { + this._listenId || (this._listenId = uniqueId('l')); + listening = _listening = listeningTo[id] = new Listening(this, obj); + } + + // Bind callbacks on obj. + const error = tryCatchOn(obj, name, callback, this); + _listening = undefined; + + if (error) throw error; + // If the target obj is not Backbone.Events, track events manually. + if (listening.interop) listening.on(name, callback); + + return this; + } + + /** + * Remove one or many callbacks. If `context` is null, removes all + * callbacks with that function. If `callback` is null, removes all + * callbacks for the event. If `name` is null, removes all bound + * callbacks for all events. + * @param {string} name + * @param {EventCallback} callback + * @param {any} [context] + * @return {EventEmitter} + */ + off(name, callback, context) { + if (!this._events) return this; + this._events = eventsApi(offApi, this._events, name, callback, { + context: context, + listeners: this._listeners, + }); + + return this; + } + + /** + * Tell this object to stop listening to either specific events ... or + * to every object it's currently listening to. + * @param {any} [obj] + * @param {string} [name] + * @param {EventCallback} [callback] + * @return {EventEmitter} + */ + stopListening(obj, name, callback) { + const listeningTo = this._listeningTo; + if (!listeningTo) return this; + + const ids = obj ? [obj._listenId] : keys(listeningTo); + for (let i = 0; i < ids.length; i++) { + const listening = listeningTo[ids[i]]; + + // If listening doesn't exist, this object is not currently + // listening to obj. Break out early. + if (!listening) break; + + listening.obj.off(name, callback, this); + if (listening.interop) listening.off(name, callback); + } + if (isEmpty(listeningTo)) this._listeningTo = undefined; + + return this; + } + + /** + * Bind an event to only be triggered a single time. After the first time + * the callback is invoked, its listener will be removed. If multiple events + * are passed in using the space-separated syntax, the handler will fire + * once for each event, not once for a combination of all events. + * @param {string} name + * @param {EventCallback} callback + * @param {any} context + * @return {EventEmitter} + */ + once(name, callback, context) { + // Map the event into a `{event: once}` object. + const events = eventsApi(onceMap, {}, name, callback, this.off.bind(this)); + if (typeof name === 'string' && (context === null || context === undefined)) callback = undefined; + return this.on(events, callback, context); + } + + /** + * Inversion-of-control versions of `once`. + * @param {any} obj + * @param {string} name + * @param {EventCallback} [callback] + * @return {EventEmitter} + */ + listenToOnce(obj, name, callback) { + // Map the event into a `{event: once}` object. + const events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj)); + return this.listenTo(obj, events); + } + + /** + * Trigger one or many events, firing all bound callbacks. Callbacks are + * passed the same arguments as `trigger` is, apart from the event name + * (unless you're listening on `"all"`, which will cause your callback to + * receive the true name of the event as the first argument). + * @param {string} name + * @return {EventEmitter} + */ + trigger(name, ...args) { + if (!this._events) return this; + + eventsApi(triggerApi, this._events, name, undefined, args); + return this; + } +} + +export default EventEmitter; diff --git a/src/helpers.js b/src/helpers.js index bf9506f5..39b1a495 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -12,13 +12,13 @@ import result from 'lodash-es/result.js'; export class NotImplementedError extends Error {} function S4() { - // Generate four random hex digits. - return (((1+Math.random())*0x10000)|0).toString(16).substring(1); + // Generate four random hex digits. + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); } export function guid() { - // Generate a pseudo-GUID by concatenating random hexadecimal. - return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); + // Generate a pseudo-GUID by concatenating random hexadecimal. + return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4(); } // Helpers @@ -29,92 +29,118 @@ export function guid() { // class properties to be extended. // export function inherits(protoProps, staticProps) { - const parent = this; - let child; - - // The constructor function for the new subclass is either defined by you - // (the "constructor" property in your `extend` definition), or defaulted - // by us to simply call the parent constructor. - if (protoProps && has(protoProps, 'constructor')) { - child = protoProps.constructor; - } else { - child = function(){ return parent.apply(this, arguments); }; - } - - // Add static properties to the constructor function, if supplied. - extend(child, parent, staticProps); - - // Set the prototype chain to inherit from `parent`, without calling - // `parent`'s constructor function and add the prototype properties. - child.prototype = create(parent.prototype, protoProps); - child.prototype.constructor = child; - - // Set a convenience property in case the parent's prototype is needed - // later. - child.__super__ = parent.prototype; - - return child; + // eslint-disable-next-line @typescript-eslint/no-this-alias + const parent = this; + let child; + + // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call the parent constructor. + if (protoProps && has(protoProps, 'constructor')) { + child = protoProps.constructor; + } else { + child = function () { + return parent.apply(this, arguments); + }; + } + + // Add static properties to the constructor function, if supplied. + extend(child, parent, staticProps); + + // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function and add the prototype properties. + child.prototype = create(parent.prototype, protoProps); + child.prototype.constructor = child; + + // Set a convenience property in case the parent's prototype is needed + // later. + child.__super__ = parent.prototype; + + return child; } +export function getResolveablePromise() { + /** + * @typedef {Object} PromiseWrapper + * @property {boolean} isResolved + * @property {boolean} isPending + * @property {boolean} isRejected + * @property {Function} resolve + * @property {Function} reject + */ + + /** @type {PromiseWrapper} */ + const wrapper = { + isResolved: false, + isPending: true, + isRejected: false, + resolve: null, + reject: null, + }; + + /** + * @typedef {Promise & PromiseWrapper} ResolveablePromise + */ -export function getResolveablePromise () { - const wrapper = { - isResolved: false, - isPending: true, - isRejected: false - }; - const promise = new Promise((resolve, reject) => { - wrapper.resolve = resolve; - wrapper.reject = reject; + const promise = /** @type {ResolveablePromise} */ ( + new Promise((resolve, reject) => { + wrapper.resolve = resolve; + wrapper.reject = reject; }) - Object.assign(promise, wrapper); - promise.then( - function (v) { - promise.isResolved = true; - promise.isPending = false; - promise.isRejected = false; - return v; - }, - function (e) { - promise.isResolved = false; - promise.isPending = false; - promise.isRejected = true; - throw (e); - } - ); - return promise; + ); + Object.assign(promise, wrapper); + promise.then( + function (v) { + promise.isResolved = true; + promise.isPending = false; + promise.isRejected = false; + return v; + }, + function (e) { + promise.isResolved = false; + promise.isPending = false; + promise.isRejected = true; + throw e; + }, + ); + return promise; } - // Throw an error when a URL is needed, and none is supplied. export function urlError() { - throw new Error('A "url" property or function must be specified'); + throw new Error('A "url" property or function must be specified'); } // Wrap an optional error callback with a fallback error event. export function wrapError(model, options) { - const error = options.error; - options.error = function(resp) { - if (error) error.call(options.context, model, resp, options); - model.trigger('error', model, resp, options); - }; + const error = options.error; + options.error = function (resp) { + if (error) error.call(options.context, model, resp, options); + model.trigger('error', model, resp, options); + }; } // Map from CRUD to HTTP for our default `sync` implementation. const methodMap = { - create: 'POST', - update: 'PUT', - patch: 'PATCH', - delete: 'DELETE', - read: 'GET' + create: 'POST', + update: 'PUT', + patch: 'PATCH', + delete: 'DELETE', + read: 'GET', }; /** - * @param {import('./model.js').Model} model + * @typedef {import('./model.js').Model} Model + * @typedef {import('./collection.js').Collection} Collection + */ + + +/** + * @param {Model | Collection} model */ export function getSyncMethod(model) { - const store = result(model, 'browserStorage') || result(model.collection, 'browserStorage'); - return store ? store.sync() : sync; + const store = result(model, 'browserStorage') || result(/** @type {Model} */(model).collection, 'browserStorage'); + return store ? store.sync() : sync; } /** diff --git a/src/history.js b/src/history.js deleted file mode 100644 index 4dbf9e69..00000000 --- a/src/history.js +++ /dev/null @@ -1,301 +0,0 @@ -// Backbone.js 1.4.0 -// (c) 2010-2019 Jeremy Ashkenas and DocumentCloud -// Backbone may be freely distributed under the MIT license. - -import extend from 'lodash-es/extend.js'; -import some from 'lodash-es/some.js'; -import { Events } from './events.js'; -import { inherits } from './helpers.js'; - -// History -// ------- - -// Handles cross-browser history management, based on either -// [pushState](http://diveintohtml5.info/history.html) and real URLs, or -// [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) -// and URL fragments. If the browser supports neither (old IE, natch), -// falls back to polling. -const History = function() { - this.handlers = []; - this.checkUrl = this.checkUrl.bind(this); - - // Ensure that `History` can be used outside of the browser. - if (typeof window !== 'undefined') { - this.location = window.location; - this.history = window.history; - } -}; - -History.extend = inherits; - -// Cached regex for stripping a leading hash/slash and trailing space. -const routeStripper = /^[#\/]|\s+$/g; -// Cached regex for stripping leading and trailing slashes. -const rootStripper = /^\/+|\/+$/g; -// Cached regex for stripping urls of hash. -const pathStripper = /#.*$/; - -// Has the history handling already been started? -History.started = false; - -// Set up all inheritable **History** properties and methods. -Object.assign(History.prototype, Events, { - - // The default interval to poll for hash changes, if necessary, is - // twenty times a second. - interval: 50, - - // Are we at the app root? - atRoot: function() { - const path = this.location.pathname.replace(/[^\/]$/, '$&/'); - return path === this.root && !this.getSearch(); - }, - - // Does the pathname match the root? - matchRoot: function() { - const path = this.decodeFragment(this.location.pathname); - const rootPath = path.slice(0, this.root.length - 1) + '/'; - return rootPath === this.root; - }, - - // Unicode characters in `location.pathname` are percent encoded so they're - // decoded for comparison. `%25` should not be decoded since it may be part - // of an encoded parameter. - decodeFragment: function(fragment) { - return decodeURI(fragment.replace(/%25/g, '%2525')); - }, - - // In IE6, the hash fragment and search params are incorrect if the - // fragment contains `?`. - getSearch: function() { - const match = this.location.href.replace(/#.*/, '').match(/\?.+/); - return match ? match[0] : ''; - }, - - // Gets the true hash value. Cannot use location.hash directly due to bug - // in Firefox where location.hash will always be decoded. - getHash: function(window) { - const match = (window || this).location.href.match(/#(.*)$/); - return match ? match[1] : ''; - }, - - // Get the pathname and search params, without the root. - getPath: function() { - const path = this.decodeFragment( - this.location.pathname + this.getSearch() - ).slice(this.root.length - 1); - return path.charAt(0) === '/' ? path.slice(1) : path; - }, - - // Get the cross-browser normalized URL fragment from the path or hash. - getFragment: function(fragment) { - if (fragment == null) { - if (this._usePushState || !this._wantsHashChange) { - fragment = this.getPath(); - } else { - fragment = this.getHash(); - } - } - return fragment.replace(routeStripper, ''); - }, - - // Start the hash change handling, returning `true` if the current URL matches - // an existing route, and `false` otherwise. - start: function(options) { - if (History.started) throw new Error('history has already been started'); - History.started = true; - - // Figure out the initial configuration. Do we need an iframe? - // Is pushState desired ... is it available? - this.options = extend({root: '/'}, this.options, options); - this.root = this.options.root; - this._wantsHashChange = this.options.hashChange !== false; - this._hasHashChange = 'onhashchange' in window && (document.documentMode === undefined|| document.documentMode > 7); - this._useHashChange = this._wantsHashChange && this._hasHashChange; - this._wantsPushState = !!this.options.pushState; - this._hasPushState = !!(this.history && this.history.pushState); - this._usePushState = this._wantsPushState && this._hasPushState; - this.fragment = this.getFragment(); - - // Normalize root to always include a leading and trailing slash. - this.root = ('/' + this.root + '/').replace(rootStripper, '/'); - - // Transition from hashChange to pushState or vice versa if both are - // requested. - if (this._wantsHashChange && this._wantsPushState) { - - // If we've started off with a route from a `pushState`-enabled - // browser, but we're currently in a browser that doesn't support it... - if (!this._hasPushState && !this.atRoot()) { - const rootPath = this.root.slice(0, -1) || '/'; - this.location.replace(rootPath + '#' + this.getPath()); - // Return immediately as browser will do redirect to new url - return true; - - // Or if we've started out with a hash-based route, but we're currently - // in a browser where it could be `pushState`-based instead... - } else if (this._hasPushState && this.atRoot()) { - this.navigate(this.getHash(), {replace: true}); - } - - } - - // Proxy an iframe to handle location events if the browser doesn't - // support the `hashchange` event, HTML5 history, or the user wants - // `hashChange` but not `pushState`. - if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) { - this.iframe = document.createElement('iframe'); - this.iframe.src = 'javascript:0'; - this.iframe.style.display = 'none'; - this.iframe.tabIndex = -1; - const body = document.body; - // Using `appendChild` will throw on IE < 9 if the document is not ready. - const iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow; - iWindow.document.open(); - iWindow.document.close(); - iWindow.location.hash = '#' + this.fragment; - } - - // Depending on whether we're using pushState or hashes, and whether - // 'onhashchange' is supported, determine how we check the URL state. - if (this._usePushState) { - addEventListener('popstate', this.checkUrl, false); - } else if (this._useHashChange && !this.iframe) { - addEventListener('hashchange', this.checkUrl, false); - } else if (this._wantsHashChange) { - this._checkUrlInterval = setInterval(this.checkUrl, this.interval); - } - - if (!this.options.silent) return this.loadUrl(); - }, - - // Disable history, perhaps temporarily. Not useful in a real app, - // but possibly useful for unit testing Routers. - stop: function() { - // Remove window listeners. - if (this._usePushState) { - removeEventListener('popstate', this.checkUrl, false); - } else if (this._useHashChange && !this.iframe) { - removeEventListener('hashchange', this.checkUrl, false); - } - - // Clean up the iframe if necessary. - if (this.iframe) { - document.body.removeChild(this.iframe); - this.iframe = null; - } - - // Some environments will throw when clearing an undefined interval. - if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); - History.started = false; - }, - - // Add a route to be tested when the fragment changes. Routes added later - // may override previous routes. - route: function(route, callback) { - this.handlers.unshift({route: route, callback: callback}); - }, - - // Checks the current URL to see if it has changed, and if it has, - // calls `loadUrl`, normalizing across the hidden iframe. - checkUrl: function(e) { - let current = this.getFragment(); - - // If the user pressed the back button, the iframe's hash will have - // changed and we should use that for comparison. - if (current === this.fragment && this.iframe) { - current = this.getHash(this.iframe.contentWindow); - } - - if (current === this.fragment) return false; - if (this.iframe) this.navigate(current); - this.loadUrl(); - }, - - // Attempt to load the current URL fragment. If a route succeeds with a - // match, returns `true`. If no defined routes matches the fragment, - // returns `false`. - loadUrl: function(fragment) { - // If the root doesn't match, no routes can match either. - if (!this.matchRoot()) return false; - fragment = this.fragment = this.getFragment(fragment); - return some(this.handlers, function(handler) { - if (handler.route.test(fragment)) { - handler.callback(fragment); - return true; - } - }); - }, - - // Save a fragment into the hash history, or replace the URL state if the - // 'replace' option is passed. You are responsible for properly URL-encoding - // the fragment in advance. - // - // The options object can contain `trigger: true` if you wish to have the - // route callback be fired (not usually desirable), or `replace: true`, if - // you wish to modify the current URL without adding an entry to the history. - navigate: function(fragment, options) { - if (!History.started) return false; - if (!options || options === true) options = {trigger: !!options}; - - // Normalize the fragment. - fragment = this.getFragment(fragment || ''); - - // Don't include a trailing slash on the root. - let rootPath = this.root; - if (fragment === '' || fragment.charAt(0) === '?') { - rootPath = rootPath.slice(0, -1) || '/'; - } - const url = rootPath + fragment; - - // Strip the fragment of the query and hash for matching. - fragment = fragment.replace(pathStripper, ''); - - // Decode for matching. - const decodedFragment = this.decodeFragment(fragment); - - if (this.fragment === decodedFragment) return; - this.fragment = decodedFragment; - - // If pushState is available, we use it to set the fragment as a real URL. - if (this._usePushState) { - this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); - - // If hash changes haven't been explicitly disabled, update the hash - // fragment to store history. - } else if (this._wantsHashChange) { - this._updateHash(this.location, fragment, options.replace); - if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) { - const iWindow = this.iframe.contentWindow; - - // Opening and closing the iframe tricks IE7 and earlier to push a - // history entry on hash-tag change. When replace is true, we don't - // want this. - if (!options.replace) { - iWindow.document.open(); - iWindow.document.close(); - } - this._updateHash(iWindow.location, fragment, options.replace); - } - // If you've told us that you explicitly don't want fallback hashchange- - // based history, then `navigate` becomes a page refresh. - } else { - return this.location.assign(url); - } - if (options.trigger) return this.loadUrl(fragment); - }, - - // Update the hash location, either replacing the current entry, or adding - // a new one to the browser history. - _updateHash: function(location, fragment, replace) { - if (replace) { - const href = location.href.replace(/(javascript:|#).*$/, ''); - location.replace(href + '#' + fragment); - } else { - // Some browsers require that `hash` contains a leading #. - location.hash = '#' + fragment; - } - } -}); - -export default History; diff --git a/src/index.js b/src/index.js index 304471c0..c9b6b687 100644 --- a/src/index.js +++ b/src/index.js @@ -1,19 +1,15 @@ /* global global */ -import { sync } from './helpers.js'; +import ElementView from './element.js'; import { Collection } from './collection.js'; import { Events } from './events.js'; -import History from './history.js'; import { Model } from './model.js'; -import { Router } from './router.js'; -import { View } from './view.js'; +import { sync } from './helpers.js'; const skeletor = { Collection, + ElementView, Events, - History, Model, - Router, - View, sync, }; @@ -21,8 +17,9 @@ Object.assign(skeletor, Events); // Establish the root object, `window` (`self`) in the browser, or `global` on the server. // We use `self` instead of `window` for `WebWorker` support. -const root = typeof self == 'object' && self.self === self && self || - typeof global == 'object' && global.global === global && global; +const root = + (typeof self == 'object' && self.self === self && self) || + (typeof global == 'object' && global.global === global && global); // Current version of the library. Keep in sync with `package.json`. skeletor.VERSION = '0.0.1'; @@ -33,7 +30,7 @@ const previousSkeletor = root.Skeletor; // Runs Skeletor.js in *noConflict* mode, returning the `Skeletor` variable // to its previous owner. Returns a reference to this Skeletor object. -skeletor.noConflict = function() { +skeletor.noConflict = function () { root.Skeletor = previousSkeletor; return this; }; diff --git a/src/listening.js b/src/listening.js new file mode 100644 index 00000000..2021a57a --- /dev/null +++ b/src/listening.js @@ -0,0 +1,56 @@ +import { eventsApi, offApi } from './utils/events.js'; + +/** + * A listening class that tracks and cleans up memory bindings + * when all callbacks have been offed. + */ +class Listening { + + /** @typedef {import('./eventemitter.js').default} EventEmitter */ + + /** + * @param {EventEmitter} listener + * @param {any} obj + */ + constructor(listener, obj) { + this.id = listener._listenId; + this.listener = listener; + this.obj = obj; + this.interop = true; + this.count = 0; + this._events = undefined; + } + + /** + * Stop's listening to a callback (or several). + * Uses an optimized counter if the listenee uses Backbone.Events. + * Otherwise, falls back to manual tracking to support events + * library interop. + * @param {string} name + * @param {Function} callback + */ + stop(name, callback) { + let cleanup; + if (this.interop) { + this._events = eventsApi(offApi, this._events, name, callback, { + context: undefined, + listeners: undefined, + }); + cleanup = !this._events; + } else { + this.count--; + cleanup = this.count === 0; + } + if (cleanup) this.cleanup(); + } + + /** + * Cleans up memory bindings between the listener and the listenee. + */ + cleanup() { + delete this.listener._listeningTo[this.obj._listenId]; + if (!this.interop) delete this.obj._listeners[this.id]; + } +} + +export default Listening; diff --git a/src/model.js b/src/model.js index a3ff4418..acea6ac0 100644 --- a/src/model.js +++ b/src/model.js @@ -1,161 +1,195 @@ -// Backbone.js 1.4.0 -// (c) 2010-2019 Jeremy Ashkenas and DocumentCloud -// Backbone may be freely distributed under the MIT license. - -// Model -// ----- -// **Models** are the basic data object in the framework -- -// frequently representing a row in a table in a database on your server. -// A discrete chunk of data and a bunch of useful, related methods for -// performing computations and transformations on that data. - -// Create a new model with the specified attributes. A client id (`cid`) -// is automatically generated and assigned for you. - -import { - getResolveablePromise, - getSyncMethod, - inherits, - urlError, - wrapError -} from './helpers.js'; -import { Events } from './events.js'; -import clone from "lodash-es/clone.js"; -import defaults from "lodash-es/defaults.js"; -import defer from "lodash-es/defer.js"; -import escape from "lodash-es/escape.js"; -import extend from "lodash-es/extend.js"; -import has from "lodash-es/has.js"; -import invert from "lodash-es/invert.js"; -import isEmpty from "lodash-es/isEmpty.js"; -import isEqual from "lodash-es/isEqual.js"; -import iteratee from "lodash-es/iteratee.js"; -import omit from "lodash-es/omit.js"; -import pick from "lodash-es/pick.js"; -import result from "lodash-es/result.js"; -import uniqueId from "lodash-es/uniqueId.js"; - -export const Model = function(attributes, options) { - let attrs = attributes || {}; - options || (options = {}); - this.preinitialize.apply(this, arguments); - this.cid = uniqueId(this.cidPrefix); - this.attributes = {}; - if (options.collection) this.collection = options.collection; - if (options.parse) attrs = this.parse(attrs, options) || {}; - const default_attrs = result(this, 'defaults'); - attrs = defaults(extend({}, default_attrs, attrs), default_attrs); - this.set(attrs, options); - this.changed = {}; - this.initialize.apply(this, arguments); -}; - -Model.extend = inherits; - -// Attach all inheritable methods to the Model prototype. -Object.assign(Model.prototype, Events, { - - // A hash of attributes whose current and previous value differ. - changed: null, - - // The value returned during the last failed validation. - validationError: null, - - // The default name for the JSON `id` attribute is `"id"`. MongoDB and - // CouchDB users may want to set this to `"_id"`. - idAttribute: 'id', - - // The prefix is used to create the client id which is used to identify models locally. - // You may want to override this if you're experiencing name clashes with model ids. - cidPrefix: 'c', - - // preinitialize is an empty function by default. You can override it with a function - // or object. preinitialize will run before any instantiation logic is run in the Model. - preinitialize: function(){}, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // Return a copy of the model's `attributes` object. - toJSON: function(options) { +import { getResolveablePromise, getSyncMethod, urlError, wrapError } from './helpers.js'; +import clone from 'lodash-es/clone.js'; +import defaults from 'lodash-es/defaults.js'; +import defer from 'lodash-es/defer.js'; +import has from 'lodash-es/has.js'; +import invert from 'lodash-es/invert.js'; +import isEmpty from 'lodash-es/isEmpty.js'; +import isEqual from 'lodash-es/isEqual.js'; +import iteratee from 'lodash-es/iteratee.js'; +import omit from 'lodash-es/omit.js'; +import pick from 'lodash-es/pick.js'; +import result from 'lodash-es/result.js'; +import uniqueId from 'lodash-es/uniqueId.js'; +import EventEmitter from './eventemitter.js'; + +/** + * @typedef {import('./collection.js').Collection} Collection + * @typedef {Record.} Attributes + * + * @typedef {Record.} Options + * @property {boolean} [validate] + * + * @typedef {Record.} ModelOptions + * @property {Collection} [collection] + * @property {boolean} [parse] + * @property {boolean} [unset] + * @property {boolean} [silent] + */ + +/** + * **Models** are the basic data object in the framework -- + * frequently representing a row in a table in a database on your server. + * A discrete chunk of data and a bunch of useful, related methods for + * performing computations and transformations on that data. + */ +class Model extends EventEmitter { + /** + * Create a new model with the specified attributes. A client id (`cid`) + * is automatically generated and assigned for you. + * @param {Attributes} attributes + * @param {ModelOptions} options + */ + constructor(attributes, options) { + super(); + let attrs = attributes || {}; + options || (options = {}); + this.preinitialize.apply(this, arguments); + this.cid = uniqueId(this.cidPrefix); + this.attributes = {}; + + // The value returned during the last failed validation. + this.validationError = null; + + this.validate = this.validate ?? null; + + if (options.collection) this.collection = options.collection; + if (options.parse) attrs = this.parse(attrs, options) || {}; + + const default_attrs = result(this, 'defaults'); + attrs = defaults(Object.assign({}, default_attrs, attrs), default_attrs); + + this.set(attrs, options); + + this.initialize.apply(this, arguments); + + // A hash of attributes whose current and previous value differ. + this.changed = {}; + } + + /** + * The default name for the JSON `id` attribute is `"id"`. MongoDB and + * CouchDB users may want to set this to `"_id"` (by overriding this getter + * in a subclass). + */ + // eslint-disable-next-line class-methods-use-this + get idAttribute() { + return 'id'; + } + + /** + * The prefix is used to create the client id which is used to identify models locally. + * You may want to override this if you're experiencing name clashes with model ids. + */ + // eslint-disable-next-line class-methods-use-this + get cidPrefix() { + return 'c'; + } + + /** + * preinitialize is an empty function by default. You can override it with a function + * or object. preinitialize will run before any instantiation logic is run in the Model. + */ + // eslint-disable-next-line class-methods-use-this + preinitialize() {} + + /** + * Initialize is an empty function by default. Override it with your own + * initialization logic. + */ + // eslint-disable-next-line class-methods-use-this + initialize() {} + + /** + * Return a copy of the model's `attributes` object. + */ + toJSON() { return clone(this.attributes); - }, + } /** * Override this if you need custom syncing semantics for *this* particular model. * @param {'create'|'update'|'patch'|'delete'|'read'} method * @param {Model} model - * @param {Object} options + * @param {Options} options */ // eslint-disable-next-line class-methods-use-this sync(method, model, options) { return getSyncMethod(model)(method, model, options); - }, + } - // Get the value of an attribute. - get: function(attr) { + /** + * Get the value of an attribute. + * @param {string} attr + */ + get(attr) { return this.attributes[attr]; - }, + } - keys: function() { + keys() { return Object.keys(this.attributes); - }, + } - values: function() { + values() { return Object.values(this.attributes); - }, + } - pairs: function() { + pairs() { return this.entries(); - }, + } - entries: function() { + entries() { return Object.entries(this.attributes); - }, + } - invert: function() { + invert() { return invert(this.attributes); - }, + } - pick: function(...args) { + pick(...args) { if (args.length === 1 && Array.isArray(args[0])) { args = args[0]; } return pick(this.attributes, args); - }, + } - omit: function(...args) { + omit(...args) { if (args.length === 1 && Array.isArray(args[0])) { args = args[0]; } return omit(this.attributes, args); - }, + } - isEmpty: function() { + isEmpty() { return isEmpty(this.attributes); - }, - - // Get the HTML-escaped value of an attribute. - escape: function(attr) { - return escape(this.get(attr)); - }, + } - // Returns `true` if the attribute contains a value that is not null - // or undefined. - has: function(attr) { + /** + * Returns `true` if the attribute contains a value that is not null + * or undefined. + * @param {string} attr + */ + has(attr) { return this.get(attr) != null; - }, + } - // Special-cased proxy to lodash's `matches` method. - matches: function(attrs) { + /** + * Special-cased proxy to lodash's `matches` method. + * @param {Attributes} attrs + */ + matches(attrs) { return !!iteratee(attrs, this)(this.attributes); - }, + } - // Set a hash of model attributes on the object, firing `"change"`. This is - // the core primitive operation of a model, updating the data and notifying - // anyone who needs to know about the change in state. The heart of the beast. - set: function(key, val, options) { + /** + * Set a hash of model attributes on the object, firing `"change"`. This is + * the core primitive operation of a model, updating the data and notifying + * anyone who needs to know about the change in state. The heart of the beast. + * @param {string|Object} key + * @param {string|Object} val + * @param {Options} [options] + */ + set(key, val, options) { if (key == null) return this; // Handle both `"key", value` and `{key: value}` -style arguments. @@ -173,10 +207,10 @@ Object.assign(Model.prototype, Events, { if (!this._validate(attrs, options)) return false; // Extract attributes and options. - const unset = options.unset; - const silent = options.silent; - const changes = []; - const changing = this._changing; + const unset = options.unset; + const silent = options.silent; + const changes = []; + const changing = this._changing; this._changing = true; if (!changing) { @@ -186,7 +220,7 @@ Object.assign(Model.prototype, Events, { const current = this.attributes; const changed = this.changed; - const prev = this._previousAttributes; + const prev = this._previousAttributes; // For each `set` attribute, update or delete the current value. for (const attr in attrs) { @@ -197,7 +231,7 @@ Object.assign(Model.prototype, Events, { } else { delete changed[attr]; } - unset ? delete current[attr] : current[attr] = val; + unset ? delete current[attr] : (current[attr] = val); } // Update the `id`. @@ -216,7 +250,7 @@ Object.assign(Model.prototype, Events, { if (changing) return this; if (!silent) { while (this._pending) { - options = this._pending; + options = /** @type {Options} */ (this._pending); this._pending = false; this.trigger('change', this, options); } @@ -224,36 +258,52 @@ Object.assign(Model.prototype, Events, { this._pending = false; this._changing = false; return this; - }, + } - // Remove an attribute from the model, firing `"change"`. `unset` is a noop - // if the attribute doesn't exist. - unset: function(attr, options) { - return this.set(attr, undefined, extend({}, options, {unset: true})); - }, + /** + * Remove an attribute from the model, firing `"change"`. `unset` is a noop + * if the attribute doesn't exist. + * @param {string} attr + * @param {Options} options + */ + unset(attr, options) { + return this.set(attr, undefined, Object.assign({}, options, { unset: true })); + } - // Clear all attributes on the model, firing `"change"`. - clear: function(options) { + /** + * Clear all attributes on the model, firing `"change"`. + * @param {Options} options + */ + clear(options) { const attrs = {}; for (const key in this.attributes) attrs[key] = undefined; - return this.set(attrs, extend({}, options, {unset: true})); - }, + return this.set(attrs, Object.assign({}, options, { unset: true })); + } - // Determine if the model has changed since the last `"change"` event. - // If you specify an attribute name, determine if that attribute has changed. - hasChanged: function(attr) { + /** + * Determine if the model has changed since the last `"change"` event. + * If you specify an attribute name, determine if that attribute has changed. + * @param {string} [attr] + */ + hasChanged(attr) { if (attr == null) return !isEmpty(this.changed); return has(this.changed, attr); - }, - - // Return an object containing all the attributes that have changed, or - // false if there are no changed attributes. Useful for determining what - // parts of a view need to be updated and/or what attributes need to be - // persisted to the server. Unset attributes will be set to undefined. - // You can also pass an attributes object to diff against the model, - // determining if there *would be* a change. - changedAttributes: function(diff) { - if (!diff) return this.hasChanged() ? clone(this.changed) : false; + } + + /** + * Return an object containing all the attributes that have changed, or + * false if there are no changed attributes. Useful for determining what + * parts of a view need to be updated and/or what attributes need to be + * persisted to the server. Unset attributes will be set to undefined. + * You can also pass an attributes object to diff against the model, + * determining if there *would be* a change. + * @param {Object} diff + */ + changedAttributes(diff) { + if (!diff) { + return this.hasChanged() ? clone(this.changed) : false; + } + const old = this._changing ? this._previousAttributes : this.attributes; const changed = {}; let hasChanged; @@ -264,51 +314,66 @@ Object.assign(Model.prototype, Events, { hasChanged = true; } return hasChanged ? changed : false; - }, + } - // Get the previous value of an attribute, recorded at the time the last - // `"change"` event was fired. - previous: function(attr) { + /** + * Get the previous value of an attribute, recorded at the time the last + * `"change"` event was fired. + * @param {string} [attr] + */ + previous(attr) { if (attr == null || !this._previousAttributes) return null; return this._previousAttributes[attr]; - }, + } - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes: function() { + /** + * Get all of the attributes of the model at the time of the previous + * `"change"` event. + */ + previousAttributes() { return clone(this._previousAttributes); - }, + } + + /** + * Fetch the model from the server, merging the response with the model's + * local attributes. Any changed attributes will trigger a "change" event. + * @param {Options} options + */ + fetch(options) { + options = Object.assign({ parse: true }, options); - // Fetch the model from the server, merging the response with the model's - // local attributes. Any changed attributes will trigger a "change" event. - fetch: function(options) { - options = extend({parse: true}, options); - const model = this; const success = options.success; - options.success = function(resp) { - const serverAttrs = options.parse ? model.parse(resp, options) : resp; - if (!model.set(serverAttrs, options)) return false; - if (success) success.call(options.context, model, resp, options); - model.trigger('sync', model, resp, options); + + options.success = (resp) => { + const serverAttrs = options.parse ? this.parse(resp, options) : resp; + if (!this.set(serverAttrs, options)) return false; + if (success) success.call(options.context, this, resp, options); + this.trigger('sync', this, resp, options); }; + wrapError(this, options); return this.sync('read', this, options); - }, + } - // Set a hash of model attributes, and sync the model to the server. - // If the server returns an attributes hash that differs, the model's - // state will be `set` again. - save: function(key, val, options) { + /** + * Set a hash of model attributes, and sync the model to the server. + * If the server returns an attributes hash that differs, the model's + * state will be `set` again. + * @param {string} key + * @param {string|Options} val + * @param {Options} [options] + */ + save(key, val, options) { // Handle both `"key", value` and `{key: value}` -style arguments. let attrs; if (key == null || typeof key === 'object') { attrs = key; - options = val; + options = /** @type {Options} */ (val); } else { (attrs = {})[key] = val; } - options = extend({validate: true, parse: true}, options); + options = Object.assign({ validate: true, parse: true }, options); const wait = options.wait; const return_promise = options.promise; const promise = return_promise && getResolveablePromise(); @@ -318,35 +383,36 @@ Object.assign(Model.prototype, Events, { // the model will be valid when the attributes, if any, are set. if (attrs && !wait) { if (!this.set(attrs, options)) return false; - } else if (!this._validate(attrs, options)) { + } else if (!this._validate(/** @type {Object} */ (attrs), options)) { return false; } // After a successful server-side save, the client is (optionally) // updated with the server-side state. - const model = this; const success = options.success; const error = options.error; const attributes = this.attributes; - options.success = function(resp) { + + options.success = (resp) => { // Ensure attributes are restored during synchronous saves. - model.attributes = attributes; - let serverAttrs = options.parse ? model.parse(resp, options) : resp; - if (wait) serverAttrs = extend({}, attrs, serverAttrs); - if (serverAttrs && !model.set(serverAttrs, options)) return false; - if (success) success.call(options.context, model, resp, options); - model.trigger('sync', model, resp, options); + this.attributes = attributes; + let serverAttrs = options.parse ? this.parse(resp, options) : resp; + if (wait) serverAttrs = Object.assign({}, attrs, serverAttrs); + if (serverAttrs && !this.set(serverAttrs, options)) return false; + if (success) success.call(options.context, this, resp, options); + this.trigger('sync', this, resp, options); return_promise && promise.resolve(); }; - options.error = function(model, e, options) { + + options.error = (model, e, options) => { error && error.call(options.context, model, e, options); return_promise && promise.reject(e); - } + }; wrapError(this, options); // Set temporary attributes if `{wait: true}` to properly find new ids. - if (attrs && wait) this.attributes = extend({}, attributes, attrs); + if (attrs && wait) this.attributes = Object.assign({}, attributes, attrs); const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update'; if (method === 'patch' && !options.attrs) options.attrs = attrs; @@ -360,27 +426,28 @@ Object.assign(Model.prototype, Events, { } else { return xhr; } + } - }, - - // Destroy this model on the server if it was already persisted. - // Optimistically removes the model from its collection, if it has one. - // If `wait: true` is passed, waits for the server to respond before removal. - destroy: function(options) { + /** + * Destroy this model on the server if it was already persisted. + * Optimistically removes the model from its collection, if it has one. + * If `wait: true` is passed, waits for the server to respond before removal. + * @param {Options} [options] + */ + destroy(options) { options = options ? clone(options) : {}; - const model = this; const success = options.success; const wait = options.wait; - const destroy = function() { - model.stopListening(); - model.trigger('destroy', model, model.collection, options); + const destroy = () => { + this.stopListening(); + this.trigger('destroy', this, this.collection, options); }; - options.success = function(resp) { + options.success = (resp) => { if (wait) destroy(); - if (success) success.call(options.context, model, resp, options); - if (!model.isNew()) model.trigger('sync', model, resp, options); + if (success) success.call(options.context, this, resp, options); + if (!this.isNew()) this.trigger('sync', this, resp, options); }; let xhr = false; @@ -392,50 +459,59 @@ Object.assign(Model.prototype, Events, { } if (!wait) destroy(); return xhr; - }, - - // Default URL for the model's representation on the server -- if you're - // using Backbone's restful methods, override this to change the endpoint - // that will be called. - url: function() { - const base = - result(this, 'urlRoot') || - result(this.collection, 'url') || - urlError(); + } + + /** + * Default URL for the model's representation on the server -- if you're + * using Backbone's restful methods, override this to change the endpoint + * that will be called. + */ + url() { + const base = result(this, 'urlRoot') || result(this.collection, 'url') || urlError(); if (this.isNew()) return base; const id = this.get(this.idAttribute); return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id); - }, + } - // **parse** converts a response into the hash of attributes to be `set` on - // the model. The default implementation is just to pass the response along. - parse: function(resp, options) { + /** + * **parse** converts a response into the hash of attributes to be `set` on + * the model. The default implementation is just to pass the response along. + * @param {Options} resp + * @param {Options} [options] + */ + parse(resp, options) { return resp; - }, - - // Create a new model with identical attributes to this one. - clone: function() { - return new this.constructor(this.attributes); - }, + } - // A model is new if it has never been saved to the server, and lacks an id. - isNew: function() { + /** + * A model is new if it has never been saved to the server, and lacks an id. + */ + isNew() { return !this.has(this.idAttribute); - }, + } - // Check if the model is currently in a valid state. - isValid: function(options) { - return this._validate({}, extend({}, options, {validate: true})); - }, + /** + * Check if the model is currently in a valid state. + * @param {Options} [options] + */ + isValid(options) { + return this._validate({}, Object.assign({}, options, { validate: true })); + } - // Run validation against the next complete set of model attributes, - // returning `true` if all is well. Otherwise, fire an `"invalid"` event. - _validate: function(attrs, options) { + /** + * Run validation against the next complete set of model attributes, + * returning `true` if all is well. Otherwise, fire an `"invalid"` event. + * @param {Attributes} attrs + * @param {Options} [options] + */ + _validate(attrs, options) { if (!options.validate || !this.validate) return true; - attrs = extend({}, this.attributes, attrs); - const error = this.validationError = this.validate(attrs, options) || null; + attrs = Object.assign({}, this.attributes, attrs); + const error = (this.validationError = this.validate(attrs, options) || null); if (!error) return true; - this.trigger('invalid', this, error, extend(options, {validationError: error})); + this.trigger('invalid', this, error, Object.assign(options, { validationError: error })); return false; } -}); +} + +export { Model }; diff --git a/src/router.js b/src/router.js deleted file mode 100644 index 9e997e35..00000000 --- a/src/router.js +++ /dev/null @@ -1,119 +0,0 @@ -// Backbone.js 1.4.0 -// (c) 2010-2019 Jeremy Ashkenas and DocumentCloud -// Backbone may be freely distributed under the MIT license. - -// Router -// ------ - -import History from './history.js'; -import extend from 'lodash-es/extend.js'; -import isFunction from 'lodash-es/isFunction.js'; -import isRegExp from 'lodash-es/isRegExp.js'; -import keys from 'lodash-es/keys.js'; -import result from 'lodash-es/result.js'; -import { Events } from './events.js'; -import { inherits } from './helpers.js'; - -// Routers map faux-URLs to actions, and fire events when routes are -// matched. Creating a new one sets its `routes` hash, if not set statically. -export const Router = function(options={}) { - this.history = options.history || new History(); - this.preinitialize.apply(this, arguments); - if (options.routes) this.routes = options.routes; - this._bindRoutes(); - this.initialize.apply(this, arguments); -}; - -Router.extend = inherits; - -// Cached regular expressions for matching named param parts and splatted -// parts of route strings. -const optionalParam = /\((.*?)\)/g; -const namedParam = /(\(\?)?:\w+/g; -const splatParam = /\*\w+/g; -const escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; - -// Set up all inheritable **Router** properties and methods. -Object.assign(Router.prototype, Events, { - - // preinitialize is an empty function by default. You can override it with a function - // or object. preinitialize will run before any instantiation logic is run in the Router. - preinitialize: function(){}, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // Manually bind a single named route to a callback. For example: - // - // this.route('search/:query/p:num', 'search', function(query, num) { - // ... - // }); - // - route: function(route, name, callback) { - if (!isRegExp(route)) route = this._routeToRegExp(route); - if (isFunction(name)) { - callback = name; - name = ''; - } - if (!callback) callback = this[name]; - this.history.route(route, (fragment) => { - const args = this._extractParameters(route, fragment); - if (this.execute(callback, args, name) !== false) { - this.trigger.apply(this, ['route:' + name].concat(args)); - this.trigger('route', name, args); - this.history.trigger('route', this, name, args); - } - }); - return this; - }, - - // Execute a route handler with the provided parameters. This is an - // excellent place to do pre-route setup or post-route cleanup. - execute: function(callback, args, name) { - if (callback) callback.apply(this, args); - }, - - // Simple proxy to `history` to save a fragment into the history. - navigate: function(fragment, options) { - this.history.navigate(fragment, options); - return this; - }, - - // Bind all defined routes to `history`. We have to reverse the - // order of the routes here to support behavior where the most general - // routes can be defined at the bottom of the route map. - _bindRoutes: function() { - if (!this.routes) return; - this.routes = result(this, 'routes'); - let route; - const routes = keys(this.routes); - while ((route = routes.pop()) != null) { - this.route(route, this.routes[route]); - } - }, - - // Convert a route string into a regular expression, suitable for matching - // against the current location hash. - _routeToRegExp: function(route) { - route = route.replace(escapeRegExp, '\\$&') - .replace(optionalParam, '(?:$1)?') - .replace(namedParam, function(match, optional) { - return optional ? match : '([^/?]+)'; - }) - .replace(splatParam, '([^?]*?)'); - return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); - }, - - // Given a route, and a URL fragment that it matches, return the array of - // extracted decoded parameters. Empty or unmatched parameters will be - // treated as `null` to normalize cross-browser behavior. - _extractParameters: function(route, fragment) { - const params = route.exec(fragment).slice(1); - return params.map(function(param, i) { - // Don't decode the search params. - if (i === params.length - 1) return param || null; - return param ? decodeURIComponent(param) : null; - }); - } -}); diff --git a/src/storage.js b/src/storage.js index aa26b9fc..26eb96e4 100644 --- a/src/storage.js +++ b/src/storage.js @@ -4,253 +4,245 @@ import * as memoryDriver from 'localforage-driver-memory'; import cloneDeep from 'lodash-es/cloneDeep.js'; import isString from 'lodash-es/isString.js'; -import localForage from "localforage/src/localforage"; +import localForage from 'localforage/src/localforage'; import mergebounce from 'mergebounce'; -import sessionStorageWrapper from "./drivers/sessionStorage.js"; +import sessionStorageWrapper from './drivers/sessionStorage.js'; import { extendPrototype as extendPrototypeWithSetItems } from 'localforage-setitems'; import { extendPrototype as extendPrototypeWithGetItems } from '@converse/localforage-getitems'; import { guid } from './helpers.js'; -const IN_MEMORY = memoryDriver._driver +const IN_MEMORY = memoryDriver._driver; localForage.defineDriver(memoryDriver); extendPrototypeWithSetItems(localForage); extendPrototypeWithGetItems(localForage); class Storage { - - constructor (id, type, batchedWrites=false) { - if (type === 'local' && !window.localStorage ) { - throw new Error("Skeletor.storage: Environment does not support localStorage."); - } else if (type === 'session' && !window.sessionStorage ) { - throw new Error("Skeletor.storage: Environment does not support sessionStorage."); - } - if (isString(type)) { - this.storeInitialized = this.initStore(type, batchedWrites); - } else { - this.store = type; - if (batchedWrites) { - this.store.debouncedSetItems = mergebounce( - items => this.store.setItems(items), - 50, - {'promise': true} - ); - } - this.storeInitialized = Promise.resolve(); - } - this.name = id; - } - - async initStore (type, batchedWrites) { - if (type === 'session') { - await localForage.setDriver(sessionStorageWrapper._driver); - } else if (type === 'local') { - await localForage.config({'driver': localForage.LOCALSTORAGE}); - } else if (type === 'in_memory') { - await localForage.config({'driver': IN_MEMORY}); - } else if (type !== 'indexed') { - throw new Error("Skeletor.storage: No storage type was specified"); - } - this.store = localForage; - if (batchedWrites) { - this.store.debouncedSetItems = mergebounce( - items => this.store.setItems(items), - 50, - {'promise': true} - ); - } - } - - flush () { - return this.store.debouncedSetItems?.flush(); - } - - async clear () { - await this.store.removeItem(this.name).catch(e => console.error(e)); - const re = new RegExp(`^${this.name}-`); - const keys = await this.store.keys(); - const removed_keys = keys.filter(k => re.test(k)); - await Promise.all(removed_keys.map(k => this.store.removeItem(k).catch(e => console.error(e)))); - } - - sync () { - const that = this; - - async function localSync (method, model, options) { - let resp, errorMessage, promise, new_attributes; - - // We get the collection (and if necessary the model attribute. - // Waiting for storeInitialized will cause another iteration of - // the event loop, after which the collection reference will - // be removed from the model. - const collection = model.collection; - if (['patch', 'update'].includes(method)) { - new_attributes = cloneDeep(model.attributes); + constructor(id, type, batchedWrites = false) { + if (type === 'local' && !window.localStorage) { + throw new Error('Skeletor.storage: Environment does not support localStorage.'); + } else if (type === 'session' && !window.sessionStorage) { + throw new Error('Skeletor.storage: Environment does not support sessionStorage.'); + } + if (isString(type)) { + this.storeInitialized = this.initStore(type, batchedWrites); + } else { + this.store = type; + if (batchedWrites) { + this.store.debouncedSetItems = mergebounce((items) => this.store.setItems(items), 50, { 'promise': true }); + } + this.storeInitialized = Promise.resolve(); + } + this.name = id; + } + + /** + * @param {'local'|'session'|'indexed'|'in_memory'} type + * @param {boolean} batchedWrites + */ + async initStore(type, batchedWrites) { + if (type === 'session') { + await localForage.setDriver(sessionStorageWrapper._driver); + } else if (type === 'local') { + await localForage.config({ 'driver': localForage.LOCALSTORAGE }); + } else if (type === 'in_memory') { + await localForage.config({ 'driver': IN_MEMORY }); + } else if (type !== 'indexed') { + throw new Error('Skeletor.storage: No storage type was specified'); + } + this.store = localForage; + if (batchedWrites) { + this.store.debouncedSetItems = mergebounce((items) => this.store.setItems(items), 50, { 'promise': true }); + } + } + + flush() { + return this.store.debouncedSetItems?.flush(); + } + + async clear() { + await this.store.removeItem(this.name).catch((e) => console.error(e)); + const re = new RegExp(`^${this.name}-`); + const keys = await this.store.keys(); + const removed_keys = keys.filter((k) => re.test(k)); + await Promise.all(removed_keys.map((k) => this.store.removeItem(k).catch((e) => console.error(e)))); + } + + sync() { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this; + + async function localSync(method, model, options) { + let resp, errorMessage, promise, new_attributes; + + // We get the collection (and if necessary the model attribute. + // Waiting for storeInitialized will cause another iteration of + // the event loop, after which the collection reference will + // be removed from the model. + const collection = model.collection; + if (['patch', 'update'].includes(method)) { + new_attributes = cloneDeep(model.attributes); + } + await that.storeInitialized; + try { + const original_attributes = model.attributes; + switch (method) { + case 'read': + if (model.id !== undefined) { + resp = await that.find(model); + } else { + resp = await that.findAll(); } - await that.storeInitialized; - try { - const original_attributes = model.attributes; - switch (method) { - case "read": - if (model.id !== undefined) { - resp = await that.find(model); - } else { - resp = await that.findAll(); - } - break; - case "create": - resp = await that.create(model, options); - break; - case 'patch': - case "update": - if (options.wait) { - // When `wait` is set to true, Skeletor waits until - // confirmation of storage before setting the values on - // the model. - // However, the new attributes needs to be sent, so it - // sets them manually on the model and then removes - // them after calling `sync`. - // Because our `sync` method is asynchronous and we - // wait for `storeInitialized`, the attributes are - // already restored once we get here, so we need to do - // the attributes dance again. - model.attributes = new_attributes; - } - promise = that.update(model, options); - if (options.wait) { - model.attributes = original_attributes; - } - resp = await promise; - break; - case "delete": - resp = await that.destroy(model, collection); - break; - } - } catch (error) { - if (error.code === 22 && that.getStorageSize() === 0) { - errorMessage = "Private browsing is unsupported"; - } else { - errorMessage = error.message; - } + break; + case 'create': + resp = await that.create(model, options); + break; + case 'patch': + case 'update': + if (options.wait) { + // When `wait` is set to true, Skeletor waits until + // confirmation of storage before setting the values on + // the model. + // However, the new attributes needs to be sent, so it + // sets them manually on the model and then removes + // them after calling `sync`. + // Because our `sync` method is asynchronous and we + // wait for `storeInitialized`, the attributes are + // already restored once we get here, so we need to do + // the attributes dance again. + model.attributes = new_attributes; } - - if (resp) { - if (options && options.success) { - // When storing, we don't pass back the response (which is - // the set attributes returned from localforage because - // Skeletor sets them again on the model and due to the async - // nature of localforage it can cause stale attributes to be - // set on a model after it's been updated in the meantime. - const data = (method === "read") ? resp : null; - options.success(data, options); - } - } else { - errorMessage = errorMessage ? errorMessage : "Record Not Found"; - if (options && options.error) { - options.error(errorMessage); - } + promise = that.update(model, options); + if (options.wait) { + model.attributes = original_attributes; } + resp = await promise; + break; + case 'delete': + resp = await that.destroy(model, collection); + break; } - localSync.__name__ = 'localSync'; - return localSync; - } - - removeCollectionReference (model, collection) { - if (!collection) { - return; - } - const ids = collection - .filter(m => (m.id !== model.id)) - .map(m => this.getItemName(m.id)); - - return this.store.setItem(this.name, ids); - } - - addCollectionReference (model, collection) { - if (!collection) { - return; - } - const ids = collection.map(m => this.getItemName(m.id)); - const new_id = this.getItemName(model.id); - if (!ids.includes(new_id)) { - ids.push(new_id); - } - return this.store.setItem(this.name, ids); - } - - getCollectionReferenceData (model) { - if (!model.collection) { - return {}; - } - const ids = model.collection.map(m => this.getItemName(m.id)); - const new_id = this.getItemName(model.id); - if (!ids.includes(new_id)) { - ids.push(new_id); - } - const result = {}; - result[this.name] = ids; - return result; - } - - async save (model) { - if (this.store.setItems) { - const items = {} - items[this.getItemName(model.id)] = model.toJSON(); - Object.assign(items, this.getCollectionReferenceData(model)); - return (this.store.debouncedSetItems) ? - this.store.debouncedSetItems(items) : - this.store.setItems(items); + } catch (error) { + if (error.code === 22 && that.getStorageSize() === 0) { + errorMessage = 'Private browsing is unsupported'; } else { - const key = this.getItemName(model.id); - const data = await this.store.setItem(key, model.toJSON()); - await this.addCollectionReference(model, model.collection); - return data; + errorMessage = error.message; } - } - - create (model, options) { - /* Add a model, giving it a (hopefully)-unique GUID, if it doesn't already - * have an id of it's own. - */ - if (!model.id) { - model.id = guid(); - model.set(model.idAttribute, model.id, options); + } + + if (resp) { + if (options && options.success) { + // When storing, we don't pass back the response (which is + // the set attributes returned from localforage because + // Skeletor sets them again on the model and due to the async + // nature of localforage it can cause stale attributes to be + // set on a model after it's been updated in the meantime. + const data = method === 'read' ? resp : null; + options.success(data, options); } - return this.save(model); - } - - update (model) { - return this.save(model); - } - - find (model) { - return this.store.getItem(this.getItemName(model.id)); - } - - async findAll () { - /* Return the array of all models currently in storage. - */ - const keys = await this.store.getItem(this.name); - if (keys?.length) { - const items = await this.store.getItems(keys); - return Object.values(items); + } else { + errorMessage = errorMessage ? errorMessage : 'Record Not Found'; + if (options && options.error) { + options.error(errorMessage); } - return []; - } - - async destroy (model, collection) { - await this.flush(); - await this.store.removeItem(this.getItemName(model.id)); - await this.removeCollectionReference(model, collection); - return model; - } - - getStorageSize () { - return this.store.length; - } - - getItemName (id) { - return this.name+"-"+id; - } + } + } + localSync.__name__ = 'localSync'; + return localSync; + } + + removeCollectionReference(model, collection) { + if (!collection) { + return; + } + const ids = collection.filter((m) => m.id !== model.id).map((m) => this.getItemName(m.id)); + + return this.store.setItem(this.name, ids); + } + + addCollectionReference(model, collection) { + if (!collection) { + return; + } + const ids = collection.map((m) => this.getItemName(m.id)); + const new_id = this.getItemName(model.id); + if (!ids.includes(new_id)) { + ids.push(new_id); + } + return this.store.setItem(this.name, ids); + } + + getCollectionReferenceData(model) { + if (!model.collection) { + return {}; + } + const ids = model.collection.map((m) => this.getItemName(m.id)); + const new_id = this.getItemName(model.id); + if (!ids.includes(new_id)) { + ids.push(new_id); + } + const result = {}; + result[this.name] = ids; + return result; + } + + async save(model) { + if (this.store.setItems) { + const items = {}; + items[this.getItemName(model.id)] = model.toJSON(); + Object.assign(items, this.getCollectionReferenceData(model)); + return this.store.debouncedSetItems ? this.store.debouncedSetItems(items) : this.store.setItems(items); + } else { + const key = this.getItemName(model.id); + const data = await this.store.setItem(key, model.toJSON()); + await this.addCollectionReference(model, model.collection); + return data; + } + } + + create(model, options) { + /* Add a model, giving it a (hopefully)-unique GUID, if it doesn't already + * have an id of it's own. + */ + if (!model.id) { + model.id = guid(); + model.set(model.idAttribute, model.id, options); + } + return this.save(model); + } + + update(model) { + return this.save(model); + } + + find(model) { + return this.store.getItem(this.getItemName(model.id)); + } + + async findAll() { + /* Return the array of all models currently in storage. + */ + const keys = await this.store.getItem(this.name); + if (keys?.length) { + const items = await this.store.getItems(keys); + return Object.values(items); + } + return []; + } + + async destroy(model, collection) { + await this.flush(); + await this.store.removeItem(this.getItemName(model.id)); + await this.removeCollectionReference(model, collection); + return model; + } + + getStorageSize() { + return this.store.length; + } + + getItemName(id) { + return this.name + '-' + id; + } } Storage.sessionStorageInitialized = localForage.defineDriver(sessionStorageWrapper); diff --git a/src/utils/events.js b/src/utils/events.js new file mode 100644 index 00000000..9b67b84a --- /dev/null +++ b/src/utils/events.js @@ -0,0 +1,174 @@ +import once from 'lodash-es/once.js'; +import keys from 'lodash-es/keys.js'; + +// Regular expression used to split event strings. +const eventSplitter = /\s+/; + +/** + * Iterates over the standard `event, callback` (as well as the fancy multiple + * space-separated events `"change blur", callback` and jQuery-style event + * maps `{event: callback}`). + */ +export function eventsApi(iteratee, events, name, callback, opts) { + let i = 0, + names; + if (name && typeof name === 'object') { + // Handle event maps. + if (callback !== undefined && 'context' in opts && opts.context === undefined) opts.context = callback; + for (names = keys(name); i < names.length; i++) { + events = eventsApi(iteratee, events, names[i], name[names[i]], opts); + } + } else if (name && eventSplitter.test(name)) { + // Handle space-separated event names by delegating them individually. + for (names = name.split(eventSplitter); i < names.length; i++) { + events = iteratee(events, names[i], callback, opts); + } + } else { + // Finally, standard events. + events = iteratee(events, name, callback, opts); + } + return events; +} + +// The reducing API that adds a callback to the `events` object. +export function onApi(events, name, callback, options) { + if (callback) { + const handlers = events[name] || (events[name] = []); + const context = options.context, + ctx = options.ctx, + listening = options.listening; + if (listening) listening.count++; + + handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening }); + } + return events; +} + +/** + * An try-catch guarded #on function, to prevent poisoning the global + * `_listening` variable. + * @param {any} obj + * @param {string} name + * @param {Function} callback + * @param {any} context + */ +export function tryCatchOn(obj, name, callback, context) { + try { + obj.on(name, callback, context); + } catch (e) { + return e; + } +} + +/** + * The reducing API that removes a callback from the `events` object. + */ +export function offApi(events, name, callback, options) { + if (!events) return; + + const context = options.context, + listeners = options.listeners; + let i = 0, + names; + + // Delete all event listeners and "drop" events. + if (!name && !context && !callback) { + for (names = keys(listeners); i < names.length; i++) { + listeners[names[i]].cleanup(); + } + return; + } + + names = name ? [name] : keys(events); + for (; i < names.length; i++) { + name = names[i]; + const handlers = events[name]; + + // Bail out if there are no events stored. + if (!handlers) { + break; + } + + // Find any remaining events. + const remaining = []; + for (let j = 0; j < handlers.length; j++) { + const handler = handlers[j]; + if ( + (callback && callback !== handler.callback && callback !== handler.callback._callback) || + (context && context !== handler.context) + ) { + remaining.push(handler); + } else { + const listening = handler.listening; + if (listening) listening.off(name, callback); + } + } + + // Replace events if there are any remaining. Otherwise, clean up. + if (remaining.length) { + events[name] = remaining; + } else { + delete events[name]; + } + } + + return events; +} + +/** + * Reduces the event callbacks into a map of `{event: onceWrapper}`. + * `offer` unbinds the `onceWrapper` after it has been called. + */ +export function onceMap(map, name, callback, offer) { + if (callback) { + const _once = (map[name] = once(function () { + offer(name, _once); + callback.apply(this, arguments); + })); + _once._callback = callback; + } + return map; +} + +/** Handles triggering the appropriate event callbacks. */ +export function triggerApi(objEvents, name, callback, args) { + if (objEvents) { + const events = objEvents[name]; + let allEvents = objEvents.all; + if (events && allEvents) allEvents = allEvents.slice(); + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, [name].concat(args)); + } + return objEvents; +} + +/** + * A difficult-to-believe, but optimized internal dispatch function for + * triggering events. Tries to keep the usual cases speedy (most internal + * Backbone events have 3 arguments). + */ +function triggerEvents(events, args) { + let ev, + i = -1; + const l = events.length, + a1 = args[0], + a2 = args[1], + a3 = args[2]; + switch (args.length) { + case 0: + while (++i < l) (ev = events[i]).callback.call(ev.ctx); + return; + case 1: + while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); + return; + case 2: + while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); + return; + case 3: + while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); + return; + default: + while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); + return; + } +} diff --git a/src/view.js b/src/view.js deleted file mode 100644 index cca6933b..00000000 --- a/src/view.js +++ /dev/null @@ -1,267 +0,0 @@ -// Backbone.js 1.4.0 -// (c) 2010-2019 Jeremy Ashkenas and DocumentCloud -// Backbone may be freely distributed under the MIT license. - -// View -// ---- - -// Views are almost more convention than they are actual code. A View -// is simply a JavaScript object that represents a logical chunk of UI in the -// DOM. This might be a single item, an entire list, a sidebar or panel, or -// even the surrounding frame which wraps your whole app. Defining a chunk of -// UI as a **View** allows you to define your DOM events declaratively, without -// having to worry about render order ... and makes it easy for the view to -// react to specific changes in the state of your models. - -import extend from "lodash-es/extend.js"; -import isElement from "lodash-es/isElement.js"; -import isFunction from "lodash-es/isFunction.js"; -import pick from "lodash-es/pick.js"; -import result from "lodash-es/result.js"; -import uniqueId from "lodash-es/uniqueId.js"; -import { Events } from './events.js'; -import { inherits, sync, urlError, wrapError } from './helpers.js'; -import { render } from 'lit-html'; - -const paddedLt = /^\s* {}); }, - afterEach: function() { - window.fetch.restore() - } - + afterEach() { + window.fetch.restore(); + }, }); - QUnit.test('new and sort', function(assert) { + QUnit.test('new and sort', function (assert) { assert.expect(6); let counter = 0; - col.on('sort', function(){ counter++; }); + col.on('sort', function () { + counter++; + }); assert.deepEqual(col.pluck('label'), ['a', 'b', 'c', 'd']); - col.comparator = function(m1, m2) { + col.comparator = function (m1, m2) { return m1.id > m2.id ? -1 : 1; }; col.sort(); assert.equal(counter, 1); assert.deepEqual(col.pluck('label'), ['a', 'b', 'c', 'd']); - col.comparator = function(model) { return model.id; }; + col.comparator = function (model) { + return model.id; + }; col.sort(); assert.equal(counter, 2); assert.deepEqual(col.pluck('label'), ['d', 'c', 'b', 'a']); assert.equal(col.length, 4); }); - QUnit.test('String comparator.', function(assert) { + QUnit.test('String comparator.', function (assert) { assert.expect(1); - const collection = new Skeletor.Collection([ - {id: 3}, - {id: 1}, - {id: 2} - ], {comparator: 'id'}); + const collection = new Skeletor.Collection([{ id: 3 }, { id: 1 }, { id: 2 }], { comparator: 'id' }); assert.deepEqual(collection.pluck('id'), [1, 2, 3]); }); - QUnit.test('new and parse', function(assert) { + QUnit.test('new and parse', function (assert) { assert.expect(3); - const Collection = Skeletor.Collection.extend({ - parse: function(data) { - return _.filter(data, function(datum) { + class Collection extends Skeletor.Collection { + parse(data) { + return _.filter(data, function (datum) { return datum.a % 2 === 0; }); } - }); - const models = [{a: 1}, {a: 2}, {a: 3}, {a: 4}]; - const collection = new Collection(models, {parse: true}); + } + const models = [{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]; + const collection = new Collection(models, { parse: true }); assert.strictEqual(collection.length, 2); assert.strictEqual(collection.first().get('a'), 2); assert.strictEqual(collection.last().get('a'), 4); }); - QUnit.test('clone preserves model and comparator', function(assert) { - assert.expect(3); - const Model = Skeletor.Model.extend(); - const comparator = function(model){ return model.id; }; - - const collection = new Skeletor.Collection([{id: 1}], { - model: Model, - comparator: comparator - }).clone(); - collection.add({id: 2}); - assert.ok(collection.at(0) instanceof Model); - assert.ok(collection.at(1) instanceof Model); - assert.strictEqual(collection.comparator, comparator); - }); - - QUnit.test('get', function(assert) { - assert.expect(6); + QUnit.test('get', function (assert) { + assert.expect(4); assert.equal(col.get(0), d); - assert.equal(col.get(d.clone()), d); assert.equal(col.get(2), b); - assert.equal(col.get({id: 1}), c); - assert.equal(col.get(c.clone()), c); + assert.equal(col.get({ id: 1 }), c); assert.equal(col.get(col.first().cid), col.first()); }); - QUnit.test('get with non-default ids', function(assert) { - assert.expect(5); - const MongoModel = Skeletor.Model.extend({idAttribute: '_id'}); - const model = new MongoModel({_id: 100}); - const collection = new Skeletor.Collection([model], {model: MongoModel}); + QUnit.test('get with non-default ids', function (assert) { + assert.expect(4); + class MongoModel extends Skeletor.Model { + get idAttribute() { + return '_id'; + } + } + + const model = new MongoModel({ _id: 100 }); + const collection = new Skeletor.Collection([model], { model: MongoModel }); assert.equal(collection.get(100), model); assert.equal(collection.get(model.cid), model); assert.equal(collection.get(model), model); assert.equal(collection.get(101), undefined); - - const collection2 = new Skeletor.Collection(); - collection2.model = MongoModel; - collection2.add(model.attributes); - assert.equal(collection2.get(model.clone()), collection2.first()); }); - QUnit.test('has', function(assert) { + QUnit.test('has', function (assert) { assert.expect(15); assert.ok(col.has(a)); assert.ok(col.has(b)); @@ -120,52 +102,54 @@ assert.ok(col.has(b.cid)); assert.ok(col.has(c.cid)); assert.ok(col.has(d.cid)); - const outsider = new Skeletor.Model({id: 4}); + const outsider = new Skeletor.Model({ id: 4 }); assert.notOk(col.has(outsider)); assert.notOk(col.has(outsider.id)); assert.notOk(col.has(outsider.cid)); }); - QUnit.test('update index when id changes', function(assert) { + QUnit.test('update index when id changes', function (assert) { assert.expect(4); const collection = new Skeletor.Collection(); collection.add([ - {id: 0, name: 'one'}, - {id: 1, name: 'two'} + { id: 0, name: 'one' }, + { id: 1, name: 'two' }, ]); const one = collection.get(0); assert.equal(one.get('name'), 'one'); - collection.on('change:name', function(model) { assert.ok(this.get(model)); }); - one.set({name: 'dalmatians', id: 101}); + collection.on('change:name', function (model) { + assert.ok(this.get(model)); + }); + one.set({ name: 'dalmatians', id: 101 }); assert.equal(collection.get(0), null); assert.equal(collection.get(101).get('name'), 'dalmatians'); }); - QUnit.test('at', function(assert) { + QUnit.test('at', function (assert) { assert.expect(2); assert.equal(col.at(2), c); assert.equal(col.at(-2), c); }); - QUnit.test('pluck', function(assert) { + QUnit.test('pluck', function (assert) { assert.expect(1); assert.equal(col.pluck('label').join(' '), 'a b c d'); }); - QUnit.test('add', function(assert) { + QUnit.test('add', function (assert) { assert.expect(14); let added, opts, secondAdded; added = opts = secondAdded = null; - e = new Skeletor.Model({id: 10, label: 'e'}); + e = new Skeletor.Model({ id: 10, label: 'e' }); otherCol.add(e); - otherCol.on('add', function() { + otherCol.on('add', function () { secondAdded = true; }); - col.on('add', function(model, collection, options){ + col.on('add', function (model, collection, options) { added = model.get('label'); opts = options; }); - col.add(e, {amazing: true}); + col.add(e, { amazing: true }); assert.equal(added, 'e'); assert.equal(col.length, 5); assert.equal(col.last(), e); @@ -173,19 +157,19 @@ assert.equal(secondAdded, null); assert.ok(opts.amazing); - const f = new Skeletor.Model({id: 20, label: 'f'}); - const g = new Skeletor.Model({id: 21, label: 'g'}); - const h = new Skeletor.Model({id: 22, label: 'h'}); + const f = new Skeletor.Model({ id: 20, label: 'f' }); + const g = new Skeletor.Model({ id: 21, label: 'g' }); + const h = new Skeletor.Model({ id: 22, label: 'h' }); const atCol = new Skeletor.Collection([f, g, h]); assert.equal(atCol.length, 3); - atCol.add(e, {at: 1}); + atCol.add(e, { at: 1 }); assert.equal(atCol.length, 4); assert.equal(atCol.at(1), e); assert.equal(atCol.last(), h); const coll = new Skeletor.Collection(new Array(2)); let addCount = 0; - coll.on('add', function(){ + coll.on('add', function () { addCount += 1; }); coll.add([undefined, f, g]); @@ -196,67 +180,71 @@ assert.equal(addCount, 7); }); - QUnit.test('add multiple models', function(assert) { + QUnit.test('add multiple models', function (assert) { assert.expect(6); - const collection = new Skeletor.Collection([{at: 0}, {at: 1}, {at: 9}]); - collection.add([{at: 2}, {at: 3}, {at: 4}, {at: 5}, {at: 6}, {at: 7}, {at: 8}], {at: 2}); + const collection = new Skeletor.Collection([{ at: 0 }, { at: 1 }, { at: 9 }]); + collection.add([{ at: 2 }, { at: 3 }, { at: 4 }, { at: 5 }, { at: 6 }, { at: 7 }, { at: 8 }], { at: 2 }); for (let i = 0; i <= 5; i++) { assert.equal(collection.at(i).get('at'), i); } }); - QUnit.test('add; at should have preference over comparator', function(assert) { + QUnit.test('add; at should have preference over comparator', function (assert) { assert.expect(1); - const Col = Skeletor.Collection.extend({ - comparator: function(m1, m2) { + class Col extends Skeletor.Collection { + comparator(m1, m2) { return m1.id > m2.id ? -1 : 1; } - }); + } - const collection = new Col([{id: 2}, {id: 3}]); - collection.add(new Skeletor.Model({id: 1}), {at: 1}); + const collection = new Col([{ id: 2 }, { id: 3 }]); + collection.add(new Skeletor.Model({ id: 1 }), { at: 1 }); assert.equal(collection.pluck('id').join(' '), '3 1 2'); }); - QUnit.test('add; at should add to the end if the index is out of bounds', function(assert) { + QUnit.test('add; at should add to the end if the index is out of bounds', function (assert) { assert.expect(1); - const collection = new Skeletor.Collection([{id: 2}, {id: 3}]); - collection.add(new Skeletor.Model({id: 1}), {at: 5}); + const collection = new Skeletor.Collection([{ id: 2 }, { id: 3 }]); + collection.add(new Skeletor.Model({ id: 1 }), { at: 5 }); assert.equal(collection.pluck('id').join(' '), '2 3 1'); }); - QUnit.test("can't add model to collection twice", function(assert) { - const collection = new Skeletor.Collection([{id: 1}, {id: 2}, {id: 1}, {id: 2}, {id: 3}]); + QUnit.test("can't add model to collection twice", function (assert) { + const collection = new Skeletor.Collection([{ id: 1 }, { id: 2 }, { id: 1 }, { id: 2 }, { id: 3 }]); assert.equal(collection.pluck('id').join(' '), '1 2 3'); }); - QUnit.test("can't add different model with same id to collection twice", function(assert) { + QUnit.test("can't add different model with same id to collection twice", function (assert) { assert.expect(1); const collection = new Skeletor.Collection(); - collection.unshift({id: 101}); - collection.add({id: 101}); + collection.unshift({ id: 101 }); + collection.add({ id: 101 }); assert.equal(collection.length, 1); }); - QUnit.test('merge in duplicate models with {merge: true}', function(assert) { + QUnit.test('merge in duplicate models with {merge: true}', function (assert) { assert.expect(3); const collection = new Skeletor.Collection(); - collection.add([{id: 1, name: 'Moe'}, {id: 2, name: 'Curly'}, {id: 3, name: 'Larry'}]); - collection.add({id: 1, name: 'Moses'}); + collection.add([ + { id: 1, name: 'Moe' }, + { id: 2, name: 'Curly' }, + { id: 3, name: 'Larry' }, + ]); + collection.add({ id: 1, name: 'Moses' }); assert.equal(collection.first().get('name'), 'Moe'); - collection.add({id: 1, name: 'Moses'}, {merge: true}); + collection.add({ id: 1, name: 'Moses' }, { merge: true }); assert.equal(collection.first().get('name'), 'Moses'); - collection.add({id: 1, name: 'Tim'}, {merge: true, silent: true}); + collection.add({ id: 1, name: 'Tim' }, { merge: true, silent: true }); assert.equal(collection.first().get('name'), 'Tim'); }); - QUnit.test('add model to multiple collections', function(assert) { + QUnit.test('add model to multiple collections', function (assert) { assert.expect(10); let counter = 0; - const m = new Skeletor.Model({id: 10, label: 'm'}); - m.on('add', function(model, collection) { + const m = new Skeletor.Model({ id: 10, label: 'm' }); + m.on('add', function (model, collection) { counter++; assert.equal(m, model); if (counter > 1) { @@ -266,12 +254,12 @@ } }); const col1 = new Skeletor.Collection([]); - col1.on('add', function(model, collection) { + col1.on('add', function (model, collection) { assert.equal(m, model); assert.equal(col1, collection); }); const col2 = new Skeletor.Collection([]); - col2.on('add', function(model, collection) { + col2.on('add', function (model, collection) { assert.equal(m, model); assert.equal(col2, collection); }); @@ -281,43 +269,48 @@ assert.equal(m.collection, col1); }); - QUnit.test('add model with parse', function(assert) { + QUnit.test('add model with parse', function (assert) { assert.expect(1); - const Model = Skeletor.Model.extend({ - parse: function(obj) { + + class Model extends Skeletor.Model { + parse(obj) { obj.value += 1; return obj; } - }); + } - const Col = Skeletor.Collection.extend({model: Model}); + class Col extends Skeletor.Collection { + get model() { + return Model; + } + } const collection = new Col(); - collection.add({value: 1}, {parse: true}); + collection.add({ value: 1 }, { parse: true }); assert.equal(collection.at(0).get('value'), 2); }); - QUnit.test('add with parse and merge', function(assert) { + QUnit.test('add with parse and merge', function (assert) { const collection = new Skeletor.Collection(); - collection.parse = function(attrs) { - return _.map(attrs, function(model) { + collection.parse = function (attrs) { + return _.map(attrs, function (model) { if (model.model) return model.model; return model; }); }; - collection.add({id: 1}); - collection.add({model: {id: 1, name: 'Alf'}}, {parse: true, merge: true}); + collection.add({ id: 1 }); + collection.add({ model: { id: 1, name: 'Alf' } }, { parse: true, merge: true }); assert.equal(collection.first().get('name'), 'Alf'); }); - QUnit.test('add model to collection with sort()-style comparator', function(assert) { + QUnit.test('add model to collection with sort()-style comparator', function (assert) { assert.expect(3); const collection = new Skeletor.Collection(); - collection.comparator = function(m1, m2) { + collection.comparator = function (m1, m2) { return m1.get('name') < m2.get('name') ? -1 : 1; }; - const tom = new Skeletor.Model({name: 'Tom'}); - const rob = new Skeletor.Model({name: 'Rob'}); - const tim = new Skeletor.Model({name: 'Tim'}); + const tom = new Skeletor.Model({ name: 'Tom' }); + const rob = new Skeletor.Model({ name: 'Rob' }); + const tim = new Skeletor.Model({ name: 'Tim' }); collection.add(tom); collection.add(rob); collection.add(tim); @@ -326,29 +319,29 @@ assert.equal(collection.indexOf(tom), 2); }); - QUnit.test('comparator that depends on `this`', function(assert) { + QUnit.test('comparator that depends on `this`', function (assert) { assert.expect(2); const collection = new Skeletor.Collection(); - collection.negative = function(num) { + collection.negative = function (num) { return -num; }; - collection.comparator = function(model) { + collection.comparator = function (model) { return this.negative(model.id); }; - collection.add([{id: 1}, {id: 2}, {id: 3}]); + collection.add([{ id: 1 }, { id: 2 }, { id: 3 }]); assert.deepEqual(collection.pluck('id'), [3, 2, 1]); - collection.comparator = function(m1, m2) { + collection.comparator = function (m1, m2) { return this.negative(m2.id) - this.negative(m1.id); }; collection.sort(); assert.deepEqual(collection.pluck('id'), [1, 2, 3]); }); - QUnit.test('remove', function(assert) { + QUnit.test('remove', function (assert) { assert.expect(12); let removed = null; let result = null; - col.on('remove', function(model, collection, options) { + col.on('remove', function (model, collection, options) { removed = model.get('label'); assert.equal(options.index, 3); assert.equal(collection.get(model), undefined, '#3693: model cannot be fetched from collection'); @@ -372,79 +365,82 @@ assert.deepEqual(result, [], 'returns empty array when nothing removed'); }); - QUnit.test('add and remove return values', function(assert) { + QUnit.test('add and remove return values', function (assert) { assert.expect(13); - const Even = Skeletor.Model.extend({ - validate: function(attrs) { + + class Even extends Skeletor.Model { + validate(attrs) { if (attrs.id % 2 !== 0) return 'odd'; } - }); + } const collection = new Skeletor.Collection(); collection.model = Even; - let list = collection.add([{id: 2}, {id: 4}], {validate: true}); + let list = collection.add([{ id: 2 }, { id: 4 }], { validate: true }); assert.equal(list.length, 2); assert.ok(list[0] instanceof Skeletor.Model); assert.equal(list[1], collection.last()); assert.equal(list[1].get('id'), 4); - list = collection.add([{id: 3}, {id: 6}], {validate: true}); + list = collection.add([{ id: 3 }, { id: 6 }], { validate: true }); assert.equal(collection.length, 3); - assert.equal(list[0], false); + assert.equal(list[0], null); assert.equal(list[1].get('id'), 6); - let result = collection.add({id: 6}); + let result = collection.add({ id: 6 }); assert.equal(result.cid, list[1].cid); - result = collection.remove({id: 6}); + result = collection.remove({ id: 6 }); assert.equal(collection.length, 2); assert.equal(result.id, 6); - list = collection.remove([{id: 2}, {id: 8}]); + list = collection.remove([{ id: 2 }, { id: 8 }]); assert.equal(collection.length, 1); assert.equal(list[0].get('id'), 2); assert.equal(list[1], null); }); - QUnit.test('shift and pop', function(assert) { + QUnit.test('shift and pop', function (assert) { assert.expect(2); - const collection = new Skeletor.Collection([{a: 'a'}, {b: 'b'}, {c: 'c'}]); + const collection = new Skeletor.Collection([{ a: 'a' }, { b: 'b' }, { c: 'c' }]); assert.equal(collection.shift().get('a'), 'a'); assert.equal(collection.pop().get('c'), 'c'); }); - QUnit.test('slice', function(assert) { + QUnit.test('slice', function (assert) { assert.expect(2); - const collection = new Skeletor.Collection([{a: 'a'}, {b: 'b'}, {c: 'c'}]); + const collection = new Skeletor.Collection([{ a: 'a' }, { b: 'b' }, { c: 'c' }]); const array = collection.slice(1, 3); assert.equal(array.length, 2); assert.equal(array[0].get('b'), 'b'); }); - QUnit.test('events are unbound on remove', function(assert) { + QUnit.test('events are unbound on remove', function (assert) { assert.expect(3); let counter = 0; const dj = new Skeletor.Model(); const emcees = new Skeletor.Collection([dj]); - emcees.on('change', function(){ counter++; }); - dj.set({name: 'Kool'}); + emcees.on('change', function () { + counter++; + }); + dj.set({ name: 'Kool' }); assert.equal(counter, 1); emcees.reset([]); assert.equal(dj.collection, undefined); - dj.set({name: 'Shadow'}); + dj.set({ name: 'Shadow' }); assert.equal(counter, 1); }); - QUnit.test('remove in multiple collections', function(assert) { + QUnit.test('remove in multiple collections', function (assert) { assert.expect(7); const modelData = { id: 5, - title: 'Othello' + title: 'Othello', }; let passed = false; const m1 = new Skeletor.Model(modelData); const m2 = new Skeletor.Model(modelData); - m2.on('remove', function() { + m2.on('remove', function () { passed = true; }); const col1 = new Skeletor.Collection([m1]); @@ -460,11 +456,11 @@ assert.equal(passed, true); }); - QUnit.test('remove same model in multiple collection', function(assert) { + QUnit.test('remove same model in multiple collection', function (assert) { assert.expect(16); let counter = 0; - const m = new Skeletor.Model({id: 5, title: 'Othello'}); - m.on('remove', function(model, collection) { + const m = new Skeletor.Model({ id: 5, title: 'Othello' }); + m.on('remove', function (model, collection) { counter++; assert.equal(m, model); if (counter > 1) { @@ -474,12 +470,12 @@ } }); const col1 = new Skeletor.Collection([m]); - col1.on('remove', function(model, collection) { + col1.on('remove', function (model, collection) { assert.equal(m, model); assert.equal(col1, collection); }); const col2 = new Skeletor.Collection([m]); - col2.on('remove', function(model, collection) { + col2.on('remove', function (model, collection) { assert.equal(m, model); assert.equal(col2, collection); }); @@ -495,10 +491,12 @@ assert.equal(counter, 2); }); - QUnit.test('model destroy removes from all collections', function(assert) { + QUnit.test('model destroy removes from all collections', function (assert) { assert.expect(3); - const m = new Skeletor.Model({id: 5, title: 'Othello'}); - m.sync = function(method, model, options) { options.success(); }; + const m = new Skeletor.Model({ id: 5, title: 'Othello' }); + m.sync = function (method, model, options) { + options.success(); + }; const col1 = new Skeletor.Collection([m]); const col2 = new Skeletor.Collection([m]); m.destroy(); @@ -507,10 +505,12 @@ assert.equal(undefined, m.collection); }); - QUnit.test('Collection: non-persisted model destroy removes from all collections', function(assert) { + QUnit.test('Collection: non-persisted model destroy removes from all collections', function (assert) { assert.expect(3); - const m = new Skeletor.Model({title: 'Othello'}); - m.sync = function(method, model, options) { throw 'should not be called'; }; + const m = new Skeletor.Model({ title: 'Othello' }); + m.sync = function (method, model, options) { + throw 'should not be called'; + }; const col1 = new Skeletor.Collection([m]); const col2 = new Skeletor.Collection([m]); m.destroy(); @@ -519,21 +519,21 @@ assert.equal(undefined, m.collection); }); - QUnit.test('fetch', function(assert) { + QUnit.test('fetch', function (assert) { assert.expect(5); const collection = new Skeletor.Collection(); collection.url = '/test'; - sinon.spy(collection, 'sync') + sinon.spy(collection, 'sync'); collection.fetch(); assert.ok(collection.sync.callCount === 1); assert.ok(collection.sync.lastCall.args[0] === 'read'); assert.ok(collection.sync.lastCall.args[1] == collection); assert.ok(collection.sync.lastCall.args[2].parse === true); - collection.fetch({parse: false}); + collection.fetch({ parse: false }); assert.ok(collection.sync.lastCall.args[2].parse === false); }); - QUnit.test('fetch with an error response triggers an error event', function(assert) { + QUnit.test('fetch with an error response triggers an error event', function (assert) { assert.expect(1); const collection = new Skeletor.Collection(); collection.on('error', () => assert.ok(true)); @@ -541,44 +541,44 @@ collection.fetch(); }); - QUnit.test('#3283 - fetch with an error response calls error with context', function(assert) { + QUnit.test('#3283 - fetch with an error response calls error with context', function (assert) { assert.expect(1); const collection = new Skeletor.Collection(); const obj = {}; const options = { context: obj, - error: function() { + error() { assert.equal(this, obj); - } + }, }; - collection.sync = function(method, model, opts) { + collection.sync = function (method, model, opts) { opts.error.call(opts.context); }; collection.fetch(options); }); - QUnit.test('ensure fetch only parses once', function(assert) { + QUnit.test('ensure fetch only parses once', function (assert) { assert.expect(1); const collection = new Skeletor.Collection(); let counter = 0; - collection.parse = models => { + collection.parse = (models) => { counter++; return models; }; collection.url = '/test'; - sinon.spy(collection, 'sync') + sinon.spy(collection, 'sync'); collection.fetch(); collection.sync.lastCall.args[2].success([]); assert.equal(counter, 1); collection.sync.restore(); }); - QUnit.test('create', function(assert) { + QUnit.test('create', function (assert) { assert.expect(5); const collection = new Skeletor.Collection(); - sinon.spy(Skeletor.Model.prototype, 'sync') + sinon.spy(Skeletor.Model.prototype, 'sync'); collection.url = '/test'; - const model = collection.create({label: 'f'}, {wait: true}); + const model = collection.create({ label: 'f' }, { wait: true }); assert.equal(Skeletor.Model.prototype.sync.callCount, 1); assert.equal(Skeletor.Model.prototype.sync.lastCall.args[0], 'create'); assert.equal(Skeletor.Model.prototype.sync.lastCall.args[1], model); @@ -587,140 +587,171 @@ Skeletor.Model.prototype.sync.restore(); }); - QUnit.test('create with validate:true enforces validation', function(assert) { + QUnit.test('create with validate:true enforces validation', function (assert) { assert.expect(3); - const ValidatingModel = Skeletor.Model.extend({ - validate: function(attrs) { + class ValidatingModel extends Skeletor.Model { + // eslint-disable-next-line class-methods-use-this + validate(attrs) { return 'fail'; } - }); - const ValidatingCollection = Skeletor.Collection.extend({ - model: ValidatingModel - }); + } + class ValidatingCollection extends Skeletor.Collection { + // eslint-disable-next-line class-methods-use-this + get model() { + return ValidatingModel; + } + } const collection = new ValidatingCollection(); - collection.on('invalid', function(coll, error, options) { + collection.on('invalid', function (coll, error, options) { assert.equal(error, 'fail'); assert.equal(options.validationError, 'fail'); }); - assert.equal(collection.create({foo: 'bar'}, {validate: true}), false); + assert.equal(collection.create({ foo: 'bar' }, { validate: true }), false); }); - QUnit.test('create will pass extra options to success callback', function(assert) { + QUnit.test('create will pass extra options to success callback', function (assert) { assert.expect(1); - const Model = Skeletor.Model.extend({ - sync: function(method, model, options) { - _.extend(options, {specialSync: true}); + class Model extends Skeletor.Model { + sync(method, model, options) { + _.extend(options, { specialSync: true }); return Skeletor.Model.prototype.sync.call(this, method, model, options); } - }); + } - const Collection = Skeletor.Collection.extend({ - model: Model, - url: '/test' - }); + class Collection extends Skeletor.Collection { + // eslint-disable-next-line class-methods-use-this + get model() { + return Model; + } + // eslint-disable-next-line class-methods-use-this + get url() { + return '/test'; + } + } const collection = new Collection(); - const success = (model, response, options) => assert.ok( - options.specialSync, 'Options were passed correctly to callback'); + const success = (model, response, options) => + assert.ok(options.specialSync, 'Options were passed correctly to callback'); collection.create({}, { success: success }); window.fetch.lastCall.args[1].success(); }); - QUnit.test('create with wait:true should not call collection.parse', function(assert) { + QUnit.test('create with wait:true should not call collection.parse', function (assert) { assert.expect(0); - const Collection = Skeletor.Collection.extend({ - url: '/test', - parse: () => assert.ok(false) - }); + class Collection extends Skeletor.Collection { + // eslint-disable-next-line class-methods-use-this + get url() { + return '/test'; + } + // eslint-disable-next-line class-methods-use-this + parse() { + assert.ok(false); + } + } const collection = new Collection(); collection.create({}, { wait: true }); window.fetch.lastCall.args[1].success(); }); - QUnit.test('a failing create returns model with errors', function(assert) { - const ValidatingModel = Skeletor.Model.extend({ - validate: function(attrs) { + QUnit.test('a failing create returns model with errors', function (assert) { + class ValidatingModel extends Skeletor.Model { + // eslint-disable-next-line class-methods-use-this + validate(attrs) { return 'fail'; } - }); - const ValidatingCollection = Skeletor.Collection.extend({ - model: ValidatingModel - }); + } + class ValidatingCollection extends Skeletor.Collection { + // eslint-disable-next-line class-methods-use-this + get model() { + return ValidatingModel; + } + } const collection = new ValidatingCollection(); - const m = collection.create({foo: 'bar'}); + const m = collection.create({ foo: 'bar' }); assert.equal(m.validationError, 'fail'); assert.equal(collection.length, 1); }); - QUnit.test('initialize', function(assert) { + QUnit.test('initialize', function (assert) { assert.expect(1); - const Collection = Skeletor.Collection.extend({ - initialize: function() { + class Collection extends Skeletor.Collection { + initialize() { this.one = 1; } - }); + } const coll = new Collection(); assert.equal(coll.one, 1); }); - QUnit.test('preinitialize', function(assert) { + QUnit.test('preinitialize', function (assert) { assert.expect(1); - const Collection = Skeletor.Collection.extend({ - preinitialize: function() { + class Collection extends Skeletor.Collection { + preinitialize() { this.one = 1; } - }); + } const coll = new Collection(); assert.equal(coll.one, 1); }); - QUnit.test('preinitialize occurs before the collection is set up', function(assert) { + QUnit.test('preinitialize occurs before the collection is set up', function (assert) { assert.expect(2); - const Collection = Skeletor.Collection.extend({ - preinitialize: function() { + class Collection extends Skeletor.Collection { + preinitialize() { assert.notEqual(this.model, FooModel); } - }); - const FooModel = Skeletor.Model.extend({id: 'foo'}); - const coll = new Collection({}, { - model: FooModel - }); + } + class FooModel extends Skeletor.Model { + constructor() { + super(); + this.id = 'foo'; + } + } + const coll = new Collection( + {}, + { + model: FooModel, + }, + ); assert.equal(coll.model, FooModel); }); - QUnit.test('toJSON', function(assert) { + QUnit.test('toJSON', function (assert) { assert.expect(1); - assert.equal(JSON.stringify(col), '[{"id":3,"label":"a"},{"id":2,"label":"b"},{"id":1,"label":"c"},{"id":0,"label":"d"}]'); + assert.equal( + JSON.stringify(col), + '[{"id":3,"label":"a"},{"id":2,"label":"b"},{"id":1,"label":"c"},{"id":0,"label":"d"}]', + ); }); - QUnit.test('where and findWhere', function(assert) { + QUnit.test('where and findWhere', function (assert) { assert.expect(8); - const model = new Skeletor.Model({a: 1}); - const coll = new Skeletor.Collection([ - model, - {a: 1}, - {a: 1, b: 2}, - {a: 2, b: 2}, - {a: 3} - ]); - assert.equal(coll.where({a: 1}).length, 3); - assert.equal(coll.where({a: 2}).length, 1); - assert.equal(coll.where({a: 3}).length, 1); - assert.equal(coll.where({b: 1}).length, 0); - assert.equal(coll.where({b: 2}).length, 2); - assert.equal(coll.where({a: 1, b: 2}).length, 1); - assert.equal(coll.findWhere({a: 1}), model); - assert.equal(coll.findWhere({a: 4}), undefined); - }); - - QUnit.test('Lodash methods', function(assert) { + const model = new Skeletor.Model({ a: 1 }); + const coll = new Skeletor.Collection([model, { a: 1 }, { a: 1, b: 2 }, { a: 2, b: 2 }, { a: 3 }]); + assert.equal(coll.where({ a: 1 }).length, 3); + assert.equal(coll.where({ a: 2 }).length, 1); + assert.equal(coll.where({ a: 3 }).length, 1); + assert.equal(coll.where({ b: 1 }).length, 0); + assert.equal(coll.where({ b: 2 }).length, 2); + assert.equal(coll.where({ a: 1, b: 2 }).length, 1); + assert.equal(coll.findWhere({ a: 1 }), model); + assert.equal(coll.findWhere({ a: 4 }), undefined); + }); + + QUnit.test('Lodash methods', function (assert) { assert.expect(16); - assert.equal(col.map(model => model.get('label')).join(' '), 'a b c d'); - assert.equal(col.some(model => model.id === 100), false); - assert.equal(col.some(model => model.id === 0), true); - assert.equal(col.reduce((m1, m2) => m1.id > m2.id ? m1 : m2).id, 3); - assert.equal(col.reduceRight((m1, m2) => m1.id > m2.id ? m1 : m2).id, 3); + assert.equal(col.map((model) => model.get('label')).join(' '), 'a b c d'); + assert.equal( + col.some((model) => model.id === 100), + false, + ); + assert.equal( + col.some((model) => model.id === 0), + true, + ); + assert.equal(col.reduce((m1, m2) => (m1.id > m2.id ? m1 : m2)).id, 3); + assert.equal(col.reduceRight((m1, m2) => (m1.id > m2.id ? m1 : m2)).id, 3); assert.equal(col.indexOf(b), 1); assert.equal(col.size(), 4); assert.equal(col.drop().length, 3); @@ -731,49 +762,49 @@ assert.deepEqual(col.difference([c, d]), [a, b]); const first = col.first(); - assert.deepEqual(col.groupBy(model => model.id)[first.id], [first]); - assert.deepEqual(col.countBy(model => model.id), {0: 1, 1: 1, 2: 1, 3: 1}); - assert.deepEqual(col.sortBy(model => model.id)[0], col.at(3)); + assert.deepEqual(col.groupBy((model) => model.id)[first.id], [first]); + assert.deepEqual( + col.countBy((model) => model.id), + { 0: 1, 1: 1, 2: 1, 3: 1 }, + ); + assert.deepEqual(col.sortBy((model) => model.id)[0], col.at(3)); assert.ok(col.keyBy('id')[first.id] === first); }); - QUnit.test('Underscore methods with object-style and property-style iteratee', function(assert) { + QUnit.test('Underscore methods with object-style and property-style iteratee', function (assert) { assert.expect(20); - const model = new Skeletor.Model({a: 4, b: 1, e: 3}); - const coll = new Skeletor.Collection([ - {a: 1, b: 1}, - {a: 2, b: 1, c: 1}, - {a: 3, b: 1}, - model - ]); - assert.equal(coll.find({a: 0}), undefined); - assert.deepEqual(coll.find({a: 4}), model); + const model = new Skeletor.Model({ a: 4, b: 1, e: 3 }); + const coll = new Skeletor.Collection([{ a: 1, b: 1 }, { a: 2, b: 1, c: 1 }, { a: 3, b: 1 }, model]); + assert.equal(coll.find({ a: 0 }), undefined); + assert.deepEqual(coll.find({ a: 4 }), model); assert.equal(coll.find('d'), undefined); assert.deepEqual(coll.find('e'), model); - assert.equal(coll.filter({a: 0}), false); - assert.deepEqual(coll.filter({a: 4}), [model]); - assert.equal(coll.some({a: 0}), false); - assert.equal(coll.some({a: 1}), true); - assert.equal(coll.every({a: 0}), false); - assert.equal(coll.every({b: 1}), true); - assert.deepEqual(coll.map({a: 2}), [false, true, false, false]); + assert.equal(coll.filter({ a: 0 }), false); + assert.deepEqual(coll.filter({ a: 4 }), [model]); + assert.equal(coll.some({ a: 0 }), false); + assert.equal(coll.some({ a: 1 }), true); + assert.equal(coll.every({ a: 0 }), false); + assert.equal(coll.every({ b: 1 }), true); + assert.deepEqual(coll.map({ a: 2 }), [false, true, false, false]); assert.deepEqual(coll.map('a'), [1, 2, 3, 4]); assert.deepEqual(coll.sortBy('a')[3], model); assert.deepEqual(coll.sortBy('e')[0], model); - assert.deepEqual(coll.countBy({a: 4}), {'false': 3, 'true': 1}); - assert.deepEqual(coll.countBy('d'), {'undefined': 4}); - assert.equal(coll.findIndex({b: 1}), 0); - assert.equal(coll.findIndex({b: 9}), -1); - assert.equal(coll.findLastIndex({b: 1}), 3); - assert.equal(coll.findLastIndex({b: 9}), -1); + assert.deepEqual(coll.countBy({ a: 4 }), { 'false': 3, 'true': 1 }); + assert.deepEqual(coll.countBy('d'), { 'undefined': 4 }); + assert.equal(coll.findIndex({ b: 1 }), 0); + assert.equal(coll.findIndex({ b: 9 }), -1); + assert.equal(coll.findLastIndex({ b: 1 }), 3); + assert.equal(coll.findLastIndex({ b: 9 }), -1); }); - QUnit.test('reset', function(assert) { + QUnit.test('reset', function (assert) { assert.expect(16); let resetCount = 0; const models = col.models; - col.on('reset', function() { resetCount += 1; }); + col.on('reset', function () { + resetCount += 1; + }); col.reset([]); assert.equal(resetCount, 1); assert.equal(col.length, 0); @@ -782,7 +813,7 @@ assert.equal(resetCount, 2); assert.equal(col.length, 4); assert.equal(col.last(), d); - col.reset(models.map(m => m.attributes)); + col.reset(models.map((m) => m.attributes)); assert.equal(resetCount, 3); assert.equal(col.length, 4); assert.ok(col.last() !== d); @@ -791,7 +822,7 @@ assert.equal(col.length, 0); assert.equal(resetCount, 4); - const f = new Skeletor.Model({id: 20, label: 'f'}); + const f = new Skeletor.Model({ id: 20, label: 'f' }); col.reset([undefined, f]); assert.equal(col.length, 2); assert.equal(resetCount, 5); @@ -801,54 +832,69 @@ assert.equal(resetCount, 6); }); - QUnit.test('reset with different values', function(assert) { - const collection = new Skeletor.Collection({id: 1}); - collection.reset({id: 1, a: 1}); + QUnit.test('reset with different values', function (assert) { + const collection = new Skeletor.Collection({ id: 1 }); + collection.reset({ id: 1, a: 1 }); assert.equal(collection.get(1).get('a'), 1); }); - QUnit.test('same references in reset', function(assert) { - const model = new Skeletor.Model({id: 1}); - const collection = new Skeletor.Collection({id: 1}); + QUnit.test('same references in reset', function (assert) { + const model = new Skeletor.Model({ id: 1 }); + const collection = new Skeletor.Collection({ id: 1 }); collection.reset(model); assert.equal(collection.get(1), model); }); - QUnit.test('reset passes caller options', function(assert) { + QUnit.test('reset passes caller options', function (assert) { assert.expect(3); - const Model = Skeletor.Model.extend({ - initialize: function(attrs, options) { + class Model extends Skeletor.Model { + initialize(attrs, options) { this.modelParameter = options.modelParameter; } - }); - const collection = new (Skeletor.Collection.extend({model: Model}))(); - collection.reset([{astring: 'green', anumber: 1}, {astring: 'blue', anumber: 2}], {modelParameter: 'model parameter'}); + } + + class Col extends Skeletor.Collection { + get model() { + return Model; + } + } + + const collection = new Col(); + collection.reset( + [ + { astring: 'green', anumber: 1 }, + { astring: 'blue', anumber: 2 }, + ], + { modelParameter: 'model parameter' }, + ); assert.equal(collection.length, 2); - collection.each(function(model) { + collection.each(function (model) { assert.equal(model.modelParameter, 'model parameter'); }); }); - QUnit.test('reset does not alter options by reference', function(assert) { + QUnit.test('reset does not alter options by reference', function (assert) { assert.expect(2); - const collection = new Skeletor.Collection([{id: 1}]); + const collection = new Skeletor.Collection([{ id: 1 }]); const origOpts = {}; - collection.on('reset', function(coll, opts){ + collection.on('reset', function (coll, opts) { assert.equal(origOpts.previousModels, undefined); assert.equal(opts.previousModels[0].id, 1); }); collection.reset([], origOpts); }); - QUnit.test('trigger custom events on models', function(assert) { + QUnit.test('trigger custom events on models', function (assert) { assert.expect(1); let fired = null; - a.on('custom', function() { fired = true; }); + a.on('custom', function () { + fired = true; + }); a.trigger('custom'); assert.equal(fired, true); }); - QUnit.test('add does not alter arguments', function(assert) { + QUnit.test('add does not alter arguments', function (assert) { assert.expect(2); const attrs = {}; const models = [attrs]; @@ -857,59 +903,77 @@ assert.ok(attrs === models[0]); }); - QUnit.test('#714: access `model.collection` in a brand new model.', function(assert) { + QUnit.test('#714: access `model.collection` in a brand new model.', function (assert) { assert.expect(2); const collection = new Skeletor.Collection(); collection.url = '/test'; - const Model = Skeletor.Model.extend({ - set: function(attrs) { + class Model extends Skeletor.Model { + set(attrs) { assert.equal(attrs.prop, 'value'); assert.equal(this.collection, collection); return this; } - }); + } collection.model = Model; - collection.create({prop: 'value'}); + collection.create({ prop: 'value' }); }); - QUnit.test('#574, remove its own reference to the .models array.', function(assert) { + QUnit.test('#574, remove its own reference to the .models array.', function (assert) { assert.expect(2); - const collection = new Skeletor.Collection([ - {id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6} - ]); + const collection = new Skeletor.Collection([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }]); assert.equal(collection.length, 6); collection.remove(collection.models); assert.equal(collection.length, 0); }); - QUnit.test('#861, adding models to a collection which do not pass validation, with validate:true', function(assert) { + QUnit.test('#861, adding models to a collection which do not pass validation, with validate:true', function (assert) { assert.expect(2); - const Model = Skeletor.Model.extend({ - validate: function(attrs) { + class Model extends Skeletor.Model { + // eslint-disable-next-line class-methods-use-this + validate(attrs) { if (attrs.id === 3) return "id can't be 3"; } - }); + } - const Collection = Skeletor.Collection.extend({ - model: Model - }); + class Collection extends Skeletor.Collection { + get model() { + return Model; + } + } const collection = new Collection(); - collection.on('invalid', function() { assert.ok(true); }); + collection.on('invalid', function () { + assert.ok(true); + }); - collection.add([{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6}], {validate: true}); + collection.add([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }], { validate: true }); assert.deepEqual(collection.pluck('id'), [1, 2, 4, 5, 6]); }); - QUnit.test('Invalid models are discarded with validate:true.', function(assert) { + QUnit.test('Invalid models are discarded with validate:true.', function (assert) { assert.expect(5); - const collection = new Skeletor.Collection(); - collection.on('test', function() { assert.ok(true); }); - collection.model = Skeletor.Model.extend({ - validate: function(attrs){ if (!attrs.valid) return 'invalid'; } + + class CollectionModel extends Skeletor.Model { + // eslint-disable-next-line class-methods-use-this + validate(attrs) { + if (!attrs.valid) return 'invalid'; + } + } + + class Collection extends Skeletor.Collection { + get model() { + return CollectionModel; + } + } + + const collection = new Collection(); + + collection.on('test', function () { + assert.ok(true); }); - const model = new collection.model({id: 1, valid: true}); - collection.add([model, {id: 2}], {validate: true}); + + const model = new CollectionModel({ id: 1, valid: true }); + collection.add([model, { id: 2 }], { validate: true }); model.trigger('test'); assert.ok(collection.get(model.cid)); assert.ok(collection.get(1)); @@ -917,111 +981,113 @@ assert.equal(collection.length, 1); }); - QUnit.test('multiple copies of the same model', function(assert) { + QUnit.test('multiple copies of the same model', function (assert) { assert.expect(3); const collection = new Skeletor.Collection(); const model = new Skeletor.Model(); collection.add([model, model]); assert.equal(collection.length, 1); - collection.add([{id: 1}, {id: 1}]); + collection.add([{ id: 1 }, { id: 1 }]); assert.equal(collection.length, 2); assert.equal(collection.last().id, 1); }); - QUnit.test('#964 - collection.get return inconsistent', function(assert) { + QUnit.test('#964 - collection.get return inconsistent', function (assert) { assert.expect(2); const collection = new Skeletor.Collection(); assert.ok(collection.get(null) === undefined); assert.ok(collection.get() === undefined); }); - QUnit.test('#1112 - passing options.model sets collection.model', function(assert) { + QUnit.test('#1112 - passing options.model sets collection.model', function (assert) { assert.expect(2); - const Model = Skeletor.Model.extend({}); - const collection = new Skeletor.Collection([{id: 1}], {model: Model}); + class Model extends Skeletor.Model {} + const collection = new Skeletor.Collection([{ id: 1 }], { model: Model }); assert.ok(collection.model === Model); assert.ok(collection.at(0) instanceof Model); }); - QUnit.test('null and undefined are invalid ids.', function(assert) { + QUnit.test('null and undefined are invalid ids.', function (assert) { assert.expect(2); - const model = new Skeletor.Model({id: 1}); + const model = new Skeletor.Model({ id: 1 }); const collection = new Skeletor.Collection([model]); - model.set({id: null}); + model.set({ id: null }); assert.ok(!collection.get('null')); - model.set({id: 1}); - model.set({id: undefined}); + model.set({ id: 1 }); + model.set({ id: undefined }); assert.ok(!collection.get('undefined')); }); - QUnit.test('falsy comparator', function(assert) { + QUnit.test('falsy comparator', function (assert) { assert.expect(4); - const Col = Skeletor.Collection.extend({ - comparator: function(model){ return model.id; } - }); + class Col extends Skeletor.Collection { + comparator(model) { + return model.id; + } + } const collection = new Col(); - const colFalse = new Col(null, {comparator: false}); - const colNull = new Col(null, {comparator: null}); - const colUndefined = new Col(null, {comparator: undefined}); + const colFalse = new Col(null, { comparator: false }); + const colNull = new Col(null, { comparator: null }); + const colUndefined = new Col(null, { comparator: undefined }); assert.ok(collection.comparator); assert.ok(!colFalse.comparator); assert.ok(!colNull.comparator); assert.ok(colUndefined.comparator); }); - QUnit.test('#1355 - `options` is passed to success callbacks', function(assert) { + QUnit.test('#1355 - `options` is passed to success callbacks', function (assert) { assert.expect(2); - const m = new Skeletor.Model({x: 1}); + const m = new Skeletor.Model({ x: 1 }); const collection = new Skeletor.Collection(); const opts = { opts: true, - success: function(coll, resp, options) { + success(coll, resp, options) { assert.ok(options.opts); - } + }, }; - collection.sync = m.sync = function( method, coll, options ){ + collection.sync = m.sync = function (method, coll, options) { options.success({}); }; collection.fetch(opts); collection.create(m, opts); }); - QUnit.test("#1412 - Trigger 'request' and 'sync' events.", function(assert) { + QUnit.test("#1412 - Trigger 'request' and 'sync' events.", function (assert) { assert.expect(4); const collection = new Skeletor.Collection(); collection.url = '/test'; - collection.on('request', function(obj, xhr, options) { + collection.on('request', function (obj, xhr, options) { assert.ok(obj === collection, "collection has correct 'request' event after fetching"); }); - collection.on('sync', function(obj, response, options) { + collection.on('sync', function (obj, response, options) { assert.ok(obj === collection, "collection has correct 'sync' event after fetching"); }); collection.fetch(); window.fetch.lastCall.args[1].success(); collection.off(); - collection.on('request', function(obj, xhr, options) { + collection.on('request', function (obj, xhr, options) { assert.ok(obj === collection.get(1), "collection has correct 'request' event after one of its models save"); }); - collection.on('sync', function(obj, response, options) { + collection.on('sync', function (obj, response, options) { assert.ok(obj === collection.get(1), "collection has correct 'sync' event after one of its models save"); }); - collection.create({id: 1}); + collection.create({ id: 1 }); window.fetch.lastCall.args[1].success(); collection.off(); }); - QUnit.test('#3283 - fetch, create calls success with context', function(assert) { + QUnit.test('#3283 - fetch, create calls success with context', function (assert) { assert.expect(2); const collection = new Skeletor.Collection(); collection.url = '/test'; const obj = {}; const options = { context: obj, - success: function() { + success() { assert.equal(this, obj); - } + }, }; collection.fetch(options); window.fetch.lastCall.args[1].success(); @@ -1029,188 +1095,220 @@ window.fetch.lastCall.args[1].success(); }); - QUnit.test('#1447 - create with wait adds model.', function(assert) { + QUnit.test('#1447 - create with wait adds model.', function (assert) { assert.expect(1); const collection = new Skeletor.Collection(); const model = new Skeletor.Model(); - model.sync = function(method, m, options){ options.success(); }; - collection.on('add', function(){ assert.ok(true); }); - collection.create(model, {wait: true}); + model.sync = function (method, m, options) { + options.success(); + }; + collection.on('add', function () { + assert.ok(true); + }); + collection.create(model, { wait: true }); }); - QUnit.test('#1448 - add sorts collection after merge.', function(assert) { + QUnit.test('#1448 - add sorts collection after merge.', function (assert) { assert.expect(1); const collection = new Skeletor.Collection([ - {id: 1, x: 1}, - {id: 2, x: 2} + { id: 1, x: 1 }, + { id: 2, x: 2 }, ]); - collection.comparator = function(model){ return model.get('x'); }; - collection.add({id: 1, x: 3}, {merge: true}); + collection.comparator = function (model) { + return model.get('x'); + }; + collection.add({ id: 1, x: 3 }, { merge: true }); assert.deepEqual(collection.pluck('id'), [2, 1]); }); - QUnit.test('#1655 - groupBy can be used with a string argument.', function(assert) { + QUnit.test('#1655 - groupBy can be used with a string argument.', function (assert) { assert.expect(3); - const collection = new Skeletor.Collection([{x: 1}, {x: 2}]); + const collection = new Skeletor.Collection([{ x: 1 }, { x: 2 }]); const grouped = collection.groupBy('x'); assert.strictEqual(_.keys(grouped).length, 2); assert.strictEqual(grouped[1][0].get('x'), 1); assert.strictEqual(grouped[2][0].get('x'), 2); }); - QUnit.test('#1655 - sortBy can be used with a string argument.', function(assert) { + QUnit.test('#1655 - sortBy can be used with a string argument.', function (assert) { assert.expect(1); - const collection = new Skeletor.Collection([{x: 3}, {x: 1}, {x: 2}]); - const values = _.map(collection.sortBy('x'), function(model) { + const collection = new Skeletor.Collection([{ x: 3 }, { x: 1 }, { x: 2 }]); + const values = _.map(collection.sortBy('x'), function (model) { return model.get('x'); }); assert.deepEqual(values, [1, 2, 3]); }); - QUnit.test('#1604 - Removal during iteration.', function(assert) { + QUnit.test('#1604 - Removal during iteration.', function (assert) { assert.expect(0); const collection = new Skeletor.Collection([{}, {}]); - collection.on('add', function() { + collection.on('add', function () { collection.at(0).destroy(); }); - collection.add({}, {at: 0}); + collection.add({}, { at: 0 }); }); - QUnit.test('#1638 - `sort` during `add` triggers correctly.', function(assert) { + QUnit.test('#1638 - `sort` during `add` triggers correctly.', function (assert) { const collection = new Skeletor.Collection(); - collection.comparator = function(model) { return model.get('x'); }; + collection.comparator = function (model) { + return model.get('x'); + }; const added = []; - collection.on('add', function(model) { - model.set({x: 3}); + collection.on('add', function (model) { + model.set({ x: 3 }); collection.sort(); added.push(model.id); }); - collection.add([{id: 1, x: 1}, {id: 2, x: 2}]); + collection.add([ + { id: 1, x: 1 }, + { id: 2, x: 2 }, + ]); assert.deepEqual(added, [1, 2]); }); - QUnit.test('fetch parses models by default', function(assert) { + QUnit.test('fetch parses models by default', function (assert) { assert.expect(1); const model = {}; - const Collection = Skeletor.Collection.extend({ - url: 'test', - model: Skeletor.Model.extend({ - parse: resp => assert.strictEqual(resp, model) - }) - }); + + class CollectionModel extends Skeletor.Model { + // eslint-disable-next-line class-methods-use-this + parse(resp) { + assert.strictEqual(resp, model); + } + } + + class Collection extends Skeletor.Collection { + get url() { + return 'test'; + } + get model() { + return CollectionModel; + } + } new Collection().fetch(); window.fetch.lastCall.args[1].success([model]); }); - QUnit.test("`sort` shouldn't always fire on `add`", function(assert) { + QUnit.test("`sort` shouldn't always fire on `add`", function (assert) { assert.expect(1); - const collection = new Skeletor.Collection([{id: 1}, {id: 2}, {id: 3}], { - comparator: 'id' + const collection = new Skeletor.Collection([{ id: 1 }, { id: 2 }, { id: 3 }], { + comparator: 'id', }); collection.sort = () => assert.ok(true); collection.add([]); - collection.add({id: 1}); - collection.add([{id: 2}, {id: 3}]); - collection.add({id: 4}); + collection.add({ id: 1 }); + collection.add([{ id: 2 }, { id: 3 }]); + collection.add({ id: 4 }); }); - QUnit.test('#1407 parse option on constructor parses collection and models', function(assert) { + QUnit.test('#1407 parse option on constructor parses collection and models', function (assert) { assert.expect(2); const model = { - namespace: [{id: 1}, {id: 2}] + namespace: [{ id: 1 }, { id: 2 }], }; - const Collection = Skeletor.Collection.extend({ - model: Skeletor.Model.extend({ - parse: function(m) { - m.name = 'test'; - return m; - } - }), - parse: function(m) { + + class CollectionModel extends Skeletor.Model { + // eslint-disable-next-line class-methods-use-this + parse(m) { + m.name = 'test'; + return m; + } + } + + class Collection extends Skeletor.Collection { + get model() { + return CollectionModel; + } + parse(m) { return m.namespace; } - }); - const collection = new Collection(model, {parse: true}); + } + const collection = new Collection(model, { parse: true }); assert.equal(collection.length, 2); assert.equal(collection.at(0).get('name'), 'test'); }); - QUnit.test('#1407 parse option on reset parses collection and models', function(assert) { + QUnit.test('#1407 parse option on reset parses collection and models', function (assert) { assert.expect(2); const model = { - namespace: [{id: 1}, {id: 2}] + namespace: [{ id: 1 }, { id: 2 }], }; - const Collection = Skeletor.Collection.extend({ - model: Skeletor.Model.extend({ - parse: function(m) { - m.name = 'test'; - return m; - } - }), - parse: function(m) { + + class CModel extends Skeletor.Model { + // eslint-disable-next-line class-methods-use-this + parse(m) { + m.name = 'test'; + return m; + } + } + + class Collection extends Skeletor.Collection { + get model() { + return CModel; + } + parse(m) { return m.namespace; } - }); + } const collection = new Collection(); - collection.reset(model, {parse: true}); + collection.reset(model, { parse: true }); assert.equal(collection.length, 2); assert.equal(collection.at(0).get('name'), 'test'); }); - - QUnit.test('Reset includes previous models in triggered event.', function(assert) { + QUnit.test('Reset includes previous models in triggered event.', function (assert) { assert.expect(1); const model = new Skeletor.Model(); const collection = new Skeletor.Collection([model]); - collection.on('reset', function(coll, options) { + collection.on('reset', function (coll, options) { assert.deepEqual(options.previousModels, [model]); }); collection.reset([]); }); - QUnit.test('set', function(assert) { + QUnit.test('set', function (assert) { const m1 = new Skeletor.Model(); - const m2 = new Skeletor.Model({id: 2}); + const m2 = new Skeletor.Model({ id: 2 }); const m3 = new Skeletor.Model(); const collection = new Skeletor.Collection([m1, m2]); // Test add/change/remove events - collection.on('add', function(model) { + collection.on('add', function (model) { assert.strictEqual(model, m3); }); - collection.on('change', function(model) { + collection.on('change', function (model) { assert.strictEqual(model, m2); }); - collection.on('remove', function(model) { + collection.on('remove', function (model) { assert.strictEqual(model, m1); }); // remove: false doesn't remove any models - collection.set([], {remove: false}); + collection.set([], { remove: false }); assert.strictEqual(collection.length, 2); // add: false doesn't add any models - collection.set([m1, m2, m3], {add: false}); + collection.set([m1, m2, m3], { add: false }); assert.strictEqual(collection.length, 2); // merge: false doesn't change any models - collection.set([m1, {id: 2, a: 1}], {merge: false}); + collection.set([m1, { id: 2, a: 1 }], { merge: false }); assert.strictEqual(m2.get('a'), undefined); // add: false, remove: false only merges existing models - collection.set([m1, {id: 2, a: 0}, m3, {id: 4}], {add: false, remove: false}); + collection.set([m1, { id: 2, a: 0 }, m3, { id: 4 }], { add: false, remove: false }); assert.strictEqual(collection.length, 2); assert.strictEqual(m2.get('a'), 0); // default options add/remove/merge as appropriate - collection.set([{id: 2, a: 1}, m3]); + collection.set([{ id: 2, a: 1 }, m3]); assert.strictEqual(collection.length, 2); assert.strictEqual(m2.get('a'), 1); // Test removing models not passing an argument - collection.off('remove').on('remove', function(model) { + collection.off('remove').on('remove', function (model) { assert.ok(model === m2 || model === m3); }); collection.set([]); @@ -1218,12 +1316,12 @@ // Test null models on set doesn't clear collection collection.off(); - collection.set([{id: 1}]); + collection.set([{ id: 1 }]); collection.set(); assert.strictEqual(collection.length, 1); }); - QUnit.test('set with only cids', function(assert) { + QUnit.test('set with only cids', function (assert) { assert.expect(3); const m1 = new Skeletor.Model(); const m2 = new Skeletor.Model(); @@ -1232,353 +1330,422 @@ assert.equal(collection.length, 2); collection.set([m1]); assert.equal(collection.length, 1); - collection.set([m1, m1, m1, m2, m2], {remove: false}); + collection.set([m1, m1, m1, m2, m2], { remove: false }); assert.equal(collection.length, 2); }); - QUnit.test('set with only idAttribute', function(assert) { + QUnit.test('set with only idAttribute', function (assert) { assert.expect(3); - const m1 = {_id: 1}; - const m2 = {_id: 2}; - const Col = Skeletor.Collection.extend({ - model: Skeletor.Model.extend({ - idAttribute: '_id' - }) - }); + const m1 = { _id: 1 }; + const m2 = { _id: 2 }; + + class CModel extends Skeletor.Model { + // eslint-disable-next-line class-methods-use-this + get idAttribute() { + return '_id'; + } + } + + class Col extends Skeletor.Collection { + get model() { + return CModel; + } + } const collection = new Col(); collection.set([m1, m2]); assert.equal(collection.length, 2); collection.set([m1]); assert.equal(collection.length, 1); - collection.set([m1, m1, m1, m2, m2], {remove: false}); + collection.set([m1, m1, m1, m2, m2], { remove: false }); assert.equal(collection.length, 2); }); - QUnit.test('set + merge with default values defined', function(assert) { - const Model = Skeletor.Model.extend({ - defaults: { - key: 'value' + QUnit.test('set + merge with default values defined', function (assert) { + class Model extends Skeletor.Model { + // eslint-disable-next-line class-methods-use-this + defaults() { + return { + key: 'value', + }; } - }); - const m = new Model({id: 1}); - const collection = new Skeletor.Collection([m], {model: Model}); + } + const m = new Model({ id: 1 }); + const collection = new Skeletor.Collection([m], { model: Model }); assert.equal(collection.first().get('key'), 'value'); - collection.set({id: 1, key: 'other'}); + collection.set({ id: 1, key: 'other' }); assert.equal(collection.first().get('key'), 'other'); - collection.set({id: 1, other: 'value'}); + collection.set({ id: 1, other: 'value' }); assert.equal(collection.first().get('key'), 'other'); assert.equal(collection.length, 1); }); - QUnit.test('merge without mutation', function(assert) { - const Model = Skeletor.Model.extend({ - initialize: function(attrs, options) { + QUnit.test('merge without mutation', function (assert) { + class Model extends Skeletor.Model { + initialize(attrs, options) { if (attrs.child) { this.set('child', new Model(attrs.child, options), options); } } - }); - const Collection = Skeletor.Collection.extend({model: Model}); - const data = [{id: 1, child: {id: 2}}]; + } + class Collection extends Skeletor.Collection { + get model() { + return Model; + } + } + const data = [{ id: 1, child: { id: 2 } }]; const collection = new Collection(data); assert.equal(collection.first().id, 1); collection.set(data); assert.equal(collection.first().id, 1); - collection.set([{id: 2, child: {id: 2}}].concat(data)); + collection.set([{ id: 2, child: { id: 2 } }].concat(data)); assert.deepEqual(collection.pluck('id'), [2, 1]); }); - QUnit.test('`set` and model level `parse`', function(assert) { - const Model = Skeletor.Model.extend({}); - const Collection = Skeletor.Collection.extend({ - model: Model, - parse: function(res) { return _.map(res.models, 'model'); } - }); - const model = new Model({id: 1}); + QUnit.test('`set` and model level `parse`', function (assert) { + class Model extends Skeletor.Model {} + class Collection extends Skeletor.Collection { + get model() { + return Model; + } + parse(res) { + return _.map(res.models, 'model'); + } + } + const model = new Model({ id: 1 }); const collection = new Collection(model); - collection.set({models: [ - {model: {id: 1}}, - {model: {id: 2}} - ]}, {parse: true}); + collection.set({ models: [{ model: { id: 1 } }, { model: { id: 2 } }] }, { parse: true }); assert.equal(collection.first(), model); }); - QUnit.test('`set` data is only parsed once', function(assert) { + QUnit.test('`set` data is only parsed once', function (assert) { const collection = new Skeletor.Collection(); - collection.model = Skeletor.Model.extend({ - parse: function(data) { + + class CModel extends Skeletor.Model { + // eslint-disable-next-line class-methods-use-this + parse(data) { assert.equal(data.parsed, undefined); data.parsed = true; return data; } - }); - collection.set({}, {parse: true}); + } + + collection.model = CModel; + collection.set({}, { parse: true }); }); - QUnit.test('`set` matches input order in the absence of a comparator', function(assert) { - const one = new Skeletor.Model({id: 1}); - const two = new Skeletor.Model({id: 2}); - const three = new Skeletor.Model({id: 3}); + QUnit.test('`set` matches input order in the absence of a comparator', function (assert) { + const one = new Skeletor.Model({ id: 1 }); + const two = new Skeletor.Model({ id: 2 }); + const three = new Skeletor.Model({ id: 3 }); const collection = new Skeletor.Collection([one, two, three]); - collection.set([{id: 3}, {id: 2}, {id: 1}]); + collection.set([{ id: 3 }, { id: 2 }, { id: 1 }]); assert.deepEqual(collection.models, [three, two, one]); - collection.set([{id: 1}, {id: 2}]); + collection.set([{ id: 1 }, { id: 2 }]); assert.deepEqual(collection.models, [one, two]); collection.set([two, three, one]); assert.deepEqual(collection.models, [two, three, one]); - collection.set([{id: 1}, {id: 2}], {remove: false}); + collection.set([{ id: 1 }, { id: 2 }], { remove: false }); assert.deepEqual(collection.models, [two, three, one]); - collection.set([{id: 1}, {id: 2}, {id: 3}], {merge: false}); + collection.set([{ id: 1 }, { id: 2 }, { id: 3 }], { merge: false }); assert.deepEqual(collection.models, [one, two, three]); - collection.set([three, two, one, {id: 4}], {add: false}); + collection.set([three, two, one, { id: 4 }], { add: false }); assert.deepEqual(collection.models, [one, two, three]); }); - QUnit.test('#1894 - Push should not trigger a sort', function(assert) { + QUnit.test('#1894 - Push should not trigger a sort', function (assert) { assert.expect(0); - const Collection = Skeletor.Collection.extend({ - comparator: 'id', - sort: function() { assert.ok(false); } - }); - new Collection().push({id: 1}); + class Collection extends Skeletor.Collection { + get comparator() { + return 'id'; + } + sort() { + assert.ok(false); + } + } + new Collection().push({ id: 1 }); }); - QUnit.test('#2428 - push duplicate models, return the correct one', function(assert) { + QUnit.test('#2428 - push duplicate models, return the correct one', function (assert) { assert.expect(1); const collection = new Skeletor.Collection(); - const model1 = collection.push({id: 101}); - const model2 = collection.push({id: 101}); + const model1 = collection.push({ id: 101 }); + const model2 = collection.push({ id: 101 }); assert.ok(model2.cid === model1.cid); }); - QUnit.test('`set` with non-normal id', function(assert) { - const Collection = Skeletor.Collection.extend({ - model: Skeletor.Model.extend({idAttribute: '_id'}) - }); - const collection = new Collection({_id: 1}); - collection.set([{_id: 1, a: 1}], {add: false}); + QUnit.test('`set` with non-normal id', function (assert) { + class CModel extends Skeletor.Model { + // eslint-disable-next-line class-methods-use-this + get idAttribute() { + return '_id'; + } + } + + class Collection extends Skeletor.Collection { + get model() { + return CModel; + } + } + const collection = new Collection({ _id: 1 }); + collection.set([{ _id: 1, a: 1 }], { add: false }); assert.equal(collection.first().get('a'), 1); }); - QUnit.test('#1894 - `sort` can optionally be turned off', function(assert) { + QUnit.test('#1894 - `sort` can optionally be turned off', function (assert) { assert.expect(0); - const Collection = Skeletor.Collection.extend({ - comparator: 'id', - sort: function() { assert.ok(false); } - }); - new Collection().add({id: 1}, {sort: false}); + class Collection extends Skeletor.Collection { + get comparator() { + return 'id'; + } + sort() { + assert.ok(false); + } + } + new Collection().add({ id: 1 }, { sort: false }); }); - QUnit.test('#1915 - `parse` data in the right order in `set`', function(assert) { - const collection = new (Skeletor.Collection.extend({ - parse: function(data) { + QUnit.test('#1915 - `parse` data in the right order in `set`', function (assert) { + class Col extends Skeletor.Collection { + parse(data) { assert.strictEqual(data.status, 'ok'); return data.data; } - }))(); - const res = {status: 'ok', data: [{id: 1}]}; - collection.set(res, {parse: true}); + } + const collection = new Col(); + const res = { status: 'ok', data: [{ id: 1 }] }; + collection.set(res, { parse: true }); }); - QUnit.test('#1939 - `parse` is passed `options`', function(assert) { - window.fetch.restore() + QUnit.test('#1939 - `parse` is passed `options`', function (assert) { + window.fetch.restore(); sinon.stub(window, 'fetch').callsFake((url, params) => { _.defer(params.success, []); - return {someHeader: 'headerValue'}; + return { someHeader: 'headerValue' }; }); const done = assert.async(); assert.expect(1); - const collection = new (Skeletor.Collection.extend({ - url: '/', - parse: function(data, options) { + class Col extends Skeletor.Collection { + get url() { + return '/'; + } + parse(data, options) { assert.strictEqual(options.xhr.someHeader, 'headerValue'); return data; } - }))(); + } + const collection = new Col(); collection.fetch({ - success: function() { done(); } + success() { + done(); + }, }); }); - QUnit.test('fetch will pass extra options to success callback', function(assert) { + QUnit.test('fetch will pass extra options to success callback', function (assert) { assert.expect(1); - const SpecialSyncCollection = Skeletor.Collection.extend({ - url: '/test', - sync: function(method, collection, options) { - _.extend(options, {specialSync: true}); + class SpecialSyncCollection extends Skeletor.Collection { + get url() { + return '/test'; + } + sync(method, collection, options) { + _.extend(options, { specialSync: true }); return Skeletor.Collection.prototype.sync.call(this, method, collection, options); } - }); + } const collection = new SpecialSyncCollection(); - const onSuccess = function(coll, resp, options) { + const onSuccess = function (coll, resp, options) { assert.ok(options.specialSync, 'Options were passed correctly to callback'); }; - collection.fetch({success: onSuccess}); + collection.fetch({ success: onSuccess }); window.fetch.lastCall.args[1].success(); }); - QUnit.test('`add` only `sort`s when necessary', function(assert) { + QUnit.test('`add` only `sort`s when necessary', function (assert) { assert.expect(2); - const collection = new (Skeletor.Collection.extend({ - comparator: 'a' - }))([{id: 1}, {id: 2}, {id: 3}]); - collection.on('sort', function() { assert.ok(true); }); - collection.add({id: 4}); // do sort, new model - collection.add({id: 1, a: 1}, {merge: true}); // do sort, comparator change - collection.add({id: 1, b: 1}, {merge: true}); // don't sort, no comparator change - collection.add({id: 1, a: 1}, {merge: true}); // don't sort, no comparator change + class Col extends Skeletor.Collection { + get comparator() { + return 'a'; + } + } + const collection = new Col([{ id: 1 }, { id: 2 }, { id: 3 }]); + collection.on('sort', function () { + assert.ok(true); + }); + collection.add({ id: 4 }); // do sort, new model + collection.add({ id: 1, a: 1 }, { merge: true }); // do sort, comparator change + collection.add({ id: 1, b: 1 }, { merge: true }); // don't sort, no comparator change + collection.add({ id: 1, a: 1 }, { merge: true }); // don't sort, no comparator change collection.add(collection.models); // don't sort, nothing new - collection.add(collection.models, {merge: true}); // don't sort + collection.add(collection.models, { merge: true }); // don't sort }); - QUnit.test('`add` only `sort`s when necessary with comparator function', function(assert) { + QUnit.test('`add` only `sort`s when necessary with comparator function', function (assert) { assert.expect(3); - const collection = new (Skeletor.Collection.extend({ - comparator: function(m1, m2) { - return m1.get('a') > m2.get('a') ? 1 : (m1.get('a') < m2.get('a') ? -1 : 0); - } - }))([{id: 1}, {id: 2}, {id: 3}]); - collection.on('sort', function() { assert.ok(true); }); - collection.add({id: 4}); // do sort, new model - collection.add({id: 1, a: 1}, {merge: true}); // do sort, model change - collection.add({id: 1, b: 1}, {merge: true}); // do sort, model change - collection.add({id: 1, a: 1}, {merge: true}); // don't sort, no model change + + class Col extends Skeletor.Collection { + comparator(m1, m2) { + return m1.get('a') > m2.get('a') ? 1 : m1.get('a') < m2.get('a') ? -1 : 0; + } + } + const collection = new Col([{ id: 1 }, { id: 2 }, { id: 3 }]); + collection.on('sort', function () { + assert.ok(true); + }); + collection.add({ id: 4 }); // do sort, new model + collection.add({ id: 1, a: 1 }, { merge: true }); // do sort, model change + collection.add({ id: 1, b: 1 }, { merge: true }); // do sort, model change + collection.add({ id: 1, a: 1 }, { merge: true }); // don't sort, no model change collection.add(collection.models); // don't sort, nothing new - collection.add(collection.models, {merge: true}); // don't sort + collection.add(collection.models, { merge: true }); // don't sort }); - QUnit.test('Attach options to collection.', function(assert) { + QUnit.test('Attach options to collection.', function (assert) { assert.expect(2); const Model = Skeletor.Model; - const comparator = function(){}; + const comparator = function () {}; const collection = new Skeletor.Collection([], { model: Model, - comparator: comparator + comparator: comparator, }); assert.ok(collection.model === Model); assert.ok(collection.comparator === comparator); }); - QUnit.test('Pass falsey for `models` for empty Col with `options`', function(assert) { + QUnit.test('Pass falsey for `models` for empty Col with `options`', function (assert) { assert.expect(9); - const opts = {a: 1, b: 2}; - _.forEach([undefined, null, false], function(falsey) { - const Collection = Skeletor.Collection.extend({ - initialize: function(models, options) { + const opts = { a: 1, b: 2 }; + _.forEach([undefined, null, false], function (falsey) { + class Collection extends Skeletor.Collection { + initialize(models, options) { assert.strictEqual(models, falsey); assert.strictEqual(options, opts); } - }); + } const collection = new Collection(falsey, opts); assert.strictEqual(collection.length, 0); }); }); - QUnit.test('`add` overrides `set` flags', function(assert) { + QUnit.test('`add` overrides `set` flags', function (assert) { const collection = new Skeletor.Collection(); - collection.once('add', function(model, coll, options) { - coll.add({id: 2}, options); + collection.once('add', function (model, coll, options) { + coll.add({ id: 2 }, options); }); - collection.set({id: 1}); + collection.set({ id: 1 }); assert.equal(collection.length, 2); }); - QUnit.test('#2606 - Collection#create, success arguments', function(assert) { + QUnit.test('#2606 - Collection#create, success arguments', function (assert) { assert.expect(1); const collection = new Skeletor.Collection(); collection.url = 'test'; - collection.create({}, { - success: function(model, resp, options) { - assert.strictEqual(resp, 'response'); - } - }); + collection.create( + {}, + { + success(model, resp, options) { + assert.strictEqual(resp, 'response'); + }, + }, + ); window.fetch.lastCall.args[1].success('response'); }); - QUnit.test('#2612 - nested `parse` works with `Collection#set`', function(assert) { + QUnit.test('#2612 - nested `parse` works with `Collection#set`', function (assert) { + class Job extends Skeletor.Model { + get items() { + if (!this._items) { + this._items = new Items(); + } + return this._items; + } - const Job = Skeletor.Model.extend({ - constructor: function() { - this.items = new Items(); - Skeletor.Model.apply(this, arguments); - }, - parse: function(attrs) { - this.items.set(attrs.items, {parse: true}); + parse(attrs) { + this.items.set(attrs.items, { parse: true }); return _.omit(attrs, 'items'); } - }); + } - const Item = Skeletor.Model.extend({ - constructor: function() { - this.subItems = new Skeletor.Collection(); - Skeletor.Model.apply(this, arguments); - }, - parse: function(attrs) { - this.subItems.set(attrs.subItems, {parse: true}); + class Item extends Skeletor.Model { + get subItems() { + if (!this._subItems) { + this._subItems = new Items(); + } + return this._subItems; + } + parse(attrs) { + this.subItems.set(attrs.subItems, { parse: true }); return _.omit(attrs, 'subItems'); } - }); + } - const Items = Skeletor.Collection.extend({ - model: Item - }); + class Items extends Skeletor.Collection { + get model() { + return Item; + } + } const data = { name: 'JobName', id: 1, - items: [{ - id: 1, - name: 'Sub1', - subItems: [ - {id: 1, subName: 'One'}, - {id: 2, subName: 'Two'} - ] - }, { - id: 2, - name: 'Sub2', - subItems: [ - {id: 3, subName: 'Three'}, - {id: 4, subName: 'Four'} - ] - }] + items: [ + { + id: 1, + name: 'Sub1', + subItems: [ + { id: 1, subName: 'One' }, + { id: 2, subName: 'Two' }, + ], + }, + { + id: 2, + name: 'Sub2', + subItems: [ + { id: 3, subName: 'Three' }, + { id: 4, subName: 'Four' }, + ], + }, + ], }; const newData = { name: 'NewJobName', id: 1, - items: [{ - id: 1, - name: 'NewSub1', - subItems: [ - {id: 1, subName: 'NewOne'}, - {id: 2, subName: 'NewTwo'} - ] - }, { - id: 2, - name: 'NewSub2', - subItems: [ - {id: 3, subName: 'NewThree'}, - {id: 4, subName: 'NewFour'} - ] - }] + items: [ + { + id: 1, + name: 'NewSub1', + subItems: [ + { id: 1, subName: 'NewOne' }, + { id: 2, subName: 'NewTwo' }, + ], + }, + { + id: 2, + name: 'NewSub2', + subItems: [ + { id: 3, subName: 'NewThree' }, + { id: 4, subName: 'NewFour' }, + ], + }, + ], }; - const job = new Job(data, {parse: true}); + const job = new Job(data, { parse: true }); assert.equal(job.get('name'), 'JobName'); assert.equal(job.items.at(0).get('name'), 'Sub1'); assert.equal(job.items.length, 2); assert.equal(job.items.get(1).subItems.get(1).get('subName'), 'One'); assert.equal(job.items.get(2).subItems.get(3).get('subName'), 'Three'); - job.set(job.parse(newData, {parse: true})); + job.set(job.parse(newData, { parse: true })); assert.equal(job.get('name'), 'NewJobName'); assert.equal(job.items.at(0).get('name'), 'NewSub1'); assert.equal(job.items.length, 2); @@ -1586,164 +1753,147 @@ assert.equal(job.items.get(2).subItems.get(3).get('subName'), 'NewThree'); }); - QUnit.test('_addReference binds all collection events & adds to the lookup hashes', function(assert) { + QUnit.test('_addReference binds all collection events & adds to the lookup hashes', function (assert) { assert.expect(8); - const calls = {add: 0, remove: 0}; - - const Collection = Skeletor.Collection.extend({ + const calls = { add: 0, remove: 0 }; - _addReference: function(model) { + class Collection extends Skeletor.Collection { + _addReference(model) { Skeletor.Collection.prototype._addReference.apply(this, arguments); calls.add++; assert.equal(model, this._byId[model.id]); assert.equal(model, this._byId[model.cid]); assert.equal(model._events.all.length, 1); - }, + } - _removeReference: function(model) { + _removeReference(model) { Skeletor.Collection.prototype._removeReference.apply(this, arguments); calls.remove++; assert.equal(this._byId[model.id], undefined); assert.equal(this._byId[model.cid], undefined); assert.equal(model.collection, undefined); } - - }); + } const collection = new Collection(); - const model = collection.add({id: 1}); + const model = collection.add({ id: 1 }); collection.remove(model); assert.equal(calls.add, 1); assert.equal(calls.remove, 1); }); - QUnit.test('Do not allow duplicate models to be `add`ed or `set`', function(assert) { + QUnit.test('Do not allow duplicate models to be `add`ed or `set`', function (assert) { const collection = new Skeletor.Collection(); - collection.add([{id: 1}, {id: 1}]); + collection.add([{ id: 1 }, { id: 1 }]); assert.equal(collection.length, 1); assert.equal(collection.models.length, 1); - collection.set([{id: 1}, {id: 1}]); + collection.set([{ id: 1 }, { id: 1 }]); assert.equal(collection.length, 1); assert.equal(collection.models.length, 1); }); - QUnit.test('#3020: #set with {add: false} should not throw.', function(assert) { + QUnit.test('#3020: #set with {add: false} should not throw.', function (assert) { assert.expect(2); const collection = new Skeletor.Collection(); - collection.set([{id: 1}], {add: false}); + collection.set([{ id: 1 }], { add: false }); assert.strictEqual(collection.length, 0); assert.strictEqual(collection.models.length, 0); }); - QUnit.test('create with wait, model instance, #3028', function(assert) { + QUnit.test('create with wait, model instance, #3028', function (assert) { assert.expect(1); const collection = new Skeletor.Collection(); - const model = new Skeletor.Model({id: 1}); - model.sync = function(){ + const model = new Skeletor.Model({ id: 1 }); + model.sync = function () { assert.equal(this.collection, collection); }; - collection.create(model, {wait: true}); + collection.create(model, { wait: true }); }); - QUnit.test('modelId', function(assert) { - const Stooge = Skeletor.Model.extend(); - const StoogeCollection = Skeletor.Collection.extend({model: Stooge}); + QUnit.test('modelId', function (assert) { + class Stooge extends Skeletor.Model {} + class StoogeCollection extends Skeletor.Collection { + get model() { + return Stooge; + } + } // Default to using `Collection::model::idAttribute`. - assert.equal(StoogeCollection.prototype.modelId({id: 1}), 1); - Stooge.prototype.idAttribute = '_id'; - assert.equal(StoogeCollection.prototype.modelId({_id: 1}), 1); - }); + assert.equal(StoogeCollection.prototype.modelId({ id: 1 }), 1); - QUnit.test('Polymorphic models work with "simple" constructors', function(assert) { - const A = Skeletor.Model.extend(); - const B = Skeletor.Model.extend(); - const C = Skeletor.Collection.extend({ - model: function(attrs) { - return attrs.type === 'a' ? new A(attrs) : new B(attrs); + class Foo extends Skeletor.Model { + get idAttribute() { + return '_id'; } - }); - const collection = new C([{id: 1, type: 'a'}, {id: 2, type: 'b'}]); - assert.equal(collection.length, 2); - assert.ok(collection.at(0) instanceof A); - assert.equal(collection.at(0).id, 1); - assert.ok(collection.at(1) instanceof B); - assert.equal(collection.at(1).id, 2); + } + class FooCollection extends Skeletor.Collection { + get model() { + return Foo; + } + } + assert.equal(FooCollection.prototype.modelId({ _id: 1 }), 1); }); - QUnit.test('Polymorphic models work with "advanced" constructors', function(assert) { - const A = Skeletor.Model.extend({idAttribute: '_id'}); - const B = Skeletor.Model.extend({idAttribute: '_id'}); - let C = Skeletor.Collection.extend({ - model: Skeletor.Model.extend({ - constructor: function(attrs) { - return attrs.type === 'a' ? new A(attrs) : new B(attrs); - }, + QUnit.test('Polymorphic models work with "simple" constructors', function (assert) { + class A extends Skeletor.Model {} + class B extends Skeletor.Model {} - idAttribute: '_id' - }) - }); - let collection = new C([{_id: 1, type: 'a'}, {_id: 2, type: 'b'}]); - assert.equal(collection.length, 2); - assert.ok(collection.at(0) instanceof A); - assert.equal(collection.at(0), collection.get(1)); - assert.ok(collection.at(1) instanceof B); - assert.equal(collection.at(1), collection.get(2)); - - C = Skeletor.Collection.extend({ - model: function(attrs) { + class C extends Skeletor.Collection { + createModel(attrs) { return attrs.type === 'a' ? new A(attrs) : new B(attrs); - }, - - modelId: function(attrs) { - return attrs.type + '-' + attrs.id; } - }); - collection = new C([{id: 1, type: 'a'}, {id: 1, type: 'b'}]); + } + + const collection = new C([ + { id: 1, type: 'a' }, + { id: 2, type: 'b' }, + ]); + assert.equal(collection.length, 2); assert.ok(collection.at(0) instanceof A); - assert.equal(collection.at(0), collection.get('a-1')); + assert.equal(collection.at(0).id, 1); assert.ok(collection.at(1) instanceof B); - assert.equal(collection.at(1), collection.get('b-1')); + assert.equal(collection.at(1).id, 2); }); - QUnit.test('Collection with polymorphic models receives default id from modelId', function(assert) { + QUnit.test('Collection with polymorphic models receives default id from modelId', function (assert) { assert.expect(6); // When the polymorphic models use 'id' for the idAttribute, all is fine. - const C1 = Skeletor.Collection.extend({ - model: function(attrs) { + class C1 extends Skeletor.Collection { + createModel(attrs) { return new Skeletor.Model(attrs); } - }); - const c1 = new C1({id: 1}); + } + const c1 = new C1({ id: 1 }); assert.equal(c1.get(1).id, 1); - assert.equal(c1.modelId({id: 1}), 1); + assert.equal(c1.modelId({ id: 1 }), 1); // If the polymorphic models define their own idAttribute, // the modelId method should be overridden, for the reason below. - const M = Skeletor.Model.extend({ - idAttribute: '_id' - }); - const C2 = Skeletor.Collection.extend({ - model: function(attrs) { + class M extends Skeletor.Model { + get idAttribute() { + return '_id'; + } + } + class C2 extends Skeletor.Collection { + createModel(attrs) { return new M(attrs); } - }); - const c2 = new C2({_id: 1}); + } + const c2 = new C2({ _id: 1 }); assert.equal(c2.get(1), undefined); assert.equal(c2.modelId(c2.at(0).attributes), undefined); - const m = new M({_id: 2}); + const m = new M({ _id: 2 }); c2.add(m); assert.equal(c2.get(2), undefined); assert.equal(c2.modelId(m.attributes), undefined); }); - QUnit.test('Collection implements Iterable, values is default iterator function', function(assert) { - /* global Symbol */ + QUnit.test('Collection implements Iterable, values is default iterator function', function (assert) { const $$iterator = typeof Symbol === 'function' && Symbol.iterator; // This test only applies to environments which define Symbol.iterator. if (!$$iterator) { @@ -1754,14 +1904,14 @@ const collection = new Skeletor.Collection([]); assert.strictEqual(collection[$$iterator], collection.values); const iterator = collection[$$iterator](); - assert.deepEqual(iterator.next(), {value: undefined, done: true}); + assert.deepEqual(iterator.next(), { value: undefined, done: true }); }); - QUnit.test('Collection.values iterates models in sorted order', function(assert) { + QUnit.test('Collection.values iterates models in sorted order', function (assert) { assert.expect(4); - const one = new Skeletor.Model({id: 1}); - const two = new Skeletor.Model({id: 2}); - const three = new Skeletor.Model({id: 3}); + const one = new Skeletor.Model({ id: 1 }); + const two = new Skeletor.Model({ id: 2 }); + const three = new Skeletor.Model({ id: 3 }); const collection = new Skeletor.Collection([one, two, three]); const iterator = collection.values(); assert.strictEqual(iterator.next().value, one); @@ -1770,11 +1920,11 @@ assert.strictEqual(iterator.next().value, undefined); }); - QUnit.test('Collection.keys iterates ids in sorted order', function(assert) { + QUnit.test('Collection.keys iterates ids in sorted order', function (assert) { assert.expect(4); - const one = new Skeletor.Model({id: 1}); - const two = new Skeletor.Model({id: 2}); - const three = new Skeletor.Model({id: 3}); + const one = new Skeletor.Model({ id: 1 }); + const two = new Skeletor.Model({ id: 2 }); + const three = new Skeletor.Model({ id: 3 }); const collection = new Skeletor.Collection([one, two, three]); const iterator = collection.keys(); assert.strictEqual(iterator.next().value, 1); @@ -1783,11 +1933,11 @@ assert.strictEqual(iterator.next().value, undefined); }); - QUnit.test('Collection.entries iterates ids and models in sorted order', function(assert) { + QUnit.test('Collection.entries iterates ids and models in sorted order', function (assert) { assert.expect(4); - const one = new Skeletor.Model({id: 1}); - const two = new Skeletor.Model({id: 2}); - const three = new Skeletor.Model({id: 3}); + const one = new Skeletor.Model({ id: 1 }); + const two = new Skeletor.Model({ id: 2 }); + const three = new Skeletor.Model({ id: 3 }); const collection = new Skeletor.Collection([one, two, three]); const iterator = collection.entries(); assert.deepEqual(iterator.next().value, [1, one]); @@ -1796,137 +1946,148 @@ assert.strictEqual(iterator.next().value, undefined); }); - QUnit.test('#3039 #3951: adding at index fires with correct at', function(assert) { + QUnit.test('#3039 #3951: adding at index fires with correct at', function (assert) { assert.expect(4); - const collection = new Skeletor.Collection([{val: 0}, {val: 4}]); - collection.on('add', function(model, coll, options) { + const collection = new Skeletor.Collection([{ val: 0 }, { val: 4 }]); + collection.on('add', function (model, coll, options) { assert.equal(model.get('val'), options.index); }); - collection.add([{val: 1}, {val: 2}, {val: 3}], {at: 1}); - collection.add({val: 5}, {at: 10}); + collection.add([{ val: 1 }, { val: 2 }, { val: 3 }], { at: 1 }); + collection.add({ val: 5 }, { at: 10 }); }); - QUnit.test('#3039: index is not sent when at is not specified', function(assert) { + QUnit.test('#3039: index is not sent when at is not specified', function (assert) { assert.expect(2); - const collection = new Skeletor.Collection([{at: 0}]); - collection.on('add', function(model, coll, options) { + const collection = new Skeletor.Collection([{ at: 0 }]); + collection.on('add', function (model, coll, options) { assert.equal(undefined, options.index); }); - collection.add([{at: 1}, {at: 2}]); + collection.add([{ at: 1 }, { at: 2 }]); }); - QUnit.test('#3199 - Order changing should trigger a sort', function(assert) { + QUnit.test('#3199 - Order changing should trigger a sort', function (assert) { assert.expect(1); - const one = new Skeletor.Model({id: 1}); - const two = new Skeletor.Model({id: 2}); - const three = new Skeletor.Model({id: 3}); + const one = new Skeletor.Model({ id: 1 }); + const two = new Skeletor.Model({ id: 2 }); + const three = new Skeletor.Model({ id: 3 }); const collection = new Skeletor.Collection([one, two, three]); - collection.on('sort', function() { + collection.on('sort', function () { assert.ok(true); }); - collection.set([{id: 3}, {id: 2}, {id: 1}]); + collection.set([{ id: 3 }, { id: 2 }, { id: 1 }]); }); - QUnit.test('#3199 - Adding a model should trigger a sort', function(assert) { + QUnit.test('#3199 - Adding a model should trigger a sort', function (assert) { assert.expect(1); - const one = new Skeletor.Model({id: 1}); - const two = new Skeletor.Model({id: 2}); - const three = new Skeletor.Model({id: 3}); + const one = new Skeletor.Model({ id: 1 }); + const two = new Skeletor.Model({ id: 2 }); + const three = new Skeletor.Model({ id: 3 }); const collection = new Skeletor.Collection([one, two, three]); - collection.on('sort', function() { + collection.on('sort', function () { assert.ok(true); }); - collection.set([{id: 1}, {id: 2}, {id: 3}, {id: 0}]); + collection.set([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 0 }]); }); - QUnit.test('#3199 - Order not changing should not trigger a sort', function(assert) { + QUnit.test('#3199 - Order not changing should not trigger a sort', function (assert) { assert.expect(0); - const one = new Skeletor.Model({id: 1}); - const two = new Skeletor.Model({id: 2}); - const three = new Skeletor.Model({id: 3}); + const one = new Skeletor.Model({ id: 1 }); + const two = new Skeletor.Model({ id: 2 }); + const three = new Skeletor.Model({ id: 3 }); const collection = new Skeletor.Collection([one, two, three]); - collection.on('sort', function() { + collection.on('sort', function () { assert.ok(false); }); - collection.set([{id: 1}, {id: 2}, {id: 3}]); + collection.set([{ id: 1 }, { id: 2 }, { id: 3 }]); }); - QUnit.test('add supports negative indexes', function(assert) { - assert.expect(1); - const collection = new Skeletor.Collection([{id: 1}]); - collection.add([{id: 2}, {id: 3}], {at: -1}); - collection.add([{id: 2.5}], {at: -2}); - collection.add([{id: 0.5}], {at: -6}); + QUnit.test('add supports negative indexes', function (assert) { + assert.expect(3); + const collection = new Skeletor.Collection([{ id: 1 }]); + collection.add([{ id: 2 }, { id: 3 }], { at: -1 }); + assert.equal(collection.pluck('id').join(','), '1,2,3'); + collection.add([{ id: 2.5 }], { at: -2 }); + assert.equal(collection.pluck('id').join(','), '1,2,2.5,3'); + collection.add([{ id: 0.5 }], { at: -6 }); assert.equal(collection.pluck('id').join(','), '0.5,1,2,2.5,3'); }); - QUnit.test('#set accepts options.at as a string', function(assert) { + QUnit.test('#set accepts options.at as a string', function (assert) { assert.expect(1); - const collection = new Skeletor.Collection([{id: 1}, {id: 2}]); - collection.add([{id: 3}], {at: '1'}); + const collection = new Skeletor.Collection([{ id: 1 }, { id: 2 }]); + collection.add([{ id: 3 }], { at: '1' }); assert.deepEqual(collection.pluck('id'), [1, 3, 2]); }); - QUnit.test('adding multiple models triggers `update` event once', function(assert) { + QUnit.test('adding multiple models triggers `update` event once', function (assert) { assert.expect(1); const collection = new Skeletor.Collection(); - collection.on('update', function() { assert.ok(true); }); - collection.add([{id: 1}, {id: 2}, {id: 3}]); + collection.on('update', function () { + assert.ok(true); + }); + collection.add([{ id: 1 }, { id: 2 }, { id: 3 }]); }); - QUnit.test('removing models triggers `update` event once', function(assert) { + QUnit.test('removing models triggers `update` event once', function (assert) { assert.expect(1); - const collection = new Skeletor.Collection([{id: 1}, {id: 2}, {id: 3}]); - collection.on('update', function() { assert.ok(true); }); - collection.remove([{id: 1}, {id: 2}]); + const collection = new Skeletor.Collection([{ id: 1 }, { id: 2 }, { id: 3 }]); + collection.on('update', function () { + assert.ok(true); + }); + collection.remove([{ id: 1 }, { id: 2 }]); }); - QUnit.test('remove does not trigger `update` when nothing removed', function(assert) { + QUnit.test('remove does not trigger `update` when nothing removed', function (assert) { assert.expect(0); - const collection = new Skeletor.Collection([{id: 1}, {id: 2}]); - collection.on('update', function() { assert.ok(false); }); - collection.remove([{id: 3}]); + const collection = new Skeletor.Collection([{ id: 1 }, { id: 2 }]); + collection.on('update', function () { + assert.ok(false); + }); + collection.remove([{ id: 3 }]); }); - QUnit.test('set triggers `set` event once', function(assert) { + QUnit.test('set triggers `set` event once', function (assert) { assert.expect(1); - const collection = new Skeletor.Collection([{id: 1}, {id: 2}]); - collection.on('update', function() { assert.ok(true); }); - collection.set([{id: 1}, {id: 3}]); + const collection = new Skeletor.Collection([{ id: 1 }, { id: 2 }]); + collection.on('update', function () { + assert.ok(true); + }); + collection.set([{ id: 1 }, { id: 3 }]); }); - QUnit.test('set does not trigger `update` event when nothing added nor removed', function(assert) { - const collection = new Skeletor.Collection([{id: 1}, {id: 2}]); - collection.on('update', function(coll, options) { + QUnit.test('set does not trigger `update` event when nothing added nor removed', function (assert) { + const collection = new Skeletor.Collection([{ id: 1 }, { id: 2 }]); + collection.on('update', function (coll, options) { assert.equal(options.changes.added.length, 0); assert.equal(options.changes.removed.length, 0); assert.equal(options.changes.merged.length, 2); }); - collection.set([{id: 1}, {id: 2}]); + collection.set([{ id: 1 }, { id: 2 }]); }); - QUnit.test('#3662 - triggering change without model will not error', function(assert) { + QUnit.test('#3662 - triggering change without model will not error', function (assert) { assert.expect(1); - const collection = new Skeletor.Collection([{id: 1}]); + const collection = new Skeletor.Collection([{ id: 1 }]); const model = collection.first(); - collection.on('change', function(m) { + collection.on('change', function (m) { assert.equal(m, undefined); }); model.trigger('change'); }); - QUnit.test('#3871 - falsy parse result creates empty collection', function(assert) { - const collection = new (Skeletor.Collection.extend({ - parse: function(data, options) {} - }))(); - collection.set('', {parse: true}); + QUnit.test('#3871 - falsy parse result creates empty collection', function (assert) { + class Col extends Skeletor.Collection { + parse(data, options) {} + } + const collection = new Col(); + collection.set('', { parse: true }); assert.equal(collection.length, 0); }); - QUnit.test("#3711 - remove's `update` event returns one removed model", function(assert) { - const model = new Skeletor.Model({id: 1, title: 'First Post'}); + QUnit.test("#3711 - remove's `update` event returns one removed model", function (assert) { + const model = new Skeletor.Model({ id: 1, title: 'First Post' }); const collection = new Skeletor.Collection([model]); - collection.on('update', function(context, options) { + collection.on('update', function (context, options) { const changed = options.changes; assert.deepEqual(changed.added, []); assert.deepEqual(changed.merged, []); @@ -1935,11 +2096,11 @@ collection.remove(model); }); - QUnit.test("#3711 - remove's `update` event returns multiple removed models", function(assert) { - const model = new Skeletor.Model({id: 1, title: 'First Post'}); - const model2 = new Skeletor.Model({id: 2, title: 'Second Post'}); + QUnit.test("#3711 - remove's `update` event returns multiple removed models", function (assert) { + const model = new Skeletor.Model({ id: 1, title: 'First Post' }); + const model2 = new Skeletor.Model({ id: 2, title: 'Second Post' }); const collection = new Skeletor.Collection([model, model2]); - collection.on('update', function(context, options) { + collection.on('update', function (context, options) { const changed = options.changes; assert.deepEqual(changed.added, []); assert.deepEqual(changed.merged, []); @@ -1950,10 +2111,10 @@ collection.remove([model, model2]); }); - QUnit.test("#3711 - set's `update` event returns one added model", function(assert) { - const model = new Skeletor.Model({id: 1, title: 'First Post'}); + QUnit.test("#3711 - set's `update` event returns one added model", function (assert) { + const model = new Skeletor.Model({ id: 1, title: 'First Post' }); const collection = new Skeletor.Collection(); - collection.on('update', function(context, options) { + collection.on('update', function (context, options) { const addedModels = options.changes.added; assert.ok(addedModels.length === 1); assert.strictEqual(addedModels[0], model); @@ -1961,11 +2122,11 @@ collection.set(model); }); - QUnit.test("#3711 - set's `update` event returns multiple added models", function(assert) { - const model = new Skeletor.Model({id: 1, title: 'First Post'}); - const model2 = new Skeletor.Model({id: 2, title: 'Second Post'}); + QUnit.test("#3711 - set's `update` event returns multiple added models", function (assert) { + const model = new Skeletor.Model({ id: 1, title: 'First Post' }); + const model2 = new Skeletor.Model({ id: 2, title: 'Second Post' }); const collection = new Skeletor.Collection(); - collection.on('update', function(context, options) { + collection.on('update', function (context, options) { const addedModels = options.changes.added; assert.ok(addedModels.length === 2); assert.strictEqual(addedModels[0], model); @@ -1974,12 +2135,12 @@ collection.set([model, model2]); }); - QUnit.test("#3711 - set's `update` event returns one removed model", function(assert) { - const model = new Skeletor.Model({id: 1, title: 'First Post'}); - const model2 = new Skeletor.Model({id: 2, title: 'Second Post'}); - const model3 = new Skeletor.Model({id: 3, title: 'My Last Post'}); + QUnit.test("#3711 - set's `update` event returns one removed model", function (assert) { + const model = new Skeletor.Model({ id: 1, title: 'First Post' }); + const model2 = new Skeletor.Model({ id: 2, title: 'Second Post' }); + const model3 = new Skeletor.Model({ id: 3, title: 'My Last Post' }); const collection = new Skeletor.Collection([model]); - collection.on('update', function(context, options) { + collection.on('update', function (context, options) { const changed = options.changes; assert.equal(changed.added.length, 2); assert.equal(changed.merged.length, 0); @@ -1989,12 +2150,12 @@ collection.set([model2, model3]); }); - QUnit.test("#3711 - set's `update` event returns multiple removed models", function(assert) { - const model = new Skeletor.Model({id: 1, title: 'First Post'}); - const model2 = new Skeletor.Model({id: 2, title: 'Second Post'}); - const model3 = new Skeletor.Model({id: 3, title: 'My Last Post'}); + QUnit.test("#3711 - set's `update` event returns multiple removed models", function (assert) { + const model = new Skeletor.Model({ id: 1, title: 'First Post' }); + const model2 = new Skeletor.Model({ id: 2, title: 'Second Post' }); + const model3 = new Skeletor.Model({ id: 3, title: 'My Last Post' }); const collection = new Skeletor.Collection([model, model2]); - collection.on('update', function(context, options) { + collection.on('update', function (context, options) { const removedModels = options.changes.removed; assert.ok(removedModels.length === 2); assert.strictEqual(removedModels[0], model); @@ -2003,12 +2164,12 @@ collection.set([model3]); }); - QUnit.test("#3711 - set's `update` event returns one merged model", function(assert) { - const model = new Skeletor.Model({id: 1, title: 'First Post'}); - const model2 = new Skeletor.Model({id: 2, title: 'Second Post'}); - const model2Update = new Skeletor.Model({id: 2, title: 'Second Post V2'}); + QUnit.test("#3711 - set's `update` event returns one merged model", function (assert) { + const model = new Skeletor.Model({ id: 1, title: 'First Post' }); + const model2 = new Skeletor.Model({ id: 2, title: 'Second Post' }); + const model2Update = new Skeletor.Model({ id: 2, title: 'Second Post V2' }); const collection = new Skeletor.Collection([model, model2]); - collection.on('update', function(context, options) { + collection.on('update', function (context, options) { const mergedModels = options.changes.merged; assert.ok(mergedModels.length === 1); assert.strictEqual(mergedModels[0].get('title'), model2Update.get('title')); @@ -2016,13 +2177,13 @@ collection.set([model2Update]); }); - QUnit.test("#3711 - set's `update` event returns multiple merged models", function(assert) { - const model = new Skeletor.Model({id: 1, title: 'First Post'}); - const modelUpdate = new Skeletor.Model({id: 1, title: 'First Post V2'}); - const model2 = new Skeletor.Model({id: 2, title: 'Second Post'}); - const model2Update = new Skeletor.Model({id: 2, title: 'Second Post V2'}); + QUnit.test("#3711 - set's `update` event returns multiple merged models", function (assert) { + const model = new Skeletor.Model({ id: 1, title: 'First Post' }); + const modelUpdate = new Skeletor.Model({ id: 1, title: 'First Post V2' }); + const model2 = new Skeletor.Model({ id: 2, title: 'Second Post' }); + const model2Update = new Skeletor.Model({ id: 2, title: 'Second Post V2' }); const collection = new Skeletor.Collection([model, model2]); - collection.on('update', function(context, options) { + collection.on('update', function (context, options) { const mergedModels = options.changes.merged; assert.ok(mergedModels.length === 2); assert.strictEqual(mergedModels[0].get('title'), model2Update.get('title')); @@ -2031,19 +2192,22 @@ collection.set([model2Update, modelUpdate]); }); - QUnit.test("#3711 - set's `update` event should not be triggered adding a model which already exists exactly alike", function(assert) { - let fired = false; - const model = new Skeletor.Model({id: 1, title: 'First Post'}); - const collection = new Skeletor.Collection([model]); - collection.on('update', function(context, options) { - fired = true; - }); - collection.set([model]); - assert.equal(fired, false); - }); + QUnit.test( + "#3711 - set's `update` event should not be triggered adding a model which already exists exactly alike", + function (assert) { + let fired = false; + const model = new Skeletor.Model({ id: 1, title: 'First Post' }); + const collection = new Skeletor.Collection([model]); + collection.on('update', function (context, options) { + fired = true; + }); + collection.set([model]); + assert.equal(fired, false); + }, + ); - QUnit.test('get models with `attributes` key', function(assert) { - const model = {id: 1, attributes: {}}; + QUnit.test('get models with `attributes` key', function (assert) { + const model = { id: 1, attributes: {} }; const collection = new Skeletor.Collection([model]); assert.ok(collection.get(model)); }); diff --git a/test/indexeddb.test.js b/test/indexeddb.test.js index 67babe9a..87804c41 100644 --- a/test/indexeddb.test.js +++ b/test/indexeddb.test.js @@ -1,91 +1,101 @@ -import * as localForage from "localforage"; -import { Collection } from "../src/collection"; +/* eslint-disable class-methods-use-this */ +import * as localForage from 'localforage'; +import { Collection } from '../src/collection'; import { Model } from '../src/model.js'; import { expect } from 'chai'; import Storage from '../src/storage.js'; +describe('Collection using IndexedDB', function () { + class TestCollection extends Collection { + get browserStorage() { + return new Storage('Collection', 'indexed'); + } + get model() { + return Model; + } + } -describe('Collection using IndexedDB', function() { + it('saves to localForage', async function () { + const collection = new TestCollection(); + await new Promise((resolve, reject) => collection.fetch({ success: () => resolve() })); + const model = await new Promise((resolve, reject) => + collection.create({ 'hello': 'world!' }, { 'success': resolve }), + ); + const id = model.get('id'); + expect(id).to.be.a('string'); + expect(model.get('hello')).to.equal('world!'); + }); - const TestCollection = Collection.extend({ - 'browserStorage': new Storage('Collection', 'indexed'), - 'model': Model - }); + it('removes from localForage', async function () { + const collection = new TestCollection(); + const model = await new Promise((resolve, reject) => + collection.create({ 'hello': 'world!' }, { 'success': resolve }), + ); + const store = model.collection.browserStorage; + const stored_model = await localForage.getItem(store.getItemName(model.id)); + expect(stored_model).to.deep.equal(model.attributes); + expect(collection.length).to.equal(1); - it('saves to localForage', async function () { - const collection = new TestCollection(); - await new Promise((resolve, reject) => collection.fetch({success: () => resolve()})); - const model = await new Promise((resolve, reject) => collection.create({'hello': 'world!'}, {'success': resolve})); - const id = model.get('id'); - expect(id).to.be.a('string'); - expect(model.get('hello')).to.equal('world!') - }); + const stored_collection = await localForage.getItem(store.name); + await new Promise((resolve, reject) => collection.get(model.id).destroy({ 'success': resolve })); + expect(collection.length).to.equal(0); + expect(await localForage.getItem(store.getItemName(model.id))).to.be.null; - it('removes from localForage', async function () { - const collection = new TestCollection(); - const model = await new Promise((resolve, reject) => collection.create({'hello': 'world!'}, {'success': resolve})); - const store = model.collection.browserStorage; - const stored_model = await localForage.getItem(store.getItemName(model.id)); - expect(stored_model).to.deep.equal(model.attributes); - expect(collection.length).to.equal(1); - - const stored_collection = await localForage.getItem(store.name); - await new Promise((resolve, reject) => collection.get(model.id).destroy({'success': resolve})); - expect(collection.length).to.equal(0); - expect(await localForage.getItem(store.getItemName(model.id))).to.be.null; - - // expect collection references to be reset - const stored_collection2 = await localForage.getItem(store.name); - expect(stored_collection2.length).to.equal(stored_collection.length - 1); - }); + // expect collection references to be reset + const stored_collection2 = await localForage.getItem(store.name); + expect(stored_collection2.length).to.equal(stored_collection.length - 1); + }); }); describe('Model using IndexedDB', function () { + class TestModel extends Model { + constructor() { + super(); + this.browserStorage = new Storage('Model', 'indexed'); + } + } - const TestModel = Model.extend({ - 'browserStorage': new Storage('Model', 'indexed'), + describe('Model flow', function () { + it('saves to localForage', async function () { + let model = new TestModel(); + try { + model = await new Promise((resolve, reject) => model.save({ 'hello': 'world!' }, { 'success': resolve })); + } catch (e) { + console.error(e); + } + expect(model.id).to.be.a('string'); + expect(model.get('hello')).to.equal('world!'); }); - describe('Model flow', function () { - - it('saves to localForage', async function () { - let model = new TestModel(); - try { - model = await new Promise((resolve, reject) => model.save({'hello': 'world!'}, {'success': resolve})); - } catch (e) { - console.error(e); - } - expect(model.id).to.be.a('string'); - expect(model.get('hello')).to.equal('world!'); - }); - - it('fetches from localForage', async function () { - const model = new TestModel(); - await new Promise((resolve, reject) => model.save({'hello': 'world!'}, {'success': resolve})); - await new Promise((resolve, reject) => model.fetch({success: resolve})); - expect(model.attributes).to.deep.equal({ - id: model.id, - hello: 'world!' - }); - }); + it('fetches from localForage', async function () { + const model = new TestModel(); + await new Promise((resolve, reject) => model.save({ 'hello': 'world!' }, { 'success': resolve })); + await new Promise((resolve, reject) => model.fetch({ success: resolve })); + expect(model.attributes).to.deep.equal({ + id: model.id, + hello: 'world!', + }); + }); - it('updates to localForage', async function () { - const model = new TestModel(); - await new Promise((resolve, reject) => model.save({'hello': 'world!'}, {'success': resolve})); - expect(model.get('hello')).to.equal('world!'); - await new Promise((resolve, reject) => model.save({'hello': 'you!'}, {'success': resolve})); - expect(model.get('hello')).to.equal('you!'); - await new Promise((resolve, reject) => model.fetch({success: resolve})); - expect(model.get('hello')).to.equal('you!'); - }); + it('updates to localForage', async function () { + const model = new TestModel(); + await new Promise((resolve, reject) => model.save({ 'hello': 'world!' }, { 'success': resolve })); + expect(model.get('hello')).to.equal('world!'); + await new Promise((resolve, reject) => model.save({ 'hello': 'you!' }, { 'success': resolve })); + expect(model.get('hello')).to.equal('you!'); + await new Promise((resolve, reject) => model.fetch({ success: resolve })); + expect(model.get('hello')).to.equal('you!'); + }); - it('removes from localForage', async function () { - const model = new TestModel(); - await new Promise((resolve, reject) => model.save({'hello': 'world!'}, {'success': resolve})); - const fetched_model = await new Promise((resolve, reject) => model.destroy({'success': resolve})); - expect(model).to.deep.equal(fetched_model); - const result = await new Promise((resolve, reject) => model.fetch({'success': () => resolve('success'), 'error': () => resolve('error')})); - expect(result).to.equal('error'); - }); + it('removes from localForage', async function () { + const model = new TestModel(); + await new Promise((resolve, reject) => model.save({ 'hello': 'world!' }, { 'success': resolve })); + const fetched_model = await new Promise((resolve, reject) => model.destroy({ 'success': resolve })); + expect(model).to.deep.equal(fetched_model); + const result = await new Promise((resolve, reject) => + model.fetch({ 'success': () => resolve('success'), 'error': () => resolve('error') }), + ); + expect(result).to.equal('error'); }); + }); }); diff --git a/test/localStorage.test.js b/test/localStorage.test.js index d03a3b9a..279bbc0c 100644 --- a/test/localStorage.test.js +++ b/test/localStorage.test.js @@ -1,275 +1,291 @@ +/* eslint-disable class-methods-use-this */ import { clone, each, extend, range, times } from 'lodash'; -import { Collection } from "../src/collection"; +import { Collection } from '../src/collection'; import { getSyncMethod, sync } from '../src/helpers.js'; import { Model } from '../src/model.js'; import { assert } from 'chai'; import Storage from '../src/storage.js'; import root from 'window-or-global'; +describe('Storage using localStorage', function () { + const attributes = { + string: 'String', + string2: 'String 2', + number: 1337, + }; -describe("Storage using localStorage", function () { + const onError = function (model, resp, options) { + throw new Error(resp); + }; - const attributes = { - string: "String", - string2: "String 2", - number: 1337 - }; + describe('on a Collection', function () { + beforeEach(() => localStorage.clear()); + + class TestModel extends Model { + // eslint-disable-next-line class-methods-use-this + defaults() { + return attributes; + } + } + + class TestCollection extends Collection { + get model() { + return TestModel; + } + get browserStorage() { + return new Storage('collectionStore', 'local'); + } + } + + it('should use `localSync`', function () { + const collection = new TestCollection(); + collection.fetch(); + const method = getSyncMethod(collection); + assert.equal(method.__name__, 'localSync'); + }); + + it('should initially be empty', function () { + const collection = new TestCollection(); + collection.fetch(); + assert.equal(collection.length, 0); + }); + + describe('create', function () { + beforeEach(() => localStorage.clear()); + + it('should have 1 model', async function () { + const collection = new TestCollection(); + const model = await new Promise((resolve, reject) => collection.create({}, { 'success': resolve })); + assert.equal(collection.length, 1); + }); + + it('should have a populated model', async function () { + const collection = new TestCollection(); + const model = await new Promise((resolve, reject) => collection.create({}, { 'success': resolve })); + assert.equal(collection.length, 1); + assert.deepEqual(model.toJSON(), extend(clone(attributes), { 'id': model.id })); + }); + + it('should have assigned an `id` to the model', async function () { + const collection = new TestCollection(); + const model = await new Promise((resolve, reject) => collection.create({}, { 'success': resolve })); + await model.collection.browserStorage.storeInitialized; + assert.isDefined(model.id); + }); + + it('should be saved to the localstorage', async function () { + const collection = new TestCollection(); + const model = await new Promise((resolve, reject) => collection.create({}, { 'success': resolve })); + await model.collection.browserStorage.storeInitialized; + assert.isNotNull(root.localStorage.getItem('localforage/collectionStore' + '-' + model.id)); + }); + }); + + describe('get (by `id`)', function () { + beforeEach(() => localStorage.clear()); - const onError = function (model, resp, options) { - throw new Error(resp); - }; + it('should find the model with its `id`', async function () { + const collection = new TestCollection(); + const model = await new Promise((resolve, reject) => collection.create({}, { 'success': resolve })); + await model.collection.browserStorage.storeInitialized; + assert.deepEqual(collection.get(model.id), model); + }); + }); + + describe('instances', function () { + beforeEach(() => localStorage.clear()); - describe("on a Collection", function () { + describe('when saved', function () { beforeEach(() => localStorage.clear()); - const TestModel = Model.extend({ - defaults: attributes - }); + it('should persist the changes', async function () { + const collection = new TestCollection(); + const model = await new Promise((resolve, reject) => collection.create({}, { 'success': resolve })); + model.save({ 'string': 'String 0' }); + collection.fetch(); - const TestCollection = Collection.extend({ - model: TestModel, - browserStorage: new Storage("collectionStore", "local") + assert.equal(model.get('string'), 'String 0'); }); - it("should use `localSync`", function () { + describe('with a new `id`', function () { + beforeEach(() => localStorage.clear()); + + it('should have a new `id`', async function () { const collection = new TestCollection(); + const model = await new Promise((resolve, reject) => collection.create({}, { 'success': resolve })); + model.save({ 'id': 1 }); collection.fetch(); - const method = getSyncMethod(collection); - assert.equal(method.__name__, 'localSync'); - }); - it("should initially be empty", function () { + assert.equal(model.id, 1); + }); + + it('should have kept its old properties', async function () { const collection = new TestCollection(); + const model = await new Promise((resolve, reject) => collection.create({}, { 'success': resolve })); + model.save({ 'id': 1 }); collection.fetch(); - assert.equal(collection.length, 0); - }); + const withId = clone(attributes); + withId.id = 1; + assert.deepEqual(model.toJSON(), withId); + }); - describe("create", function () { - beforeEach(() => localStorage.clear()); - - it("should have 1 model", async function () { - const collection = new TestCollection(); - const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve})); - assert.equal(collection.length, 1); - }); - - it("should have a populated model", async function () { - const collection = new TestCollection(); - const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve})); - assert.equal(collection.length, 1); - assert.deepEqual(model.toJSON(), extend(clone(attributes), {'id': model.id})); - }); - - it("should have assigned an `id` to the model", async function () { - const collection = new TestCollection(); - const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve})); - await model.collection.browserStorage.storeInitialized; - assert.isDefined(model.id); - }); - - it("should be saved to the localstorage", async function () { - const collection = new TestCollection(); - const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve})); - await model.collection.browserStorage.storeInitialized; - assert.isNotNull(root.localStorage.getItem('localforage/collectionStore'+'-'+model.id)); - }); + it('should be saved in localstorage by new id', async function () { + const collection = new TestCollection(); + const model = await new Promise((resolve, reject) => collection.create({}, { 'success': resolve })); + model.save({ 'id': 1 }); + await new Promise((resolve, reject) => collection.fetch({ 'success': resolve })); + assert.isNotNull(root.localStorage.getItem('localforage/collectionStore-1')); + }); }); + }); - describe("get (by `id`)", function () { - beforeEach(() => localStorage.clear()); + describe('destroy', function () { + beforeEach(() => localStorage.clear()); - it("should find the model with its `id`", async function () { - const collection = new TestCollection(); - const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve})); - await model.collection.browserStorage.storeInitialized; - assert.deepEqual(collection.get(model.id), model); - }); + it('should remove all items from the collection and its store', async function () { + const collection = new TestCollection(); + await Promise.all( + range(5).map((i) => new Promise((resolve, reject) => collection.create({}, { 'success': resolve }))), + ); + assert.equal(collection.length, 5); + while (collection.length) { + collection.at(0).destroy(); + } + const beforeFetchLength = collection.length; + collection.fetch(); + const afterFetchLength = collection.length; + + assert.equal(beforeFetchLength, 0); + assert.equal(afterFetchLength, 0); }); - - describe("instances", function () { - beforeEach(() => localStorage.clear()); - - describe("when saved", function () { - beforeEach(() => localStorage.clear()); - - it("should persist the changes", async function () { - const collection = new TestCollection(); - const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve})); - model.save({'string': "String 0"}); - collection.fetch(); - - assert.equal(model.get("string"), "String 0"); - }); - - describe("with a new `id`", function () { - beforeEach(() => localStorage.clear()); - - it("should have a new `id`", async function () { - const collection = new TestCollection(); - const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve})); - model.save({'id': 1}); - collection.fetch(); - - assert.equal(model.id, 1); - }); - - it("should have kept its old properties", async function () { - const collection = new TestCollection(); - const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve})); - model.save({'id': 1}); - collection.fetch(); - - const withId = clone(attributes); - withId.id = 1; - assert.deepEqual(model.toJSON(), withId); - }); - - it("should be saved in localstorage by new id", async function () { - const collection = new TestCollection(); - const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve})); - model.save({'id': 1}); - await new Promise((resolve, reject) => collection.fetch({'success': resolve})); - assert.isNotNull(root.localStorage.getItem('localforage/collectionStore-1')); - }); - }); - }); - - - describe("destroy", function () { - beforeEach(() => localStorage.clear()); - - it("should remove all items from the collection and its store", async function () { - const collection = new TestCollection(); - await Promise.all(range(5).map(i => new Promise((resolve, reject) => collection.create({}, {'success': resolve})))); - assert.equal(collection.length, 5); - while (collection.length) { - collection.at(0).destroy(); - } - const beforeFetchLength = collection.length; - collection.fetch(); - const afterFetchLength = collection.length; - - assert.equal(beforeFetchLength, 0); - assert.equal(afterFetchLength, 0); - }); - }); - - describe("with a different `idAttribute`", function () { - - it("should use the custom `idAttribute`", async function () { - const TestModel = Model.extend({ - defaults: attributes, - idAttribute: "_id" - }); - const TestCollection = Collection.extend({ - model: TestModel, - browserStorage: new Storage("collection2Store", "local") - }); - - const collection = new TestCollection(); - const model = await new Promise(resolve => collection.create({}, {'success': resolve})); - assert.equal(collection.first().id, collection.first().get("_id")); - }); - }); + }); + + describe('with a different `idAttribute`', function () { + it('should use the custom `idAttribute`', async function () { + class TestModel extends Model { + get idAttribute() { + return '_id'; + } + + defaults() { + return attributes; + } + } + + class TestCollection extends Collection { + get model() { + return TestModel; + } + get browserStorage() { + return new Storage('collection2Store', 'local'); + } + } + + const collection = new TestCollection(); + const model = await new Promise((resolve) => collection.create({}, { 'success': resolve })); + assert.equal(collection.first().id, collection.first().get('_id')); }); + }); }); + }); - describe("on a Model", function () { - beforeEach(() => localStorage.clear()); + describe('on a Model', function () { + beforeEach(() => localStorage.clear()); - const TestModel = Model.extend({ - defaults: attributes, - browserStorage: new Storage("modelStore", "local") - }); + class TestModel extends Model { + constructor() { + super(); + this.browserStorage = new Storage('modelStore', 'local'); + } + + // eslint-disable-next-line class-methods-use-this + defaults() { + return attributes; + } + } + + it('should use `localSync`', function () { + const model = new TestModel(); + assert.equal(getSyncMethod(model).__name__, 'localSync'); + }); - it("should use `localSync`", function () { - const model = new TestModel(); - assert.equal(getSyncMethod(model).__name__, 'localSync'); - }); + describe('fetch', function () { + beforeEach(() => localStorage.clear()); - describe("fetch", function () { - beforeEach(() => localStorage.clear()); + it('should fire sync event on fetch', function (done) { + const model = new TestModel(attributes); + model.on('sync', () => done()); + model.fetch(); + }); + }); - it('should fire sync event on fetch', function(done) { - const model = new TestModel(attributes); - model.on('sync', () => done()); - model.fetch(); - }); + describe('save', function () { + beforeEach(() => localStorage.clear()); + + it('should have assigned an `id` to the model', async function () { + const model = new TestModel(); + await new Promise((resolve, reject) => model.save(null, { 'success': resolve })); + model.fetch(); + assert.isDefined(model.id); + }); + + it('should be saved to the localstorage', async function () { + const model = new TestModel(); + await new Promise((resolve, reject) => model.save(null, { 'success': resolve })); + assert.isNotNull(root.localStorage.getItem('localforage/modelStore' + '-' + model.id)); + }); + + describe('with new attributes', function () { + it('should persist the changes', async function () { + const model = new TestModel(); + await new Promise((resolve, reject) => model.save({ number: 42 }, { 'success': resolve })); + model.fetch(); + assert.deepEqual(model.toJSON(), extend(clone(attributes), { id: model.id, number: 42 })); }); + }); - describe("save", function () { - beforeEach(() => localStorage.clear()); - - it("should have assigned an `id` to the model", async function () { - const model = new TestModel(); - await new Promise((resolve, reject) => model.save(null, {'success': resolve})); - model.fetch(); - assert.isDefined(model.id); - }); - - it("should be saved to the localstorage", async function () { - const model = new TestModel(); - await new Promise((resolve, reject) => model.save(null, {'success': resolve})); - assert.isNotNull(root.localStorage.getItem('localforage/modelStore'+'-'+model.id)); - }); - - describe("with new attributes", function () { - - it("should persist the changes", async function () { - const model = new TestModel(); - await new Promise((resolve, reject) => model.save({number: 42}, {'success': resolve})); - model.fetch(); - assert.deepEqual(model.toJSON(), extend(clone(attributes), {id: model.id, number: 42})); - }); - }); - - describe('fires events', function () { - - it('should fire sync event on save', function(done) { - const model = new TestModel(); - model.on('sync', () => done()); - model.save({foo: 'baz'}); - }); - }); + describe('fires events', function () { + it('should fire sync event on save', function (done) { + const model = new TestModel(); + model.on('sync', () => done()); + model.save({ foo: 'baz' }); }); + }); + }); - describe("destroy", function () { - - it("should have removed the instance from the store", async function () { - const model = new TestModel(); - await new Promise((resolve, reject) => model.save(null, {'success': resolve})); - const store = model.browserStorage.store; - let item = await store.getItem(model.browserStorage.getItemName(model.id)); - assert.isNotNull(item); - await new Promise((resolve, reject) => model.destroy({'success': resolve})); - item = await store.getItem(model.browserStorage.getItemName(model.id)); - assert.isNull(item); - }); - }); + describe('destroy', function () { + it('should have removed the instance from the store', async function () { + const model = new TestModel(); + await new Promise((resolve, reject) => model.save(null, { 'success': resolve })); + const store = model.browserStorage.store; + let item = await store.getItem(model.browserStorage.getItemName(model.id)); + assert.isNotNull(item); + await new Promise((resolve, reject) => model.destroy({ 'success': resolve })); + item = await store.getItem(model.browserStorage.getItemName(model.id)); + assert.isNull(item); + }); }); + }); }); -describe("Without browserStorage", function () { - beforeEach(() => localStorage.clear()); - - describe("on a Collection", function () { +describe('Without browserStorage', function () { + beforeEach(() => localStorage.clear()); - it("should use `ajaxSync`", function () { - const TestCollection = Collection.extend(); - const collection = new TestCollection(); - const method = getSyncMethod(collection); - assert.equal(method, sync); - }); + describe('on a Collection', function () { + it('should use `ajaxSync`', function () { + class TestCollection extends Collection {} + const collection = new TestCollection(); + const method = getSyncMethod(collection); + assert.equal(method, sync); }); + }); - describe("on a Model", function () { - - it("should use `ajaxSync`", function () { - const TestModel = Model.extend(); - const model = new TestModel(); - const method = getSyncMethod(model); - assert.equal(method, sync); - }); + describe('on a Model', function () { + it('should use `ajaxSync`', function () { + const model = new Model(); + const method = getSyncMethod(model); + assert.equal(method, sync); }); + }); }); diff --git a/test/model.js b/test/model.js index 64f2d21d..3c438416 100644 --- a/test/model.js +++ b/test/model.js @@ -1,216 +1,184 @@ -(function(QUnit) { +/* eslint-disable class-methods-use-this */ - const ProxyModel = Skeletor.Model.extend(); - const Klass = Skeletor.Collection.extend({ - url: function() { return '/collection'; } - }); +(function (QUnit) { + class ProxyModel extends Skeletor.Model {} + class Klass extends Skeletor.Collection { + url() { + return '/collection'; + } + } let doc, collection; QUnit.module('Skeletor.Model', { - - beforeEach: function(assert) { + beforeEach(assert) { sinon.stub(window, 'fetch').callsFake(() => {}); doc = new ProxyModel({ id: '1-the-tempest', title: 'The Tempest', author: 'Bill Shakespeare', - length: 123 + length: 123, }); collection = new Klass(); collection.add(doc); }, - afterEach: function(assert) { - window.fetch.restore() - } + afterEach(assert) { + window.fetch.restore(); + }, }); - - QUnit.test('initialize', function(assert) { + QUnit.test('initialize', function (assert) { assert.expect(3); - const Model = Skeletor.Model.extend({ - initialize: function() { + class Model extends Skeletor.Model { + initialize() { this.one = 1; assert.equal(this.collection, collection); } - }); - const model = new Model({}, {collection: collection}); + } + const model = new Model({}, { collection: collection }); assert.equal(model.one, 1); assert.equal(model.collection, collection); }); - QUnit.test('Object.prototype properties are overridden by attributes', function(assert) { + QUnit.test('Object.prototype properties are overridden by attributes', function (assert) { assert.expect(1); - const model = new Skeletor.Model({hasOwnProperty: true}); + const model = new Skeletor.Model({ hasOwnProperty: true }); assert.equal(model.get('hasOwnProperty'), true); }); - QUnit.test('initialize with attributes and options', function(assert) { + QUnit.test('initialize with attributes and options', function (assert) { assert.expect(1); - const Model = Skeletor.Model.extend({ - initialize: function(attributes, options) { + class Model extends Skeletor.Model { + initialize(attributes, options) { this.one = options.one; } - }); - const model = new Model({}, {one: 1}); + } + const model = new Model({}, { one: 1 }); assert.equal(model.one, 1); }); - QUnit.test('initialize with parsed attributes', function(assert) { + QUnit.test('initialize with parsed attributes', function (assert) { assert.expect(1); - const Model = Skeletor.Model.extend({ - parse: function(attrs) { + class Model extends Skeletor.Model { + parse(attrs) { attrs.value += 1; return attrs; } - }); - const model = new Model({value: 1}, {parse: true}); + } + const model = new Model({ value: 1 }, { parse: true }); assert.equal(model.get('value'), 2); }); - - QUnit.test('preinitialize', function(assert) { + QUnit.test('preinitialize', function (assert) { assert.expect(2); - const Model = Skeletor.Model.extend({ - - preinitialize: function() { + class Model extends Skeletor.Model { + preinitialize() { this.one = 1; } - }); - const model = new Model({}, {collection: collection}); + } + const model = new Model({}, { collection: collection }); assert.equal(model.one, 1); assert.equal(model.collection, collection); }); - QUnit.test('preinitialize occurs before the model is set up', function(assert) { + QUnit.test('preinitialize occurs before the model is set up', function (assert) { assert.expect(6); - const Model = Skeletor.Model.extend({ - - preinitialize: function() { + class Model extends Skeletor.Model { + preinitialize() { assert.equal(this.collection, undefined); assert.equal(this.cid, undefined); assert.equal(this.id, undefined); } - }); - const model = new Model({id: 'foo'}, {collection: collection}); + } + const model = new Model({ id: 'foo' }, { collection: collection }); assert.equal(model.collection, collection); assert.equal(model.id, 'foo'); assert.notEqual(model.cid, undefined); }); - QUnit.test('parse can return null', function(assert) { + QUnit.test('parse can return null', function (assert) { assert.expect(1); - const Model = Skeletor.Model.extend({ - parse: function(attrs) { + class Model extends Skeletor.Model { + parse(attrs) { attrs.value += 1; return null; } - }); - const model = new Model({value: 1}, {parse: true}); + } + const model = new Model({ value: 1 }, { parse: true }); assert.equal(JSON.stringify(model.toJSON()), '{}'); }); - QUnit.test('url', function(assert) { + QUnit.test('url', function (assert) { assert.expect(3); doc.urlRoot = null; assert.equal(doc.url(), '/collection/1-the-tempest'); doc.collection.url = '/collection/'; assert.equal(doc.url(), '/collection/1-the-tempest'); doc.collection = null; - assert.raises(function() { doc.url(); }); + assert.raises(function () { + doc.url(); + }); doc.collection = collection; }); - QUnit.test('url when using urlRoot, and uri encoding', function(assert) { + QUnit.test('url when using urlRoot, and uri encoding', function (assert) { assert.expect(2); - const Model = Skeletor.Model.extend({ - urlRoot: '/collection' - }); + class Model extends Skeletor.Model { + constructor(attributes, options) { + super(attributes, options); + this.urlRoot = '/collection'; + } + } const model = new Model(); assert.equal(model.url(), '/collection'); - model.set({id: '+1+'}); + model.set({ id: '+1+' }); assert.equal(model.url(), '/collection/%2B1%2B'); }); - QUnit.test('url when using urlRoot as a function to determine urlRoot at runtime', function(assert) { + QUnit.test('url when using urlRoot as a function to determine urlRoot at runtime', function (assert) { assert.expect(2); - const Model = Skeletor.Model.extend({ - urlRoot: function() { + class Model extends Skeletor.Model { + urlRoot() { return '/nested/' + this.get('parentId') + '/collection'; } - }); + } - const model = new Model({parentId: 1}); + const model = new Model({ parentId: 1 }); assert.equal(model.url(), '/nested/1/collection'); - model.set({id: 2}); + model.set({ id: 2 }); assert.equal(model.url(), '/nested/1/collection/2'); }); - QUnit.test('underscore methods', function(assert) { + QUnit.test('underscore methods', function (assert) { assert.expect(5); - const model = new Skeletor.Model({foo: 'a', bar: 'b', baz: 'c'}); - const model2 = model.clone(); + const model = new Skeletor.Model({ foo: 'a', bar: 'b', baz: 'c' }); assert.deepEqual(model.keys(), ['foo', 'bar', 'baz']); assert.deepEqual(model.values(), ['a', 'b', 'c']); - assert.deepEqual(model.invert(), {a: 'foo', b: 'bar', c: 'baz'}); - assert.deepEqual(model.pick('foo', 'baz'), {foo: 'a', baz: 'c'}); - assert.deepEqual(model.omit('foo', 'bar'), {baz: 'c'}); + assert.deepEqual(model.invert(), { a: 'foo', b: 'bar', c: 'baz' }); + assert.deepEqual(model.pick('foo', 'baz'), { foo: 'a', baz: 'c' }); + assert.deepEqual(model.omit('foo', 'bar'), { baz: 'c' }); }); - QUnit.test('clone', function(assert) { - assert.expect(10); - var a = new Skeletor.Model({foo: 1, bar: 2, baz: 3}); - var b = a.clone(); - assert.equal(a.get('foo'), 1); - assert.equal(a.get('bar'), 2); - assert.equal(a.get('baz'), 3); - assert.equal(b.get('foo'), a.get('foo'), 'Foo should be the same on the clone.'); - assert.equal(b.get('bar'), a.get('bar'), 'Bar should be the same on the clone.'); - assert.equal(b.get('baz'), a.get('baz'), 'Baz should be the same on the clone.'); - a.set({foo: 100}); - assert.equal(a.get('foo'), 100); - assert.equal(b.get('foo'), 1, 'Changing a parent attribute does not change the clone.'); - - var foo = new Skeletor.Model({p: 1}); - var bar = new Skeletor.Model({p: 2}); - bar.set(foo.clone().attributes, {unset: true}); - assert.equal(foo.get('p'), 1); - assert.equal(bar.get('p'), undefined); - }); - - QUnit.test('isNew', function(assert) { + QUnit.test('isNew', function (assert) { assert.expect(6); - var a = new Skeletor.Model({foo: 1, bar: 2, baz: 3}); + var a = new Skeletor.Model({ foo: 1, bar: 2, baz: 3 }); assert.ok(a.isNew(), 'it should be new'); - a = new Skeletor.Model({foo: 1, bar: 2, baz: 3, id: -5}); + a = new Skeletor.Model({ foo: 1, bar: 2, baz: 3, id: -5 }); assert.ok(!a.isNew(), 'any defined ID is legal, negative or positive'); - a = new Skeletor.Model({foo: 1, bar: 2, baz: 3, id: 0}); + a = new Skeletor.Model({ foo: 1, bar: 2, baz: 3, id: 0 }); assert.ok(!a.isNew(), 'any defined ID is legal, including zero'); assert.ok(new Skeletor.Model().isNew(), 'is true when there is no id'); - assert.ok(!new Skeletor.Model({id: 2}).isNew(), 'is false for a positive integer'); - assert.ok(!new Skeletor.Model({id: -5}).isNew(), 'is false for a negative integer'); + assert.ok(!new Skeletor.Model({ id: 2 }).isNew(), 'is false for a positive integer'); + assert.ok(!new Skeletor.Model({ id: -5 }).isNew(), 'is false for a negative integer'); }); - QUnit.test('get', function(assert) { + QUnit.test('get', function (assert) { assert.expect(2); assert.equal(doc.get('title'), 'The Tempest'); assert.equal(doc.get('author'), 'Bill Shakespeare'); }); - QUnit.test('escape', function(assert) { - assert.expect(5); - assert.equal(doc.escape('title'), 'The Tempest'); - doc.set({audience: 'Bill & Bob'}); - assert.equal(doc.escape('audience'), 'Bill & Bob'); - doc.set({audience: 'Tim > Joan'}); - assert.equal(doc.escape('audience'), 'Tim > Joan'); - doc.set({audience: 10101}); - assert.equal(doc.escape('audience'), '10101'); - doc.unset('audience'); - assert.equal(doc.escape('audience'), ''); - }); - - QUnit.test('has', function(assert) { + QUnit.test('has', function (assert) { assert.expect(10); var model = new Skeletor.Model(); @@ -224,7 +192,7 @@ 'empty': '', 'name': 'name', 'null': null, - 'undefined': undefined + 'undefined': undefined, }); assert.strictEqual(model.has('0'), true); @@ -241,50 +209,58 @@ assert.strictEqual(model.has('undefined'), false); }); - QUnit.test('matches', function(assert) { + QUnit.test('matches', function (assert) { assert.expect(4); var model = new Skeletor.Model(); - assert.strictEqual(model.matches({name: 'Jonas', cool: true}), false); + assert.strictEqual(model.matches({ name: 'Jonas', cool: true }), false); - model.set({name: 'Jonas', cool: true}); + model.set({ name: 'Jonas', cool: true }); - assert.strictEqual(model.matches({name: 'Jonas'}), true); - assert.strictEqual(model.matches({name: 'Jonas', cool: true}), true); - assert.strictEqual(model.matches({name: 'Jonas', cool: false}), false); + assert.strictEqual(model.matches({ name: 'Jonas' }), true); + assert.strictEqual(model.matches({ name: 'Jonas', cool: true }), true); + assert.strictEqual(model.matches({ name: 'Jonas', cool: false }), false); }); - QUnit.test('matches with predicate', function(assert) { - var model = new Skeletor.Model({a: 0}); + QUnit.test('matches with predicate', function (assert) { + var model = new Skeletor.Model({ a: 0 }); - assert.strictEqual(model.matches(function(attr) { - return attr.a > 1 && attr.b != null; - }), false); + assert.strictEqual( + model.matches(function (attr) { + return attr.a > 1 && attr.b != null; + }), + false, + ); - model.set({a: 3, b: true}); + model.set({ a: 3, b: true }); - assert.strictEqual(model.matches(function(attr) { - return attr.a > 1 && attr.b != null; - }), true); + assert.strictEqual( + model.matches(function (attr) { + return attr.a > 1 && attr.b != null; + }), + true, + ); }); - QUnit.test('set and unset', function(assert) { + QUnit.test('set and unset', function (assert) { assert.expect(8); - var a = new Skeletor.Model({id: 'id', foo: 1, bar: 2, baz: 3}); + var a = new Skeletor.Model({ id: 'id', foo: 1, bar: 2, baz: 3 }); var changeCount = 0; - a.on('change:foo', function() { changeCount += 1; }); - a.set({foo: 2}); + a.on('change:foo', function () { + changeCount += 1; + }); + a.set({ foo: 2 }); assert.equal(a.get('foo'), 2, 'Foo should have changed.'); assert.equal(changeCount, 1, 'Change count should have incremented.'); // set with value that is not new shouldn't fire change event - a.set({foo: 2}); + a.set({ foo: 2 }); assert.equal(a.get('foo'), 2, 'Foo should NOT have changed, still 2'); assert.equal(changeCount, 1, 'Change count should NOT have incremented.'); - a.validate = function(attrs) { + a.validate = function (attrs) { assert.equal(attrs.foo, undefined, 'validate:true passed while unsetting'); }; - a.unset('foo', {validate: true}); + a.unset('foo', { validate: true }); assert.equal(a.get('foo'), undefined, 'Foo should have changed'); delete a.validate; assert.equal(changeCount, 2, 'Change count should have incremented for unset.'); @@ -293,53 +269,64 @@ assert.equal(a.id, undefined, 'Unsetting the id should remove the id property.'); }); - QUnit.test('#2030 - set with failed validate, followed by another set triggers change', function(assert) { - var attr = 0, main = 0, error = 0; - var Model = Skeletor.Model.extend({ - validate: function(attrs) { + QUnit.test('#2030 - set with failed validate, followed by another set triggers change', function (assert) { + var attr = 0, + main = 0, + error = 0; + + class Model extends Skeletor.Model { + validate(attrs) { if (attrs.x > 1) { error++; return 'this is an error'; } } + } + var model = new Model({ x: 0 }); + model.on('change:x', function () { + attr++; + }); + model.on('change', function () { + main++; }); - var model = new Model({x: 0}); - model.on('change:x', function() { attr++; }); - model.on('change', function() { main++; }); - model.set({x: 2}, {validate: true}); - model.set({x: 1}, {validate: true}); + model.set({ x: 2 }, { validate: true }); + model.set({ x: 1 }, { validate: true }); assert.deepEqual([attr, main, error], [1, 1, 1]); }); - QUnit.test('set triggers changes in the correct order', function(assert) { + QUnit.test('set triggers changes in the correct order', function (assert) { var value = null; var model = new Skeletor.Model(); - model.on('last', function(){ value = 'last'; }); - model.on('first', function(){ value = 'first'; }); + model.on('last', function () { + value = 'last'; + }); + model.on('first', function () { + value = 'first'; + }); model.trigger('first'); model.trigger('last'); assert.equal(value, 'last'); }); - QUnit.test('set falsy values in the correct order', function(assert) { + QUnit.test('set falsy values in the correct order', function (assert) { assert.expect(2); - var model = new Skeletor.Model({result: 'result'}); - model.on('change', function() { + var model = new Skeletor.Model({ result: 'result' }); + model.on('change', function () { assert.equal(model.changed.result, undefined); assert.equal(model.previous('result'), false); }); - model.set({result: undefined}, {silent: true}); - model.set({result: null}, {silent: true}); - model.set({result: false}, {silent: true}); - model.set({result: undefined}); + model.set({ result: undefined }, { silent: true }); + model.set({ result: null }, { silent: true }); + model.set({ result: false }, { silent: true }); + model.set({ result: undefined }); }); - QUnit.test('nested set triggers with the correct options', function(assert) { + QUnit.test('nested set triggers with the correct options', function (assert) { var model = new Skeletor.Model(); var o1 = {}; var o2 = {}; var o3 = {}; - model.on('change', function(__, options) { + model.on('change', function (__, options) { switch (model.get('a')) { case 1: assert.equal(options, o1); @@ -354,31 +341,38 @@ model.set('a', 1, o1); }); - QUnit.test('multiple unsets', function(assert) { + QUnit.test('multiple unsets', function (assert) { assert.expect(1); var i = 0; - var counter = function(){ i++; }; - var model = new Skeletor.Model({a: 1}); + var counter = function () { + i++; + }; + var model = new Skeletor.Model({ a: 1 }); model.on('change:a', counter); - model.set({a: 2}); + model.set({ a: 2 }); model.unset('a'); model.unset('a'); assert.equal(i, 2, 'Unset does not fire an event for missing attributes.'); }); - QUnit.test('unset and changedAttributes', function(assert) { + QUnit.test('unset and changedAttributes', function (assert) { assert.expect(1); - var model = new Skeletor.Model({a: 1}); - model.on('change', function() { + var model = new Skeletor.Model({ a: 1 }); + model.on('change', function () { assert.ok('a' in model.changedAttributes(), 'changedAttributes should contain unset properties'); }); model.unset('a'); }); - QUnit.test('using a non-default id attribute.', function(assert) { + QUnit.test('using a non-default id attribute.', function (assert) { assert.expect(5); - var MongoModel = Skeletor.Model.extend({idAttribute: '_id'}); - var model = new MongoModel({id: 'eye-dee', _id: 25, title: 'Model'}); + class MongoModel extends Skeletor.Model { + get idAttribute() { + return '_id'; + } + } + + const model = new MongoModel({ id: 'eye-dee', _id: 25, title: 'Model' }); assert.equal(model.get('id'), 'eye-dee'); assert.equal(model.id, 25); assert.equal(model.isNew(), false); @@ -387,61 +381,67 @@ assert.equal(model.isNew(), true); }); - QUnit.test('setting an alternative cid prefix', function(assert) { + QUnit.test('setting an alternative cid prefix', function (assert) { assert.expect(4); - var Model = Skeletor.Model.extend({ - cidPrefix: 'm' - }); - var model = new Model(); + class Model extends Skeletor.Model { + get cidPrefix() { + return 'm'; + } + } + let model = new Model(); assert.equal(model.cid.charAt(0), 'm'); model = new Skeletor.Model(); assert.equal(model.cid.charAt(0), 'c'); - var Collection = Skeletor.Collection.extend({ - model: Model - }); - var col = new Collection([{id: 'c5'}, {id: 'c6'}, {id: 'c7'}]); + class Collection extends Skeletor.Collection { + get model() { + return Model; + } + } + const col = new Collection([{ id: 'c5' }, { id: 'c6' }, { id: 'c7' }]); assert.equal(col.get('c6').cid.charAt(0), 'm'); - col.set([{id: 'c6', value: 'test'}], { + col.set([{ id: 'c6', value: 'test' }], { merge: true, add: true, - remove: false + remove: false, }); assert.ok(col.get('c6').has('value')); }); - QUnit.test('set an empty string', function(assert) { + QUnit.test('set an empty string', function (assert) { assert.expect(1); - var model = new Skeletor.Model({name: 'Model'}); - model.set({name: ''}); + var model = new Skeletor.Model({ name: 'Model' }); + model.set({ name: '' }); assert.equal(model.get('name'), ''); }); - QUnit.test('setting an object', function(assert) { + QUnit.test('setting an object', function (assert) { assert.expect(1); var model = new Skeletor.Model({ - custom: {foo: 1} + custom: { foo: 1 }, }); - model.on('change', function() { + model.on('change', function () { assert.ok(1); }); model.set({ - custom: {foo: 1} // no change should be fired + custom: { foo: 1 }, // no change should be fired }); model.set({ - custom: {foo: 2} // change event should be fired + custom: { foo: 2 }, // change event should be fired }); }); - QUnit.test('clear', function(assert) { + QUnit.test('clear', function (assert) { assert.expect(3); var changed; - var model = new Skeletor.Model({id: 1, name: 'Model'}); - model.on('change:name', function(){ changed = true; }); - model.on('change', function() { + var model = new Skeletor.Model({ id: 1, name: 'Model' }); + model.on('change:name', function () { + changed = true; + }); + model.on('change', function () { var changedAttrs = model.changedAttributes(); assert.ok('name' in changedAttrs); }); @@ -450,115 +450,126 @@ assert.equal(model.get('name'), undefined); }); - QUnit.test('defaults', function(assert) { + QUnit.test('defaults', function (assert) { assert.expect(9); - var Defaulted = Skeletor.Model.extend({ - defaults: { - one: 1, - two: 2 + class Defaulted extends Skeletor.Model { + defaults() { + return { + one: 1, + two: 2, + }; } - }); - var model = new Defaulted({two: undefined}); + } + let model = new Defaulted({ two: undefined }); assert.equal(model.get('one'), 1); assert.equal(model.get('two'), 2); - model = new Defaulted({two: 3}); + model = new Defaulted({ two: 3 }); assert.equal(model.get('one'), 1); assert.equal(model.get('two'), 3); - Defaulted = Skeletor.Model.extend({ - defaults: function() { + + class Defaulted2 extends Skeletor.Model { + defaults() { return { one: 3, - two: 4 + two: 4, }; } - }); - model = new Defaulted({two: undefined}); + } + + model = new Defaulted2({ two: undefined }); assert.equal(model.get('one'), 3); assert.equal(model.get('two'), 4); - Defaulted = Skeletor.Model.extend({ - defaults: {hasOwnProperty: true} - }); - model = new Defaulted(); + + class Defaulted3 extends Skeletor.Model { + defaults() { + return { + hasOwnProperty: true, + }; + } + } + model = new Defaulted3(); assert.equal(model.get('hasOwnProperty'), true); - model = new Defaulted({hasOwnProperty: undefined}); + model = new Defaulted3({ hasOwnProperty: undefined }); assert.equal(model.get('hasOwnProperty'), true); - model = new Defaulted({hasOwnProperty: false}); + model = new Defaulted3({ hasOwnProperty: false }); assert.equal(model.get('hasOwnProperty'), false); }); - QUnit.test('change, hasChanged, changedAttributes, previous, previousAttributes', function(assert) { + QUnit.test('change, hasChanged, changedAttributes, previous, previousAttributes', function (assert) { assert.expect(9); - var model = new Skeletor.Model({name: 'Tim', age: 10}); + var model = new Skeletor.Model({ name: 'Tim', age: 10 }); assert.deepEqual(model.changedAttributes(), false); - model.on('change', function() { + model.on('change', function () { assert.ok(model.hasChanged('name'), 'name changed'); assert.ok(!model.hasChanged('age'), 'age did not'); - assert.ok(_.isEqual(model.changedAttributes(), {name: 'Rob'}), 'changedAttributes returns the changed attrs'); + assert.ok(_.isEqual(model.changedAttributes(), { name: 'Rob' }), 'changedAttributes returns the changed attrs'); assert.equal(model.previous('name'), 'Tim'); - assert.ok(_.isEqual(model.previousAttributes(), {name: 'Tim', age: 10}), 'previousAttributes is correct'); + assert.ok(_.isEqual(model.previousAttributes(), { name: 'Tim', age: 10 }), 'previousAttributes is correct'); }); assert.equal(model.hasChanged(), false); assert.equal(model.hasChanged(undefined), false); - model.set({name: 'Rob'}); + model.set({ name: 'Rob' }); assert.equal(model.get('name'), 'Rob'); }); - QUnit.test('changedAttributes', function(assert) { + QUnit.test('changedAttributes', function (assert) { assert.expect(3); - var model = new Skeletor.Model({a: 'a', b: 'b'}); + const model = new Skeletor.Model({ a: 'a', b: 'b' }); assert.deepEqual(model.changedAttributes(), false); - assert.equal(model.changedAttributes({a: 'a'}), false); - assert.equal(model.changedAttributes({a: 'b'}).a, 'b'); + assert.equal(model.changedAttributes({ a: 'a' }), false); + assert.equal(model.changedAttributes({ a: 'b' }).a, 'b'); }); - QUnit.test('change with options', function(assert) { + QUnit.test('change with options', function (assert) { assert.expect(2); var value; - var model = new Skeletor.Model({name: 'Rob'}); - model.on('change', function(m, options) { + var model = new Skeletor.Model({ name: 'Rob' }); + model.on('change', function (m, options) { value = options.prefix + m.get('name'); }); - model.set({name: 'Bob'}, {prefix: 'Mr. '}); + model.set({ name: 'Bob' }, { prefix: 'Mr. ' }); assert.equal(value, 'Mr. Bob'); - model.set({name: 'Sue'}, {prefix: 'Ms. '}); + model.set({ name: 'Sue' }, { prefix: 'Ms. ' }); assert.equal(value, 'Ms. Sue'); }); - QUnit.test('change after initialize', function(assert) { + QUnit.test('change after initialize', function (assert) { assert.expect(1); var changed = 0; - var attrs = {id: 1, label: 'c'}; + var attrs = { id: 1, label: 'c' }; var obj = new Skeletor.Model(attrs); - obj.on('change', function() { changed += 1; }); + obj.on('change', function () { + changed += 1; + }); obj.set(attrs); assert.equal(changed, 0); }); - QUnit.test('save within change event', function(assert) { + QUnit.test('save within change event', function (assert) { assert.expect(1); - const env = this; - const model = new Skeletor.Model({firstName: 'Roger', lastName: 'Penrose'}); + const model = new Skeletor.Model({ firstName: 'Roger', lastName: 'Penrose' }); model.url = '/test'; - model.on('change', function() { + model.on('change', function () { sinon.spy(model, 'sync'); model.save(); const syncArgs = model.sync.lastCall.args; assert.ok(_.isEqual(syncArgs[1], model)); model.sync.restore(); }); - model.set({lastName: 'Hicks'}); + model.set({ lastName: 'Hicks' }); }); - QUnit.test('validate after save', function(assert) { + QUnit.test('validate after save', function (assert) { assert.expect(2); - var lastError, model = new Skeletor.Model(); - model.validate = function(attrs) { + var lastError, + model = new Skeletor.Model(); + model.validate = function (attrs) { if (attrs.admin) return "Can't change admin status."; }; - model.sync = function(method, m, options) { - options.success.call(this, {admin: true}); + model.sync = function (method, m, options) { + options.success.call(this, { admin: true }); }; - model.on('invalid', function(m, error) { + model.on('invalid', function (m, error) { lastError = error; }); model.save(null); @@ -567,21 +578,21 @@ assert.equal(model.validationError, "Can't change admin status."); }); - QUnit.test('save', function(assert) { + QUnit.test('save', function (assert) { sinon.spy(doc, 'sync'); assert.expect(2); - doc.save({title: 'Henry V'}); + doc.save({ title: 'Henry V' }); const syncArgs = doc.sync.lastCall.args; assert.equal(syncArgs[0], 'update'); assert.ok(_.isEqual(syncArgs[1], doc)); doc.sync.restore(); }); - QUnit.test('save and return promise', async function(assert) { + QUnit.test('save and return promise', async function (assert) { const done = assert.async(); sinon.spy(doc, 'sync'); assert.expect(3); - const promise = doc.save({title: 'Henry V'}, {'promise': true, 'wait': true}); + const promise = doc.save({ title: 'Henry V' }, { 'promise': true, 'wait': true }); assert.equal(promise.isResolved, false); const ajaxSettings = window.fetch.lastCall.args[1]; ajaxSettings.success(); @@ -593,81 +604,81 @@ done(); }); - QUnit.test('save, fetch, destroy triggers error event when an error occurs', function(assert) { + QUnit.test('save, fetch, destroy triggers error event when an error occurs', function (assert) { assert.expect(3); var model = new Skeletor.Model(); - model.on('error', function() { + model.on('error', function () { assert.ok(true); }); - model.sync = function(method, m, options) { + model.sync = function (method, m, options) { options.error(); }; - model.save({data: 2, id: 1}); + model.save({ data: 2, id: 1 }); model.fetch(); model.destroy(); }); - QUnit.test('#3283 - save, fetch, destroy calls success with context', function(assert) { + QUnit.test('#3283 - save, fetch, destroy calls success with context', function (assert) { assert.expect(3); var model = new Skeletor.Model(); var obj = {}; var options = { context: obj, - success: function() { + success() { assert.equal(this, obj); - } + }, }; - model.sync = function(method, m, opts) { + model.sync = function (method, m, opts) { opts.success.call(opts.context); }; - model.save({data: 2, id: 1}, options); + model.save({ data: 2, id: 1 }, options); model.fetch(options); model.destroy(options); }); - QUnit.test('#3283 - save, fetch, destroy calls error with context', function(assert) { + QUnit.test('#3283 - save, fetch, destroy calls error with context', function (assert) { assert.expect(3); const model = new Skeletor.Model(); var obj = {}; var options = { context: obj, - error: function() { + error() { assert.equal(this, obj); - } + }, }; - model.sync = function(method, m, opts) { + model.sync = function (method, m, opts) { opts.error.call(opts.context); }; - model.save({data: 2, id: 1}, options); + model.save({ data: 2, id: 1 }, options); model.fetch(options); model.destroy(options); }); - QUnit.test('#3470 - save and fetch with parse false', function(assert) { + QUnit.test('#3470 - save and fetch with parse false', function (assert) { assert.expect(2); let i = 0; const model = new Skeletor.Model(); - model.parse = function() { + model.parse = function () { assert.ok(false); }; - model.sync = function(method, m, options) { - options.success({i: ++i}); + model.sync = function (method, m, options) { + options.success({ i: ++i }); }; - model.fetch({parse: false}); + model.fetch({ parse: false }); assert.equal(model.get('i'), i); - model.save(null, {parse: false}); + model.save(null, { parse: false }); assert.equal(model.get('i'), i); }); - QUnit.test('save with PATCH', function(assert) { + QUnit.test('save with PATCH', function (assert) { sinon.spy(doc, 'sync'); - doc.clear().set({id: 1, a: 1, b: 2, c: 3, d: 4}); + doc.clear().set({ id: 1, a: 1, b: 2, c: 3, d: 4 }); doc.save(); let syncArgs = doc.sync.lastCall.args; assert.equal(syncArgs[0], 'update'); assert.equal(syncArgs[2].attrs, undefined); - doc.save({b: 2, d: 4}, {patch: true}); + doc.save({ b: 2, d: 4 }, { patch: true }); syncArgs = doc.sync.lastCall.args; assert.equal(syncArgs[0], 'patch'); assert.equal(_.size(syncArgs[2].attrs), 2); @@ -678,9 +689,9 @@ doc.sync.restore(); }); - QUnit.test('save with PATCH and different attrs', function(assert) { + QUnit.test('save with PATCH and different attrs', function (assert) { sinon.spy(doc, 'sync'); - doc.clear().save({b: 2, d: 4}, {patch: true, attrs: {B: 1, D: 3}}); + doc.clear().save({ b: 2, d: 4 }, { patch: true, attrs: { B: 1, D: 3 } }); const syncArgs = doc.sync.lastCall.args; const ajaxSettings = window.fetch.lastCall.args[1]; assert.equal(syncArgs[2].attrs.D, 3); @@ -690,52 +701,61 @@ doc.sync.restore(); }); - QUnit.test('save in positional style', function(assert) { + QUnit.test('save in positional style', function (assert) { assert.expect(1); const model = new Skeletor.Model(); - model.sync = function(method, m, options) { + model.sync = function (method, m, options) { options.success(); }; model.save('title', 'Twelfth Night'); assert.equal(model.get('title'), 'Twelfth Night'); }); - QUnit.test('save with non-object success response', function(assert) { + QUnit.test('save with non-object success response', function (assert) { assert.expect(2); const model = new Skeletor.Model(); - model.sync = function(method, m, options) { + model.sync = function (method, m, options) { options.success('', options); options.success(null, options); }; - model.save({testing: 'empty'}, { - success: function(m) { - assert.deepEqual(m.attributes, {testing: 'empty'}); - } - }); + model.save( + { testing: 'empty' }, + { + success(m) { + assert.deepEqual(m.attributes, { testing: 'empty' }); + }, + }, + ); }); - QUnit.test('save with wait and supplied id', function(assert) { - const Model = Skeletor.Model.extend({ - urlRoot: '/collection' - }); + QUnit.test('save with wait and supplied id', function (assert) { + class Model extends Skeletor.Model { + constructor(attributes, options) { + super(attributes, options); + this.urlRoot = '/collection'; + } + } const model = new Model(); model.save({ id: 42 }, { wait: true }); const url = window.fetch.lastCall.args[0]; assert.equal(url, '/collection/42'); }); - QUnit.test('save will pass extra options to success callback', function(assert) { + QUnit.test('save will pass extra options to success callback', function (assert) { assert.expect(1); - const SpecialSyncModel = Skeletor.Model.extend({ - sync: function(method, m, options) { - _.extend(options, {specialSync: true}); + class SpecialSyncModel extends Skeletor.Model { + constructor(attributes, options) { + super(attributes, options); + this.urlRoot = '/test'; + } + sync(method, m, options) { + _.extend(options, { specialSync: true }); return Skeletor.Model.prototype.sync.call(this, method, m, options); - }, - urlRoot: '/test' - }); + } + } const model = new SpecialSyncModel(); - const onSuccess = function(m, response, options) { + const onSuccess = function (m, response, options) { assert.ok(options.specialSync, 'Options were passed correctly to callback'); }; model.save(null, { success: onSuccess }); @@ -743,7 +763,7 @@ ajaxSettings.success(); }); - QUnit.test('fetch', function(assert) { + QUnit.test('fetch', function (assert) { assert.expect(2); sinon.spy(doc, 'sync'); doc.fetch(); @@ -753,18 +773,21 @@ doc.sync.restore(); }); - QUnit.test('fetch will pass extra options to success callback', function(assert) { + QUnit.test('fetch will pass extra options to success callback', function (assert) { assert.expect(1); - const SpecialSyncModel = Skeletor.Model.extend({ - sync: function(method, m, options) { - _.extend(options, {specialSync: true}); + class SpecialSyncModel extends Skeletor.Model { + constructor(attributes, options) { + super(attributes, options); + this.urlRoot = '/test'; + } + sync(method, m, options) { + _.extend(options, { specialSync: true }); return Skeletor.Model.prototype.sync.call(this, method, m, options); - }, - urlRoot: '/test' - }); + } + } const model = new SpecialSyncModel(); - const onSuccess = function(m, response, options) { + const onSuccess = function (m, response, options) { assert.ok(options.specialSync, 'Options were passed correctly to callback'); }; model.fetch({ success: onSuccess }); @@ -772,7 +795,7 @@ ajaxSettings.success(); }); - QUnit.test('destroy', function(assert) { + QUnit.test('destroy', function (assert) { assert.expect(3); sinon.spy(doc, 'sync'); doc.destroy(); @@ -784,18 +807,23 @@ doc.sync.restore(); }); - QUnit.test('destroy will pass extra options to success callback', function(assert) { + QUnit.test('destroy will pass extra options to success callback', function (assert) { assert.expect(1); - const SpecialSyncModel = Skeletor.Model.extend({ - sync: function(method, m, options) { - _.extend(options, {specialSync: true}); - return Skeletor.Model.prototype.sync.call(this, method, m, options); - }, - urlRoot: '/test' - }); - const model = new SpecialSyncModel({id: 'id'}); - const onSuccess = function(m, response, options) { + class SpecialSyncModel extends Skeletor.Model { + constructor(attributes, options) { + super(attributes, options); + this.urlRoot = '/test'; + } + + sync(method, m, options) { + Object.assign(options, { specialSync: true }); + return super.sync(method, m, options); + } + } + + const model = new SpecialSyncModel({ id: 'id' }); + const onSuccess = function (m, response, options) { assert.ok(options.specialSync, 'Options were passed correctly to callback'); }; model.destroy({ success: onSuccess }); @@ -803,151 +831,133 @@ ajaxSettings.success(); }); - QUnit.test('non-persisted destroy', function(assert) { + QUnit.test('non-persisted destroy', function (assert) { assert.expect(1); - var a = new Skeletor.Model({foo: 1, bar: 2, baz: 3}); - a.sync = function() { throw 'should not be called'; }; + var a = new Skeletor.Model({ foo: 1, bar: 2, baz: 3 }); + a.sync = function () { + throw 'should not be called'; + }; a.destroy(); assert.ok(true, 'non-persisted model should not call sync'); }); - QUnit.test('validate', function(assert) { + QUnit.test('validate', function (assert) { var lastError; var model = new Skeletor.Model(); - model.validate = function(attrs) { + model.validate = function (attrs) { if (attrs.admin !== this.get('admin')) return "Can't change admin status."; }; - model.on('invalid', function(m, error) { + model.on('invalid', function (m, error) { lastError = error; }); - var result = model.set({a: 100}); + var result = model.set({ a: 100 }); assert.equal(result, model); assert.equal(model.get('a'), 100); assert.equal(lastError, undefined); - result = model.set({admin: true}); + result = model.set({ admin: true }); assert.equal(model.get('admin'), true); - result = model.set({a: 200, admin: false}, {validate: true}); + result = model.set({ a: 200, admin: false }, { validate: true }); assert.equal(lastError, "Can't change admin status."); assert.equal(result, false); assert.equal(model.get('a'), 100); }); - QUnit.test('validate on unset and clear', function(assert) { + QUnit.test('validate on unset and clear', function (assert) { assert.expect(6); var error; - var model = new Skeletor.Model({name: 'One'}); - model.validate = function(attrs) { + var model = new Skeletor.Model({ name: 'One' }); + model.validate = function (attrs) { if (!attrs.name) { error = true; return 'No thanks.'; } }; - model.set({name: 'Two'}); + model.set({ name: 'Two' }); assert.equal(model.get('name'), 'Two'); assert.equal(error, undefined); - model.unset('name', {validate: true}); + model.unset('name', { validate: true }); assert.equal(error, true); assert.equal(model.get('name'), 'Two'); - model.clear({validate: true}); + model.clear({ validate: true }); assert.equal(model.get('name'), 'Two'); delete model.validate; model.clear(); assert.equal(model.get('name'), undefined); }); - QUnit.test('validate with error callback', function(assert) { + QUnit.test('validate with error callback', function (assert) { assert.expect(8); var lastError, boundError; var model = new Skeletor.Model(); - model.validate = function(attrs) { + model.validate = function (attrs) { if (attrs.admin) return "Can't change admin status."; }; - model.on('invalid', function(m, error) { + model.on('invalid', function (m, error) { boundError = true; }); - var result = model.set({a: 100}, {validate: true}); + var result = model.set({ a: 100 }, { validate: true }); assert.equal(result, model); assert.equal(model.get('a'), 100); assert.equal(model.validationError, null); assert.equal(boundError, undefined); - result = model.set({a: 200, admin: true}, {validate: true}); + result = model.set({ a: 200, admin: true }, { validate: true }); assert.equal(result, false); assert.equal(model.get('a'), 100); assert.equal(model.validationError, "Can't change admin status."); assert.equal(boundError, true); }); - QUnit.test('defaults always extend attrs (#459)', function(assert) { + QUnit.test('defaults always extend attrs (#459)', function (assert) { assert.expect(2); - var Defaulted = Skeletor.Model.extend({ - defaults: {one: 1}, - initialize: function(attrs, opts) { + class Defaulted extends Skeletor.Model { + defaults() { + return { one: 1 }; + } + + initialize(attrs, opts) { assert.equal(this.attributes.one, 1); } - }); + } var providedattrs = new Defaulted({}); var emptyattrs = new Defaulted(); }); - QUnit.test('Inherit class properties', function(assert) { - assert.expect(6); - var Parent = Skeletor.Model.extend({ - instancePropSame: function() {}, - instancePropDiff: function() {} - }, { - classProp: function() {} - }); - var Child = Parent.extend({ - instancePropDiff: function() {} - }); - - var adult = new Parent(); - var kid = new Child(); - - assert.equal(Child.classProp, Parent.classProp); - assert.notEqual(Child.classProp, undefined); - - assert.equal(kid.instancePropSame, adult.instancePropSame); - assert.notEqual(kid.instancePropSame, undefined); - - assert.notEqual(Child.prototype.instancePropDiff, Parent.prototype.instancePropDiff); - assert.notEqual(Child.prototype.instancePropDiff, undefined); - }); - - QUnit.test("Nested change events don't clobber previous attributes", function(assert) { + QUnit.test("Nested change events don't clobber previous attributes", function (assert) { assert.expect(4); new Skeletor.Model() - .on('change:state', function(m, newState) { - assert.equal(m.previous('state'), undefined); - assert.equal(newState, 'hello'); - // Fire a nested change event. - m.set({other: 'whatever'}); - }) - .on('change:state', function(m, newState) { - assert.equal(m.previous('state'), undefined); - assert.equal(newState, 'hello'); - }) - .set({state: 'hello'}); - }); - - QUnit.test('hasChanged/set should use same comparison', function(assert) { + .on('change:state', function (m, newState) { + assert.equal(m.previous('state'), undefined); + assert.equal(newState, 'hello'); + // Fire a nested change event. + m.set({ other: 'whatever' }); + }) + .on('change:state', function (m, newState) { + assert.equal(m.previous('state'), undefined); + assert.equal(newState, 'hello'); + }) + .set({ state: 'hello' }); + }); + + QUnit.test('hasChanged/set should use same comparison', function (assert) { assert.expect(2); - var changed = 0, model = new Skeletor.Model({a: null}); - model.on('change', function() { - assert.ok(this.hasChanged('a')); - }) - .on('change:a', function() { - changed++; - }) - .set({a: undefined}); + var changed = 0, + model = new Skeletor.Model({ a: null }); + model + .on('change', function () { + assert.ok(this.hasChanged('a')); + }) + .on('change:a', function () { + changed++; + }) + .set({ a: undefined }); assert.equal(changed, 1); }); - QUnit.test('#582, #425, change:attribute callbacks should fire after all changes have occurred', function(assert) { + QUnit.test('#582, #425, change:attribute callbacks should fire after all changes have occurred', function (assert) { assert.expect(9); var model = new Skeletor.Model(); - var assertion = function() { + var assertion = function () { assert.equal(model.get('a'), 'a'); assert.equal(model.get('b'), 'b'); assert.equal(model.get('c'), 'c'); @@ -957,117 +967,125 @@ model.on('change:b', assertion); model.on('change:c', assertion); - model.set({a: 'a', b: 'b', c: 'c'}); + model.set({ a: 'a', b: 'b', c: 'c' }); }); - QUnit.test('#871, set with attributes property', function(assert) { + QUnit.test('#871, set with attributes property', function (assert) { assert.expect(1); var model = new Skeletor.Model(); - model.set({attributes: true}); + model.set({ attributes: true }); assert.ok(model.has('attributes')); }); - QUnit.test('set value regardless of equality/change', function(assert) { + QUnit.test('set value regardless of equality/change', function (assert) { assert.expect(1); - var model = new Skeletor.Model({x: []}); + var model = new Skeletor.Model({ x: [] }); var a = []; - model.set({x: a}); + model.set({ x: a }); assert.ok(model.get('x') === a); }); - QUnit.test('set same value does not trigger change', function(assert) { + QUnit.test('set same value does not trigger change', function (assert) { assert.expect(0); - var model = new Skeletor.Model({x: 1}); - model.on('change change:x', function() { assert.ok(false); }); - model.set({x: 1}); - model.set({x: 1}); + var model = new Skeletor.Model({ x: 1 }); + model.on('change change:x', function () { + assert.ok(false); + }); + model.set({ x: 1 }); + model.set({ x: 1 }); }); - QUnit.test('unset does not fire a change for undefined attributes', function(assert) { + QUnit.test('unset does not fire a change for undefined attributes', function (assert) { assert.expect(0); - var model = new Skeletor.Model({x: undefined}); - model.on('change:x', function(){ assert.ok(false); }); + var model = new Skeletor.Model({ x: undefined }); + model.on('change:x', function () { + assert.ok(false); + }); model.unset('x'); }); - QUnit.test('set: undefined values', function(assert) { + QUnit.test('set: undefined values', function (assert) { assert.expect(1); - var model = new Skeletor.Model({x: undefined}); + var model = new Skeletor.Model({ x: undefined }); assert.ok('x' in model.attributes); }); - QUnit.test('hasChanged works outside of change events, and true within', function(assert) { + QUnit.test('hasChanged works outside of change events, and true within', function (assert) { assert.expect(6); - var model = new Skeletor.Model({x: 1}); - model.on('change:x', function() { + var model = new Skeletor.Model({ x: 1 }); + model.on('change:x', function () { assert.ok(model.hasChanged('x')); assert.equal(model.get('x'), 1); }); - model.set({x: 2}, {silent: true}); + model.set({ x: 2 }, { silent: true }); assert.ok(model.hasChanged()); assert.equal(model.hasChanged('x'), true); - model.set({x: 1}); + model.set({ x: 1 }); assert.ok(model.hasChanged()); assert.equal(model.hasChanged('x'), true); }); - QUnit.test('hasChanged gets cleared on the following set', function(assert) { + QUnit.test('hasChanged gets cleared on the following set', function (assert) { assert.expect(4); var model = new Skeletor.Model(); - model.set({x: 1}); + model.set({ x: 1 }); assert.ok(model.hasChanged()); - model.set({x: 1}); + model.set({ x: 1 }); assert.ok(!model.hasChanged()); - model.set({x: 2}); + model.set({ x: 2 }); assert.ok(model.hasChanged()); model.set({}); assert.ok(!model.hasChanged()); }); - QUnit.test('save with `wait` succeeds without `validate`', function(assert) { + QUnit.test('save with `wait` succeeds without `validate`', function (assert) { assert.expect(1); const model = new Skeletor.Model(); sinon.spy(model, 'sync'); model.url = '/test'; - model.save({x: 1}, {wait: true}); + model.save({ x: 1 }, { wait: true }); const syncArgs = model.sync.lastCall.args; assert.ok(syncArgs[1] === model); model.sync.restore(); }); - QUnit.test("save without `wait` doesn't set invalid attributes", function(assert) { + QUnit.test("save without `wait` doesn't set invalid attributes", function (assert) { var model = new Skeletor.Model(); - model.validate = function() { return 1; }; - model.save({a: 1}); + model.validate = function () { + return 1; + }; + model.save({ a: 1 }); assert.equal(model.get('a'), undefined); }); - QUnit.test("save doesn't validate twice", function(assert) { + QUnit.test("save doesn't validate twice", function (assert) { var model = new Skeletor.Model(); var times = 0; - model.sync = function() {}; - model.validate = function() { ++times; }; + model.sync = function () {}; + model.validate = function () { + ++times; + }; model.save({}); assert.equal(times, 1); }); - QUnit.test('`hasChanged` for falsey keys', function(assert) { + QUnit.test('`hasChanged` for falsey keys', function (assert) { assert.expect(2); var model = new Skeletor.Model(); - model.set({x: true}, {silent: true}); + model.set({ x: true }, { silent: true }); assert.ok(!model.hasChanged(0)); assert.ok(!model.hasChanged('')); }); - QUnit.test('`previous` for falsey keys', function(assert) { + QUnit.test('`previous` for falsey keys', function (assert) { assert.expect(2); - var model = new Skeletor.Model({'0': true, '': true}); - model.set({'0': false, '': false}, {silent: true}); + var model = new Skeletor.Model({ '0': true, '': true }); + model.set({ '0': false, '': false }, { silent: true }); assert.equal(model.previous(0), true); assert.equal(model.previous(''), true); }); - QUnit.test('`save` with `wait` sends correct attributes', function(assert) { + QUnit.test('`save` with `wait` sends correct attributes', function (assert) { assert.expect(5); let changed = 0; const model = new Skeletor.Model({ x: 1, y: 2 }); @@ -1084,179 +1102,198 @@ assert.equal(changed, 1); }); - QUnit.test("a failed `save` with `wait` doesn't leave attributes behind", function(assert) { + QUnit.test("a failed `save` with `wait` doesn't leave attributes behind", function (assert) { assert.expect(1); const model = new Skeletor.Model(); model.url = '/test'; - model.save({x: 1}, {wait: true}); + model.save({ x: 1 }, { wait: true }); assert.equal(model.get('x'), undefined); }); - QUnit.test('#1030 - `save` with `wait` results in correct attributes if success is called during sync', function(assert) { - assert.expect(2); - const model = new Skeletor.Model({x: 1, y: 2}); - model.sync = function(method, m, options) { - options.success(); - }; - model.on('change:x', function() { assert.ok(true); }); - model.save({x: 3}, {wait: true}); - assert.equal(model.get('x'), 3); - }); + QUnit.test( + '#1030 - `save` with `wait` results in correct attributes if success is called during sync', + function (assert) { + assert.expect(2); + const model = new Skeletor.Model({ x: 1, y: 2 }); + model.sync = function (method, m, options) { + options.success(); + }; + model.on('change:x', function () { + assert.ok(true); + }); + model.save({ x: 3 }, { wait: true }); + assert.equal(model.get('x'), 3); + }, + ); - QUnit.test('save with wait validates attributes', function(assert) { + QUnit.test('save with wait validates attributes', function (assert) { const model = new Skeletor.Model(); model.url = '/test'; - model.validate = function() { assert.ok(true); }; - model.save({x: 1}, {wait: true}); + model.validate = function () { + assert.ok(true); + }; + model.save({ x: 1 }, { wait: true }); }); - QUnit.test('save turns on parse flag', function(assert) { - const Model = Skeletor.Model.extend({ - sync: function(method, m, options) { assert.ok(options.parse); } - }); + QUnit.test('save turns on parse flag', function (assert) { + class Model extends Skeletor.Model { + sync(method, m, options) { + assert.ok(options.parse); + } + } new Model().save(); }); - QUnit.test("nested `set` during `'change:attr'`", function(assert) { + QUnit.test("nested `set` during `'change:attr'`", function (assert) { assert.expect(2); let events = []; const model = new Skeletor.Model(); - model.on('all', function(event) { events.push(event); }); - model.on('change', function() { - model.set({z: true}, {silent: true}); + model.on('all', function (event) { + events.push(event); + }); + model.on('change', function () { + model.set({ z: true }, { silent: true }); }); - model.on('change:x', function() { - model.set({y: true}); + model.on('change:x', function () { + model.set({ y: true }); }); - model.set({x: true}); + model.set({ x: true }); assert.deepEqual(events, ['change:y', 'change:x', 'change']); events = []; - model.set({z: true}); + model.set({ z: true }); assert.deepEqual(events, []); }); - QUnit.test('nested `change` only fires once', function(assert) { + QUnit.test('nested `change` only fires once', function (assert) { assert.expect(1); const model = new Skeletor.Model(); - model.on('change', function() { + model.on('change', function () { assert.ok(true); - model.set({x: true}); + model.set({ x: true }); }); - model.set({x: true}); + model.set({ x: true }); }); - QUnit.test("nested `set` during `'change'`", function(assert) { + QUnit.test("nested `set` during `'change'`", function (assert) { assert.expect(6); let count = 0; const model = new Skeletor.Model(); - model.on('change', function() { + model.on('change', function () { switch (count++) { case 0: - assert.deepEqual(this.changedAttributes(), {x: true}); + assert.deepEqual(this.changedAttributes(), { x: true }); assert.equal(model.previous('x'), undefined); - model.set({y: true}); + model.set({ y: true }); break; case 1: - assert.deepEqual(this.changedAttributes(), {x: true, y: true}); + assert.deepEqual(this.changedAttributes(), { x: true, y: true }); assert.equal(model.previous('x'), undefined); - model.set({z: true}); + model.set({ z: true }); break; case 2: - assert.deepEqual(this.changedAttributes(), {x: true, y: true, z: true}); + assert.deepEqual(this.changedAttributes(), { x: true, y: true, z: true }); assert.equal(model.previous('y'), undefined); break; default: assert.ok(false); } }); - model.set({x: true}); + model.set({ x: true }); }); - QUnit.test('nested `change` with silent', function(assert) { + QUnit.test('nested `change` with silent', function (assert) { assert.expect(3); let count = 0; const model = new Skeletor.Model(); - model.on('change:y', function() { assert.ok(false); }); - model.on('change', function() { + model.on('change:y', function () { + assert.ok(false); + }); + model.on('change', function () { switch (count++) { case 0: - assert.deepEqual(this.changedAttributes(), {x: true}); - model.set({y: true}, {silent: true}); - model.set({z: true}); + assert.deepEqual(this.changedAttributes(), { x: true }); + model.set({ y: true }, { silent: true }); + model.set({ z: true }); break; case 1: - assert.deepEqual(this.changedAttributes(), {x: true, y: true, z: true}); + assert.deepEqual(this.changedAttributes(), { x: true, y: true, z: true }); break; case 2: - assert.deepEqual(this.changedAttributes(), {z: false}); + assert.deepEqual(this.changedAttributes(), { z: false }); break; default: assert.ok(false); } }); - model.set({x: true}); - model.set({z: false}); + model.set({ x: true }); + model.set({ z: false }); }); - QUnit.test('nested `change:attr` with silent', function(assert) { + QUnit.test('nested `change:attr` with silent', function (assert) { assert.expect(0); const model = new Skeletor.Model(); - model.on('change:y', function(){ assert.ok(false); }); - model.on('change', function() { - model.set({y: true}, {silent: true}); - model.set({z: true}); + model.on('change:y', function () { + assert.ok(false); + }); + model.on('change', function () { + model.set({ y: true }, { silent: true }); + model.set({ z: true }); }); - model.set({x: true}); + model.set({ x: true }); }); - QUnit.test('multiple nested changes with silent', function(assert) { + QUnit.test('multiple nested changes with silent', function (assert) { assert.expect(1); const model = new Skeletor.Model(); - model.on('change:x', function() { - model.set({y: 1}, {silent: true}); - model.set({y: 2}); + model.on('change:x', function () { + model.set({ y: 1 }, { silent: true }); + model.set({ y: 2 }); }); - model.on('change:y', function(m, val) { + model.on('change:y', function (m, val) { assert.equal(val, 2); }); - model.set({x: true}); + model.set({ x: true }); }); - QUnit.test('multiple nested changes with silent', function(assert) { + QUnit.test('multiple nested changes with silent', function (assert) { assert.expect(1); const changes = []; const model = new Skeletor.Model(); - model.on('change:b', function(m, val) { changes.push(val); }); - model.on('change', function() { - model.set({b: 1}); + model.on('change:b', function (m, val) { + changes.push(val); + }); + model.on('change', function () { + model.set({ b: 1 }); }); - model.set({b: 0}); + model.set({ b: 0 }); assert.deepEqual(changes, [0, 1]); }); - QUnit.test('basic silent change semantics', function(assert) { + QUnit.test('basic silent change semantics', function (assert) { assert.expect(1); const model = new Skeletor.Model(); - model.set({x: 1}); - model.on('change', function(){ assert.ok(true); }); - model.set({x: 2}, {silent: true}); - model.set({x: 1}); + model.set({ x: 1 }); + model.on('change', function () { + assert.ok(true); + }); + model.set({ x: 2 }, { silent: true }); + model.set({ x: 1 }); }); - QUnit.test('nested set multiple times', function(assert) { + QUnit.test('nested set multiple times', function (assert) { assert.expect(1); const model = new Skeletor.Model(); - model.on('change:b', function() { + model.on('change:b', function () { assert.ok(true); }); - model.on('change:a', function() { - model.set({b: true}); - model.set({b: true}); + model.on('change:a', function () { + model.set({ b: true }); + model.set({ b: true }); }); - model.set({a: true}); + model.set({ a: true }); }); - QUnit.test('#1122 - clear does not alter options.', function(assert) { + QUnit.test('#1122 - clear does not alter options.', function (assert) { assert.expect(1); const model = new Skeletor.Model(); const options = {}; @@ -1264,7 +1301,7 @@ assert.ok(!options.unset); }); - QUnit.test('#1122 - unset does not alter options.', function(assert) { + QUnit.test('#1122 - unset does not alter options.', function (assert) { assert.expect(1); const model = new Skeletor.Model(); const options = {}; @@ -1272,216 +1309,262 @@ assert.ok(!options.unset); }); - QUnit.test('#1355 - `options` is passed to success callbacks', function(assert) { + QUnit.test('#1355 - `options` is passed to success callbacks', function (assert) { assert.expect(3); const model = new Skeletor.Model(); const opts = { - success: function( m, resp, options ) { + success(m, resp, options) { assert.ok(options); - } + }, }; - model.sync = function(method, m, options) { + model.sync = function (method, m, options) { options.success(); }; - model.save({id: 1}, opts); + model.save({ id: 1 }, opts); model.fetch(opts); model.destroy(opts); }); - QUnit.test("#1412 - Trigger 'sync' event.", function(assert) { + QUnit.test("#1412 - Trigger 'sync' event.", function (assert) { assert.expect(3); - const model = new Skeletor.Model({id: 1}); - model.sync = function(method, m, options) { options.success(); }; - model.on('sync', function(){ assert.ok(true); }); + const model = new Skeletor.Model({ id: 1 }); + model.sync = function (method, m, options) { + options.success(); + }; + model.on('sync', function () { + assert.ok(true); + }); model.fetch(); model.save(); model.destroy(); }); - QUnit.test('#1365 - Destroy: New models execute success callback.', function(assert) { + QUnit.test('#1365 - Destroy: New models execute success callback.', function (assert) { const done = assert.async(); assert.expect(2); new Skeletor.Model() - .on('sync', function() { assert.ok(false); }) - .on('destroy', function(){ assert.ok(true); }) - .destroy({success: function(){ - assert.ok(true); - done(); - }}); + .on('sync', function () { + assert.ok(false); + }) + .on('destroy', function () { + assert.ok(true); + }) + .destroy({ + success() { + assert.ok(true); + done(); + }, + }); }); - QUnit.test('#1433 - Save: An invalid model cannot be persisted.', function(assert) { + QUnit.test('#1433 - Save: An invalid model cannot be persisted.', function (assert) { assert.expect(1); var model = new Skeletor.Model(); - model.validate = function(){ return 'invalid'; }; - model.sync = function(){ assert.ok(false); }; + model.validate = function () { + return 'invalid'; + }; + model.sync = function () { + assert.ok(false); + }; assert.strictEqual(model.save(), false); }); - QUnit.test("#1377 - Save without attrs triggers 'error'.", function(assert) { + QUnit.test("#1377 - Save without attrs triggers 'error'.", function (assert) { assert.expect(1); - var Model = Skeletor.Model.extend({ - url: '/test/', - sync: function(method, m, options){ options.success(); }, - validate: function(){ return 'invalid'; } - }); - var model = new Model({id: 1}); - model.on('invalid', function(){ assert.ok(true); }); + + class Model extends Skeletor.Model { + constructor(attributes, options) { + super(attributes, options); + this.url = '/test/'; + } + sync(method, m, options) { + options.success(); + } + validate() { + return 'invalid'; + } + } + + const model = new Model({ id: 1 }); + model.on('invalid', () => assert.ok(true)); model.save(); }); - QUnit.test('#1545 - `undefined` can be passed to a model constructor without coersion', function(assert) { - var Model = Skeletor.Model.extend({ - defaults: {one: 1}, - initialize: function(attrs, opts) { + QUnit.test('#1545 - `undefined` can be passed to a model constructor without coersion', function (assert) { + class Model extends Skeletor.Model { + defaults() { + return { + one: 1, + }; + } + initialize(attrs, opts) { assert.equal(attrs, undefined); } - }); - var emptyattrs = new Model(); - var undefinedattrs = new Model(undefined); + } + new Model(); // Empty attrs + new Model(undefined); // Undefined attrs }); - QUnit.test('#1478 - Model `save` does not trigger change on unchanged attributes', function(assert) { + QUnit.test('#1478 - Model `save` does not trigger change on unchanged attributes', function (assert) { const done = assert.async(); assert.expect(0); - const Model = Skeletor.Model.extend({ - sync: function(method, m, options) { - setTimeout(function(){ + class Model extends Skeletor.Model { + sync(method, m, options) { + setTimeout(function () { options.success(); done(); }, 0); } - }); - new Model({x: true}) - .on('change:x', function(){ assert.ok(false); }) - .save(null, {wait: true}); - }); + } - QUnit.test('#1664 - Changing from one value, silently to another, back to original triggers a change.', function(assert) { - assert.expect(1); - const model = new Skeletor.Model({x: 1}); - model.on('change:x', function() { assert.ok(true); }); - model.set({x: 2}, {silent: true}); - model.set({x: 3}, {silent: true}); - model.set({x: 1}); + new Model({ x: true }) + .on('change:x', function () { + assert.ok(false); + }) + .save(null, { wait: true }); }); - QUnit.test('#1664 - multiple silent changes nested inside a change event', function(assert) { + QUnit.test( + '#1664 - Changing from one value, silently to another, back to original triggers a change.', + function (assert) { + assert.expect(1); + const model = new Skeletor.Model({ x: 1 }); + model.on('change:x', function () { + assert.ok(true); + }); + model.set({ x: 2 }, { silent: true }); + model.set({ x: 3 }, { silent: true }); + model.set({ x: 1 }); + }, + ); + + QUnit.test('#1664 - multiple silent changes nested inside a change event', function (assert) { assert.expect(2); const changes = []; const model = new Skeletor.Model(); - model.on('change', function() { - model.set({a: 'c'}, {silent: true}); - model.set({b: 2}, {silent: true}); - model.unset('c', {silent: true}); + model.on('change', function () { + model.set({ a: 'c' }, { silent: true }); + model.set({ b: 2 }, { silent: true }); + model.unset('c', { silent: true }); }); - model.on('change:a change:b change:c', function(m, val) { changes.push(val); }); - model.set({a: 'a', b: 1, c: 'item'}); + model.on('change:a change:b change:c', function (m, val) { + changes.push(val); + }); + model.set({ a: 'a', b: 1, c: 'item' }); assert.deepEqual(changes, ['a', 1, 'item']); - assert.deepEqual(model.attributes, {a: 'c', b: 2}); + assert.deepEqual(model.attributes, { a: 'c', b: 2 }); }); - QUnit.test('#1791 - `attributes` is available for `parse`', function(assert) { - var Model = Skeletor.Model.extend({ - parse: function() { this.has('a'); } // shouldn't throw an error - }); - var model = new Model(null, {parse: true}); + QUnit.test('#1791 - `attributes` is available for `parse`', function (assert) { + class Model extends Skeletor.Model { + parse() { + this.has('a'); + } // shouldn't throw an error + } + new Model(null, { parse: true }); assert.expect(0); }); - QUnit.test('silent changes in last `change` event back to original triggers change', function(assert) { + QUnit.test('silent changes in last `change` event back to original triggers change', function (assert) { assert.expect(2); const changes = []; const model = new Skeletor.Model(); - model.on('change:a change:b change:c', function(m, val) { changes.push(val); }); - model.on('change', function() { - model.set({a: 'c'}, {silent: true}); + model.on('change:a change:b change:c', function (m, val) { + changes.push(val); + }); + model.on('change', function () { + model.set({ a: 'c' }, { silent: true }); }); - model.set({a: 'a'}); + model.set({ a: 'a' }); assert.deepEqual(changes, ['a']); - model.set({a: 'a'}); + model.set({ a: 'a' }); assert.deepEqual(changes, ['a', 'a']); }); - QUnit.test('#1943 change calculations should use _.isEqual', function(assert) { - const model = new Skeletor.Model({a: {key: 'value'}}); - model.set('a', {key: 'value'}, {silent: true}); + QUnit.test('#1943 change calculations should use _.isEqual', function (assert) { + const model = new Skeletor.Model({ a: { key: 'value' } }); + model.set('a', { key: 'value' }, { silent: true }); assert.equal(model.changedAttributes(), false); }); - QUnit.test('#1964 - final `change` event is always fired, regardless of interim changes', function(assert) { + QUnit.test('#1964 - final `change` event is always fired, regardless of interim changes', function (assert) { assert.expect(1); const model = new Skeletor.Model(); - model.on('change:property', function() { + model.on('change:property', function () { model.set('property', 'bar'); }); - model.on('change', function() { + model.on('change', function () { assert.ok(true); }); model.set('property', 'foo'); }); - QUnit.test('isValid', function(assert) { - const model = new Skeletor.Model({valid: true}); - model.validate = function(attrs) { + QUnit.test('isValid', function (assert) { + const model = new Skeletor.Model({ valid: true }); + model.validate = function (attrs) { if (!attrs.valid) return 'invalid'; }; assert.equal(model.isValid(), true); - assert.equal(model.set({valid: false}, {validate: true}), false); + assert.equal(model.set({ valid: false }, { validate: true }), false); assert.equal(model.isValid(), true); - model.set({valid: false}); + model.set({ valid: false }); assert.equal(model.isValid(), false); - assert.ok(!model.set('valid', false, {validate: true})); + assert.ok(!model.set('valid', false, { validate: true })); }); - QUnit.test('#1179 - isValid returns true in the absence of validate.', function(assert) { + QUnit.test('#1179 - isValid returns true in the absence of validate.', function (assert) { assert.expect(1); const model = new Skeletor.Model(); model.validate = null; assert.ok(model.isValid()); }); - QUnit.test('#1961 - Creating a model with {validate:true} will call validate and use the error callback', function(assert) { - var Model = Skeletor.Model.extend({ - validate: function(attrs) { - if (attrs.id === 1) return "This shouldn't happen"; + QUnit.test( + '#1961 - Creating a model with {validate:true} will call validate and use the error callback', + function (assert) { + class Model extends Skeletor.Model { + validate(attrs) { + if (attrs.id === 1) return "This shouldn't happen"; + } } - }); - var model = new Model({id: 1}, {validate: true}); - assert.equal(model.validationError, "This shouldn't happen"); - }); + const model = new Model({ id: 1 }, { validate: true }); + assert.equal(model.validationError, "This shouldn't happen"); + }, + ); - QUnit.test('toJSON receives attrs during save(..., {wait: true})', function(assert) { + QUnit.test('toJSON receives attrs during save(..., {wait: true})', function (assert) { assert.expect(1); - const Model = Skeletor.Model.extend({ - url: '/test', - toJSON: function() { + class Model extends Skeletor.Model { + constructor(attributes, options) { + super(attributes, options); + this.url = '/test'; + } + toJSON() { assert.strictEqual(this.attributes.x, 1); return _.clone(this.attributes); } - }); + } const model = new Model(); - model.save({x: 1}, {wait: true}); + model.save({ x: 1 }, { wait: true }); }); - QUnit.test('#2034 - nested set with silent only triggers one change', function(assert) { + QUnit.test('#2034 - nested set with silent only triggers one change', function (assert) { assert.expect(1); const model = new Skeletor.Model(); - model.on('change', function() { - model.set({b: true}, {silent: true}); + model.on('change', function () { + model.set({ b: true }, { silent: true }); assert.ok(true); }); - model.set({a: true}); + model.set({ a: true }); }); - QUnit.test('#3778 - id will only be updated if it is set', function(assert) { + QUnit.test('#3778 - id will only be updated if it is set', function (assert) { assert.expect(2); - const model = new Skeletor.Model({id: 1}); + const model = new Skeletor.Model({ id: 1 }); model.id = 2; - model.set({foo: 'bar'}); + model.set({ foo: 'bar' }); assert.equal(model.id, 2); - model.set({id: 3}); + model.set({ id: 3 }); assert.equal(model.id, 3); }); - })(QUnit); diff --git a/test/router.js b/test/router.js deleted file mode 100644 index 3c99e5c2..00000000 --- a/test/router.js +++ /dev/null @@ -1,1079 +0,0 @@ -(function(QUnit) { - - var router = null; - var location = null; - var lastRoute = null; - var lastArgs = []; - - var onRoute = function(routerParam, route, args) { - lastRoute = route; - lastArgs = args; - }; - - var Location = function(href) { - this.replace(href); - }; - - _.extend(Location.prototype, { - - parser: document.createElement('a'), - - replace: function(href) { - this.parser.href = href; - _.extend(this, _.pick(this.parser, - 'href', - 'hash', - 'host', - 'search', - 'fragment', - 'pathname', - 'protocol' - )); - - // In IE, anchor.pathname does not contain a leading slash though - // window.location.pathname does. - if (!(/^\//).test(this.pathname)) this.pathname = '/' + this.pathname; - }, - - toString: function() { - return this.href; - } - - }); - - QUnit.module('Skeletor.Router', { - - beforeEach: function() { - location = new Location('http://example.com'); - const history = _.extend(new Skeletor.History(), {location: location}); - history.interval = 9; - history.start({pushState: false}); - router = new Router({testing: 101, history}); - lastRoute = null; - lastArgs = []; - router.history.on('route', onRoute); - }, - - afterEach: function() { - router.history.stop(); - router.history.off('route', onRoute); - } - - }); - - var ExternalObject = { - value: 'unset', - - routingFunction: function(value) { - this.value = value; - } - }; - ExternalObject.routingFunction = _.bind(ExternalObject.routingFunction, ExternalObject); - - var Router = Skeletor.Router.extend({ - - count: 0, - - routes: { - 'noCallback': 'noCallback', - 'counter': 'counter', - 'search/:query': 'search', - 'search/:query/p:page': 'search', - 'charñ': 'charUTF', - 'char%C3%B1': 'charEscaped', - 'contacts': 'contacts', - 'contacts/new': 'newContact', - 'contacts/:id': 'loadContact', - 'route-event/:arg': 'routeEvent', - 'optional(/:item)': 'optionalItem', - 'named/optional/(y:z)': 'namedOptional', - 'splat/*args/end': 'splat', - ':repo/compare/*from...*to': 'github', - 'decode/:named/*splat': 'decode', - '*first/complex-*part/*rest': 'complex', - 'query/:entity': 'query', - 'function/:value': ExternalObject.routingFunction, - '*anything': 'anything' - }, - - preinitialize: function(options) { - this.testpreinit = 'foo'; - }, - - initialize: function(options) { - this.testing = options.testing; - this.route('implicit', 'implicit'); - }, - - counter: function() { - this.count++; - }, - - implicit: function() { - this.count++; - }, - - search: function(query, page) { - this.query = query; - this.page = page; - }, - - charUTF: function() { - this.charType = 'UTF'; - }, - - charEscaped: function() { - this.charType = 'escaped'; - }, - - contacts: function() { - this.contact = 'index'; - }, - - newContact: function() { - this.contact = 'new'; - }, - - loadContact: function() { - this.contact = 'load'; - }, - - optionalItem: function(arg) { - this.arg = arg !== undefined ? arg : null; - }, - - splat: function(args) { - this.args = args; - }, - - github: function(repo, from, to) { - this.repo = repo; - this.from = from; - this.to = to; - }, - - complex: function(first, part, rest) { - this.first = first; - this.part = part; - this.rest = rest; - }, - - query: function(entity, args) { - this.entity = entity; - this.queryArgs = args; - }, - - anything: function(whatever) { - this.anything = whatever; - }, - - namedOptional: function(z) { - this.z = z; - }, - - decode: function(named, path) { - this.named = named; - this.path = path; - }, - - routeEvent: function(arg) { - } - - }); - - QUnit.test('initialize', function(assert) { - assert.expect(1); - assert.equal(router.testing, 101); - }); - - QUnit.test('preinitialize', function(assert) { - assert.expect(1); - assert.equal(router.testpreinit, 'foo'); - }); - - QUnit.test('routes (simple)', function(assert) { - assert.expect(4); - location.replace('http://example.com#search/news'); - router.history.checkUrl(); - assert.equal(router.query, 'news'); - assert.equal(router.page, undefined); - assert.equal(lastRoute, 'search'); - assert.equal(lastArgs[0], 'news'); - }); - - QUnit.test('routes (simple, but unicode)', function(assert) { - assert.expect(4); - location.replace('http://example.com#search/тест'); - router.history.checkUrl(); - assert.equal(router.query, 'тест'); - assert.equal(router.page, undefined); - assert.equal(lastRoute, 'search'); - assert.equal(lastArgs[0], 'тест'); - }); - - QUnit.test('routes (two part)', function(assert) { - assert.expect(2); - location.replace('http://example.com#search/nyc/p10'); - router.history.checkUrl(); - assert.equal(router.query, 'nyc'); - assert.equal(router.page, '10'); - }); - - QUnit.test('routes via navigate', function(assert) { - assert.expect(2); - router.history.navigate('search/manhattan/p20', {trigger: true}); - assert.equal(router.query, 'manhattan'); - assert.equal(router.page, '20'); - }); - - QUnit.test('routes via navigate with params', function(assert) { - assert.expect(1); - router.history.navigate('query/test?a=b', {trigger: true}); - assert.equal(router.queryArgs, 'a=b'); - }); - - QUnit.test('routes via navigate for backwards-compatibility', function(assert) { - assert.expect(2); - router.history.navigate('search/manhattan/p20', true); - assert.equal(router.query, 'manhattan'); - assert.equal(router.page, '20'); - }); - - QUnit.test('reports matched route via nagivate', function(assert) { - assert.expect(1); - assert.ok(router.history.navigate('search/manhattan/p20', true)); - }); - - QUnit.test('route precedence via navigate', function(assert) { - assert.expect(6); - - // Check both 0.9.x and backwards-compatibility options - _.each([{trigger: true}, true], function(options) { - router.history.navigate('contacts', options); - assert.equal(router.contact, 'index'); - router.history.navigate('contacts/new', options); - assert.equal(router.contact, 'new'); - router.history.navigate('contacts/foo', options); - assert.equal(router.contact, 'load'); - }); - }); - - QUnit.test('loadUrl is not called for identical routes.', function(assert) { - assert.expect(0); - router.history.loadUrl = function() { assert.ok(false); }; - location.replace('http://example.com#route'); - router.history.navigate('route'); - router.history.navigate('/route'); - router.history.navigate('/route'); - }); - - QUnit.test('use implicit callback if none provided', function(assert) { - assert.expect(1); - router.count = 0; - router.navigate('implicit', {trigger: true}); - assert.equal(router.count, 1); - }); - - QUnit.test('routes via navigate with {replace: true}', function(assert) { - assert.expect(1); - location.replace('http://example.com#start_here'); - router.history.checkUrl(); - location.replace = function(href) { - assert.strictEqual(href, new Location('http://example.com#end_here').href); - }; - router.history.navigate('end_here', {replace: true}); - }); - - QUnit.test('routes (splats)', function(assert) { - assert.expect(1); - location.replace('http://example.com#splat/long-list/of/splatted_99args/end'); - router.history.checkUrl(); - assert.equal(router.args, 'long-list/of/splatted_99args'); - }); - - QUnit.test('routes (github)', function(assert) { - assert.expect(3); - location.replace('http://example.com#backbone/compare/1.0...braddunbar:with/slash'); - router.history.checkUrl(); - assert.equal(router.repo, 'backbone'); - assert.equal(router.from, '1.0'); - assert.equal(router.to, 'braddunbar:with/slash'); - }); - - QUnit.test('routes (optional)', function(assert) { - assert.expect(2); - location.replace('http://example.com#optional'); - router.history.checkUrl(); - assert.ok(!router.arg); - location.replace('http://example.com#optional/thing'); - router.history.checkUrl(); - assert.equal(router.arg, 'thing'); - }); - - QUnit.test('routes (complex)', function(assert) { - assert.expect(3); - location.replace('http://example.com#one/two/three/complex-part/four/five/six/seven'); - router.history.checkUrl(); - assert.equal(router.first, 'one/two/three'); - assert.equal(router.part, 'part'); - assert.equal(router.rest, 'four/five/six/seven'); - }); - - QUnit.test('routes (query)', function(assert) { - assert.expect(5); - location.replace('http://example.com#query/mandel?a=b&c=d'); - router.history.checkUrl(); - assert.equal(router.entity, 'mandel'); - assert.equal(router.queryArgs, 'a=b&c=d'); - assert.equal(lastRoute, 'query'); - assert.equal(lastArgs[0], 'mandel'); - assert.equal(lastArgs[1], 'a=b&c=d'); - }); - - QUnit.test('routes (anything)', function(assert) { - assert.expect(1); - location.replace('http://example.com#doesnt-match-a-route'); - router.history.checkUrl(); - assert.equal(router.anything, 'doesnt-match-a-route'); - }); - - QUnit.test('routes (function)', function(assert) { - assert.expect(3); - router.on('route', function(name) { - assert.ok(name === ''); - }); - assert.equal(ExternalObject.value, 'unset'); - location.replace('http://example.com#function/set'); - router.history.checkUrl(); - assert.equal(ExternalObject.value, 'set'); - }); - - QUnit.test('Decode named parameters, not splats.', function(assert) { - assert.expect(2); - location.replace('http://example.com#decode/a%2Fb/c%2Fd/e'); - router.history.checkUrl(); - assert.strictEqual(router.named, 'a/b'); - assert.strictEqual(router.path, 'c/d/e'); - }); - - QUnit.test('fires event when router doesn\'t have callback on it', function(assert) { - assert.expect(1); - router.on('route:noCallback', function() { assert.ok(true); }); - location.replace('http://example.com#noCallback'); - router.history.checkUrl(); - }); - - QUnit.test('No events are triggered if #execute returns false.', function(assert) { - assert.expect(1); - const MyRouter = Skeletor.Router.extend({ - routes: { - foo: function() { - assert.ok(true); - } - }, - execute: function(callback, args) { - callback.apply(this, args); - return false; - } - }); - - const history = _.extend(router.history, {location: location}); - const myRouter = new MyRouter({history}); - - myRouter.on('route route:foo', function() { - assert.ok(false); - }); - - myRouter.history.on('route', function() { - assert.ok(false); - }); - - location.replace('http://example.com#foo'); - myRouter.history.checkUrl(); - }); - - QUnit.test('#933, #908 - leading slash', function(assert) { - assert.expect(2); - location.replace('http://example.com/root/foo'); - - router.history.stop(); - router.history = _.extend(new Skeletor.History(), {location: location}); - router.history.start({root: '/root', hashChange: false, silent: true}); - assert.strictEqual(router.history.getFragment(), 'foo'); - - router.history.stop(); - router.history = _.extend(new Skeletor.History(), {location: location}); - router.history.start({root: '/root/', hashChange: false, silent: true}); - assert.strictEqual(router.history.getFragment(), 'foo'); - }); - - QUnit.test('#967 - Route callback gets passed encoded values.', function(assert) { - assert.expect(3); - var route = 'has%2Fslash/complex-has%23hash/has%20space'; - router.history.navigate(route, {trigger: true}); - assert.strictEqual(router.first, 'has/slash'); - assert.strictEqual(router.part, 'has#hash'); - assert.strictEqual(router.rest, 'has space'); - }); - - QUnit.test('correctly handles URLs with % (#868)', function(assert) { - assert.expect(3); - location.replace('http://example.com#search/fat%3A1.5%25'); - router.history.checkUrl(); - location.replace('http://example.com#search/fat'); - router.history.checkUrl(); - assert.equal(router.query, 'fat'); - assert.equal(router.page, undefined); - assert.equal(lastRoute, 'search'); - }); - - QUnit.test('#2666 - Hashes with UTF8 in them.', function(assert) { - assert.expect(2); - router.history.navigate('charñ', {trigger: true}); - assert.equal(router.charType, 'UTF'); - router.history.navigate('char%C3%B1', {trigger: true}); - assert.equal(router.charType, 'UTF'); - }); - - QUnit.test('#1185 - Use pathname when hashChange is not wanted.', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/path/name#hash'); - router.history = _.extend(new Skeletor.History(), {location: location}); - router.history.start({hashChange: false}); - var fragment = router.history.getFragment(); - assert.strictEqual(fragment, location.pathname.replace(/^\//, '')); - }); - - QUnit.test('#1206 - Strip leading slash before location.assign.', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/root/'); - router.history = _.extend(new Skeletor.History(), {location: location}); - router.history.start({hashChange: false, root: '/root/'}); - location.assign = function(pathname) { - assert.strictEqual(pathname, '/root/fragment'); - }; - router.history.navigate('/fragment'); - }); - - QUnit.test('#1387 - Root fragment without trailing slash.', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/root'); - router.history = _.extend(new Skeletor.History(), {location: location}); - router.history.start({hashChange: false, root: '/root/', silent: true}); - assert.strictEqual(router.history.getFragment(), ''); - }); - - QUnit.test('#1366 - History does not prepend root to fragment.', function(assert) { - assert.expect(2); - router.history.stop(); - location.replace('http://example.com/root/'); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function(state, title, url) { - assert.strictEqual(url, '/root/x'); - } - } - }); - router.history.start({ - root: '/root/', - pushState: true, - hashChange: false - }); - router.history.navigate('x'); - assert.strictEqual(router.history.fragment, 'x'); - }); - - QUnit.test('Normalize root.', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/root'); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function(state, title, url) { - assert.strictEqual(url, '/root/fragment'); - } - } - }); - router.history.start({ - pushState: true, - root: '/root', - hashChange: false - }); - router.history.navigate('fragment'); - }); - - QUnit.test('Normalize root.', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/root#fragment'); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function(state, title, url) {}, - replaceState: function(state, title, url) { - assert.strictEqual(url, '/root/fragment'); - } - } - }); - router.history.start({ - pushState: true, - root: '/root' - }); - }); - - QUnit.test('Normalize root.', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/root'); - router.history = _.extend(new Skeletor.History(), {location: location}); - router.history.loadUrl = function() { assert.ok(true); }; - router.history.start({ - pushState: true, - root: '/root' - }); - }); - - QUnit.test('Normalize root - leading slash.', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/root'); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function() {}, - replaceState: function() {} - } - }); - router.history.start({root: 'root'}); - assert.strictEqual(router.history.root, '/root/'); - }); - - QUnit.test('Transition from hashChange to pushState.', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/root#x/y'); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function() {}, - replaceState: function(state, title, url) { - assert.strictEqual(url, '/root/x/y'); - } - } - }); - router.history.start({ - root: 'root', - pushState: true - }); - }); - - QUnit.test('#1619: Router: Normalize empty root', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/'); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function() {}, - replaceState: function() {} - } - }); - router.history.start({root: ''}); - assert.strictEqual(router.history.root, '/'); - }); - - QUnit.test('#1619: Router: nagivate with empty root', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/'); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function(state, title, url) { - assert.strictEqual(url, '/fragment'); - } - } - }); - router.history.start({ - pushState: true, - root: '', - hashChange: false - }); - router.history.navigate('fragment'); - }); - - QUnit.test('Transition from pushState to hashChange.', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/root/x/y?a=b'); - location.replace = function(url) { - assert.strictEqual(url, '/root#x/y?a=b'); - }; - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: null, - replaceState: null - } - }); - router.history.start({ - root: 'root', - pushState: true - }); - }); - - QUnit.test('#1695 - hashChange to pushState with search.', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/root#x/y?a=b'); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function() {}, - replaceState: function(state, title, url) { - assert.strictEqual(url, '/root/x/y?a=b'); - } - } - }); - router.history.start({ - root: 'root', - pushState: true - }); - }); - - QUnit.test('#1746 - Router allows empty route.', function(assert) { - assert.expect(1); - var MyRouter = Skeletor.Router.extend({ - routes: {'': 'empty'}, - empty: function() {}, - route: function(route) { - assert.strictEqual(route, ''); - } - }); - new MyRouter(); - }); - - QUnit.test('#1794 - Trailing space in fragments.', function(assert) { - assert.expect(1); - var history = new Skeletor.History(); - assert.strictEqual(history.getFragment('fragment '), 'fragment'); - }); - - QUnit.test('#1820 - Leading slash and trailing space.', function(assert) { - assert.expect(1); - var history = new Skeletor.History(); - assert.strictEqual(history.getFragment('/fragment '), 'fragment'); - }); - - QUnit.test('#1980 - Optional parameters.', function(assert) { - assert.expect(2); - location.replace('http://example.com#named/optional/y'); - router.history.checkUrl(); - assert.strictEqual(router.z, undefined); - location.replace('http://example.com#named/optional/y123'); - router.history.checkUrl(); - assert.strictEqual(router.z, '123'); - }); - - QUnit.test('#2062 - Trigger "route" event on router instance.', function(assert) { - assert.expect(2); - router.on('route', function(name, args) { - assert.strictEqual(name, 'routeEvent'); - assert.deepEqual(args, ['x', null]); - }); - location.replace('http://example.com#route-event/x'); - router.history.checkUrl(); - }); - - QUnit.test('#2255 - Extend routes by making routes a function.', function(assert) { - assert.expect(1); - var RouterBase = Skeletor.Router.extend({ - routes: function() { - return { - home: 'root', - index: 'index.html' - }; - } - }); - - var RouterExtended = RouterBase.extend({ - routes: function() { - var _super = RouterExtended.__super__.routes; - return _.extend(_super(), {show: 'show', search: 'search'}); - } - }); - - var myRouter = new RouterExtended(); - assert.deepEqual({home: 'root', index: 'index.html', show: 'show', search: 'search'}, myRouter.routes); - }); - - QUnit.test('#2538 - hashChange to pushState only if both requested.', function(assert) { - assert.expect(0); - router.history.stop(); - location.replace('http://example.com/root?a=b#x/y'); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function() {}, - replaceState: function() { assert.ok(false); } - } - }); - router.history.start({ - root: 'root', - pushState: true, - hashChange: false - }); - }); - - QUnit.test('No hash fallback.', function(assert) { - assert.expect(0); - router.history.stop(); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function() {}, - replaceState: function() {} - } - }); - - var MyRouter = Skeletor.Router.extend({ - routes: { - hash: function() { assert.ok(false); } - } - }); - var myRouter = new MyRouter(); - - location.replace('http://example.com/'); - router.history.start({ - pushState: true, - hashChange: false - }); - location.replace('http://example.com/nomatch#hash'); - router.history.checkUrl(); - }); - - QUnit.test('#2656 - No trailing slash on root.', function(assert) { - assert.expect(1); - router.history.stop(); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function(state, title, url) { - assert.strictEqual(url, '/root'); - } - } - }); - location.replace('http://example.com/root/path'); - router.history.start({pushState: true, hashChange: false, root: 'root'}); - router.history.navigate(''); - }); - - QUnit.test('#2656 - No trailing slash on root.', function(assert) { - assert.expect(1); - router.history.stop(); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function(state, title, url) { - assert.strictEqual(url, '/'); - } - } - }); - location.replace('http://example.com/path'); - router.history.start({pushState: true, hashChange: false}); - router.history.navigate(''); - }); - - QUnit.test('#2656 - No trailing slash on root.', function(assert) { - assert.expect(1); - router.history.stop(); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function(state, title, url) { - assert.strictEqual(url, '/root?x=1'); - } - } - }); - location.replace('http://example.com/root/path'); - router.history.start({pushState: true, hashChange: false, root: 'root'}); - router.history.navigate('?x=1'); - }); - - QUnit.test('#2765 - Fragment matching sans query/hash.', function(assert) { - assert.expect(2); - router.history.stop(); - const history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function(state, title, url) { - assert.strictEqual(url, '/path?query#hash'); - } - } - }); - const MyRouter = Skeletor.Router.extend({ - routes: { - path: function() { assert.ok(true); } - } - }); - const myRouter = new MyRouter({history}); - - location.replace('http://example.com/'); - myRouter.history.start({pushState: true, hashChange: false}); - myRouter.history.navigate('path?query#hash', true); - }); - - QUnit.test('Do not decode the search params.', function(assert) { - assert.expect(1); - const MyRouter = Skeletor.Router.extend({ - routes: { - path: function(params) { - assert.strictEqual(params, 'x=y%3Fz'); - } - } - }); - const myRouter = new MyRouter({history: router.history}); - myRouter.history.navigate('path?x=y%3Fz', true); - }); - - QUnit.test('Navigate to a hash url.', function(assert) { - assert.expect(1); - router.history.stop(); - const MyRouter = Skeletor.Router.extend({ - routes: { - path: function(params) { - assert.strictEqual(params, 'x=y'); - } - } - }); - const history = _.extend(new Skeletor.History(), {location: location}); - history.start({pushState: true}); - const myRouter = new MyRouter({history}); - location.replace('http://example.com/path?x=y#hash'); - myRouter.history.checkUrl(); - }); - - QUnit.test('#navigate to a hash url.', function(assert) { - assert.expect(1); - router.history.stop(); - const MyRouter = Skeletor.Router.extend({ - routes: { - path: function(params) { - assert.strictEqual(params, 'x=y'); - } - } - }); - const history = _.extend(new Skeletor.History(), {location: location}); - history.start({pushState: true}); - const myRouter = new MyRouter({history}); - myRouter.history.navigate('path?x=y#hash', true); - }); - - QUnit.test('unicode pathname', function(assert) { - assert.expect(1); - location.replace('http://example.com/myyjä'); - router.history.stop(); - const MyRouter = Skeletor.Router.extend({ - routes: { - myyjä: function() { - assert.ok(true); - } - } - }); - const history = _.extend(new Skeletor.History(), {location: location}); - const myRouter = new MyRouter({history}); - myRouter.history.start({pushState: true}); - }); - - QUnit.test('unicode pathname with % in a parameter', function(assert) { - assert.expect(1); - location.replace('http://example.com/myyjä/foo%20%25%3F%2f%40%25%20bar'); - location.pathname = '/myyj%C3%A4/foo%20%25%3F%2f%40%25%20bar'; - router.history.stop(); - const MyRouter = Skeletor.Router.extend({ - routes: { - 'myyjä/:query': function(query) { - assert.strictEqual(query, 'foo %?/@% bar'); - } - } - }); - const history = _.extend(new Skeletor.History(), {location: location}); - const myRouter = new MyRouter({history}); - myRouter.history.start({pushState: true}); - }); - - QUnit.test('newline in route', function(assert) { - assert.expect(1); - location.replace('http://example.com/stuff%0Anonsense?param=foo%0Abar'); - router.history.stop(); - const MyRouter = Skeletor.Router.extend({ - routes: { - 'stuff\nnonsense': function() { - assert.ok(true); - } - } - }); - const history = _.extend(new Skeletor.History(), {location: location}); - const myRouter = new MyRouter({history}); - myRouter.history.start({pushState: true}); - }); - - QUnit.test('Router#execute receives callback, args, name.', function(assert) { - assert.expect(3); - location.replace('http://example.com#foo/123/bar?x=y'); - router.history.stop(); - const MyRouter = Skeletor.Router.extend({ - routes: {'foo/:id/bar': 'foo'}, - foo: function() {}, - execute: function(callback, args, name) { - assert.strictEqual(callback, this.foo); - assert.deepEqual(args, ['123', 'x=y']); - assert.strictEqual(name, 'foo'); - } - }); - const history = _.extend(new Skeletor.History(), {location: location}); - const myRouter = new MyRouter({history}); - myRouter.history.start(); - }); - - QUnit.test('pushState to hashChange with only search params.', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com?a=b'); - location.replace = function(url) { - assert.strictEqual(url, '/#?a=b'); - }; - router.history = _.extend(new Skeletor.History(), { - location: location, - history: null - }); - router.history.start({pushState: true}); - }); - - QUnit.test('#3123 - History#navigate decodes before comparison.', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/shop/search?keyword=short%20dress'); - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: function() { assert.ok(false); }, - replaceState: function() { assert.ok(false); } - } - }); - router.history.start({pushState: true}); - router.history.navigate('shop/search?keyword=short%20dress', true); - assert.strictEqual(router.history.fragment, 'shop/search?keyword=short dress'); - }); - - QUnit.test('#3175 - Urls in the params', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com#login?a=value&backUrl=https%3A%2F%2Fwww.msn.com%2Fidp%2Fidpdemo%3Fspid%3Dspdemo%26target%3Db'); - const history = _.extend(new Skeletor.History(), {location: location}); - const myRouter = new Skeletor.Router({history}); - myRouter.route('login', function(params) { - assert.strictEqual(params, 'a=value&backUrl=https%3A%2F%2Fwww.msn.com%2Fidp%2Fidpdemo%3Fspid%3Dspdemo%26target%3Db'); - }); - myRouter.history.start(); - }); - - QUnit.test('#3358 - pushState to hashChange transition with search params', function(assert) { - assert.expect(1); - router.history.stop(); - location.replace('http://example.com/root?foo=bar'); - location.replace = function(url) { - assert.strictEqual(url, '/root#?foo=bar'); - }; - router.history = _.extend(new Skeletor.History(), { - location: location, - history: { - pushState: undefined, - replaceState: undefined - } - }); - router.history.start({root: '/root', pushState: true}); - }); - - QUnit.test('Paths that don\'t match the root should not match no root', function(assert) { - assert.expect(0); - location.replace('http://example.com/foo'); - router.history.stop(); - const MyRouter = Skeletor.Router.extend({ - routes: { - foo: function() { - assert.ok(false, 'should not match unless root matches'); - } - } - }); - const history = _.extend(new Skeletor.History(), {location: location}); - const myRouter = new MyRouter({history}); - myRouter.history.start({root: 'root', pushState: true}); - }); - - QUnit.test('Paths that don\'t match the root should not match roots of the same length', function(assert) { - assert.expect(0); - location.replace('http://example.com/xxxx/foo'); - router.history.stop(); - router.history = _.extend(new Skeletor.History(), {location: location}); - const MyRouter = Skeletor.Router.extend({ - routes: { - foo: function() { - assert.ok(false, 'should not match unless root matches'); - } - } - }); - const history = _.extend(new Skeletor.History(), {location: location}); - const myRouter = new MyRouter({history}); - myRouter.history.start({root: 'root', pushState: true}); - }); - - QUnit.test('roots with regex characters', function(assert) { - assert.expect(1); - location.replace('http://example.com/x+y.z/foo'); - router.history.stop(); - const MyRouter = Skeletor.Router.extend({ - routes: {foo: function() { assert.ok(true); }} - }); - const history = _.extend(new Skeletor.History(), {location: location}); - const myRouter = new MyRouter({history}); - myRouter.history.start({root: 'x+y.z', pushState: true}); - }); - - QUnit.test('roots with unicode characters', function(assert) { - assert.expect(1); - location.replace('http://example.com/®ooτ/foo'); - router.history.stop(); - const MyRouter = Skeletor.Router.extend({ - routes: {foo: function() { assert.ok(true); }} - }); - const history = _.extend(new Skeletor.History(), {location: location}); - const myRouter = new MyRouter({history}); - myRouter.history.start({root: '®ooτ', pushState: true}); - }); - - QUnit.test('roots without slash', function(assert) { - assert.expect(1); - location.replace('http://example.com/®ooτ'); - router.history.stop(); - const MyRouter = Skeletor.Router.extend({ - routes: {'': function() { assert.ok(true); }} - }); - const history = _.extend(new Skeletor.History(), {location: location}); - const myRouter = new MyRouter({history}); - myRouter.history.start({root: '®ooτ', pushState: true}); - }); - - QUnit.test('#4025 - navigate updates URL hash as is', function(assert) { - assert.expect(1); - const route = 'search/has%20space'; - router.history.navigate(route); - assert.strictEqual(location.hash, '#' + route); - }); - -})(QUnit); diff --git a/test/sessionStorage.test.js b/test/sessionStorage.test.js index efc3729a..a827efa9 100644 --- a/test/sessionStorage.test.js +++ b/test/sessionStorage.test.js @@ -1,280 +1,313 @@ -import { clone, uniq } from 'lodash'; +/* eslint-disable class-methods-use-this */ +import { clone } from 'lodash'; import { Model } from '../src/model.js'; -import { Collection } from "../src/collection"; +import { Collection } from '../src/collection'; import { expect } from 'chai'; import Storage from '../src/storage.js'; import root from 'window-or-global'; - const attributes = { - string: 'String', - string2: 'String 2', - number: 1337 + string: 'String', + string2: 'String 2', + number: 1337, }; -const SavedModel = Model.extend({ - browserStorage: new Storage('SavedModel', 'session'), - defaults: attributes, - urlRoot: '/test/' -}); +class SavedModel extends Model { + constructor(attributes, options) { + super(attributes, options); + this.browserStorage = new Storage('SavedModel', 'session'); + } + + defaults() { + return attributes; + } +} + +class AjaxModel extends Model { + defaults() { + return attributes; + } +} + +class SavedCollection extends Collection { + get model() { + return AjaxModel; + } + get browserStorage() { + return new Storage('SavedCollection', 'session'); + } +} -const AjaxModel = Model.extend({ - defaults: attributes -}); +describe('Storage Model using sessionStorage', function () { + beforeEach(() => sessionStorage.clear()); + + it('is saved with the given name', async function () { + const mySavedModel = new SavedModel({ 'id': 10 }); + await new Promise((success) => mySavedModel.save(null, { success })); + const item = root.sessionStorage.getItem('localforage/SavedModel-10'); + const parsed = JSON.parse(item); + expect(parsed.id).to.equal(10); + expect(parsed.string).to.equal('String'); + expect(parsed.string2).to.equal('String 2'); + expect(parsed.number).to.equal(1337); + }); + + it('can be converted to JSON', function () { + const mySavedModel = new SavedModel({ 'id': 10 }); + mySavedModel.save(); + expect(mySavedModel.toJSON()).to.eql({ + string: 'String', + id: 10, + number: 1337, + string2: 'String 2', + }); + }); -const SavedCollection = Collection.extend({ - model: AjaxModel, - browserStorage: new Storage('SavedCollection', 'session') -}); + describe('once saved', function () { + beforeEach(() => sessionStorage.clear()); + it('can be fetched from sessionStorage', function () { + const newModel = new SavedModel({ 'id': 10 }); + newModel.fetch(); + expect(newModel.get('string')).to.equal('String'); + expect(newModel.get('string2')).to.equal('String 2'); + expect(newModel.get('number')).to.equal(1337); + }); -describe('Storage Model using sessionStorage', function () { - beforeEach(() => sessionStorage.clear()); + it('passes fetch calls to success', function (done) { + const mySavedModel = new SavedModel({ 'id': 10 }); + mySavedModel.save(); + mySavedModel.fetch({ + success(model, response, options) { + expect(model).to.equal(mySavedModel); + done(); + }, + }); + }); - it('is saved with the given name', async function () { - const mySavedModel = new SavedModel({'id': 10}); - await new Promise((resolve, reject) => mySavedModel.save(null, {'success': resolve})); - const item = root.sessionStorage.getItem('localforage/SavedModel-10'); - const parsed = JSON.parse(item); - expect(parsed.id).to.equal(10); - expect(parsed.string).to.equal('String'); - expect(parsed.string2).to.equal('String 2'); - expect(parsed.number).to.equal(1337); + it('can be updated', async function () { + const mySavedModel = new SavedModel({ 'id': 10 }); + await new Promise((resolve, reject) => + mySavedModel.save({ 'string': 'New String', 'number2': 1234 }, { 'success': resolve }), + ); + expect(mySavedModel.pick('string', 'number2')).to.eql({ + 'string': 'New String', + 'number2': 1234, + }); }); - it('can be converted to JSON', function () { - const mySavedModel = new SavedModel({'id': 10}); - mySavedModel.save(); - expect(mySavedModel.toJSON()).to.eql({ - string: 'String', - id: 10, - number: 1337, - string2: 'String 2' - }); + it('persists its update to sessionStorage', async function () { + const mySavedModel = new SavedModel({ 'id': 10 }); + await new Promise((resolve, reject) => + mySavedModel.save({ 'string': 'New String', 'number2': 1234 }, { 'success': resolve }), + ); + const item = root.sessionStorage.getItem(`localforage/SavedModel-${mySavedModel.id}`); + expect(item).to.be.a('string'); + const parsed = JSON.parse(item); + expect(parsed).to.deep.equal({ + id: 10, + string: 'New String', + string2: 'String 2', + number: 1337, + number2: 1234, + }); }); - describe('once saved', function () { - beforeEach(() => sessionStorage.clear()); - - it('can be fetched from sessionStorage', function () { - const newModel = new SavedModel({'id': 10}); - newModel.fetch(); - expect(newModel.get('string')).to.equal('String'); - expect(newModel.get('string2')).to.equal('String 2'); - expect(newModel.get('number')).to.equal(1337); - }); - - it('passes fetch calls to success', function(done) { - const mySavedModel = new SavedModel({'id': 10}); - mySavedModel.save(); - mySavedModel.fetch({ - success(model, response, options) { - expect(model).to.equal(mySavedModel); - done(); - } - }); - }); - - it('can be updated', async function () { - const mySavedModel = new SavedModel({'id': 10}); - await new Promise((resolve, reject) => mySavedModel.save({'string': 'New String', 'number2': 1234}, {'success': resolve})); - expect(mySavedModel.pick('string', 'number2')).to.eql({ - 'string': 'New String', - 'number2': 1234 - }); - }); - - it('persists its update to sessionStorage', async function () { - const mySavedModel = new SavedModel({'id': 10}); - await new Promise((resolve, reject) => mySavedModel.save({'string': 'New String', 'number2': 1234}, {'success': resolve})); - const item = root.sessionStorage.getItem(`localforage/SavedModel-${mySavedModel.id}`); - expect(item).to.be.a('string'); - const parsed = JSON.parse(item); - expect(parsed).to.deep.equal({ - id: 10, - string: 'New String', - string2: 'String 2', - number: 1337, - number2: 1234 - }); - }); - - it('saves to sessionStorage with patch', async function () { - const mySavedModel = new SavedModel({'id': 10}); - await new Promise(success => mySavedModel.save(null, {success})); - await new Promise(success => mySavedModel.save({'string': 'New String', 'number2': 1234}, {'patch': true, success})); - const item = root.sessionStorage.getItem(`localforage/SavedModel-${mySavedModel.id}`); - expect(item).to.be.a('string'); - const parsed = JSON.parse(item); - expect(parsed).to.deep.equal({ - string: 'New String', - string2: 'String 2', - id: 10, - number: 1337, - number2: 1234 - }); - }); - - it('can be destroyed', async function () { - const mySavedModel = new SavedModel({'id': 10}); - await new Promise((resolve, reject) => mySavedModel.destroy({'success': resolve})); - const item = root.sessionStorage.getItem('localforage/SavedModel-10'); - expect(item).to.be.null; - }); + it('saves to sessionStorage with patch', async function () { + const mySavedModel = new SavedModel({ 'id': 10 }); + await new Promise((success) => mySavedModel.save(null, { success })); + await new Promise((success) => + mySavedModel.save({ 'string': 'New String', 'number2': 1234 }, { 'patch': true, success }), + ); + const item = root.sessionStorage.getItem(`localforage/SavedModel-${mySavedModel.id}`); + expect(item).to.be.a('string'); + const parsed = JSON.parse(item); + expect(parsed).to.deep.equal({ + string: 'New String', + string2: 'String 2', + id: 10, + number: 1337, + number2: 1234, + }); }); - describe('with storage updated from elsewhere', function () { - beforeEach(() => sessionStorage.clear()); + it('can be destroyed', async function () { + const mySavedModel = new SavedModel({ 'id': 10 }); + await new Promise((resolve, reject) => mySavedModel.destroy({ 'success': resolve })); + const item = root.sessionStorage.getItem('localforage/SavedModel-10'); + expect(item).to.be.null; + }); + }); + + describe('with storage updated from elsewhere', function () { + beforeEach(() => sessionStorage.clear()); + + it('will re-fetch new data', async function () { + const newModel = new SavedModel({ 'id': 10 }); + await new Promise((resolve, reject) => newModel.save({ 'string': 'String' }, { 'success': resolve })); + await new Promise((resolve, reject) => newModel.fetch({ 'success': resolve })); + expect(newModel.get('string')).to.equal('String'); + + const mySavedModel = new SavedModel({ 'id': 10 }); + await new Promise((resolve, reject) => + mySavedModel.save({ 'string': 'Brand new string' }, { 'success': resolve }), + ); + await new Promise((resolve, reject) => newModel.fetch({ 'success': resolve })); + expect(newModel.get('string')).to.equal('Brand new string'); + }); + }); - it('will re-fetch new data', async function () { - const newModel = new SavedModel({'id': 10}); - await new Promise((resolve, reject) => newModel.save({'string': 'String'}, {'success': resolve})); - await new Promise((resolve, reject) => newModel.fetch({'success': resolve})); - expect(newModel.get('string')).to.equal('String'); + describe('with a different idAttribute', function () { + beforeEach(() => sessionStorage.clear()); - const mySavedModel = new SavedModel({'id': 10}); - await new Promise((resolve, reject) => mySavedModel.save({'string': 'Brand new string'}, {'success': resolve})); - await new Promise((resolve, reject) => newModel.fetch({'success': resolve})); - expect(newModel.get('string')).to.equal('Brand new string'); - }); + class DifferentIdAttribute extends Model { + get idAttribute() { + return 'number'; + } + + constructor(attributes, options) { + super(attributes, options); + this.browserStorage = new Storage('DifferentId', 'session'); + } + + // eslint-disable-next-line class-methods-use-this + defaults() { + return attributes; + } + } + + it('can be saved with the new value', async function () { + const mySavedModel = new DifferentIdAttribute(attributes); + await new Promise((resolve, reject) => mySavedModel.save(null, { 'success': resolve })); + const item = root.sessionStorage.getItem('localforage/DifferentId-1337'); + const parsed = JSON.parse(item); + + expect(item).to.be.a('string'); + expect(parsed.string).to.be.a('string'); }); - describe('with a different idAttribute', function () { - beforeEach(() => sessionStorage.clear()); - - const DifferentIdAttribute = Model.extend({ - browserStorage: new Storage('DifferentId', 'session'), - idAttribute: 'number' - }); - - it('can be saved with the new value', async function () { - const mySavedModel = new DifferentIdAttribute(attributes); - await new Promise((resolve, reject) => mySavedModel.save(null, {'success': resolve})); - const item = root.sessionStorage.getItem('localforage/DifferentId-1337'); - const parsed = JSON.parse(item); - - expect(item).to.be.a('string'); - expect(parsed.string).to.be.a('string'); - }); - - it('can be fetched with the new value', async function () { - const mySavedModel = new DifferentIdAttribute(attributes); - root.sessionStorage.setItem('localforage/DifferentId-1337', JSON.stringify(attributes)); - const newModel = new DifferentIdAttribute({'number': 1337 }); - await new Promise((resolve, reject) => newModel.fetch({'success': resolve})); - expect(newModel.id).to.equal(1337); - expect(newModel.get('string')).to.be.a('string'); - }); + it('can be fetched with the new value', async function () { + const mySavedModel = new DifferentIdAttribute(attributes); + root.sessionStorage.setItem('localforage/DifferentId-1337', JSON.stringify(attributes)); + const newModel = new DifferentIdAttribute({ 'number': 1337 }); + await new Promise((resolve, reject) => newModel.fetch({ 'success': resolve })); + expect(newModel.id).to.equal(1337); + expect(newModel.get('string')).to.be.a('string'); }); + }); - describe('New sessionStorage model', function () { - beforeEach(() => sessionStorage.clear()); + describe('New sessionStorage model', function () { + beforeEach(() => sessionStorage.clear()); - it('creates a new item in sessionStorage', async function () { - const mySavedModel = new SavedModel(); - await new Promise((resolve, reject) => mySavedModel.save({'data': 'value'}, {'success': resolve})); - const item = root.sessionStorage.getItem(`localforage/SavedModel-${mySavedModel.id}`); - const parsed = JSON.parse(item); - expect(parsed).to.eql(mySavedModel.attributes); - }); + it('creates a new item in sessionStorage', async function () { + const mySavedModel = new SavedModel(); + await new Promise((resolve, reject) => mySavedModel.save({ 'data': 'value' }, { 'success': resolve })); + const item = root.sessionStorage.getItem(`localforage/SavedModel-${mySavedModel.id}`); + const parsed = JSON.parse(item); + expect(parsed).to.eql(mySavedModel.attributes); }); + }); }); describe('browserStorage Collection using sessionStorage', function () { + beforeEach(() => sessionStorage.clear()); + + it('saves to sessionStorage', function () { + const mySavedCollection = new SavedCollection(); + mySavedCollection.create(attributes); + expect(mySavedCollection.length).to.equal(1); + }); + + it('saves to sessionStorage when wait=true', async function () { + const mySavedCollection = new SavedCollection(); + await new Promise((success) => mySavedCollection.create(attributes, { 'wait': true, success })); + expect(mySavedCollection.length).to.equal(1); + const attrs2 = { string: 'String' }; + await new Promise((success) => mySavedCollection.create(attrs2, { 'wait': true, success })); + expect(mySavedCollection.length).to.equal(2); + await new Promise((success) => mySavedCollection.fetch({ success })); + expect(mySavedCollection.length).to.equal(2); + }); + + it('cannot duplicate id in sessionStorage', async function () { + const item = clone(attributes); + item.id = 5; + const newCollection = new SavedCollection([item]); + await new Promise((resolve, reject) => newCollection.create(item, { 'success': resolve })); + await new Promise((resolve, reject) => newCollection.create(item, { 'success': resolve })); + const localItem = root.sessionStorage.getItem('localforage/SavedCollection-5'); + expect(newCollection.length).to.equal(1); + expect(JSON.parse(localItem).id).to.equal(5); + }); + + describe('pulling from sessionStorage', function () { beforeEach(() => sessionStorage.clear()); - it('saves to sessionStorage', function () { - const mySavedCollection = new SavedCollection(); - mySavedCollection.create(attributes); - expect(mySavedCollection.length).to.equal(1); + it('saves into the sessionStorage', async function () { + const mySavedCollection = new SavedCollection(); + const model = await new Promise((resolve, reject) => + mySavedCollection.create(attributes, { 'success': resolve }), + ); + const item = root.sessionStorage.getItem(`localforage/SavedCollection-${model.id}`); + expect(item).to.be.a('string'); }); - it('saves to sessionStorage when wait=true', async function () { - const mySavedCollection = new SavedCollection(); - await new Promise(success => mySavedCollection.create(attributes, {'wait': true, success})); - expect(mySavedCollection.length).to.equal(1); - const attrs2 = { string: 'String' }; - await new Promise(success => mySavedCollection.create(attrs2, {'wait': true, success})); - expect(mySavedCollection.length).to.equal(2); - await new Promise(success => mySavedCollection.fetch({success})); - expect(mySavedCollection.length).to.equal(2); + it('saves the right data', async function () { + const mySavedCollection = new SavedCollection(); + const model = await new Promise((resolve, reject) => + mySavedCollection.create(attributes, { 'success': resolve }), + ); + const item = root.sessionStorage.getItem(`localforage/SavedCollection-${model.id}`); + const parsed = JSON.parse(item); + expect(parsed.id).to.equal(model.id); + expect(parsed.string).to.equal('String'); + }); + it('reads from sessionStorage', async function () { + const mySavedCollection = new SavedCollection(); + let model = await new Promise((resolve, reject) => mySavedCollection.create(attributes, { 'success': resolve })); + const newCollection = new SavedCollection(); + model = await new Promise((resolve, reject) => newCollection.fetch({ 'success': resolve })); + expect(newCollection.length).to.equal(1); + const newModel = newCollection.at(0); + expect(newModel.get('string')).to.equal('String'); }); - it('cannot duplicate id in sessionStorage', async function () { - const item = clone(attributes); - item.id = 5; - const newCollection = new SavedCollection([item]); - await new Promise((resolve, reject) => newCollection.create(item, {'success': resolve})); - await new Promise((resolve, reject) => newCollection.create(item, {'success': resolve})); - const localItem = root.sessionStorage.getItem('localforage/SavedCollection-5'); - expect(newCollection.length).to.equal(1); - expect(JSON.parse(localItem).id).to.equal(5); + it('destroys models and removes from collection', async function () { + const mySavedCollection = new SavedCollection(); + const model = await new Promise((resolve, reject) => + mySavedCollection.create(attributes, { 'success': resolve }), + ); + const item = root.sessionStorage.getItem(`localforage/SavedCollection-${model.id}`); + const parsed = JSON.parse(item); + const newModel = mySavedCollection.get(parsed.id); + await new Promise((resolve, reject) => newModel.destroy({ 'success': resolve })); + const removed = root.sessionStorage.getItem(`localforage/SavedCollection-${parsed.id}`); + expect(removed).to.be.null; + expect(mySavedCollection.length).to.equal(0); }); + }); + describe('will fetch from sessionStorage if updated separately', function () { + beforeEach(() => sessionStorage.clear()); - describe('pulling from sessionStorage', function () { - beforeEach(() => sessionStorage.clear()); - - it('saves into the sessionStorage', async function () { - const mySavedCollection = new SavedCollection(); - const model = await new Promise((resolve, reject) => mySavedCollection.create(attributes, {'success': resolve})); - const item = root.sessionStorage.getItem(`localforage/SavedCollection-${model.id}`); - expect(item).to.be.a('string'); - }); - - it('saves the right data', async function () { - const mySavedCollection = new SavedCollection(); - const model = await new Promise((resolve, reject) => mySavedCollection.create(attributes, {'success': resolve})); - const item = root.sessionStorage.getItem(`localforage/SavedCollection-${model.id}`); - const parsed = JSON.parse(item); - expect(parsed.id).to.equal(model.id); - expect(parsed.string).to.equal('String'); - }); - - it('reads from sessionStorage', async function () { - const mySavedCollection = new SavedCollection(); - let model = await new Promise((resolve, reject) => mySavedCollection.create(attributes, {'success': resolve})); - const newCollection = new SavedCollection(); - model = await new Promise((resolve, reject) => newCollection.fetch({'success': resolve})); - expect(newCollection.length).to.equal(1); - const newModel = newCollection.at(0); - expect(newModel.get('string')).to.equal('String'); - }); - - it('destroys models and removes from collection', async function () { - const mySavedCollection = new SavedCollection(); - const model = await new Promise((resolve, reject) => mySavedCollection.create(attributes, {'success': resolve})); - const item = root.sessionStorage.getItem(`localforage/SavedCollection-${model.id}`); - const parsed = JSON.parse(item); - const newModel = mySavedCollection.get(parsed.id); - await new Promise((resolve, reject) => newModel.destroy({'success': resolve})); - const removed = root.sessionStorage.getItem(`localforage/SavedCollection-${parsed.id}`); - expect(removed).to.be.null; - expect(mySavedCollection.length).to.equal(0); - }); + it('fetches the items from the original collection', async function () { + const mySavedCollection = new SavedCollection(); + await new Promise((resolve, reject) => mySavedCollection.create(attributes, { 'success': resolve })); + const newCollection = new SavedCollection(); + await new Promise((resolve, reject) => newCollection.fetch({ 'success': resolve })); + expect(newCollection.length).to.equal(1); }); - describe('will fetch from sessionStorage if updated separately', function () { - beforeEach(() => sessionStorage.clear()); - - it('fetches the items from the original collection', async function () { - const mySavedCollection = new SavedCollection(); - await new Promise((resolve, reject) => mySavedCollection.create(attributes, {'success': resolve})); - const newCollection = new SavedCollection(); - await new Promise((resolve, reject) => newCollection.fetch({'success': resolve})); - expect(newCollection.length).to.equal(1); - }); - - it('will update future changes', async function () { - const mySavedCollection = new SavedCollection(); - const newAttributes = clone(attributes); - mySavedCollection.create(newAttributes); - await new Promise((resolve, reject) => mySavedCollection.create(newAttributes, {'success': resolve})); - - const newCollection = new SavedCollection(); - await new Promise((resolve, reject) => newCollection.fetch({'success': resolve})); - expect(newCollection.length).to.equal(2); - }); + it('will update future changes', async function () { + const mySavedCollection = new SavedCollection(); + const newAttributes = clone(attributes); + mySavedCollection.create(newAttributes); + await new Promise((resolve, reject) => mySavedCollection.create(newAttributes, { 'success': resolve })); + + const newCollection = new SavedCollection(); + await new Promise((resolve, reject) => newCollection.fetch({ 'success': resolve })); + expect(newCollection.length).to.equal(2); }); + }); }); diff --git a/test/setup/dom-setup.js b/test/setup/dom-setup.js index 56c8d2d3..fd66c424 100644 --- a/test/setup/dom-setup.js +++ b/test/setup/dom-setup.js @@ -1,4 +1,3 @@ -document.querySelector('body').insertAdjacentHTML("beforeEnd", - '
' + - '
' -); +document + .querySelector('body') + .insertAdjacentHTML('beforeEnd', '
' + '
'); diff --git a/test/sync.js b/test/sync.js index 8d668177..d626471f 100644 --- a/test/sync.js +++ b/test/sync.js @@ -1,7 +1,10 @@ +/* eslint-disable class-methods-use-this */ (function (QUnit) { - const Library = Skeletor.Collection.extend({ - url: () => '/library', - }); + class Library extends Skeletor.Collection { + get url() { + return '/library'; + } + } let library; const attrs = { diff --git a/test/view.js b/test/view.js deleted file mode 100644 index 6b445129..00000000 --- a/test/view.js +++ /dev/null @@ -1,485 +0,0 @@ -(function(QUnit) { - - let view; - - QUnit.module('Skeletor.View', { - - beforeEach: function() { - document.querySelector('#qunit-fixture').insertAdjacentHTML( - 'beforeEnd', - '

Test

' - ); - - view = new Skeletor.View({ - id: 'test-view', - className: 'test-view', - other: 'non-special-option' - }); - }, - - afterEach: function() { - const el = document.querySelector('#testElement'); - el.parentElement.removeChild(el); - const view_el = document.querySelector('#test-view'); - view_el && view_el.parentElement.removeChild(view_el); - } - - }); - - QUnit.test('constructor', function(assert) { - assert.expect(3); - assert.equal(view.el.id, 'test-view'); - assert.equal(view.el.className, 'test-view'); - assert.equal(view.el.other, undefined); - }); - - QUnit.test('$', function(assert) { - assert.expect(2); - var myView = new Skeletor.View(); - myView.setElement('

test

'); - var result = myView.$('a b'); - - assert.strictEqual(result[0].innerHTML, 'test'); - assert.ok(result.length === +result.length); - }); - - QUnit.test('initialize', function(assert) { - assert.expect(1); - var View = Skeletor.View.extend({ - initialize: function() { - this.one = 1; - } - }); - - assert.strictEqual(new View().one, 1); - }); - - QUnit.test('preinitialize', function(assert) { - assert.expect(1); - var View = Skeletor.View.extend({ - preinitialize: function() { - this.one = 1; - } - }); - - assert.strictEqual(new View().one, 1); - }); - - QUnit.test('preinitialize occurs before the view is set up', function(assert) { - assert.expect(2); - var View = Skeletor.View.extend({ - preinitialize: function() { - assert.equal(this.el, undefined); - } - }); - var _view = new View({}); - assert.notEqual(_view.el, undefined); - }); - - QUnit.test('render', function(assert) { - assert.expect(1); - var myView = new Skeletor.View(); - assert.equal(myView.render(), myView, '#render returns the view instance'); - }); - - QUnit.test('delegateEvents', function(assert) { - assert.expect(6); - let counter1 = 0, counter2 = 0; - - const myView = new Skeletor.View({el: '#testElement'}); - myView.increment = () => counter1++; - myView.el.addEventListener('click', () => counter2++); - - const events = {'click h1': 'increment'}; - - myView.delegateEvents(events); - myView.el.querySelector('h1').click(); - assert.equal(counter1, 1); - assert.equal(counter2, 1); - - myView.el.querySelector('h1').click(); - assert.equal(counter1, 2); - assert.equal(counter2, 2); - - myView.delegateEvents(events); - myView.el.querySelector('h1').click(); - assert.equal(counter1, 3); - assert.equal(counter2, 3); - }); - - QUnit.test('delegate', function(assert) { - assert.expect(3); - const myView = new Skeletor.View({el: '#testElement'}); - myView.delegate('click', 'h1', () => assert.ok(true)); - myView.delegate('click', () => assert.ok(true)); - myView.el.querySelector('h1').click(); - assert.equal(myView.delegate(), myView, '#delegate returns the view instance'); - }); - - QUnit.test('delegateEvents allows functions for callbacks', function(assert) { - assert.expect(3); - const myView = new Skeletor.View({el: '#testElement'}); - myView.counter = 0; - const events = { - click: () => myView.counter++ - }; - - myView.delegateEvents(events); - myView.el.querySelector('h1').click(); - assert.equal(myView.counter, 1); - - myView.el.querySelector('h1').click(); - assert.equal(myView.counter, 2); - - myView.delegateEvents(events); - myView.el.querySelector('h1').click(); - assert.equal(myView.counter, 3); - }); - - QUnit.test('delegateEvents ignore undefined methods', function(assert) { - assert.expect(0); - var myView = new Skeletor.View({el: '

'}); - myView.delegateEvents({click: 'undefinedMethod'}); - myView.el.click(); - }); - - QUnit.test('undelegateEvents', function(assert) { - assert.expect(7); - let counter1 = 0, counter2 = 0; - - const myView = new Skeletor.View({el: '#testElement'}); - myView.increment = function() { counter1++; }; - myView.el.addEventListener('click', () => counter2++); - - const events = {'click h1': 'increment'}; - - myView.delegateEvents(events); - myView.el.querySelector('h1').click(); - assert.equal(counter1, 1); - assert.equal(counter2, 1); - - myView.undelegateEvents(); - myView.el.querySelector('h1').click(); - assert.equal(counter1, 1); - assert.equal(counter2, 2); - - myView.delegateEvents(events); - myView.el.querySelector('h1').click(); - assert.equal(counter1, 2); - assert.equal(counter2, 3); - - assert.equal(myView.undelegateEvents(), myView, '#undelegateEvents returns the view instance'); - }); - - QUnit.test('undelegate', function(assert) { - assert.expect(1); - var myView = new Skeletor.View({el: '#testElement'}); - myView.delegate('click', function() { assert.ok(false); }); - myView.delegate('click', 'h1', function() { assert.ok(false); }); - myView.undelegate('click'); - myView.el.querySelector('h1').click(); - myView.el.click(); - assert.equal(myView.undelegate(), myView, '#undelegate returns the view instance'); - }); - - QUnit.test('undelegate with passed handler', function(assert) { - assert.expect(1); - const myView = new Skeletor.View({el: '#testElement'}); - const listener = () => assert.ok(false); - myView.delegate('click', listener); - myView.delegate('click', function() { assert.ok(true); }); - myView.undelegate('click', listener); - myView.el.click(); - }); - - QUnit.test('undelegate with selector', function(assert) { - assert.expect(2); - const myView = new Skeletor.View({el: '#testElement'}); - myView.delegate('click', () => assert.ok(true)); - myView.delegate('click', 'h1', () => assert.ok(false)); - myView.undelegate('click', 'h1'); - myView.el.querySelector('h1').click(); - myView.el.click(); - }); - - QUnit.test('undelegate with handler and selector', function(assert) { - assert.expect(2); - const myView = new Skeletor.View({el: '#testElement'}); - myView.delegate('click', () => assert.ok(true)); - const handler = () => assert.ok(false); - myView.delegate('click', 'h1', handler); - myView.undelegate('click', 'h1', handler); - myView.el.querySelector('h1').click(); - myView.el.click(); - }); - - QUnit.test('tagName can be provided as a string', function(assert) { - assert.expect(1); - var View = Skeletor.View.extend({ - tagName: 'span' - }); - - assert.equal(new View().el.tagName, 'SPAN'); - }); - - QUnit.test('tagName can be provided as a function', function(assert) { - assert.expect(1); - const View = Skeletor.View.extend({ - tagName: () => 'p' - }); - assert.ok(new View().el.matches('p')); - }); - - QUnit.test('_ensureElement with DOM node el', function(assert) { - assert.expect(1); - var View = Skeletor.View.extend({ - el: document.body - }); - - assert.equal(new View().el, document.body); - }); - - QUnit.test('_ensureElement with string el', function(assert) { - assert.expect(3); - var View = Skeletor.View.extend({ - el: 'body' - }); - assert.strictEqual(new View().el, document.body); - - View = Skeletor.View.extend({ - el: '#testElement > h1' - }); - assert.strictEqual(new View().el, document.querySelector('#testElement > h1')); - - View = Skeletor.View.extend({ - el: '#nonexistent' - }); - assert.ok(!new View().el); - }); - - QUnit.test('with className and id functions', function(assert) { - assert.expect(2); - var View = Skeletor.View.extend({ - className: function() { - return 'className'; - }, - id: function() { - return 'id'; - } - }); - - assert.strictEqual(new View().el.className, 'className'); - assert.strictEqual(new View().el.id, 'id'); - }); - - QUnit.test('with attributes', function(assert) { - assert.expect(2); - var View = Skeletor.View.extend({ - attributes: { - 'id': 'id', - 'class': 'class' - } - }); - - assert.strictEqual(new View().el.className, 'class'); - assert.strictEqual(new View().el.id, 'id'); - }); - - QUnit.test('with attributes as a function', function(assert) { - assert.expect(1); - var View = Skeletor.View.extend({ - attributes: function() { - return {'class': 'dynamic'}; - } - }); - - assert.strictEqual(new View().el.className, 'dynamic'); - }); - - QUnit.test('should default to className/id properties', function(assert) { - assert.expect(4); - const View = Skeletor.View.extend({ - className: 'backboneClass', - id: 'backboneId', - attributes: { - 'class': 'attributeClass', - 'id': 'attributeId' - } - }); - const myView = new View(); - assert.strictEqual(myView.el.className, 'backboneClass'); - assert.strictEqual(myView.el.id, 'backboneId'); - assert.strictEqual(myView.el.getAttribute('class'), 'backboneClass'); - assert.strictEqual(myView.el.getAttribute('id'), 'backboneId'); - }); - - QUnit.test('multiple views per element', function(assert) { - assert.expect(3); - let count = 0; - const el = document.createElement('p'); - - const View = Skeletor.View.extend({ - el: el, - events: { - click: function() { - count++; - } - } - }); - - const view1 = new View(); - el.click(); - assert.equal(1, count); - - const view2 = new View(); - el.click(); - assert.equal(3, count); - - view1.delegateEvents(); - el.click(); - assert.equal(5, count); - }); - - QUnit.test('custom events', function(assert) { - assert.expect(2); - const View = Skeletor.View.extend({ - el: document.querySelector('body'), - events: { - 'fake$event': () => assert.ok(true) - } - }); - const myView = new View(); - const event = new Event('fake$event') - const body = document.querySelector('body'); - body.dispatchEvent(event); - body.dispatchEvent(event); - myView.undelegateEvents(); - body.dispatchEvent(event); - }); - - QUnit.test('#1048 - setElement uses provided object.', function(assert) { - assert.expect(2); - let el = document.querySelector('body'); - const myView = new Skeletor.View({el: el}); - assert.ok(myView.el === el); - const new_el = document.createElement('div'); - myView.setElement(el = new_el); - assert.ok(myView.el === new_el); - }); - - QUnit.test('#986 - Undelegate before changing element.', function(assert) { - assert.expect(1); - const button1 = document.createElement('button'); - const button2 = document.createElement('button'); - const View = Skeletor.View.extend({ - events: { - click: function(e) { - assert.ok(myView.el === e.target); - } - } - }); - - const myView = new View({el: button1}); - myView.setElement(button2); - button1.click(); - button2.click(); - }); - - QUnit.test('#1172 - Clone attributes object', function(assert) { - assert.expect(2); - var View = Skeletor.View.extend({ - attributes: {foo: 'bar'} - }); - - var view1 = new View({id: 'foo'}); - assert.strictEqual(view1.el.id, 'foo'); - - var view2 = new View(); - assert.ok(!view2.el.id); - }); - - QUnit.test('views stopListening', function(assert) { - assert.expect(0); - var View = Skeletor.View.extend({ - initialize: function() { - this.listenTo(this.model, 'all x', function() { assert.ok(false); }); - this.listenTo(this.collection, 'all x', function() { assert.ok(false); }); - } - }); - - var myView = new View({ - model: new Skeletor.Model(), - collection: new Skeletor.Collection() - }); - - myView.stopListening(); - myView.model.trigger('x'); - myView.collection.trigger('x'); - }); - - QUnit.test('Provide function for el.', function(assert) { - assert.expect(2); - const View = Skeletor.View.extend({ - el: () => '

' - }); - const myView = new View(); - assert.ok(myView.el.matches('p')); - assert.ok(myView.el.querySelectorAll('a').length); - }); - - QUnit.test('events passed in options', function(assert) { - assert.expect(1); - let counter = 0; - - const View = Skeletor.View.extend({ - el: '#testElement', - increment: function() { - counter++; - } - }); - - const myView = new View({ - events: { - 'click h1': 'increment' - } - }); - - myView.el.querySelector('h1').click(); - myView.el.querySelector('h1').click(); - assert.equal(counter, 2); - }); - - QUnit.test('remove', function(assert) { - assert.expect(2); - var myView = new Skeletor.View(); - document.body.appendChild(view.el); - - myView.delegate('click', function() { assert.ok(false); }); - myView.listenTo(myView, 'all x', function() { assert.ok(false); }); - - assert.equal(myView.remove(), myView, '#remove returns the view instance'); - myView.el.click(); - myView.trigger('x'); - - // In IE8 and below, parentNode still exists but is not document.body. - assert.notEqual(myView.el.parentNode, document.body); - }); - - QUnit.test('setElement', function(assert) { - assert.expect(2); - const myView = new Skeletor.View({ - events: { - click: () => assert.ok(false) - } - }); - myView.events = { - click: () => assert.ok(true) - }; - const oldEl = myView.el; - myView.setElement(document.createElement('div')); - oldEl.click(); - myView.el.click(); - assert.notEqual(oldEl, myView.el); - }); - -})(QUnit); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..ccfefdb8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "include": [ + "src/**/*" + ], + "compilerOptions": { + "target": "es2020", + "module": "esnext", + + "allowJs": true, + "checkJs": true, + + "rootDir": "./src", + "outDir": "./types/", + "baseUrl": "./src/", + + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + + "strict": false, + "noImplicitAny": false, + + "skipLibCheck": true, + + "moduleResolution": "node", + "resolveJsonModule": true + } +}