Merge branch master of gitlab2.rz.ifi.lmu.de:uni2work/uni2work
This commit is contained in:
parent
77f09f05f5
commit
f2cf9344e7
9
.babelrc
9
.babelrc
@ -1,10 +1,17 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", { "useBuiltIns": "usage" }]
|
||||
["env", {
|
||||
"useBuiltIns": "usage",
|
||||
"targets": { "node": "current" }
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-decorators", { "legacy": true }],
|
||||
["@babel/plugin-proposal-class-properties", { "loose": true }],
|
||||
["@babel/plugin-proposal-private-methods", { "loose": true }],
|
||||
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
|
||||
["@babel/plugin-transform-modules-commonjs"],
|
||||
["@babel/transform-runtime"]
|
||||
]
|
||||
}
|
||||
|
||||
@ -11,9 +11,10 @@
|
||||
"flatpickr": "readonly",
|
||||
"$": "readonly"
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"requireConfigFile": false,
|
||||
"ecmaFeatures": {
|
||||
"legacyDecorators": true
|
||||
}
|
||||
|
||||
229
CHANGELOG.md
229
CHANGELOG.md
@ -2,6 +2,39 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [26.1.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.1.0...v26.1.1) (2022-06-06)
|
||||
|
||||
## [26.1.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.0.1...v26.1.0) (2022-06-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **apis:** further integrate servant ([bf2ff2d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bf2ff2dc9c4a986e9088feca41c00123bf2c785a))
|
||||
* **apis:** integrate servant ([e3d504b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e3d504bd113f76854748929201c7e476c2911ee0))
|
||||
* **apis:** support servant-generic ([e8bbaa0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e8bbaa0463afb11998d1a21a9e4ff068ba2b7ea3))
|
||||
* **apis:** version negotiation ([76e0bcf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/76e0bcf693afeb15edae0b8170c3a813b10ae062))
|
||||
* **app-settings:** add duration after which finalized WorkflowWorkflows will be archived ([465a92b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/465a92b9829db9a9969cf370c635bbf9407f07f5))
|
||||
* **external-apis:** add ExternalApisList ([4216785](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4216785e90931ec54dc723166442a7acbe491851))
|
||||
* **external-apis:** create new external api registrations ([559f9db](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/559f9db7d58e26758209e61ef5ac8adc48b25221))
|
||||
* **external-apis:** idents, info, pong, delete, and expiry ([90679e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/90679e00952ee4d6df584b52ceba99c0216e222e))
|
||||
* **help:** update help instructions, add support times ([5201a93](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5201a93de67e5589cca8511722f8513ab5f12fb8))
|
||||
* link api docs ([b277bd8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b277bd8424bdb8aef6c0c2e22acd70854f6ba18b))
|
||||
* **nix:** add postman for api debugging ([5a964f3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5a964f347c9306989f753a69cd56a92404aed699))
|
||||
* **servant:** dry-run support ([47df8a3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/47df8a312f72c7d019a536e7f2155ab7bd52febf))
|
||||
* **workflows:** add archived timestamp ([088c2f5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/088c2f5c751e868dbe50b7fdc04fc9f3f0206966))
|
||||
* **workflows:** implement archive and list page actions ([fac92f9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fac92f9b5053d9f11dc1d72c81d290acd888cd63))
|
||||
* **workflows:** implement archive routes ([4416094](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/441609436a4e4a78f4ff826603a2a2be2dcabb41))
|
||||
* **workflows:** implement breadcrumbs for archive routes ([4adaf1e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4adaf1e806cf3bd1181c78c3d75d57692ed62356))
|
||||
* **workflows:** implement handlers for listing all workflows ([85c24f7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/85c24f713a974bcb932513da162d5224c2d76fa3))
|
||||
* **workflows:** restrict all (except admin) workflow lists on non-archived ([97723ad](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/97723ad895c080819c29105717aeb9cfdd19c7a1))
|
||||
* **workflows:** set archived timestamp on state change ([23b1065](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/23b106554567db8d75478389ef043f17b7b43458))
|
||||
* **workflows:** show info and warnings about scheduled/performed archivation ([424692d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/424692d61110042462d7d88310290a8f682bd23a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **lms:** direct upload did not commit to DB ([e7cea4a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e7cea4aa6c5b05285e5c47d815067a4d45315024))
|
||||
|
||||
## [25.24.5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.24.4...v25.24.5) (2022-06-07)
|
||||
|
||||
|
||||
@ -42,6 +75,40 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
* **async-tabel, async-form:** removed destroyAll call ([236009e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/236009e508245ba10aa5f45b9514c8186ab186cf))
|
||||
* **migration:** dont force app settings ([4486a00](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4486a00d45ff9783115064b640b58f006b2e78d7))
|
||||
* **test:** add missing workflow instance ([6c92440](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6c92440c6419012ef96a7410e769c8724a8d2206))
|
||||
* **workflows:** add missing Hashable instance for WorkflowWorkflowListType ([6e46e4e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6e46e4e9ef34130582a3511ff01b42f4428dd1a1))
|
||||
* **workflows:** correct interpolation of archived state in headings ([84d3327](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/84d3327db37aa1177cc2e9b40fa9fc3e03ec7867))
|
||||
|
||||
## [26.0.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.0.0...v26.0.1) (2022-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** various fe incompatabilities with updated tooling ([46530c6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/46530c6c644bec65c2219fd9b5830bc5bb8e9baf))
|
||||
|
||||
## [26.0.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.29.3...v26.0.0) (2022-04-22)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **system-message:** fix on-volatile-cluster-settings model default
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **system-message:** fix on-volatile-cluster-settings model default ([4027f31](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4027f3144b613dbe67ca44f7a3142c13ff6f4dff))
|
||||
* **webpack:** switch to wp-5 assets ([ec4d710](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ec4d710ce0a5429160365397ad17c23b71c0a4cf))
|
||||
|
||||
## [25.29.3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.29.2...v25.29.3) (2022-04-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
* **system-message:** add volatile cluster setting model default ([6655582](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6655582ace098808bfcea90ca85fce2fe0024d2b))
|
||||
|
||||
## [25.29.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.29.1...v25.29.2) (2022-04-21)
|
||||
|
||||
## [25.29.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.29.0...v25.29.1) (2022-04-21)
|
||||
|
||||
* **uploadcache:** set default to localhost ([eeb22de](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eeb22dec9737e014c20b852438da4373219884b0))
|
||||
|
||||
@ -52,6 +119,9 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* update lodash.debounce and defer imports ([f03dae6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f03dae66e92a36ca7cea01fa8b508f7f8612200a))
|
||||
|
||||
## [25.29.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.28.0...v25.29.0) (2022-04-21)
|
||||
* **lms:** correct lms table column sorting key ([9ee4767](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9ee476736ccd361559edd3570dc27ba83ff8a334))
|
||||
|
||||
## [25.23.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.23.0...v25.23.1) (2022-02-23)
|
||||
@ -61,6 +131,15 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
|
||||
### Features
|
||||
|
||||
* **workflows:** additional text field types ([4a34344](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4a34344c332291ceaed192ee1f0a73d97d855eeb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **workflows:** always show navigation item ([82a4ecc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/82a4eccaa4fc5d6df3ff0bc71a025973f12c7dfd))
|
||||
* **workflows:** properly distinguish anonymous/automatic ([21a1fb5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/21a1fb543b347a871b21db636d217e0816bd5388))
|
||||
|
||||
## [25.28.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.27.0...v25.28.0) (2022-04-20)
|
||||
* **log:** remove container log setting in order to use stdout ([8f460bd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8f460bd0a3aba9013f2ee2d20a1465487ecfe629))
|
||||
|
||||
## [25.22.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.21.24...v25.22.0) (2022-01-12)
|
||||
@ -69,10 +148,36 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
### Features
|
||||
|
||||
* **status:** show instance running time ([8743719](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8743719183abfa10d089c6e7e765e49f6da3c50d))
|
||||
* **forms:** honeypots for unauthorized users ([8085c30](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8085c30420dcca9d8c493da8241a5e10d9ef8122))
|
||||
* **frontend:** remove deprecated tail.DateTime ([6bfbff4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6bfbff41f6081dd65b0a4afa9155a125f31de973))
|
||||
* **system-msg:** add volatile cluster settings to system message forms ([f8f9dc0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f8f9dc0621a18862bcf83364b874e176553ed7b1))
|
||||
* **system-msg:** display system status messages on volatile cluster settings only ([da253f7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/da253f7fbdb919bb9cc52310148dcc28c382cfa4))
|
||||
* **system-msg:** display volatile cluster settings in msg list ([32bed15](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/32bed159615b96ebab23766d748a844b1ba42f41))
|
||||
* **translation mistakes:** courses and landing page ([9cff722](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9cff7220231a227859c771b720084e8a6952b5c1))
|
||||
* **translation mistakes:** done ([229e379](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/229e3793c6266a2936ffb32854cb14e0bb0ba510))
|
||||
* **translation mistakes:** megre request ([ff07768](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ff077680b6fde39f4f82ba162781e471a9f3071c))
|
||||
* **translation mistakes:** until admin ([43e5f9f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/43e5f9f7aa340d564557ec499859049dcb2ac194))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **system-msg:** use correct required features for form elems ([b99cda0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b99cda06aa22b3b9a3c026f80873cb2b68d93cdb))
|
||||
|
||||
## [25.27.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.26.0...v25.27.0) (2022-02-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **communication:** add recipient option for course participants in at least one tutorial ([8dabb63](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8dabb63603341c7e2d7dadb95deeb77f864c14a0))
|
||||
* **course-users:** export eppn to csv and json ([3c79703](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3c797039cc0784a2831167c11ed1b7bb8ff78daa))
|
||||
* **course-users:** export eppn to csv and json ([6feefeb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6feefeb3e16c8b2327cdb2746a8263394f9293f9))
|
||||
* **courses:** add search bars for shorthands and titles ([8e1b9b9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8e1b9b9abae0c794ce12f10e5f75f410f903d927))
|
||||
* **exam-users:** allow resolving exam users by eppn on csv-import ([6a041dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6a041dc4c9c6ae62c7c7f1641eeb2f5417f32b8d))
|
||||
* **exam-users:** allow resolving exam users by eppn on csv-import ([d677d35](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d677d35319332aae28410d1b7cdf09d295920fac))
|
||||
* **exam-users:** export eppn for exam users ([ff1fe20](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ff1fe20efed340529a3a12858d82d668e5fe2e85))
|
||||
* **exam-users:** export eppn for exam users ([d4ba513](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4ba513d7948cc6e7136bd05543edb9d9764f78c))
|
||||
* **submissions:** add option 'Set corrections as done' ([880eb3b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/880eb3b1ad2d5e4b12f767eb68d29eabfbd79e17))
|
||||
* **submissions:** Apply suggestions to reduce lines of code ([2f1ecd3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2f1ecd397936f4e4ac018eb4f9aaf49da37e1e81))
|
||||
* **smtp:** case-insensitive from-domain comparison for reply-to instead option ([859f5b8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/859f5b8494ce326fcdf13ed8fcca9355273fb42e))
|
||||
|
||||
## [25.21.24](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.21.23...v25.21.24) (2022-01-07)
|
||||
@ -88,6 +193,31 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **exams:** exam results of non-registered users now show correctly ([b294b1c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b294b1cfc4bab4b5ec5247d37097873748759727))
|
||||
* **submissions:** add check if users in `groupMembers` are already submittors on submission ([4854d9c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4854d9c8661cc47447a794a4f2963e7e40fe678d))
|
||||
* **submissions:** notDE, notEN for unambiguous negation ([ae66fdf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ae66fdfb8aa51cd7f6e6ad4e263e52cd7043abcb))
|
||||
* **submissions:** shorter solution: remove check for `CourseParticipantActive` ([a358cdd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a358cdd100065014a1b7dc1055d7fc2ea1265011))
|
||||
|
||||
## [25.26.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.25.0...v25.26.0) (2022-02-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **csv:** add export-exam-label as csv option ([de917a8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/de917a8d8646c4df599d945bc4979e226dd5192f))
|
||||
* **eo-exams:** select column for exam list in case of actions ([42f58da](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42f58da44fe1b0cc016979b9cde0300832cc0ae1))
|
||||
* **eoexamsr:** implement label sorting ([808c2fc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/808c2fc7708cbf69172cc348e8a2c47c641287b1))
|
||||
* **eoexamsr:** introduce GET param to control synced display ([09261ac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/09261ac7578d8abdcaa39bbdcf12fc6ddad9ce22))
|
||||
* **eoexamsr:** use user get-synced setting if no param present ([e60d125](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e60d125e05bcf152b57fa132b4957405c40ae03f))
|
||||
* **labels:** actions for setting and removing labels ([9e81f03](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e81f03742586ef40e8ff896e303ba11d95f120a))
|
||||
* **labels:** hide csv export option for non-exam offices ([ec55a40](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ec55a40bc11500105de0b9b9c5cbeb1ba035d53f))
|
||||
* **labels:** label filter ([544b9ef](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/544b9ef76260eb9aecb1a5ee2ca3467f08cd99f7))
|
||||
* **labels:** set export label on exam csv export ([4557fdd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4557fdda928fa5f24f4da48aff44fa8b8d955344))
|
||||
* **labels:** set export label on external exam csv export ([2071f59](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2071f5912d4d183abb55f258450b374821f426e6))
|
||||
* **profile:** upsert eo-labels on form submit ([cae652b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cae652b512137d3a85248d164fb16a6a7d8c097f))
|
||||
* **user:** add get-labels to exam office user setting ([5a3c590](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5a3c590b72394fb1fd4544aace2a762300b4551d))
|
||||
* **user:** add get-labels user setting ([6a10bd7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6a10bd78f55068eca71a0c190dcee5b066430b93))
|
||||
* **user:** introduce exam office user settings ([6788f92](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6788f923ed10759a4d5235c119b94e4c2a35a8b4))
|
||||
|
||||
* **email:** rename settings parameter and switch to safe default ([5aa096f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5aa096f56acb37269b681abafef67b8a375f4d64))
|
||||
|
||||
## [25.21.22](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.21.21...v25.21.22) (2022-01-06)
|
||||
@ -104,6 +234,27 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix typo ([0dffa04](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0dffa04caa141e8e5c060ba342610ebd0049d548))
|
||||
* **subs:** primarily order subs list by assigned time ([803d8b3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/803d8b30df9d8764dd515476f942c0e08e1da1f6))
|
||||
* avoid column-off-by-one with URL-links to tr elems ([dbdd3dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dbdd3dc5652ea274c91efc1c25f161c6fd3d850d))
|
||||
* hlint ([2286086](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/22860863e59f0f927d6424552b913297fee24f14))
|
||||
* **eo-exams:** display exams without label ([5fe01ce](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5fe01ce8dc87e4a318be1885a053b7403813cffa))
|
||||
* **eo-exams:** fix eo-labels query ([eba56e4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eba56e4d621aa62fdee7535c8b83e0b61f0352d8))
|
||||
* **labels:** correct forced bool value for no export label ([7b16351](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7b16351e4b84eba0be1db7d723b4953e991b42b5))
|
||||
* **labels:** fix exam-label delete action ([b1991ee](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b1991eead90a21a296fa0436485ea2532223c72d))
|
||||
* **labels:** implement label deletion on ProfileR ([da39b05](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/da39b05627058a1472929b46ae1c2adb0b1fe2c9))
|
||||
* **tests:** complete test user definition ([11b7786](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/11b77867acd83a1fe23341d6aa9b0e3fd66506c0))
|
||||
|
||||
## [25.25.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.24.1...v25.25.0) (2022-01-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **communication:** support attachments in course/tutorial comm's ([5bd9ea8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5bd9ea85e8f0e4387cf47116bf42c4441bdbe8b3))
|
||||
* **file-field:** cumulative size limit ([b749039](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b749039636c61157b5fc0bea9848ab9828ee671c))
|
||||
|
||||
## [25.24.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.24.0...v25.24.1) (2021-12-29)
|
||||
|
||||
* **email:** instead of sender set reply-to only ([4c8f7e1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4c8f7e1267fe50196d664d733eb794ffaf55aa1c))
|
||||
* **ldap:** fix type in department descriptor ([9697d8c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9697d8c7fa8bbe6db6418107acb21c387cd4672c))
|
||||
* **ldap:** update phone numbers and company data from ldap ([991ee9c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/991ee9c704dea31e65bb347af582f8a81a72aca4))
|
||||
@ -129,6 +280,29 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **courses:** enhanced description of study modules ([89fadb2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89fadb242037151ea792667cab85fc502b135f57))
|
||||
|
||||
## [25.24.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.23.0...v25.24.0) (2021-12-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **course:** show study module on course overview page ([dbc5e99](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dbc5e99109285d4427832820a77a6b47a8098f62))
|
||||
* **course:** study modules as new course property ([cb00de7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cb00de7960c91d87f5f8fb7ecb29dd15cb61a5a3))
|
||||
|
||||
## [25.23.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.4...v25.23.0) (2021-12-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **check-all:** added shift click functionality ([da1c8b5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/da1c8b54510ee1436fefe97ba32372a08299b83e))
|
||||
* **checkrange:** added tooltip ([ce6f09d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce6f09dd857f53dc8c350d7d29b2164c78645b59))
|
||||
* **checkrange:** new util checkrange ([337bf73](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/337bf73067f2b98450d0388a1c064f0d2f9c456c))
|
||||
* **checkrange:** unchecking a range is possible ([154f2e3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/154f2e35cc0e154ff80002b2e0aff3a76afa1ed6))
|
||||
* **erweiterung such-filter usersr:** first try ([da3b339](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/da3b3391bd5aa9990dfb2818847cf8524ee68a9d))
|
||||
* **messages:** added frontend translation class ([61c773f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/61c773f51cddb65dd0529f17799cbf7871023137))
|
||||
* **tooltips:** added translatable tooltip ([e74b610](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e74b61065a5de811bd411c0e863fddf9b9baada0))
|
||||
|
||||
* **status:** module imports fixed ([c59ecf5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c59ecf5019af67c849f32962b8ce9485a7adab1c))
|
||||
* **status:** nix files inaccessible on build server ([1bb500a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1bb500ab027993a97848d357a3382c8975f113eb))
|
||||
|
||||
@ -158,6 +332,12 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
* **build:** bump version numbers for containers ([6678ddc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6678ddcf1aef920e2b04a491f086ac721cae07c9))
|
||||
|
||||
## [25.21.14](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.21.13...v25.21.14) (2021-10-07)
|
||||
* **check-all:** correct constructor argument ([02ce82e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/02ce82e9d2026730fd4716a2c0b070c38a6fc53f))
|
||||
* **frontend-tooltips:** icon is shown ([86ee2fb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/86ee2fb14c05e3b6a78c6c51bf961b6c41d3e5c5))
|
||||
* **modal:** modals are never destroyed ([7dbe1ac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7dbe1ac08aacbda3b145a0da394706273dd6c639))
|
||||
* **modal:** modals are never destroyed ([53dab90](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/53dab90810675f743ece284883da9c4c0e84270e))
|
||||
|
||||
## [25.22.4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.3...v25.22.4) (2021-10-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
@ -167,6 +347,9 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
* **holidays:** minor improvement to memoization ([f411fde](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f411fde42d913d5a9ef357674ab77934c8e1ba42))
|
||||
|
||||
## [25.21.13](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.21.12...v25.21.13) (2021-10-05)
|
||||
* **routes:** make access to workflows free ([29c54db](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/29c54db06f01659a3a6419009964a85cd11d5441))
|
||||
|
||||
## [25.22.3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.2...v25.22.3) (2021-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
@ -205,6 +388,52 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
## [25.21.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.21.1...v25.21.2) (2021-09-20)
|
||||
|
||||
## [25.21.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.21.0...v25.21.1) (2021-09-20)
|
||||
* **navigation:** always link workflows nav to instances ([adf9709](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/adf9709567d9a320f2c17d3c5dde940c2f9d8862))
|
||||
|
||||
## [25.22.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.1...v25.22.2) (2021-10-13)
|
||||
|
||||
## [25.22.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.0...v25.22.1) (2021-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **course-admins:** display course admins as admins instead of assistants ([f1fe444](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f1fe4447fbe7e96e55aaf284c7083338b5135ab6))
|
||||
|
||||
## [25.22.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.21.0...v25.22.0) (2021-08-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **event-manager:** added method to register a list of listeners ([1a8fb23](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1a8fb230441f73b9b1fe593f4df1005a06d9628e))
|
||||
* **event-manager:** mutation observers can be managed via the event manager ([34b4f48](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/34b4f48386c8fff4569b12eb3f7919e7c77d33c0))
|
||||
* **http-client:** added possibility to remove specific interceptors ([0823df3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0823df33b5f157af290aca9a65f1df429048e53b))
|
||||
* **tutoriumsdaten:** application restore ([d4a73e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4a73e699a399b02cadcea03e614c789671ee6d1))
|
||||
* **tutoriumsdaten:** firts draft ([e972788](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e972788f540a9ce6c3fdf841313057b62a579d72))
|
||||
* **tutoriumsdaten:** termin ([ebcb234](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ebcb23429ff09c56ba4a00a6cb8f082ee83e1fe8))
|
||||
* **util_registry:** impelmented destroyAll(scope) method in the utilRegistry ([f1ef2e5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f1ef2e5ec776ad8a1fb7737eb0ed4f79218afd61))
|
||||
* implemented an event manager ([c1c3536](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c1c35369d1ae017971e6d8cbafc06844f02fd00d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **async-form:** destroy all after response is processed ([14a16c7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/14a16c7283483ff22ce22b070a430a61d24a7f35))
|
||||
* **communication-recipients:** fixed undefined error with context and a few minor issues ([03ac803](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/03ac80342e0f3fcb1db2adf34f86a2c0d8fabf0f))
|
||||
* **enter-is-tab.js:** implemented destroy method in enter-is-tab Util ([d1b9952](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d1b995269060c670d85f715671bfc9947b9f3e9a))
|
||||
* **hide-columns:** removed clear storage from destroy method ([b3b0d65](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b3b0d6585068ecbc665e819b31c94f4d96a0fec5))
|
||||
* **hide-colums:** small fix ([50a3ac1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/50a3ac1790f26c1e6f983d3687352d7811173110))
|
||||
* **http-client:** strict equality check ([5078b56](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5078b56c16513f3b289bc4824ce0c7914b1a220d))
|
||||
* **interactive-fieldset:** small fix ([4c2c683](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4c2c68327e75f5f51271853159c232fdd7bba21e))
|
||||
* **navigate-away-promp:** removed unnecessary destroyAll ([7a07159](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7a0715906c8336ed64bbfabdf614746ade9f661f))
|
||||
* **password:** added cleanUP ([204ce39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/204ce39f7c2f9c405971aa59da068a5389dda417))
|
||||
* **show-hide:** storage manager is not cleared ([9453689](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/945368972da36f4ff0da99f38c71ea9a1c7157d7))
|
||||
* **storage-manager:** clear is working without options as well ([f1c50e1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f1c50e137f4f11140c1171dd9d86bc5511b9e771))
|
||||
* **tooltips:** correct regex match ([c8d36ea](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c8d36ea52dbef0a2e1aaec6aedaf7edd524975dc))
|
||||
* **tooltips:** removed else case ([8d0241e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8d0241e727efb5051e5848f2a0c835d7a8d3ae6b))
|
||||
* **util-registry:** filtering activeUtilInstances when a util is destroyed ([5b4ac75](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5b4ac7587438e7adb21e0ff3d9d629b6ff00263a))
|
||||
* **util-registry:** handle negative indices correctly ([cbc03f5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cbc03f57c56df9c96a84da15a68d26904b75a65f))
|
||||
* fixed a few minor issues ([6320cd9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6320cd927a84445f056dc782fb440d276cb26009))
|
||||
* prompt not shwowing up after submit/close ([abe8415](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/abe84156d508dca8fce549b24d5902d24afc0dbf))
|
||||
* smaller fixes and typos ([1f978e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1f978e65a82a91fb728a7ee2970a4fd9e6beb521))
|
||||
|
||||
## [25.21.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.20.2...v25.21.0) (2021-08-20)
|
||||
|
||||
|
||||
@ -226,6 +226,10 @@ cookies:
|
||||
http-only: true
|
||||
secure: "_env:COOKIES_SECURE:true"
|
||||
|
||||
external-apis-ping-interval: 300
|
||||
external-apis-pong-timeout: 600
|
||||
external-apis-expiry: 1200
|
||||
|
||||
user-defaults:
|
||||
max-favourites: 0
|
||||
max-favourite-terms: 2
|
||||
@ -236,6 +240,8 @@ user-defaults:
|
||||
download-files: false
|
||||
warning-days: 1209600
|
||||
show-sex: false
|
||||
exam-office-get-synced: true
|
||||
exam-office-get-labels: true
|
||||
|
||||
# During central allocations lecturer-given ratings of applications (as
|
||||
# ExamGrades) are combined with a central priority.
|
||||
@ -292,5 +298,8 @@ file-source-prewarm:
|
||||
|
||||
bot-mitigations:
|
||||
- only-logged-in-table-sorting
|
||||
- unauthorized-form-honeypots
|
||||
|
||||
volatile-cluster-settings-cache-time: 10
|
||||
|
||||
communication-attachments-max-size: 20971520 # 20MiB
|
||||
|
||||
@ -10,6 +10,8 @@
|
||||
--color-success-dark: #1ca64c
|
||||
--color-info: #c4c4c4
|
||||
--color-info-dark: #919191
|
||||
--color-nonactive: #efefef
|
||||
--color-nonactive-dark: #9a989e
|
||||
--color-lightblack: #1A2A36
|
||||
--color-lightwhite: #fcfffa
|
||||
--color-grey: #B1B5C0
|
||||
@ -165,7 +167,7 @@ h4
|
||||
margin-top: var(--current-header-height)
|
||||
margin-left: 0
|
||||
|
||||
:target:not(table, .show-hide__toggle)::before
|
||||
:target:not(table, tr, .show-hide__toggle)::before
|
||||
content: ""
|
||||
display: block
|
||||
height: var(--current-header-height)
|
||||
@ -740,6 +742,9 @@ section
|
||||
.notification-success
|
||||
color: var(--color-success-dark)
|
||||
|
||||
.notification-nonactive
|
||||
color: var(--color-nonactive)
|
||||
|
||||
// "Heated" element.
|
||||
// Set custom property "--hotness" to a value from 0 to 1 to turn
|
||||
// the element's background to a color on a gradient from green to red.
|
||||
@ -1444,6 +1449,15 @@ a.breadcrumbs__home
|
||||
&__label
|
||||
grid-area: label
|
||||
|
||||
.apidocs
|
||||
pre
|
||||
display: block
|
||||
box-shadow: inset 0 0 4px 4px var(--color-grey-light)
|
||||
white-space: pre-wrap
|
||||
overflow-x: auto
|
||||
tab-size: 2
|
||||
padding: 10px
|
||||
|
||||
.news__system-messages
|
||||
overflow-y: auto
|
||||
max-height: 75vh
|
||||
@ -1476,6 +1490,9 @@ a.breadcrumbs__home
|
||||
|
||||
&--success
|
||||
border-left-color: var(--color-success)
|
||||
|
||||
&--disabled
|
||||
border-left-color: var(--color-nonactive)
|
||||
|
||||
|
||||
.active-allocations__wrapper
|
||||
@ -1737,3 +1754,28 @@ video
|
||||
font-size: .5em
|
||||
font-family: var(--font-monospace)
|
||||
color: var(--color-fontsec)
|
||||
|
||||
.exam-office-label
|
||||
--lbl-padding-vert: 5px
|
||||
--lbl-padding-hori: 15px
|
||||
padding: var(--lbl-padding-vert) var(--lbl-padding-hori)
|
||||
border-radius: 20px 20px 20px 20px
|
||||
font-weight: 600
|
||||
text-align: center
|
||||
width: fit-content
|
||||
margin: 0 auto
|
||||
&.success
|
||||
background-color: var(--color-success-dark)
|
||||
color: var(--color-lightwhite)
|
||||
&.error
|
||||
background-color: var(--color-error)
|
||||
color: var(--color-lightwhite)
|
||||
&.warning
|
||||
background-color: var(--color-warning)
|
||||
color: var(--color-lightwhite)
|
||||
&.info
|
||||
background-color: var(--color-lightblack)
|
||||
color: var(--color-lightwhite)
|
||||
&.nonactive
|
||||
background-color: var(--color-nonactive)
|
||||
color: var(--color-nonactive-dark)
|
||||
|
||||
103
frontend/src/lib/event-manager/event-manager.js
Normal file
103
frontend/src/lib/event-manager/event-manager.js
Normal file
@ -0,0 +1,103 @@
|
||||
|
||||
export const EVENT_TYPE = {
|
||||
CLICK : 'click',
|
||||
KEYDOWN : 'keydown',
|
||||
INVALID : 'invalid',
|
||||
CHANGE : 'change',
|
||||
MOUSE_OVER : 'mouseover',
|
||||
MOUSE_OUT : 'mouseout',
|
||||
SUBMIT : 'submit',
|
||||
INPUT : 'input',
|
||||
FOCUS_OUT : 'focusout',
|
||||
BEFOREUNLOAD : 'beforeunload',
|
||||
HASH_CHANGE : 'hashchange',
|
||||
};
|
||||
|
||||
|
||||
|
||||
export class EventManager {
|
||||
_registeredListeners;
|
||||
_mutationObservers;
|
||||
|
||||
|
||||
constructor() {
|
||||
this._registeredListeners = [];
|
||||
this._mutationObservers = [];
|
||||
}
|
||||
|
||||
registerNewListener(eventWrapper) {
|
||||
this._debugLog('registerNewListener', eventWrapper);
|
||||
let element = eventWrapper.element;
|
||||
element.addEventListener(eventWrapper.eventType, eventWrapper.eventHandler, eventWrapper.options);
|
||||
this._registeredListeners.push(eventWrapper);
|
||||
}
|
||||
|
||||
registerListeners(eventWrappers) {
|
||||
eventWrappers.forEach((eventWrapper) => this.registerNewListener(eventWrapper));
|
||||
}
|
||||
|
||||
registerNewMutationObserver(callback, domNode, config) {
|
||||
let observer = new MutationObserver(callback);
|
||||
observer.observe(domNode, config);
|
||||
this._mutationObservers.push(observer);
|
||||
}
|
||||
|
||||
removeAllEventListenersFromUtil() {
|
||||
this._debugLog('removeAllEventListenersFromUtil');
|
||||
for (let eventWrapper of this._registeredListeners) {
|
||||
let element = eventWrapper.element;
|
||||
element.removeEventListener(eventWrapper.eventType, eventWrapper.eventHandler);
|
||||
}
|
||||
this._registeredListeners = [];
|
||||
}
|
||||
|
||||
removeAllObserversFromUtil() {
|
||||
this._mutationObservers.forEach((observer) => observer.disconnect());
|
||||
this.mutationObservers = [];
|
||||
}
|
||||
|
||||
cleanUp() {
|
||||
this.removeAllObserversFromUtil();
|
||||
this.removeAllEventListenersFromUtil();
|
||||
}
|
||||
|
||||
|
||||
|
||||
_debugLog() {}
|
||||
//_debugLog(fName, ...args) {
|
||||
// console.log(`[DEBUGLOG] EventManager.${fName}`, { args: args, instance: this });
|
||||
//}
|
||||
}
|
||||
|
||||
export class EventWrapper {
|
||||
_eventType;
|
||||
_eventHandler;
|
||||
_element;
|
||||
_options;
|
||||
|
||||
constructor(_eventType, _eventHandler, _element, _options) {
|
||||
if(!_eventType || !_eventHandler || !_element) {
|
||||
throw new Error('Not enough arguments!');
|
||||
}
|
||||
this._eventType = _eventType;
|
||||
this._eventHandler = _eventHandler;
|
||||
this._element = _element;
|
||||
this._options = _options;
|
||||
}
|
||||
|
||||
get eventType() {
|
||||
return this._eventType;
|
||||
}
|
||||
|
||||
get eventHandler() {
|
||||
return this._eventHandler;
|
||||
}
|
||||
|
||||
get element() {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
get options() {
|
||||
return this._options;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import * as debounce from 'lodash.debounce';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
export const MOVEMENT_INDICATOR_ELEMENT_CLASS = 'movement-indicator';
|
||||
const MOVEMENT_DEBOUNCE = 250;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/* global global:writable */
|
||||
|
||||
import * as semver from 'semver';
|
||||
import semver from 'semver';
|
||||
import sodium from 'sodium-javascript';
|
||||
|
||||
import { HttpClient } from '../../services/http-client/http-client';
|
||||
@ -186,14 +186,14 @@ export class StorageManager {
|
||||
}
|
||||
}
|
||||
|
||||
clear(options) {
|
||||
clear(options=this._options) {
|
||||
this._debugLog('clear', options);
|
||||
|
||||
if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
|
||||
throw new Error('StorageManager.clear called with unsupported location option');
|
||||
}
|
||||
|
||||
const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING;
|
||||
const locations = ((options !== undefined) && options.location !== undefined)? [options.location] : this._location_shadowing;
|
||||
|
||||
for (const location of locations) {
|
||||
switch (location) {
|
||||
@ -204,7 +204,10 @@ export class StorageManager {
|
||||
case LOCATION.WINDOW:
|
||||
return this._clearWindow();
|
||||
case LOCATION.HISTORY:
|
||||
return this._clearHistory(options && options.history);
|
||||
if(options && options.history)
|
||||
return this._clearHistory(options.history);
|
||||
else
|
||||
return;
|
||||
default:
|
||||
console.error('StorageManager.clear cannot clear with unsupported location');
|
||||
}
|
||||
@ -466,7 +469,7 @@ export class StorageManager {
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
}).then(
|
||||
(response) => response.json()
|
||||
(response) => response.json(),
|
||||
).then((response) => {
|
||||
console.log('storage-manager got key from response:', response, 'with options:', options);
|
||||
if (response.salt !== requestBody.salt || response.timestamp !== requestBody.timestamp) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const DEBUG_MODE = /localhost/.test(window.location.href) ? 0 : 0;
|
||||
|
||||
import * as defer from 'lodash.defer';
|
||||
import defer from 'lodash.defer';
|
||||
|
||||
class Overhang {
|
||||
colSpan;
|
||||
|
||||
22
frontend/src/lib/tooltips/frontend-tooltips.js
Normal file
22
frontend/src/lib/tooltips/frontend-tooltips.js
Normal file
@ -0,0 +1,22 @@
|
||||
export class FrontendTooltips {
|
||||
|
||||
static addToolTip(element, text) {
|
||||
let tooltipWrap = document.createElement('span');
|
||||
tooltipWrap.className = 'tooltip';
|
||||
|
||||
let tooltipContent = document.createElement('span');
|
||||
tooltipContent.className = 'tooltip__content';
|
||||
tooltipContent.appendChild(document.createTextNode(text));
|
||||
tooltipWrap.append(tooltipContent);
|
||||
|
||||
let tooltipHandle = document.createElement('span');
|
||||
tooltipHandle.className = 'tooltip__handle';
|
||||
let icon = document.createElement('i');
|
||||
icon.classList.add('fas');
|
||||
icon.classList.add('fa-question-circle');
|
||||
tooltipHandle.append(icon);
|
||||
tooltipWrap.append(tooltipHandle);
|
||||
|
||||
element.append(tooltipWrap);
|
||||
}
|
||||
}
|
||||
17
frontend/src/messages.js
Normal file
17
frontend/src/messages.js
Normal file
@ -0,0 +1,17 @@
|
||||
export class Translations {
|
||||
static translations = {
|
||||
'checkrangeTooltip' : {
|
||||
'de' : 'Shift-Klick, um mehrere Zellen zu markieren.',
|
||||
'en' : 'Shift-click to mark multiple cells.',
|
||||
},
|
||||
};
|
||||
|
||||
static getTranslation(key, language) {
|
||||
let json = Translations.translations[key];
|
||||
if(language === 'en') {
|
||||
return json.en;
|
||||
} else {
|
||||
return json.de;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -15,6 +15,18 @@ export class HttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
removeResponseInterceptor(interceptor) {
|
||||
//performs a reference check. if the interceptor is bound, when adding it, the reference of the bound function needs to be the same when removing it later.
|
||||
|
||||
if (typeof interceptor !== 'function') {
|
||||
throw new Error(`Cannot remove Interceptor ${interceptor}, because it is not of type function`);
|
||||
}
|
||||
if(this._responseInterceptors.filter(el => el == interceptor).length === 0) {
|
||||
throw new Error(`Could not find Response Interceptor ${interceptor}.`);
|
||||
}
|
||||
this._responseInterceptors = this._responseInterceptors.filter(el => el !== interceptor);
|
||||
}
|
||||
|
||||
_baseUrl;
|
||||
|
||||
setBaseUrl(baseUrl) {
|
||||
|
||||
@ -75,7 +75,7 @@ describe('HttpClient', () => {
|
||||
expect(httpClient._responseInterceptors.length).toBe(2);
|
||||
});
|
||||
|
||||
describe('get called', () => {
|
||||
describe('get called and removed', () => {
|
||||
let intercepted1;
|
||||
let intercepted2;
|
||||
const interceptors = {
|
||||
@ -111,6 +111,14 @@ describe('HttpClient', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can be removed', () => {
|
||||
expect(httpClient._responseInterceptors.length).toBe(2);
|
||||
httpClient.removeResponseInterceptor(interceptors.interceptor1);
|
||||
expect(httpClient._responseInterceptors.length).toBe(1);
|
||||
expect(() => {httpClient.removeResponseInterceptor(interceptors.interceptor1);}).toThrow();
|
||||
expect(httpClient._responseInterceptors.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import * as toposort from 'toposort';
|
||||
import toposort from 'toposort';
|
||||
|
||||
const DEBUG_MODE = /localhost/.test(window.location.href) ? 1 : 0;
|
||||
|
||||
export class UtilRegistry {
|
||||
|
||||
_registeredUtils = new Array();
|
||||
_activeUtilInstances = new Array();
|
||||
_registeredUtilClasses = new Array(); //{utilClass}
|
||||
_activeUtilInstancesWrapped = new Array(); //{utilClass, scope, element, instance}
|
||||
_appInstance;
|
||||
|
||||
/**
|
||||
@ -33,7 +33,7 @@ export class UtilRegistry {
|
||||
console.log('registering util "' + util.name + '"');
|
||||
console.log({ util });
|
||||
}
|
||||
this._registeredUtils.push(util);
|
||||
this._registeredUtilClasses.push(util);
|
||||
}
|
||||
|
||||
deregister(name, destroy) {
|
||||
@ -44,7 +44,7 @@ export class UtilRegistry {
|
||||
this._destroyUtilInstances(name);
|
||||
}
|
||||
|
||||
this._registeredUtils.splice(utilIndex, 1);
|
||||
this._registeredUtilClasses.splice(utilIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ export class UtilRegistry {
|
||||
|
||||
initAll(scope = document.body) {
|
||||
let startedInstances = new Array();
|
||||
const setupInstances = this._registeredUtils.map((util) => this.setup(util, scope)).flat();
|
||||
const setupInstances = this._registeredUtilClasses.map((util) => this.setup(util, scope)).flat();
|
||||
|
||||
const orderedInstances = setupInstances.filter(_isStartOrdered);
|
||||
|
||||
@ -97,6 +97,20 @@ export class UtilRegistry {
|
||||
return startedInstances;
|
||||
}
|
||||
|
||||
destroyAll(scope = document.body) {
|
||||
let utilsInScope = this._getUtilInstancesWithinScope(scope);
|
||||
|
||||
utilsInScope.forEach((util) => {
|
||||
if(DEBUG_MODE > 2) {
|
||||
console.log('Destroying Util: ', {util});
|
||||
}
|
||||
util.destroy();
|
||||
this._activeUtilInstancesWrapped = this._activeUtilInstancesWrapped.filter(utilWrapped => {
|
||||
return utilWrapped.element === util._element;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setup(util, scope = document.body) {
|
||||
if (DEBUG_MODE > 2) {
|
||||
console.log('setting up util', { util });
|
||||
@ -130,12 +144,12 @@ export class UtilRegistry {
|
||||
});
|
||||
}
|
||||
|
||||
this._activeUtilInstances.push(...instances);
|
||||
this._activeUtilInstancesWrapped.push(...instances);
|
||||
return instances;
|
||||
}
|
||||
|
||||
find(name) {
|
||||
return this._registeredUtils.find((util) => util.name === name);
|
||||
return this._registeredUtilClasses.find((util) => util.name === name);
|
||||
}
|
||||
|
||||
_findUtilElements(util, scope) {
|
||||
@ -146,24 +160,33 @@ export class UtilRegistry {
|
||||
}
|
||||
|
||||
_findUtilIndex(name) {
|
||||
return this._registeredUtils.findIndex((util) => util.name === name);
|
||||
return this._registeredUtilClasses.findIndex((util) => util.name === name);
|
||||
}
|
||||
|
||||
_getUtilInstancesWithinScope(scope) {
|
||||
let utilInstances = [];
|
||||
|
||||
for (let activeUtilInstance of this._activeUtilInstancesWrapped) {
|
||||
let util = activeUtilInstance.util;
|
||||
if(this._findUtilElements(util, scope).length > 0) {
|
||||
utilInstances.push(activeUtilInstance.instance);
|
||||
}
|
||||
}
|
||||
return utilInstances;
|
||||
}
|
||||
|
||||
_destroyUtilInstances(name) {
|
||||
this._activeUtilInstances
|
||||
this._activeUtilInstancesWrapped
|
||||
.map((util, index) => ({
|
||||
util: util,
|
||||
index: index,
|
||||
}))
|
||||
.filter((activeUtil) => activeUtil.util.name === name)
|
||||
.filter((activeUtil) => activeUtil.util.util.name === name)
|
||||
.forEach((activeUtil) => {
|
||||
// destroy util instance
|
||||
activeUtil.util.destroy();
|
||||
delete this._activeUtilInstances[activeUtil.index];
|
||||
activeUtil.util.instance.destroy();
|
||||
this._activeUtilInstancesWrapped = this._activeUtilInstancesWrapped.splice(activeUtil.index, 1);
|
||||
});
|
||||
|
||||
// get rid of now empty array slots
|
||||
this._activeUtilInstances = this._activeUtilInstances.filter((util) => !!util);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -24,24 +24,6 @@ describe('UtilRegistry', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('deregister()', () => {
|
||||
it('should remove util', () => {
|
||||
// register util
|
||||
utilRegistry.register(TestUtil1);
|
||||
let foundUtil = utilRegistry.find(TestUtil1.name);
|
||||
expect(foundUtil).toBeTruthy();
|
||||
|
||||
// deregister util
|
||||
utilRegistry.deregister(TestUtil1.name);
|
||||
foundUtil = utilRegistry.find(TestUtil1.name);
|
||||
expect(foundUtil).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should destroy util instances if requested', () => {
|
||||
pending('TBD');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setup()', () => {
|
||||
|
||||
it('should catch errors thrown by the utility', () => {
|
||||
@ -107,6 +89,51 @@ describe('UtilRegistry', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('deregister()', () => {
|
||||
let testScope;
|
||||
let testElement1;
|
||||
let testElement2;
|
||||
|
||||
beforeEach(() => {
|
||||
testScope = document.createElement('div');
|
||||
testElement1 = document.createElement('div');
|
||||
testElement2 = document.createElement('div');
|
||||
testElement1.classList.add('util1');
|
||||
testElement2.classList.add('util1');
|
||||
testScope.appendChild(testElement1);
|
||||
testScope.appendChild(testElement2);
|
||||
});
|
||||
|
||||
it('should remove util', () => {
|
||||
// register util
|
||||
utilRegistry.register(TestUtil1);
|
||||
let foundUtil = utilRegistry.find(TestUtil1.name);
|
||||
expect(foundUtil).toBeTruthy();
|
||||
|
||||
// deregister util
|
||||
utilRegistry.deregister(TestUtil1.name);
|
||||
foundUtil = utilRegistry.find(TestUtil1.name);
|
||||
expect(foundUtil).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should destroy util instances if requested', () => {
|
||||
utilRegistry.register(TestUtil1);
|
||||
let foundUtil = utilRegistry.find(TestUtil1.name);
|
||||
expect(foundUtil).toBeTruthy();
|
||||
|
||||
utilRegistry.setup(TestUtil1, testScope);
|
||||
let firstActiveUtil = utilRegistry._activeUtilInstancesWrapped[0];
|
||||
expect(utilRegistry._activeUtilInstancesWrapped.length).toEqual(2);
|
||||
expect(utilRegistry._activeUtilInstancesWrapped[0].element).toEqual(testElement1);
|
||||
|
||||
spyOn(firstActiveUtil.instance, 'destroy');
|
||||
|
||||
utilRegistry.deregister(TestUtil1.name, true);
|
||||
expect(utilRegistry._activeUtilInstancesWrapped[0]).toBeFalsy();
|
||||
expect(firstActiveUtil.instance.destroy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('initAll()', () => {
|
||||
it('should setup all the utilities', () => {
|
||||
spyOn(utilRegistry, 'setup');
|
||||
@ -172,6 +199,44 @@ describe('UtilRegistry', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroyAll()', () => {
|
||||
let testScope;
|
||||
let testElement;
|
||||
let firstUtil;
|
||||
|
||||
beforeEach( () => {
|
||||
testScope = document.createElement('div');
|
||||
testElement = document.createElement('div');
|
||||
testElement.classList.add('util3');
|
||||
testScope.appendChild(testElement);
|
||||
|
||||
utilRegistry.register(TestUtil3);
|
||||
utilRegistry.initAll(testScope);
|
||||
|
||||
firstUtil = utilRegistry._activeUtilInstancesWrapped[0];
|
||||
spyOn(firstUtil.instance, 'destroy');
|
||||
});
|
||||
|
||||
it('Util should be destroyed', () => {
|
||||
utilRegistry.destroyAll(testScope);
|
||||
expect(utilRegistry._activeUtilInstancesWrapped.length).toBe(0);
|
||||
expect(firstUtil.instance.destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Util out of scope should not be destroyed', () => {
|
||||
let outOfScope = document.createElement('div');
|
||||
expect(utilRegistry._activeUtilInstancesWrapped.length).toEqual(1);
|
||||
|
||||
utilRegistry.destroyAll(outOfScope);
|
||||
|
||||
expect(utilRegistry._activeUtilInstancesWrapped.length).toEqual(1);
|
||||
expect(utilRegistry._activeUtilInstancesWrapped[0]).toBe(firstUtil);
|
||||
expect(firstUtil.instance.destroy).not.toHaveBeenCalled();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// test utilities
|
||||
@ -181,6 +246,8 @@ class TestUtil1 {
|
||||
this.element = element;
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
}
|
||||
|
||||
@Utility({ selector: '#util2' })
|
||||
@ -190,6 +257,7 @@ class TestUtil2 { }
|
||||
class TestUtil3 {
|
||||
constructor() {}
|
||||
start() {}
|
||||
destroy() {}
|
||||
}
|
||||
|
||||
@Utility({ selector: '#throws' })
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import './alerts.sass';
|
||||
|
||||
const ALERTS_INITIALIZED_CLASS = 'alerts--initialized';
|
||||
@ -32,6 +33,9 @@ export class Alerts {
|
||||
_element;
|
||||
_app;
|
||||
|
||||
_eventManager;
|
||||
_boundResponseInterceptor;
|
||||
|
||||
constructor(element, app) {
|
||||
if (!element) {
|
||||
throw new Error('Alerts util has to be called with an element!');
|
||||
@ -40,6 +44,9 @@ export class Alerts {
|
||||
this._element = element;
|
||||
this._app = app;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
this._boundResponseInterceptor = this._responseInterceptor.bind(this);
|
||||
|
||||
if (this._element.classList.contains(ALERTS_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
@ -47,21 +54,30 @@ export class Alerts {
|
||||
this._togglerElement = this._element.querySelector('.' + ALERTS_TOGGLER_CLASS);
|
||||
this._alertElements = this._gatherAlertElements();
|
||||
|
||||
if (this._togglerElement) {
|
||||
this._initToggler();
|
||||
}
|
||||
|
||||
this._initAlerts();
|
||||
|
||||
// register http client interceptor to filter out Alerts Header
|
||||
this._setupHttpInterceptor();
|
||||
|
||||
// mark initialized
|
||||
this._element.classList.add(ALERTS_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this._togglerElement) {
|
||||
this._initToggler();
|
||||
}
|
||||
this._initAlerts();
|
||||
|
||||
// register http client interceptor to filter out Alerts Header
|
||||
this._setupHttpInterceptor();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
console.log('TBD: Destroy Alert');
|
||||
this._eventManager.cleanUp();
|
||||
this._app.httpClient.removeResponseInterceptor(this._boundResponseInterceptor);
|
||||
|
||||
if(this._alertElements) {
|
||||
this._alertElements.forEach(element => element.remove());
|
||||
}
|
||||
|
||||
if(this._element.classList.contains(ALERTS_INITIALIZED_CLASS))
|
||||
this._element.classList.remove(ALERTS_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
_gatherAlertElements() {
|
||||
@ -71,10 +87,12 @@ export class Alerts {
|
||||
}
|
||||
|
||||
_initToggler() {
|
||||
this._togglerElement.addEventListener('click', () => {
|
||||
let clickListenerToggler = new EventWrapper(EVENT_TYPE.CLICK, () => {
|
||||
this._alertElements.forEach((alertEl) => this._toggleAlert(alertEl, true));
|
||||
this._togglerElement.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS);
|
||||
});
|
||||
}, this._togglerElement);
|
||||
|
||||
this._eventManager.registerNewListener(clickListenerToggler);
|
||||
}
|
||||
|
||||
_initAlerts() {
|
||||
@ -88,9 +106,11 @@ export class Alerts {
|
||||
}
|
||||
|
||||
const closeEl = alertElement.querySelector('.' + ALERT_CLOSER_CLASS);
|
||||
closeEl.addEventListener('click', () => {
|
||||
const closeAlertEvent = new EventWrapper(EVENT_TYPE.CLICK, (() => {
|
||||
this._toggleAlert(alertElement);
|
||||
});
|
||||
}).bind(this), closeEl);
|
||||
|
||||
this._eventManager.registerNewListener(closeAlertEvent);
|
||||
|
||||
if (autoHideDelay > 0 && alertElement.matches(ALERT_AUTOCLOSING_MATCHER)) {
|
||||
window.setTimeout(() => this._toggleAlert(alertElement), autoHideDelay * 1000);
|
||||
@ -118,7 +138,7 @@ export class Alerts {
|
||||
}
|
||||
|
||||
_setupHttpInterceptor() {
|
||||
this._app.httpClient.addResponseInterceptor(this._responseInterceptor.bind(this));
|
||||
this._app.httpClient.addResponseInterceptor(this._boundResponseInterceptor);
|
||||
}
|
||||
|
||||
_elevateAlerts() {
|
||||
@ -145,7 +165,7 @@ export class Alerts {
|
||||
|
||||
this._elevateAlerts();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_createAlertElement(type, content, icon = 'info-circle') {
|
||||
const alertElement = document.createElement('div');
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { Alerts } from './alerts';
|
||||
import { Alerts, ALERTS_INITIALIZED_CLASS } from './alerts';
|
||||
|
||||
const MOCK_APP = {
|
||||
httpClient: {
|
||||
addResponseInterceptor: () => {},
|
||||
removeResponseInterceptor: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
@ -19,6 +20,12 @@ describe('Alerts', () => {
|
||||
expect(alerts).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should destory alerts', () => {
|
||||
alerts.destroy();
|
||||
expect(alerts._eventManager._registeredListeners.length).toBe(0);
|
||||
expect(alerts._element.classList).not.toContain(ALERTS_INITIALIZED_CLASS);
|
||||
});
|
||||
|
||||
it('should throw if called without an element', () => {
|
||||
expect(() => {
|
||||
new Alerts();
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import './asidenav.sass';
|
||||
|
||||
const FAVORITES_BTN_CLASS = 'navbar__list-item--favorite';
|
||||
@ -15,6 +16,7 @@ export class Asidenav {
|
||||
|
||||
_element;
|
||||
_asidenavSubmenus;
|
||||
_eventManager;
|
||||
|
||||
constructor(element) {
|
||||
if (!element) {
|
||||
@ -23,6 +25,8 @@ export class Asidenav {
|
||||
|
||||
this._element = element;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(ASIDENAV_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
@ -35,19 +39,24 @@ export class Asidenav {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._asidenavSubmenus.forEach((union) => {
|
||||
union.listItem.removeEventListener(union.hoverHandler);
|
||||
});
|
||||
|
||||
this._eventManager.cleanUp();
|
||||
|
||||
if(this._element.classList.contains(ASIDENAV_INITIALIZED_CLASS))
|
||||
this._element.classList.remove(ASIDENAV_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
_initFavoritesButton() {
|
||||
const favoritesBtn = document.querySelector('.' + FAVORITES_BTN_CLASS);
|
||||
if (favoritesBtn) {
|
||||
favoritesBtn.addEventListener('click', (event) => {
|
||||
|
||||
const favoritesButtonEvent = new EventWrapper(EVENT_TYPE.CLICK, (event) => {
|
||||
favoritesBtn.classList.toggle(FAVORITES_BTN_ACTIVE_CLASS);
|
||||
this._element.classList.toggle(ASIDENAV_EXPANDED_CLASS);
|
||||
event.preventDefault();
|
||||
}, true);
|
||||
}, favoritesBtn, true);
|
||||
|
||||
this._eventManager.registerNewListener(favoritesButtonEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +71,8 @@ export class Asidenav {
|
||||
|
||||
this._asidenavSubmenus.forEach((union) => {
|
||||
union.hoverHandler = this._createMouseoverHandler(union);
|
||||
union.listItem.addEventListener('mouseover', union.hoverHandler);
|
||||
let currentHoverEvent = new EventWrapper(EVENT_TYPE.MOUSE_OVER, union.hoverHandler, union.listItem);
|
||||
this._eventManager.registerNewListener(currentHoverEvent);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Asidenav } from './asidenav';
|
||||
import { Asidenav, ASIDENAV_INITIALIZED_CLASS } from './asidenav';
|
||||
|
||||
describe('Asidenav', () => {
|
||||
|
||||
@ -13,6 +13,12 @@ describe('Asidenav', () => {
|
||||
expect(asidenav).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should destory asidenav', () => {
|
||||
asidenav.destroy();
|
||||
expect(asidenav._eventManager._registeredListeners.length).toBe(0);
|
||||
expect(asidenav._element.classList).not.toContain(ASIDENAV_INITIALIZED_CLASS);
|
||||
});
|
||||
|
||||
it('should throw if called without an element', () => {
|
||||
expect(() => {
|
||||
new Asidenav();
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { Datepicker } from '../form/datepicker';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import './async-form.sass';
|
||||
|
||||
const ASYNC_FORM_INITIALIZED_CLASS = 'check-all--initialized';
|
||||
@ -20,6 +21,8 @@ export class AsyncForm {
|
||||
_element;
|
||||
_app;
|
||||
|
||||
_eventManager;
|
||||
|
||||
constructor(element, app) {
|
||||
if (!element) {
|
||||
throw new Error('Async Form Utility cannot be setup without an element!');
|
||||
@ -28,17 +31,23 @@ export class AsyncForm {
|
||||
this._element = element;
|
||||
this._app = app;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(ASYNC_FORM_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._element.addEventListener('submit', this._submitHandler);
|
||||
const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, this._submitHandler.bind(this), this._element);
|
||||
this._eventManager.registerNewListener(submitEvent);
|
||||
|
||||
this._element.classList.add(ASYNC_FORM_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// TODO
|
||||
this._eventManager.cleanUp();
|
||||
|
||||
if(this._element.classList.contains(ASYNC_FORM_INITIALIZED_CLASS))
|
||||
this._element.classList.remove(ASYNC_FORM_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
_processResponse(response) {
|
||||
@ -85,14 +94,13 @@ export class AsyncForm {
|
||||
headers: headers,
|
||||
body: body,
|
||||
}).then(
|
||||
(response) => response.json()
|
||||
(response) => response.json(),
|
||||
).then(
|
||||
(response) => this._processResponse(response[0])
|
||||
(response) => this._processResponse(response[0]),
|
||||
).catch(() => {
|
||||
const failureMessage = this._app.i18n.get('asyncFormFailure');
|
||||
this._processResponse({ content: failureMessage });
|
||||
|
||||
this._element.classList.remove(ASYNC_FORM_LOADING_CLASS);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AsyncForm } from './async-form';
|
||||
import { AsyncForm, ASYNC_FORM_INITIALIZED_CLASS } from './async-form';
|
||||
|
||||
describe('AsyncForm', () => {
|
||||
|
||||
@ -13,6 +13,12 @@ describe('AsyncForm', () => {
|
||||
expect(asyncForm).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should destroy asyncForm', () => {
|
||||
asyncForm.destroy();
|
||||
expect(asyncForm._eventManager._registeredListeners.length).toBe(0);
|
||||
expect(asyncForm._element.classList).not.toContain(ASYNC_FORM_INITIALIZED_CLASS);
|
||||
});
|
||||
|
||||
it('should throw if called without an element', () => {
|
||||
expect(() => {
|
||||
new AsyncForm();
|
||||
|
||||
@ -2,8 +2,9 @@ import { Utility } from '../../core/utility';
|
||||
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
|
||||
import { Datepicker } from '../form/datepicker';
|
||||
import { HttpClient } from '../../services/http-client/http-client';
|
||||
import * as debounce from 'lodash.debounce';
|
||||
import * as throttle from 'lodash.throttle';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import debounce from 'lodash.debounce';
|
||||
import throttle from 'lodash.throttle';
|
||||
import './async-table-filter.sass';
|
||||
import './async-table.sass';
|
||||
|
||||
@ -30,6 +31,8 @@ export class AsyncTable {
|
||||
_element;
|
||||
_app;
|
||||
|
||||
_eventManager;
|
||||
|
||||
_asyncTableHeader;
|
||||
_asyncTableId;
|
||||
|
||||
@ -66,6 +69,8 @@ export class AsyncTable {
|
||||
this._element = element;
|
||||
this._app = app;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
@ -144,7 +149,11 @@ export class AsyncTable {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
console.log('TBD: Destroy AsyncTable');
|
||||
this._windowStorage.clear(this._windowStorage._options);
|
||||
this._eventManager.cleanUp();
|
||||
this._active = false;
|
||||
if (this._element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS))
|
||||
this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
_startSortableHeaders() {
|
||||
@ -156,7 +165,8 @@ export class AsyncTable {
|
||||
this._windowStorage.save('horizPos', (this._scrollTable || {}).scrollLeft);
|
||||
this._linkClickHandler(event);
|
||||
};
|
||||
th.element.addEventListener('click', th.clickHandler);
|
||||
const linkClickEvent = new EventWrapper(EVENT_TYPE.CLICK, th.clickHandler.bind(this), th.element);
|
||||
this._eventManager.registerNewListener(linkClickEvent);
|
||||
});
|
||||
}
|
||||
|
||||
@ -179,7 +189,9 @@ export class AsyncTable {
|
||||
}
|
||||
this._linkClickHandler(event);
|
||||
};
|
||||
link.element.addEventListener('click', link.clickHandler);
|
||||
|
||||
const clickEvent = new EventWrapper(EVENT_TYPE.CLICK, link.clickHandler.bind(this), link.element);
|
||||
this._eventManager.registerNewListener(clickEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -190,7 +202,8 @@ export class AsyncTable {
|
||||
|
||||
if (this._pagesizeForm) {
|
||||
const pagesizeSelect = this._pagesizeForm.querySelector('[name=' + this._asyncTableId + '-pagesize]');
|
||||
pagesizeSelect.addEventListener('change', this._changePagesizeHandler);
|
||||
const pageSizeChangeEvent = new EventWrapper(EVENT_TYPE.CHANGE, this._changePagesizeHandler.bind(this), pagesizeSelect);
|
||||
this._eventManager.registerNewListener(pageSizeChangeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,17 +240,7 @@ export class AsyncTable {
|
||||
const debouncedUpdateFromTableFilter = throttle((() => this._updateFromTableFilter(tableFilterForm)).bind(this), FILTER_DEBOUNCE, { leading: true, trailing: false });
|
||||
|
||||
[...this._tableFilterInputs.search, ...this._tableFilterInputs.input].forEach((input) => {
|
||||
const submitLockObserver = new MutationObserver((mutations, observer) => {
|
||||
for (const mutation of mutations) {
|
||||
// if the submit lock has been released, trigger an update and disconnect this observer
|
||||
if (mutation.target === input && mutation.attributeName === ATTR_SUBMIT_LOCKED && mutation.oldValue === 'true' && mutation.target.getAttribute(mutation.attributeName) === 'false') {
|
||||
debouncedUpdateFromTableFilter();
|
||||
observer.disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
this._cancelPendingUpdates.push(() => { submitLockObserver.disconnect(); });
|
||||
this._cancelPendingUpdates.push(() => { this._eventManager.removeAllObserversFromUtil();});
|
||||
|
||||
const debouncedInput = debounce(() => {
|
||||
const submitLockedAttr = input.getAttribute(ATTR_SUBMIT_LOCKED);
|
||||
@ -246,7 +249,16 @@ export class AsyncTable {
|
||||
debouncedUpdateFromTableFilter();
|
||||
} else if (submitLockedAttr === 'true') {
|
||||
// observe the submit lock of the input element
|
||||
submitLockObserver.observe(input, {
|
||||
this._eventManager.registerNewMutationObserver(((mutations, observer) => {
|
||||
for (const mutation of mutations) {
|
||||
// if the submit lock has been released, trigger an update and disconnect this observer
|
||||
if (mutation.target === input && mutation.attributeName === ATTR_SUBMIT_LOCKED && mutation.oldValue === 'true' && mutation.target.getAttribute(mutation.attributeName) === 'false') {
|
||||
debouncedUpdateFromTableFilter();
|
||||
observer.disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).bind(this), input, {
|
||||
attributes: true,
|
||||
attributeFilter: [ATTR_SUBMIT_LOCKED],
|
||||
attributeOldValue: true,
|
||||
@ -254,33 +266,42 @@ export class AsyncTable {
|
||||
}
|
||||
}, INPUT_DEBOUNCE);
|
||||
this._cancelPendingUpdates.push(debouncedInput.cancel);
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
|
||||
const inputHandler =() => {
|
||||
this._ignoreRequest = true;
|
||||
debouncedInput();
|
||||
});
|
||||
};
|
||||
const inputEvent = new EventWrapper(EVENT_TYPE.INPUT, inputHandler.bind(this), input );
|
||||
this._eventManager.registerNewListener(inputEvent);
|
||||
});
|
||||
|
||||
this._tableFilterInputs.change.forEach((input) => {
|
||||
input.addEventListener('change', () => {
|
||||
|
||||
const changeHandler = () => {
|
||||
//if (this._element.classList.contains(ASYNC_TABLE_LOADING_CLASS))
|
||||
this._ignoreRequest = true;
|
||||
debouncedUpdateFromTableFilter();
|
||||
});
|
||||
};
|
||||
const changeEvent = new EventWrapper(EVENT_TYPE.CHANGE, changeHandler.bind(this), input);
|
||||
this._eventManager.registerNewListener(changeEvent);
|
||||
});
|
||||
|
||||
this._tableFilterInputs.select.forEach((input) => {
|
||||
input.addEventListener('change', () => {
|
||||
const selectChangeHandler = () => {
|
||||
this._ignoreRequest = true;
|
||||
debouncedUpdateFromTableFilter();
|
||||
});
|
||||
};
|
||||
const selectEvent = new EventWrapper(EVENT_TYPE.CHANGE, selectChangeHandler.bind(this), input);
|
||||
this._eventManager.registerNewListener(selectEvent);
|
||||
});
|
||||
|
||||
tableFilterForm.addEventListener('submit', (event) =>{
|
||||
const submitEventHandler = (event) =>{
|
||||
event.preventDefault();
|
||||
this._ignoreRequest = true;
|
||||
debouncedUpdateFromTableFilter();
|
||||
});
|
||||
};
|
||||
const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, submitEventHandler.bind(this), tableFilterForm);
|
||||
this._eventManager.registerNewListener(submitEvent);
|
||||
}
|
||||
|
||||
_updateFromTableFilter(tableFilterForm) {
|
||||
@ -362,7 +383,7 @@ export class AsyncTable {
|
||||
url = window.location.origin + window.location.pathname + url;
|
||||
}
|
||||
this._updateTableFrom(url);
|
||||
}
|
||||
};
|
||||
|
||||
_getClickDestination(el) {
|
||||
if (!el.matches('a') && !el.querySelector('a')) {
|
||||
@ -386,7 +407,7 @@ export class AsyncTable {
|
||||
}
|
||||
|
||||
this._updateTableFrom(url.href);
|
||||
}
|
||||
};
|
||||
|
||||
// fetches new sorted element from url with params and replaces contents of current element
|
||||
_updateTableFrom(url, callback, isPopState) {
|
||||
@ -425,6 +446,7 @@ export class AsyncTable {
|
||||
this._active = false;
|
||||
this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
|
||||
this._element.dataset['currentTableUrl'] = url.href;
|
||||
|
||||
// update table with new
|
||||
this._element.innerHTML = response.element.innerHTML;
|
||||
|
||||
@ -435,12 +457,12 @@ export class AsyncTable {
|
||||
callback(this._element);
|
||||
this._windowStorage.remove('cssIdPrefix');
|
||||
}
|
||||
}).catch((err) => console.error(err)
|
||||
}).catch((err) => console.error(err),
|
||||
).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS));
|
||||
}
|
||||
|
||||
_debugLog() {}
|
||||
// _debugLog(fName, ...args) {
|
||||
//_debugLog(fName, ...args) {
|
||||
// console.log(`[DEBUGLOG] AsyncTable.${fName}`, { args: args, instance: this });
|
||||
// }
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AsyncTable } from './async-table';
|
||||
import { AsyncTable, ASYNC_TABLE_INITIALIZED_CLASS } from './async-table';
|
||||
|
||||
const AppTestMock = {
|
||||
httpClient: {
|
||||
@ -50,4 +50,11 @@ describe('AsyncTable', () => {
|
||||
new AsyncTable();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should destroy Async Table', () => {
|
||||
asyncTable.start();
|
||||
asyncTable.destroy();
|
||||
expect(asyncTable._eventManager._registeredListeners.length).toBe(0);
|
||||
expect(asyncTable._element.classList).not.toContain(ASYNC_TABLE_INITIALIZED_CLASS);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@ const DEBUG_MODE = /localhost/.test(window.location.href) ? 0 : 0;
|
||||
|
||||
import { Utility } from '../../core/utility';
|
||||
import { TableIndices } from '../../lib/table/table';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
const CHECKBOX_SELECTOR = '[type="checkbox"]';
|
||||
|
||||
@ -13,11 +14,15 @@ const CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized';
|
||||
export class CheckAll {
|
||||
_element;
|
||||
|
||||
_eventManager;
|
||||
|
||||
_columns = new Array();
|
||||
_checkAllColumns = new Array();
|
||||
|
||||
_tableIndices;
|
||||
|
||||
_lastCheckedCell = null;
|
||||
|
||||
constructor(element, app) {
|
||||
if (!element) {
|
||||
throw new Error('Check All utility cannot be setup without an element!');
|
||||
@ -25,6 +30,8 @@ export class CheckAll {
|
||||
|
||||
this._element = element;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(CHECK_ALL_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
@ -36,12 +43,25 @@ export class CheckAll {
|
||||
if (DEBUG_MODE > 0)
|
||||
console.log(this._columns);
|
||||
|
||||
this._findCheckboxColumns().forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId])));
|
||||
let checkboxColumns = this._findCheckboxColumns();
|
||||
|
||||
checkboxColumns.forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId], this._eventManager)));
|
||||
|
||||
// mark initialized
|
||||
this._element.classList.add(CHECK_ALL_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
this._checkAllColumns.forEach((column) => {
|
||||
if (column._checkAllCheckBox !== undefined)
|
||||
column._checkAllCheckBox.remove();
|
||||
});
|
||||
|
||||
if(this._element.classList.contains(CHECK_ALL_INITIALIZED_CLASS))
|
||||
this._element.classList.remove(CHECK_ALL_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
_gatherColumns() {
|
||||
for (const rowIndex of Array(this._tableIndices.maxRow + 1).keys()) {
|
||||
for (const colIndex of Array(this._tableIndices.maxCol + 1).keys()) {
|
||||
@ -81,14 +101,17 @@ class CheckAllColumn {
|
||||
_app;
|
||||
_table;
|
||||
_column;
|
||||
|
||||
_eventManager;
|
||||
|
||||
_checkAllCheckbox;
|
||||
_checkboxId = 'check-all-checkbox-' + Math.floor(Math.random() * 100000);
|
||||
|
||||
constructor(table, app, column) {
|
||||
constructor(table, app, column, eventManager) {
|
||||
this._column = column;
|
||||
this._table = table;
|
||||
this._app = app;
|
||||
this._eventManager = eventManager;
|
||||
|
||||
const th = this._column.filter(element => element.tagName == 'TH')[0];
|
||||
if (!th)
|
||||
@ -97,12 +120,14 @@ class CheckAllColumn {
|
||||
this._checkAllCheckbox = document.createElement('input');
|
||||
this._checkAllCheckbox.setAttribute('type', 'checkbox');
|
||||
this._checkAllCheckbox.setAttribute('id', this._checkboxId);
|
||||
|
||||
th.insertBefore(this._checkAllCheckbox, th.firstChild);
|
||||
|
||||
// set up new checkbox
|
||||
this._app.utilRegistry.initAll(th);
|
||||
|
||||
this._checkAllCheckbox.addEventListener('input', this._onCheckAllCheckboxInput.bind(this));
|
||||
const checkBoxInputEvent = new EventWrapper(EVENT_TYPE.INPUT, this._onCheckAllCheckboxInput.bind(this), this._checkAllCheckbox);
|
||||
this._eventManager.registerNewListener(checkBoxInputEvent);
|
||||
this._setupCheckboxListeners();
|
||||
}
|
||||
|
||||
@ -113,14 +138,15 @@ class CheckAllColumn {
|
||||
_setupCheckboxListeners() {
|
||||
this._column
|
||||
.flatMap(cell => cell.tagName == 'TH' ? new Array() : Array.from(cell.querySelectorAll(CHECKBOX_SELECTOR)))
|
||||
.forEach(checkbox =>
|
||||
checkbox.addEventListener('input', this._updateCheckAllCheckboxState.bind(this))
|
||||
);
|
||||
.forEach(checkbox => {
|
||||
const checkBoxUpdateEvent = new EventWrapper(EVENT_TYPE.INPUT, this._updateCheckAllCheckboxState.bind(this), checkbox);
|
||||
this._eventManager.registerNewListener(checkBoxUpdateEvent);
|
||||
});
|
||||
}
|
||||
|
||||
_updateCheckAllCheckboxState() {
|
||||
const allChecked = this._column.every(cell =>
|
||||
cell.tagName == 'TH' || cell.querySelector(CHECKBOX_SELECTOR).checked
|
||||
cell.tagName == 'TH' || cell.querySelector(CHECKBOX_SELECTOR).checked,
|
||||
);
|
||||
this._checkAllCheckbox.checked = allChecked;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { CheckAll } from './check-all';
|
||||
import { CheckAll, CHECK_ALL_INITIALIZED_CLASS } from './check-all';
|
||||
|
||||
const MOCK_APP = {
|
||||
utilRegistry: {
|
||||
@ -24,4 +24,11 @@ describe('CheckAll', () => {
|
||||
new CheckAll();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should destroy CheckAll', () => {
|
||||
checkAll.destroy();
|
||||
expect(checkAll._eventManager._registeredListeners.length).toBe(0);
|
||||
expect(checkAll._element.classList).not.toEqual(jasmine.arrayContaining([CHECK_ALL_INITIALIZED_CLASS]));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import './course-teaser.sass';
|
||||
|
||||
const COURSE_TEASER_INITIALIZED_CLASS = 'course-teaser--initialized';
|
||||
@ -12,16 +13,30 @@ const COURSE_TEASER_CHEVRON_CLASS = 'course-teaser__chevron';
|
||||
export class CourseTeaser {
|
||||
|
||||
_element;
|
||||
_eventManager;
|
||||
|
||||
constructor(element) {
|
||||
if (!element) {
|
||||
throw new Error('CourseTeaser utility cannot be setup without an element!');
|
||||
}
|
||||
this._eventManager = new EventManager();
|
||||
if (element.classList.contains(COURSE_TEASER_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
this._element = element;
|
||||
element.addEventListener('click', e => this._onToggleExpand(e));
|
||||
const clickHandler = e => this._onToggleExpand(e);
|
||||
const clickEvent = new EventWrapper(EVENT_TYPE.CLICK, clickHandler.bind(this), element);
|
||||
this._eventManager.registerNewListener(clickEvent);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
if(this._element.classList.contains(COURSE_TEASER_EXPANDED_CLASS)) {
|
||||
this._element.classList.remove(COURSE_TEASER_EXPANDED_CLASS);
|
||||
}
|
||||
if (this._element.classList.contains(COURSE_TEASER_INITIALIZED_CLASS)) {
|
||||
this._element.classList.remove(COURSE_TEASER_INITIALIZED_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
_onToggleExpand(event) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import { HttpClient } from '../../services/http-client/http-client';
|
||||
|
||||
import moment from 'moment';
|
||||
@ -58,6 +59,7 @@ export class ExamCorrect {
|
||||
_lastColumnIndex;
|
||||
|
||||
_storageManager;
|
||||
_eventManager;
|
||||
|
||||
constructor(element, app) {
|
||||
if (!element) {
|
||||
@ -71,6 +73,8 @@ export class ExamCorrect {
|
||||
this._element = element;
|
||||
this._app = app;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
// TODO work in progress
|
||||
// this._storageManager = new StorageManager('EXAM_CORRECT', '0.0.0', { location: LOCATION.SESSION, encryption: { all: { tag: 'exam-correct', exam: this._element.getAttribute('uw-exam-correct') } } });
|
||||
this._storageManager = new StorageManager('EXAM_CORRECT', '0.0.0', { location: LOCATION.WINDOW });
|
||||
@ -88,20 +92,28 @@ export class ExamCorrect {
|
||||
this._resultPassSelect = resultDetailCell && resultDetailCell.querySelector('select.uw-exam-correct__pass');
|
||||
this._partDeleteBoxes = [...this._element.querySelectorAll('input.uw-exam-correct--delete-exam-part')];
|
||||
|
||||
if (this._sendBtn)
|
||||
this._sendBtn.addEventListener('click', this._sendCorrectionHandler.bind(this));
|
||||
else console.error('ExamCorrect utility could not detect send button!');
|
||||
if (this._sendBtn){
|
||||
const sendClickEvent = new EventWrapper(EVENT_TYPE.CLICK, this._sendCorrectionHandler.bind(this), this._sendBtn);
|
||||
this._eventManager.registerNewListener(sendClickEvent);
|
||||
} else {
|
||||
console.error('ExamCorrect utility could not detect send button!');
|
||||
}
|
||||
|
||||
if (this._userInput)
|
||||
this._userInput.addEventListener('focusout', this._validateUserInput.bind(this));
|
||||
else throw new Error('ExamCorrect utility could not detect user input!');
|
||||
if (this._userInput) {
|
||||
const focusOutEvent = new EventWrapper(EVENT_TYPE.FOCUS_OUT, this._validateUserInput.bind(this), this._userInput);
|
||||
this._eventManager.registerNewListener(focusOutEvent);
|
||||
} else {
|
||||
throw new Error('ExamCorrect utility could not detect user input!');
|
||||
}
|
||||
|
||||
for (let deleteBox of this._partDeleteBoxes) {
|
||||
deleteBox.addEventListener('change', (() => { this._updatePartDeleteDisabled(deleteBox); }).bind(this));
|
||||
const deleteBoxChangeEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => { this._updatePartDeleteDisabled(deleteBox); }).bind(this), deleteBox);
|
||||
this._eventManger.registerNewListener(deleteBoxChangeEvent);
|
||||
}
|
||||
|
||||
for (let input of [this._userInput, ...this._partInputs]) {
|
||||
input.addEventListener('keypress', this._inputKeypress.bind(this));
|
||||
const inputKeyDownEvent = new EventWrapper(EVENT_TYPE.KEYDOWN, this._inputKeypress.bind(this), input);
|
||||
this._eventManager.registerNewListener(inputKeyDownEvent);
|
||||
}
|
||||
|
||||
if (!this._userInputStatus) {
|
||||
@ -121,27 +133,29 @@ export class ExamCorrect {
|
||||
|
||||
this._cIndices = new Map(
|
||||
[...this._element.querySelectorAll('[uw-exam-correct-header]')]
|
||||
.map((header) => [header.getAttribute('uw-exam-correct-header'), header.cellIndex])
|
||||
.map((header) => [header.getAttribute('uw-exam-correct-header'), header.cellIndex]),
|
||||
);
|
||||
|
||||
if (this._resultSelect && this._resultGradeSelect) {
|
||||
this._resultSelect.addEventListener('change', () => {
|
||||
const resultSelectEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => {
|
||||
if (this._resultSelect.value !== 'grade')
|
||||
this._resultGradeSelect.classList.add('grade-hidden');
|
||||
else
|
||||
this._resultGradeSelect.classList.remove('grade-hidden');
|
||||
});
|
||||
}).bind(this), this._resultSelect );
|
||||
this._eventManager.registerNewListener(resultSelectEvent);
|
||||
|
||||
if (this._resultSelect.value !== 'grade')
|
||||
this._resultGradeSelect.classList.add('grade-hidden');
|
||||
}
|
||||
if (this._resultSelect && this._resultPassSelect) {
|
||||
this._resultSelect.addEventListener('change', () => {
|
||||
const resultPassSelectEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => {
|
||||
if (this._resultSelect.value !== 'pass')
|
||||
this._resultPassSelect.classList.add('pass-hidden');
|
||||
else
|
||||
this._resultPassSelect.classList.remove('pass-hidden');
|
||||
});
|
||||
}).bind(this), this._resultSelect);
|
||||
this._eventManager.registerNewListener(resultPassSelectEvent);
|
||||
|
||||
if (this._resultSelect.value !== 'pass')
|
||||
this._resultPassSelect.classList.add('pass-hidden');
|
||||
@ -158,9 +172,7 @@ export class ExamCorrect {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._sendBtn.removeEventListener('click', this._sendCorrectionHandler);
|
||||
this._userInput.removeEventListener('change', this._validateUserInput);
|
||||
// TODO destroy handlers on user input candidate elements
|
||||
this._eventManager.cleanUp();
|
||||
}
|
||||
|
||||
_updatePartDeleteDisabled(deleteBox) {
|
||||
@ -208,9 +220,9 @@ export class ExamCorrect {
|
||||
headers: EXAM_CORRECT_HEADERS,
|
||||
body: JSON.stringify(body),
|
||||
}).then(
|
||||
(response) => response.json()
|
||||
(response) => response.json(),
|
||||
).then(
|
||||
(response) => this._processResponse(body, response, body.user)
|
||||
(response) => this._processResponse(body, response, body.user),
|
||||
).catch((error) => {
|
||||
console.error('Error while validating user input', error);
|
||||
});
|
||||
@ -300,9 +312,9 @@ export class ExamCorrect {
|
||||
headers: EXAM_CORRECT_HEADERS,
|
||||
body: JSON.stringify(body),
|
||||
}).then(
|
||||
(response) => response.json()
|
||||
(response) => response.json(),
|
||||
).then(
|
||||
(response) => this._processResponse(body, response, user, undefined, { results: results, result: result })
|
||||
(response) => this._processResponse(body, response, user, undefined, { results: results, result: result }),
|
||||
).catch((error) => {
|
||||
console.error('Error while processing response', error);
|
||||
});
|
||||
@ -541,8 +553,8 @@ export class ExamCorrect {
|
||||
headers: EXAM_CORRECT_HEADERS,
|
||||
body: JSON.stringify(body),
|
||||
}).then(
|
||||
(response) => response.json()
|
||||
).then((response) => this._processResponse(body, response, userElem.getAttribute(EXAM_CORRECT_USER_ATTR), row, { results: results.partResults, result: results.result })
|
||||
(response) => response.json(),
|
||||
).then((response) => this._processResponse(body, response, userElem.getAttribute(EXAM_CORRECT_USER_ATTR), row, { results: results.partResults, result: results.result }),
|
||||
).catch(console.error);
|
||||
}
|
||||
|
||||
|
||||
@ -9,8 +9,10 @@ const AUTO_SUBMIT_BUTTON_HIDDEN_CLASS = 'hidden';
|
||||
selector: AUTO_SUBMIT_BUTTON_UTIL_SELECTOR,
|
||||
})
|
||||
export class AutoSubmitButton {
|
||||
_element;
|
||||
|
||||
constructor(element) {
|
||||
this._element = element;
|
||||
if (!element) {
|
||||
throw new Error('Auto Submit Button utility needs to be passed an element!');
|
||||
}
|
||||
@ -24,6 +26,7 @@ export class AutoSubmitButton {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// TODO
|
||||
this._element.classList.remove(AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS);
|
||||
this._element.classList.remove(AUTO_SUBMIT_BUTTON_HIDDEN_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
27
frontend/src/utils/form/auto-submit-button.spec.js
Normal file
27
frontend/src/utils/form/auto-submit-button.spec.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { AutoSubmitButton, AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS, AUTO_SUBMIT_BUTTON_HIDDEN_CLASS } from './auto-submit-button.js';
|
||||
|
||||
describe('Auto-submit-button', () => {
|
||||
|
||||
let autoSubmitButton;
|
||||
|
||||
beforeEach(() => {
|
||||
const element = document.createElement('div');
|
||||
autoSubmitButton = new AutoSubmitButton(element);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(autoSubmitButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should destory auto-submit-button', () => {
|
||||
autoSubmitButton.destroy();
|
||||
expect(autoSubmitButton._element.classList).not.toContain(AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS);
|
||||
expect(autoSubmitButton._element.classList).not.toContain(AUTO_SUBMIT_BUTTON_HIDDEN_CLASS);
|
||||
});
|
||||
|
||||
it('should throw if called without an element', () => {
|
||||
expect(() => {
|
||||
new AutoSubmitButton();
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
@ -1,5 +1,6 @@
|
||||
import * as debounce from 'lodash.debounce';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
export const AUTO_SUBMIT_INPUT_UTIL_SELECTOR = '[uw-auto-submit-input]';
|
||||
|
||||
@ -12,6 +13,8 @@ export class AutoSubmitInput {
|
||||
|
||||
_element;
|
||||
|
||||
_eventManager;
|
||||
|
||||
_form;
|
||||
_debouncedHandler;
|
||||
|
||||
@ -22,6 +25,8 @@ export class AutoSubmitInput {
|
||||
|
||||
this._element = element;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
@ -33,15 +38,19 @@ export class AutoSubmitInput {
|
||||
|
||||
this._debouncedHandler = debounce(this.autoSubmit, 500);
|
||||
|
||||
this._element.addEventListener('input', this._debouncedHandler);
|
||||
const inputEvent = new EventWrapper(EVENT_TYPE.INPUT, this._debouncedHandler.bind(this), this._element);
|
||||
this._eventManager.registerNewListener(inputEvent);
|
||||
|
||||
this._element.classList.add(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._element.removeEventListener('input', this._debouncedHandler);
|
||||
this._eventManager.cleanUp();
|
||||
if(this._element.classList.contains(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS))
|
||||
this._element.classList.remove(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
autoSubmit = () => {
|
||||
autoSubmit() {
|
||||
this._form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
30
frontend/src/utils/form/auto-submit-input.spec.js
Normal file
30
frontend/src/utils/form/auto-submit-input.spec.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { AutoSubmitInput, AUTO_SUBMIT_INPUT_INITIALIZED_CLASS } from './auto-submit-input.js';
|
||||
|
||||
describe('Auto-submit-input', () => {
|
||||
|
||||
let autoSubmitInput;
|
||||
|
||||
beforeEach(() => {
|
||||
const form = document.createElement('form');
|
||||
const element = document.createElement('input');
|
||||
element.setAttribute('type', 'text');
|
||||
form.append(element);
|
||||
autoSubmitInput = new AutoSubmitInput(element);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(autoSubmitInput).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should destory auto-submit-button', () => {
|
||||
autoSubmitInput.destroy();
|
||||
expect(autoSubmitInput._eventManager._registeredListeners.length).toBe(0);
|
||||
expect(autoSubmitInput._element.classList).not.toEqual(jasmine.arrayContaining([AUTO_SUBMIT_INPUT_INITIALIZED_CLASS]));
|
||||
});
|
||||
|
||||
it('should throw if called without an element', () => {
|
||||
expect(() => {
|
||||
new AutoSubmitInput();
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,5 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
const MASS_INPUT_SELECTOR = '.massinput';
|
||||
const RECIPIENT_CATEGORIES_SELECTOR = '.recipient-categories';
|
||||
@ -14,62 +15,81 @@ const RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR = '.recipient-category__checke
|
||||
})
|
||||
export class CommunicationRecipients {
|
||||
massInputElement;
|
||||
_element;
|
||||
|
||||
_eventManager;
|
||||
|
||||
constructor(element) {
|
||||
if (!element) {
|
||||
throw new Error('Communication Recipient utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
this.massInputElement = element.closest(MASS_INPUT_SELECTOR);
|
||||
this._element = element;
|
||||
this._eventManager = new EventManager();
|
||||
this.massInputElement = this._element.closest(MASS_INPUT_SELECTOR);
|
||||
|
||||
this.setupRecipientCategories();
|
||||
|
||||
const recipientObserver = new MutationObserver(this.setupRecipientCategories.bind(this));
|
||||
recipientObserver.observe(this.massInputElement, { childList: true });
|
||||
this._eventManager.registerNewMutationObserver(this.setupRecipientCategories.bind(this), this.massInputElement, { childList: true });
|
||||
}
|
||||
|
||||
setupRecipientCategories() {
|
||||
Array.from(this.massInputElement.querySelectorAll(RECIPIENT_CATEGORY_SELECTOR)).forEach(setupRecipientCategory);
|
||||
Array.from(this.massInputElement.querySelectorAll(RECIPIENT_CATEGORY_SELECTOR)).forEach(this.setupRecipientCategory.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
function setupRecipientCategory(recipientCategoryElement) {
|
||||
const categoryCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKBOX_SELECTOR);
|
||||
const categoryOptions = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_OPTIONS_SELECTOR);
|
||||
_removeCheckedCounter() {
|
||||
let checkedCounters = this._element.querySelectorAll(RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR);
|
||||
checkedCounters.forEach((checkedCounter) => {
|
||||
checkedCounter.innerHTML = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (categoryOptions) {
|
||||
const categoryCheckboxes = Array.from(categoryOptions.querySelectorAll('[type="checkbox"]'));
|
||||
const toggleAllCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR);
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
this._removeCheckedCounter();
|
||||
}
|
||||
|
||||
// setup category checkbox to toggle all child checkboxes if changed
|
||||
categoryCheckbox.addEventListener('change', () => {
|
||||
categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => {
|
||||
checkbox.checked = categoryCheckbox.checked;
|
||||
});
|
||||
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
|
||||
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
|
||||
});
|
||||
|
||||
// update counter and toggle checkbox initially
|
||||
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
|
||||
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
|
||||
|
||||
// register change listener for individual checkboxes
|
||||
categoryCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', () => {
|
||||
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
|
||||
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
|
||||
});
|
||||
});
|
||||
|
||||
// register change listener for toggle all checkbox
|
||||
if (toggleAllCheckbox) {
|
||||
toggleAllCheckbox.addEventListener('change', () => {
|
||||
setupRecipientCategory(recipientCategoryElement) {
|
||||
const categoryCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKBOX_SELECTOR);
|
||||
const categoryOptions = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_OPTIONS_SELECTOR);
|
||||
|
||||
if (categoryOptions) {
|
||||
const categoryCheckboxes = Array.from(categoryOptions.querySelectorAll('[type="checkbox"]'));
|
||||
const toggleAllCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR);
|
||||
|
||||
// setup category checkbox to toggle all child checkboxes if changed
|
||||
const categoryToggleEvent = new EventWrapper(EVENT_TYPE.CHANGE,(() => {
|
||||
categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => {
|
||||
checkbox.checked = toggleAllCheckbox.checked;
|
||||
checkbox.checked = categoryCheckbox.checked;
|
||||
});
|
||||
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
|
||||
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
|
||||
}).bind(this), categoryCheckbox );
|
||||
this._eventManager.registerNewListener(categoryToggleEvent);
|
||||
|
||||
// update counter and toggle checkbox initially
|
||||
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
|
||||
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
|
||||
|
||||
// register change listener for individual checkboxes
|
||||
categoryCheckboxes.forEach(checkbox => {
|
||||
const individualToggleEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => {
|
||||
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
|
||||
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
|
||||
}).bind(this), checkbox);
|
||||
this._eventManager.registerNewListener(individualToggleEvent);
|
||||
});
|
||||
|
||||
// register change listener for toggle all checkbox
|
||||
if (toggleAllCheckbox) {
|
||||
const toggleAllEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => {
|
||||
categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => {
|
||||
checkbox.checked = toggleAllCheckbox.checked;
|
||||
});
|
||||
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
|
||||
}).bind(this), toggleAllCheckbox);
|
||||
this._eventManager.registerNewListener(toggleAllEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,727 +0,0 @@
|
||||
@charset "UTF-8";
|
||||
/*
|
||||
| tail.datetime - The vanilla way to select dates and times!
|
||||
| @file ./less/tail.datetime-default-green.less
|
||||
| @author SamBrishes <sam@pytes.net>
|
||||
| @version 0.4.13 - Beta
|
||||
|
|
||||
| @website https://github.com/pytesNET/tail.DateTime
|
||||
| @license X11 / MIT License
|
||||
| @copyright Copyright © 2018 - 2019 SamBrishes, pytesNET <info@pytes.net>
|
||||
*/
|
||||
|
||||
/* @start MAIN CALENDAR */
|
||||
.tail-datetime-calendar, .tail-datetime-calendar *, .tail-datetime-calendar *:before,
|
||||
.tail-datetime-calendar *:after{
|
||||
box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
}
|
||||
.tail-datetime-calendar{
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 275px;
|
||||
height: auto;
|
||||
margin: 15px;
|
||||
padding: 0;
|
||||
z-index: 15;
|
||||
display: block;
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
direction: ltr;
|
||||
border-collapse: separate;
|
||||
/* font-family: "Open Sans", Calibri, Arial, sans-serif; */
|
||||
background-color: white;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3125);
|
||||
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3125);
|
||||
}
|
||||
.tail-datetime-calendar:after{
|
||||
clear: both;
|
||||
content: "";
|
||||
display: block;
|
||||
font-size: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.tail-datetime-calendar.calendar-static{
|
||||
top: auto;
|
||||
left: auto;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
position: static;
|
||||
visibility: visible;
|
||||
}
|
||||
.tail-datetime-calendar button.calendar-close{
|
||||
top: 100%;
|
||||
right: 15px;
|
||||
color: #303438;
|
||||
width: 35px;
|
||||
height: 25px;
|
||||
margin: 1px 0 0 0;
|
||||
padding: 5px 10px;
|
||||
opacity: 0.5;
|
||||
outline: none;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
line-height: 1.125em;
|
||||
text-shadow: none;
|
||||
background-color: white;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC\
|
||||
9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDEyIDE2Ij48cGF0aCBmaWxsPSIjMzAzNDM4IiBkP\
|
||||
SJNNy40OCA4bDMuNzUgMy43NS0xLjQ4IDEuNDhMNiA5LjQ4bC0zLjc1IDMuNzUtMS40OC0xLjQ4TDQuNTIgOCAuNzcgNC4y\
|
||||
NWwxLjQ4LTEuNDhMNiA2LjUybDMuNzUtMy43NSAxLjQ4IDEuNDhMNy40OCA4eiIvPjwvc3ZnPg==");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
border-radius: 0 0 3px 3px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3125);
|
||||
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3125);
|
||||
transition: opacity 142ms linear;
|
||||
-webkit-transition: opacity 142ms linear;
|
||||
}
|
||||
.tail-datetime-calendar button.calendar-close:hover{
|
||||
opacity: 1;
|
||||
}
|
||||
/* @end MAIN CALENDAR */
|
||||
|
||||
/* @start CALENDAR TOOLTIP */
|
||||
.tail-datetime-calendar .calendar-tooltip{
|
||||
color: white;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
position: absolute;
|
||||
background-color: #202428;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-tooltip:before{
|
||||
top: -7px;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0 0 0 -6px;
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-width: 0 7px 7px 7px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #202428 transparent;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-tooltip .tooltip-inner{
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 4px 7px;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
}
|
||||
/* @end CALENDAR TOOLTIP */
|
||||
|
||||
/* @start CALENDAR ACTIONs */
|
||||
.tail-datetime-calendar .calendar-actions{
|
||||
color: white;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: table;
|
||||
overflow: hidden;
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
background-color: var(--color-primary);
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions span{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
line-height: 36px;
|
||||
text-shadow: -1px -1px 0 var(--color-dark);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions span[data-action]{
|
||||
cursor: pointer;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions span.action{
|
||||
width: 36px;
|
||||
font-size: 22px;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions span.label{
|
||||
width: auto;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions span:first-child:before{
|
||||
right: -1px;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions span:last-child:before{
|
||||
left: -1px;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions span:first-child:hover:before,
|
||||
.tail-datetime-calendar .calendar-actions span:last-child:hover:before{
|
||||
display: none;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions span[data-action]:hover{
|
||||
background-color: var(--color-dark);
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions span.action-prev{
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC\
|
||||
9zdmciIHdpZHRoPSI2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgNiAxNiI+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iT\
|
||||
TYgMkwwIDhsNiA2VjJ6Ii8+PC9zdmc+");
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions span.action-next{
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC\
|
||||
9zdmciIHdpZHRoPSI2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgNiAxNiI+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iT\
|
||||
TAgMTRsNi02LTYtNnYxMnoiLz48L3N2Zz4=");
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions span.action-submit{
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC\
|
||||
9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDEyIDE2Ij48cGF0aCBmaWxsPSIjZmZmZmZmIiBkP\
|
||||
SJNMTIgNWwtOCA4LTQtNCAxLjUtMS41TDQgMTBsNi41LTYuNUwxMiA1eiIvPjwvc3ZnPg==");
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions span.action-cancel{
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC\
|
||||
9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDEyIDE2Ij48cGF0aCBmaWxsPSIjZmZmZmZmIiBkP\
|
||||
SJNNy40OCA4bDMuNzUgMy43NS0xLjQ4IDEuNDhMNiA5LjQ4bC0zLjc1IDMuNzUtMS40OC0xLjQ4TDQuNTIgOCAuNzcgNC4y\
|
||||
NWwxLjQ4LTEuNDhMNiA2LjUybDMuNzUtMy43NSAxLjQ4IDEuNDhMNy40OCA4eiIvPjwvc3ZnPg==");
|
||||
}
|
||||
/* @end CALENDAR ACTIONs */
|
||||
|
||||
/* @start CALENDAR DATEPICKER */
|
||||
.tail-datetime-calendar .calendar-datepicker{
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table{
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr th,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td{
|
||||
color: #303438;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
text-shadow: none;
|
||||
line-height: 30px;
|
||||
background-color: transparent;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
border-radius: 0px;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr th{
|
||||
color: white;
|
||||
background-color: var(--color-lightblack);
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td{
|
||||
cursor: pointer;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td span.inner{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.date-disabled{
|
||||
cursor: not-allowed;
|
||||
color: #909498;
|
||||
background-color: #F0F0F0;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.date-disabled:after{
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
width: 35px;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
background-color: #bfbfbf;
|
||||
transform-origin: 2px -5px;
|
||||
transform: rotate(-45deg);
|
||||
-moz-transform: rotate(-45deg);
|
||||
-webkit-transform: rotate(-45deg);
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.date-previous,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.date-next{
|
||||
color: #909498;
|
||||
background-color: #F0F0F0;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.date-today:before,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td .tooltip-tick{
|
||||
top: 5px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: 20;
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.date-today:before{
|
||||
left: 5px;
|
||||
background-color: #E67D1E;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td .tooltip-tick{
|
||||
right: 5px;
|
||||
background-color: #202428;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td .tooltip-tick:before,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td .tooltip-tick:after{
|
||||
display: none;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day{
|
||||
width: 14.28571429%;
|
||||
height: 35px;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week span.inner,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day span.inner{
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
line-height: 29px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week:hover span.inner,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day:hover span.inner{
|
||||
border-color: #cccccc;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week.date-disabled span.inner,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day.date-disabled span.inner,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week.date-disabled:hover span.inner,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day.date-disabled:hover span.inner{
|
||||
border-color: transparent;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week.date-select span.inner,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day.date-select span.inner,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week.date-select:hover span.inner,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day.date-select:hover span.inner{
|
||||
color: var(--color-fontsec);
|
||||
border-color: var(--color-fontsec);
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade{
|
||||
width: 33.33333333%;
|
||||
height: 40px;
|
||||
transition: color 142ms linear;
|
||||
-webkit-transition: color 142ms linear;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month.date-today:before,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year.date-today:before,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade.date-today:before{
|
||||
left: 50%;
|
||||
margin-left: -2.5px;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month span.inner,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year span.inner,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade span.inner{
|
||||
width: auto;
|
||||
height: 31px;
|
||||
line-height: 29px;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month span.inner:before,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year span.inner:before,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade span.inner:before,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month span.inner:after,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year span.inner:after,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade span.inner:after{
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
content: "";
|
||||
z-index: 15;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
transition: all 142ms linear;
|
||||
-webkit-transition: all 142ms linear;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month span.inner:before,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year span.inner:before,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade span.inner:before{
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month:hover span.inner:before,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year:hover span.inner:before,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade:hover span.inner:before{
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
border-top-color: #cccccc;
|
||||
border-left-color: #cccccc;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month span.inner:after,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year span.inner:after,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade span.inner:after{
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month:hover span.inner:after,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year:hover span.inner:after,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade:hover span.inner:after{
|
||||
right: 6px;
|
||||
bottom: 6px;
|
||||
border-right-color: #cccccc;
|
||||
border-bottom-color: #cccccc;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year,
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade{
|
||||
width: 25%;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade span.inner{
|
||||
height: 54px;
|
||||
padding: 7px 15px;
|
||||
text-align: left;
|
||||
line-height: 20px;
|
||||
}
|
||||
/* @end CALENDAR DATEPICKER */
|
||||
|
||||
/* @start CALENDAR TIMEPICKER */
|
||||
.tail-datetime-calendar .calendar-timepicker{
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
text-align: center;
|
||||
border-width: 1px 0 0 0;
|
||||
border-style: solid;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field{
|
||||
width: 28%;
|
||||
margin: 0;
|
||||
padding: 15px 0 7px 0;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field:first-of-type{
|
||||
text-align: right;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field:last-of-type{
|
||||
text-align: left;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input[type="text"]{
|
||||
color: #303438;
|
||||
width: 100%;
|
||||
height: 29px;
|
||||
margin: 0;
|
||||
z-index: 4;
|
||||
padding: 3px 20px 3px 5px;
|
||||
outline: 0;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
line-height: 23px;
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
-webkit-appearance: textfield;
|
||||
background-color: #F0F0F0;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
border-radius: 3px;
|
||||
box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
transition: color 142ms linear, border 142ms linear, background 142ms linear;
|
||||
-webkit-transition: color 142ms linear, border 142ms linear, background 142ms linear;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input[type="text"]:hover{
|
||||
color: #303438;
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input[type="text"]:focus{
|
||||
color: #303438;
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input[type="text"]:disabled{
|
||||
cursor: not-allowed;
|
||||
color: #A0A4A8;
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step{
|
||||
min-width: 0px;
|
||||
width: 20px;
|
||||
height: 15px;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: 15;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
background-color: #F0F0F0;
|
||||
box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
transition: border 142ms linear, background 142ms linear;
|
||||
-webkit-transition: border 142ms linear, background 142ms linear;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step:before{
|
||||
top: 4px;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0 0 0 -4px;
|
||||
padding: 0;
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
transition: border 142ms linear;
|
||||
-webkit-transition: border 142ms linear;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step.step-up{
|
||||
top: 15px;
|
||||
border-width: 0 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: white;
|
||||
border-radius: 0 2px 0 0;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step.step-up:hover{
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step.step-up:before{
|
||||
border-width: 0 4px 5px 4px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #303438 transparent;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step.step-down{
|
||||
top: 29px;
|
||||
border-width: 1px 0 0 1px;
|
||||
border-style: solid;
|
||||
border-color: white;
|
||||
border-radius: 0 0 2px 0;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step.step-down:hover{
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step.step-down:before{
|
||||
border-width: 5px 4px 0 4px;
|
||||
border-style: solid;
|
||||
border-color: #303438 transparent transparent transparent;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:focus + button.step-up{
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:focus + button.step-up:hover{
|
||||
background-color: var(--color-dark);
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:focus + button.step-up:before{
|
||||
border-bottom-color: white;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:focus + button + button.step-down{
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:focus + button + button.step-down:hover{
|
||||
background-color: var(--color-dark);
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:focus + button + button.step-down:before{
|
||||
border-top-color: white;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:disabled + button.step-up{
|
||||
cursor: not-allowed;
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:disabled + button.step-up:hover{
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:disabled + button.step-up:before{
|
||||
border-bottom-color: #A0A4A8;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:disabled + button + button.step-down{
|
||||
cursor: not-allowed;
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:disabled + button + button.step-down:hover{
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:disabled + button + button.step-down:before{
|
||||
border-top-color: #A0A4A8;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker .timepicker-field label{
|
||||
color: #303438;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch{
|
||||
cursor: pointer;
|
||||
margin: 15px 0 -5px 0;
|
||||
display: block;
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch:before,
|
||||
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch:after{
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch:before{
|
||||
content: attr(data-am);
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch:after{
|
||||
content: attr(data-pm);
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch input[type="checkbox"]{
|
||||
display: none;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch input[type="checkbox"] + span{
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch input[type="checkbox"] + span:before{
|
||||
width: 50px;
|
||||
height: 16px;
|
||||
content: "";
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--color-primary);
|
||||
border-radius: 14px;
|
||||
transition: border 284ms linear;
|
||||
-webkit-transition: border 284ms linear;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch input[type="checkbox"] + span:after{
|
||||
top: 3px;
|
||||
left: 4px;
|
||||
right: 30px;
|
||||
width: auto;
|
||||
height: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
background-color: var(--color-primary);
|
||||
border-radius: 15px;
|
||||
vertical-align: top;
|
||||
transition: left 284ms linear, right 284ms linear 284ms, background 284ms linear;
|
||||
-webkit-transition: left 284ms linear, right 284ms linear 284ms, background 284ms linear;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch input[type="checkbox"]:checked + span:before{
|
||||
border-color: #E67D1E;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch input[type="checkbox"]:checked + span:after{
|
||||
left: 30px;
|
||||
right: 4px;
|
||||
background-color: #E67D1E;
|
||||
transition: right 284ms linear, left 284ms linear 284ms, background 284ms linear;
|
||||
-webkit-transition: right 284ms linear, left 284ms linear 284ms, background 284ms linear;
|
||||
}
|
||||
.tail-datetime-calendar .calendar-actions + .calendar-timepicker{
|
||||
border-width: 0;
|
||||
}
|
||||
/* @end CALENDAR TIMEPICKER */
|
||||
|
||||
/* @start RTL */
|
||||
.tail-datetime-calendar.rtl{
|
||||
direction: rtl;
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-actions span.action-next,
|
||||
.tail-datetime-calendar.rtl .calendar-actions span.action-prev{
|
||||
transform: rotate(180deg);
|
||||
-moz-transform: rotate(180deg);
|
||||
-webkit-transform: rotate(180deg);
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.date-disabled:after{
|
||||
right: 3px;
|
||||
transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-webkit-transform: rotate(45deg);
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.date-today:before{
|
||||
right: 5px;
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td .tooltip-tick{
|
||||
left: 5px;
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-month.date-today:before,
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-year.date-today:before,
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-decade.date-today:before{
|
||||
right: 50%;
|
||||
margin-right: -2.5px;
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-month:hover span.inner:before,
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-year:hover span.inner:before,
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-decade:hover span.inner:before{
|
||||
right: 6px;
|
||||
border-right-color: #cccccc;
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-month span.inner:after,
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-year span.inner:after,
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-decade span.inner:after{
|
||||
left: 0;
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-month:hover span.inner:after,
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-year:hover span.inner:after,
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-decade:hover span.inner:after{
|
||||
left: 6px;
|
||||
border-left-color: #cccccc;
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-decade span.inner{
|
||||
text-align: right;
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-timepicker .timepicker-field:first-child{
|
||||
text-align: left;
|
||||
padding-left: 0;
|
||||
padding-right: 25px;
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-timepicker .timepicker-field:last-child{
|
||||
text-align: right;
|
||||
padding-left: 25px;
|
||||
padding-right: 0;
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-timepicker .timepicker-field:first-child input[type="text"]{
|
||||
margin-left: -1px;
|
||||
margin-right: 0;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
.tail-datetime-calendar.rtl .calendar-timepicker .timepicker-field:last-child input[type="text"]{
|
||||
margin-left: 0;
|
||||
margin-right: -1px;
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
/* @end RTL */
|
||||
|
||||
/*# sourceMappingURL=tail.datetime-default-green.map */
|
||||
@ -1,16 +1,9 @@
|
||||
import datetime from 'tail.datetime';
|
||||
import './datepicker.css';
|
||||
import { Utility } from '../../core/utility';
|
||||
import moment from 'moment';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
import * as defer from 'lodash.defer';
|
||||
import defer from 'lodash.defer';
|
||||
|
||||
const KEYCODE_ESCAPE = 27;
|
||||
const Z_INDEX_MODAL = 9999;
|
||||
|
||||
// should be the same as ATTR_SUBMIT_LOCKED in async-table util
|
||||
// TODO move to global config
|
||||
const ATTR_DATEPICKER_OPEN = 'submit-locked';
|
||||
|
||||
// INTERNAL (Uni2work specific) formats for formatting dates and/or times
|
||||
const FORM_DATE_FORMAT = {
|
||||
@ -19,10 +12,6 @@ const FORM_DATE_FORMAT = {
|
||||
'datetime-local': moment.HTML5_FMT.DATETIME_LOCAL_SECONDS,
|
||||
};
|
||||
|
||||
// FANCY (tail.datetime specific) formats for displaying dates and/or times
|
||||
const FORM_DATE_FORMAT_DATE_DT = 'dd.mm.YYYY';
|
||||
const FORM_DATE_FORMAT_TIME_DT = 'HH:ii:ss';
|
||||
|
||||
// FANCY (moment specific) formats for displaying dates and/or times
|
||||
const FORM_DATE_FORMAT_DATE_MOMENT = 'DD.MM.YYYY';
|
||||
const FORM_DATE_FORMAT_TIME_MOMENT = 'HH:mm:ss';
|
||||
@ -35,38 +24,7 @@ const FORM_DATE_FORMAT_MOMENT = {
|
||||
const DATEPICKER_UTIL_SELECTOR = 'input[type="date"], input[type="time"], input[type="datetime-local"]';
|
||||
|
||||
const DATEPICKER_INITIALIZED_CLASS = 'datepicker--initialized';
|
||||
const DATEPICKER_OPEN_CLASS = 'calendar-open';
|
||||
|
||||
const DATEPICKER_CONFIG = {
|
||||
'global': {
|
||||
// set default time to 00:00:00
|
||||
timeHours: 0,
|
||||
timeMinutes: 0,
|
||||
timeSeconds: 0,
|
||||
|
||||
weekStart: 1,
|
||||
dateFormat: FORM_DATE_FORMAT_DATE_DT,
|
||||
timeFormat: FORM_DATE_FORMAT_TIME_DT,
|
||||
|
||||
// prevent the instance from closing when selecting a date before selecting a time
|
||||
stayOpen: true,
|
||||
|
||||
// hide the close button (we handle closing the datepicker manually by clicking outside)
|
||||
closeButton: false,
|
||||
|
||||
// disable the decades view because nobody will ever need it (i.e. cap the switch to the more relevant year view)
|
||||
viewDecades: false,
|
||||
},
|
||||
'datetime-local': {},
|
||||
'date': {
|
||||
// disable date picker
|
||||
timeFormat: false,
|
||||
},
|
||||
'time': {
|
||||
// disable time picker
|
||||
dateFormat: false,
|
||||
},
|
||||
};
|
||||
|
||||
@Utility({
|
||||
selector: DATEPICKER_UTIL_SELECTOR,
|
||||
@ -76,12 +34,13 @@ export class Datepicker {
|
||||
// singleton Map that maps a formID to a Map of Datepicker objects
|
||||
static datepickerCollections;
|
||||
|
||||
datepickerInstance;
|
||||
_element;
|
||||
elementType;
|
||||
initialValue;
|
||||
_locale;
|
||||
|
||||
_eventManager;
|
||||
|
||||
_unloadIsDueToSubmit = false;
|
||||
|
||||
constructor(element) {
|
||||
@ -102,54 +61,18 @@ export class Datepicker {
|
||||
|
||||
this._element = element;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
// store the previously set type to select the input format
|
||||
this.elementType = this._element.getAttribute('type');
|
||||
|
||||
// store initial value prior to changing type
|
||||
this.initialValue = this._element.value || this._element.getAttribute('value');
|
||||
|
||||
// manually set the type attribute to text because datepicker handles displaying the date
|
||||
this._element.setAttribute('type', 'text');
|
||||
|
||||
// get all relevant config options for this datepicker type
|
||||
const datepickerGlobalConfig = DATEPICKER_CONFIG['global'];
|
||||
const datepickerConfig = DATEPICKER_CONFIG[this.elementType];
|
||||
|
||||
// additional position config (optional data-datepicker-position attribute in html) that can specialize the global config
|
||||
const datepickerPosition = this._element.dataset.datepickerPosition;
|
||||
if (datepickerPosition) {
|
||||
datepickerGlobalConfig.position = datepickerPosition;
|
||||
}
|
||||
|
||||
if (!datepickerConfig || !FORM_DATE_FORMAT[this.elementType]) {
|
||||
if (!FORM_DATE_FORMAT[this.elementType]) {
|
||||
throw new Error('Datepicker utility called on unsupported element!');
|
||||
}
|
||||
|
||||
// FIXME dirty hack below; fix tail.datetime instead
|
||||
|
||||
// get date object from internal format before datetime does nasty things with it
|
||||
let parsedMomentDate = moment(this.initialValue, [ FORM_DATE_FORMAT[this.elementType], FORM_DATE_FORMAT_MOMENT[this.elementType] ], true);
|
||||
if (parsedMomentDate && parsedMomentDate.isValid()) {
|
||||
parsedMomentDate = parsedMomentDate.toDate();
|
||||
} else {
|
||||
parsedMomentDate = undefined;
|
||||
}
|
||||
|
||||
// initialize tail.datetime (datepicker) instance and let it do weird stuff with the element value
|
||||
this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig, locale: this._locale });
|
||||
|
||||
// reset date to something sane
|
||||
if (parsedMomentDate)
|
||||
this.datepickerInstance.selectDate(parsedMomentDate);
|
||||
|
||||
// insert the datepicker element (dt) after the form
|
||||
this._element.form.parentNode.insertBefore(this.datepickerInstance.dt, this._element.form.nextSibling);
|
||||
|
||||
// if the input element is in any open modal, increase the z-index of the datepicker
|
||||
if (this._element.closest('.modal--open')) {
|
||||
this.datepickerInstance.dt.style.zIndex = Z_INDEX_MODAL;
|
||||
}
|
||||
|
||||
// register this datepicker instance with the formID of the given element in the datepicker collection
|
||||
const formID = this._element.form.id;
|
||||
const elemID = this._element.id;
|
||||
@ -166,90 +89,14 @@ export class Datepicker {
|
||||
}
|
||||
|
||||
start() {
|
||||
const setDatepickerDate = () => {
|
||||
// try to parse the current input element value with fancy and internal format string
|
||||
const parsedMomentDate = moment(this._element.value, FORM_DATE_FORMAT_MOMENT[this.elementType]);
|
||||
const parsedMomentDateInternal = moment(this._element.value, FORM_DATE_FORMAT[this.elementType]);
|
||||
|
||||
// only set the datepicker date if the input is either in valid fancy format or in valid internal format
|
||||
if (parsedMomentDate.isValid()) {
|
||||
this.datepickerInstance.selectDate(parsedMomentDate.toDate());
|
||||
} else if (parsedMomentDateInternal.isValid()) {
|
||||
this.datepickerInstance.selectDate(parsedMomentDateInternal.toDate());
|
||||
}
|
||||
|
||||
// reregister change event to prevent event loop
|
||||
this._element.addEventListener('change', setDatepickerDate, { once: true });
|
||||
};
|
||||
// change the selected date in the tail.datetime instance if the value of the input element is changed
|
||||
this._element.addEventListener('change', setDatepickerDate, { once: true });
|
||||
|
||||
|
||||
// create a mutation observer that observes the datepicker instance class and sets
|
||||
// the datepicker-open DOM attribute of the input element if the datepicker has been opened
|
||||
const datepickerInstanceObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (!mutation.oldValue.includes(DATEPICKER_OPEN_CLASS) && this.datepickerInstance.dt.getAttribute('class').includes(DATEPICKER_OPEN_CLASS)) {
|
||||
this._element.setAttribute(ATTR_DATEPICKER_OPEN, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
datepickerInstanceObserver.observe(this.datepickerInstance.dt, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
attributeOldValue: true,
|
||||
});
|
||||
|
||||
|
||||
// close the instance on focusout of any element if another input is focussed that is neither the timepicker nor _element
|
||||
window.addEventListener('focusout', event => {
|
||||
const hasFocus = event.relatedTarget !== null;
|
||||
const focussedIsNotTimepicker = !this.datepickerInstance.dt.contains(event.relatedTarget);
|
||||
const focussedIsNotElement = event.relatedTarget !== this._element;
|
||||
const focussedIsInDocument = window.document.contains(event.relatedTarget);
|
||||
if (hasFocus && focussedIsNotTimepicker && focussedIsNotElement && focussedIsInDocument)
|
||||
this.closeDatepickerInstance();
|
||||
});
|
||||
|
||||
// close the instance on click on any element outside of the datepicker (except the input element itself)
|
||||
window.addEventListener('click', event => {
|
||||
const targetIsOutside = !this.datepickerInstance.dt.contains(event.target)
|
||||
&& event.target !== this.datepickerInstance.dt;
|
||||
const targetIsInDocument = window.document.contains(event.target);
|
||||
const targetIsNotElement = event.target !== this._element;
|
||||
if (targetIsOutside && targetIsInDocument && targetIsNotElement)
|
||||
this.closeDatepickerInstance();
|
||||
});
|
||||
|
||||
// close the instance on escape keydown events
|
||||
this._element.addEventListener('keydown', event => {
|
||||
if (event.keyCode === KEYCODE_ESCAPE) {
|
||||
this.closeDatepickerInstance();
|
||||
}
|
||||
});
|
||||
|
||||
// format the date value of the form input element of this datepicker before form submission
|
||||
this._element.form.addEventListener('submit', this._submitHandler.bind(this));
|
||||
const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, this._submitHandler.bind(this), this._element.form);
|
||||
this._eventManager.registerNewListener(submitEvent);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.datepickerInstance.remove();
|
||||
}
|
||||
|
||||
|
||||
// DATEPICKER INSTANCE CONTROL
|
||||
|
||||
/**
|
||||
* Closes the datepicker instance, releasing the lock on the input element.
|
||||
*/
|
||||
closeDatepickerInstance() {
|
||||
if (!this._element.datepicker-open) {
|
||||
throw new Error('Cannot close already closed datepicker instance!');
|
||||
}
|
||||
|
||||
this._element.setAttribute(ATTR_DATEPICKER_OPEN, false);
|
||||
this.datepickerInstance.close();
|
||||
this._eventManager.cleanUp();
|
||||
this._element.classList.remove(DATEPICKER_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
|
||||
@ -267,11 +114,13 @@ export class Datepicker {
|
||||
|
||||
_submitHandler() {
|
||||
this._unloadIsDueToSubmit = true;
|
||||
this._element.setAttribute('type', 'text');
|
||||
this.formatElementValue(false);
|
||||
|
||||
defer(() => { // Restore state after event loop is settled
|
||||
this._unloadIsDueToSubmit = false;
|
||||
this.formatElementValue(true);
|
||||
this._element.setAttribute('type', this.elementType);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
const ENTER_IS_TAB_INITIALIZED_CLASS = 'enter-as-tab--initialized';
|
||||
const AREA_SELECTOR = 'input, textarea';
|
||||
@ -10,6 +11,8 @@ const AREA_SELECTOR = 'input, textarea';
|
||||
export class EnterIsTab {
|
||||
_element;
|
||||
|
||||
_eventManager;
|
||||
|
||||
constructor(element) {
|
||||
|
||||
if(!element) {
|
||||
@ -17,6 +20,8 @@ export class EnterIsTab {
|
||||
}
|
||||
|
||||
this._element = element;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(ENTER_IS_TAB_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
@ -27,27 +32,32 @@ export class EnterIsTab {
|
||||
|
||||
|
||||
start() {
|
||||
this._element.addEventListener('keydown', (e) => {
|
||||
if(e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
let currentInputFieldId = this._element.id;
|
||||
let inputAreas = document.querySelectorAll(AREA_SELECTOR);
|
||||
let nextInputArea = null;
|
||||
for (let i = 0; i < inputAreas.length; i++) {
|
||||
if(inputAreas[i].id === currentInputFieldId) {
|
||||
nextInputArea = inputAreas[i+1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(nextInputArea) {
|
||||
nextInputArea.focus();
|
||||
let eventWrapper = new EventWrapper(EVENT_TYPE.KEYDOWN, this._captureEnter.bind(this), this._element);
|
||||
this._eventManager.registerNewListener(eventWrapper);
|
||||
}
|
||||
|
||||
_captureEnter (e) {
|
||||
if(e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
let currentInputFieldId = this._element.id;
|
||||
let inputAreas = document.querySelectorAll(AREA_SELECTOR);
|
||||
let nextInputArea = null;
|
||||
for (let i = 0; i < inputAreas.length; i++) {
|
||||
if(inputAreas[i].id === currentInputFieldId) {
|
||||
nextInputArea = inputAreas[i+1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(nextInputArea) {
|
||||
nextInputArea.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
console.log('TBD: Destroy EnterIsTab');
|
||||
this._eventManager.cleanUp();
|
||||
if(this._element.classList.contains(ENTER_IS_TAB_INITIALIZED_CLASS))
|
||||
this._element.classList.remove(ENTER_IS_TAB_INITIALIZED_CLASS);
|
||||
}
|
||||
}
|
||||
8
frontend/src/utils/form/enter-is-tab.md
Normal file
8
frontend/src/utils/form/enter-is-tab.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Enter is Tab Utility
|
||||
When the user presses enter on a form that uses this utility, the enter is converted to a tab in order to not send the form.
|
||||
|
||||
## Attribute:
|
||||
`uw-enter-as-tab`
|
||||
|
||||
## Example usage:
|
||||
<input type="text" value="" uw-enter-as-tab="" class=" enter-as-tab--initialized">
|
||||
@ -1,4 +1,5 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
const FORM_ERROR_REMOVER_INITIALIZED_CLASS = 'form-error-remover--initialized';
|
||||
const FORM_ERROR_REMOVER_INPUTS_SELECTOR = 'input:not([type="hidden"]), textarea, select';
|
||||
@ -13,6 +14,8 @@ export class FormErrorRemover {
|
||||
|
||||
_element;
|
||||
|
||||
_eventManager;
|
||||
|
||||
constructor(element) {
|
||||
if (!element)
|
||||
throw new Error('Form Error Remover utility needs to be passed an element!');
|
||||
@ -24,6 +27,7 @@ export class FormErrorRemover {
|
||||
return;
|
||||
|
||||
this._element = element;
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
this._element.classList.add(FORM_ERROR_REMOVER_INITIALIZED_CLASS);
|
||||
}
|
||||
@ -35,11 +39,18 @@ export class FormErrorRemover {
|
||||
const inputElements = Array.from(this._element.querySelectorAll(FORM_ERROR_REMOVER_INPUTS_SELECTOR));
|
||||
|
||||
inputElements.forEach((inputElement) => {
|
||||
inputElement.addEventListener('input', () => {
|
||||
const inputEvent = new EventWrapper(EVENT_TYPE.INPUT, (() => {
|
||||
if (!inputElement.willValidate || inputElement.validity.vaild) {
|
||||
FORM_GROUP_WITH_ERRORS_CLASSES.forEach(c => { this._element.classList.remove(c); });
|
||||
}
|
||||
});
|
||||
}).bind(this), inputElement);
|
||||
this._eventManager.registerNewListener(inputEvent);
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
this._element.classList.remove(FORM_ERROR_REMOVER_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import * as defer from 'lodash.defer';
|
||||
import defer from 'lodash.defer';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
const FORM_ERROR_REPORTER_INITIALIZED_CLASS = 'form-error-remover--initialized';
|
||||
|
||||
@ -10,12 +11,16 @@ export class FormErrorReporter {
|
||||
_element;
|
||||
_err;
|
||||
|
||||
_eventManager;
|
||||
|
||||
constructor(element) {
|
||||
if (!element)
|
||||
throw new Error('Form Error Reporter utility needs to be passed an element!');
|
||||
|
||||
this._element = element;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(FORM_ERROR_REPORTER_INITIALIZED_CLASS))
|
||||
return;
|
||||
|
||||
@ -24,11 +29,23 @@ export class FormErrorReporter {
|
||||
|
||||
start() {
|
||||
if (this._element.willValidate) {
|
||||
this._element.addEventListener('invalid', this.report.bind(this));
|
||||
this._element.addEventListener('change', () => { defer(this.report.bind(this)); } );
|
||||
let invalidElementEvent = new EventWrapper(EVENT_TYPE.INVALID, this.report.bind(this), this._element);
|
||||
this._eventManager.registerNewListener(invalidElementEvent);
|
||||
|
||||
let changedElementEvent = new EventWrapper(EVENT_TYPE.CHANGE, () => { defer(this.report.bind(this)); }, this._element);
|
||||
this._eventManager.registerNewListener(changedElementEvent);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
|
||||
this._removeError();
|
||||
|
||||
if(this._element.classList.contains(FORM_ERROR_REPORTER_INITIALIZED_CLASS))
|
||||
this._element.classList.remove(FORM_ERROR_REPORTER_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
report() {
|
||||
const msg = this._element.validity.valid ? null : this._element.validationMessage;
|
||||
|
||||
@ -37,10 +54,7 @@ export class FormErrorReporter {
|
||||
if (!target)
|
||||
return;
|
||||
|
||||
if (this._err && this._err.parentNode) {
|
||||
this._err.parentNode.removeChild(this._err);
|
||||
this._err = undefined;
|
||||
}
|
||||
this._removeError();
|
||||
|
||||
if (!msg) {
|
||||
target.classList.remove('standalone-field--has-error', 'form-group--has-error');
|
||||
@ -65,4 +79,11 @@ export class FormErrorReporter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_removeError() {
|
||||
if (this._err && this._err.parentNode) {
|
||||
this._err.parentNode.removeChild(this._err);
|
||||
this._err = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
const INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR = '.interactive-fieldset__target';
|
||||
|
||||
@ -15,6 +16,8 @@ export class InteractiveFieldset {
|
||||
|
||||
_element;
|
||||
|
||||
_eventManager;
|
||||
|
||||
conditionalInput;
|
||||
conditionalValue;
|
||||
target;
|
||||
@ -28,6 +31,8 @@ export class InteractiveFieldset {
|
||||
|
||||
this._element = element;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(INTERACTIVE_FIELDSET_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
@ -62,13 +67,11 @@ export class InteractiveFieldset {
|
||||
this.childInputs = Array.from(this._element.querySelectorAll(INTERACTIVE_FIELDSET_CHILD_SELECTOR)).filter(child => child.closest('[uw-interactive-fieldset]') === this._element);
|
||||
|
||||
// add event listener
|
||||
const observer = new MutationObserver(this._updateVisibility.bind(this));
|
||||
observer.observe(this.conditionalInput, { attributes: true, attributeFilter: ['data-interactive-fieldset-hidden'] });
|
||||
this.conditionalInput.addEventListener('input', this._updateVisibility.bind(this));
|
||||
|
||||
this._eventManager.registerNewMutationObserver(this._updateVisibility.bind(this), this.conditionalInput, { attributes: true, attributeFilter: ['data-interactive-fieldset-hidden'] });
|
||||
const inputEvent = new EventWrapper(EVENT_TYPE.INPUT, this._updateVisibility.bind(this), this.conditionalInput);
|
||||
this._eventManager.registerNewListener(inputEvent);
|
||||
// mark as initialized
|
||||
this._element.classList.add(INTERACTIVE_FIELDSET_INITIALIZED_CLASS);
|
||||
|
||||
}
|
||||
|
||||
start() {
|
||||
@ -77,7 +80,8 @@ export class InteractiveFieldset {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// TODO
|
||||
this._eventManager.cleanUp();
|
||||
this._element.classList.remove(INTERACTIVE_FIELDSET_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
_updateVisibility() {
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import { AUTO_SUBMIT_BUTTON_UTIL_SELECTOR } from './auto-submit-button';
|
||||
import { AUTO_SUBMIT_INPUT_UTIL_SELECTOR } from './auto-submit-input';
|
||||
|
||||
import { InteractiveFieldset } from './interactive-fieldset';
|
||||
import { Datepicker } from './datepicker';
|
||||
|
||||
import * as defer from 'lodash.defer';
|
||||
import defer from 'lodash.defer';
|
||||
|
||||
/**
|
||||
* Key generator from an arbitrary number of FormData objects.
|
||||
@ -26,16 +27,21 @@ const NAVIGATE_AWAY_PROMPT_UTIL_OPTOUT = '[uw-no-navigate-away-prompt]';
|
||||
export class NavigateAwayPrompt {
|
||||
|
||||
_element;
|
||||
_app;
|
||||
|
||||
_eventManager;
|
||||
|
||||
_initFormData;
|
||||
_unloadDueToSubmit = false;
|
||||
|
||||
constructor(element) {
|
||||
constructor(element, app) {
|
||||
if (!element) {
|
||||
throw new Error('Navigate Away Prompt utility needs to be passed an element!');
|
||||
}
|
||||
|
||||
this._element = element;
|
||||
this._app = app;
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS)) {
|
||||
return;
|
||||
@ -65,15 +71,18 @@ export class NavigateAwayPrompt {
|
||||
return;
|
||||
|
||||
this._initFormData = new FormData(this._element);
|
||||
window.addEventListener('beforeunload', this._beforeUnloadHandler.bind(this));
|
||||
const beforeUnloadEvent = new EventWrapper(EVENT_TYPE.BEFOREUNLOAD, this._beforeUnloadHandler.bind(this), window);
|
||||
this._eventManager.registerNewListener(beforeUnloadEvent);
|
||||
|
||||
this._element.addEventListener('submit', () => {
|
||||
const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, (() => {
|
||||
this._unloadDueToSubmit = true;
|
||||
defer(() => { this._unloadDueToSubmit = false; } ); // Restore state after event loop is settled
|
||||
});
|
||||
}).bind(this), this._element);
|
||||
this._eventManager.registerNewListener(submitEvent);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
this._element.classList.remove(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
@ -98,7 +107,7 @@ export class NavigateAwayPrompt {
|
||||
// allow the event to happen if the form was not touched by the
|
||||
// user (i.e. if the current FormData is equal to the initial FormData)
|
||||
// or the unload event was initiated by a form submit
|
||||
if (!formDataHasChanged || this._unloadDueToSubmit)
|
||||
if (!formDataHasChanged || this.unloadDueToSubmit || this._parentModalIsClosed())
|
||||
return;
|
||||
|
||||
// cancel the unload event. This is the standard to force the prompt to appear.
|
||||
@ -108,4 +117,13 @@ export class NavigateAwayPrompt {
|
||||
// for all non standard compliant browsers we return a truthy value to activate the prompt.
|
||||
return true;
|
||||
}
|
||||
|
||||
_parentModalIsClosed() {
|
||||
const parentModal = this._element.closest('.modal');
|
||||
if (!parentModal)
|
||||
return false;
|
||||
|
||||
const modalClosed = !parentModal.classList.contains('modal--open');
|
||||
return modalClosed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
const REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS = 'reactive-submit-button--initialized';
|
||||
|
||||
@ -12,12 +13,15 @@ export class ReactiveSubmitButton {
|
||||
_requiredInputs;
|
||||
_submitButton;
|
||||
|
||||
_eventManager;
|
||||
|
||||
constructor(element) {
|
||||
if (!element) {
|
||||
throw new Error('Reactive Submit Button utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
this._element = element;
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
@ -51,16 +55,18 @@ export class ReactiveSubmitButton {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// TODO
|
||||
this._eventManager.removeAllEventListenersFromUtil();
|
||||
this._element.classList.remove(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
setupInputs() {
|
||||
this._requiredInputs.forEach((el) => {
|
||||
const checkbox = el.getAttribute('type') === 'checkbox';
|
||||
const eventType = checkbox ? 'change' : 'input';
|
||||
el.addEventListener(eventType, () => {
|
||||
const eventType = checkbox ? EVENT_TYPE.CHANGE : EVENT_TYPE.INPUT;
|
||||
const valEvent = new EventWrapper(eventType,(() => {
|
||||
this.updateButtonState();
|
||||
});
|
||||
}).bind(this), el );
|
||||
this._eventManager.registerNewListener(valEvent);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
import './hide-columns.sass';
|
||||
|
||||
import { TableIndices } from '../../lib/table/table';
|
||||
@ -29,6 +31,7 @@ const HIDE_COLUMNS_INITIALIZED = 'uw-hide-columns--initialized';
|
||||
export class HideColumns {
|
||||
|
||||
_storageManager = new StorageManager('HIDE_COLUMNS', '1.1.0', { location: LOCATION.LOCAL });
|
||||
_eventManager;
|
||||
|
||||
_element;
|
||||
_elementWrapper;
|
||||
@ -36,8 +39,6 @@ export class HideColumns {
|
||||
|
||||
_autoHide;
|
||||
|
||||
_mutationObserver;
|
||||
|
||||
_tableIndices;
|
||||
|
||||
headerToHider = new Map();
|
||||
@ -62,6 +63,7 @@ export class HideColumns {
|
||||
return false;
|
||||
|
||||
this._element = element;
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
this._tableIndices = new TableIndices(this._element);
|
||||
|
||||
@ -82,12 +84,17 @@ export class HideColumns {
|
||||
|
||||
[...this._element.querySelectorAll('th')].filter(th => !th.hasAttribute(HIDE_COLUMNS_NO_HIDE)).forEach(th => this.setupHideButton(th));
|
||||
|
||||
this._mutationObserver = new MutationObserver(this._tableMutated.bind(this));
|
||||
this._mutationObserver.observe(this._element, { childList: true, subtree: true });
|
||||
this._eventManager.registerNewMutationObserver(this._tableMutated.bind(this), this._element, { childList: true, subtree: true });
|
||||
|
||||
this._element.classList.add(HIDE_COLUMNS_INITIALIZED);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
this._tableUtilContainer.remove();
|
||||
this._element.classList.remove(HIDE_COLUMNS_INITIALIZED);
|
||||
}
|
||||
|
||||
setupHideButton(th) {
|
||||
const preHidden = this.isHiddenTH(th);
|
||||
|
||||
@ -104,34 +111,41 @@ export class HideColumns {
|
||||
|
||||
this.addHeaderHider(th, hider);
|
||||
|
||||
th.addEventListener('mouseover', () => {
|
||||
const mouseOverEvent = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => {
|
||||
hider.classList.add(TABLE_HIDER_VISIBLE_CLASS);
|
||||
});
|
||||
th.addEventListener('mouseout', () => {
|
||||
}).bind(this), th);
|
||||
this._eventManager.registerNewListener(mouseOverEvent);
|
||||
|
||||
const mouseOutEvent = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => {
|
||||
if (hider.classList.contains(TABLE_HIDER_CLASS)) {
|
||||
hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS);
|
||||
}
|
||||
});
|
||||
}).bind(this), th);
|
||||
this._eventManager.registerNewListener(mouseOutEvent);
|
||||
|
||||
hider.addEventListener('click', (event) => {
|
||||
const hideClickEvent = new EventWrapper(EVENT_TYPE.CLICK, ((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.switchColumnDisplay(th);
|
||||
// this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider));
|
||||
});
|
||||
}).bind(this), hider);
|
||||
this._eventManager.registerNewListener(hideClickEvent);
|
||||
|
||||
hider.addEventListener('mouseover', () => {
|
||||
const mouseOverHider = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => {
|
||||
hider.classList.add(TABLE_HIDER_VISIBLE_CLASS);
|
||||
const currentlyHidden = this.hiderStatus(th);
|
||||
this.updateHiderIcon(hider, !currentlyHidden);
|
||||
});
|
||||
hider.addEventListener('mouseout', () => {
|
||||
}).bind(this), hider);
|
||||
this._eventManager.registerNewListener(mouseOverHider);
|
||||
|
||||
const mouseOutHider = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => {
|
||||
if (hider.classList.contains(TABLE_HIDER_CLASS)) {
|
||||
hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS);
|
||||
}
|
||||
const currentlyHidden = this.hiderStatus(th);
|
||||
this.updateHiderIcon(hider, currentlyHidden);
|
||||
});
|
||||
}).bind(this), hider);
|
||||
this._eventManager.registerNewListener(mouseOutHider);
|
||||
|
||||
new ResizeObserver(() => { this.repositionHider(hider); }).observe(th);
|
||||
|
||||
@ -144,12 +158,12 @@ export class HideColumns {
|
||||
switchColumnDisplay(th, hidden) {
|
||||
hidden = typeof(hidden) === 'undefined' ? !this.isHiddenTH(th) : !!hidden;
|
||||
|
||||
this.cellColumns(th).forEach(columnIndex => this.updateColumnDisplay(columnIndex, hidden));
|
||||
Array.from(this.cellColumns(th)).forEach(columnIndex => this.updateColumnDisplay(columnIndex, hidden));
|
||||
}
|
||||
|
||||
updateColumnDisplay(columnIndex, hidden) {
|
||||
// console.debug('updateColumnDisplay', { columnIndex, hidden });
|
||||
this._element.rows.forEach(row => {
|
||||
Array.from(this._element.rows).forEach(row => {
|
||||
const cell = this.getCol(row, columnIndex);
|
||||
|
||||
if (cell) {
|
||||
@ -221,7 +235,7 @@ export class HideColumns {
|
||||
}
|
||||
|
||||
updateHiderIcon(hider, hidden) {
|
||||
hider.getElementsByClassName('fas').forEach(hiderIcon => {
|
||||
Array.from(hider.getElementsByClassName('fas')).forEach(hiderIcon => {
|
||||
hiderIcon.classList.remove(hidden ? 'fa-eye' : 'fa-eye-slash');
|
||||
hiderIcon.classList.add(hidden ? 'fa-eye-slash' : 'fa-eye');
|
||||
});
|
||||
|
||||
@ -9,43 +9,54 @@ const CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized';
|
||||
selector: 'input[type="checkbox"]:not([uw-no-checkbox]), input[type="radio"]:not([uw-no-radiobox])',
|
||||
})
|
||||
export class Checkbox {
|
||||
|
||||
_element;
|
||||
_wrapperEl;
|
||||
|
||||
constructor(element) {
|
||||
if (!element) {
|
||||
throw new Error('Checkbox utility cannot be setup without an element!');
|
||||
}
|
||||
this._element = element;
|
||||
|
||||
const isRadio = element.type === 'radio';
|
||||
const isRadio = this._element.type === 'radio';
|
||||
const box_class = isRadio ? RADIOBOX_CLASS : CHECKBOX_CLASS;
|
||||
|
||||
if (isRadio && element.closest('.radio-group')) {
|
||||
if (isRadio && this._element.closest('.radio-group')) {
|
||||
// Don't initialize radiobox, if radio is part of a group
|
||||
return false;
|
||||
}
|
||||
|
||||
if (element.classList.contains(CHECKBOX_INITIALIZED_CLASS)) {
|
||||
if (this._element.classList.contains(CHECKBOX_INITIALIZED_CLASS)) {
|
||||
// throw new Error('Checkbox utility already initialized!');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (element.parentElement.classList.contains(box_class)) {
|
||||
if (this._element.parentElement.classList.contains(box_class)) {
|
||||
// throw new Error('Checkbox element\'s wrapper already has class '' + box_class + ''!');
|
||||
return false;
|
||||
}
|
||||
|
||||
const siblingEl = element.nextSibling;
|
||||
const parentEl = element.parentElement;
|
||||
const siblingEl = this._element.nextSibling;
|
||||
const parentEl = this._element.parentElement;
|
||||
|
||||
const wrapperEl = document.createElement('div');
|
||||
wrapperEl.classList.add(box_class);
|
||||
this._wrapperEl = document.createElement('div');
|
||||
this._wrapperEl.classList.add(box_class);
|
||||
|
||||
const labelEl = document.createElement('label');
|
||||
labelEl.setAttribute('for', element.id);
|
||||
|
||||
wrapperEl.appendChild(element);
|
||||
wrapperEl.appendChild(labelEl);
|
||||
this._wrapperEl.appendChild(element);
|
||||
this._wrapperEl.appendChild(labelEl);
|
||||
|
||||
parentEl.insertBefore(wrapperEl, siblingEl);
|
||||
parentEl.insertBefore(this._wrapperEl, siblingEl);
|
||||
|
||||
element.classList.add(CHECKBOX_INITIALIZED_CLASS);
|
||||
this._element.classList.add(CHECKBOX_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._wrapperEl !== undefined)
|
||||
this._wrapperEl.remove();
|
||||
this._element.classList.remove(CHECKBOX_INITIALIZED_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
125
frontend/src/utils/inputs/checkrange.js
Normal file
125
frontend/src/utils/inputs/checkrange.js
Normal file
@ -0,0 +1,125 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { TableIndices } from '../../lib/table/table';
|
||||
import { FrontendTooltips } from '../../lib/tooltips/frontend-tooltips';
|
||||
import { Translations } from '../../messages';
|
||||
|
||||
|
||||
const CHECKRANGE_INITIALIZED_CLASS = 'checkrange--initialized';
|
||||
const CHECKBOX_SELECTOR = '[type="checkbox"]';
|
||||
|
||||
|
||||
@Utility({
|
||||
selector: 'table:not([uw-no-check-all])',
|
||||
})
|
||||
export class CheckRange {
|
||||
_lastCheckedCell = null;
|
||||
_element;
|
||||
_tableIndices;
|
||||
_columns = new Array();
|
||||
|
||||
constructor(element) {
|
||||
if(!element) {
|
||||
throw new Error('Check Range Utility cannot be setup without an element');
|
||||
}
|
||||
|
||||
this._element = element;
|
||||
|
||||
if (this._element.classList.contains(CHECKRANGE_INITIALIZED_CLASS))
|
||||
return false;
|
||||
|
||||
this._tableIndices = new TableIndices(this._element);
|
||||
|
||||
this._gatherColumns();
|
||||
|
||||
let checkboxColumns = this._findCheckboxColumns();
|
||||
|
||||
checkboxColumns.forEach(columnId => this._setUpShiftClickOnColumn(columnId));
|
||||
|
||||
this._element.classList.add(CHECKRANGE_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
_setUpShiftClickOnColumn(columnId) {
|
||||
if (!this._columns || columnId < 0 || columnId >= this._columns.length) return;
|
||||
let column = this._columns[columnId];
|
||||
let language = document.documentElement.lang;
|
||||
let toolTipMessage = Translations.getTranslation('checkrangeTooltip', language);
|
||||
FrontendTooltips.addToolTip(column[0], toolTipMessage);
|
||||
column.forEach(el => el.addEventListener('click', (ev) => {
|
||||
|
||||
if(ev.shiftKey && this.lastCheckedCell !== null) {
|
||||
let lastClickedIndex = this._tableIndices.rowIndex(this._lastCheckedCell);
|
||||
let currentCellIndex = this._tableIndices.rowIndex(el);
|
||||
let cell = this._columns[columnId][currentCellIndex];
|
||||
if(currentCellIndex > lastClickedIndex)
|
||||
this._handleCellsInBetween(cell, lastClickedIndex, currentCellIndex, columnId);
|
||||
else
|
||||
this._handleCellsInBetween(cell, currentCellIndex, lastClickedIndex, columnId);
|
||||
} else {
|
||||
this._lastCheckedCell = el;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
_handleCellsInBetween(cell, firstRowIndex, lastRowIndex, columnId) {
|
||||
if(this._isChecked(cell)) {
|
||||
this._uncheckMultipleCells(firstRowIndex, lastRowIndex, columnId);
|
||||
} else {
|
||||
this._checkMultipleCells(firstRowIndex, lastRowIndex, columnId);
|
||||
}
|
||||
}
|
||||
|
||||
_checkMultipleCells(firstRowIndex, lastRowIndex, columnId) {
|
||||
for(let i=firstRowIndex; i<=lastRowIndex; i++) {
|
||||
let cell = this._columns[columnId][i];
|
||||
if (cell.tagName !== 'TH') {
|
||||
cell.querySelector(CHECKBOX_SELECTOR).checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_uncheckMultipleCells(firstRowIndex, lastRowIndex, columnId) {
|
||||
for(let i=firstRowIndex; i<=lastRowIndex; i++) {
|
||||
let cell = this._columns[columnId][i];
|
||||
if (cell.tagName !== 'TH') {
|
||||
cell.querySelector(CHECKBOX_SELECTOR).checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_isChecked(cell) {
|
||||
return cell.querySelector(CHECKBOX_SELECTOR).checked;
|
||||
}
|
||||
|
||||
|
||||
_gatherColumns() {
|
||||
for (const rowIndex of Array(this._tableIndices.maxRow + 1).keys()) {
|
||||
for (const colIndex of Array(this._tableIndices.maxCol + 1).keys()) {
|
||||
|
||||
const cell = this._tableIndices.getCell(rowIndex, colIndex);
|
||||
|
||||
if (!cell)
|
||||
continue;
|
||||
|
||||
if (!this._columns[colIndex])
|
||||
this._columns[colIndex] = new Array();
|
||||
|
||||
this._columns[colIndex][rowIndex] = cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_findCheckboxColumns() {
|
||||
let checkboxColumnIds = new Array();
|
||||
this._columns.forEach((col, i) => {
|
||||
if (this._isCheckboxColumn(col)) {
|
||||
checkboxColumnIds.push(i);
|
||||
}
|
||||
});
|
||||
return checkboxColumnIds;
|
||||
}
|
||||
|
||||
_isCheckboxColumn(col) {
|
||||
return col.every(cell => cell.tagName == 'TH' || cell.querySelector(CHECKBOX_SELECTOR))
|
||||
&& col.some(cell => cell.querySelector(CHECKBOX_SELECTOR));
|
||||
}
|
||||
}
|
||||
5
frontend/src/utils/inputs/checkrange.md
Normal file
5
frontend/src/utils/inputs/checkrange.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Checkrange Utility
|
||||
Is set on the table header of a specific row. Remembers the last checked checkbox. When the users shift-clicks another checkbox in the same row, all checkboxes in between are also checked.
|
||||
|
||||
# Attribute: table:not([uw-no-check-all]
|
||||
(will be setup on all tables which use the util check-all)
|
||||
@ -1,4 +1,5 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import './file-input.sass';
|
||||
|
||||
const FILE_INPUT_CLASS = 'file-input';
|
||||
@ -19,6 +20,8 @@ export class FileInput {
|
||||
_fileList;
|
||||
_label;
|
||||
|
||||
_eventManager;
|
||||
|
||||
constructor(element, app) {
|
||||
if (!element) {
|
||||
throw new Error('FileInput utility cannot be setup without an element!');
|
||||
@ -27,6 +30,8 @@ export class FileInput {
|
||||
this._element = element;
|
||||
this._app = app;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(FILE_INPUT_INITIALIZED_CLASS)) {
|
||||
throw new Error('FileInput utility already initialized!');
|
||||
}
|
||||
@ -40,11 +45,11 @@ export class FileInput {
|
||||
this._label = this._createFileLabel();
|
||||
this._updateLabel();
|
||||
|
||||
// add change listener
|
||||
this._element.addEventListener('change', () => {
|
||||
const changeInputEv = new EventWrapper(EVENT_TYPE.CHANGE,(() => {
|
||||
this._updateLabel();
|
||||
this._renderFileList();
|
||||
});
|
||||
}).bind(this), this._element );
|
||||
this._eventManager.registerNewListener(changeInputEv);
|
||||
|
||||
// add util class for styling and mark as initialized
|
||||
this._element.classList.add(FILE_INPUT_CLASS);
|
||||
@ -52,7 +57,10 @@ export class FileInput {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// TODO
|
||||
this._eventManager.cleanUp();
|
||||
this._fileList.remove();
|
||||
this._label.remove();
|
||||
this._element.classList.remove(FILE_INPUT_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
_renderFileList() {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
const FILE_MAX_SIZE_INITIALIZED_CLASS = 'file-max-size--initialized';
|
||||
|
||||
@ -9,6 +10,8 @@ export class FileMaxSize {
|
||||
_element;
|
||||
_app;
|
||||
|
||||
_eventManager;
|
||||
|
||||
constructor(element, app) {
|
||||
if (!element)
|
||||
throw new Error('FileMaxSize utility cannot be setup without an element!');
|
||||
@ -16,6 +19,8 @@ export class FileMaxSize {
|
||||
this._element = element;
|
||||
this._app = app;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(FILE_MAX_SIZE_INITIALIZED_CLASS)) {
|
||||
throw new Error('FileMaxSize utility already initialized!');
|
||||
}
|
||||
@ -24,7 +29,13 @@ export class FileMaxSize {
|
||||
}
|
||||
|
||||
start() {
|
||||
this._element.addEventListener('change', this._change.bind(this));
|
||||
const changeEv = new EventWrapper(EVENT_TYPE.CHANGE, this._change.bind(this), this._element);
|
||||
this._eventManager.registerNewListener(changeEv);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
this._element.classList.remove(FILE_MAX_SIZE_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
_change() {
|
||||
|
||||
@ -2,6 +2,7 @@ import { Checkbox } from './checkbox';
|
||||
import { FileInput } from './file-input';
|
||||
import { FileMaxSize } from './file-max-size';
|
||||
import { Password } from './password';
|
||||
import { CheckRange } from './checkrange';
|
||||
|
||||
import './inputs.sass';
|
||||
import './radio-group.sass';
|
||||
@ -11,4 +12,5 @@ export const InputUtils = [
|
||||
FileInput,
|
||||
FileMaxSize,
|
||||
Password,
|
||||
CheckRange,
|
||||
];
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
const PASSWORD_INITIALIZED_CLASS = 'password-input--initialized';
|
||||
|
||||
@ -9,6 +10,9 @@ export class Password {
|
||||
_element;
|
||||
_iconEl;
|
||||
_toggleContainerEl;
|
||||
_wrapperEl;
|
||||
|
||||
_eventManager;
|
||||
|
||||
constructor(element) {
|
||||
if (!element)
|
||||
@ -18,25 +22,26 @@ export class Password {
|
||||
return false;
|
||||
|
||||
this._element = element;
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
this._element.classList.add('password-input__input');
|
||||
|
||||
const siblingEl = this._element.nextSibling;
|
||||
const parentEl = this._element.parentElement;
|
||||
|
||||
const wrapperEl = document.createElement('div');
|
||||
wrapperEl.classList.add('password-input__wrapper');
|
||||
wrapperEl.appendChild(this._element);
|
||||
this._wrapperEl = document.createElement('div');
|
||||
this._wrapperEl.classList.add('password-input__wrapper');
|
||||
this._wrapperEl.appendChild(this._element);
|
||||
|
||||
this._toggleContainerEl = document.createElement('div');
|
||||
this._toggleContainerEl.classList.add('password-input__toggle');
|
||||
wrapperEl.appendChild(this._toggleContainerEl);
|
||||
this._wrapperEl.appendChild(this._toggleContainerEl);
|
||||
|
||||
this._iconEl = document.createElement('i');
|
||||
this._iconEl.classList.add('fas', 'fa-fw');
|
||||
this._toggleContainerEl.appendChild(this._iconEl);
|
||||
|
||||
parentEl.insertBefore(wrapperEl, siblingEl);
|
||||
parentEl.insertBefore(this._wrapperEl, siblingEl);
|
||||
|
||||
this._element.classList.add(PASSWORD_INITIALIZED_CLASS);
|
||||
}
|
||||
@ -44,17 +49,31 @@ export class Password {
|
||||
start() {
|
||||
this.updateVisibleIcon(this.isVisible());
|
||||
|
||||
this._toggleContainerEl.addEventListener('mouseover', () => {
|
||||
const mouseOverEv = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => {
|
||||
this.updateVisibleIcon(!this.isVisible());
|
||||
});
|
||||
this._toggleContainerEl.addEventListener('mouseout', () => {
|
||||
}).bind(this), this._toggleContainerEl);
|
||||
|
||||
const mouseOutEv = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => {
|
||||
this.updateVisibleIcon(this.isVisible());
|
||||
});
|
||||
this._toggleContainerEl.addEventListener('click', (event) => {
|
||||
}).bind(this), this._toggleContainerEl);
|
||||
|
||||
const clickEv = new EventWrapper(EVENT_TYPE.CLICK, ((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.setVisible(!this.isVisible());
|
||||
});
|
||||
}).bind(this), this._toggleContainerEl );
|
||||
|
||||
this._eventManager.registerListeners([mouseOverEv, mouseOutEv, clickEv]);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
this._iconEl.remove();
|
||||
this._toggleContainerEl.remove();
|
||||
this._wrapperEl.remove();
|
||||
this._iconEl.remove();
|
||||
|
||||
this._element.classList.remove(PASSWORD_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
|
||||
@ -9,39 +9,51 @@ const RADIO_INITIALIZED_CLASS = 'radio--initialized';
|
||||
})
|
||||
export class Radio {
|
||||
|
||||
_element;
|
||||
_wrapperEl;
|
||||
_labelEl;
|
||||
|
||||
constructor(element) {
|
||||
if (!element) {
|
||||
throw new Error('Radio utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.closest('.radio-group')) {
|
||||
this._element = element;
|
||||
|
||||
if (this._element.closest('.radio-group')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (element.classList.contains(RADIO_INITIALIZED_CLASS)) {
|
||||
if (this._element.classList.contains(RADIO_INITIALIZED_CLASS)) {
|
||||
// throw new Error('Radio utility already initialized!');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (element.parentElement.classList.contains(RADIO_CLASS)) {
|
||||
if (this._element.parentElement.classList.contains(RADIO_CLASS)) {
|
||||
// throw new Error('Radio element\'s wrapper already has class '' + RADIO_CLASS + ''!');
|
||||
return false;
|
||||
}
|
||||
|
||||
const siblingEl = element.nextSibling;
|
||||
const parentEl = element.parentElement;
|
||||
const siblingEl = this._element.nextSibling;
|
||||
const parentEl = this._element.parentElement;
|
||||
|
||||
const wrapperEl = document.createElement('div');
|
||||
wrapperEl.classList.add(RADIO_CLASS);
|
||||
this._wrapperEl = document.createElement('div');
|
||||
this._wrapperEl.classList.add(RADIO_CLASS);
|
||||
|
||||
const labelEl = document.createElement('label');
|
||||
labelEl.setAttribute('for', element.id);
|
||||
this._labelEl = document.createElement('label');
|
||||
this._labelEl.setAttribute('for', this._element.id);
|
||||
|
||||
wrapperEl.appendChild(element);
|
||||
wrapperEl.appendChild(labelEl);
|
||||
this._wrapperEl.appendChild(this._element);
|
||||
this._wrapperEl.appendChild(this._labelEl);
|
||||
|
||||
parentEl.insertBefore(wrapperEl, siblingEl);
|
||||
parentEl.insertBefore(this._wrapperEl, siblingEl);
|
||||
|
||||
element.classList.add(RADIO_INITIALIZED_CLASS);
|
||||
this._element.classList.add(RADIO_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._labelEl.remove();
|
||||
this._wrapperEl.remove();
|
||||
this._element.classList.remove(RADIO_INITIALIZED_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { Utility } from '../../core/utility';
|
||||
import { Datepicker } from '../form/datepicker';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import './mass-input.sass';
|
||||
|
||||
const MASS_INPUT_CELL_SELECTOR = '.massinput__cell';
|
||||
@ -29,6 +30,8 @@ export class MassInput {
|
||||
|
||||
_changedAdd = new Array();
|
||||
|
||||
_eventManager;
|
||||
|
||||
constructor(element, app) {
|
||||
if (!element) {
|
||||
throw new Error('Mass Input utility cannot be setup without an element!');
|
||||
@ -37,6 +40,8 @@ export class MassInput {
|
||||
this._element = element;
|
||||
this._app = app;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (global !== undefined)
|
||||
this._global = global;
|
||||
else if (window !== undefined)
|
||||
@ -64,9 +69,10 @@ export class MassInput {
|
||||
buttons.forEach((button) => {
|
||||
this._setupSubmitButton(button);
|
||||
});
|
||||
|
||||
this._massInputForm.addEventListener('submit', this._massInputFormSubmitHandler.bind(this));
|
||||
this._massInputForm.addEventListener('keypress', this._keypressHandler.bind(this));
|
||||
|
||||
const submitEv = new EventWrapper(EVENT_TYPE.SUBMIT, this._massInputFormSubmitHandler.bind(this), this._massInputForm);
|
||||
const keyPressEv = new EventWrapper(EVENT_TYPE.KEYDOWN, this._keypressHandler.bind(this), this._massInputForm);
|
||||
this._eventManager.registerListeners([submitEv, keyPressEv]);
|
||||
|
||||
Array.from(this._element.querySelectorAll(MASS_INPUT_ADD_CELL_SELECTOR)).forEach(this._setupChangedHandlers.bind(this));
|
||||
|
||||
@ -76,14 +82,16 @@ export class MassInput {
|
||||
|
||||
destroy() {
|
||||
this._reset();
|
||||
this._eventManager.cleanUp();
|
||||
this._element.classList.remove(MASS_INPUT_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
_setupChangedHandlers(addCell) {
|
||||
Array.from(addCell.querySelectorAll(MASS_INPUT_ADD_CHANGE_FIELD_SELECTOR)).forEach(inputElem => {
|
||||
if (inputElem.closest('[uw-mass-input]') !== this._element)
|
||||
return;
|
||||
|
||||
inputElem.addEventListener('change', () => { this._changedAdd.push(addCell); });
|
||||
const changeEv = new EventWrapper(EVENT_TYPE.CHANGE, (() => { this._changedAdd.push(addCell); }).bind(this), inputElem);
|
||||
this._eventManager.registerNewListener(changeEv);
|
||||
});
|
||||
}
|
||||
|
||||
@ -198,7 +206,7 @@ export class MassInput {
|
||||
if (this._massInputFormSubmitHandler) {
|
||||
return this._massInputFormSubmitHandler(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_getMassInputSubmitButtons() {
|
||||
return Array.from(this._element.querySelectorAll('button[type="submit"][name][value], .' + MASS_INPUT_SUBMIT_BUTTON_CLASS));
|
||||
@ -207,13 +215,13 @@ export class MassInput {
|
||||
_setupSubmitButton(button) {
|
||||
button.setAttribute('type', 'button');
|
||||
button.classList.add(MASS_INPUT_SUBMIT_BUTTON_CLASS);
|
||||
button.addEventListener('click', this._massInputFormSubmitHandler);
|
||||
const buttonClickEv = new EventWrapper(EVENT_TYPE.CLICK, this._massInputFormSubmitHandler.bind(this), button);
|
||||
this._eventManager.registerNewListener(buttonClickEv);
|
||||
}
|
||||
|
||||
_resetSubmitButton(button) {
|
||||
button.setAttribute('type', 'submit');
|
||||
button.classList.remove(MASS_INPUT_SUBMIT_BUTTON_CLASS);
|
||||
button.removeEventListener('click', this._massInputFormSubmitHandler);
|
||||
}
|
||||
|
||||
_processResponse(responseElement) {
|
||||
@ -268,9 +276,6 @@ export class MassInput {
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this._element.classList.remove(MASS_INPUT_INITIALIZED_CLASS);
|
||||
this._massInputForm.removeEventListener('submit', this._massInputFormSubmitHandler);
|
||||
this._massInputForm.removeEventListener('keypress', this._keypressHandler);
|
||||
|
||||
const buttons = this._getMassInputSubmitButtons();
|
||||
buttons.forEach((button) => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import './modal.sass';
|
||||
|
||||
const MODAL_HEADERS = {
|
||||
@ -28,12 +29,16 @@ const MODALS_WRAPPER_OPEN_CLASS = 'modals-wrapper--open';
|
||||
})
|
||||
export class Modal {
|
||||
|
||||
_eventManager;
|
||||
|
||||
_element;
|
||||
_app;
|
||||
|
||||
_modalsWrapper;
|
||||
_modalOverlay;
|
||||
_modalUrl;
|
||||
_triggerElement;
|
||||
_closerElement;
|
||||
|
||||
constructor(element, app) {
|
||||
if (!element) {
|
||||
@ -42,6 +47,7 @@ export class Modal {
|
||||
|
||||
this._element = element;
|
||||
this._app = app;
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(MODAL_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
@ -66,7 +72,7 @@ export class Modal {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// TODO
|
||||
throw new Error('Destroying modals is not possible.');
|
||||
}
|
||||
|
||||
_ensureModalWrapper() {
|
||||
@ -92,40 +98,43 @@ export class Modal {
|
||||
if (!triggerSelector.startsWith('#')) {
|
||||
triggerSelector = '#' + triggerSelector;
|
||||
}
|
||||
const triggerElement = document.querySelector(triggerSelector);
|
||||
this._triggerElement = document.querySelector(triggerSelector);
|
||||
|
||||
if (!triggerElement) {
|
||||
if (!this._triggerElement) {
|
||||
throw new Error('Trigger element for Modal not found: "' + triggerSelector + '"');
|
||||
}
|
||||
|
||||
triggerElement.classList.add(MODAL_TRIGGER_CLASS);
|
||||
triggerElement.addEventListener('click', this._onTriggerClicked, false);
|
||||
this._modalUrl = triggerElement.getAttribute('href');
|
||||
this._triggerElement.classList.add(MODAL_TRIGGER_CLASS);
|
||||
const triggerEvent = new EventWrapper(EVENT_TYPE.CLICK, this._onTriggerClicked.bind(this), this._triggerElement, false);
|
||||
this._eventManager.registerNewListener(triggerEvent);
|
||||
this._modalUrl = this._triggerElement.getAttribute('href');
|
||||
}
|
||||
|
||||
_setupCloser() {
|
||||
const closerElement = document.createElement('div');
|
||||
this._element.insertBefore(closerElement, null);
|
||||
closerElement.classList.add(MODAL_CLOSER_CLASS);
|
||||
closerElement.addEventListener('click', this._onCloseClicked, false);
|
||||
this._modalOverlay.addEventListener('click', this._onCloseClicked, false);
|
||||
this._closerElement = document.createElement('div');
|
||||
this._element.insertBefore(this._closerElement, null);
|
||||
this._closerElement.classList.add(MODAL_CLOSER_CLASS);
|
||||
const closerElEvent = new EventWrapper(EVENT_TYPE.CLICK, this._onCloseClicked.bind(this), this._closerElement, false);
|
||||
this._eventManager.registerNewListener(closerElEvent);
|
||||
const overlayClose = new EventWrapper(EVENT_TYPE.CLICK, this._onCloseClicked.bind(this), this._modalOverlay, false);
|
||||
this._eventManager.registerNewListener(overlayClose);
|
||||
}
|
||||
|
||||
_onTriggerClicked = (event) => {
|
||||
event.preventDefault();
|
||||
this._open();
|
||||
}
|
||||
};
|
||||
|
||||
_onCloseClicked = (event) => {
|
||||
event.preventDefault();
|
||||
this._close();
|
||||
}
|
||||
};
|
||||
|
||||
_onKeyUp = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
this._close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_open() {
|
||||
this._element.classList.add(MODAL_OPEN_CLASS);
|
||||
@ -153,9 +162,9 @@ export class Modal {
|
||||
url: url,
|
||||
headers: MODAL_HEADERS,
|
||||
}).then(
|
||||
(response) => this._app.htmlHelpers.parseResponse(response)
|
||||
(response) => this._app.htmlHelpers.parseResponse(response),
|
||||
).then(
|
||||
(response) => this._processResponse(response.element)
|
||||
(response) => this._processResponse(response.element),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import './navbar.sass';
|
||||
import * as throttle from 'lodash.throttle';
|
||||
import throttle from 'lodash.throttle';
|
||||
|
||||
export const HEADER_CONTAINER_UTIL_SELECTOR = '.navbar__list-item--container-selector .navbar__link-wrapper';
|
||||
const HEADER_CONTAINER_INITIALIZED_CLASS = 'navbar-header-container--initialized';
|
||||
@ -18,6 +19,8 @@ export class NavHeaderContainerUtil {
|
||||
|
||||
_throttleUpdateWasOpen;
|
||||
|
||||
_eventManager;
|
||||
|
||||
constructor(element) {
|
||||
if (!element) {
|
||||
throw new Error('Navbar Header Container utility needs to be passed an element!');
|
||||
@ -29,6 +32,9 @@ export class NavHeaderContainerUtil {
|
||||
|
||||
this._element = element;
|
||||
this.radioButton = document.getElementById(`${this._element.id}-radio`);
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (!this.radioButton) {
|
||||
throw new Error('Navbar Header Container utility could not find associated radio button!');
|
||||
}
|
||||
@ -58,8 +64,9 @@ export class NavHeaderContainerUtil {
|
||||
if (!this.container)
|
||||
return;
|
||||
|
||||
window.addEventListener('click', this.clickHandler.bind(this));
|
||||
this.radioButton.addEventListener('change', this.throttleUpdateWasOpen.bind(this));
|
||||
const clickEv = new EventWrapper(EVENT_TYPE.CLICK, this.clickHandler.bind(this), window);
|
||||
const changeEv = new EventWrapper(EVENT_TYPE.CHANGE, this.throttleUpdateWasOpen.bind(this), this.radioButton);
|
||||
this._eventManager.registerListeners([clickEv, changeEv]);
|
||||
}
|
||||
|
||||
clickHandler() {
|
||||
@ -81,7 +88,10 @@ export class NavHeaderContainerUtil {
|
||||
this.wasOpen = this.isOpen();
|
||||
}
|
||||
|
||||
destroy() { /* TODO */ }
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
this._element.classList.remove(HEADER_CONTAINER_INITIALIZED_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import './pageactions.sass';
|
||||
import * as throttle from 'lodash.throttle';
|
||||
import throttle from 'lodash.throttle';
|
||||
|
||||
export const PAGEACTION_SECONDARY_UTIL_SELECTOR = '.pagenav__list-item';
|
||||
const PAGEACTION_SECONDARY_INITIALIZED_CLASS = '.pagenav-list-item--initialized';
|
||||
@ -17,9 +18,12 @@ export class PageActionSecondaryUtil {
|
||||
closeButton;
|
||||
container;
|
||||
wasOpen;
|
||||
_closer;
|
||||
|
||||
_throttleUpdateWasOpen;
|
||||
|
||||
_eventManager;
|
||||
|
||||
constructor(element) {
|
||||
if (!element) {
|
||||
throw new Error('Pageaction Secondary utility needs to be passed an element!');
|
||||
@ -31,6 +35,8 @@ export class PageActionSecondaryUtil {
|
||||
|
||||
this._element = element;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
const childContainer = this._element.querySelector('.pagenav-item__children');
|
||||
|
||||
if (!childContainer) {
|
||||
@ -43,7 +49,7 @@ export class PageActionSecondaryUtil {
|
||||
const links = Array.from(this._element.querySelectorAll('.pagenav-item__link')).filter(l => !childContainer.contains(l));
|
||||
|
||||
if (!links || Array.from(links).length !== 1) {
|
||||
throw new Error('Pageaction Secondary utility could not find associated link!');
|
||||
throw new Error('Pageaction Secondary utility could not find associated link!');
|
||||
}
|
||||
this.navIdent = links[0].id;
|
||||
}
|
||||
@ -71,9 +77,9 @@ export class PageActionSecondaryUtil {
|
||||
throw new Error('Pageaction Secondary utility could not find associated container!');
|
||||
}
|
||||
|
||||
const closer = this._element.querySelector('.pagenav-item__close-label');
|
||||
if (closer) {
|
||||
closer.classList.add('pagenav-item__close-label--hidden');
|
||||
this._closer = this._element.querySelector('.pagenav-item__close-label');
|
||||
if (this._closer) {
|
||||
this._closer.classList.add('pagenav-item__close-label--hidden');
|
||||
}
|
||||
|
||||
this.updateWasOpen();
|
||||
@ -85,12 +91,12 @@ export class PageActionSecondaryUtil {
|
||||
start() {
|
||||
if (!this.container)
|
||||
return;
|
||||
|
||||
window.addEventListener('click', this.clickHandler.bind(this));
|
||||
this.radioButton.addEventListener('change', this.throttleUpdateWasOpen.bind(this));
|
||||
const windowClickEv = new EventWrapper(EVENT_TYPE.CLICK, ((event) => this.clickHandler(event)).bind(this), window);
|
||||
const radioButtonChangeEv = new EventWrapper(EVENT_TYPE.CHANGE, this.throttleUpdateWasOpen.bind(this), this.radioButton);
|
||||
this._eventManager.registerListeners([windowClickEv, radioButtonChangeEv]);
|
||||
}
|
||||
|
||||
clickHandler() {
|
||||
clickHandler(event) {
|
||||
if (!this.container.contains(event.target) && window.document.contains(event.target) && this.wasOpen) {
|
||||
this.close();
|
||||
}
|
||||
@ -109,7 +115,12 @@ export class PageActionSecondaryUtil {
|
||||
this.wasOpen = this.isOpen();
|
||||
}
|
||||
|
||||
destroy() { /* TODO */ }
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
if(this._closer && this._closer.classList.contains('pagenav-item__close-label--hidden'))
|
||||
this._closer.classList.remove('pagenav-item__close-label--hidden');
|
||||
this._element.classList.remove(PAGEACTION_SECONDARY_INITIALIZED_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
import './show-hide.sass';
|
||||
|
||||
const SHOW_HIDE_LOCAL_STORAGE_KEY = 'SHOW_HIDE';
|
||||
@ -16,6 +17,7 @@ export class ShowHide {
|
||||
_showHideId;
|
||||
_element;
|
||||
|
||||
_eventManager;
|
||||
_storageManager = new StorageManager(SHOW_HIDE_LOCAL_STORAGE_KEY, '1.0.0', { location: LOCATION.LOCAL });
|
||||
|
||||
constructor(element) {
|
||||
@ -24,13 +26,15 @@ export class ShowHide {
|
||||
}
|
||||
|
||||
this._element = element;
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
if (this._element.classList.contains(SHOW_HIDE_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// register click listener
|
||||
this._addClickListener();
|
||||
const clickEv = new EventWrapper(EVENT_TYPE.CLICK, this._clickHandler.bind(this), this._element);
|
||||
this._eventManager.registerNewListener(clickEv);
|
||||
|
||||
// param showHideId
|
||||
if (this._element.dataset.showHideId) {
|
||||
@ -58,17 +62,18 @@ export class ShowHide {
|
||||
}
|
||||
|
||||
this._checkHash();
|
||||
|
||||
window.addEventListener('hashchange', this._checkHash.bind(this));
|
||||
const hashChangeEv = new EventWrapper(EVENT_TYPE.HASH_CHANGE, this._checkHash.bind(this), window);
|
||||
this._eventManager.registerNewListener(hashChangeEv);
|
||||
|
||||
// mark as initialized
|
||||
this._element.classList.add(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS);
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
|
||||
_addClickListener() {
|
||||
this._element.addEventListener('click', this._clickHandler.bind(this));
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
if (this._element.parentElement.classList.contains(SHOW_HIDE_COLLAPSED_CLASS))
|
||||
this._element.parentElement.classList.remove(SHOW_HIDE_COLLAPSED_CLASS);
|
||||
this._element.classList.remove(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS);
|
||||
}
|
||||
|
||||
_show() {
|
||||
|
||||
@ -21,7 +21,7 @@ export class SortTable {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
console.log('TBD destroy SortTable');
|
||||
this._storageManager.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Utility } from '../../core/utility';
|
||||
import './tooltips.sass';
|
||||
import { MovementObserver } from '../../lib/movement-observer/movement-observer';
|
||||
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
|
||||
|
||||
const TOOLTIP_CLASS = 'tooltip';
|
||||
const TOOLTIP_INITIALIZED_CLASS = 'tooltip--initialized';
|
||||
@ -17,6 +18,7 @@ export class Tooltip {
|
||||
_content;
|
||||
|
||||
_movementObserver;
|
||||
_eventManager;
|
||||
|
||||
_openedPersistent = false;
|
||||
|
||||
@ -45,16 +47,19 @@ export class Tooltip {
|
||||
this._element = element;
|
||||
this._handle = element.querySelector('.tooltip__handle') || element;
|
||||
|
||||
this._eventManager = new EventManager();
|
||||
|
||||
this._movementObserver = new MovementObserver(this._handle, { leadingCallback: this.close.bind(this) });
|
||||
|
||||
element.classList.add(TOOLTIP_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
start() {
|
||||
this._element.addEventListener('mouseover', () => { this.open(false); });
|
||||
this._element.addEventListener('mouseout', this._leave.bind(this));
|
||||
this._content.addEventListener('mouseout', this._leave.bind(this));
|
||||
this._element.addEventListener('click', this._click.bind(this));
|
||||
const mouseOverEv = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => { this.open(false); }).bind(this), this._element);
|
||||
const mouseOutEv = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (this._leave.bind(this)).bind(this), this._element);
|
||||
const contentMouseOut = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (this._leave.bind(this)).bind(this), this._content);
|
||||
const clickEv = new EventWrapper(EVENT_TYPE.CLICK, this._click.bind(this), this._element);
|
||||
this._eventManager.registerListeners([mouseOverEv, mouseOutEv, contentMouseOut, clickEv]);
|
||||
}
|
||||
|
||||
open(persistent) {
|
||||
@ -183,5 +188,10 @@ export class Tooltip {
|
||||
}
|
||||
|
||||
|
||||
destroy() {}
|
||||
destroy() {
|
||||
this._eventManager.cleanUp();
|
||||
this._movementObserver.unobserve();
|
||||
const toolTipsRegex = RegExp(/\btooltip--.+\b/, 'g');
|
||||
this._element.className = this._element.className.replace(toolTipsRegex, '');
|
||||
}
|
||||
};
|
||||
|
||||
BIN
messages.tar.bz2
BIN
messages.tar.bz2
Binary file not shown.
@ -40,11 +40,11 @@ StudySubTermsChildName: Child-Name
|
||||
MailTestFormEmail: Email address
|
||||
MailTestFormLanguages: Language settings
|
||||
TestDownload: Download test
|
||||
BearerTokenUsageWarning: Using this interface you are able to encode essentially arbitrary permissions inte bearer tokens. This allows you to freely hand permissions off arbitrarily and without relevant restrictions. Only use this interface if you have discussed the consequences of the specific token, that you want to issue, with an experienced developer!
|
||||
BearerTokenUsageWarning: Using this interface you are able to encode essentially arbitrary permissions into bearer tokens. This allows you to freely hand permissions off arbitrarily and without relevant restrictions. Only use this interface if you have discussed the consequences of the specific token, that you want to issue, with an experienced developer!
|
||||
BearerTokenAuthorityGroups: Authority (groups)
|
||||
BearerTokenAuthorityGroupsTip: All primary users of the groups listed here need to have the requisite permissions to access a route in order for the created token to grant permission to do so as well.
|
||||
BearerTokenAuthorityGroupMissing: Group is required
|
||||
BearerTokenAuthorityUsers: Authority (users
|
||||
BearerTokenAuthorityUsers: Authority (users)
|
||||
BearerTokenAuthorityUsersTip: All users listed here need to have the requisite permissions to access a route in order for the created token to grant permission to do so as well. The user issuing the token using this interface also needs to have permission to access that route (they are automatically added to the list of authorities).
|
||||
BearerTokenAuthorityUnknownUser email: Could not find any user with email #{email}
|
||||
BearerTokenRoutes: Permitted routes
|
||||
|
||||
@ -125,7 +125,20 @@ InvalidCredentialsADTooManyContextIds: Benutzereintrag trägt zu viele Sicherhei
|
||||
InvalidCredentialsADAccountExpired: Benutzereintrag abgelaufen
|
||||
InvalidCredentialsADPasswordMustChange: Passwort muss geändert werden
|
||||
InvalidCredentialsADAccountLockedOut: Benutzereintrag wurde durch Eindringlingserkennung gesperrt
|
||||
|
||||
LoginTitle: Authentifizierung
|
||||
|
||||
FormFieldRequiredTip: Gekennzeichnete Pflichtfelder sind immer auszufüllen
|
||||
FormFieldWorkflowDatasetTip: Mindestens ein gekennzeichnetes Feld pro Datensatz muss ausgefüllt werden
|
||||
|
||||
LoginTitle: Authentifizierung
|
||||
FormHoneypotWebsite: Webseite (URL)
|
||||
FormHoneypotWebsiteTip: Link zu Ihrer Webseite
|
||||
FormHoneypotWebsitePlaceholder: URL
|
||||
FormHoneypotEmail: E-Mail
|
||||
FormHoneypotEmailTip: Ihre E-Mail Adresse
|
||||
FormHoneypotEmailPlaceholder: E-Mail
|
||||
FormHoneypotName: Name
|
||||
FormHoneypotNameTip: Ihr Name oder Ihre E-Mail Adresse
|
||||
FormHoneypotNamePlaceholder: Name
|
||||
FormHoneypotComment: Kommentar
|
||||
FormHoneypotCommentPlaceholder: Kommentar
|
||||
FormHoneypotFilled: Bitte füllen Sie keines der verstecken Felder aus
|
||||
@ -126,6 +126,20 @@ InvalidCredentialsADTooManyContextIds: Account carries to many security identifi
|
||||
InvalidCredentialsADAccountExpired: Account expired
|
||||
InvalidCredentialsADPasswordMustChange: Password needs to be changed
|
||||
InvalidCredentialsADAccountLockedOut: Account disabled by intruder detection
|
||||
|
||||
LoginTitle: Authentication
|
||||
|
||||
FormFieldRequiredTip: Required fields
|
||||
FormFieldWorkflowDatasetTip: At least one of the marked fields must be filled
|
||||
LoginTitle: Authentication
|
||||
FormHoneypotWebsite: Website (URL)
|
||||
FormHoneypotWebsiteTip: Link to your website
|
||||
FormHoneypotWebsitePlaceholder !ident-ok: URL
|
||||
FormHoneypotEmail: Email
|
||||
FormHoneypotEmailTip: Your email address
|
||||
FormHoneypotEmailPlaceholder: Email
|
||||
FormHoneypotName !ident-ok: Name
|
||||
FormHoneypotNameTip: Your name or your email address
|
||||
FormHoneypotNamePlaceholder !ident-ok: Name
|
||||
FormHoneypotComment: Comment
|
||||
FormHoneypotCommentPlaceholder: Comment
|
||||
FormHoneypotFilled: Please do not fill in any of the hidden fields
|
||||
|
||||
@ -4,6 +4,8 @@ FilterTerm !ident-ok: Semester
|
||||
FilterCourseSchoolShort: Institut
|
||||
FilterRegistered: Angemeldet
|
||||
FilterCourseSearch: Volltext-Suche
|
||||
FilterCourseSearchShorthand: Kürzel-Suche
|
||||
FilterCourseSearchTitle: Titel-Suche
|
||||
FilterCourseRegistered: Registriert
|
||||
FilterCourseRegisterOpen: Anmeldung möglich
|
||||
FilterCourseAllocation: Zentralanmeldung
|
||||
@ -187,6 +189,7 @@ LecturerFor: Dozent:in
|
||||
LecturersFor: Dozierende
|
||||
AssistantFor: Assistent:in
|
||||
AssistantsFor: Assistent:innen
|
||||
CourseAdminFor: Kursadministration
|
||||
TutorsFor n@Int: #{pluralDE n "Tutor:in" "Tutor:innen"}
|
||||
CorrectorsFor n@Int: #{pluralDE n "Korrektor:in" "Korrektor:innen"}
|
||||
CourseParticipantsHeading: Kursteilnehmer:innen
|
||||
@ -278,4 +281,4 @@ LecturerInvitationAccepted lType@Text csh@CourseShorthand: Sie wurden als #{lTyp
|
||||
CourseExamRegistrationTime: Angemeldet seit
|
||||
CourseParticipantStateIsActiveFilter: Ansicht
|
||||
CourseApply: Zum Kurs bewerben
|
||||
CourseAdministrator: Kursadministrator:in
|
||||
CourseAdministrator: Kursadministrator:in
|
||||
|
||||
@ -4,6 +4,8 @@ FilterTerm: Semester
|
||||
FilterCourseSchoolShort: Department
|
||||
FilterRegistered: Enrolled
|
||||
FilterCourseSearch: Text search
|
||||
FilterCourseSearchShorthand: Shorthand search
|
||||
FilterCourseSearchTitle: Title search
|
||||
FilterCourseRegistered: Registered
|
||||
FilterCourseRegisterOpen: Enrolment is allowed
|
||||
FilterCourseAllocation: Central allocation
|
||||
@ -187,6 +189,7 @@ LecturerFor: Lecturer
|
||||
LecturersFor: Lecturers
|
||||
AssistantFor: Assistant
|
||||
AssistantsFor: Assistants
|
||||
CourseAdminFor: Course administration
|
||||
TutorsFor n: #{pluralEN n "Tutor" "Tutors"}
|
||||
CorrectorsFor n: #{pluralEN n "Corrector" "Correctors"}
|
||||
CourseParticipantsHeading: Course participants
|
||||
@ -244,8 +247,8 @@ CourseLecInviteExplanation: You were invited to be a course administrator.
|
||||
CourseUserHasPersonalisedSheetFilesFilter: Participant has personalised sheet files for
|
||||
HeadingCourseMembers: Participants
|
||||
CourseAssistant: Assistant
|
||||
CourseParticipantStateIsInactive: Participant is CourseParticipantStateIsInactive
|
||||
CourseParticipantStateIsActive: Participant is aktive
|
||||
CourseParticipantStateIsInactive: Participant is inactive
|
||||
CourseParticipantStateIsActive: Participant is active
|
||||
CourseUserSendMail: Send message to participant
|
||||
CourseUserRegisterTutorial: Register tutorial
|
||||
CourseUserRegisterExam: Register exam
|
||||
@ -277,4 +280,4 @@ LecturerInvitationAccepted lType csh: You were registered as #{lType} for #{csh}
|
||||
CourseExamRegistrationTime: Registered since
|
||||
CourseParticipantStateIsActiveFilter: View
|
||||
CourseApply: Apply for course
|
||||
CourseAdministrator: Course administrator
|
||||
CourseAdministrator: Course administrator
|
||||
|
||||
@ -55,3 +55,24 @@ ExamOfficeFieldSubscribed: Abboniert
|
||||
UtilExamClosed: Noten gemeldet
|
||||
ExamFinishedOffice: Noten bekannt gegeben
|
||||
ExamOfficeFieldForced: Forcierte Einsicht
|
||||
|
||||
ExamOfficeGetSynced: Synchronisiert-Status in Prüfungsliste anzeigen
|
||||
ExamOfficeGetSyncedTip: Soll unter „Prüfungen“ der Synchronisiert-Status zu jeder Prüfung angezeigt werden? (Ein Deaktivieren dieser Option kann zu kürzeren Ladezeiten der Prüfungsliste führen.)
|
||||
|
||||
ExamLabel: Prüfungs-Label
|
||||
ExamOfficeGetLabels: Labels in Prüfungsliste anzeigen
|
||||
ExamOfficeGetLabelsTip: Sollen unter „Prüfungen“ die gesetzten Labels zu jeder Prüfung angezeigt werden?
|
||||
ExamOfficeLabels: Prüfungs-Labels
|
||||
ExamOfficeLabelsTip: Sie können hier Labels anlegen und verwalten, welche sie einzelnen Prüfungen über die Prüfungsliste (siehe „Prüfungen“) zuweisen können.
|
||||
ExamOfficeLabelName !ident-ok: Name
|
||||
ExamOfficeLabelStatus !ident-ok: Status
|
||||
ExamOfficeLabelPriority: Priorität
|
||||
ExamOfficeLabelAlreadyExists: Es existiert bereits ein Prüfungs-Label mit diesem Namen!
|
||||
ExamOfficeExamsNoLabel: Kein Label
|
||||
ExamSetLabel: Label setzen
|
||||
ExamLabelsSet n@Int: #{n} Prüfungs-#{pluralDE n "Label" "Labels"} gesetzt
|
||||
ExamRemoveLabel: Label entfernen
|
||||
ExamLabelsRemoved n@Int: #{n} Prüfungs-#{pluralDE n "Label" "Labels"} entfernt
|
||||
ExamOfficeLabelSetLabelOnExport: Prüfungs-Label beim Export setzen
|
||||
ExamOfficeLabelSetLabelOnExportTip t@Text: Soll beim CSV-Export automatisch das Export-Label für die jeweilige Prüfung gesetzt werden? Von Ihnen gesetzte Prüfungs-Label sind ausschließlich für Sie sichtbar und können von jedem Prüfungsbeauftragten unabhängig voneinander verwaltet bzw. verwendet werden. Ihr aktuell für den CSV-Export eingestelltes Prüfungs-Label ist „#{t}“. Sie können das zu setzende Prüfungs-Label unter „Export-Optionen“ oder in Ihren persönlichen Benutzereinstellungen ändern.
|
||||
ExamOfficeLabelSetLabelOnExportForcedTip: Soll beim CSV-Export automatisch das Export-Label für die jeweilige Prüfung gesetzt werden? Von Ihnen gesetzte Prüfungs-Label sind ausschließlich für Sie sichtbar und können von jedem Prüfungsbeauftragten unabhängig voneinander verwaltet bzw. verwendet werden. Sie haben aktuell kein Export-Label festgelegt und können diese Option daher nicht auswählen. Sie können das beim CSV-Export zu setzende Prüfungs-Label unter „Export-Optionen“ oder in Ihren persönlichen Benutzereinstellungen wählen.
|
||||
|
||||
@ -53,3 +53,24 @@ ExamOfficeFieldSubscribed: subscribed
|
||||
UtilExamClosed: Exam achievements registered
|
||||
ExamFinishedOffice: Exam achievements published
|
||||
ExamOfficeFieldForced: Forced access
|
||||
|
||||
ExamOfficeGetSynced: Show synchronised status in exam list
|
||||
ExamOfficeGetSyncedTip: Should the synchronised status be displayed in “Exams”? (Disabling this option may lead to shorter loading times of the exam list.)
|
||||
|
||||
ExamLabel: Exam label
|
||||
ExamOfficeGetLabels: Show labels in exam list
|
||||
ExamOfficeGetLabelsTip: Should the labels of each exam be displayed in “Exams”?
|
||||
ExamOfficeLabels: Exam labels
|
||||
ExamOfficeLabelsTip: Here you can add and manage labels, which you can assign exam list entries (see “Exams”).
|
||||
ExamOfficeLabelName: Name
|
||||
ExamOfficeLabelStatus: Status
|
||||
ExamOfficeLabelPriority: Priority
|
||||
ExamOfficeLabelAlreadyExists: There already exists an exam label with this name!
|
||||
ExamOfficeExamsNoLabel: No label
|
||||
ExamSetLabel: Set label
|
||||
ExamLabelsSet n: Successfully set #{n} exam #{pluralEN n "label" "labels"}
|
||||
ExamRemoveLabel: Remove label
|
||||
ExamLabelsRemoved n: Successfully removed #{n} exam #{pluralEN n "label" "labels"}
|
||||
ExamOfficeLabelSetLabelOnExport: Set exam label while exporting
|
||||
ExamOfficeLabelSetLabelOnExportTip t: Should the export label be set for the respective exam? Your set exam labels are exclusively visible to you and may be managed and used by each exam office member independently. Your saved exam label for CSV export is currently “#{t}”. You can change the exam label set while exporting under “Export options” or in your user settings.
|
||||
ExamOfficeLabelSetLabelOnExportForcedTip: Should the export label be set for the respective exam? Your set exam labels are exclusively visible to you and may be managed and used by each exam office member independently. You do not currently have any exam label selected as export label and therefor cannot active this setting. To set an exam label as export label, go to “Export options” or your user settings.
|
||||
|
||||
@ -159,6 +159,8 @@ SubmissionDownloadMatriculations: Mit Matrikelnummern
|
||||
SubmissionDownloadGroups: Mit festen Abgabegruppen
|
||||
CorrAutoSetCorrector: Korrekturen verteilen
|
||||
CorrDelete: Abgaben löschen
|
||||
CorrSetCorrectionsDone: Korrekturen als abgeschlossen markieren
|
||||
SetCorrectionsDone b@Bool: Korrekturen als #{notDE b} abgeschlossen markiert
|
||||
SubmissionCorrected: Korrigiert
|
||||
CorrectionSheets: Übersicht Korrekturen nach Blättern
|
||||
CorrectionCorrectors: Übersicht Korrekturen nach Korrektor:innen
|
||||
@ -186,6 +188,7 @@ UploadModeExtensionRestrictionTip: Komma-separiert. Wenn keine Dateiendungen ang
|
||||
UploadModeExtensionRestrictionMultipleTip: Einschränkung von Dateiendungen erfolgt für alle hochgeladenen Dateien, auch innerhalb von ZIP-Archiven.
|
||||
FileUploadMaxSize maxSize@Text: Datei darf maximal #{maxSize} groß sein
|
||||
FileUploadMaxSizeMultiple maxSize@Text: Dateien dürfen jeweils maximal #{maxSize} groß sein
|
||||
FileUploadCumulativeMaxSize maxSize@Text: Dateien dürfen insgesamt maximal #{maxSize} groß sein
|
||||
|
||||
InvalidPseudonym pseudonym@Text: Invalides Pseudonym "#{pseudonym}"
|
||||
InvalidPseudonymSubmissionIgnored oPseudonyms@Text iPseudonym@Text: Abgabe mit Pseudonymen „#{oPseudonyms}“ wurde ignoriert, da „#{iPseudonym}“ nicht automatisiert zu einem validen Pseudonym korrigiert werden konnte.
|
||||
@ -261,4 +264,4 @@ CorrectionTableCsvSheetNameCourseCorrections tid@TermId ssh@SchoolId csh@CourseS
|
||||
CorrectionTableCsvNameCorrections: abgaben
|
||||
CorrectionTableCsvSheetNameCorrections: Abgaben
|
||||
CorrectionTableCsvNameCourseUserCorrections tid@TermId ssh@SchoolId csh@CourseShorthand displayName@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase displayName}-abgaben
|
||||
CorrectionTableCsvSheetNameCourseUserCorrections tid@TermId ssh@SchoolId csh@CourseShorthand displayName@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase displayName} Abgaben
|
||||
CorrectionTableCsvSheetNameCourseUserCorrections tid@TermId ssh@SchoolId csh@CourseShorthand displayName@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase displayName} Abgaben
|
||||
|
||||
@ -159,6 +159,8 @@ SubmissionDownloadMatriculations: With matriculation numbers
|
||||
SubmissionDownloadGroups: With registered submission groups
|
||||
CorrAutoSetCorrector: Distribute corrections
|
||||
CorrDelete: Delete submissions
|
||||
CorrSetCorrectionsDone: Set corrections as done
|
||||
SetCorrectionsDone b: Set corrections as #{notEN b} done
|
||||
SubmissionCorrected: Marked
|
||||
CorrectionSheets: Corrections by sheet
|
||||
CorrectionCorrectors: Corrections by corrector
|
||||
@ -186,6 +188,8 @@ UploadModeExtensionRestrictionTip: Comma-separated. If no file extensions are sp
|
||||
UploadModeExtensionRestrictionMultipleTip: Checks for valid file extension are performed for all uploaded files, including those packed within zip-archives.
|
||||
FileUploadMaxSize maxSize: File may be up to #{maxSize} in size
|
||||
FileUploadMaxSizeMultiple maxSize: Files may each be up to #{maxSize} in size
|
||||
FileUploadCumulativeMaxSize maxSize: Files may be no larger than #{maxSize} in total
|
||||
|
||||
InvalidPseudonym pseudonym: Invalid pseudonym “#{pseudonym}”
|
||||
InvalidPseudonymSubmissionIgnored oPseudonyms iPseudonym: The submission with pseudonyms “#{oPseudonyms}” has been ignored since “#{iPseudonym}” could not be automatically corrected to be a valid pseudonym.
|
||||
PseudonymAutocorrections: Suggestions:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
HelpRequestHeading: Support request / Suggestion
|
||||
HelpRequestHeading: Support request/Suggestion
|
||||
HelpIntroduction: If you have trouble using this website or if you find something that could be improved, please contact us even if you were already able to solve your problem by yourself! We are continually making changes and try to keep the site as intuitive as possible even for new users.
|
||||
|
||||
HelpProblemPage: Problematic page
|
||||
@ -7,7 +7,7 @@ HelpUser: My user account
|
||||
HelpEmail: Email
|
||||
HelpAnonymous: Send no answers (anonymous)
|
||||
HelpSubject: Subject
|
||||
HelpRequest: Support request / Suggestion
|
||||
HelpRequest: Support request/Suggestion
|
||||
HelpSent: Your support request has been sent.
|
||||
AdditionalFaqs: More frequently asked questions
|
||||
HelpName: Name
|
||||
|
||||
@ -21,9 +21,9 @@ AuthTagExamCorrector: User is exam corrector
|
||||
AuthTagTutor: User is tutor
|
||||
AuthTagTutorControl: Tutors have control over their tutorial
|
||||
AuthTagTime: Time restrictions are fulfilled
|
||||
AuthTagStaffTime: Time restrictions wrt. staff are fulfilled
|
||||
AuthTagStaffTime: Time restrictions for teaching staff are fulfilled
|
||||
AuthTagAllocationTime: Time restrictions due to a central allocation are fulfilled
|
||||
AuthTagCourseTime: Time restrictions wrt. course visibility are fulfilled
|
||||
AuthTagCourseTime: Time restrictions for course visibility are fulfilled
|
||||
AuthTagCourseRegistered: User is enrolled in course
|
||||
AuthTagAllocationRegistered: User participates in central allocation
|
||||
AuthTagTutorialRegistered: User is tutorial participant
|
||||
|
||||
@ -3,6 +3,8 @@ CsvOptionsTip: Diese Einstellungen betreffen primär den CSV-Export; beim Import
|
||||
CsvFormatOptions: Dateiformat
|
||||
CsvTimestamp: Zeitstempel
|
||||
CsvTimestampTip: Soll an den Namen jeder exportierten CSV-Datei ein Zeitstempel vorne angehängt werden?
|
||||
CsvExportLabel: Prüfungs-Label bei Export
|
||||
CsvExportLabelTip: Soll beim CSV-Export von Prüfungsleistungen automatisch ein gegebenes Label für diese Prüfung gesetzt werden?
|
||||
CsvPresetRFC: Standard-Konform (RFC 4180)
|
||||
CsvPresetExcel: Excel-Kompatibel
|
||||
CsvCustom: Benutzerdefiniert
|
||||
|
||||
@ -3,6 +3,8 @@ CsvOptionsTip: These settings primarily affect CSV export. During import most se
|
||||
CsvFormatOptions: File format
|
||||
CsvTimestamp: Timestamp
|
||||
CsvTimestampTip: Should the name of every exported csv file contain a timestamp?
|
||||
CsvExportLabel: Exam label on export
|
||||
CsvExportLabelTip: Should a given label be automatically set for an exam of which results are exported to CSV?
|
||||
CsvPresetRFC: Standards-compliant (RFC 4180)
|
||||
CsvPresetExcel: Excel compatible
|
||||
CsvCustom: User defined
|
||||
|
||||
@ -113,3 +113,5 @@ AllocNotifyNewCourseDefault: Systemweite Einstellung
|
||||
AllocNotifyNewCourseForceOff: Nein
|
||||
AllocNotifyNewCourseForceOn: Ja
|
||||
Settings: Individuelle Benutzereinstellungen
|
||||
|
||||
FormExamOffice: Prüfungsverwaltung
|
||||
|
||||
@ -113,4 +113,6 @@ LanguageChanged: Language changed successfully
|
||||
AllocNotifyNewCourseDefault: System-wide setting
|
||||
AllocNotifyNewCourseForceOff: No
|
||||
AllocNotifyNewCourseForceOn: Yes
|
||||
Settings: Settings
|
||||
Settings: Settings
|
||||
|
||||
FormExamOffice: Exam Office
|
||||
@ -8,6 +8,7 @@ SystemMessageLastChanged: Zuletzt geändert
|
||||
SystemMessageLastUnhide: Zuletzt un-versteckt
|
||||
SystemMessageFrom: Sichtbar ab
|
||||
SystemMessageTo: Sichtbar bis
|
||||
SystemMessageOnVolatileClusterSettings: Sichtbar bei VolatileCluster-Einstellungen
|
||||
SystemMessageAuthenticatedOnly: Nur angemeldet
|
||||
SystemMessageSeverity: Schwere
|
||||
SystemMessagePriority: Priorität
|
||||
@ -38,3 +39,7 @@ SystemMessageEditTranslationSuccess: Übersetzung angepasst.
|
||||
SystemMessageDeleteTranslationSuccess: Übersetzung entfernt.
|
||||
|
||||
RFC1766: RFC1766-Sprachcode
|
||||
|
||||
SystemMessageOnVolatileClusterSettingKey: VolatileCluster-Einstellung
|
||||
SystemMessageOnVolatileClusterSettingValue: Wert
|
||||
SystemMessageOnVolatileClusterSettingKeyExists: Für diese Einstellung existiert bereits ein Wert!
|
||||
|
||||
@ -8,6 +8,7 @@ SystemMessageLastChanged: Last changed
|
||||
SystemMessageLastUnhide: Last unhidden
|
||||
SystemMessageFrom: Visible from
|
||||
SystemMessageTo: Visible to
|
||||
SystemMessageOnVolatileClusterSettings: Visible on VolatileCluster settings
|
||||
SystemMessageAuthenticatedOnly: Only logged in users
|
||||
SystemMessageSeverity: Severity
|
||||
SystemMessagePriority: Priority
|
||||
@ -38,3 +39,7 @@ SystemMessageEditTranslationSuccess: Successfully edited translation.
|
||||
SystemMessageDeleteTranslationSuccess: Successfully deleted translation.
|
||||
|
||||
RFC1766: RFC1766 language code
|
||||
|
||||
SystemMessageOnVolatileClusterSettingKey: VolatileCluster setting
|
||||
SystemMessageOnVolatileClusterSettingValue: Value
|
||||
SystemMessageOnVolatileClusterSettingKeyExists: There already exists a value for this setting!
|
||||
|
||||
@ -9,4 +9,6 @@ MultiEmailFieldTip: Es sind mehrere, Komma-separierte, E-Mail-Adressen möglich
|
||||
WeekDay: Wochentag
|
||||
LdapIdentificationOrEmail: Fraport AG-Kennung / E-Mail-Adresse
|
||||
Months num@Int64: #{num} #{pluralDE num "Monat" "Monate"}
|
||||
Days num@Int64: #{num} #{pluralDE num "Tag" "Tage"}
|
||||
Days num@Int64: #{num} #{pluralDE num "Tag" "Tage"}
|
||||
|
||||
ClusterVolatileQuickActionsEnabled: Schnellzugriffsmenü aktiv
|
||||
|
||||
@ -9,4 +9,6 @@ MultiEmailFieldTip: Multiple emails addresses may be specified (comma-separated)
|
||||
WeekDay: Day of the week
|
||||
LdapIdentificationOrEmail: Fraport AG-Kennung / email address
|
||||
Months num: #{num} #{pluralEN num "Month" "Months"}
|
||||
Days num: #{num} #{pluralEN num "Day" "Days"}
|
||||
Days num: #{num} #{pluralEN num "Day" "Days"}
|
||||
|
||||
ClusterVolatileQuickActionsEnabled: Quick actions enabled
|
||||
|
||||
@ -162,4 +162,7 @@ BreadcrumbMessageList: Systemnachrichten
|
||||
BreadcrumbGlossary: Begriffsverzeichnis
|
||||
BreadcrumbLogin !ident-ok: Login
|
||||
BreadcrumbNews: Aktuell
|
||||
BreadcrumbSubmissionAuthorshipStatements: Eigenständigkeitserklärungen
|
||||
BreadcrumbSubmissionAuthorshipStatements: Eigenständigkeitserklärungen
|
||||
BreadcrumbExternalApis: Externe APIs
|
||||
BreadcrumbApiDocs: API Dokumentation
|
||||
BreadcrumbSwagger !ident-ok: OpenAPI 2.0 (Swagger)
|
||||
|
||||
@ -163,3 +163,6 @@ BreadcrumbSheetOldUnassigned: Submissions without corrector
|
||||
BreadcrumbLogin: Login
|
||||
BreadcrumbNews: News
|
||||
BreadcrumbSubmissionAuthorshipStatements: Statements of Authorship
|
||||
BreadcrumbExternalApis: External APIs
|
||||
BreadcrumbApiDocs: API documentation
|
||||
BreadcrumbSwagger: OpenAPI 2.0 (Swagger)
|
||||
|
||||
@ -130,4 +130,7 @@ MenuLmsUsers: Export E-Lernen Benutzer
|
||||
MenuLmsUserlist: Melden E-Lernen Benutzer
|
||||
MenuLmsResult: Melden Ergebnisse E-Lernen
|
||||
MenuLmsUpload: Hochladen
|
||||
MenuLmsDirect: Direkter Upload
|
||||
MenuLmsDirect: Direkter Upload
|
||||
|
||||
MenuApiDocs: API-Dokumentation (Englisch)
|
||||
MenuSwagger !ident-ok: OpenAPI 2.0 (Swagger)
|
||||
|
||||
@ -132,3 +132,6 @@ MenuLmsUserlist: Upload E-Learning Users
|
||||
MenuLmsResult: Upload E-Learning Results
|
||||
MenuLmsUpload: Upload
|
||||
MenuLmsDirect: Direct Upload
|
||||
|
||||
MenuApiDocs: API documentation
|
||||
MenuSwagger: OpenAPI 2.0 (Swagger)
|
||||
|
||||
@ -10,7 +10,7 @@ LegalHeading: Legal
|
||||
VersionHeading: Version history
|
||||
SystemMessageHeading: Uni2work system message
|
||||
SystemMessageListHeading: Uni2work system message
|
||||
HeadingHelpRequest: Support request / Suggestion
|
||||
HeadingHelpRequest: Support request/Suggestion
|
||||
ProfileHeading: Settings
|
||||
ProfileDataHeading: Personal information
|
||||
CorrectorsChange: Adjust correctors
|
||||
|
||||
@ -22,6 +22,7 @@ TableExamName !ident-ok: Name
|
||||
TableExamTime: Termin
|
||||
TableExamRegistration: Prüfungsanmeldung
|
||||
TableExamResult: Prüfungsergebnis
|
||||
TableExamLabel !ident-ok: Label
|
||||
TableSheet: Blatt
|
||||
TableLastEdit: Letzte Änderung
|
||||
TableSubmission: Abgabenummer
|
||||
@ -62,4 +63,7 @@ CsvExport: CSV-Export
|
||||
TableProportion c@Text of'@Text prop@Rational !ident-ok: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%)
|
||||
TableProportionNoRatio c@Text of'@Text !ident-ok: #{c}/#{of'}
|
||||
TableExamFinished: Ergebnisse sichtbar ab
|
||||
TableDiffDaysTooltip: Zeitspanne nach ISO 8601. Beispiel: "P2Y3M4D" ist eine Zeitspanne von 2 Jahren, 3 Monaten und 4 Tagen.
|
||||
TableDiffDaysTooltip: Zeitspanne nach ISO 8601. Beispiel: "P2Y3M4D" ist eine Zeitspanne von 2 Jahren, 3 Monaten und 4 Tagen.
|
||||
TableExamOfficeLabel: Label-Name
|
||||
TableExamOfficeLabelStatus: Label-Farbe
|
||||
TableExamOfficeLabelPriority: Label-Priorität
|
||||
|
||||
@ -22,6 +22,7 @@ TableExamName: Name
|
||||
TableExamTime: Time
|
||||
TableExamRegistration: Exam registration
|
||||
TableExamResult: Exam result
|
||||
TableExamLabel: Label
|
||||
TableSheet: Sheet
|
||||
TableLastEdit: Latest edit
|
||||
TableSubmission: Submission-number
|
||||
@ -62,4 +63,7 @@ CsvExport: CSV export
|
||||
TableProportion c of' prop: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%)
|
||||
TableProportionNoRatio c of': #{c}/#{of'}
|
||||
TableExamFinished: Results visible from
|
||||
TableDiffDaysTooltip: Duration given according to ISO 8601. Example: "P2Y3M4D" is a period of 2 years, 3 months and 4 days.
|
||||
TableDiffDaysTooltip: Duration given according to ISO 8601. Example: "P2Y3M4D" is a period of 2 years, 3 months and 4 days.
|
||||
TableExamOfficeLabel: Label name
|
||||
TableExamOfficeLabelStatus: Label colour
|
||||
TableExamOfficeLabelPriority: Label priority
|
||||
|
||||
@ -4,6 +4,7 @@ RGCourseParticipants: Kursteilnehmer:innen
|
||||
RGCourseLecturers: Kursverwalter:innen
|
||||
RGCourseCorrectors: Korrektor:innen
|
||||
RGCourseTutors: Tutor:innen
|
||||
RGCourseParticipantsInTutorial: Kursteilnehmer:innen, die in mindestens einem Tutorium angemeldet sind
|
||||
RGCourseUnacceptedApplicants: Nicht akzeptierte Bewerber:innen
|
||||
RecipientToggleAll: Alle/Keine
|
||||
CommCourseTestSubject customSubject@Text !ident-ok: [TEST] #{customSubject}
|
||||
@ -17,6 +18,8 @@ RGTutorialParticipants tutn@TutorialName: Tutorium-Teilnehmer:innen (#{tutn})
|
||||
RGExamRegistered examn@ExamName: Angemeldet zur Prüfung „#{examn}“
|
||||
RGSheetSubmittor shn@SheetName: Abgebende für das Übungsblatt „#{shn}“
|
||||
CommSubject: Betreff
|
||||
CommAttachments: Anhänge
|
||||
CommAttachmentsTip: Im Allgemeinen ist es vorzuziehen Dateien, die Sie mit den Empfängern teilen möchten, als Material hochzuladen (und ggf. in der Nachricht zu verlinken). So ist die Datei für die Empfänger dauerhaft abrufbar und auch Personen, die sich z.B. erst später zum Kurs anmelden, haben Zugriff auf die Datei.
|
||||
CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt
|
||||
CommTestSuccess: Nachricht wurde zu Testzwecken nur an Sie selbst versandt
|
||||
|
||||
@ -53,6 +56,7 @@ UploadSpecificFileMaxSizeNegative: Maximale Dateigröße darf nicht negativ sein
|
||||
UploadSpecificFileEmptyOk: Leere Uploads erlauben
|
||||
UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}"
|
||||
GenericFileFieldFileTooLarge file@FilePath: „#{file}“ ist zu groß
|
||||
GenericFileFieldCumulativeTooLarge: Hochgeladene Dateien sind zu groß
|
||||
GenericFileFieldInvalidExtension file@FilePath: „#{file}” hat keine zulässige Dateiendung
|
||||
OnlyUploadOneFile: Bitte nur eine Datei hochladen.
|
||||
UploadAtLeastOneNonemptyFile: Bitte mindestens eine nichtleere Datei hochladen.
|
||||
@ -133,6 +137,7 @@ MessageError: Fehler
|
||||
MessageWarning: Warnung
|
||||
MessageInfo !ident-ok: Information
|
||||
MessageSuccess: Erfolg
|
||||
MessageNonactive: Inaktiv
|
||||
ShortFieldPrimary: HF
|
||||
ShortFieldSecondary: NF
|
||||
SheetGradingPassPoints': Bestehen nach Punkten
|
||||
|
||||
@ -4,6 +4,7 @@ RGCourseParticipants: Course participants
|
||||
RGCourseLecturers: Course administrators
|
||||
RGCourseCorrectors: Course correctors
|
||||
RGCourseTutors: Course tutors
|
||||
RGCourseParticipantsInTutorial: Course participants who are registered for at least one tutorial
|
||||
RGCourseUnacceptedApplicants: Applicants not accepted
|
||||
RecipientToggleAll: All/None
|
||||
CommCourseTestSubject customSubject: [TEST] #{customSubject}
|
||||
@ -17,6 +18,8 @@ RGTutorialParticipants tutn: Tutorial participants (#{tutn})
|
||||
RGExamRegistered examn: Registered for exam “#{examn}”
|
||||
RGSheetSubmittor shn: Submitted for exercise sheet “#{shn}”
|
||||
CommSubject: Subject
|
||||
CommAttachments: Attachments
|
||||
CommAttachmentsTip: In general it is preferable to upload files as course material instead of sending them as attachments. You can then link to the material from the message. The file is then permanently accessable to the recipients and to persons that, for example, register for the Course at a later date.
|
||||
CommSuccess n: Message was sent to #{n} #{pluralEN n "recipient" "recipients"}
|
||||
CommTestSuccess: Message was sent only to yourself for testing purposes
|
||||
|
||||
@ -53,6 +56,7 @@ UploadSpecificFileMaxSizeNegative: Maximum filesize may not be negative
|
||||
UploadSpecificFileEmptyOk: Allow empty uploads
|
||||
UnknownPseudonymWord pseudonymWord: Invalid pseudonym-word “#{pseudonymWord}”
|
||||
GenericFileFieldFileTooLarge file: “#{file}” is too large
|
||||
GenericFileFieldCumulativeTooLarge: Uploaded files are too large
|
||||
GenericFileFieldInvalidExtension file: “#{file}” does not have an acceptable file extension
|
||||
OnlyUploadOneFile: Please only upload one file
|
||||
UploadAtLeastOneNonemptyFile: Please upload at least one nonempty file.
|
||||
@ -133,6 +137,7 @@ MessageError: Error
|
||||
MessageWarning: Warning
|
||||
MessageInfo: Information
|
||||
MessageSuccess: Success
|
||||
MessageNonactive: Inactive
|
||||
ShortFieldPrimary: Mj
|
||||
ShortFieldSecondary: Mn
|
||||
SheetGradingPassPoints': Passing by points
|
||||
|
||||
18
models/exam-office/exam-labels.model
Normal file
18
models/exam-office/exam-labels.model
Normal file
@ -0,0 +1,18 @@
|
||||
ExamOfficeLabel
|
||||
user UserId
|
||||
name ExamOfficeLabelName
|
||||
status MessageStatus
|
||||
priority Int -- determines label ordering
|
||||
UniqueExamOfficeLabel user name
|
||||
deriving Generic
|
||||
|
||||
ExamOfficeExamLabel
|
||||
exam ExamId
|
||||
label ExamOfficeLabelId
|
||||
UniqueExamOfficeExamLabel exam
|
||||
deriving Generic
|
||||
ExamOfficeExternalExamLabel
|
||||
externalExam ExternalExamId
|
||||
label ExamOfficeLabelId
|
||||
UniqueExamOfficeExternalExamLabel externalExam
|
||||
deriving Generic
|
||||
9
models/external-apis.model
Normal file
9
models/external-apis.model
Normal file
@ -0,0 +1,9 @@
|
||||
ExternalApi
|
||||
ident UUID Maybe
|
||||
authority Jwt
|
||||
keys JwkSet
|
||||
baseUrl BaseUrl
|
||||
config ExternalApiConfig
|
||||
lastAlive UTCTime
|
||||
UniqueExternalApiIdent ident !force
|
||||
deriving Generic
|
||||
@ -1,8 +1,9 @@
|
||||
-- Messages shown to all users as soon as they visit the site/log in (i.e.: "System is going down for maintenance next sunday")
|
||||
-- Only administrators (of any school) should be able to create these via a web-interface
|
||||
SystemMessage
|
||||
SystemMessage json
|
||||
from UTCTime Maybe -- Message is not shown before this date has passed (never shown, if null)
|
||||
to UTCTime Maybe -- Message is shown until this date has passed (shown forever, if null)
|
||||
onVolatileClusterSettings SystemMessageVolatileClusterSettings default="'[]'::jsonb" -- Message is shown when given volatile cluster settings have given values
|
||||
newsOnly Bool default=false
|
||||
authenticatedOnly Bool -- Show message to all users upon visiting the site or only upon login?
|
||||
severity MessageStatus -- Success, Warning, Error, Info, ...
|
||||
|
||||
@ -17,7 +17,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create
|
||||
lastAuthentication UTCTime Maybe -- last login date
|
||||
created UTCTime default=now()
|
||||
lastLdapSynchronisation UTCTime Maybe
|
||||
ldapPrimaryKey Text Maybe
|
||||
ldapPrimaryKey UserEduPersonPrincipalName Maybe
|
||||
tokensIssuedAfter UTCTime Maybe -- do not accept bearer tokens issued before this time (accept all tokens if null)
|
||||
matrikelnummer UserMatriculation Maybe -- usually a number; AVS Personalnummer; nicht Fraport Personalnummer!
|
||||
firstName Text -- For export in tables, pre-split firstName from displayName
|
||||
@ -39,6 +39,8 @@ User json -- Each Uni2work user has a corresponding row in this table; create
|
||||
mobile Text Maybe
|
||||
companyPersonalNumber Text Maybe -- Company will become a new table, but if company=fraport, some information is received via LDAP
|
||||
companyDepartment Text Maybe -- thus we store such information for ease of reference directly, if available
|
||||
examOfficeGetSynced Bool default=true -- whether synced status should be displayed for exam results by default
|
||||
examOfficeGetLabels Bool default=true -- whether labels should be displayed for exam results by default
|
||||
UniqueAuthentication ident -- Column 'ident' can be used as a row-key in this table
|
||||
UniqueEmail email -- Column 'email' can be used as a row-key in this table
|
||||
deriving Show Eq Ord Generic -- Haskell-specific settings for runtime-value representing a row in memory
|
||||
@ -57,8 +59,8 @@ UserSystemFunction
|
||||
UniqueUserSystemFunction user function
|
||||
deriving Generic
|
||||
UserExamOffice
|
||||
user UserId
|
||||
field StudyTermsId
|
||||
user UserId
|
||||
field StudyTermsId
|
||||
UniqueUserExamOffice user field
|
||||
deriving Generic
|
||||
UserSchool -- Managed by users themselves, encodes "schools of interest"
|
||||
|
||||
21044
package-lock.json
generated
21044
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
128
package.json
128
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uni2work",
|
||||
"version": "25.24.5",
|
||||
"version": "26.1.1",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@ -10,7 +10,6 @@
|
||||
"test": "run-s frontend:test yesod:test i18n:test",
|
||||
"lint": "run-s frontend:lint yesod:lint",
|
||||
"build": "run-s frontend:build yesod:build",
|
||||
"cbt": "./cbt.sh",
|
||||
"yesod:db": "./db.sh",
|
||||
"yesod:start": "./start.sh",
|
||||
"yesod:lint": "./hlint.sh",
|
||||
@ -45,85 +44,84 @@
|
||||
"defaults"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.10.5",
|
||||
"@babel/core": "^7.11.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.10.5",
|
||||
"@babel/plugin-transform-runtime": "^7.11.0",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@commitlint/cli": "^10.0.0",
|
||||
"@commitlint/config-conventional": "^10.0.0",
|
||||
"@fortawesome/fontawesome-pro": "^5.14.0",
|
||||
"autoprefixer": "^9.8.6",
|
||||
"@babel/cli": "^7.17.10",
|
||||
"@babel/core": "^7.18.2",
|
||||
"@babel/eslint-parser": "^7.18.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.17.12",
|
||||
"@babel/plugin-proposal-decorators": "^7.18.2",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.17.12",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.18.2",
|
||||
"@babel/plugin-transform-runtime": "^7.18.2",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@commitlint/cli": "^17.0.2",
|
||||
"@commitlint/config-conventional": "^17.0.2",
|
||||
"@fortawesome/fontawesome-pro": "^6.1.1",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-loader": "^8.2.5",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.5",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"cbt_tunnels": "^1.2.2",
|
||||
"changelog-parser": "^2.8.0",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"copy-webpack-plugin": "^6.0.3",
|
||||
"css-loader": "^2.1.1",
|
||||
"eslint": "^5.16.0",
|
||||
"file-loader": "^5.1.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"glob": "^7.1.6",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^2.7.0",
|
||||
"jasmine-core": "^3.6.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"karma": "^5.1.1",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"changelog-parser": "^2.8.1",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.7.1",
|
||||
"eslint": "^8.17.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"glob": "^8.0.3",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"husky": "^8.0.1",
|
||||
"jasmine-core": "^4.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"karma": "^6.3.20",
|
||||
"karma-chrome-launcher": "^3.1.1",
|
||||
"karma-cli": "^2.0.0",
|
||||
"karma-jasmine": "^2.0.1",
|
||||
"karma-jasmine-html-reporter": "^1.5.4",
|
||||
"karma-jasmine": "^5.0.1",
|
||||
"karma-jasmine-html-reporter": "^2.0.0",
|
||||
"karma-mocha-reporter": "^2.2.5",
|
||||
"karma-webpack": "^3.0.5",
|
||||
"lint-staged": "^8.2.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"mini-css-extract-plugin": "^0.8.2",
|
||||
"karma-webpack": "^5.0.0",
|
||||
"lint-staged": "^13.0.0",
|
||||
"mini-css-extract-plugin": "^2.6.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"null-loader": "^2.0.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"null-loader": "^4.0.1",
|
||||
"optimize-css-assets-webpack-plugin": "^6.0.1",
|
||||
"postcss-loader": "^7.0.0",
|
||||
"postcss-preset-env": "^7.7.1",
|
||||
"real-favicon-webpack-plugin": "^0.2.3",
|
||||
"remove-files-webpack-plugin": "^1.4.3",
|
||||
"remove-files-webpack-plugin": "^1.5.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise": "^4.2.6",
|
||||
"resolve-url-loader": "^3.1.1",
|
||||
"sass": "^1.26.10",
|
||||
"sass-loader": "^7.3.1",
|
||||
"semver": "^6.3.0",
|
||||
"standard-version": "^9.1.0",
|
||||
"standard-version-updater-yaml": "^1.0.2",
|
||||
"style-loader": "^0.23.1",
|
||||
"terser-webpack-plugin": "^2.3.8",
|
||||
"tmp": "^0.1.0",
|
||||
"typeface-roboto": "0.0.75",
|
||||
"typeface-source-code-pro": "^1.1.3",
|
||||
"typeface-source-sans-pro": "0.0.75",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-manifest-plugin": "^2.2.0",
|
||||
"webpack-plugin-hash-output": "^3.2.1"
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sass": "^1.52.2",
|
||||
"sass-loader": "^13.0.0",
|
||||
"semver": "^7.3.7",
|
||||
"standard-version": "^9.5.0",
|
||||
"standard-version-updater-yaml": "^1.0.3",
|
||||
"style-loader": "^3.3.1",
|
||||
"terser-webpack-plugin": "^5.3.3",
|
||||
"tmp": "^0.2.1",
|
||||
"typeface-roboto": "1.1.13",
|
||||
"typeface-source-code-pro": "^1.1.13",
|
||||
"typeface-source-sans-pro": "1.1.13",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-manifest-plugin": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@juggle/resize-observer": "^2.5.0",
|
||||
"core-js": "^3.6.5",
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@juggle/resize-observer": "^3.3.1",
|
||||
"core-js": "^3.22.8",
|
||||
"css.escape": "^1.5.1",
|
||||
"js-cookie": "^2.2.1",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.defer": "^4.1.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"moment": "^2.27.0",
|
||||
"npm": "^6.14.8",
|
||||
"sodium-javascript": "^0.5.6",
|
||||
"tail.datetime": "git+ssh://git@gitlab2.rz.ifi.lmu.de/uni2work/tail.DateTime.git#uni2work",
|
||||
"moment": "^2.29.3",
|
||||
"npm": "^8.12.1",
|
||||
"sodium-javascript": "^0.8.0",
|
||||
"toposort": "^2.0.2",
|
||||
"whatwg-fetch": "^3.4.0"
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
20
package.yaml
20
package.yaml
@ -1,9 +1,10 @@
|
||||
name: uniworx
|
||||
version: 25.24.5
|
||||
version: 26.1.1
|
||||
dependencies:
|
||||
- base
|
||||
- yesod
|
||||
- yesod-core
|
||||
- yesod-persistent
|
||||
- yesod-auth
|
||||
- yesod-static
|
||||
- yesod-form
|
||||
@ -119,6 +120,7 @@ dependencies:
|
||||
- hsass
|
||||
- semigroupoids
|
||||
- http-types
|
||||
- http-client
|
||||
- jose-jwt
|
||||
- mono-traversable
|
||||
- mono-traversable-keys
|
||||
@ -146,6 +148,19 @@ dependencies:
|
||||
- unidecode
|
||||
- pandoc
|
||||
- pandoc-types
|
||||
- insert-ordered-containers
|
||||
- servant
|
||||
- servant-server
|
||||
- servant-swagger
|
||||
- servant-docs
|
||||
- servant-client
|
||||
- servant-client-core
|
||||
- servant-quickcheck
|
||||
- swagger2
|
||||
- haskell-src-meta
|
||||
- network-uri
|
||||
- vault
|
||||
- tagged
|
||||
- token-bucket
|
||||
- async
|
||||
- pointedlist
|
||||
@ -158,11 +173,11 @@ dependencies:
|
||||
- fastcdc
|
||||
- bimap
|
||||
- list-t
|
||||
- insert-ordered-containers
|
||||
- topograph
|
||||
- network-uri
|
||||
- psqueues
|
||||
- nonce
|
||||
- semver
|
||||
- IntervalMap
|
||||
- haskell-src-meta
|
||||
- either
|
||||
@ -321,6 +336,7 @@ tests:
|
||||
- quickcheck-io
|
||||
- network-arbitrary
|
||||
- lens-properties
|
||||
- http-media
|
||||
ghc-options:
|
||||
- -fno-warn-orphans
|
||||
- -threaded -rtsopts "-with-rtsopts=-N -T"
|
||||
|
||||
1539
records.json
1539
records.json
File diff suppressed because it is too large
Load Diff
25
routes
25
routes
@ -73,6 +73,8 @@
|
||||
|
||||
/help HelpR GET POST !free
|
||||
|
||||
/external-apis ExternalApisR ServantApiExternalApis getServantApi
|
||||
|
||||
/user ProfileR GET POST !free
|
||||
/user/profile ProfileDataR GET !free
|
||||
/user/authpreds AuthPredsR GET POST !free
|
||||
@ -82,7 +84,7 @@
|
||||
/user/storage-key StorageKeyR POST !free
|
||||
|
||||
/exam-office ExamOfficeR !exam-office:
|
||||
/ EOExamsR GET !system-exam-office
|
||||
/ EOExamsR GET POST !system-exam-office
|
||||
/fields EOFieldsR GET POST
|
||||
/users EOUsersR GET POST !system-exam-office
|
||||
/users/invite EOUsersInviteR GET POST !system-exam-office
|
||||
@ -253,6 +255,27 @@
|
||||
!/#UUID CryptoUUIDDispatchR GET !free -- just redirect
|
||||
-- !/*{CI FilePath} CryptoFileNameDispatchR GET !free -- Disabled until preliminary check for valid cID exists
|
||||
|
||||
-- for users
|
||||
/qualification QualificationAllR GET !free
|
||||
/qualification/#SchoolId QualificationSchoolR GET !free -- TODO
|
||||
/qualification/#SchoolId/#QualificationShorthand QualificationR GET !free -- must be logged in though
|
||||
-- OSIS CSV Export Demo
|
||||
/lms LmsAllR GET POST
|
||||
/lms/#SchoolId LmsSchoolR GET
|
||||
/lms/#SchoolId/#QualificationShorthand LmsR GET POST
|
||||
/lms/#SchoolId/#QualificationShorthand/edit LmsEditR GET POST
|
||||
/lms/#SchoolId/#QualificationShorthand/users LmsUsersR GET
|
||||
/lms/#SchoolId/#QualificationShorthand/users/direct LmsUsersDirectR GET
|
||||
/lms/#SchoolId/#QualificationShorthand/userlist LmsUserlistR GET POST
|
||||
/lms/#SchoolId/#QualificationShorthand/userlist/upload LmsUserlistUploadR GET POST
|
||||
/lms/#SchoolId/#QualificationShorthand/userlist/direct LmsUserlistDirectR POST
|
||||
/lms/#SchoolId/#QualificationShorthand/result LmsResultR GET POST
|
||||
/lms/#SchoolId/#QualificationShorthand/result/upload LmsResultUploadR GET POST
|
||||
/lms/#SchoolId/#QualificationShorthand/result/direct LmsResultDirectR POST
|
||||
/api ApiDocsR GET !free
|
||||
/swagger SwaggerR GET !free
|
||||
/swagger.json SwaggerJsonR GET !free
|
||||
|
||||
!/*WellKnownFileName WellKnownR GET !free
|
||||
|
||||
-- for users
|
||||
|
||||
@ -147,6 +147,10 @@ import Handler.Error
|
||||
import Handler.Upload
|
||||
import Handler.Qualification
|
||||
import Handler.LMS
|
||||
import Handler.ApiDocs
|
||||
import Handler.Swagger
|
||||
|
||||
import ServantApi () -- YesodSubDispatch instances
|
||||
|
||||
-- This line actually creates our YesodDispatch instance. It is the second half
|
||||
-- of the call to mkYesodData which occurs in Foundation.hs. Please see the
|
||||
|
||||
@ -28,7 +28,7 @@ dummyForm :: ( RenderMessage (HandlerSite m) FormMessage
|
||||
) => WForm m (FormResult (CI Text))
|
||||
dummyForm = do
|
||||
mr <- getMessageRender
|
||||
wreq (ciField & addDatalist userList) (fslpI MsgDummyIdent (mr MsgDummyIdentPlaceholder) & noAutocomplete & addName PostLoginDummy) Nothing
|
||||
wreq (ciField & addDatalist userList) (fslpI MsgDummyIdent (mr MsgDummyIdentPlaceholder) & addAttr "autocomplete" "username" & addName PostLoginDummy) Nothing
|
||||
where
|
||||
userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent])
|
||||
toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent)
|
||||
@ -40,7 +40,7 @@ dummyLogin :: forall site.
|
||||
( YesodAuth site
|
||||
, YesodPersist site
|
||||
, SqlBackendCanRead (YesodPersistBackend site)
|
||||
, RenderMessage site AFormMessage
|
||||
, RenderAFormSite site
|
||||
, RenderMessage site DummyMessage
|
||||
, RenderMessage site (ValueRequired site)
|
||||
, Button site ButtonSubmit
|
||||
@ -52,7 +52,7 @@ dummyLogin = AuthPlugin{..}
|
||||
|
||||
apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
|
||||
apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do
|
||||
((loginRes, _), _) <- runFormPost $ renderWForm FormStandard dummyForm
|
||||
((loginRes, _), _) <- runFormPost $ renderWForm FormLogin dummyForm
|
||||
tp <- getRouteToParent
|
||||
case loginRes of
|
||||
FormFailure errs -> do
|
||||
@ -69,7 +69,7 @@ dummyLogin = AuthPlugin{..}
|
||||
|
||||
apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
|
||||
apLogin toMaster = do
|
||||
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormStandard dummyForm
|
||||
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormLogin dummyForm
|
||||
let loginForm = wrapForm login FormSettings
|
||||
{ formMethod = POST
|
||||
, formAction = Just . SomeRoute . toMaster $ PluginR apName []
|
||||
|
||||
@ -182,8 +182,8 @@ campusForm :: ( RenderMessage (HandlerSite m) FormMessage
|
||||
campusForm = do
|
||||
MsgRenderer mr <- getMsgRenderer
|
||||
aFormToWForm $ CampusLogin
|
||||
<$> areq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "") Nothing
|
||||
<*> areq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder)) Nothing
|
||||
<$> areq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "" & addAttr "autocomplete" "username") Nothing
|
||||
<*> areq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder) & addAttr "autocomplete" "current-password") Nothing
|
||||
|
||||
apLdap :: Text
|
||||
apLdap = "LDAP"
|
||||
@ -191,7 +191,7 @@ apLdap = "LDAP"
|
||||
campusLogin :: forall site.
|
||||
( YesodAuth site
|
||||
, RenderMessage site CampusMessage
|
||||
, RenderMessage site AFormMessage
|
||||
, RenderAFormSite site
|
||||
, RenderMessage site (ValueRequired site)
|
||||
, RenderMessage site ADInvalidCredentials
|
||||
, Button site ButtonSubmit
|
||||
@ -203,7 +203,7 @@ campusLogin pool mode = AuthPlugin{..}
|
||||
|
||||
apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
|
||||
apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do
|
||||
((loginRes, _), _) <- runFormPost $ renderWForm FormStandard campusForm
|
||||
((loginRes, _), _) <- runFormPost $ renderWForm FormLogin campusForm
|
||||
tp <- getRouteToParent
|
||||
|
||||
resp <- formResultMaybe loginRes $ \CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> Just <$> do
|
||||
@ -258,7 +258,7 @@ campusLogin pool mode = AuthPlugin{..}
|
||||
|
||||
apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
|
||||
apLogin toMaster = do
|
||||
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormStandard campusForm
|
||||
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormLogin campusForm
|
||||
let loginForm = wrapForm login FormSettings
|
||||
{ formMethod = POST
|
||||
, formAction = Just . SomeRoute . toMaster $ PluginR apName []
|
||||
|
||||
@ -37,8 +37,8 @@ hashForm :: ( RenderMessage (HandlerSite m) FormMessage
|
||||
hashForm = do
|
||||
MsgRenderer mr <- getMsgRenderer
|
||||
aFormToWForm $ HashLogin
|
||||
<$> areq ciField (fslpI MsgPWHashIdent (mr MsgPWHashIdentPlaceholder)) Nothing
|
||||
<*> areq passwordField (fslpI MsgPWHashPassword (mr MsgPWHashPasswordPlaceholder)) Nothing
|
||||
<$> areq ciField (fslpI MsgPWHashIdent (mr MsgPWHashIdentPlaceholder) & addAttr "autocomplete" "username") Nothing
|
||||
<*> areq passwordField (fslpI MsgPWHashPassword (mr MsgPWHashPasswordPlaceholder) & addAttr "autocomplete" "current-password") Nothing
|
||||
|
||||
apHash :: Text
|
||||
apHash = "PWHash"
|
||||
@ -49,7 +49,7 @@ hashLogin :: forall site.
|
||||
, SqlBackendCanRead (YesodPersistBackend site)
|
||||
, PersistRecordBackend User (YesodPersistBackend site)
|
||||
, RenderMessage site PWHashMessage
|
||||
, RenderMessage site AFormMessage
|
||||
, RenderAFormSite site
|
||||
, RenderMessage site (ValueRequired site)
|
||||
, Button site ButtonSubmit
|
||||
) => PWHashAlgorithm -> AuthPlugin site
|
||||
@ -60,7 +60,7 @@ hashLogin pwHashAlgo = AuthPlugin{..}
|
||||
|
||||
apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
|
||||
apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do
|
||||
((loginRes, _), _) <- runFormPost $ renderWForm FormStandard hashForm
|
||||
((loginRes, _), _) <- runFormPost $ renderWForm FormLogin hashForm
|
||||
tp <- getRouteToParent
|
||||
|
||||
resp <- formResultMaybe loginRes $ \HashLogin{..} -> Just <$> do
|
||||
@ -81,7 +81,7 @@ hashLogin pwHashAlgo = AuthPlugin{..}
|
||||
|
||||
apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
|
||||
apLogin toMaster = do
|
||||
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormStandard hashForm
|
||||
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormLogin hashForm
|
||||
let loginForm = wrapForm login FormSettings
|
||||
{ formMethod = POST
|
||||
, formAction = Just . SomeRoute . toMaster $ PluginR apName []
|
||||
|
||||
19
src/Control/Monad/Trans/Except/Instances.hs
Normal file
19
src/Control/Monad/Trans/Except/Instances.hs
Normal file
@ -0,0 +1,19 @@
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Control.Monad.Trans.Except.Instances
|
||||
() where
|
||||
|
||||
import ClassyPrelude
|
||||
|
||||
import Control.Monad.Trans.Except (ExceptT(..), runExceptT)
|
||||
|
||||
import Control.Arrow (left)
|
||||
|
||||
|
||||
newtype UnliftIOExceptTError e = UnliftIOExceptTError { getUnliftIOExceptTError :: e }
|
||||
deriving (Read, Show, Generic, Typeable)
|
||||
deriving newtype (Exception)
|
||||
|
||||
|
||||
instance (Exception e, MonadUnliftIO m) => MonadUnliftIO (ExceptT e m) where
|
||||
withRunInIO cont = ExceptT (withRunInIO $ \runInner -> fmap (left getUnliftIOExceptTError) . try $ cont (either (throwIO . UnliftIOExceptTError) return <=< runInner . runExceptT))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user