Merge branch master of gitlab2.rz.ifi.lmu.de:uni2work/uni2work

This commit is contained in:
Sarah Vaupel 2022-06-09 21:09:18 +02:00
parent 77f09f05f5
commit f2cf9344e7
311 changed files with 13234 additions and 17548 deletions

View File

@ -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"]
]
}

View File

@ -11,9 +11,10 @@
"flatpickr": "readonly",
"$": "readonly"
},
"parser": "babel-eslint",
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 2018,
"requireConfigFile": false,
"ecmaFeatures": {
"legacyDecorators": true
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View 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;
}
}

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View 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
View 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;
}
}
};

View File

@ -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) {

View File

@ -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);
});
});
});
});

View File

@ -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);
}
}

View File

@ -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' })

View File

@ -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');

View File

@ -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();

View File

@ -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);
});
}

View File

@ -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();

View File

@ -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);
});
}
};
}

View File

@ -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();

View File

@ -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 });
// }
}

View File

@ -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);
});
});

View File

@ -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;
}

View File

@ -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]));
});
});

View File

@ -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) {

View File

@ -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);
}

View File

@ -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);
}
}

View 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();
});
});

View File

@ -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();
}
}

View 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();
});
});

View File

@ -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);
}
}
}
}

View File

@ -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 */

View File

@ -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);
});
}

View File

@ -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);
}
}

View 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">

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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() {

View File

@ -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;
}
}

View File

@ -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);
});
}

View File

@ -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');
});

View File

@ -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);
}
}

View 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));
}
}

View 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)

View File

@ -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() {

View File

@ -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() {

View File

@ -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,
];

View File

@ -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() {

View File

@ -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);
}
}

View File

@ -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) => {

View File

@ -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),
);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -21,7 +21,7 @@ export class SortTable {
}
destroy() {
console.log('TBD destroy SortTable');
this._storageManager.clear();
}
}

View File

@ -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, '');
}
};

Binary file not shown.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -113,3 +113,5 @@ AllocNotifyNewCourseDefault: Systemweite Einstellung
AllocNotifyNewCourseForceOff: Nein
AllocNotifyNewCourseForceOn: Ja
Settings: Individuelle Benutzereinstellungen
FormExamOffice: Prüfungsverwaltung

View File

@ -113,4 +113,6 @@ LanguageChanged: Language changed successfully
AllocNotifyNewCourseDefault: System-wide setting
AllocNotifyNewCourseForceOff: No
AllocNotifyNewCourseForceOn: Yes
Settings: Settings
Settings: Settings
FormExamOffice: Exam Office

View File

@ -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!

View File

@ -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!

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -0,0 +1,9 @@
ExternalApi
ident UUID Maybe
authority Jwt
keys JwkSet
baseUrl BaseUrl
config ExternalApiConfig
lastAlive UTCTime
UniqueExternalApiIdent ident !force
deriving Generic

View File

@ -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, ...

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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"

File diff suppressed because it is too large Load Diff

25
routes
View File

@ -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

View File

@ -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

View File

@ -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 []

View File

@ -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 []

View File

@ -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 []

View 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